diff --git a/docs/specs/theme.md b/docs/specs/theme.md index faef4daf..8da1dfb2 100644 --- a/docs/specs/theme.md +++ b/docs/specs/theme.md @@ -107,10 +107,21 @@ are still visible on `document.body`. ThemePicker also performs a browser layout-effect restore after mount so website hydration cannot leave the picker state saying a theme is active while xterm.js sees fallback colors. -`theme.css` declares the theme-dependent `--color-*` tokens on `body` because +`theme.css` declares the theme-dependent `--color-*` tokens — and the +`--mt-font-size` / `--mt-font-family` typography tokens — on `body` because `--vscode-*` variables also live there. Keep the parallel `@theme` -declarations so Tailwind can generate utility classes, but treat the body-level -declarations as the runtime source of truth. +declarations so Tailwind can generate utility classes, and the `:root` +copies of the `--mt-*` font tokens, but treat the body-level declarations as +the runtime source of truth. Every token whose value reads any `var()` +chain — an indirect binding like +`--color-door-bg: var(--color-header-inactive-bg)` counts, not just a direct +`--vscode-*` read — must appear at both levels, with the same value: CSS +resolves `var()` inside a custom-property declaration at the element where +the property is declared, so a document-level declaration cannot see a +variable `applyTheme()` wrote to `body.style`, and the token resolves to +nothing in every host where `applyTheme()` is the sole writer (standalone, +website, Pocket). `lib/src/lib/themes/consumed-keys.test.ts` pins the two +lists together. Dynamic palette tokens (`--color-door-bg`, `--color-door-fg`, `--color-focus-ring`, and the four `--color-alarm-vs-*` tokens) also have body-level baseline bindings matching the `@theme` declarations, so direct diff --git a/lib/src/lib/themes/consumed-keys.test.ts b/lib/src/lib/themes/consumed-keys.test.ts index 0b4d1e9d..2fc933c9 100644 --- a/lib/src/lib/themes/consumed-keys.test.ts +++ b/lib/src/lib/themes/consumed-keys.test.ts @@ -31,3 +31,42 @@ describe('CONSUMED_VSCODE_KEYS / bundle-themes.mjs parity', () => { expect(extra).toEqual([]); }); }); + +// theme.css declares the same tokens twice: once above the body block (in +// `@theme`, so Tailwind generates utility classes, or in `:root`) and once on +// `body` (so they can actually see the --vscode-* variables applyTheme() writes +// to body.style). CSS resolves var() inside a custom-property declaration at +// the element where the property is declared, so a token whose value reads any +// var() chain *only* above the body block resolves to nothing in every host +// where applyTheme() is the sole writer — standalone, website, Pocket. The +// check compares values, not just presence, so repointing one level's binding +// without the other fails too. This pins the two lists together. +describe('theme.css var() bindings are mirrored onto body', () => { + const here = dirname(fileURLToPath(import.meta.url)); + const themeCss = readFileSync(resolve(here, '../../theme.css'), 'utf8'); + + function declarations(block: string): Map { + const out = new Map(); + for (const m of block.matchAll(/^\s*(--[\w-]+)\s*:\s*([^;]+);/gm)) out.set(m[1], m[2].trim()); + return out; + } + + function blockBody(pattern: RegExp): string { + const match = themeCss.match(pattern); + if (!match) throw new Error(`Could not locate ${pattern} in theme.css`); + return match[1]; + } + + const documentLevel = new Map([ + ...declarations(blockBody(/@theme \{([\s\S]*?)\n\}/)), + ...declarations(blockBody(/\n:root \{([\s\S]*?)\n\}/)), + ]); + const bodyLevel = declarations(blockBody(/\nbody \{([\s\S]*?)\n\}/)); + + it('every document-level token bound to a var() chain is mirrored onto body', () => { + const missing = [...documentLevel] + .filter(([name, value]) => value.includes('var(') && bodyLevel.get(name) !== value) + .map(([name]) => name); + expect(missing).toEqual([]); + }); +}); diff --git a/lib/src/theme.css b/lib/src/theme.css index bd5c9af8..03130688 100644 --- a/lib/src/theme.css +++ b/lib/src/theme.css @@ -97,11 +97,17 @@ /* --- Body-level dynamic theme bindings --- * - * These mirror the @theme block above but are declared on `body`. CSS resolves - * var() inside a custom-property declaration at the element where the property - * is declared, so body-level declarations can see --vscode-* variables that - * applyTheme() writes to body.style. */ + * These mirror the :root and @theme blocks above but are declared on `body`. + * CSS resolves var() inside a custom-property declaration at the element where + * the property is declared, so body-level declarations can see --vscode-* + * variables that applyTheme() writes to body.style. Every token above whose + * value reads any var() chain — indirect bindings like var(--color-door-bg) + * included, not just direct --vscode-* reads — must be repeated here with the + * same value, or it resolves to nothing in the hosts where applyTheme() is the + * only writer (standalone, website, Pocket); consumed-keys.test.ts pins the two + * lists together. */ body { + --mt-font-size: var(--vscode-font-size); --mt-font-family: var(--vscode-editor-font-family); --font-sans: var(--vscode-editor-font-family); --font-mono: var(--vscode-editor-font-family);