3751 Commits
Author SHA1 Message Date
Vance Ingalls c44371a298 Merge pull request #3082 from heygen-com/release/v0.7.98
chore: release v0.7.98
v0.7.98
2026-08-06 23:59:15 -07:00
Vance Ingalls 6114749d8e chore: release v0.7.98 2026-08-06 23:37:23 -07:00
Vance Ingalls a26be13f32 Merge pull request #3081 from heygen-com/canary-error-router-state
fix(producer): record routing state on the failure path
2026-08-06 23:35:52 -07:00
Vance IngallsandClaude Opus 5 a3d13e2673 fix(producer): record routing state on the failure path
de_parallel_router is present on 95.4% of render_complete events and 0.83%
of render_error. Capture context itself survives failures fine (capture_mode
is on 98.6% of them), so this is not renders failing before capture — the
routing state specifically is being dropped.

Cause is ordering. deParallelRouter is assigned twice: once before the
capture-observability update, and again inside syncCapturePlan where routing
is actually resolved — including the 'reverted' case, which the earlier
assignment cannot know. The update in between recorded whatever was true
first, so a render that failed while routed reported no routing state at all.
The existing comment at the earlier call site says it is recorded there
precisely so hard failures carry it; that intent was correct and the value
just arrived too late.

This matters for the #2840 ramp specifically. The per-install circuit breaker
only arms on a revert, which requires the render to finish and self-detect —
it cannot catch a crash or hang. Those are exactly the failure modes a
percentage ramp exists to bound, and they were the ones telemetry could not
see.

Also makes the ffprobe contract sweep resilient per entry. A dangling symlink
under packages/studio/data/projects threw ENOENT on stat and aborted the whole
traversal, so every package sorting after 'studio' — both studio-server
callers included — silently stopped being checked. main is currently red on
this. The manifest assertion is what caught it, which is what it was added
for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 22:41:26 -07:00
Miguel Ángel 02a24055c7 Merge pull request #3080 from heygen-com/release/v0.7.97
chore: release v0.7.97
v0.7.97
2026-08-06 22:29:11 -07:00
Miguel Ángel c4b41072c7 chore: release v0.7.97 2026-08-07 05:16:27 +00:00
Miguel Ángel c7b2199650 Merge pull request #3076 from heygen-com/fix-studio-resize-box
fix(studio): resize an animated element to the size it was dropped at
2026-08-06 18:01:05 -07:00
Vance Ingalls 066af73c13 Merge pull request #3075 from heygen-com/release/v0.7.96
chore: release v0.7.96
v0.7.96
2026-08-06 17:36:58 -07:00
Miguel Ángel 12e637fb25 fix(studio): cover legacy resize boxes 2026-08-07 00:31:27 +00:00
Miguel Angel Simon Sierra a693b12cca fix(studio): correct against the scale the resize actually commits
A near-uniform drag collapses to the `scale` shorthand, but the finalize step
measured the element at the per-axis pair it computed rather than the single
value the commit writes. The element was measured at a scaleY the file never
gets, so the position correction came out tilted by the difference.

Adds a sweep over the shapes a composition produces — shrink, grow, first
resize, rotated, steeply rotated, non-uniform, near-zero, inline-sized, no
position write, animated position, and two drags in a row — each checking the
element renders on its drop point from the PERSISTED scale and position. The
geometry model is calibrated against real gesture traces: the same inputs
reproduce the rects the browser reported to three decimals.
2026-08-06 17:21:51 -07:00
Vance IngallsandClaude Opus 5 ae72e8c096 chore(ci): re-trigger checks after the Actions outage
The pull_request webhook for this branch was dropped during the GitHub
Actions incident (major_outage 15:22-00:0x UTC), so #3075 received only the
WIP check and a skipped Mintlify. Dropped webhooks are not replayed, and
ci.yml has no workflow_dispatch, so an empty commit is the only way to fire
the event without closing the PR.

No content change: the release commit 026e6941a is unmodified beneath this.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 17:13:04 -07:00
Miguel Angel Simon Sierra ee5ae9619c fix(studio): measure the resize correction from the pre-gesture position
A scale resize measured its drop-point correction while the gesture's own
translation was still applied, but the position commit adds that correction
onto the element's PRE-gesture position, which it reads from the gesture's
base attributes. The two disagreed by the whole drag distance, so the commit
persisted a position a drag-length from where the element was dropped: it
held the drop point for one frame and then slid off.

