@weasel-js/theme 1.5.2 → 1.6.0

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 CHANGED
@@ -1,387 +1,6 @@
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 { hexToOklchDeg, oklchDegToHex, srgbU8ToOklab } from '@weasel-js/paint';
4
-
5
- function hexToRgb(hex) {
6
- const n = Number.parseInt(hex.slice(1), 16);
7
- return [n >> 16 & 255, n >> 8 & 255, n & 255];
8
- }
9
- var toHex = oklchDegToHex;
10
- var toLch = hexToOklchDeg;
11
- function chromaCap(L, H) {
12
- return toLch(toHex(L, 0.5, H)).C;
13
- }
14
- function luminance(hex) {
15
- const [r, g, b] = hexToRgb(hex).map((v) => v / 255);
16
- const f = (u) => u <= 0.04045 ? u / 12.92 : ((u + 0.055) / 1.055) ** 2.4;
17
- return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b);
18
- }
19
- function contrast(a, b) {
20
- const [hi, lo] = [luminance(a), luminance(b)].sort((p, q) => q - p);
21
- return (hi + 0.05) / (lo + 0.05);
22
- }
23
- function hueGap(a, b) {
24
- const d = Math.abs(a - b) % 360;
25
- return Math.min(d, 360 - d);
26
- }
27
- function toLab(hex) {
28
- const [r, g, b] = hexToRgb(hex);
29
- return srgbU8ToOklab(r, g, b);
30
- }
31
- var CHROMA_WEIGHT = 3;
32
- function deltaE(a, b) {
33
- const [l1, a1, b1] = toLab(a);
34
- const [l2, a2, b2] = toLab(b);
35
- return Math.hypot(l1 - l2, CHROMA_WEIGHT * (a1 - a2), CHROMA_WEIGHT * (b1 - b2));
36
- }
37
- function vividAt(L, H) {
38
- return toHex(L, chromaCap(L, H), H);
39
- }
40
-
41
- // src/engine/color/generate.ts
42
- function peakLightness(H) {
43
- let bestL = 0.5;
44
- let bestC = -1;
45
- for (let L = 0.32; L <= 0.96; L += 0.01) {
46
- const c = chromaCap(L, H);
47
- if (c > bestC) {
48
- bestC = c;
49
- bestL = L;
50
- }
51
- }
52
- return bestL;
53
- }
54
- var PEAK = [];
55
- for (let H = 0; H < 360; H += 1) PEAK.push(peakLightness(H));
56
- var peakAt = (H) => PEAK[(Math.round(H) % 360 + 360) % 360];
57
- function place(H, c) {
58
- const L = peakAt(H) + (c.lightnessTarget - peakAt(H)) * c.lightnessPull;
59
- return toHex(L, chromaCap(L, H) * c.chromaFraction, H);
60
- }
61
- var HUE_NAMES = [
62
- [15, "rose"],
63
- [45, "red"],
64
- [62, "orange"],
65
- [85, "amber"],
66
- [100, "gold"],
67
- [122, "yellow"],
68
- [138, "lime"],
69
- [165, "green"],
70
- [195, "teal"],
71
- [218, "cyan"],
72
- [245, "sky"],
73
- [275, "blue"],
74
- [310, "violet"],
75
- [345, "fuchsia"]
76
- ];
77
- function hueName(H) {
78
- for (const [ceil, name] of HUE_NAMES) if (H < ceil) return name;
79
- return "rose";
80
- }
81
- function uniqueNames(hues) {
82
- const seen = /* @__PURE__ */ new Map();
83
- return hues.map((h) => {
84
- const base = hueName(h);
85
- const n = (seen.get(base) ?? 0) + 1;
86
- seen.set(base, n);
87
- return n === 1 ? base : `${base}-${n}`;
88
- });
89
- }
90
- function equalizeChroma(hexes, k, anchored) {
91
- if (k <= 0) return hexes;
92
- const lch = hexes.map(toLch);
93
- const target = lch.reduce((sum, x) => sum + x.C, 0) / lch.length;
94
- return lch.map((x, i) => {
95
- const wanted = x.C + (target - x.C) * k;
96
- if (anchored[i]) return toHex(x.L, Math.min(wanted, chromaCap(x.L, x.H)), x.H);
97
- if (chromaCap(x.L, x.H) >= wanted) return toHex(x.L, wanted, x.H);
98
- let bestL = x.L;
99
- let bestDistance = Infinity;
100
- for (let L = 0.3; L <= 0.96; L += 5e-3) {
101
- if (chromaCap(L, x.H) >= wanted) {
102
- const d = Math.abs(L - x.L);
103
- if (d < bestDistance) {
104
- bestDistance = d;
105
- bestL = L;
106
- }
107
- }
108
- }
109
- if (bestDistance === Infinity) {
110
- const pk = peakAt(x.H);
111
- return toHex(pk, chromaCap(pk, x.H), x.H);
112
- }
113
- return toHex(bestL, wanted, x.H);
114
- });
115
- }
116
- function farthestFirst(hues) {
117
- const order = [0];
118
- while (order.length < hues.length) {
119
- let best = -1;
120
- let bestGap = -1;
121
- for (let i = 0; i < hues.length; i += 1) {
122
- if (order.includes(i)) continue;
123
- const gap = Math.min(...order.map((j) => hueGap(hues[i], hues[j])));
124
- if (gap > bestGap) {
125
- bestGap = gap;
126
- best = i;
127
- }
128
- }
129
- order.push(best);
130
- }
131
- return order;
132
- }
133
- function floorDegrees(c) {
134
- return c.hueFloor <= 0 ? 0 : c.hueFloor * 360 / Math.max(1, c.count);
135
- }
136
- function searchHues(c) {
137
- const minGapDeg = floorDegrees(c);
138
- const anchorHues = c.anchors.map((a) => a.hue);
139
- const anchorHexes = c.anchors.map(toHexPreview);
140
- const freeCount = c.count - c.anchors.length;
141
- if (freeCount < 0) return null;
142
- if (freeCount === 0) return [];
143
- const weighted = (hex) => {
144
- const [L, a, b] = toLab(hex);
145
- return [L, CHROMA_WEIGHT * a, CHROMA_WEIGHT * b];
146
- };
147
- const between = (p, q) => Math.hypot(p[0] - q[0], p[1] - q[1], p[2] - q[2]);
148
- const placed = /* @__PURE__ */ new Map();
149
- const at = (H) => {
150
- const key = (Math.round(H) % 360 + 360) % 360;
151
- let v = placed.get(key);
152
- if (!v) {
153
- const hex = place(key, c);
154
- const lch = toLch(hex);
155
- v = {
156
- hex,
157
- hue: lch.H,
158
- contrast: contrast(hex, c.surface),
159
- chroma: lch.C,
160
- surfaceDistance: deltaE(hex, c.surface),
161
- lab: weighted(hex)
162
- };
163
- placed.set(key, v);
164
- }
165
- return v;
166
- };
167
- const anchorMeta = anchorHexes.map((hex) => {
168
- const lch = toLch(hex);
169
- return {
170
- hex,
171
- hue: lch.H,
172
- contrast: contrast(hex, c.surface),
173
- chroma: lch.C,
174
- surfaceDistance: deltaE(hex, c.surface),
175
- lab: weighted(hex)
176
- };
177
- });
178
- const metaFor = (free) => [...anchorMeta, ...free.map(at)];
179
- const penalty = (free) => {
180
- const m = metaFor(free);
181
- let p = 0;
182
- if (minGapDeg > 0) {
183
- for (let i = 0; i < m.length; i += 1) {
184
- for (let j = i + 1; j < m.length; j += 1) {
185
- const short = minGapDeg - hueGap(m[i].hue, m[j].hue);
186
- if (short > 0) p += short;
187
- }
188
- }
189
- }
190
- if (c.minContrast > 0) {
191
- for (const x of m) {
192
- const short = c.minContrast - x.contrast;
193
- if (short > 0) p += short * 40;
194
- }
195
- }
196
- if (c.minSurfaceDistance > 0) {
197
- for (const x of m) {
198
- const short = c.minSurfaceDistance - x.surfaceDistance;
199
- if (short > 0) p += short * 400;
200
- }
201
- }
202
- if (c.minDistance > 0) {
203
- for (let i = 0; i < m.length; i += 1) {
204
- for (let j = i + 1; j < m.length; j += 1) {
205
- const short = c.minDistance - between(m[i].lab, m[j].lab);
206
- if (short > 0) p += short * 400;
207
- }
208
- }
209
- }
210
- return p;
211
- };
212
- const score = (free) => {
213
- const m = metaFor(free);
214
- return m.reduce((sum, x) => sum + x.chroma, 0) / m.length;
215
- };
216
- const seeds = [];
217
- const seedStep = Math.max(6, Math.round(freeCount * 1.6));
218
- for (let start = 0; start < 360; start += seedStep) {
219
- seeds.push(Array.from({ length: freeCount }, (_, i) => Math.round((start + i * 360 / freeCount) % 360)));
220
- }
221
- if (anchorHues.length > 0) {
222
- const base = anchorHues[0];
223
- const span = 360 - 2 * Math.max(minGapDeg, 1);
224
- for (let jitter = -8; jitter <= 8; jitter += 2) {
225
- for (const divisor of [Math.max(1, freeCount - 1), freeCount]) {
226
- seeds.push(
227
- Array.from(
228
- { length: freeCount },
229
- (_, i) => Math.round(((base + minGapDeg + jitter + i * span / divisor) % 360 + 360) % 360)
230
- )
231
- );
232
- }
233
- }
234
- }
235
- const STEPS = [-16, -8, -4, -2, -1, 1, 2, 4, 8, 16];
236
- const penaltyAt = (m, i) => {
237
- let p = 0;
238
- const x = m[i];
239
- if (c.minContrast > 0) {
240
- const short = c.minContrast - x.contrast;
241
- if (short > 0) p += short * 40;
242
- }
243
- if (c.minSurfaceDistance > 0) {
244
- const short = c.minSurfaceDistance - x.surfaceDistance;
245
- if (short > 0) p += short * 400;
246
- }
247
- for (let j = 0; j < m.length; j += 1) {
248
- if (j === i) continue;
249
- if (minGapDeg > 0) {
250
- const short = minGapDeg - hueGap(x.hue, m[j].hue);
251
- if (short > 0) p += short;
252
- }
253
- if (c.minDistance > 0) {
254
- const short = c.minDistance - between(x.lab, m[j].lab);
255
- if (short > 0) p += short * 400;
256
- }
257
- }
258
- return p;
259
- };
260
- const descend = (hues, useChroma, keepLegal) => {
261
- const current = [...hues];
262
- let meta = metaFor(current);
263
- const offset = anchorMeta.length;
264
- for (let iteration = 0; iteration < 120; iteration += 1) {
265
- let improved = false;
266
- for (let i = 0; i < freeCount; i += 1) {
267
- const slot = offset + i;
268
- const beforeP = penaltyAt(meta, slot);
269
- const beforeC = meta[slot].chroma;
270
- for (const delta of STEPS) {
271
- const next = ((current[i] + delta) % 360 + 360) % 360;
272
- if (next === current[i]) continue;
273
- const candidate = at(next);
274
- const trialMeta = meta.slice();
275
- trialMeta[slot] = candidate;
276
- const afterP = penaltyAt(trialMeta, slot);
277
- const better = useChroma ? (keepLegal ? afterP <= beforeP : true) && candidate.chroma > beforeC : afterP < beforeP;
278
- if (!better) continue;
279
- if (keepLegal && afterP > beforeP) continue;
280
- current[i] = next;
281
- meta = trialMeta;
282
- improved = true;
283
- break;
284
- }
285
- }
286
- if (!improved) break;
287
- }
288
- return current;
289
- };
290
- let best = null;
291
- let bestScore = -1;
292
- for (const seed of seeds) {
293
- const repaired = penalty(seed) > 0 ? descend(seed, false, false) : seed;
294
- if (penalty(repaired) > 0) continue;
295
- const tuned = descend(repaired, true, true);
296
- if (penalty(tuned) > 0) continue;
297
- const s = score(tuned);
298
- if (s > bestScore) {
299
- bestScore = s;
300
- best = tuned;
301
- }
302
- }
303
- return best;
304
- }
305
- function pairwiseMin(hexes) {
306
- if (hexes.length < 2) return 1;
307
- let min = Infinity;
308
- for (let i = 0; i < hexes.length; i += 1) {
309
- for (let j = i + 1; j < hexes.length; j += 1) min = Math.min(min, deltaE(hexes[i], hexes[j]));
310
- }
311
- return min;
312
- }
313
- function summarize(swatches, surface) {
314
- const cs = swatches.map((s) => s.lch.C);
315
- const ls = swatches.map((s) => s.lch.L);
316
- let gap = 360;
317
- for (let i = 0; i < swatches.length; i += 1) {
318
- for (let j = i + 1; j < swatches.length; j += 1) {
319
- gap = Math.min(gap, hueGap(swatches[i].lch.H, swatches[j].lch.H));
320
- }
321
- }
322
- return {
323
- meanChroma: cs.reduce((a, b) => a + b, 0) / (cs.length || 1),
324
- chromaSpread: Math.max(...cs) - Math.min(...cs),
325
- lightnessSpread: Math.max(...ls) - Math.min(...ls),
326
- minHueGap: swatches.length > 1 ? gap : 360,
327
- hueFloorShare: swatches.length > 1 ? gap * swatches.length / 360 : 1,
328
- minContrast: Math.min(...swatches.map((s) => contrast(s.hex, surface))),
329
- minSurfaceDistance: Math.min(...swatches.map((s) => deltaE(s.hex, surface))),
330
- minDistance: pairwiseMin(swatches.map((s) => s.hex))
331
- };
332
- }
333
- function generate(c) {
334
- const found = searchHues(c);
335
- const feasible = found !== null;
336
- const free = found ?? Array.from(
337
- { length: Math.max(0, c.count - c.anchors.length) },
338
- (_, i) => Math.round(i * 360 / Math.max(1, c.count - c.anchors.length))
339
- );
340
- const hues = [...c.anchors.map((a) => a.hue), ...free];
341
- const anchoredFlags = [...c.anchors.map(() => true), ...free.map(() => false)];
342
- const raw = [
343
- ...c.anchors.map(toHexPreview),
344
- ...free.map((h) => place(h, c))
345
- ];
346
- const hexes = equalizeChroma(raw, c.equalize, anchoredFlags);
347
- const order = c.order === "farthest" ? farthestFirst(hues) : [...hues.keys()].sort((a, b) => hues[a] - hues[b]);
348
- const names = uniqueNames(order.map((i) => toLch(hexes[i]).H));
349
- const swatches = order.map((i, position) => ({
350
- name: c.anchors[i]?.name ?? names[position],
351
- hex: hexes[i],
352
- lch: toLch(hexes[i]),
353
- anchored: anchoredFlags[i],
354
- contrast: contrast(hexes[i], c.surface)
355
- }));
356
- return { swatches, stats: summarize(swatches, c.surface), feasible };
357
- }
358
- var DEFAULT_CONSTRAINTS = {
359
- count: 10,
360
- hueFloor: 0.9,
361
- minContrast: 4,
362
- surface: "#181a1e",
363
- lightnessTarget: 0.72,
364
- lightnessPull: 0.4,
365
- chromaFraction: 0.95,
366
- equalize: 0,
367
- minSurfaceDistance: 0.25,
368
- minDistance: 0.22,
369
- anchors: [],
370
- order: "farthest"
371
- };
372
- function toHexPreview(a) {
373
- const cap = chromaCap(a.lightness, a.hue);
374
- return toHex(a.lightness, a.chroma === void 0 ? cap : Math.min(a.chroma, cap), a.hue);
375
- }
376
- function anchorFromHex(hex, name) {
377
- const { L, C, H } = toLch(hex);
378
- return {
379
- name: name ?? hueName(H),
380
- hue: Math.round(H),
381
- lightness: Number(L.toFixed(3)),
382
- chroma: Number(C.toFixed(4))
383
- };
384
- }
1
+ import { overrideKey, AXES_EXT, ALPHA_EXT } from './chunk-GI7ZAXIB.js';
2
+ import { mergeAxes, toLch, toHex, generate, DEFAULT_CONSTRAINTS, fullSelection, pick, enumerateSelections, resolveTokens, pickAll, themeAxes, selectionKey, isByAxis, contrast } from './chunk-QKRFHBRB.js';
3
+ export { CHROMA_WEIGHT, DEFAULT_CONSTRAINTS, anchorFromHex, chromaCap, contrast, deltaE, floorDegrees, generate, hexToRgb, hueGap, hueName, luminance, toHex, toHexPreview, toLab, toLch, vividAt } from './chunk-QKRFHBRB.js';
385
4
 
