feat(themes): per-theme HTML+GSAP template overrides#15
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
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):{{prop}}{{tokens.colors.bg}}).{{prop|raw}}{{prop|json}}JSON.stringify-safe for embedding inside<script>.{{#each items}}…{{this}}…{{/each}}{{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
LoadedThemegains atemplates: Template[]field; the disk loader populates it.resolveTemplateRegistry(projectDir)returns[...BUILTIN_TEMPLATES, ...activeTheme.templates].planScriptacceptsopts.availableTemplates; when supplied, the planner's tool catalog includes theme templates and validators accept them.assembleMasteracceptsopts.templates; scene render uses the right registry./script/planand/script/generatethread 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 viastroke-dashoffset, cyan dot orbiting the highlight ring (continuous theta tween viaonUpdatewith finite repeat math so the deterministic capture engine can seek), mono eyebrow, gradient-clipped wordmark, per-word tagline stagger. Honors the dreamspace tokens +power4.outease.The metadata sidecar declares
whenToUse,durationRange: { min: 4, max: 6 },hookOnly: true, props schema, andexamplePropsso the studio could render a preview.Test plan
loadThemeRegistrysurfacesdreamspace.templatesas[dreamspace__cold-open]resolveTemplateRegistryreturns[...BUILTIN_TEMPLATES, dreamspace__cold-open](9 → 10 templates after merge)dreamspace__cold-open+props = exampleProps, reassembled, browser-verified: 4 rings draw, eyebrow + wordmark + 5 tagline words render with the dreamspace token paletteHow designers add new templates
docs/design-systems/<theme>/templates/<name>.html.<name>.jsonsidecar describing when the planner should pick 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
examplePropsrendered.