diff --git a/docs/changelog.mdx b/docs/changelog.mdx index cfd139e036..647f59b659 100644 --- a/docs/changelog.mdx +++ b/docs/changelog.mdx @@ -8,6 +8,54 @@ Recent HyperFrames releases, including user-facing features, fixes, and migratio {/* New release entries are prepended by `bun run changelog:draft --write`. */} + +## Composition variables no longer take over theme token names + +A declared composition variable is now written to the CSS custom property +`--hf-var-` instead of the bare `--`. + +**Reserved theme-token ids change immediately, with no deprecation period.** +Fifteen names belong to the page hosting a composition: `accent`, `accent-2`, +`accent-3`, `accent2`, `bg`, `border`, `brand`, `fg`, `ink`, `muted`, +`primary`, `secondary`, `surface`, `tertiary`, `text`. A variable using one of +those ids is now written only to `--hf-var-`, never to the bare name. The +old behaviour was already broken rather than useful: a variable declared +`accent` overwrote the very `--accent` token the composition was trying to +read, so a composition mapping the value `blue` onto `var(--accent, #18181b)` +rendered the CSS keyword `blue`, and no host theme could win it back. + +**Every other id keeps the bare alias for one deprecation cycle.** A variable +called `speed` still writes both `--hf-var-speed` and `--speed`. The bare +alias is deprecated and will be removed in a future release. + +### Are you affected + +You are affected if a composition binds `var(--)` for an id it declares in +`data-composition-variables`. List the custom properties your compositions +read, then compare that list against your declared ids: + +```bash +grep -rho 'var(--[A-Za-z0-9_-]*' . --include='*.html' | sort -u +``` + +Any match on a reserved name in the list above stops resolving to the declared +value once you upgrade. Any other match still resolves, through the deprecated +alias. + +### What to change + +Rewrite the binding to the namespaced name: `var(--accent)` becomes +`var(--hf-var-accent)`. If the value should follow a host theme when one is +present and fall back to the declared variable otherwise, write +`var(--accent, var(--hf-var-accent))` instead. Declarations, `--variables` +overrides, `data-variable-values`, `data-var-text`, `data-var-src`, and +`getVariables()` are unchanged; only the CSS custom property name moves. + + .card-title { - color: var(--accent); + color: var(--hf-var-accent); } ``` - `data-var-text` replaces the element’s own text. - `data-var-src` replaces an image, video, audio, or source URL. -- Scalar variables are available as CSS custom properties such as - `var(--accent)`. +- Scalar variables are available as CSS custom properties named + `--hf-var-`, so the `accent` variable above is read as + `var(--hf-var-accent)`. + +The slug is the id lowercased, with anything that is not a letter, digit or +hyphen replaced by a hyphen. So `accentColor` is readable as +`var(--hf-var-accentcolor)`, and `swap_at` as `var(--hf-var-swap-at)`. + + +**How a variable is named in CSS.** Every scalar variable is written to +`--hf-var-`. It is also written to the bare `--`, but only when the id +is not one of the fifteen reserved theme-token names: `accent`, `accent-2`, +`accent-3`, `accent2`, `bg`, `border`, `brand`, `fg`, `ink`, `muted`, +`primary`, `secondary`, `surface`, `tertiary`, `text`. Those names belong to +the page hosting the composition, so a variable never writes them. A variable +called `accent` cannot shadow the host theme's `--accent`. + +The bare `--` alias is deprecated and will be removed in a future release. +Bind `var(--hf-var-)` in new work. When a value should follow the host +theme if there is one and fall back to the declared variable otherwise, write +`var(--accent, var(--hf-var-accent))`. + Use `window.__hyperframes.getVariables()` only when the result needs conditions, loops, or derived values: diff --git a/docs/deploy/templates-on-lambda.mdx b/docs/deploy/templates-on-lambda.mdx index e9e6d9dcd4..49a045db2a 100644 --- a/docs/deploy/templates-on-lambda.mdx +++ b/docs/deploy/templates-on-lambda.mdx @@ -30,7 +30,7 @@ example exposes a headline and accent color: data-height="1080" data-duration="5" data-no-timeline - style="color:var(--accent)" + style="color:var(--hf-var-accent)" >

