Merge pull request #2108 from heygen-com/docs/sdk-attachsync-and-variable-crud

docs(sdk): document attachSync, variable CRUD, and getRootElements/getAllAnimationIds
This commit is contained in:
Vance Ingalls
2026-07-09 11:59:28 -07:00
committed by GitHub
6 changed files with 300 additions and 1 deletions
+17
View File
@@ -32,6 +32,23 @@ The callback references `comp` before it is declared — that is intentional and
The `dispatch` callback is optional. Omitting it means `commitPreview()` is a no-op, which is useful if you want to handle op derivation yourself.
## Keeping the preview in sync
Everything above wires up hit-testing and drag — but the iframe still won't reflect edits made any other way (an inspector panel calling `comp.setStyle()` directly, an undo, a collaborator's change replayed via `applyPatches()`). `attachSync` closes that gap: call it once you have both `preview` and `comp`, and every future edit — including undo/redo — mirrors onto the live iframe automatically.
```typescript
const detach = preview.attachSync(comp);
// later, when the editor unmounts or swaps compositions:
detach();
```
`attachSync` does an immediate full sync of `comp`'s current state first (so re-opening a composition with existing overrides isn't a blank iframe), then subscribes to the same `patch` event your other listeners use. You don't need to write your own mirroring code, and you don't need a separate mechanism for undo/redo — both flow through the same subscription. Script-tag edits (GSAP script rewrites) are the one thing it never mirrors, since replaying a live `<script>` tag doesn't re-execute it.
<Note>
Calling `attachSync` again with a different `comp` detaches the previous subscription first — useful if your editor swaps which composition an iframe is bound to without remounting it.
</Note>
## Hit-testing: finding what the user clicked
`preview.elementAtPoint(x, y)` performs a synchronous hit-test at coordinates in the iframe's own coordinate space and returns the nearest `[data-hf-id]` element, or `null` for a transparent hit.
+16
View File
@@ -92,6 +92,7 @@ interface PreviewAdapter {
cancelPreview(): void;
select(ids: string[], opts?: { additive?: boolean }): void;
on(event: "selection", handler: (ids: string[]) => void): () => void;
attachSync(comp: Composition): () => void;
}
```
@@ -119,6 +120,21 @@ interface PreviewAdapter {
Fired when the preview host changes the selection (for example, the user clicks an element). Returns an unsubscribe function. In the current release, callers listen to the session's own `selectionchange` event instead — this hook is wired in a future stage.
</ParamField>
<ParamField path="attachSync" type="(comp: Composition) => () => void">
Mirrors a composition's edits onto the adapter's own live document: an immediate full sync of the composition's current overrides, then a subscription that replays every future `patch` event — including undo/redo, since both fire through the same event with forward or inverse patches. Calling `attachSync` again while already attached detaches the previous subscription first. Returns an unsubscribe function.
Script-tag patches (`/script/gsap` and any future `/script/*` path) are never mirrored — rewriting a live `<script>` tag's content doesn't re-execute it, and re-running GSAP setup from scratch would conflict with running timeline state. Every other patch kind (style, text, attribute, timing, hold, element add/remove, stylesheet, variable value, variable declaration) mirrors as-is.
```typescript
const adapter = createIframePreviewAdapter(iframe, dispatch);
const comp = await openComposition(html, { preview: adapter });
const detach = adapter.attachSync(comp);
// later, if the host tears down the preview:
detach();
```
</ParamField>
### ElementAtPointResult
```typescript
+83
View File
@@ -110,6 +110,65 @@ comp.setVariableValue("brandFont", { name: "Inter", source: "https://fonts.googl
comp.setVariableValue("heroImage", { url: "/assets/hero.jpg", fit: "cover" });
```
<Note>
`setVariableValue` only updates a variable's current value — it refuses to create an undeclared variable. Use `declareVariable` to create one.
</Note>
### `getVariableValue`
```typescript
getVariableValue(id: string): string | number | boolean | FontValue | ImageValue | undefined
```
Return a declared variable's current `default` value, or `undefined` if the id is undeclared or has no value set.
```typescript
const color = comp.getVariableValue("brandColor"); // "#6C5CE7"
```
### `listVariables`
```typescript
listVariables(): CompositionVariable[]
```
Return every declared variable's full schema — `id`, `type`, `label`, `default`, and any type-specific fields (`min`/`max`/`step` for numbers, `options` for enums, `source` for fonts, etc). Returns `[]` when the composition declares no variables.
```typescript
for (const v of comp.listVariables()) {
console.log(v.id, v.type, v.default);
}
```
### `declareVariable`
```typescript
declareVariable(decl: CompositionVariable): void
```
Create a new variable declaration, or fully replace an existing one (type, label, default, and all other schema fields — not just the value). This is the only path that creates the schema entry from scratch; `setVariableValue` intentionally refuses to.
```typescript
comp.declareVariable({
id: "brandColor",
type: "color",
label: "Brand color",
default: "#6C5CE7",
});
```
### `removeVariable`
```typescript
removeVariable(id: string): void
```
Remove a variable's declaration entirely. Any live `var.{id}` overrides and `data-var-*` DOM references are left untouched — this only removes the schema entry.
```typescript
comp.removeVariable("brandColor");
```
### `getElementTimings`
```typescript
@@ -300,6 +359,18 @@ const images = elements.filter((el) => el.tag === "img");
For elements inside inlined sub-compositions, use `scopedId` (e.g. `"hf-host/hf-leaf"`) as the dispatch target, not `id`. Top-level elements have `scopedId === id`.
</Note>
### `getRootElements`
```typescript
getRootElements(): ElementSnapshot[]
```
Return only top-level elements, each carrying its full subtree — no id appears twice (unlike `getElements`, which flattens every nested element into the same array). Use this when you need one entry per top-level clip rather than every element in the tree.
```typescript
const roots = comp.getRootElements();
```
### `getElement`
```typescript
@@ -337,6 +408,18 @@ const allImages = comp.find({ tag: "img" });
const track1Ids = comp.find({ track: 1 });
```
### `getAllAnimationIds`
```typescript
getAllAnimationIds(): Set<string>
```
Return every GSAP tween id parsed from the composition's script, regardless of whether its target selector currently matches a live DOM element. This differs from a given `ElementSnapshot`'s own `animationIds` field, which only lists tweens whose selector resolves to that specific element.
```typescript
const allIds = comp.getAllAnimationIds();
```
---
## Selection
+17 -1
View File
@@ -263,7 +263,9 @@ comp.dispatch({
| `type` | Key fields | What it does |
|--------|-----------|--------------|
| `setVariableValue` | `id`, `value` | Sets a composition variable by id. Value may be a string, number, boolean, `FontValue`, or `ImageValue`. |
| `setVariableValue` | `id`, `value` | Sets a composition variable's current value by id. Value may be a string, number, boolean, `FontValue`, or `ImageValue`. Refuses to create an undeclared variable. |
| `declareVariable` | `decl` | Creates a new variable declaration, or fully replaces an existing one (type, label, default, and all other schema fields — not just the value). The only op that creates the schema entry from scratch. |
| `removeVariable` | `id` | Removes a variable's declaration entirely. Live `var.{id}` overrides and `data-var-*` DOM references are left untouched. |
```typescript
// Scalar variable
@@ -293,6 +295,20 @@ comp.dispatch({
fit: "cover",
},
});
// Create a new variable declaration
comp.dispatch({
type: "declareVariable",
decl: {
id: "brandColor",
type: "color",
label: "Brand color",
default: "#6C5CE7",
},
});
// Remove a variable's declaration
comp.dispatch({ type: "removeVariable", id: "brandColor" });
```
---
+101
View File
@@ -350,6 +350,105 @@ interface ElasticHold {
---
## CompositionVariable
The full schema for one declared variable — used by `comp.declareVariable()`, returned from `comp.listVariables()`. A closed union discriminated on `type`; every variant shares the same base fields plus its own type-specific ones.
```typescript
interface CompositionVariableBase {
id: string;
type: "string" | "number" | "color" | "boolean" | "enum" | "font" | "image";
label: string;
description?: string;
}
interface StringVariable extends CompositionVariableBase {
type: "string";
default: string;
placeholder?: string;
maxLength?: number;
}
interface NumberVariable extends CompositionVariableBase {
type: "number";
default: number;
min?: number;
max?: number;
step?: number;
unit?: string;
}
interface ColorVariable extends CompositionVariableBase {
type: "color";
default: string;
brandRole?: string;
}
interface BooleanVariable extends CompositionVariableBase {
type: "boolean";
default: boolean;
}
interface EnumVariable extends CompositionVariableBase {
type: "enum";
default: string;
options: { value: string; label: string }[];
}
interface FontVariable extends CompositionVariableBase {
type: "font";
default: string;
source?: string;
default_name?: string;
default_source?: string;
}
interface ImageVariable extends CompositionVariableBase {
type: "image";
default: string;
brandRole?: string;
}
type CompositionVariable =
| StringVariable
| NumberVariable
| ColorVariable
| BooleanVariable
| EnumVariable
| FontVariable
| ImageVariable;
```
<ResponseField name="id" type="string">
Stable identifier, referenced by `setVariableValue`, `getVariableValue`, `removeVariable`, and `var.{id}` override-set keys.
</ResponseField>
<ResponseField name="type" type="string">
Discriminant. Determines which variant's extra fields apply and what shape `default` takes.
</ResponseField>
<ResponseField name="label" type="string">
Human-readable name for a variables panel UI.
</ResponseField>
<ResponseField name="default" type="string | number | boolean">
The variable's fallback value. `font` and `image` variants store their fallback as a plain string here (a font-family name / image URL) — the richer `FontValue`/`ImageValue` object shapes are only used as `setVariableValue`'s runtime argument, not as `default`'s stored type.
</ResponseField>
<ResponseField name="brandRole" type="string (color, image)">
Optional semantic label for brand-system tooling, e.g. `"color:primary"` or `"logo:primary"`.
</ResponseField>
<ResponseField name="options" type="{ value: string; label: string }[] (enum only)">
The selectable choices for an `enum` variable.
</ResponseField>
<ResponseField name="source" type="string (font only)">
Font stylesheet URL (e.g. a Google Fonts CSS link) paired with the `default` font-family name.
</ResponseField>
---
## FontValue
Object value for a `font` composition variable. Always an object — never a raw CSS string.
@@ -619,6 +718,8 @@ type EditOp =
| { type: "setClassStyle"; selector: string; styles: Record<string, string | null> }
| { type: "setCompositionMetadata"; width?: number; height?: number; duration?: number }
| { type: "setVariableValue"; id: string; value: string | number | boolean | FontValue | ImageValue }
| { type: "declareVariable"; decl: CompositionVariable }
| { type: "removeVariable"; id: string }
| { type: "addGsapTween"; target: HfId; tween: GsapTweenSpec }
| { type: "setGsapTween"; animationId: string; properties: Partial<GsapTweenSpec> }
| { type: "removeGsapTween"; animationId: string }
+66
View File
@@ -230,6 +230,72 @@ const images = all.filter((el) => el.tag === "img");
---
## Id & Scope Utilities
```typescript
import { resolveScoped, findById, bareId, escapeHfId, isNewHostBoundary } from "@hyperframes/sdk";
```
Low-level id-resolution helpers used internally by dispatch, patch replay, and query — exposed for hosts building their own DOM-facing tooling against the same document model.
### resolveScoped
```typescript
function resolveScoped(document: Document, id: string): Element | null;
```
Resolve an `HfId` — bare (`"hf-title"`) or scoped (`"hf-host/hf-leaf"`) — to its `Element`. For an ambiguous bare id (the same id appearing both top-level and inside a sub-composition), prefers the canonical top-level match, matching `getElement()`'s own preference. This is the single resolution rule every mutation and patch-replay path shares — using your own `querySelector` instead can silently target the wrong duplicate.
### findById
```typescript
function findById(document: Document, id: string): Element | null;
```
Thin alias for `resolveScoped` kept for call-site clarity where "find" reads better than "resolve." Identical behavior.
### bareId
```typescript
function bareId(scopedId: string): string;
```
Strip a scoped id down to its leaf segment: `bareId("hf-host/hf-leaf")` returns `"hf-leaf"`. A no-op on an already-bare id.
### escapeHfId
```typescript
function escapeHfId(id: string): string;
```
Escape an id for safe interpolation into a `querySelectorAll` attribute-value selector (backslashes and double quotes). Use this if you're writing a raw `[data-hf-id="..."]` selector yourself instead of going through `resolveScoped`/`findById`.
### isNewHostBoundary
```typescript
function isNewHostBoundary(el: Element): boolean;
```
Returns `true` when `el` is the root of an inlined sub-composition — that is, it carries a `data-composition-file` attribute whose value differs from its parent's (or the parent has none). Use this to detect "entering a new sub-composition" while walking the tree, without hardcoding the attribute name.
---
## Variable Utilities
```typescript
import { readVariableDefault } from "@hyperframes/sdk";
```
### readVariableDefault
```typescript
function readVariableDefault(document: Document, id: string): unknown;
```
Read a declared variable's current `default` value directly from the document's `data-composition-variables` schema attribute, bypassing the session layer. This is the same function `comp.getVariableValue()` calls internally; prefer the typed `Composition` method in session code — use this only when you're working against a raw `Document` outside of an open session (matching `buildDocument`/`buildRoots`'s "same functions the SDK uses internally" pattern above).
---
## Constants
### ORIGIN\_APPLY\_PATCHES