386
5
  // src/engine/steps.ts
387
6
  var stepList = (x) => Array.isArray(x) ? [...new Set(x.filter((s) => typeof s === "string"))] : [];
@@ -422,6 +41,17 @@ function ownSteps(def) {
422
41
  }
423
42
  return names;
424
43
  }
44
+ function fillFromParent(child, parent, axes) {
45
+ if (!isByAxis(child) || parent === void 0) return child;
46
+ const values = Object.keys(axes[child.by]?.values ?? {});
47
+ const out = { ...child };
48
+ for (const v of values) {
49
+ if (v in out) continue;
50
+ out[v] = isByAxis(parent) && parent.by === child.by ? parent[v] : parent;
51
+ if (out[v] === void 0) delete out[v];
52
+ }
53
+ return out;
54
+ }
425
55
  function mergeChain(def, lookup, seen = /* @__PURE__ */ new Set()) {
426
56
  if (seen.has(def.name)) throw new Error(`extends cycle at theme "${def.name}"`);
427
57
  if (!def.extends) return def;
@@ -432,8 +62,13 @@ function mergeChain(def, lookup, seen = /* @__PURE__ */ new Set()) {
432
62
  for (const layer of LAYERS) out[layer] = { ...parent[layer] ?? {}, ...def[layer] ?? {} };
433
63
  const shadowed = ownSteps(def);
434
64
  const inherited = Object.entries(parent.pins ?? {}).filter(([name]) => !shadowed.has(name));
435
- out.pins = { ...Object.fromEntries(inherited), ...def.pins ?? {} };
436
- out.axes = mergeAxes(parent.axes ?? {}, def.axes ?? {});
65
+ const axes = mergeAxes(parent.axes ?? {}, def.axes ?? {});
66
+ const parentPins = parent.pins ?? {};
67
+ const own = Object.entries(def.pins ?? {}).map(([name, v]) => [name, fillFromParent(v, parentPins[name], axes)]);
68
+ out.pins = { ...Object.fromEntries(inherited), ...Object.fromEntries(own) };
69
+ out.axes = axes;
70
+ out.tones = def.tones ?? parent.tones;
71
+ if (out.tones === void 0) delete out.tones;
437
72
  return out;
438
73
  }
439
74
 
@@ -596,14 +231,23 @@ function lightnessRamp(p) {
596
231
  const at = (i) => n === 1 ? 0 : i / (n - 1);
597
232
  const lightBias = p.lightBias ?? 0;
598
233
  const max = envelopeMax(lightBias, p.darkBias);
599
- let { hue, peak } = p;
600
- const anchorIndex = p.steps.findIndex((s) => p.anchor?.[s] !== void 0);
601
- if (anchorIndex !== -1) {
602
- const a = toLch(p.anchor[p.steps[anchorIndex]]);
603
- const e = envelope(at(anchorIndex), lightBias, p.darkBias);
604
- hue = a.H;
605
- peak = e > 1e-6 ? a.C * max / e : a.C;
606
- }
234
+ const anchors = p.steps.flatMap((s, i) => {
235
+ const hex = p.anchor?.[s];
236
+ if (hex === void 0) return [];
237
+ const { C, H } = toLch(hex);
238
+ return [{ i, hue: H, peak: C }];
239
+ });
240
+ const hueAndPeak = (i) => {
241
+ if (anchors.length === 0) return p;
242
+ const next = anchors.findIndex((a2) => a2.i >= i);
243
+ if (next === 0) return anchors[0];
244
+ if (next === -1) return anchors[anchors.length - 1];
245
+ const a = anchors[next - 1];
246
+ const b = anchors[next];
247
+ const f = (i - a.i) / (b.i - a.i);
248
+ const arc = ((b.hue - a.hue) % 360 + 540) % 360 - 180;
249
+ return { hue: ((a.hue + arc * f) % 360 + 360) % 360, peak: a.peak + (b.peak - a.peak) * f };
250
+ };
607
251
  const out = {};
608
252
  p.steps.forEach((step, i) => {
609
253
  const fixed = p.anchor?.[step];
@@ -613,6 +257,7 @@ function lightnessRamp(p) {
613
257
  }
614
258
  const t = at(i);
615
259
  const L = p.lightness[0] + (p.lightness[1] - p.lightness[0]) * (t + (smoothstep(t) - t) * p.curve);
260
+ const { hue, peak } = hueAndPeak(i);
616
261
  out[step] = toHex(L, Math.min(peak * envelope(t, lightBias, p.darkBias) / max, MAX_CHROMA), hue);
617
262
  });
618
263
  return out;
@@ -1236,6 +881,7 @@ function derive(definition, selection = {}, lookup) {
1236
881
  }
1237
882
  for (const [name, v] of Object.entries(def.pins ?? {})) {
1238
883
  const prior = has(tokens, name) ? tokens[name] : void 0;
884
+ if (prior && !pick(v, sel).ok) continue;
1239
885
  const token = toToken(v, `pins.${name}`, prior);
1240
886
  if (!token) {
1241
887
  if (!prior) failed.add(name);
@@ -1328,7 +974,15 @@ function bakeOver(definition, merged, above, lookup) {
1328
974
  const value = build(0, {});
1329
975
  if (value !== void 0) tokens[name] = value;
1330
976
  }
1331
- return { name: definition.name, extends: definition.extends ?? null, axes, tokens };
977
+ const ramps = Object.fromEntries(Object.entries(definition.ramps ?? {}).map(([name, entry]) => [name, declaredSteps(entry)]));
978
+ return {
979
+ name: definition.name,
980
+ extends: definition.extends ?? null,
981
+ axes,
982
+ tokens,
983
+ ...Object.keys(ramps).length > 0 ? { ramps } : {},
984
+ ...definition.tones !== void 0 ? { tones: definition.tones } : {}
985
+ };
1332
986
  }
1333
987
 
1334
988
  // src/engine/emit/css.ts
@@ -1520,6 +1174,9 @@ function emitCss(themes) {
1520
1174
  var REF3 = /^\{([^}.]+)\}$/;
1521
1175
  function toDTCG(theme) {
1522
1176
  const axes = themeAxes(theme);
1177
+ const defaults = fullSelection(axes);
1178
+ const nonMode = Object.keys(axes).filter((a) => a !== "mode");
1179
+ const declaredModes = Object.keys(axes.mode?.values ?? {});
1523
1180
  const typeOf = (name) => {
1524
1181
  for (let t = theme; t; t = t.extends) {
1525
1182
  if (!Object.hasOwn(t.tokens, name)) continue;
@@ -1542,35 +1199,52 @@ function toDTCG(theme) {
1542
1199
  ...t.alpha !== void 0 ? { $extensions: { [ALPHA_EXT]: t.alpha } } : {}
1543
1200
  };
1544
1201
  };
1545
- const flatten = (v) => {
1546
- if (!isByAxis(v)) return v;
1547
- if (v.by === "mode") {
1548
- const out = { by: "mode" };
1549
- for (const [mode, x] of Object.entries(v)) {
1550
- if (mode === "by") continue;
1551
- const inner = flatten(x);
1552
- if (inner !== void 0) out[mode] = inner;
1553
- }
1554
- return out;
1202
+ const scan2 = (v, used, modeKeys) => {
1203
+ if (!isByAxis(v)) return;
1204
+ used.add(v.by);
1205
+ for (const [k, x] of Object.entries(v)) {
1206
+ if (k === "by") continue;
1207
+ if (v.by === "mode") modeKeys.add(k);
1208
+ scan2(x, used, modeKeys);
1555
1209
  }
1556
- const fallback = axes[v.by]?.default;
1557
- const chosen = fallback !== void 0 ? v[fallback] : void 0;
1558
- return chosen === void 0 ? void 0 : flatten(chosen);
1559
- };
1560
- const branches = (v) => {
1561
- if (!isByAxis(v)) return [];
1562
- return Object.entries(v).flatMap(([mode, x]) => mode === "by" ? [] : [[mode, x]]);
1563
1210
  };
1564
- const modes = {};
1565
- for (const mode of Object.keys(axes.mode?.values ?? {})) modes[mode] = {};
1566
- const primitives = {};
1211
+ const base = { primitives: {}, modes: {} };
1212
+ for (const mode of declaredModes) base.modes[mode] = {};
1213
+ const overrides = {};
1214
+ const varies = {};
1567
1215
  for (const [name, raw] of Object.entries(theme.tokens)) {
1568
- const v = flatten(raw);
1569
- if (v === void 0) continue;
1570
- if (!isByAxis(v)) put(primitives, name, v);
1571
- else for (const [mode, t] of branches(v)) put(modes[mode] ??= {}, name, t);
1216
+ const used = /* @__PURE__ */ new Set();
1217
+ const modeKeys = /* @__PURE__ */ new Set();
1218
+ scan2(raw, used, modeKeys);
1219
+ const varying = nonMode.filter((a) => used.has(a));
1220
+ if (varying.length > 0) varies[name] = varying;
1221
+ const modes = used.has("mode") ? [.../* @__PURE__ */ new Set([...declaredModes, ...modeKeys])] : [];
1222
+ for (const combo of enumerateSelections(Object.fromEntries(varying.map((a) => [a, axes[a]])))) {
1223
+ const key = overrideKey(axes, combo);
1224
+ const layer = key === "" ? base : overrides[key] ??= { primitives: {}, modes: {} };
1225
+ const sel = { ...defaults, ...combo };
1226
+ if (modes.length === 0) {
1227
+ const p = pick(raw, sel);
1228
+ if (p.ok) put(layer.primitives, name, p.value);
1229
+ continue;
1230
+ }
1231
+ const picked = modes.map((mode) => [mode, pick(raw, { ...sel, mode })]);
1232
+ const first = picked[0][1];
1233
+ if (first.ok && picked.every(([, p]) => p.ok && p.value === first.value)) {
1234
+ put(layer.primitives, name, first.value);
1235
+ continue;
1236
+ }
1237
+ for (const [mode, p] of picked) if (p.ok) put(layer.modes[mode] ??= {}, name, p.value);
1238
+ }
1572
1239
  }
1573
- return { name: theme.name, ...axes.mode ? { defaultMode: axes.mode.default } : {}, primitives, modes };
1240
+ const ext = { axes, varies, overrides };
1241
+ return {
1242
+ name: theme.name,
1243
+ ...axes.mode ? { defaultMode: axes.mode.default } : {},
1244
+ primitives: base.primitives,
1245
+ modes: base.modes,
1246
+ ...Object.keys(axes).length > 0 ? { $extensions: { [AXES_EXT]: ext } } : {}
1247
+ };
1574
1248
  }
1575
1249
 
1576
1250
  // src/hooks.ts
@@ -1581,11 +1255,23 @@ var TOKEN_HOOKS = [
1581
1255
  value: "4px",
1582
1256
  description: "Gap between a Disclosure\u2019s twisty and its label."
1583
1257
  },
1258
+ {
1259
+ name: "disclosure-fill",
1260
+ type: "color",
1261
+ value: "var(--wzl-accent-fg)",
1262
+ description: "Fill of a Disclosure\u2019s fold mark."
1263
+ },
1584
1264
  {
1585
1265
  name: "disclosure-target",
1586
1266
  type: "dimension",
1587
1267
  value: "20px",
1588
- description: "Hit-target square of a Disclosure twisty, and of a labkit LayerList row\u2019s."
1268
+ description: "Hit-target square of a Disclosure twisty, and the twisty column of a LayerList row."
1269
+ },
1270
+ {
1271
+ name: "input-surface",
1272
+ type: "color",
1273
+ value: "var(--wzl-surface-sunken)",
1274
+ description: "Surface a text Input\u2019s frame sits on. Set it on any container whose own background is the default \u2014 a sunken rail or panel \u2014 where an unset field is the same color as what is behind it."
1589
1275
  },
1590
1276
  {
1591
1277
  name: "number-field-width",
@@ -1593,6 +1279,36 @@ var TOKEN_HOOKS = [
1593
1279
  value: "9ch",
1594
1280
  description: "Width of a NumberField\u2019s input."
1595
1281
  },
1282
+ {
1283
+ name: "params-label-align",
1284
+ type: "string",
1285
+ value: "start",
1286
+ description: "Text alignment of row labels in PropertyPanel, Prefs and labkit\u2019s ControlPanel."
1287
+ },
1288
+ {
1289
+ name: "params-label-case",
1290
+ type: "string",
1291
+ value: "uppercase",
1292
+ description: "text-transform of every label and title in PropertyPanel, Prefs and labkit\u2019s ControlPanel."
1293
+ },
1294
+ {
1295
+ name: "params-label-tracking",
1296
+ type: "dimension",
1297
+ value: "var(--wzl-tracking-wide)",
1298
+ description: "Letter spacing of every label and title in PropertyPanel, Prefs and labkit\u2019s ControlPanel. Unset, rows take --wzl-tracking-wide and titles --wzl-tracking-wider."
1299
+ },
1300
+ {
1301
+ name: "params-label-width",
1302
+ type: "dimension",
1303
+ value: "auto",
1304
+ description: "Inline size of row labels in PropertyPanel, Prefs and labkit\u2019s ControlPanel."
1305
+ },
1306
+ {
1307
+ name: "prefs-rail-width",
1308
+ type: "dimension",
1309
+ value: "216px",
1310
+ description: "Width of the Prefs navigation rail."
1311
+ },
1596
1312
  {
1597
1313
  name: "prefs-column-width",
1598
1314
  type: "dimension",
@@ -1620,8 +1336,20 @@ var TOKEN_HOOKS = [
1620
1336
  {
1621
1337
  name: "property-readout-w",
1622
1338
  type: "dimension",
1623
- value: "2.8em",
1624
- description: "Minimum width of a Properties row\u2019s value readout."
1339
+ value: "calc(4.5ch + 2px)",
1340
+ description: "Width of a Properties row\u2019s value readout \u2014 four digits, so the controls beside a column of readouts end on one line. A range whose widest value needs more widens its own readout."
1341
+ },
1342
+ {
1343
+ name: "select-border",
1344
+ type: "border",
1345
+ value: "1px solid var(--wzl-border)",
1346
+ description: "Border of a Select\u2019s trigger. Unset, a boxed trigger draws --wzl-border and a bare one a transparent border of the same width."
1347
+ },
1348
+ {
1349
+ name: "select-fg",
1350
+ type: "color",
1351
+ value: "var(--wzl-fg)",
1352
+ description: "Text color of a Select\u2019s trigger."
1625
1353
  },
1626
1354
  {
1627
1355
  name: "swatch-size",
@@ -1640,6 +1368,12 @@ var TOKEN_HOOKS = [
1640
1368
  type: "dimension",
1641
1369
  value: "32px",
1642
1370
  description: "Width reserved for the Timeline\u2019s value axis."
1371
+ },
1372
+ {
1373
+ name: "tree-indent",
1374
+ type: "dimension",
1375
+ value: "calc(var(--wzl-control-h-xs) + var(--wzl-space-2))",
1376
+ description: "Indent of each Tree level. Unset, it is one twisty and one gap, so a child\u2019s twisty sits under its parent\u2019s label."
1643
1377
  }
1644
1378
  ];
1645
1379
 
@@ -1784,6 +1518,28 @@ function generateTokens(definitions) {
1784
1518
  };
1785
1519
  }
1786
1520
 
1787
- 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 };
1521
+ // src/store.ts
1522
+ var serializeDefinition = (definition) => `${JSON.stringify(definition, null, 2)}
1523
+ `;
1524
+ function httpThemeApi(base = document.baseURI, fetchImpl = (input, init) => fetch(input, init)) {
1525
+ const url = (path) => new URL(`__theme/${path}`, base).toString();
1526
+ const read = async (response) => {
1527
+ if (!response.ok && response.status !== 409 && response.status !== 400) throw new Error(`${response.status} ${response.statusText}`);
1528
+ return await response.json();
1529
+ };
1530
+ return {
1531
+ list: async () => (await read(await fetchImpl(url("list"), void 0))).themes,
1532
+ get: async (name) => read(await fetchImpl(url(name), void 0)),
1533
+ put: async (name, definition, baseHash) => read(
1534
+ await fetchImpl(url(name), {
1535
+ method: "PUT",
1536
+ headers: { "Content-Type": "application/json" },
1537
+ body: JSON.stringify({ definition, hash: baseHash })
1538
+ })
1539
+ )
1540
+ };
1541
+ }
1542
+
1543
+ export { axisDependencies, bake, bakeChain, categoricalRamp, cssValue, declaredSteps, derive, emitCss, emitManifest, emitThemes, generateTokens, httpThemeApi, lightnessRamp, mergeChain, scale, serializeDefinition, toDTCG };
1788
1544
  //# sourceMappingURL=engine.js.map
1789
1545
  //# sourceMappingURL=engine.js.map