Skip to content

feat(themes): per-theme HTML+GSAP template overrides#15

Merged
cuio merged 2 commits intomainfrom
feat/theme-shipped-templates
Apr 25, 2026
Merged

feat(themes): per-theme HTML+GSAP template overrides#15
cuio merged 2 commits intomainfrom
feat/theme-shipped-templates

Conversation

@cuio
Copy link
Copy Markdown
Owner

@cuio cuio commented Apr 25, 2026

Summary

Themes can now ship their own scene templates that the planner picks from and the assembler renders, alongside the built-ins. Closes the loop between design-system specs (already in docs/design-systems/) and the production renderer — Claude design ships JSX as reference, this PR lets the same theme ship the production HTML.

How a theme ships a template

docs/design-systems/<theme-id>/templates/
├── cold-open.html       ← required: the markup with {{prop}} substitution
├── cold-open.json       ← optional: metadata + propsSchema sidecar
├── manifesto.html
└── manifesto.json

Templates register with namespaced ids <theme-id>__<basename> (e.g. dreamspace__cold-open). Double-underscore so the id is safe inside CSS class selectors — a dot would be parsed as a class separator and silently break the template's scoped styles, which is the bug I hit on the first port and fixed before shipping.

Template engine

Tight Mustache subset (packages/core/src/script/themes/templateEngine.ts, ~100 LOC, no deps):

