@weasel-js/theme 1.5.1 → 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,397 +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 { 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
- }
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';
395
4
 
396
5
  // src/engine/steps.ts
397
6
  var stepList = (x) => Array.isArray(x) ? [...new Set(x.filter((s) => typeof s === "string"))] : [];
@@ -432,6 +41,17 @@ function ownSteps(def) {
432
41
  }
433
42
  return names;
434
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
+ }
435
55
  function mergeChain(def, lookup, seen = /* @__PURE__ */ new Set()) {
436
56
  if (seen.has(def.name)) throw new Error(`extends cycle at theme "${def.name}"`);
437
57
  if (!def.extends) return def;
@@ -442,8 +62,13 @@ function mergeChain(def, lookup, seen = /* @__PURE__ */ new Set()) {
442
62
  for (const layer of LAYERS) out[layer] = { ...parent[layer] ?? {}, ...def[layer] ?? {} };
443
63
  const shadowed = ownSteps(def);
444
64
  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 ?? {});
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;
447
72
  return out;
448
73
  }
449
74
 
@@ -606,14 +231,23 @@ function lightnessRamp(p) {
606
231
  const at = (i) => n === 1 ? 0 : i / (n - 1);
607
232
  const lightBias = p.lightBias ?? 0;
608
233
  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
- }
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
+ };
617
251
  const out = {};
618
252
  p.steps.forEach((step, i) => {
619
253
  const fixed = p.anchor?.[step];
@@ -623,6 +257,7 @@ function lightnessRamp(p) {
623
257
  }
624
258
  const t = at(i);
625
259
  const L = p.lightness[0] + (p.lightness[1] - p.lightness[0]) * (t + (smoothstep(t) - t) * p.curve);
260
+ const { hue, peak } = hueAndPeak(i);
626
261
  out[step] = toHex(L, Math.min(peak * envelope(t, lightBias, p.darkBias) / max, MAX_CHROMA), hue);
627
262
  });
628
263
  return out;
