mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 12:54:29 +00:00
fd02e6efc9b9155252c2a3486dbd2d3826d6719e
97
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7a4853dfe6 |
refactor: extract @hyperframes/studio-server from core (#1757)
* refactor: extract @hyperframes/studio-server package from core Moves all studio-api routes, helpers, and Hono server wiring from packages/core/src/studio-api/ into a new standalone packages/studio-server package (@hyperframes/studio-server). Core keeps thin re-export stubs at @hyperframes/core/studio-api and the subpath helpers (screenshot-clip, draft-markers, etc.) for backward compatibility. Consumer imports (cli studioServer, vite adapter/config, producer htmlCompiler, studio manualEditsTypes) are updated to import from @hyperframes/studio-server directly. Also exports rewriteInlineStyleAssetUrls from @hyperframes/core root (was in compiler/rewriteSubCompPaths.ts but not re-exported), required by @hyperframes/studio-server/helpers/subComposition. Removes postcss-selector-parser from @hyperframes/core dependencies (moved to @hyperframes/studio-server which owns the routes that used it). Depends on @hyperframes/parsers (PR #1755). * fix(ci): add parsers+studio-server to Dockerfile and build before preview tests * fix(ci): build @hyperframes/studio-server before Test and studio load smoke Studio's vite.config.ts imports @hyperframes/studio-server, which resolves via its "node" export condition to built dist. The Test and studio-load-smoke jobs only built parsers + core, so esbuild's config load failed to resolve the package entry. Build studio-server too. * fix(studio): repoint sdkCutoverParity test import to studio-server sourceMutation moved from core's studio-api to @hyperframes/studio-server; the test still imported the deleted core path. This was masked while studio's vite.config failed to load (couldn't resolve studio-server); now that the config loads, the test runs and the stale import surfaced. |
||
|
|
cdf9c817e1 |
refactor: extract @hyperframes/parsers from core (#1755)
## Summary Extracts the GSAP parser/writer suite, HTML parser, hf-ids, spring-ease, and the shared composition data types out of `@hyperframes/core/src/parsers/` into a new, independently-publishable **`@hyperframes/parsers`** package. This is the foundation of the [#1749](https://github.com/heygen-com/hyperframes/issues/1749) effort: make HyperFrames' parsing/linting/validation usable as plain libraries in a Node app, without shelling out to the CLI. Parsers is the standalone base every other extracted package builds on. **Part 1 of 3** — splits #1754 into independently-reviewable pieces. Parts 2 (lint) and 3 (studio-server) stack on this branch. ## What moves | | | |---|---| | Source moved out of core | **~9,900 LOC** (`src/parsers/` → `packages/parsers/src/`) | | Total lines removed from core (incl. tests + goldens) | ~19,600 | | Files relocated | 39 | | Tests carried over | **660 passing** (5 skipped, 3 todo) | The big movers: `gsapParser` / `gsapParserAcorn` (the recast + acorn dual parsers), `gsapWriterAcorn`, `gsapSerialize`, `gsapUnroll`, `htmlParser`, `hfIds`, `springEase`, `stableIds`, plus the `__goldens__` corpus. ## Bundle footprint of the new package | Artifact | Size | |---|---| | `dist/` (unpacked) | 1.7 MB | | npm tarball (packed) | 409 KB | | `dist/index.js` | 90 KB (**~21 KB gzipped**) | | Heaviest entries | `gsapWriterAcorn.js` 93 KB · `gsapParser.js` 91 KB | Most of the weight is the GSAP AST machinery (recast/babel/acorn). It's tree-shakeable via subpath entries (`@hyperframes/parsers/hf-ids`, `/gsap-constants`, etc.) so a consumer that only needs `hf-ids` (2 KB) doesn't pull the parsers. ## How `@hyperframes/core` changes The interesting part: **core sheds its entire AST toolchain.** | core `dependencies` | before | after | |---|---|---| | count | 9 | 6 | | removed | — | `@babel/parser`, `acorn`, `acorn-walk`, `magic-string`, `recast` | | added | — | `@hyperframes/parsers`, `linkedom` | Before this PR, importing `@hyperframes/core` at all dragged in babel + recast + acorn just to construct types. Now those live behind `@hyperframes/parsers`, and a consumer that only wants core's runtime/compiler types never resolves the parser stack. Core keeps thin `@deprecated` re-export stubs at the old subpaths (`@hyperframes/core/gsap-parser`, `/gsap-constants`, …) so nothing downstream breaks. ## Design notes - **`"bun"` export condition before `"node"`** in every package export. Bun resolves the TypeScript source directly (no pre-built `dist/`), while Node/tsx/Docker contexts fall through to `"node"` → `dist/`. This keeps the dev loop zero-build while published artifacts stay Node-consumable. - `@hyperframes/parsers` is **standalone** — zero `@hyperframes/*` dependencies — so it can be the base of the stack. ## Test plan - [x] `bun run --filter @hyperframes/parsers test` — 660 tests pass - [x] `bun run --filter @hyperframes/sdk test` — 382 tests pass - [x] `bun run build` — full monorepo build succeeds - [x] Fallow audit passes on CI |
||
|
|
37ac138041 |
feat(studio): draggable 3D-transform cube in the design panel (#1710)
* feat(studio): draggable 3D-transform cube in the design panel
Add a Figma-style draggable cube to the 3D Transform section so users can set an
element's 3D orientation by dragging instead of typing degrees. Drag tilts the
element (rotationX/Y); Shift-drag rolls it (rotationZ); a recenter button resets
the 3D transform to identity. The cube previews the orientation live and commits
on release.
It's an input affordance over the existing keyframe-aware commit path
(commitAnimatedProperty) — a drag at the playhead writes/updates keyframes just
like the numeric fields, no new mutation infra.
- transform3dProjection.ts: pure unit-cube projection with back-face culling and
painter ordering (no 3D dependency), unit-tested.
- Transform3DCube.tsx: the SVG drag widget (pointer-capture, draft→commit).
- Surface the two missing numeric fields (RotZ, Perspective). Perspective drives
the new editable `transformPerspective` prop (per-element depth) rather than
CSS `perspective` (which only affects children).
* feat(studio): polish 3D cube — collapsed by default, compact lit cube, live drag preview
Address review of the first cut:
- 3D Transform section is now collapsible and collapsed by default (it was tall
and ate panel space).
- Redesign the cube: compact and centered (was full-width), resting isometric
camera so it reads as a 3D cube at identity instead of a flat square,
directional per-face lighting, gradient backdrop + grounding shadow.
- Live element preview while dragging: onLivePreviewProps gsap.sets the live
transform on the preview element so it moves WITH the cube; release still
commits via the keyframe-aware path.
- Extract Cube3dControl to keep the panel component under the complexity gate.
* fix(studio): persist static 3D transform + refine cube edges
The cube (and the RotX/RotY numeric fields) didn't stick on an element whose
only tween is a position 'set' — commitAnimatedProperty tried to convert the
zero-duration hold into keyframes, so the rotation was never written and the
cube snapped back. Handle the static-set case: merge the property into the set
(update-property) so a static 3D rotation/perspective persists, and the cube
reads it back from runtime.
Also refine the cube rendering: muted teal lit faces with edges that brighten
with how front-facing each face is (crisp bevels, not flat neon outlines), a
soft halo glow, and a stronger grounding shadow.
* feat(studio): 3D transform — keyframe diamonds, flash-free commits, in-cube perspective
- Keyframe diamonds: RotX/RotY/RotZ + Perspective (and Z/Scale) now each carry a
KeyframeNavigation diamond, so 3D transforms can be keyframed like Layout X/Y.
Refactored the six fields onto a shared Transform3dField.
- Flash-free: static-set 3D commits now use instantPatch (in-place runtime patch,
no soft reload), and the set fast-path was widened to the 3D channels
(rotationX/Y/Z, z, transformPerspective) — dragging the cube / scrubbing a 3D
field no longer flashes.
- In-cube perspective: a Persp slider lives in the cube widget and the cube's
foreshortening reflects transformPerspective live.
* feat(studio): 3D cube X/Y/Z axis gizmo + gated flash-diagnostic logs
- Axis gizmo: render the rotated X (red) / Y (green) / Z (blue) vectors from the
cube center — away-facing axes dimmed behind the cube, toward-facing on top
with a tip dot + label — so orientation is readable at a glance.
- Flash diagnostics: add a gated, JSON-stringified [hf-3d:*] logger (on in dev or
via window.__hfDebug). Instruments the commit path (which branch + picked
tween), the cube pose/axis commits, and — the key signal — applyPreviewSync's
instant-patch-vs-soft-reload decision (a soft reload IS the flash). Reproduce
with the console open to pinpoint any remaining flash to a specific commit.
* fix(studio): make the 3D cube mirror the element's orientation 1:1
The resting isometric camera made the cube always look tilted, so at rotation
0/0/0 the cube showed a 3D pose while the element was flat — the cube didn't
represent the element. Drop the decorative camera (VIEW_RX/RY = 0): the cube now
faces front at identity, exactly matching the un-rotated element, and tilts to
match as the element rotates. The X/Y/Z axis gizmo keeps the flat-at-rest state
readable.
Flash status (from the gated [hf-3d:*] logs): every commit now reports
'instant (no flash)' via instantPatch — the soft-reload flashes are resolved.
* fix(studio): stop design-panel flicker — read transform channels live
Each 3D commit bumps the gsap cache; the panel then re-read runtime values, but
readGsapRuntimeValuesForPanel only included props already present in the parsed
gsapAnimations. A just-set rotationX isn't in the parse yet, so for that window
the cube + fields dropped it and flickered to 0. Always read the core transform
channels (x/y/rotation/rotationX/Y/Z/z/scale/transformPerspective/opacity)
directly via gsap.getProperty — which reflects the in-place instant patch — so
the panel shows the true current value with no flicker.
* refactor(studio): extract collectPanelPropKeys to keep panel reader under complexity gate
* feat(studio): keyframable 3D transforms — convert a static set to keyframes
The cube/3D fields stored rotation as a static 'set', and convert-to-keyframes
flatly refused to convert a set (gsapParser.ts) — so two 3D 'keyframes' just
overwrote the same static value with no interpolation.
Now a set converts to an animatable to(): resolveConversionProps emits both
endpoints from the set's value (visual unchanged until edited), and both writers
flip set→to, drop the immediateRender hold, and add a duration. The element's
clip duration is threaded through the convert chain (3D field → handler →
convertToKeyframes → route → parser) so the keyframes span the whole clip and
land in range at any playhead. Click a 3D field's diamond to convert, then edit
at different playheads to animate. Acorn writer mirrored; recast round-trip test
added.
* feat(studio): keyframe toggle on the 3D cube
The cube had no keyframe affordance, so dragging it only ever wrote the static
set (logs showed every rotation commit as path:static-set) and nothing
interpolated — converting required clicking a numeric field's diamond, which
isn't discoverable while driving the cube.
Add a keyframe diamond button to the cube widget: it converts the 3D
('other'-group) static set to keyframes spanning the element's clip, and lights
up when the transform is already keyframed. Once keyframed, cube drags + numeric
edits add keyframes at the playhead and the 3D rotation interpolates.
* feat(studio): auto-keyframe 3D transforms on animated elements + stop AssetsTab 404 loop
3D transforms now auto-keyframe like drag/resize/rotate: when the element is
already animated (its clip has keyframes), editing a 3D prop converts the static
set to keyframes so edits at other playheads interpolate — no manual keyframe
toggle needed. Purely static elements still write a static set (and the cube's
keyframe button remains a manual opt-in for them).
Also fix the AssetsTab media-manifest fetch: it was keyed on the assets array
reference (new each render) so it re-fetched the (usually missing) manifest on
every re-render — spamming 404s and churning the left sidebar during cube drags.
Key on a stable join and cache the 404 so a missing manifest is fetched once.
* fix(studio): cube writes one keyframe per drag (no duplicate keyframes)
The cube committed rotationX/Y/Z as separate add-keyframe mutations; the first
axis's auto-keyframe convert shifted the tween so the second axis computed a
slightly different percentage → two adjacent keyframes instead of one.
Add a batched commitAnimatedProperties that writes all changed props into ONE
keyframe, and route the cube through it (commitAnimatedProperty is now a thin
single-prop wrapper). Threaded through the panel chain; numeric fields keep the
single-prop path. Set-path and keyframe-path extracted to helpers to stay under
the complexity gate.
* refactor(studio): extract AudioRow from AssetsTab to satisfy file-size check
The manifest-404 fix touched AssetsTab.tsx, which was already over the 600-line
cap (702). Move the self-contained AudioRow sub-component to its own file,
bringing AssetsTab to 493 lines.
* fix(studio): self-heal stale animationId on 3D property commit
A 3D property edit (cube drag / field) picks its target from the panel's
selectedGsapAnimations cache. When keyframes were just removed or the script
changed underneath, that id is gone server-side and the commit POST 404s
('animation not found'). The raw commitMutation already toasts but rethrows,
so the rejection escaped as an uncaught promise. Catch it in
commitAnimatedProperties and bump the cache so the panel re-syncs and the
next edit self-heals.
* fix(studio): batch the 3D reset into one commit (was six flashes)
Reset 3D orientation looped six props (rotationX/Y/Z, z, scale,
transformPerspective) through the single-property commit, so one click
triggered six separate soft-reloads — six preview flashes. Batch them into
one onCommitAnimatedProperties call (one keyframe, one reload), matching the
cube-drag path.
* fix(studio): 3D-edit a static element writes a set, not keyframes
Editing the 3D transform of an element with no keyframes created a keyframed
tween (Case 3 made a tl.to() + convert, a flat tween converted to keyframes).
A static element should stay static — same as manual drag / resize / rotate,
which tl.set() it. Route no-keyframe elements to a set: update an existing one
in place, or create a dedicated tl.set carrying all axes in ONE add mutation.
The single mutation also avoids the per-axis id race (a flat tween's
group-derived id shifts after the first prop, 404-ing the next and polluting
an unrelated tween).
* feat(studio): instant 3D keyframe edits via in-place tween rebuild
Dragging the cube on an animated element soft-reloaded the iframe on every
edit (a flash). GSAP compiles object-form keyframes ({ "0%": {...} }) into
sub-tweens at creation and ignores later vars.keyframes mutations, so the value
can't be patched the way a tl.set can. Instead REBUILD the tween in place: kill
it and recreate it on the same parent timeline at the same position with the
edited keyframe merged and all other vars preserved, then re-seek — no iframe
reload, no flash. Resolution is now channel-aware for keyframe tweens too, so a
rotation edit lands on the rotation tween, never a co-located position tween.
Declines (→ soft reload) for array-form, motionPath, or dynamic values.
* feat(studio): static 3D transform persists as off-timeline gsap.set (no 0% keyframe)
Adjusting a 3D transform on an element with no keyframes created a
tl.set(...,0), which the timeline renders as a 0% keyframe diamond — even
though it's a static hold, not animated. Persist a newly-created static 3D
hold as a base gsap.set(...) instead: it runs immediately, sits OFF the
timeline, and shows no keyframe marker (matching the manual-drag UX).
- Model: GsapAnimation.global marks a base gsap.set vs an on-timeline tl.set.
- Parser (recast + acorn): parse a STRING-LITERAL gsap.set("#sel", {...}) as an
editable global set so it round-trips and re-edits in place; variable-target
gsap.set(el, ...) holds stay opaque surrounding source (unchanged).
- Serializer + writers: emit gsap.set(sel, props) (no timeline var, no position)
when global; in-place updates keep it a gsap.set.
- add mutation gains global; commitStaticSet sends it when creating a holder.
* fix(studio): static manual drag persists as off-timeline gsap.set, instant (no flash/diamond)
After keyframes are removed, manually dragging a now-static element wrote a
tl.set(...,0) — an on-timeline hold that shows a 0% keyframe diamond and
soft-reloaded on the first nudge (a flash/teleport between the overlay and the
committed position). Make the static position/rotation drag persist as a base
gsap.set (off-timeline, no marker), like the 3D path.
A gsap.set has no runtime tween to patch, so add a 'global-set' instant-patch
that applies the value straight to the element (gsap.set(el, props)) — the
element is static on these channels, so it reflects instantly with no soft
reload. Existing tl.set holds keep the tween 'set' patch; only global sets use
global-set. Create now carries the instant patch too, so the first nudge is
flash-free.
* fix(studio): a base gsap.set shows no keyframe diamond (timeline + panel)
A base gsap.set is parsed as an editable set (for idempotent re-edits), but
synthesizeFlatTweenKeyframes turned it into a synthetic 0% keyframe, so the
timeline track and the panel field showed a phantom keyframe diamond for a
static, non-animated value. Return null for a global set so it contributes no
keyframes — it's an off-timeline static hold, not a keyframe.
* fix(studio): a static set never shows a keyframe diamond (timeline + panel)
A set (gsap.set OR tl.set) is a static hold — a value applied at one point,
not an animated keyframe — so it must not synthesize a phantom keyframe. The
prior fix only skipped GLOBAL gsap.set; on-timeline tl.set holds (and ones a
split/conversion produced) still showed a diamond. Skip every set, which also
aligns the AST keyframe cache with the runtime scan (it already drops every
zero-duration set).
* fix(studio): batch set-property edits (reset 3D no longer 404s)
Reset 3D fires 6 props (rotationX/Y/Z, z, scale, perspective) at a set;
commitSetProps updated them one at a time. A set's id is GROUP-derived, so the
moment scale lands on a rotation set its id shifts (-other -> mixed), 404-ing
the next prop (perspective never got set). Add an update-properties mutation
(merge many props in one call) and have commitSetProps/commitStaticSet use it —
one round-trip, no mid-loop id shift.
* style(studio): fix format + trim 3D-patch helper complexity
oxfmt the runtime-patch file (the failing Format/Preflight check) and reduce
the complexity of the new helpers: flatten keyframeVarsCarryChannel with .some,
extract finiteNumericProps from applyGlobalSet, suppress the inherently-defensive
rebuildKeyframeTween guard chain.
* chore(studio): remove [hf-3d:*] debug logs (3D transform verified working)
Strip the log3d call sites + the debug3d util now that the 3D transform /
static-set / keyframe-rebuild paths are confirmed working.
* chore(studio): strategic [hf-pos:*] logs for position-commit path audit
Temporary DEV-gated logs to confirm which path each drag takes: single drag →
GSAP code path (single-gsap), multi-select/group drag → DEPRECATED CSS-var path
(group-css, applyStudioPathOffset → --hf-studio-offset), and the single CSS
fallback (single-css). To be removed once group drag is routed through GSAP.
* fix(studio): route multi-select group drag through GSAP code path
Group drag committed positions via the deprecated --hf-studio-offset CSS
var (applyStudioPathOffset) and outright blocked GSAP-animated elements.
Single drag already routes through tryGsapDragIntercept (tl.set /
keyframes / gsap.set); group drag now does the same per element, so a
multi-select move writes real GSAP code with no CSS-var fallback. Removed
the now-dead CSS group commit.
* feat(studio): live candidate highlight while marquee-selecting
The marquee only revealed what it selected on mouse-up, so it was easy to
grab too much or too little. Now each element the marquee box currently
intersects is outlined live (studio-accent) as you drag, before release —
so you can see the selection forming. Shares one synchronous OBB/SAT
intersection pass between the live highlight and the commit; the async
source-probe still runs only once, on mouse-up.
* chore(studio): remove temporary [hf-pos:*] position-path debug logs
Investigation done — group drag now routes through the GSAP code path, so
the CSS-vs-GSAP path-audit scaffolding (logPos / debugPos) is no longer
needed. Removes the util and its imports/calls.
* fix(studio): marquee selects/highlights elements at their real positions
The marquee derived element boxes from elementObbCorners, whose
non-identity-transform branch reconstructed the box from offsetLeft/offsetTop
plus the element's own transform matrix — ignoring the matrix translate
(m.e/m.f) and any ancestor transforms. Mid-GSAP-animation (elements carry a
translate() transform), that put boxes at their pre-translate layout
position, so the marquee highlighted/selected the wrong elements vs. the
box shown when you click an element directly.
Route the marquee through the same toOverlayRect basis the selection and
group boxes use (a getBoundingClientRect-based AABB). Now highlight ==
selection-commit == the click-selection box, at the element's real on-screen
position. Drops the buggy OBB/SAT path (elementObbCorners,
marqueeIntersectsObb); AABB matches the selection box, which never rotated.
Adds dev-only [hf-marquee:*] tracing (per-element rect + intersect + skip
reason, JSON) to debug what the marquee sees; stripped from prod builds.
* fix(studio): off-canvas elements no longer render a selection-style border
OffCanvasIndicators drew two layers per partly-off-screen element: a dashed
sliver on the protruding part, plus a solid studio-accent border (with the
selection box-shadow) over the on-canvas portion. That solid border only
ever draws for UNSELECTED elements (selected ones get a real selection box
via the filter), so an unselected off-canvas element looked selected.
Removed the solid inside layer — the dashed protruding sliver stays as the
off-canvas hint.
* chore(studio): remove [hf-marquee:*] debug logs
Marquee position fix is verified; strip the dev-only tracing scaffolding
(logMarquee/debugLabel/debug param) back to the lean intersection loop.
* fix(studio): convert a global gsap.set to a seekable timeline tween + review cleanups
Primary fix: converting a global `gsap.set` to keyframes flipped only the
method (set->to), leaving the callee object `gsap` — emitting `gsap.to(...)`,
an off-timeline tween that fires once at load and isn't on the paused master
`window.__timelines` (the engine can't seek/render it). Reachable from the
cube's keyframe toggle + maybeAutoKeyframeSet on the global sets commitStaticSet
creates. Now re-roots onto the timeline var and adds the position arg, in both
the recast and acorn writers; covered by a convert test seeded from gsap.set in
each path.
Review cleanups: drop dead confirmDelete/<DeleteConfirm> in AudioRow; drop the
always-zero viewRx/viewRy camera params from the 3D projection; un-export four
internal-only symbols (clears fallow unused-exports); re-add the collectMarqueeHits
complexity suppression dropped with the debug scaffolding.
* chore(studio): green the CI gate + 3D panel expanded by default
- File-size: extract the marquee/candidate render into MarqueeOverlay so
DomEditOverlay drops back under the 600-line cap.
- Fallow complexity: suppress the 8 accepted-complexity findings from the 3D/
runtime work (resolveRuntimeTween, readRuntimeKeyframes, hasNonHoldTweenForElement,
commitKeyframeProps, scored, ImageCard, selectionShapeStyles, off-canvas effect)
with the bare directive the linter recognizes.
- 3D transform panel now defaults to expanded (the cube gizmo is the headline).
|
||
|
|
364992203e |
feat(studio): motion editing — speed-curve editor, class-tween attribution, per-keyframe size & ease (#1705)
Speed-curve editor: a fixed-square cubic-bezier graph (grid, linear reference,
draggable handles, live preview) for editing eases; conventional preset grid.
Class/selector tweens: attribute `gsap.from(".dot", …)`-style tweens to every
matching element so they surface in the inspector and keep their timeline
keyframe diamonds when the clip is selected.
Apply-to-all easing: a "Set all…" control sets easeEach and strips every
per-keyframe ease override in one mutation (AE select-all + F9). Implemented in
BOTH gsap writers — the acorn writer and the recast writer (the default server
path); the recast side was missing resetKeyframeEases, so "Set all" set easeEach
but left per-keyframe eases in place.
Per-keyframe size: resizing an animated element writes a width/height keyframe
at the playhead — other keyframes keep their size — instead of a global
gsap.set hold; static elements keep the simple global resize. The extra size
tween exposed a motion-path bug (the overlay read whichever tween contained the
playhead), fixed with an opt-in requireChannels filter so the path only reads
the positional tween.
Inferred Timing: derive Start/End/Duration from an element's animations when it
has no authored clip range, instead of showing 0.00s.
Ease labels now surface the raw GSAP token (power2.out, back.out, …) instead of
invented names ("Smooth slowdown") that confused authors.
Also pass the preview iframe to the inspector's animation hook so element
resolution runs, and remove the unused editDebugLog facility.
|
||
|
|
97db811a2f |
feat(studio): per-keyframe ease presets, velocity fitting, gesture smoothing (#1694)
Per-keyframe speed-curve editing, velocity-based ease fitting, and Gaussian gesture smoothing. Easy Ease presets, per-segment KeyframeEaseList with a bezier editor, AE-convention ease fitting, position-only set-tween rows, and AnimationCard extraction. |
||
|
|
0e75eb2510 |
fix(core): render standalone sub-composition previews + surface captured video assets (cli) (#1631)
* fix(core): escape digit-leading id selectors in standalone sub-composition preview
A CSS identifier cannot start with a digit, so an authored rule like
`#01-wall-pushes-back { ... }` is an invalid selector and the browser drops
the whole rule — taking the root's size/background with it. A full
composition masks this (the host stretches/paints the frame), but a
standalone preview has no host, so the root collapses to height:0 +
transparent and renders blank.
extractFullDocumentParts now rewrites `#<digit-leading-id>` selectors to
their escaped valid form (`#\30 1-...`, still matching the element id),
scoped to ids actually present and matched only as `#id` not followed by an
ident char so hex colors are never touched. Also harden the <template>
inner-HTML extraction to use the DOM instead of a greedy regex. Tests added.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit
|
||
|
|
091137e3c3 |
feat(studio): instant, flicker-free manual editing (#1605)
* chore(producer): shim __filename/__dirname in the CJS banner Bundled CJS deps like wawoff2 call __dirname; without the shim they throw "__dirname is not defined in ES module" at render time. Also ignore .zed/. * chore(producer): use a template literal for the CJS banner (review nit) * feat(core): add GSAP keyframe + motion-path source mutations Array-form keyframe removal in both the recast and acorn writers, plus update/add/remove-motion-path-point and add-motion-path. Exclude _auto and data from tween property-group classification. * fix(core): address #1554 review — data-exclusion test, split-fix doc, motion-path sentinel, parity blocks - Regression test for the `data` GSAP-key exclusion (parallel to _auto). - splitAnimationsInScript: documented that .fromTo()/.to() correctly stay out of the from-branch (only .from() reverts) and the <= boundary; added mid-flight straddle tests. - addMotionPathToScript failure path returns id: null (was empty-string sentinel); caller updated. - Parity blocks for addKeyframeToScript array-form + updateKeyframeInScript (mirroring removeKeyframeFromScript). Surfaced a latent acorn array-form partial-props merge bug — documented as it.skip with a ready assertion (acorn cutover follow-up). * feat(core): route motion-path mutations through studio-api + fix clip stamping Wire the new mutations into the file save route. Only authored clips suppress descendant stamping, so auto-stamped animated scenes can inline-expand. Hide in-flow timed clips with `display:none` only when they are LEAF clips (no nested timed clips). `display:none` on a container removes its whole subtree, hiding descendants that are still inside their own visibility window — e.g. an in-flow composition root whose effective window clamps to the timeline end would black out a child video that should still show (the hdr-hlg regression). Containers keep `visibility:hidden`, which a visible descendant can override; only leaves leave the flow, which is all the split-overlap case needs. * feat(core): strip legacy path-offset/rotation + drop obsolete studio lint rule A position or rotation add/set mutation makes the GSAP timeline the single source of truth for that channel, so any lingering --hf-studio-offset / --hf-studio-rotation CSS var must be cleared to avoid double-applying. stripStudioEditsFromTarget now clears both channels, and the add-strip fires for the position AND rotation property groups. Also removes the obsolete `gsap_studio_edit_blocked` lint rule: it warned that Studio cannot save drag/resize edits to elements in a registered timeline — the exact premise the single-source work inverts (the timeline is now the edit target). Removed the rule, its now-unused TIMELINE_REGISTRY_ASSIGN_PATTERN import, and its 5 tests. * fix(core): address #1555 review — complete hold-sync, invalidate clip cache, strip rotation channel - HOLD_SYNC_MUTATION_TYPES: add add-motion-path (load-bearing — addMotionPathToScript authors past t=0 → first-frame snap-to-(0,0) without the hold), update-meta, shift-positions, scale-positions, split-animations. (add stays out: flat tweens only, syncPositionHoldsBeforeKeyframes is a no-op for non-keyframed tweens.) - init.ts: timedClip in-flow/leaf WeakMaps now invalidate on clipTreeSignature change; visible/hidden branches both go through isTimedClipInFlow (was .get() by accident). - keyframesWriteRotation mirrors keyframesWritePosition so a rotation-only keyframe set strips the stale --hf-studio-rotation channel. * feat(studio): GSAP runtime read layer + shared helpers * fix(studio): address #1607 review — cold-parse vs fetch-error budgets, isZeroDurationSet, array-ease tests - useGsapAnimationFetchFallback: discriminate resolved/fetch-error/cold; only the cold (warm-but-zero) race gets the full ~600ms retry budget — a hard fetch error retries once. - Extract isZeroDurationSet (was !(duration>0) duplicated); rejects NaN, documents intent. - parsePercentageKeyframes: cite GSAP even-index spread; tests that a per-entry/interior ease is stripped without shifting the other keyframes' percentages. * feat(studio): GSAP drag/commit/bridge editing infra * fix(studio): address #1608 review — facade awaits commit, strict stale-parse guard, clearProps restore BLOCKER: useSafeGsapCommitMutation now RETURNS the (.catch-chained) commit promise and the commitMutation facade awaits it — so await session.commitMutation(...) resolves AFTER the server save, fixing both consumers (useEnableKeyframes + useGestureCommit's showToast/requestSeek/idle, which were firing before the save landed). SafeGsapCommitMutation return type widened void→Promise<void> (fire-and-forget consumers ignore it). - stale-parse guard uses hasNonHoldTweenForElement (a leftover hold set no longer counts as live). - commitFlatViaKeyframes snapshots dragged gsap values before clearProps + restores after seek, so a failed commit leaves the dropped pose, not a cleared element. * feat(studio): motion-path geometry + commit helpers * docs(studio): address #1609 review — document occlusion fade-in invariant, donut limit, nearestPointOnPath t-semantics * feat(studio): on-canvas motion-path overlay * fix(studio): address #1610 review — scope dblclick to pan-surface, kind-aware geometry guard, gate createMode, screen-space drag threshold * feat(studio): keyframes flag, gesture recording + timeline/selection refinements * fix(studio): address #1611 review — fetch-first keyframe path, gated hydration, dev-gated debug + gesture warn, per-group gesture tweens - useEnableKeyframes: parse current source first (null-vs-[] distinction) so a delete-all's empty parse isn't overridden by a stale selectedGsapAnimations cache. - useStudioUrlState: freeze the hydration effect's time dep once hydrated (was re-running every tick). - useGestureRecording: dev-gated console.warn when the live-preview runtime throws (was silent). - playerStore: gate window.__playerStore behind dev (guarded import.meta.env.DEV). - useGestureCommit: partition recorded keyframes by property group → one add-with-keyframes per group, so a mixed gesture no longer yields an untagged legacy tween. * feat(studio): single-source manual offset + rotation via the GSAP timeline Dragging or rotating an element writes into the GSAP timeline (the single source of truth) instead of a parallel --hf-studio-offset / --hf-studio-rotation CSS var: static elements commit a tl.set (idempotent on re-edit), tweened elements edit keyframes, and the live preview moves via gsap.set so what you see equals what is written and renders. Removes the dual-channel CSS-var/transform reconciliation behind the fling / disappear / runaway / double-stack / wrong-start bug class — for BOTH position and rotation (gesture base read from the gsap transform, gsap.set live preview, tl.set/ keyframe commit, dropped the handleDom*Commit CSS fallbacks). Subcompositions edit the same single-source way, which surfaced and fixes: - resolve a subcomp element's source file via the composition-id map (the runtime drops the source linkage when inlining the subcomposition); - a selected element's selection box AND motion path use basic visibility, not the occlusion heuristic (a backgroundless opacity-1 scene above it is not an opaque cover); - soft reload rebuilds ONLY the committed composition's timeline, leaving other compositions' timelines intact (no cross-composition revert); - read keyframes from the element's OWN composition timeline (scan all timelines, not the first unstable key); - delete-all uses a soft reload too, so editing no longer hard-reloads the iframe. * fix(studio): address #1567 review — drop drag-intercept flag, harden softReload onerror, tighten runtime ladder, per-group gestures - DROP STUDIO_GSAP_DRAG_INTERCEPT_ENABLED: single-source GSAP intercept is the only position/rotation channel; the false branch silently killed drag+rotate (and let GSAP elements into the keyframe-corrupting CSS path). Removed flag + dead branch + env def + tests. - gsapSoftReload: plugin onerror no longer fakes success — signals onAsyncFailure so the caller full-reloads; honors __hfMotionPathPluginLoading so a concurrent reload can't queue a dup script. - gsapDragCommit: resolveDragRuntime narrows the as-any ladder; a mid-seek throw logs + drops partial reads (no phantom identity) and re-applies the drag override in finally. - MotionPathOverlay: park-timer cleanup keyed on animId change. - useGestureCommit: partitionKeyframesByGroup wraps the add-with-keyframes sites (per #1611 review). * feat(studio): patchRuntimeTweenInPlace — update a tween's values in place Defensive runtime helper: locate the element's tween in window.__timelines via the shared resolveRuntimeTween scan, update its set/keyframe vars, invalidate, and re-seek the playhead — without re-running the whole composition. Returns false (caller falls back to soft reload) for any shape it can't safely patch (no tween, dynamic/computed keyframes, motionPath arc, channel mismatch, or any error). Foundation for instant, flicker-free manual edits. * fix(studio): address #1612 review — channel-aware set resolution + decline dynamic-expression patches - resolveRuntimeTween gains an optional channels[] hint; for kind:set it prefers the set whose vars carry one of the patched channels and never returns a disjoint-only set (e.g. won't write {x,y} into a co-located {rotation} set). patchRuntimeTweenInPlace derives channels from the props. - patchSet declines (returns false → soft reload) when overwriting a string/dynamic vars[ch], instead of silently dropping the computed expression. * feat(studio): instantPatch fast path in runCommit A commit carrying an instantPatch option tries patchRuntimeTweenInPlace first; on success the preview updates in place with NO reload (instant), on false it falls back to the existing soft reload. Extracts the preview-sync tail into a testable applyPreviewSync helper. No behavior change when instantPatch is absent. * feat(studio): route static position/rotation set drags through instantPatch Static-element position and rotation set commits now attach instantPatch{selector, change:{kind:set}} so the drag updates in place with no reload. Structural ops (new tween add, delete-all, convert/split/materialize) and keyframe edits deliberately omit it and keep the soft reload — keyframe instant-patch needs object-form keyframe support in patchRuntimeTweenInPlace (deferred). * fix(studio): address #1613 review — derive instantPatch from the mutation, patch both coalesced commits, wire onAsyncFailure - commitStaticGsapPosition/Rotation derive instantPatch.change.props from the actual update-property mutation(s) sent (one source of truth → findUnsafeMutationValues-validated values flow into the patch; can't drift). - Coalesced x/y: the intermediate x commit also carries instantPatch{x}, the y commit {x,y}, so a second-POST failure still leaves the preview patched for what persisted. - applyPreviewSync passes reloadPreview as onAsyncFailure (plugin-CDN load error → full reload); per U4 the synchronous false still does NOT escalate. - (channel disambiguation from #1612 verified end-to-end: {x,y}→position set, {rotation}→rotation set.) * feat(studio): no full iframe remount for soft-reloadable edits A softReload edit (and the SDK single-script refresh) no longer escalates to a full reloadPreview() iframe remount when applySoftReload returns false — the live gsap.set already shows the value, and a remount is the worst flash + re-inlines subcomps (reverting their keyframes). verifyTimelinesPopulated now checks the expected target keys the re-run registers, so a correct scoped re-run doesn't spuriously report empty. Full reload stays only for the structural (no-softReload) and ambiguous-script paths. * feat(studio): pre-load MotionPathPlugin so motion-path edits don't async-flash ensureMotionPathPluginLoaded() runs once at the preview iframe-load seam (NLELayout onIframeLoad), eagerly loading + registering MotionPathPlugin without killing the timeline. So when a user adds a motion path to a composition that didn't originally use one, the soft reload runs synchronously instead of taking the kill-then-await-CDN async path (the flash). Idempotent + defensive; the existing async fallback stays for genuine cold-start/CDN-failure. * fix(studio): don't re-save + reload when source editor syncs externally The SourceEditor's CodeMirror update listener fired onChange on ANY docChanged — including the programmatic dispatch that syncs external content (e.g. a manual-edit commit writing the source back into the open editor). That made the editor re-save the file and bump refreshKey, fully reloading the preview iframe on every drag/keyframe edit — defeating the in-place instant patch and causing the flash. Annotate the programmatic sync (ExternalSync) and skip onChange for it, so only real keystrokes save. * fix(core): inject MotionPathPlugin into preview when a composition uses motionPath A studio-created motion path writes a gsap motionPath tween into the single-source timeline, but the preview HTML only loaded gsap core — so the first render threw "Invalid property motionPath ... Missing plugin?". Detect motionPath usage and inject MotionPathPlugin right after the composition's gsap script, version-matched to it. * fix(studio): dedup __hfMotionPathPluginLoading type decl (restack artifact) * fix(studio): address #1605 review — distinguish soft-reload failure modes + observability, SourceEditor focus guard BLOCKER: applySoftReload now returns SoftReloadResult ('applied' | 'verify-failed' | 'cannot-soft-reload') instead of a bare bool. applyPreviewSync + sdkRefresh escalate to a full reloadPreview() on the PERMANENT 'cannot-soft-reload' (no gsap/rebind hook/scopable key/script, or sync re-run threw) — fixing the silent-stale-preview U4 dropped — but still suppress the TRANSIENT 'verify-failed' (live gsap.set is correct). Telemetry: gsap_soft_reload_outcome (origin/result/escalated) + gsap_instant_patch_fallback, so the U4 invariant is enforced, not asserted. - SourceEditor: skip the programmatic external-sync replace while the editor is focused, so an in-flight commit doesn't clobber the user's uncommitted keystrokes (ExternalSync kept for unfocused). - Verified ensureMotionPathPluginLoaded already guards __hfMotionPathPluginLoading (no double-append). * fix(core): align __clipTree and __clipManifest ids via stableClipId Timeline inline expansion was dead for nested children inside index.html: the tree keyed id-less elements by a synthetic __clip-N while the manifest keyed them null, so parent<->child never joined. Both now resolve identity through stableClipId (id || data-hf-id), which every generated element has. * fix(core): strip baked runtime + tag comp root in preview assembly Comps that ship a baked inline runtime were double-loaded (preview injects its own) and the baked copy failed to parse inline (Unexpected token '<'). Strip it in buildSubCompositionHtml + the disk-fallback preview path. Also tag the comp root with data-composition-file so the studio resolves a comp's top-level elements to the right source file instead of defaulting to index.html (which made the GSAP panel parse the wrong, multi-timeline file). * feat(studio): set motion-path destination from a toolbar toggle Replaces the double-click-on-canvas UX (which painted text over the preview) with a 'Set motion destination' toggle next to Snap/Grid, shown only when the selected element can take a path. While armed, one canvas press places the destination. Also removes the dead TimelinePropertyRows component. * fix(studio): center timeline keyframe diamonds on their percentage Dropped clampDiamondLeft, which forced boundary keyframes fully inside the clip so a 0% diamond sat half a diamond right of the 0% point. Each diamond's midpoint now sits exactly on its % (the clip is overflow-visible). * fix(studio): resize static elements via tl.set, not a single-stop keyframes tween Resizing an element with no size animation wrote keyframes:{ <playhead%>: {width,height} } — one mid-point stop GSAP can't interpolate, so it rendered NaN/0 dimensions at every other frame and the element vanished (worst off 0%). Added commitStaticGsapSize (mirrors commitStaticGsapPosition): a static resize now writes tl.set({width,height}), held at all frames; re-resizing updates it in place. * fix(studio): negative-cache failed media probes Only successful probes were cached, so CORS/404 cross-origin media was re-probed every rAF-driven timeline re-derive, flooding the console. Remember failed URLs and skip them. * fix(studio): type window.setTimeout handle as number ReturnType<typeof window.setTimeout> infers NodeJS.Timeout when @types/node is present and clashes with the DOM number the call returns. Type it number. * fix(studio): drag/resize disappearance, stale-ID duplicates, soft-reload clearProps - Fix soft-reload clearProps destroying element inline styles — save cssText, clear, restore, strip only transform - Fix resize no-op on re-resize: delete+add instead of two update-property - Route set tweens through static resize path (convertToKeyframes skips sets) - Re-fetch animation ID before drag commit to prevent stale-ID duplicates - Guard editDebugLog for Node test environments - Fix NLELayout setState-during-render (move reset to useEffect) - Stop SnapToolbar pointer events propagating to canvas deselect handler - Enable click-to-add waypoints on cubic motion paths - Add whole-path drag offset (Alt+drag shifts all keyframes together) - Add Canvas shortcuts section to ShortcutsPanel - Extract useMotionPathData + commitGsapPositionFromDrag (filesize compliance) - Delete dead code (getElementDepth, isElementVisibleInPreview, unused exports) |
||
|
|
758eda995c |
fix(sdk,studio): restore DOM edit cutover parity (#1565)
- Add splitStyleDeclarations with quote/paren-aware CSS parsing - Fix backslash escape handling inside quoted CSS string values - Close html-attribute safety gap in SDK cutover (event handlers, dangerous URIs) - Consolidate HTML attribute safety constants to core/utils/htmlAttrSafety.ts - Extract NON_HTML_CHILD_TAGS set for foreign-content decline gate - Add sdkCutoverParity test corpus (shorthand/longhand, mixed batches) |
||
|
|
967bf9f9ed |
refactor(core): gate acorn GSAP writer behind cutover flag; keep recast default (WS-3F) (#1573)
* refactor(core): retire recast/babel, route all GSAP mutations to acorn (WS-E/3.F) - Delete gsapParser.ts (2595-line recast-based parser/writer) - Delete gsapParser.test.ts, gsapParser.stress.test.ts, gsapParser.test-helpers.ts - Add gsapParserExports.ts: re-export umbrella for gsap-parser subpath - Move SplitAnimationsOptions/SplitAnimationsResult to gsapSerialize.ts - executeGsapMutation: async->sync, static acorn imports replace loadGsapParser() - Fix 3 function name mismatches in files.ts switch cases - generators/hyperframes.ts: imports from gsapSerialize (blocker resolved) - gsapWriterAcorn.ts: SplitAnimationsOptions from gsapSerialize - Parity tests: recast oracle removed; acorn-only regression (14 pass) - Remove recast and @babel/parser from core/package.json Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(sdk): harden mutation handlers + widen variable API (code-review) Self-contained review fixes for the SDK-hotspot stack (#1569–#1573). The dispatch path (_dispatch → applyOp) never runs validateOp, so the new WS-D/WS-3.C guards were advisory-only; re-enforce them in the handlers. - addElement: null-guard the resolved parent (no more `as Element` masking a null → crash on unknown parent id); reject <script> and multi-root fragments via parseInsertableFragment instead of inserting raw markup / silently dropping extra roots. - addWithKeyframes / replaceWithKeyframes: bail on empty keyframes (no degenerate `keyframes: {}` tween) and when the animationId resolves to nothing (no silent degrade-to-add leaving a duplicate tween). - isObjectVariableValue: exclude arrays so an array override value can't be misclassified as a font/image object and written into the variable model. - Composition.setVariableValue: widen the public interface signature to `… | FontValue | ImageValue` to match the impl + EditOp (B2 object-valued variables were unreachable via the typed API). - mutate.gsap.test.ts: import addKeyframeToScript from gsap-writer-acorn — the gsap-parser subpath no longer re-exports write fns after recast retire, so the test threw at runtime (red suite). - Dedup: export EXCLUDED_TAGS from hfIds.ts and drop the verbatim HF_EXCLUDED_TAGS copy in mutate.ts. Adds guard regression tests. SDK 340/340, core hfIds 13/13, build green, fallow --gate new-only clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sdk): variable-model dedup + undo/scoped-parent correctness; test honesty (code-review) Second batch of review fixes for the SDK-hotspot stack. - Variable model (#7, #13): extract readVariableDefault/writeVariableDefault into a shared engine/variableModel.ts used by both mutate.ts (forward) and apply-patches.ts (replay), so the model shape can't diverge. Add clearVariableDefault and make a `variable` remove patch DELETE the decl's `default` key — the exact inverse of a first-set on a default-less variable. Previously undo of such a set no-op'd and stranded the value. - addElement scoped parent (#8): record the caller's id verbatim (scoped "hf-host/hf-leaf" path or composition id) as the patch parentId instead of the bare data-hf-id, so redo/replay re-resolves the SAME parent via resolveScoped rather than the canonical top-level dup (or document.body). - resolveTimings honesty (#5): correct the header + test that claimed a live "preview == render" parity — neither path consumes the resolver yet (anchor inputs are Pacific/backend-deferred). It's a pure-function property, not a current guarantee. - GSAP writer parity (#12): the recast oracle was deleted in WS-3.F, leaving the WS-3.C keyframe ops comparing acorn output to itself. Pin them as golden inline snapshots and drop the now-dead recast scaffolding (replaceWithKfRecast, removeAnimRecast alias). Remaining pre-WS-3.C parity blocks noted as follow-up. Adds regression tests (undo of default-less variable; scoped-parent redo). SDK 342/342, core timingResolver+parity green, build + fallow --gate new-only clean. Not changed (need design / out of scope): #9 pre-#1569 persisted-override CSS replay (moot for unreleased data; proper fix is render-time CSS derivation), #11 replaceWithKeyframes stale positional id (mitigated by the missing-id no-op guard + type doc; full fix needs non-positional ids). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sdk): replay CSS-prop derivation for legacy var overrides; stale-id selector guard (code-review) Final review-fix batch — the two items deferred from the prior pass. - #9 legacy variable-override CSS: applyOverrideSet now derives the `--{id}` CSS custom prop from any scalar `var.{id}` override on replay (and removes it for a null override). Sets written before the model/CSS split carried only `var.{id}`; without this, replaying them updated the JSON model but left `var(--{id})` bindings rendering the schema default. Replay-path only — the undo path (applyOne) is untouched, so #1569's separate-patch undo correctness is preserved. Object (font/image) values are never CSS, so they are skipped. - #11 stale positional id: replaceWithKeyframes now requires the located animation to still target the caller's `targetSelector`. Position-derived ids re-point after structural edits; a stale id resolving to a DIFFERENT element's tween previously got silently replaced. It now bails (no-op) unless the id still points at the expected selector. Adds regression tests (legacy var.{id}-only override restores CSS; object override writes no CSS; stale-id-wrong-selector replace is a no-op). SDK 345/345, build + fallow --gate new-only clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(core): gate acorn GSAP writer behind cutover flag; keep recast default (WS-3F) Product decision pivot: acorn no longer replaces recast as the GSAP writer. Recast remains the default server writer; acorn runs only when STUDIO_SDK_CUTOVER_ENABLED=true (or =1) is set server-side — the same env flag name as the client Vite var, so a single switch flips both sides. Changes: - Restore gsapParser.ts (recast writer) + test/stress/helper files deleted by 3F - Restore @babel/parser + recast deps in packages/core/package.json - Add isAcornGsapWriterEnabled() + loadGsapParser() to files.ts (lines 59-82) - Split executeGsapMutation into async dispatcher + executeGsapMutationRecast (recast, async via loadGsapParser) + executeGsapMutationAcorn (acorn, sync) - Dispatcher defaults to recast; acorn branch taken only when flag is on - Restore gsapWriter.parity.test.ts, gsapWriterParity.acorn.test.ts, and gsapWriterParity.corpus.test.ts to true recast-vs-acorn differential suites (not acorn-vs-itself) - Exempt gsapParser.ts in .fallowrc.jsonc health.ignore + ignoreExports (pre-existing complexity + barrel re-exports consumed outside diff scope) - Add fallow-ignore-file code-duplication to files.ts (intentional parallel switch bodies for two writers) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
8a13a07074 |
feat(studio): add storyboard manifest contract, parser, and read API (#1528)
First PR in the Studio storyboarding stack. Establishes the parseable contract the storyboard UI reads from; no UI yet. - core/storyboard: StoryboardManifest/Frame/Globals types + a lenient STORYBOARD.md parser (frontmatter + status/src/duration/transition_in, freeform narrative tolerated, never throws, records warnings). Exposed as @hyperframes/core/storyboard (browser-safe). - studio-api: GET /projects/:id/storyboard returns the normalized manifest with per-frame srcExists; missing file -> exists:false, not 404. - fixture: packages/studio/fixtures/storyboard-sample for dogfooding the storyboard view in later PRs (built/animated frames + one outline). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
1ad158d2f0 | feat(runtime): apply color grading in preview and render | ||
|
|
3109acb88a | feat(core): add color grading schema and lut parsing | ||
|
|
b9bd9ed91d |
fix: resolve computed GSAP timelines + drag improvements in Studio (#1506)
* feat(core): add param-substitution utility for GSAP timeline inlining U1: clone + shadow-aware identifier substitution over acorn ESTree, plus provenance tagging and a GsapProvenance type. Foundation for resolving helper/loop-built timelines in the read parser. * feat(core): inline helper-built and bounded-loop GSAP timelines U2: expansion pre-pass that rewrites the analysis AST so a helper called N times, a literal-bounds for-loop, a for-of, or a forEach over an inline array each become concrete per-call/per-iteration tl.* statements with substituted positions and provenance tags. Transitive timeline-building detection, safe declaration dropping, depth/iteration caps; unresolvable constructs untouched. * feat(core): resolve computed GSAP timelines in the read parser U3: parseGsapScriptAcorn runs the inlining pre-pass before analysis, so helper-built and bounded-loop timelines resolve at true positions with motionPath arcs recognized; each tween carries provenance. Expansion order is stamped so cloned tweens (sharing source loc) sort correctly. Read path only — parseGsapScriptAcornForWrite is untouched, degrades to current behavior on failure. The add-to-basket addCycle case now yields 7 resolved animations. * feat(studio): runtime-authoritative keyframes for dynamic timelines Phase 2 (U4-U6): the live-runtime scanner returns tween-relative keyframes with per-tween timing and converts them to clip-relative when given clip dims, fixing the timeline-vs-clip-relative bug; it extracts motionPath into arcPath (shared buildArcPath) so the Arc Motion panel activates for data-driven arcs; the cache leaves statically-unresolvable tweens to the runtime scan. Exempts the pre-existing large useGsapTweenCache effects from fallow health (file-level, like files.ts) rather than suppression comments. * feat(studio): surface keyframe editability from provenance U9: editabilityForProvenance(provenance) -> direct|unroll|override (core, re-exported from the acorn subpath). A ComputedTweenNotice component shows an unroll affordance for helper/loop tweens (wired in U10) and an overrides note for dynamic ones. Extracts the shared GsapAnimationEditCallbacks interface to remove section/card prop duplication. * feat(core): lint understands computed timelines (acorn parser) U7: the GSAP lint rule now loads parseGsapScriptAcorn (which inlines helpers and bounded loops) instead of the recast parser, so overlapping_gsap_tweens and related findings reflect true resolved positions for computed timelines — and keeps recast out of the lint graph entirely. Literal compositions are unchanged (parity), all 182 lint tests pass. * docs: document the computed-timeline keyframe editing model U8: keyframes.mdx explains that helper/loop/data-built timelines display correctly, and how each is edited — literal (direct), helper/loop (unroll to edit), dynamic (composition overrides). Nothing is permanently locked. * feat: unroll computed timelines into literal tweens (U10) Adds unrollComputedTimeline (core): serializes a parsed timeline's resolved animations back to literal tl.* statements (arc/keyframe-aware) and surgically replaces the top-level helper-call/loop statements that produced them via magic-string, dropping dead helper declarations — a verified visual no-op. Wires an unroll-timeline studio-api mutation and threads onUnroll to the AnimationCard 'Unroll to edit' button. Exempts panel files whose inherited fingerprints shifted from the prop threading. * feat(runtime): declarative keyframe override layer for dynamic tweens (U11) Adds applyKeyframeOverrides: fetches a gsap-overrides.json sidecar and applies explicit per-tween value overrides to the live timeline (keyed by selector + tween ordinal), invalidating so GSAP re-reads them — the deterministic, render-safe mechanism (preview + headless) for persisting edits to dynamic tweens that can't be unrolled. Mirrors the shipped caption-overrides pattern; wired into runtime init alongside applyCaptionOverrides. * refactor: drop the keyframe override layer; rely on unroll + source Removes the gsap-overrides.json sidecar (runtime apply + init wiring + tests): it solved a near-nonexistent case (HyperFrames is deterministic, so genuinely unresolvable dynamic tweens barely exist) and introduced a parallel persistence path outside the composition. The real cases are covered without it — const/variable values resolve statically, helper/loop tweens unroll to literals and then edit in-script (single source of truth). Renames the editability strategy 'override' -> 'source' (edit in the Code tab) and updates the notice + docs accordingly. * fix(studio): drag outside tween range creates new keyframe, picks nearest tween Fixes the GSAP drag intercept to pick the position tween closest to the playhead (not the one with the most keyframes), and when dragging outside all tweens' ranges, creates a brand-new keyframed tween instead of destructively extending/replacing the nearest one. Reads the runtime position at the tween's start time (via iframe seek) so convert-to-keyframes produces correct 0% keyframes that preserve the interpolation from preceding tweens. * fix(studio): drag outside tween range creates new keyframe, picks nearest tween Also reverts all fallow health.ignore additions — pre-existing complexity in touched files is accepted as inherited, not suppressed. |
||
|
|
121cdd2d9f |
fix(telemetry): attribute studio renders to the browser user (joinable funnel) (#1492)
Studio-triggered renders emit render_complete / render_error from the CLI preview-server process, which stamps every event with the install's anonymousId (client.ts drainQueueToPayload). The browser, meanwhile, fires studio_session_start / studio_render_start under its own getAnonymousId(). So the render outcome and the render start never share a person_id — verified in data: of 15,125 users who started a studio render in 30d, ZERO have any render_complete under any source, and the 898 studio-tagged completers are disjoint server UUIDs. The studio render funnel — the product's core value moment and strongest retention signal — is therefore unmeasurable. Thread the browser's telemetry id through to the render-outcome events: - client.ts: trackEvent takes an optional distinctId; drainQueueToPayload uses `event.distinctId ?? config.anonymousId`. CLI renders unchanged. - events.ts: trackRenderComplete/trackRenderError forward an optional distinctId. - studioRenderTelemetry.ts: emitStudioRender* pass opts.distinctId through. - core studio-api (types.ts + routes/render.ts): the render route reads `telemetryDistinctId` from the request body (validated string) and passes it to the adapter's startRender, which already forwards opts to the emitters. - studio (useRenderQueue.ts): include getAnonymousId() as telemetryDistinctId in the render POST — the same id studio_* events already use. Result: studio render_complete/error now carry the browser user's id and join studio_session_start / studio_render_start. Older clients that don't send the field fall back to anonymousId (no regression). No new tracking surface — it's the existing anonymous studio id. Tests: per-event override forwarding (events), studio render distinctId threading + older-client fallback (studioRenderTelemetry), and route body → adapter forwarding incl. non-string rejection (core render route). |
||
|
|
fada399539 |
fix(core): route renders/file serving through resolveWithinProject chokepoint (#1477)
The `/projects/:id/renders/file/*` route joined attacker-controlled wildcard input straight onto rendersDir with a bare join() + readFileSync and no containment check — the only project-scoped filesystem route that skipped the resolveWithinProject chokepoint every sibling route uses. Literal/encoded `../` traversal is collapsed upstream by Hono's WHATWG URL normalization (verified empirically), so the plain LFI is not reachable over HTTP. But a symlink living inside rendersDir and pointing outside it was still followed and served verbatim (verified: leaked an external secret, 200 OK). Routing through resolveWithinProject canonicalizes with realpath before serving, closing the symlink escape and making the route's safety independent of the URL layer's normalization behavior. Adds regression coverage: serves an in-dir file, rejects an escaping symlink (403), and still serves a symlink that stays inside rendersDir. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b8fa4b5dd2 |
refactor(core): swap studio-api read path from recast to acorn parser (T6e) (#1392)
* refactor(core): swap studio-api read path from recast to acorn parser (T6e)
* fix(core,sdk): code-review findings — 5 correctness bugs + 2 cleanup
- gsapParserAcorn: top-level variable targets now resolved via program-scope
null-key fallback in lookupBindingFromAncestors (const el = querySelector...)
- gsapParserAcorn: fromTo guard requires args.length >= 3, preventing undefined
args[2]/args[3] access when fewer args supplied
- gsapWriterAcorn: remove fuzzing fallback in removeAnimationFromScript that
silently deleted the wrong animation (from→to ID conversion)
- gsapWriterAcorn: valueToCode guards NaN → "0" to avoid broken tween props;
safeKey regex aligned to ASCII-only (matching gsapSerialize)
- mutate: handleSetGsapTween now includes stagger in extras (was in addGsapTween
but missing from setGsapTween)
- apply-patches: script case now mirrors stylesheet — op=remove calls
setGsapScript("") instead of silently ignoring the patch
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(core): add trust-model header to T6d parity suite
Documents the recast-baseline trust relationship and clarifies that
motionPath parity tests live in the Phase 3b commit (PR #1379) since
the acorn motionPath parser is also added there.
Addresses #1370 R1-N1 (Rames).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
|
||
|
|
11b050de9a |
feat(studio): scale GSAP positions on clip resize + shift on drag + diamond fixes (#1448)
Resize: proportionally scale all GSAP animation positions and durations to fit the new clip duration via scalePositionsInScript. This preserves clip-relative keyframe percentages — diamonds don't move during resize, nothing disappears. Modeled after After Effects Time Stretch behavior. Drag: shift all GSAP positions by the time delta (unchanged from before). Diamond rendering: - Clamp diamonds at 0%/100% so they stay fully visible at clip edges - Filter out-of-range keyframes using predicted percentages during resize - Clamp connection lines to clip boundaries - PropertyRows: same edge clamping for SVG diamonds Parser: scalePositionsInScript (proportional position + duration scaling), shiftPositionsInScript (rigid shift), scale-positions + shift-positions mutation types, 5 shift tests passing. |
||
|
|
6f677292ae |
refactor(core): simplify packages/core — dead code, dedup, type safety (#1413)
- Delete unused mediaPreloader module, 5 dead RuntimeState fields, emitPerformanceMetric, lintScriptUrls, 5 variable type guards - Consolidate compiler utilities: unify CSS URL regex, relative URL predicate, MIME map, @import regex, bulk asset rewrite delegation - Cache extractGsapWindows per script (eliminates 2 redundant recast parses per lint run), share stripJsComments and script extraction - Deduplicate GSAP parser: share serializeValue/safeJsKey, centralize converted-id fallback (6 sites), keyframe codegen (3 sites), waypoint extraction, insert-after-anchor, script hoisting - Replace 88 bare any annotations with typed AstNode/AstPath interfaces - Derive RuntimeBridgeControlAction from HyperframeControlAction, alias RuntimePickerElementInfo, share macOS font profiler - Gate generateHyperframesStyles on includeStyles, collapse 4 GSAP property mutation cases into 2 - Extract magic numbers into named constants, replace 5 double casts with type guards and typed accessors (runtime/globals.ts), reduce function complexity in htmlParser and files route |
||
|
|
1c47ba9981 |
refactor(core): route project paths through a single resolveWithinProject chokepoint (#1398)
Structural follow-up to the symlink-escape fix. The recurring miss (#465 fixed isSafePath but left render.ts; the sweep then turned up play.ts, htmlBundler, ...) is because containment was enforced by convention — "remember to call isSafePath after every resolve()" — which a new call site can silently skip. Add resolveWithinProject(base, relativePath) -> string | null (resolve + containment in one call) and route the studio-api + bundler sites through it, so a caller cannot resolve a project-relative path without the guard: - studio-api routes/files.ts (read, rename, duplicate, upload-dir), preview.ts (sub-comp + static asset), render.ts (composition) — all the resolve()+isSafePath() pairs collapse to a single call. - compiler/htmlBundler.ts: its local safePath helper was exactly this; drop it for the shared one. Left intentionally on isSafePath: files.ts upload (resolves a name against a validated sub-dir but contains against the project root) and htmlBundler's CSS @import (resolves against the CSS file's dir, contains against the root) — these resolve and contain against *different* bases, which the single-base chokepoint doesn't model. Exported from @hyperframes/core and re-exported from studio-api/helpers for back-compat. Adds resolveWithinProject unit tests; all existing studio-api route tests pass unchanged (behavior is identical — same resolve, same containment, same reject paths). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5b6c62e151 | fix(studio): surface gesture recording controls (#1390) | ||
|
|
953bab319b |
fix(core): block symlink-based path escape in studio-api isSafePath (#1397)
* fix(core): block symlink-based path escape in studio-api isSafePath path.resolve() collapses ./.. but does not dereference symlinks, so a symlink living inside the project dir but pointing outside it (e.g. project/link -> /etc) passed the prefix check, letting a downstream read/write/stat follow it to a file outside the project root. The `..` traversal case was already blocked; symlink traversal was the gap. Canonicalize both base and target with realpathSync before comparing. The target may not exist yet (new-file writes), so canonicalize the deepest existing ancestor and re-attach the trailing not-yet-existing segments, which cannot be symlinks at check time. Fail closed if base is unresolvable. Adds safePath.test.ts covering: in-base allow, not-yet-existing write target, `..` escape, existing-file-through-symlink escape, write-target under a symlinked parent, file-symlink escape, in-base symlink allow, symlinked-base canonicalization, and base-missing fail-closed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(core,cli): route render + play composition paths through isSafePath Review on #1397 found a third call site with the same vulnerable startsWith pattern. Apply Rule 2: fix every site sharing the contract (gate an attacker-influenced path before a symlink-following fs op). - studio-api routes/render.ts: body.composition (from c.req.json()) was checked with `resolved.startsWith(resolve(project.dir) + sep)`, which doesn't dereference symlinks — an in-project symlink to an external target escaped the project root. Now uses isSafePath(). - cli commands/play.ts: the `/composition/*` server route used `filePath.startsWith(project.dir)` with no trailing-separator guard, so both a sibling dir sharing the prefix (`<dir>-evil`) and symlink escapes passed. Now uses isSafePath() via @hyperframes/core/studio-api (the same lazy-import pattern commands/validate.ts already uses). Tests: render.test.ts gains a "composition path safety" block (in-base allow, `..` reject, in-project-symlink-to-outside reject, in-project symlink staying inside allow). The shared render test adapter now points at a real dir since isSafePath fails closed on an unresolvable base (production project dirs always exist on disk). Not in this change: compiler/htmlBundler.ts has the same class at two sites (safePath helper + inline CSS @import check), but the compiler sits below studio-api in the dependency graph and can't import isSafePath without a backwards edge; that fix needs the helper promoted to a neutral module and is tracked as a follow-up. renderArgs.ts / videoFrameExtractor.ts carry the trailing-sep guard and a local-CLI/engine-internal threat model. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(core): promote isSafePath to a shared module + harden htmlBundler Per review on #1397: extend the symlink-escape fix to the compiler, and remove the duplicated path-safety logic. - Move isSafePath to packages/core/src/safePath.ts (a neutral package-root module). studio-api/helpers/safePath.ts re-exports it for back-compat (keeping walkDir), and it's now exported from the core entrypoint so non-studio-api layers can use it. compiler/ sits below studio-api in the dep graph, so it could not import the helper from its old home without a backwards edge — the promotion removes that constraint. - compiler/htmlBundler.ts: route both containment checks (the safePath helper and the inline CSS @import check) through isSafePath. The bundler reads+inlines these files, so an in-project symlink pointing outside the root would otherwise bake external content into the output. All callers already skip on a null/false result, so nothing is read on rejection. Tests: safePath.test.ts moves with the impl; htmlBundler.test.ts gains a case proving an in-project sub-composition script is inlined while a script reached through an escaping symlink is not (positive control + leak assertion). Deferred (tracked for a dedicated follow-up, see PR thread): the relative()-based isPathInside family (core/compiler/assetPaths, producer/services/fileServer, producer/utils/paths and their callers in the render pipeline) is symlink-blind in the same way, and engine videoFrameExtractor's asset resolver needs a caller-side gate (its http downloads land outside the project root, so a single-root check is wrong). Both are regression-sensitive render-pipeline surfaces that warrant their own focused, well-tested pass. renderArgs.ts is intentionally left: it is filesystem-free by design (injected stat) and its threat model is the user's own --composition CLI arg. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(core): hedge symlink tests for Windows + copy before reverse (review nits) Addresses Via's non-blocking review notes on #1397: - Wrap every symlinkSync in the new tests with a tryCreateSymlink helper that returns false (and the test early-returns) when creation throws, mirroring the preview.test.ts convention. Non-symlink-privileged Windows runners no longer risk crashing the suite on EPERM. - safePath.ts: `[...trailing].reverse()` instead of mutating `trailing` in place — harmless today (single return) but future-proof against a looping edit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
3bcab3dc29 | fix(studio): reject unsafe keyframe values (#1389) | ||
|
|
ab7f69c1f5 |
test(core): align file-tree test with backup-only hiding (#1366) (#1400)
main went red again at
|
||
|
|
e2cc134c77 |
test(core): fix contradictory composition-discovery file-tree test (#1385) (#1399)
#1385 ("exclude dot-directories from composition discovery",
|
||
|
|
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 |
||
|
|
b952dc9ce0 |
fix(core): exclude dot-directories and node_modules from studio composition discovery and lint (#1385)
Projects that vendor tooling assets under dot-directories ended up with every example/preset HTML inside them listed and preview-rendered in the comps sidebar, and the studio Lint badge inflated with findings from files that are not part of the video. walkDir only skipped three exact names (.thumbnails, node_modules, .git), so any other dot-directory (.hyperframes/, .cache/, ...) was walked. Add an isInHiddenOrVendorDir helper that rejects paths with a dot-directory or node_modules segment and apply it to composition discovery and the studio lint route. The file tree is deliberately left unfiltered - this only gates discovery. Fixes #1384 |
||
|
|
aec3c3b58c | fix(studio): journal source writebacks (#1388) | ||
|
|
cee6fd02d6 |
fix(cli): verify browser/ffmpeg binaries exist before render starts (#1365)
## Problem Windows renders commonly fail with environment errors before any real work starts: - `Browser was not found at the configured executablePath (...chrome-headless-shell.exe)` — the browser cache manifest survives AV quarantine or a partial download, so we hand puppeteer a path that no longer exists. - `[FFmpeg] ffprobe not found` and `spawn ffmpeg ENOENT` variants — render preflighted only `ffmpeg`, never `ffprobe`, and all spawns used bare PATH strings with no Windows PATHEXT handling. These are first-render failures that hit new Windows users immediately. ## Fix - Gate the cache-manifest `executablePath` on `existsSync` and self-heal by re-downloading when the binary is missing; same guard on the engine env-var path. - New shared environment preflight (`packages/cli/src/browser/preflight.ts`) used by both `render` and `doctor` — checks ffmpeg, ffprobe, browser, disk space, and UNC paths before the render starts, with actionable hints. - Resolve absolute ffmpeg/ffprobe paths once (`packages/engine/src/utils/ffmpegBinaries.ts`) and pass them to every engine spawn instead of relying on PATH. - Map opaque Windows ffmpeg exit codes to actionable messages. ## Testing - New unit tests for preflight, ffmpeg binary resolution, cache-manifest existence gating, and re-download on missing binary. - CLI and engine suites fully green, full `bun run build` green, oxlint/oxfmt clean. - Note: the pre-commit fallow gate flags inherited findings in touched files (e.g. `audioExtractor.ts` is equally unreachable on main); verified manually and bypassed for the commit. |
||
|
|
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. |
||
|
|
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. |
||
|
|
a0ee97210b |
fix(sdk,core): css tokenizer, override-set replay, setattribute safety, persist errors (#1350)
* fix(sdk,core): css tokenizer, override-set replay, setattribute safety, persist errors * test(sdk,ci): smoke test + explicit sdk-tests CI gate Smoke test covers the full public surface: openComposition → setStyle/setText/dispatch(moveElement) → serialize applyPatches + ORIGIN_APPLY_PATCHES tagging batch() coalescing + transactional rollback on throw undo/redo round-trip persist adapter write + persist:error surfacing T3 embedded mode: override-set apply on open + getOverrides round-trip Adds sdk-tests CI job so SDK coverage is explicitly named and required — prevents a repeat of the demo-next vitest-never-ran incident. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(sdk): export adapter types, awaitable flush(), never-coalesce mode - Export PersistAdapter, PreviewAdapter, PersistVersionEntry from package root — callers can now write typed fakes without reaching into internals - Add flush(): Promise<void> to Composition interface + CompositionImpl — app-close handlers can await a clean drain of the persist queue - coalesceMs <= 0 disables coalescing entirely in createHistory — enables deterministic test scenarios without per-entry timestamp manipulation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(sdk): p2 edge cases — setText no-text-node, override-remove non-existent, flush in smoke - setText on element with no prior text node (firstTextIdx=-1 path) - applyOverrideSet null removal on non-existent prop is a no-op (no throw) - smoke persist test uses comp.flush() instead of setTimeout - can() JSDoc clarifies Phase 3b false-return is intentional feature-detection Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * ci: trigger regression suite * fix(ci): add packages/sdk/package.json to Dockerfile.test workspace copy bun install --frozen-lockfile fails in the regression Docker build because the lockfile references the sdk workspace member but its package.json was not copied into the image before the install step. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
45d4a71ed0 |
feat(core): GSAP-aware split engine for timeline clip splitting (#1330)
* refactor(studio): extract shared timeline components and deduplicate code 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 * feat(core): GSAP-aware split engine for timeline clip splitting Add splitAnimationsInScript to the GSAP parser — correctly re-times animations when a timeline clip is split at an arbitrary position: - Animations before split: kept on original, properties inherited via tl.set inserted before other tweens for correct GSAP state recording - Animations after split: retargeted via AST selector update - Spanning animations: trimmed on original, continuation added for new element with correct position and duration - Keyframes: classified by total per-keyframe duration - Reverse iteration prevents stale animation ID collisions Enhance splitElementInHtml: - CSS rule duplication via PostCSS for ID-based styles - Server-side ID deduplication for repeated splits - Media playback-start adjustment for video/audio Add split-animations route to gsap-mutations endpoint. |
||
|
|
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. |
||
|
|
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). |
||
|
|
96b8d617d8 |
feat(studio): GSAP parser — arc path mutations + keyframe CRUD (#1301)
Add parser-level mutations for arc paths, keyframe add/remove/update, convert-to-keyframes, and _auto flag for 100% keyframes. Wire route handlers for new mutation types. |
||
|
|
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> |
||
|
|
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) |
||
|
|
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> |
||
|
|
1a23938bef |
feat(core): implement createPreviewAdapter (R7, Task 3) (#1291)
* test(core): data-hf-id survives id/selector patch (R1, T7) Locks the preservation guarantee the write-back design depends on: a Studio edit targeting by id or selector (it never sends hfId) must not strip an existing data-hf-id, or the stable handle is destroyed by the next edit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(core): escape hfId in selector + warn on duplicate match (R1, T7 review) Addresses review on #1272 (Miguel P3 + Rames): findTargetElement interpolated target.hfId raw into a [data-hf-id="..."] selector. Escape it (CSS attr-value injection guard) and warn when a hfId matches more than one element instead of silently patching an arbitrary one. Adds an injection-guard test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(core): implement createPreviewAdapter — greens 20 T10 tests (R7, Task 3) elementAtPoint: resolvePoint callback → walk ancestors for data-hf-id, skip data-hf-root without data-hf-id (stage root), skip opacity-0 elements. applyDraft: find element by hfId, record originalTranslate, set --hf-studio-offset-x/y (move) or --hf-studio-width/height (resize), mark data-hf-studio-manual-edit-gesture. revertDraft: remove draft CSS props, clear gesture marker, restore originalTranslate if one was recorded. commitPreview: extract patch (move→moveElement, resize→resize with w/h renamed to width/height), clear gesture marker, return patch or null. getElementTimings: scan [data-hf-id] elements, parse data-start/data-end as floats, return map with undefined fields for absent attributes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(core): remove explicit data-hf-id from htmlParser tests so ensureHfIds mints hf- ids Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
740a83abb3 |
feat(core): hf-id write-back to disk + serve-time surfacing (R7, Tasks 1-2) (#1289)
* feat(core): clip-model hf- ids minted at parse, emitted as data-hf-id (R1) * docs(core): document legacy-id round-trip in clip-model readback (R1 review) Addresses Rames' review on #1270: clarifies that a pre-R1 clip authored with id="my-title" round-trips as data-hf-id="my-title" (non-hf-shaped but stable, exact-match) by design — targeting uses exact [data-hf-id="…"] match and does not require the hf- shape; legacy values re-mint only at the R7 write-back. Not a bug. Comment-only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(core): fix misleading legacy-id migration comment in htmlParser.ts The original comment said legacy data-hf-id values "are re-minted only once the R7 write-back persists freshly-minted ids to source" — which is incorrect. ensureHfIds skips elements that already carry data-hf-id, so legacy values (e.g. data-hf-id="my-title") persist indefinitely and are NOT automatically re-minted. Exact-match targeting still works correctly. Update comment to reflect actual behaviour. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(studio): sourcePatcher data-hf-id targeting (R1, T3) * fix(studio): warn on duplicate match in execDataAttrPattern (R1, T3 review) Addresses Rames' review on #1271: execDataAttrPattern returned the first regex match without checking for a second. A duplicate id/data-hf-id in source (id drift) would silently patch one element and leave the other stale. Now warns when more than one element matches. By the mint contract it should never fire. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(studio): pin hfId-is-authoritative-over-selector contract (R1, T3 review) Adds test: "hfId match is authoritative — selector is not used as a narrowing filter". When hfId matches element A and selector points at element B, findTagByTarget returns A without consulting selector as a narrowing filter. Pins the intended behaviour so a future refactor cannot silently start narrowing by selector. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(core): sourceMutation data-hf-id targeting (R1, T7) * test(core): update htmlParser baselines for R1 hf- id format Elements now get data-hf-id minted by ensureHfIds; parser reads data-hf-id as model id, so HTML id attrs are no longer the model id. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(core): data-hf-id survives id/selector patch (R1, T7) Locks the preservation guarantee the write-back design depends on: a Studio edit targeting by id or selector (it never sends hfId) must not strip an existing data-hf-id, or the stable handle is destroyed by the next edit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(core): escape hfId in selector + warn on duplicate match (R1, T7 review) Addresses review on #1272 (Miguel P3 + Rames): findTargetElement interpolated target.hfId raw into a [data-hf-id="..."] selector. Escape it (CSS attr-value injection guard) and warn when a hfId matches more than one element instead of silently patching an arbitrary one. Adds an injection-guard test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(core): previewAdapter contract failing tests (T10 spec for R7) * feat(core): hf-id write-back to disk + serve-time surfacing (R7, Task 1-2) * test(core): replace tautological stability tests with real disk tests for persistHfIdsIfNeeded Prior tests only exercised normalizeHfIds (pure function) and the existing pin guard in ensureHfIds — both pass on the parent commit without any Task 1 code. Replace with three tests that exercise the actual disk write-back: - writes data-hf-id to disk when source is untagged - does not rewrite disk when source is already tagged (idempotent) - returned id matches id written to disk (serve-time == persist-time invariant) These fail on the parent commit (persistHfIdsIfNeeded doesn't exist) and green after Task 1. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(core): route-level tests for data-hf-id surfacing and disk write-back (R7, Task 1-2) Two integration tests against the preview route (via Hono test harness): - served HTML carries data-hf-id on body elements (>= 2 matches for div+p) - disk file contains data-hf-id after first GET (write-back verified via readFileSync) These fail on the parent commit (no hfIdPersist wiring in preview.ts) and green after Task 1. Closes the verification gap flagged in review. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
72e4f1a0f6 |
test(core): previewAdapter contract failing tests (T10 spec for R7) (#1286)
* feat(core): clip-model hf- ids minted at parse, emitted as data-hf-id (R1) * docs(core): document legacy-id round-trip in clip-model readback (R1 review) Addresses Rames' review on #1270: clarifies that a pre-R1 clip authored with id="my-title" round-trips as data-hf-id="my-title" (non-hf-shaped but stable, exact-match) by design — targeting uses exact [data-hf-id="…"] match and does not require the hf- shape; legacy values re-mint only at the R7 write-back. Not a bug. Comment-only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(core): fix misleading legacy-id migration comment in htmlParser.ts The original comment said legacy data-hf-id values "are re-minted only once the R7 write-back persists freshly-minted ids to source" — which is incorrect. ensureHfIds skips elements that already carry data-hf-id, so legacy values (e.g. data-hf-id="my-title") persist indefinitely and are NOT automatically re-minted. Exact-match targeting still works correctly. Update comment to reflect actual behaviour. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(studio): sourcePatcher data-hf-id targeting (R1, T3) * fix(studio): warn on duplicate match in execDataAttrPattern (R1, T3 review) Addresses Rames' review on #1271: execDataAttrPattern returned the first regex match without checking for a second. A duplicate id/data-hf-id in source (id drift) would silently patch one element and leave the other stale. Now warns when more than one element matches. By the mint contract it should never fire. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(studio): pin hfId-is-authoritative-over-selector contract (R1, T3 review) Adds test: "hfId match is authoritative — selector is not used as a narrowing filter". When hfId matches element A and selector points at element B, findTagByTarget returns A without consulting selector as a narrowing filter. Pins the intended behaviour so a future refactor cannot silently start narrowing by selector. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(core): sourceMutation data-hf-id targeting (R1, T7) * test(core): update htmlParser baselines for R1 hf- id format Elements now get data-hf-id minted by ensureHfIds; parser reads data-hf-id as model id, so HTML id attrs are no longer the model id. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(core): data-hf-id survives id/selector patch (R1, T7) Locks the preservation guarantee the write-back design depends on: a Studio edit targeting by id or selector (it never sends hfId) must not strip an existing data-hf-id, or the stable handle is destroyed by the next edit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(core): escape hfId in selector + warn on duplicate match (R1, T7 review) Addresses review on #1272 (Miguel P3 + Rames): findTargetElement interpolated target.hfId raw into a [data-hf-id="..."] selector. Escape it (CSS attr-value injection guard) and warn when a hfId matches more than one element instead of silently patching an arbitrary one. Adds an injection-guard test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(core): previewAdapter contract failing tests (T10 spec for R7) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
1cd4ad6ab8 |
feat(core): sourceMutation data-hf-id targeting (R1, T7) (#1272)
* feat(core): clip-model hf- ids minted at parse, emitted as data-hf-id (R1) * docs(core): document legacy-id round-trip in clip-model readback (R1 review) Addresses Rames' review on #1270: clarifies that a pre-R1 clip authored with id="my-title" round-trips as data-hf-id="my-title" (non-hf-shaped but stable, exact-match) by design — targeting uses exact [data-hf-id="…"] match and does not require the hf- shape; legacy values re-mint only at the R7 write-back. Not a bug. Comment-only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(core): fix misleading legacy-id migration comment in htmlParser.ts The original comment said legacy data-hf-id values "are re-minted only once the R7 write-back persists freshly-minted ids to source" — which is incorrect. ensureHfIds skips elements that already carry data-hf-id, so legacy values (e.g. data-hf-id="my-title") persist indefinitely and are NOT automatically re-minted. Exact-match targeting still works correctly. Update comment to reflect actual behaviour. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(studio): sourcePatcher data-hf-id targeting (R1, T3) * fix(studio): warn on duplicate match in execDataAttrPattern (R1, T3 review) Addresses Rames' review on #1271: execDataAttrPattern returned the first regex match without checking for a second. A duplicate id/data-hf-id in source (id drift) would silently patch one element and leave the other stale. Now warns when more than one element matches. By the mint contract it should never fire. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(studio): pin hfId-is-authoritative-over-selector contract (R1, T3 review) Adds test: "hfId match is authoritative — selector is not used as a narrowing filter". When hfId matches element A and selector points at element B, findTagByTarget returns A without consulting selector as a narrowing filter. Pins the intended behaviour so a future refactor cannot silently start narrowing by selector. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(core): sourceMutation data-hf-id targeting (R1, T7) * test(core): update htmlParser baselines for R1 hf- id format Elements now get data-hf-id minted by ensureHfIds; parser reads data-hf-id as model id, so HTML id attrs are no longer the model id. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(core): data-hf-id survives id/selector patch (R1, T7) Locks the preservation guarantee the write-back design depends on: a Studio edit targeting by id or selector (it never sends hfId) must not strip an existing data-hf-id, or the stable handle is destroyed by the next edit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(core): escape hfId in selector + warn on duplicate match (R1, T7 review) Addresses review on #1272 (Miguel P3 + Rames): findTargetElement interpolated target.hfId raw into a [data-hf-id="..."] selector. Escape it (CSS attr-value injection guard) and warn when a hfId matches more than one element instead of silently patching an arbitrary one. Adds an injection-guard test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
63fd322df1 |
test(core): address review feedback on T6a / T10 / T7 stubs (#1268)
T6a (#1263): add fromTo corpus script (third parser method) with negative position values to exercise UnaryExpression arm; drop unused breatheRepeats from COMPLEX_SCRIPT; generate fromto.parsed.json + fromto.serialized.js goldens. T10 (#1262): split applyDraft move/resize into two stubs; add applyDraft edge- case describe block (concurrent gestures, idempotent revert, playhead-change stability, nested sub-composition root); add getElementTimings stub for absent data-start/end on a data-hf-id element. T7 (#1267): expand to full parity with T3 — add text-content, attribute, and fallthrough stubs (Core sourceMutation supports all patch types via patchElementInHtml). |
||
|
|
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. |
||
|
|
1fdce71c8b |
test(core): add T10 PreviewAdapter contract stubs (spec for R7) (#1262)
* test(studio): add T5b rotation+motion build-patches characterization Extends manualEditsDomPatches.test.ts with rotation and motion pairs. Same 4-pattern structure: populated, empty, clear restores originals, build/clear symmetry. Merges duplicate manualEditsTypes import block. * test(studio): add T5c review-fix gaps in manualEditsDomPatches characterization Fixes four gaps identified in max-setting code review: - Box-size clear: replace arrayContaining with full ordered toEqual (30 ops) - Box-size / pathOffset / rotation clear: add empty-string coercion tests (origVal||null must produce null, not set property to "") - Rotation clear: add test for absent STUDIO_ORIGINAL_ROTATION_TRANSFORM_ORIGIN_ATTR - Motion clear: prove input-independence by calling with both empty and populated element and asserting identical output * refactor(core): extract maxEndTime+serialize to parsers/test-utils.ts (TU) Deduplicate helpers shared by T1 (htmlParser.roundtrip.test.ts) and T2 (stableIds.test.ts). Both files inline identical implementations; extract to test-utils.ts so future parser tests (T6a…) import one copy. Also fix lefthook fallow command to unset GIT_DIR+GIT_INDEX_FILE before running — those vars are set by git in worktree hook context and block fallow’s internal temp-worktree creation. * test(core): add T10 PreviewAdapter contract stubs (spec for R7) All 14 tests are it.todo, following the T4 pattern. The stubs define the full createPreviewAdapter interface — elementAtPoint (root exclusion, hf-id ancestor walk, opacity filter), applyDraft/revertDraft (draft marker lifecycle), commitPreview (patch derivation), and getElementTimings (data-start/data-end reader). createPreviewAdapter does not exist yet; R7 implements it and converts these stubs to real assertions. |
||
|
|
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 |
||
|
|
20894ab9a3 |
fix: respect user timeouts on low-memory systems (#1221)
Closes #1219 ## Problem On 8GB RAM machines, renders time out at 5% with `Runtime.callFunctionOn timed out` during the duration probe. User-set timeout env vars (`PRODUCER_PUPPETEER_PROTOCOL_TIMEOUT_MS`) are silently ignored by the calibration path, and there are no CLI flags to control timeouts directly. ## Root causes 1. **Calibration timeout cap overrides user settings** — `createCaptureCalibrationConfig` used `Math.min(cfg.protocolTimeout, 30_000)`, meaning even if the user set 300s, calibration still capped at 30s. On slow hardware this causes unnecessary timeouts. 2. **8GB systems get no low-memory treatment** — `getLowMemoryFlags()`, `getGpuMemBudgetMb()`, `memoryAdaptiveCacheLimit()`, and `memoryAdaptiveCacheBytesMb()` all used `< 8192` as the threshold. Systems reporting exactly 8192 MB (common for 8GB machines) fell through to the "plenty of memory" path, getting no Chrome heap reduction or cache limits. 3. **No CLI flags for key timeouts** — Users had to discover the correct env var names (`PRODUCER_PUPPETEER_PROTOCOL_TIMEOUT_MS`, `PRODUCER_PLAYER_READY_TIMEOUT_MS`) by reading source. The non-existent `PUPPETEER_PROTOCOL_TIMEOUT` and `--browser-timeout` were common guesses that did nothing. ## Changes - `captureCost.ts`: `Math.min` → `Math.max` so the 30s calibration default is a floor, not a ceiling. User-set higher timeouts are now respected. - `browserManager.ts`: `>= 8192` → `> 8192` in `getLowMemoryFlags()` and `<= 8192` in `getGpuMemBudgetMb()` so 8GB systems get reduced Chrome heap and GPU memory budget. - `config.ts`: `< 8192` → `<= 8192` in `memoryAdaptiveCacheLimit()` and `memoryAdaptiveCacheBytesMb()` so 8GB systems get reduced frame cache limits. - `render.ts`: Added `--protocol-timeout <ms>` and `--player-ready-timeout <ms>` CLI flags, wired through `resolveConfig` overrides. - Updated calibration tests to match the new floor-not-ceiling behavior. - Added fallow suppressions for pre-existing unused exports in `captureCost.ts`. ## Test plan - [x] Engine config tests pass (`vitest run src/config.test.ts`) - [x] Browser manager tests pass (`vitest run src/services/browserManager.test.ts`) - [x] Calibration safeguard tests pass (4/4 in `renderOrchestrator.test.ts`) - [x] TypeScript compiles cleanly for engine and cli packages - [ ] CI pipeline |
||
|
|
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) |
||
|
|
d1aad77fd7 |
feat(studio): split clip at playhead for media elements [7/10] (#1189)
* 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(studio): add split clip feature with timeline context menu and hotkey Add splitElementInHtml to core source mutation helpers — clones an element at the split time, adjusts data-start/data-duration/data-media-start for both halves, and inserts the clone after the original. Wire through: split-element API endpoint, handleTimelineElementSplit in useTimelineEditing, clip context menu (right-click → Split at Xs), toolbar split button, and S keyboard shortcut. Edge cases: locked/implicit clips blocked, media trim offset adjusted by playback rate, unique ID generation with collision avoidance, undo via edit history. |