Skip to content

Commit 79044aa

Browse files
committed
docs(sdk-playground): accurate README with stage coverage and CanResult
1 parent ff4dca2 commit 79044aa

2 files changed

Lines changed: 97 additions & 46 deletions

File tree

packages/sdk-playground/README.md

Lines changed: 43 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,19 @@ bun run --cwd packages/sdk-playground dev
1010

1111
Serves at `http://localhost:5173`. On first load it reads `packages/sdk-playground/composition.html` from disk (if present) or falls back to a built-in demo composition.
1212

13+
## What this demonstrates
14+
15+
The playground exercises the full SDK surface end-to-end in a real browser against a
16+
file-backed persist adapter. It was built to verify SDK stages 3b and 4 before Studio
17+
migration begins:
18+
19+
| SDK stage | What is exercised |
20+
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
21+
| Stage 3a — Session API | `openComposition`, `dispatch`, `undo`/`redo`, `batch`, `on('patch')`, `on('selectionchange')`, `on('persist:error')`, `flush` |
22+
| Stage 3b — GSAP engine | `addGsapTween`, `setGsapTween`, `removeGsapTween`, `addLabel`, `removeLabel`, `setClassStyle`, `setTiming` (GSAP-script sync) |
23+
| Stage 4 (partial) | `can()` returning `CanResult`, `getOverrides()`, `selection()` proxy, `find()`, `setVariableValue` |
24+
| Stage 5 (partial) | `FsAdapter` — file-backed persistence with version history; `FileAdapter` — browser fetch adapter; `PlaygroundPreview` — concrete `PreviewAdapter` impl |
25+
1326
## Features
1427

1528
### File persistence
@@ -22,7 +35,7 @@ Full composition rendered in a sandboxed `<iframe>`. Supports:
2235

2336
- **Play / Pause / Seek** via the transport bar
2437
- **Click-to-select** elements (highlights in the tree and properties panel)
25-
- **Drag-to-reposition** — drag any element to a new position; on drop the playground calls `comp.setStyle(id, { left, top })`
38+
- **Drag-to-reposition** — drag any element; on drop calls `comp.setStyle(id, { left, top })`
2639

2740
### Element tree
2841

@@ -35,50 +48,55 @@ Editable per-element properties for the selected element:
3548
| Section | SDK op |
3649
| ---------- | --------------------------------------------------------------------------- |
3750
| Content | `comp.setText(id, value)` |
38-
| Typography | `comp.setStyle(id, { fontSize, fontWeight, color, fontFamily })` |
39-
| Box | `comp.setStyle(id, { top, left, width, height })` |
40-
| Attributes | `comp.element(id).setAttribute(name, value)`shows all non-internal attrs |
51+
| Typography | `comp.setStyle(id, { fontSize, fontWeight, color })` |
52+
| Box | `comp.setStyle(id, { background, opacity, left, top })` |
53+
| Attributes | `comp.element(id).setAttribute(name, value)` — all non-internal attrs |
4154
| Danger | `comp.element(id).removeElement()` |
42-
| Animations | `comp.setTiming(id, { start, duration })`inline form per GSAP tween |
55+
| Animations | Lists tween IDs + inline "Add tween" form via `comp.addGsapTween(id, spec)` |
4356

4457
### Timeline
4558

46-
DAW-style per-element tween blocks. Drag handles to trim start/end; drag body to move. All edits go through `comp.setTiming(id, { start, duration })` which keeps the GSAP script and DOM attributes in sync.
59+
DAW-style per-element tween blocks. Drag handles to trim start/end; drag body to move. All edits go through `comp.setTiming(id, { start, duration })`, which keeps the GSAP script and `data-start`/`data-end` attributes in sync.
4760

4861
### Ops panel
4962

5063
Full op surface, grouped by feature:
5164

