@weasel-js/theme 1.9.4 → 1.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,7 +5,7 @@ definition into CSS custom properties plus a parallel TS export.
5
5
 
6
6
  Part of [weasel](https://github.com/orochi235/weasel), a domain-agnostic 2D
7
7
  scene-graph canvas kit for React. See the
8
- [API reference](https://orochi235.github.io/weasel/api/).
8
+ [API reference](https://orochi235.github.io/weasel/#pkg/theme).
9
9
 
10
10
  ## Install
11
11
 
@@ -76,6 +76,85 @@ beside it. The resolved record carries that CSS as is, because a canvas has no
76
76
  `currentColor`; flatten it with `resolveCurrentColor(value, resolved['--wzl-fg'])`,
77
77
  passing the color of the text it is drawn beside.
78
78
 
79
+ ## Between two modes
80
+
81
+ `blendTheme(theme, weights, selection?)` is the theme partway between values
82
+ of its axes, as `resolveTheme` returns one:
83
+
84
+ ```ts
85
+ blendTheme(acme, { mode: { dark: 0.25, light: 0.75 } });
86
+ ```
87
+
88
+ It resolves the theme at each weighted selection and mixes what each token
89
+ resolved to: a color in OKLCH, a number with one unit linearly, and anything
90
+ else the heaviest selection's. `applyTheme(el, theme, selection, { weights })`
91
+ holds that blend where it would hold the selection, so calling it with the
92
+ next weights is one frame of a crossfade.
93
+
94
+ ## As a prefs schema
95
+
96
+ `@weasel-js/theme/prefs` turns a theme into a `@weasel-js/prefs` schema, and a
97
+ store over that schema back into a theme:
98
+
99
+ ```ts
100
+ import { openPrefsSync } from '@weasel-js/prefs';
101
+ import { applyTheme, weaselTheme } from '@weasel-js/theme';
102
+ import { themeFromPrefs, themePrefsSchema, themePrefsSelection } from '@weasel-js/theme/prefs';
103
+
104
+ const store = openPrefsSync(themePrefsSchema(weaselTheme), { storage, prefix: 'theme.' });
105
+ store.set('accent.accent-base', '#ff3366');
106
+ store.set('surface.surface', '#fffdf8', { at: { 'axis.mode': 'light' } });
107
+
108
+ applyTheme(document.documentElement, themeFromPrefs(store, weaselTheme), themePrefsSelection(store));
109
+ ```
110
+
111
+ The schema holds an `enum` per axis under `axis` (`axis.mode`,
112
+ `axis.density`) and a page per token group, each token at `<group>.<token>`
113
+ with no `--wzl-`: a `color`, a `number` in the token's unit, or a `string` of
114
+ CSS text for a value neither holds, such as a font stack or a `var()`. Override
115
+ hooks are leaves too. Every leaf may be split by every axis, and its default
116
+ is what the theme resolves to, already split along the axes the token differs
117
+ by, so a store nothing was written to reads the theme as it is.
118
+
119
+ `themePrefsPins(store)` is the `pins` those values amount to. Only a leaf with
120
+ a value of its own is a pin, so a token left alone keeps its rule: pin
121
+ `accent-base` and `--wzl-accent` still follows it. A store reads such an alias
122
+ from the base theme, where it has not moved; `themePrefsValues(store, base)`
123
+ gives the values and loci for a form with each unset leaf read from the pinned
124
+ theme.
125
+
126
+ ## Desktop themes
127
+
128
+ Each is a pins-only theme over the built-in one, on its own entry so an app
129
+ that never asks for it never loads it:
130
+
131
+ ```ts
132
+ import { applyTheme } from '@weasel-js/theme';
133
+ import { win9xTheme } from '@weasel-js/theme/themes/win9x';
134
+
135
+ applyTheme(document.documentElement, win9xTheme); // opens in light mode
136
+ ```
137
+
138
+ - `win9xTheme`: Windows 95 and 98. Gray faces in two-pixel bevels, square
139
+ corners, a navy-to-blue title strip, and the system sans at 11, 12, or 14px
140
+ by density.
141
+ - `platinumTheme`, from `@weasel-js/theme/themes/platinum`: Mac OS 8 and 9's
142
+ Platinum. Light gray faces with a black outline and a soft two-tone edge,
143
+ rounded buttons, white fields, and pinstriped window titles centered on the
144
+ strip. The same entry exports `classicMacTheme`, which it extends: the faces,
145
+ sizes, and geometry every classic Mac OS look shares, with no color of its
146
+ own. A System 7 theme is `defineTheme({ name, extends: classicMacTheme, pins })`
147
+ with that era's colors and edges.
148
+
149
+ They open in light mode, the only one the originals had; the dark mode is the
150
+ theme's own. The system faces are named first in the font stacks and are not
151
+ bundled, so a machine without them draws the next face in the stack. The
152
+ bevels and flat button faces are override hooks the theme pins (see below),
153
+ which the controls in `@weasel-js/ui` read wherever they draw a raised face, a
154
+ well, or a button that stays down; the description of each hook lists them.
155
+ To title every group as a window, wrap the app in
156
+ `<DefaultMotif motif={motifs.titlebar()}>` from `@weasel-js/ui`.
157
+
79
158
  ## The engine
80
159
 
81
160
  `@weasel-js/theme/engine` is for authoring layered theme definitions: seeds,
@@ -157,10 +236,11 @@ names the token bare (`"ref": "gray-800"`).
157
236
 
158
237
  ## Override hooks
159
238
 
160
- Ten custom properties resize kit components and **no theme declares them**:
161
- `--wzl-swatch-size`, `--wzl-timeline-label-w`, `--wzl-number-field-width` and
162
- the rest. Kit CSS reads each one with a fallback, so setting it on any
163
- container is what changes it, and leaving it alone takes the fallback:
239
+ Some custom properties restyle kit components and **the built-in theme does
240
+ not declare them**: `--wzl-swatch-size`, `--wzl-timeline-label-w`,
241
+ `--wzl-number-field-width` and the rest. Kit CSS reads each one with a
242
+ fallback, so setting it on any container is what changes it, and leaving it
243
+ alone takes the fallback:
164
244
 
165
245
  ```css
166
246
  .dense-panel { --wzl-swatch-size: 20px; --wzl-number-field-width: 6ch; }
@@ -175,6 +255,13 @@ import { TOKEN_MANIFEST } from '@weasel-js/theme';
175
255
  const hooks = TOKEN_MANIFEST.filter((t) => t.hook);
176
256
  ```
177
257
 
258
+ A theme built with `defineTheme` can pin one too, which is how a theme changes
259
+ what a token cannot say: `--wzl-bevel-raised`, `--wzl-bevel-sunken` and
260
+ `--wzl-bevel-pressed` are the box shadows of a raised face, a well and a pressed
261
+ button, and the `--wzl-button-*` hooks replace a Button's glass with a flat
262
+ face. A pin's value is fixed where the theme is applied, so one written with
263
+ `var()` does not follow a token a container overrides further down.
264
+
178
265
  They stay out of `tokens.css` on purpose. A `:root` value would outrank the
179
266
  fallback written beside every read, so the two would have to agree forever, and
180
267
  `npm run check:token-reads` would stop requiring that the fallback be there at
@@ -0,0 +1,160 @@
1
+ import { pick, mergeAxes, fullSelection } from './chunk-P5F3GZV6.js';
2
+ import { parseHexBytes } from '@weasel-js/paint';
3
+
4
+ // src/dtcg/value.ts
5
+ function fontStack(value) {
6
+ return value.map((f) => typeof f === "string" && /\s/.test(f) ? `'${f}'` : String(f)).join(", ");
7
+ }
8
+ function serializeTokenValue(type, value) {
9
+ if (Array.isArray(value)) {
10
+ if (type === "cubicBezier") return `cubic-bezier(${value.join(", ")})`;
11
+ if (type === "fontFamily") return fontStack(value);
12
+ return value.join(", ");
13
+ }
14
+ return String(value);
15
+ }
16
+ var BEZIER = /^cubic-bezier\(\s*([^)]*)\)$/i;
17
+ function parseTokenValue(type, text) {
18
+ const raw = text.trim();
19
+ if (type === "cubicBezier") {
20
+ const inner = BEZIER.exec(raw)?.[1] ?? raw;
21
+ const parts = inner.split(",").map((p) => Number(p.trim()));
22
+ return parts.length === 4 && parts.every((n) => Number.isFinite(n)) ? parts : raw;
23
+ }
24
+ if (type === "fontFamily") {
25
+ const families = raw.split(",").map((f) => f.trim().replace(/^['"]|['"]$/g, "").trim()).filter((f) => f !== "");
26
+ return families.length > 0 ? families : raw;
27
+ }
28
+ return raw;
29
+ }
30
+
31
+ // src/chainTokens.ts
32
+ function localSelections(chain, selection) {
33
+ let axes = {};
34
+ return chain.map((link) => {
35
+ axes = mergeAxes(axes, link.axes);
36
+ return { ...selection, ...fullSelection(axes, selection) };
37
+ });
38
+ }
39
+ function tokenAt(chain, name, selection) {
40
+ const local = localSelections(chain, selection);
41
+ for (let i = chain.length - 1; i >= 0; i--) {
42
+ const { tokens } = chain[i];
43
+ if (!Object.hasOwn(tokens, name)) continue;
44
+ const picked = pick(tokens[name], local[i]);
45
+ if (picked.ok) return picked.value;
46
+ }
47
+ return void 0;
48
+ }
49
+ function tokensAt(chain, selection) {
50
+ const local = localSelections(chain, selection);
51
+ const out = {};
52
+ chain.forEach((link, i) => {
53
+ for (const [name, v] of Object.entries(link.tokens)) {
54
+ const picked = pick(v, local[i]);
55
+ if (picked.ok) out[name] = picked.value;
56
+ }
57
+ });
58
+ return out;
59
+ }
60
+ function withAlpha(color, alpha) {
61
+ const rgba = parseRgba(color.trim());
62
+ if (!rgba) return mixAlpha(color.trim(), alpha);
63
+ const [r, g, b, a] = rgba;
64
+ return `rgba(${r}, ${g}, ${b}, ${round(a * alpha, 3)})`;
65
+ }
66
+ function mixAlpha(color, alpha) {
67
+ return `color-mix(in srgb, ${color} ${round(alpha * 100, 4)}%, transparent)`;
68
+ }
69
+ function atAlpha(color, alpha) {
70
+ const rgba = parseRgba(color.trim());
71
+ if (!rgba) return `rgb(from ${color.trim()} r g b / ${alpha})`;
72
+ const [r, g, b] = rgba;
73
+ return `rgba(${r}, ${g}, ${b}, ${round(alpha, 3)})`;
74
+ }
75
+ function round(n, places) {
76
+ return Number(n.toFixed(places));
77
+ }
78
+ function parseRgba(s) {
79
+ if (s.toLowerCase() === "transparent") return [0, 0, 0, 0];
80
+ return parseHex(s) ?? parseRgbFn(s);
81
+ }
82
+ function parseHex(s) {
83
+ const bytes = parseHexBytes(s);
84
+ return bytes && [bytes[0], bytes[1], bytes[2], bytes[3] / 255];
85
+ }
86
+ function parseRgbFn(s) {
87
+ const m = /^rgba?\(([^()]*)\)$/i.exec(s);
88
+ if (!m) return null;
89
+ const [channels, alphaPart, extra] = m[1].split("/");
90
+ if (extra !== void 0) return null;
91
+ const parts = channels.trim().split(/\s*,\s*|\s+/);
92
+ if (alphaPart !== void 0) parts.push(alphaPart.trim());
93
+ if (parts.length !== 3 && parts.length !== 4) return null;
94
+ const channel = (p) => p.endsWith("%") ? Number.parseFloat(p) / 100 * 255 : Number(p);
95
+ const alpha = (p) => p.endsWith("%") ? Number.parseFloat(p) / 100 : Number(p);
96
+ const [r, g, b] = parts.slice(0, 3).map(channel);
97
+ const a = parts[3] === void 0 ? 1 : alpha(parts[3]);
98
+ if (![r, g, b, a].every(Number.isFinite)) return null;
99
+ return [Math.round(r), Math.round(g), Math.round(b), a];
100
+ }
101
+
102
+ // src/dtcg/resolve.ts
103
+ var REF = /^\{([^}]+)\}$/;
104
+ function refTarget(value) {
105
+ if (typeof value !== "string") return null;
106
+ const m = REF.exec(value.trim());
107
+ if (!m) return null;
108
+ const path = m[1];
109
+ const dot = path.indexOf(".");
110
+ return dot === -1 ? path : path.slice(dot + 1);
111
+ }
112
+ function resolveTokens(tokens) {
113
+ const out = {};
114
+ const inProgress = /* @__PURE__ */ new Set();
115
+ const resolveOne = (name) => {
116
+ const cached = out[name];
117
+ if (cached !== void 0) return cached;
118
+ const token = tokens[name];
119
+ if (!token) throw new Error(`Unknown token "${name}"`);
120
+ if (inProgress.has(name)) {
121
+ throw new Error(`Reference cycle at token "${name}" (${[...inProgress].join(" \u2192 ")})`);
122
+ }
123
+ inProgress.add(name);
124
+ const target = refTarget(token.value);
125
+ let value;
126
+ if (target === null) {
127
+ value = serializeTokenValue(token.type, token.value);
128
+ } else if (!tokens[target]) {
129
+ throw new Error(`Token "${name}" references "${target}", which is not defined`);
130
+ } else {
131
+ value = resolveOne(target);
132
+ }
133
+ if (token.alpha !== void 0) value = withAlpha(value, token.alpha);
134
+ inProgress.delete(name);
135
+ out[name] = value;
136
+ return value;
137
+ };
138
+ for (const name of Object.keys(tokens)) resolveOne(name);
139
+ return out;
140
+ }
141
+
142
+ // src/resolveTheme.ts
143
+ function themeChain(theme) {
144
+ const out = [];
145
+ for (let t = theme; t; t = t.extends) out.unshift(t);
146
+ return out;
147
+ }
148
+ function themeAxes(theme) {
149
+ return themeChain(theme).reduce((axes, t) => mergeAxes(axes, t.axes), {});
150
+ }
151
+ function resolveTheme(theme, selection = {}) {
152
+ const sel = fullSelection(themeAxes(theme), selection);
153
+ const out = {};
154
+ for (const [name, value] of Object.entries(resolveTokens(tokensAt(themeChain(theme), sel)))) out[`--wzl-${name}`] = value;
155
+ return out;
156
+ }
157
+
158
+ export { atAlpha, mixAlpha, parseTokenValue, resolveTheme, resolveTokens, serializeTokenValue, themeAxes, themeChain, tokenAt, tokensAt, withAlpha };
159
+ //# sourceMappingURL=chunk-32JKTYS2.js.map
160
+ //# sourceMappingURL=chunk-32JKTYS2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/dtcg/value.ts","../src/chainTokens.ts","../src/dtcg/color.ts","../src/dtcg/resolve.ts","../src/resolveTheme.ts"],"names":[],"mappings":"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","file":"chunk-32JKTYS2.js","sourcesContent":["import type { TokenValue } from './types.ts';\n\n/**\n * Quote family names containing whitespace; leave everything else bare.\n * Generic keywords (`sans-serif`) and hyphen-prefixed identifiers\n * (`-apple-system`) break if quoted, and neither contains a space.\n */\nfunction fontStack(value: readonly (string | number)[]): string {\n return value.map((f) => (typeof f === 'string' && /\\s/.test(f) ? `'${f}'` : String(f))).join(', ');\n}\n\n/**\n * A token's value as CSS renders it. A DTCG value is not always a string: a\n * `fontFamily` is a list of names and a `cubicBezier` is four numbers, and both\n * have a spelling that belongs in a stylesheet rather than in JSON.\n */\nexport function serializeTokenValue(type: string, value: TokenValue): string {\n if (Array.isArray(value)) {\n if (type === 'cubicBezier') return `cubic-bezier(${value.join(', ')})`;\n if (type === 'fontFamily') return fontStack(value);\n return value.join(', ');\n }\n return String(value);\n}\n\nconst BEZIER = /^cubic-bezier\\(\\s*([^)]*)\\)$/i;\n\n/**\n * The inverse of {@link serializeTokenValue}: text a person edited, back in the\n * shape a definition stores. Text that does not parse for its type is kept as\n * text rather than thrown away, so a half-typed value stays editable.\n */\nexport function parseTokenValue(type: string, text: string): TokenValue {\n const raw = text.trim();\n if (type === 'cubicBezier') {\n const inner = BEZIER.exec(raw)?.[1] ?? raw;\n const parts = inner.split(',').map((p) => Number(p.trim()));\n return parts.length === 4 && parts.every((n) => Number.isFinite(n)) ? parts : raw;\n }\n if (type === 'fontFamily') {\n const families = raw\n .split(',')\n .map((f) => f.trim().replace(/^['\"]|['\"]$/g, '').trim())\n .filter((f) => f !== '');\n return families.length > 0 ? families : raw;\n }\n return raw;\n}\n","import { fullSelection, mergeAxes, pick, type AxisDefs, type Selection, type Varying } from './axes.ts';\nimport type { FlatTokens, RawToken } from './dtcg/types.ts';\n\n/** One theme of an `extends` chain, as far as choosing its tokens goes. */\nexport interface ChainLink {\n readonly axes: AxisDefs;\n readonly tokens: Readonly<Record<string, Varying<RawToken>>>;\n}\n\n/**\n * The selection each theme of a root-first chain reads its own tokens at. An axis value a theme and\n * everything it extends never declared (one a descendant added) becomes that theme's default for the\n * axis, which is what the stylesheet's cascade gives it: no block of the ancestor matches the new\n * value, so its default declarations show through.\n */\nfunction localSelections(chain: readonly ChainLink[], selection: Selection): Selection[] {\n let axes: AxisDefs = {};\n return chain.map((link) => {\n axes = mergeAxes(axes, link.axes);\n return { ...selection, ...fullSelection(axes, selection) };\n });\n}\n\n/** `name` from the nearest theme of a root-first chain that has it at `selection`. */\nexport function tokenAt(chain: readonly ChainLink[], name: string, selection: Selection): RawToken | undefined {\n const local = localSelections(chain, selection);\n for (let i = chain.length - 1; i >= 0; i--) {\n const { tokens } = chain[i];\n if (!Object.hasOwn(tokens, name)) continue;\n const picked = pick(tokens[name], local[i]);\n if (picked.ok) return picked.value;\n }\n return undefined;\n}\n\n/** Every token of a root-first chain at `selection`, each from the nearest theme that has it there. */\nexport function tokensAt(chain: readonly ChainLink[], selection: Selection): FlatTokens {\n const local = localSelections(chain, selection);\n const out: Record<string, RawToken> = {};\n chain.forEach((link, i) => {\n for (const [name, v] of Object.entries(link.tokens)) {\n const picked = pick(v, local[i]);\n if (picked.ok) out[name] = picked.value;\n }\n });\n return out;\n}\n","import { parseHexBytes } from '@weasel-js/paint';\n\n/**\n * A resolved color at `alpha`, multiplied into any alpha it already carries. The\n * JS side of the alpha extension; the CSS side emits `color-mix()` instead so DOM\n * chrome keeps tracking a downstream override of the referenced token.\n *\n * Hex and `rgb()`/`rgba()` become an `rgba()` literal. Anything else — a named\n * color, `hsl()`, `oklch()`, a `var()` — becomes the same `color-mix()` the CSS\n * side writes, which the browser resolves.\n */\nexport function withAlpha(color: string, alpha: number): string {\n const rgba = parseRgba(color.trim());\n if (!rgba) return mixAlpha(color.trim(), alpha);\n const [r, g, b, a] = rgba;\n return `rgba(${r}, ${g}, ${b}, ${round(a * alpha, 3)})`;\n}\n\n/** `color` at `alpha` as a `color-mix()` with transparent, for a color only the browser can resolve. */\nexport function mixAlpha(color: string, alpha: number): string {\n return `color-mix(in srgb, ${color} ${round(alpha * 100, 4)}%, transparent)`;\n}\n\n/** A resolved color with its alpha replaced by `alpha`, as `rgb(from c r g b / alpha)` computes it. */\nexport function atAlpha(color: string, alpha: number): string {\n const rgba = parseRgba(color.trim());\n if (!rgba) return `rgb(from ${color.trim()} r g b / ${alpha})`;\n const [r, g, b] = rgba;\n return `rgba(${r}, ${g}, ${b}, ${round(alpha, 3)})`;\n}\n\ntype Rgba = [number, number, number, number];\n\nfunction round(n: number, places: number): number {\n return Number(n.toFixed(places));\n}\n\nfunction parseRgba(s: string): Rgba | null {\n if (s.toLowerCase() === 'transparent') return [0, 0, 0, 0];\n return parseHex(s) ?? parseRgbFn(s);\n}\n\nfunction parseHex(s: string): Rgba | null {\n const bytes = parseHexBytes(s);\n return bytes && [bytes[0], bytes[1], bytes[2], bytes[3] / 255];\n}\n\nfunction parseRgbFn(s: string): Rgba | null {\n const m = /^rgba?\\(([^()]*)\\)$/i.exec(s);\n if (!m) return null;\n const [channels, alphaPart, extra] = m[1].split('/');\n if (extra !== undefined) return null;\n const parts = channels.trim().split(/\\s*,\\s*|\\s+/);\n if (alphaPart !== undefined) parts.push(alphaPart.trim());\n if (parts.length !== 3 && parts.length !== 4) return null;\n\n const channel = (p: string) => (p.endsWith('%') ? (Number.parseFloat(p) / 100) * 255 : Number(p));\n const alpha = (p: string) => (p.endsWith('%') ? Number.parseFloat(p) / 100 : Number(p));\n const [r, g, b] = parts.slice(0, 3).map(channel);\n const a = parts[3] === undefined ? 1 : alpha(parts[3]);\n if (![r, g, b, a].every(Number.isFinite)) return null;\n return [Math.round(r), Math.round(g), Math.round(b), a];\n}\n","import { withAlpha } from './color.ts';\nimport type { FlatTokens, ResolvedTokenMap, TokenValue } from './types.ts';\nimport { serializeTokenValue } from './value.ts';\n\n/** `{color.gray-100}` → `gray-100`. The type group is dropped, per the naming rule. */\nconst REF = /^\\{([^}]+)\\}$/;\n\nfunction refTarget(value: TokenValue): string | null {\n if (typeof value !== 'string') return null;\n const m = REF.exec(value.trim());\n if (!m) return null;\n const path = m[1];\n const dot = path.indexOf('.');\n return dot === -1 ? path : path.slice(dot + 1);\n}\n\n/**\n * Resolve flat tokens to final CSS-ready strings.\n *\n * Merge modes before calling: this function has no notion of modes, only of a\n * complete token set.\n */\nexport function resolveTokens(tokens: FlatTokens): ResolvedTokenMap {\n const out: ResolvedTokenMap = {};\n const inProgress = new Set<string>();\n\n const resolveOne = (name: string): string => {\n const cached = out[name];\n if (cached !== undefined) return cached;\n\n const token = tokens[name];\n if (!token) throw new Error(`Unknown token \"${name}\"`);\n\n if (inProgress.has(name)) {\n throw new Error(`Reference cycle at token \"${name}\" (${[...inProgress].join(' → ')})`);\n }\n inProgress.add(name);\n\n const target = refTarget(token.value);\n let value: string;\n if (target === null) {\n value = serializeTokenValue(token.type, token.value);\n } else if (!tokens[target]) {\n throw new Error(`Token \"${name}\" references \"${target}\", which is not defined`);\n } else {\n value = resolveOne(target);\n }\n\n if (token.alpha !== undefined) value = withAlpha(value, token.alpha);\n\n inProgress.delete(name);\n out[name] = value;\n return value;\n };\n\n for (const name of Object.keys(tokens)) resolveOne(name);\n return out;\n}\n\n/** Merge a mode's tokens over the mode-invariant set. Later wins. */\nexport function mergeTokens(base: FlatTokens, mode: FlatTokens): FlatTokens {\n return { ...base, ...mode };\n}\n","import { fullSelection, mergeAxes, type AxisDefs, type Selection } from './axes.ts';\nimport { tokensAt } from './chainTokens.ts';\nimport { resolveTokens } from './dtcg/resolve.ts';\nimport type { TokenName } from './generated/themes.ts';\nimport type { Theme } from './theme.ts';\n\n/** The output of `resolveTheme`: every token of a theme, for one selection, keyed\n * by CSS custom-property name and flattened to a final CSS value. */\nexport type ResolvedTheme = Readonly<Record<TokenName, string>>;\n\n/** Root-first, so the leaf theme's tokens land last. */\nexport function themeChain(theme: Theme): Theme[] {\n const out: Theme[] = [];\n for (let t: Theme | null = theme; t; t = t.extends) out.unshift(t);\n return out;\n}\n\n/** Every axis the chain declares. An axis declared more than once keeps every value; a descendant's value entries and default win. */\nexport function themeAxes(theme: Theme): AxisDefs {\n return themeChain(theme).reduce<AxisDefs>((axes, t) => mergeAxes(axes, t.axes), {});\n}\n\n/**\n * Merge the extends chain at a selection, resolve every alias, and key the\n * result by CSS custom-property name. A missing or unknown axis value takes the\n * axis default, and a theme in the chain reads a value it never declared at its\n * own default for that axis.\n *\n * Pure — no DOM. An unresolvable reference throws rather than falling back.\n */\nexport function resolveTheme(theme: Theme, selection: Selection = {}): ResolvedTheme {\n const sel = fullSelection(themeAxes(theme), selection);\n const out: Record<string, string> = {};\n for (const [name, value] of Object.entries(resolveTokens(tokensAt(themeChain(theme), sel)))) out[`--wzl-${name}`] = value;\n return out as ResolvedTheme;\n}\n"]}