Commit Graph
474 Commits
Author SHA1 Message Date
Miguel Ángel 868c56fdbb chore: release v0.6.89 2026-06-10 23:26:13 +00:00
Miguel Ángel 036b991cbb fix(runtime): preserve authored muted attr; clean up WebAudio on end (#1319)
onSetMuted/onSetMediaOutputMuted set el.muted = effective on every
<video> and <audio> element. When the bridge sent onSetMuted(false),
it unmuted avatar <video muted> elements whose baked-in lip-sync audio
should never play — causing double audio alongside the separate TTS.
Fix: el.muted = effective || el.defaultMuted.

AudioBufferSourceNode fires 'ended' when playback completes naturally,
but _activeSources was never cleaned up. This kept isActive() true
permanently, which force-muted all HTML audio elements via the
outputMuted flag in syncRuntimeMedia — causing audio to disappear
after the WebAudio buffer finished (~5s for short TTS clips).

Add onended listener that removes the source from _activeSources and
restores el.muted to its pre-WebAudio value. All side-effects are
guarded by idx !== -1 so a stale ended event after stopAll() is a
no-op and cannot clobber bridge state set between stop and the async
event delivery.
2026-06-10 19:09:13 -04:00
Miguel Ángel f0b499b582 fix(studio): keyframe bug fixes — gate delete hooks, fix value corruption, gesture recording (#1314)
- Gate stripStudioEditsFromTarget/bakeVisibilityOnDelete behind a
  stripStudioEdits flag on the delete mutation type so they only fire on
  user-initiated deletes, not on internal delete-then-recreate drags.

- Add bakeVisibilityOnDelete to the remove-all-keyframes handler so
  elements with CSS opacity:0 stay visible after collapsing keyframes.

- Fix integer rounding in readAllAnimatedProperties: use 3-decimal
  precision for visual properties (opacity, scale, rotation) instead of
  Math.round which corrupted mid-fade values to 0.

- Guard VISUAL_BASELINE against cross-tween contamination by querying
  __timelines for properties animated by other tweens on the same element.

- Harden bakeVisibilityOnDelete: reverse-scan keyframes for the last one
  containing opacity, guard against relative values (+=/-=/*=), and add
  Number.isFinite check.

- Fix falsy-zero doubling in drag commit: replace || fallback with
  Number.isFinite so a base GSAP position of 0 is correctly preserved.

- Fix gesture recording sign inversion: remove pointerElementOffset
  subtraction from dx/dy formula and instead apply it once to basePosition
  so the element center tracks the pointer.

- Fix TypeScript build errors in gsapSoftReload.ts (6 double-casts).

- Strip all diagnostic logs from production code.
2026-06-10 18:53:06 -04:00
Miguel Ángel d13ae13670 chore: release v0.6.88 2026-06-09 23:34:42 +00:00
Miguel Ángel 192417b7cd fix(core): guard element stamping to iframe-only (Studio preview) 2026-06-09 23:34:08 +00:00
Miguel Ángel 02475ce9f7 chore: release v0.6.87 2026-06-09 22:36:11 +00:00
Miguel Ángel 869fc411a3 chore: release v0.6.85 2026-06-09 22:32:34 +00:00
Miguel Ángel a468550f82 feat(studio): keyframe system — parser, runtime, timeline UI, design panel, gesture recording (#1311)
* feat(studio): runtime hooks — global time compiler + keyframe runtime

Add the runtime bridge layer: global time compilation (tween % → clip %),
soft reload after mutations, runtime keyframe preview, and keyframe
commit helper.

* feat(studio): runtime hooks — global time compiler + keyframe runtime

Add the runtime bridge layer: global time compilation (tween % → clip %),
soft reload after mutations, runtime keyframe preview, and keyframe
commit helper.

* feat(studio): keyframe cache + commit hooks

Add hooks for keyframe cache population (tween → clip-relative %),
mutation dispatch, keyframe snapping, and audio beat detection.

* feat(studio): timeline UI — dopesheet diamonds + keyboard nav

Add dopesheet strip with diamond keyframe indicators, timeline property
rows, keyboard navigation (J/Shift+J/Delete/K), and feature gate
(STUDIO_KEYFRAMES_ENABLED defaults to false).

* feat(studio): design panel — arc controls + ease curve + stagger

Add arc path controls (curviness slider, auto-rotate), motion path SVG
overlay, ease curve visualization, stagger controls, and expanded
animation card. Includes border-radius editor dependency from #1217.

* feat(studio): gesture recording core

Add gesture recording engine with RAF sampling, modifier key property
mapping (Shift→rotationXY, Alt→rotation, Cmd→opacity),
Ramer-Douglas-Peucker simplification, and ghost trail SVG overlay.

* fix(studio): keyframe drag + recording bug bash

21 fixes: capture GSAP base at drag start, translate:none before
gsap.set, skip reapplyPathOffsets for GSAP elements, clamp recording
seek, _auto flag for 100% keyframes, overlay flash fix, block edits
during recording.

* feat(studio): keyframe integration wiring + docs

Wire App.tsx recording orchestration, TimelineToolbar K/R buttons,
PropertyPanel per-property diamonds, shortcuts panel, toast
notifications, and keyframes guide documentation. All gated on
STUDIO_KEYFRAMES_ENABLED (default false).
2026-06-09 18:30:23 -04:00
Miguel Ángel 96b8d617d8 feat(studio): GSAP parser — arc path mutations + keyframe CRUD (#1301)
Add parser-level mutations for arc paths, keyframe add/remove/update,
convert-to-keyframes, and _auto flag for 100% keyframes. Wire route
handlers for new mutation types.
2026-06-09 18:28:44 -04:00
Vance IngallsandClaude Sonnet 4.6 0923bc0787 feat(studio): carry hfId on TimelineElement, wire through buildPatchTarget (R7, T5b) (#1299)
* feat(studio): carry hfId on TimelineElement, wire through buildPatchTarget (R7, T5b)

* refactor(studio): extract readHfId helper, fix empty-string normalization, add comments (R7 review)

- Extract readHfId(el) to domEditingLayers.ts — centralizes `?.trim() || undefined`
  normalisation; guards against empty-string data-hf-id reaching findTagByTarget
- Wire readHfId into domEditingLayers.ts and useDomEditCommits.ts (the one site
  that still used `?? undefined` instead of `|| undefined`)
- Re-export readHfId through domEditing.ts public API
- Add readHfId unit tests: present, absent, empty-string, whitespace-only
- Add comment on PatchTarget: runtime validation lives in findTagByTarget, type is docs-only
- Suppress pre-existing unused re-exports in timelineDOM.ts (backward-compat re-exports
  brought into fallow scope by the T5b hfId changes)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(core): clear data-hf-id on split clone to prevent dual-match (R7 review)

cloneNode(true) copies all attributes including data-hf-id. Without clearing it,
both halves of a split share the same hf-id; the server's findByHfId picks the first
match and silently patches the wrong clip. Remove the attribute from the clone so
write-back re-mints a fresh id on the next preview load.

Adds a test: splitElementInHtml — hfId clone isolation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(studio): add hfId to DomEditLayerItem + getDomLayerPatchTarget return type (R7 review)

- Add hfId to DomEditLayerItem interface (domEditingTypes.ts) so layer item
  construction in collectDomEditLayerItems compiles
- Widen getDomLayerPatchTarget return type to include hfId + populate it from
  data-hf-id attribute (domEditingElement.ts)
- Widen findDomEditSelectionTarget to check hfId-first when no id/selector
- Widen Pick types in domEditOverlayGeometry.ts and useGsapScriptCommits.ts
- Add hfId to buildMissingCompositionElements element construction
- Add hfId-targeted test coverage in domEditing.test.ts,
  domEditOverlayGeometry.test.ts, timelineIframeHelpers.test.ts
- Update hfIds.test.ts KNOWN LIMITATION labels — write-back landed in R7 T1-2

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 15:15:27 -07:00
Vance Ingalls 2071c85c9a feat(studio,core): populate hfId in DomEditSelection + widen MutationTarget (R7, T5a) (#1296)
## Summary

- Adds `hfId` field to `resolveDomEditSelection` — reads `data-hf-id` off the live element and stores it in `DomEditSelection.hfId`
- `DomEditSelection extends PatchTarget` which already declares `hfId?: string`, so this is a single new line at the return site
- Widens `MutationTarget` in `files.ts` to include `hfId?: string` (type hygiene — the value already survives through `parseMutationBody`'s by-reference pass, so this is documentation not a behaviour change)

## Why

R7 / Task 5a. The full hf-id write-back and patch-engine infrastructure (R1 + R7 Tasks 0–4, PRs #1269–#1292) is server-complete. The only missing piece was: the Studio client never read `data-hf-id` off a hit-tested element, so `target.hfId` was always `undefined` and the `hfId`-first lookup branches in both patch engines were unreachable in production. This PR fixes the selection side — the commit wire (#1297) completes the path.

## Test plan

- [ ] `packages/studio/src/components/editor/domEditingLayers.test.ts` — two new tests with jsdom environment:
  - `resolveDomEditSelection` on an element with `data-hf-id` → `selection.hfId` is populated
  - element without `data-hf-id` → `selection.hfId` is `undefined`
- [ ] All 65 studio test files pass, all 72 core test files pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-06-09 14:18:14 -07:00
Miguel Ángel 04c77fa7aa chore: release v0.6.86 2026-06-09 19:50:45 +00:00
xiayeandClaude Opus 4.7 5a0966dfeb fix(runtime,player): replay bridge state on iframe ready to repair race
The audio-locked attribute was correctly setting `muted = true` and posting
`set-muted` to the iframe runtime, but on warm-cache reloads of claude.ai
and inside the Claude desktop Electron client, the iframe finishes loading
*after* the parent has already sent control messages — the iframe runtime's
postMessage listener isn't installed yet, so the messages are silently
dropped. Audio plays unmuted with no UI to recover.

Confirmed via:
- "First open" on claude.ai: cold cache, iframe slow → listener up before
  `set-muted` lands → audio muted 
- "Hard refresh" on claude.ai: warm cache, iframe fast → listener up after
  message arrives → message lost → audio plays 
- Claude desktop: Electron renderer consistently fast → race always loses
  → audio plays 

Fix: add a `{source: "hf-preview", type: "ready"}` event the runtime emits
once `installRuntimeControlBridge` has registered the listener. The player
listens for it and replays current bridge state (`set-muted`, `set-volume`,
`set-playback-rate`). Pre-ready messages are now safe to send — they'll be
replayed once the runtime can receive them.

The replay is idempotent — re-asserting defaults is a no-op — so it's also
safe across iframe reloads (new runtime instance emits ready again).

Tests: 6 new (1 bridge: ready posted on install; 5 player: replays muted /
volume / playback-rate / audio-locked-forced-mute / handles second ready /
ignores ready from wrong source). Suites green: core 1387, player 137.

Refs:
- Investigation: heygen-com/hyperframes#1300 (UA-fallback attempt — unrelated
  to actual root cause)
- claude.ai-web.log analysis revealed cross-origin iframe + race condition,
  not attribute stripping as originally hypothesized

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-09 12:22:27 -07:00
Miguel Ángel acd8e11789 chore: release v0.6.85 2026-06-09 17:14:52 +00:00
Vance IngallsandClaude Sonnet 4.6 3d7d7c0291 refactor(core,studio): extract draft-marker constants + R7 code-review fixes (Task 4) (#1292)
* refactor(core,studio): extract draft-marker constants to core (R7, Task 4)

Create draftMarkers.ts in core with 5 shared CSS custom property names and the
gesture DOM attribute. PreviewAdapter imports from draftMarkers.ts instead of
hardcoding strings. Adds @hyperframes/core/studio-api/draft-markers export
subpath. Studio's manualEditsTypes.ts re-exports the shared constants from core
so all existing call sites are unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(core): address R7 code-review findings (C1–C14, P6–P7)

- previewAdapter: auto-revert previous gesture in applyDraft (C3); clearDraftProps
  on commitPreview not just revertDraft (C4); isVisible NaN→visible for JSDOM (P7);
  remove redundant GestureState.hfId field (C12); remove Array.from (C14);
  extract clearDraftProps/revertGesture helpers (C5/C6)
- hfIdPersist: replace string-equality change detection with attribute count to
  avoid false-positive writes on single-quoted HTML (C1); re-read disk before
  write for TOCTOU guard (C7); remove normalizeHfIds wrapper (C11)
- preview.ts: remove dead null-check on normalizedDisk after diskMain guard (C9);
  catch path re-reads disk fresh instead of using stale pre-request snapshot (C8)
- hfIds.test.ts: replace tautological second stability test with cross-document
  content-keyed id stability test (P6)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(core): follow-up R7 review fixes — CSS.escape fallback, invariant docs, new edge-case tests

- hfIdPersist: remove ensureHfIds re-export (P2); add JSDoc invariant note;
  improve TOCTOU comment; pass err to console.warn
- preview.ts: split import — ensureHfIds from parsers/hfIds.js (not re-export)
- previewAdapter: CSS.escape + inline fallback for non-browser environments;
  add JSDoc for atTime caller-seek contract; add 0.01 opacity-threshold comment
- previewAdapter.test: rename atTime test to clarify adapter-does-not-seek;
  add nested-hf-root-without-id test; add resize→move prop-leak test;
  add revertDraft-after-commit no-op test

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(core): bundle-vs-disk id-stability test; comment double ensureHfIds (P3)

- preview.test: add "bundle returning untagged HTML gets same ids as disk" test —
  guards against id divergence when bundler reads a pre-write cache snapshot;
  content-keyed FNV1a minting ensures served ids == disk ids for same source HTML
- preview.ts: comment the second ensureHfIds call explaining it's intentional for
  adapter-injected elements and idempotent on the no-bundle path (P3 from miguel)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs(core): wire-contract comment on mintHfId + fallow suppressions (R7)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 01:13:54 -07:00
Vance IngallsandClaude Sonnet 4.6 1a23938bef feat(core): implement createPreviewAdapter (R7, Task 3) (#1291)
* test(core): data-hf-id survives id/selector patch (R1, T7)

Locks the preservation guarantee the write-back design depends on: a
Studio edit targeting by id or selector (it never sends hfId) must not strip
an existing data-hf-id, or the stable handle is destroyed by the next edit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(core): escape hfId in selector + warn on duplicate match (R1, T7 review)

Addresses review on #1272 (Miguel P3 + Rames): findTargetElement interpolated
target.hfId raw into a [data-hf-id="..."] selector. Escape it (CSS attr-value
injection guard) and warn when a hfId matches more than one element instead of
silently patching an arbitrary one. Adds an injection-guard test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(core): implement createPreviewAdapter — greens 20 T10 tests (R7, Task 3)

elementAtPoint: resolvePoint callback → walk ancestors for data-hf-id,
skip data-hf-root without data-hf-id (stage root), skip opacity-0 elements.

applyDraft: find element by hfId, record originalTranslate, set
--hf-studio-offset-x/y (move) or --hf-studio-width/height (resize),
mark data-hf-studio-manual-edit-gesture.

revertDraft: remove draft CSS props, clear gesture marker, restore
originalTranslate if one was recorded.

commitPreview: extract patch (move→moveElement, resize→resize with w/h
renamed to width/height), clear gesture marker, return patch or null.

getElementTimings: scan [data-hf-id] elements, parse data-start/data-end
as floats, return map with undefined fields for absent attributes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(core): remove explicit data-hf-id from htmlParser tests so ensureHfIds mints hf- ids

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 00:39:53 -07:00
Vance IngallsandClaude Sonnet 4.6 740a83abb3 feat(core): hf-id write-back to disk + serve-time surfacing (R7, Tasks 1-2) (#1289)
* feat(core): clip-model hf- ids minted at parse, emitted as data-hf-id (R1)

* docs(core): document legacy-id round-trip in clip-model readback (R1 review)

Addresses Rames' review on #1270: clarifies that a pre-R1 clip authored with
id="my-title" round-trips as data-hf-id="my-title" (non-hf-shaped but stable,
exact-match) by design — targeting uses exact [data-hf-id="…"] match and does
not require the hf- shape; legacy values re-mint only at the R7 write-back. Not
a bug. Comment-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(core): fix misleading legacy-id migration comment in htmlParser.ts

The original comment said legacy data-hf-id values "are re-minted only
once the R7 write-back persists freshly-minted ids to source" — which is
incorrect. ensureHfIds skips elements that already carry data-hf-id, so
legacy values (e.g. data-hf-id="my-title") persist indefinitely and are
NOT automatically re-minted. Exact-match targeting still works correctly.
Update comment to reflect actual behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(studio): sourcePatcher data-hf-id targeting (R1, T3)

* fix(studio): warn on duplicate match in execDataAttrPattern (R1, T3 review)

Addresses Rames' review on #1271: execDataAttrPattern returned the first regex
match without checking for a second. A duplicate id/data-hf-id in source (id
drift) would silently patch one element and leave the other stale. Now warns
when more than one element matches. By the mint contract it should never fire.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(studio): pin hfId-is-authoritative-over-selector contract (R1, T3 review)

Adds test: "hfId match is authoritative — selector is not used as a
narrowing filter". When hfId matches element A and selector points at
element B, findTagByTarget returns A without consulting selector as a
narrowing filter. Pins the intended behaviour so a future refactor
cannot silently start narrowing by selector.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(core): sourceMutation data-hf-id targeting (R1, T7)

* test(core): update htmlParser baselines for R1 hf- id format

Elements now get data-hf-id minted by ensureHfIds; parser reads
data-hf-id as model id, so HTML id attrs are no longer the model id.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(core): data-hf-id survives id/selector patch (R1, T7)

Locks the preservation guarantee the write-back design depends on: a
Studio edit targeting by id or selector (it never sends hfId) must not strip
an existing data-hf-id, or the stable handle is destroyed by the next edit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(core): escape hfId in selector + warn on duplicate match (R1, T7 review)

Addresses review on #1272 (Miguel P3 + Rames): findTargetElement interpolated
target.hfId raw into a [data-hf-id="..."] selector. Escape it (CSS attr-value
injection guard) and warn when a hfId matches more than one element instead of
silently patching an arbitrary one. Adds an injection-guard test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(core): previewAdapter contract failing tests (T10 spec for R7)

* feat(core): hf-id write-back to disk + serve-time surfacing (R7, Task 1-2)

* test(core): replace tautological stability tests with real disk tests for persistHfIdsIfNeeded

Prior tests only exercised normalizeHfIds (pure function) and the existing
pin guard in ensureHfIds — both pass on the parent commit without any Task 1
code. Replace with three tests that exercise the actual disk write-back:

- writes data-hf-id to disk when source is untagged
- does not rewrite disk when source is already tagged (idempotent)
- returned id matches id written to disk (serve-time == persist-time invariant)

These fail on the parent commit (persistHfIdsIfNeeded doesn't exist) and
green after Task 1.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(core): route-level tests for data-hf-id surfacing and disk write-back (R7, Task 1-2)

Two integration tests against the preview route (via Hono test harness):

- served HTML carries data-hf-id on body elements (>= 2 matches for div+p)
- disk file contains data-hf-id after first GET (write-back verified via readFileSync)

These fail on the parent commit (no hfIdPersist wiring in preview.ts) and
green after Task 1. Closes the verification gap flagged in review.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 00:28:52 -07:00
Vance IngallsandClaude Opus 4.8 72e4f1a0f6 test(core): previewAdapter contract failing tests (T10 spec for R7) (#1286)
* feat(core): clip-model hf- ids minted at parse, emitted as data-hf-id (R1)

* docs(core): document legacy-id round-trip in clip-model readback (R1 review)

Addresses Rames' review on #1270: clarifies that a pre-R1 clip authored with
id="my-title" round-trips as data-hf-id="my-title" (non-hf-shaped but stable,
exact-match) by design — targeting uses exact [data-hf-id="…"] match and does
not require the hf- shape; legacy values re-mint only at the R7 write-back. Not
a bug. Comment-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(core): fix misleading legacy-id migration comment in htmlParser.ts

The original comment said legacy data-hf-id values "are re-minted only
once the R7 write-back persists freshly-minted ids to source" — which is
incorrect. ensureHfIds skips elements that already carry data-hf-id, so
legacy values (e.g. data-hf-id="my-title") persist indefinitely and are
NOT automatically re-minted. Exact-match targeting still works correctly.
Update comment to reflect actual behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(studio): sourcePatcher data-hf-id targeting (R1, T3)

* fix(studio): warn on duplicate match in execDataAttrPattern (R1, T3 review)

Addresses Rames' review on #1271: execDataAttrPattern returned the first regex
match without checking for a second. A duplicate id/data-hf-id in source (id
drift) would silently patch one element and leave the other stale. Now warns
when more than one element matches. By the mint contract it should never fire.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(studio): pin hfId-is-authoritative-over-selector contract (R1, T3 review)

Adds test: "hfId match is authoritative — selector is not used as a
narrowing filter". When hfId matches element A and selector points at
element B, findTagByTarget returns A without consulting selector as a
narrowing filter. Pins the intended behaviour so a future refactor
cannot silently start narrowing by selector.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(core): sourceMutation data-hf-id targeting (R1, T7)

* test(core): update htmlParser baselines for R1 hf- id format

Elements now get data-hf-id minted by ensureHfIds; parser reads
data-hf-id as model id, so HTML id attrs are no longer the model id.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(core): data-hf-id survives id/selector patch (R1, T7)

Locks the preservation guarantee the write-back design depends on: a
Studio edit targeting by id or selector (it never sends hfId) must not strip
an existing data-hf-id, or the stable handle is destroyed by the next edit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(core): escape hfId in selector + warn on duplicate match (R1, T7 review)

Addresses review on #1272 (Miguel P3 + Rames): findTargetElement interpolated
target.hfId raw into a [data-hf-id="..."] selector. Escape it (CSS attr-value
injection guard) and warn when a hfId matches more than one element instead of
silently patching an arbitrary one. Adds an injection-guard test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(core): previewAdapter contract failing tests (T10 spec for R7)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 00:28:21 -07:00
Vance IngallsandClaude Opus 4.8 1cd4ad6ab8 feat(core): sourceMutation data-hf-id targeting (R1, T7) (#1272)
* feat(core): clip-model hf- ids minted at parse, emitted as data-hf-id (R1)

* docs(core): document legacy-id round-trip in clip-model readback (R1 review)

Addresses Rames' review on #1270: clarifies that a pre-R1 clip authored with
id="my-title" round-trips as data-hf-id="my-title" (non-hf-shaped but stable,
exact-match) by design — targeting uses exact [data-hf-id="…"] match and does
not require the hf- shape; legacy values re-mint only at the R7 write-back. Not
a bug. Comment-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(core): fix misleading legacy-id migration comment in htmlParser.ts

The original comment said legacy data-hf-id values "are re-minted only
once the R7 write-back persists freshly-minted ids to source" — which is
incorrect. ensureHfIds skips elements that already carry data-hf-id, so
legacy values (e.g. data-hf-id="my-title") persist indefinitely and are
NOT automatically re-minted. Exact-match targeting still works correctly.
Update comment to reflect actual behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(studio): sourcePatcher data-hf-id targeting (R1, T3)

* fix(studio): warn on duplicate match in execDataAttrPattern (R1, T3 review)

Addresses Rames' review on #1271: execDataAttrPattern returned the first regex
match without checking for a second. A duplicate id/data-hf-id in source (id
drift) would silently patch one element and leave the other stale. Now warns
when more than one element matches. By the mint contract it should never fire.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(studio): pin hfId-is-authoritative-over-selector contract (R1, T3 review)

Adds test: "hfId match is authoritative — selector is not used as a
narrowing filter". When hfId matches element A and selector points at
element B, findTagByTarget returns A without consulting selector as a
narrowing filter. Pins the intended behaviour so a future refactor
cannot silently start narrowing by selector.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(core): sourceMutation data-hf-id targeting (R1, T7)

* test(core): update htmlParser baselines for R1 hf- id format

Elements now get data-hf-id minted by ensureHfIds; parser reads
data-hf-id as model id, so HTML id attrs are no longer the model id.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(core): data-hf-id survives id/selector patch (R1, T7)

Locks the preservation guarantee the write-back design depends on: a
Studio edit targeting by id or selector (it never sends hfId) must not strip
an existing data-hf-id, or the stable handle is destroyed by the next edit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(core): escape hfId in selector + warn on duplicate match (R1, T7 review)

Addresses review on #1272 (Miguel P3 + Rames): findTargetElement interpolated
target.hfId raw into a [data-hf-id="..."] selector. Escape it (CSS attr-value
injection guard) and warn when a hfId matches more than one element instead of
silently patching an arbitrary one. Adds an injection-guard test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 00:27:52 -07:00
Vance IngallsandClaude Sonnet 4.6 8d6b7cf515 feat(core): clip-model hf- ids minted at parse, emitted as data-hf-id (R1) (#1270)
* feat(core): clip-model hf- ids minted at parse, emitted as data-hf-id (R1)

* docs(core): document legacy-id round-trip in clip-model readback (R1 review)

Addresses Rames' review on #1270: clarifies that a pre-R1 clip authored with
id="my-title" round-trips as data-hf-id="my-title" (non-hf-shaped but stable,
exact-match) by design — targeting uses exact [data-hf-id="…"] match and does
not require the hf- shape; legacy values re-mint only at the R7 write-back. Not
a bug. Comment-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(core): fix misleading legacy-id migration comment in htmlParser.ts

The original comment said legacy data-hf-id values "are re-minted only
once the R7 write-back persists freshly-minted ids to source" — which is
incorrect. ensureHfIds skips elements that already carry data-hf-id, so
legacy values (e.g. data-hf-id="my-title") persist indefinitely and are
NOT automatically re-minted. Exact-match targeting still works correctly.
Update comment to reflect actual behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(core): update htmlParser tests for R1 hf- id minting

Pre-R1 tests expected clip ids to reflect legacy `id=` attributes.
After R1, ensureHfIds runs first and mints data-hf-id — so clip.id
reflects the minted hf- value unless the element already has data-hf-id.

Fix: add explicit data-hf-id to test HTML elements where tests assert
specific id values. Update no-id test to expect hf- format (/^hf-[a-z0-9]{4}$/)
instead of the pre-R1 generated-id fallback (/^element-\d+$/).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 00:19:31 -07:00
Vance Ingalls c12854b47c feat(core): ensureHfIds node-id pass + shared hf- mint helper (R1) (#1269)
## What

Adds `ensureHfIds(html: string): string` in `packages/core/src/parsers/hfIds.ts`. Single DOM pass (via linkedom) that mints a `data-hf-id` attribute on every eligible element before the caller sees the markup.

**Id derivation:** FNV-1a 32-bit hash of `tagName | sorted-attrs(\x00/\x01 separated) | ownText`, last 4 chars of base-36, `hf-` prefix. Collision resolution appends a sibling counter and re-hashes. Preserves existing ids (elements with `data-hf-id` already set are skipped). Excludes non-visual tags: `script`, `style`, `template`, `meta`, `link`, `noscript`, `base`.

**Fragment handling:** detects bare HTML fragments (no `<!doctype` / `<html`) and wraps in a full document shell before parsing, then returns `body.innerHTML` — matching the pattern used by `parseSourceDocument` in `sourceMutation`.

## Why

Counter-based ids (`element-0`, `element-1`, …) are positional. Inserting a new layer at position 0 shifts every id below it. The R1 milestone requires content-based, stable ids so that targeting operations (split, patch, probe) stay valid across re-parses and element insertions. T2 spec (`stableIds.test.ts`) defines the contract: same content → same id, adding a sibling doesn't change other ids, format matches `/^hf-[a-z0-9]{4}$/`.

## How

- `toHfId(hash)` — `slice(-4)` of `hash.toString(36)` for better distribution across the suffix space
- `data-hf-id` is excluded from the hash input (prevents circular dependency on the attribute being set)
- Already-assigned ids tracked in a `Set`; duplicates get a counter suffix before re-hashing
- Fragment detection: `/<!doctype|<html[\s>]/i.test(html)` — if bare, wrap→parse→`body.innerHTML`

## Test plan

- [x] T2 spec: `packages/core/src/parsers/stableIds.test.ts` — 7 tests pass (3 were `.fails` stubs targeting R1; 4 were pre-existing baselines that must not regress)
- [x] No changes to existing htmlParser tests needed at this layer (wiring is PR #1270)
2026-06-08 23:28:25 -07:00
Miguel Ángel 4adc9b1108 chore: release v0.6.84 2026-06-09 01:30:37 +00:00
Miguel Ángel 908b455e5e fix(core): apply playbackRate to all media duration resolution sites (#1288)
Extract readElementPlaybackRate() to eliminate clamping duplication across
media.ts, init.ts, startResolver.ts, and timeline.ts. Apply the rate
division to the two remaining sites that were missed:

- startResolver.ts: visibility loop used raw source duration, hiding
  slowed-down videos mid-playback when no data-duration was set
- timeline.ts: resolveMediaElementDurationSeconds underreported the end
  window sent to the renderer, affecting preview parity

Also adds direct tests for readElementPlaybackRate().
2026-06-08 21:29:04 -04:00
Miguel Ángel 48711ab135 chore: release v0.6.83 2026-06-09 01:18:39 +00:00
Miguel Ángel 7b6c87fa88 fix(core): account for playbackRate in media duration resolution (#1287)
* fix(core): account for playbackRate in media duration resolution

resolveDurationSeconds computed sourceDuration as (element.duration - mediaStart)
without dividing by playbackRate. A 5s source at 0.5x should span 10s on the
timeline, but was capped at 5s — causing the video to go black once the raw
source was exhausted.

Read defaultPlaybackRate from the element (same clamping as refreshRuntimeMediaCache)
and divide sourceDuration by it so the effective timeline window matches the
actual playback speed.

* test(core): add regression test for playbackRate in resolveDurationSeconds

Pins the fix: a 5s source at 0.5x playbackRate must resolve to 10s effective
duration when resolveDurationSeconds is provided (mirroring the init.ts callback
pattern). Without the rate division, this would return 5s and clip early.
2026-06-08 21:17:41 -04:00
Miguel Ángel 1cc9b0d0ed chore: release v0.6.82 2026-06-08 20:47:43 +00:00
Miguel Ángel 1bcd6ec3b3 fix(core): re-register hf-timelines-built listener in maybePublishRenderReady (#1279)
Compositions that defer gsap.timeline() registration past DOMContentLoaded
(via setTimeout, template instantiation, or dynamic script loading) hit a
race where __renderReady stays false forever:

1. At DOMContentLoaded, __hfTimelinesBuilding is false — init.ts skips
   the hf-timelines-built listener and sets __renderReady = true
2. The deferred script runs, calls gsap.timeline().to() which sets
   __hfTimelinesBuilding = true via the batching proxy
3. The deferred maybePublishRenderReady() sees building=true, sets
   __renderReady = false, but never registers a listener to retry
4. __renderReady stays false, __hf.duration returns 0, pollHfReady
   times out with "Composition has zero duration"

Fix: when maybePublishRenderReady encounters __hfTimelinesBuilding=true,
register a one-shot hf-timelines-built listener to retry — matching the
pattern already used at init time for the synchronous batching case.

Closes #1260
2026-06-08 16:45:40 -04:00
Vance Ingalls 63fd322df1 test(core): address review feedback on T6a / T10 / T7 stubs (#1268)
T6a (#1263): add fromTo corpus script (third parser method) with negative
position values to exercise UnaryExpression arm; drop unused breatheRepeats
from COMPLEX_SCRIPT; generate fromto.parsed.json + fromto.serialized.js goldens.

T10 (#1262): split applyDraft move/resize into two stubs; add applyDraft edge-
case describe block (concurrent gestures, idempotent revert, playhead-change
stability, nested sub-composition root); add getElementTimings stub for absent
data-start/end on a data-hf-id element.

T7 (#1267): expand to full parity with T3 — add text-content, attribute, and
fallthrough stubs (Core sourceMutation supports all patch types via
patchElementInHtml).
2026-06-07 19:57:57 -07:00
Vance Ingalls 1e705b1107 test(core,studio): add T3+T7 hfId targeting stubs (spec for R1) (#1267)
* test(studio): add T5b rotation+motion build-patches characterization

Extends manualEditsDomPatches.test.ts with rotation and motion pairs.
Same 4-pattern structure: populated, empty, clear restores originals,
build/clear symmetry. Merges duplicate manualEditsTypes import block.

* test(studio): add T5c review-fix gaps in manualEditsDomPatches characterization

Fixes four gaps identified in max-setting code review:
- Box-size clear: replace arrayContaining with full ordered toEqual (30 ops)
- Box-size / pathOffset / rotation clear: add empty-string coercion tests
  (origVal||null must produce null, not set property to "")
- Rotation clear: add test for absent STUDIO_ORIGINAL_ROTATION_TRANSFORM_ORIGIN_ATTR
- Motion clear: prove input-independence by calling with both empty and populated
  element and asserting identical output

* refactor(core): extract maxEndTime+serialize to parsers/test-utils.ts (TU)

Deduplicate helpers shared by T1 (htmlParser.roundtrip.test.ts) and T2
(stableIds.test.ts). Both files inline identical implementations; extract
to test-utils.ts so future parser tests (T6a…) import one copy.

Also fix lefthook fallow command to unset GIT_DIR+GIT_INDEX_FILE before
running — those vars are set by git in worktree hook context and block
fallow’s internal temp-worktree creation.

* test(core): add T10 PreviewAdapter contract stubs (spec for R7)

All 14 tests are it.todo, following the T4 pattern. The stubs define the
full createPreviewAdapter interface — elementAtPoint (root exclusion,
hf-id ancestor walk, opacity filter), applyDraft/revertDraft (draft
marker lifecycle), commitPreview (patch derivation), and getElementTimings
(data-start/data-end reader).

createPreviewAdapter does not exist yet; R7 implements it and converts
these stubs to real assertions.

* test(core): add T6a GSAP parser golden baselines (Recast/Babel snapshot)

6 toMatchFileSnapshot tests across 3 representative scripts (minimal,
moderate, complex). Captures parseGsapScript + serializeGsapAnimations
output before the Recast → Meriyah swap so any parser change is detected
as a golden diff rather than a silent behavioral regression.

Goldens live in src/parsers/__goldens__/ and are checked in. Add
__goldens__/** to fallow ignorePatterns (data files, not modules) and to
.prettierignore so oxfmt does not reformat vitest-written snapshot files.

* test(core,studio): add T3+T7 hfId targeting stubs (spec for R1)

T3 (sourcePatcher.test.ts): 5 it.todo stubs for PatchTarget.hfId targeting
— style, text, attribute patches plus preservation and fallthrough cases.

T7 (sourceMutation.test.ts): 2 it.todo stubs for SourceMutationTarget.hfId
— basic patch and data-hf-id survival after patch.

Neither interface has hfId yet. R1 adds the field + [data-hf-id="…"] branch
in findTagByTarget / findTargetElement, then converts these to real assertions.
2026-06-07 19:23:27 -07:00
Vance Ingalls 184ef03317 test(core): add T6a GSAP parser golden baselines (Recast/Babel snapshot) (#1263)
* test(studio): add T5b rotation+motion build-patches characterization

Extends manualEditsDomPatches.test.ts with rotation and motion pairs.
Same 4-pattern structure: populated, empty, clear restores originals,
build/clear symmetry. Merges duplicate manualEditsTypes import block.

* test(studio): add T5c review-fix gaps in manualEditsDomPatches characterization

Fixes four gaps identified in max-setting code review:
- Box-size clear: replace arrayContaining with full ordered toEqual (30 ops)
- Box-size / pathOffset / rotation clear: add empty-string coercion tests
  (origVal||null must produce null, not set property to "")
- Rotation clear: add test for absent STUDIO_ORIGINAL_ROTATION_TRANSFORM_ORIGIN_ATTR
- Motion clear: prove input-independence by calling with both empty and populated
  element and asserting identical output

* refactor(core): extract maxEndTime+serialize to parsers/test-utils.ts (TU)

Deduplicate helpers shared by T1 (htmlParser.roundtrip.test.ts) and T2
(stableIds.test.ts). Both files inline identical implementations; extract
to test-utils.ts so future parser tests (T6a…) import one copy.

Also fix lefthook fallow command to unset GIT_DIR+GIT_INDEX_FILE before
running — those vars are set by git in worktree hook context and block
fallow’s internal temp-worktree creation.

* test(core): add T10 PreviewAdapter contract stubs (spec for R7)

All 14 tests are it.todo, following the T4 pattern. The stubs define the
full createPreviewAdapter interface — elementAtPoint (root exclusion,
hf-id ancestor walk, opacity filter), applyDraft/revertDraft (draft
marker lifecycle), commitPreview (patch derivation), and getElementTimings
(data-start/data-end reader).

createPreviewAdapter does not exist yet; R7 implements it and converts
these stubs to real assertions.

* test(core): add T6a GSAP parser golden baselines (Recast/Babel snapshot)

6 toMatchFileSnapshot tests across 3 representative scripts (minimal,
moderate, complex). Captures parseGsapScript + serializeGsapAnimations
output before the Recast → Meriyah swap so any parser change is detected
as a golden diff rather than a silent behavioral regression.

Goldens live in src/parsers/__goldens__/ and are checked in. Add
__goldens__/** to fallow ignorePatterns (data files, not modules) and to
.prettierignore so oxfmt does not reformat vitest-written snapshot files.
2026-06-07 19:23:06 -07:00
Vance Ingalls 1fdce71c8b test(core): add T10 PreviewAdapter contract stubs (spec for R7) (#1262)
* test(studio): add T5b rotation+motion build-patches characterization

Extends manualEditsDomPatches.test.ts with rotation and motion pairs.
Same 4-pattern structure: populated, empty, clear restores originals,
build/clear symmetry. Merges duplicate manualEditsTypes import block.

* test(studio): add T5c review-fix gaps in manualEditsDomPatches characterization

Fixes four gaps identified in max-setting code review:
- Box-size clear: replace arrayContaining with full ordered toEqual (30 ops)
- Box-size / pathOffset / rotation clear: add empty-string coercion tests
  (origVal||null must produce null, not set property to "")
- Rotation clear: add test for absent STUDIO_ORIGINAL_ROTATION_TRANSFORM_ORIGIN_ATTR
- Motion clear: prove input-independence by calling with both empty and populated
  element and asserting identical output

* refactor(core): extract maxEndTime+serialize to parsers/test-utils.ts (TU)

Deduplicate helpers shared by T1 (htmlParser.roundtrip.test.ts) and T2
(stableIds.test.ts). Both files inline identical implementations; extract
to test-utils.ts so future parser tests (T6a…) import one copy.

Also fix lefthook fallow command to unset GIT_DIR+GIT_INDEX_FILE before
running — those vars are set by git in worktree hook context and block
fallow’s internal temp-worktree creation.

* test(core): add T10 PreviewAdapter contract stubs (spec for R7)

All 14 tests are it.todo, following the T4 pattern. The stubs define the
full createPreviewAdapter interface — elementAtPoint (root exclusion,
hf-id ancestor walk, opacity filter), applyDraft/revertDraft (draft
marker lifecycle), commitPreview (patch derivation), and getElementTimings
(data-start/data-end reader).

createPreviewAdapter does not exist yet; R7 implements it and converts
these stubs to real assertions.
2026-06-07 19:22:34 -07:00
Vance Ingalls ffc54c7867 refactor(core): extract maxEndTime+serialize to parsers/test-utils.ts (TU) (#1261)
* test(studio): add T5b rotation+motion build-patches characterization

Extends manualEditsDomPatches.test.ts with rotation and motion pairs.
Same 4-pattern structure: populated, empty, clear restores originals,
build/clear symmetry. Merges duplicate manualEditsTypes import block.

* test(studio): add T5c review-fix gaps in manualEditsDomPatches characterization

Fixes four gaps identified in max-setting code review:
- Box-size clear: replace arrayContaining with full ordered toEqual (30 ops)
- Box-size / pathOffset / rotation clear: add empty-string coercion tests
  (origVal||null must produce null, not set property to "")
- Rotation clear: add test for absent STUDIO_ORIGINAL_ROTATION_TRANSFORM_ORIGIN_ATTR
- Motion clear: prove input-independence by calling with both empty and populated
  element and asserting identical output

* refactor(core): extract maxEndTime+serialize to parsers/test-utils.ts (TU)

Deduplicate helpers shared by T1 (htmlParser.roundtrip.test.ts) and T2
(stableIds.test.ts). Both files inline identical implementations; extract
to test-utils.ts so future parser tests (T6a…) import one copy.

Also fix lefthook fallow command to unset GIT_DIR+GIT_INDEX_FILE before
running — those vars are set by git in worktree hook context and block
fallow’s internal temp-worktree creation.
2026-06-07 19:22:07 -07:00
James 4b8749c642 chore: release v0.6.81 2026-06-07 22:03:14 +00:00
Miguel Ángel 0bf15119f8 feat: font resolution pipeline — compositions capture and embed their own fonts (#1255)
Compositions are now self-contained: the compiler captures font files
and embeds them as woff2 data URIs, eliminating silent render-time
fallback when the render environment lacks the author's fonts.

Resolution order (each tier falls through to the next):
1. Existing @font-face → use as-is
2. Bundled alias (38 cross-platform mappings) → embed data URI
3. Google Fonts → fetch, cache, embed
4. Local system font → locate on OS, compress to woff2, embed
5. Local @font-face paths → read file, compress, inline as data URI
6. External CDN stylesheets → fetch CSS, extract @font-face, inline
7. Alias map fallback → closest bundled equivalent
8. Actionable error with guidance

Key changes:
- System font locator (macOS/Windows/Linux) with path-bounding and
  symlink defense (realpathSync + O_NOFOLLOW)
- woff2 compression via wawoff2 (WASM, cross-platform)
- Multi-weight/style variant capture with length-sorted token matching
- External stylesheet inlining with SSRF defense (assertPublicHttpsUrl,
  HTTPS-only, private-host blocking, 2MB cap, 4-concurrent limit)
- Studio auto-import via GET /fonts/file API + renderAliasFor() derived
  from shared FONT_ALIAS_MAP (no more hand-curated drift)
- failClosedFontFetch throws on unresolved fonts in distributed renders
- Single source of truth: @hyperframes/core/fonts/aliases
- system_font_will_alias lint rule (escalates to warning for distributed)
- Default to Inter + JetBrains Mono in templates and CSS reset
2026-06-07 17:52:19 -04:00
Vance IngallsandClaude Sonnet 4.6 806b226b34 test(core): add T9 CompositionVariable font/image parse spec (before R1) (#1246)
* test(core): add T9 CompositionVariable font/image parse spec (before R1)

* fix(core): mark font+image spec tests as it.fails() — not yet implemented

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 11:41:21 -07:00
48fcf4ada5 feat(registry): add morph-text component and text-effects catalog section (#1247)
* feat(gsap): add innerText support to GSAP inspector for counter animations (#1244)

Adds 'innerText' as a supported GSAP property so number roll-up animations
(count-up from 0 to some value) are visible and editable in the GSAP inspector
panel.

- Add 'innerText' to SUPPORTED_PROPS in gsapConstants.ts
- Add label 'Counter Value', tooltip, and step constraint (1) in
  gsapAnimationConstants.ts

The snap modifier that controls integer rounding is already preserved
verbatim via the EXTRAS_KEYS round-trip, so rounding behavior survives
edits without any additional UI changes.

Closes #1179

* feat(registry): add text-effects catalog section and morph-text component

Introduces a new "Text Effects" catalog section (below Effects) for text-focused visual components.

- Add `text-effects` BlockCategory to core registry types with violet color
- Add `text-effect` tag resolver in resolveBlockCategory (checked before generic `effect` tag)
- Tag caption-blend-difference, texture-mask-text, and morph-text with `text-effect`
- Update studio catalog order and color map to include text-effects
- Add morph-text component: gooey SVG threshold morph cycling through editable statements
  using GSAP seekable proxy pattern for deterministic/seekable rendering

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(registry): add morph-text preview video

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(registry): fix morph-text.html formatting

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs(catalog): add Text Effects section and morph-text page

Moves caption-blend-difference and texture-mask-text out of Effects into a new
"Text Effects" section below it. Adds morph-text component page with install
instructions and preview video.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(registry): add demo.html for morph-text catalog preview rendering

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(registry): address PR review feedback on morph-text and text-effects

- Restore `effect` tag on caption-blend-difference and texture-mask-text
  alongside `text-effect` so existing tag-equality searches/analytics still match
- Fix morphPause script fallback from "0.25" to "1.5" to match data attribute default
- Add Math.max(0, ...) guard to blur values (intent clarity)
- Add prefers-reduced-motion: skip morph and show first word statically
- Remove CATEGORY_ORDER record from useBlockCatalog; derive order from
  BLOCK_CATEGORIES array (single source of truth, no drift)
- Add comment to demo.html documenting its purpose (catalog preview script only)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 10:55:13 -07:00
Vance IngallsandClaude Sonnet 4.6 a890d093e1 test(core): add T2 stable id spec for parse-to-hf id contract (before R1) (#1245)
* test(studio): add T4 op-contract stubs for editor dispatch boundary

* test(core): add T2 stable id spec for parse-to-hf id contract (before R1)

* test(core): mark pre-R1 spec tests as it.fails so CI passes

The three [spec] tests document intended R1 behavior that the parser
does not yet implement. Using it.fails() makes them green while the
spec is pre-R1; they will flip red again once R1 lands and starts
returning hf- prefixed ids.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 10:38:48 -07:00
Miguel Ángel 53eb8215c6 chore: release v0.6.80 2026-06-07 13:35:24 +00:00
Miguel Ángel ebd156bcc1 fix: batch GSAP timeline construction to prevent main-thread hang (#1231) (#1249)
* fix: batch GSAP timeline construction to prevent main-thread hang (#1231)

Compositions with thousands of tl.to() calls (e.g. 8,562 in the
reported case) block Chrome's main thread synchronously during HTML
parsing, preventing DOMContentLoaded from firing before Puppeteer's
navigation timeout. This caused render jobs to hang indefinitely at
'Initializing calibration session...' with no error message.

Root cause: GSAP's timeline API is synchronous — each tl.to() call
registers a tween immediately on the main thread. A script with 8k+
calls holds the thread for seconds, starving the browser event loop and
delaying DCL past the navigation timeout window.

Fix: install a property trap on window.gsap in HF_EARLY_STUB (injected
at the top of <head>, before GSAP or user scripts load). When GSAP
assigns itself to window.gsap, the setter intercepts the real gsap
object and wraps gsap.timeline() to return a proxy that queues tween
descriptors (to/from/fromTo/set) instead of calling them synchronously.
A requestAnimationFrame-based flush loop drains 100 tweens per frame,
yielding the main thread between batches so DCL can fire.

When the queue is drained, the stub sets window.__hfTimelinesBuilding =
false and dispatches a 'hf-timelines-built' CustomEvent. init.ts checks
this flag at DOMContentLoaded time; if building is still in progress it
defers bindRootTimelineIfAvailable() until the event fires, then sets
window.__renderReady = true as normal. pollHfReady continues to gate
on both __renderReady and window.__hf.duration > 0, so the render
pipeline does not start until the full timeline is bound.

- Batch size: 100 tweens/rAF tick (empirical; ~4ms/batch at 8k scale)
- Yield mechanism: requestAnimationFrame (cooperative, no setTimeout(0))
- Determinism: 'hf-timelines-built' event guarantees sequencing
- Proxy forwards: pause/seek/totalTime/time/duration/add/paused/
  timeScale/play delegate to the real timeline immediately
- No GSAP package changes; no navigation timeout increase

Fixes #1231

* style: apply oxfmt formatting to producer stub files

* fix(producer): unwrap proxy children in add(), gate setter return on args.length

Addresses two latent correctness concerns from code review:

1. proxy.add() now unwraps __hfReal from any proxy child before passing it
   to the real timeline. GSAP's internal tween graph (_first/_next/_prev
   linkage) requires real timeline instances — proxy objects lack internal
   fields like _dp that GSAP's iteration paths expect.

2. totalTime/time/paused/timeScale now return proxy when called in setter form
   (args.length > 0). Previously these returned the real timeline, causing
   callers who chain .to(...) after a setter call to bypass batching.

Also: build-hf-early-stub.ts now runs oxfmt on the generated output file
so the format check passes in CI on every build.

* fix(producer): gate __hf.duration=0 while GSAP timelines are batching

The HF_BRIDGE_SCRIPT duration getter now returns 0 whenever
window.__hfTimelinesBuilding is true (set by HF_EARLY_STUB while the rAF
batch loop is draining queued tl.to() calls).

pollHfReady in the engine polls until window.__hf.duration > 0, so
returning 0 keeps the engine waiting until the hf-timelines-built event
fires and all tweens are committed to the real GSAP timelines.

Without this gate, normal compositions (style-6, style-13, vignelli)
were being captured mid-batch — the real timelines were empty so GSAP
could not seek them, producing frozen/blank frames in the output video.

* fix(producer): flush GSAP batching under virtual time

* fix(producer): gate render bridge on runtime readiness

* fix(producer): preserve timeline child binding under batching
2026-06-07 09:31:13 -04:00
Miguel Ángel 29d6f1eac9 fix(render): add end-to-end observability (#1248) 2026-06-06 23:55:58 -04:00
Vance Ingalls 37d02f26a2 test(core): add T8 override-set merge semantics suite for getVariables (#1241)
## What

Extends `getVariables.test.ts` with T8 from the SDK migration test plan: override-set merge semantics.

## Tests (4 new)

- **last-write-wins** — calling `setOverrides` twice; second value wins
- **sparse override** — override one key, unmentioned declared defaults survive intact
- **batch override (brand kit)** — setting all keys at once via a single override object
- **manual override after batch** — replacing one key from a kit batch; others untouched

## Scope note

Tests are labeled **"flat-merge, current behaviour"** — `getVariables` does `{...defaults, ...overrides}`. Dotted-key path resolution (`"headline.color"` as `id.prop`) is a future SDK concern; these tests validate the flat-merge contract that exists today, not the future path-resolution semantics.

## Stack

Stacked on T1 (#1240). Prerequisite for T11 (#1242), T4 (#1243).
2026-06-06 18:04:44 -07:00
Vance Ingalls dd9567446b test(core): add T1 round-trip idempotence suite for parse/serialize (#1240)
## What

Adds `htmlParser.roundtrip.test.ts` — T1 from the SDK migration test plan.

Tests that `parseHtml → generateHyperframesHtml → parseHtml` is lossless for element structure and timing. Scope is DOM/timing only; GSAP script round-trip is T6 territory.

## Tests

**Inline fixtures (5):**
- element count + ids preserved
- `startTime` / `duration` preserved
- element types preserved (`text`, `video`, `img`, `audio`)
- double-serialize stability (`serialize(parse(serialize(parse(html)))) === serialize(parse(html))`)
- empty stage doesn't throw

**Registry block sampling (10):** first 10 blocks in `registry/blocks/` — each asserts element count survives a round-trip.

## Finding

Stability test surfaced a real bug: `generateHyperframesHtml` defaults `compositionId` to `` `comp-${Date.now()}` ``. Since `ParsedHtml` doesn't capture this, every re-serialize emits a different id. The test works around it by passing a fixed `compositionId: "test-comp"` so structural instability is still detectable. The root cause is tracked as **R1 (stable hf- ids)**.

## Stack

Prerequisite for: T8 (#1241), T11 (#1242), T4 (#1243)
2026-06-06 15:38:25 -07:00
Miguel Ángel 05aac941fc feat(gsap): add innerText support to GSAP inspector for counter animations (#1244)
Adds 'innerText' as a supported GSAP property so number roll-up animations
(count-up from 0 to some value) are visible and editable in the GSAP inspector
panel.

- Add 'innerText' to SUPPORTED_PROPS in gsapConstants.ts
- Add label 'Counter Value', tooltip, and step constraint (1) in
  gsapAnimationConstants.ts

The snap modifier that controls integer rounding is already preserved
verbatim via the EXTRAS_KEYS round-trip, so rounding behavior survives
edits without any additional UI changes.

Closes #1179
2026-06-06 17:27:08 -04:00
Miguel Ángel 731bc78f63 chore: release v0.6.79 2026-06-06 20:04:55 +00:00
Miguel Ángel 272f731a67 chore: release v0.6.78 2026-06-06 20:00:52 +00:00
Miguel Ángel cf0b6f1b95 chore: release v0.6.77 2026-06-06 18:08:36 +00:00
Miguel Ángel 164167341f chore: release v0.6.76 2026-06-06 04:19:56 +00:00
Miguel Ángel bb9dbfdebd chore: bump version to 0.6.75 2026-06-05 22:17:42 -04:00
Miguel Ángel a7cc9161a7 chore: release v0.6.74 2026-06-05 18:41:55 -04:00
Miguel ÁngelandClaude Sonnet 4.6 b6a14ea9f5 fix(runtime): make audio/media sync boundary inclusive to match visibility fix (#1173)
`init.ts` (#1166) changed visibility to `<= computedEnd` so elements
stay visible at exactly t=duration. Audio clock (`init.ts:1908`) and
`syncRuntimeMedia` (`media.ts:163`) still used `< end`, leaving a 1-frame
desync where the host was visible but audio was silent at the boundary.

Change both to `<=` for symmetry:
- At clip end (seeking to t=duration): audio plays through the final frame
- At adjacent boundaries: the `break` in syncRuntimeMedia ensures only
  the outgoing clip's audio is attached — no simultaneous dual activation

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 16:38:34 -04:00