@weasel-js/theme 1.4.4 → 1.5.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/dist/engine.js ADDED
@@ -0,0 +1,1761 @@
1
+ import { ALPHA_EXT } from './chunk-PJMTWM34.js';
2
+ import { mergeAxes, fullSelection, pick, enumerateSelections, resolveTokens, pickAll, themeAxes, isByAxis, selectionKey } from './chunk-LL2ERJPZ.js';
3
+ import { srgbU8ToOklab, oklabToOklch, oklchToOklab, oklabToSrgbU8 } from '@weasel-js/paint';
4
+
5
+ var clamp255 = (v) => Math.max(0, Math.min(255, Math.round(v)));
6
+ function hexToRgb(hex) {
7
+ const n = Number.parseInt(hex.slice(1), 16);
8
+ return [n >> 16 & 255, n >> 8 & 255, n & 255];
9
+ }
10
+ function toHex(L, C, H) {
11
+ const [l, a, b] = oklchToOklab(L, C, H * Math.PI / 180);
12
+ const [r, g, bl] = oklabToSrgbU8(l, a, b);
13
+ return `#${[r, g, bl].map((v) => clamp255(v).toString(16).padStart(2, "0")).join("")}`;
14
+ }
15
+ function toLch(hex) {
16
+ const [r, g, b] = hexToRgb(hex);
17
+ const [L, A, B] = srgbU8ToOklab(r, g, b);
18
+ const [l, c, h] = oklabToOklch(L, A, B);
19
+ return { L: l, C: c, H: (h * 180 / Math.PI % 360 + 360) % 360 };
20
+ }
21
+ function chromaCap(L, H) {
22
+ return toLch(toHex(L, 0.5, H)).C;
23
+ }
24
+ function luminance(hex) {
25
+ const [r, g, b] = hexToRgb(hex).map((v) => v / 255);
26
+ const f = (u) => u <= 0.04045 ? u / 12.92 : ((u + 0.055) / 1.055) ** 2.4;
27
+ return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b);
28
+ }
29
+ function contrast(a, b) {
30
+ const [hi, lo] = [luminance(a), luminance(b)].sort((p, q) => q - p);
31
+ return (hi + 0.05) / (lo + 0.05);
32
+ }
33
+ function hueGap(a, b) {
34
+ const d = Math.abs(a - b) % 360;
35
+ return Math.min(d, 360 - d);
36
+ }
37
+ function toLab(hex) {
38
+ const [r, g, b] = hexToRgb(hex);
39
+ return srgbU8ToOklab(r, g, b);
40
+ }
41
+ var CHROMA_WEIGHT = 3;
42
+ function deltaE(a, b) {
43
+ const [l1, a1, b1] = toLab(a);
44
+ const [l2, a2, b2] = toLab(b);
45
+ return Math.hypot(l1 - l2, CHROMA_WEIGHT * (a1 - a2), CHROMA_WEIGHT * (b1 - b2));
46
+ }
47
+ function vividAt(L, H) {
48
+ return toHex(L, chromaCap(L, H), H);
49
+ }
50
+
51
+ // src/engine/color/generate.ts
52
+ function peakLightness(H) {
53
+ let bestL = 0.5;
54
+ let bestC = -1;
55
+ for (let L = 0.32; L <= 0.96; L += 0.01) {
56
+ const c = chromaCap(L, H);
57
+ if (c > bestC) {
58
+ bestC = c;
59
+ bestL = L;
60
+ }
61
+ }
62
+ return bestL;
63
+ }
64
+ var PEAK = [];
65
+ for (let H = 0; H < 360; H += 1) PEAK.push(peakLightness(H));
66
+ var peakAt = (H) => PEAK[(Math.round(H) % 360 + 360) % 360];
67
+ function place(H, c) {
68
+ const L = peakAt(H) + (c.lightnessTarget - peakAt(H)) * c.lightnessPull;
69
+ return toHex(L, chromaCap(L, H) * c.chromaFraction, H);
70
+ }
71
+ var HUE_NAMES = [
72
+ [15, "rose"],
73
+ [45, "red"],
74
+ [62, "orange"],
75
+ [85, "amber"],
76
+ [100, "gold"],
77
+ [122, "yellow"],
78
+ [138, "lime"],
79
+ [165, "green"],
80
+ [195, "teal"],
81
+ [218, "cyan"],
82
+ [245, "sky"],
83
+ [275, "blue"],
84
+ [310, "violet"],
85
+ [345, "fuchsia"]
86
+ ];
87
+ function hueName(H) {
88
+ for (const [ceil, name] of HUE_NAMES) if (H < ceil) return name;
89
+ return "rose";
90
+ }
91
+ function uniqueNames(hues) {
92
+ const seen = /* @__PURE__ */ new Map();
93
+ return hues.map((h) => {
94
+ const base = hueName(h);
95
+ const n = (seen.get(base) ?? 0) + 1;
96
+ seen.set(base, n);
97
+ return n === 1 ? base : `${base}-${n}`;
98
+ });
99
+ }
100
+ function equalizeChroma(hexes, k, anchored) {
101
+ if (k <= 0) return hexes;
102
+ const lch = hexes.map(toLch);
103
+ const target = lch.reduce((sum, x) => sum + x.C, 0) / lch.length;
104
+ return lch.map((x, i) => {
105
+ const wanted = x.C + (target - x.C) * k;
106
+ if (anchored[i]) return toHex(x.L, Math.min(wanted, chromaCap(x.L, x.H)), x.H);
107
+ if (chromaCap(x.L, x.H) >= wanted) return toHex(x.L, wanted, x.H);
108
+ let bestL = x.L;
109
+ let bestDistance = Infinity;
110
+ for (let L = 0.3; L <= 0.96; L += 5e-3) {
111
+ if (chromaCap(L, x.H) >= wanted) {
112
+ const d = Math.abs(L - x.L);
113
+ if (d < bestDistance) {
114
+ bestDistance = d;
115
+ bestL = L;
116
+ }
117
+ }
118
+ }
119
+ if (bestDistance === Infinity) {
120
+ const pk = peakAt(x.H);
121
+ return toHex(pk, chromaCap(pk, x.H), x.H);
122
+ }
123
+ return toHex(bestL, wanted, x.H);
124
+ });
125
+ }
126
+ function farthestFirst(hues) {
127
+ const order = [0];
128
+ while (order.length < hues.length) {
129
+ let best = -1;
130
+ let bestGap = -1;
131
+ for (let i = 0; i < hues.length; i += 1) {
132
+ if (order.includes(i)) continue;
133
+ const gap = Math.min(...order.map((j) => hueGap(hues[i], hues[j])));
134
+ if (gap > bestGap) {
135
+ bestGap = gap;
136
+ best = i;
137
+ }
138
+ }
139
+ order.push(best);
140
+ }
141
+ return order;
142
+ }
143
+ function floorDegrees(c) {
144
+ return c.hueFloor <= 0 ? 0 : c.hueFloor * 360 / Math.max(1, c.count);
145
+ }
146
+ function searchHues(c) {
147
+ const minGapDeg = floorDegrees(c);
148
+ const anchorHues = c.anchors.map((a) => a.hue);
149
+ const anchorHexes = c.anchors.map(toHexPreview);
150
+ const freeCount = c.count - c.anchors.length;
151
+ if (freeCount < 0) return null;
152
+ if (freeCount === 0) return [];
153
+ const weighted = (hex) => {
154
+ const [L, a, b] = toLab(hex);
155
+ return [L, CHROMA_WEIGHT * a, CHROMA_WEIGHT * b];
156
+ };
157
+ const between = (p, q) => Math.hypot(p[0] - q[0], p[1] - q[1], p[2] - q[2]);
158
+ const placed = /* @__PURE__ */ new Map();
159
+ const at = (H) => {
160
+ const key = (Math.round(H) % 360 + 360) % 360;
161
+ let v = placed.get(key);
162
+ if (!v) {
163
+ const hex = place(key, c);
164
+ const lch = toLch(hex);
165
+ v = {
166
+ hex,
167
+ hue: lch.H,
168
+ contrast: contrast(hex, c.surface),
169
+ chroma: lch.C,
170
+ surfaceDistance: deltaE(hex, c.surface),
171
+ lab: weighted(hex)
172
+ };
173
+ placed.set(key, v);
174
+ }
175
+ return v;
176
+ };
177
+ const anchorMeta = anchorHexes.map((hex) => {
178
+ const lch = toLch(hex);
179
+ return {
180
+ hex,
181
+ hue: lch.H,
182
+ contrast: contrast(hex, c.surface),
183
+ chroma: lch.C,
184
+ surfaceDistance: deltaE(hex, c.surface),
185
+ lab: weighted(hex)
186
+ };
187
+ });
188
+ const metaFor = (free) => [...anchorMeta, ...free.map(at)];
189
+ const penalty = (free) => {
190
+ const m = metaFor(free);
191
+ let p = 0;
192
+ if (minGapDeg > 0) {
193
+ for (let i = 0; i < m.length; i += 1) {
194
+ for (let j = i + 1; j < m.length; j += 1) {
195
+ const short = minGapDeg - hueGap(m[i].hue, m[j].hue);
196
+ if (short > 0) p += short;
197
+ }
198
+ }
199
+ }
200
+ if (c.minContrast > 0) {
201
+ for (const x of m) {
202
+ const short = c.minContrast - x.contrast;
203
+ if (short > 0) p += short * 40;
204
+ }
205
+ }
206
+ if (c.minSurfaceDistance > 0) {
207
+ for (const x of m) {
208
+ const short = c.minSurfaceDistance - x.surfaceDistance;
209
+ if (short > 0) p += short * 400;
210
+ }
211
+ }
212
+ if (c.minDistance > 0) {
213
+ for (let i = 0; i < m.length; i += 1) {
214
+ for (let j = i + 1; j < m.length; j += 1) {
215
+ const short = c.minDistance - between(m[i].lab, m[j].lab);
216
+ if (short > 0) p += short * 400;
217
+ }
218
+ }
219
+ }
220
+ return p;
221
+ };
222
+ const score = (free) => {
223
+ const m = metaFor(free);
224
+ return m.reduce((sum, x) => sum + x.chroma, 0) / m.length;
225
+ };
226
+ const seeds = [];
227
+ const seedStep = Math.max(6, Math.round(freeCount * 1.6));
228
+ for (let start = 0; start < 360; start += seedStep) {
229
+ seeds.push(Array.from({ length: freeCount }, (_, i) => Math.round((start + i * 360 / freeCount) % 360)));
230
+ }
231
+ if (anchorHues.length > 0) {
232
+ const base = anchorHues[0];
233
+ const span = 360 - 2 * Math.max(minGapDeg, 1);
234
+ for (let jitter = -8; jitter <= 8; jitter += 2) {
235
+ for (const divisor of [Math.max(1, freeCount - 1), freeCount]) {
236
+ seeds.push(
237
+ Array.from(
238
+ { length: freeCount },
239
+ (_, i) => Math.round(((base + minGapDeg + jitter + i * span / divisor) % 360 + 360) % 360)
240
+ )
241
+ );
242
+ }
243
+ }
244
+ }
245
+ const STEPS = [-16, -8, -4, -2, -1, 1, 2, 4, 8, 16];
246
+ const penaltyAt = (m, i) => {
247
+ let p = 0;
248
+ const x = m[i];
249
+ if (c.minContrast > 0) {
250
+ const short = c.minContrast - x.contrast;
251
+ if (short > 0) p += short * 40;
252
+ }
253
+ if (c.minSurfaceDistance > 0) {
254
+ const short = c.minSurfaceDistance - x.surfaceDistance;
255
+ if (short > 0) p += short * 400;
256
+ }
257
+ for (let j = 0; j < m.length; j += 1) {
258
+ if (j === i) continue;
259
+ if (minGapDeg > 0) {
260
+ const short = minGapDeg - hueGap(x.hue, m[j].hue);
261
+ if (short > 0) p += short;
262
+ }
263
+ if (c.minDistance > 0) {
264
+ const short = c.minDistance - between(x.lab, m[j].lab);
265
+ if (short > 0) p += short * 400;
266
+ }
267
+ }
268
+ return p;
269
+ };
270
+ const descend = (hues, useChroma, keepLegal) => {
271
+ const current = [...hues];
272
+ let meta = metaFor(current);
273
+ const offset = anchorMeta.length;
274
+ for (let iteration = 0; iteration < 120; iteration += 1) {
275
+ let improved = false;
276
+ for (let i = 0; i < freeCount; i += 1) {
277
+ const slot = offset + i;
278
+ const beforeP = penaltyAt(meta, slot);
279
+ const beforeC = meta[slot].chroma;
280
+ for (const delta of STEPS) {
281
+ const next = ((current[i] + delta) % 360 + 360) % 360;
282
+ if (next === current[i]) continue;
283
+ const candidate = at(next);
284
+ const trialMeta = meta.slice();
285
+ trialMeta[slot] = candidate;
286
+ const afterP = penaltyAt(trialMeta, slot);
287
+ const better = useChroma ? (keepLegal ? afterP <= beforeP : true) && candidate.chroma > beforeC : afterP < beforeP;
288
+ if (!better) continue;
289
+ if (keepLegal && afterP > beforeP) continue;
290
+ current[i] = next;
291
+ meta = trialMeta;
292
+ improved = true;
293
+ break;
294
+ }
295
+ }
296
+ if (!improved) break;
297
+ }
298
+ return current;
299
+ };
300
+ let best = null;
301
+ let bestScore = -1;
302
+ for (const seed of seeds) {
303
+ const repaired = penalty(seed) > 0 ? descend(seed, false, false) : seed;
304
+ if (penalty(repaired) > 0) continue;
305
+ const tuned = descend(repaired, true, true);
306
+ if (penalty(tuned) > 0) continue;
307
+ const s = score(tuned);
308
+ if (s > bestScore) {
309
+ bestScore = s;
310
+ best = tuned;
311
+ }
312
+ }
313
+ return best;
314
+ }
315
+ function pairwiseMin(hexes) {
316
+ if (hexes.length < 2) return 1;
317
+ let min = Infinity;
318
+ for (let i = 0; i < hexes.length; i += 1) {
319
+ for (let j = i + 1; j < hexes.length; j += 1) min = Math.min(min, deltaE(hexes[i], hexes[j]));
320
+ }
321
+ return min;
322
+ }
323
+ function summarize(swatches, surface) {
324
+ const cs = swatches.map((s) => s.lch.C);
325
+ const ls = swatches.map((s) => s.lch.L);
326
+ let gap = 360;
327
+ for (let i = 0; i < swatches.length; i += 1) {
328
+ for (let j = i + 1; j < swatches.length; j += 1) {
329
+ gap = Math.min(gap, hueGap(swatches[i].lch.H, swatches[j].lch.H));
330
+ }
331
+ }
332
+ return {
333
+ meanChroma: cs.reduce((a, b) => a + b, 0) / (cs.length || 1),
334
+ chromaSpread: Math.max(...cs) - Math.min(...cs),
335
+ lightnessSpread: Math.max(...ls) - Math.min(...ls),
336
+ minHueGap: swatches.length > 1 ? gap : 360,
337
+ hueFloorShare: swatches.length > 1 ? gap * swatches.length / 360 : 1,
338
+ minContrast: Math.min(...swatches.map((s) => contrast(s.hex, surface))),
339
+ minSurfaceDistance: Math.min(...swatches.map((s) => deltaE(s.hex, surface))),
340
+ minDistance: pairwiseMin(swatches.map((s) => s.hex))
341
+ };
342
+ }
343
+ function generate(c) {
344
+ const found = searchHues(c);
345
+ const feasible = found !== null;
346
+ const free = found ?? Array.from(
347
+ { length: Math.max(0, c.count - c.anchors.length) },
348
+ (_, i) => Math.round(i * 360 / Math.max(1, c.count - c.anchors.length))
349
+ );
350
+ const hues = [...c.anchors.map((a) => a.hue), ...free];
351
+ const anchoredFlags = [...c.anchors.map(() => true), ...free.map(() => false)];
352
+ const raw = [
353
+ ...c.anchors.map(toHexPreview),
354
+ ...free.map((h) => place(h, c))
355
+ ];
356
+ const hexes = equalizeChroma(raw, c.equalize, anchoredFlags);
357
+ const order = c.order === "farthest" ? farthestFirst(hues) : [...hues.keys()].sort((a, b) => hues[a] - hues[b]);
358
+ const names = uniqueNames(order.map((i) => toLch(hexes[i]).H));
359
+ const swatches = order.map((i, position) => ({
360
+ name: c.anchors[i]?.name ?? names[position],
361
+ hex: hexes[i],
362
+ lch: toLch(hexes[i]),
363
+ anchored: anchoredFlags[i],
364
+ contrast: contrast(hexes[i], c.surface)
365
+ }));
366
+ return { swatches, stats: summarize(swatches, c.surface), feasible };
367
+ }
368
+ var DEFAULT_CONSTRAINTS = {
369
+ count: 10,
370
+ hueFloor: 0.9,
371
+ minContrast: 4,
372
+ surface: "#181a1e",
373
+ lightnessTarget: 0.72,
374
+ lightnessPull: 0.4,
375
+ chromaFraction: 0.95,
376
+ equalize: 0,
377
+ minSurfaceDistance: 0.25,
378
+ minDistance: 0.22,
379
+ anchors: [],
380
+ order: "farthest"
381
+ };
382
+ function toHexPreview(a) {
383
+ const cap = chromaCap(a.lightness, a.hue);
384
+ return toHex(a.lightness, a.chroma === void 0 ? cap : Math.min(a.chroma, cap), a.hue);
385
+ }
386
+ function anchorFromHex(hex, name) {
387
+ const { L, C, H } = toLch(hex);
388
+ return {
389
+ name: name ?? hueName(H),
390
+ hue: Math.round(H),
391
+ lightness: Number(L.toFixed(3)),
392
+ chroma: Number(C.toFixed(4))
393
+ };
394
+ }
395
+
396
+ // src/engine/steps.ts
397
+ var stepList = (x) => Array.isArray(x) ? [...new Set(x.filter((s) => typeof s === "string"))] : [];
398
+ function inEveryBranch(v, read) {
399
+ if (!isByAxis(v)) return read(v);
400
+ const lists = Object.entries(v).filter(([k]) => k !== "by").map(([, x]) => inEveryBranch(x, read));
401
+ return lists.length === 0 ? [] : lists.reduce((kept, list) => kept.filter((s) => list.includes(s)));
402
+ }
403
+ function alwaysDeclaredSteps(entry) {
404
+ return inEveryBranch(entry, (e) => typeof e === "object" && e !== null ? inEveryBranch(e.steps, stepList) : []);
405
+ }
406
+ function declaredSteps(entry) {
407
+ const out = /* @__PURE__ */ new Set();
408
+ const branches = (v, visit) => {
409
+ if (!isByAxis(v)) {
410
+ visit(v);
411
+ return;
412
+ }
413
+ for (const [k, x] of Object.entries(v)) if (k !== "by") branches(x, visit);
414
+ };
415
+ branches(entry, (e) => {
416
+ if (typeof e !== "object" || e === null) return;
417
+ branches(e.steps, (steps) => {
418
+ if (Array.isArray(steps)) {
419
+ for (const s of steps) if (typeof s === "string") out.add(s);
420
+ }
421
+ });
422
+ });
423
+ return [...out];
424
+ }
425
+
426
+ // src/engine/merge.ts
427
+ var LAYERS = ["seeds", "ramps", "scales", "semantics", "components", "pins"];
428
+ function ownSteps(def) {
429
+ const names = /* @__PURE__ */ new Set();
430
+ for (const layer of [def.ramps, def.scales]) {
431
+ for (const [name, entry] of Object.entries(layer ?? {})) for (const step of alwaysDeclaredSteps(entry)) names.add(`${name}-${step}`);
432
+ }
433
+ return names;
434
+ }
435
+ function mergeChain(def, lookup, seen = /* @__PURE__ */ new Set()) {
436
+ if (seen.has(def.name)) throw new Error(`extends cycle at theme "${def.name}"`);
437
+ if (!def.extends) return def;
438
+ const parentDef = lookup?.(def.extends);
439
+ if (!parentDef) throw new Error(`Theme "${def.name}" extends "${def.extends}", which is not defined`);
440
+ const parent = mergeChain(parentDef, lookup, /* @__PURE__ */ new Set([...seen, def.name]));
441
+ const out = { ...def };
442
+ for (const layer of LAYERS) out[layer] = { ...parent[layer] ?? {}, ...def[layer] ?? {} };
443
+ const shadowed = ownSteps(def);
444
+ const inherited = Object.entries(parent.pins ?? {}).filter(([name]) => !shadowed.has(name));
445
+ out.pins = { ...Object.fromEntries(inherited), ...def.pins ?? {} };
446
+ out.axes = mergeAxes(parent.axes ?? {}, def.axes ?? {});
447
+ return out;
448
+ }
449
+
450
+ // src/engine/deps.ts
451
+ var SEED_REF = /^\{seeds\.([\w-]+)\}$/;
452
+ var TOKEN_REF = /^\{([^}.]+)\}$/;
453
+ var node = () => ({ axes: /* @__PURE__ */ new Set(), seeds: /* @__PURE__ */ new Set(), edges: /* @__PURE__ */ new Set() });
454
+ function scan(v, into) {
455
+ if (typeof v === "string") {
456
+ const s = v.trim();
457
+ const seed = SEED_REF.exec(s);
458
+ if (seed) into.seeds.add(seed[1]);
459
+ else {
460
+ const ref = TOKEN_REF.exec(s);
461
+ if (ref) into.edges.add(ref[1]);
462
+ }
463
+ return;
464
+ }
465
+ if (Array.isArray(v)) {
466
+ for (const x of v) scan(x, into);
467
+ return;
468
+ }
469
+ if (typeof v === "object" && v !== null) {
470
+ if (isByAxis(v)) into.axes.add(v.by);
471
+ for (const [k, x] of Object.entries(v)) if (k !== "by") scan(x, into);
472
+ }
473
+ }
474
+ function scanSteps(entry, into) {
475
+ if (isByAxis(entry)) {
476
+ into.axes.add(entry.by);
477
+ for (const [k, x] of Object.entries(entry)) if (k !== "by") scanSteps(x, into);
478
+ } else if (typeof entry === "object" && entry !== null) {
479
+ scan(entry.steps, into);
480
+ }
481
+ }
482
+ var stepsKey = (ramp) => `ramps.${ramp}`;
483
+ function leaves(v) {
484
+ return isByAxis(v) ? Object.entries(v).filter(([k]) => k !== "by").flatMap(([, x]) => leaves(x)) : [v];
485
+ }
486
+ function axisDependencies(definition, lookup) {
487
+ const def = mergeChain(definition, lookup);
488
+ const nodes = /* @__PURE__ */ new Map();
489
+ const stepRamps = /* @__PURE__ */ new Map();
490
+ for (const [name, ramp] of Object.entries(def.ramps ?? {})) {
491
+ const shape = node();
492
+ scanSteps(ramp, shape);
493
+ nodes.set(stepsKey(name), shape);
494
+ const n = node();
495
+ scan(ramp, n);
496
+ for (const step of declaredSteps(ramp)) {
497
+ nodes.set(`${name}-${step}`, n);
498
+ stepRamps.set(`${name}-${step}`, (stepRamps.get(`${name}-${step}`) ?? /* @__PURE__ */ new Set()).add(name));
499
+ }
500
+ }
501
+ for (const [name, s] of Object.entries(def.scales ?? {})) {
502
+ const n = node();
503
+ scan(s, n);
504
+ for (const step of declaredSteps(s)) nodes.set(`${name}-${step}`, n);
505
+ }
506
+ const rampsOf = (name, seen = /* @__PURE__ */ new Set()) => {
507
+ const out = new Set(stepRamps.get(name));
508
+ if (seen.has(name)) return out;
509
+ seen.add(name);
510
+ const add = (from) => {
511
+ for (const r of rampsOf(from, seen)) out.add(r);
512
+ };
513
+ const rule = def.semantics?.[name];
514
+ for (const leaf of rule === void 0 ? [] : leaves(rule)) {
515
+ if (typeof leaf !== "object" || leaf === null) continue;
516
+ const l = leaf;
517
+ if ("ramp" in l) out.add(l.ramp);
518
+ else if ("from" in l) add(l.from);
519
+ else if ("ref" in l) add(l.ref);
520
+ }
521
+ const pin = def.pins?.[name];
522
+ if (pin !== void 0) {
523
+ const n = node();
524
+ scan(pin, n);
525
+ for (const e of n.edges) add(e);
526
+ }
527
+ return out;
528
+ };
529
+ for (const [name, rule] of Object.entries(def.semantics ?? {})) {
530
+ const n = node();
531
+ scan(rule, n);
532
+ for (const leaf of leaves(rule)) {
533
+ if ("ref" in leaf) n.edges.add(leaf.ref);
534
+ if ("from" in leaf) {
535
+ n.edges.add(leaf.from);
536
+ for (const r of rampsOf(leaf.from)) {
537
+ n.edges.add(stepsKey(r));
538
+ if (leaf.dir !== "away") for (const s of declaredSteps(def.ramps?.[r])) n.edges.add(`${r}-${s}`);
539
+ }
540
+ }
541
+ if ("step" in leaf) {
542
+ n.edges.add(stepsKey(leaf.ramp));
543
+ for (const s of leaves(leaf.step)) n.edges.add(`${leaf.ramp}-${String(s)}`);
544
+ }
545
+ if ("contrast" in leaf) {
546
+ n.edges.add(stepsKey(leaf.ramp));
547
+ for (const a of leaf.contrast.against) n.edges.add(a);
548
+ for (const s of declaredSteps(def.ramps?.[leaf.ramp])) n.edges.add(`${leaf.ramp}-${s}`);
549
+ }
550
+ }
551
+ nodes.set(name, n);
552
+ }
553
+ for (const layer of [def.components, def.pins]) {
554
+ for (const [name, v] of Object.entries(layer ?? {})) {
555
+ const n = node();
556
+ scan(v, n);
557
+ nodes.set(name, n);
558
+ }
559
+ }
560
+ const order = Object.keys(def.axes ?? {});
561
+ const sorted = (s) => order.filter((a) => s.has(a));
562
+ const ownOf = (n) => {
563
+ const out = new Set(n.axes);
564
+ for (const seed of n.seeds) {
565
+ const s = node();
566
+ scan(def.seeds?.[seed], s);
567
+ for (const a of s.axes) out.add(a);
568
+ }
569
+ return out;
570
+ };
571
+ const ownSets = /* @__PURE__ */ new Map();
572
+ for (const [name, n] of nodes) ownSets.set(name, ownOf(n));
573
+ const all = /* @__PURE__ */ new Map();
574
+ for (const [name, own] of ownSets) all.set(name, new Set(own));
575
+ let changed = true;
576
+ while (changed) {
577
+ changed = false;
578
+ for (const [name, n] of nodes) {
579
+ const out = all.get(name);
580
+ for (const e of n.edges) {
581
+ const from = all.get(e);
582
+ if (!from) continue;
583
+ for (const a of from) if (!out.has(a)) {
584
+ out.add(a);
585
+ changed = true;
586
+ }
587
+ }
588
+ }
589
+ }
590
+ const result = {};
591
+ for (const name of nodes.keys()) if (!name.includes(".")) result[name] = { own: sorted(ownSets.get(name)), all: sorted(all.get(name)) };
592
+ return result;
593
+ }
594
+
595
+ // src/engine/ramps.ts
596
+ var MAX_CHROMA = 0.4;
597
+ var smoothstep = (t) => t * t * (3 - 2 * t);
598
+ var envelope = (t, lightBias, darkBias) => Math.sin(Math.PI * t) + lightBias * (1 - t) + darkBias * t;
599
+ function envelopeMax(lightBias, darkBias) {
600
+ let max = 0;
601
+ for (let i = 0; i <= 1e3; i += 1) max = Math.max(max, envelope(i / 1e3, lightBias, darkBias));
602
+ return max;
603
+ }
604
+ function lightnessRamp(p) {
605
+ const n = p.steps.length;
606
+ const at = (i) => n === 1 ? 0 : i / (n - 1);
607
+ const lightBias = p.lightBias ?? 0;
608
+ const max = envelopeMax(lightBias, p.darkBias);
609
+ let { hue, peak } = p;
610
+ const anchorIndex = p.steps.findIndex((s) => p.anchor?.[s] !== void 0);
611
+ if (anchorIndex !== -1) {
612
+ const a = toLch(p.anchor[p.steps[anchorIndex]]);
613
+ const e = envelope(at(anchorIndex), lightBias, p.darkBias);
614
+ hue = a.H;
615
+ peak = e > 1e-6 ? a.C * max / e : a.C;
616
+ }
617
+ const out = {};
618
+ p.steps.forEach((step, i) => {
619
+ const fixed = p.anchor?.[step];
620
+ if (fixed !== void 0) {
621
+ out[step] = fixed.toLowerCase();
622
+ return;
623
+ }
624
+ const t = at(i);
625
+ const L = p.lightness[0] + (p.lightness[1] - p.lightness[0]) * (t + (smoothstep(t) - t) * p.curve);
626
+ out[step] = toHex(L, Math.min(peak * envelope(t, lightBias, p.darkBias) / max, MAX_CHROMA), hue);
627
+ });
628
+ return out;
629
+ }
630
+ function categoricalRamp(steps, gates, anchors) {
631
+ const palette = generate({ ...DEFAULT_CONSTRAINTS, ...gates, count: steps.length, anchors });
632
+ const colors = {};
633
+ steps.forEach((step, i) => {
634
+ colors[step] = palette.swatches[i].hex.toLowerCase();
635
+ });
636
+ return { colors, feasible: palette.feasible };
637
+ }
638
+
639
+ // src/engine/scales.ts
640
+ function scale(steps, p) {
641
+ if (p.step === void 0 === (p.ratio === void 0)) {
642
+ throw new Error("A scale takes exactly one of `step` and `ratio`");
643
+ }
644
+ const out = {};
645
+ steps.forEach((name, i) => {
646
+ const v = p.step !== void 0 ? p.base + p.step * i : p.base * p.ratio ** i;
647
+ out[name] = `${Math.round(v)}px`;
648
+ });
649
+ return out;
650
+ }
651
+
652
+ // src/engine/semantics.ts
653
+ var TOKEN_REF2 = /^\{([^}.]+)\}$/;
654
+ var HEX = /^#[0-9a-f]{6}$/i;
655
+ var MAX_DEPTH = 32;
656
+ var isRecord = (x) => typeof x === "object" && x !== null && !Array.isArray(x);
657
+ var refTarget = (value) => typeof value === "string" ? TOKEN_REF2.exec(value.trim())?.[1] : void 0;
658
+ function deriveSemantics(rules, ctx) {
659
+ const done = /* @__PURE__ */ new Map();
660
+ const failed = /* @__PURE__ */ new Set();
661
+ const inProgress = /* @__PURE__ */ new Set();
662
+ const get = (name) => {
663
+ if (done.has(name)) return done.get(name);
664
+ if (failed.has(name) || !Object.hasOwn(rules, name)) return void 0;
665
+ if (inProgress.has(name)) throw new Error(`Semantic cycle at "${name}" (${[...inProgress].join(" \u2192 ")})`);
666
+ inProgress.add(name);
667
+ const p = pick(rules[name], ctx.sel);
668
+ let derived;
669
+ if (!p.ok) {
670
+ ctx.issues.push({ kind: "missing-axis-value", path: `semantics.${name}`, axis: p.axis, value: p.value });
671
+ } else if (!isRecord(p.value)) {
672
+ ctx.issues.push({ kind: "invalid", path: `semantics.${name}`, message: "expected an object" });
673
+ } else {
674
+ derived = deriveOne(name, p.value, ctx, env);
675
+ }
676
+ inProgress.delete(name);
677
+ if (derived) done.set(name, derived);
678
+ else failed.add(name);
679
+ return derived;
680
+ };
681
+ const color = (value, depth = 0) => {
682
+ if (typeof value !== "string" || depth > MAX_DEPTH) return void 0;
683
+ const v = value.trim();
684
+ const m = TOKEN_REF2.exec(v);
685
+ if (!m) return HEX.test(v) ? v.toLowerCase() : void 0;
686
+ return solid(ctx.pinned(m[1]) ?? ctx.tokens[m[1]] ?? get(m[1])?.token ?? ctx.component(m[1]), depth + 1);
687
+ };
688
+ const solid = (t, depth = 0) => t && t.alpha === void 0 ? color(t.value, depth) : void 0;
689
+ const rampStep = (name) => {
690
+ if (!Object.hasOwn(ctx.tokens, name)) return void 0;
691
+ for (const [ramp, steps] of Object.entries(ctx.ramps)) {
692
+ const index = name.startsWith(`${ramp}-`) ? steps.indexOf(name.slice(ramp.length + 1)) : -1;
693
+ if (index >= 0) return { ramp, index };
694
+ }
695
+ return void 0;
696
+ };
697
+ const positionAt = (name, depth = 0) => {
698
+ if (depth > MAX_DEPTH) return void 0;
699
+ const step = rampStep(name);
700
+ if (step) return step;
701
+ const pin = ctx.pinned(name);
702
+ if (pin) {
703
+ const target = refTarget(pin.value);
704
+ return target === void 0 ? void 0 : positionAt(target, depth + 1);
705
+ }
706
+ return Object.hasOwn(rules, name) ? get(name)?.position : void 0;
707
+ };
708
+ const positionOf = (name) => Object.hasOwn(rules, name) ? positionAt(name) : void 0;
709
+ const env = { get, color, positionOf, positionAt };
710
+ for (const name of Object.keys(rules)) get(name);
711
+ return new Map(Object.keys(rules).filter((n) => done.has(n)).map((n) => [n, done.get(n)]));
712
+ }
713
+ var lightnessOf = (hex) => hex ? toLch(hex).L : Number.NaN;
714
+ function deriveOne(name, r, ctx, env) {
715
+ const description = r.description;
716
+ const invalid = (message) => {
717
+ ctx.issues.push({ kind: "invalid", path: `semantics.${name}`, message });
718
+ return void 0;
719
+ };
720
+ if ("value" in r) {
721
+ return { rule: "value", token: { type: r.type ?? "unknown", value: r.value, alpha: void 0, description } };
722
+ }
723
+ if ("ref" in r) {
724
+ const type = r.type ?? ctx.tokens[r.ref]?.type ?? env.get(r.ref)?.token.type ?? "unknown";
725
+ return { rule: "ref", position: env.positionAt(r.ref), token: { type, value: `{${r.ref}}`, alpha: r.alpha, description } };
726
+ }
727
+ if ("step" in r) {
728
+ const picked = pick(r.step, ctx.sel);
729
+ if (!picked.ok) {
730
+ ctx.issues.push({ kind: "missing-axis-value", path: `semantics.${name}.step`, axis: picked.axis, value: picked.value });
731
+ return void 0;
732
+ }
733
+ return atStep(name, r.ramp, (ctx.ramps[r.ramp] ?? []).indexOf(picked.value), "step", description, ctx);
734
+ }
735
+ const rampColor = (ramp, step) => env.color(`{${ramp}-${step}}`);
736
+ const darkening = (ramp, steps) => {
737
+ const first = lightnessOf(rampColor(ramp, steps[0]));
738
+ const last = lightnessOf(rampColor(ramp, steps[steps.length - 1]));
739
+ if (Number.isNaN(first) || Number.isNaN(last) || first === last) return invalid(`cannot tell which end of ramp "${ramp}" is darker`);
740
+ return last < first ? 1 : -1;
741
+ };
742
+ if ("from" in r) {
743
+ const from = env.positionOf(r.from);
744
+ if (!from) return invalid(`"${r.from}" does not end on a ramp step`);
745
+ const { ramp, index } = from;
746
+ const steps = ctx.ramps[ramp];
747
+ const last = steps.length - 1;
748
+ let sign;
749
+ if (r.dir === "darker" || r.dir === "lighter") {
750
+ const darker = darkening(ramp, steps);
751
+ if (darker === void 0) return void 0;
752
+ sign = r.dir === "darker" ? darker : -darker;
753
+ } else {
754
+ sign = index <= last / 2 ? 1 : -1;
755
+ }
756
+ const target = index + sign * r.offset;
757
+ if (target < 0 || target > last) invalid(`offset ${r.offset} runs off ramp "${ramp}"`);
758
+ return atStep(name, ramp, Math.max(0, Math.min(last, target)), "offset", description, ctx);
759
+ }
760
+ if ("contrast" in r) {
761
+ for (const a of r.contrast.against) requireKnown(name, a, ctx.known);
762
+ const steps = ctx.ramps[r.ramp];
763
+ if (!steps) return invalid(`no ramp "${r.ramp}"`);
764
+ if (r.contrast.against.length === 0) return invalid("contrast needs at least one surface");
765
+ const colors = steps.map((s) => rampColor(r.ramp, s));
766
+ const against = r.contrast.against.map((a) => env.color(`{${a}}`));
767
+ if (colors.some((c) => !c) || against.some((hex) => !hex)) return invalid("contrast needs solid colors on both sides");
768
+ const darker = darkening(r.ramp, steps);
769
+ if (darker === void 0) return void 0;
770
+ const L = colors.map(lightnessOf);
771
+ const surfaces = against.map(lightnessOf);
772
+ const meanL = surfaces.reduce((sum, l) => sum + l, 0) / surfaces.length;
773
+ const last = steps.length - 1;
774
+ const sign = Math.abs(L[last] - meanL) >= Math.abs(L[0] - meanL) ? 1 : -1;
775
+ const lo = Math.min(...surfaces);
776
+ const hi = Math.max(...surfaces);
777
+ const worst = (i) => Math.min(...against.map((hex) => contrast(colors[i], hex)));
778
+ const walk = (toLast) => {
779
+ const order = steps.map((_, i) => toLast ? i : last - i);
780
+ const past = toLast === darker > 0 ? (l) => l < lo : (l) => l > hi;
781
+ const start = order.findIndex((i) => past(L[i]));
782
+ return start < 0 ? [] : order.slice(start);
783
+ };
784
+ const walked = [...walk(sign > 0), ...walk(sign < 0)];
785
+ const between = steps.map((_, i) => i).filter((i) => !walked.includes(i)).sort((a, b) => worst(b) - worst(a));
786
+ for (const i of [...walked, ...between]) {
787
+ if (worst(i) >= r.contrast.min) return atStep(name, r.ramp, i, "contrast", description, ctx);
788
+ }
789
+ let best = { index: sign > 0 ? 0 : last, ratio: -1 };
790
+ for (const i of steps.map((_, k) => sign > 0 ? k : last - k)) {
791
+ const ratio = worst(i);
792
+ if (ratio > best.ratio) best = { index: i, ratio };
793
+ }
794
+ ctx.issues.push({
795
+ kind: "contrast-unmet",
796
+ token: name,
797
+ min: r.contrast.min,
798
+ against: [...r.contrast.against],
799
+ picked: steps[best.index],
800
+ ratio: best.ratio
801
+ });
802
+ return atStep(name, r.ramp, best.index, "contrast", description, ctx);
803
+ }
804
+ return invalid("unrecognized rule");
805
+ }
806
+ function requireKnown(owner, name, known) {
807
+ if (!known(name)) throw new Error(`Semantic "${owner}" references "${name}", which is not defined`);
808
+ }
809
+ function checkSemantics(rules, sel, tokens, known, issues) {
810
+ const solid = (name) => {
811
+ for (let n = name; ; ) {
812
+ if (!Object.hasOwn(tokens, n)) return null;
813
+ const t = tokens[n];
814
+ if (t.alpha !== void 0 || typeof t.value !== "string") return void 0;
815
+ const v = t.value.trim();
816
+ const m = TOKEN_REF2.exec(v);
817
+ if (!m) return HEX.test(v) ? v.toLowerCase() : void 0;
818
+ n = m[1];
819
+ }
820
+ };
821
+ for (const name of Object.keys(rules)) {
822
+ const p = pick(rules[name], sel);
823
+ const check = p.ok && isRecord(p.value) ? p.value.check : void 0;
824
+ if (!check) continue;
825
+ for (const a of check.against) requireKnown(name, a, known);
826
+ if (!Object.hasOwn(tokens, name)) continue;
827
+ const path = `semantics.${name}.check`;
828
+ if (check.against.length === 0) {
829
+ issues.push({ kind: "invalid", path, message: "check needs at least one surface" });
830
+ continue;
831
+ }
832
+ const hex = solid(name);
833
+ if (hex === null) continue;
834
+ if (!hex) {
835
+ issues.push({ kind: "invalid", path, message: `"${name}" is not a solid color` });
836
+ continue;
837
+ }
838
+ for (const a of check.against) {
839
+ const other = solid(a);
840
+ if (other === null) continue;
841
+ if (!other) {
842
+ issues.push({ kind: "invalid", path, message: `"${a}" is not a solid color` });
843
+ continue;
844
+ }
845
+ const ratio = contrast(hex, other);
846
+ if (ratio < check.contrast) issues.push({ kind: "check-failed", token: name, against: a, min: check.contrast, ratio });
847
+ }
848
+ }
849
+ }
850
+ function atStep(name, ramp, index, rule, description, ctx) {
851
+ const steps = ctx.ramps[ramp];
852
+ if (!steps || index < 0 || index >= steps.length) {
853
+ ctx.issues.push({ kind: "invalid", path: `semantics.${name}`, message: `no such step on ramp "${ramp}"` });
854
+ return void 0;
855
+ }
856
+ return {
857
+ rule,
858
+ position: { ramp, index },
859
+ token: { type: "color", value: `{${ramp}-${steps[index]}}`, alpha: void 0, description }
860
+ };
861
+ }
862
+
863
+ // src/engine/derive.ts
864
+ var SEED_REF2 = /^\{seeds\.([\w-]+)\}$/;
865
+ var REF = /^\{([^}]+)\}$/;
866
+ var HEX2 = /^#[0-9a-f]{6}$/i;
867
+ var UNTYPED = "unknown";
868
+ var PARAMS = ["kind", "steps", "lightness", "curve", "hue", "chroma", "anchor", "gates", "anchors", "base", "step", "ratio"];
869
+ var has = (o, key) => Object.hasOwn(o, key);
870
+ function refName(value) {
871
+ return typeof value === "string" ? REF.exec(value.trim())?.[1] : void 0;
872
+ }
873
+ function settle(v, path, ctx) {
874
+ if (isByAxis(v)) {
875
+ const picked = pick(v, ctx.sel);
876
+ if (!picked.ok) {
877
+ ctx.issues.push({ kind: "missing-axis-value", path, axis: picked.axis, value: picked.value });
878
+ return { value: void 0, ok: false };
879
+ }
880
+ return settle(picked.value, path, ctx);
881
+ }
882
+ if (Array.isArray(v)) {
883
+ let ok = true;
884
+ const value = v.map((x, i) => {
885
+ const s = settle(x, `${path}.${i}`, ctx);
886
+ ok = s.ok && ok;
887
+ return s.value;
888
+ });
889
+ return { value, ok };
890
+ }
891
+ if (typeof v === "object" && v !== null) {
892
+ let ok = true;
893
+ const value = {};
894
+ for (const [k, x] of Object.entries(v)) {
895
+ const s = settle(x, `${path}.${k}`, ctx);
896
+ ok = s.ok && ok;
897
+ value[k] = s.value;
898
+ }
899
+ return { value, ok };
900
+ }
901
+ if (typeof v === "string") {
902
+ const m = SEED_REF2.exec(v.trim());
903
+ if (!m) return { value: v, ok: true };
904
+ if (has(ctx.seeds, m[1])) return { value: ctx.seeds[m[1]], ok: true };
905
+ if (!ctx.declaredSeeds.has(m[1])) ctx.issues.push({ kind: "invalid", path, message: `unknown seed "${m[1]}"` });
906
+ return { value: void 0, ok: false };
907
+ }
908
+ return { value: v, ok: true };
909
+ }
910
+ var Reader = class {
911
+ ok = true;
912
+ issues;
913
+ constructor(issues) {
914
+ this.issues = issues;
915
+ }
916
+ fail(path, message) {
917
+ this.issues.push({ kind: "invalid", path, message });
918
+ this.ok = false;
919
+ }
920
+ num(x, path) {
921
+ if (typeof x === "number" && Number.isFinite(x)) return x;
922
+ this.fail(path, "expected a number");
923
+ return Number.NaN;
924
+ }
925
+ optNum(x, path, fallback) {
926
+ return x === void 0 ? fallback : this.num(x, path);
927
+ }
928
+ record(x, path) {
929
+ if (x === void 0) return {};
930
+ if (typeof x === "object" && x !== null && !Array.isArray(x)) return x;
931
+ this.fail(path, "expected an object");
932
+ return {};
933
+ }
934
+ };
935
+ var isRecord2 = (x) => typeof x === "object" && x !== null && !Array.isArray(x);
936
+ var stepNames = (x) => Array.isArray(x) && x.every((s) => typeof s === "string") ? x : void 0;
937
+ function rampColors(name, r, steps, ctx) {
938
+ const path = `ramps.${name}`;
939
+ const read = new Reader(ctx.issues);
940
+ if (r.kind === "lightness") {
941
+ const l = r.lightness;
942
+ let lightness = [0, 0];
943
+ if (Array.isArray(l) && l.length === 2) lightness = [read.num(l[0], `${path}.lightness.0`), read.num(l[1], `${path}.lightness.1`)];
944
+ else read.fail(`${path}.lightness`, "expected two numbers");
945
+ const chroma = read.record(r.chroma, `${path}.chroma`);
946
+ const atLeastZero = (v, at) => {
947
+ if (v < 0) read.fail(at, "expected a number \u2265 0");
948
+ return v;
949
+ };
950
+ const peak = r.chroma === void 0 ? 0 : atLeastZero(read.num(chroma.peak, `${path}.chroma.peak`), `${path}.chroma.peak`);
951
+ const anchor = {};
952
+ for (const [step, v] of Object.entries(read.record(r.anchor, `${path}.anchor`))) {
953
+ if (typeof v === "string" && HEX2.test(v)) anchor[step] = v;
954
+ else read.fail(`${path}.anchor.${step}`, "expected a hex color");
955
+ }
956
+ const params = {
957
+ steps,
958
+ lightness,
959
+ curve: read.optNum(r.curve, `${path}.curve`, 0),
960
+ hue: read.optNum(r.hue, `${path}.hue`, 0),
961
+ peak,
962
+ lightBias: atLeastZero(read.optNum(chroma.lightBias, `${path}.chroma.lightBias`, 0), `${path}.chroma.lightBias`),
963
+ darkBias: atLeastZero(read.optNum(chroma.darkBias, `${path}.chroma.darkBias`, 0), `${path}.chroma.darkBias`),
964
+ anchor
965
+ };
966
+ return read.ok ? lightnessRamp(params) : void 0;
967
+ }
968
+ if (r.kind === "categorical") {
969
+ const gates = {};
970
+ for (const [k, v] of Object.entries(read.record(r.gates, `${path}.gates`))) {
971
+ if (!has(DEFAULT_CONSTRAINTS, k) || k === "count" || k === "anchors") read.fail(`${path}.gates.${k}`, "unknown gate");
972
+ else if (typeof v !== typeof DEFAULT_CONSTRAINTS[k]) {
973
+ read.fail(`${path}.gates.${k}`, `expected a ${typeof DEFAULT_CONSTRAINTS[k]}`);
974
+ } else gates[k] = v;
975
+ }
976
+ const anchors = [];
977
+ const rawAnchors = r.anchors ?? [];
978
+ if (!Array.isArray(rawAnchors)) read.fail(`${path}.anchors`, "expected a list");
979
+ else {
980
+ rawAnchors.forEach((a, i) => {
981
+ const at = `${path}.anchors.${i}`;
982
+ const o = read.record(a, at);
983
+ if (typeof o.name !== "string") read.fail(`${at}.name`, "expected a string");
984
+ anchors.push({
985
+ name: String(o.name),
986
+ hue: read.num(o.hue, `${at}.hue`),
987
+ lightness: read.num(o.lightness, `${at}.lightness`),
988
+ chroma: read.optNum(o.chroma, `${at}.chroma`)
989
+ });
990
+ });
991
+ }
992
+ if (!read.ok) return void 0;
993
+ const { colors, feasible } = categoricalRamp(steps, gates, anchors);
994
+ if (!feasible) ctx.issues.push({ kind: "infeasible-ramp", ramp: name });
995
+ return colors;
996
+ }
997
+ read.fail(`${path}.kind`, 'expected "lightness" or "categorical"');
998
+ return void 0;
999
+ }
1000
+ function scaleValues(name, s, steps, issues) {
1001
+ const path = `scales.${name}`;
1002
+ const read = new Reader(issues);
1003
+ const base = read.num(s.base, `${path}.base`);
1004
+ const step = read.optNum(s.step, `${path}.step`);
1005
+ const ratio = read.optNum(s.ratio, `${path}.ratio`);
1006
+ if (!read.ok) return void 0;
1007
+ try {
1008
+ return { values: scale(steps, { base, step, ratio }), rule: step !== void 0 ? "linear" : "geometric" };
1009
+ } catch (e) {
1010
+ issues.push({ kind: "invalid", path, message: e.message });
1011
+ return void 0;
1012
+ }
1013
+ }
1014
+ function checkReferences(tokens, failed) {
1015
+ const done = /* @__PURE__ */ new Set();
1016
+ for (const start of Object.keys(tokens)) {
1017
+ const trail = [];
1018
+ let name = start;
1019
+ while (name !== void 0 && !done.has(name)) {
1020
+ if (trail.includes(name)) throw new Error(`Reference cycle at token "${name}" (${[...trail, name].join(" \u2192 ")})`);
1021
+ trail.push(name);
1022
+ const target = refName(tokens[name].value);
1023
+ if (target !== void 0 && !has(tokens, target)) {
1024
+ if (!failed.has(target)) throw new Error(`Token "${name}" references "${target}", which is not defined`);
1025
+ name = void 0;
1026
+ } else {
1027
+ name = target;
1028
+ }
1029
+ }
1030
+ for (const n of trail) done.add(n);
1031
+ }
1032
+ }
1033
+ function inferTypes(tokens, provenance, failed, issues) {
1034
+ for (let changed = true; changed; ) {
1035
+ changed = false;
1036
+ const memo = /* @__PURE__ */ new Map();
1037
+ const typeOf = (start) => {
1038
+ const trail = [];
1039
+ let type = UNTYPED;
1040
+ for (let name = start; name !== void 0 && has(tokens, name); name = refName(tokens[name].value)) {
1041
+ const known = memo.get(name);
1042
+ if (known !== void 0) {
1043
+ type = known;
1044
+ break;
1045
+ }
1046
+ trail.push(name);
1047
+ if (tokens[name].type !== UNTYPED) {
1048
+ type = tokens[name].type;
1049
+ break;
1050
+ }
1051
+ }
1052
+ for (const name of trail) {
1053
+ if (type !== UNTYPED && tokens[name].type === UNTYPED) tokens[name] = { ...tokens[name], type };
1054
+ memo.set(name, type);
1055
+ }
1056
+ return type;
1057
+ };
1058
+ for (const name of Object.keys(tokens)) typeOf(name);
1059
+ for (const name of Object.keys(tokens)) {
1060
+ const p = provenance[name];
1061
+ const generated = p.generated;
1062
+ const target = generated?.type === UNTYPED ? refName(generated.value) : void 0;
1063
+ const type = target === void 0 ? UNTYPED : typeOf(target);
1064
+ if (!generated || type === UNTYPED) continue;
1065
+ provenance[name] = { ...p, generated: { ...generated, type } };
1066
+ if (tokens[name].type === UNTYPED) {
1067
+ tokens[name] = { ...tokens[name], type };
1068
+ changed = true;
1069
+ }
1070
+ }
1071
+ }
1072
+ for (const [name, token] of Object.entries(tokens)) {
1073
+ if (token.type !== UNTYPED || refName(token.value) !== void 0 || failed.has(name)) continue;
1074
+ issues.push({ kind: "untyped-pin", token: name });
1075
+ }
1076
+ }
1077
+ var isPinObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v) && "value" in v;
1078
+ var NAMED_LAYERS = ["ramps", "scales", "semantics", "components", "pins"];
1079
+ var SAFE_NAME = /^[A-Za-z0-9_-]+$/;
1080
+ var isSafeName = (name) => SAFE_NAME.test(name);
1081
+ var UNSAFE = (name) => `"${name}" cannot name a token: use letters, digits, "-" and "_"`;
1082
+ function withoutUnsafeNames(def, issues) {
1083
+ let out = def;
1084
+ for (const layer of NAMED_LAYERS) {
1085
+ const entries = def[layer];
1086
+ if (!entries || Object.keys(entries).every(isSafeName)) continue;
1087
+ for (const n of Object.keys(entries)) {
1088
+ if (!isSafeName(n)) issues.push({ kind: "invalid", path: `${layer}.${n}`, message: UNSAFE(n) });
1089
+ }
1090
+ out = { ...out, [layer]: Object.fromEntries(Object.entries(entries).filter(([n]) => isSafeName(n))) };
1091
+ }
1092
+ return out;
1093
+ }
1094
+ function derive(definition, selection = {}, lookup) {
1095
+ const issues = [];
1096
+ const def = withoutUnsafeNames(mergeChain(definition, lookup), issues);
1097
+ const sel = fullSelection(def.axes ?? {}, selection);
1098
+ const seeds = {};
1099
+ for (const [k, v] of Object.entries(def.seeds ?? {})) {
1100
+ const picked = pick(v, sel);
1101
+ if (picked.ok) seeds[k] = picked.value;
1102
+ else issues.push({ kind: "missing-axis-value", path: `seeds.${k}`, axis: picked.axis, value: picked.value });
1103
+ }
1104
+ const ctx = { sel, seeds, declaredSeeds: new Set(Object.keys(def.seeds ?? {})), issues };
1105
+ const tokens = {};
1106
+ const provenance = {};
1107
+ const failed = /* @__PURE__ */ new Set();
1108
+ const producer = /* @__PURE__ */ new Map();
1109
+ const put = (name, token, layer, rule, path) => {
1110
+ const earlier = producer.get(name);
1111
+ if (earlier !== void 0) {
1112
+ issues.push({ kind: "invalid", path, message: `"${name}" is already produced by ${earlier}` });
1113
+ return;
1114
+ }
1115
+ producer.set(name, path);
1116
+ tokens[name] = token;
1117
+ provenance[name] = { layer, rule, pinned: false };
1118
+ };
1119
+ const failEntry = (name, entry, steps) => {
1120
+ for (const step of steps ?? declaredSteps(entry)) failed.add(`${name}-${step}`);
1121
+ };
1122
+ const settleEntry = (entry, path) => {
1123
+ let raw = entry;
1124
+ if (isByAxis(raw)) {
1125
+ const picked = pick(raw, sel);
1126
+ if (!picked.ok) {
1127
+ issues.push({ kind: "missing-axis-value", path, axis: picked.axis, value: picked.value });
1128
+ return { ok: false };
1129
+ }
1130
+ raw = picked.value;
1131
+ }
1132
+ if (!isRecord2(raw)) {
1133
+ issues.push({ kind: "invalid", path, message: "expected an object" });
1134
+ return { ok: false };
1135
+ }
1136
+ const r = { ...raw };
1137
+ let settled = true;
1138
+ for (const key of PARAMS) {
1139
+ if (!has(raw, key)) continue;
1140
+ const s = settle(raw[key], `${path}.${key}`, ctx);
1141
+ r[key] = s.value;
1142
+ settled = s.ok && settled;
1143
+ }
1144
+ const steps = stepNames(r.steps);
1145
+ if (!steps && settled) issues.push({ kind: "invalid", path: `${path}.steps`, message: "expected a list of step names" });
1146
+ if (steps?.includes("by")) {
1147
+ issues.push({ kind: "invalid", path: `${path}.steps`, message: '"by" is reserved and cannot name a step' });
1148
+ return { ok: false, steps };
1149
+ }
1150
+ const unsafe = steps?.find((s) => !isSafeName(s));
1151
+ if (unsafe !== void 0) {
1152
+ issues.push({ kind: "invalid", path: `${path}.steps`, message: UNSAFE(unsafe) });
1153
+ return { ok: false, steps };
1154
+ }
1155
+ return settled && steps ? { ok: true, r, steps } : { ok: false, steps };
1156
+ };
1157
+ const describe = (r, step) => {
1158
+ const d = isRecord2(r.describe) ? r.describe[step] : void 0;
1159
+ return typeof d === "string" ? d : void 0;
1160
+ };
1161
+ const rampSteps = {};
1162
+ for (const [name, entry] of Object.entries(def.ramps ?? {})) {
1163
+ const path = `ramps.${name}`;
1164
+ const e = settleEntry(entry, path);
1165
+ if (e.steps) rampSteps[name] = e.steps;
1166
+ const colors = e.ok ? rampColors(name, e.r, e.steps, ctx) : void 0;
1167
+ if (!e.ok || !colors) {
1168
+ failEntry(name, entry, e.steps);
1169
+ continue;
1170
+ }
1171
+ for (const step of e.steps) {
1172
+ put(`${name}-${step}`, { type: "color", value: colors[step], alpha: void 0, description: describe(e.r, step) }, "ramps", String(e.r.kind), path);
1173
+ }
1174
+ }
1175
+ for (const [name, entry] of Object.entries(def.scales ?? {})) {
1176
+ const path = `scales.${name}`;
1177
+ const e = settleEntry(entry, path);
1178
+ const out = e.ok ? scaleValues(name, e.r, e.steps, issues) : void 0;
1179
+ if (!e.ok || !out) {
1180
+ failEntry(name, entry, e.steps);
1181
+ continue;
1182
+ }
1183
+ for (const st of e.steps) {
1184
+ put(`${name}-${st}`, { type: "dimension", value: out.values[st], alpha: void 0, description: describe(e.r, st) }, "scales", out.rule, path);
1185
+ }
1186
+ }
1187
+ const seeded = (obj, path) => {
1188
+ const m = typeof obj.value === "string" ? SEED_REF2.exec(obj.value.trim()) : null;
1189
+ if (!m) return obj;
1190
+ if (has(seeds, m[1])) return { ...obj, value: seeds[m[1]] };
1191
+ if (path !== void 0 && !ctx.declaredSeeds.has(m[1])) issues.push({ kind: "invalid", path, message: `unknown seed "${m[1]}"` });
1192
+ return void 0;
1193
+ };
1194
+ const pinned = (name) => {
1195
+ const v = def.pins?.[name];
1196
+ const picked = v === void 0 ? void 0 : pick(v, sel);
1197
+ if (!picked?.ok) return void 0;
1198
+ return seeded(isPinObject(picked.value) ? picked.value : { value: picked.value });
1199
+ };
1200
+ const semantics = def.semantics ?? {};
1201
+ const components = def.components ?? {};
1202
+ const component = (name) => {
1203
+ const picked = has(components, name) ? pick(components[name], sel) : void 0;
1204
+ if (!picked?.ok) return void 0;
1205
+ return seeded(isPinObject(picked.value) ? picked.value : { value: picked.value });
1206
+ };
1207
+ const known = (name) => has(tokens, name) || failed.has(name) || has(semantics, name) || has(components, name) || has(def.pins ?? {}, name);
1208
+ const derived = deriveSemantics(semantics, { sel, tokens, ramps: rampSteps, pinned, component, known, issues });
1209
+ for (const name of Object.keys(semantics)) {
1210
+ const d = derived.get(name);
1211
+ if (d) put(name, d.token, "semantics", d.rule, `semantics.${name}`);
1212
+ else failed.add(name);
1213
+ }
1214
+ const toToken = (v, path, prior) => {
1215
+ const picked = pick(v, sel);
1216
+ if (!picked.ok) {
1217
+ issues.push({ kind: "missing-axis-value", path, axis: picked.axis, value: picked.value });
1218
+ return void 0;
1219
+ }
1220
+ const obj = seeded(isPinObject(picked.value) ? picked.value : { value: picked.value }, path);
1221
+ if (!obj) return void 0;
1222
+ return { type: obj.type ?? prior?.type ?? UNTYPED, value: obj.value, alpha: obj.alpha, description: obj.description ?? prior?.description };
1223
+ };
1224
+ for (const [name, v] of Object.entries(components)) {
1225
+ const path = `components.${name}`;
1226
+ const token = toToken(v, path, void 0);
1227
+ if (token) put(name, token, "components", "value", path);
1228
+ else failed.add(name);
1229
+ }
1230
+ for (const [name, v] of Object.entries(def.pins ?? {})) {
1231
+ const prior = has(tokens, name) ? tokens[name] : void 0;
1232
+ const token = toToken(v, `pins.${name}`, prior);
1233
+ if (!token) {
1234
+ if (!prior) failed.add(name);
1235
+ continue;
1236
+ }
1237
+ tokens[name] = token;
1238
+ provenance[name] = prior ? { ...provenance[name], pinned: true, generated: prior } : { layer: "pins", rule: "value", pinned: false };
1239
+ }
1240
+ checkReferences(tokens, failed);
1241
+ checkSemantics(semantics, sel, tokens, known, issues);
1242
+ inferTypes(tokens, provenance, failed, issues);
1243
+ return { tokens, provenance, issues };
1244
+ }
1245
+
1246
+ // src/engine/bake.ts
1247
+ function normalize(t) {
1248
+ return {
1249
+ type: t.type,
1250
+ value: t.value,
1251
+ ...t.alpha !== void 0 ? { alpha: t.alpha } : {},
1252
+ ...t.description !== void 0 ? { description: t.description } : {}
1253
+ };
1254
+ }
1255
+ var same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
1256
+ function table(def, axes, lookup) {
1257
+ const out = /* @__PURE__ */ new Map();
1258
+ for (const sel of enumerateSelections(axes)) {
1259
+ const { tokens } = derive(def, sel, lookup);
1260
+ const normalized = {};
1261
+ for (const [name, t] of Object.entries(tokens)) normalized[name] = normalize(t);
1262
+ out.set(selectionKey(axes, sel), normalized);
1263
+ }
1264
+ return out;
1265
+ }
1266
+ function definitionOrder(def) {
1267
+ const names = /* @__PURE__ */ new Set();
1268
+ for (const layer of [def.ramps, def.scales]) {
1269
+ for (const [name, entry] of Object.entries(layer ?? {})) for (const step of declaredSteps(entry)) names.add(`${name}-${step}`);
1270
+ }
1271
+ for (const layer of [def.semantics, def.components, def.pins]) for (const name of Object.keys(layer ?? {})) names.add(name);
1272
+ return [...names];
1273
+ }
1274
+ function bake(definition, lookup) {
1275
+ return bakeChain(definition, lookup).at(-1);
1276
+ }
1277
+ function bakeChain(definition, lookup) {
1278
+ const merged = mergeChain(definition, lookup);
1279
+ const parentDef = definition.extends ? lookup?.(definition.extends) : void 0;
1280
+ const above = parentDef ? bakeChain(parentDef, lookup) : [];
1281
+ return [...above, bakeOver(definition, merged, above, lookup)];
1282
+ }
1283
+ function bakeOver(definition, merged, above, lookup) {
1284
+ const axes = merged.axes ?? {};
1285
+ const own = table(definition, axes, lookup);
1286
+ const at = (sel, name) => own.get(selectionKey(axes, sel))?.[name];
1287
+ const inherited = (sel, name) => {
1288
+ const full = fullSelection(axes, sel);
1289
+ for (let i = above.length - 1; i >= 0; i--) {
1290
+ const tokens2 = above[i].tokens;
1291
+ if (!Object.hasOwn(tokens2, name)) continue;
1292
+ const picked = pick(tokens2[name], full);
1293
+ if (picked.ok) return picked.value;
1294
+ }
1295
+ return void 0;
1296
+ };
1297
+ const produced = new Set([...own.values()].flatMap((t) => Object.keys(t)));
1298
+ const ordered = definitionOrder(merged).filter((n) => produced.has(n));
1299
+ const names = [...ordered, ...[...produced].filter((n) => !ordered.includes(n))];
1300
+ const selections = enumerateSelections(axes);
1301
+ const tokens = {};
1302
+ for (const name of names) {
1303
+ if (selections.every((sel) => same(at(sel, name), inherited(sel, name)))) continue;
1304
+ const varying = Object.keys(axes).filter(
1305
+ (axis) => selections.some((sel) => Object.keys(axes[axis].values).some((v) => !same(at(sel, name), at({ ...sel, [axis]: v }, name))))
1306
+ );
1307
+ const build = (i, sel) => {
1308
+ if (i === varying.length) return at(fullSelection(axes, sel), name);
1309
+ const axis = varying[i];
1310
+ const branch = { by: axis };
1311
+ let any = false;
1312
+ for (const v of Object.keys(axes[axis].values)) {
1313
+ const x = build(i + 1, { ...sel, [axis]: v });
1314
+ if (x !== void 0) {
1315
+ branch[v] = x;
1316
+ any = true;
1317
+ }
1318
+ }
1319
+ return any ? branch : void 0;
1320
+ };
1321
+ const value = build(0, {});
1322
+ if (value !== void 0) tokens[name] = value;
1323
+ }
1324
+ return { name: definition.name, extends: definition.extends ?? null, axes, tokens };
1325
+ }
1326
+
1327
+ // src/engine/emit/css.ts
1328
+ var REF2 = /^\{([^}]+)\}$/;
1329
+ function refTarget2(token) {
1330
+ const m = typeof token.value === "string" ? REF2.exec(token.value.trim()) : null;
1331
+ if (!m) return null;
1332
+ const path = m[1];
1333
+ const dot = path.indexOf(".");
1334
+ return dot === -1 ? path : path.slice(dot + 1);
1335
+ }
1336
+ function cssValue(name, token) {
1337
+ const target = refTarget2(token);
1338
+ if (target === null) return resolveTokens({ [name]: token })[name];
1339
+ if (token.alpha === void 0) return `var(--wzl-${target})`;
1340
+ return `color-mix(in srgb, var(--wzl-${target}) ${Math.round(token.alpha * 100)}%, transparent)`;
1341
+ }
1342
+ function axisSets(axes) {
1343
+ const names = Object.keys(axes);
1344
+ const sets = [];
1345
+ for (let mask = 1; mask < 1 << names.length; mask += 1) sets.push(names.filter((_, i) => mask & 1 << i));
1346
+ return sets.sort((a, b) => a.length - b.length);
1347
+ }
1348
+ function combinations(axes, set) {
1349
+ let out = [{}];
1350
+ for (const axis of set) out = out.flatMap((s) => Object.keys(axes[axis].values).map((v) => ({ ...s, [axis]: v })));
1351
+ return out;
1352
+ }
1353
+ function schemeOf(axes, axis, value) {
1354
+ const def = axes[axis];
1355
+ if (!def) return void 0;
1356
+ if (!(value in def.values)) return schemeOf(axes, axis, def.default);
1357
+ return def.values[value].scheme ?? (Object.values(def.values).some((v) => v.scheme) ? "normal" : void 0);
1358
+ }
1359
+ function checkSchemes({ name, axes }) {
1360
+ const carrying = Object.keys(axes).filter((a) => Object.values(axes[a].values).some((v) => v.scheme));
1361
+ if (carrying.length > 1) throw new Error(`Theme "${name}": only one axis may carry a color scheme, but ${carrying.join(" and ")} do`);
1362
+ }
1363
+ function combineAxes(own, other) {
1364
+ const out = {};
1365
+ for (const [name, def] of Object.entries(own)) out[name] = { ...def, values: { ...other[name]?.values, ...def.values } };
1366
+ for (const [name, def] of Object.entries(other)) if (!out[name]) out[name] = def;
1367
+ return out;
1368
+ }
1369
+ var Resolved = class {
1370
+ axes;
1371
+ names;
1372
+ chain;
1373
+ memo = /* @__PURE__ */ new Map();
1374
+ constructor(axes, chain) {
1375
+ this.axes = axes;
1376
+ this.chain = chain;
1377
+ this.names = [...new Set([...chain].reverse().flatMap((t) => Object.keys(t.tokens)))];
1378
+ }
1379
+ token(name, sel) {
1380
+ const full = fullSelection(this.axes, sel);
1381
+ for (const t of this.chain) {
1382
+ if (!Object.hasOwn(t.tokens, name)) continue;
1383
+ const picked = pick(t.tokens[name], full);
1384
+ if (picked.ok) return picked.value;
1385
+ }
1386
+ return void 0;
1387
+ }
1388
+ /** What a browser computes: every `var()` substituted, down to literals. */
1389
+ expanded(name, sel, stack = []) {
1390
+ const key = JSON.stringify(fullSelection(this.axes, sel));
1391
+ const memo = this.memo.get(key) ?? this.memo.set(key, /* @__PURE__ */ new Map()).get(key);
1392
+ const hit = memo.get(name);
1393
+ if (hit !== void 0) return hit;
1394
+ const token = this.token(name, sel);
1395
+ if (!token) return `<unset ${name}>`;
1396
+ if (stack.includes(name)) return "<cycle>";
1397
+ const value = cssValue(name, token).replace(/var\(--wzl-([^)]+)\)/g, (_, n) => this.expanded(n, sel, [...stack, name]));
1398
+ memo.set(name, value);
1399
+ return value;
1400
+ }
1401
+ };
1402
+ function chainOf(baked, byName) {
1403
+ if (!baked.extends) return [baked];
1404
+ const parent = byName.get(baked.extends);
1405
+ if (!parent) throw new Error(`Theme "${baked.name}" extends "${baked.extends}", which is not being emitted`);
1406
+ return [baked, ...chainOf(parent, byName)];
1407
+ }
1408
+ var union = (axes, ...sets) => {
1409
+ const all = new Set(sets.flatMap((s) => s ?? []));
1410
+ return Object.keys(axes).filter((a) => all.has(a));
1411
+ };
1412
+ function blocks(lines, plan, selector) {
1413
+ const { axes, names } = plan;
1414
+ for (const set of axisSets(axes)) {
1415
+ const members = names.filter((n) => plan.all(n).join(",") === set.join(","));
1416
+ const hasOwn = (n) => plan.own(n).length > 0;
1417
+ const ownFirst = [...members.filter(hasOwn), ...members.filter((n) => !hasOwn(n))];
1418
+ const schemeAxis = set.length === 1 ? set[0] : void 0;
1419
+ for (const combo of combinations(axes, set)) {
1420
+ const scheme = schemeAxis ? schemeOf(plan.schemes, schemeAxis, combo[schemeAxis]) : void 0;
1421
+ if (ownFirst.length === 0 && !scheme) continue;
1422
+ const attrs = set.map((a) => `[data-wzl-${a}='${combo[a]}']`).join("");
1423
+ const decls = ownFirst.flatMap((name) => {
1424
+ const token = plan.value(name, combo);
1425
+ return token ? [` --wzl-${name}: ${cssValue(name, token)};`] : [];
1426
+ });
1427
+ lines.push(...selector(attrs), ...scheme ? [` color-scheme: ${scheme};`] : [], ...decls, "}", "");
1428
+ }
1429
+ }
1430
+ }
1431
+ function emitCss(themes) {
1432
+ const defaults = themes.filter((t) => t.isDefault);
1433
+ if (defaults.length !== 1) throw new Error(`emitCss needs exactly one default theme, got ${defaults.length}`);
1434
+ for (const t of themes) checkSchemes(t.baked);
1435
+ const byName = new Map(themes.map((t) => [t.baked.name, t.baked]));
1436
+ const def = defaults[0];
1437
+ const defResolved = new Resolved(def.baked.axes, [def.baked]);
1438
+ const diffed = themes.filter((t) => !t.isDefault).map((t) => {
1439
+ const axes2 = combineAxes(t.baked.axes, def.baked.axes);
1440
+ const resolved = new Resolved(t.baked.axes, chainOf(t.baked, byName));
1441
+ const selections = enumerateSelections(axes2);
1442
+ const differs = resolved.names.filter(
1443
+ (n) => selections.some((sel) => {
1444
+ const mine = resolved.token(n, sel);
1445
+ if (!mine) return false;
1446
+ const theirs = defResolved.token(n, sel);
1447
+ return !theirs || cssValue(n, mine) !== cssValue(n, theirs) || resolved.expanded(n, sel) !== defResolved.expanded(n, sel);
1448
+ })
1449
+ );
1450
+ return { t, axes: axes2, resolved, differs };
1451
+ });
1452
+ const redeclared = new Set(diffed.flatMap((d) => d.differs));
1453
+ const others = diffed.map(({ t, axes: axes2, resolved }) => ({
1454
+ theme: t.baked.name,
1455
+ axes: axes2,
1456
+ schemes: t.baked.axes,
1457
+ names: resolved.names.filter((n) => redeclared.has(n)),
1458
+ all: (n) => union(axes2, t.deps[n]?.all, def.deps[n]?.all),
1459
+ own: (n) => union(axes2, t.deps[n]?.own, def.deps[n]?.own),
1460
+ value: (n, sel) => resolved.token(n, sel)
1461
+ }));
1462
+ const lines = [
1463
+ "/* GENERATED by packages/theme/scripts/build-tokens.ts \u2014 do not edit.",
1464
+ " * Source: packages/theme/themes/<theme>.json",
1465
+ " */",
1466
+ ""
1467
+ ];
1468
+ const { axes, name: theme } = def.baked;
1469
+ const rootValues = pickAll(def.baked.tokens, fullSelection(axes, {}));
1470
+ lines.push(":root {");
1471
+ for (const axis of Object.values(axes)) {
1472
+ const scheme = axis.values[axis.default]?.scheme;
1473
+ if (scheme) lines.push(` color-scheme: ${scheme};`);
1474
+ }
1475
+ for (const [name, token] of Object.entries(rootValues)) {
1476
+ if (token.description) lines.push(` /* ${token.description} */`);
1477
+ lines.push(` --wzl-${name}: ${cssValue(name, token)};`);
1478
+ }
1479
+ lines.push("}", "");
1480
+ const reset = Object.keys(rootValues).filter((n) => redeclared.has(n) && (def.deps[n]?.all.length ?? 0) === 0);
1481
+ if (reset.length > 0) {
1482
+ lines.push(`[data-wzl-theme='${theme}'] {`, ...reset.map((n) => ` --wzl-${n}: ${cssValue(n, rootValues[n])};`), "}", "");
1483
+ }
1484
+ blocks(
1485
+ lines,
1486
+ {
1487
+ theme,
1488
+ axes,
1489
+ schemes: axes,
1490
+ names: Object.keys(def.baked.tokens),
1491
+ all: (n) => def.deps[n]?.all ?? [],
1492
+ own: (n) => def.deps[n]?.own ?? [],
1493
+ value: (n, sel) => pickAll(def.baked.tokens, fullSelection(axes, sel))[n]
1494
+ },
1495
+ (attrs) => [`[data-wzl-theme='${theme}']${attrs},`, `${attrs} {`]
1496
+ );
1497
+ for (const plan of others) {
1498
+ const invariant = plan.names.filter((n) => plan.all(n).length === 0);
1499
+ if (invariant.length > 0) {
1500
+ lines.push(`:root[data-wzl-theme='${plan.theme}'],`, `[data-wzl-theme='${plan.theme}'] {`);
1501
+ for (const n of invariant) {
1502
+ const token = plan.value(n, {});
1503
+ if (token) lines.push(` --wzl-${n}: ${cssValue(n, token)};`);
1504
+ }
1505
+ lines.push("}", "");
1506
+ }
1507
+ blocks(lines, plan, (attrs) => [`[data-wzl-theme='${plan.theme}']${attrs} {`]);
1508
+ }
1509
+ return lines.join("\n");
1510
+ }
1511
+
1512
+ // src/engine/emit/dtcg.ts
1513
+ var REF3 = /^\{([^}.]+)\}$/;
1514
+ var modeOnly = (theme, what) => new Error(`DTCG export supports a mode axis only; ${theme} ${what}`);
1515
+ function toDTCG(theme) {
1516
+ const axes = themeAxes(theme);
1517
+ const typeOf = (name) => {
1518
+ for (let t = theme; t; t = t.extends) {
1519
+ if (!Object.hasOwn(t.tokens, name)) continue;
1520
+ let v = t.tokens[name];
1521
+ while (isByAxis(v)) v = Object.entries(v).find(([k]) => k !== "by")?.[1];
1522
+ if (v !== void 0) return v.type;
1523
+ }
1524
+ return void 0;
1525
+ };
1526
+ const alias = (value) => {
1527
+ const target = typeof value === "string" ? REF3.exec(value.trim())?.[1] : void 0;
1528
+ const type = target === void 0 ? void 0 : typeOf(target);
1529
+ return type === void 0 ? value : `{${type}.${target}}`;
1530
+ };
1531
+ const put = (into, name, t) => {
1532
+ into[t.type] ??= { $type: t.type };
1533
+ into[t.type][name] = {
1534
+ $value: alias(t.value),
1535
+ ...t.description ? { $description: t.description } : {},
1536
+ ...t.alpha !== void 0 ? { $extensions: { [ALPHA_EXT]: t.alpha } } : {}
1537
+ };
1538
+ };
1539
+ const branches = (name, v) => {
1540
+ if (!isByAxis(v)) return [];
1541
+ if (v.by !== "mode") throw modeOnly(`"${name}"`, `varies by ${v.by}`);
1542
+ return Object.entries(v).flatMap(([mode, x]) => {
1543
+ if (mode === "by") return [];
1544
+ if (isByAxis(x)) throw modeOnly(`"${name}"`, `varies by ${x.by} inside mode`);
1545
+ return [[mode, x]];
1546
+ });
1547
+ };
1548
+ const modes = {};
1549
+ for (const mode of Object.keys(axes.mode?.values ?? {})) modes[mode] = {};
1550
+ const primitives = {};
1551
+ for (const [name, v] of Object.entries(theme.tokens)) {
1552
+ if (!isByAxis(v)) put(primitives, name, v);
1553
+ else for (const [mode, t] of branches(name, v)) put(modes[mode] ??= {}, name, t);
1554
+ }
1555
+ return { name: theme.name, ...axes.mode ? { defaultMode: axes.mode.default } : {}, primitives, modes };
1556
+ }
1557
+
1558
+ // src/hooks.ts
1559
+ var TOKEN_HOOKS = [
1560
+ {
1561
+ name: "disclosure-gap",
1562
+ type: "dimension",
1563
+ value: "4px",
1564
+ description: "Gap between a Disclosure\u2019s twisty and its label."
1565
+ },
1566
+ {
1567
+ name: "disclosure-target",
1568
+ type: "dimension",
1569
+ value: "20px",
1570
+ description: "Hit-target square of a Disclosure twisty, and of a labkit LayerList row\u2019s."
1571
+ },
1572
+ {
1573
+ name: "number-field-width",
1574
+ type: "dimension",
1575
+ value: "9ch",
1576
+ description: "Width of a NumberField\u2019s input."
1577
+ },
1578
+ {
1579
+ name: "prefs-column-width",
1580
+ type: "dimension",
1581
+ value: "300px",
1582
+ description: "Width of the Prefs category column."
1583
+ },
1584
+ {
1585
+ name: "prop-number-width",
1586
+ type: "dimension",
1587
+ value: "9ch",
1588
+ description: "Width of a numeric field in the Properties panel."
1589
+ },
1590
+ {
1591
+ name: "prop-text-width",
1592
+ type: "dimension",
1593
+ value: "16ch",
1594
+ description: "Width of a text field in the Properties panel."
1595
+ },
1596
+ {
1597
+ name: "property-readout-w",
1598
+ type: "dimension",
1599
+ value: "2.8em",
1600
+ description: "Minimum width of a Properties row\u2019s value readout."
1601
+ },
1602
+ {
1603
+ name: "swatch-size",
1604
+ type: "dimension",
1605
+ value: "28px",
1606
+ description: "Minimum cell of a swatch grid \u2014 PatternPicker, and any panel laying swatches out the same way."
1607
+ },
1608
+ {
1609
+ name: "timeline-label-w",
1610
+ type: "dimension",
1611
+ value: "120px",
1612
+ description: "Width of the Timeline\u2019s track-label gutter."
1613
+ },
1614
+ {
1615
+ name: "timeline-value-axis-w",
1616
+ type: "dimension",
1617
+ value: "32px",
1618
+ description: "Width reserved for the Timeline\u2019s value axis."
1619
+ }
1620
+ ];
1621
+
1622
+ // src/engine/emit/manifest.ts
1623
+ var groupOf = (name) => name.includes("-") ? name.slice(0, name.indexOf("-")) : name;
1624
+ var row = (name, type, value, description, hook) => ` { name: '--wzl-${name}', type: ${JSON.stringify(type)}, group: ${JSON.stringify(groupOf(name))}, defaultValue: ${JSON.stringify(value)}, description: ${JSON.stringify(description)}, hook: ${hook} },`;
1625
+ function emitManifest({ baked }) {
1626
+ const all = pickAll(baked.tokens, fullSelection(baked.axes, {}));
1627
+ const resolved = resolveTokens(all);
1628
+ const rows = Object.entries(all).map(
1629
+ ([name, token]) => row(name, token.type, resolved[name], token.description ?? "", false)
1630
+ );
1631
+ for (const h of [...TOKEN_HOOKS].sort((a, b) => a.name.localeCompare(b.name))) {
1632
+ rows.push(row(h.name, h.type, h.value, h.description, true));
1633
+ }
1634
+ return [
1635
+ "// GENERATED by packages/theme/scripts/build-tokens.ts \u2014 do not edit.",
1636
+ "",
1637
+ "/** One row of `TOKEN_MANIFEST`: a token, its DTCG type, the name-prefix",
1638
+ " * group it belongs to, its value in the default theme and selection, and",
1639
+ " * the description it was authored with (empty string if none). */",
1640
+ "export interface TokenManifestEntry {",
1641
+ " readonly name: string;",
1642
+ " readonly type: string;",
1643
+ " readonly group: string;",
1644
+ " readonly defaultValue: string;",
1645
+ " readonly description: string;",
1646
+ " /** A consumer override hook rather than a theme token: no theme declares",
1647
+ " * it, kit CSS reads it with `defaultValue` as the fallback, and setting",
1648
+ " * it on any container is what changes it. */",
1649
+ " readonly hook: boolean;",
1650
+ "}",
1651
+ "",
1652
+ "/** Every token of the default theme at its default selection, plus every",
1653
+ " * consumer override hook, described. Meant for tooling that presents the",
1654
+ " * token set \u2014 docs pages, token browsers, forge's CSS-vars panel \u2014 not",
1655
+ " * for resolving values at runtime. */",
1656
+ "export const TOKEN_MANIFEST: readonly TokenManifestEntry[] = [",
1657
+ rows.join("\n"),
1658
+ "];",
1659
+ ""
1660
+ ].join("\n");
1661
+ }
1662
+
1663
+ // src/engine/emit/themes.ts
1664
+ function chainOf2(baked, byName) {
1665
+ const parent = baked.extends ? byName.get(baked.extends) : void 0;
1666
+ if (baked.extends && !parent) throw new Error(`Theme "${baked.name}" extends "${baked.extends}", which is not in themes/`);
1667
+ return [...parent ? chainOf2(parent, byName) : [], baked];
1668
+ }
1669
+ function tokensAt(chain, sel) {
1670
+ return Object.assign({}, ...chain.map((t) => pickAll(t.tokens, sel)));
1671
+ }
1672
+ function emitThemes(themes) {
1673
+ const byName = new Map(themes.map((t) => [t.baked.name, t.baked]));
1674
+ const names = /* @__PURE__ */ new Set();
1675
+ const entries = themes.map(({ baked }) => {
1676
+ const chain = chainOf2(baked, byName);
1677
+ const selections = enumerateSelections(baked.axes).map((sel) => {
1678
+ const resolved = resolveTokens(tokensAt(chain, sel));
1679
+ for (const n of Object.keys(resolved)) names.add(`--wzl-${n}`);
1680
+ const body = Object.entries(resolved).map(([n, v]) => ` '--wzl-${n}': ${JSON.stringify(v)},`).join("\n");
1681
+ return ` '${selectionKey(baked.axes, sel)}': {
1682
+ ${body}
1683
+ },`;
1684
+ });
1685
+ return [
1686
+ ` ${JSON.stringify(baked.name)}: {`,
1687
+ ` name: ${JSON.stringify(baked.name)},`,
1688
+ ` axes: ${JSON.stringify(baked.axes)},`,
1689
+ " selections: {",
1690
+ selections.join("\n"),
1691
+ " },",
1692
+ " },"
1693
+ ].join("\n");
1694
+ });
1695
+ const sources = Object.fromEntries(themes.map((t) => [t.definition.name, t.definition]));
1696
+ const bakedAll = Object.fromEntries(themes.map((t) => [t.baked.name, t.baked]));
1697
+ return [
1698
+ "// GENERATED by packages/theme/scripts/build-tokens.ts \u2014 do not edit.",
1699
+ "",
1700
+ "import type { AxisDefs } from '../axes';",
1701
+ "import type { ThemeDefinition } from '../definition';",
1702
+ "import type { BakedTheme } from '../engine/bake';",
1703
+ "",
1704
+ "/** Every design token the built-in themes declare, spelled as the CSS",
1705
+ " * custom property it becomes. */",
1706
+ "export type TokenName =",
1707
+ `${[...names].sort().map((n) => ` | '${n}'`).join("\n")};`,
1708
+ "",
1709
+ "/** A built-in theme with its aliases already collapsed: every selection",
1710
+ " * (keyed `mode=dark`, axes in declaration order) maps every token name to",
1711
+ " * a final CSS value. */",
1712
+ "export interface GeneratedTheme {",
1713
+ " readonly name: string;",
1714
+ " readonly axes: AxisDefs;",
1715
+ " readonly selections: Readonly<Record<string, Readonly<Record<TokenName, string>>>>;",
1716
+ "}",
1717
+ "",
1718
+ "/** The built-in themes, resolved, keyed by theme name. Read this to list",
1719
+ " * or preview themes without resolving a token graph yourself. */",
1720
+ "export const THEMES = {",
1721
+ entries.join("\n"),
1722
+ "} as const satisfies Record<string, GeneratedTheme>;",
1723
+ "",
1724
+ "/** The built-in themes as authored: their unbaked definitions. */",
1725
+ `export const THEME_SOURCES: Readonly<Record<string, ThemeDefinition>> = ${JSON.stringify(sources, null, 2)};`,
1726
+ "",
1727
+ "/** The built-in themes with every rule run and references intact. This is",
1728
+ " * what `weaselTheme` is built from, so the runtime entry bundles no generator. */",
1729
+ `export const BAKED_THEMES: Readonly<Record<string, BakedTheme>> = ${JSON.stringify(bakedAll, null, 2)};`,
1730
+ ""
1731
+ ].join("\n");
1732
+ }
1733
+
1734
+ // src/engine/emit/tokens.ts
1735
+ function generateTokens(definitions) {
1736
+ const byName = new Map(definitions.map((d) => [d.name, d]));
1737
+ const lookup = (name) => byName.get(name);
1738
+ const roots = definitions.filter((d) => !d.extends);
1739
+ if (roots.length !== 1) throw new Error(`themes/ needs exactly one theme that extends nothing; found ${roots.length}`);
1740
+ const ordered = [roots[0], ...definitions.filter((d) => d.extends)];
1741
+ const problems = ordered.flatMap(
1742
+ (def) => enumerateSelections(mergeChain(def, lookup).axes ?? {}).flatMap(
1743
+ (sel) => derive(def, sel, lookup).issues.map((issue) => `${def.name} ${JSON.stringify(sel)}: ${JSON.stringify(issue)}`)
1744
+ )
1745
+ );
1746
+ if (problems.length > 0) return { ok: false, problems };
1747
+ const themes = ordered.map((definition) => ({
1748
+ definition,
1749
+ baked: bake(definition, lookup),
1750
+ deps: axisDependencies(definition, lookup),
1751
+ isDefault: definition === roots[0]
1752
+ }));
1753
+ return {
1754
+ ok: true,
1755
+ files: { "tokens.css": emitCss(themes), "themes.ts": emitThemes(themes), "manifest.ts": emitManifest(themes[0]) }
1756
+ };
1757
+ }
1758
+
1759
+ export { CHROMA_WEIGHT, DEFAULT_CONSTRAINTS, anchorFromHex, axisDependencies, bake, bakeChain, categoricalRamp, chromaCap, contrast, cssValue, declaredSteps, deltaE, derive, emitCss, emitManifest, emitThemes, floorDegrees, generate, generateTokens, hexToRgb, hueGap, hueName, lightnessRamp, luminance, mergeChain, scale, toDTCG, toHex, toHexPreview, toLab, toLch, vividAt };
1760
+ //# sourceMappingURL=engine.js.map
1761
+ //# sourceMappingURL=engine.js.map