52-
| Section | SDK op |
53-
| ---------------------------- | --------------------------------------------------------------------------------- |
54-
| PreviewAdapter.select() | `preview.select([id])` |
55-
| setStyle | `comp.setStyle(id, styles)` |
56-
| setText | `comp.setText(id, value)` |
57-
| addGsapTween | `comp.addGsapTween(target, spec)` |
58-
| setTiming | `comp.setTiming(id, { start, duration })` |
59-
| setGsapTween | `comp.setGsapTween(animId, updates)` |
60-
| moveElement | `comp.moveElement(id, { parent, index })` |
61-
| setClassStyle | `comp.dispatch({ type: "setClassStyle", selector, styles })` |
62-
| setAttribute / removeElement | `comp.element(id).setAttribute()` / `.removeElement()` |
63-
| setVariableValue | `comp.setVariableValue(id, value)` |
64-
| find(query) | `comp.find({ tag, text, name, track })` |
65-
| selection() proxy | `comp.selection().setStyle()` / `.removeElement()` |
66-
| listVersions / loadFrom | `adapter.listVersions()` / `adapter.loadFrom()` |
67-
| History / inspect | `comp.undo()`, `comp.redo()`, `comp.can()`, `comp.getOverrides()`, `comp.flush()` |
65+
| Section | SDK op |
66+
| ------------------------------ | ----------------------------------------------------------------------------------------------- |
67+
| PreviewAdapter.select() | `preview.select([id])` |
68+
| setStyle | `comp.setStyle(id, styles)` |
69+
| setText | `comp.setText(id, value)` |
70+
| addGsapTween | `comp.addGsapTween(target, spec)` |
71+
| setGsapTween / removeGsapTween | `comp.setGsapTween(animId, { duration })` / `comp.removeGsapTween(animId)` |
72+
| addLabel / removeLabel | `comp.dispatch({ type: "addLabel", name, position })` / `removeLabel` |
73+
| setClassStyle | `comp.dispatch({ type: "setClassStyle", selector, styles })` |
74+
| setAttribute / removeElement | `comp.element(id).setAttribute()` / `.removeElement()` |
75+
| setVariableValue | `comp.setVariableValue(id, value)` |
76+
| find(query) | `comp.find({ tag, text })` |
77+
| selection() proxy | `comp.selection().setStyle()` / `.removeElement()` |
78+
| listVersions / loadFrom | `adapter.listVersions(path)` / `adapter.loadFrom(path, key)` |
79+
| History / inspect | `comp.undo()`, `comp.redo()`, `comp.can(op) → CanResult`, `comp.getOverrides()`, `comp.flush()` |
80+
81+
`can(op)` returns a `CanResult`: `{ok: true}` or `{ok: false, code, message, hint?}`. The ops panel logs the full result object so you can inspect validation failures in real time.
6882

6983
### Editor modal
7084

71-
Click "Open editor" to view and directly edit the raw composition HTML. Saving re-opens the composition through the SDK.
85+
Click "Open editor" to view and directly edit the raw composition HTML. Saving re-opens the composition through the SDK, so all patches and history are reset cleanly.
86+
87+
### Event log
88+
89+
Every `patch`, `undo`, `redo`, `selectionchange`, and `persist:error` event is logged with its payload. Useful for verifying RFC 6902 patch shape and override-set accumulation.
7290

7391
---
7492

7593
## Planned / not yet wired
7694

95+
- `addGsapKeyframe` / `setGsapKeyframe` / `removeGsapKeyframe` — ops are implemented in the SDK; not yet exposed in the UI
7796
- `comp.setTrackVariable(trackId, variableId)` — variable binding per track
7897
- `comp.addElement(spec)` — create new elements from the UI
7998
- `comp.duplicateElement(id)` — duplicate with offset
8099
- Selection multi-select (current: single-select only)
81100
- Timeline zoom and horizontal scroll for long compositions
82-
- Version history browser — list/preview/restore past versions inline (API is implemented; UI shows only list + load-oldest)
83-
- `comp.on('change', cb)` live event log fed from SDK event stream
101+
- Version history browser — list/preview/restore past versions inline (listVersions/loadFrom API is implemented; UI shows raw list + load-oldest button only)
84102
- Render to video via `@hyperframes/producer` integration

packages/sdk-playground/src/main.ts

