@tenphi/glaze 0.1.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/index.cjs ADDED
@@ -0,0 +1,1196 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+
3
+ //#region src/okhsl-color-math.ts
4
+ const OKLab_to_LMS_M = [
5
+ [
6
+ 1,
7
+ .3963377773761749,
8
+ .2158037573099136
9
+ ],
10
+ [
11
+ 1,
12
+ -.1055613458156586,
13
+ -.0638541728258133
14
+ ],
15
+ [
16
+ 1,
17
+ -.0894841775298119,
18
+ -1.2914855480194092
19
+ ]
20
+ ];
21
+ const LMS_to_linear_sRGB_M = [
22
+ [
23
+ 4.076741636075959,
24
+ -3.307711539258062,
25
+ .2309699031821041
26
+ ],
27
+ [
28
+ -1.2684379732850313,
29
+ 2.6097573492876878,
30
+ -.3413193760026569
31
+ ],
32
+ [
33
+ -.004196076138675526,
34
+ -.703418617935936,
35
+ 1.7076146940746113
36
+ ]
37
+ ];
38
+ const linear_sRGB_to_LMS_M = [
39
+ [
40
+ .4122214708,
41
+ .5363325363,
42
+ .0514459929
43
+ ],
44
+ [
45
+ .2119034982,
46
+ .6806995451,
47
+ .1073969566
48
+ ],
49
+ [
50
+ .0883024619,
51
+ .2817188376,
52
+ .6299787005
53
+ ]
54
+ ];
55
+ const LMS_to_OKLab_M = [
56
+ [
57
+ .2104542553,
58
+ .793617785,
59
+ -.0040720468
60
+ ],
61
+ [
62
+ 1.9779984951,
63
+ -2.428592205,
64
+ .4505937099
65
+ ],
66
+ [
67
+ .0259040371,
68
+ .7827717662,
69
+ -.808675766
70
+ ]
71
+ ];
72
+ const OKLab_to_linear_sRGB_coefficients = [
73
+ [[-1.8817030993265873, -.8093650129914302], [
74
+ 1.19086277,
75
+ 1.76576728,
76
+ .59662641,
77
+ .75515197,
78
+ .56771245
79
+ ]],
80
+ [[1.8144407988010998, -1.194452667805235], [
81
+ .73956515,
82
+ -.45954404,
83
+ .08285427,
84
+ .12541073,
85
+ -.14503204
86
+ ]],
87
+ [[.13110757611180954, 1.813339709266608], [
88
+ 1.35733652,
89
+ -.00915799,
90
+ -1.1513021,
91
+ -.50559606,
92
+ .00692167
93
+ ]]
94
+ ];
95
+ const TAU = 2 * Math.PI;
96
+ const K1 = .206;
97
+ const K2 = .03;
98
+ const K3 = (1 + K1) / (1 + K2);
99
+ const EPSILON = 1e-10;
100
+ const constrainAngle = (angle) => (angle % 360 + 360) % 360;
101
+ const toe = (x) => .5 * (K3 * x - K1 + Math.sqrt((K3 * x - K1) * (K3 * x - K1) + 4 * K2 * K3 * x));
102
+ const toeInv = (x) => (x ** 2 + K1 * x) / (K3 * (x + K2));
103
+ const dot3 = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
104
+ const dotXY = (a, b) => a[0] * b[0] + a[1] * b[1];
105
+ const transform = (input, matrix) => [
106
+ dot3(input, matrix[0]),
107
+ dot3(input, matrix[1]),
108
+ dot3(input, matrix[2])
109
+ ];
110
+ const cubed3 = (lms) => [
111
+ lms[0] ** 3,
112
+ lms[1] ** 3,
113
+ lms[2] ** 3
114
+ ];
115
+ const cbrt3 = (lms) => [
116
+ Math.cbrt(lms[0]),
117
+ Math.cbrt(lms[1]),
118
+ Math.cbrt(lms[2])
119
+ ];
120
+ const clampVal = (v, min, max) => Math.max(min, Math.min(max, v));
121
+ const OKLabToLinearSRGB = (lab) => {
122
+ return transform(cubed3(transform(lab, OKLab_to_LMS_M)), LMS_to_linear_sRGB_M);
123
+ };
124
+ const computeMaxSaturationOKLC = (a, b) => {
125
+ const okCoeff = OKLab_to_linear_sRGB_coefficients;
126
+ const lmsToRgb = LMS_to_linear_sRGB_M;
127
+ const tmp2 = [a, b];
128
+ const tmp3 = [
129
+ 0,
130
+ a,
131
+ b
132
+ ];
133
+ let chnlCoeff;
134
+ let chnlLMS;
135
+ if (dotXY(okCoeff[0][0], tmp2) > 1) {
136
+ chnlCoeff = okCoeff[0][1];
137
+ chnlLMS = lmsToRgb[0];
138
+ } else if (dotXY(okCoeff[1][0], tmp2) > 1) {
139
+ chnlCoeff = okCoeff[1][1];
140
+ chnlLMS = lmsToRgb[1];
141
+ } else {
142
+ chnlCoeff = okCoeff[2][1];
143
+ chnlLMS = lmsToRgb[2];
144
+ }
145
+ const [k0, k1, k2, k3, k4] = chnlCoeff;
146
+ const [wl, wm, ws] = chnlLMS;
147
+ let sat = k0 + k1 * a + k2 * b + k3 * (a * a) + k4 * a * b;
148
+ const dotYZ = (mat, vec) => mat[1] * vec[1] + mat[2] * vec[2];
149
+ const kl = dotYZ(OKLab_to_LMS_M[0], tmp3);
150
+ const km = dotYZ(OKLab_to_LMS_M[1], tmp3);
151
+ const ks = dotYZ(OKLab_to_LMS_M[2], tmp3);
152
+ const l_ = 1 + sat * kl;
153
+ const m_ = 1 + sat * km;
154
+ const s_ = 1 + sat * ks;
155
+ const l = l_ ** 3;
156
+ const m = m_ ** 3;
157
+ const s = s_ ** 3;
158
+ const lds = 3 * kl * l_ * l_;
159
+ const mds = 3 * km * m_ * m_;
160
+ const sds = 3 * ks * s_ * s_;
161
+ const lds2 = 6 * kl * kl * l_;
162
+ const mds2 = 6 * km * km * m_;
163
+ const sds2 = 6 * ks * ks * s_;
164
+ const f = wl * l + wm * m + ws * s;
165
+ const f1 = wl * lds + wm * mds + ws * sds;
166
+ const f2 = wl * lds2 + wm * mds2 + ws * sds2;
167
+ sat = sat - f * f1 / (f1 * f1 - .5 * f * f2);
168
+ return sat;
169
+ };
170
+ const findCuspOKLCH = (a, b) => {
171
+ const S_cusp = computeMaxSaturationOKLC(a, b);
172
+ const rgb_at_max = OKLabToLinearSRGB([
173
+ 1,
174
+ S_cusp * a,
175
+ S_cusp * b
176
+ ]);
177
+ const L_cusp = Math.cbrt(1 / Math.max(Math.max(rgb_at_max[0], rgb_at_max[1]), Math.max(rgb_at_max[2], 0)));
178
+ return [L_cusp, L_cusp * S_cusp];
179
+ };
180
+ const findGamutIntersectionOKLCH = (a, b, l1, c1, l0, cusp) => {
181
+ const lmsToRgb = LMS_to_linear_sRGB_M;
182
+ const tmp3 = [
183
+ 0,
184
+ a,
185
+ b
186
+ ];
187
+ const floatMax = Number.MAX_VALUE;
188
+ let t;
189
+ const dotYZ = (mat, vec) => mat[1] * vec[1] + mat[2] * vec[2];
190
+ const dotXYZ = (vec, x, y, z) => vec[0] * x + vec[1] * y + vec[2] * z;
191
+ if ((l1 - l0) * cusp[1] - (cusp[0] - l0) * c1 <= 0) {
192
+ const denom = c1 * cusp[0] + cusp[1] * (l0 - l1);
193
+ t = denom === 0 ? 0 : cusp[1] * l0 / denom;
194
+ } else {
195
+ const denom = c1 * (cusp[0] - 1) + cusp[1] * (l0 - l1);
196
+ t = denom === 0 ? 0 : cusp[1] * (l0 - 1) / denom;
197
+ const dl = l1 - l0;
198
+ const dc = c1;
199
+ const kl = dotYZ(OKLab_to_LMS_M[0], tmp3);
200
+ const km = dotYZ(OKLab_to_LMS_M[1], tmp3);
201
+ const ks = dotYZ(OKLab_to_LMS_M[2], tmp3);
202
+ const L = l0 * (1 - t) + t * l1;
203
+ const C = t * c1;
204
+ const l_ = L + C * kl;
205
+ const m_ = L + C * km;
206
+ const s_ = L + C * ks;
207
+ const l = l_ ** 3;
208
+ const m = m_ ** 3;
209
+ const s = s_ ** 3;
210
+ const ldt = 3 * (dl + dc * kl) * l_ * l_;
211
+ const mdt = 3 * (dl + dc * km) * m_ * m_;
212
+ const sdt = 3 * (dl + dc * ks) * s_ * s_;
213
+ const ldt2 = 6 * (dl + dc * kl) ** 2 * l_;
214
+ const mdt2 = 6 * (dl + dc * km) ** 2 * m_;
215
+ const sdt2 = 6 * (dl + dc * ks) ** 2 * s_;
216
+ const r_ = dotXYZ(lmsToRgb[0], l, m, s) - 1;
217
+ const r1 = dotXYZ(lmsToRgb[0], ldt, mdt, sdt);
218
+ const r2 = dotXYZ(lmsToRgb[0], ldt2, mdt2, sdt2);
219
+ const ur = r1 / (r1 * r1 - .5 * r_ * r2);
220
+ let tr = -r_ * ur;
221
+ const g_ = dotXYZ(lmsToRgb[1], l, m, s) - 1;
222
+ const g1 = dotXYZ(lmsToRgb[1], ldt, mdt, sdt);
223
+ const g2 = dotXYZ(lmsToRgb[1], ldt2, mdt2, sdt2);
224
+ const ug = g1 / (g1 * g1 - .5 * g_ * g2);
225
+ let tg = -g_ * ug;
226
+ const b_ = dotXYZ(lmsToRgb[2], l, m, s) - 1;
227
+ const b1 = dotXYZ(lmsToRgb[2], ldt, mdt, sdt);
228
+ const b2 = dotXYZ(lmsToRgb[2], ldt2, mdt2, sdt2);
229
+ const ub = b1 / (b1 * b1 - .5 * b_ * b2);
230
+ let tb = -b_ * ub;
231
+ tr = ur >= 0 ? tr : floatMax;
232
+ tg = ug >= 0 ? tg : floatMax;
233
+ tb = ub >= 0 ? tb : floatMax;
234
+ t += Math.min(tr, Math.min(tg, tb));
235
+ }
236
+ return t;
237
+ };
238
+ const computeSt = (cusp) => [cusp[1] / cusp[0], cusp[1] / (1 - cusp[0])];
239
+ const computeStMid = (a, b) => [.11516993 + 1 / (7.4477897 + 4.1590124 * b + a * (-2.19557347 + 1.75198401 * b + a * (-2.13704948 - 10.02301043 * b + a * (-4.24894561 + 5.38770819 * b + 4.69891013 * a)))), .11239642 + 1 / (1.6132032 - .68124379 * b + a * (.40370612 + .90148123 * b + a * (-.27087943 + .6122399 * b + a * (.00299215 - .45399568 * b - .14661872 * a))))];
240
+ const getCs = (L, a, b, cusp) => {
241
+ const cMax = findGamutIntersectionOKLCH(a, b, L, 1, L, cusp);
242
+ const stMax = computeSt(cusp);
243
+ const k = cMax / Math.min(L * stMax[0], (1 - L) * stMax[1]);
244
+ const stMid = computeStMid(a, b);
245
+ let ca = L * stMid[0];
246
+ let cb = (1 - L) * stMid[1];
247
+ const cMid = .9 * k * Math.sqrt(Math.sqrt(1 / (1 / ca ** 4 + 1 / cb ** 4)));
248
+ ca = L * .4;
249
+ cb = (1 - L) * .8;
250
+ return [
251
+ Math.sqrt(1 / (1 / ca ** 2 + 1 / cb ** 2)),
252
+ cMid,
253
+ cMax
254
+ ];
255
+ };
256
+ /**
257
+ * Convert OKHSL (h: 0–360, s: 0–1, l: 0–1) to linear sRGB.
258
+ * Channels may exceed [0, 1] near gamut boundaries — caller must clamp if needed.
259
+ */
260
+ function okhslToLinearSrgb(h, s, l) {
261
+ const L = toeInv(l);
262
+ let a = 0;
263
+ let b = 0;
264
+ const hNorm = constrainAngle(h) / 360;
265
+ if (L !== 0 && L !== 1 && s !== 0) {
266
+ const a_ = Math.cos(TAU * hNorm);
267
+ const b_ = Math.sin(TAU * hNorm);
268
+ const [c0, cMid, cMax] = getCs(L, a_, b_, findCuspOKLCH(a_, b_));
269
+ const mid = .8;
270
+ const midInv = 1.25;
271
+ let t, k0, k1, k2;
272
+ if (s < mid) {
273
+ t = midInv * s;
274
+ k0 = 0;
275
+ k1 = mid * c0;
276
+ k2 = 1 - k1 / cMid;
277
+ } else {
278
+ t = 5 * (s - .8);
279
+ k0 = cMid;
280
+ k1 = .2 * cMid ** 2 * 1.25 ** 2 / c0;
281
+ k2 = 1 - k1 / (cMax - cMid);
282
+ }
283
+ const c = k0 + t * k1 / (1 - k2 * t);
284
+ a = c * a_;
285
+ b = c * b_;
286
+ }
287
+ return OKLabToLinearSRGB([
288
+ L,
289
+ a,
290
+ b
291
+ ]);
292
+ }
293
+ /**
294
+ * Compute relative luminance Y from linear sRGB channels.
295
+ * Per WCAG 2: Y = 0.2126·R + 0.7152·G + 0.0722·B
296
+ */
297
+ function relativeLuminanceFromLinearRgb(rgb) {
298
+ return .2126 * rgb[0] + .7152 * rgb[1] + .0722 * rgb[2];
299
+ }
300
+ /**
301
+ * WCAG 2 contrast ratio from two luminance values.
302
+ */
303
+ function contrastRatioFromLuminance(yA, yB) {
304
+ const lighter = Math.max(yA, yB);
305
+ const darker = Math.min(yA, yB);
306
+ return (lighter + .05) / (darker + .05);
307
+ }
308
+ const sRGBLinearToGamma = (val) => {
309
+ const sign = val < 0 ? -1 : 1;
310
+ const abs = Math.abs(val);
311
+ return abs > .0031308 ? sign * (1.055 * Math.pow(abs, 1 / 2.4) - .055) : 12.92 * val;
312
+ };
313
+ const sRGBGammaToLinear = (val) => {
314
+ const sign = val < 0 ? -1 : 1;
315
+ const abs = Math.abs(val);
316
+ return abs <= .04045 ? val / 12.92 : sign * Math.pow((abs + .055) / 1.055, 2.4);
317
+ };
318
+ /**
319
+ * Convert OKHSL to gamma-encoded sRGB (clamped to 0–1).
320
+ */
321
+ function okhslToSrgb(h, s, l) {
322
+ const lin = okhslToLinearSrgb(h, s, l);
323
+ return [
324
+ Math.max(0, Math.min(1, sRGBLinearToGamma(lin[0]))),
325
+ Math.max(0, Math.min(1, sRGBLinearToGamma(lin[1]))),
326
+ Math.max(0, Math.min(1, sRGBLinearToGamma(lin[2])))
327
+ ];
328
+ }
329
+ /**
330
+ * Convert OKHSL (h: 0–360, s: 0–1, l: 0–1) to OKLab [L, a, b].
331
+ */
332
+ function okhslToOklab(h, s, l) {
333
+ const L = toeInv(l);
334
+ let a = 0;
335
+ let b = 0;
336
+ const hNorm = constrainAngle(h) / 360;
337
+ if (L !== 0 && L !== 1 && s !== 0) {
338
+ const a_ = Math.cos(TAU * hNorm);
339
+ const b_ = Math.sin(TAU * hNorm);
340
+ const [c0, cMid, cMax] = getCs(L, a_, b_, findCuspOKLCH(a_, b_));
341
+ const mid = .8;
342
+ const midInv = 1.25;
343
+ let t, k0, k1, k2;
344
+ if (s < mid) {
345
+ t = midInv * s;
346
+ k0 = 0;
347
+ k1 = mid * c0;
348
+ k2 = 1 - k1 / cMid;
349
+ } else {
350
+ t = 5 * (s - .8);
351
+ k0 = cMid;
352
+ k1 = .2 * cMid ** 2 * 1.25 ** 2 / c0;
353
+ k2 = 1 - k1 / (cMax - cMid);
354
+ }
355
+ const c = k0 + t * k1 / (1 - k2 * t);
356
+ a = c * a_;
357
+ b = c * b_;
358
+ }
359
+ return [
360
+ L,
361
+ a,
362
+ b
363
+ ];
364
+ }
365
+ const linearSrgbToOklab = (rgb) => {
366
+ return transform(cbrt3(transform(rgb, linear_sRGB_to_LMS_M)), LMS_to_OKLab_M);
367
+ };
368
+ const oklabToOkhsl = (lab) => {
369
+ const L = lab[0];
370
+ const a = lab[1];
371
+ const b = lab[2];
372
+ const C = Math.sqrt(a * a + b * b);
373
+ if (C < EPSILON) return [
374
+ 0,
375
+ 0,
376
+ toe(L)
377
+ ];
378
+ const a_ = a / C;
379
+ const b_ = b / C;
380
+ let h = Math.atan2(b, a) * (180 / Math.PI);
381
+ h = constrainAngle(h);
382
+ const [c0, cMid, cMax] = getCs(L, a_, b_, findCuspOKLCH(a_, b_));
383
+ const mid = .8;
384
+ const midInv = 1.25;
385
+ let s;
386
+ if (C < cMid) {
387
+ const k1 = mid * c0;
388
+ s = C / (k1 + C * (1 - k1 / cMid)) / midInv;
389
+ } else {
390
+ const k0 = cMid;
391
+ const k1 = .2 * cMid ** 2 * 1.25 ** 2 / c0;
392
+ const k2 = 1 - k1 / (cMax - cMid);
393
+ const cDiff = C - k0;
394
+ s = mid + cDiff / (k1 + cDiff * k2) / 5;
395
+ }
396
+ const l = toe(L);
397
+ return [
398
+ h,
399
+ clampVal(s, 0, 1),
400
+ clampVal(l, 0, 1)
401
+ ];
402
+ };
403
+ /**
404
+ * Convert gamma-encoded sRGB (0–1 per channel) to OKHSL.
405
+ * Returns [h, s, l] where h: 0–360, s: 0–1, l: 0–1.
406
+ */
407
+ function srgbToOkhsl(rgb) {
408
+ return oklabToOkhsl(linearSrgbToOklab([
409
+ sRGBGammaToLinear(rgb[0]),
410
+ sRGBGammaToLinear(rgb[1]),
411
+ sRGBGammaToLinear(rgb[2])
412
+ ]));
413
+ }
414
+ /**
415
+ * Parse a hex color string (#rgb or #rrggbb) to sRGB [r, g, b] in 0–1 range.
416
+ * Returns null if the string is not a valid hex color.
417
+ */
418
+ function parseHex(hex) {
419
+ const h = hex.startsWith("#") ? hex.slice(1) : hex;
420
+ if (h.length === 3) {
421
+ const r = parseInt(h[0] + h[0], 16);
422
+ const g = parseInt(h[1] + h[1], 16);
423
+ const b = parseInt(h[2] + h[2], 16);
424
+ if (isNaN(r) || isNaN(g) || isNaN(b)) return null;
425
+ return [
426
+ r / 255,
427
+ g / 255,
428
+ b / 255
429
+ ];
430
+ }
431
+ if (h.length === 6) {
432
+ const r = parseInt(h.slice(0, 2), 16);
433
+ const g = parseInt(h.slice(2, 4), 16);
434
+ const b = parseInt(h.slice(4, 6), 16);
435
+ if (isNaN(r) || isNaN(g) || isNaN(b)) return null;
436
+ return [
437
+ r / 255,
438
+ g / 255,
439
+ b / 255
440
+ ];
441
+ }
442
+ return null;
443
+ }
444
+ /**
445
+ * Format OKHSL values as a CSS `okhsl(H S% L%)` string.
446
+ * h: 0–360, s: 0–100, l: 0–100 (percentage scale for s and l).
447
+ */
448
+ function formatOkhsl(h, s, l) {
449
+ return `okhsl(${h.toFixed(1)} ${s.toFixed(1)}% ${l.toFixed(1)}%)`;
450
+ }
451
+ /**
452
+ * Format OKHSL values as a CSS `rgb(R, G, B)` string with fractional 0–255 values.
453
+ * h: 0–360, s: 0–100, l: 0–100 (percentage scale for s and l).
454
+ */
455
+ function formatRgb(h, s, l) {
456
+ const [r, g, b] = okhslToSrgb(h, s / 100, l / 100);
457
+ return `rgb(${(r * 255).toFixed(3)}, ${(g * 255).toFixed(3)}, ${(b * 255).toFixed(3)})`;
458
+ }
459
+ /**
460
+ * Format OKHSL values as a CSS `hsl(H, S%, L%)` string.
461
+ * h: 0–360, s: 0–100, l: 0–100 (percentage scale for s and l).
462
+ */
463
+ function formatHsl(h, s, l) {
464
+ const [r, g, b] = okhslToSrgb(h, s / 100, l / 100);
465
+ const max = Math.max(r, g, b);
466
+ const min = Math.min(r, g, b);
467
+ const delta = max - min;
468
+ let hh = 0;
469
+ let ss = 0;
470
+ const ll = (max + min) / 2;
471
+ if (delta > 0) {
472
+ ss = ll > .5 ? delta / (2 - max - min) : delta / (max + min);
473
+ if (max === r) hh = ((g - b) / delta + (g < b ? 6 : 0)) * 60;
474
+ else if (max === g) hh = ((b - r) / delta + 2) * 60;
475
+ else hh = ((r - g) / delta + 4) * 60;
476
+ }
477
+ return `hsl(${hh.toFixed(1)}, ${(ss * 100).toFixed(1)}%, ${(ll * 100).toFixed(1)}%)`;
478
+ }
479
+ /**
480
+ * Format OKHSL values as a CSS `oklch(L% C H)` string.
481
+ * h: 0–360, s: 0–100, l: 0–100 (percentage scale for s and l).
482
+ */
483
+ function formatOklch(h, s, l) {
484
+ const [L, a, b] = okhslToOklab(h, s / 100, l / 100);
485
+ const C = Math.sqrt(a * a + b * b);
486
+ let hh = Math.atan2(b, a) * (180 / Math.PI);
487
+ hh = constrainAngle(hh);
488
+ return `oklch(${(L * 100).toFixed(1)}% ${C.toFixed(4)} ${hh.toFixed(1)})`;
489
+ }
490
+
491
+ //#endregion
492
+ //#region src/contrast-solver.ts
493
+ /**
494
+ * OKHSL Contrast Solver
495
+ *
496
+ * Finds the closest OKHSL lightness that satisfies a WCAG 2 contrast target
497
+ * against a base color. Used by glaze when resolving dependent colors
498
+ * with `ensureContrast`.
499
+ */
500
+ const CONTRAST_PRESETS = {
501
+ AA: 4.5,
502
+ AAA: 7,
503
+ "AA-large": 3,
504
+ "AAA-large": 4.5
505
+ };
506
+ function resolveMinContrast(value) {
507
+ if (typeof value === "number") return Math.max(1, value);
508
+ return CONTRAST_PRESETS[value];
509
+ }
510
+ const CACHE_SIZE = 512;
511
+ const luminanceCache = /* @__PURE__ */ new Map();
512
+ const cacheOrder = [];
513
+ function cachedLuminance(h, s, l) {
514
+ const lRounded = Math.round(l * 1e4) / 1e4;
515
+ const key = `${h}|${s}|${lRounded}`;
516
+ const cached = luminanceCache.get(key);
517
+ if (cached !== void 0) return cached;
518
+ const y = relativeLuminanceFromLinearRgb(okhslToLinearSrgb(h, s, lRounded));
519
+ if (luminanceCache.size >= CACHE_SIZE) {
520
+ const evict = cacheOrder.shift();
521
+ luminanceCache.delete(evict);
522
+ }
523
+ luminanceCache.set(key, y);
524
+ cacheOrder.push(key);
525
+ return y;
526
+ }
527
+ /**
528
+ * Binary search one branch [lo, hi] for the nearest passing lightness to `preferred`.
529
+ */
530
+ function searchBranch(h, s, lo, hi, yBase, target, epsilon, maxIter, preferred) {
531
+ const yLo = cachedLuminance(h, s, lo);
532
+ const yHi = cachedLuminance(h, s, hi);
533
+ const crLo = contrastRatioFromLuminance(yLo, yBase);
534
+ const crHi = contrastRatioFromLuminance(yHi, yBase);
535
+ if (crLo < target && crHi < target) {
536
+ if (crLo >= crHi) return {
537
+ lightness: lo,
538
+ contrast: crLo,
539
+ met: false
540
+ };
541
+ return {
542
+ lightness: hi,
543
+ contrast: crHi,
544
+ met: false
545
+ };
546
+ }
547
+ let low = lo;
548
+ let high = hi;
549
+ for (let i = 0; i < maxIter; i++) {
550
+ if (high - low < epsilon) break;
551
+ const mid = (low + high) / 2;
552
+ if (contrastRatioFromLuminance(cachedLuminance(h, s, mid), yBase) >= target) if (mid < preferred) low = mid;
553
+ else high = mid;
554
+ else if (mid < preferred) high = mid;
555
+ else low = mid;
556
+ }
557
+ const yLow = cachedLuminance(h, s, low);
558
+ const yHigh = cachedLuminance(h, s, high);
559
+ const crLow = contrastRatioFromLuminance(yLow, yBase);
560
+ const crHigh = contrastRatioFromLuminance(yHigh, yBase);
561
+ const lowPasses = crLow >= target;
562
+ const highPasses = crHigh >= target;
563
+ if (lowPasses && highPasses) {
564
+ if (Math.abs(low - preferred) <= Math.abs(high - preferred)) return {
565
+ lightness: low,
566
+ contrast: crLow,
567
+ met: true
568
+ };
569
+ return {
570
+ lightness: high,
571
+ contrast: crHigh,
572
+ met: true
573
+ };
574
+ }
575
+ if (lowPasses) return {
576
+ lightness: low,
577
+ contrast: crLow,
578
+ met: true
579
+ };
580
+ if (highPasses) return {
581
+ lightness: high,
582
+ contrast: crHigh,
583
+ met: true
584
+ };
585
+ return coarseScan(h, s, lo, hi, yBase, target, epsilon, maxIter);
586
+ }
587
+ /**
588
+ * Fallback coarse scan when binary search is unstable near gamut edges.
589
+ */
590
+ function coarseScan(h, s, lo, hi, yBase, target, epsilon, maxIter) {
591
+ const STEPS = 64;
592
+ const step = (hi - lo) / STEPS;
593
+ let bestL = lo;
594
+ let bestCr = 0;
595
+ let bestMet = false;
596
+ for (let i = 0; i <= STEPS; i++) {
597
+ const l = lo + step * i;
598
+ const cr = contrastRatioFromLuminance(cachedLuminance(h, s, l), yBase);
599
+ if (cr >= target && !bestMet) {
600
+ bestL = l;
601
+ bestCr = cr;
602
+ bestMet = true;
603
+ } else if (cr >= target && bestMet) {
604
+ bestL = l;
605
+ bestCr = cr;
606
+ } else if (!bestMet && cr > bestCr) {
607
+ bestL = l;
608
+ bestCr = cr;
609
+ }
610
+ }
611
+ if (bestMet && bestL > lo + step) {
612
+ let rLo = bestL - step;
613
+ let rHi = bestL;
614
+ for (let i = 0; i < maxIter; i++) {
615
+ if (rHi - rLo < epsilon) break;
616
+ const mid = (rLo + rHi) / 2;
617
+ const cr = contrastRatioFromLuminance(cachedLuminance(h, s, mid), yBase);
618
+ if (cr >= target) {
619
+ rHi = mid;
620
+ bestL = mid;
621
+ bestCr = cr;
622
+ } else rLo = mid;
623
+ }
624
+ }
625
+ return {
626
+ lightness: bestL,
627
+ contrast: bestCr,
628
+ met: bestMet
629
+ };
630
+ }
631
+ /**
632
+ * Find the OKHSL lightness that satisfies a WCAG 2 contrast target
633
+ * against a base color, staying as close to `preferredLightness` as possible.
634
+ */
635
+ function findLightnessForContrast(options) {
636
+ const { hue, saturation, preferredLightness, baseLinearRgb, ensureContrast: ensureContrastInput, lightnessRange = [0, 1], epsilon = 1e-4, maxIterations = 14 } = options;
637
+ const target = resolveMinContrast(ensureContrastInput);
638
+ const yBase = relativeLuminanceFromLinearRgb(baseLinearRgb);
639
+ const crPref = contrastRatioFromLuminance(cachedLuminance(hue, saturation, preferredLightness), yBase);
640
+ if (crPref >= target) return {
641
+ lightness: preferredLightness,
642
+ contrast: crPref,
643
+ met: true,
644
+ branch: "preferred"
645
+ };
646
+ const [minL, maxL] = lightnessRange;
647
+ const darkerResult = preferredLightness > minL ? searchBranch(hue, saturation, minL, preferredLightness, yBase, target, epsilon, maxIterations, preferredLightness) : null;
648
+ const lighterResult = preferredLightness < maxL ? searchBranch(hue, saturation, preferredLightness, maxL, yBase, target, epsilon, maxIterations, preferredLightness) : null;
649
+ const darkerPasses = darkerResult?.met ?? false;
650
+ const lighterPasses = lighterResult?.met ?? false;
651
+ if (darkerPasses && lighterPasses) {
652
+ if (Math.abs(darkerResult.lightness - preferredLightness) <= Math.abs(lighterResult.lightness - preferredLightness)) return {
653
+ ...darkerResult,
654
+ branch: "darker"
655
+ };
656
+ return {
657
+ ...lighterResult,
658
+ branch: "lighter"
659
+ };
660
+ }
661
+ if (darkerPasses) return {
662
+ ...darkerResult,
663
+ branch: "darker"
664
+ };
665
+ if (lighterPasses) return {
666
+ ...lighterResult,
667
+ branch: "lighter"
668
+ };
669
+ const candidates = [];
670
+ if (darkerResult) candidates.push({
671
+ ...darkerResult,
672
+ branch: "darker"
673
+ });
674
+ if (lighterResult) candidates.push({
675
+ ...lighterResult,
676
+ branch: "lighter"
677
+ });
678
+ if (candidates.length === 0) return {
679
+ lightness: preferredLightness,
680
+ contrast: crPref,
681
+ met: false,
682
+ branch: "preferred"
683
+ };
684
+ candidates.sort((a, b) => b.contrast - a.contrast);
685
+ return candidates[0];
686
+ }
687
+
688
+ //#endregion
689
+ //#region src/glaze.ts
690
+ /**
691
+ * Glaze — OKHSL-based color theme generator.
692
+ *
693
+ * Generates robust light, dark, and high-contrast colors from a hue/saturation
694
+ * seed, preserving contrast for UI pairs via explicit dependencies.
695
+ */
696
+ let globalConfig = {
697
+ darkLightness: [10, 90],
698
+ darkDesaturation: .1,
699
+ states: {
700
+ dark: "@dark",
701
+ highContrast: "@high-contrast"
702
+ },
703
+ modes: {
704
+ dark: true,
705
+ highContrast: false
706
+ }
707
+ };
708
+ function pairNormal(p) {
709
+ return Array.isArray(p) ? p[0] : p;
710
+ }
711
+ function pairHC(p) {
712
+ return Array.isArray(p) ? p[1] : p;
713
+ }
714
+ function validateColorDefs(defs) {
715
+ const names = new Set(Object.keys(defs));
716
+ for (const [name, def] of Object.entries(defs)) {
717
+ if (def.contrast !== void 0 && !def.base) throw new Error(`glaze: color "${name}" has "contrast" without "base".`);
718
+ if (def.l !== void 0 && def.base !== void 0) console.warn(`glaze: color "${name}" has both "l" and "base". "l" takes precedence.`);
719
+ if (def.base && !names.has(def.base)) throw new Error(`glaze: color "${name}" references non-existent base "${def.base}".`);
720
+ if (def.l === void 0 && def.base === void 0) throw new Error(`glaze: color "${name}" must have either "l" (root) or "base" + "contrast" (dependent).`);
721
+ }
722
+ const visited = /* @__PURE__ */ new Set();
723
+ const inStack = /* @__PURE__ */ new Set();
724
+ function dfs(name) {
725
+ if (inStack.has(name)) throw new Error(`glaze: circular base reference detected involving "${name}".`);
726
+ if (visited.has(name)) return;
727
+ inStack.add(name);
728
+ const def = defs[name];
729
+ if (def.base && def.l === void 0) dfs(def.base);
730
+ inStack.delete(name);
731
+ visited.add(name);
732
+ }
733
+ for (const name of names) dfs(name);
734
+ }
735
+ function topoSort(defs) {
736
+ const result = [];
737
+ const visited = /* @__PURE__ */ new Set();
738
+ function visit(name) {
739
+ if (visited.has(name)) return;
740
+ visited.add(name);
741
+ const def = defs[name];
742
+ if (def.base && def.l === void 0) visit(def.base);
743
+ result.push(name);
744
+ }
745
+ for (const name of Object.keys(defs)) visit(name);
746
+ return result;
747
+ }
748
+ function mapLightnessDark(l, mode) {
749
+ if (mode === "static") return l;
750
+ const [lo, hi] = globalConfig.darkLightness;
751
+ if (mode === "fixed") return l * (hi - lo) / 100 + lo;
752
+ return (100 - l) * (hi - lo) / 100 + lo;
753
+ }
754
+ function mapSaturationDark(s, mode) {
755
+ if (mode === "static") return s;
756
+ return s * (1 - globalConfig.darkDesaturation);
757
+ }
758
+ /**
759
+ * Resolve the effective lightness from a contrast delta.
760
+ */
761
+ function resolveContrastLightness(baseLightness, contrast) {
762
+ if (contrast < 0) return clamp(baseLightness + contrast, 0, 100);
763
+ const candidate = baseLightness + contrast;
764
+ if (candidate > 100) return clamp(baseLightness - contrast, 0, 100);
765
+ return clamp(candidate, 0, 100);
766
+ }
767
+ function clamp(v, min, max) {
768
+ return Math.max(min, Math.min(max, v));
769
+ }
770
+ function resolveRootColor(_name, def, _ctx, isHighContrast) {
771
+ const rawL = def.l;
772
+ return {
773
+ lightL: clamp(isHighContrast ? pairHC(rawL) : pairNormal(rawL), 0, 100),
774
+ sat: clamp(def.sat ?? 1, 0, 1)
775
+ };
776
+ }
777
+ function resolveDependentColor(name, def, ctx, isHighContrast, isDark) {
778
+ const baseName = def.base;
779
+ const baseResolved = ctx.resolved.get(baseName);
780
+ if (!baseResolved) throw new Error(`glaze: base "${baseName}" not yet resolved for "${name}".`);
781
+ const mode = def.mode ?? "auto";
782
+ const sat = clamp(def.sat ?? 1, 0, 1);
783
+ let baseL;
784
+ if (isDark && isHighContrast) baseL = baseResolved.darkContrast.l * 100;
785
+ else if (isDark) baseL = baseResolved.dark.l * 100;
786
+ else if (isHighContrast) baseL = baseResolved.lightContrast.l * 100;
787
+ else baseL = baseResolved.light.l * 100;
788
+ const rawContrast = def.contrast ?? 0;
789
+ let contrast = isHighContrast ? pairHC(rawContrast) : pairNormal(rawContrast);
790
+ if (isDark && mode === "auto") contrast = -contrast;
791
+ const preferredL = resolveContrastLightness(baseL, contrast);
792
+ const rawEnsureContrast = def.ensureContrast;
793
+ if (rawEnsureContrast !== void 0) {
794
+ const minCr = isHighContrast ? pairHC(rawEnsureContrast) : pairNormal(rawEnsureContrast);
795
+ const effectiveSat = isDark ? mapSaturationDark(sat * ctx.saturation / 100, mode) : sat * ctx.saturation / 100;
796
+ let baseH;
797
+ let baseS;
798
+ let baseLNorm;
799
+ if (isDark && isHighContrast) {
800
+ baseH = baseResolved.darkContrast.h;
801
+ baseS = baseResolved.darkContrast.s;
802
+ baseLNorm = baseResolved.darkContrast.l;
803
+ } else if (isDark) {
804
+ baseH = baseResolved.dark.h;
805
+ baseS = baseResolved.dark.s;
806
+ baseLNorm = baseResolved.dark.l;
807
+ } else if (isHighContrast) {
808
+ baseH = baseResolved.lightContrast.h;
809
+ baseS = baseResolved.lightContrast.s;
810
+ baseLNorm = baseResolved.lightContrast.l;
811
+ } else {
812
+ baseH = baseResolved.light.h;
813
+ baseS = baseResolved.light.s;
814
+ baseLNorm = baseResolved.light.l;
815
+ }
816
+ const baseLinearRgb = okhslToLinearSrgb(baseH, baseS, baseLNorm);
817
+ return {
818
+ l: findLightnessForContrast({
819
+ hue: ctx.hue,
820
+ saturation: effectiveSat,
821
+ preferredLightness: preferredL / 100,
822
+ baseLinearRgb,
823
+ ensureContrast: minCr
824
+ }).lightness * 100,
825
+ sat
826
+ };
827
+ }
828
+ return {
829
+ l: clamp(preferredL, 0, 100),
830
+ sat
831
+ };
832
+ }
833
+ function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
834
+ const mode = def.mode ?? "auto";
835
+ const isRoot = def.l !== void 0;
836
+ let lightL;
837
+ let sat;
838
+ if (isRoot) {
839
+ const root = resolveRootColor(name, def, ctx, isHighContrast);
840
+ lightL = root.lightL;
841
+ sat = root.sat;
842
+ } else {
843
+ const dep = resolveDependentColor(name, def, ctx, isHighContrast, isDark);
844
+ lightL = dep.l;
845
+ sat = dep.sat;
846
+ }
847
+ let finalL;
848
+ let finalSat;
849
+ if (isDark && isRoot) {
850
+ finalL = mapLightnessDark(lightL, mode);
851
+ finalSat = mapSaturationDark(sat * ctx.saturation / 100, mode);
852
+ } else if (isDark && !isRoot) {
853
+ finalL = lightL;
854
+ finalSat = mapSaturationDark(sat * ctx.saturation / 100, mode);
855
+ } else {
856
+ finalL = lightL;
857
+ finalSat = sat * ctx.saturation / 100;
858
+ }
859
+ return {
860
+ h: ctx.hue,
861
+ s: clamp(finalSat, 0, 1),
862
+ l: clamp(finalL / 100, 0, 1)
863
+ };
864
+ }
865
+ function resolveAllColors(hue, saturation, defs) {
866
+ validateColorDefs(defs);
867
+ const order = topoSort(defs);
868
+ const ctx = {
869
+ hue,
870
+ saturation,
871
+ defs,
872
+ resolved: /* @__PURE__ */ new Map()
873
+ };
874
+ const lightMap = /* @__PURE__ */ new Map();
875
+ for (const name of order) {
876
+ const variant = resolveColorForScheme(name, defs[name], ctx, false, false);
877
+ lightMap.set(name, variant);
878
+ ctx.resolved.set(name, {
879
+ name,
880
+ light: variant,
881
+ dark: variant,
882
+ lightContrast: variant,
883
+ darkContrast: variant,
884
+ mode: defs[name].mode ?? "auto"
885
+ });
886
+ }
887
+ const lightHCMap = /* @__PURE__ */ new Map();
888
+ for (const name of order) ctx.resolved.set(name, {
889
+ ...ctx.resolved.get(name),
890
+ lightContrast: lightMap.get(name)
891
+ });
892
+ for (const name of order) {
893
+ const variant = resolveColorForScheme(name, defs[name], ctx, false, true);
894
+ lightHCMap.set(name, variant);
895
+ ctx.resolved.set(name, {
896
+ ...ctx.resolved.get(name),
897
+ lightContrast: variant
898
+ });
899
+ }
900
+ const darkMap = /* @__PURE__ */ new Map();
901
+ for (const name of order) ctx.resolved.set(name, {
902
+ name,
903
+ light: lightMap.get(name),
904
+ dark: lightMap.get(name),
905
+ lightContrast: lightHCMap.get(name),
906
+ darkContrast: lightHCMap.get(name),
907
+ mode: defs[name].mode ?? "auto"
908
+ });
909
+ for (const name of order) {
910
+ const variant = resolveColorForScheme(name, defs[name], ctx, true, false);
911
+ darkMap.set(name, variant);
912
+ ctx.resolved.set(name, {
913
+ ...ctx.resolved.get(name),
914
+ dark: variant
915
+ });
916
+ }
917
+ const darkHCMap = /* @__PURE__ */ new Map();
918
+ for (const name of order) ctx.resolved.set(name, {
919
+ ...ctx.resolved.get(name),
920
+ darkContrast: darkMap.get(name)
921
+ });
922
+ for (const name of order) {
923
+ const variant = resolveColorForScheme(name, defs[name], ctx, true, true);
924
+ darkHCMap.set(name, variant);
925
+ ctx.resolved.set(name, {
926
+ ...ctx.resolved.get(name),
927
+ darkContrast: variant
928
+ });
929
+ }
930
+ const result = /* @__PURE__ */ new Map();
931
+ for (const name of order) result.set(name, {
932
+ name,
933
+ light: lightMap.get(name),
934
+ dark: darkMap.get(name),
935
+ lightContrast: lightHCMap.get(name),
936
+ darkContrast: darkHCMap.get(name),
937
+ mode: defs[name].mode ?? "auto"
938
+ });
939
+ return result;
940
+ }
941
+ const formatters = {
942
+ okhsl: formatOkhsl,
943
+ rgb: formatRgb,
944
+ hsl: formatHsl,
945
+ oklch: formatOklch
946
+ };
947
+ function formatVariant(v, format = "okhsl") {
948
+ return formatters[format](v.h, v.s * 100, v.l * 100);
949
+ }
950
+ function resolveModes(override) {
951
+ return {
952
+ dark: override?.dark ?? globalConfig.modes.dark,
953
+ highContrast: override?.highContrast ?? globalConfig.modes.highContrast
954
+ };
955
+ }
956
+ function buildTokenMap(resolved, prefix, states, modes, format = "okhsl") {
957
+ const tokens = {};
958
+ for (const [name, color] of resolved) {
959
+ const key = `#${prefix}${name}`;
960
+ const entry = { "": formatVariant(color.light, format) };
961
+ if (modes.dark) entry[states.dark] = formatVariant(color.dark, format);
962
+ if (modes.highContrast) entry[states.highContrast] = formatVariant(color.lightContrast, format);
963
+ if (modes.dark && modes.highContrast) entry[`${states.dark} & ${states.highContrast}`] = formatVariant(color.darkContrast, format);
964
+ tokens[key] = entry;
965
+ }
966
+ return tokens;
967
+ }
968
+ function buildJsonMap(resolved, modes, format = "okhsl") {
969
+ const result = {};
970
+ for (const [name, color] of resolved) {
971
+ const entry = { light: formatVariant(color.light, format) };
972
+ if (modes.dark) entry.dark = formatVariant(color.dark, format);
973
+ if (modes.highContrast) entry.lightContrast = formatVariant(color.lightContrast, format);
974
+ if (modes.dark && modes.highContrast) entry.darkContrast = formatVariant(color.darkContrast, format);
975
+ result[name] = entry;
976
+ }
977
+ return result;
978
+ }
979
+ function createTheme(hue, saturation, initialColors) {
980
+ let colorDefs = initialColors ? { ...initialColors } : {};
981
+ return {
982
+ get hue() {
983
+ return hue;
984
+ },
985
+ get saturation() {
986
+ return saturation;
987
+ },
988
+ colors(defs) {
989
+ colorDefs = {
990
+ ...colorDefs,
991
+ ...defs
992
+ };
993
+ },
994
+ color(name, def) {
995
+ if (def === void 0) return colorDefs[name];
996
+ colorDefs[name] = def;
997
+ },
998
+ remove(names) {
999
+ const list = Array.isArray(names) ? names : [names];
1000
+ for (const name of list) delete colorDefs[name];
1001
+ },
1002
+ has(name) {
1003
+ return name in colorDefs;
1004
+ },
1005
+ list() {
1006
+ return Object.keys(colorDefs);
1007
+ },
1008
+ reset() {
1009
+ colorDefs = {};
1010
+ },
1011
+ export() {
1012
+ return {
1013
+ hue,
1014
+ saturation,
1015
+ colors: { ...colorDefs }
1016
+ };
1017
+ },
1018
+ extend(options) {
1019
+ return createTheme(options.hue ?? hue, options.saturation ?? saturation, options.colors ? {
1020
+ ...colorDefs,
1021
+ ...options.colors
1022
+ } : { ...colorDefs });
1023
+ },
1024
+ resolve() {
1025
+ return resolveAllColors(hue, saturation, colorDefs);
1026
+ },
1027
+ tokens(options) {
1028
+ return buildTokenMap(resolveAllColors(hue, saturation, colorDefs), "", {
1029
+ dark: options?.states?.dark ?? globalConfig.states.dark,
1030
+ highContrast: options?.states?.highContrast ?? globalConfig.states.highContrast
1031
+ }, resolveModes(options?.modes), options?.format);
1032
+ },
1033
+ json(options) {
1034
+ return buildJsonMap(resolveAllColors(hue, saturation, colorDefs), resolveModes(options?.modes), options?.format);
1035
+ }
1036
+ };
1037
+ }
1038
+ function createPalette(themes) {
1039
+ return {
1040
+ tokens(options) {
1041
+ const states = {
1042
+ dark: options?.states?.dark ?? globalConfig.states.dark,
1043
+ highContrast: options?.states?.highContrast ?? globalConfig.states.highContrast
1044
+ };
1045
+ const modes = resolveModes(options?.modes);
1046
+ const allTokens = {};
1047
+ for (const [themeName, theme] of Object.entries(themes)) {
1048
+ const resolved = theme.resolve();
1049
+ let prefix = "";
1050
+ if (options?.prefix === true) prefix = `${themeName}-`;
1051
+ else if (typeof options?.prefix === "object" && options.prefix !== null) prefix = options.prefix[themeName] ?? `${themeName}-`;
1052
+ const tokens = buildTokenMap(resolved, prefix, states, modes, options?.format);
1053
+ Object.assign(allTokens, tokens);
1054
+ }
1055
+ return allTokens;
1056
+ },
1057
+ json(options) {
1058
+ const modes = resolveModes(options?.modes);
1059
+ const result = {};
1060
+ for (const [themeName, theme] of Object.entries(themes)) result[themeName] = buildJsonMap(theme.resolve(), modes, options?.format);
1061
+ return result;
1062
+ }
1063
+ };
1064
+ }
1065
+ function createColorToken(input) {
1066
+ const defs = { __color__: {
1067
+ l: input.l,
1068
+ sat: input.sat,
1069
+ mode: input.mode
1070
+ } };
1071
+ return {
1072
+ resolve() {
1073
+ return resolveAllColors(input.hue, input.saturation, defs).get("__color__");
1074
+ },
1075
+ token(options) {
1076
+ return buildTokenMap(resolveAllColors(input.hue, input.saturation, defs), "", {
1077
+ dark: options?.states?.dark ?? globalConfig.states.dark,
1078
+ highContrast: options?.states?.highContrast ?? globalConfig.states.highContrast
1079
+ }, resolveModes(options?.modes), options?.format)["#__color__"];
1080
+ },
1081
+ json(options) {
1082
+ return buildJsonMap(resolveAllColors(input.hue, input.saturation, defs), resolveModes(options?.modes), options?.format)["__color__"];
1083
+ }
1084
+ };
1085
+ }
1086
+ /**
1087
+ * Create a single-hue glaze theme.
1088
+ *
1089
+ * @example
1090
+ * ```ts
1091
+ * const primary = glaze({ hue: 280, saturation: 80 });
1092
+ * // or shorthand:
1093
+ * const primary = glaze(280, 80);
1094
+ * ```
1095
+ */
1096
+ function glaze(hueOrOptions, saturation) {
1097
+ if (typeof hueOrOptions === "number") return createTheme(hueOrOptions, saturation ?? 100);
1098
+ return createTheme(hueOrOptions.hue, hueOrOptions.saturation);
1099
+ }
1100
+ /**
1101
+ * Configure global glaze settings.
1102
+ */
1103
+ glaze.configure = function configure(config) {
1104
+ globalConfig = {
1105
+ darkLightness: config.darkLightness ?? globalConfig.darkLightness,
1106
+ darkDesaturation: config.darkDesaturation ?? globalConfig.darkDesaturation,
1107
+ states: {
1108
+ dark: config.states?.dark ?? globalConfig.states.dark,
1109
+ highContrast: config.states?.highContrast ?? globalConfig.states.highContrast
1110
+ },
1111
+ modes: {
1112
+ dark: config.modes?.dark ?? globalConfig.modes.dark,
1113
+ highContrast: config.modes?.highContrast ?? globalConfig.modes.highContrast
1114
+ }
1115
+ };
1116
+ };
1117
+ /**
1118
+ * Compose multiple themes into a palette.
1119
+ */
1120
+ glaze.palette = function palette(themes) {
1121
+ return createPalette(themes);
1122
+ };
1123
+ /**
1124
+ * Create a theme from a serialized export.
1125
+ */
1126
+ glaze.from = function from(data) {
1127
+ return createTheme(data.hue, data.saturation, data.colors);
1128
+ };
1129
+ /**
1130
+ * Create a standalone single-color token.
1131
+ */
1132
+ glaze.color = function color(input) {
1133
+ return createColorToken(input);
1134
+ };
1135
+ /**
1136
+ * Create a theme from a hex color string.
1137
+ * Extracts hue and saturation from the color.
1138
+ */
1139
+ glaze.fromHex = function fromHex(hex) {
1140
+ const rgb = parseHex(hex);
1141
+ if (!rgb) throw new Error(`glaze: invalid hex color "${hex}".`);
1142
+ const [h, s] = srgbToOkhsl(rgb);
1143
+ return createTheme(h, s * 100);
1144
+ };
1145
+ /**
1146
+ * Create a theme from RGB values (0–255).
1147
+ * Extracts hue and saturation from the color.
1148
+ */
1149
+ glaze.fromRgb = function fromRgb(r, g, b) {
1150
+ const [h, s] = srgbToOkhsl([
1151
+ r / 255,
1152
+ g / 255,
1153
+ b / 255
1154
+ ]);
1155
+ return createTheme(h, s * 100);
1156
+ };
1157
+ /**
1158
+ * Get the current global configuration (for testing/debugging).
1159
+ */
1160
+ glaze.getConfig = function getConfig() {
1161
+ return { ...globalConfig };
1162
+ };
1163
+ /**
1164
+ * Reset global configuration to defaults.
1165
+ */
1166
+ glaze.resetConfig = function resetConfig() {
1167
+ globalConfig = {
1168
+ darkLightness: [10, 90],
1169
+ darkDesaturation: .1,
1170
+ states: {
1171
+ dark: "@dark",
1172
+ highContrast: "@high-contrast"
1173
+ },
1174
+ modes: {
1175
+ dark: true,
1176
+ highContrast: false
1177
+ }
1178
+ };
1179
+ };
1180
+
1181
+ //#endregion
1182
+ exports.contrastRatioFromLuminance = contrastRatioFromLuminance;
1183
+ exports.findLightnessForContrast = findLightnessForContrast;
1184
+ exports.formatHsl = formatHsl;
1185
+ exports.formatOkhsl = formatOkhsl;
1186
+ exports.formatOklch = formatOklch;
1187
+ exports.formatRgb = formatRgb;
1188
+ exports.glaze = glaze;
1189
+ exports.okhslToLinearSrgb = okhslToLinearSrgb;
1190
+ exports.okhslToOklab = okhslToOklab;
1191
+ exports.okhslToSrgb = okhslToSrgb;
1192
+ exports.parseHex = parseHex;
1193
+ exports.relativeLuminanceFromLinearRgb = relativeLuminanceFromLinearRgb;
1194
+ exports.resolveMinContrast = resolveMinContrast;
1195
+ exports.srgbToOkhsl = srgbToOkhsl;
1196
+ //# sourceMappingURL=index.cjs.map