@weasel-js/theme 1.5.0 → 1.5.2

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
@@ -1,7 +1,7 @@
1
1
  # @weasel-js/theme
2
2
 
3
- Design tokens shared by weasel-ui and weasel-hud — generated from a DTCG
4
- source into CSS custom properties plus a parallel TS export.
3
+ Design tokens shared by weasel-ui and weasel-hud — generated from a theme
4
+ 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
@@ -38,11 +38,13 @@ import { defineTheme, applyTheme } from '@weasel-js/theme';
38
38
 
39
39
  const acme = defineTheme({
40
40
  name: 'acme',
41
- tokens: { 'accent-base': '#ff3366' }, // aliases rebase onto it
42
- modes: { light: { surface: '#fffdf8' } },
41
+ pins: {
42
+ 'accent-base': '#ff3366', // aliases rebase onto it
43
+ surface: { by: 'mode', light: '#fffdf8' }, // dark keeps the base's
44
+ },
43
45
  });
44
46
 
45
- applyTheme(document.documentElement, acme, 'light');
47
+ applyTheme(document.documentElement, acme, { mode: 'light' });
46
48
  ```
47
49
 
48
50
  `extends` defaults to the built-in theme, so a partial theme can never be
@@ -50,18 +52,45 @@ accidentally incomplete. Overriding a primitive rebases every alias that
50
52
  references it — set `accent-base` and `--wzl-accent`, `--wzl-accent-hover`,
51
53
  `--wzl-focus-ring` and `--wzl-glass-tint` all follow.
52
54
 
53
- Themes exported from a design tool load through `loadDTCG(json)` —
54
- `interstellarTheme` in `@weasel-js/labkit` is a worked example, authored as a
55
- DTCG document and loaded at import time.
55
+ Themes exported from a design tool load through `loadDTCG(json)`.
56
+ `interstellarTheme` in `@weasel-js/labkit` is a worked example of the other
57
+ path: a pins-only definition in JSON, passed to `defineTheme`.
56
58
 
57
- In React, `<ThemeProvider theme={acme} mode="light">` from
59
+ In React, `<ThemeProvider theme={acme} selection={{ mode: 'light' }}>` from
58
60
  `@weasel-js/theme/react` does the same and publishes the resolved record via
59
61
  `useTheme()` — which is how canvas and WebGL surfaces stay in sync without
60
62
  reading the DOM.
61
63
 
64
+ ## The engine
65
+
66
+ `@weasel-js/theme/engine` is for authoring layered theme definitions: seeds,
67
+ generated ramps and scales, semantic rules, components and pins. `derive`
68
+ produces a definition's tokens for one selection, with where each came from and
69
+ any issues; `bake` folds every selection into the tokens a runtime `Theme`
70
+ holds; `toDTCG` writes a theme out as a DTCG document. The runtime entry never
71
+ loads it.
72
+
73
+ ```ts
74
+ import { bake, derive } from '@weasel-js/theme/engine';
75
+ import { weaselTheme, type Theme, type ThemeDefinition } from '@weasel-js/theme';
76
+
77
+ const slate: ThemeDefinition = {
78
+ name: 'slate',
79
+ axes: { mode: { default: 'dark', values: { dark: { scheme: 'dark' }, light: { scheme: 'light' } } } },
80
+ ramps: { slate: { kind: 'lightness', steps: ['100', '500', '900'], lightness: [0.96, 0.22] } },
81
+ semantics: {
82
+ surface: { by: 'mode', dark: { ramp: 'slate', step: '900' }, light: { ramp: 'slate', step: '100' } },
83
+ fg: { by: 'mode', dark: { ref: 'slate-100' }, light: { ref: 'slate-900' } },
84
+ },
85
+ };
86
+
87
+ const { tokens, provenance, issues } = derive(slate, { mode: 'light' });
88
+ const theme: Theme = { ...bake(slate), extends: weaselTheme };
89
+ ```
90
+
62
91
  ## Editing tokens
63
92
 
64
- `src/generated/` is generated — never edit it. Change `tokens/weasel/*.json`,
93
+ `src/generated/` is generated — never edit it. Change `themes/weasel.json`,
65
94
  then:
66
95
 
67
96
  ```sh
@@ -70,10 +99,34 @@ npm run gen:tokens -w @weasel-js/theme
70
99
 
71
100
  CI re-runs the generator and fails if the committed output differs.
72
101
 
73
- Token names are flat leaf keys inside `$type` groups: `color.fg-muted` becomes
74
- `--wzl-fg-muted`. The group carries `$type` and contributes nothing to the name,
75
- because `--wzl-accent` and `--wzl-accent-base` are both real tokens and DTCG
76
- forbids a token that is also a group.
102
+ Token names are the definition's keys: `fg-muted` becomes `--wzl-fg-muted`, and
103
+ a reference in a pin's value is written `{fg-muted}`, while a semantic's `ref`
104
+ names the token bare (`"ref": "gray-800"`).
105
+
106
+ ## Override hooks
107
+
108
+ Ten custom properties resize kit components and **no theme declares them**:
109
+ `--wzl-swatch-size`, `--wzl-timeline-label-w`, `--wzl-number-field-width` and
110
+ the rest. Kit CSS reads each one with a fallback, so setting it on any
111
+ container is what changes it, and leaving it alone takes the fallback:
112
+
113
+ ```css
114
+ .dense-panel { --wzl-swatch-size: 20px; --wzl-number-field-width: 6ch; }
115
+ ```
116
+
117
+ They are declared in `src/hooks.ts` and land in `TOKEN_MANIFEST` with
118
+ `hook: true`, which is where to find the current list, each one's fallback and
119
+ what it sizes:
120
+
121
+ ```ts
122
+ import { TOKEN_MANIFEST } from '@weasel-js/theme';
123
+ const hooks = TOKEN_MANIFEST.filter((t) => t.hook);
124
+ ```
125
+
126
+ They stay out of `tokens.css` on purpose. A `:root` value would outrank the
127
+ fallback written beside every read, so the two would have to agree forever, and
128
+ `npm run check:token-reads` would stop requiring that the fallback be there at
129
+ all. Adding a hook means adding it to `src/hooks.ts` and regenerating.
77
130
 
78
131
  ## Licenses
79
132
 
@@ -0,0 +1,175 @@
1
+ // src/axes.ts
2
+ function isByAxis(v) {
3
+ return typeof v === "object" && v !== null && !Array.isArray(v) && typeof v.by === "string";
4
+ }
5
+ function fullSelection(axes, selection = {}) {
6
+ const out = {};
7
+ for (const [name, def] of Object.entries(axes)) {
8
+ const v = selection[name];
9
+ out[name] = v !== void 0 && v in def.values ? v : def.default;
10
+ }
11
+ return out;
12
+ }
13
+ function mergeAxes(parent, child) {
14
+ const out = { ...parent };
15
+ for (const [name, def] of Object.entries(child)) {
16
+ out[name] = { ...def, values: { ...parent[name]?.values, ...def.values } };
17
+ }
18
+ return out;
19
+ }
20
+ function enumerateSelections(axes) {
21
+ let out = [{}];
22
+ for (const [name, def] of Object.entries(axes)) {
23
+ out = out.flatMap((s) => Object.keys(def.values).map((v) => ({ ...s, [name]: v })));
24
+ }
25
+ return out;
26
+ }
27
+ function selectionKey(axes, selection) {
28
+ const full = fullSelection(axes, selection);
29
+ return Object.keys(axes).map((a) => `${a}=${full[a]}`).join(",");
30
+ }
31
+ function pick(v, selection) {
32
+ let cur = v;
33
+ while (isByAxis(cur)) {
34
+ const axis = cur.by;
35
+ const value = selection[axis];
36
+ if (value === void 0 || value === "by" || !(value in cur)) return { ok: false, axis, value: value ?? "" };
37
+ cur = cur[value];
38
+ }
39
+ return { ok: true, value: cur };
40
+ }
41
+ function pickAll(record, selection) {
42
+ const out = {};
43
+ for (const [name, v] of Object.entries(record)) {
44
+ const picked = pick(v, selection);
45
+ if (picked.ok) out[name] = picked.value;
46
+ }
47
+ return out;
48
+ }
49
+
50
+ // src/dtcg/value.ts
51
+ function fontStack(value) {
52
+ return value.map((f) => typeof f === "string" && /\s/.test(f) ? `'${f}'` : String(f)).join(", ");
53
+ }
54
+ function serializeTokenValue(type, value) {
55
+ if (Array.isArray(value)) {
56
+ if (type === "cubicBezier") return `cubic-bezier(${value.join(", ")})`;
57
+ if (type === "fontFamily") return fontStack(value);
58
+ return value.join(", ");
59
+ }
60
+ return String(value);
61
+ }
62
+ var BEZIER = /^cubic-bezier\(\s*([^)]*)\)$/i;
63
+ function parseTokenValue(type, text) {
64
+ const raw = text.trim();
65
+ if (type === "cubicBezier") {
66
+ const inner = BEZIER.exec(raw)?.[1] ?? raw;
67
+ const parts = inner.split(",").map((p) => Number(p.trim()));
68
+ return parts.length === 4 && parts.every((n) => Number.isFinite(n)) ? parts : raw;
69
+ }
70
+ if (type === "fontFamily") {
71
+ const families = raw.split(",").map((f) => f.trim().replace(/^['"]|['"]$/g, "").trim()).filter((f) => f !== "");
72
+ return families.length > 0 ? families : raw;
73
+ }
74
+ return raw;
75
+ }
76
+
77
+ // src/dtcg/color.ts
78
+ function withAlpha(color, alpha) {
79
+ const rgba = parseRgba(color.trim());
80
+ if (!rgba) return `color-mix(in srgb, ${color.trim()} ${round(alpha * 100, 4)}%, transparent)`;
81
+ const [r, g, b, a] = rgba;
82
+ return `rgba(${r}, ${g}, ${b}, ${round(a * alpha, 3)})`;
83
+ }
84
+ function round(n, places) {
85
+ return Number(n.toFixed(places));
86
+ }
87
+ function parseRgba(s) {
88
+ if (s.toLowerCase() === "transparent") return [0, 0, 0, 0];
89
+ return parseHex(s) ?? parseRgbFn(s);
90
+ }
91
+ function parseHex(s) {
92
+ const m = /^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.exec(s);
93
+ if (!m) return null;
94
+ const body = m[1];
95
+ const full = body.length <= 4 ? body.replace(/./g, (c) => c + c) : body;
96
+ const byte = (i) => Number.parseInt(full.slice(i, i + 2), 16);
97
+ return [byte(0), byte(2), byte(4), full.length === 8 ? byte(6) / 255 : 1];
98
+ }
99
+ function parseRgbFn(s) {
100
+ const m = /^rgba?\(([^()]*)\)$/i.exec(s);
101
+ if (!m) return null;
102
+ const [channels, alphaPart, extra] = m[1].split("/");
103
+ if (extra !== void 0) return null;
104
+ const parts = channels.trim().split(/\s*,\s*|\s+/);
105
+ if (alphaPart !== void 0) parts.push(alphaPart.trim());
106
+ if (parts.length !== 3 && parts.length !== 4) return null;
107
+ const channel = (p) => p.endsWith("%") ? Number.parseFloat(p) / 100 * 255 : Number(p);
108
+ const alpha = (p) => p.endsWith("%") ? Number.parseFloat(p) / 100 : Number(p);
109
+ const [r, g, b] = parts.slice(0, 3).map(channel);
110
+ const a = parts[3] === void 0 ? 1 : alpha(parts[3]);
111
+ if (![r, g, b, a].every(Number.isFinite)) return null;
112
+ return [Math.round(r), Math.round(g), Math.round(b), a];
113
+ }
114
+
115
+ // src/dtcg/resolve.ts
116
+ var REF = /^\{([^}]+)\}$/;
117
+ function refTarget(value) {
118
+ if (typeof value !== "string") return null;
119
+ const m = REF.exec(value.trim());
120
+ if (!m) return null;
121
+ const path = m[1];
122
+ const dot = path.indexOf(".");
123
+ return dot === -1 ? path : path.slice(dot + 1);
124
+ }
125
+ function resolveTokens(tokens) {
126
+ const out = {};
127
+ const inProgress = /* @__PURE__ */ new Set();
128
+ const resolveOne = (name) => {
129
+ const cached = out[name];
130
+ if (cached !== void 0) return cached;
131
+ const token = tokens[name];
132
+ if (!token) throw new Error(`Unknown token "${name}"`);
133
+ if (inProgress.has(name)) {
134
+ throw new Error(`Reference cycle at token "${name}" (${[...inProgress].join(" \u2192 ")})`);
135
+ }
136
+ inProgress.add(name);
137
+ const target = refTarget(token.value);
138
+ let value;
139
+ if (target === null) {
140
+ value = serializeTokenValue(token.type, token.value);
141
+ } else if (!tokens[target]) {
142
+ throw new Error(`Token "${name}" references "${target}", which is not defined`);
143
+ } else {
144
+ value = resolveOne(target);
145
+ }
146
+ if (token.alpha !== void 0) value = withAlpha(value, token.alpha);
147
+ inProgress.delete(name);
148
+ out[name] = value;
149
+ return value;
150
+ };
151
+ for (const name of Object.keys(tokens)) resolveOne(name);
152
+ return out;
153
+ }
154
+
155
+ // src/resolveTheme.ts
156
+ function themeChain(theme) {
157
+ const out = [];
158
+ for (let t = theme; t; t = t.extends) out.unshift(t);
159
+ return out;
160
+ }
161
+ function themeAxes(theme) {
162
+ return themeChain(theme).reduce((axes, t) => mergeAxes(axes, t.axes), {});
163
+ }
164
+ function resolveTheme(theme, selection = {}) {
165
+ const sel = fullSelection(themeAxes(theme), selection);
166
+ let merged = {};
167
+ for (const t of themeChain(theme)) merged = { ...merged, ...pickAll(t.tokens, sel) };
168
+ const out = {};
169
+ for (const [name, value] of Object.entries(resolveTokens(merged))) out[`--wzl-${name}`] = value;
170
+ return out;
171
+ }
172
+
173
+ export { enumerateSelections, fullSelection, isByAxis, mergeAxes, parseTokenValue, pick, pickAll, resolveTheme, resolveTokens, selectionKey, serializeTokenValue, themeAxes };
174
+ //# sourceMappingURL=chunk-LL2ERJPZ.js.map
175
+ //# sourceMappingURL=chunk-LL2ERJPZ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/axes.ts","../src/dtcg/value.ts","../src/dtcg/color.ts","../src/dtcg/resolve.ts","../src/resolveTheme.ts"],"names":[],"mappings":"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","file":"chunk-LL2ERJPZ.js","sourcesContent":["/** One value of an axis. `scheme` becomes `color-scheme` in that value's CSS blocks. */\nexport interface AxisValue {\n readonly scheme?: 'dark' | 'light';\n}\n\nexport interface AxisDef {\n readonly default: string;\n readonly values: Readonly<Record<string, AxisValue>>;\n}\n\n/** Key order is declaration order, which selectors and selection keys follow. */\nexport type AxisDefs = Readonly<Record<string, AxisDef>>;\n\n/** A partial choice of axis values, e.g. `{ mode: 'light' }`. */\nexport type Selection = Readonly<Record<string, string>>;\n\n/** `{ by: 'mode', dark: …, light: … }`: a value that differs per axis value. */\nexport interface ByAxis<T> {\n readonly by: string;\n readonly [axisValue: string]: Varying<T> | string;\n}\n\nexport type Varying<T> = T | ByAxis<T>;\n\nexport type Picked<T> = { ok: true; value: T } | { ok: false; axis: string; value: string };\n\nexport function isByAxis(v: unknown): v is ByAxis<unknown> {\n return typeof v === 'object' && v !== null && !Array.isArray(v) && typeof (v as { by?: unknown }).by === 'string';\n}\n\n/** Every axis given a known value; anything missing or unknown takes its default. */\nexport function fullSelection(axes: AxisDefs, selection: Selection = {}): Selection {\n const out: Record<string, string> = {};\n for (const [name, def] of Object.entries(axes)) {\n const v = selection[name];\n out[name] = v !== undefined && v in def.values ? v : def.default;\n }\n return out;\n}\n\n/** A child's axes over its parent's: an axis both declare keeps both sets of values, the child's entries and default winning. */\nexport function mergeAxes(parent: AxisDefs, child: AxisDefs): AxisDefs {\n const out: Record<string, AxisDef> = { ...parent };\n for (const [name, def] of Object.entries(child)) {\n out[name] = { ...def, values: { ...parent[name]?.values, ...def.values } };\n }\n return out;\n}\n\nexport function enumerateSelections(axes: AxisDefs): Selection[] {\n let out: Record<string, string>[] = [{}];\n for (const [name, def] of Object.entries(axes)) {\n out = out.flatMap((s) => Object.keys(def.values).map((v) => ({ ...s, [name]: v })));\n }\n return out;\n}\n\n/** `mode=dark,density=compact`, axes in declaration order. */\nexport function selectionKey(axes: AxisDefs, selection: Selection): string {\n const full = fullSelection(axes, selection);\n return Object.keys(axes).map((a) => `${a}=${full[a]}`).join(',');\n}\n\nexport function pick<T>(v: Varying<T>, selection: Selection): Picked<T> {\n let cur: unknown = v;\n while (isByAxis(cur)) {\n const axis = cur.by;\n const value = selection[axis];\n if (value === undefined || value === 'by' || !(value in cur)) return { ok: false, axis, value: value ?? '' };\n cur = cur[value];\n }\n return { ok: true, value: cur as T };\n}\n\n/** Every entry of a varying record that has a value for this selection, in record order. */\nexport function pickAll<T>(record: Readonly<Record<string, Varying<T>>>, selection: Selection): Record<string, T> {\n const out: Record<string, T> = {};\n for (const [name, v] of Object.entries(record)) {\n const picked = pick(v, selection);\n if (picked.ok) out[name] = picked.value;\n }\n return out;\n}\n","import type { TokenValue } from './types';\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","/**\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 `color-mix(in srgb, ${color.trim()} ${round(alpha * 100, 4)}%, transparent)`;\n const [r, g, b, a] = rgba;\n return `rgba(${r}, ${g}, ${b}, ${round(a * 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 m = /^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.exec(s);\n if (!m) return null;\n const body = m[1];\n const full = body.length <= 4 ? body.replace(/./g, (c) => c + c) : body;\n const byte = (i: number) => Number.parseInt(full.slice(i, i + 2), 16);\n return [byte(0), byte(2), byte(4), full.length === 8 ? byte(6) / 255 : 1];\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';\nimport type { FlatTokens, ResolvedTokenMap, TokenValue } from './types';\nimport { serializeTokenValue } from './value';\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, pickAll, type AxisDefs, type Selection } from './axes';\nimport { resolveTokens } from './dtcg/resolve';\nimport type { FlatTokens } from './dtcg/types';\nimport type { TokenName } from './generated/themes';\nimport type { Theme } from './theme';\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.\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 let merged: FlatTokens = {};\n for (const t of themeChain(theme)) merged = { ...merged, ...pickAll(t.tokens, sel) };\n const out: Record<string, string> = {};\n for (const [name, value] of Object.entries(resolveTokens(merged))) out[`--wzl-${name}`] = value;\n return out as ResolvedTheme;\n}\n"]}
@@ -0,0 +1,6 @@
1
+ // src/dtcg/types.ts
2
+ var ALPHA_EXT = "com.weasel.alpha";
3
+
4
+ export { ALPHA_EXT };
5
+ //# sourceMappingURL=chunk-PJMTWM34.js.map
6
+ //# sourceMappingURL=chunk-PJMTWM34.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/dtcg/types.ts"],"names":[],"mappings":";AACO,IAAM,SAAA,GAAY","file":"chunk-PJMTWM34.js","sourcesContent":["/** The one `$extensions` key this kit defines. */\nexport const ALPHA_EXT = 'com.weasel.alpha';\n\nexport type TokenValue = string | number | readonly (string | number)[];\n\n/**\n * A single design token as authored, before aliases are resolved: a DTCG-shaped\n * `{ type, value }` pair where `value` may still be a `{ref}` to another token.\n */\nexport interface RawToken {\n readonly type: string;\n readonly value: TokenValue;\n /** Render the referenced color at this alpha, 0–1. */\n readonly alpha?: number | undefined;\n readonly description?: string | undefined;\n}\n\n/** A token graph flattened to one level, keyed by token name (no `--wzl-`\n * prefix). Still unresolved — see `ResolvedTokenMap` for the output side. */\nexport type FlatTokens = Record<string, RawToken>;\n\n/** A resolved theme: every token name mapped to its final CSS-ready string.\n * Named ...Map, not ...Tokens, to stay distinct from hud's public `ResolvedTokens`. */\nexport type ResolvedTokenMap = Record<string, string>;\n"]}