export interface KeyframeStep { o: Record; } export interface ScrollSceneOptions { selector: string; keyframes: Record>; range?: string; timeline?: "view" | "scroll" | string; animation?: string; duration?: string; easing?: string; fill?: string; } export interface TransitionPortalOptions { name: string; old?: Record; new?: Record; duration?: string; easing?: string; } function declarations(values: Record): string { return Object.entries(values).map(([property, value]) => `${property.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}: ${value};`).join(" "); } /** Builds declarative native CSS scenes and view transitions without a client runtime. */ export class StageManager { private readonly scenes: ScrollSceneOptions[] = []; private readonly portals: TransitionPortalOptions[] = []; scene(name: string, options: Omit): this { this.scenes.push({ ...options, animation: name }); return this; } portal(name: string, options: Omit = {}): this { this.portals.push({ name, ...options }); return this; } css(): string { return [ ...this.scenes.flatMap((scene) => { const animation = scene.animation ?? "muddle-scene"; const timeline = scene.timeline ?? "view"; return [ `@keyframes ${animation} { ${Object.entries(scene.keyframes).map(([offset, step]) => { const values = "to" in step && typeof step.to === "object" ? step.to : step as Record; return `${offset} { ${declarations(values)} }`; }).join(" ")} }`, `${scene.selector} { animation-name: ${animation}; animation-duration: ${scene.duration ?? "1ms"}; animation-timing-function: ${scene.easing ?? "linear"}; animation-fill-mode: ${scene.fill ?? "both"}; animation-timeline: ${timeline === "view" ? "view()" : timeline === "scroll" ? "scroll()" : timeline}; animation-range: ${scene.range ?? "entry 0% cover 100%"}; }`, ]; }), ...this.portals.flatMap((portal) => { const timing = `animation-duration: ${portal.duration ?? "240ms"}; animation-timing-function: ${portal.easing ?? "ease"};`; return [ portal.old ? `::view-transition-old(${portal.name}) { ${timing} ${declarations(portal.old)} }` : "", portal.new ? `::view-transition-new(${portal.name}) { ${timing} ${declarations(portal.new)} }` : "", ].filter(Boolean); }), ].join("\n"); } } export function stage(): StageManager { return new StageManager(); }