Move the element back to that base before measuring, so the residual and the
commit share one origin. For an element whose position is a static hold that
usually means no correction at all, which is the right answer: scaling about
the centre already leaves it on the drop point.
2026-08-06 17:05:37 -07:00
Miguel Angel Simon Sierra b18fd62e0e fix(studio): keep an animated element on the drop point when resized
An element whose position is animated left the drop point anyway. The
finalize step wrote its correction as a static position hold, and the
element's position tween rendered its own value a frame later and won.
Before that it stood down entirely on such elements, on the grounds that
a keyframed path has no single anchor to preserve, which had the same
visible result: the element moved.

It has an anchor, the frame the user is looking at. The correction now
goes into that tween at the playhead, through commitGsapPositionFromDrag,
which is the same commit a drag on the same element already uses. Static
holds keep the existing path.

This is the difference the debug log showed between an element carrying
position:to, which moved after release, and one carrying position:set,
which did not.
2026-08-06 17:02:14 -07:00
Miguel Angel Simon Sierra 7dc18771d1 fix(studio): pin the drop point on a first resize
The finalize step measures where the committed scale put the box and
shifts the position hold by the difference. Whether the commit had
actually rendered when it measured was luck: on a first resize the
timeline had not re-seeked, so it measured the element at its natural
size still sitting on the drop point, saw no residual, and skipped the
correction. The scale then landed, GSAP rendered it about the element's
centre, and the element jumped by the whole drag distance. Elements
resized before got a correction only because their previous scale made
the residual non-zero by accident.

The committed scale is now applied to the live element before measuring,
so the measurement means what its comment says either way, and a skipped
correction is logged rather than silent.

