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,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