import { openComposition } from "@hyperframes/sdk";
import { createFileAdapter } from "./fileAdapter.js";
import type { Composition, GsapTweenSpec, PreviewAdapter, FindQuery } from "@hyperframes/sdk";
import { parseGsapScriptAcorn } from "@hyperframes/core/gsap-parser-acorn";
import type { GsapAnimation } from "@hyperframes/core";
// fallow-ignore-next-line unresolved-imports
import gsapRaw from "gsap/dist/gsap.min.js?raw";
// ── Demo composition ──────────────────────────────────────────────────────────
const DEMO_HTML = `
SDK Playground
@hyperframes/sdk · Phase 3b
v0.6
`.trim();
// ── App state ─────────────────────────────────────────────────────────────────
let comp: Composition | null = null;
let playgroundPreview: PlaygroundPreview | null = null;
let selectedId: string | null = null;
let playMode = false;
let patchCount = 0;
let activeTab: "properties" | "ops" = "properties";
let lastTweenId = "";
let updateTimer: ReturnType | undefined;
let timelineDuration = 0;
let prevPlayPct = 0;
// ── Small value helpers ─────────────────────────────────────────────────────
/** parseFloat with a numeric fallback — isolates the `?? / ||` so callers stay simple. */
function numOr(value: string | number | undefined, fallback: number): number {
const n = typeof value === "number" ? value : parseFloat(value ?? "");
return isNaN(n) ? fallback : n;
}
/** Coerce a raw input string to a number when it parses, else keep the string. */
function coerceNum(raw: string): string | number {
const n = Number(raw);
return isNaN(n) ? raw : n;
}
function getFrame(): HTMLIFrameElement {
return document.getElementById("preview-frame") as HTMLIFrameElement;
}
function postToFrame(message: unknown): void {
getFrame().contentWindow?.postMessage(message, "*");
}
// ── Preview adapter ───────────────────────────────────────────────────────────
class PlaygroundPreview implements PreviewAdapter {
private handlers: Array<(ids: string[]) => void> = [];
elementAtPoint(_x: number, _y: number) {
return null;
}
applyDraft(_id: string, _props: { dx?: number; dy?: number; width?: number; height?: number }) {}
commitPreview() {}
cancelPreview() {}
select(ids: string[], _opts?: { additive?: boolean }) {
for (const h of this.handlers) h([...ids]);
}
on(event: "selection", handler: (ids: string[]) => void): () => void {
if (event !== "selection") return () => {};
this.handlers.push(handler);
return () => {
this.handlers = this.handlers.filter((h) => h !== handler);
};
}
attachSync(_comp: Composition): () => void {
return () => {};
}
}
// ── Preview iframe ────────────────────────────────────────────────────────────
// Bridge script injected into the preview iframe. Pure string — runs in the
// sandboxed iframe, talks to the parent over postMessage.
const BRIDGE_SCRIPT = ``;
function buildSrcdoc(html: string, selId: string | null): string {
// Baked-in highlight covers the initial render; postMessage updates it live without reload
const highlight = selId
? `[data-hf-id="${selId}"]{outline:2px solid #3b82f6!important;outline-offset:1px;}`
: "";
return `
${html}
${BRIDGE_SCRIPT}
`;
}
function schedulePreviewUpdate() {
clearTimeout(updateTimer);
updateTimer = setTimeout(updatePreviewNow, 350);
}
function updatePreviewNow() {
if (!comp) return;
getFrame().srcdoc = buildSrcdoc(comp.serialize(), selectedId);
}
function sendSelectionToIframe(id: string | null) {
postToFrame({ type: "hf:select", id });
}
function updatePreviewScale() {
const outer = document.getElementById("preview-scaler-outer")!;
const inner = document.getElementById("preview-scaler-inner")!;
const scale = outer.offsetWidth / 1280;
inner.style.transform = `scale(${scale})`;
outer.style.height = `${720 * scale}px`;
}
// ── Log ───────────────────────────────────────────────────────────────────────
const LOG_COLORS: Record = {
patch: "#60a5fa",
undo: "#fbbf24",
redo: "#fbbf24",
selectionchange: "#a78bfa",
"persist:error": "#f87171",
op: "#34d399",
info: "#6b7280",
};
function logBody(data: unknown): HTMLElement {
if (typeof data === "string") {
const span = document.createElement("span");
span.style.color = "#9ca3af";
span.textContent = data;
return span;
}
const pre = document.createElement("pre");
pre.textContent = JSON.stringify(data, null, 2);
return pre;
}
function logEntry(type: string, data: unknown) {
const logEl = document.getElementById("log-entries")!;
const color = LOG_COLORS[type] ?? "#9ca3af";
const d = document.createElement("div");
d.className = "log-entry";
d.style.borderLeftColor = color;
const typeSpan = document.createElement("span");
typeSpan.className = "log-type";
typeSpan.style.color = color;
typeSpan.textContent = `[${type}]`;
d.appendChild(typeSpan);
d.appendChild(logBody(data));
logEl.prepend(d);
while (logEl.children.length > 300) logEl.lastElementChild?.remove();
}
// ── Timeline ──────────────────────────────────────────────────────────────────
const TRACK_COLORS = ["#3b82f6", "#8b5cf6", "#f59e0b", "#10b981", "#f87171", "#06b6d4"];
function selectorToHfId(selector: string): string | null {
const m = /\[data-hf-id=['"]([^'"]+)['"]\]/.exec(selector);
if (m) return m[1] ?? null;
if (/^#/.test(selector.trim())) return selector.trim().slice(1);
return null;
}
function gsapScriptOf(html: string): string | null {
const m = /