chore: import upstream snapshot with attribution
regression / regression-shards (style-16-prod style-9-prod style-17-prod iframe-render-compat variables-prod mp4-h265-sdr, shard-4) (push) Has been cancelled
regression / regression-shards (style-4-prod style-11-prod style-2-prod animejs-adapter typegpu-adapter parallel-capture-regression, shard-5) (push) Has been cancelled
regression / regression-shards (style-7-prod style-8-prod style-10-prod css-spinner-render-compat webm-transparency mp4-h264-sdr webm-vp9, shard-3) (push) Has been cancelled
regression / regression-shards (sub-composition-video style-18-prod raf-ball-render-compat font-variant-numeric sub-comp-t0 sub-comp-id-selector, shard-7) (push) Has been cancelled
Windows render verification / Detect changes (push) Has been cancelled
Windows render verification / Preflight (lint + format) (push) Has been cancelled
Windows render verification / Render on windows-latest (push) Has been cancelled
Windows render verification / Tests on windows-latest (push) Has been cancelled
CI / Detect changes (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Fallow audit (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Producer: integration tests (push) Has been cancelled
CI / Producer: unit tests (push) Has been cancelled
CI / File size check (push) Has been cancelled
CI / Test: skills (push) Has been cancelled
CI / Skills: manifest in sync (push) Has been cancelled
CI / CLI: npx shim (macos-latest) (push) Has been cancelled
CI / CLI: npx shim (ubuntu-latest) (push) Has been cancelled
CI / CLI: npx shim (windows-latest) (push) Has been cancelled
CI / SDK: unit + contract + smoke (push) Has been cancelled
CI / Test: runtime contract (push) Has been cancelled
CI / Studio: load smoke (push) Has been cancelled
CI / Smoke: global install (push) Has been cancelled
CI / CLI smoke (required) (push) Has been cancelled
CI / Semantic PR title (push) Has been cancelled
Player perf / Detect changes (push) Has been cancelled
Player perf / Preflight (lint + format) (push) Has been cancelled
Player perf / player-perf (push) Has been cancelled
Player perf / Perf: drift (push) Has been cancelled
Player perf / Perf: fps (push) Has been cancelled
Player perf / Perf: parity (push) Has been cancelled
Player perf / Perf: scrub (push) Has been cancelled
Player perf / Perf: load (push) Has been cancelled
preview-regression / Detect changes (push) Has been cancelled
preview-regression / Preflight (lint + format) (push) Has been cancelled
preview-regression / Preview parity (push) Has been cancelled
preview-regression / preview-regression (push) Has been cancelled
regression / regression (push) Has been cancelled
regression / Detect changes (push) Has been cancelled
regression / Preflight (lint + format) (push) Has been cancelled
regression / regression-shards (hdr-regression style-5-prod style-3-prod mov-prores, shard-1) (push) Has been cancelled
regression / regression-shards (overlay-montage-prod style-12-prod chat missing-host-comp-id png-sequence portrait-edge-bleed, shard-6) (push) Has been cancelled
regression / regression-shards (style-13-prod style-6-prod vignelli-stacking gsap-letters-render-compat audio-mux-parity, shard-8) (push) Has been cancelled
regression / regression-shards (style-15-prod hdr-hlg-regression style-1-prod many-cuts vfr-screen-recording render-symlinked-assets, shard-2) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Docs / Validate docs (push) Has been cancelled
Sync skills to ClawHub / Publish changed skills (push) Has been cancelled
regression / regression-shards (style-16-prod style-9-prod style-17-prod iframe-render-compat variables-prod mp4-h265-sdr, shard-4) (push) Has been cancelled
regression / regression-shards (style-4-prod style-11-prod style-2-prod animejs-adapter typegpu-adapter parallel-capture-regression, shard-5) (push) Has been cancelled
regression / regression-shards (style-7-prod style-8-prod style-10-prod css-spinner-render-compat webm-transparency mp4-h264-sdr webm-vp9, shard-3) (push) Has been cancelled
regression / regression-shards (sub-composition-video style-18-prod raf-ball-render-compat font-variant-numeric sub-comp-t0 sub-comp-id-selector, shard-7) (push) Has been cancelled
Windows render verification / Detect changes (push) Has been cancelled
Windows render verification / Preflight (lint + format) (push) Has been cancelled
Windows render verification / Render on windows-latest (push) Has been cancelled
Windows render verification / Tests on windows-latest (push) Has been cancelled
CI / Detect changes (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Fallow audit (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Producer: integration tests (push) Has been cancelled
CI / Producer: unit tests (push) Has been cancelled
CI / File size check (push) Has been cancelled
CI / Test: skills (push) Has been cancelled
CI / Skills: manifest in sync (push) Has been cancelled
CI / CLI: npx shim (macos-latest) (push) Has been cancelled
CI / CLI: npx shim (ubuntu-latest) (push) Has been cancelled
CI / CLI: npx shim (windows-latest) (push) Has been cancelled
CI / SDK: unit + contract + smoke (push) Has been cancelled
CI / Test: runtime contract (push) Has been cancelled
CI / Studio: load smoke (push) Has been cancelled
CI / Smoke: global install (push) Has been cancelled
CI / CLI smoke (required) (push) Has been cancelled
CI / Semantic PR title (push) Has been cancelled
Player perf / Detect changes (push) Has been cancelled
Player perf / Preflight (lint + format) (push) Has been cancelled
Player perf / player-perf (push) Has been cancelled
Player perf / Perf: drift (push) Has been cancelled
Player perf / Perf: fps (push) Has been cancelled
Player perf / Perf: parity (push) Has been cancelled
Player perf / Perf: scrub (push) Has been cancelled
Player perf / Perf: load (push) Has been cancelled
preview-regression / Detect changes (push) Has been cancelled
preview-regression / Preflight (lint + format) (push) Has been cancelled
preview-regression / Preview parity (push) Has been cancelled
preview-regression / preview-regression (push) Has been cancelled
regression / regression (push) Has been cancelled
regression / Detect changes (push) Has been cancelled
regression / Preflight (lint + format) (push) Has been cancelled
regression / regression-shards (hdr-regression style-5-prod style-3-prod mov-prores, shard-1) (push) Has been cancelled
regression / regression-shards (overlay-montage-prod style-12-prod chat missing-host-comp-id png-sequence portrait-edge-bleed, shard-6) (push) Has been cancelled
regression / regression-shards (style-13-prod style-6-prod vignelli-stacking gsap-letters-render-compat audio-mux-parity, shard-8) (push) Has been cancelled
regression / regression-shards (style-15-prod hdr-hlg-regression style-1-prod many-cuts vfr-screen-recording render-symlinked-assets, shard-2) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Docs / Validate docs (push) Has been cancelled
Sync skills to ClawHub / Publish changed skills (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
---
|
||||
title: "Canvas & Preview Integration"
|
||||
description: "Connect a same-origin composition iframe to the SDK for hit-testing, draft preview, and selection."
|
||||
---
|
||||
|
||||
The SDK's `PreviewAdapter` interface decouples the editing model from the visual surface. For browser-based editors, `createIframePreviewAdapter` bridges the SDK to a same-origin `<iframe>` containing the composition, giving you synchronous hit-testing, 60fps drag preview, and selection management — all without touching the model until the user commits.
|
||||
|
||||
<Note>
|
||||
The iframe must be same-origin (e.g. `srcdoc` or a `blob:` URL). Cross-origin iframe access throws a `DOMException`; the adapter does not guard this, so enforcing same-origin is the caller's responsibility.
|
||||
</Note>
|
||||
|
||||
## Embedding the composition
|
||||
|
||||
Render the composition HTML into a same-origin `<iframe>` in your editor shell, then pass that element plus a dispatch callback to `createIframePreviewAdapter`:
|
||||
|
||||
```typescript
|
||||
import { openComposition, createIframePreviewAdapter } from "@hyperframes/sdk";
|
||||
|
||||
// Assume `compositionHtml` is the composition's source HTML string.
|
||||
const iframe = document.querySelector<HTMLIFrameElement>("#composition-frame")!;
|
||||
|
||||
// Build the adapter first so you can pass it to openComposition.
|
||||
// The dispatch callback is called by commitPreview() after a drag completes.
|
||||
const preview = createIframePreviewAdapter(iframe, (op) => {
|
||||
comp.dispatch(op);
|
||||
});
|
||||
|
||||
const comp = await openComposition(compositionHtml, { preview });
|
||||
```
|
||||
|
||||
The callback references `comp` before it is declared — that is intentional and safe: the arrow function captures `comp` by closure and is only ever invoked later (by `commitPreview()` on pointer-up), by which point `comp` is assigned. This is the standard way to break the adapter ⇄ session circular dependency.
|
||||
|
||||
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.
|
||||
|
||||
```typescript
|
||||
iframe.addEventListener("click", (e) => {
|
||||
// e.clientX / e.clientY are in the outer frame's space.
|
||||
// If the iframe is positioned, convert to iframe-local coords.
|
||||
const rect = iframe.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
|
||||
const hit = preview.elementAtPoint(x, y);
|
||||
if (hit) {
|
||||
// hit.id — the data-hf-id value
|
||||
// hit.tag — the lowercased tag name (e.g. "div", "img", "video")
|
||||
preview.select([hit.id]);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
The hit-test skips elements whose computed opacity is `0` (including ancestors with `opacity: 0`), and for `<img>` elements it samples the alpha at the clicked pixel using an offscreen canvas — a transparent pixel falls through to the element behind it. Cross-origin images that taint the canvas fall back to treating the pixel as opaque.
|
||||
|
||||
The `opts.atTime` parameter is accepted but does not seek the GSAP timeline. It reflects whatever frame the composition is currently paused at in the iframe. Accurate out-of-time-band opacity queries are a future capability.
|
||||
|
||||
### Walking a click target to the nearest HF element
|
||||
|
||||
If you are working with events on the iframe's `contentDocument` directly (e.g. via a `message` bridge), use the exported `resolveNearestHfElement` function. It walks up the DOM from any node until it finds a `[data-hf-id]` ancestor, skipping the root:
|
||||
|
||||
```typescript
|
||||
import { resolveNearestHfElement } from "@hyperframes/sdk";
|
||||
|
||||
// Inside the iframe's own document context:
|
||||
iframeDoc.addEventListener("click", (e) => {
|
||||
const result = resolveNearestHfElement(
|
||||
e.target as Element | null,
|
||||
(el) => {
|
||||
// Return false to treat this element as invisible and continue the walk.
|
||||
const style = el.ownerDocument.defaultView?.getComputedStyle(el);
|
||||
return style ? parseFloat(style.opacity) !== 0 : true;
|
||||
},
|
||||
);
|
||||
if (result) {
|
||||
// result.id, result.tag
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
`resolveNearestHfElement` returns `null` when the walk exits the tree without finding a `[data-hf-id]` node, when the matching node carries `[data-hf-root]` (the root is transparent to selection), or when `isVisible` returns `false` for that node.
|
||||
|
||||
## Draft loop: 60fps drag without model mutations
|
||||
|
||||
The draft loop keeps the model clean during a drag. The SDK is **not** in the 60fps path — you call `preview.applyDraft` on every `pointermove` and `preview.commitPreview` once on `pointerup`. The model sees exactly one `moveElement` op per drag, rather than hundreds.
|
||||
|
||||
`applyDraft` sets the element's CSS `translate` directly inside the iframe — the pre-drag value composed with the accumulated delta — so the drag is visible in any composition without composition-side CSS, and works on GSAP-animated elements (a `translate` set after GSAP's first parse composes with the animated transform instead of being overwritten). Nothing in the SDK model changes. `cancelPreview` restores the pre-drag translate; `commitPreview` derives one `moveElement` op and mirrors the committed position onto the live element.
|
||||
|
||||
```typescript
|
||||
let dragging = false;
|
||||
let startX = 0;
|
||||
let startY = 0;
|
||||
let targetId: string | null = null;
|
||||
|
||||
iframe.addEventListener("pointerdown", (e) => {
|
||||
const rect = iframe.getBoundingClientRect();
|
||||
const hit = preview.elementAtPoint(e.clientX - rect.left, e.clientY - rect.top);
|
||||
if (!hit) return;
|
||||
|
||||
dragging = true;
|
||||
targetId = hit.id;
|
||||
startX = e.clientX;
|
||||
startY = e.clientY;
|
||||
iframe.setPointerCapture(e.pointerId);
|
||||
});
|
||||
|
||||
iframe.addEventListener("pointermove", (e) => {
|
||||
if (!dragging || !targetId) return;
|
||||
|
||||
const dx = e.clientX - startX;
|
||||
const dy = e.clientY - startY;
|
||||
|
||||
// applyDraft at 60fps — no model mutation, no patch event
|
||||
preview.applyDraft(targetId, { dx, dy });
|
||||
});
|
||||
|
||||
iframe.addEventListener("pointerup", () => {
|
||||
if (!dragging || !targetId) return;
|
||||
|
||||
// Derives a moveElement op from the accumulated dx/dy, dispatches it
|
||||
// through the callback you passed to createIframePreviewAdapter, then
|
||||
// clears the CSS vars and internal draft state.
|
||||
preview.commitPreview();
|
||||
|
||||
dragging = false;
|
||||
targetId = null;
|
||||
});
|
||||
|
||||
iframe.addEventListener("pointercancel", () => {
|
||||
// Clears the CSS vars. Model is never touched.
|
||||
preview.cancelPreview();
|
||||
dragging = false;
|
||||
targetId = null;
|
||||
});
|
||||
```
|
||||
|
||||
`DraftProps` accepts `dx`, `dy`, `width`, and `height`. Width and height are accepted by the interface but resize support (mapping to a `setStyle` op) is not yet wired — only `dx`/`dy` drive the draft CSS vars today.
|
||||
|
||||
Call `cancelPreview()` instead of `commitPreview()` to discard the drag without emitting any op. The model is never mutated and the CSS vars are cleared.
|
||||
|
||||
## Selection
|
||||
|
||||
`preview.select(ids, opts?)` sets the selection state and fires the session's `selectionchange` event on any listeners. Pass `{ additive: true }` to extend the current selection rather than replace it.
|
||||
|
||||
```typescript
|
||||
// Replace selection
|
||||
preview.select(["hf-title"]);
|
||||
|
||||
// Extend selection (e.g. shift-click)
|
||||
preview.select(["hf-logo"], { additive: true });
|
||||
|
||||
// Clear selection
|
||||
preview.select([]);
|
||||
```
|
||||
|
||||
Listen to selection changes on the session via `comp.on("selectionchange", ...)` — the adapter fires that event, not a separate event on the iframe.
|
||||
|
||||
## Pairing with embedded override mode
|
||||
|
||||
For template-driven products you typically open the composition in embedded override mode and store only the sparse delta, not the full HTML. The preview adapter works identically in that mode — pass it the same way:
|
||||
|
||||
```typescript
|
||||
const comp = await openComposition(templateHtml, {
|
||||
preview,
|
||||
overrides: existingOverrides,
|
||||
history: false,
|
||||
});
|
||||
```
|
||||
|
||||
See [Embedded Override Mode](/sdk/guides/embedded-override-mode) for the full pattern.
|
||||
|
||||
## What to build next
|
||||
|
||||
Once hit-testing and drag are working, you can use the affordance resolver to drive a context-aware inspector panel for whatever element is selected. See [Editing Affordances](/sdk/guides/editing-affordances) for how to translate a live element into capability flags and section applicability.
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Adapter reference" icon="plug" href="/sdk/reference/adapters">
|
||||
Full `PreviewAdapter`, `PersistAdapter`, and related type documentation.
|
||||
</Card>
|
||||
<Card title="Editing Affordances" icon="sliders" href="/sdk/guides/editing-affordances">
|
||||
Resolve which edit controls to show for the selected element.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
---
|
||||
title: "Editing Affordances"
|
||||
description: "Resolve capability flags and inspector sections for a selected element so your editor panel is element-aware."
|
||||
---
|
||||
|
||||
`resolveElementAffordances` answers the question "what can the user do with this element right now?" given a live DOM element and its SDK model entry. It returns two objects: `capabilities` (boolean flags for each edit action) and `sections` (which inspector panels apply). You use these to conditionally render controls in your editor's detail panel rather than showing a fixed field set for all elements.
|
||||
|
||||
<Note>
|
||||
This API lives on the `@hyperframes/sdk/editing` subpath, available since `@hyperframes/sdk@0.7.22`. If the import fails to resolve, upgrade: `npm install @hyperframes/sdk@latest`.
|
||||
</Note>
|
||||
|
||||
## Import
|
||||
|
||||
```typescript
|
||||
import { resolveElementAffordances } from "@hyperframes/sdk/editing";
|
||||
```
|
||||
|
||||
## Signature
|
||||
|
||||
```typescript
|
||||
function resolveElementAffordances(
|
||||
liveEl: HTMLElement,
|
||||
modelEl: Pick<HyperFramesElement, "text" | "animationIds" | "start"> | null,
|
||||
ctx?: AffordanceContext,
|
||||
): EditingAffordances
|
||||
```
|
||||
|
||||
- `liveEl` — a live, laid-out `HTMLElement` from the composition iframe. The resolver calls `getComputedStyle` on it, so it must be attached to a rendered document.
|
||||
- `modelEl` — the SDK model element (`comp.getElement(id)`), or `null` when the element exists in the live DOM but not in the SDK source model (generated elements). Passing `null` restricts affordances: style editing is disabled and `reasonIfDisabled` is set.
|
||||
- `ctx` — optional context for studio-specific concepts. For most custom editors, omit it or leave all flags at their defaults (`false`).
|
||||
|
||||
### `AffordanceContext`
|
||||
|
||||
```typescript
|
||||
interface AffordanceContext {
|
||||
isCompositionHost?: boolean; // default false
|
||||
isCompositionRoot?: boolean; // default false
|
||||
isInsideLockedComposition?: boolean; // default false
|
||||
isMasterView?: boolean; // default false
|
||||
}
|
||||
```
|
||||
|
||||
These flags are studio-specific. In a standalone custom editor you can omit `ctx` entirely.
|
||||
|
||||
## Return value
|
||||
|
||||
```typescript
|
||||
interface EditingAffordances {
|
||||
capabilities: DomEditCapabilities;
|
||||
sections: EditingSectionApplicability;
|
||||
}
|
||||
```
|
||||
|
||||
### `capabilities`
|
||||
|
||||
```typescript
|
||||
interface DomEditCapabilities {
|
||||
canSelect: boolean;
|
||||
canEditStyles: boolean;
|
||||
/** Directly editable authored left/top style fields. */
|
||||
canMove: boolean;
|
||||
/** Directly editable authored width/height style fields. */
|
||||
canResize: boolean;
|
||||
/** Canvas translate-drag maps here, not to canMove. */
|
||||
canApplyManualOffset: boolean;
|
||||
canApplyManualSize: boolean;
|
||||
canApplyManualRotation: boolean;
|
||||
/** Set when canEditStyles / canMove / canResize is false; explains why. */
|
||||
reasonIfDisabled?: string;
|
||||
}
|
||||
```
|
||||
|
||||
<Warning>
|
||||
`canMove` and `canResize` indicate that the element has authored `left`/`top`/`width`/`height` style values that can be directly edited as fields. A canvas drag operation maps to `canApplyManualOffset`, not `canMove`. Do not gate drag handles on `canMove`.
|
||||
</Warning>
|
||||
|
||||
### `sections`
|
||||
|
||||
```typescript
|
||||
interface EditingSectionApplicability {
|
||||
text: boolean; // true when the element has editable text content
|
||||
media: boolean; // true for <video> and <audio>
|
||||
colorGrading: boolean; // true for <video> and <img> — element-level only
|
||||
timing: boolean; // true when data-start is present or animationCount > 0
|
||||
animation: boolean; // true when animationCount > 0
|
||||
}
|
||||
```
|
||||
|
||||
`sections.colorGrading` is element-level only: it tells you this particular element supports color grading controls. Studio shows the color grading panel for supported media elements.
|
||||
|
||||
## End-to-end example
|
||||
|
||||
```typescript
|
||||
import { openComposition, createIframePreviewAdapter } from "@hyperframes/sdk";
|
||||
import { resolveElementAffordances } from "@hyperframes/sdk/editing";
|
||||
|
||||
const iframe = document.querySelector<HTMLIFrameElement>("#composition-frame")!;
|
||||
const preview = createIframePreviewAdapter(iframe, (op) => comp.dispatch(op));
|
||||
const comp = await openComposition(compositionHtml, { preview });
|
||||
|
||||
function onElementSelected(hfId: string) {
|
||||
// 1. Get the live DOM element from the iframe.
|
||||
const liveEl = iframe.contentDocument?.querySelector<HTMLElement>(
|
||||
`[data-hf-id="${hfId}"]`,
|
||||
);
|
||||
if (!liveEl) return;
|
||||
|
||||
// 2. Get the SDK model element — null for script-generated elements.
|
||||
const modelEl = comp.getElement(hfId);
|
||||
|
||||
// 3. Resolve affordances.
|
||||
const { capabilities, sections } = resolveElementAffordances(liveEl, modelEl);
|
||||
|
||||
// 4. Render controls conditionally.
|
||||
renderInspector({ capabilities, sections, hfId });
|
||||
}
|
||||
|
||||
function renderInspector({
|
||||
capabilities,
|
||||
sections,
|
||||
hfId,
|
||||
}: {
|
||||
capabilities: ReturnType<typeof resolveElementAffordances>["capabilities"];
|
||||
sections: ReturnType<typeof resolveElementAffordances>["sections"];
|
||||
hfId: string;
|
||||
}) {
|
||||
// Show a disabled-state tooltip when editing is locked.
|
||||
if (!capabilities.canEditStyles && capabilities.reasonIfDisabled) {
|
||||
showDisabledBanner(capabilities.reasonIfDisabled);
|
||||
return;
|
||||
}
|
||||
|
||||
// Style panel — always show when editing is allowed.
|
||||
if (capabilities.canEditStyles) {
|
||||
showStylePanel(hfId);
|
||||
}
|
||||
|
||||
// Position/size fields — only for absolutely positioned elements with px values.
|
||||
if (capabilities.canMove) {
|
||||
showPositionFields(hfId);
|
||||
}
|
||||
if (capabilities.canResize) {
|
||||
showSizeFields(hfId);
|
||||
}
|
||||
|
||||
// Canvas drag handles — use canApplyManualOffset, NOT canMove.
|
||||
if (capabilities.canApplyManualOffset) {
|
||||
showDragHandles(hfId);
|
||||
}
|
||||
|
||||
// Section panels.
|
||||
if (sections.text) showTextPanel(hfId);
|
||||
if (sections.media) showMediaPanel(hfId);
|
||||
if (sections.colorGrading && myFeatureFlags.colorGrading) showColorGradingPanel(hfId);
|
||||
if (sections.timing) showTimingPanel(hfId);
|
||||
if (sections.animation) showAnimationPanel(hfId);
|
||||
}
|
||||
```
|
||||
|
||||
## Gotchas
|
||||
|
||||
**Browser-only.** `resolveElementAffordances` calls `getComputedStyle` internally. It must not be imported or called in Node, a server action, or any non-browser path. The SDK adapter (`@hyperframes/sdk/editing`) is separate from the pure core resolver precisely to keep the Node SDK path free of browser globals.
|
||||
|
||||
**Needs a laid-out DOM.** `canMove` and `canResize` require a live computed `position`, `left`, and `top`. If you call `resolveElementAffordances` before the iframe finishes layout (e.g. synchronously after `srcdoc` assignment), both flags will be `false`. Wait for the iframe's `load` event or the composition's first rendered frame before resolving affordances.
|
||||
|
||||
**`null` model means select-only.** When `modelEl` is `null` (the element is generated at runtime and has no source entry), the resolver sets `existsInSource: false`. This gives `canSelect: true` but `canEditStyles: false`, `canMove: false`, and all manual-geometry flags `false`, with `reasonIfDisabled` set to a human-readable explanation. You can still show the selection highlight; just hide or disable all edit controls.
|
||||
|
||||
**`canApplyManualOffset` vs `canMove`.** `canMove` is true only when the element has absolute/fixed positioning AND authored `left`/`top` px values AND no transform-driven geometry. `canApplyManualOffset` is true for all non-root non-host elements regardless of positioning. A translate-based canvas drag (the `applyDraft` / `commitPreview` flow in [Canvas Integration](/sdk/guides/canvas-integration)) uses `canApplyManualOffset` as its gate — it does not require `canMove`.
|
||||
|
||||
**`colorGrading` is element-level, not feature-level.** `sections.colorGrading` tells you the element type supports grading (`<video>` or `<img>`). Your own feature flag is a separate AND condition. Never replace your feature flag with this field.
|
||||
|
||||
## Type reference
|
||||
|
||||
The full type definitions — `EditingAffordances`, `DomEditCapabilities`, `EditingSectionApplicability`, `AffordanceContext`, `HyperFramesElement` — are documented at [`/sdk/reference/types`](/sdk/reference/types).
|
||||
|
||||
The pure DOM-free resolver (`resolveEditingAffordances`, `EditableElementFacts`) lives in `@hyperframes/core` and is documented at [`/packages/core`](/packages/core).
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Canvas Integration" icon="frame" href="/sdk/guides/canvas-integration">
|
||||
Get the live element and selection via hit-testing and the iframe adapter.
|
||||
</Card>
|
||||
<Card title="Types reference" icon="brackets-curly" href="/sdk/reference/types">
|
||||
Complete type definitions for the SDK public surface.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -0,0 +1,197 @@
|
||||
---
|
||||
title: "Embedded Override Mode"
|
||||
description: "Layer a sparse delta on top of a reusable base composition so the host stores only what changed."
|
||||
---
|
||||
|
||||
Embedded override mode is the pattern for template-driven products: you maintain one base composition HTML file and store only the per-instance _delta_ (the `OverrideSet`) alongside it. When a user edits their instance, the SDK accumulates further changes into that same delta — the base stays untouched.
|
||||
|
||||
This is exactly how a canvas embedder (for example, AI Studio) persists in-composition edits as a stable, reopenable delta.
|
||||
|
||||
## Opening with overrides
|
||||
|
||||
Pass the stored delta as `overrides` when you call `openComposition`. The SDK replays the override set onto the base template in one pass, so the session exposes the user's exact edited state immediately:
|
||||
|
||||
```typescript
|
||||
import { openComposition } from "@hyperframes/sdk";
|
||||
|
||||
// Stored delta — loaded from your database or storage layer.
|
||||
const storedOverrides = {
|
||||
"hf-title.text": "Acme Corp Launch",
|
||||
"hf-logo.attr.src": "/customers/acme/logo.png",
|
||||
"hf-subtitle.style.color": "#0EA5E9",
|
||||
};
|
||||
|
||||
const comp = await openComposition(templateHtml, {
|
||||
overrides: storedOverrides,
|
||||
history: false, // host owns undo — see below
|
||||
});
|
||||
```
|
||||
|
||||
After `openComposition` returns, `comp.getElements()` reflects the overridden state. Any further edits accumulate into the same delta automatically.
|
||||
|
||||
## Reading the current delta
|
||||
|
||||
Call `getOverrides()` to retrieve the current delta for storage. It returns a shallow copy of the internal override set — safe to serialize and stash:
|
||||
|
||||
```typescript
|
||||
comp.setText("hf-cta", "Get started free");
|
||||
comp.setStyle("hf-cta", { backgroundColor: "#22C55E" });
|
||||
|
||||
const nextOverrides = comp.getOverrides();
|
||||
// { "hf-title.text": "Acme Corp Launch", ..., "hf-cta.text": "Get started free", ... }
|
||||
|
||||
await db.saveOverrides(sessionId, nextOverrides);
|
||||
```
|
||||
|
||||
On the next session open, pass `nextOverrides` as `overrides` again — the user sees their exact state.
|
||||
|
||||
## Override set key format
|
||||
|
||||
Keys follow the pattern `hfId.prop.path`. The common forms are:
|
||||
|
||||
| Key | Meaning |
|
||||
|-----|---------|
|
||||
| `"hf-title.text"` | Inner text of element `hf-title` |
|
||||
| `"hf-logo.attr.src"` | `src` attribute on `hf-logo` |
|
||||
| `"hf-x.style.fontSize"` | Inline `fontSize` style on `hf-x` |
|
||||
| `"hf-card"` with value `null` | Removal marker — element was deleted |
|
||||
|
||||
A `null` value is a removal marker. When the SDK serializes the composition it omits that element entirely. This lets the host distinguish "never touched" (key absent) from "user deleted" (key present, value `null`).
|
||||
|
||||
Sub-composition elements use scoped IDs — `"hf-host/hf-leaf.text"` — where the host and leaf are separated by `/`.
|
||||
|
||||
### Font and image variable overrides
|
||||
|
||||
Variable overrides (from `setVariableValue`) live under the `var.{id}` key. Font and image values are objects, not strings:
|
||||
|
||||
```typescript
|
||||
import type { FontValue, ImageValue } from "@hyperframes/sdk";
|
||||
|
||||
// Font variable — name is the CSS font-family, source is the stylesheet URL.
|
||||
const fontOverride: FontValue = {
|
||||
name: "Inter",
|
||||
source: "https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap",
|
||||
};
|
||||
|
||||
// Image variable — url is the src; alt and fit are optional.
|
||||
const imageOverride: ImageValue = {
|
||||
url: "/customers/acme/hero.jpg",
|
||||
alt: "Acme hero image",
|
||||
fit: "cover",
|
||||
};
|
||||
|
||||
comp.setVariableValue("brand-font", fontOverride);
|
||||
comp.setVariableValue("hero-image", imageOverride);
|
||||
|
||||
const overrides = comp.getOverrides();
|
||||
// {
|
||||
// "var.brand-font": { name: "Inter", source: "https://…" },
|
||||
// "var.hero-image": { url: "/customers/acme/hero.jpg", alt: "…", fit: "cover" },
|
||||
// }
|
||||
```
|
||||
|
||||
When you read an `OverrideSet` value at a `var.*` key, narrow before treating it as a scalar — the type is `string | number | boolean | Record<string, unknown> | null`.
|
||||
|
||||
## Disabling SDK undo
|
||||
|
||||
In embedded mode the host typically owns the undo stack. The SDK already leaves history **off by default** in embedded mode (any session opened with `overrides`), so you usually don't need to do anything. Pass `history: false` explicitly only to make that intent obvious in standalone code paths, or as a guard if a call site may or may not supply `overrides`:
|
||||
|
||||
```typescript
|
||||
const comp = await openComposition(templateHtml, {
|
||||
overrides: storedOverrides,
|
||||
history: false,
|
||||
});
|
||||
```
|
||||
|
||||
<Note>
|
||||
`history: false` only disables the SDK's internal undo stack. Persistence (the `persist` adapter, if you supply one) is independent — omitting it does not affect autosave. In embedded mode, however, the host typically owns both undo and persistence, so you usually pass neither.
|
||||
</Note>
|
||||
|
||||
## Host-owned undo via `applyPatches()`
|
||||
|
||||
When the host pops an undo entry and needs to replay the inverse patches back into the SDK, use `applyPatches()`. The SDK applies the patches to the live document, updates the internal override set, and emits a `patch` event tagged with `ORIGIN_APPLY_PATCHES`.
|
||||
|
||||
```typescript
|
||||
import { ORIGIN_APPLY_PATCHES } from "@hyperframes/sdk";
|
||||
|
||||
// Host undo stack entry — inversePatches came from a prior 'patch' event.
|
||||
function hostUndo(inversePatches: JsonPatchOp[]) {
|
||||
comp.applyPatches(inversePatches);
|
||||
}
|
||||
|
||||
// Guard against undo loops — skip ORIGIN_APPLY_PATCHES events in the patch listener.
|
||||
comp.on("patch", ({ patches, inversePatches, origin }) => {
|
||||
if (origin === ORIGIN_APPLY_PATCHES) return; // already from host undo — do not re-push
|
||||
hostHistory.push({ patches, inversePatches });
|
||||
});
|
||||
```
|
||||
|
||||
`applyPatches()` accepts an optional `opts.origin` override if you want to tag the event with a different sentinel, but the default `ORIGIN_APPLY_PATCHES` is the expected value for host undo/redo flows.
|
||||
|
||||
See [Undo, Redo, and Patches](/sdk/guides/undo-redo-and-patches) for the full patch event contract and inverse patch handling.
|
||||
|
||||
## Full example
|
||||
|
||||
```typescript
|
||||
import { openComposition, ORIGIN_APPLY_PATCHES } from "@hyperframes/sdk";
|
||||
import type { JsonPatchOp, OverrideSet } from "@hyperframes/sdk";
|
||||
|
||||
async function openUserSession(
|
||||
templateHtml: string,
|
||||
storedOverrides: OverrideSet,
|
||||
) {
|
||||
const hostHistory: Array<{ patches: readonly JsonPatchOp[]; inversePatches: readonly JsonPatchOp[] }> = [];
|
||||
let historyIndex = hostHistory.length;
|
||||
|
||||
const comp = await openComposition(templateHtml, {
|
||||
overrides: storedOverrides,
|
||||
history: false,
|
||||
});
|
||||
|
||||
// Record every user edit in the host undo stack.
|
||||
comp.on("patch", ({ patches, inversePatches, origin }) => {
|
||||
if (origin === ORIGIN_APPLY_PATCHES) return;
|
||||
// Discard any redo entries ahead of the cursor.
|
||||
hostHistory.splice(historyIndex);
|
||||
hostHistory.push({ patches, inversePatches });
|
||||
historyIndex = hostHistory.length;
|
||||
});
|
||||
|
||||
return {
|
||||
comp,
|
||||
|
||||
undo() {
|
||||
if (historyIndex === 0) return;
|
||||
const { inversePatches } = hostHistory[--historyIndex];
|
||||
comp.applyPatches([...inversePatches]);
|
||||
},
|
||||
|
||||
redo() {
|
||||
if (historyIndex >= hostHistory.length) return;
|
||||
const { patches } = hostHistory[historyIndex++];
|
||||
comp.applyPatches([...patches]);
|
||||
},
|
||||
|
||||
async save() {
|
||||
// Only store the delta — not the full HTML.
|
||||
return comp.getOverrides();
|
||||
},
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="openComposition reference" icon="code" href="/sdk/reference/open-composition">
|
||||
Full `OpenCompositionOptions` including `overrides`, `history`, and `persist`.
|
||||
</Card>
|
||||
<Card title="Types reference" icon="shapes" href="/sdk/reference/types">
|
||||
`OverrideSet`, `FontValue`, `ImageValue`, `PatchEvent`, and `ORIGIN_APPLY_PATCHES`.
|
||||
</Card>
|
||||
<Card title="Undo, Redo, and Patches" icon="clock-rotate-left" href="/sdk/guides/undo-redo-and-patches">
|
||||
Patch event format, inverse patch contract, and undo loop prevention.
|
||||
</Card>
|
||||
<Card title="Canvas integration" icon="browsers" href="/sdk/guides/canvas-integration">
|
||||
Wiring the SDK to a live preview iframe inside an editor canvas.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
---
|
||||
title: "Persistence"
|
||||
description: "Autosave composition edits through pluggable adapters — filesystem, memory, or your own storage backend."
|
||||
---
|
||||
|
||||
When you pass a `persist` adapter to `openComposition`, the SDK subscribes to every `change` event and schedules an async write automatically. You never call a save method after each edit — the adapter handles it.
|
||||
|
||||
```typescript
|
||||
import { openComposition } from "@hyperframes/sdk";
|
||||
import { createFsAdapter } from "@hyperframes/sdk/adapters/fs";
|
||||
|
||||
const adapter = createFsAdapter({ root: "./project" });
|
||||
|
||||
const comp = await openComposition(html, {
|
||||
persist: adapter,
|
||||
persistPath: "composition.html", // default; can be omitted
|
||||
});
|
||||
|
||||
comp.setText("hf-title", "Draft title");
|
||||
// Autosaved on the next tick — no explicit save call needed.
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
<ParamField path="persist" type="PersistAdapter">
|
||||
The storage adapter. The SDK calls `adapter.write(persistPath, html)` after every change. Omit to skip persistence (headless / agent use).
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="persistPath" type="string">
|
||||
The path key passed to the adapter on every write. Default: `"composition.html"`. Immutable for the session lifetime — changing the path mid-session is not supported.
|
||||
</ParamField>
|
||||
|
||||
## Flushing pending writes
|
||||
|
||||
The persist queue coalesces rapid edits, so the last write may be deferred when your application closes. Call `flush()` to drain any pending write before exit:
|
||||
|
||||
```typescript
|
||||
// Ensure the latest state is committed before the process exits.
|
||||
await comp.flush();
|
||||
comp.dispose();
|
||||
```
|
||||
|
||||
`flush()` resolves when the in-flight write completes. If there is nothing pending it resolves immediately. It is a no-op when no `persist` adapter was provided.
|
||||
|
||||
## Handling write failures
|
||||
|
||||
Write errors are emitted as events rather than thrown exceptions — a failed autosave must not crash an interactive session. Subscribe with `comp.on('persist:error', …)` to handle them:
|
||||
|
||||
```typescript
|
||||
comp.on("persist:error", ({ error }) => {
|
||||
console.error("Autosave failed:", error.message, error.cause);
|
||||
showToast("Could not save — retrying next change.");
|
||||
});
|
||||
```
|
||||
|
||||
The event carries `{ error: { message: string; hint?: string; cause?: unknown } }`. The SDK retries on the next `change` event, so transient errors recover automatically.
|
||||
|
||||
## Shipped adapters
|
||||
|
||||
### `createMemoryAdapter()` — tests and demos
|
||||
|
||||
An in-process store — no file I/O, no Node.js required. Use it in unit tests, demos, or browser sessions where you want version history without disk access.
|
||||
|
||||
```typescript
|
||||
import { createMemoryAdapter } from "@hyperframes/sdk";
|
||||
|
||||
const adapter = createMemoryAdapter();
|
||||
|
||||
const comp = await openComposition(html, {
|
||||
persist: adapter,
|
||||
});
|
||||
|
||||
comp.setText("hf-title", "v1");
|
||||
await comp.flush();
|
||||
comp.setText("hf-title", "v2");
|
||||
await comp.flush();
|
||||
|
||||
const versions = await adapter.listVersions("composition.html");
|
||||
// versions[0] is newest — key "v2", versions[1] is "v1"
|
||||
|
||||
const v1Html = await adapter.loadFrom("composition.html", versions[1].key);
|
||||
```
|
||||
|
||||
`createMemoryAdapter()` also exposes `injectFault(message)` — a test helper that makes the next write fire `persist:error` instead of committing. This lets you exercise your error-handling path without real I/O failures.
|
||||
|
||||
### `createFsAdapter(opts)` — Node.js local dev
|
||||
|
||||
Writes to the filesystem and keeps a rolling version history under `.hf-versions/`. Node.js only — do not use in browser builds.
|
||||
|
||||
<Note>
|
||||
`createFsAdapter` is imported from the `@hyperframes/sdk/adapters/fs` **subpath**, not the main barrel like `createMemoryAdapter`. The fs adapter pulls in Node's `fs` module, so keeping it on a separate subpath lets browser bundles tree-shake it away — importing it from the root would drag `node:fs` into a browser build.
|
||||
</Note>
|
||||
|
||||
```typescript
|
||||
import { createFsAdapter } from "@hyperframes/sdk/adapters/fs";
|
||||
|
||||
const adapter = createFsAdapter({
|
||||
root: "./project", // required — directory for composition files
|
||||
maxVersions: 20, // optional — versions to retain per file (default: 20)
|
||||
});
|
||||
```
|
||||
|
||||
<ParamField path="root" type="string">
|
||||
Root directory. The adapter writes `{root}/{persistPath}` and keeps version files under `{root}/.hf-versions/{persistPath}/`.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="maxVersions" type="number">
|
||||
Maximum version snapshots to retain per file. Oldest are pruned automatically. Default: `20`.
|
||||
</ParamField>
|
||||
|
||||
**Listing and restoring versions:**
|
||||
|
||||
```typescript
|
||||
const versions = await adapter.listVersions("composition.html");
|
||||
// [ { key: "1751234567890-0003", timestamp: 1751234567890 }, ... ]
|
||||
// Entries are newest-first.
|
||||
|
||||
const olderHtml = await adapter.loadFrom("composition.html", versions[2].key);
|
||||
if (olderHtml) {
|
||||
// Re-open the old snapshot.
|
||||
const restored = await openComposition(olderHtml, { persist: adapter });
|
||||
}
|
||||
```
|
||||
|
||||
Version keys are `{timestamp}-{counter}` strings. `listVersions` returns entries newest-first; `timestamp` is the Unix milliseconds at the time of the write.
|
||||
|
||||
## Implementing a custom adapter
|
||||
|
||||
Any object that satisfies the `PersistAdapter` interface works as a drop-in. The interface is:
|
||||
|
||||
```typescript
|
||||
export interface PersistAdapter {
|
||||
read(path: string): Promise<string | undefined>;
|
||||
write(path: string, content: string): Promise<void>;
|
||||
flush(): Promise<void>;
|
||||
listVersions(path: string): Promise<PersistVersionEntry[]>;
|
||||
loadFrom(path: string, versionKey: string): Promise<string | undefined>;
|
||||
on(event: "persist:error", handler: (event: PersistErrorEvent) => void): () => void;
|
||||
}
|
||||
```
|
||||
|
||||
**Contract:**
|
||||
|
||||
- `read()` returns `undefined` for a path that has never been written.
|
||||
- `write()` is idempotent — a second write to the same path overwrites.
|
||||
- `flush()` resolves when any queued writes are committed. Return `Promise.resolve()` if writes are synchronous.
|
||||
- `listVersions()` returns entries newest-first. Entries without stored `content` are valid — `loadFrom` is called lazily by the consumer.
|
||||
- `loadFrom()` returns `undefined` when the version key is not found.
|
||||
- `on('persist:error', handler)` registers an error listener. Errors **must not** propagate as thrown exceptions — surface them through the event instead. Returns an unsubscribe function.
|
||||
|
||||
**Minimal example — an HTTP / S3-style adapter:**
|
||||
|
||||
```typescript
|
||||
import type { PersistAdapter, PersistVersionEntry, PersistErrorEvent } from "@hyperframes/sdk";
|
||||
|
||||
export function createHttpAdapter(baseUrl: string): PersistAdapter {
|
||||
const errorHandlers: Array<(e: PersistErrorEvent) => void> = [];
|
||||
|
||||
return {
|
||||
async read(path) {
|
||||
const res = await fetch(`${baseUrl}/${path}`);
|
||||
if (res.status === 404) return undefined;
|
||||
return res.text();
|
||||
},
|
||||
|
||||
async write(path, content) {
|
||||
try {
|
||||
await fetch(`${baseUrl}/${path}`, {
|
||||
method: "PUT",
|
||||
body: content,
|
||||
headers: { "Content-Type": "text/html" },
|
||||
});
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
errorHandlers.forEach((h) => h({ error: { message, cause: err } }));
|
||||
}
|
||||
},
|
||||
|
||||
async flush() {
|
||||
// No queuing — writes are immediate.
|
||||
},
|
||||
|
||||
async listVersions(path): Promise<PersistVersionEntry[]> {
|
||||
const res = await fetch(`${baseUrl}/${path}/versions`);
|
||||
if (!res.ok) return [];
|
||||
return res.json() as Promise<PersistVersionEntry[]>;
|
||||
},
|
||||
|
||||
async loadFrom(path, versionKey) {
|
||||
const res = await fetch(`${baseUrl}/${path}/versions/${versionKey}`);
|
||||
if (!res.ok) return undefined;
|
||||
return res.text();
|
||||
},
|
||||
|
||||
on(event, handler) {
|
||||
if (event !== "persist:error") return () => {};
|
||||
errorHandlers.push(handler);
|
||||
return () => {
|
||||
const i = errorHandlers.indexOf(handler);
|
||||
if (i !== -1) errorHandlers.splice(i, 1);
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
<Note>
|
||||
`write()` must never throw — catch errors and emit them through `on('persist:error', …)` instead. The SDK trusts that writes are fire-and-forget from its side; thrown exceptions break the internal persist queue.
|
||||
</Note>
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Adapter reference" icon="plug" href="/sdk/reference/adapters">
|
||||
Full `PersistAdapter`, `PersistVersionEntry`, and `PreviewAdapter` type definitions.
|
||||
</Card>
|
||||
<Card title="openComposition reference" icon="code" href="/sdk/reference/open-composition">
|
||||
All `OpenCompositionOptions` fields including `persist`, `persistPath`, `preview`, and `overrides`.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
---
|
||||
title: "Querying & Editing Elements"
|
||||
description: "Find elements by property, make typed mutations, group edits with batch, and work with element handles and selection."
|
||||
---
|
||||
|
||||
The SDK exposes two layers for working with composition elements: a **query API** for reading the current document state, and a **mutation API** of typed methods (plus `dispatch()`) for making changes. Every query and every edit operates on stable `hf-id` strings — there is no cursor, no "current selection required" invariant, and no DOM reference that can go stale.
|
||||
|
||||
## Query API
|
||||
|
||||
### `getElements()`
|
||||
|
||||
Returns a flat array of `ElementSnapshot` objects representing every element in the composition — including elements nested inside sub-compositions — in document order.
|
||||
|
||||
```typescript
|
||||
const elements = comp.getElements();
|
||||
|
||||
// Filter in userland
|
||||
const images = elements.filter((el) => el.tag === "img");
|
||||
const timed = elements.filter((el) => el.start !== null);
|
||||
```
|
||||
|
||||
Each `ElementSnapshot` (aliased from `HyperFramesElement`) carries:
|
||||
|
||||
| Field | Type | Notes |
|
||||
|-------|------|-------|
|
||||
| `id` | `string` | Leaf `hf-id` — unique within its sub-composition scope |
|
||||
| `scopedId` | `string` | Canonical dispatch target (see below) |
|
||||
| `tag` | `string` | Lowercase HTML tag name |
|
||||
| `text` | `string \| null` | Direct text content of the element |
|
||||
| `inlineStyles` | `Record<string, string>` | camelCase property names |
|
||||
| `attributes` | `Record<string, string>` | All attributes except `style`, `class`, and `data-hf-*` |
|
||||
| `classNames` | `string[]` | |
|
||||
| `start` | `number \| null` | Seconds — null when `data-start` is absent |
|
||||
| `duration` | `number \| null` | Seconds — null when neither `data-duration` nor `data-end` is present |
|
||||
| `trackIndex` | `number \| null` | |
|
||||
| `animationIds` | `string[]` | GSAP tween IDs targeting this element |
|
||||
|
||||
### `getElement(id)`
|
||||
|
||||
Fetches a single element snapshot by `id` or `scopedId`. Returns `null` when no element matches.
|
||||
|
||||
```typescript
|
||||
const el = comp.getElement("hf-title");
|
||||
if (el) {
|
||||
console.log(el.text, el.inlineStyles.color);
|
||||
}
|
||||
```
|
||||
|
||||
Both bare ids (top-level elements) and scoped ids (sub-composition elements) are accepted. A bare id resolves only against top-level elements: a leaf that exists **only** inside a sub-composition returns `null` even if its id is unique in the document — use `find()` to discover its scoped form (`"hf-HOST/hf-LEAF"`), then pass that to `getElement`.
|
||||
|
||||
### `find(query)`
|
||||
|
||||
Returns an array of `scopedId` strings for every element that matches all supplied `FindQuery` fields. All fields are optional; an empty query matches everything (equivalent to `getElements().map(el => el.scopedId)`).
|
||||
|
||||
```typescript
|
||||
import type { FindQuery } from "@hyperframes/sdk";
|
||||
|
||||
// By tag
|
||||
const imgIds = comp.find({ tag: "img" });
|
||||
|
||||
// By text content (substring match)
|
||||
const headlineIds = comp.find({ text: "Launch" });
|
||||
|
||||
// By data-name attribute
|
||||
const logoIds = comp.find({ name: "brand-logo" });
|
||||
|
||||
// By track index
|
||||
const track1Ids = comp.find({ track: 1 });
|
||||
|
||||
// Filter to elements inside a specific sub-composition (by host hf-id)
|
||||
const innerIds = comp.find({ composition: "hf-intro-scene" });
|
||||
|
||||
// Combine fields — all must match
|
||||
const [targetId] = comp.find({ tag: "h1", track: 0 });
|
||||
```
|
||||
|
||||
`FindQuery` fields:
|
||||
|
||||
| Field | Type | Matches |
|
||||
|-------|------|---------|
|
||||
| `tag` | `string` | Exact tag name |
|
||||
| `text` | `string` | Substring of `el.text` |
|
||||
| `name` | `string` | Exact value of the `data-name` attribute |
|
||||
| `track` | `number` | Exact `trackIndex` |
|
||||
| `composition` | `string` | Elements whose `scopedId` starts with `"<host-id>/"` |
|
||||
|
||||
### `scopedId` for sub-composition elements
|
||||
|
||||
When a composition embeds another composition as a sub-clip, the inner elements are addressable with a scoped id of the form `"hf-HOST/hf-LEAF"` (arbitrary depth: `"hf-A/hf-B/hf-C"`). Always use the `scopedId` as the dispatch target for sub-composition elements — passing a bare leaf id to `setText` or `setStyle` will not resolve correctly when the same leaf id appears in multiple nested scopes.
|
||||
|
||||
```typescript
|
||||
// Wrong — bare id for a sub-composition element
|
||||
comp.setText("hf-inner-title", "New text"); // may silently no-op
|
||||
|
||||
// Correct — use the scopedId returned by find() or getElements()
|
||||
const [id] = comp.find({ name: "inner-title" }); // returns "hf-scene/hf-inner-title"
|
||||
if (id) comp.setText(id, "New text");
|
||||
```
|
||||
|
||||
## Editing with typed methods
|
||||
|
||||
Typed methods are the primary editing surface. Each one dispatches a single `EditOp` and returns immediately. The change is visible in the next `getElements()` call.
|
||||
|
||||
### `setText(id, value)`
|
||||
|
||||
Sets the direct text content of an element.
|
||||
|
||||
```typescript
|
||||
comp.setText("hf-title", "Shipped.");
|
||||
comp.setText("hf-subtitle", "Available now in all regions.");
|
||||
```
|
||||
|
||||
### `setStyle(id, styles)`
|
||||
|
||||
Merges inline styles. Property names are camelCase. Pass `null` for a property to remove it.
|
||||
|
||||
```typescript
|
||||
comp.setStyle("hf-card", {
|
||||
backgroundColor: "#1A1A2E",
|
||||
borderRadius: "16px",
|
||||
opacity: "0.9",
|
||||
});
|
||||
|
||||
// Remove a previously-set inline style
|
||||
comp.setStyle("hf-card", { opacity: null });
|
||||
```
|
||||
|
||||
### `setAttribute(id, name, value)`
|
||||
|
||||
Sets an HTML attribute. Pass `null` to remove it.
|
||||
|
||||
```typescript
|
||||
comp.setAttribute("hf-hero-img", "src", "/assets/hero-v2.jpg");
|
||||
comp.setAttribute("hf-hero-img", "alt", "Product screenshot");
|
||||
|
||||
// Remove an attribute
|
||||
comp.setAttribute("hf-hero-img", "loading", null);
|
||||
```
|
||||
|
||||
### `removeElement(id)`
|
||||
|
||||
Removes an element and all its children from the composition.
|
||||
|
||||
```typescript
|
||||
comp.removeElement("hf-old-badge");
|
||||
```
|
||||
|
||||
### `addElement(parent, index, html)`
|
||||
|
||||
Inserts an HTML fragment as a child of `parent` at zero-based sibling position `index`. Pass `null` for `parent` to insert at the document body root. Returns the minted `hf-id` of the inserted root element.
|
||||
|
||||
```typescript
|
||||
const newId = comp.addElement("hf-card", 0, `<span class="badge">New</span>`);
|
||||
// newId is a fresh stable hf-id, e.g. "hf-a3k7"
|
||||
|
||||
// Append at the end (index >= child count)
|
||||
comp.addElement("hf-card", 999, `<div class="footer-note">v2.0</div>`);
|
||||
```
|
||||
|
||||
The inserted HTML must be a single-root fragment and must not contain `<script>` tags.
|
||||
|
||||
### `setVariableValue(id, value)`
|
||||
|
||||
Sets a composition variable by its variable id.
|
||||
|
||||
```typescript
|
||||
// String variable
|
||||
comp.setVariableValue("tagline", "The fast path to production.");
|
||||
|
||||
// Font variable
|
||||
comp.setVariableValue("headingFont", {
|
||||
name: "Inter",
|
||||
source: "https://fonts.googleapis.com/css2?family=Inter:wght@400;700",
|
||||
});
|
||||
|
||||
// Image variable
|
||||
comp.setVariableValue("heroBg", {
|
||||
url: "/assets/hero.jpg",
|
||||
fit: "cover",
|
||||
});
|
||||
```
|
||||
|
||||
## `batch()` — group edits into one step
|
||||
|
||||
Wrap related mutations in `batch()` to coalesce them into a single undo entry, a single persist write, and a single `change` event. This is the right tool any time two or more edits are logically inseparable.
|
||||
|
||||
```typescript
|
||||
comp.batch(() => {
|
||||
comp.setText("hf-title", "Summer Drop");
|
||||
comp.setStyle("hf-title", { color: "#FF6B35", fontSize: "112px" });
|
||||
comp.setTiming("hf-title", { start: 0, duration: 3.5 });
|
||||
comp.setAttribute("hf-logo", "src", "/assets/logo-summer.svg");
|
||||
});
|
||||
// One undo entry. One disk write. One change event.
|
||||
```
|
||||
|
||||
Batches are transactional: if the callback throws, all DOM mutations from that batch are rolled back and the composition is restored to its pre-batch state.
|
||||
|
||||
Batches can nest — only the outermost boundary emits events and triggers a persist write.
|
||||
|
||||
## Ergonomic handles
|
||||
|
||||
### `comp.element(id)` → `ElementHandle`
|
||||
|
||||
Returns a curried handle that holds the `id` string and exposes the same mutation methods as the top-level `comp.*` methods. Useful when you are making several edits to the same element.
|
||||
|
||||
```typescript
|
||||
const title = comp.element("hf-title");
|
||||
|
||||
title.setText("Shipped.");
|
||||
title.setStyle({ color: "#FFD60A", letterSpacing: "-0.02em" });
|
||||
title.setTiming({ start: 0.5, duration: 3 });
|
||||
```
|
||||
|
||||
The handle holds only the `id` string — there is no stale DOM reference hazard. Calling methods on it after `dispose()` will silently no-op via the underlying dispatch path.
|
||||
|
||||
### `comp.selection()` → `SelectionProxy`
|
||||
|
||||
Returns a proxy that resolves the *current* selection at call time and applies mutations to every selected id in one batch.
|
||||
|
||||
```typescript
|
||||
comp.setSelection(["hf-title", "hf-subtitle"]);
|
||||
|
||||
const sel = comp.selection();
|
||||
console.log(sel.ids); // ["hf-title", "hf-subtitle"]
|
||||
|
||||
// Applies setStyle to both ids as a single batch
|
||||
sel.setStyle({ opacity: "0.5" });
|
||||
```
|
||||
|
||||
The selection is a runtime concept — it does not persist and is not included in patches or the override set. Use `getSelection()` to read the current selection, and `setSelection(ids)` to change it programmatically. Pass an empty array to clear.
|
||||
|
||||
```typescript
|
||||
const current = comp.getSelection();
|
||||
comp.setSelection(["hf-logo"]);
|
||||
comp.setSelection([]); // clear
|
||||
```
|
||||
|
||||
<Note>
|
||||
`SelectionProxy` and `ElementHandle` expose the same five methods: `setStyle`, `setText`, `setAttribute`, `setTiming`, and `removeElement`. They are intentionally symmetric — switch between them freely without rethinking your call site.
|
||||
</Note>
|
||||
|
||||
## Related
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Composition reference" icon="cube" href="/sdk/reference/composition">
|
||||
Full method signatures for every Composition method.
|
||||
</Card>
|
||||
<Card title="Types reference" icon="brackets-curly" href="/sdk/reference/types">
|
||||
HyperFramesElement, FindQuery, ElementHandle, SelectionProxy, and more.
|
||||
</Card>
|
||||
<Card title="Edit Operations reference" icon="pen-to-square" href="/sdk/reference/edit-operations">
|
||||
Every EditOp variant — for the dispatch() and can() layers.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
---
|
||||
title: "Timing & Animation"
|
||||
description: "Set clip timing, elastic holds, GSAP tweens, and keyframed animations on composition elements."
|
||||
---
|
||||
|
||||
Every clip in a HyperFrames composition has a position on the timeline (`data-start`, `data-duration`) and an optional animation attached to it. The SDK exposes typed helpers for both: the timing API controls *when* an element appears and how long it stays on screen; the animation API controls *how* it moves through that window.
|
||||
|
||||
## Clip Timing
|
||||
|
||||
### Reading timings
|
||||
|
||||
`getElementTimings()` returns a `Record<HfId, ElementTimingSnapshot>` — one entry per element that carries timing data. Values are derived from the live DOM on every call (never cached in the snapshot). Keys are `scopedId`: for top-level elements this equals the bare `id`, but for elements inside a sub-composition it is the scoped form `"hf-HOST/hf-LEAF"`.
|
||||
|
||||
```typescript
|
||||
import { openComposition } from "@hyperframes/sdk";
|
||||
|
||||
const comp = await openComposition(html);
|
||||
const timings = comp.getElementTimings();
|
||||
|
||||
for (const [id, t] of Object.entries(timings)) {
|
||||
console.log(id, t.enterAt, "→", t.exitAt, "labels:", t.labels);
|
||||
}
|
||||
```
|
||||
|
||||
Each `ElementTimingSnapshot` contains:
|
||||
|
||||
<ResponseField name="enterAt" type="number">
|
||||
Absolute timeline position (seconds) at which the element enters.
|
||||
</ResponseField>
|
||||
<ResponseField name="exitAt" type="number">
|
||||
Absolute timeline position (seconds) at which the element exits.
|
||||
</ResponseField>
|
||||
<ResponseField name="labels" type="string[]">
|
||||
GSAP timeline label names whose numeric position falls within `[enterAt, exitAt]`. Parsed fresh from the GSAP script on every `getElementTimings()` call — never stale.
|
||||
</ResponseField>
|
||||
|
||||
<Note>
|
||||
`getElementTimings()` only includes elements that have `data-start` and either `data-duration` or `data-end` attributes. Untimed elements are omitted. The method prefers `data-duration` over `data-end − data-start` when both exist, matching the behavior of `setTiming`.
|
||||
</Note>
|
||||
|
||||
### Setting timing on one element
|
||||
|
||||
```typescript
|
||||
// Move hf-title to start at 1.0 s, last 2.5 s, on track 0
|
||||
comp.setTiming("hf-title", { start: 1.0, duration: 2.5, trackIndex: 0 });
|
||||
```
|
||||
|
||||
All three fields are optional — pass only what you want to change:
|
||||
|
||||
<ParamField path="start" type="number">
|
||||
Clip start time in seconds.
|
||||
</ParamField>
|
||||
<ParamField path="duration" type="number">
|
||||
Clip duration in seconds.
|
||||
</ParamField>
|
||||
<ParamField path="trackIndex" type="number">
|
||||
Zero-based track index.
|
||||
</ParamField>
|
||||
|
||||
### Updating multiple elements in one batch
|
||||
|
||||
`setElementTiming(map)` dispatches one `setTiming` op per entry inside a single batch, so the history records one undo step and patch listeners see one event:
|
||||
|
||||
```typescript
|
||||
comp.setElementTiming({
|
||||
"hf-title": { start: 0.5, duration: 2.0 },
|
||||
"hf-logo": { start: 0.0, duration: 5.0, trackIndex: 1 },
|
||||
"hf-cta": { start: 3.0, duration: 2.5 },
|
||||
});
|
||||
```
|
||||
|
||||
Unknown ids are silently skipped.
|
||||
|
||||
### Elastic holds
|
||||
|
||||
An elastic hold freezes or loops a portion of an element's timeline window. Use it to hold a static frame between two animated segments without changing clip duration.
|
||||
|
||||
```typescript
|
||||
comp.setHold("hf-card", {
|
||||
start: 1.5, // hold begins at this time within the composition
|
||||
end: 4.0, // hold ends at this time
|
||||
fill: "freeze", // "freeze" | "loop"
|
||||
});
|
||||
```
|
||||
|
||||
`ElasticHold` fields:
|
||||
|
||||
<ParamField path="start" type="number">
|
||||
Absolute composition time at which the hold begins.
|
||||
</ParamField>
|
||||
<ParamField path="end" type="number">
|
||||
Absolute composition time at which the hold ends.
|
||||
</ParamField>
|
||||
<ParamField path="fill" type='"freeze" | "loop"'>
|
||||
`"freeze"` holds the last frame until `end`; `"loop"` repeats the segment from `start` back to `start`.
|
||||
</ParamField>
|
||||
|
||||
---
|
||||
|
||||
## GSAP Tweens
|
||||
|
||||
GSAP tweens are the primary animation primitive. The SDK reads tween IDs from `element.animationIds` (returned by the query API) and writes through `addGsapTween`, `setGsapTween`, and `removeGsapTween`.
|
||||
|
||||
### Adding a tween
|
||||
|
||||
```typescript
|
||||
const animId = comp.addGsapTween("hf-title", {
|
||||
method: "from",
|
||||
position: 0.5,
|
||||
duration: 0.6,
|
||||
ease: "power3.out",
|
||||
fromProperties: { opacity: 0, y: 40 },
|
||||
});
|
||||
// animId is the newly-assigned animation ID — store it for subsequent edits
|
||||
```
|
||||
|
||||
`addGsapTween` returns the newly-assigned animation ID as a `string`.
|
||||
|
||||
### GsapTweenSpec fields
|
||||
|
||||
<ParamField path="method" type='"from" | "to" | "fromTo" | "set"'>
|
||||
The GSAP timeline method to call.
|
||||
</ParamField>
|
||||
<ParamField path="position" type="number | string">
|
||||
Timeline position: a number (seconds) or a label-relative string (e.g. `"intro+=0.3"`). Number-only is required for `addWithKeyframes` / `replaceWithKeyframes` — see [Keyframes](#keyframes) below.
|
||||
</ParamField>
|
||||
<ParamField path="duration" type="number">
|
||||
Tween duration in seconds.
|
||||
</ParamField>
|
||||
<ParamField path="ease" type="string">
|
||||
GSAP ease string, e.g. `"power2.inOut"`.
|
||||
</ParamField>
|
||||
<ParamField path="fromProperties" type="Record<string, unknown>">
|
||||
Starting properties — used with `"from"` and `"fromTo"`.
|
||||
</ParamField>
|
||||
<ParamField path="toProperties" type="Record<string, unknown>">
|
||||
Ending properties — used with `"fromTo"`.
|
||||
</ParamField>
|
||||
<ParamField path="properties" type="Record<string, unknown>">
|
||||
Animation target properties — used with `"to"` tweens.
|
||||
</ParamField>
|
||||
<ParamField path="repeat" type="number">
|
||||
Number of repeats (`-1` = infinite). Maps to GSAP's `repeat` option.
|
||||
</ParamField>
|
||||
<ParamField path="yoyo" type="boolean">
|
||||
When `true`, alternates direction on each repeat.
|
||||
</ParamField>
|
||||
<ParamField path="stagger" type="number | Record<string, unknown>">
|
||||
GSAP stagger amount (seconds) or a full stagger config object.
|
||||
</ParamField>
|
||||
|
||||
### Modifying a tween
|
||||
|
||||
```typescript
|
||||
// Change ease and duration — all other fields stay as-is
|
||||
comp.setGsapTween(animId, { ease: "elastic.out(1, 0.5)", duration: 0.9 });
|
||||
```
|
||||
|
||||
`setGsapTween` takes the animation ID and a `Partial<GsapTweenSpec>` — only the keys you pass are updated.
|
||||
|
||||
### Removing a tween
|
||||
|
||||
```typescript
|
||||
comp.removeGsapTween(animId);
|
||||
```
|
||||
|
||||
### Looking up animation IDs
|
||||
|
||||
Query the element snapshot to find animation IDs attached to a given element:
|
||||
|
||||
```typescript
|
||||
const el = comp.getElement("hf-title");
|
||||
const [firstAnimId] = el?.animationIds ?? [];
|
||||
if (firstAnimId) {
|
||||
comp.setGsapTween(firstAnimId, { ease: "none" });
|
||||
}
|
||||
```
|
||||
|
||||
### Feature-detecting advanced ops
|
||||
|
||||
Some GSAP operations (such as `setGsapKeyframe`, arc paths, and label ops) require the parser engine, which ships in a later phase. Use `can()` to gate them before dispatching:
|
||||
|
||||
```typescript
|
||||
const check = comp.can({
|
||||
type: "setGsapTween",
|
||||
animationId: firstAnimId,
|
||||
properties: { ease: "power3.inOut" },
|
||||
});
|
||||
|
||||
if (!check.ok) {
|
||||
if (check.code === "E_NO_GSAP_TIMELINE") {
|
||||
// Parser engine not yet available — show a disabled state or skip
|
||||
console.warn("GSAP timeline not available:", check.message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
comp.dispatch({ type: "setGsapTween", animationId: firstAnimId, properties: { ease: "power3.inOut" } });
|
||||
```
|
||||
|
||||
Stable error codes returned by `can()`:
|
||||
|
||||
| Code | Meaning |
|
||||
|------|---------|
|
||||
| `E_TARGET_NOT_FOUND` | The target `HfId` does not exist in the document. |
|
||||
| `E_NO_ROOT` | The document has no root element. |
|
||||
| `E_NO_GSAP_TIMELINE` | Op requires the GSAP parser engine, which is not yet available. |
|
||||
| `E_NO_GSAP_SCRIPT` | The composition has no embedded GSAP script. |
|
||||
|
||||
---
|
||||
|
||||
## Keyframes
|
||||
|
||||
For keyframe-based animations, use `addWithKeyframes` and `replaceWithKeyframes`. Both operate on the GSAP CSS-keyframes layer and return the minted animation ID.
|
||||
|
||||
### Adding a keyframed tween
|
||||
|
||||
```typescript
|
||||
const animId = comp.addWithKeyframes(
|
||||
"#hf-badge", // CSS selector targeting the element
|
||||
1.0, // timeline position in seconds (number only)
|
||||
0.8, // duration in seconds
|
||||
[
|
||||
{ percentage: 0, properties: { opacity: 0, scale: 0.8 } },
|
||||
{ percentage: 60, properties: { opacity: 1, scale: 1.05 }, ease: "power2.out" },
|
||||
{ percentage: 100, properties: { scale: 1 } },
|
||||
],
|
||||
"power2.inOut", // optional overall ease
|
||||
);
|
||||
```
|
||||
|
||||
Returns the new animation ID string, or `""` if the op was rejected.
|
||||
|
||||
### Replacing an existing keyframed tween
|
||||
|
||||
```typescript
|
||||
const newAnimId = comp.replaceWithKeyframes(
|
||||
oldAnimId,
|
||||
"#hf-badge",
|
||||
1.0,
|
||||
1.2,
|
||||
[
|
||||
{ percentage: 0, properties: { x: -60, opacity: 0 } },
|
||||
{ percentage: 100, properties: { x: 0, opacity: 1 }, ease: "back.out(1.7)" },
|
||||
],
|
||||
);
|
||||
// newAnimId !== oldAnimId — position-derived IDs renumber after the remove
|
||||
```
|
||||
|
||||
<Warning>
|
||||
`replaceWithKeyframes` is equivalent to `removeGsapTween` + `addWithKeyframes` in one atomic op. Because position-derived tween IDs renumber after the removal step, the returned ID is always a **new** ID and must not be assumed equal to the input `animationId`. Re-query `element.animationIds` after a replace to get the current set.
|
||||
</Warning>
|
||||
|
||||
### KeyframeSpec fields
|
||||
|
||||
<ParamField path="percentage" type="number">
|
||||
Position within the tween as a percentage (0–100).
|
||||
</ParamField>
|
||||
<ParamField path="properties" type="Record<string, number | string>">
|
||||
CSS / GSAP properties at this keyframe.
|
||||
</ParamField>
|
||||
<ParamField path="ease" type="string">
|
||||
Per-keyframe ease applied *from* this keyframe to the next.
|
||||
</ParamField>
|
||||
<ParamField path="auto" type="boolean">
|
||||
GSAP endpoint flag — when `true`, this keyframe picks up the element's current value automatically.
|
||||
</ParamField>
|
||||
|
||||
### Lower-level keyframe and label ops
|
||||
|
||||
For lower-level operations — individual keyframe mutations (`addGsapKeyframe`, `setGsapKeyframe`, `removeGsapKeyframe`), property removal, arc paths, label insertion, and animation splitting — use `dispatch()` directly with the corresponding `EditOp` types. See the [Edit Operations reference](/sdk/reference/edit-operations) for the full op union.
|
||||
|
||||
---
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Edit Operations" icon="code" href="/sdk/reference/edit-operations">
|
||||
Full reference for every op type in the `EditOp` union, including lower-level keyframe and arc ops.
|
||||
</Card>
|
||||
<Card title="Types" icon="brackets-curly" href="/sdk/reference/types">
|
||||
`GsapTweenSpec`, `KeyframeSpec`, `ElasticHold`, `ElementTimingSnapshot`, and all related types.
|
||||
</Card>
|
||||
<Card title="GSAP Animation Guide" icon="wand-magic-sparkles" href="/guides/gsap-animation">
|
||||
Authoring GSAP timelines in composition HTML.
|
||||
</Card>
|
||||
<Card title="Keyframes Guide" icon="film" href="/guides/keyframes">
|
||||
Keyframe authoring patterns and best practices.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
---
|
||||
title: "Undo, Redo & Patches"
|
||||
description: "Use the SDK's built-in undo stack, subscribe to patch events, and integrate with a host application's own history."
|
||||
---
|
||||
|
||||
The SDK ships undo/redo by default for standalone sessions and emits RFC 6902 JSON patches on every change. Patch events are the integration seam for host application history, collaborative editing, audit logs, and embedded override mode.
|
||||
|
||||
## Built-in Undo and Redo
|
||||
|
||||
Standalone sessions (those without an `overrides` option) automatically attach a history module. You call `undo()` and `redo()` directly on the composition, and guard UI state with `canUndo()` and `canRedo()`.
|
||||
|
||||
```typescript
|
||||
import { openComposition } from "@hyperframes/sdk";
|
||||
|
||||
const comp = await openComposition(html);
|
||||
|
||||
comp.setText("hf-title", "Draft A");
|
||||
comp.setText("hf-title", "Draft B");
|
||||
|
||||
comp.canUndo(); // true
|
||||
comp.undo(); // reverts "Draft B" → "Draft A"
|
||||
comp.redo(); // reapplies "Draft B"
|
||||
comp.canRedo(); // false — already at tip
|
||||
```
|
||||
|
||||
<Tip>
|
||||
`canUndo()` and `canRedo()` return `false` when the stack is empty and also when `history: false` was passed to `openComposition`. Check them before showing undo/redo controls.
|
||||
</Tip>
|
||||
|
||||
### Opting out of the built-in history
|
||||
|
||||
Pass `history: false` when the host application owns the undo stack and you want the SDK to emit patches without building a parallel internal stack:
|
||||
|
||||
```typescript
|
||||
const comp = await openComposition(html, {
|
||||
overrides: storedOverrides,
|
||||
history: false,
|
||||
});
|
||||
// comp.undo() / comp.redo() are no-ops — host drives history via applyPatches()
|
||||
```
|
||||
|
||||
<Note>
|
||||
`history: false` only disables the SDK undo stack. Persistence (auto-save) is independent — passing `history: false` does not suppress disk writes.
|
||||
</Note>
|
||||
|
||||
### Coalescing rapid edits
|
||||
|
||||
The history module coalesces patch events into one undo entry when two conditions are met: the same op types and same element paths are touched within a configurable window. This prevents a slider drag from flooding the stack with hundreds of individual entries.
|
||||
|
||||
The default coalesce window is 300 ms. Override it in `openComposition`:
|
||||
|
||||
```typescript
|
||||
const comp = await openComposition(html, {
|
||||
coalesceMs: 500, // ms — increase for slower interactions
|
||||
});
|
||||
```
|
||||
|
||||
For more on the coalesce contract and the `HistoryOptions` type, see [Utilities](/sdk/reference/utilities).
|
||||
|
||||
---
|
||||
|
||||
## Patch Events
|
||||
|
||||
Every committed change emits a `patch` event. Subscribe to mirror SDK edits into your host state, audit log, or collaboration channel.
|
||||
|
||||
```typescript
|
||||
const off = comp.on("patch", ({ formatVersion, patches, inversePatches, origin, opTypes }) => {
|
||||
if (formatVersion !== 1) throw new Error("Unexpected patch format version");
|
||||
saveToDB({ patches, inversePatches, origin, opTypes });
|
||||
});
|
||||
|
||||
// Unsubscribe when done
|
||||
off();
|
||||
```
|
||||
|
||||
### PatchEvent fields
|
||||
|
||||
<ResponseField name="formatVersion" type="1">
|
||||
Always `1` in the current SDK. Check this and reject unknown versions — a bump means a breaking change.
|
||||
</ResponseField>
|
||||
<ResponseField name="patches" type="JsonPatchOp[]">
|
||||
Forward patches that were applied: the add/remove/replace ops describing how the document changed.
|
||||
</ResponseField>
|
||||
<ResponseField name="inversePatches" type="JsonPatchOp[]">
|
||||
Inverse patches that undo this change. Store these alongside `patches` to support host-owned undo.
|
||||
</ResponseField>
|
||||
<ResponseField name="origin" type="unknown">
|
||||
The origin tag that was passed to `dispatch()` or `batch()`. Use it to route or filter events.
|
||||
</ResponseField>
|
||||
<ResponseField name="opTypes" type="string[]">
|
||||
Semantic names of the operations that produced this event (e.g. `["setStyle"]`). Useful for analytics and history labels. Not versioned — treat as informational.
|
||||
</ResponseField>
|
||||
|
||||
### JsonPatchOp
|
||||
|
||||
The SDK emits and accepts only the add/remove/replace subset of RFC 6902:
|
||||
|
||||
```typescript
|
||||
interface JsonPatchOp {
|
||||
op: "add" | "remove" | "replace";
|
||||
path: string; // JSON Pointer (RFC 6901), e.g. "/elements/hf-title/style/color"
|
||||
value?: unknown;
|
||||
}
|
||||
```
|
||||
|
||||
The SDK never emits `move`, `copy`, or `test` ops, and `applyPatches()` ignores them if passed.
|
||||
|
||||
---
|
||||
|
||||
## Origin Model
|
||||
|
||||
Every mutation is tagged with an origin string. The SDK defines two built-in origins:
|
||||
|
||||
| Constant | Value | When used |
|
||||
|----------|-------|-----------|
|
||||
| `ORIGIN_LOCAL` | `"local"` | Default for all `dispatch()` and `batch()` calls |
|
||||
| `ORIGIN_APPLY_PATCHES` | `"@hyperframes/sdk:applyPatches"` | Automatically applied by `applyPatches()` |
|
||||
|
||||
Both are exported from `@hyperframes/sdk`.
|
||||
|
||||
### Setting a custom origin
|
||||
|
||||
Pass `origin` in the options to `dispatch()` or `batch()` to tag the event for downstream routing:
|
||||
|
||||
```typescript
|
||||
import { ORIGIN_LOCAL } from "@hyperframes/sdk";
|
||||
|
||||
comp.dispatch(
|
||||
{ type: "setStyle", target: "hf-title", styles: { color: "#0EA5E9" } },
|
||||
{ origin: "ui:color-picker" },
|
||||
);
|
||||
|
||||
comp.batch(() => {
|
||||
comp.setText("hf-cta", "Buy Now");
|
||||
comp.setStyle("hf-cta", { backgroundColor: "#22C55E" });
|
||||
}, { origin: "template-wizard" });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Host-Owned History with applyPatches
|
||||
|
||||
When `history: false`, the host maintains its own undo stack from the patch events the SDK emits. To replay an undo step back into the SDK, call `applyPatches()` with the inverse patches from that stack entry:
|
||||
|
||||
```typescript
|
||||
// Host undo stack entry (built from PatchEvent)
|
||||
type HostEntry = {
|
||||
patches: JsonPatchOp[];
|
||||
inversePatches: JsonPatchOp[];
|
||||
};
|
||||
|
||||
let history: HostEntry[] = [];
|
||||
|
||||
comp.on("patch", ({ patches, inversePatches, origin }) => {
|
||||
// Skip — this is the SDK replaying a host-initiated undo
|
||||
if (origin === ORIGIN_APPLY_PATCHES) return;
|
||||
history.push({ patches, inversePatches });
|
||||
});
|
||||
|
||||
function hostUndo() {
|
||||
const entry = history.pop();
|
||||
if (!entry) return;
|
||||
comp.applyPatches(entry.inversePatches);
|
||||
}
|
||||
```
|
||||
|
||||
### Preventing undo loops
|
||||
|
||||
`applyPatches()` auto-tags its patch event with `ORIGIN_APPLY_PATCHES`. Your listener **must** skip that origin or you create an infinite loop:
|
||||
|
||||
```typescript
|
||||
import { ORIGIN_APPLY_PATCHES } from "@hyperframes/sdk";
|
||||
|
||||
comp.on("patch", ({ patches, inversePatches, origin }) => {
|
||||
if (origin === ORIGIN_APPLY_PATCHES) return; // <-- required guard
|
||||
history.push({ patches, inversePatches });
|
||||
});
|
||||
```
|
||||
|
||||
<Warning>
|
||||
Omitting the `ORIGIN_APPLY_PATCHES` guard causes every `applyPatches()` call to push a new entry onto the host stack, which triggers another `applyPatches()`, and so on. Always check `origin` first.
|
||||
</Warning>
|
||||
|
||||
`ORIGIN_APPLY_PATCHES` is a namespaced string (`"@hyperframes/sdk:applyPatches"`) rather than a `Symbol` so it survives `postMessage`, structured clone, and JSON round-trips — important when the host forwards patch events across frames or workers.
|
||||
|
||||
### applyPatches with a custom origin
|
||||
|
||||
If you want downstream listeners to receive an explicit marker different from the default:
|
||||
|
||||
```typescript
|
||||
comp.applyPatches(entry.inversePatches, { origin: "host:undo" });
|
||||
```
|
||||
|
||||
Any origin other than `ORIGIN_APPLY_PATCHES` will enter the history module if `history` is still attached. Use `history: false` or a `trackedOrigins` filter when running host-owned history to avoid double-stacking.
|
||||
|
||||
---
|
||||
|
||||
## Embedded Override Mode Integration
|
||||
|
||||
In embedded (T3) mode, the host stores only the sparse override delta for each user. Pair `history: false` with `applyPatches()` to let the host drive undo while the SDK updates the composition state:
|
||||
|
||||
```typescript
|
||||
import { openComposition, ORIGIN_APPLY_PATCHES } from "@hyperframes/sdk";
|
||||
|
||||
const comp = await openComposition(baseTemplateHtml, {
|
||||
overrides: storedUserOverrides,
|
||||
history: false,
|
||||
});
|
||||
|
||||
let hostStack: HostEntry[] = [];
|
||||
|
||||
comp.on("patch", ({ patches, inversePatches, origin }) => {
|
||||
if (origin === ORIGIN_APPLY_PATCHES) return;
|
||||
hostStack.push({ patches, inversePatches });
|
||||
// Persist the updated override set
|
||||
saveOverrides(comp.getOverrides());
|
||||
});
|
||||
|
||||
function undo() {
|
||||
const entry = hostStack.pop();
|
||||
if (entry) comp.applyPatches(entry.inversePatches);
|
||||
}
|
||||
```
|
||||
|
||||
See [Embedded Override Mode](/sdk/guides/embedded-override-mode) for the full T3 integration pattern.
|
||||
|
||||
---
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Composition API" icon="code" href="/sdk/reference/composition">
|
||||
Full method signatures for `undo`, `redo`, `canUndo`, `canRedo`, `on`, `applyPatches`, `dispatch`, and `batch`.
|
||||
</Card>
|
||||
<Card title="Types" icon="brackets-curly" href="/sdk/reference/types">
|
||||
`PatchEvent`, `JsonPatchOp`, `ORIGIN_APPLY_PATCHES`, `ORIGIN_LOCAL`, and related types.
|
||||
</Card>
|
||||
<Card title="Utilities" icon="wrench" href="/sdk/reference/utilities">
|
||||
`HistoryOptions`, `coalesceMs`, `trackedOrigins`, and `createHistory` internals.
|
||||
</Card>
|
||||
<Card title="Embedded Override Mode" icon="layer-group" href="/sdk/guides/embedded-override-mode">
|
||||
Full T3 embedded integration with host-owned history.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
---
|
||||
title: "SDK Overview"
|
||||
description: "Headless, framework-neutral composition editing engine — query, mutate, patch, and persist without a browser UI."
|
||||
---
|
||||
|
||||
`@hyperframes/sdk` is the editing engine inside HyperFrames Studio and the CLI. It opens composition HTML, exposes query and mutation APIs, emits RFC 6902 JSON patches, tracks undo/redo, and persists changes through pluggable adapters — all without requiring React, Studio, or a browser UI.
|
||||
|
||||
```bash
|
||||
npm install @hyperframes/sdk
|
||||
```
|
||||
|
||||
## Mental model
|
||||
|
||||
**Every edit targets a stable `hf-id`.** The SDK stamps all elements with `data-hf-id` identifiers before any query or mutation runs. This means edits never depend on mouse state or a UI selection — agents and backend jobs operate on the same surface as a Studio user.
|
||||
|
||||
**Typed methods are sugar over `dispatch()`.** `comp.setText("hf-title", "Hello")` is exactly equivalent to `comp.dispatch({ type: "setText", target: "hf-title", value: "Hello" })`. Both go through the same validation and emit the same patch event. Use typed methods for clarity; use `dispatch()` when you're building data-driven automation or want to feed programmatic op arrays.
|
||||
|
||||
**Patches are the source of truth for history and sync.** Every committed change emits a `PatchEvent` with forward patches and inverse patches (RFC 6902 `add`/`remove`/`replace` ops). The undo stack replays inverse patches; embedded hosts replay the same patches into their own state machine; collaboration layers forward them to other clients. You can subscribe to `patch` events and mirror SDK mutations anywhere without re-parsing the HTML.
|
||||
|
||||
**Adapters decouple persistence and preview.** The SDK never reaches the filesystem or an iframe directly. You supply a `PersistAdapter` (memory, filesystem, S3, HTTP — same interface) and optionally a `PreviewAdapter`. The session fires `persist:error` events on write failures instead of throwing. This makes the SDK equally at home in a Node.js agent, a browser editor, and a CI pipeline.
|
||||
|
||||
**Standalone vs embedded mode.** In standalone mode you own the HTML and the SDK owns history and autosave. In embedded (override) mode you supply a base template and a sparse `OverrideSet` delta; the SDK folds the delta onto the template at open time, accumulates further edits into the override set, and lets you store only the delta — the base template stays untouched.
|
||||
|
||||
## Guides
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Quickstart" icon="bolt" href="/sdk/quickstart">
|
||||
Open a composition, make edits, serialize, and add persistence in five minutes.
|
||||
</Card>
|
||||
<Card title="Querying & Editing Elements" icon="magnifying-glass" href="/sdk/guides/querying-and-editing">
|
||||
getElements, find, typed methods, batch, element handles, and selection.
|
||||
</Card>
|
||||
<Card title="Timing & Animation" icon="clock" href="/sdk/guides/timing-and-animation">
|
||||
setTiming, setHold, GSAP tween and keyframe operations.
|
||||
</Card>
|
||||
<Card title="Undo, Redo & Patches" icon="arrow-rotate-left" href="/sdk/guides/undo-redo-and-patches">
|
||||
History module, patch events, applyPatches, and origin tagging.
|
||||
</Card>
|
||||
<Card title="Persistence" icon="floppy-disk" href="/sdk/guides/persistence">
|
||||
Memory, filesystem, and custom adapters. Version history and flush.
|
||||
</Card>
|
||||
<Card title="Embedded Override Mode" icon="layers" href="/sdk/guides/embedded-override-mode">
|
||||
Template-driven products with host-owned undo and delta storage.
|
||||
</Card>
|
||||
<Card title="Canvas Integration" icon="paintbrush" href="/sdk/guides/canvas-integration">
|
||||
Connecting the SDK to an iframe preview surface.
|
||||
</Card>
|
||||
<Card title="Editing Affordances" icon="sliders" href="/sdk/guides/editing-affordances">
|
||||
Resolve which controls to show for a live element.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Reference
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="openComposition" icon="door-open" href="/sdk/reference/open-composition">
|
||||
The single entry point — options, modes, and examples.
|
||||
</Card>
|
||||
<Card title="Composition" icon="cube" href="/sdk/reference/composition">
|
||||
Full method reference for the session object.
|
||||
</Card>
|
||||
<Card title="Edit Operations" icon="pen-to-square" href="/sdk/reference/edit-operations">
|
||||
Every EditOp variant with field-level documentation.
|
||||
</Card>
|
||||
<Card title="Types" icon="brackets-curly" href="/sdk/reference/types">
|
||||
HyperFramesElement, FindQuery, PatchEvent, OverrideSet, and more.
|
||||
</Card>
|
||||
<Card title="Adapters" icon="plug" href="/sdk/reference/adapters">
|
||||
PersistAdapter and PreviewAdapter interfaces and built-in implementations.
|
||||
</Card>
|
||||
<Card title="Utilities" icon="wrench" href="/sdk/reference/utilities">
|
||||
buildDocument, flatElements, UnsupportedOpError, and helper exports.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
<Tip>
|
||||
If you want to render a composition to MP4 or WebM rather than edit it, see the [CLI](/packages/cli) or [producer](/packages/producer). The SDK is the editing layer — rendering is a separate pipeline.
|
||||
</Tip>
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
---
|
||||
title: "SDK Quickstart"
|
||||
description: "Open a composition, query and edit elements, serialize, and add autosave in minutes."
|
||||
---
|
||||
|
||||
This guide walks you through the core SDK loop from scratch. You will open a composition HTML string, find and edit elements by their stable `hf-id`, serialize the result, and then extend the example to save changes to disk automatically.
|
||||
|
||||
## Open, edit, serialize
|
||||
|
||||
<Steps>
|
||||
<Step title="Install the package">
|
||||
```bash
|
||||
npm install @hyperframes/sdk
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step title="Open a composition">
|
||||
`openComposition` parses the HTML, stamps any elements that lack `data-hf-id` attributes, and returns a `Composition` session.
|
||||
|
||||
```typescript
|
||||
import { openComposition } from "@hyperframes/sdk";
|
||||
|
||||
const html = `<div class="clip" data-start="0" data-duration="5"
|
||||
data-hf-id="hf-title">Launch day</div>`;
|
||||
|
||||
const comp = await openComposition(html);
|
||||
```
|
||||
|
||||
The call is async because it runs the ID-stamping pass over the DOM before returning.
|
||||
</Step>
|
||||
|
||||
<Step title="Find elements">
|
||||
Use `getElements()` for a flat snapshot of everything in the composition, or `find()` to filter by tag, text content, `data-name` attribute, track index, or sub-composition host.
|
||||
|
||||
```typescript
|
||||
// All elements
|
||||
const all = comp.getElements();
|
||||
|
||||
// Elements whose text contains "Launch"
|
||||
const ids = comp.find({ text: "Launch" });
|
||||
|
||||
// A specific element by its hf-id
|
||||
const el = comp.getElement("hf-title");
|
||||
console.log(el?.text); // "Launch day"
|
||||
```
|
||||
|
||||
`find()` returns an array of `scopedId` strings. For top-level elements, `scopedId === id`. For elements inside inlined sub-compositions, it is `"hf-HOST/hf-LEAF"`.
|
||||
</Step>
|
||||
|
||||
<Step title="Edit elements with typed methods">
|
||||
Typed methods are the most readable way to mutate a composition. Each one translates directly into a dispatched `EditOp` and emits a patch event.
|
||||
|
||||
```typescript
|
||||
// Change text
|
||||
comp.setText("hf-title", "Launch day — we shipped!");
|
||||
|
||||
// Change inline styles (camelCase property names)
|
||||
comp.setStyle("hf-title", {
|
||||
color: "#FFD60A",
|
||||
fontSize: "96px",
|
||||
fontWeight: "700",
|
||||
});
|
||||
|
||||
// Set or clear an attribute (null removes it)
|
||||
comp.setAttribute("hf-logo", "src", "/assets/logo-v2.png");
|
||||
|
||||
// Adjust clip timing
|
||||
comp.setTiming("hf-title", { start: 0.5, duration: 4 });
|
||||
|
||||
// Set a composition variable
|
||||
comp.setVariableValue("brandColor", "#6C5CE7");
|
||||
```
|
||||
|
||||
Use `batch()` when several mutations should collapse into one undo entry, one persist write, and one `change` event:
|
||||
|
||||
```typescript
|
||||
comp.batch(() => {
|
||||
comp.setText("hf-title", "Launch day — we shipped!");
|
||||
comp.setStyle("hf-title", { color: "#FFD60A" });
|
||||
comp.setTiming("hf-title", { start: 0.5, duration: 4 });
|
||||
});
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step title="Serialize and dispose">
|
||||
`serialize()` returns the full updated HTML string. Call `dispose()` when you are done to release event handlers and any internal state.
|
||||
|
||||
```typescript
|
||||
const updatedHtml = comp.serialize();
|
||||
comp.dispose();
|
||||
|
||||
// updatedHtml is ready to write to disk, send to a renderer, or store in a database.
|
||||
```
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Add a persistence adapter
|
||||
|
||||
The headless pattern above is fine for one-shot transforms. When you want the SDK to autosave after every edit, pass a `PersistAdapter`.
|
||||
|
||||
The filesystem adapter (`@hyperframes/sdk/adapters/fs`) writes to a local directory and keeps a rolling version history.
|
||||
|
||||
```typescript
|
||||
import { openComposition } from "@hyperframes/sdk";
|
||||
import { createFsAdapter } from "@hyperframes/sdk/adapters/fs";
|
||||
import { readFile } from "node:fs/promises";
|
||||
|
||||
// Load the current HTML from disk (or use a template string on first run).
|
||||
const html = await readFile("./project/index.html", "utf8").catch(() => "<div></div>");
|
||||
|
||||
const comp = await openComposition(html, {
|
||||
persist: createFsAdapter({ root: "./project" }),
|
||||
persistPath: "index.html",
|
||||
});
|
||||
|
||||
// Persistence failures surface as events, not thrown errors.
|
||||
comp.on("persist:error", ({ error }) => {
|
||||
console.error("Autosave failed:", error.message);
|
||||
});
|
||||
|
||||
comp.setText("hf-title", "Autosaved title");
|
||||
comp.setStyle("hf-title", { color: "#22C55E" });
|
||||
|
||||
// Drain any pending write before the process exits.
|
||||
await comp.flush();
|
||||
comp.dispose();
|
||||
```
|
||||
|
||||
The adapter writes `./project/index.html` after every mutation and keeps up to 20 version snapshots under `./project/.hf-versions/`.
|
||||
|
||||
<Note>
|
||||
Disabling undo (`history: false`) does **not** disable autosave. The two are independent. Passing `history: false` is only necessary when you are managing the undo stack yourself.
|
||||
</Note>
|
||||
|
||||
## Next steps
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Querying & Editing Elements" icon="magnifying-glass" href="/sdk/guides/querying-and-editing">
|
||||
FindQuery fields, scopedId for sub-compositions, batch semantics, and element handles.
|
||||
</Card>
|
||||
<Card title="Undo, Redo & Patches" icon="arrow-rotate-left" href="/sdk/guides/undo-redo-and-patches">
|
||||
History module, patch events for host sync, and applyPatches loop prevention.
|
||||
</Card>
|
||||
<Card title="Persistence" icon="floppy-disk" href="/sdk/guides/persistence">
|
||||
Adapter selection, version history, and implementing a custom adapter.
|
||||
</Card>
|
||||
<Card title="openComposition reference" icon="door-open" href="/sdk/reference/open-composition">
|
||||
Full option reference — adapters, overrides, coalesce window, and more.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
@@ -0,0 +1,338 @@
|
||||
---
|
||||
title: "Adapters"
|
||||
description: "Persistence and preview adapter interfaces, contracts, and the built-in factory functions."
|
||||
---
|
||||
|
||||
The SDK decouples editing sessions from storage and preview surfaces through two injectable interfaces: `PersistAdapter` and `PreviewAdapter`. Both ship with concrete factory functions you pass to `openComposition()`. You can also implement either interface directly for custom storage backends (S3, IndexedDB, HTTP) or custom preview surfaces.
|
||||
|
||||
## PersistAdapter
|
||||
|
||||
```typescript
|
||||
import type { PersistAdapter } from "@hyperframes/sdk";
|
||||
```
|
||||
|
||||
Injectable storage adapter. Decouples the SDK from the underlying persistence mechanism so the same session code runs in tests (memory), local dev (filesystem), and production (cloud storage).
|
||||
|
||||
### Interface
|
||||
|
||||
```typescript
|
||||
interface PersistAdapter {
|
||||
read(path: string): Promise<string | undefined>;
|
||||
write(path: string, content: string): Promise<void>;
|
||||
flush(): Promise<void>;
|
||||
listVersions(path: string): Promise<PersistVersionEntry[]>;
|
||||
loadFrom(path: string, versionKey: string): Promise<string | undefined>;
|
||||
on(event: "persist:error", handler: (event: PersistErrorEvent) => void): () => void;
|
||||
}
|
||||
```
|
||||
|
||||
<ParamField path="read" type="(path: string) => Promise<string | undefined>">
|
||||
Returns the stored content for `path`, or `undefined` for a path that has never been written. Never throws for a missing path.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="write" type="(path: string, content: string) => Promise<void>">
|
||||
Persists `content` at `path`. Idempotent — a second call with the same path overwrites the prior value. Write failures must not propagate as thrown exceptions; fire `persist:error` instead.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="flush" type="() => Promise<void>">
|
||||
Forces any queued or in-flight writes to commit before resolving. Call before process exit or navigation to prevent data loss.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="listVersions" type="(path: string) => Promise<PersistVersionEntry[]>">
|
||||
Returns the version history for `path` ordered newest-first. Returns an empty array when no versions exist. See `PersistVersionEntry` below.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="loadFrom" type="(path: string, versionKey: string) => Promise<string | undefined>">
|
||||
Returns the HTML content for a specific version identified by `versionKey`. Returns `undefined` when the key does not exist.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="on" type='(event: "persist:error", handler) => () => void'>
|
||||
Subscribes to write failures. Returns an unsubscribe function. Adapters must emit this event — not throw — when a write fails, so the session continues running even when storage is temporarily unavailable.
|
||||
</ParamField>
|
||||
|
||||
### Contract summary
|
||||
|
||||
- `read()` returns `undefined` for a path that has never been written — never throws ENOENT or a 404 equivalent.
|
||||
- `write()` is idempotent; a second write to the same path replaces the stored content.
|
||||
- `flush()` resolves when any pending writes are committed to durable storage.
|
||||
- `listVersions()` returns entries newest-first; `loadFrom()` uses the keys from those entries.
|
||||
- Write errors are emitted via `on('persist:error')`, never thrown — the session keeps running.
|
||||
|
||||
### PersistVersionEntry
|
||||
|
||||
```typescript
|
||||
interface PersistVersionEntry {
|
||||
/** Opaque key identifying this version (adapter-defined format). */
|
||||
key: string;
|
||||
/** Full HTML content — may be omitted by adapters that load content lazily via loadFrom(). */
|
||||
content?: string;
|
||||
timestamp?: number;
|
||||
}
|
||||
```
|
||||
|
||||
The `key` is adapter-defined and opaque to callers — pass it directly to `loadFrom()`. The filesystem adapter encodes milliseconds and a counter into the key; the memory adapter uses an incrementing `"v1"`, `"v2"` … scheme.
|
||||
|
||||
---
|
||||
|
||||
## PreviewAdapter
|
||||
|
||||
```typescript
|
||||
import type { PreviewAdapter } from "@hyperframes/sdk";
|
||||
```
|
||||
|
||||
Injectable preview surface adapter. Decouples the SDK from the host's rendering layer. The SDK is **not** in the 60fps draft loop: your pointer-move handler calls `applyDraft()` directly on the adapter at 60fps, and the SDK only gets involved once per gesture when `commitPreview()` fires to derive and dispatch the resulting op.
|
||||
|
||||
### Interface
|
||||
|
||||
```typescript
|
||||
interface PreviewAdapter {
|
||||
elementAtPoint(x: number, y: number, opts?: { atTime?: number }): ElementAtPointResult | null;
|
||||
applyDraft(id: string, props: DraftProps): void;
|
||||
commitPreview(): void;
|
||||
cancelPreview(): void;
|
||||
select(ids: string[], opts?: { additive?: boolean }): void;
|
||||
on(event: "selection", handler: (ids: string[]) => void): () => void;
|
||||
attachSync(comp: Composition): () => void;
|
||||
}
|
||||
```
|
||||
|
||||
<ParamField path="elementAtPoint" type="(x, y, opts?) => ElementAtPointResult | null">
|
||||
Synchronous hit-test at composition coordinates `(x, y)`. Returns the nearest `[data-hf-id]` element under the point, or `null` for a transparent hit (the composition root, an opacity-0 element, or nothing at all). Requires a same-origin iframe — cross-origin access throws a DOMException. The `atTime` option reflects GSAP state at the current playhead; seeking to a speculative time is not supported.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="applyDraft" type="(id: string, props: DraftProps) => void">
|
||||
Visually translates the preview element at 60fps during a drag: sets the element's CSS `translate` to its pre-drag value composed with the accumulated delta. Works on GSAP-animated elements (a `translate` set after GSAP's first parse composes with the animated transform). The **SDK is not called here** — this is a direct write to the preview surface by your pointer-move handler. Switching `id` mid-drag reverts the previous element's draft first.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="commitPreview" type="() => void">
|
||||
Called once on pointer-up. Reads the accumulated draft delta, derives a `moveElement` op from it, dispatches it into the SDK, emits a patch event, and mirrors the committed position onto the live element (so it holds without a reload). This is the only moment the SDK becomes aware of a drag. If dispatch throws, the draft translate is reverted and the error propagates.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="cancelPreview" type="() => void">
|
||||
Restores the element's pre-drag `translate` without dispatching any op. The model is never changed. Call this on `Escape` keydown or when a drag is aborted.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="select" type="(ids: string[], opts?: { additive?: boolean }) => void">
|
||||
Sets the preview selection and fires `selectionchange` on the session. Pass `{ additive: true }` to merge `ids` into the current selection rather than replacing it.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="on" type='(event: "selection", handler: (ids: string[]) => void) => () => void'>
|
||||
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
|
||||
interface ElementAtPointResult {
|
||||
id: string;
|
||||
tag: string;
|
||||
}
|
||||
```
|
||||
|
||||
The `id` is the element's `data-hf-id` value; `tag` is its lowercase tag name (e.g. `"div"`, `"img"`).
|
||||
|
||||
### DraftProps
|
||||
|
||||
```typescript
|
||||
interface DraftProps {
|
||||
dx?: number;
|
||||
dy?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
```
|
||||
|
||||
`dx` and `dy` are the accumulated drag deltas in composition pixels. `width` and `height` are defined in the interface for forward compatibility but are not yet wired to any op.
|
||||
|
||||
<Note>
|
||||
`ElementAtPointResult` and `DraftProps` are the structural shapes a `PreviewAdapter` produces and consumes. They are **not** re-exported from the `@hyperframes/sdk` barrel — you implement against these shapes rather than importing them.
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Factory Functions
|
||||
|
||||
### createMemoryAdapter
|
||||
|
||||
```typescript
|
||||
import { createMemoryAdapter } from "@hyperframes/sdk";
|
||||
|
||||
function createMemoryAdapter(): PersistAdapter & { injectFault(message: string): void };
|
||||
```
|
||||
|
||||
Returns a `PersistAdapter` backed by an in-process `Map`. Writes are synchronous; `flush()` is a no-op. Versions are keyed `"v1"`, `"v2"` … and stored in memory with full content.
|
||||
|
||||
The returned value also exposes `injectFault(message)` — a test helper that causes the **next** `write()` call to fire a `persist:error` event with `message` instead of committing. Use this in unit tests to verify your error-handling code path.
|
||||
|
||||
```typescript
|
||||
const persist = createMemoryAdapter();
|
||||
|
||||
const comp = await openComposition(html, { persist });
|
||||
comp.setText("hf-title", "Hello");
|
||||
await comp.flush();
|
||||
|
||||
const saved = await persist.read("composition.html");
|
||||
```
|
||||
|
||||
<Note>
|
||||
`createMemoryAdapter()` is best suited for tests, demos, and ephemeral in-process sessions. For local development, use `createFsAdapter()` so edits survive restarts.
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
### createFsAdapter
|
||||
|
||||
```typescript
|
||||
import { createFsAdapter } from "@hyperframes/sdk/adapters/fs";
|
||||
|
||||
function createFsAdapter(opts: FsAdapterOptions): PersistAdapter;
|
||||
```
|
||||
|
||||
**Node.js only.** Returns a `PersistAdapter` that reads and writes files under a root directory. Import from the `@hyperframes/sdk/adapters/fs` subpath — this module uses Node `fs/promises` and is excluded from the browser-safe main bundle.
|
||||
|
||||
#### FsAdapterOptions
|
||||
|
||||
```typescript
|
||||
interface FsAdapterOptions {
|
||||
/** Root directory for composition files. */
|
||||
root: string;
|
||||
/** Max versions to keep per file. Default: 20. */
|
||||
maxVersions?: number;
|
||||
}
|
||||
```
|
||||
|
||||
<ParamField path="root" type="string" required>
|
||||
Absolute or relative path to the directory where composition files are written. Created with `mkdir -p` on first write.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="maxVersions" type="number">
|
||||
Maximum number of historical versions retained per file. Oldest versions are pruned automatically when the limit is exceeded. Defaults to `20`.
|
||||
</ParamField>
|
||||
|
||||
The adapter writes the current composition at `{root}/{path}` and stores version snapshots in `{root}/.hf-versions/{path}/`. Version keys encode `Date.now()` and a monotonic counter (`"1750000000000-0001"`), so `listVersions()` returns them newest-first by lexicographic descending sort.
|
||||
|
||||
```typescript
|
||||
import { openComposition } from "@hyperframes/sdk";
|
||||
import { createFsAdapter } from "@hyperframes/sdk/adapters/fs";
|
||||
|
||||
const comp = await openComposition(html, {
|
||||
persist: createFsAdapter({ root: "./project", maxVersions: 50 }),
|
||||
persistPath: "index.html",
|
||||
});
|
||||
|
||||
comp.setText("hf-title", "Saved");
|
||||
await comp.flush();
|
||||
|
||||
// List saved versions
|
||||
const adapter = createFsAdapter({ root: "./project" });
|
||||
const versions = await adapter.listVersions("index.html");
|
||||
const previous = await adapter.loadFrom("index.html", versions[1].key);
|
||||
```
|
||||
|
||||
<Warning>
|
||||
`createFsAdapter` uses Node.js `fs/promises`. Do not import it in browser or edge environments — import from `@hyperframes/sdk/adapters/fs` (the subpath) so bundlers can tree-shake it.
|
||||
</Warning>
|
||||
|
||||
---
|
||||
|
||||
### createHeadlessAdapter
|
||||
|
||||
```typescript
|
||||
import { createHeadlessAdapter } from "@hyperframes/sdk";
|
||||
|
||||
function createHeadlessAdapter(): PreviewAdapter;
|
||||
```
|
||||
|
||||
Returns a no-op `PreviewAdapter` for headless use: agents, CI pipelines, and server-side rendering. All methods are stubs — `elementAtPoint` always returns `null`, `applyDraft` and `commitPreview` are no-ops, and the `"selection"` event never fires.
|
||||
|
||||
Pass this adapter when you open a composition for programmatic editing and do not need a live preview surface.
|
||||
|
||||
```typescript
|
||||
import { openComposition, createHeadlessAdapter } from "@hyperframes/sdk";
|
||||
|
||||
const comp = await openComposition(html, {
|
||||
preview: createHeadlessAdapter(),
|
||||
});
|
||||
```
|
||||
|
||||
<Note>
|
||||
`openComposition` defaults to a headless preview adapter when none is supplied, so you rarely need to pass it explicitly. The main use case is making the intent clear in code that runs in both headless and browser environments.
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
### createIframePreviewAdapter
|
||||
|
||||
```typescript
|
||||
import { createIframePreviewAdapter } from "@hyperframes/sdk";
|
||||
|
||||
function createIframePreviewAdapter(
|
||||
iframe: HTMLIFrameElement,
|
||||
dispatch?: (op: EditOp) => void,
|
||||
): PreviewAdapter;
|
||||
```
|
||||
|
||||
Returns a `PreviewAdapter` that bridges the SDK to a same-origin `<iframe>` containing the composition. Provides real hit-testing via `elementsFromPoint` (z-stack aware), draft drag support, and selection management.
|
||||
|
||||
**Requirements:**
|
||||
- The iframe must be same-origin (e.g. a `srcdoc` or `blob:` URL). Cross-origin access to `contentDocument` throws a `DOMException`.
|
||||
- Pass your session's `dispatch` callback to enable `commitPreview()` — without it, pointer-up is a no-op on the model.
|
||||
|
||||
**Image-alpha hit-testing:** For `<img>` elements, the adapter samples the alpha channel of the pixel under the pointer using an `OffscreenCanvas`. Transparent pixels fall through to the element behind. Cross-origin images that taint the canvas are treated as opaque (safe fallback, logged once per src).
|
||||
|
||||
```typescript
|
||||
import { openComposition, createIframePreviewAdapter } from "@hyperframes/sdk";
|
||||
|
||||
const iframe = document.querySelector<HTMLIFrameElement>("#preview-frame")!;
|
||||
|
||||
const comp = await openComposition(html);
|
||||
|
||||
const preview = createIframePreviewAdapter(iframe, (op) => comp.dispatch(op));
|
||||
|
||||
// Hit-test at pointer position
|
||||
const hit = preview.elementAtPoint(pointerX, pointerY);
|
||||
if (hit) {
|
||||
preview.select([hit.id]);
|
||||
}
|
||||
|
||||
// Drag: call applyDraft at 60fps, commitPreview on pointer-up
|
||||
preview.applyDraft(hit.id, { dx: 12, dy: -5 });
|
||||
preview.commitPreview();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Export Map
|
||||
|
||||
| Symbol | Imported from |
|
||||
|--------|---------------|
|
||||
| `PersistAdapter`, `PreviewAdapter`, `PersistVersionEntry` | `@hyperframes/sdk` (types only) |
|
||||
| `createMemoryAdapter` | `@hyperframes/sdk` |
|
||||
| `createHeadlessAdapter` | `@hyperframes/sdk` |
|
||||
| `createIframePreviewAdapter`, `resolveNearestHfElement` | `@hyperframes/sdk` |
|
||||
| `createFsAdapter`, `FsAdapterOptions` | `@hyperframes/sdk/adapters/fs` |
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Persistence Guide" icon="floppy-disk" href="/sdk/guides/persistence">
|
||||
How to wire adapters into openComposition, handle errors, and restore versions.
|
||||
</Card>
|
||||
<Card title="Canvas Integration" icon="browser" href="/sdk/guides/canvas-integration">
|
||||
Building a visual editor canvas with the iframe preview adapter and hit-testing.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
@@ -0,0 +1,756 @@
|
||||
---
|
||||
title: "Composition"
|
||||
description: "The main editing surface returned by openComposition — query, mutate, animate, and serialize a composition."
|
||||
---
|
||||
|
||||
`Composition` is the object returned by [`openComposition`](/sdk/reference/open-composition). Every SDK edit goes through this interface. Typed methods are convenience wrappers over `dispatch()`; all validation runs in `dispatch()` regardless of which entry point you use.
|
||||
|
||||
```typescript
|
||||
import { openComposition } from "@hyperframes/sdk";
|
||||
import type { Composition } from "@hyperframes/sdk";
|
||||
|
||||
const comp: Composition = await openComposition(html);
|
||||
```
|
||||
|
||||
For a practical walkthrough see [Querying and editing](/sdk/guides/querying-and-editing).
|
||||
|
||||
---
|
||||
|
||||
## Typed edit methods
|
||||
|
||||
Typed methods are the recommended way to apply common edits. Each one calls `dispatch()` internally, so all patch events, history, and persistence behavior is identical.
|
||||
|
||||
### `setStyle`
|
||||
|
||||
```typescript
|
||||
setStyle(id: HfId, styles: Record<string, string | null>): void
|
||||
```
|
||||
|
||||
Apply inline CSS styles to one element. Use camelCase property names (matching `CSSStyleDeclaration`). Pass `null` as a value to remove that property.
|
||||
|
||||
```typescript
|
||||
comp.setStyle("hf-title", { color: "#FFD60A", fontSize: "96px" });
|
||||
comp.setStyle("hf-card", { borderRadius: null }); // removes border-radius
|
||||
```
|
||||
|
||||
### `setText`
|
||||
|
||||
```typescript
|
||||
setText(id: HfId, value: string): void
|
||||
```
|
||||
|
||||
Replace the display text of an element. Targets the element's direct text content, not all descendant text.
|
||||
|
||||
```typescript
|
||||
comp.setText("hf-headline", "Product launch — June 2026");
|
||||
```
|
||||
|
||||
### `setAttribute`
|
||||
|
||||
```typescript
|
||||
setAttribute(id: HfId, name: string, value: string | null): void
|
||||
```
|
||||
|
||||
Set or remove an HTML attribute. Pass `null` to remove the attribute entirely.
|
||||
|
||||
```typescript
|
||||
comp.setAttribute("hf-logo", "src", "/assets/logo-v2.png");
|
||||
comp.setAttribute("hf-video", "autoplay", null); // removes autoplay
|
||||
```
|
||||
|
||||
### `setTiming`
|
||||
|
||||
```typescript
|
||||
setTiming(id: HfId, timing: { start?: number; duration?: number; trackIndex?: number }): void
|
||||
```
|
||||
|
||||
Update the `data-start`, `data-duration`, and/or `data-track` attributes of one element. All fields are optional; omitted fields are left unchanged.
|
||||
|
||||
```typescript
|
||||
comp.setTiming("hf-title", { start: 0.5, duration: 2.5 });
|
||||
comp.setTiming("hf-cta", { trackIndex: 1 });
|
||||
```
|
||||
|
||||
### `removeElement`
|
||||
|
||||
```typescript
|
||||
removeElement(id: HfId): void
|
||||
```
|
||||
|
||||
Remove an element and all its descendants from the composition. The inverse is an `addElement` of the removed subtree, so `undo()` restores it exactly.
|
||||
|
||||
```typescript
|
||||
comp.removeElement("hf-old-badge");
|
||||
```
|
||||
|
||||
### `addElement`
|
||||
|
||||
```typescript
|
||||
addElement(parent: HfId | null, index: number, html: string): HfId
|
||||
```
|
||||
|
||||
Insert an HTML fragment as a child of `parent` at zero-based sibling `index`. Pass `null` for `parent` to insert at the document body root. The fragment must be single-root and must not contain `<script>` tags. Returns the minted `hf-id` of the inserted root element.
|
||||
|
||||
```typescript
|
||||
const newId = comp.addElement("hf-scene", 0, '<div class="clip">New clip</div>');
|
||||
comp.setText(newId, "Inserted clip");
|
||||
```
|
||||
|
||||
### `setVariableValue`
|
||||
|
||||
```typescript
|
||||
setVariableValue(id: string, value: string | number | boolean | FontValue | ImageValue): void
|
||||
```
|
||||
|
||||
Update a composition variable. Font variables require a `FontValue` object `{ name, source }`; image variables require an `ImageValue` object `{ url, alt?, fit? }`. Scalar variables accept `string | number | boolean`.
|
||||
|
||||
```typescript
|
||||
comp.setVariableValue("brandColor", "#6C5CE7");
|
||||
comp.setVariableValue("brandFont", { name: "Inter", source: "https://fonts.googleapis.com/css2?family=Inter" });
|
||||
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` / `updateVariableDeclaration` / `removeVariableDeclaration`
|
||||
|
||||
```typescript
|
||||
declareVariable(declaration: CompositionVariable): void
|
||||
updateVariableDeclaration(id: string, declaration: CompositionVariable): void
|
||||
removeVariableDeclaration(id: string): void
|
||||
```
|
||||
|
||||
Manage the `data-composition-variables` schema itself. `declareVariable` adds a new
|
||||
typed declaration (creating the attribute when the composition has none);
|
||||
`updateVariableDeclaration` replaces an existing declaration wholesale — the id is
|
||||
immutable, rename by remove + declare; `removeVariableDeclaration` deletes a
|
||||
declaration (the last removal drops the whole attribute). All three validate via
|
||||
`can()` (`E_DUPLICATE_VARIABLE`, `E_VARIABLE_NOT_FOUND`, `E_INVALID_ARGS`,
|
||||
`E_INVALID_VARIABLE_ID` — the id must match `/^[A-Za-z_][A-Za-z0-9_-]*$/` since it
|
||||
becomes a CSS custom-property name, `data-var-*` value, and CLI `--variables` key;
|
||||
and `E_FRAGMENT_COMPOSITION`). Declarations live on the composition's declaration
|
||||
root: the `<html>` element for a full document, or the composition root element
|
||||
(`[data-composition-id]`) for a template / fragment sub-composition. Despite its
|
||||
name, `E_FRAGMENT_COMPOSITION` now fires only when the source has **no** root
|
||||
element at all to carry the schema — a fragment that has a composition root
|
||||
accepts declarations. (The code is kept for compatibility; the fix is "add a
|
||||
composition root element", not necessarily "add an `<html>`".)
|
||||
|
||||
```typescript
|
||||
comp.declareVariable({ id: "title", type: "string", label: "Title", default: "Hello" });
|
||||
comp.updateVariableDeclaration("title", { id: "title", type: "string", label: "Headline", default: "Hello" });
|
||||
comp.removeVariableDeclaration("title");
|
||||
```
|
||||
|
||||
<Note>`removeVariable(id)` is a shorthand alias of `removeVariableDeclaration(id)`.</Note>
|
||||
|
||||
### `getVariableDeclarations` / `getVariableValues` / `validateVariableValues` / `getVariableUsage`
|
||||
|
||||
```typescript
|
||||
getVariableDeclarations(): CompositionVariable[]
|
||||
getVariableValues(overrides?: Record<string, unknown>): Record<string, unknown>
|
||||
validateVariableValues(values: Record<string, unknown>): VariableValidationIssue[]
|
||||
getVariableUsage(): VariableUsageReport
|
||||
```
|
||||
|
||||
Read-only variable APIs (no dispatch). `getVariableDeclarations` returns the typed
|
||||
schema (same strict filter the render pipeline uses). `getVariableValues` resolves
|
||||
values exactly like the runtime's `getVariables()` — declared defaults merged under
|
||||
the given overrides — so callers can predict what a composition script will read for
|
||||
a given `--variables` payload. `validateVariableValues` runs the same checks as
|
||||
`--strict-variables` (`undeclared` / `type-mismatch` / `enum-out-of-range`).
|
||||
`getVariableUsage` statically scans every inline script for `getVariables()` reads
|
||||
and cross-references the schema: `{ usedIds, unusedDeclarations, undeclaredReads,
|
||||
scanIncomplete }` — `scanIncomplete` flips when scripts access variables dynamically,
|
||||
making `usedIds` a lower bound.
|
||||
|
||||
### `setPreviewVariables`
|
||||
|
||||
```typescript
|
||||
setPreviewVariables(values: Record<string, unknown> | null): boolean
|
||||
```
|
||||
|
||||
Apply ephemeral variable values to the preview surface (never persisted — use
|
||||
`setVariableValue` to change a default). Pass `null` to restore declared defaults.
|
||||
Delegates to the preview adapter's optional `setPreviewVariables`; returns `false`
|
||||
when no adapter support is available.
|
||||
|
||||
### `getElementTimings`
|
||||
|
||||
```typescript
|
||||
getElementTimings(): Record<HfId, ElementTimingSnapshot>
|
||||
```
|
||||
|
||||
Return a map of enter/exit times and active GSAP labels for every timed element. The SDK derives `enterAt`/`exitAt` using `data-duration` when present, falling back to `data-end − data-start`. GSAP labels are parsed fresh from the script each call. Elements with no timing attributes are excluded.
|
||||
|
||||
```typescript
|
||||
const timings = comp.getElementTimings();
|
||||
for (const [id, t] of Object.entries(timings)) {
|
||||
console.log(id, t.enterAt, t.exitAt, t.labels);
|
||||
}
|
||||
```
|
||||
|
||||
### `setElementTiming`
|
||||
|
||||
```typescript
|
||||
setElementTiming(map: Record<HfId, { start?: number; duration?: number; trackIndex?: number }>): void
|
||||
```
|
||||
|
||||
Apply a sparse timing map in one batch. All entries are dispatched inside a single `batch()` call so the history sees one undo step. Entries for unknown IDs are silently skipped.
|
||||
|
||||
```typescript
|
||||
comp.setElementTiming({
|
||||
"hf-title": { start: 0, duration: 2 },
|
||||
"hf-caption": { start: 1.5, duration: 1.5 },
|
||||
"hf-cta": { start: 3, duration: 2 },
|
||||
});
|
||||
```
|
||||
|
||||
### `setHold`
|
||||
|
||||
```typescript
|
||||
setHold(id: HfId, hold: ElasticHold): void
|
||||
```
|
||||
|
||||
Set an elastic hold window — a `{ start, end, fill }` range that either freezes or loops the element during a pause in the timeline.
|
||||
|
||||
```typescript
|
||||
comp.setHold("hf-hero", { start: 2, end: 5, fill: "freeze" });
|
||||
```
|
||||
|
||||
### `addGsapTween`
|
||||
|
||||
```typescript
|
||||
addGsapTween(target: HfId, tween: GsapTweenSpec): string
|
||||
```
|
||||
|
||||
Add a GSAP tween to the composition's timeline targeting the given element. Returns the newly assigned `animationId`.
|
||||
|
||||
```typescript
|
||||
const animId = comp.addGsapTween("hf-title", {
|
||||
method: "from",
|
||||
position: 0,
|
||||
duration: 0.6,
|
||||
ease: "power3.out",
|
||||
fromProperties: { opacity: 0, y: 40 },
|
||||
});
|
||||
```
|
||||
|
||||
### `setGsapTween`
|
||||
|
||||
```typescript
|
||||
setGsapTween(animationId: string, properties: Partial<GsapTweenSpec>): void
|
||||
```
|
||||
|
||||
Update properties on an existing tween. Only the fields you supply are changed; the rest remain.
|
||||
|
||||
```typescript
|
||||
comp.setGsapTween(animId, { ease: "back.out(1.7)", duration: 0.8 });
|
||||
```
|
||||
|
||||
### `removeGsapTween`
|
||||
|
||||
```typescript
|
||||
removeGsapTween(animationId: string): void
|
||||
```
|
||||
|
||||
Remove a tween by animation ID.
|
||||
|
||||
```typescript
|
||||
comp.removeGsapTween(animId);
|
||||
```
|
||||
|
||||
### `addWithKeyframes`
|
||||
|
||||
```typescript
|
||||
addWithKeyframes(
|
||||
targetSelector: string,
|
||||
position: number,
|
||||
duration: number,
|
||||
keyframes: KeyframeSpec[],
|
||||
ease?: string,
|
||||
): string
|
||||
```
|
||||
|
||||
Add a new keyframed tween for `targetSelector` at the given timeline position and duration. Returns the newly minted `animationId`. Position is a number in seconds (not a label-relative string).
|
||||
|
||||
```typescript
|
||||
const animId = comp.addWithKeyframes(
|
||||
"#hf-logo",
|
||||
0.5,
|
||||
1.2,
|
||||
[
|
||||
{ percentage: 0, properties: { opacity: 0, scale: 0.8 } },
|
||||
{ percentage: 100, properties: { opacity: 1, scale: 1 } },
|
||||
],
|
||||
"power2.out",
|
||||
);
|
||||
```
|
||||
|
||||
### `replaceWithKeyframes`
|
||||
|
||||
```typescript
|
||||
replaceWithKeyframes(
|
||||
animationId: string,
|
||||
targetSelector: string,
|
||||
position: number,
|
||||
duration: number,
|
||||
keyframes: KeyframeSpec[],
|
||||
ease?: string,
|
||||
): string
|
||||
```
|
||||
|
||||
Atomically remove an existing tween and add a replacement keyframed tween. Returns the replacement's `animationId`. Because position-derived IDs renumber after the removal, the returned ID may differ from the input `animationId` — treat the return value as the new canonical ID.
|
||||
|
||||
```typescript
|
||||
const newId = comp.replaceWithKeyframes(
|
||||
oldAnimId,
|
||||
"#hf-card",
|
||||
1.0,
|
||||
0.8,
|
||||
[
|
||||
{ percentage: 0, properties: { x: -100 } },
|
||||
{ percentage: 100, properties: { x: 0 } },
|
||||
],
|
||||
);
|
||||
```
|
||||
|
||||
### `undo` / `redo`
|
||||
|
||||
```typescript
|
||||
undo(): void
|
||||
redo(): void
|
||||
```
|
||||
|
||||
Step backward or forward through the undo history. No-ops when history is disabled (`history: false` in options) or in embedded mode.
|
||||
|
||||
```typescript
|
||||
comp.setText("hf-title", "Draft");
|
||||
comp.undo(); // reverts to original text
|
||||
comp.redo(); // re-applies "Draft"
|
||||
```
|
||||
|
||||
### `canUndo` / `canRedo`
|
||||
|
||||
```typescript
|
||||
canUndo(): boolean
|
||||
canRedo(): boolean
|
||||
```
|
||||
|
||||
Return `true` when a step in that direction is available. Use to drive the enabled state of undo/redo UI controls.
|
||||
|
||||
```typescript
|
||||
undoButton.disabled = !comp.canUndo();
|
||||
redoButton.disabled = !comp.canRedo();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Query
|
||||
|
||||
### `getElements`
|
||||
|
||||
```typescript
|
||||
getElements(): ElementSnapshot[]
|
||||
```
|
||||
|
||||
Return a flat array of all elements in the composition, including elements nested inside sub-compositions. Each `ElementSnapshot` carries the element's `id`, `scopedId`, `tag`, `inlineStyles`, `classNames`, `attributes`, `text`, timing fields, and `animationIds`. The array is a fresh copy; mutations to the returned objects have no effect.
|
||||
|
||||
```typescript
|
||||
const elements = comp.getElements();
|
||||
const images = elements.filter((el) => el.tag === "img");
|
||||
```
|
||||
|
||||
<Note>
|
||||
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
|
||||
getElement(id: HfId): ElementSnapshot | null
|
||||
```
|
||||
|
||||
Return the snapshot for one element by ID. Accepts both bare IDs (top-level elements) and scoped IDs (sub-composition elements). Returns `null` if no element matches.
|
||||
|
||||
```typescript
|
||||
const el = comp.getElement("hf-title");
|
||||
if (el) {
|
||||
console.log(el.tag, el.inlineStyles);
|
||||
}
|
||||
```
|
||||
|
||||
### `find`
|
||||
|
||||
```typescript
|
||||
find(query: FindQuery): string[]
|
||||
```
|
||||
|
||||
Search elements by structured query. Returns an array of `scopedId` strings for matching elements. All fields in `FindQuery` are optional and combined with AND logic.
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `tag` | `string` | Exact HTML tag name, lowercase (`"div"`, `"img"`). |
|
||||
| `text` | `string` | Substring match against the element's `text` field. |
|
||||
| `name` | `string` | Exact match against `data-name` attribute. |
|
||||
| `track` | `number` | Exact track index (`data-track`). |
|
||||
| `composition` | `string` | Filter to elements inside a specific sub-composition host by its `hf-id`. |
|
||||
|
||||
```typescript
|
||||
const headlineIds = comp.find({ name: "headline" });
|
||||
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
|
||||
|
||||
### `selection`
|
||||
|
||||
```typescript
|
||||
selection(): SelectionProxy
|
||||
```
|
||||
|
||||
Return a `SelectionProxy` that resolves `getSelection()` at call time and dispatches operations to all selected elements. The proxy captures the ID list when you call `selection()` — subsequent selection changes do not affect an already-captured proxy.
|
||||
|
||||
```typescript
|
||||
const sel = comp.selection();
|
||||
sel.setStyle({ opacity: "0.5" });
|
||||
sel.removeElement(); // removes all currently selected elements
|
||||
```
|
||||
|
||||
### `element`
|
||||
|
||||
```typescript
|
||||
element(id: HfId): ElementHandle
|
||||
```
|
||||
|
||||
Return a curried `ElementHandle` for a single element. The handle stores only the ID string, so there is no stale-reference hazard if the DOM changes between calls.
|
||||
|
||||
```typescript
|
||||
const title = comp.element("hf-title");
|
||||
title.setText("Hello");
|
||||
title.setStyle({ fontWeight: "700" });
|
||||
```
|
||||
|
||||
### `getSelection`
|
||||
|
||||
```typescript
|
||||
getSelection(): string[]
|
||||
```
|
||||
|
||||
Return a copy of the current selection as an array of `hf-id` strings. Returns `[]` when nothing is selected.
|
||||
|
||||
```typescript
|
||||
const ids = comp.getSelection();
|
||||
console.log(`${ids.length} elements selected`);
|
||||
```
|
||||
|
||||
### `setSelection`
|
||||
|
||||
```typescript
|
||||
setSelection(ids: string[]): void
|
||||
```
|
||||
|
||||
Replace the current selection. Fires `selectionchange`. Pass `[]` to clear the selection. Duplicate IDs are deduplicated automatically.
|
||||
|
||||
```typescript
|
||||
comp.setSelection(["hf-title", "hf-subtitle"]);
|
||||
comp.setSelection([]); // clear
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Advanced / agent
|
||||
|
||||
### `dispatch`
|
||||
|
||||
```typescript
|
||||
dispatch(op: EditOp, opts?: { origin?: unknown }): void
|
||||
```
|
||||
|
||||
Apply a data-shaped edit operation. All typed methods call this internally. Use `dispatch` when you need to apply op types that do not have a typed wrapper, when you are building automation that constructs ops programmatically, or when you need to set a custom `origin`.
|
||||
|
||||
```typescript
|
||||
comp.dispatch({ type: "setStyle", target: "hf-card", styles: { borderRadius: "24px" } });
|
||||
comp.dispatch({ type: "reorderElements", entries: [{ target: "hf-bg", zIndex: 0 }] });
|
||||
comp.dispatch({ type: "setCompositionMetadata", width: 1920, height: 1080, duration: 30 });
|
||||
```
|
||||
|
||||
For the full op catalog see [Edit operations](/sdk/reference/edit-operations).
|
||||
|
||||
You can also target multiple elements with a single `dispatch` by passing an array to `target`:
|
||||
|
||||
```typescript
|
||||
comp.dispatch({ type: "setText", target: ["hf-title", "hf-subtitle"], value: "Coming soon" });
|
||||
```
|
||||
|
||||
### `batch`
|
||||
|
||||
```typescript
|
||||
batch(fn: () => void, opts?: { origin?: unknown }): void
|
||||
```
|
||||
|
||||
Coalesce multiple dispatches into one undo entry and one `patch` event. If the callback throws, all DOM mutations that ran inside the batch are rolled back atomically and the model is restored to its state before `batch()` was called.
|
||||
|
||||
```typescript
|
||||
comp.batch(() => {
|
||||
comp.setText("hf-title", "Version 2");
|
||||
comp.setStyle("hf-title", { color: "#22C55E" });
|
||||
comp.setTiming("hf-title", { start: 1, duration: 3 });
|
||||
});
|
||||
```
|
||||
|
||||
### `can`
|
||||
|
||||
```typescript
|
||||
can(op: EditOp): CanResult
|
||||
```
|
||||
|
||||
Dry-run validation. Returns `{ ok: true }` when `dispatch(op)` would succeed, or `{ ok: false, code, message, hint? }` when it would fail or be a no-op.
|
||||
|
||||
Use this as a feature-detection gate before rendering controls that depend on GSAP timeline availability, or before showing UI that applies optional edits.
|
||||
|
||||
```typescript
|
||||
const result = comp.can({
|
||||
type: "setGsapTween",
|
||||
animationId: "anim-1",
|
||||
properties: { ease: "power3.out" },
|
||||
});
|
||||
|
||||
if (result.ok) {
|
||||
comp.setGsapTween("anim-1", { ease: "power3.out" });
|
||||
} else {
|
||||
console.warn(result.code, result.message);
|
||||
}
|
||||
```
|
||||
|
||||
Stable error codes: `E_TARGET_NOT_FOUND`, `E_NO_ROOT`, `E_NO_GSAP_TIMELINE`, `E_NO_GSAP_SCRIPT`.
|
||||
|
||||
For the full op catalog see [Edit operations](/sdk/reference/edit-operations).
|
||||
|
||||
---
|
||||
|
||||
## Events
|
||||
|
||||
All `on()` overloads return an unsubscribe function. Call it to remove the listener.
|
||||
|
||||
```typescript
|
||||
on(event: "change", handler: () => void): () => void
|
||||
on(event: "selectionchange", handler: (ids: string[]) => void): () => void
|
||||
on(event: "patch", handler: (event: PatchEvent) => void): () => void
|
||||
on(event: "persist:error", handler: (event: PersistErrorEvent) => void): () => void
|
||||
```
|
||||
|
||||
### `change`
|
||||
|
||||
Fires after every committed mutation (whether typed method, `dispatch`, or `batch`). Use this to refresh a canvas or other derived view.
|
||||
|
||||
```typescript
|
||||
const off = comp.on("change", () => {
|
||||
canvas.render(comp.getElements());
|
||||
});
|
||||
|
||||
off(); // unsubscribe
|
||||
```
|
||||
|
||||
### `selectionchange`
|
||||
|
||||
Fires when the selection changes, with the new ID array as the argument.
|
||||
|
||||
```typescript
|
||||
comp.on("selectionchange", (ids) => {
|
||||
propertiesPanel.show(ids);
|
||||
});
|
||||
```
|
||||
|
||||
### `patch`
|
||||
|
||||
Fires after every committed change with a `PatchEvent` containing RFC 6902 forward and inverse patches, the `origin`, and semantic `opTypes`. Use this to mirror edits into a host history stack, collaboration layer, or audit log.
|
||||
|
||||
```typescript
|
||||
comp.on("patch", ({ patches, inversePatches, origin, opTypes }) => {
|
||||
if (origin !== ORIGIN_APPLY_PATCHES) {
|
||||
hostHistory.push({ patches, inversePatches });
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
<Warning>
|
||||
Always guard against `ORIGIN_APPLY_PATCHES` in `patch` listeners. Failing to skip that origin causes an infinite loop when your host replays inverse patches back into the SDK via `applyPatches()`.
|
||||
</Warning>
|
||||
|
||||
### `persist:error`
|
||||
|
||||
Fires when the persist adapter fails to write. The session continues; edits accumulate in memory and the queue retries on the next mutation.
|
||||
|
||||
```typescript
|
||||
comp.on("persist:error", ({ error }) => {
|
||||
toast.error(`Autosave failed: ${error.message}`);
|
||||
});
|
||||
```
|
||||
|
||||
For undo/redo and patch patterns see [Undo, redo, and patches](/sdk/guides/undo-redo-and-patches).
|
||||
|
||||
---
|
||||
|
||||
## Serialization
|
||||
|
||||
### `serialize`
|
||||
|
||||
```typescript
|
||||
serialize(): string
|
||||
```
|
||||
|
||||
Return the current composition as an HTML string reflecting all mutations applied since `openComposition`. The base HTML is never modified; this always returns a fresh serialization of the live document.
|
||||
|
||||
```typescript
|
||||
const updatedHtml = comp.serialize();
|
||||
await fs.writeFile("output.html", updatedHtml);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Embedded extras
|
||||
|
||||
These methods are only meaningful in embedded / override mode. See the [embedded override mode guide](/sdk/guides/embedded-override-mode).
|
||||
|
||||
### `getOverrides`
|
||||
|
||||
```typescript
|
||||
getOverrides(): OverrideSet
|
||||
```
|
||||
|
||||
Return a copy of the current override set — the sparse delta accumulated on top of the base template. Store this instead of the full HTML when the base template is shared.
|
||||
|
||||
```typescript
|
||||
const delta = comp.getOverrides();
|
||||
await db.saveUserOverrides(userId, delta);
|
||||
```
|
||||
|
||||
### `applyPatches`
|
||||
|
||||
```typescript
|
||||
applyPatches(patches: readonly JsonPatchOp[], opts?: { origin?: unknown }): void
|
||||
```
|
||||
|
||||
Apply RFC 6902 patches directly to the live document. The default origin is `ORIGIN_APPLY_PATCHES`, which prevents `patch` listeners from forwarding these back in an undo loop. Use this when the host owns the undo stack and needs to replay inverse patches from its own history.
|
||||
|
||||
```typescript
|
||||
// Host undo: replay inverse patches from the host stack
|
||||
const { inversePatches } = hostHistory.pop();
|
||||
comp.applyPatches(inversePatches);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Lifecycle
|
||||
|
||||
### `flush`
|
||||
|
||||
```typescript
|
||||
flush(): Promise<void>
|
||||
```
|
||||
|
||||
Drain the persist queue. Resolves when any pending write has been committed to the adapter. No-op when no persist adapter was provided. Call this before process exit or navigation away to avoid losing queued writes.
|
||||
|
||||
```typescript
|
||||
comp.setText("hf-title", "Final title");
|
||||
await comp.flush();
|
||||
comp.dispose();
|
||||
```
|
||||
|
||||
### `dispose`
|
||||
|
||||
```typescript
|
||||
dispose(): void
|
||||
```
|
||||
|
||||
Tear down the session: unsubscribe preview adapter listeners, stop the persist queue, stop the history module, and clear all event handlers. After `dispose()`, the `Composition` object is inert; continued calls produce no-ops or errors.
|
||||
|
||||
```typescript
|
||||
comp.dispose();
|
||||
```
|
||||
|
||||
<Note>
|
||||
Always call `dispose()` when you are done with a session. Skipping it leaks listeners attached to the preview adapter.
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Related
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="openComposition" icon="door-open" href="/sdk/reference/open-composition">
|
||||
Session factory — all options and modes.
|
||||
</Card>
|
||||
<Card title="Edit operations" icon="bolt" href="/sdk/reference/edit-operations">
|
||||
Full op catalog for dispatch() and can().
|
||||
</Card>
|
||||
<Card title="Types reference" icon="file-code" href="/sdk/reference/types">
|
||||
All exported TypeScript types — ElementSnapshot, PatchEvent, GsapTweenSpec, and more.
|
||||
</Card>
|
||||
<Card title="Querying and editing" icon="magnifying-glass" href="/sdk/guides/querying-and-editing">
|
||||
Practical guide to find, getElements, and typed edits.
|
||||
</Card>
|
||||
<Card title="Undo, redo, and patches" icon="arrow-uturn-left" href="/sdk/guides/undo-redo-and-patches">
|
||||
History, patch events, ORIGIN_APPLY_PATCHES guard.
|
||||
</Card>
|
||||
<Card title="Embedded override mode" icon="layers" href="/sdk/guides/embedded-override-mode">
|
||||
Template-driven products with host-owned history.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
@@ -0,0 +1,557 @@
|
||||
---
|
||||
title: "Edit Operations"
|
||||
description: "The complete EditOp catalog for dispatch(), can(), and batch()."
|
||||
---
|
||||
|
||||
Every mutation in the SDK is expressed as an `EditOp` — a plain data object with a discriminated `type` field. You can submit ops individually through `dispatch()`, validate them with `can()`, or group them into a single undo/persist step with `batch()`.
|
||||
|
||||
<Tip>
|
||||
Every element op requires an explicit `target` (an `HfId` string or `HfId[]` array). There is no selection-implicit mutation — the SDK never reads the current selection to decide what to edit. The typed methods on `Composition` (such as `comp.setText()` and `comp.setStyle()`) are convenience sugar that construct and dispatch these same ops.
|
||||
</Tip>
|
||||
|
||||
## dispatch, batch, and can
|
||||
|
||||
### dispatch
|
||||
|
||||
```typescript
|
||||
comp.dispatch(op: EditOp, opts?: { origin?: unknown }): void
|
||||
```
|
||||
|
||||
Applies `op` immediately. Emits a `patch` event, persists if an adapter is attached, and records a history entry. The optional `origin` is forwarded verbatim in the resulting `PatchEvent`; use it to label the source of the change (e.g. `"user"`, `"agent"`, or your own string constant).
|
||||
|
||||
```typescript
|
||||
comp.dispatch(
|
||||
{ type: "setStyle", target: "hf-title", styles: { color: "#FFD60A" } },
|
||||
{ origin: "agent" },
|
||||
);
|
||||
```
|
||||
|
||||
### batch
|
||||
|
||||
```typescript
|
||||
comp.batch(fn: () => void, opts?: { origin?: unknown }): void
|
||||
```
|
||||
|
||||
Groups all `dispatch()` calls made inside `fn` into a single undo step, a single persist write, and a single `patch` event. Use `batch()` when several mutations belong together logically.
|
||||
|
||||
```typescript
|
||||
comp.batch(() => {
|
||||
comp.dispatch({ type: "setText", target: "hf-title", value: "Launch Day" });
|
||||
comp.dispatch({ type: "setStyle", target: "hf-title", styles: { fontSize: "96px" } });
|
||||
comp.dispatch({ type: "setTiming", target: "hf-title", start: 0.5, duration: 3 });
|
||||
});
|
||||
```
|
||||
|
||||
### can
|
||||
|
||||
```typescript
|
||||
comp.can(op: EditOp): CanResult
|
||||
```
|
||||
|
||||
Dry-runs `op` without mutating the document. Returns `{ ok: true }` when `dispatch(op)` would succeed, or `{ ok: false; code: string; message: string; hint?: string }` when it would be a no-op or error.
|
||||
|
||||
Use `can()` as a feature-detection gate before rendering controls or applying optional operations:
|
||||
|
||||
```typescript
|
||||
const result = comp.can({
|
||||
type: "setGsapTween",
|
||||
animationId: "anim-1",
|
||||
properties: { ease: "power3.out" },
|
||||
});
|
||||
|
||||
if (result.ok) {
|
||||
comp.setGsapTween("anim-1", { ease: "power3.out" });
|
||||
} else {
|
||||
console.warn(result.code, result.message);
|
||||
}
|
||||
```
|
||||
|
||||
Stable `code` values for `ok: false`:
|
||||
|
||||
| Code | Meaning |
|
||||
|------|---------|
|
||||
| `E_TARGET_NOT_FOUND` | The `target` hf-id does not exist in the document. |
|
||||
| `E_NO_ROOT` | The document has no root element (empty HTML). |
|
||||
| `E_NO_GSAP_TIMELINE` | Op requires a parsed GSAP timeline; parser engine not yet active. |
|
||||
| `E_NO_GSAP_SCRIPT` | Op requires a GSAP `<script>` block; none found in the document. |
|
||||
|
||||
<Note>
|
||||
Several Phase-3b GSAP ops (`addGsapTween`, `setGsapTween`, `removeGsapTween`, `addGsapKeyframe`, `setGsapKeyframe`, `removeGsapKeyframe`, etc.) return `{ ok: false, code: 'E_NO_GSAP_TIMELINE' }` from `can()` until the parser engine ships. `dispatch()` still applies those ops structurally even when `can()` returns false.
|
||||
</Note>
|
||||
|
||||
See also: [`Composition`](/sdk/reference/composition) for the typed-method wrappers, [`Types`](/sdk/reference/types) for `CanResult` and `EditOp`.
|
||||
|
||||
---
|
||||
|
||||
## Element edits
|
||||
|
||||
These ops target one or more elements by explicit hf-id. `target` accepts a single `HfId` string or an `HfId[]` array; when an array is given the op is applied to each id individually within a single batch.
|
||||
|
||||
| `type` | Key fields | What it does |
|
||||
|--------|-----------|--------------|
|
||||
| `setStyle` | `target`, `styles` | Merges CSS inline styles. `null` values remove that property. |
|
||||
| `setText` | `target`, `value` | Replaces the element's direct text content. |
|
||||
| `setAttribute` | `target`, `name`, `value` | Sets or removes an HTML attribute. `null` removes it. Does not touch `style`, `class`, or `data-hf-*`. |
|
||||
| `setTiming` | `target`, `start?`, `duration?`, `trackIndex?` | Updates one or more timing attributes (`data-start`, `data-duration`, `data-track`). Omitted fields are unchanged. |
|
||||
| `setHold` | `target`, `hold` | Sets an elastic hold window; see `ElasticHold` shape below. |
|
||||
| `moveElement` | `target`, `x`, `y` | Repositions the element by setting `data-x` / `data-y` (not CSS `left`/`top`). |
|
||||
| `removeElement` | `target` | Removes the element and all its children from the document. Inverse of `addElement`. |
|
||||
|
||||
### setStyle
|
||||
|
||||
```typescript
|
||||
comp.dispatch({
|
||||
type: "setStyle",
|
||||
target: "hf-card",
|
||||
styles: {
|
||||
borderRadius: "24px",
|
||||
backgroundColor: "#1A1A1A",
|
||||
color: null, // removes the color property
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
`styles` keys are camelCase property names, matching `CSSStyleDeclaration` convention.
|
||||
|
||||
### setText
|
||||
|
||||
```typescript
|
||||
comp.dispatch({
|
||||
type: "setText",
|
||||
target: ["hf-headline", "hf-sub"],
|
||||
value: "Coming soon",
|
||||
});
|
||||
```
|
||||
|
||||
### setAttribute
|
||||
|
||||
```typescript
|
||||
comp.dispatch({
|
||||
type: "setAttribute",
|
||||
target: "hf-logo",
|
||||
name: "src",
|
||||
value: "/assets/logo-v2.png",
|
||||
});
|
||||
|
||||
// Remove an attribute:
|
||||
comp.dispatch({
|
||||
type: "setAttribute",
|
||||
target: "hf-video",
|
||||
name: "autoplay",
|
||||
value: null,
|
||||
});
|
||||
```
|
||||
|
||||
### setTiming
|
||||
|
||||
```typescript
|
||||
comp.dispatch({
|
||||
type: "setTiming",
|
||||
target: "hf-title",
|
||||
start: 1.5,
|
||||
duration: 3,
|
||||
trackIndex: 0,
|
||||
});
|
||||
```
|
||||
|
||||
### setHold
|
||||
|
||||
`hold` is an `ElasticHold` object:
|
||||
|
||||
```typescript
|
||||
comp.dispatch({
|
||||
type: "setHold",
|
||||
target: "hf-badge",
|
||||
hold: {
|
||||
start: 2,
|
||||
end: 5,
|
||||
fill: "freeze", // "freeze" | "loop"
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### moveElement
|
||||
|
||||
Sets the element's position via `data-x` / `data-y` attributes. Coordinates are in composition-space pixels.
|
||||
|
||||
```typescript
|
||||
comp.dispatch({
|
||||
type: "moveElement",
|
||||
target: "hf-logo",
|
||||
x: 120,
|
||||
y: 48,
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Structure
|
||||
|
||||
These ops mutate document structure (add/remove/reorder elements, apply class-level styles, or change composition metadata). They do **not** take a `target` field in the same form as element edits.
|
||||
|
||||
| `type` | Key fields | What it does |
|
||||
|--------|-----------|--------------|
|
||||
| `addElement` | `parent`, `index`, `html` | Inserts an HTML fragment. Returns the minted hf-id via the typed `comp.addElement()` method. |
|
||||
| `reorderElements` | `entries` | Sets inline `z-index` on one or more elements to reorder their stacking. |
|
||||
| `setClassStyle` | `selector`, `styles` | Merges CSS rule styles for a class selector. `null` values remove properties. |
|
||||
| `deleteAllForSelector` | `selector` | Removes all elements matching the CSS selector from the document. |
|
||||
| `setCompositionMetadata` | `width?`, `height?`, `duration?` | Updates top-level composition dimensions and/or total duration. |
|
||||
|
||||
### addElement
|
||||
|
||||
`parent` is the hf-id of the parent element, or `null` to insert at the document body root. `index` is the zero-based sibling index (append if `>= childCount`). `html` must be a single-root HTML fragment and must not contain `<script>`.
|
||||
|
||||
```typescript
|
||||
comp.dispatch({
|
||||
type: "addElement",
|
||||
parent: "hf-scene-1",
|
||||
index: 2,
|
||||
html: '<div class="clip" data-start="3" data-duration="2">New layer</div>',
|
||||
});
|
||||
```
|
||||
|
||||
Use the typed `comp.addElement(parent, index, html)` method to get back the minted hf-id.
|
||||
|
||||
### reorderElements
|
||||
|
||||
Each entry sets `z-index` on one element. Elements must be non-statically positioned for `z-index` to take effect — the caller must ensure `position` is set.
|
||||
|
||||
```typescript
|
||||
comp.dispatch({
|
||||
type: "reorderElements",
|
||||
entries: [
|
||||
{ target: "hf-bg", zIndex: 0 },
|
||||
{ target: "hf-logo", zIndex: 10 },
|
||||
{ target: "hf-text", zIndex: 20 },
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### setClassStyle
|
||||
|
||||
```typescript
|
||||
comp.dispatch({
|
||||
type: "setClassStyle",
|
||||
selector: ".caption",
|
||||
styles: { fontSize: "14px", fontWeight: "600" },
|
||||
});
|
||||
```
|
||||
|
||||
### deleteAllForSelector
|
||||
|
||||
```typescript
|
||||
comp.dispatch({
|
||||
type: "deleteAllForSelector",
|
||||
selector: ".debug-overlay",
|
||||
});
|
||||
```
|
||||
|
||||
### setCompositionMetadata
|
||||
|
||||
```typescript
|
||||
comp.dispatch({
|
||||
type: "setCompositionMetadata",
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
duration: 30,
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Variables
|
||||
|
||||
| `type` | Key fields | What it does |
|
||||
|--------|-----------|--------------|
|
||||
| `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
|
||||
comp.dispatch({
|
||||
type: "setVariableValue",
|
||||
id: "brandColor",
|
||||
value: "#6C5CE7",
|
||||
});
|
||||
|
||||
// Font variable (object-valued — never a CSS string)
|
||||
comp.dispatch({
|
||||
type: "setVariableValue",
|
||||
id: "brand-font",
|
||||
value: {
|
||||
name: "Inter",
|
||||
source: "https://fonts.googleapis.com/css2?family=Inter:wght@400;700",
|
||||
},
|
||||
});
|
||||
|
||||
// Image variable (object-valued)
|
||||
comp.dispatch({
|
||||
type: "setVariableValue",
|
||||
id: "hero-image",
|
||||
value: {
|
||||
url: "/assets/hero.jpg",
|
||||
alt: "Product hero",
|
||||
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" });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## GSAP tweens
|
||||
|
||||
These ops add, edit, and remove GSAP tween entries in the composition's GSAP script block. They operate by `animationId` — a stable string identifier minted when a tween is created. Use `comp.addGsapTween()` or `addWithKeyframes` to mint a new id; the typed wrapper returns it directly.
|
||||
|
||||
<Note>
|
||||
Several GSAP ops require the parser engine to be active. Until it ships, `can()` returns `{ ok: false, code: 'E_NO_GSAP_TIMELINE' }` for these ops. `dispatch()` still applies the op structurally.
|
||||
</Note>
|
||||
|
||||
| `type` | Key fields | What it does |
|
||||
|--------|-----------|--------------|
|
||||
| `addGsapTween` | `target`, `tween` | Adds a new tween for a target hf-id. Returns the minted `animationId` via the typed method. |
|
||||
| `setGsapTween` | `animationId`, `properties` | Partially updates an existing tween's `GsapTweenSpec` fields. |
|
||||
| `removeGsapTween` | `animationId` | Removes the tween entirely. |
|
||||
| `removeGsapProperty` | `animationId`, `property`, `from?` | Removes one animated property from a tween. `from: true` removes from `fromProperties`; otherwise removes from `toProperties` / `properties`. |
|
||||
|
||||
### addGsapTween
|
||||
|
||||
`tween` is a `GsapTweenSpec` object:
|
||||
|
||||
```typescript
|
||||
comp.dispatch({
|
||||
type: "addGsapTween",
|
||||
target: "hf-title",
|
||||
tween: {
|
||||
method: "from",
|
||||
position: 0,
|
||||
duration: 0.8,
|
||||
ease: "power3.out",
|
||||
fromProperties: { opacity: 0, y: 40 },
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
`GsapTweenSpec` fields:
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `method` | `"from" \| "to" \| "fromTo" \| "set"` | GSAP tween method. |
|
||||
| `position` | `number \| string` | Timeline position. Accepts numbers (seconds) or label-relative strings (`"intro+=0.5"`). |
|
||||
| `duration` | `number` | Tween duration in seconds. |
|
||||
| `ease` | `string` | GSAP ease string (e.g. `"power3.out"`). |
|
||||
| `fromProperties` | `Record<string, unknown>` | Start-state properties (used by `from` and `fromTo`). |
|
||||
| `toProperties` | `Record<string, unknown>` | End-state properties (used by `fromTo`). |
|
||||
| `properties` | `Record<string, unknown>` | Animated properties for `to` tweens. |
|
||||
| `repeat` | `number` | Repeat count (`-1` = infinite). |
|
||||
| `yoyo` | `boolean` | Reverse on alternating repeats. |
|
||||
| `stagger` | `number \| Record<string, unknown>` | Stagger config for multi-target tweens. |
|
||||
|
||||
### setGsapTween
|
||||
|
||||
Only the fields you supply are changed; omit any field to leave it unchanged.
|
||||
|
||||
```typescript
|
||||
comp.dispatch({
|
||||
type: "setGsapTween",
|
||||
animationId: "anim-1",
|
||||
properties: { ease: "elastic.out(1, 0.3)", duration: 1.2 },
|
||||
});
|
||||
```
|
||||
|
||||
### removeGsapProperty
|
||||
|
||||
```typescript
|
||||
// Remove 'scale' from the to-properties of an existing tween
|
||||
comp.dispatch({
|
||||
type: "removeGsapProperty",
|
||||
animationId: "anim-1",
|
||||
property: "scale",
|
||||
});
|
||||
|
||||
// Remove 'opacity' from the from-properties
|
||||
comp.dispatch({
|
||||
type: "removeGsapProperty",
|
||||
animationId: "anim-2",
|
||||
property: "opacity",
|
||||
from: true,
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Keyframes
|
||||
|
||||
Keyframe ops work with tweens that use CSS `@keyframes`-style percentage arrays rather than a single `fromProperties`/`toProperties` shape.
|
||||
|
||||
| `type` | Key fields | What it does |
|
||||
|--------|-----------|--------------|
|
||||
| `setGsapKeyframe` | `animationId`, `keyframeIndex`, `position?`, `value?`, `ease?` | Updates one keyframe by index inside an existing keyframed tween. |
|
||||
| `addGsapKeyframe` | `animationId`, `position`, `value` | Appends a new keyframe at a given percentage position. |
|
||||
| `removeGsapKeyframe` | `animationId`, `percentage` | Removes the keyframe at a specific percentage. |
|
||||
| `removeAllKeyframes` | `animationId` | Clears all keyframes from a tween, leaving the tween shell intact. |
|
||||
| `convertToKeyframes` | `animationId`, `resolvedFromValues?` | Converts a `from`/`to`/`fromTo` tween into keyframe form. `resolvedFromValues` provides live computed values for the 0% stop. |
|
||||
| `materializeKeyframes` | `animationId`, `keyframes`, `easeEach?`, `resolvedSelector?` | Writes a complete keyframe set to an existing tween, replacing any previous keyframes. |
|
||||
| `addWithKeyframes` | `targetSelector`, `position`, `duration`, `keyframes`, `ease?` | Creates a new keyframed tween for the given CSS selector. Returns minted `animationId` via typed method. |
|
||||
| `replaceWithKeyframes` | `animationId`, `targetSelector`, `position`, `duration`, `keyframes`, `ease?` | Atomically removes an existing tween and inserts a new keyframed tween. |
|
||||
| `splitIntoPropertyGroups` | `animationId` | Splits a multi-property keyframed tween into one tween per animated property. |
|
||||
| `splitAnimations` | `originalId`, `newId`, `splitTime`, `elementStart`, `elementDuration` | Splits one tween into two at `splitTime`. The second half gets `newId`. |
|
||||
| `unrollDynamicAnimations` | `animationId`, `elements` | Converts a selector-targeted tween that matches multiple elements into per-element keyframe tweens. |
|
||||
|
||||
### materializeKeyframes
|
||||
|
||||
`keyframes` is an array of `{ percentage, properties, ease? }` objects:
|
||||
|
||||
```typescript
|
||||
comp.dispatch({
|
||||
type: "materializeKeyframes",
|
||||
animationId: "anim-3",
|
||||
keyframes: [
|
||||
{ percentage: 0, properties: { opacity: 0, y: 30 } },
|
||||
{ percentage: 100, properties: { opacity: 1, y: 0 }, ease: "power2.out" },
|
||||
],
|
||||
easeEach: "none",
|
||||
resolvedSelector: "#hf-title",
|
||||
});
|
||||
```
|
||||
|
||||
### addWithKeyframes / replaceWithKeyframes
|
||||
|
||||
`position` is a number (seconds) — unlike `GsapTweenSpec.position`, label-relative strings are not accepted here.
|
||||
|
||||
```typescript
|
||||
// Create a new keyframed tween:
|
||||
comp.dispatch({
|
||||
type: "addWithKeyframes",
|
||||
targetSelector: "#hf-card",
|
||||
position: 1.5,
|
||||
duration: 0.6,
|
||||
keyframes: [
|
||||
{ percentage: 0, properties: { scale: 0.8, opacity: 0 } },
|
||||
{ percentage: 100, properties: { scale: 1, opacity: 1 } },
|
||||
],
|
||||
ease: "back.out(1.7)",
|
||||
});
|
||||
|
||||
// Replace an existing tween atomically:
|
||||
comp.dispatch({
|
||||
type: "replaceWithKeyframes",
|
||||
animationId: "anim-4",
|
||||
targetSelector: "#hf-card",
|
||||
position: 1.5,
|
||||
duration: 0.6,
|
||||
keyframes: [
|
||||
{ percentage: 0, properties: { scale: 0.9 } },
|
||||
{ percentage: 100, properties: { scale: 1 } },
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
<Note>
|
||||
After `replaceWithKeyframes`, position-derived tween IDs renumber. Re-query `comp.getElement(id).animationIds` to discover the new ID rather than assuming it matches the old one.
|
||||
</Note>
|
||||
|
||||
`KeyframeSpec` fields:
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `percentage` | `number` | Keyframe stop position (0–100). |
|
||||
| `properties` | `Record<string, number \| string>` | CSS / GSAP properties at this stop. |
|
||||
| `ease` | `string` | Ease applied from this stop to the next. |
|
||||
| `auto` | `boolean` | GSAP endpoint flag — emitted as numeric `_auto: 1`. |
|
||||
|
||||
---
|
||||
|
||||
## Labels
|
||||
|
||||
GSAP timeline labels mark named positions (in seconds) in the master timeline. Labels are referenced in `GsapTweenSpec.position` as strings like `"intro"` or `"intro+=0.5"`.
|
||||
|
||||
| `type` | Key fields | What it does |
|
||||
|--------|-----------|--------------|
|
||||
| `addLabel` | `name`, `position` | Adds a named label at a timeline position (seconds). |
|
||||
| `removeLabel` | `name` | Removes a named label. |
|
||||
|
||||
```typescript
|
||||
comp.dispatch({ type: "addLabel", name: "intro", position: 0 });
|
||||
comp.dispatch({ type: "addLabel", name: "outro", position: 8 });
|
||||
|
||||
// Tween positioned relative to a label:
|
||||
comp.dispatch({
|
||||
type: "addGsapTween",
|
||||
target: "hf-cta",
|
||||
tween: {
|
||||
method: "from",
|
||||
position: "outro-=0.5",
|
||||
duration: 0.4,
|
||||
fromProperties: { opacity: 0 },
|
||||
},
|
||||
});
|
||||
|
||||
comp.dispatch({ type: "removeLabel", name: "intro" });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Arc paths
|
||||
|
||||
Arc path ops control the motion path of a GSAP tween — the curve along which an element travels.
|
||||
|
||||
| `type` | Key fields | What it does |
|
||||
|--------|-----------|--------------|
|
||||
| `setArcPath` | `animationId`, `config` | Creates or replaces the arc-path config on a tween. |
|
||||
| `updateArcSegment` | `animationId`, `segmentIndex`, `update` | Updates one segment's curviness or control points. |
|
||||
| `removeArcPath` | `animationId` | Removes the arc-path from a tween, reverting to a straight-line path. |
|
||||
|
||||
### setArcPath
|
||||
|
||||
`config` shape:
|
||||
|
||||
```typescript
|
||||
comp.dispatch({
|
||||
type: "setArcPath",
|
||||
animationId: "anim-5",
|
||||
config: {
|
||||
enabled: true,
|
||||
autoRotate: true, // boolean, or a number (degrees offset)
|
||||
segments: [
|
||||
{
|
||||
curviness: 1.5,
|
||||
cp1: { x: 200, y: -80 }, // first control point
|
||||
cp2: { x: 400, y: 20 }, // second control point
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `enabled` | `boolean` | Whether the arc path is active. |
|
||||
| `autoRotate` | `boolean \| number` | `true` = auto-rotate to follow path tangent; a number offsets the rotation by that many degrees. |
|
||||
| `segments` | `Array<{ curviness?, cp1?, cp2? }>` | Per-segment path definition. `curviness` is a GSAP Bezier curviness value; `cp1`/`cp2` are control-point coordinates in composition space. |
|
||||
|
||||
### updateArcSegment
|
||||
|
||||
```typescript
|
||||
comp.dispatch({
|
||||
type: "updateArcSegment",
|
||||
animationId: "anim-5",
|
||||
segmentIndex: 0,
|
||||
update: { curviness: 2, cp1: { x: 220, y: -100 } },
|
||||
});
|
||||
```
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
---
|
||||
title: "openComposition"
|
||||
description: "Open a composition HTML string for editing. Returns a Composition session."
|
||||
---
|
||||
|
||||
`openComposition` is the single entry point for every SDK session. It parses the composition HTML, stamps stable `hf-id` attributes on any elements that lack them, and returns a [`Composition`](/sdk/reference/composition) ready to receive edits.
|
||||
|
||||
```typescript
|
||||
import { openComposition } from "@hyperframes/sdk";
|
||||
|
||||
const comp = await openComposition(html, opts?);
|
||||
```
|
||||
|
||||
## Signature
|
||||
|
||||
```typescript
|
||||
openComposition(html: string, opts?: OpenCompositionOptions): Promise<Composition>
|
||||
```
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `html` | `string` | The full composition HTML string. |
|
||||
| `opts` | `OpenCompositionOptions` | Optional configuration — adapters, overrides, history behavior. |
|
||||
|
||||
## Modes
|
||||
|
||||
The same function covers three deployment patterns. Which mode you get depends entirely on what you pass in `opts`.
|
||||
|
||||
**Standalone (default)** — You own the HTML, and the SDK owns history and persistence. Pass a `persist` adapter to enable autosave; undo/redo attaches automatically unless you opt out with `history: false`.
|
||||
|
||||
**Embedded / override mode** — Pass `overrides` to open a shared base template with a user-specific delta applied on top. The SDK accumulates further edits into the override set, and you store only the delta. History is disabled by default in embedded mode (the host's stack drives undo). See the [embedded override mode guide](/sdk/guides/embedded-override-mode).
|
||||
|
||||
**Headless / agent** — Omit both `persist` and `overrides`. The SDK is a pure in-memory transform. Call `serialize()` when done and discard the session.
|
||||
|
||||
## Options
|
||||
|
||||
<ParamField path="persist" type="PersistAdapter">
|
||||
Storage adapter for autosave. After every committed change the SDK enqueues a write to this adapter. The session fires `persist:error` events instead of throwing on write failures.
|
||||
|
||||
Built-in adapters: `createMemoryAdapter()` (tests/demos) and `createFsAdapter()` (Node.js). Custom adapters implement the `PersistAdapter` interface, exported from `@hyperframes/sdk`. (`createHeadlessAdapter()` is a `PreviewAdapter` for the `preview` field below — not a persist adapter.)
|
||||
|
||||
See the [persistence guide](/sdk/guides/persistence) for adapter selection and configuration.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="persistPath" type="string" default='"composition.html"'>
|
||||
Relative path the persist queue passes to the adapter's `write` call. Immutable for the lifetime of the session — changing it would create a new file on disk without removing the old one. You must close and reopen the session to change the save path.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="preview" type="PreviewAdapter">
|
||||
Live-preview adapter that bridges the SDK model to a rendering surface. When provided, the adapter's selection events update `comp.getSelection()` so pointer-driven hit tests stay in sync with the model without crossing the mutation path.
|
||||
|
||||
Built-in: `createIframePreviewAdapter()` for same-origin composition iframes, `createHeadlessAdapter()` when no preview surface exists.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="overrides" type="OverrideSet">
|
||||
Sparse `{ "hfId.prop.path": value }` map that is applied on top of the base HTML before the session starts. Every subsequent edit accumulates into this map. The current map is readable via `comp.getOverrides()`.
|
||||
|
||||
Providing `overrides` activates **embedded mode**: history defaults to off, and the SDK emits patches rather than owning the undo stack. The host stores only the delta and replays it on each open, so the base template can be updated independently.
|
||||
|
||||
See the [embedded override mode guide](/sdk/guides/embedded-override-mode).
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="trackedOrigins" type="unknown[]">
|
||||
Whitelist of origin values whose mutations enter the undo stack. Defaults to all origins except `ORIGIN_APPLY_PATCHES` (the sentinel used by `applyPatches()`). Use this when you dispatch edits from multiple sources and only want some of them to be undoable — for example, to exclude programmatic bulk updates from user-facing undo history.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="coalesceMs" type="number" default={300}>
|
||||
Auto-coalesce window in milliseconds. Consecutive mutations arriving within this window are merged into a single undo entry. Increase for slower interactions, decrease for tighter history granularity, or set to `0` to disable coalescing entirely.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="history" type="false">
|
||||
Pass `false` to skip attaching the undo/redo module. The session's `undo()`, `redo()`, `canUndo()`, and `canRedo()` become no-ops.
|
||||
|
||||
Use this when the host owns the undo stack and the SDK's history module would duplicate it. Note that disabling history does **not** disable persistence — autosave runs independently so that disabling undo does not silently drop disk writes.
|
||||
|
||||
In embedded mode (when `overrides` is provided), history defaults to off and this option is unnecessary.
|
||||
</ParamField>
|
||||
|
||||
## Examples
|
||||
|
||||
### Headless agent edit
|
||||
|
||||
```typescript
|
||||
import { openComposition } from "@hyperframes/sdk";
|
||||
|
||||
const comp = await openComposition(html);
|
||||
|
||||
const [titleId] = comp.find({ name: "headline" });
|
||||
if (titleId) {
|
||||
comp.setText(titleId, "New headline");
|
||||
comp.setStyle(titleId, { color: "#FFD60A" });
|
||||
}
|
||||
|
||||
const updatedHtml = comp.serialize();
|
||||
comp.dispose();
|
||||
```
|
||||
|
||||
### Standalone with filesystem persistence
|
||||
|
||||
```typescript
|
||||
import { openComposition } from "@hyperframes/sdk";
|
||||
import { createFsAdapter } from "@hyperframes/sdk/adapters/fs";
|
||||
|
||||
const comp = await openComposition(html, {
|
||||
persist: createFsAdapter({ root: "./project" }),
|
||||
persistPath: "index.html",
|
||||
});
|
||||
|
||||
comp.on("persist:error", ({ error }) => {
|
||||
console.error("Autosave failed:", error.message);
|
||||
});
|
||||
|
||||
comp.setText("hf-title", "Launch day");
|
||||
await comp.flush(); // drain any pending write before process exit
|
||||
comp.dispose();
|
||||
```
|
||||
|
||||
### Embedded mode with overrides
|
||||
|
||||
```typescript
|
||||
import { openComposition } from "@hyperframes/sdk";
|
||||
|
||||
// Stored override delta for one customer
|
||||
const savedOverrides = {
|
||||
"hf-title.text": "Acme — Q3 Review",
|
||||
"hf-logo.attr.src": "/customers/acme/logo.png",
|
||||
};
|
||||
|
||||
const comp = await openComposition(baseTemplateHtml, {
|
||||
overrides: savedOverrides,
|
||||
history: false,
|
||||
});
|
||||
|
||||
// Host drives undo; SDK just accumulates overrides
|
||||
comp.on("patch", ({ patches, inversePatches, origin }) => {
|
||||
if (origin !== ORIGIN_APPLY_PATCHES) {
|
||||
hostHistoryStack.push({ patches, inversePatches });
|
||||
}
|
||||
});
|
||||
|
||||
comp.setStyle("hf-title", { color: "#0EA5E9" });
|
||||
|
||||
// Persist the updated delta only — not the full HTML
|
||||
const nextOverrides = comp.getOverrides();
|
||||
await db.save(customerId, nextOverrides);
|
||||
comp.dispose();
|
||||
```
|
||||
|
||||
### Disabling history to reduce overhead
|
||||
|
||||
```typescript
|
||||
const comp = await openComposition(html, {
|
||||
persist: createFsAdapter({ root: "./output" }),
|
||||
history: false, // host owns undo; persist still runs
|
||||
});
|
||||
```
|
||||
|
||||
<Note>
|
||||
History defaults to **on** in standalone mode and **off** in embedded mode. Passing `history: false` in standalone mode is only necessary when you are managing the undo stack yourself and do not want the SDK to create a parallel copy.
|
||||
</Note>
|
||||
|
||||
## Related
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Composition" icon="cube" href="/sdk/reference/composition">
|
||||
The full editing surface returned by openComposition.
|
||||
</Card>
|
||||
<Card title="Persistence guide" icon="floppy-disk" href="/sdk/guides/persistence">
|
||||
Adapter selection, version history, and custom adapters.
|
||||
</Card>
|
||||
<Card title="Embedded override mode" icon="layers" href="/sdk/guides/embedded-override-mode">
|
||||
Template-driven products with host-owned history.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
@@ -0,0 +1,769 @@
|
||||
---
|
||||
title: "Types"
|
||||
description: "All exported types from @hyperframes/sdk."
|
||||
---
|
||||
|
||||
This page documents every type exported from `@hyperframes/sdk`. Types are verified against `packages/sdk/src/types.ts` and the export barrel at `packages/sdk/src/index.ts`.
|
||||
|
||||
See also: [`Composition`](/sdk/reference/composition) for the main session interface, [`Edit Operations`](/sdk/reference/edit-operations) for the `EditOp` catalog.
|
||||
|
||||
---
|
||||
|
||||
## HfId
|
||||
|
||||
```typescript
|
||||
type HfId = string;
|
||||
```
|
||||
|
||||
A stable HyperFrames element identifier — the value of the `data-hf-id` attribute stamped on every editable element by `ensureHfIds`. Use `HfId` wherever you would write a raw `string` to signal that the value is an element id.
|
||||
|
||||
---
|
||||
|
||||
## HyperFramesElement
|
||||
|
||||
Full DOM-level view of one editable element. Returned by `comp.getElements()` and `comp.getElement(id)`.
|
||||
|
||||
```typescript
|
||||
interface HyperFramesElement {
|
||||
readonly id: string;
|
||||
readonly scopedId: string;
|
||||
readonly tag: string;
|
||||
readonly children: readonly HyperFramesElement[];
|
||||
readonly inlineStyles: Readonly<Record<string, string>>;
|
||||
readonly classNames: readonly string[];
|
||||
readonly attributes: Readonly<Record<string, string>>;
|
||||
readonly text: string | null;
|
||||
readonly start: number | null;
|
||||
readonly duration: number | null;
|
||||
readonly trackIndex: number | null;
|
||||
readonly animationIds: readonly string[];
|
||||
}
|
||||
```
|
||||
|
||||
<ResponseField name="id" type="string">
|
||||
The element's `data-hf-id` value — unique within the top-level document.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="scopedId" type="string">
|
||||
Fully-qualified scoped identifier: `host-chain-prefix/leaf`, separated by `/`. For top-level elements `scopedId === id`. For elements inside inlined sub-compositions the format is `"hf-HOST/hf-LEAF"` (any depth). Use `scopedId` — not `id` — as the canonical key in `dispatch()` targets, `getElement()`, `find()`, and `OverrideSet` keys when addressing sub-composition elements.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="tag" type="string">
|
||||
Lowercase HTML tag name (`"div"`, `"img"`, `"p"`, …).
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="children" type="readonly HyperFramesElement[]">
|
||||
Direct child elements. Build a flat list with `flatElements()` from `@hyperframes/sdk` utilities.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="inlineStyles" type="Readonly<Record<string, string>>">
|
||||
Inline CSS properties in camelCase, mirroring `CSSStyleDeclaration` convention (`fontSize`, not `font-size`).
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="classNames" type="readonly string[]">
|
||||
The element's class list.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="attributes" type="Readonly<Record<string, string>>">
|
||||
All HTML attributes except `style`, `class`, and `data-hf-*` (those are represented elsewhere in the model).
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="text" type="string | null">
|
||||
The element's direct text content, intended as the `setText` target. Not a full descendant-text snapshot.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="start" type="number | null">
|
||||
Timing start in seconds (`data-start`). `null` when the element carries no timing attribute.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="duration" type="number | null">
|
||||
Timing duration in seconds (`data-duration`). `null` when not set.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="trackIndex" type="number | null">
|
||||
Zero-based track index (`data-track`). `null` when not set.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="animationIds" type="readonly string[]">
|
||||
IDs of GSAP tweens whose target is this element. Pass these to the GSAP tween ops in `dispatch()`.
|
||||
</ResponseField>
|
||||
|
||||
---
|
||||
|
||||
## ElementSnapshot
|
||||
|
||||
```typescript
|
||||
type ElementSnapshot = HyperFramesElement;
|
||||
```
|
||||
|
||||
Type alias for `HyperFramesElement`. Used in return types of `getElements()` and `getElement()` to signal that the value is a read-only snapshot — it does not update after subsequent mutations.
|
||||
|
||||
---
|
||||
|
||||
## SdkDocument
|
||||
|
||||
The SDK's in-memory document model. Returned by `buildDocument()`.
|
||||
|
||||
```typescript
|
||||
interface SdkDocument {
|
||||
readonly roots: readonly HyperFramesElement[];
|
||||
readonly gsapScript: string | null;
|
||||
readonly styles: string | null;
|
||||
readonly width: number | null;
|
||||
readonly height: number | null;
|
||||
readonly compositionDuration: number | null;
|
||||
readonly html: string;
|
||||
}
|
||||
```
|
||||
|
||||
<ResponseField name="roots" type="readonly HyperFramesElement[]">
|
||||
Top-level elements of the composition body. Walk the tree via `.children`.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="gsapScript" type="string | null">
|
||||
Raw contents of the composition's GSAP `<script>` block, if present.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="styles" type="string | null">
|
||||
Raw contents of the composition's `<style>` block, if present.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="width" type="number | null">
|
||||
Composition width in pixels from `data-width` on the root element. `null` if not set.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="height" type="number | null">
|
||||
Composition height in pixels from `data-height`. `null` if not set.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="compositionDuration" type="number | null">
|
||||
Total duration in seconds from `data-duration` on the root element. `null` if not set.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="html" type="string">
|
||||
Build-time snapshot of the `ensureHfIds`-stamped HTML. This value is **never updated** after mutations — call `comp.serialize()` to get the current document state.
|
||||
</ResponseField>
|
||||
|
||||
---
|
||||
|
||||
## OverrideSet
|
||||
|
||||
```typescript
|
||||
type OverrideSet = Record<
|
||||
string,
|
||||
string | number | boolean | Record<string, unknown> | null
|
||||
>;
|
||||
```
|
||||
|
||||
A sparse map of overrides layered on top of a base composition template. Used in embedded override mode (T3) — pass as `overrides` to `openComposition()`, or read the accumulated delta with `comp.getOverrides()`.
|
||||
|
||||
**Key format:** `hfId.prop.path`, where:
|
||||
|
||||
- `hfId.style.fontSize` — inline style property override
|
||||
- `hfId.text` — text content override
|
||||
- `hfId.attr.src` — attribute override
|
||||
- `hfId` (key only, value `null`) — element removed by user
|
||||
- `var.{id}` — variable override; value is a scalar or an object (`FontValue` / `ImageValue`)
|
||||
|
||||
**`null` value** = removal marker: the element or property was deleted by the user.
|
||||
|
||||
```typescript
|
||||
const overrides: OverrideSet = {
|
||||
"hf-title.text": "Customer headline",
|
||||
"hf-logo.attr.src": "/customers/acme/logo.png",
|
||||
"hf-title.style.color": "#0EA5E9",
|
||||
"hf-old-badge": null, // element removed
|
||||
"var.brand-font": { // font variable
|
||||
name: "Inter",
|
||||
source: "https://fonts.googleapis.com/css2?family=Inter",
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
<Warning>
|
||||
The value union includes `Record<string, unknown>` to admit object-valued variables (font, image). Code that reads an `OverrideSet` value must narrow before assuming a scalar — an object value type-checks anywhere `unknown` is accepted.
|
||||
</Warning>
|
||||
|
||||
---
|
||||
|
||||
## FindQuery
|
||||
|
||||
Passed to `comp.find(query)` to search elements by descriptor.
|
||||
|
||||
```typescript
|
||||
interface FindQuery {
|
||||
tag?: string;
|
||||
text?: string;
|
||||
name?: string;
|
||||
track?: number;
|
||||
composition?: string;
|
||||
}
|
||||
```
|
||||
|
||||
<ResponseField name="tag" type="string">
|
||||
Match by lowercase HTML tag name (`"img"`, `"p"`, …).
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="text" type="string">
|
||||
Match by the element's direct text content (case-sensitive substring match — implemented with `String.includes`).
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="name" type="string">
|
||||
Match by `data-name` attribute value.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="track" type="number">
|
||||
Match by zero-based track index (`data-track`).
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="composition" type="string">
|
||||
Restrict results to elements inside a specific sub-composition, identified by the host element's hf-id.
|
||||
</ResponseField>
|
||||
|
||||
All fields are optional; non-null fields are ANDed.
|
||||
|
||||
```typescript
|
||||
// Find all text elements on track 2:
|
||||
const ids = comp.find({ tag: "p", track: 2 });
|
||||
|
||||
// Find inside a sub-composition:
|
||||
const subIds = comp.find({ composition: "hf-scene-2", tag: "img" });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## GsapTweenSpec
|
||||
|
||||
Describes a single GSAP tween entry to create or update.
|
||||
|
||||
```typescript
|
||||
interface GsapTweenSpec {
|
||||
method: "from" | "to" | "fromTo" | "set";
|
||||
position?: number | string;
|
||||
duration?: number;
|
||||
ease?: string;
|
||||
fromProperties?: Record<string, unknown>;
|
||||
toProperties?: Record<string, unknown>;
|
||||
properties?: Record<string, unknown>;
|
||||
repeat?: number;
|
||||
yoyo?: boolean;
|
||||
stagger?: number | Record<string, unknown>;
|
||||
}
|
||||
```
|
||||
|
||||
<ResponseField name="method" type='"from" | "to" | "fromTo" | "set"'>
|
||||
GSAP tween method. `"from"` animates from the given values to the computed values; `"to"` animates toward the given values; `"fromTo"` animates from `fromProperties` to `toProperties`; `"set"` snaps without interpolation.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="position" type="number | string">
|
||||
Timeline insertion position. A number is seconds from the timeline start. A string is a label-relative offset such as `"intro"` or `"outro-=0.5"`. Omit to append.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="duration" type="number">
|
||||
Tween duration in seconds.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="ease" type="string">
|
||||
GSAP ease string, e.g. `"power3.out"`, `"elastic.out(1, 0.3)"`, `"none"`.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="fromProperties" type="Record<string, unknown>">
|
||||
Start-state property map — used by `"from"` and `"fromTo"` methods.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="toProperties" type="Record<string, unknown>">
|
||||
End-state property map — used by `"fromTo"` method.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="properties" type="Record<string, unknown>">
|
||||
Animated property map — used by `"to"` method.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="repeat" type="number">
|
||||
How many times to repeat. `-1` = infinite.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="yoyo" type="boolean">
|
||||
Reverse direction on alternating repeats.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="stagger" type="number | Record<string, unknown>">
|
||||
GSAP stagger config for multi-target tweens.
|
||||
</ResponseField>
|
||||
|
||||
---
|
||||
|
||||
## KeyframeSpec
|
||||
|
||||
A single keyframe entry for `addWithKeyframes` and `replaceWithKeyframes`. This is a structural shape, not a runtime export — you pass plain objects of this form to those methods; it is **not** re-exported from the `@hyperframes/sdk` barrel, so there is nothing to import.
|
||||
|
||||
```typescript
|
||||
interface KeyframeSpec {
|
||||
percentage: number;
|
||||
properties: Record<string, number | string>;
|
||||
ease?: string;
|
||||
auto?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
<ResponseField name="percentage" type="number">
|
||||
Keyframe stop position within the tween, from 0 to 100.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="properties" type="Record<string, number | string>">
|
||||
CSS and GSAP property values at this keyframe stop.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="ease" type="string">
|
||||
Ease applied from this stop to the next.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="auto" type="boolean">
|
||||
GSAP endpoint flag. When `true`, emitted as numeric `_auto: 1` in the generated GSAP script.
|
||||
</ResponseField>
|
||||
|
||||
---
|
||||
|
||||
## ElasticHold
|
||||
|
||||
Controls the elastic hold (freeze or loop) window for an element.
|
||||
|
||||
```typescript
|
||||
interface ElasticHold {
|
||||
start: number;
|
||||
end: number;
|
||||
fill: "freeze" | "loop";
|
||||
}
|
||||
```
|
||||
|
||||
<ResponseField name="start" type="number">
|
||||
Start of the hold window in seconds.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="end" type="number">
|
||||
End of the hold window in seconds.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="fill" type='"freeze" | "loop"'>
|
||||
`"freeze"` holds the last frame statically; `"loop"` cycles the animation within the window.
|
||||
</ResponseField>
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||
```typescript
|
||||
interface FontValue {
|
||||
name: string;
|
||||
source: string;
|
||||
}
|
||||
```
|
||||
|
||||
<ResponseField name="name" type="string">
|
||||
CSS `font-family` value, e.g. `"Inter"` or `"'Playfair Display'"`.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="source" type="string">
|
||||
Stylesheet URL to load for this font, e.g. a Google Fonts CSS URL.
|
||||
</ResponseField>
|
||||
|
||||
---
|
||||
|
||||
## ImageValue
|
||||
|
||||
Object value for an `image` composition variable. Always an object — never a raw URL string.
|
||||
|
||||
```typescript
|
||||
interface ImageValue {
|
||||
url: string;
|
||||
alt?: string;
|
||||
fit?: "cover" | "contain" | "fill" | "none" | "scale-down";
|
||||
}
|
||||
```
|
||||
|
||||
<ResponseField name="url" type="string">
|
||||
Image source URL.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="alt" type="string">
|
||||
Alternative text for accessibility.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="fit" type='"cover" | "contain" | "fill" | "none" | "scale-down"'>
|
||||
Object-fit behavior for the image within its container. Maps to the CSS `object-fit` property.
|
||||
</ResponseField>
|
||||
|
||||
---
|
||||
|
||||
## JsonPatchOp
|
||||
|
||||
An emit-only subset of [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902) JSON Patch. The SDK never emits `move`, `copy`, or `test` ops, and `applyPatches()` ignores operations outside this subset.
|
||||
|
||||
```typescript
|
||||
interface JsonPatchOp {
|
||||
op: "add" | "remove" | "replace";
|
||||
path: string;
|
||||
value?: unknown;
|
||||
}
|
||||
```
|
||||
|
||||
<ResponseField name="op" type='"add" | "remove" | "replace"'>
|
||||
The patch operation type.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="path" type="string">
|
||||
RFC 6902 JSON Pointer path to the document location being changed.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="value" type="unknown">
|
||||
New value for `"add"` and `"replace"` ops. Omitted for `"remove"`.
|
||||
</ResponseField>
|
||||
|
||||
<Warning>
|
||||
Hosts feeding patches back into `applyPatches()` must restrict themselves to `add`, `remove`, and `replace`. Other RFC 6902 op types are silently ignored.
|
||||
</Warning>
|
||||
|
||||
---
|
||||
|
||||
## PatchEvent
|
||||
|
||||
Emitted by `comp.on('patch', handler)` after every committed change (single `dispatch()` or completed `batch()`).
|
||||
|
||||
```typescript
|
||||
interface PatchEvent {
|
||||
readonly formatVersion: 1;
|
||||
readonly patches: readonly JsonPatchOp[];
|
||||
readonly inversePatches: readonly JsonPatchOp[];
|
||||
readonly origin: unknown;
|
||||
readonly opTypes: readonly string[];
|
||||
}
|
||||
```
|
||||
|
||||
<ResponseField name="formatVersion" type="1">
|
||||
Always `1`. A bump in `formatVersion` is a breaking change. Hosts should check this value once on startup and reject unknown versions.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="patches" type="readonly JsonPatchOp[]">
|
||||
Forward patches that transform the previous document state into the new state.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="inversePatches" type="readonly JsonPatchOp[]">
|
||||
Inverse patches that undo this change — apply to the new state to return to the previous state. Store these for host-side undo stacks.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="origin" type="unknown">
|
||||
Re-emitted verbatim from the mutation entry. Identify automated patches by checking `origin === ORIGIN_APPLY_PATCHES`.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="opTypes" type="readonly string[]">
|
||||
Semantic op names from the `EditOp.type` field (e.g. `["setStyle", "setText"]`). Intended for analytics and history labels. **Not versioned** — do not use in branching logic.
|
||||
</ResponseField>
|
||||
|
||||
**Loop prevention.** When a host mirrors patch events into the SDK via `applyPatches()`, it must skip events that originated from that call to avoid infinite loops:
|
||||
|
||||
```typescript
|
||||
comp.on("patch", ({ patches, inversePatches, origin }) => {
|
||||
if (origin === ORIGIN_APPLY_PATCHES) return;
|
||||
saveToHostHistory({ patches, inversePatches });
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CanResult
|
||||
|
||||
Returned by `comp.can(op)`. See [`Edit Operations`](/sdk/reference/edit-operations) for the full usage pattern.
|
||||
|
||||
```typescript
|
||||
type CanResult =
|
||||
| { ok: true }
|
||||
| { ok: false; code: string; message: string; hint?: string };
|
||||
```
|
||||
|
||||
<ResponseField name="ok" type="boolean">
|
||||
`true` when `dispatch(op)` would succeed. `false` when it would be a no-op or error.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="code" type="string">
|
||||
Stable error code for `ok: false` — safe to use in `switch` statements. See the code table in [Edit Operations](/sdk/reference/edit-operations#dispatch-batch-and-can).
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="message" type="string">
|
||||
Human-readable explanation of why the op cannot proceed.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="hint" type="string">
|
||||
Optional actionable suggestion to resolve the issue.
|
||||
</ResponseField>
|
||||
|
||||
---
|
||||
|
||||
## ElementTimingSnapshot
|
||||
|
||||
Resolved timing for one element, returned as values in the `Record<HfId, ElementTimingSnapshot>` from `comp.getElementTimings()`.
|
||||
|
||||
```typescript
|
||||
interface ElementTimingSnapshot {
|
||||
enterAt: number;
|
||||
exitAt: number;
|
||||
labels: string[];
|
||||
}
|
||||
```
|
||||
|
||||
<ResponseField name="enterAt" type="number">
|
||||
Time in seconds when the element enters. Derived from `data-duration` (preferred) or `data-end − data-start` as fallback, using the same logic as `setTiming`.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="exitAt" type="number">
|
||||
Time in seconds when the element exits.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="labels" type="string[]">
|
||||
GSAP `addLabel` names whose numeric position falls within `[enterAt, exitAt]` for this element. Parsed fresh from the GSAP script on every `getElementTimings()` call — never cached.
|
||||
</ResponseField>
|
||||
|
||||
---
|
||||
|
||||
## SelectionProxy
|
||||
|
||||
Proxy returned by `comp.selection()`. Resolves the current selection at call time and applies ops per-id within a single batch. Selection changes between calls are picked up automatically since `ids` is resolved fresh each time you call `comp.selection()`.
|
||||
|
||||
```typescript
|
||||
interface SelectionProxy {
|
||||
readonly ids: readonly string[];
|
||||
setStyle(styles: Record<string, string | null>): void;
|
||||
setText(value: string): void;
|
||||
setAttribute(name: string, value: string | null): void;
|
||||
setTiming(timing: { start?: number; duration?: number; trackIndex?: number }): void;
|
||||
removeElement(): void;
|
||||
}
|
||||
```
|
||||
|
||||
<ResponseField name="ids" type="readonly string[]">
|
||||
The hf-ids in the current selection at the moment `comp.selection()` was called.
|
||||
</ResponseField>
|
||||
|
||||
```typescript
|
||||
const sel = comp.selection();
|
||||
if (sel.ids.length > 0) {
|
||||
sel.setStyle({ opacity: "0.5" });
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ElementHandle
|
||||
|
||||
A curried element handle returned by `comp.element(id)`. Holds only the id string — no stale-reference hazard — and provides the same mutation methods as typed methods on `Composition`.
|
||||
|
||||
```typescript
|
||||
interface ElementHandle {
|
||||
readonly id: string;
|
||||
setStyle(styles: Record<string, string | null>): void;
|
||||
setText(value: string): void;
|
||||
setAttribute(name: string, value: string | null): void;
|
||||
setTiming(timing: { start?: number; duration?: number; trackIndex?: number }): void;
|
||||
removeElement(): void;
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
const title = comp.element("hf-title");
|
||||
title.setText("Launch Day");
|
||||
title.setStyle({ color: "#FFD60A", fontSize: "96px" });
|
||||
title.setTiming({ start: 0.5, duration: 3 });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ORIGIN_APPLY_PATCHES / ORIGIN_LOCAL
|
||||
|
||||
```typescript
|
||||
const ORIGIN_APPLY_PATCHES = "@hyperframes/sdk:applyPatches";
|
||||
const ORIGIN_LOCAL = "local";
|
||||
```
|
||||
|
||||
Reserved `origin` tag constants:
|
||||
|
||||
- **`ORIGIN_APPLY_PATCHES`** — automatically set by `comp.applyPatches()`. Listeners **must** skip this origin to prevent undo loops when mirroring patch events back into the SDK. Uses a namespaced string (not a unique Symbol) so the sentinel survives realm boundaries such as `postMessage` and structured clone, which T3 embedded hosts may use to forward patch events.
|
||||
- **`ORIGIN_LOCAL`** — default origin used when `dispatch()` or `batch()` is called without an explicit `origin` option. Signals UI-driven user edits.
|
||||
|
||||
```typescript
|
||||
comp.on("patch", ({ origin }) => {
|
||||
if (origin === ORIGIN_APPLY_PATCHES) return; // skip — would loop
|
||||
if (origin === ORIGIN_LOCAL) {
|
||||
// user-driven edit — sync to collaboration layer
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## EditOp
|
||||
|
||||
The full union of all dispatchable edit operations. See [Edit Operations](/sdk/reference/edit-operations) for the complete catalog with field descriptions and code examples.
|
||||
|
||||
```typescript
|
||||
type EditOp =
|
||||
| { type: "setStyle"; target: HfId | HfId[]; styles: Record<string, string | null> }
|
||||
| { type: "setText"; target: HfId | HfId[]; value: string }
|
||||
| { type: "setAttribute"; target: HfId | HfId[]; name: string; value: string | null }
|
||||
| { type: "setTiming"; target: HfId | HfId[]; start?: number; duration?: number; trackIndex?: number }
|
||||
| { type: "setHold"; target: HfId | HfId[]; hold: ElasticHold }
|
||||
| { type: "moveElement"; target: HfId | HfId[]; x: number; y: number }
|
||||
| { type: "removeElement"; target: HfId | HfId[] }
|
||||
| { type: "addElement"; parent: HfId | null; index: number; html: string }
|
||||
| { type: "reorderElements"; entries: Array<{ target: HfId; zIndex: number }> }
|
||||
| { 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 }
|
||||
| { type: "removeGsapProperty"; animationId: string; property: string; from?: boolean }
|
||||
| { type: "setGsapKeyframe"; animationId: string; keyframeIndex: number; position?: number; value?: Record<string, unknown>; ease?: string }
|
||||
| { type: "addGsapKeyframe"; animationId: string; position: number; value: Record<string, unknown> }
|
||||
| { type: "removeGsapKeyframe"; animationId: string; percentage: number }
|
||||
| { type: "removeAllKeyframes"; animationId: string }
|
||||
| { type: "convertToKeyframes"; animationId: string; resolvedFromValues?: Record<string, number | string> }
|
||||
| { type: "materializeKeyframes"; animationId: string; keyframes: Array<{ percentage: number; properties: Record<string, number | string>; ease?: string }>; easeEach?: string; resolvedSelector?: string }
|
||||
| { type: "splitIntoPropertyGroups"; animationId: string }
|
||||
| { type: "splitAnimations"; originalId: string; newId: string; splitTime: number; elementStart: number; elementDuration: number }
|
||||
| { type: "addLabel"; name: string; position: number }
|
||||
| { type: "removeLabel"; name: string }
|
||||
| { type: "setArcPath"; animationId: string; config: { enabled: boolean; autoRotate: boolean | number; segments: Array<{ curviness?: number; cp1?: { x: number; y: number }; cp2?: { x: number; y: number } }> } }
|
||||
| { type: "updateArcSegment"; animationId: string; segmentIndex: number; update: { curviness?: number; cp1?: { x: number; y: number }; cp2?: { x: number; y: number } } }
|
||||
| { type: "removeArcPath"; animationId: string }
|
||||
| { type: "unrollDynamicAnimations"; animationId: string; elements: Array<{ selector: string; keyframes: Array<{ percentage: number; properties: Record<string, number | string> }>; easeEach?: string }> }
|
||||
| { type: "addWithKeyframes"; targetSelector: string; position: number; duration: number; keyframes: KeyframeSpec[]; ease?: string }
|
||||
| { type: "replaceWithKeyframes"; animationId: string; targetSelector: string; position: number; duration: number; keyframes: KeyframeSpec[]; ease?: string }
|
||||
| { type: "deleteAllForSelector"; selector: string };
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PersistErrorEvent
|
||||
|
||||
Emitted by `comp.on('persist:error', handler)` when an autosave write fails. Failures are non-fatal — the session continues operating normally.
|
||||
|
||||
```typescript
|
||||
interface PersistErrorEvent {
|
||||
error: { message: string; hint?: string; cause?: unknown };
|
||||
}
|
||||
```
|
||||
|
||||
<ResponseField name="error.message" type="string">
|
||||
Human-readable description of the persist failure.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="error.hint" type="string">
|
||||
Optional suggestion for resolving the failure.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="error.cause" type="unknown">
|
||||
Underlying error object, if available.
|
||||
</ResponseField>
|
||||
|
||||
@@ -0,0 +1,396 @@
|
||||
---
|
||||
title: "Utilities & Constants"
|
||||
description: "History module, persist queue, document utilities, constants, and error types exported from @hyperframes/sdk."
|
||||
---
|
||||
|
||||
This page covers everything exported from `@hyperframes/sdk` that is not covered in [`openComposition`](/sdk/reference/open-composition), the [`Composition` interface](/sdk/reference/composition), [edit operations](/sdk/reference/edit-operations), [types](/sdk/reference/types), or [adapters](/sdk/reference/adapters).
|
||||
|
||||
---
|
||||
|
||||
## History Module
|
||||
|
||||
```typescript
|
||||
import { createHistory } from "@hyperframes/sdk";
|
||||
import type { HistoryModule, HistoryOptions, HistoryEntry } from "@hyperframes/sdk";
|
||||
```
|
||||
|
||||
Optional undo/redo module that wires onto a `Composition` session via the `"patch"` event. Standalone sessions created by `openComposition()` attach a history module automatically — you only need `createHistory` directly when you are building a host application that manages its own undo stack, or when you want non-default coalesce/depth settings.
|
||||
|
||||
### createHistory
|
||||
|
||||
```typescript
|
||||
function createHistory(session: Composition, opts?: HistoryOptions): HistoryModule;
|
||||
```
|
||||
|
||||
Subscribes to `session.on('patch')` and builds an undo/redo stack. Coalesces rapid same-operation bursts on the same element into a single undo entry so a slider drag produces one undo step, not hundreds.
|
||||
|
||||
```typescript
|
||||
import { openComposition, createHistory } from "@hyperframes/sdk";
|
||||
|
||||
// Custom history — host controls undo/redo buttons
|
||||
const comp = await openComposition(html, { history: false });
|
||||
const history = createHistory(comp, { coalesceMs: 500, maxEntries: 50 });
|
||||
|
||||
comp.setText("hf-title", "Draft v1");
|
||||
|
||||
history.canUndo(); // true
|
||||
history.undo(); // reverts to original
|
||||
history.redo(); // re-applies
|
||||
|
||||
history.dispose(); // unsubscribes from patch events
|
||||
```
|
||||
|
||||
### HistoryModule
|
||||
|
||||
```typescript
|
||||
interface HistoryModule {
|
||||
undo(): boolean;
|
||||
redo(): boolean;
|
||||
canUndo(): boolean;
|
||||
canRedo(): boolean;
|
||||
dispose(): void;
|
||||
}
|
||||
```
|
||||
|
||||
<ParamField path="undo" type="() => boolean">
|
||||
Pops the top entry from the undo stack and applies its inverse patches via `session.applyPatches()` tagged with `ORIGIN_APPLY_PATCHES`. Returns `true` when an entry was popped, `false` when the stack was empty.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="redo" type="() => boolean">
|
||||
Pops the top entry from the redo stack and re-applies its forward patches. Returns `true` when an entry was popped, `false` when the stack was empty. Any new op clears the redo stack.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="canUndo" type="() => boolean">
|
||||
Returns `true` when there is at least one entry on the undo stack.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="canRedo" type="() => boolean">
|
||||
Returns `true` when there is at least one entry on the redo stack.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="dispose" type="() => void">
|
||||
Unsubscribes from the session's `"patch"` event and clears both stacks. Call when the session is closed.
|
||||
</ParamField>
|
||||
|
||||
### HistoryOptions
|
||||
|
||||
```typescript
|
||||
interface HistoryOptions {
|
||||
trackedOrigins?: unknown[];
|
||||
coalesceMs?: number;
|
||||
maxEntries?: number;
|
||||
}
|
||||
```
|
||||
|
||||
<ParamField path="trackedOrigins" type="unknown[]">
|
||||
Only ops whose `origin` value appears in this array enter the undo stack. When omitted, all origins are tracked except `ORIGIN_APPLY_PATCHES` (which is always excluded to prevent undo loops). Use this to restrict the undo stack to UI-driven edits while letting programmatic patches pass through silently.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="coalesceMs" type="number">
|
||||
Window in milliseconds within which same-operation bursts on the same paths are merged into a single undo entry. The timestamp slides forward on each coalesced event, so continuous editing keeps merging until there is a gap longer than `coalesceMs`. Default: `300`.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="maxEntries" type="number">
|
||||
Maximum depth of the undo stack. Oldest entries are dropped when the limit is exceeded. Default: `100`.
|
||||
</ParamField>
|
||||
|
||||
### HistoryEntry
|
||||
|
||||
```typescript
|
||||
interface HistoryEntry {
|
||||
readonly patches: readonly JsonPatchOp[];
|
||||
readonly inversePatches: readonly JsonPatchOp[];
|
||||
readonly opTypes: readonly string[];
|
||||
readonly origin: unknown;
|
||||
readonly timestamp: number;
|
||||
}
|
||||
```
|
||||
|
||||
Entries are internal to the history module — you do not create them directly. They mirror the shape of `PatchEvent` fields so the history module can reconstruct the forward and inverse change sets without re-parsing.
|
||||
|
||||
<Note>
|
||||
Standalone sessions (the default) attach history automatically. Pass `{ history: false }` to `openComposition()` when you want to manage undo/redo yourself via `createHistory` or the host's own stack.
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Persist Queue
|
||||
|
||||
```typescript
|
||||
import { createPersistQueue } from "@hyperframes/sdk";
|
||||
import type { PersistQueueModule, PersistQueueOptions } from "@hyperframes/sdk";
|
||||
```
|
||||
|
||||
Optional module that subscribes to `"change"` events on a session and schedules async writes through a `PersistAdapter`. One write is in flight at a time; the latest HTML always wins (last-write-wins coalescing). Standalone sessions wired with a `persist` adapter attach this automatically via `openComposition()`. Use `createPersistQueue` directly only when you are building an embedded host that owns persistence separately from the SDK session.
|
||||
|
||||
### createPersistQueue
|
||||
|
||||
```typescript
|
||||
function createPersistQueue(
|
||||
session: Composition,
|
||||
adapter: PersistAdapter,
|
||||
opts?: PersistQueueOptions,
|
||||
): PersistQueueModule;
|
||||
```
|
||||
|
||||
```typescript
|
||||
import { openComposition, createPersistQueue } from "@hyperframes/sdk";
|
||||
import { createFsAdapter } from "@hyperframes/sdk/adapters/fs";
|
||||
|
||||
const adapt = createFsAdapter({ root: "./project" });
|
||||
const comp = await openComposition(html, { history: false });
|
||||
|
||||
const queue = createPersistQueue(comp, adapt, {
|
||||
path: "my-comp.html",
|
||||
onError: ({ error }) => console.error("Write failed:", error.message),
|
||||
});
|
||||
|
||||
comp.setText("hf-title", "Autosaved");
|
||||
|
||||
// Force immediate flush before app close
|
||||
await queue.flush();
|
||||
queue.dispose();
|
||||
```
|
||||
|
||||
### PersistQueueModule
|
||||
|
||||
```typescript
|
||||
interface PersistQueueModule {
|
||||
flush(): Promise<void>;
|
||||
dispose(): void;
|
||||
}
|
||||
```
|
||||
|
||||
<ParamField path="flush" type="() => Promise<void>">
|
||||
Cancels any pending debounced write and immediately writes the current serialized HTML to the adapter. Resolves when the write commits. Use before app close or page unload.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="dispose" type="() => void">
|
||||
Cancels any pending write and unsubscribes from the session's `"change"` event. Does not flush — call `flush()` first if you need to ensure the final state is written.
|
||||
</ParamField>
|
||||
|
||||
### PersistQueueOptions
|
||||
|
||||
```typescript
|
||||
interface PersistQueueOptions {
|
||||
path?: string;
|
||||
onError?: (e: PersistErrorEvent) => void;
|
||||
}
|
||||
```
|
||||
|
||||
<ParamField path="path" type="string">
|
||||
The adapter path to write to. Passed directly to `adapter.write(path, content)`. Default: `"composition.html"`.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="onError" type="(e: PersistErrorEvent) => void">
|
||||
Called when `adapter.write()` rejects. Receives a `PersistErrorEvent` with `{ error: { message, cause? } }`. Use this to surface persistence failures in your UI or logging layer.
|
||||
</ParamField>
|
||||
|
||||
---
|
||||
|
||||
## Document Utilities
|
||||
|
||||
```typescript
|
||||
import { buildDocument, buildRoots, flatElements } from "@hyperframes/sdk";
|
||||
```
|
||||
|
||||
Low-level utilities for building the `SdkDocument` model and `HyperFramesElement` trees from parsed HTML. These are the same functions the SDK uses internally on every `openComposition()` call. You rarely need them directly — they are exposed for hosts that parse HTML outside of a session, or for testing.
|
||||
|
||||
### buildDocument
|
||||
|
||||
```typescript
|
||||
function buildDocument(html: string): SdkDocument;
|
||||
```
|
||||
|
||||
Parses an HTML string into the SDK document model. Calls `ensureHfIds` first so every element gets a stable `data-hf-id`. Uses `linkedom` for DOM parsing — node-safe, works in agents, CI, and server-side code. Returns an `SdkDocument` snapshot; mutations on the live session do not update the returned value.
|
||||
|
||||
### buildRoots
|
||||
|
||||
```typescript
|
||||
function buildRoots(document: Document): HyperFramesElement[];
|
||||
```
|
||||
|
||||
Builds the element tree from an already-parsed (hf-id-stamped) `linkedom` `Document`. Walks the live DOM directly — no serialize/re-parse round trip. This is what the session's query API uses against its mutable document after each op.
|
||||
|
||||
### flatElements
|
||||
|
||||
```typescript
|
||||
function flatElements(roots: readonly HyperFramesElement[]): HyperFramesElement[];
|
||||
```
|
||||
|
||||
Returns every element from `roots` and all their descendants in document order (depth-first pre-order). Useful for searching across the full element tree without writing your own recursive walk.
|
||||
|
||||
```typescript
|
||||
import { buildDocument, flatElements } from "@hyperframes/sdk";
|
||||
|
||||
const doc = buildDocument(html);
|
||||
const all = flatElements(doc.roots);
|
||||
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
|
||||
|
||||
```typescript
|
||||
import { ORIGIN_APPLY_PATCHES } from "@hyperframes/sdk";
|
||||
|
||||
const ORIGIN_APPLY_PATCHES = "@hyperframes/sdk:applyPatches";
|
||||
```
|
||||
|
||||
Reserved origin tag emitted by `applyPatches()` and by the history module's `undo()` / `redo()` methods. Host patch listeners **must** skip this origin to avoid undo loops:
|
||||
|
||||
```typescript
|
||||
comp.on("patch", ({ origin, patches }) => {
|
||||
if (origin === ORIGIN_APPLY_PATCHES) return; // SDK-internal replay — skip
|
||||
forwardToCollaborationLayer(patches);
|
||||
});
|
||||
```
|
||||
|
||||
The value is a namespaced string rather than a `Symbol` so it survives realm boundaries — `postMessage`, structured clone, and JSON serialization all preserve it correctly. T3 embedded hosts that forward patch events across frames or workers rely on this property. The namespace prefix (`@hyperframes/sdk:`) makes accidental collision with a host-chosen origin string negligible.
|
||||
|
||||
### ORIGIN\_LOCAL
|
||||
|
||||
```typescript
|
||||
import { ORIGIN_LOCAL } from "@hyperframes/sdk";
|
||||
|
||||
const ORIGIN_LOCAL = "local";
|
||||
```
|
||||
|
||||
Default origin applied when you call typed methods (`setText`, `setStyle`, …) or `dispatch()` without an explicit `origin` option. You can filter on `"local"` to track only user-driven UI edits in a history module's `trackedOrigins` list.
|
||||
|
||||
---
|
||||
|
||||
## Errors
|
||||
|
||||
### UnsupportedOpError
|
||||
|
||||
```typescript
|
||||
import { UnsupportedOpError } from "@hyperframes/sdk";
|
||||
```
|
||||
|
||||
Thrown by `dispatch()` when an op type is not handled by the current engine version. The `code` property is stable and part of the public API contract — switch on it rather than the message string.
|
||||
|
||||
```typescript
|
||||
class UnsupportedOpError extends Error {
|
||||
readonly code = "E_UNSUPPORTED_OP";
|
||||
}
|
||||
```
|
||||
|
||||
Feature-detect before dispatching optional ops with `comp.can(op)` to avoid this error in the hot path:
|
||||
|
||||
```typescript
|
||||
const result = comp.can({ type: "setGsapTween", animationId: id, properties: { ease: "power2.out" } });
|
||||
if (!result.ok) {
|
||||
console.warn(result.message);
|
||||
return;
|
||||
}
|
||||
comp.setGsapTween(id, { ease: "power2.out" });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## resolveNearestHfElement
|
||||
|
||||
```typescript
|
||||
import { resolveNearestHfElement } from "@hyperframes/sdk";
|
||||
|
||||
function resolveNearestHfElement(
|
||||
el: Element | null,
|
||||
isVisible: (el: Element) => boolean,
|
||||
): ElementAtPointResult | null;
|
||||
```
|
||||
|
||||
Walks from `el` upward through `parentElement`, returning the nearest ancestor (inclusive) that carries `[data-hf-id]` and is not `[data-hf-root]`. Returns `null` when the walk exits the DOM without finding a match, when the matched node is the composition root, or when `isVisible(node)` returns `false` for the matched node.
|
||||
|
||||
This is a pure function (no `window` or DOM API calls beyond `getAttribute`) and is unit-testable in a plain Node environment. `createIframePreviewAdapter` uses it internally to translate raw hit-test results into SDK element IDs. Full treatment of hit-testing and the visual editor canvas pattern is in the [Canvas Integration guide](/sdk/guides/canvas-integration).
|
||||
|
||||
---
|
||||
|
||||
## resolveElementAffordances
|
||||
|
||||
```typescript
|
||||
import { resolveElementAffordances } from "@hyperframes/sdk/editing";
|
||||
```
|
||||
|
||||
Determines which editing operations are available for a live element given its current DOM state and model. Imported from the `@hyperframes/sdk/editing` subpath. Full documentation is in the [Editing Affordances guide](/sdk/guides/editing-affordances).
|
||||
|
||||
---
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Undo, Redo & Patches" icon="clock-rotate-left" href="/sdk/guides/undo-redo-and-patches">
|
||||
How the history module, ORIGIN_APPLY_PATCHES, and applyPatches() work together.
|
||||
</Card>
|
||||
<Card title="Persistence" icon="floppy-disk" href="/sdk/guides/persistence">
|
||||
Wiring a PersistAdapter, handling errors, and restoring from version history.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
Reference in New Issue
Block a user