Syntax Meaning
{{prop}} HTML-escaped value lookup. Dot paths supported ({{tokens.colors.bg}}).
{{prop|raw}} Unescaped — only for trusted markup.
{{prop|json}} JSON.stringify-safe for embedding inside <script>.
{{#each items}}…{{this}}…{{/each}} Iterate. Inside the body: {{this}}, {{this.label}}, {{@index}}.

Always-injected context: {{scene_id}}, {{template_id}}, {{duration}}, {{is_hook}}, {{audio_src}}, full {{tokens.*}} tree, plus aliases {{colors.*}}, {{fonts.*}}, {{motion.*}}.

Wiring

  • LoadedTheme gains a templates: Template[] field; the disk loader populates it.
  • New resolveTemplateRegistry(projectDir) returns [...BUILTIN_TEMPLATES, ...activeTheme.templates].
  • planScript accepts opts.availableTemplates; when supplied, the planner's tool catalog includes theme templates and validators accept them.
  • assembleMaster accepts opts.templates; scene render uses the right registry.
  • Studio /script/plan and /script/generate thread the merged registry through both calls.

Example port: Dreamspace T1 Cold Open

docs/design-systems/dreamspace/templates/cold-open.html — full port from the JSX reference: 4 concentric SVG rings drawing in via stroke-dashoffset, cyan dot orbiting the highlight ring (continuous theta tween via onUpdate with finite repeat math so the deterministic capture engine can seek), mono eyebrow, gradient-clipped wordmark, per-word tagline stagger. Honors the dreamspace tokens + power4.out ease.

The metadata sidecar declares whenToUse, durationRange: { min: 4, max: 6 }, hookOnly: true, props schema, and exampleProps so the studio could render a preview.

Test plan

  • loadThemeRegistry surfaces dreamspace.templates as [dreamspace__cold-open]
  • resolveTemplateRegistry returns [...BUILTIN_TEMPLATES, dreamspace__cold-open] (9 → 10 templates after merge)
  • Forced one scene to dreamspace__cold-open + props = exampleProps, reassembled, browser-verified: 4 rings draw, eyebrow + wordmark + 5 tagline words render with the dreamspace token palette
  • Lint, format, typecheck pass
  • Studio's SSR cache-watcher picks up template file changes without restart

How designers add new templates

  1. Claude design generates new template JSX in the theme's reference set.
  2. Port the JSX → HTML+GSAP, save as docs/design-systems/<theme>/templates/<name>.html.
  3. Write the <name>.json sidecar describing when the planner should pick it.
  4. The cache-watcher fires, the planner sees the new template on the next plan, the assembler renders it.

No code edits required. The framework's built-in templates remain as the universal safety net — themes only need to ship templates where they can offer a meaningfully better look than the built-ins.

Future work

  • More Dreamspace ports (T2 prompt-to-app, T3 by-the-numbers, T4 stack-layers, T5 manifesto, T6 launch-card) — straightforward but each takes time.
  • Auto-port from Remotion JSX via Babel-standalone analysis (eliminates manual porting). Big project — not in scope here.
  • Live preview in the studio's theme picker showing the template's exampleProps rendered.

ishan pandey added 2 commits April 26, 2026 00:02
Themes can now ship their own scene templates that the planner picks
from and the assembler renders, alongside the built-ins. Closes the
loop between design-system specs (already in docs/design-systems/) and
the production renderer — Claude design ships the JSX as reference,
this PR lets the same theme ship the production HTML.

Mechanism:
- Drop <theme>/templates/<id>.html with Mustache-style {{prop}} +
  {{#each items}} substitution. Optional <id>.json sidecar carries
  metadata (description, whenToUse, durationRange, hookOnly,
  propsSchema, exampleProps).
- Templates register with namespaced ids `<theme-id>__<basename>`
  (double-underscore so the id is safe inside CSS class selectors —
  a dot would be parsed as a class separator and silently break the
  template's scoped styles, which is exactly the bug I hit on the
  first port and fixed before shipping).
- LoadedTheme gains a `templates: Template[]` field; the disk loader
  populates it via loadThemeTemplates(folder, themeId).
- New resolveTemplateRegistry(projectDir) returns
  [...BUILTIN_TEMPLATES, ...activeTheme.templates] for callers.
- planScript accepts opts.availableTemplates (defaults to built-ins);
  when supplied, the planner's tool catalog includes theme templates
  by their namespaced id and validators accept them.
- assembleMaster accepts opts.templates (defaults to built-ins) so
  scene render uses the right registry.
- Studio /script/plan and /script/generate now thread the merged
  registry through both planScript and assembleMaster.

Engine (packages/core/src/script/themes/templateEngine.ts):
Tight Mustache subset: {{prop}} HTML-escaped (dot paths supported),
{{prop|raw}} unescaped, {{prop|json}} script-tag-safe JSON,
{{#each items}}…{{this}}…{{this.x}}…{{@index}}…{{/each}} loops.
~100 LOC, no external deps. Always-injected context exposes
{{scene_id}}, {{template_id}}, {{duration}}, {{tokens.colors.*}}, etc.

Loader (packages/core/src/script/themes/templateLoader.ts):
Scans <theme>/templates/*.html + optional .json sidecar, returns
Template[] objects with the namespaced id and full render function.
Missing sidecar produces a permissive default — the template still
loads but the planner has less context for picking it.

Example port:
- docs/design-systems/dreamspace/templates/cold-open.html — full
  port of Dreamspace T1 Cold Open from the JSX reference. 4
  concentric SVG rings drawing in via stroke-dashoffset, cyan dot
  orbiting the highlight ring (continuous theta tween via onUpdate
  with FINITE repeat math so the deterministic capture engine can
  seek), mono eyebrow, gradient-clipped wordmark, per-word tagline
  stagger. Honors the dreamspace token palette + ease (power4.out).
- docs/design-systems/dreamspace/templates/cold-open.json — metadata
  sidecar declaring whenToUse, durationRange, hookOnly: true, props
  schema, exampleProps.

Verified end-to-end with the existing my-first-video project:
- loadThemeRegistry surfaces dreamspace.templates as
  [dreamspace__cold-open]
- /script/plan now sees it in the planner catalog
- assembleMaster with the merged registry renders the scene
  correctly — 4 rings drawn, eyebrow + wordmark + 5 tagline words
  visible, dreamspace tokens applied throughout

Docs:
- docs/design-systems/README.md gets a "Theme-shipped templates"
  section covering the HTML format, the metadata sidecar, the
  always-available context, the GSAP requirement (no infinite
  repeats), and the runtime wiring.
Resolves conflicts with PR #14 (prompt caching + multi-theme awareness +
per-scene theme override). Both feature sets are kept — themes can ship
templates AND the planner gets cached + multi-theme aware.

Conflict resolutions:
- planner.ts PlanOptions: kept both availableThemes (PR #14) and
  availableTemplates (PR #15) fields.
- planner.ts call site: kept the cache-controlled SystemSegment[]
  build from PR #14 + the templateRegistry threaded through to
  buildToolDefinition from PR #15.
- studio-api/routes/script.ts: pass BOTH otherThemes (peer-theme
  cross-pollination) AND availableTemplates (theme-shipped templates)
  to planScript.
- docs/design-systems/README.md: merged the 'Mixing themes per
  scene' + 'Themes from Claude design' + 'Cost notes' sections from
  PR #14 with the 'Theme-shipped templates' subsystem doc from #15.
@cuio cuio merged commit 0ef866e into main Apr 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant