From e6b8e396f4e99a43fc9f9a2663dec6bb6664814d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Sun, 9 Aug 2026 07:09:01 +0000 Subject: [PATCH] fix(core,sdk): namespace composition variables so they stop shadowing theme tokens A declared composition variable was written to the composition root as a CSS custom property named after its own id, with no namespace. A variable called accent therefore set --accent inline and shadowed the host theme for that whole subtree. That is worse than a naming clash. The accent enum values are green, blue and violet, which are not colours, they are selectors a composition maps onto theme slots. A representative composition maps blue to var(--accent, #18181b). The runtime then set --accent to blue, so the lookup resolved to the CSS keyword and no host theme could win. green and violet escaped only because they route to --brand and --accent-2, which nothing shadowed, which is why this survived: it was invisible for two of three values. Variables are now written to --hf-var-. The bare name is still written as a deprecated alias, but only for ids that are not reserved theme tokens, which is what actually fixes the collision. Four writers had the bug, not one: the runtime bindings, the scoped getVariables path, the compiler stylesheet, and the SDK mutate and apply-patches path. Fixing only the runtime left the compiler emitting the bare name into compiled output, where a host theme supplied as an inline style attribute still rendered the keyword. All four now route through one helper, and the helper takes the raw id so callers cannot derive the name themselves. That last point closed a real defect rather than a tidy-up. Two sites derived the property name differently, one verbatim and one slugged, so an id like Accent produced two disjoint property sets: one path reserved it, the other aliased it, and an SDK edit silently never landed. A test pins that every injection path derives one name per id. Docs that taught binding the bare name are corrected, including the capstone, whose ink variable is reserved and would have re-skinned the ground while quietly ignoring the ink. Rendered output is unchanged there, so the published videos stay accurate. --- docs/changelog.mdx | 48 +++++++ docs/concepts/variables.mdx | 26 +++- docs/deploy/templates-on-lambda.mdx | 7 +- docs/prompting/capstone.mdx | 9 +- docs/prompting/design-systems.mdx | 4 +- docs/prompting/variables-and-templating.mdx | 19 ++- docs/reference/html-schema.mdx | 6 +- packages/core/package-subpaths.json | 6 + packages/core/package.json | 10 ++ .../core/src/compiler/htmlBundler.test.ts | 88 ++++++++++++ packages/core/src/compiler/htmlBundler.ts | 16 ++- .../src/runtime/applyVariableBindings.test.ts | 74 +++++++++- .../core/src/runtime/applyVariableBindings.ts | 18 ++- .../core/src/runtime/getVariables.test.ts | 58 +++++++- packages/core/src/runtime/getVariables.ts | 20 +-- packages/core/src/runtime/themeTokens.test.ts | 126 ++++++++++++++++++ packages/core/src/runtime/themeTokens.ts | 87 ++++++++++++ packages/sdk/src/adapters/iframe.sync.test.ts | 7 +- packages/sdk/src/engine/apply-patches.ts | 39 ++++-- packages/sdk/src/engine/mutate.ts | 68 ++++++---- .../sdk/src/session.variabledecls.test.ts | 112 +++++++++++++++- skills-manifest.json | 2 +- skills/hyperframes-core/SKILL.md | 1 + .../references/variables-and-media.md | 5 +- 24 files changed, 777 insertions(+), 79 deletions(-) create mode 100644 packages/core/src/runtime/themeTokens.test.ts create mode 100644 packages/core/src/runtime/themeTokens.ts 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`).