Lines changed: 54 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const DEMO_HTML = `
1111
<div data-hf-id="hf-stage" data-hf-root style="width:1280px;height:720px;background:#111827;position:relative;" data-duration="6">
1212
<style>.badge{background:#3b82f6;border-radius:6px;}</style>
1313
<div data-hf-id="hf-headline" style="position:absolute;top:200px;left:140px;font-size:72px;font-weight:700;color:#f9fafb;font-family:system-ui,sans-serif;">SDK Playground</div>
14-
<div data-hf-id="hf-sub" style="position:absolute;top:300px;left:142px;font-size:28px;color:#9ca3af;font-family:system-ui,sans-serif;">@hyperframes/sdk &middot; Phase 3b</div>
14+
<div data-hf-id="hf-sub" style="position:absolute;top:300px;left:142px;font-size:28px;color:#9ca3af;font-family:system-ui,sans-serif;">@hyperframes/sdk &middot; Phase 3b + 4</div>
1515
<div data-hf-id="hf-badge" class="badge" style="position:absolute;top:390px;left:142px;padding:10px 24px;font-size:20px;font-weight:600;color:#fff;font-family:system-ui,sans-serif;">v0.6</div>
1616
<script>
1717
var tl = gsap.timeline({ paused: true });
@@ -936,37 +936,51 @@ function buildSelectionProxySection(): HTMLDivElement {
936936
}
937937

938938
function buildVersionsSection(): HTMLDivElement {
939-
const display = mkNote("");
940-
display.style.maxHeight = "80px";
941-
display.style.overflowY = "auto";
939+
const display = document.createElement("div");
940+
display.style.cssText = "max-height:100px;overflow-y:auto;margin-top:4px;";
942941
const list = mkBtn("List versions", "", () => listVersionsInto(display));
943-
const loadOldest = mkBtn("Load oldest", "", () => loadOldestVersion());
944-
return opSection("listVersions / loadFrom", opRow(list, loadOldest), display);
942+
return opSection("listVersions / loadFrom", opRow(list), display);
945943
}
946944

947945
async function listVersionsInto(display: HTMLElement) {
948946
const { adapter } = await createFileAdapter();
949947
const versions = await adapter.listVersions("composition.html");
950-
display.textContent = versions.length ? versions.map(versionLabel).join("\n") : "(no versions)";
948+
display.innerHTML = "";
949+
if (!versions.length) {
950+
display.textContent = "(no versions saved yet)";
951+
return;
952+
}
951953
logEntry("info", { versions: versions.map((v) => v.key) });
954+
for (const v of versions) {
955+
const row = document.createElement("div");
956+
row.className = "op-note";
957+
row.style.cssText += "cursor:pointer;padding:3px 4px;border-radius:3px;";
958+
row.textContent = versionLabel(v);
959+
row.title = `Click to restore ${v.key}`;
960+
row.addEventListener("mouseenter", () => (row.style.background = "#374151"));
961+
row.addEventListener("mouseleave", () => (row.style.background = ""));
962+
row.addEventListener("click", () => loadVersion(adapter, v.key));
963+
display.appendChild(row);
964+
}
952965
}
953966

954967
function versionLabel(v: { key: string; timestamp?: number }): string {
955-
return `${v.key} (${new Date(v.timestamp ?? 0).toLocaleTimeString()})`;
956-
}
957-
958-
async function loadOldestVersion() {
959-
const { adapter } = await createFileAdapter();
960-
const versions = await adapter.listVersions("composition.html");
961-
const oldest = versions[versions.length - 1];
962-
if (!oldest) {
963-
logEntry("info", "no versions");
968+
const ts = v.timestamp ?? 0;
969+
const time = ts > 0 ? new Date(ts).toLocaleTimeString() : "unknown time";
970+
return `${time} (${v.key})`;
971+
}
972+
973+
async function loadVersion(
974+
adapter: import("@hyperframes/sdk/adapters/types").PersistAdapter,
975+
key: string,
976+
): Promise<void> {
977+
const html = await adapter.loadFrom("composition.html", key);
978+
if (!html) {
979+
logEntry("info", `version ${key} not found`);
964980
return;
965981
}
966-
const html = await adapter.loadFrom("composition.html", oldest.key);
967-
if (!html) return;
968-
await openEditor(html, `v${oldest.key}`);
969-
logEntry("info", `loaded version ${oldest.key}`);
982+
await openEditor(html, `restored ${key.split("_")[0]}`);
983+
logEntry("info", `loaded version ${key}`);
970984
}
971985

972986
function buildHistorySection(): HTMLDivElement {
@@ -978,19 +992,38 @@ function buildHistorySection(): HTMLDivElement {
978992
comp!.redo();
979993
logEntry("redo", "dispatched");
980994
});
995+
996+
const canResult = document.createElement("div");
997+
canResult.className = "op-note";
998+
canResult.style.cssText += "font-size:10px;padding:2px 4px;";
999+
canResult.textContent = "—";
1000+
9811001
const canCheck = mkBtn("can(addGsapTween)?", "", () => {
9821002
const r = comp!.can({
9831003
type: "addGsapTween",
9841004
target: "hf-badge",
9851005
tween: { method: "to", duration: 0.5 },
9861006
});
9871007
logEntry("info", { "can(addGsapTween)": r });
1008+
if (r.ok) {
1009+
canResult.textContent = "✓ ok";
1010+
canResult.style.color = "#34d399";
1011+
} else {
1012+
canResult.textContent = `✗ ${r.code}: ${r.message}`;
1013+
canResult.style.color = "#f87171";
1014+
}
9881015
});
1016+
9891017
const overrides = mkBtn("getOverrides()", "", () => logEntry("info", comp!.getOverrides()));
9901018
const flush = mkBtn("flush", "", () => {
9911019
comp!.flush().then(() => logEntry("info", "flush complete"));
9921020
});
993-
return opSection("History / inspect", opRow(undo, redo), opRow(canCheck, overrides, flush));
1021+
return opSection(
1022+
"History / inspect",
1023+
opRow(undo, redo),
1024+
opRow(canCheck, canResult),
1025+
opRow(overrides, flush),
1026+
);
9941027
}
9951028

9961029
const OPS_SECTIONS = [

0 commit comments

Comments
 (0)