Commit Graph
371 Commits
Author SHA1 Message Date
a95e49dbda fix(core,player,studio): bound trimmed audio playback to the clip window (#1430)
* fix(player): bound the parent audio proxy to its clip window

When iframe autoplay is blocked, audible playback is promoted to a parent-frame
audio proxy. The proxy read the clip's data-start/data-duration once at adopt
time and mirrorTime() only skipped (never paused) the element outside that
window — so a trimmed/moved music clip kept playing the full source past its
on-timeline end, even though the iframe element was correctly paused.

Fix: the proxy keeps a reference to its source iframe element and re-reads
data-start/data-duration each mirror tick (live trims/moves apply), pauses the
proxy when the playhead leaves [start, start+duration), and resumes it when the
playhead re-enters during parent-owned playback.

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

Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>

* fix(core,studio): bound trimmed audio playback to the clip window

Trimmed audio played to the source file's natural end instead of
stopping at the clip edge, on every audio path:

- WebAudio (the audible path in Studio): schedulePlayback now passes
  the clip's data-duration as the third start() arg, so the decoded
  buffer stops at the trimmed edge instead of running to the file end.
- Runtime element gating: the duration resolver caps each clip by its
  own data-duration (min of source length, host window, authored
  duration), so a trimmed <audio>/<video> element pauses at its edge.

Studio trim UX:

- Resize live-patches the media-start/playback-start offset, so a
  start-edge drag trims into the source instead of only repositioning
  the clip.
- AudioWaveform windows the rendered peaks to the trimmed slice so the
  waveform tracks the clip edges.

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

Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>

* fix(player,core): gate proxy playback to the live clip window

Review follow-ups on the parent-audio-proxy / WebAudio bound:
- seekAll now re-reads live source bounds (_refreshEntryBounds) before
  gating, so a paused scrub right after a trim/move uses the current clip
  window instead of the adopt-time one.
- playAll and clip adoption only start a proxy when the playhead is inside
  the clip's window (_playEntryIfActive), so bulk starts / promotion no
  longer blip audio for clips outside their window until the next tick.
- The WebAudio buffer is now bounded by the host-composition window too
  (matching resolveDurationSeconds), so a sub-composition-nested clip stops
  at the same edge on the WebAudio and HTMLMedia paths.

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

Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>

* fix(core,player): reschedule bounded WebAudio on rate change; guard NaN bounds

A bounded WebAudio source's wall-clock length is baked into start()'s duration
arg (in buffer-sample seconds) at its scheduling rate. Mutating playbackRate in
place on a later rate change does not rescale that bound, so a trimmed clip ends
early (fast) or late (slow). setRate now reports whether the rate changed and
exposes hasBoundedActiveSources(); the runtime stopAll()+reschedules active
clips at the new rate when any bounded source is live. The per-clip schedule
loop is extracted to a shared closure so play() and the rate path agree.

Also guard _refreshEntryBounds against a non-numeric duration attribute parsing
to NaN, which would make every window check false and let the proxy play past
its clip end.

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

Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>

---------

Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>
2026-06-14 17:14:49 -07:00
Carlos Alcaraz Gregor 5c8b637369 fix(studio): route rotation field edits through the animation like X/Y/W/H (#1427) 2026-06-14 13:12:43 -04:00
Miguel Ángel a241f2591e fix(studio): break all 7 circular dependency cycles and fix rules-of-hooks violation (#1422) 2026-06-13 19:23:05 -04:00
Miguel ÁngelandMiguel Ángel 7bff49ecf0 refactor(studio): simplify hooks, split contexts, remove dead code (#1416)
* fix(studio): guard Zustand no-op setters and fix useConsoleErrorCapture memory leak

- Guard setIsPlaying to skip set() when value unchanged (eliminates 60
  notifications/sec during reverse playback)
- Guard caption store selectGroup to bail before set() when group missing
  (prevents empty Zustand notifications)
- Guard clearSelection to skip when already empty
- Fix useConsoleErrorCapture: restore original console.error, remove error
  event listener, and delete __hfErrorCapture flag on cleanup

* fix(studio): delete dead files and unused exports

Remove 7 dead files (audioBeatDetection, keyframeSnapping,
timelineInspector, DopesheetStrip, StaggerControls,
TimelineLayerPanel, TimelineEditorNotice) and their test companions.

Delete unused computeFitToChildrenSize export from propertyPanelHelpers.

Fix re-export indirection: useDomEditCommits and studioMotionOps.test
now import patch builders directly from manualEditsDomPatches instead
of the re-export passthrough in manualEditsDom.

* fix(studio): eliminate effect-chain state mirroring for lint findings, hover, and GSAP fetch

Move lint findingsByElement sync from App.tsx into useLintModal where
the value is produced, removing the mirroring useEffect. Consolidate
4 hover-clearing effects in useDomSelection into 2 (one unconditional
on context change, one conditional combining caption mode, selection
match, and disconnected element checks). Fold the GSAP retry effect
into the fetch effect in useGsapTweenCache, scheduling a single retry
via setTimeout when the initial fetch returns 0 animations.

Eliminates 3 unnecessary render cycles from effect chains.

* fix(studio): memoize renderQueue, toolbar, and canvas rect to prevent re-render cascade

- Wrap renderQueue object in useMemo so StudioContext consumers don't
  re-render on every App render
- Memoize timelineToolbar JSX so NLELayout memo isn't defeated
- Move canvasRect getBoundingClientRect() from render-time IIFE to a
  useLayoutEffect-backed ref, eliminating layout thrashing
- Track and clear setTimeout handles in refreshPreviewDocumentVersion
  to prevent stale timer accumulation on rapid calls and unmount

* refactor(studio): consolidate GSAP shared primitives — defaults, iframe access, keyframe parsing

Extract duplicated PROPERTY_DEFAULTS, IframeGsap interface, iframe
accessors (getIframeGsap, queryIframeElement), percentage keyframe
parsing, and toAbsoluteTime into a single gsapShared.ts module.

Removes ~120 lines of copy-pasted logic across 8 hook files, reducing
drift risk between the duplicate implementations.

* fix(studio): remove dead store fields, dead file, duplicate helper, and unsafe assertions

* refactor(studio): deduplicate selector helpers, rounding utils, percentage computation, and iframe access

* fix(studio): split StudioContext into Shell + Playback to prevent cascade re-renders

* refactor(studio): decompose useGsapScriptCommits into focused mutation hooks

* refactor(studio): decompose useFileManager into focused file operation hooks

Extract useFileTree (tree loading, refresh, derived assets/compositions)
and useEditorSave (debounced save with history tracking) from the 508-LOC
useFileManager. The parent hook composes both and retains file I/O,
click-to-source, upload/import, and CRUD — preserving the same public
interface so no consumers change.

* refactor(studio): decompose useDomEditCommits into focused commit hooks

Extract geometry (path offset, box size, rotation) and element lifecycle
(delete, z-index reorder) into useDomGeometryCommits and
useElementLifecycleOps. Parent keeps persistDomEditOperations as core
and composes all sub-hooks — public interface unchanged.

* refactor(studio): simplify useAppHotkeys with declarative command table

* refactor(studio): simplify useAppHotkeys with declarative command table

Replace 15 individual useRef callback refs with a single cbRef object.
Extract keydown dispatch into pure dispatchModifierKey/dispatchPlainKey
functions. Merge duplicate undo/redo logic into shared applyHistory.
Extract cross-origin listener boilerplate into safeAddListener/safeRemoveListener.

Hook body: 204 LOC (down from 445). Public API unchanged.

* fix(studio): remove unused getDomEditTargetKey import

* refactor(studio): decompose useDomEditSession into focused editing hooks

Extract GSAP-aware geometry intercepts (move/resize/rotation) and
animated property commit into useGsapAwareEditing, and selection
wiring, GSAP cache management, preview sync, and selection handlers
into useDomEditWiring. The parent remains a pure composition shell.

* style(studio): fix formatting in 5 files

* fix(studio): trim App.tsx to 598 lines (under 600 limit)

---------

Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>
2026-06-13 18:25:11 -04:00
Carlos Alcaraz Gregor 3c5607063f fix(studio): disable the rotation field when the element can't be rotated (#1412) 2026-06-13 11:02:36 -04:00
Miguel Ángel 5b6c62e151 fix(studio): surface gesture recording controls (#1390) 2026-06-12 23:16:41 -04:00
Miguel Ángel 3bcab3dc29 fix(studio): reject unsafe keyframe values (#1389) 2026-06-12 22:48:19 -04:00
Miguel Ángel 5f12e692d5 fix(studio): save retries, mutation queue circuit breaker, save_failure diagnostics (#1366)
* fix(studio): save retries, mutation queue circuit breaker, save_failure diagnostics

Save failures could silently drop user work: code-editor saves fired a single
PUT with no retry, DOM-edit failures drained the whole queue against a failing
server, and several failure paths only logged to the console.

- Retry code-editor saves with exponential backoff instead of dropping the
  edit on the first failed PUT.
- Circuit breaker on the DOM-edit save queue: a failing server pauses the
  queue with a user-visible error state instead of burning every queued
  mutation against it.
- save_failure events now carry error_message, status_code, and source on
  every emission path; style/attribute DOM-edit failures that previously only
  logged to the console now emit telemetry too.
- Route unawaited commitMutation call sites (GSAP drag, property scrubbing,
  undo/redo, text fields) through a safe wrapper that reports failures via
  telemetry instead of unhandledrejection.

Follow-ups (deferred): version/ETag conflict guard on file PUTs, offline
save queue.

* fix(studio): narrow save retry changes for fallow
2026-06-12 22:02:17 -04:00
Miguel Ángel aec3c3b58c fix(studio): journal source writebacks (#1388) 2026-06-12 20:40:12 -04:00
Miguel Ángel 1ea0ed55e3 fix(studio): stabilize manual drag targets (#1393)
## Problem

Studio manual drag had two bad target paths in `hf-keyframes-test`:

- Drag could start from a cached hover selection instead of the selected overlay, so moving `#cta` could write an offset to the wrong element while the overlay appeared correct.
- `#stage` is the visual 1920x1080 canvas, but the composition metadata lives on `<html data-composition-id ...>`. Studio treated `#stage` as a normal movable layer, so dragging it moved the coordinate basis the overlay depends on and left Undo with no reliable saved edit to reverse.

## Fix

- Manual drag now starts only from the selected overlay bounds; canvas pointer-down no longer starts movement from `hoverSelectionRef`.
- Added a structural root-layer heuristic: a direct body child matching the composition root's declared `data-width` / `data-height` is selectable and style-editable, but cannot receive manual offset/size/rotation edits.
- Manual geometry commits now return/rethrow the save promise so failed writes roll back optimistic DOM changes instead of silently sticking.
- GSAP-targeted CSS fallback edits are blocked before applying manual geometry when the GSAP drag intercept is unavailable.

## Tests

- New regression coverage for stale-hover drag start and full-canvas root-stage capability resolution.
- Focused DOM-edit suite: 119/119 pass.
- Full Studio suite: 800 pass, 18 todo.
- Studio typecheck clean; oxlint/oxfmt clean; lefthook pre-commit clean.
- Studio build passes with the existing Vite chunk-size warning.
- Browser verified with `agent-browser`: attempted `#stage` drag writes no offset and keeps Undo disabled; `#cta` drag writes exactly one offset to `#cta`; Undo returns the fixture to zero offsets.

Local browser artifacts: `artifacts/studio-cta-drag/stage-cta-drag-fix.webm`, `artifacts/studio-cta-drag/stage-cta-drag-fix-final.png`.
2026-06-12 19:16:36 -04:00
Miguel ÁngelandMiguel Ángel d0a7f7d839 fix(studio): remove motion tab from right panel (#1391)
The motion panel was behind a feature flag defaulting to false and never
shipped. Remove the tab button, MotionPanel component, feature flag,
and all associated wiring (EaseCurveEditor, SpringEaseEditor,
MotionPanelFields, MotionPathOverlay). The underlying motion data
infrastructure (studioMotion, studioMotionOps) remains intact for the
GSAP panel.

Co-authored-by: Miguel Ángel <miguelangelsisi098@gmail.com>
2026-06-12 16:37:05 -04:00
Vance IngallsandClaude Fable 5 7a99ccec6d fix(core): honor root data-duration when GSAP timeline ends short (#1378)
* fix(core): honor root data-duration when GSAP timeline ends short

The authored-duration floor only counted child composition clips, never
the root element's own data-duration. A composition whose GSAP timeline
ended even 0.1s short of its declared data-duration reported the shorter
timeline length from player.getDuration() — and the studio's adapter
selection (docDuration <= adapterDur) then silently rejected the
audio-capable runtime player, downgrading preview playback to the
seek-scrubbing adapter, which never starts media elements or WebAudio.
Result: total audio silence with zero errors anywhere.

- include the root's declared data-duration in
  resolveAuthoredCompositionDurationFloorSeconds, making data-duration
  the source of truth for playable length (per the documented contract)
- console.warn in the studio when playback falls back to the
  seek-driven adapter, since the downgrade loses audio invisibly

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(studio): release static-seek adapter on native win, warn once on downgrade

Review findings on the previous commit, all in the static-seek fallback
path of useTimelinePlayer.getAdapter:

- A cached static-seek adapter was never paused when adapter selection
  later resolved a native adapter (the early returns bypass the fallback
  branch entirely), leaving its private rAF loop seeking the player while
  the native transport also drives it. The core data-duration fix makes
  this switch path much more common. releaseStaticSeekCache() now runs
  at every native-adapter return and at unmount.
- The downgrade warning fired on every cache miss — and the cache key can
  never hold for __timelines compositions because wrapTimeline() returns
  a fresh object per call, so it fired every rAF tick. It now warns once
  per downgrade streak (re-armed when a native adapter takes over).
- The warning interpolated adapterDur (the native __player duration,
  0 when absent) instead of the selected adapter's duration, and used a
  one-off "[hyperframes-studio]" prefix instead of the file's
  "[useTimelinePlayer]" convention.

The fallback cache logic moved to playbackAdapter.ts (with unit tests for
warn-once, cache identity, and pause-on-replace/release), which also
keeps useTimelinePlayer.ts inside the studio 600-line limit. Also
corrected a stale "no DOM reads" comment on the runtime transport tick —
the duration floor has always queried the DOM per call, and now also
reads the root's declared data-duration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 11:01:39 -07:00
Miguel Ángel 9e5b3d644f fix(studio): remove unused onSelectElementById after indicator removal (#1377) 2026-06-12 11:34:16 -04:00
Miguel Ángel 3ba01129e7 fix(studio): remove off-screen element indicators for UX redesign (#1376) 2026-06-12 11:23:18 -04:00
Miguel Ángel c3554dcffe fix(studio): disable keyframes feature flag by default, release v0.6.93 2026-06-12 00:59:17 -04:00
Miguel Ángel 6babf5be7d fix(studio): update drag intercept test for true default 2026-06-12 00:32:01 -04:00
Miguel Ángel a433d83fec fix(studio): trim PropertyPanel to 600-line limit 2026-06-12 00:29:36 -04:00
Miguel Ángel d49ee416a3 fix(studio): gesture recording replaces existing position keyframes (#1360)
Gesture recording uses replace-with-keyframes mutation to replace existing
position-group tween. Fix N1 sign inversion and N9 wheel startPointer
with pointerElementOffset subtraction.
2026-06-12 00:20:08 -04:00
Miguel Ángel 191a33dc2c fix(studio): gesture recording replaces existing position keyframes (#1359)
* fix(core): per-property-group keyframe foundations

Add PropertyGroupName type system (position/scale/size/rotation/visual/other),
PROPERTY_GROUPS constant, classifyPropertyGroup/classifyTweenPropertyGroup
functions. Parser generates group-aware animation IDs, resolves position strings
(+=, -=, <, >), uses numeric matching with 2% tolerance, and preserves IDs
across all mutations.

* fix(core): add split-into-property-groups and replace-with-keyframes mutations

Server-side mutations for atomic property-group splitting and keyframe
replacement. Client commitMutation returns early on changed:false instead
of throwing.

* fix(studio): per-property-group intercept routing + drag/resize fixes

Rewire GSAP runtime bridge for property-group routing: drag sends only {x,y}
to position group, resize routes to scale group via data-hf-studio-original-width,
rotation routes to rotation group. Add resolveGroupTween helper, from-extend
with split-first-then-position-only pattern, autoKeyframeEnabled guards,
GSAP base + delta fix in drag draft, cancel-restores-GSAP-x/y from data attrs.

* fix(studio): keyframe cache propertyGroup tagging + timeline UI fixes

Tag cached keyframes with propertyGroup for group-aware operations.
Add tweenPercentage for accurate keyframe matching, activeKeyframePct
for diamond-click targeting, context menu offset, selected diamond z-index,
clearProps after kill in soft reload.

* fix(studio): property panel group-aware keyframe routing

Add animIdForProp helper routing keyframe diamonds to correct property-group
animation. Wire StudioPreviewArea delete/move/toggle handlers to use
propertyGroup for routing. Fix per-property epsilon in rdpSimplify.

* fix(studio): gesture recording replaces existing position keyframes

Gesture recording uses replace-with-keyframes mutation to replace existing
position-group tween. Fix N1 sign inversion and N9 wheel startPointer
with pointerElementOffset subtraction.
2026-06-12 00:19:41 -04:00
Miguel Ángel c435a4ee46 fix(studio): property panel group-aware keyframe routing (#1358)
* fix(core): per-property-group keyframe foundations

Add PropertyGroupName type system (position/scale/size/rotation/visual/other),
PROPERTY_GROUPS constant, classifyPropertyGroup/classifyTweenPropertyGroup
functions. Parser generates group-aware animation IDs, resolves position strings
(+=, -=, <, >), uses numeric matching with 2% tolerance, and preserves IDs
across all mutations.

* fix(core): add split-into-property-groups and replace-with-keyframes mutations

Server-side mutations for atomic property-group splitting and keyframe
replacement. Client commitMutation returns early on changed:false instead
of throwing.

* fix(studio): per-property-group intercept routing + drag/resize fixes

Rewire GSAP runtime bridge for property-group routing: drag sends only {x,y}
to position group, resize routes to scale group via data-hf-studio-original-width,
rotation routes to rotation group. Add resolveGroupTween helper, from-extend
with split-first-then-position-only pattern, autoKeyframeEnabled guards,
GSAP base + delta fix in drag draft, cancel-restores-GSAP-x/y from data attrs.

* fix(studio): keyframe cache propertyGroup tagging + timeline UI fixes

Tag cached keyframes with propertyGroup for group-aware operations.
Add tweenPercentage for accurate keyframe matching, activeKeyframePct
for diamond-click targeting, context menu offset, selected diamond z-index,
clearProps after kill in soft reload.

* fix(studio): property panel group-aware keyframe routing

Add animIdForProp helper routing keyframe diamonds to correct property-group
animation. Wire StudioPreviewArea delete/move/toggle handlers to use
propertyGroup for routing. Fix per-property epsilon in rdpSimplify.
2026-06-12 00:19:31 -04:00
Miguel Ángel caf23eff8a fix(studio): keyframe cache propertyGroup tagging + timeline UI (#1357)
* fix(core): per-property-group keyframe foundations

Add PropertyGroupName type system (position/scale/size/rotation/visual/other),
PROPERTY_GROUPS constant, classifyPropertyGroup/classifyTweenPropertyGroup
functions. Parser generates group-aware animation IDs, resolves position strings
(+=, -=, <, >), uses numeric matching with 2% tolerance, and preserves IDs
across all mutations.

* fix(core): add split-into-property-groups and replace-with-keyframes mutations

Server-side mutations for atomic property-group splitting and keyframe
replacement. Client commitMutation returns early on changed:false instead
of throwing.

* fix(studio): per-property-group intercept routing + drag/resize fixes

Rewire GSAP runtime bridge for property-group routing: drag sends only {x,y}
to position group, resize routes to scale group via data-hf-studio-original-width,
rotation routes to rotation group. Add resolveGroupTween helper, from-extend
with split-first-then-position-only pattern, autoKeyframeEnabled guards,
GSAP base + delta fix in drag draft, cancel-restores-GSAP-x/y from data attrs.

* fix(studio): keyframe cache propertyGroup tagging + timeline UI fixes

Tag cached keyframes with propertyGroup for group-aware operations.
Add tweenPercentage for accurate keyframe matching, activeKeyframePct
for diamond-click targeting, context menu offset, selected diamond z-index,
clearProps after kill in soft reload.
2026-06-12 00:19:23 -04:00
Miguel Ángel 95029e083e fix(studio): per-property-group intercept routing + drag/resize fixes (#1356)
* fix(core): per-property-group keyframe foundations

Add PropertyGroupName type system (position/scale/size/rotation/visual/other),
PROPERTY_GROUPS constant, classifyPropertyGroup/classifyTweenPropertyGroup
functions. Parser generates group-aware animation IDs, resolves position strings
(+=, -=, <, >), uses numeric matching with 2% tolerance, and preserves IDs
across all mutations.

* fix(core): add split-into-property-groups and replace-with-keyframes mutations

Server-side mutations for atomic property-group splitting and keyframe
replacement. Client commitMutation returns early on changed:false instead
of throwing.

* fix(studio): per-property-group intercept routing + drag/resize fixes

Rewire GSAP runtime bridge for property-group routing: drag sends only {x,y}
to position group, resize routes to scale group via data-hf-studio-original-width,
rotation routes to rotation group. Add resolveGroupTween helper, from-extend
with split-first-then-position-only pattern, autoKeyframeEnabled guards,
GSAP base + delta fix in drag draft, cancel-restores-GSAP-x/y from data attrs.
2026-06-12 00:19:13 -04:00
Miguel Ángel b6bf1b1190 fix(core): split-into-property-groups and replace-with-keyframes mutations (#1355)
* fix(core): per-property-group keyframe foundations

Add PropertyGroupName type system (position/scale/size/rotation/visual/other),
PROPERTY_GROUPS constant, classifyPropertyGroup/classifyTweenPropertyGroup
functions. Parser generates group-aware animation IDs, resolves position strings
(+=, -=, <, >), uses numeric matching with 2% tolerance, and preserves IDs
across all mutations.

* fix(core): add split-into-property-groups and replace-with-keyframes mutations

Server-side mutations for atomic property-group splitting and keyframe
replacement. Client commitMutation returns early on changed:false instead
of throwing.
2026-06-12 00:12:26 -04:00
Vance IngallsandClaude Fable 5 fc3ab76ce8 fix(studio,core): persist manual position edits for GSAP-owned elements (#1346)
* feat(sdk): scaffold @hyperframes/sdk — engine layer (model, RFC 6902 patches, mutate, apply-patches)

* fix(sdk): make engine-layer PR self-contained — trim index.ts, guard indexed access

- index.ts no longer exports document/session/history/persist-queue (those
  modules land in the next stacked PR); branch now typechecks standalone
- setOwnText: optional-chain children[i] access (TS2532 under
  noUncheckedIndexedAccess)
- fallow suppressions for buildPatchEvent + adapters/types.ts — consumers
  arrive in #1325

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(sdk): fail loudly on Phase 3b ops; add sdk to root build pipeline

- applyOp throws UnsupportedOpError (code E_UNSUPPORTED_OP) for the 9
  parser-backed ops instead of silently no-opping — callers must never
  believe an animation edit succeeded when nothing was mutated
- validateOp returns false for Phase 3b ops so can() feature-detects
- root package.json build filter now includes @hyperframes/sdk (package is
  dist-only; top-level build previously produced no SDK artifacts).
  publish.yml intentionally NOT updated — sdk stays unpublished until
  Phase 3 completes.

Adversarial-review findings F3 + F4.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(sdk): cross-realm origin sentinel, dual width/height channel, contract docs

Round-2 review (Rames/Miguel) on the engine layer:

- ORIGIN_APPLY_PATCHES: unique symbol → namespaced string
  ('@hyperframes/sdk:applyPatches'). Symbols are realm-local — they don't
  survive postMessage/structured-clone, which T3 embedded hosts may forward
  patch events across. Namespaced string keeps collision risk negligible.
- setCompositionMetadata width/height: runtime treats data-width/data-height
  as a forced override of inline style (init.ts applyCompositionSizing).
  Style is always written; the data-* attr is updated when already present
  so the edit isn't clobbered on load. Absent attrs stay absent — inverses
  stay exact. Mirrored in the patch applier; 3 new tests.
- JsonPatchOp documented as the emit-only RFC 6902 subset
  (add/remove/replace); applier header notes move/copy/test are ignored.
- SdkDocument.html documented as a build-time snapshot (serialize() is the
  live state).
- patches.ts path-grammar comment fixed: timing/{start|end|trackIndex}.

NOT changed (with reasons, see PR reply): moveElement left/top matches
Studio's own inline-style commit convention (sourcePatcher); package version
follows the repo-wide single-version policy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(sdk): moveElement writes data-x/data-y, not left/top CSS

HF elements use data-x/data-y for positioning (read by htmlParser.ts,
emitted by hyperframes generator). CSS left/top is not the runtime convention.

Adds inverse round-trip test for prior position restore.

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

* chore: update bun.lock after sdk package registration

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

* feat(sdk): session API, optional history + persist-queue, adapters — Phase 3a complete

* fix(sdk): address review — live-DOM query cache, single parse, style parse dedup

- getElements/getElement/find now walk the live linkedom DOM via buildRoots
  with a lazily-built cache invalidated on dispatch/applyPatches — no
  serialize→ensureHfIds→parseHTML round trip per query
- openComposition parses once (parseMutable); dropped discarded _doc
  constructor param and the redundant buildDocument call
- document.ts buildElement reuses model.ts getElementStyles — removes
  duplicated parseInlineStyles (also fixes custom-prop camelCase mangling)
- JSDoc note: empty batch() still fires change handlers

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(sdk): restore full public exports now session/document modules exist

index.ts re-exports document/session/history/persist-queue (trimmed in the
engine-layer PR to keep it self-contained); drops the temporary fallow
suppressions whose consumers now exist.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(sdk): coalesce history by patch paths; replay override-set on open

Adversarial-review findings F1 + F2:

- history: coalescing now requires identical patch paths in addition to
  op types + origin + window. Previously two rapid setStyle calls on
  DIFFERENT elements merged into one entry carrying the second forward +
  first inverse — undo then reverted the wrong element and stranded the
  latest edit. Slider drags on one property still coalesce.
- T3 init: openComposition({ overrides }) now replays the stored
  override-set onto the freshly-parsed base before exposing the session
  (new keyToPath inverse mapping + applyOverrideSet). Previously the
  overrides were copied into the map but never applied — reopening an
  embedded composition showed and serialized the base template.
- examples: GSAP calls now feature-detect with can() (Phase 3b ops throw
  UnsupportedOpError as of the engine-layer fix); UnsupportedOpError
  re-exported from the package entry.
- 8 new session tests: coalesce same-path / cross-element / cross-prop,
  override round-trip (style/text/attr/timing/removal/restore-base).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(sdk): transactional batch rollback, sorted coalesce key, root-priority unify

Round-2 review (Rames/Miguel) on the session layer:

- batch() is now transactional: on throw, accumulated inverse patches are
  replayed in reverse and the override-set snapshot restored — the model is
  exactly as it was at batch entry. Previously a throwing batch left the DOM
  partially mutated with no patch trail, no history entry, no recovery path.
  2 new tests (model unchanged + undo is no-op after throwing batch).
- history coalesce key sorts opTypes — same op-type set coalesces regardless
  of dispatch order within a batch.
- applyPatches comment documents that emitted PatchEvents carry an empty
  inversePatches array (hosts keep their own inverse log).
- document.ts extractDimensions/extractDuration now use the engine's
  findRoot — dimension extraction and mutations agree on the root element
  ([data-hf-root] > #stage > first child). Dimensions prefer the runtime's
  data-width/data-height forced-override attrs, falling back to inline style.
- ownText documented: snapshot .text is trimmed display text; setText writes
  verbatim.

Deferred to follow-up (acknowledged, not ship-blocking): persist-queue flush
error surfacing, debounce window, path default, history ring-buffer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(lint): add gsap_studio_edit_blocked rule for manual timeline + GSAP element targeting

* fix(studio,core): persist manual position edits for GSAP-owned elements

- sourceMutation: linkedom CSSStyleDeclaration silently drops CSS custom
  properties and transform longhands via setProperty; patch the style
  attribute string directly so --hf-studio-offset-* and translate survive
  the server round-trip (positions never reached disk before this)
- gsapAnimatesTransform(): GSAP owns the full transform stack when it tweens
  ANY transform prop (scale, rotation, ...), not just x/y — it folds CSS
  translate into its cache once at init, zeroes the longhand once, and never
  re-reads it
- applyStudioPathOffset: for GSAP-owned elements keep translate:none live and
  sync the offset into GSAP's cache via gsap.set; writing the longhand
  double-applied the offset (disappearing elements, scrub snap-back)
- buildPathOffsetPatches: emit the var() translate expression explicitly so
  the persisted file re-folds on reload (live inline is none)
- StudioPathOffsetSnapshot: capture/restore GSAP x/y — the drag-response
  probe mutates GSAP's cache, which inline-style restore cannot undo (click
  made elements jump by the probe distance)
- reapplyPathOffsets: skip GSAP-owned elements (was x/y-only) to stop
  seek-time double-apply
- STUDIO_GSAP_DRAG_INTERCEPT flag (default off): keyframe drag intercept is
  opt-in until its recording path is hardened; commits take the CSS persist
  path

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(studio): remove duplicate flag declaration, trim useDomEditCommits to 600 lines

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 12:30:14 -07:00
Carlos Alcaraz Gregor c52165d1b6 fix(studio): accept razor splits at the canvas clamp boundary (#1340) 2026-06-11 10:26:49 -04:00
Miguel Ángel a5bc52bad1 fix(studio): add GSAP drag intercept feature flag, disabled by default (#1341)
Add VITE_STUDIO_ENABLE_GSAP_DRAG_INTERCEPT env var (default: false) to
gate the GSAP drag/resize/rotation intercept in useDomEditSession. When
off, dragging GSAP elements falls through to the standard CSS path
instead of committing via script mutation.

Manual dragging (STUDIO_PREVIEW_MANUAL_EDITING_ENABLED) remains on.
2026-06-11 02:08:32 -04:00
Miguel Ángel ef18613975 feat(studio): razor/blade tool UI for timeline clip splitting (#1331)
Wire the razor tool into Studio's timeline UI:

- B enters razor mode (crosshair cursor + red vertical guide line)
- Click any clip to split at the click position
- Shift+click splits all clips across every track at that time
- V or Escape exits razor mode
- Toolbar shows selection arrow / scissors toggle

Add useRazorSplit hook for split orchestration (HTML + GSAP mutation).
Add activeTool state to playerStore. Add preview reload after timeline
move/resize operations so the composition re-renders with updated timing.
2026-06-10 23:56:09 -04:00
Miguel Ángel ab08260201 refactor(studio): extract shared timeline components and deduplicate code (#1329)
Extract shared utilities to reduce duplication across timeline components:

- PlayheadIndicator: shared playhead rendering (was duplicated in
  TimelineCanvas and TimelineEditorNotice)
- useContextMenuDismiss: outside-click/Escape dismiss pattern (was
  duplicated in ClipContextMenu and KeyframeDiamondContextMenu)
- TimelineCallbacks: shared callback interfaces for drop and edit
  operations (was duplicated in NLELayout and Timeline props)
- useTimelineZoom: consolidated zoom store selectors
- timelineElementSplit: shared canSplitElement, buildPatchTarget, and
  readFileContent utilities
- gsapParser.test-helpers: shared test utilities for parser specs
2026-06-10 23:45:03 -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
8fcbb63a37 docs(readme): swap hero media to hyperframes-logo-motion (#1315)
* docs(readme): swap hero media to hyperframes-logo-motion

Replaces the prior hfgif-1280.webp hero with a new logo-motion clip
Bin trimmed for the launch. Converted the source MP4 to animated webp
(the existing hero's format) so it auto-plays in the GitHub README the
same way the old one did - MP4 sources don't render inline or autoplay
in <img> tags.

- New asset: static.heygen.ai/hyperframes-oss/docs/images/
  hyperframes-logo-motion-1280.webp (1280x720, 85 frames, 199KB)
- ffmpeg conversion: scale=1280, libwebp_anim, q=80, loop=0

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(studio): format 5 hooks files (oxfmt)

* style: remove unused imports in studio hooks (pre-existing lint failures)

CI Lint on main was already failing with 5 unused-import errors in
packages/studio/src/hooks/. Removed the unused symbols to unblock the
README hero PR's CI:

- gsapRuntimeBridge.ts: resolveTweenStart, resolveTweenDuration
- useGsapScriptCommits.ts: usePlayerStore
- useTimelineEditing.ts: PatchTarget (type-only)
- gsapDragCommit.ts: readGsapProperty

Bundled into the README PR per James's request to fix CI in-place.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* test(studio): add childRects: [] to DomEditOverlay test mock

useDomEditOverlayRects' return type added a childRects: OverlayRect[]
field; the DomEditOverlay test's mock didn't get updated and was
returning an object without it, so DomEditOverlay.tsx's
'childRects.length > 0' check threw TypeError on undefined.

One-line mock-vs-hook contract realignment.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* test(studio): drive player-store currentTime in selection-hydration test (#1311 follow-up)

The 'hydrates seek first, preserves the initial url state, then restores
selection' test was failing because PR #1311 (keyframes feat) changed
useStudioUrlState to read currentTime from the player store via
usePlayerStore((s) => s.currentTime), removing it from the hook's prop
shape. The test was still trying to drive currentTime via the harness
prop, which is now a no-op — so the selection-hydration useEffect's
time-stability guard

  Math.abs(currentTime - stableTimeRef.current!) > 0.05

never passed (store currentTime stayed at 0 while stableTimeRef caught
the 4.2 seek target). buildDomSelectionFromTarget was never reached,
applyDomSelection was never called, and the assertion got 0 calls.

Fix: setState the store's currentTime to 4.2 ahead of the rerender so
the hook's selector picks it up and the time-stability guard passes.
Harness prop kept as-is — it's a no-op but doesn't hurt.

Pre-existing failure on main HEAD 81416ab3; surfaced as CI gate on the
unrelated docs/readme-hero-motion-update PR.

* test(studio): stub getBoundingClientRect + flush RAF in DomEditOverlay test

The 'renders selected bounds right after clicking a movable selection'
test asserts the selection box appears after pointerdown, but happy-dom
returns 0 for newly-created elements' getBoundingClientRect. The
overlay's compRect updates via a RAF loop that early-returns when iframe
width is 0; the keyframes PR a468550f added a compRect.width > 0 guard
to the selection-box render path, so compRect=0 silently gates the box
off and the assertion fails.

Stub Element.prototype.getBoundingClientRect to return 800x450 for the
test, and flush two RAFs after render so the compRect state update lands
before the pointerdown assertion. Restore the prototype at test end.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Miguel Sierra <miguel.sierra@heygen.com>
2026-06-09 23:39:22 -07:00
Miguel Ángel 81416ab3c9 refactor(studio): split oversized files to pass 600-line check (#1313) 2026-06-09 19:53:14 -04:00
Miguel Ángel 6776bb9994 chore(studio): render queue improvements + producer build (#1306)
Render queue progress indicators, download improvements, and producer
build optimizations. Independent of keyframe feature.
2026-06-09 18:31:06 -04: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
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 82c754f27a feat(studio): wire hfId through DOM-edit patch targets, activate hfId lookup path (R7, T5a) (#1297) 2026-06-09 14:19:46 -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
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 42c247d28f feat(studio): sourcePatcher data-hf-id targeting (R1, T3) (#1271)
* 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>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 00:27:28 -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 5b29bea0b1 test(studio): add T5c review-fix gaps in manualEditsDomPatches characterization (#1259)
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
2026-06-07 19:20:42 -07:00
Vance Ingalls 63ff0c16c3 test(studio): add T5b rotation+motion build-patches characterization (#1258)
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.
2026-06-07 19:15:25 -07:00
Vance Ingalls 19f46cd19a test(studio): add T5a pathOffset+boxSize build-patches characterization (#1257)
## Summary

T5 (part 1 of 3) — characterization test suite for `manualEditsDomPatches.ts`.

The source module exports 8 functions (4 build/clear pairs) that write and restore draft-marker attributes and inline styles onto iframe elements before source-patch operations. It had zero tests.

This PR covers the **pathOffset** and **boxSize** pairs with 4 patterns each:

- **populated** — fully-configured element produces exact expected `PatchOperation[]` in declaration order
- **empty** — bare element yields only the mandatory marker attribute op
- **clear restores originals** — `buildClear*` reads `STUDIO_ORIGINAL_*` attrs and produces correct restore values
- **build/clear symmetry** — every `{type, property}` key that `build*` can emit is also addressed by `buildClear*`; an orphan here means a property stranded in committed source HTML

Uses `@vitest-environment happy-dom` matching the Studio package convention. Element setup via `document.createElement` + `style.setProperty` / `setAttribute`.

## Stack

- **#1257** (this PR) — pathOffset + boxSize pairs
- **#1258** — rotation + motion pairs
- **#1259** — review-fix gaps (ordered clear assertion, coercion paths, edge cases)
2026-06-07 19:07:59 -07: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
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 Ingalls bdbd8b1db0 test(studio): add T4 op-contract stubs for editor dispatch boundary (#1243) 2026-06-07 09:59:43 -07:00
Vance Ingalls 9aebc8db70 test(studio): add T11 history coalescing and cross-prop separation suite (#1242)
## What

Extends `editHistory.test.ts` with T11 from the SDK migration test plan: history coalescing gaps and origin guard stubs.

## Tests

**New passing test (1):**
- **cross-prop coalescing separation** — two edits within the coalesce window but with *different* `coalesceKey` values produce two separate undo entries, not one coalesced entry. Fills the gap left by the existing same-file coalescing tests (lines 176–243).

**`.todo` stubs (2):**
- `gesture-start/commit collapses intermediate drag steps into one undo entry` — requires gesture lifecycle API not yet built
- `origin:applyPatches edits excluded from undo stack` — requires SDK session object (`session.on("patch", ...)`, `session.dispatch(...)`) which doesn't exist yet; needed to prevent undo loops when SDK patches are applied

## Stack

Stacked on T8 (#1241). Prerequisite for T4 (#1243).
2026-06-06 18:27:26 -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 7a0cb085bb feat(studio): draggable layer reorder with z-index persistence (#1216)
* feat(studio): add drag-to-reorder in layers panel with z-index persistence

Layers panel now sorts siblings by computed z-index (descending) to
reflect visual stacking order. Users can drag layer rows to reorder
them within a sibling group — on drop, sequential z-index values are
assigned and persisted via the existing inline-style patch pipeline
with a single preview reload.

- sortLayersByZIndex: recursive sibling-group sort by computed z-index
- useLayerDrag: pointer-capture drag gesture with 4px threshold,
  insertion indicator line, and depth-constrained sibling reorder
- handleDomZIndexReorderCommit: batch z-index commit with coalesced
  undo entry and single skipRefresh=false on the final patch

* fix(studio): harden layer drag-to-reorder edge cases

- Guard drag initiation against locked compositions by checking
  data-timeline-locked ancestors in isLayerDraggable
- Show not-allowed cursor and reduced opacity on non-draggable layer rows
- Fire toast when attempting to drag a layer with no same-depth siblings
- Preserve z-index spacing on reorder by redistributing existing values
  instead of flattening to sequential integers
- Auto-set position:relative on unpositioned elements when z-index is
  applied so the stacking order actually takes visual effect
- Add tests for isLayerDraggable (anonymous, id, selector, locked, free)

* fix(studio): handle z-index ties in layer reorder + trim file sizes

- Fall back to sequential z-index when any duplicates exist in the
  sibling set, not just when all values are identical — fixes silent
  no-op reorder when tied values preserve DOM-order stacking
- Trim useDomEditSession.ts from 602 to 600 lines (CI file-size gate)

* test(studio): add duplicate z-index tiebreak test for layer sorting

Cover the [2, 1, 2] case where tied z-index values fall back to
reverse DOM order — locks the hasDupes fix against regressions.

* style(studio): fix oxfmt formatting in LayersPanel
2026-06-05 20:08:22 -04:00
Miguel Ángel 2c73fa9c8f fix(studio): fix PropertyPanel format and file size regression (#1229)
The fit-to-children merge re-inlined TimingSection that was already
extracted to propertyPanelTimingSection.ts, pushing the file to 687
lines (over the 600 limit) and introducing format issues.

- Removed duplicate TimingSection, import from extracted module
- Extracted computeFitToChildrenSize to propertyPanelHelpers
- Formatted PropertyPanel.tsx (608 lines, down from 687)
2026-06-05 18:40:35 -04:00
Miguel Ángel 4d96886655 feat(studio): add fit-to-children button in property panel (#1224)
Adds an icon button next to W/H fields that computes the bounding
box union of all visible children and resizes the element to fit.
Uses BCR union scaled to composition pixels, filters visibility:hidden.
2026-06-05 18:11:52 -04:00