Welcome

@@ -38,7 +38,10 @@ example exposes a headline and accent color: ``` -See [Variables](/concepts/variables) for every type and binding method. +A scalar variable is exposed as the CSS custom property `--hf-var-`, which +is why the stage binds `var(--hf-var-accent)` rather than `var(--accent)`. See +[Variables](/concepts/variables) for every type and binding method, and for the +reserved theme-token names a variable never writes. ## Test locally diff --git a/docs/prompting/capstone.mdx b/docs/prompting/capstone.mdx index 4c38cacf1f..d996ca0c16 100644 --- a/docs/prompting/capstone.mdx +++ b/docs/prompting/capstone.mdx @@ -97,7 +97,7 @@ That is the same catch-and-tighten loop [Iterating](/prompting/iterating) teache > > **Technique, pinned:** continuous world-camera via GSAP transforms (the dolly is the film); parallax layers in every region; `data-chart`, `world-map`, `sdf-iris`, and a caption component adapted from the registry; per-character typing at the open; matched-motion handoffs at every region boundary (the wire IS the match); full footage pipeline (generated clip → HEVC → auto-proxy → background removal → lower-third → word-synced captions); real beat grid from `hyperframes beats`; real Three.js via the frame adapter; seeded two-frame-hold confetti; SFX on hits; VO-paced reveals throughout. > -> **Variables:** expose `ground` (default `#0a0a0a`) and `ink` (default `#3CE6AC`) as composition variables on the single root file, bound via CSS custom properties everywhere (including the duotoned mural), so one `--variables` call re-skins the entire journey. It will be rendered twice: the default brand palette, and a second full render with `{"ground":"#0d1420","ink":"#c8ff3d"}`. +> **Variables:** expose `ground` (default `#0a0a0a`) and `ink` (default `#3CE6AC`) as composition variables on the single root file, bound everywhere via their namespaced CSS custom properties `var(--hf-var-ground)` and `var(--hf-var-ink)` (including the duotoned mural), so one `--variables` call re-skins the entire journey. It will be rendered twice: the default brand palette, and a second full render with `{"ground":"#0d1420","ink":"#c8ff3d"}`. > > **Architecture constraint (technical):** single composition file — one `index.html`, one variable scope. The world is one wide scene space traversed by a camera transform; regions may still be timed `class="clip"` sections synchronized to the camera's arrival (so capture stays efficient), but their positions form one continuous world and the wire + ruler + protagonist chip layers persist across 100% of the film. No `data-composition-src` sub-files. > @@ -142,6 +142,13 @@ hyperframes render --variables '{"ground":"#0d1420","ink":"#c8ff3d"}' --strict-v *The same composition, navy and acid green — the wire, ruler, chart, map, glass, and even the generated mural all follow the override.* +Both colors are bound as `var(--hf-var-ground)` and `var(--hf-var-ink)`, not as +`var(--ground)` and `var(--ink)`. Every declared variable is written to +`--hf-var-`, and `ink` is one of the reserved theme-token names (see +[Variables](/concepts/variables)), so it is written to the namespaced property +only. A `var(--ink)` binding would have re-skinned the ground and quietly +ignored the ink. + The mural trick from the previous capstone carries over. The artwork is generated in grayscale, then duotoned live by CSS layers driven by the `ground` and `ink` variables. Even the raster re-skins. Keep color in CSS and shapes in the raster whenever a template frame needs to survive a re-skin. ## What the prompt actually adds diff --git a/docs/prompting/design-systems.mdx b/docs/prompting/design-systems.mdx index 2f1c345540..c786b5a07b 100644 --- a/docs/prompting/design-systems.mdx +++ b/docs/prompting/design-systems.mdx @@ -78,7 +78,9 @@ The constant parts come from that one authoring spec, or from one set of importe When `frame.md` changes, rerun the workflow steps that generate or assemble the affected compositions. That's how the authored HTML picks up the new brand value. -Some compositions already exist and have to re-skin at render time. Declare the shared brand tokens as composition variables instead. Every scalar variable is applied as a `--{id}` CSS custom property on the composition root, so `var(--id)` in your CSS follows the override. +Some compositions already exist and have to re-skin at render time. Declare the shared brand tokens as composition variables instead. Every scalar variable is applied to the composition root as the CSS custom property `--hf-var-{slug}`, so `var(--hf-var-{slug})` in your CSS follows the override. + +Brand work is where the exact name matters. Fifteen role names belong to the page hosting the composition (`accent`, `bg`, `ink`, `primary`, `surface` and the rest of the reserved list in [Variables](/concepts/variables)), and a variable never writes them. So a variable called `accent` is readable as `var(--hf-var-accent)` only, and a stylesheet still saying `var(--accent)` will not follow the override. Bind the namespaced name, or write `var(--accent, var(--hf-var-accent))` when a host theme should win wherever one is defined. This is where design systems and templating meet. The brand is shared. The content is parameterized. diff --git a/docs/prompting/variables-and-templating.mdx b/docs/prompting/variables-and-templating.mdx index cf69717c22..be7e818e44 100644 --- a/docs/prompting/variables-and-templating.mdx +++ b/docs/prompting/variables-and-templating.mdx @@ -179,10 +179,19 @@ full list and the compile-time-vs-live-DOM rule behind it. An authored CSS custom property always wins over a same-named variable. Say your composition already defines its own `:root { --accent: ... }` as a - hand-written theme token. A variable called `accent` never overwrites it — the - authored value stands. A render-time `--variables` override still wins over - both. So when you need to override an authored value per render, use - `--variables`, not a same-named declared variable. + hand-written theme token. A variable called `accent` never overwrites it, and + it cannot: `accent` is one of the reserved theme-token names, so the variable + is only ever written to `--hf-var-accent`. A render-time `--variables` + override lands on `--hf-var-accent` too, so a `var(--accent)` binding keeps + showing the authored value. To drive that property per render, bind + `var(--hf-var-accent)`, or `var(--accent, var(--hf-var-accent))` if the + authored theme should still win whenever it is present. + + For an id that is *not* a reserved theme-token name, the bare `--{slug}` alias + is still written: an authored `--{slug}` wins over the declared default, and a + render-time `--variables` override wins over both. That alias is deprecated + and will be removed in a future release, so prefer `var(--hf-var-{slug})`. The + reserved names are listed in [Variables](/concepts/variables). ## Related @@ -213,7 +222,7 @@ capstone page. This is the clause in the [full capstone prompt](/prompting/capstone#the-prompt-word-for-word) that buys the piece — prompt language you can lift for your own video: -> **Variables:** expose `ground` (default `#0a0a0a`) and `ink` (default `#3CE6AC`) as composition variables on the single root file, bound via CSS custom properties everywhere (including the duotoned mural), so one `--variables` call re-skins the entire journey. It will be rendered twice: the default brand palette, and a second full render with `{"ground":"#0d1420","ink":"#c8ff3d"}`. +> **Variables:** expose `ground` (default `#0a0a0a`) and `ink` (default `#3CE6AC`) as composition variables on the single root file, bound everywhere via their namespaced CSS custom properties `var(--hf-var-ground)` and `var(--hf-var-ink)` (including the duotoned mural), so one `--variables` call re-skins the entire journey. It will be rendered twice: the default brand palette, and a second full render with `{"ground":"#0d1420","ink":"#c8ff3d"}`. > > **Architecture constraint (technical):** single composition file — one `index.html`, one variable scope. […] No `data-composition-src` sub-files. diff --git a/docs/reference/html-schema.mdx b/docs/reference/html-schema.mdx index c71613c98c..0596abf7d0 100644 --- a/docs/reference/html-schema.mdx +++ b/docs/reference/html-schema.mdx @@ -281,9 +281,13 @@ render or a nested host: > ``` -Use `data-var-text`, `data-var-src`, or CSS `var(--variableId)` for direct +Use `data-var-text`, `data-var-src`, or CSS `var(--hf-var-)` for direct bindings. Use `getVariables()` when the value affects logic. +A scalar variable is written to `--hf-var-`. The bare `--` is a +deprecated alias, and it is not written at all when `` is one of the fifteen +reserved theme-token names listed in [Variables](/concepts/variables). + ## Animation contract A composition using GSAP must: diff --git a/packages/core/package-subpaths.json b/packages/core/package-subpaths.json index 2a61955dc0..77481e17d9 100644 --- a/packages/core/package-subpaths.json +++ b/packages/core/package-subpaths.json @@ -158,6 +158,12 @@ "types": "./dist/compiler/htmlDocument.d.ts", "environments": ["browser", "bun", "node"] }, + "./runtime/theme-tokens": { + "source": "./src/runtime/themeTokens.ts", + "runtime": "./dist/runtime/themeTokens.js", + "types": "./dist/runtime/themeTokens.d.ts", + "environments": ["browser", "bun", "node"] + }, "./runtime/position-edits": { "source": "./src/runtime/positionEdits.ts", "runtime": "./dist/runtime/positionEdits.js", diff --git a/packages/core/package.json b/packages/core/package.json index af846f4539..ad09109402 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -167,6 +167,12 @@ "import": "./src/compiler/htmlDocument.ts", "types": "./src/compiler/htmlDocument.ts" }, + "./runtime/theme-tokens": { + "bun": "./src/runtime/themeTokens.ts", + "node": "./dist/runtime/themeTokens.js", + "import": "./src/runtime/themeTokens.ts", + "types": "./src/runtime/themeTokens.ts" + }, "./runtime/position-edits": { "bun": "./src/runtime/positionEdits.ts", "node": "./dist/runtime/positionEdits.js", @@ -403,6 +409,10 @@ "import": "./dist/compiler/htmlDocument.js", "types": "./dist/compiler/htmlDocument.d.ts" }, + "./runtime/theme-tokens": { + "import": "./dist/runtime/themeTokens.js", + "types": "./dist/runtime/themeTokens.d.ts" + }, "./runtime/position-edits": { "import": "./dist/runtime/positionEdits.js", "types": "./dist/runtime/positionEdits.d.ts" diff --git a/packages/core/src/compiler/htmlBundler.test.ts b/packages/core/src/compiler/htmlBundler.test.ts index b820d536fa..e2eea63726 100644 --- a/packages/core/src/compiler/htmlBundler.test.ts +++ b/packages/core/src/compiler/htmlBundler.test.ts @@ -1388,3 +1388,91 @@ describe("bundleToSingleHtml", () => { } }); }); + +/** The ` ``` - `data-var-src="id"` substitutes the element's `src` (URL string or image `{url}`); the authored `src` is the fallback. - `data-var-text="id"` substitutes the element's own text; element children (nested clips, animated spans) are preserved. -- Every scalar variable is applied automatically as a `--{id}` CSS custom property on the composition root, so `var(--id)` CSS responds to overrides — no `setProperty` boilerplate. +- Every scalar variable is applied automatically as the `--hf-var-{slug}` CSS custom property on the composition root, so `var(--hf-var-{slug})` CSS responds to overrides (no `setProperty` boilerplate). +- The bare `--{slug}` is written too, but **only when the id is not a reserved theme-token name**: `accent`, `accent-2`, `accent-3`, `accent2`, `bg`, `border`, `brand`, `fg`, `ink`, `muted`, `primary`, `secondary`, `surface`, `tertiary`, `text`. Those belong to the host page's theme, so a variable called `accent` never shadows the theme's `--accent`. The bare alias is deprecated and will be removed in a future release, so bind `var(--hf-var-{slug})`; use `var(--accent, var(--hf-var-accent))` when the host theme should win and the declared value is only the fallback. - Bindings resolve identically in preview and render, and per-instance for sub-compositions. - Caveat: media with audio should keep a real fallback `src` — render audio extraction reads the authored attribute (lint: `media_variable_src_no_fallback`).