A `<br>` is a void element but `resolveSingleChildTextTarget` treated a lone
`<br>` child as the element's text target. So `getOwnText(<h1>A<br>B</h1>)`
read the `<br>`'s (empty) textContent → `text: null`, which consumers surface
as "not editable", and `setOwnText` wrote into the `<br>`, corrupting it
(serialized as invalid `</br>`).
Exclude void elements from the single-child text target; read `<br>` as "\n";
and rebuild the text/`<br>` run from the newline-separated value on write,
reusing existing `<br>` nodes so their identity (data-hf-id) survives an
in-place edit.
- Script-mirror filter changed from an exact "/script/gsap" match to
path.startsWith("/script/") — the documented contract is "never mirror
script-tag rewrites," not just today's one known path; startsWith covers
any future script-kind patch under the same intent.
- _syncDetach is now cleared when the caller invokes the returned detach
function directly, not only on the next attachSync call — avoids holding
a stale (already-unsubscribed) reference between an explicit detach() and
a later attachSync(other).
- The initial applyOverrideSet call is now wrapped in try/catch: a bad
initial snapshot no longer prevents the ongoing patch subscription from
attaching, matching the SDK's existing swallow-and-warn precedent for
silent-failure paths (adapters/iframe.ts's tainted-canvas warning).
- Added a test proving declareVariable/removeVariable (the /variable-decls/
patches PR #2098 introduces) mirror onto the live document's
data-composition-variables attribute — the existing suite only covered
setVariableValue's CSS-custom-property path, not the schema-metadata path.
setVariableValue is the headline case the sync spec was built for and
had no coverage; setTiming (data-start/data-end mirroring) was also
untested. Both regression-checked by temporarily breaking the
underlying mutate/apply-patches code paths and confirming the new
assertions fail.
Adds attachSync(comp) to PreviewAdapter/IframePreviewAdapter — does an
immediate full sync via the existing applyOverrideSet, then subscribes to
comp.on('patch', ...) and replays every future patch (forward or inverse —
undo/redo included) via the existing applyPatchesToDocument, pointed at the
iframe's live document instead of the offscreen linkedom one. No new
mutation logic; both functions already work against any
{document, wrapped, stamped}-shaped object.
Also adds a no-op attachSync stub to HeadlessPreviewAdapter, required to
keep it satisfying the widened PreviewAdapter interface.
Closes the gap that made pacific's canvas-react hand-roll its own
override-application code (applyOverrideToIframe.ts) with two separate
mechanisms (diffing for normal edits, verbatim op-replay for undo/redo) —
subscribing to the patch stream directly needs only one.
- validateOp now handles declareVariable/removeVariable (E_NO_ROOT when no
composition root), matching setVariableValue's existing case — previously
comp.can() returned E_UNKNOWN_OP for both.
- removeVariable's undo-inverse now tags its {decl, index} reinsert payload
with __kind: "reinsert" instead of relying on structural "decl"/"index"
key presence to disambiguate it from a plain declareVariable patch.
VariableDecl has an open index signature, so a real variable schema could
legally declare its own "decl"/"index" fields and be misinterpreted by the
old structural check; a regression test pins the exact collision.
- getVariableValue's return type tightened from `unknown` to
`string | number | boolean | FontValue | ImageValue | undefined`, matching
setVariableValue's parameter type for round-trip symmetry. The underlying
unknown-typed read is cast once at this SDK boundary.
- Added a redo test for declareVariable/removeVariable (existing tests only
covered undo).
Closes the remaining Tier 2/3 gaps from the SDK surface audit that motivated
#2092 — real, contained fixes short of the two genuinely architectural items
(a live-DOM apply adapter, structural editing ops) that need their own design
pass, not a quick patch.
Variable CRUD was write-only and creation-blocked: setVariableValue existed,
but there was no getVariableValue, listVariables, declareVariable, or
removeVariable — and writeVariableDefault intentionally refuses to create an
undeclared variable ("keep the schema authoritative"), so a variables panel
(list what exists, read current values, let someone add one) could not be
built against the SDK at all.
- getVariableValue(id) / listVariables(): thin reads over the existing
readVariableDefault / a new listVariableDecls.
- declareVariable(decl) / removeVariable(id): new EditOps with full
undo/redo support via a new patch path (/variable-decls/{id}, distinct
from /variables/{id} which is default-only) — removeVariable's inverse
bundles the original array index so undo reinserts at the same position
instead of appending, mirroring handleRemoveElement's siblingIndex.
Export gaps (same shape as #2092's fixes — the logic already existed,
just wasn't reachable): resolveScoped, findById, escapeHfId from
engine/model.ts; readVariableDefault from engine/variableModel.ts.
17 new tests across mutate.test.ts (declareVariable/removeVariable engine
semantics + undo), session.test.ts (Composition-level API), and smoke.test.ts
(export-surface import check). 439/439 sdk tests passing. Full workspace
build (incl. studio) verified clean.