@@ -638,12 +273,19 @@ function categoricalRamp(steps, gates, anchors) {
638
273
 
639
274
  // src/engine/scales.ts
640
275
  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`");
276
+ const rules = [p.step, p.ratio, p.factors].filter((r) => r !== void 0);
277
+ if (rules.length !== 1) {
278
+ throw new Error("A scale takes exactly one of `step`, `ratio` and `factors`");
279
+ }
280
+ if (p.factors && p.factors.length !== steps.length) {
281
+ throw new Error(`A scale needs one factor per step; got ${p.factors.length} for ${steps.length} steps`);
643
282
  }
644
283
  const out = {};
645
284
  steps.forEach((name, i) => {
646
- const v = p.step !== void 0 ? p.base + p.step * i : p.base * p.ratio ** i;
285
+ let v;
286
+ if (p.factors !== void 0) v = p.base * p.factors[i];
287
+ else if (p.step !== void 0) v = p.base + p.step * i;
288
+ else v = p.base * p.ratio ** i;
647
289
  out[name] = `${Math.round(v)}px`;
648
290
  });
649
291
  return out;
@@ -865,7 +507,7 @@ var SEED_REF2 = /^\{seeds\.([\w-]+)\}$/;
865
507
  var REF = /^\{([^}]+)\}$/;
866
508
  var HEX2 = /^#[0-9a-f]{6}$/i;
867
509
  var UNTYPED = "unknown";
868
- var PARAMS = ["kind", "steps", "lightness", "curve", "hue", "chroma", "anchor", "gates", "anchors", "base", "step", "ratio"];
510
+ var PARAMS = ["kind", "steps", "lightness", "curve", "hue", "chroma", "anchor", "gates", "anchors", "base", "step", "ratio", "factors"];
869
511
  var has = (o, key) => Object.hasOwn(o, key);
870
512
  function refName(value) {
871
513
  return typeof value === "string" ? REF.exec(value.trim())?.[1] : void 0;
@@ -925,6 +567,14 @@ var Reader = class {
925
567
  optNum(x, path, fallback) {
926
568
  return x === void 0 ? fallback : this.num(x, path);
927
569
  }
570
+ optNums(x, path) {
571
+ if (x === void 0) return void 0;
572
+ if (!Array.isArray(x)) {
573
+ this.fail(path, "expected an array of numbers");
574
+ return void 0;
575
+ }
576
+ return x.map((v, i) => this.num(v, `${path}.${i}`));
577
+ }
928
578
  record(x, path) {
929
579
  if (x === void 0) return {};
930
580
  if (typeof x === "object" && x !== null && !Array.isArray(x)) return x;
@@ -1003,9 +653,11 @@ function scaleValues(name, s, steps, issues) {
1003
653
  const base = read.num(s.base, `${path}.base`);
1004
654
  const step = read.optNum(s.step, `${path}.step`);
1005
655
  const ratio = read.optNum(s.ratio, `${path}.ratio`);
656
+ const factors = read.optNums(s.factors, `${path}.factors`);
1006
657
  if (!read.ok) return void 0;
1007
658
  try {
1008
- return { values: scale(steps, { base, step, ratio }), rule: step !== void 0 ? "linear" : "geometric" };
659
+ const rule = factors !== void 0 ? "factors" : step !== void 0 ? "linear" : "geometric";
660
+ return { values: scale(steps, { base, step, ratio, factors }), rule };
1009
661
  } catch (e) {
1010
662
  issues.push({ kind: "invalid", path, message: e.message });
1011
663
  return void 0;
@@ -1229,6 +881,7 @@ function derive(definition, selection = {}, lookup) {
1229
881
  }
1230
882
  for (const [name, v] of Object.entries(def.pins ?? {})) {
1231
883
  const prior = has(tokens, name) ? tokens[name] : void 0;
884
+ if (prior && !pick(v, sel).ok) continue;
1232
885
  const token = toToken(v, `pins.${name}`, prior);
1233
886
  if (!token) {
1234
887
  if (!prior) failed.add(name);
@@ -1321,7 +974,15 @@ function bakeOver(definition, merged, above, lookup) {
1321
974
  const value = build(0, {});
1322
975
  if (value !== void 0) tokens[name] = value;
1323
976
  }
1324
- 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
+ };
1325
986
  }
1326
987
 
1327
988
  // src/engine/emit/css.ts
@@ -1511,9 +1172,11 @@ function emitCss(themes) {
1511
1172
 
1512
1173
  // src/engine/emit/dtcg.ts
1513
1174
  var REF3 = /^\{([^}.]+)\}$/;
1514
- var modeOnly = (theme, what) => new Error(`DTCG export supports a mode axis only; ${theme} ${what}`);
1515
1175
  function toDTCG(theme) {
1516
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 ?? {});
1517
1180
  const typeOf = (name) => {
1518
1181
  for (let t = theme; t; t = t.extends) {
1519
1182
  if (!Object.hasOwn(t.tokens, name)) continue;
@@ -1536,23 +1199,52 @@ function toDTCG(theme) {
1536
1199
  ...t.alpha !== void 0 ? { $extensions: { [ALPHA_EXT]: t.alpha } } : {}
1537
1200
  };
1538
1201
  };
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
- });
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);
1209
+ }
1547
1210
  };
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);
1211
+ const base = { primitives: {}, modes: {} };
1212
+ for (const mode of declaredModes) base.modes[mode] = {};
1213
+ const overrides = {};
1214
+ const varies = {};
1215
+ for (const [name, raw] of Object.entries(theme.tokens)) {
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
+ }
1554
1239
  }
1555
- 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
+ };
1556
1248
  }
1557
1249
 
1558
1250
  // src/hooks.ts
@@ -1563,11 +1255,23 @@ var TOKEN_HOOKS = [
1563
1255
  value: "4px",
1564
1256
  description: "Gap between a Disclosure\u2019s twisty and its label."
1565
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
+ },
1566
1264
  {
1567
1265
  name: "disclosure-target",
1568
1266
  type: "dimension",
1569
1267
  value: "20px",
1570
- 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."
1571
1275
  },
1572
1276
  {
1573
1277
  name: "number-field-width",
@@ -1575,6 +1279,36 @@ var TOKEN_HOOKS = [
1575
1279
  value: "9ch",
1576
1280
  description: "Width of a NumberField\u2019s input."
1577
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
+ },
1578
1312
  {
1579
1313
  name: "prefs-column-width",
1580
1314
  type: "dimension",
@@ -1587,6 +1321,12 @@ var TOKEN_HOOKS = [
1587
1321
  value: "9ch",
1588
1322
  description: "Width of a numeric field in the Properties panel."
1589
1323
  },
1324
+ {
1325
+ name: "prop-row-h",
1326
+ type: "dimension",
1327
+ value: "20px",
1328
+ description: "Floor under every Properties row, whatever control it holds. Unset, it is the panel\u2019s own field height, so it follows density \u2014 20px at the default one."
1329
+ },
1590
1330
  {
1591
1331
  name: "prop-text-width",
1592
1332
  type: "dimension",
@@ -1596,8 +1336,20 @@ var TOKEN_HOOKS = [
1596
1336
  {
1597
1337
  name: "property-readout-w",
1598
1338
  type: "dimension",
1599
- value: "2.8em",
1600
- 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."
1601
1353
  },
1602
1354
  {
1603
1355
  name: "swatch-size",
@@ -1616,11 +1368,21 @@ var TOKEN_HOOKS = [
1616
1368
  type: "dimension",
1617
1369
  value: "32px",
1618
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."
1619
1377
  }
1620
1378
  ];
1621
1379
 
1622
1380
  // src/engine/emit/manifest.ts
1623
- var groupOf = (name) => name.includes("-") ? name.slice(0, name.indexOf("-")) : name;
1381
+ var MERGED_GROUPS = { danger: "status", warning: "status", success: "status" };
1382
+ var groupOf = (name) => {
1383
+ const prefix = name.includes("-") ? name.slice(0, name.indexOf("-")) : name;
1384
+ return MERGED_GROUPS[prefix] ?? prefix;
1385
+ };
1624
1386
  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
1387
  function emitManifest({ baked }) {
1626
1388
  const all = pickAll(baked.tokens, fullSelection(baked.axes, {}));
@@ -1756,6 +1518,28 @@ function generateTokens(definitions) {
1756
1518
  };
1757
1519
  }
1758
1520
 
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 };
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 };
1760
1544
  //# sourceMappingURL=engine.js.map
1761
1545
  //# sourceMappingURL=engine.js.map