Confirmed against a real session: a first resize of a 630px chip now
reports residual -109.93 and lands on the drop point, where it previously
logged no scale-finalize at all.
2026-08-06 17:02:14 -07:00
Hblee 9769ba2c7b feat(sdk): expose a paint query for transparent-composition hit-testing (#3070)
* feat(sdk): expose a paint query for transparent-composition hit-testing

A host layering a transparent composition over other content has to know
whether a point carries ink before it decides to swallow a click. The adapter
only answered "what element is here", so AI Studio wrote its own answer and
could not reach the per-pixel alpha the adapter already samples for <img>.

Add PreviewAdapter.paintsAt, plus the pieces it is built from on a new
./adapters/iframe subpath so a host with different hit-test policy can compose
its own walk.

The walk is geometric rather than elementsFromPoint-based: that stack omits
pointer-events:none nodes, and a decorative overlay carrying it still paints,
so a z-stack query would report no ink over visible artwork — the direction
that makes a composition vanish from under the cursor.

fullBleedFraction is an option rather than a constant because "a layer covering
the whole frame is background, not artwork" is host policy, not a fact about
the composition.

* fix(sdk): scope the full-bleed frame to the root under the point

compositionFrameArea took the smallest [data-composition-id] in the whole
document, so an unrelated sub-composition sized the reference frame for points
nowhere near it: a 300x300 badge in a corner made every mid-size painter in a
1920x1080 outer frame read as full-bleed, and the composition went
click-through under artwork the user can plainly see. That is the direction the
fail-safe exists to avoid, and the docs already described the intended
behaviour — the innermost root CONTAINING the point.

Also read the alpha channel instead of matching known transparent spellings.
Only the `transparent` keyword computes to rgba(0, 0, 0, 0); a faded-out white
stays rgba(255, 255, 255, 0), which the set counted as painted. That erred
toward absorbing clicks rather than losing them, so it was a false positive
rather than a hazard, but it is wrong.

Both are pinned by tests that fail when the fix is reverted.

* fix(sdk): stop the paint query answering "no ink" over visible artwork

Four cases where the walk landed on the wrong side of its own fail-safe.

The full-bleed veto tested the winner's border-box area even when the alpha
sampler had just read an opaque pixel there, so a full-frame transparent PNG or
SVG overlay — the case this feature exists for — reported background over
visibly opaque artwork, with no fullBleedFraction that worked. Ink now carries
how it was established, and a measured pixel is never vetoed. An image whose
pixels could NOT be read stays inferred, so a tainted CDN overlay still yields
to the veto rather than absorbing every click.

Composition roots were excluded from candidacy outright, so a root carrying a
background answered false even at fraction 0, where the docs promise every
painting box counts. Roots are candidates now; the veto discounts them without
a special case, since a root's box is the frame.

An <img> with a clear pixel early-returned past its own background, padding
plate and border, which any other element would have counted.

A same-origin iframe mid-navigation exposes a readable but empty document, so
the !doc guard never fired and a loading composition answered a confident
"no ink" — the exact failure the null convention exists to prevent.

Also: the sort comparator's epsilon tie was intransitive, leaving the
smallest-first guarantee (and the lazy single-sample property that rides on it)
engine-dependent; the guide's pass-through recipe called a function that does
not exist and hand-waved the coordinate mapping that makes it correct; and the
reference now states the under-counts alongside the over-counts, the walk's
blindness to runtime-mounted content, and compositionPaintsAt's preconditions.

Each fix is pinned by a test that fails when the fix is reverted.

* refactor(sdk)!: invert the paint query to isProvablyEmptyAt

paintsAt handed callers three falsy bottom values with opposite safe readings:
false meant "no ink, pass the click through", null meant "not knowable, treat as
painted", and undefined from an adapter without the method also meant painted.
The idiomatic `if (!preview.paintsAt?.(x, y)) passThrough()` therefore did the
dangerous thing for two of the three, and the convention needed defending in the
interface docstring, the reference and the guide — plus a dedicated comment and
a pinning test on the headless adapter to stop null regressing to false.

Inverting the polarity collapses the tri-state to a plain boolean and makes the
safe reading structural: true only when the composition was readable and nothing
painted there, so ink, an unreadable or still-loading document, and a missing
implementation all land on "keep the composition clickable". The prose stays as
rationale, but nothing depends on a reader remembering it.

PaintsAtOptions becomes PaintQueryOptions, since it now describes the walk that
both the adapter method and the exported compositionPaintsAt share rather than
one method's arguments. compositionPaintsAt keeps its ink-positive name: it
answers the other question, and its docstring points callers who need the
fail-safe contract at the adapter.

Nothing is released yet, so no consumer is on the old name.
2026-08-06 16:27:38 -07:00
Miguel Angel Simon Sierra 20ef798620 fix(studio): stop a uniform resize writing a scale GSAP ignores
A uniform drag committed the `scale` shorthand. If the tween's keyframes
already stated `scaleX` and `scaleY`, the commit left both forms in the
same keyframe, and GSAP animates each property name independently, so the
longhands ran alongside the shorthand and won.

The resize therefore computed the right number, wrote it to the file, and
did nothing: the element snapped back to its old size the moment the
handle was released. Reproduced from a real session, where a drop at 384px
on a 630px element wrote {scaleX: 1, scaleY: 1, scale: 0.61} and rendered
at the original size.

The mixing hazard was already known in the other direction, where a
non-uniform drag takes a rewrite path that normalizes every keyframe to
the longhands. This makes the condition symmetric: whenever the tween
already speaks longhands, a uniform drag speaks them too.
2026-08-06 16:18:13 -07:00
Miguel Angel Simon Sierra 477f77629b fix(studio): resize from the element's real box, not a 200px guess
Resizing an element whose size is driven by a scale animation committed a
scale computed against a hardcoded 200px fallback, because the only
original size the draft recorded was the element's INLINE width, and a
composition sizes its elements from the stylesheet.

A 630px chip dropped at 1260px wide committed a scale of 6.3 instead of 2,
so it landed at over three times the size it was dropped at. The next drag
compounded it, because that wrong scale then counted as the element's live
one.

The draft now records the box it measured, once, before it writes a width
of its own, and the intercept reads that. The inline attributes keep their
own job of restoring an inline style, which is why they cannot answer this
question.
2026-08-06 15:53:36 -07:00
Vance Ingalls 026e6941ac chore: release v0.7.96 2026-08-06 14:53:26 -07:00
Vance Ingalls 5a7f465768 Merge pull request #3074 from heygen-com/canary-emit-reason
feat(core,cli): emit the canary decision reason alongside the assignment
2026-08-06 14:42:27 -07:00
Vance IngallsandClaude Opus 5 b3990ac789 feat(studio): emit the canary reason on Studio events too
Review caught that the same anti-pattern was still live in the Studio binding:
canaryEventProperties destructured only `enabled` and dropped the reason. Its
own doc comment promised 'identical shape to the CLI, so a rollout spanning
both reads as one flag' — which the CLI-only fix had just made false.

This matters beyond symmetry. A CLI-launched Studio adopts the CLI's decisions
and shares its bucket seed, so a cohort flip can surface on either surface.
Emitting attribution on only one leaves Studio-observed flips unattributable
and makes the two flip counts irreconcilable — and Studio is the surface most
likely to expose a shared-seed-with-diverging-id pattern, which is the open
question the reason exists to answer.

Also adds the no_unit_id emission test the CLI side advertised but never
asserted, and a Studio pair pinning that a URL override and a cohort roll
produce the same assignment with different reasons.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 14:39:56 -07:00
Vance IngallsandClaude Opus 5 076657a639 feat(core,cli): emit the canary decision reason alongside the assignment
The calibration contract deferred this until the stability check came back
dirty. It did: the first fleet read found 304 installs (1.08%) reporting both
values for a canary whose percentage never moved, and the genuinely anomalous
ones could not be separated from a developer toggling HF_CANARY_*, because the
assignment alone is identical in both cases.

resolveCanary has always computed the reason and canaryEventProperties dropped
it. Now every canary emits canary_reason_<name> beside its assignment.

Deliberately outside the $feature/ namespace: PostHog treats those as flag
values, and a non-boolean there would corrupt the flag's own breakdowns.

Two of the six wire values are immediately useful beyond override attribution.
'excluded' identifies CI installs, which today have to be dropped by joining
on is_ci — conflating them with out_of_cohort is what made the first accuracy
read look like a significant failure (9.22% against a 10% target) when it was
not. 'no_unit_id' surfaces the fails-closed corner.

The reason is optional on the core helper so existing callers are unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 13:57:08 -07:00
Miguel Ángel fc0298de4a chore: release v0.7.95 (#3068) v0.7.95 2026-08-06 10:12:20 -07:00
WaterrrForever 3b65321a20 feat(cli): classify identity persistence on every telemetry event (#3065)
* feat(cli): classify identity persistence on every telemetry event

Install-grain metrics currently trust every anonymousId equally, but
ephemeral/isolated-HOME workloads mint a fresh id per run — one machine
produced 2,956 rotating render identities since Jul 30 (94.4% seen on a
single render command), inflating acquisition and diluting per-install
penetration while looking like real product usage.

Every event now carries:

- identity_persistence: durable (id loaded from a preexisting config —
  proven to survive a process boundary) | unknown (minted+persisted this
  run; an ephemeral HOME is indistinguishable from a genuine first run
  from inside one process) | process_only (persist failed). Sticky per
  process so a fresh install re-reading its own write cannot self-promote.
- config_write_outcome: ok | ok_unmirrored | failed for the identity-
  establishing write; absent when the id came from disk.
- invocation_id: random uuid per CLI process, so one invocation's events
  group even when the install identity is untrustworthy (unlike run_id,
  which needs an orchestrator to set HYPERFRAMES_RUN_ID).

Install metrics can then count only durable identities, and a daily
churn monitor can alert on the unknown share.

* fix(cli): require the anonymousId to come off disk before classifying durable

Review finding: materializeConfig mints a replacement anonymousId when a
hand-edited/image-baked config lacks one. That replacement only reaches
disk when the bucket-seed backfill happens to write; with a seed present
the read path performs no write at all, so the install re-mints a fresh
id every run while the unconditional durable branch stamped each of them
with the one label durable-only counting is allowed to trust.

durable now requires parseNonEmptyString(parsed.anonymousId): a minted
replacement classifies like a fresh mint — by the backfill write outcome
when that path runs (unknown/process_only), and process_only on the
no-write path where the id provably dies with the process. Two tests pin
both shapes.
2026-08-06 22:19:14 +08:00
Miguel Ángel 88853f170f fix(studio): route rooted timeline media through preview (#3061) 2026-08-05 22:14:28 -07:00
Miguel ÁngelandCodex 96861cbafc perf(studio-server): coordinate cancelable thumbnail generation (#2720)
* perf(studio): schedule adaptive timeline thumbnails

* perf(studio): bound thumbnail decoding resources

* perf(studio): virtualize timeline thumbnail media

* perf(studio): prioritize timeline thumbnail work

* perf(studio-server): coordinate cancelable thumbnail generation

---------

Co-authored-by: Codex <codex@local>
2026-08-05 20:41:35 -07:00
James Russo 27d113e56c docs: add user interview booking links (#3059) 2026-08-05 12:35:58 -07:00
Ular Kimsanov b793e42efd Merge pull request #3010 from heygen-com/docs/pages-show-not-tell
docs: make the pages show the thing they describe
2026-08-05 12:28:46 -07:00
ukimsanov aa928f399a style(scripts): oxfmt the nav-suppression assertion
The long assert.match line tripped oxfmt --check in CI (Preflight/Format red,
which cascaded regression/player-perf/preview-regression into skip-then-fail). The
pre-commit format hook doesn't cover scripts/*.mjs, so it slipped through. Wrapped
per the formatter. Flagged by Magi and Rames.
2026-08-05 11:47:59 -07:00
ukimsanov fed2baf278 docs: correct composition offset rationale; pin nav fix; label-in-name
- html-schema: the previous clause claimed a composition host's data-media-start is
  never read. It is: readElementPlaybackStart (media.ts:16) resolves
  data-playback-start ?? data-media-start and timeline.ts calls it on composition
  clips. Rewrote to the accurate reason both reviewers gave — composition hosts are
  only inspected by the playback-start-first readers, media-start works as a
  fallback, but playback-start is what Studio writes/normalises to.
- motion test: pin the HoverVideo click-suppression (preventDefault +
  stopPropagation). Removing it left the gate 12/12; now it fails. This is the bug
  that escaped static review and only surfaced by driving the live preview.
- replica-compare: fold the visible 'Sound off/on' text into the aria-label so the
  accessible name contains it (WCAG 2.5.3, Rames).

Round-5 findings from Magi and Rames.
2026-08-05 11:32:21 -07:00
ukimsanov 83db94e75e docs(examples): stop the sound control from following the card's link
Grid cards with a public source are wrapped in an <a>; the sound/play button
lives inside it, so a click — mouse or keyboard — bubbled up and navigated to
GitHub instead of toggling sound. preventDefault + stopPropagation on the control.
Found by driving the live preview (self-review).
2026-08-05 11:12:55 -07:00
ukimsanov f3d1d531d1 docs(examples): reset control state on offscreen release; precise a11y labels
- replica-compare: the offscreen teardown now resets muted (element + React state),
  matching HoverVideo — a pair unmuted before it scrolled away no longer returns
  reading 'Sound on' over a paused, sourceless pair (Magi blocker).
- Both controls' aria-labels now follow the mode: under reduced motion the button
  plays/pauses the whole comparison, and when a preview is already autoplaying
  muted the action is 'unmute', not 'play with sound' (self-review + Rames).
- Hardened the motion-suite assertion to scope 'startBoth' to toggleSound's body
  (a defined-but-unused helper no longer satisfies it) and pin the offscreen
  muted-reset transition (Rames mutation-test gap).
- html-schema: 'hyperframes validate' inspects <audio> only; note that no
  media-start-only reader inspects a composition host, so the kind rule strands
  nobody (Rames).
2026-08-05 10:59:29 -07:00
ukimsanov c49fea2e0c docs: complete the media-start layer map + fix the third page
Per Rames: add the engine audio mixer (audioMixer.ts:350 -> ffmpeg -ss) to the
media-start-only readers — it's the live path that makes a lone data-playback-start
a shipped output bug (trimmed picture over untrimmed audio). 'Set one' isn't enough;
give the element-kind rule: <video>/<audio> use data-media-start, nested composition
uses data-playback-start (Studio writes it; it's the child-timeline offset). Also fix
editing-existing-videos.mdx, which offered both names as interchangeable for front
trim of a clip — the exact kind where they aren't.
2026-08-05 10:45:32 -07:00
ukimsanov 630c8900f5 docs(examples): reduced-motion play starts both films; precise media-start layers
- replica-compare: the voluntary control now starts and pauses BOTH films (not
  just the reference), and the replica-sync effect attaches in view regardless of
  the preference, so a reduced-motion visitor who presses play sees the whole
  synchronized pair. Added a focused source-level assertion to the motion-check
  suite (the repo has no React runtime harness for snippets).
- html-schema: describe each layer precisely instead of grouping the CLI —
  timing compiler, HTML parser, producer audio, and 'hyperframes validate' read
  only data-media-start; runtime, Studio and 'hyperframes snapshot' read
  data-playback-start first (Studio also writes it).

Round-3 findings from Magi.
2026-08-05 10:41:34 -07:00
ukimsanov b541245f1a docs(studio): note the third arrow-key meaning after merging Timeline navigation
The merged '## Timeline navigation' section documents a third arrow-key behaviour;
the 'selected area decides' paragraph read as exhaustive with only two. Fold in the
timeline-focus case and link the section. Coherence note from Rames.
2026-08-05 10:25:06 -07:00
ukimsanov d373c3f4a0 fix(catalog): explicit section ownership, no body-sniffing heuristic
carriedSectionsFrom() decided whether a ## Usage section was generated by matching
its first line against a list of historical opener phrases — so a hand-written
Usage section that happened to open that way was classified as generated and
silently deleted on regeneration. Ownership is now purely set membership: a
section is generated iff its heading is one the template emits, and ambiguous
'usage' is no longer in that set (the template never emits it), so any ## Usage is
carried. Exported carriedSectionsFrom behind an entrypoint guard and added two
executable preservation fixtures. Flagged by Magi (#5).
2026-08-05 10:23:37 -07:00
ukimsanov c9fd7465c9 docs(examples): allow voluntary playback under reduced motion; a11y parity
Both snippet players now keep a source assigned whenever in view (preload='none'
so nothing downloads until asked), gating only autoplay on reduced motion — so a
reduced-motion visitor can press play/unmute instead of being left with an inert
poster. HoverVideo renders a control on hasAudio={false} cards too (a play/pause
toggle), and ReplicaCompare no longer calls play() on a source it just removed.
Gave ReplicaCompare's button the same aria-pressed + focus-visible as HoverVideo.
Round-2 finding from Magi (#1) and Rames.
2026-08-05 10:17:40 -07:00
ukimsanov 8e658bd6c9 docs(sdk): fix dispatch/can contract, second stale GSAP site, media-start layers
- edit-operations: dispatch() does not consult can() (session.ts:608 -> applyOp
  with no validation; a no-timeline addGsapTween/addLabel is a no-op but a missing
  target still writes via selector fallback). Use Rames's wording: call can() first
  and skip on failure — no false 'applies nothing' guarantee.
- timing-and-animation: the second E_NO_GSAP_TIMELINE site — gated setGsapTween on
  an error it cannot return and called shipped parser code 'a later phase'. Rewrote
  to gate addGsapTween (which can return it); dropped the stale can() comment in
  types.ts:595.
- html-schema: data-playback-start is read by runtime, Studio and CLI (Studio also
  writes it, timelineEditingHelpers.ts); only the compile path is media-start only.
  Document the layered precedence instead of calling it runtime-only.

Round-2 findings from Magi (#2/#3/#4) and Rames.
2026-08-05 10:13:17 -07:00
ukimsanov aa49f7d924 Merge remote-tracking branch 'origin/main' into docs/pages-show-not-tell 2026-08-05 05:56:59 -07:00
ukimsanov aa077e000d docs(examples): intersection-gate the recreate pair too
ReplicaCompare now assigns/decodes its reference+replica only near the viewport
and releases both (pause + removeAttribute + load) on exit, closing the last
eager-load surface from Magi's #5. Sync + reduced-motion guard preserved.
2026-08-05 05:54:49 -07:00
ukimsanov 8cadefa5d8 fix(scripts): import the changelog style note instead of duplicating it
changelog-weekly.ts hard-copied the CHANGELOG_STYLE_NOTE literal that set-version.ts
exports; both feed the release gate, so a drift silently weakened it. Import the
constant so the two markers cannot diverge. Flagged by Copilot and Rames.
2026-08-05 05:51:16 -07:00
ukimsanov 0467bc8129 docs(examples): make hover-audio accessible, lazy-loaded, and guard-complete
HoverVideo now: (1) exposes a focusable button that toggles/plays sound by
keyboard, touch and pointer — hover is an enhancement, not the only path;
(2) gates the source behind an IntersectionObserver so offscreen cards no longer
download/decode, releasing the buffer on exit; (3) still blocks autoplay under
reduced motion while allowing voluntary playback via the button. Routed all 30
Thirty-Days cards through it (hasAudio={false} for the 12 silent ones) so no raw
<video autoPlay> bypasses the motion guard. Flagged by Magi (P1 #4/#5/#6).
2026-08-05 05:48:23 -07:00
ukimsanov 0f6259d461 docs: correct three source-contradiction findings from review
- edit-operations: E_NO_GSAP_TIMELINE is raised only by addGsapTween and addLabel
  (mutate.ts:1747), not the set/remove tween ops; drop the false 'dispatch() still
  applies structurally' claim (mutate.gsap.test.ts verifies zero patches); add the
  requirement to the Labels section where addLabel lives.
- html-schema: data-media-start is read by parser + timing compiler + runtime;
  data-playback-start is a runtime-only alias, so a lone playback-start doesn't
  shift trim/split. Document the surfaces instead of claiming one universal value.
- testing-local-changes: bun unlink cleanup used a relative cd that resolves under
  the video project; use an absolute checkout path.

Flagged by Magi (P1 #2/#3, P2 #7) and Rames.
2026-08-05 05:43:35 -07:00
ukimsanov 6ff6601dd0 fix(catalog): restore the required Related topics section on generated pages
My generator rebuild replaced the '## Related topics' section (still required by
docs/AGENTS.md) with the provenance footer, dropping it from all 168 generated
Catalog pages. Emit it again as the final section so pages end with it, and stop
carriedSectionsFrom() breaking at the footer marker so a human section appended
below the generated tail survives a regeneration. Adds a per-page regeneration
assertion so a future drop fails CI. Flagged by Magi (P1) and Rames.
2026-08-05 05:38:58 -07:00
ukimsanov b62c3fd656 docs(theme): brand the Playground CTA + align the navbar logo
Repaint the Playground pill with the HyperFrames mark's own cyan-to-green gradient
(#06E3FA -> #4FDB5E) with dark text, instead of the muted primary-dark. Left the
site-wide primary untouched so link contrast stays AA. Also nudge the logo 16px so
its left edge lines up with the sidebar text instead of sitting at the navbar padding.
2026-08-05 04:02:41 -07:00
ukimsanov c53c7efd1e docs(nav): lift 30 Days above the update feeds in Explore 2026-08-05 03:54:45 -07:00
ukimsanov 4b1a526fac docs(examples): fix silent recreate references (stale keys -> new audio keys)
The reverse-*-reference.mp4 keys on the CDN were serving the original silent cut
(immutable cache never took the audio re-upload). Re-encoded from the audio sources
to reverse-*-reference-snd.mp4 and pointed ReplicaCompare at them, so the reference
now actually plays sound on click.
2026-08-05 03:40:27 -07:00
ukimsanov 821189586f docs(examples): k3 promo now hover-to-hear with its audio version from X 2026-08-05 03:29:39 -07:00
ukimsanov 630c6c880d docs(examples): replace two padded launch films + wire figma audio
The claude-design (54s) and music-to-video (181s) docs films were the real ~40s/~45s
films padded with black / a held end-logo. Swapped both for their correct X launch
versions (40s and 90s) which also carry audio, and wired figma to its X source. All
three now hover-to-hear.
2026-08-05 03:21:46 -07:00
ukimsanov 59a2ee31c9 docs(examples): more hover-audio + a 'Where HyperFrames plugs in' strip
Swapped five more grid films (launch, keyframes, pr-to-video, skills, sound) to
their content-matched X launch versions with audio. Added an integrations strip
(Codex, Vercel, Ollama, hyperframes.dev community) from the same archive. Skipped
the square/different-cut sources (figma, claude-design, music-to-video) to avoid
cropping or swapping in a different edit.
2026-08-05 03:09:14 -07:00
ukimsanov e6bc8f139c docs(examples): hover-audio on six more grid films from the X launch archive
variables, texture, website-to-video, vfx, frame.md and inspector each have a
content-matched launch film on @HeyGen's X with audio; re-encoded those to -s.mp4
and wired HoverVideo. Launch grid now has 10 cards with hover-to-hear.
2026-08-05 02:58:25 -07:00