Commit Graph
19 Commits
Author SHA1 Message Date
Vance IngallsandClaude Opus 4.8 8f15e9f09b feat(studio): extend SDK shadow to delete/timing/gsap-add + default on (#1473)
* feat(studio): default SDK shadow dispatch on for parity telemetry

Shadow mode keeps the server patch path authoritative (no user-visible
change) and emits sdk_shadow_dispatch parity signal. Default it on so we
collect addressing/serialize-drift telemetry from all traffic before any
cutover. Disable via VITE_STUDIO_SDK_SHADOW_ENABLED=false.

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

* feat(studio): shadow parity for delete/timing/gsap ops + wire delete

Extends shadow visibility past the property-edit path. Adds a can()-first
shadow core (pure addressing/validity pre-check, works even for GSAP which
has no snapshot value) plus runShadowDelete/runShadowTiming/runShadowGsapTween.
Parity coverage: delete = getElement null (full); timing = snapshot
start/duration/trackIndex (full); gsap = can()+dispatch+returned-id only
(animationIds is a stub, tween values are script-level — full fidelity needs
serialize() round-trip diffing, out of scope).

Wires the delete runner end-to-end via an onElementDeleted callback
(useDomEditSession → useDomEditCommits → useElementLifecycleOps), fired after
the server delete succeeds. Server stays authoritative. Timing/GSAP wiring
follows (each needs threading sdkSession into useTimelineEditing /
useGsapScriptCommits).

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

* feat(studio): wire timing + GSAP-add shadow dispatch

Timing: thread sdkSession into useTimelineEditing; fire runShadowTiming after
move/resize persist (server authoritative). Moved the useSdkSession call above
useTimelineEditing so both share the single session (no duplicate).

GSAP: thread sdkSession through useGsapScriptCommits → useGsapAnimationOps;
shadow addGsapAnimation via runShadowGsapTween after the server add. Only the
add path is shadowed — delete/update key on the server's animationId, which
doesn't resolve in the SDK's independent id-space (would emit false
cannot_dispatch). "set" has no SDK method, so it's skipped.

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

* fix(studio): address #1473 review — no-persist shadow session + fallow gate

Blocker (Rames): the shadow runners dispatched on the live persisted SDK
session, so each shadow op fired the persist queue → an HTTP write of the SDK's
serialize() output, clobbering the studio's authoritative write (default-on
shipped this). Fix: open the shadow session WITHOUT persist — it reads from the
server but never writes back. Shadow dispatches mutate the in-memory model only
and are discarded on the next reload-on-change. Cutover (Step 3c+) must re-add
persist together with self-write suppression. No persist consumer exists in
this stack (cutover is not in main), so this is safe and keeps default-on.

Fallow CI gate (Miguel):
- drop unused `export` on RecordEditInput (dead-type)
- suppress pre-existing CRAP with reasons: commitMutation, addGsapAnimation;
  file-level complexity on useTimelineEditing (shadow .then() branches nudge
  several callbacks over threshold — telemetry-only)
- suppress 3 pre-existing clones surfaced by adjacent edits (save-error
  formatter, prop-drilling passthrough, file-change reload handler)

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

* fix(studio): scrub user content from shadow property-path telemetry

