Commit Graph
95 Commits
Author SHA1 Message Date
Miguel Ángel 2dfae0c8b2 chore: release v0.7.28 2026-07-03 20:00:49 +00:00
Vance Ingalls 12c399990b chore: release v0.7.27 2026-07-03 11:37:22 -07:00
Miguel Ángel 65b2093396 chore: release v0.7.26 (#1863) 2026-07-02 00:36:07 -07:00
Miguel Ángel 2186d3b72e chore: release v0.7.25 (#1852) 2026-07-01 20:02:43 -07:00
Vance Ingalls 1d4c5d5ec3 chore: release v0.7.24 2026-07-01 14:57:39 -07:00
Vance Ingalls 5149394b00 chore: release v0.7.23 2026-07-01 14:43:16 -07:00
Vance Ingalls 36e01f41d2 chore: release v0.7.22 2026-06-30 14:49:36 -07:00
Vance IngallsandClaude Opus 4.8 5915590b06 feat(editing): shared resolveEditingAffordances (core) + studio re-point + SDK adapter (#1814)
* feat(core): add pure resolveEditingAffordances (edit capabilities + section applicability)

* fix(core): replace prohibited as-cast and !-assertions in isIdentityTransform

* refactor(studio): consume core resolveEditingAffordances; drop duplicated capability + section logic

- affordances.ts: add matrix3d identity-transform branch (was missing, caused test regression)
- domEditingLayers: add domEditSelectionToFacts mapper; resolveDomEditCapabilities is now a thin
  wrapper over core (kept for backward-compat — tests + barrel import it); isTextEditableSelection
  delegates to core sections.text; drop parsePx + isIdentityTransform imports (now in core)
- PropertyPanel: import resolveEditingAffordances + domEditSelectionToFacts; compute sections once;
  replace isMediaElement/isColorGradingCapableElement/timing inline check with sections.*
- propertyPanelMediaSection: delete isMediaElement (no remaining callers)
- propertyPanelColorGradingSection: delete isColorGradingCapableElement (no remaining callers)

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

* feat(sdk): add browser-only resolveElementAffordances adapter over core

* fix(sdk): add position to inlineStyles, replace ! assertion with guard in test

- Add missing 'position' key to inlineStyles in affordances.ts to match computedStyles
- Replace non-null assertion (doc.defaultView!) with proper null guard in test

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

* fix(editing): resolve code-review findings on affordances feature

Max-effort review (8 verified findings) fixes:

Correctness regressions (studio behavior):
- SVG selection crash: dropped `classNames` from EditableElementFacts
  entirely (it was never read by the resolver), which removes the
  `.className.split()` calls that throw on SVGElement (className is an
  SVGAnimatedString, not a string). Masked in tests by happy-dom.
- Timing panel hidden for GSAP-only layers: domEditSelectionToFacts now
  takes animationCount from the caller; PropertyPanel feeds the live
  gsapAnimations prop (selection.gsapAnimations is never populated).

Cleanups:
- Removed dead inline `position` key from SDK adapter (core reads position
  only from computedStyles).
- Added sections-only `resolveEditingSections` export; PropertyPanel uses it
  so panel re-renders no longer re-run the capability geometry parse.
- Declared happy-dom in packages/sdk devDependencies (was root-hoist only).
- Deduped the two capability fact-construction sites behind a shared
  capabilityFacts() helper.
- parsePx now has a single source of truth in core; studio domEditingDom
  re-exports it so the copies can't drift. isIdentityTransform is now
  core-internal (studio's only consumer moved to core in the prior task).

bun.lock also reconciles stale 0.7.17->0.7.21 package versions.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 13:46:32 -07:00
Miguel Angel Simon Sierra 812a5d4246 chore: release v0.7.21 2026-06-29 21:13:39 -07:00
Miguel Angel Simon Sierra faea9f2267 chore: release v0.7.20 2026-06-29 18:34:29 -07:00
Miguel Ángel f3177872a1 chore: release v0.7.19 2026-06-29 21:45:34 +00:00
Miguel Ángel c811a2750a chore: release v0.7.18 2026-06-28 22:55:15 +00:00
Miguel Angel Simon Sierra 3351fb1a6d chore: release v0.7.17 2026-06-27 13:54:58 -04:00
Miguel Ángel fb4d49d248 chore: release v0.7.16 (#1771) 2026-06-27 12:16:46 -04:00
Miguel Ángel a14d342bdb chore: release v0.7.15 2026-06-27 10:00:26 +00:00
Miguel Ángel 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
2026-06-27 00:46:26 -04:00
Miguel Ángel 8f7fb49d16 chore: release v0.7.14 2026-06-27 00:39:16 +00:00
Miguel Ángel e3edbd55cf chore: release v0.7.13 2026-06-26 22:43:22 +00:00
Miguel Ángel 1d3c68ef69 chore: release v0.7.12 2026-06-26 21:49:37 +00:00
James 413d8187fd chore: release v0.7.11 2026-06-26 18:37:23 +00:00
Miguel Ángel 0c1e236dcd chore: release v0.7.10 (#1734) 2026-06-26 00:51:34 -04:00
Miguel Angel Simon Sierra 8db190dd43 chore: release v0.7.9 2026-06-25 21:32:51 -04:00
Miguel Angel Simon Sierra 764aa02a3a chore: release v0.7.8 2026-06-25 18:59:53 -04:00
Miguel Ángel 1494f715fb chore: release v0.7.7 (#1719) 2026-06-25 11:54:10 -04:00
Miguel Ángel eee376cb5e chore: release v0.7.6 (#1707) 2026-06-24 20:17:20 -04:00
Miguel Ángel 649c216394 chore: release v0.7.5 2026-06-24 04:41:56 +00:00
Miguel Ángel ba77a0bd72 chore: release v0.7.4 2026-06-24 00:10:04 +00:00
Miguel Ángel 997823b6b5 chore: release v0.7.3 2026-06-22 23:07:48 -04:00
Miguel Ángel b8d6c4aa21 chore: release v0.7.2 2026-06-22 22:44:19 +00:00
Miguel Ángel 3a6742bd13 chore: release v0.7.1 2026-06-22 14:09:55 -04:00
Miguel Ángel a97f80433f chore: release v0.7.0 2026-06-22 11:23:33 -04:00
Miguel Ángel e385d213f3 chore: release v0.6.121 (#1627) 2026-06-21 17:59:01 -04:00
Miguel Ángel 6c71cf0240 chore: release v0.6.120 (#1624) 2026-06-21 11:17:48 -04:00
Vance Ingalls b33a7457a7 chore: release v0.6.119 (#1620) 2026-06-21 01:03:18 -07:00
Miguel Ángel 0d2198e654 chore: release v0.6.118 2026-06-20 18:24:11 -04:00
Miguel Ángel 12d5273812 chore: release v0.6.117 2026-06-20 18:05:59 -04:00
Miguel Ángel 6b279267cd chore: release v0.6.116 2026-06-20 21:25:34 +00:00
Miguel Ángel d6135ca155 chore: release v0.6.115 2026-06-20 00:19:28 +00:00
Miguel Ángel 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)
2026-06-19 15:37:10 -04:00
Vance Ingalls 4e32c5e0fe chore: release v0.6.114 2026-06-19 04:38:05 -07:00
Vance Ingalls a38fc4e778 chore: release v0.6.113 2026-06-19 00:35:41 -07:00
Vance IngallsandClaude Opus 4.8 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>
2026-06-19 00:22:14 -07:00
Vance IngallsandClaude Opus 4.8 37efbcb955 feat(sdk): image-alpha hit-test phase 1 (WS-G) (#1574)
* feat(sdk): image-alpha hit-test phase 1 (WS-G)

Extends the WS-A1 iframe adapter with image-alpha hit-testing:

- Replace `elementFromPoint` with `elementsFromPoint` (z-stack) so
  a transparent-image hit falls through to the layer behind.
- For `<img>` hits: map client point → natural-pixel coords via a
  pure `mapPointToImagePixel` fn (object-fit cover/contain/fill aware);
  draw to an offscreen canvas once (cached by `currentSrc`); sample
  alpha via pure `alphaIsOpaque`. Transparent pixel → miss, continue
  the stack.
- Cross-origin images that taint the canvas → SecurityError fallback
  → treat pixel as opaque (never drop an unverifiable hit).
- Phase 2 (per-pixel alpha via `drawElement`) NOT built; gated on a
  perf spike per plan.

Tests: alphaIsOpaque thresholds, mapPointToImagePixel (fill/cover/contain
+ out-of-box→null), z-stack fallthrough, taint→opaque fallback, non-image
WS-A1 regression.

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

* fix(sdk): correct image-alpha hit-test edge cases (WS-G review)

- map within the content box, not the border box (object-fit positions
  the image inside border+padding; getBoundingClientRect was off for a
  bordered/padded <img>)
- normalize vertical-first object-position keyword pairs ("bottom left")
- guard natural.width/height===0 in cover/contain (Infinity/NaN scale)
- fall back to elementFromPoint when elementsFromPoint is unavailable
- guard `instanceof win.HTMLImageElement` when the constructor is absent
- bound _imgCanvasCache with a FIFO cap so it can't leak one canvas per src
- drop the redundant taint-probe getImageData; the real pixel read already
  surfaces lazy taint
- one opacity walk per candidate: the hf node is on the already-checked
  ancestor chain, so the resolver no longer re-walks for visibility
- remove dead `fit==="fill"||` clause and unused resolveToken param
- tests: none-fit, object-position keyword/px/reversed-pair, zero-natural,
  no-throw-without-HTMLImageElement + elementsFromPoint fallback

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

* fix(sdk): image-alpha review fixes (WS-G) — transform/taint/cache-key/memory

Addresses the phase-1 gaps flagged in review (was documentation-only):
- CSS rotation/skew on the image or an ancestor now fails safe to opaque instead
  of sampling the wrong pixel (getBoundingClientRect is axis-aligned). Full
  transform-inverse mapping stays phase 2. No-op where DOMMatrix is unavailable.
- Cross-origin canvas taint now warns once per src (was silent) so the
  fall-back-to-opaque path is visible, not "hit-test feels wrong".
- Canvas cache keyed on src + natural dimensions (was src only) so a
  srcset/responsive re-render of the same URL doesn't reuse a stale canvas.
- Pathological-size guard: images above a pixel budget skip alpha-testing
  (opaque) to bound OffscreenCanvas memory.
- Docs: border/padding clicks fall through (intentional) noted on imageAlphaOpaqueAt.

Tests: removed the duplicate a=0 threshold case; added transparent-over-
transparent-over-div fallthrough. iframe.test.ts 59/59. tsc/oxlint/oxfmt green.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 00:12:45 -07:00
Vance IngallsandClaude Opus 4.8 dd6fad6bb2 fix(sdk): code-review follow-ups (WS-B/C/3.C, #1569/#1570/#1572) (#1588)
Addresses the still-outstanding review concerns from merged PRs #1569 / #1570 /
#1572 not already hoisted into #1573.

WS-B (#1569):
- validateVariables requires discriminant fields for object-valued font/image
  ({name,source} / {url}); a {name:42} font or {foo:42} image previously passed
  runtime validation and surfaced as a bogus font-family / missing image.
- Dropped ImageValue's [key:string]:unknown index signature (let any {url}-shaped
  object through, swallowed typos); explicit alt?/fit? instead.
- Documented the OverrideSet widening for SDK consumers.

WS-C (#1570):
- getElementTimings caches parsed GSAP labels by exact script text (avoids a full
  acorn re-parse per read; content-key invalidates on edit).
- Documented end-inclusive label window + best-effort extractGsapLabels catch.

WS-3.C (#1572):
- Added typed Composition.addWithKeyframes / replaceWithKeyframes (was asymmetric
  with addGsapTween; Studio had to use raw dispatch).
- Extracted shared KeyframeSpec type; documented position as seconds/number-only.

Gates: build + core 18/18 + sdk 19/19 + oxlint + oxfmt + fallow + typecheck all green.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 00:07:53 -07:00
Vance IngallsandClaude Opus 4.8 418f198b33 feat(sdk): ws-3c — addWithKeyframes + replaceWithKeyframes SDK ops (acorn writer) (#1572)
* feat(sdk): ws-b variables/brand — object-valued font/image + B1 JSON model

B1 — setVariableValue now drives the runtime JSON model
(data-composition-variables) so preview == render. CSS custom prop is
kept as a secondary compat write for compositions that CSS-bind directly
to --{id}.

B2 — object-valued font ({name, source}) and image ({url}) variable
types added core-to-SDK. Object values write to the JSON model only;
scalars write both model + explicit CSS style patches. Explicit style-path
patches in forward/inverse ensure apply-patches.ts handles each path type
purely (model vs CSS), so inverse patches restore exact pre-call state
without ambiguity.

Changed files:
  packages/core/src/core.types.ts         — font/image to CompositionVariableType + interfaces
  packages/core/src/lint/rules/composition.ts — accept font/image in lint message
  packages/core/src/parsers/htmlParser.ts     — validate font/image variable declarations
  packages/core/src/parsers/htmlParser.test.ts — tests for new variable types
  packages/core/src/runtime/validateVariables.ts — checkType for font/image
  packages/sdk/src/types.ts            — FontValue/ImageValue; widen OverrideSet + EditOp
  packages/sdk/src/index.ts            — re-export FontValue/ImageValue
  packages/sdk/src/session.ts          — widen setVariableValue signature
  packages/sdk/src/engine/patches.ts   — valueChange helper for object-valued patches
  packages/sdk/src/engine/mutate.ts    — handleSetVariableValue: B1+B2 with explicit CSS patches
  packages/sdk/src/engine/apply-patches.ts — variable case: model-only (CSS via explicit patch)
  packages/sdk/src/engine/mutate.test.ts   — B1+B2 round-trip + inverse tests

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

* feat(sdk): ws-c elastic timing + word-alignment resolver (WS-C)

C1: getElementTimings/setElementTiming typed session methods + setHold typed
wrapper. getElementTimings reads data-duration (preferred) or data-end−data-start
(fallback) — same attr-preference as handleSetTiming. setElementTiming dispatches
a sparse map as one batch → one patch event → one undo step. setHold mirrors
setVariableValue pattern.

Also fixes a pre-existing apply-patches.ts gap: the timing/duration patch case was
absent, causing undo of duration changes to silently no-op. Added the duration
branch so inverse patches restore data-duration correctly.

C2: packages/core/src/compiler/timingResolver.ts — shared pure resolveTimings()
consumed by BOTH preview (sdk session) and render (timingCompiler) paths. Word-
anchored elements get enterAt = wordTimings[k].start + offset; elastic hold =
max(0, slotEnd − (enterAt + enterDuration + exitDuration)), clamped ≥ 0; never
timescales animated content. Un-anchored elements keep authored timing (align-on-
adjust). Deterministic + pure: no Date.now, no Math.random, no DOM.

extractGsapLabels() added to gsapParserAcorn.ts to parse tl.addLabel() calls for
the getElementTimings labels field.

Tests: timingResolver.test.ts (10 pure-function tests including preview==render
parity golden test); session.timings.test.ts (15 session-layer tests covering
duration-authored, end-authored, label extraction, batching, undo, and setHold
regression).

Gates: build ✓ · bun test (sdk+core/compiler) 434/434 ✓ · oxlint 0 warnings ✓ ·
oxfmt --check ✓ · fallow --gate new-only ✓ (complexity suppressed on 2 new
inline functions, duplication warn-only pre-existing)

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

* feat(sdk): addElement forward op — mint hf-id, inverse = removeElement (WS-D)

Implements WS-D: the addElement EditOp and session.addElement() typed method.

- types.ts: addElement op (parent/index/html) added to EditOp union;
  addElement(parent, index, html): HfId added to Composition interface
- mutate.ts: handleAddElement inserts a single-root HTML fragment at
  parent+index, minting ids against the LIVE document's existing id set
  (not a fresh fragment set) via collectDocumentHfIds + mintFragmentIds;
  forward = patchAdd, inverse = patchRemove; MutationResult.meta.newId
  carries the minted root id
- mutate.ts: validateOp case rejects missing parent, negative index,
  empty html, zero-element html, and <script> in html
- session.ts: typed addElement(parent, index, html) returns minted id
  via result.meta.newId
- mutate.test.ts: 16 tests covering insert position, append semantics,
  id uniqueness, content-collision rehash, nested fragments, forward/
  inverse symmetry, undo, add/undo/redo stability, parent:null body
  insertion, serialize round-trip, and all five validateOp rejection codes

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

* feat(sdk): ws-3c — addWithKeyframes + replaceWithKeyframes SDK ops (acorn writer)

Port add-with-keyframes / replace-with-keyframes from the server recast path
to the acorn/magic-string writer and expose them as typed SDK EditOps.

- gsapWriterAcorn.ts: extend buildKeyframeObjectCode and
  addAnimationWithKeyframesToScript to accept `auto?: boolean` on keyframes
  (emits `_auto: 1` matching the recast writer)
- types.ts: add `addWithKeyframes` and `replaceWithKeyframes` to EditOp union
- mutate.ts: add handleAddWithKeyframes, handleReplaceWithKeyframes, and
  applyGsapWithKeyframesOp dispatch sub-function; validateOp cases for both
- sdkCutover.ts: add sdkAddWithKeyframesPersist + sdkReplaceWithKeyframesPersist
  (shared via dispatchWithKeyframes to eliminate clone)
- useGsapAnimationOps.ts: wire addWithKeyframes + replaceWithKeyframes
  callbacks with SDK-first / server fallback pattern
- gsapWriter.parity.test.ts: add parity tests for _auto endpoint round-trip and
  replaceWithKeyframes (remove + addWithKeyframes) differential golden harness;
  import removeAnimationFromScript from both writers

Landmine note: tween IDs are position-derived — replaceWithKeyframes removes the
old tween (renumbering survivors) then inserts the replacement at the end; the
MutationResult patch pair restores the whole GSAP script on undo, not a per-ID
inverse, so ID-held references in callers must re-parse after structural edits.

Gate: WS-3.F (retire recast / executeGsapMutation) — NOT started here.
Remaining Studio callers (gsapDragCommit, gsapRuntimeBridge, useGestureCommit,
useEnableKeyframes) remain on the server commitMutation path until WS-3.F.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 23:15:02 -07:00
Vance IngallsandClaude Opus 4.8 0ca01c88cf feat(sdk): addElement forward op — mint hf-id, inverse = removeElement (WS-D) (#1571)
* feat(sdk): ws-c elastic timing + word-alignment resolver (WS-C)

C1: getElementTimings/setElementTiming typed session methods + setHold typed
wrapper. getElementTimings reads data-duration (preferred) or data-end−data-start
(fallback) — same attr-preference as handleSetTiming. setElementTiming dispatches
a sparse map as one batch → one patch event → one undo step. setHold mirrors
setVariableValue pattern.

Also fixes a pre-existing apply-patches.ts gap: the timing/duration patch case was
absent, causing undo of duration changes to silently no-op. Added the duration
branch so inverse patches restore data-duration correctly.

C2: packages/core/src/compiler/timingResolver.ts — shared pure resolveTimings()
consumed by BOTH preview (sdk session) and render (timingCompiler) paths. Word-
anchored elements get enterAt = wordTimings[k].start + offset; elastic hold =
max(0, slotEnd − (enterAt + enterDuration + exitDuration)), clamped ≥ 0; never
timescales animated content. Un-anchored elements keep authored timing (align-on-
adjust). Deterministic + pure: no Date.now, no Math.random, no DOM.

extractGsapLabels() added to gsapParserAcorn.ts to parse tl.addLabel() calls for
the getElementTimings labels field.

Tests: timingResolver.test.ts (10 pure-function tests including preview==render
parity golden test); session.timings.test.ts (15 session-layer tests covering
duration-authored, end-authored, label extraction, batching, undo, and setHold
regression).

Gates: build ✓ · bun test (sdk+core/compiler) 434/434 ✓ · oxlint 0 warnings ✓ ·
oxfmt --check ✓ · fallow --gate new-only ✓ (complexity suppressed on 2 new
inline functions, duplication warn-only pre-existing)

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

* feat(sdk): addElement forward op — mint hf-id, inverse = removeElement (WS-D)

Implements WS-D: the addElement EditOp and session.addElement() typed method.

- types.ts: addElement op (parent/index/html) added to EditOp union;
  addElement(parent, index, html): HfId added to Composition interface
- mutate.ts: handleAddElement inserts a single-root HTML fragment at
  parent+index, minting ids against the LIVE document's existing id set
  (not a fresh fragment set) via collectDocumentHfIds + mintFragmentIds;
  forward = patchAdd, inverse = patchRemove; MutationResult.meta.newId
  carries the minted root id
- mutate.ts: validateOp case rejects missing parent, negative index,
  empty html, zero-element html, and <script> in html
- session.ts: typed addElement(parent, index, html) returns minted id
  via result.meta.newId
- mutate.test.ts: 16 tests covering insert position, append semantics,
  id uniqueness, content-collision rehash, nested fragments, forward/
  inverse symmetry, undo, add/undo/redo stability, parent:null body
  insertion, serialize round-trip, and all five validateOp rejection codes

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 23:05:41 -07:00
Vance IngallsandClaude Opus 4.8 f65e229663 feat(sdk): ws-c elastic timing + word-alignment resolver (WS-C) (#1570)
C1: getElementTimings/setElementTiming typed session methods + setHold typed
wrapper. getElementTimings reads data-duration (preferred) or data-end−data-start
(fallback) — same attr-preference as handleSetTiming. setElementTiming dispatches
a sparse map as one batch → one patch event → one undo step. setHold mirrors
setVariableValue pattern.

Also fixes a pre-existing apply-patches.ts gap: the timing/duration patch case was
absent, causing undo of duration changes to silently no-op. Added the duration
branch so inverse patches restore data-duration correctly.

C2: packages/core/src/compiler/timingResolver.ts — shared pure resolveTimings()
consumed by BOTH preview (sdk session) and render (timingCompiler) paths. Word-
anchored elements get enterAt = wordTimings[k].start + offset; elastic hold =
max(0, slotEnd − (enterAt + enterDuration + exitDuration)), clamped ≥ 0; never
timescales animated content. Un-anchored elements keep authored timing (align-on-
adjust). Deterministic + pure: no Date.now, no Math.random, no DOM.

extractGsapLabels() added to gsapParserAcorn.ts to parse tl.addLabel() calls for
the getElementTimings labels field.

Tests: timingResolver.test.ts (10 pure-function tests including preview==render
parity golden test); session.timings.test.ts (15 session-layer tests covering
duration-authored, end-authored, label extraction, batching, undo, and setHold
regression).

Gates: build ✓ · bun test (sdk+core/compiler) 434/434 ✓ · oxlint 0 warnings ✓ ·
oxfmt --check ✓ · fallow --gate new-only ✓ (complexity suppressed on 2 new
inline functions, duplication warn-only pre-existing)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 23:05:06 -07:00
Vance Ingalls d0e520dbd9 feat(sdk): ws-b variables/brand — object-valued font/image + B1 JSON model (#1569)
## WS-B — variables / brand, object-valued (end-to-end)

Part of the AI Studio (Pacific) SDK integration. **Base of the SDK-hotspot stack** (`main → ws-b → ws-c → ws-d → ws-3c → ws-3f`).

### Problem
The variable system was split-brained: SDK `setVariableValue` wrote a `--{id}` CSS custom prop, while the runtime `getVariables()` read a separate JSON model (`data-composition-variables` / `__hfVariables`). The two never connected, and there was no `--brand-*` convention. Variables were scalar-only.

### What this does
- **B1 — one source of truth.** `setVariableValue` now drives the runtime variable model (`data-composition-variables` / `__hfVariables`), with CSS compatibility emitted as explicit `stylePath`-based patches alongside the model patch. A brand kit is a variables JSON; a batch of `setVariableValue` re-skins in one frame.
- **B2 — object-valued variables.** The `CompositionVariable` union extends from scalar-only to typed objects: `font` (`{name, source}`) and `image` (`{url, …}`), end-to-end (core union → SDK op → runtime merge). Colors stay scalar (per §7 LOCKED decision).

### Implementation notes
CSS compatibility was moved out of `apply-patches.ts` (where it was incorrectly writing CSS props as a side-effect of model patches, breaking inverse/undo) and into explicit patches emitted in `mutate.ts`. Forward emits `[modelPatch, cssPatch]` for scalars; inverse correctly generates `patchRemove` for the CSS prop when there was no prior CSS prop. Font/image variables never become CSS props.

### Files (12 changed, +441 −32)
- `packages/core`: `core.types.ts`, `lint/rules/composition.ts`, `parsers/htmlParser.ts` (+test), `runtime/validateVariables.ts`
- `packages/sdk`: `engine/mutate.ts` (+test), `engine/apply-patches.ts`, `engine/patches.ts`, `index.ts`, `types.ts`

### Gates
- `bun run build` 
- `bun test` SDK 304/0  · `validateVariables.test.ts` 13/0 
- `bunx oxlint` 0/0  · `bunx oxfmt --check` 
- `fallow audit --gate new-only`  (complexity inherited only)

> The +8 new `htmlParser.test.ts` font/image tests fail under the pre-existing `DOMParser is not defined` happy-dom limitation (main already carries 425 such failures) — not a logic bug; the pure runtime logic is covered by `validateVariables.test.ts`.

### Deferred
Brand-kit picker UI and `batch(setVariableValue × N)` wiring are Pacific-side; per-composition variable scoping beyond `__hfVariablesByComp`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-06-18 23:03:52 -07:00
Vance Ingalls 1bab79ef4c chore: release v0.6.112 2026-06-18 01:14:30 -07:00
Vance IngallsandClaude Opus 4.8 28bfe09f21 feat(sdk): ws-3 — reorderElements op (batch z-index update) (#1502)
* feat(sdk): ws-3 — reorderElements op (batch z-index update)

Adds the reorderElements EditOp: each entry sets inline zIndex on one element.
Last-write-wins per target so a duplicated target collapses to a single zIndex
patch. Positioning is unchanged — z-index only takes effect on non-static
elements, so the caller must ensure the target is positioned.

Also fixes single-dispatch undo to reverse the inverse patch list (parity with
batch()): an op emitting multiple patches whose undo order matters — a duplicated
reorderElements target, an aliased multi-target, or a nested parent+child
removeElement — must undo in reverse application order, or undo lands on an
intermediate value / drops a subtree.

validateOp resolves every entry target (E_TARGET_NOT_FOUND for unknown ids;
empty entries is a clean no-op). Tests cover set/inverse/validate/duplicate-target.

Rebuilt standalone on main (reorderElements only depends on handleSetStyle).

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

* refactor(sdk): remove unused HTTP persist adapter

The HTTP PersistAdapter (createHttpAdapter) was dead weight after the studio
cutover went single-writer (ws-4): Studio's writeProjectFile is the sole writer
and useSdkSession opens with no persist queue, so the adapter's write/flush/
listVersions/loadFrom were never used — only read() was, to fetch the
composition source. Replace those two read() calls with a direct optional fetch
(GET /files/<path>?optional=1) and drop the adapter + its export-map entries.

Saved for later re-introduction (when a non-Studio SDK host needs server-backed
persist) at docs/hyperframes/plans/sdk-http-adapter/ (outside the repo).

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

* feat(studio): resolver shadow for z-index reorder targets

The z-index reorder commit takes the server path (no SDK persist), but the
resolver-shadow tripwire is decoupled from cutover — so it should still record
whether the SDK resolves each reordered element (reorderElements' targets), the
same as timing/delete already do before their cutover gate. This gives wild
resolver-parity telemetry on z-index targets before z-index reorder is cut over.

Threads an onReorderShadow callback (sdkSession-bound, mirrors onTrySdkDelete)
from useDomEditSession → useDomEditCommits → useElementLifecycleOps, called with
the reordered elements' hf-ids in handleDomZIndexReorderCommit. Read-only,
divergence-only, never throws — same contract as recordResolverParity elsewhere.

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

* fix(studio): guard project-file read path against traversal (CodeQL CSRF)

readProjectFileOptional interpolated a user-influenced composition path into the
fetch URL, which CodeQL flagged as client-side request forgery. Reject NUL/`..`
up front (mirrors the existing guard in timelineEditingHelpers) and
encodeURIComponent the projectId too, so both values stay confined to single
segments of the same-origin URL. Unsafe path → undefined (graceful for the
optional read).

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 23:51:47 -07:00