Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions docs/specs/theme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
39 changes: 39 additions & 0 deletions lib/src/lib/themes/consumed-keys.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> {
const out = new Map<string, string>();
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([]);
});
});
14 changes: 10 additions & 4 deletions lib/src/theme.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down