Addresses #1473 review concern (Rames): inline-style and text-content edits
put user content into the sdk_shadow_dispatch mismatch expected/actual fields.
Redact before emit — text-content values fully redacted (length only), others
length-capped at 64. The in-memory parity result keeps raw values, so the
parity logic and tests are unaffected.

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-15 18:08:29 -07: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
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 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 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 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
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 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
Miguel Ángel c1699ec98b feat(studio): runtime-first dynamic keyframe system [8/10] (#1190)
* feat(core): spring physics solver + runtime fixes + spring ease editor

* feat(core): spring physics solver + runtime fixes + spring ease editor

Revert totalTime nudge that caused black first frames in from() tweens.
Keep stale CSS offset cleanup. Regenerate baselines for offset cleanup.

* ci: trigger regression run

* test(producer): regenerate heygen-promo-preview-assets and style-9-prod baselines

Baselines regenerated inside Dockerfile.test on the devbox to match
the current runtime init.ts changes. Both pass the full regression
harness with the videoStreamDurationSeconds PSNR fix.

* feat(studio): design panel integration, timeline polish, feature flag

* fix(studio): rotation-aware drag + auto-keyframing for resize and rotation

U1: stripGsapTranslateFromTransform now rotates the offset vector by the
element's CSS rotation angle before subtracting from m41/m42. Fixes
elements drifting from cursor during drag when rotated.

U2+U3: Add tryGsapResizeIntercept and tryGsapRotationIntercept to the
runtime bridge. Resize and rotation handle changes now create keyframes
via the same async pipeline as position drag. CSS path guards prevent
double-persistence for GSAP-animated elements.

* fix(studio): counter-rotate drag offset for css-rotated elements

CSS compose order is translate → rotate → transform. The drag offset
(in pre-rotation translate space) was added directly to GSAP x/y
(in post-rotation transform space). Now counter-rotates the offset
by the element's CSS --hf-studio-rotation angle before adding.

* feat(studio): add 'delete all keyframes' to diamond context menu

* fix(studio): include all animated properties in every keyframe commit

Position, resize, and rotation intercepts now read ALL animated
property values from gsap.getProperty() at commit time and include
them in the keyframe. Prevents other properties from jumping to
interpolated values between surrounding keyframes when only one
property (e.g., width) was explicitly changed.

* feat(core): spring physics solver + runtime fixes + spring ease editor

* feat(core): spring physics solver + runtime fixes + spring ease editor

Revert totalTime nudge that caused black first frames in from() tweens.
Keep stale CSS offset cleanup. Regenerate baselines for offset cleanup.

* ci: trigger regression run

* feat(core): spring physics solver + runtime fixes + spring ease editor

* feat(core): spring physics solver + runtime fixes + spring ease editor

Revert totalTime nudge that caused black first frames in from() tweens.
Keep stale CSS offset cleanup. Regenerate baselines for offset cleanup.

* ci: trigger regression run

* feat(studio): runtime-first dynamic keyframe system with auto-materialization

Read GSAP keyframe data from the live runtime instead of only the AST parser.
Dynamic keyframes (loops, variables, computed selectors) now show diamonds
on timeline clips and animation cards in the design panel.

On first edit, dynamic code is automatically materialized:
- Unresolved keyframes (keyframes: kf) replaced with static object
- Unresolved selectors (tl.to(sel, ...)) entire loop unrolled into
  individual static tl.to() calls per element

Key changes:
- Parser: hasUnresolvedKeyframes/hasUnresolvedSelector flags
- Runtime bridge: scanAllRuntimeKeyframes reads tween.vars from iframe
- Tween cache: interval-based runtime scan for dynamic animations
- materializeKeyframesInScript + unrollDynamicAnimations parser functions
- Keyframe cache dual-writes both sourceFile#id and index.html#id keys
- commitMutation updates cache from mutation response
- easeEach placement fix (inside keyframes object, not tween vars)
2026-06-05 12:08:03 -04:00
Miguel Ángel 7a0883264d feat(studio): keyframe hooks wiring — session, cache, toolbar [5/6] (#1171)
* feat(core): GSAP keyframe parsing, mutations, and API routes

* feat(core): spring physics solver + runtime fixes + spring ease editor

* feat(core): spring physics solver + runtime fixes + spring ease editor

Revert totalTime nudge that caused black first frames in from() tweens.
Keep stale CSS offset cleanup. Regenerate baselines for offset cleanup.

* ci: trigger regression run

* fix(producer): use video stream duration for PSNR checkpoint range

The regression harness used container duration (format.duration) to
compute PSNR checkpoints. Audio padding can extend the container past
the last video frame, causing the final checkpoint to reference a
non-existent frame index and fail with "Unable to parse PSNR output".

Add videoStreamDurationSeconds to VideoMetadata and use it for the
PSNR sample range calculation.

* test(producer): regenerate heygen-promo-preview-assets and style-9-prod baselines

Baselines regenerated inside Dockerfile.test on the devbox to match
the current runtime init.ts changes. Both pass the full regression
harness with the videoStreamDurationSeconds PSNR fix.

* test(producer): allow 2-frame PSNR tolerance for style-9-prod

A single transition frame at 10.742s renders with marginal PSNR
(26.6 dB vs 30 threshold) on CI runners but passes on the devbox
Docker image. This is consistent with other sub-composition tests
that allow 2-10 frame failures for cross-environment variance.

* feat(studio): GSAP runtime bridge + optimistic update pattern

* feat(studio): keyframe diamonds, navigation controls, context menu

* feat(studio): keyframe hooks wiring — session, commits, cache, toolbar toggle
2026-06-05 11:51:52 -04:00
Miguel Ángel ae717331ce fix(studio): soft-reload GSAP property edits, preserve shader cache (#1129)
* fix(studio): soft-reload GSAP property edits without iframe reload

GSAP property value edits (opacity, x, scale, etc.) now update the live
timeline inside the preview iframe without triggering a full iframe
reload. This preserves the WebGL context and shader transition cache,
eliminating the loading overlay that appeared on every property edit.

Implementation:
- New gsapSoftReload.ts: kills the old GSAP timeline, re-executes the
  updated script, calls __hfForceTimelineRebind(), and re-seeks to the
  current time. Falls back to full reload on failure.
- useGsapScriptCommits: passes softReload: true for property value edits
  via the existing (previously unused) softReload flag on commitMutation.
- hyper-shader.ts: exposes __hfSuppressSceneMutations on the window so
  the soft-reload can suppress the MutationObserver during re-execution.
- hyper-shader.ts: getDocumentScriptSignature now excludes pure GSAP
  animation scripts from the cache key hash, so full reloads (undo,
  external changes) don't invalidate transition caches when only
  animation values changed.

* fix(studio): wrap soft-reload script in IIFE to avoid const redeclaration

The new script ran in the same global scope as the old one, causing
Identifier tl has already been declared errors from const/let
re-declarations. Wrapping in an IIFE creates a new lexical scope.
Also remove the old script element before inserting the new one.

* fix(studio): return scriptText from mutation API, drop client-side HTML parsing

The mutation API already has the extracted GSAP script text (newScript)
after rewriting. Return it as scriptText in the response so
applySoftReload receives the script directly instead of parsing HTML
client-side. This avoids DOMParser compatibility issues across test
environments and is more reliable than regex-based extraction.

* fix(studio): align soft-reload script heuristic with server-side parser

The client's findGsapScriptElement only matched gsap.timeline and
__timelines. The server's extractGsapScriptBlock also matches .to( and
.set(. Aligned the client heuristic to prevent silent fallback to full
reload for compositions that use tl.to() without gsap.timeline in the
same script.

* fix(studio): address hf#1129 review — multi-script guard, scope docs

- Return false (fallback to full reload) when multiple GSAP scripts
  exist in the document, since it's ambiguous which one to replace
- Add docstring scoping the optimization to root-document scripts
  (template-wrapped sub-compositions fall back to full reload)
- Add code comment explaining the IIFE scope constraint
- Add test for the multi-script guard

* fix(studio): align cache key filter with soft-reload script heuristic

isGsapAnimationOnlyScript now also matches .to( and .set( patterns,
matching findGsapScriptElement. Scripts using only tl.to() without
gsap.timeline were excluded from soft-reload but still busted the
shader cache on full-reload paths (undo, external changes).
2026-05-29 23:34:57 -04:00
Miguel Ángel 1284213886 fix(studio): gsap panel bug bash — clamping, overlay, click cycling, visibility toggle (#1126)
* fix(studio): gsap panel bug bash — clamping, overlay, click cycling, visibility toggle

- opacity/autoAlpha clamped to [0,1] (display 0–100%) — eliminates -30%/190% edits
- `visibility` renders as a boolean toggle; only available to add in `set` tweens
- ease curve section: use aspect-ratio container so control circles are not oval
- MetricField scroll only fires when the input is focused (was triggering on scroll-over)
- preview overlay clipped to its container (overflow-hidden) — no bleed into panels
- `fromTo` method label updated to "From → To" (was "Animate", same as `to`)
- repeated click at same position cycles through stacked/overlapping elements (#1124, #1125)
  resolveAllVisualDomEditTargets returns the full z-stack; subsequent same-spot clicks
  advance through all selectable layers at that coordinate
- fallow-ignore-next-line complexity on pre-existing complex functions surfaced by
  branching from fix/gsap-fromto-panel rather than main

Closes #1124, #1125

* fix(studio): address Vai+Rames follow-up notes on hf#1122

- extract buildTweenSummary to gsapAnimationHelpers.ts (now testable)
- add tests for all buildTweenSummary branches including fromTo
- extract requireAnimation/requireFromToAnimation helpers in files.ts,
  eliminating the parse→find→guard pattern repeated across three switch
  cases and removing the fallow-ignore-next-line complexity bypass
- add 400 guard: add mutation with fromProperties on non-fromTo method
  now returns 400 instead of silently dropping fromProperties
- add test for the 400 guard

* fix(studio): buildTweenSummary formats percent props as 0-100% not 0-1

* fix(studio): show all .html files as compositions in sidebar

The Comps sidebar only listed index.html and files under a compositions/
subdirectory. Any other .html file in the project root was invisible and
could not be loaded as a composition preview.

Broadened the filter in useFileManager and the activeCompPath guard in
App.tsx to treat every .html file as a selectable composition.

Also excluded App.tsx from the filesize pre-commit check — the file is
already 652 lines (decomposition tracked in PR #724).

* fix(studio): detect compositions by data-composition-id, not path convention

The previous approach filtered compositions by path convention (index.html
or compositions/ subdirectory). Any .html file outside that convention was
invisible in the Comps sidebar.

The server now scans each .html file for data-composition-id and returns
a compositions[] field in the project API response. The client uses this
server-provided list instead of filtering locally. This means any .html
file that is a real HyperFrames composition shows up regardless of where
it lives in the project tree.

* fix(studio): rename Ask agent to Copy prompt to AI agent, show context preview

Updated the property panel button label from "Ask agent" to "Copy prompt
to AI agent". Updated the modal title to match. Added a collapsible
"Context included in prompt" details section to the modal that shows the
element metadata that will be included when copying.

* fix(studio): wire contextPreview to agent modal

Passes composition path, source file, selector, tag, and text content
to the AskAgentModal so the context preview section is visible.

* fix(core): seek timeline to current time after initial bind

When bindRootTimelineIfAvailable captured a GSAP timeline for the first
time, it paused it but never seeked to state.currentTime. This left
fromTo tweens stuck at their immediateRender "from" state (e.g. opacity 0)
even after the user scrubbed past the tween's end. The polling rebind
path already seeked to previousTime — the initial bind was the only path
that skipped it.

* feat(core): add gsap_timeline_not_registered lint rule

Warns when a composition creates gsap.timeline() but never registers it
in window.__timelines. Without registration, the runtime cannot discover
the timeline, and animations will not play during preview or render.

Skips the warning for sub-compositions (template-based) which inherit
the parent's timeline context.

* fix(studio): address hf#1126 review feedback

- Extract buildAgentContextPreview into domEditingAgentPrompt.ts and
  import it in App.tsx, removing the inline computation that pushed
  App.tsx past the 600-line CI gate
- Switch isCompositionFile from sync readFileSync to async readFile with
  Promise.all, and use a regex test instead of string includes
- Move PERCENT_PROPS from AnimationCard.tsx and gsapAnimationHelpers.ts
  into gsapAnimationConstants.ts (single source of truth)
- Add regression test for the totalTime initial-bind seek fix in
  init.test.ts — verifies the captured timeline receives a totalTime
  call on initial bind

* refactor(studio): extract App.tsx below 600 LOC, remove lefthook exemption

Extracted inspector state, studio context construction, and drag overlay
into useStudioContextValue.ts. Deduplicated block handler args via a
shared blockCtx memo. App.tsx drops from 657 to 588 lines.

Removed the App.tsx exemption from lefthook.yml — the file now passes
the 600-line gate without special-casing. Added domEditing.ts barrel to
fallowrc ignoreExports (re-exports not traceable by static analysis).
2026-05-29 22:18:27 -04:00
Miguel Ángel 6a0c9a5e22 fix(studio): surface fromTo from-state in GSAP design panel (#1122)
* fix(studio): surface fromTo from-state in GSAP design panel

Closes #1121.

The core already parsed, serialized, and mutated fromProperties end to end
(gsapParser.ts, applyUpdatesToCall, buildTweenStatementCode). The panel
never wired it in — AnimationCard only read animation.properties, so
fromTo start values were invisible and silently un-editable.

Changes:
- files.ts: add update-from-property / add-from-property /
  remove-from-property mutation types; pass fromProperties through the
  add case; add fromTo to the method union
- useGsapScriptCommits: updateGsapFromProperty, addGsapFromProperty,
  removeGsapFromProperty; addGsapAnimation extended to fromTo with
  { opacity:0 } → { opacity:1 } defaults
- gsapAnimationConstants: fromTo added to ADD_METHODS / ADD_METHOD_LABELS
  ("From → To") so it can be authored from the panel
- AnimationCard: From section with per-row edit/remove and + From property
  picker (orange accent to distinguish from To section); buildTweenSummary
  includes from-state description for fromTo; PropertyRow and
  AddPropertyTrigger extracted to eliminate the structural duplication
  between From and To rows
- GsapAnimationSection / PropertyPanel / useDomEditSession /
  DomEditContext / StudioRightPanel: thread the three new callbacks
  through the full prop/context chain

* test(studio): add API-level tests for fromProperties mutation routes

Covers the three new mutation types introduced in the fromTo panel fix:
- update-from-property: asserts value written and sibling keys preserved
- update-from-property: asserts 400 for non-fromTo animation
- add-from-property: asserts new key merged without clobbering existing keys
- remove-from-property: asserts targeted key removed, others intact
- remove-from-property: asserts 400 for non-fromTo animation
- add with method "fromTo": asserts fromProperties written to source

All exercised at the HTTP route layer via the same Hono app harness
as the existing gsap-mutations tests.
2026-05-29 13:18:05 -04:00
Miguel Ángel fb2e21090f feat(studio): GSAP tween editing in Design panel (#1102)
* feat(studio): GSAP tween editing in Design panel

Add a GSAP animation editor to the studio Design panel: select an element,
view and edit its tweens (properties, easing, timing), add/delete animations,
and drag custom bezier speed curves — all persisted back to the composition
HTML. Gated behind VITE_STUDIO_ENABLE_GSAP_PANEL.

Parsing of existing GSAP source now uses a recast + Babel AST parser instead of
regex, giving scope resolution, stable tween IDs, and round-trip preservation of
extras and unresolved raw values.

recast compiles to CommonJS that calls require("fs"), which breaks browser and
Vite SSR bundles. To contain it, @hyperframes/core is split into an isomorphic
layer and a Node-only AST layer:

- gsapSerialize.ts holds the recast-free helpers (serialization, keyframe
  conversion, validation, shared types). htmlParser.ts is now fully isomorphic.
- parseGsapScript and the script-mutation helpers live in gsapParser.ts,
  reachable only via the @hyperframes/core/gsap-parser subpath, loaded
  server-side by the studio-api mutation routes and the linter via dynamic
  import (recast stays external under SSR).
- The barrel and the gsap-constants subpath are recast-free, so studio browser
  bundles never trace recast.

Adds AST parser unit + stress coverage and e2e helpers for the panel.

* fix(lint): await async lintHyperframeHtml in all callers

lintHyperframeHtml became async (gsap rules use dynamic import)
but lintProject and check-hyperframe-static weren't awaiting it,
causing typecheck failures and runtime crashes in CI.

Also wire LintRule type in gsap rules to fix fallow unused-type
finding, and suppress render.ts exported-for-tests symbols.
2026-05-28 19:16:34 -04:00