hextimator 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/cli.js ADDED
@@ -0,0 +1,2395 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { readFileSync, writeFileSync } from "fs";
5
+ import { dirname, join } from "path";
6
+ import { fileURLToPath } from "url";
7
+ import { parseArgs } from "util";
8
+
9
+ // src/convert/matrices.ts
10
+ var M1 = [
11
+ [0.4122214708, 0.5363325363, 0.0514459929],
12
+ [0.2119034982, 0.6806995451, 0.1073969566],
13
+ [0.0883024619, 0.2817188376, 0.6299787005]
14
+ ];
15
+ var M2 = [
16
+ [0.2104542553, 0.793617785, -0.0040720468],
17
+ [1.9779984951, -2.428592205, 0.4505937099],
18
+ [0.0259040371, 0.7827717662, -0.808675766]
19
+ ];
20
+ var M1_INV = [
21
+ [4.0767416621, -3.3077115913, 0.2309699292],
22
+ [-1.2684380046, 2.6097574011, -0.3413193965],
23
+ [-0.0041960863, -0.7034186147, 1.707614701]
24
+ ];
25
+ var M2_INV = [
26
+ [1, 0.3963377774, 0.2158037573],
27
+ [1, -0.1055613458, -0.0638541728],
28
+ [1, -0.0894841775, -1.291485548]
29
+ ];
30
+ function multiplyMatrix3(m, v) {
31
+ return [
32
+ m[0][0] * v[0] + m[0][1] * v[1] + m[0][2] * v[2],
33
+ m[1][0] * v[0] + m[1][1] * v[1] + m[1][2] * v[2],
34
+ m[2][0] * v[0] + m[2][1] * v[1] + m[2][2] * v[2]
35
+ ];
36
+ }
37
+
38
+ // src/convert/p3-matrices.ts
39
+ var LINEAR_SRGB_TO_P3 = [
40
+ [0.8224619688, 0.1775380313, 0],
41
+ [0.0331941989, 0.9668058012, 0],
42
+ [0.0170826307, 0.0723974407, 0.9105199286]
43
+ ];
44
+ var LINEAR_P3_TO_SRGB = [
45
+ [1.2249401762, -0.2249401763, 0],
46
+ [-0.0420569547, 1.0420569547, 0],
47
+ [-0.0196375546, -0.0786360456, 1.0982736002]
48
+ ];
49
+
50
+ // src/convert/gamut.ts
51
+ var DEG_TO_RAD = Math.PI / 180;
52
+ var EPSILON = 1e-4;
53
+ var MAX_ITERATIONS = 16;
54
+ function oklchToLinearRgbRaw(l, c, h) {
55
+ const hRad = h * DEG_TO_RAD;
56
+ const a = c * Math.cos(hRad);
57
+ const b = c * Math.sin(hRad);
58
+ const lms_ = multiplyMatrix3(M2_INV, [l, a, b]);
59
+ const lms = lms_.map((v) => v * v * v);
60
+ return multiplyMatrix3(M1_INV, lms);
61
+ }
62
+ function isInGamut(r, g, b) {
63
+ return r >= -EPSILON && r <= 1 + EPSILON && g >= -EPSILON && g <= 1 + EPSILON && b >= -EPSILON && b <= 1 + EPSILON;
64
+ }
65
+ function gamutMapOklch(color) {
66
+ if (color.c <= EPSILON) {
67
+ return { ...color, c: 0 };
68
+ }
69
+ const [r, g, b] = oklchToLinearRgbRaw(color.l, color.c, color.h);
70
+ if (isInGamut(r, g, b)) {
71
+ return color;
72
+ }
73
+ let lo = 0;
74
+ let hi = color.c;
75
+ for (let i = 0; i < MAX_ITERATIONS; i++) {
76
+ const mid = (lo + hi) / 2;
77
+ const [rm, gm, bm] = oklchToLinearRgbRaw(color.l, mid, color.h);
78
+ if (isInGamut(rm, gm, bm)) {
79
+ lo = mid;
80
+ } else {
81
+ hi = mid;
82
+ }
83
+ }
84
+ return { ...color, c: lo };
85
+ }
86
+ function gamutMapOklchToP3(color) {
87
+ if (color.c <= EPSILON) {
88
+ return { ...color, c: 0 };
89
+ }
90
+ const [rSrgb, gSrgb, bSrgb] = oklchToLinearRgbRaw(color.l, color.c, color.h);
91
+ const [rP3, gP3, bP3] = multiplyMatrix3(LINEAR_SRGB_TO_P3, [
92
+ rSrgb,
93
+ gSrgb,
94
+ bSrgb
95
+ ]);
96
+ if (isInGamut(rP3, gP3, bP3)) {
97
+ return color;
98
+ }
99
+ let lo = 0;
100
+ let hi = color.c;
101
+ for (let i = 0; i < MAX_ITERATIONS; i++) {
102
+ const mid = (lo + hi) / 2;
103
+ const [rm, gm, bm] = oklchToLinearRgbRaw(color.l, mid, color.h);
104
+ const [rP3m, gP3m, bP3m] = multiplyMatrix3(LINEAR_SRGB_TO_P3, [rm, gm, bm]);
105
+ if (isInGamut(rP3m, gP3m, bP3m)) {
106
+ lo = mid;
107
+ } else {
108
+ hi = mid;
109
+ }
110
+ }
111
+ return { ...color, c: lo };
112
+ }
113
+
114
+ // src/convert/linear-oklab.ts
115
+ function linearRgbToOklab(color) {
116
+ const lms = multiplyMatrix3(M1, [color.r, color.g, color.b]);
117
+ const lms_ = lms.map((v) => Math.cbrt(v));
118
+ const [l, a, b] = multiplyMatrix3(M2, lms_);
119
+ return { space: "oklab", l, a, b, alpha: color.alpha };
120
+ }
121
+ function oklabToLinearRgb(color) {
122
+ const lms_ = multiplyMatrix3(M2_INV, [color.l, color.a, color.b]);
123
+ const lms = lms_.map((v) => v * v * v);
124
+ const [r, g, b] = multiplyMatrix3(M1_INV, lms);
125
+ return { space: "linear-rgb", r, g, b, alpha: color.alpha };
126
+ }
127
+
128
+ // src/convert/oklab-oklch.ts
129
+ var RAD_TO_DEG = 180 / Math.PI;
130
+ var DEG_TO_RAD2 = Math.PI / 180;
131
+ function oklabToOklch(color) {
132
+ const c = Math.sqrt(color.a * color.a + color.b * color.b);
133
+ let h = Math.atan2(color.b, color.a) * RAD_TO_DEG;
134
+ if (h < 0) h += 360;
135
+ return { space: "oklch", l: color.l, c, h, alpha: color.alpha };
136
+ }
137
+ function oklchToOklab(color) {
138
+ const hRad = color.h * DEG_TO_RAD2;
139
+ return {
140
+ space: "oklab",
141
+ l: color.l,
142
+ a: color.c * Math.cos(hRad),
143
+ b: color.c * Math.sin(hRad),
144
+ alpha: color.alpha
145
+ };
146
+ }
147
+
148
+ // src/convert/srgb-hsl.ts
149
+ function srgbToHsl(color) {
150
+ const r = color.r / 255;
151
+ const g = color.g / 255;
152
+ const b = color.b / 255;
153
+ const max = Math.max(r, g, b);
154
+ const min = Math.min(r, g, b);
155
+ const d = max - min;
156
+ const l = (max + min) / 2;
157
+ if (d === 0) {
158
+ return { space: "hsl", h: 0, s: 0, l: l * 100, alpha: color.alpha };
159
+ }
160
+ const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
161
+ let h;
162
+ if (max === r) {
163
+ h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
164
+ } else if (max === g) {
165
+ h = ((b - r) / d + 2) / 6;
166
+ } else {
167
+ h = ((r - g) / d + 4) / 6;
168
+ }
169
+ return {
170
+ space: "hsl",
171
+ h: h * 360,
172
+ s: s * 100,
173
+ l: l * 100,
174
+ alpha: color.alpha
175
+ };
176
+ }
177
+ function hslToSrgb(color) {
178
+ const h = color.h / 360;
179
+ const s = color.s / 100;
180
+ const l = color.l / 100;
181
+ if (s === 0) {
182
+ const v = Math.round(l * 255);
183
+ return { space: "srgb", r: v, g: v, b: v, alpha: color.alpha };
184
+ }
185
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
186
+ const p = 2 * l - q;
187
+ return {
188
+ space: "srgb",
189
+ r: Math.round(hueToRgb(p, q, h + 1 / 3) * 255),
190
+ g: Math.round(hueToRgb(p, q, h) * 255),
191
+ b: Math.round(hueToRgb(p, q, h - 1 / 3) * 255),
192
+ alpha: color.alpha
193
+ };
194
+ }
195
+ function hueToRgb(p, q, t) {
196
+ if (t < 0) t += 1;
197
+ if (t > 1) t -= 1;
198
+ if (t < 1 / 6) return p + (q - p) * 6 * t;
199
+ if (t < 1 / 2) return q;
200
+ if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
201
+ return p;
202
+ }
203
+
204
+ // src/convert/srgb-linear.ts
205
+ function gammaDecodeChannel(c) {
206
+ const s = c / 255;
207
+ return s <= 0.04045 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
208
+ }
209
+ function gammaEncodeChannel(c) {
210
+ const s = c <= 31308e-7 ? c * 12.92 : 1.055 * c ** (1 / 2.4) - 0.055;
211
+ return Math.round(Math.min(255, Math.max(0, s * 255)));
212
+ }
213
+ function srgbToLinear(color) {
214
+ return {
215
+ space: "linear-rgb",
216
+ r: gammaDecodeChannel(color.r),
217
+ g: gammaDecodeChannel(color.g),
218
+ b: gammaDecodeChannel(color.b),
219
+ alpha: color.alpha
220
+ };
221
+ }
222
+ function linearToSrgb(color) {
223
+ return {
224
+ space: "srgb",
225
+ r: gammaEncodeChannel(color.r),
226
+ g: gammaEncodeChannel(color.g),
227
+ b: gammaEncodeChannel(color.b),
228
+ alpha: color.alpha
229
+ };
230
+ }
231
+
232
+ // src/convert/srgb-p3.ts
233
+ function gammaDecode(s) {
234
+ return s <= 0.04045 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
235
+ }
236
+ function gammaEncode(c) {
237
+ return c <= 31308e-7 ? c * 12.92 : 1.055 * c ** (1 / 2.4) - 0.055;
238
+ }
239
+ function displayP3ToLinearSrgb(color) {
240
+ const linP3 = [
241
+ gammaDecode(color.r),
242
+ gammaDecode(color.g),
243
+ gammaDecode(color.b)
244
+ ];
245
+ const [r, g, b] = multiplyMatrix3(LINEAR_P3_TO_SRGB, linP3);
246
+ return { space: "linear-rgb", r, g, b, alpha: color.alpha };
247
+ }
248
+ function linearSrgbToDisplayP3(color) {
249
+ const [rLin, gLin, bLin] = multiplyMatrix3(LINEAR_SRGB_TO_P3, [
250
+ color.r,
251
+ color.g,
252
+ color.b
253
+ ]);
254
+ return {
255
+ space: "display-p3",
256
+ r: gammaEncode(rLin),
257
+ g: gammaEncode(gLin),
258
+ b: gammaEncode(bLin),
259
+ alpha: color.alpha
260
+ };
261
+ }
262
+ function srgbToDisplayP3(color) {
263
+ const rLin = color.r <= 0.04045 * 255 ? color.r / 255 / 12.92 : ((color.r / 255 + 0.055) / 1.055) ** 2.4;
264
+ const gLin = color.g <= 0.04045 * 255 ? color.g / 255 / 12.92 : ((color.g / 255 + 0.055) / 1.055) ** 2.4;
265
+ const bLin = color.b <= 0.04045 * 255 ? color.b / 255 / 12.92 : ((color.b / 255 + 0.055) / 1.055) ** 2.4;
266
+ const [rP3, gP3, bP3] = multiplyMatrix3(LINEAR_SRGB_TO_P3, [
267
+ rLin,
268
+ gLin,
269
+ bLin
270
+ ]);
271
+ return {
272
+ space: "display-p3",
273
+ r: gammaEncode(rP3),
274
+ g: gammaEncode(gP3),
275
+ b: gammaEncode(bP3),
276
+ alpha: color.alpha
277
+ };
278
+ }
279
+ function displayP3ToSrgb(color) {
280
+ const linP3 = [
281
+ gammaDecode(color.r),
282
+ gammaDecode(color.g),
283
+ gammaDecode(color.b)
284
+ ];
285
+ const [rLin, gLin, bLin] = multiplyMatrix3(LINEAR_P3_TO_SRGB, linP3);
286
+ function encodeChannel(c) {
287
+ const s = c <= 31308e-7 ? c * 12.92 : 1.055 * c ** (1 / 2.4) - 0.055;
288
+ return Math.round(Math.min(255, Math.max(0, s * 255)));
289
+ }
290
+ return {
291
+ space: "srgb",
292
+ r: encodeChannel(rLin),
293
+ g: encodeChannel(gLin),
294
+ b: encodeChannel(bLin),
295
+ alpha: color.alpha
296
+ };
297
+ }
298
+
299
+ // src/convert/index.ts
300
+ var gamutMap = ((color) => gamutMapOklch(color));
301
+ var gamutMapP3 = ((color) => gamutMapOklchToP3(color));
302
+ function chain(...fns) {
303
+ return (color) => fns.reduce((c, fn) => fn(c), color);
304
+ }
305
+ var conversions = {
306
+ // srgb ↔ linear-rgb
307
+ "srgb->linear-rgb": srgbToLinear,
308
+ "linear-rgb->srgb": linearToSrgb,
309
+ // linear-rgb ↔ oklab
310
+ "linear-rgb->oklab": linearRgbToOklab,
311
+ "oklab->linear-rgb": oklabToLinearRgb,
312
+ // oklab ↔ oklch
313
+ "oklab->oklch": oklabToOklch,
314
+ "oklch->oklab": oklchToOklab,
315
+ // srgb ↔ hsl
316
+ "srgb->hsl": srgbToHsl,
317
+ "hsl->srgb": hslToSrgb,
318
+ // srgb → oklab / oklch
319
+ "srgb->oklab": chain(srgbToLinear, linearRgbToOklab),
320
+ "srgb->oklch": chain(srgbToLinear, linearRgbToOklab, oklabToOklch),
321
+ // oklab / oklch → srgb (gamut-mapped)
322
+ "oklab->srgb": chain(oklabToLinearRgb, linearToSrgb),
323
+ "oklch->srgb": chain(gamutMap, oklchToOklab, oklabToLinearRgb, linearToSrgb),
324
+ // linear-rgb → oklch
325
+ "linear-rgb->oklch": chain(linearRgbToOklab, oklabToOklch),
326
+ "oklch->linear-rgb": chain(gamutMap, oklchToOklab, oklabToLinearRgb),
327
+ // hsl ↔ linear-rgb
328
+ "hsl->linear-rgb": chain(hslToSrgb, srgbToLinear),
329
+ "linear-rgb->hsl": chain(linearToSrgb, srgbToHsl),
330
+ // hsl ↔ oklab
331
+ "hsl->oklab": chain(hslToSrgb, srgbToLinear, linearRgbToOklab),
332
+ "oklab->hsl": chain(oklabToLinearRgb, linearToSrgb, srgbToHsl),
333
+ // hsl ↔ oklch
334
+ "hsl->oklch": chain(hslToSrgb, srgbToLinear, linearRgbToOklab, oklabToOklch),
335
+ "oklch->hsl": chain(
336
+ gamutMap,
337
+ oklchToOklab,
338
+ oklabToLinearRgb,
339
+ linearToSrgb,
340
+ srgbToHsl
341
+ ),
342
+ // display-p3
343
+ "display-p3->srgb": displayP3ToSrgb,
344
+ "srgb->display-p3": srgbToDisplayP3,
345
+ "display-p3->linear-rgb": displayP3ToLinearSrgb,
346
+ "linear-rgb->display-p3": linearSrgbToDisplayP3,
347
+ "display-p3->oklab": chain(displayP3ToLinearSrgb, linearRgbToOklab),
348
+ "oklab->display-p3": chain(oklabToLinearRgb, linearSrgbToDisplayP3),
349
+ "display-p3->oklch": chain(
350
+ displayP3ToLinearSrgb,
351
+ linearRgbToOklab,
352
+ oklabToOklch
353
+ ),
354
+ "oklch->display-p3": chain(
355
+ gamutMapP3,
356
+ oklchToOklab,
357
+ oklabToLinearRgb,
358
+ linearSrgbToDisplayP3
359
+ ),
360
+ "display-p3->hsl": chain(displayP3ToSrgb, srgbToHsl),
361
+ "hsl->display-p3": chain(hslToSrgb, srgbToDisplayP3)
362
+ };
363
+ function convert(color, to) {
364
+ if (color.space === to) {
365
+ return { ...color };
366
+ }
367
+ const key = `${color.space}->${to}`;
368
+ const fn = conversions[key];
369
+ if (!fn) {
370
+ throw new Error(`Unsupported conversion: ${color.space} \u2192 ${to}`);
371
+ }
372
+ return fn(color);
373
+ }
374
+
375
+ // src/parse/parseTuple.ts
376
+ function tryParseTuple(t, assumeSpace = "srgb") {
377
+ switch (assumeSpace) {
378
+ case "srgb": {
379
+ const [r, g, b] = t;
380
+ return { space: "srgb", r, g, b, alpha: 1 };
381
+ }
382
+ case "hsl": {
383
+ const [h, s, l] = t;
384
+ return { space: "hsl", h, s, l, alpha: 1 };
385
+ }
386
+ case "oklch": {
387
+ const [l, c, h] = t;
388
+ return { space: "oklch", l, c, h, alpha: 1 };
389
+ }
390
+ case "oklab": {
391
+ const [l, a, b] = t;
392
+ return { space: "oklab", l, a, b, alpha: 1 };
393
+ }
394
+ case "linear-rgb": {
395
+ const [r, g, b] = t;
396
+ return { space: "linear-rgb", r, g, b, alpha: 1 };
397
+ }
398
+ default:
399
+ return null;
400
+ }
401
+ }
402
+
403
+ // src/parse/parseCommaSeparated.ts
404
+ function tryParseCommaSeparated(input, assumeSpace = "srgb") {
405
+ const parts = input.split(",").map((p) => p.trim());
406
+ if (parts.length < 3 || parts.length > 4) return null;
407
+ const numbers = parts.map(parseFloat);
408
+ if (numbers.some(Number.isNaN)) return null;
409
+ return tryParseTuple(
410
+ numbers.length === 4 ? [numbers[0], numbers[1], numbers[2], numbers[3]] : [numbers[0], numbers[1], numbers[2]],
411
+ assumeSpace
412
+ );
413
+ }
414
+
415
+ // src/parse/parseCSSFunction.ts
416
+ var CSS_FUNC_REGULAR_EXPRESSION = /^(rgba?|hsla?|oklch|oklab|lab|color)\(\s*(.+?)\s*\)$/;
417
+ function parseCSSArgs(raw) {
418
+ let body = raw;
419
+ const slashIdx = body.lastIndexOf("/");
420
+ if (slashIdx !== -1) {
421
+ body = body.slice(0, slashIdx).trim();
422
+ }
423
+ const parts = body.includes(",") ? body.split(",").map((p) => p.trim()) : body.split(/\s+/);
424
+ if (parts.length === 4) {
425
+ parts.pop();
426
+ }
427
+ if (parts.length !== 3) {
428
+ return null;
429
+ }
430
+ const values = parts.map((p) => parseNumericValue(p));
431
+ if (values.some(Number.isNaN)) {
432
+ return null;
433
+ }
434
+ return { values };
435
+ }
436
+ function parseNumericValue(raw, percentScale) {
437
+ const s = raw.trim();
438
+ if (s === "none") return 0;
439
+ if (s.endsWith("%")) {
440
+ const base = parseFloat(s);
441
+ return percentScale ? base / 100 * percentScale : base / 100;
442
+ }
443
+ return parseFloat(s);
444
+ }
445
+ function buildRGB(args) {
446
+ const [r, g, b] = args.values;
447
+ return { space: "srgb", r, g, b, alpha: 1 };
448
+ }
449
+ function buildHSL(args) {
450
+ const [h, s, l] = args.values;
451
+ return { space: "hsl", h, s, l, alpha: 1 };
452
+ }
453
+ function buildOKLCH(args) {
454
+ const [l, c, h] = args.values;
455
+ return { space: "oklch", l, c, h, alpha: 1 };
456
+ }
457
+ function buildOKLab(args) {
458
+ const [l, a, b] = args.values;
459
+ return { space: "oklab", l, a, b, alpha: 1 };
460
+ }
461
+ function tryParseColorFunction(argsRaw) {
462
+ const parts = argsRaw.trim().split(/\s+/);
463
+ if (parts.length < 4) return null;
464
+ const spaceId = parts[0];
465
+ const rest = parts.slice(1).join(" ");
466
+ const args = parseCSSArgs(rest);
467
+ if (!args) return null;
468
+ const [r, g, b] = args.values;
469
+ switch (spaceId) {
470
+ case "srgb-linear":
471
+ return { space: "linear-rgb", r, g, b, alpha: 1 };
472
+ case "display-p3":
473
+ return { space: "display-p3", r, g, b, alpha: 1 };
474
+ default:
475
+ return null;
476
+ }
477
+ }
478
+ function tryParseCSSFunction(input) {
479
+ const match = input.match(CSS_FUNC_REGULAR_EXPRESSION);
480
+ if (!match) return null;
481
+ const [, funcName, argsRaw] = match;
482
+ if (funcName === "color") {
483
+ return tryParseColorFunction(argsRaw);
484
+ }
485
+ const args = parseCSSArgs(argsRaw);
486
+ if (!args) return null;
487
+ switch (funcName) {
488
+ case "rgb":
489
+ case "rgba":
490
+ return buildRGB(args);
491
+ case "hsl":
492
+ case "hsla":
493
+ return buildHSL(args);
494
+ case "oklch":
495
+ return buildOKLCH(args);
496
+ case "oklab":
497
+ return buildOKLab(args);
498
+ default:
499
+ return null;
500
+ }
501
+ }
502
+
503
+ // src/parse/parseHex.ts
504
+ var HEX_PATTERNS = {
505
+ prefixed: /^#([0-9a-f]{3,8})$/,
506
+ // e.g. #FF6666
507
+ numeric: /^0x([0-9a-f]{6}|[0-9a-f]{8})$/,
508
+ // e.g. 0xFF6666
509
+ bare: /^([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/
510
+ // e.g. F66, FF6666
511
+ };
512
+ function parseHexDigits(hex) {
513
+ if (hex.length === 3 || hex.length === 4) {
514
+ hex = [...hex].map((c) => c + c).join("");
515
+ }
516
+ if (hex.length !== 6 && hex.length !== 8) {
517
+ return null;
518
+ }
519
+ const r = parseInt(hex.slice(0, 2), 16);
520
+ const g = parseInt(hex.slice(2, 4), 16);
521
+ const b = parseInt(hex.slice(4, 6), 16);
522
+ if ([r, g, b].some(Number.isNaN)) {
523
+ return null;
524
+ }
525
+ return { space: "srgb", r, g, b, alpha: 1 };
526
+ }
527
+ function tryParseHex(input) {
528
+ for (const pattern of Object.values(HEX_PATTERNS)) {
529
+ const match = input.match(pattern);
530
+ if (match) {
531
+ return parseHexDigits(match[1]);
532
+ }
533
+ }
534
+ return null;
535
+ }
536
+
537
+ // src/parse/parseNumeric.ts
538
+ function tryParseNumeric(n) {
539
+ if (!Number.isInteger(n) || n < 0 || n > 4294967295) return null;
540
+ if (n <= 16777215) {
541
+ return {
542
+ space: "srgb",
543
+ r: n >> 16 & 255,
544
+ g: n >> 8 & 255,
545
+ b: n & 255,
546
+ alpha: 1
547
+ };
548
+ }
549
+ return {
550
+ space: "srgb",
551
+ r: n >> 24 & 255,
552
+ g: n >> 16 & 255,
553
+ b: n >> 8 & 255,
554
+ alpha: 1
555
+ };
556
+ }
557
+
558
+ // src/parse/parse.ts
559
+ var ColorParseError = class extends Error {
560
+ constructor(input, message) {
561
+ super(message ?? `Failed to parse color: ${String(input)}`);
562
+ this.input = input;
563
+ this.name = "ColorParseError";
564
+ }
565
+ };
566
+ function parse(input, assumeSpace) {
567
+ if (isColor(input)) return input;
568
+ if (typeof input === "number") {
569
+ const result = tryParseNumeric(input);
570
+ if (result) return result;
571
+ throw new ColorParseError(input);
572
+ }
573
+ if (Array.isArray(input)) {
574
+ const result = tryParseTuple(input, assumeSpace);
575
+ if (result) return result;
576
+ throw new ColorParseError(input);
577
+ }
578
+ if (typeof input === "string") {
579
+ const normalized = _normalizeInput(input);
580
+ const cssResult = tryParseCSSFunction(normalized);
581
+ if (cssResult) return cssResult;
582
+ const hexResult = tryParseHex(normalized);
583
+ if (hexResult) return hexResult;
584
+ const commaResult = tryParseCommaSeparated(normalized, assumeSpace);
585
+ if (commaResult) return commaResult;
586
+ throw new ColorParseError(input, `Unrecognized color format: ${input}`);
587
+ }
588
+ throw new ColorParseError(input);
589
+ }
590
+ function _normalizeInput(raw) {
591
+ return raw.trim().toLowerCase();
592
+ }
593
+ function isColor(value) {
594
+ return typeof value === "object" && value !== null && "space" in value && typeof value.space === "string";
595
+ }
596
+
597
+ // src/a11y/cvd/matrices.ts
598
+ var PROTAN_A = [
599
+ [0.152286, 1.052583, -0.204868],
600
+ [0.114503, 0.786281, 0.099216],
601
+ [-3882e-6, -0.048116, 1.051998]
602
+ ];
603
+ var PROTAN_B = [
604
+ [0.152286, 1.052583, -0.204868],
605
+ [0.114503, 0.786281, 0.099216],
606
+ [-3882e-6, -0.048116, 1.051998]
607
+ ];
608
+ var PROTAN_SEP = [
609
+ 48e-5,
610
+ 393e-5,
611
+ -441e-5
612
+ ];
613
+ var DEUTAN_A = [
614
+ [0.367322, 0.860646, -0.227968],
615
+ [0.280085, 0.672501, 0.047413],
616
+ [-0.01182, 0.04294, 0.968881]
617
+ ];
618
+ var DEUTAN_B = [
619
+ [0.367322, 0.860646, -0.227968],
620
+ [0.280085, 0.672501, 0.047413],
621
+ [-0.01182, 0.04294, 0.968881]
622
+ ];
623
+ var DEUTAN_SEP = [
624
+ -281e-5,
625
+ -611e-5,
626
+ 892e-5
627
+ ];
628
+ var TRITAN_A = [
629
+ [1.255528, -0.076749, -0.178779],
630
+ [-0.078411, 0.930809, 0.147602],
631
+ [4733e-6, 0.691367, 0.3039]
632
+ ];
633
+ var TRITAN_B = [
634
+ [1.255528, -0.076749, -0.178779],
635
+ [-0.078411, 0.930809, 0.147602],
636
+ [4733e-6, 0.691367, 0.3039]
637
+ ];
638
+ var TRITAN_SEP = [
639
+ 0.03901,
640
+ -0.02788,
641
+ -0.01113
642
+ ];
643
+ var BRETTEL = {
644
+ protanopia: { a: PROTAN_A, b: PROTAN_B, sep: PROTAN_SEP },
645
+ deuteranopia: { a: DEUTAN_A, b: DEUTAN_B, sep: DEUTAN_SEP },
646
+ tritanopia: { a: TRITAN_A, b: TRITAN_B, sep: TRITAN_SEP }
647
+ };
648
+ function simulateCVD(rgb, type, severity) {
649
+ const s = Math.max(0, Math.min(1, severity));
650
+ if (s === 0) return [rgb[0], rgb[1], rgb[2]];
651
+ let simulated;
652
+ if (type === "achromatopsia") {
653
+ const y = 0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2];
654
+ simulated = [y, y, y];
655
+ } else {
656
+ const params = BRETTEL[type];
657
+ const dotSep = rgb[0] * params.sep[0] + rgb[1] * params.sep[1] + rgb[2] * params.sep[2];
658
+ const matrix = dotSep >= 0 ? params.a : params.b;
659
+ simulated = multiplyMatrix3(matrix, rgb);
660
+ }
661
+ if (s < 1) {
662
+ return [
663
+ rgb[0] + s * (simulated[0] - rgb[0]),
664
+ rgb[1] + s * (simulated[1] - rgb[1]),
665
+ rgb[2] + s * (simulated[2] - rgb[2])
666
+ ];
667
+ }
668
+ return simulated;
669
+ }
670
+
671
+ // src/a11y/cvd/adapt.ts
672
+ var ERROR_REDISTRIBUTION = {
673
+ protanopia: [
674
+ [0, 0, 0],
675
+ [0.7, 1, 0],
676
+ [0.7, 0, 1]
677
+ ],
678
+ deuteranopia: [
679
+ [1, 0, 0],
680
+ [0.7, 0, 0],
681
+ [0, 0.7, 1]
682
+ ],
683
+ tritanopia: [
684
+ [1, 0, 0.7],
685
+ [0, 1, 0.7],
686
+ [0, 0, 0]
687
+ ]
688
+ };
689
+ function daltonize(rgb, type, severity) {
690
+ const s = Math.max(0, Math.min(1, severity));
691
+ if (s === 0) return [rgb[0], rgb[1], rgb[2]];
692
+ if (type === "achromatopsia") {
693
+ const y = 0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2];
694
+ const target = y > 0.5 ? 1 : 0;
695
+ return [
696
+ rgb[0] + s * 0.3 * (target - rgb[0]),
697
+ rgb[1] + s * 0.3 * (target - rgb[1]),
698
+ rgb[2] + s * 0.3 * (target - rgb[2])
699
+ ];
700
+ }
701
+ const simulated = simulateCVD(rgb, type, 1);
702
+ const err = [
703
+ rgb[0] - simulated[0],
704
+ rgb[1] - simulated[1],
705
+ rgb[2] - simulated[2]
706
+ ];
707
+ const redist = ERROR_REDISTRIBUTION[type];
708
+ const correction = [
709
+ redist[0][0] * err[0] + redist[0][1] * err[1] + redist[0][2] * err[2],
710
+ redist[1][0] * err[0] + redist[1][1] * err[1] + redist[1][2] * err[2],
711
+ redist[2][0] * err[0] + redist[2][1] * err[1] + redist[2][2] * err[2]
712
+ ];
713
+ return [
714
+ Math.max(0, Math.min(1, rgb[0] + s * correction[0])),
715
+ Math.max(0, Math.min(1, rgb[1] + s * correction[1])),
716
+ Math.max(0, Math.min(1, rgb[2] + s * correction[2]))
717
+ ];
718
+ }
719
+ function daltonizeColor(color, type, severity = 1) {
720
+ const linear = convert(color, "linear-rgb");
721
+ const corrected = daltonize([linear.r, linear.g, linear.b], type, severity);
722
+ return convert(
723
+ {
724
+ space: "linear-rgb",
725
+ r: corrected[0],
726
+ g: corrected[1],
727
+ b: corrected[2],
728
+ alpha: linear.alpha
729
+ },
730
+ "oklch"
731
+ );
732
+ }
733
+ function adaptPalette(palette, type, severity = 1) {
734
+ const result = {};
735
+ for (const role of Object.keys(palette)) {
736
+ const scale = palette[role];
737
+ const newScale = {
738
+ DEFAULT: scale.DEFAULT,
739
+ strong: scale.strong,
740
+ weak: scale.weak,
741
+ foreground: scale.foreground
742
+ };
743
+ for (const variant of Object.keys(scale)) {
744
+ newScale[variant] = daltonizeColor(parse(scale[variant]), type, severity);
745
+ }
746
+ result[role] = newScale;
747
+ }
748
+ return result;
749
+ }
750
+
751
+ // src/a11y/cvd/simulate.ts
752
+ function simulateColor(color, type, severity = 1) {
753
+ const linear = convert(color, "linear-rgb");
754
+ const simulated = simulateCVD([linear.r, linear.g, linear.b], type, severity);
755
+ const simulatedLinear = {
756
+ space: "linear-rgb",
757
+ r: Math.max(0, simulated[0]),
758
+ g: Math.max(0, simulated[1]),
759
+ b: Math.max(0, simulated[2]),
760
+ alpha: linear.alpha
761
+ };
762
+ return convert(simulatedLinear, "oklch");
763
+ }
764
+ function simulatePalette(palette, type, severity = 1) {
765
+ const result = {};
766
+ for (const role of Object.keys(palette)) {
767
+ const scale = palette[role];
768
+ const newScale = {
769
+ DEFAULT: scale.DEFAULT,
770
+ strong: scale.strong,
771
+ weak: scale.weak,
772
+ foreground: scale.foreground
773
+ };
774
+ for (const variant of Object.keys(scale)) {
775
+ newScale[variant] = simulateColor(parse(scale[variant]), type, severity);
776
+ }
777
+ result[role] = newScale;
778
+ }
779
+ return result;
780
+ }
781
+
782
+ // src/format/serializeColor.ts
783
+ function round(value, decimals) {
784
+ const factor = 10 ** decimals;
785
+ return Math.round(value * factor) / factor;
786
+ }
787
+ function clamp(value, min, max) {
788
+ return Math.min(max, Math.max(min, value));
789
+ }
790
+ function rgbToHex(rgb) {
791
+ const r = Math.round(clamp(rgb.r, 0, 255));
792
+ const g = Math.round(clamp(rgb.g, 0, 255));
793
+ const b = Math.round(clamp(rgb.b, 0, 255));
794
+ return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
795
+ }
796
+ function serializeColor(color, colorFormat = "hex") {
797
+ switch (colorFormat) {
798
+ case "hex": {
799
+ return rgbToHex(convert(color, "srgb"));
800
+ }
801
+ case "rgb": {
802
+ const rgb = convert(color, "srgb");
803
+ return `rgb(${Math.round(rgb.r)}, ${Math.round(rgb.g)}, ${Math.round(rgb.b)})`;
804
+ }
805
+ case "rgb-raw": {
806
+ const rgb = convert(color, "srgb");
807
+ return `${Math.round(rgb.r)} ${Math.round(rgb.g)} ${Math.round(rgb.b)}`;
808
+ }
809
+ case "hsl": {
810
+ const hsl = convert(color, "hsl");
811
+ return `hsl(${round(hsl.h, 1)}, ${round(hsl.s, 1)}%, ${round(hsl.l, 1)}%)`;
812
+ }
813
+ case "hsl-raw": {
814
+ const hsl = convert(color, "hsl");
815
+ return `${round(hsl.h, 1)} ${round(hsl.s, 1)}% ${round(hsl.l, 1)}%`;
816
+ }
817
+ case "oklch": {
818
+ const oklch = convert(color, "oklch");
819
+ return `oklch(${round(oklch.l, 4)} ${round(oklch.c, 4)} ${round(oklch.h, 1)})`;
820
+ }
821
+ case "oklch-raw": {
822
+ const oklch = convert(color, "oklch");
823
+ return `${round(oklch.l, 4)} ${round(oklch.c, 4)} ${round(oklch.h, 1)}`;
824
+ }
825
+ case "p3": {
826
+ const p3 = convert(color, "display-p3");
827
+ return `color(display-p3 ${round(clamp(p3.r, 0, 1), 5)} ${round(clamp(p3.g, 0, 1), 5)} ${round(clamp(p3.b, 0, 1), 5)})`;
828
+ }
829
+ case "p3-raw": {
830
+ const p3 = convert(color, "display-p3");
831
+ return `${round(clamp(p3.r, 0, 1), 5)} ${round(clamp(p3.g, 0, 1), 5)} ${round(clamp(p3.b, 0, 1), 5)}`;
832
+ }
833
+ default: {
834
+ throw new Error(`Unsupported color format: ${colorFormat}`);
835
+ }
836
+ }
837
+ }
838
+
839
+ // src/format/buildTokenEntries.ts
840
+ function isColorObject(input) {
841
+ return typeof input === "object" && input !== null && "space" in input;
842
+ }
843
+ function buildTokenEntries(palette, options) {
844
+ const colorFormat = options?.colors ?? "hex";
845
+ const entries = [];
846
+ const roles = Object.keys(palette);
847
+ for (const role of roles) {
848
+ const scale = palette[role];
849
+ const variants = Object.keys(scale);
850
+ for (const variant of variants) {
851
+ const raw = scale[variant];
852
+ if (!isColorObject(raw)) continue;
853
+ entries.push({
854
+ role: options?.roleNames?.[role] ?? role,
855
+ variant: options?.variantNames?.[variant] ?? variant,
856
+ isDefault: variant === "DEFAULT",
857
+ value: serializeColor(raw, colorFormat)
858
+ });
859
+ }
860
+ }
861
+ return entries;
862
+ }
863
+
864
+ // src/format/formatters.ts
865
+ function toFlatKey(entry, sep) {
866
+ if (entry.isDefault && entry.variant === "DEFAULT") return entry.role;
867
+ return `${entry.role}${sep}${entry.variant}`;
868
+ }
869
+ function formatObject(entries, sep) {
870
+ const result = {};
871
+ for (const entry of entries) {
872
+ result[toFlatKey(entry, sep)] = entry.value;
873
+ }
874
+ return result;
875
+ }
876
+ function formatCSS(entries, sep) {
877
+ const result = {};
878
+ for (const entry of entries) {
879
+ result[`--${toFlatKey(entry, sep)}`] = entry.value;
880
+ }
881
+ return result;
882
+ }
883
+ function formatSCSS(entries, sep) {
884
+ const result = {};
885
+ for (const entry of entries) {
886
+ result[`$${toFlatKey(entry, sep)}`] = entry.value;
887
+ }
888
+ return result;
889
+ }
890
+ function formatTailwind(entries) {
891
+ const result = {};
892
+ for (const { role, variant, value } of entries) {
893
+ if (!result[role]) result[role] = {};
894
+ result[role][variant] = value;
895
+ }
896
+ return result;
897
+ }
898
+ function formatTailwindCSS(entries, sep) {
899
+ const lines = entries.map((entry) => {
900
+ const key = toFlatKey(entry, sep);
901
+ return ` --color-${key}: ${entry.value};`;
902
+ });
903
+ return `@theme {
904
+ ${lines.join("\n")}
905
+ }`;
906
+ }
907
+ function formatJSON(entries, sep) {
908
+ return JSON.stringify(formatObject(entries, sep), null, 2);
909
+ }
910
+
911
+ // src/format/format.ts
912
+ function format(palette, options, standaloneTokens) {
913
+ const entries = buildTokenEntries(palette, options);
914
+ if (standaloneTokens) {
915
+ entries.push(...standaloneTokens);
916
+ }
917
+ const sep = options?.separator ?? "-";
918
+ switch (options?.as) {
919
+ case "css":
920
+ return formatCSS(entries, sep);
921
+ case "scss":
922
+ return formatSCSS(entries, sep);
923
+ case "tailwind":
924
+ return formatTailwind(entries);
925
+ case "tailwind-css":
926
+ return formatTailwindCSS(entries, sep);
927
+ case "json":
928
+ return formatJSON(entries, sep);
929
+ default:
930
+ return formatObject(entries, sep);
931
+ }
932
+ }
933
+
934
+ // src/generate/consts.ts
935
+ var DEFAULT_LIGHT_THEME_LIGHTNESS = 0.7;
936
+ var DEFAULT_DARK_THEME_LIGHTNESS = 0.6;
937
+
938
+ // src/generate/utils.ts
939
+ var FOREGROUND_DARK_L_VALUE = 0.97;
940
+ var FOREGROUND_LIGHT_L_VALUE = 0.1;
941
+ var FOREGROUND_MAX_CHROMA = 0.01;
942
+ var FALLBACK_STRONG_DELTA_DARK = 0.05;
943
+ var FALLBACK_STRONG_DELTA_LIGHT = -0.05;
944
+ var FALLBACK_WEAK_DELTA_DARK = -0.05;
945
+ var FALLBACK_WEAK_DELTA_LIGHT = 0.05;
946
+ var VARIANT_DELTA = 0.1;
947
+ var CONTRAST_MARGIN = 0.15;
948
+ function resolveContrastRatio(value) {
949
+ if (value === void 0 || value === "AAA") return 7;
950
+ if (value === "AA") return 4.5;
951
+ return value;
952
+ }
953
+ function clampHueShift(hueShift, totalVariants) {
954
+ if (totalVariants <= 0) return hueShift;
955
+ const max = 360 / (totalVariants + 1);
956
+ const sign = Math.sign(hueShift);
957
+ return sign * Math.min(Math.abs(hueShift), max);
958
+ }
959
+ function wrapHue(h) {
960
+ return (h % 360 + 360) % 360;
961
+ }
962
+ function expandColorToScale(color, themeType, options) {
963
+ const {
964
+ baselineLValueDark,
965
+ baselineLValueLight,
966
+ minContrastRatio: minContrastRatioOption,
967
+ foregroundLValueDark = FOREGROUND_DARK_L_VALUE,
968
+ foregroundLValueLight = FOREGROUND_LIGHT_L_VALUE,
969
+ foregroundMaxChroma: foregroundMaxChromaOption = FOREGROUND_MAX_CHROMA,
970
+ strongDeltaDark,
971
+ strongDeltaLight,
972
+ weakDeltaDark,
973
+ weakDeltaLight
974
+ } = options ?? {};
975
+ const themeAdjustments = themeType === "light" ? options?.light : options?.dark;
976
+ const foregroundMaxChroma = themeAdjustments?.foregroundMaxChroma ?? foregroundMaxChromaOption;
977
+ const minContrast = resolveContrastRatio(
978
+ themeAdjustments?.minContrastRatio ?? minContrastRatioOption
979
+ );
980
+ const contrastTarget = minContrast + CONTRAST_MARGIN;
981
+ const hasExplicitDeltas = strongDeltaDark !== void 0 || strongDeltaLight !== void 0 || weakDeltaDark !== void 0 || weakDeltaLight !== void 0;
982
+ const themeLightness = resolveThemeLightness(themeType, options);
983
+ const colorOKLCH = convert(color, "oklch");
984
+ const maxChroma = themeType === "light" ? options?.light?.maxChroma : options?.dark?.maxChroma;
985
+ const normalizedColorOKLCH = {
986
+ ...colorOKLCH,
987
+ l: themeType === "light" ? baselineLValueLight ?? themeLightness : baselineLValueDark ?? themeLightness,
988
+ c: maxChroma !== void 0 ? Math.min(colorOKLCH.c, maxChroma) : colorOKLCH.c
989
+ };
990
+ const candidates = [foregroundLValueLight, foregroundLValueDark].map((l) => ({
991
+ ...normalizedColorOKLCH,
992
+ l,
993
+ c: Math.min(normalizedColorOKLCH.c, foregroundMaxChroma)
994
+ }));
995
+ const [candidateA, candidateB] = themeType === "light" ? candidates : [...candidates].reverse();
996
+ const contrastA = calculateContrast(normalizedColorOKLCH, candidateA);
997
+ const contrastB = calculateContrast(normalizedColorOKLCH, candidateB);
998
+ const [preferred, fallback] = contrastA >= contrastB ? [candidateA, candidateB] : [candidateB, candidateA];
999
+ let foregroundColorOKLCH = calculateContrast(normalizedColorOKLCH, preferred) > minContrast ? preferred : fallback;
1000
+ if (calculateContrast(normalizedColorOKLCH, foregroundColorOKLCH) < contrastTarget) {
1001
+ const direction = preferred.l < normalizedColorOKLCH.l ? 1 : -1;
1002
+ let lo = direction === 1 ? normalizedColorOKLCH.l : 0;
1003
+ let hi = direction === 1 ? 1 : normalizedColorOKLCH.l;
1004
+ for (let i = 0; i < 20; i++) {
1005
+ const mid = (lo + hi) / 2;
1006
+ const testColor = { ...normalizedColorOKLCH, l: mid };
1007
+ if (calculateContrast(testColor, preferred) > contrastTarget) {
1008
+ if (direction === 1) hi = mid;
1009
+ else lo = mid;
1010
+ } else {
1011
+ if (direction === 1) lo = mid;
1012
+ else hi = mid;
1013
+ }
1014
+ }
1015
+ normalizedColorOKLCH.l = (lo + hi) / 2;
1016
+ foregroundColorOKLCH = preferred;
1017
+ }
1018
+ let strongColorOKLCH;
1019
+ let weakColorOKLCH;
1020
+ if (hasExplicitDeltas) {
1021
+ const sd = themeType === "light" ? strongDeltaLight ?? FALLBACK_STRONG_DELTA_LIGHT : strongDeltaDark ?? FALLBACK_STRONG_DELTA_DARK;
1022
+ const wd = themeType === "light" ? weakDeltaLight ?? FALLBACK_WEAK_DELTA_LIGHT : weakDeltaDark ?? FALLBACK_WEAK_DELTA_DARK;
1023
+ strongColorOKLCH = {
1024
+ ...normalizedColorOKLCH,
1025
+ l: normalizedColorOKLCH.l + sd
1026
+ };
1027
+ weakColorOKLCH = {
1028
+ ...normalizedColorOKLCH,
1029
+ l: normalizedColorOKLCH.l + wd
1030
+ };
1031
+ } else {
1032
+ const contrastDirection = themeType === "light" ? -1 : 1;
1033
+ let boundaryL = findContrastBoundaryLightness(
1034
+ normalizedColorOKLCH,
1035
+ foregroundColorOKLCH,
1036
+ contrastTarget
1037
+ );
1038
+ let distToBoundary = boundaryL !== null ? Math.abs(normalizedColorOKLCH.l - boundaryL) : 0;
1039
+ if (distToBoundary < VARIANT_DELTA) {
1040
+ const shift = Math.min(VARIANT_DELTA - distToBoundary, VARIANT_DELTA / 2);
1041
+ const awayFromForeground = foregroundColorOKLCH.l < normalizedColorOKLCH.l ? 1 : -1;
1042
+ normalizedColorOKLCH.l = Math.max(
1043
+ 0,
1044
+ Math.min(1, normalizedColorOKLCH.l + shift * awayFromForeground)
1045
+ );
1046
+ boundaryL = findContrastBoundaryLightness(
1047
+ normalizedColorOKLCH,
1048
+ foregroundColorOKLCH,
1049
+ contrastTarget
1050
+ );
1051
+ distToBoundary = boundaryL !== null ? Math.abs(normalizedColorOKLCH.l - boundaryL) : 0;
1052
+ }
1053
+ const strongDelta = Math.min(VARIANT_DELTA, distToBoundary);
1054
+ const weakCandidate = normalizedColorOKLCH.l - VARIANT_DELTA * contrastDirection;
1055
+ const weakCandidateColor = { ...normalizedColorOKLCH, l: weakCandidate };
1056
+ const weakContrast = calculateContrast(
1057
+ weakCandidateColor,
1058
+ foregroundColorOKLCH
1059
+ );
1060
+ let weakDelta = VARIANT_DELTA;
1061
+ if (weakContrast < contrastTarget) {
1062
+ let lo = 0;
1063
+ let hi = VARIANT_DELTA;
1064
+ for (let i = 0; i < 20; i++) {
1065
+ const mid = (lo + hi) / 2;
1066
+ const testL = normalizedColorOKLCH.l - mid * contrastDirection;
1067
+ const testColor = { ...normalizedColorOKLCH, l: testL };
1068
+ if (calculateContrast(testColor, foregroundColorOKLCH) > contrastTarget) {
1069
+ lo = mid;
1070
+ } else {
1071
+ hi = mid;
1072
+ }
1073
+ }
1074
+ weakDelta = lo;
1075
+ }
1076
+ strongColorOKLCH = {
1077
+ ...normalizedColorOKLCH,
1078
+ l: Math.max(
1079
+ 0,
1080
+ Math.min(1, normalizedColorOKLCH.l + strongDelta * contrastDirection)
1081
+ )
1082
+ };
1083
+ weakColorOKLCH = {
1084
+ ...normalizedColorOKLCH,
1085
+ l: Math.max(
1086
+ 0,
1087
+ Math.min(1, normalizedColorOKLCH.l - weakDelta * contrastDirection)
1088
+ )
1089
+ };
1090
+ }
1091
+ const rawHueShift = options?.hueShift ?? 0;
1092
+ if (rawHueShift !== 0) {
1093
+ const clamped = clampHueShift(rawHueShift, 2);
1094
+ strongColorOKLCH = {
1095
+ ...strongColorOKLCH,
1096
+ h: wrapHue(strongColorOKLCH.h + clamped)
1097
+ };
1098
+ weakColorOKLCH = {
1099
+ ...weakColorOKLCH,
1100
+ h: wrapHue(weakColorOKLCH.h - clamped)
1101
+ };
1102
+ strongColorOKLCH = ensureContrast(
1103
+ strongColorOKLCH,
1104
+ foregroundColorOKLCH,
1105
+ contrastTarget
1106
+ );
1107
+ weakColorOKLCH = ensureContrast(
1108
+ weakColorOKLCH,
1109
+ foregroundColorOKLCH,
1110
+ contrastTarget
1111
+ );
1112
+ }
1113
+ return {
1114
+ DEFAULT: { ...normalizedColorOKLCH },
1115
+ strong: { ...strongColorOKLCH },
1116
+ weak: { ...weakColorOKLCH },
1117
+ foreground: { ...foregroundColorOKLCH }
1118
+ };
1119
+ }
1120
+ var LIGHT_THEME_LIGHTNESS_RANGE = [0.4, 0.99];
1121
+ var DARK_THEME_LIGHTNESS_RANGE = [0.2, 0.8];
1122
+ function resolveThemeLightness(themeType, options) {
1123
+ const themeAdjustments = themeType === "light" ? options?.light : options?.dark;
1124
+ const range = themeType === "light" ? LIGHT_THEME_LIGHTNESS_RANGE : DARK_THEME_LIGHTNESS_RANGE;
1125
+ if (themeAdjustments?.lightness !== void 0) {
1126
+ return Math.min(Math.max(themeAdjustments.lightness, range[0]), range[1]);
1127
+ }
1128
+ return themeType === "light" ? DEFAULT_LIGHT_THEME_LIGHTNESS : DEFAULT_DARK_THEME_LIGHTNESS;
1129
+ }
1130
+ function findContrastBoundaryLightness(defaultColor, foregroundColor, targetContrast = 7) {
1131
+ const defaultOKLCH = convert(defaultColor, "oklch");
1132
+ const foregroundOKLCH = convert(foregroundColor, "oklch");
1133
+ const defaultContrast = calculateContrast(defaultColor, foregroundColor);
1134
+ if (defaultContrast <= targetContrast) {
1135
+ return null;
1136
+ }
1137
+ const { r: fr, g: fg, b: fb } = convert(foregroundColor, "linear-rgb");
1138
+ const foregroundLuminance = 0.2126 * fr + 0.7152 * fg + 0.0722 * fb;
1139
+ let tLo = 0;
1140
+ let tHi = 1;
1141
+ for (let i = 0; i < 20; i++) {
1142
+ const tMid = (tLo + tHi) / 2;
1143
+ const l = defaultOKLCH.l + tMid * (foregroundOKLCH.l - defaultOKLCH.l);
1144
+ const testColor = { ...defaultOKLCH, l };
1145
+ const { r, g, b } = convert(testColor, "linear-rgb");
1146
+ const testLuminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
1147
+ const lighter = Math.max(testLuminance, foregroundLuminance);
1148
+ const darker = Math.min(testLuminance, foregroundLuminance);
1149
+ const contrast = (lighter + 0.05) / (darker + 0.05);
1150
+ if (contrast > targetContrast) {
1151
+ tLo = tMid;
1152
+ } else {
1153
+ tHi = tMid;
1154
+ }
1155
+ }
1156
+ return defaultOKLCH.l + (tLo + tHi) / 2 * (foregroundOKLCH.l - defaultOKLCH.l);
1157
+ }
1158
+ function ensureContrast(variant, foreground, target) {
1159
+ if (calculateContrast(variant, foreground) >= target) return variant;
1160
+ const direction = foreground.l < variant.l ? 1 : -1;
1161
+ let lo = direction === 1 ? variant.l : 0;
1162
+ let hi = direction === 1 ? 1 : variant.l;
1163
+ for (let i = 0; i < 20; i++) {
1164
+ const mid = (lo + hi) / 2;
1165
+ const test = { ...variant, l: mid };
1166
+ if (calculateContrast(test, foreground) >= target) {
1167
+ if (direction === 1) hi = mid;
1168
+ else lo = mid;
1169
+ } else {
1170
+ if (direction === 1) lo = mid;
1171
+ else hi = mid;
1172
+ }
1173
+ }
1174
+ return { ...variant, l: (lo + hi) / 2 };
1175
+ }
1176
+ function calculateContrast(colorA, colorB) {
1177
+ const luminance = (color) => {
1178
+ const { r, g, b } = convert(color, "linear-rgb");
1179
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
1180
+ };
1181
+ const L1 = luminance(colorA);
1182
+ const L2 = luminance(colorB);
1183
+ const lighter = Math.max(L1, L2);
1184
+ const darker = Math.min(L1, L2);
1185
+ return (lighter + 0.05) / (darker + 0.05);
1186
+ }
1187
+
1188
+ // src/generate/generateAccent.ts
1189
+ function generateAccent(accent, themeType, options) {
1190
+ const invertBaseAndAccent = themeType === "dark" && options?.invertDarkModeBaseAccent;
1191
+ if (invertBaseAndAccent) {
1192
+ const accentOklch = convert(accent, "oklch");
1193
+ const baseHueShift = options?.baseHueShift ?? 0;
1194
+ let baseOklch = options?.baseColor ? convert(parse(options.baseColor), "oklch") : convert(accent, "oklch");
1195
+ if (baseHueShift !== 0 && !options?.baseColor) {
1196
+ baseOklch = {
1197
+ ...baseOklch,
1198
+ h: wrapHue(accentOklch.h + baseHueShift)
1199
+ };
1200
+ }
1201
+ const maxChroma = options?.dark?.maxChroma;
1202
+ const invertedAccent = {
1203
+ ...baseOklch,
1204
+ c: maxChroma !== void 0 ? Math.min(accentOklch.c, maxChroma) : accentOklch.c
1205
+ };
1206
+ return expandColorToScale(invertedAccent, themeType, options);
1207
+ }
1208
+ return expandColorToScale(accent, themeType, options);
1209
+ }
1210
+
1211
+ // src/generate/generateBase.ts
1212
+ var BASELINE_DARK_L_VALUE = 0.2;
1213
+ var BASELINE_LIGHT_L_VALUE = 0.97;
1214
+ var BASELINE_MAX_CHROMA = 0.01;
1215
+ var STRONG_DELTA_DARK = -0.1;
1216
+ var STRONG_DELTA_LIGHT = 0.02;
1217
+ var WEAK_DELTA_DARK = 0.1;
1218
+ var WEAK_DELTA_LIGHT = -0.03;
1219
+ function generateBase(color, themeType, options) {
1220
+ const invertBaseAndAccent = themeType === "dark" && options?.invertDarkModeBaseAccent;
1221
+ const preferredBaseColorInput = invertBaseAndAccent ? color ?? options?.baseColor : options?.baseColor ?? color;
1222
+ const themeAdjustments = themeType === "light" ? options?.light : options?.dark;
1223
+ const baselineMaxChroma = themeAdjustments?.baseMaxChroma ?? options?.baseMaxChroma ?? BASELINE_MAX_CHROMA;
1224
+ const preferredBaseColor = convert(parse(preferredBaseColorInput), "oklch");
1225
+ let baseHue = preferredBaseColor.h;
1226
+ const baseChroma = Math.min(preferredBaseColor.c, baselineMaxChroma);
1227
+ const baseHueShift = options?.baseHueShift ?? 0;
1228
+ if (baseHueShift !== 0 && !options?.baseColor && !invertBaseAndAccent) {
1229
+ baseHue = wrapHue(convert(color, "oklch").h + baseHueShift);
1230
+ }
1231
+ const normalizedPreferredBaseColor = {
1232
+ ...preferredBaseColor,
1233
+ h: baseHue,
1234
+ c: baseChroma,
1235
+ l: themeType === "light" ? BASELINE_LIGHT_L_VALUE : BASELINE_DARK_L_VALUE
1236
+ };
1237
+ return expandColorToScale(normalizedPreferredBaseColor, themeType, {
1238
+ baselineLValueDark: BASELINE_DARK_L_VALUE,
1239
+ baselineLValueLight: BASELINE_LIGHT_L_VALUE,
1240
+ strongDeltaDark: STRONG_DELTA_DARK,
1241
+ strongDeltaLight: STRONG_DELTA_LIGHT,
1242
+ weakDeltaDark: WEAK_DELTA_DARK,
1243
+ weakDeltaLight: WEAK_DELTA_LIGHT,
1244
+ minContrastRatio: options?.minContrastRatio,
1245
+ hueShift: options?.hueShift,
1246
+ foregroundMaxChroma: options?.foregroundMaxChroma,
1247
+ light: options?.light,
1248
+ dark: options?.dark
1249
+ });
1250
+ }
1251
+
1252
+ // src/generate/generateSemanticColors.ts
1253
+ var POSITIVE_RANGE = [135, 160];
1254
+ var NEGATIVE_RANGE = [5, 25];
1255
+ var WARNING_RANGE = [45, 65];
1256
+ function generateSemanticColors(color, themeType, options) {
1257
+ const positiveBaseColor = parse(
1258
+ options?.semanticColors?.positive ?? _determineBaseColorFromRange(
1259
+ color,
1260
+ options?.semanticColorRanges?.positive ?? POSITIVE_RANGE,
1261
+ { includeInputAsCandidate: true }
1262
+ )
1263
+ );
1264
+ const negativeBaseColor = parse(
1265
+ options?.semanticColors?.negative ?? _determineBaseColorFromRange(
1266
+ color,
1267
+ options?.semanticColorRanges?.negative ?? NEGATIVE_RANGE
1268
+ )
1269
+ );
1270
+ const warningBaseColor = parse(
1271
+ options?.semanticColors?.warning ?? _determineBaseColorFromRange(
1272
+ color,
1273
+ options?.semanticColorRanges?.warning ?? WARNING_RANGE
1274
+ )
1275
+ );
1276
+ const scaleOptions = {
1277
+ light: options?.light,
1278
+ dark: options?.dark,
1279
+ minContrastRatio: options?.minContrastRatio,
1280
+ hueShift: options?.hueShift,
1281
+ foregroundMaxChroma: options?.foregroundMaxChroma
1282
+ };
1283
+ const positiveColorScale = expandColorToScale(
1284
+ positiveBaseColor,
1285
+ themeType,
1286
+ scaleOptions
1287
+ );
1288
+ const negativeColorScale = expandColorToScale(
1289
+ negativeBaseColor,
1290
+ themeType,
1291
+ scaleOptions
1292
+ );
1293
+ const warningColorScale = expandColorToScale(
1294
+ warningBaseColor,
1295
+ themeType,
1296
+ scaleOptions
1297
+ );
1298
+ return {
1299
+ positive: positiveColorScale,
1300
+ negative: negativeColorScale,
1301
+ warning: warningColorScale
1302
+ };
1303
+ }
1304
+ function _determineBaseColorFromRange(color, range, options) {
1305
+ const complementaryColor = _getComplementaryColor(color);
1306
+ const splitComplementaryColors = _getSplitComplementaryColors(complementaryColor);
1307
+ const targetColors = [
1308
+ ...options?.includeInputAsCandidate ? [color] : [],
1309
+ complementaryColor,
1310
+ ...splitComplementaryColors
1311
+ ];
1312
+ for (const targetColor of targetColors) {
1313
+ const h = convert(targetColor, "oklch").h;
1314
+ const arc = (range[1] - range[0] + 360) % 360;
1315
+ const dist = (h - range[0] + 360) % 360;
1316
+ const inRange = dist <= arc;
1317
+ if (inRange) {
1318
+ return convert({ ...convert(color, "oklch"), l: 0.5, h }, "oklch");
1319
+ }
1320
+ }
1321
+ const candidate1 = convert(
1322
+ { ...convert(color, "oklch"), l: 0.5, h: range[0] },
1323
+ "oklch"
1324
+ );
1325
+ const candidate2 = convert(
1326
+ { ...convert(color, "oklch"), l: 0.5, h: range[1] },
1327
+ "oklch"
1328
+ );
1329
+ const dist1 = Math.min(
1330
+ ...targetColors.map((t) => _hueDistance(range[0], convert(t, "oklch").h))
1331
+ );
1332
+ const dist2 = Math.min(
1333
+ ...targetColors.map((t) => _hueDistance(range[1], convert(t, "oklch").h))
1334
+ );
1335
+ return dist1 < dist2 ? candidate1 : candidate2;
1336
+ }
1337
+ function _hueDistance(a, b) {
1338
+ const diff = Math.abs(a - b);
1339
+ return Math.min(diff, 360 - diff);
1340
+ }
1341
+ function _getComplementaryColor(color) {
1342
+ const colorOKLCH = convert(color, "oklch");
1343
+ return convert({ ...colorOKLCH, h: (colorOKLCH.h + 180) % 360 }, "srgb");
1344
+ }
1345
+ function _getSplitComplementaryColors(color) {
1346
+ const colorOKLCH = convert(color, "oklch");
1347
+ return [
1348
+ convert({ ...colorOKLCH, h: (colorOKLCH.h + 150) % 360 }, "srgb"),
1349
+ convert({ ...colorOKLCH, h: (colorOKLCH.h + 210) % 360 }, "srgb")
1350
+ ];
1351
+ }
1352
+
1353
+ // src/generate/generate.ts
1354
+ function generate(color, themeType, options) {
1355
+ return {
1356
+ base: generateBase(color, themeType, options),
1357
+ accent: generateAccent(color, themeType, options),
1358
+ ...generateSemanticColors(color, themeType, options)
1359
+ };
1360
+ }
1361
+
1362
+ // src/HextimatePaletteBuilder.ts
1363
+ var HextimatePaletteBuilder = class _HextimatePaletteBuilder {
1364
+ lightPalette;
1365
+ darkPalette;
1366
+ inputColor;
1367
+ options;
1368
+ operations = [];
1369
+ standaloneTokens = [];
1370
+ weakSideVariants = ["weak"];
1371
+ strongSideVariants = ["strong"];
1372
+ betweenVariants = [];
1373
+ presetFormatDefaults;
1374
+ constructor(color, options) {
1375
+ this.inputColor = color;
1376
+ this.options = options ?? {};
1377
+ this.lightPalette = generate(color, "light", options);
1378
+ this.darkPalette = generate(color, "dark", options);
1379
+ this.applyToken("brand-exact", color);
1380
+ const colorOKLCH = convert(color, "oklch");
1381
+ const lightFg = { ...colorOKLCH, l: 0.97, c: Math.min(colorOKLCH.c, 0.01) };
1382
+ const darkFg = { ...colorOKLCH, l: 0.1, c: Math.min(colorOKLCH.c, 0.01) };
1383
+ const brandForeground = calculateContrast(color, lightFg) >= calculateContrast(color, darkFg) ? lightFg : darkFg;
1384
+ this.applyToken("brand-exact-foreground", brandForeground);
1385
+ }
1386
+ /**
1387
+ * Adds a custom role with given name and color.
1388
+ * The color is expanded into a full scale and added to both light and dark palettes.
1389
+ *
1390
+ * The color can be a direct color value or a derived token referencing an existing role.
1391
+ *
1392
+ * e.g. `addRole('cta', '#ff0066')` adds a "cta" role with the specified hue as the base.
1393
+ * `addRole('cta', { from: 'accent', hue: 180 })` adds a "cta" role with a complementary hue to accent.
1394
+ *
1395
+ * @param name Role name (e.g. "cta", "banner")
1396
+ * @param color Base color or derived token for the role
1397
+ */
1398
+ addRole(name, color) {
1399
+ this.operations.push({ method: "addRole", args: [name, color] });
1400
+ this.applyRole(name, color);
1401
+ return this;
1402
+ }
1403
+ /**
1404
+ * Adds a variant to all roles, derived from an existing variant or placed between two variants.
1405
+ *
1406
+ * e.g. `addVariant('placeholder', { from: 'weak' })` adds a "placeholder" variant one step past "weak" across all roles and themes.
1407
+ * `addVariant('highlight', { between: ['DEFAULT', 'strong'] })` adds a "highlight" variant that is exactly between "DEFAULT" and "strong" across all roles and themes.
1408
+ *
1409
+ * @param name Variant name (e.g. "placeholder", "highlight")
1410
+ * @param placement Placement of the variant, either `{ from: 'weak' }` or `{ between: ['DEFAULT', 'strong'] }`
1411
+ */
1412
+ addVariant(name, placement) {
1413
+ this.operations.push({ method: "addVariant", args: [name, placement] });
1414
+ this.applyVariant(name, placement);
1415
+ return this;
1416
+ }
1417
+ /**
1418
+ * Adds a standalone/one-off token that doesn't fit the role+variant structure.
1419
+ * The value can be a direct color, a derived token based on an existing role+variant, or an object specifying different values for light and dark themes.
1420
+ *
1421
+ * e.g. `addToken('brand', '#3a86ff')` adds a "brand" token with the specified color in both themes.
1422
+ * `addToken('brand', { light: '#3a86ff', dark: '#ff0066' })` adds a "brand" token with different colors in light and dark themes.
1423
+ *
1424
+ * It can also be used to override specific tokens after generation.
1425
+ * `addToken('base-strong', '#ff0066')` overrides the automatically generated "base-strong" variant with a custom color.
1426
+ *
1427
+ * @param name Token name (e.g. "brand", "logo")
1428
+ * @param value Token value, which can be an exact color, or derived from an existing role+variant.
1429
+ */
1430
+ addToken(name, value) {
1431
+ this.operations.push({ method: "addToken", args: [name, value] });
1432
+ this.applyToken(name, value);
1433
+ return this;
1434
+ }
1435
+ /**
1436
+ * Applies a preset that configures roles, tokens, and format defaults
1437
+ * for a specific framework or convention (e.g. shadcn/ui).
1438
+ *
1439
+ * Preset format defaults are used as a base in `.format()` — any options
1440
+ * you pass to `.format()` will override the preset's defaults.
1441
+ *
1442
+ * @example
1443
+ * import { hextimate, presets } from 'hextimator';
1444
+ *
1445
+ * const theme = hextimate('#6366F1')
1446
+ * .preset(presets.shadcn)
1447
+ * .format();
1448
+ *
1449
+ * // Override preset's color format:
1450
+ * const theme = hextimate('#6366F1')
1451
+ * .preset(presets.shadcn)
1452
+ * .format({ colors: 'hsl-raw' });
1453
+ */
1454
+ preset(preset) {
1455
+ this.operations.push({ method: "preset", args: [preset] });
1456
+ if (preset.generation) {
1457
+ const userOptions = { ...this.options };
1458
+ Object.assign(this.options, preset.generation, userOptions);
1459
+ this.lightPalette = generate(
1460
+ this.inputColor,
1461
+ "light",
1462
+ this.resolvedOptions()
1463
+ );
1464
+ this.darkPalette = generate(
1465
+ this.inputColor,
1466
+ "dark",
1467
+ this.resolvedOptions()
1468
+ );
1469
+ }
1470
+ for (const role of preset.roles ?? []) {
1471
+ this.applyRole(role.name, role.color);
1472
+ }
1473
+ for (const variant of preset.variants ?? []) {
1474
+ this.applyVariant(variant.name, variant.placement);
1475
+ }
1476
+ for (const token of preset.tokens ?? []) {
1477
+ this.applyToken(token.name, token.value);
1478
+ }
1479
+ if (preset.format) {
1480
+ this.presetFormatDefaults = {
1481
+ ...this.presetFormatDefaults,
1482
+ ...preset.format,
1483
+ roleNames: {
1484
+ ...this.presetFormatDefaults?.roleNames,
1485
+ ...preset.format.roleNames
1486
+ },
1487
+ variantNames: {
1488
+ ...this.presetFormatDefaults?.variantNames,
1489
+ ...preset.format.variantNames
1490
+ }
1491
+ };
1492
+ }
1493
+ return this;
1494
+ }
1495
+ /**
1496
+ *
1497
+ * Simulates how the palette would look for a given type and severity of CVD.
1498
+ * This is a destructive operation that permanently alters the palette, but can be useful for testing and previewing.
1499
+ *
1500
+ * It should not be used to generate the final output for users with CVD. For that, use `adaptFor` instead.
1501
+ *
1502
+ * e.g. `simulate('deuteranopia', 0.5)` simulates moderate deuteranopia, allowing you to see how the colors would appear to users with that condition.
1503
+ * @param type Type of CVD to simulate (e.g. "protanopia", "deuteranopia", "tritanopia")
1504
+ * @param severity Severity of the CVD simulation, from 0 (no effect) to 1 (full simulation). Defaults to 1 for a complete simulation.
1505
+ */
1506
+ simulate(type, severity = 1) {
1507
+ this.operations.push({ method: "simulate", args: [type, severity] });
1508
+ this.lightPalette = simulatePalette(this.lightPalette, type, severity);
1509
+ this.darkPalette = simulatePalette(this.darkPalette, type, severity);
1510
+ return this;
1511
+ }
1512
+ /**
1513
+ * Adapts the palette for a given type and severity of CVD,
1514
+ * altering the colors to improve accessibility while maintaining as much of the original intent as possible.
1515
+ *
1516
+ * To preview how the original palette would appear to users with CVD, use `simulate`.
1517
+ *
1518
+ * A typical use case would be to generate a normal palette, then fork it and adapt the fork for CVD.
1519
+ * Then you can also preview by chaining `adaptFor` and `simulate`
1520
+ *
1521
+ * e.g.
1522
+ * ```ts
1523
+ * const normalTheme = hextimate('#ff6600');
1524
+ * const cvdTheme = normalTheme.fork().adaptFor('deuteranopia');
1525
+ * const simulatedCVD = normalTheme.fork().simulate('deuteranopia');
1526
+ * ````
1527
+ *
1528
+ * @param type Type of CVD to adapt for (e.g. "protanopia", "deuteranopia", "tritanopia")
1529
+ * @param severity Severity of the CVD adaptation, from 0 (no change) to 1 (full adaptation). Defaults to 1 for a complete adaptation.
1530
+ */
1531
+ adaptFor(type, severity = 1) {
1532
+ this.operations.push({ method: "adaptFor", args: [type, severity] });
1533
+ this.lightPalette = adaptPalette(this.lightPalette, type, severity);
1534
+ this.darkPalette = adaptPalette(this.darkPalette, type, severity);
1535
+ return this;
1536
+ }
1537
+ /**
1538
+ * Creates a new builder instance with the same operations history, allowing you to generate a related palette with a different base color or options.
1539
+ *
1540
+ * e.g. `fork('#ff6677')` creates a new builder with the same
1541
+ * roles, variants, tokens, and adjustments, but based on a different input color.
1542
+ *
1543
+ * `fork({ light: { lightness: 0.8 } })` creates a new builder with the same color but different light theme adjustments.
1544
+ *
1545
+ * @param colorOrOptions Either a new base color for the palette, or an object with new generation options to override (e.g. light/dark adjustments, hue shift, contrast ratio).
1546
+ * @param maybeOptions If the first argument is a color, this can be an optional second argument with generation options to override.
1547
+ * @returns A new builder instance with the same operations history but different base color and/or options.
1548
+ */
1549
+ fork(colorOrOptions, maybeOptions) {
1550
+ let newColor;
1551
+ let newOptions;
1552
+ if (maybeOptions !== void 0) {
1553
+ newColor = parse(colorOrOptions);
1554
+ newOptions = {
1555
+ ...this.options,
1556
+ ...maybeOptions
1557
+ };
1558
+ } else if (colorOrOptions !== void 0 && typeof colorOrOptions === "object" && !Array.isArray(colorOrOptions) && !("space" in colorOrOptions)) {
1559
+ newColor = this.inputColor;
1560
+ newOptions = {
1561
+ ...this.options,
1562
+ ...colorOrOptions
1563
+ };
1564
+ } else if (colorOrOptions !== void 0) {
1565
+ newColor = parse(colorOrOptions);
1566
+ newOptions = { ...this.options };
1567
+ } else {
1568
+ newColor = this.inputColor;
1569
+ newOptions = { ...this.options };
1570
+ }
1571
+ const builder = new _HextimatePaletteBuilder(newColor, newOptions);
1572
+ for (const op of this.operations) {
1573
+ switch (op.method) {
1574
+ case "addRole":
1575
+ builder.addRole(...op.args);
1576
+ break;
1577
+ case "addVariant":
1578
+ builder.addVariant(...op.args);
1579
+ break;
1580
+ case "addToken":
1581
+ builder.addToken(...op.args);
1582
+ break;
1583
+ case "simulate":
1584
+ builder.simulate(...op.args);
1585
+ break;
1586
+ case "adaptFor":
1587
+ builder.adaptFor(...op.args);
1588
+ break;
1589
+ case "preset":
1590
+ builder.preset(...op.args);
1591
+ break;
1592
+ }
1593
+ }
1594
+ return builder;
1595
+ }
1596
+ format(options) {
1597
+ const mergedOptions = this.presetFormatDefaults ? {
1598
+ ...this.presetFormatDefaults,
1599
+ ...options,
1600
+ roleNames: {
1601
+ ...this.presetFormatDefaults.roleNames,
1602
+ ...options?.roleNames
1603
+ },
1604
+ variantNames: {
1605
+ ...this.presetFormatDefaults.variantNames,
1606
+ ...options?.variantNames
1607
+ }
1608
+ } : options;
1609
+ const colorFormat = mergedOptions?.colors ?? "hex";
1610
+ const lightTokens = this.resolveStandaloneTokens(
1611
+ "light",
1612
+ this.lightPalette,
1613
+ colorFormat
1614
+ );
1615
+ const darkTokens = this.resolveStandaloneTokens(
1616
+ "dark",
1617
+ this.darkPalette,
1618
+ colorFormat
1619
+ );
1620
+ return {
1621
+ light: format(this.lightPalette, mergedOptions, lightTokens),
1622
+ dark: format(this.darkPalette, mergedOptions, darkTokens)
1623
+ };
1624
+ }
1625
+ applyRole(name, color) {
1626
+ const parsedColor = this.isDerivedToken(color) ? this.resolveDerivedToken(color, this.lightPalette, "light") : parse(color);
1627
+ const opts = this.resolvedOptions();
1628
+ this.lightPalette[name] = expandColorToScale(parsedColor, "light", {
1629
+ light: opts.light,
1630
+ dark: opts.dark,
1631
+ minContrastRatio: opts.minContrastRatio,
1632
+ hueShift: opts.hueShift
1633
+ });
1634
+ this.darkPalette[name] = expandColorToScale(parsedColor, "dark", {
1635
+ light: opts.light,
1636
+ dark: opts.dark,
1637
+ minContrastRatio: opts.minContrastRatio,
1638
+ hueShift: opts.hueShift
1639
+ });
1640
+ }
1641
+ applyVariant(name, placement) {
1642
+ if ("from" in placement) {
1643
+ const edge = placement.from;
1644
+ for (const palette of [this.lightPalette, this.darkPalette]) {
1645
+ for (const role of Object.keys(palette)) {
1646
+ const scale = palette[role];
1647
+ scale[name] = this.computeBeyondVariant(scale, edge);
1648
+ }
1649
+ }
1650
+ const sideVariants = this.getSideVariantsFor(edge);
1651
+ if (sideVariants) {
1652
+ sideVariants.push(name);
1653
+ this.redistributeAllScales(sideVariants);
1654
+ this.recomputeBetweenVariants();
1655
+ }
1656
+ if (placement.chroma || placement.hue) {
1657
+ for (const palette of [this.lightPalette, this.darkPalette]) {
1658
+ for (const role of Object.keys(palette)) {
1659
+ const scale = palette[role];
1660
+ const oklch = convert(parse(scale[name]), "oklch");
1661
+ scale[name] = {
1662
+ ...oklch,
1663
+ c: Math.max(0, oklch.c + (placement.chroma ?? 0)),
1664
+ h: wrapHue(oklch.h + (placement.hue ?? 0))
1665
+ };
1666
+ }
1667
+ }
1668
+ }
1669
+ } else {
1670
+ this.betweenVariants.push({ name, refs: placement.between });
1671
+ for (const palette of [this.lightPalette, this.darkPalette]) {
1672
+ for (const role of Object.keys(palette)) {
1673
+ const scale = palette[role];
1674
+ scale[name] = this.computeBetweenVariant(
1675
+ scale,
1676
+ placement.between[0],
1677
+ placement.between[1]
1678
+ );
1679
+ }
1680
+ }
1681
+ }
1682
+ }
1683
+ applyToken(name, value) {
1684
+ this.standaloneTokens.push({ name, value });
1685
+ }
1686
+ resolveStandaloneTokens(themeType, palette, colorFormat) {
1687
+ return this.standaloneTokens.map(({ name, value }) => {
1688
+ const color = this.resolveTokenValue(value, themeType, palette);
1689
+ return {
1690
+ role: name,
1691
+ variant: "DEFAULT",
1692
+ isDefault: true,
1693
+ value: serializeColor(color, colorFormat ?? "hex")
1694
+ };
1695
+ });
1696
+ }
1697
+ resolveTokenValue(value, themeType, palette) {
1698
+ if (typeof value === "object" && value !== null && !Array.isArray(value) && "light" in value && "dark" in value) {
1699
+ const themeValue = themeType === "light" ? value.light : value.dark;
1700
+ return this.resolveTokenSingle(themeValue, palette, themeType);
1701
+ }
1702
+ return this.resolveTokenSingle(
1703
+ value,
1704
+ palette,
1705
+ themeType
1706
+ );
1707
+ }
1708
+ resolveTokenSingle(value, palette, themeType) {
1709
+ if (this.isDerivedToken(value)) {
1710
+ return this.resolveDerivedToken(value, palette, themeType);
1711
+ }
1712
+ return parse(value);
1713
+ }
1714
+ isDerivedToken(value) {
1715
+ return typeof value === "object" && value !== null && !Array.isArray(value) && "from" in value;
1716
+ }
1717
+ resolveDerivedToken(token, palette, themeType, resolving) {
1718
+ const [role, variant = "DEFAULT"] = token.from.split(".");
1719
+ const scale = palette[role];
1720
+ let sourceColor;
1721
+ if (scale) {
1722
+ sourceColor = scale[variant];
1723
+ if (!sourceColor) {
1724
+ throw new Error(
1725
+ `Unknown variant "${variant}" in token reference "${token.from}"`
1726
+ );
1727
+ }
1728
+ } else {
1729
+ const standalone = this.standaloneTokens.find((t) => t.name === role);
1730
+ if (!standalone) {
1731
+ throw new Error(
1732
+ `Unknown role "${role}" in token reference "${token.from}"`
1733
+ );
1734
+ }
1735
+ const seen = resolving ?? /* @__PURE__ */ new Set();
1736
+ if (seen.has(role)) {
1737
+ throw new Error(`Circular token reference detected: "${token.from}"`);
1738
+ }
1739
+ seen.add(role);
1740
+ sourceColor = this.resolveTokenValueWithChain(
1741
+ standalone.value,
1742
+ themeType,
1743
+ palette,
1744
+ seen
1745
+ );
1746
+ }
1747
+ const oklch = convert(parse(sourceColor), "oklch");
1748
+ let emphasisOffset = 0;
1749
+ if (token.emphasis) {
1750
+ const contrastDirection = themeType === "light" ? -1 : 1;
1751
+ emphasisOffset = token.emphasis * contrastDirection;
1752
+ }
1753
+ return {
1754
+ ...oklch,
1755
+ l: Math.max(
1756
+ 0,
1757
+ Math.min(1, oklch.l + (token.lightness ?? 0) + emphasisOffset)
1758
+ ),
1759
+ c: Math.max(0, oklch.c + (token.chroma ?? 0)),
1760
+ h: wrapHue(oklch.h + (token.hue ?? 0))
1761
+ };
1762
+ }
1763
+ resolveTokenValueWithChain(value, themeType, palette, resolving) {
1764
+ if (typeof value === "object" && value !== null && !Array.isArray(value) && "light" in value && "dark" in value) {
1765
+ const themeValue = themeType === "light" ? value.light : value.dark;
1766
+ return this.resolveTokenSingleWithChain(
1767
+ themeValue,
1768
+ palette,
1769
+ themeType,
1770
+ resolving
1771
+ );
1772
+ }
1773
+ return this.resolveTokenSingleWithChain(
1774
+ value,
1775
+ palette,
1776
+ themeType,
1777
+ resolving
1778
+ );
1779
+ }
1780
+ resolveTokenSingleWithChain(value, palette, themeType, resolving) {
1781
+ if (this.isDerivedToken(value)) {
1782
+ return this.resolveDerivedToken(value, palette, themeType, resolving);
1783
+ }
1784
+ return parse(value);
1785
+ }
1786
+ getSideVariantsFor(variantName) {
1787
+ if (this.weakSideVariants.includes(variantName)) {
1788
+ return this.weakSideVariants;
1789
+ }
1790
+ if (this.strongSideVariants.includes(variantName)) {
1791
+ return this.strongSideVariants;
1792
+ }
1793
+ const sampleRole = Object.keys(this.lightPalette).find((r) => r !== "base");
1794
+ if (!sampleRole) return null;
1795
+ const scale = this.lightPalette[sampleRole];
1796
+ if (!scale[variantName]) return null;
1797
+ const defaultOKLCH = convert(parse(scale.DEFAULT), "oklch");
1798
+ const edgeOKLCH = convert(parse(scale[variantName]), "oklch");
1799
+ const foregroundOKLCH = convert(parse(scale.foreground), "oklch");
1800
+ const isTowardForeground = Math.sign(edgeOKLCH.l - defaultOKLCH.l) === Math.sign(foregroundOKLCH.l - defaultOKLCH.l);
1801
+ return isTowardForeground ? this.strongSideVariants : this.weakSideVariants;
1802
+ }
1803
+ redistributeAllScales(sideVariants) {
1804
+ const isStrongSide = sideVariants === this.strongSideVariants;
1805
+ const totalVariants = this.weakSideVariants.length + this.strongSideVariants.length;
1806
+ const rawShift = this.options.hueShift ?? 0;
1807
+ const clampedShift = clampHueShift(rawShift, totalVariants);
1808
+ const hueShiftPerStep = isStrongSide ? clampedShift : -clampedShift;
1809
+ const contrastTarget = resolveContrastRatio(this.options.minContrastRatio) + 0.15;
1810
+ for (const role of Object.keys(this.lightPalette)) {
1811
+ for (const [palette, themeType] of [
1812
+ [this.lightPalette, "light"],
1813
+ [this.darkPalette, "dark"]
1814
+ ]) {
1815
+ const scale = palette[role];
1816
+ const defaultOKLCH = convert(parse(scale.DEFAULT), "oklch");
1817
+ const foregroundOKLCH = convert(parse(scale.foreground), "oklch");
1818
+ const existingVariant = sideVariants.find((v) => scale[v]);
1819
+ let sideDirection;
1820
+ if (existingVariant) {
1821
+ const existingL = convert(parse(scale[existingVariant]), "oklch").l;
1822
+ sideDirection = Math.sign(existingL - defaultOKLCH.l) || (themeType === "light" ? 1 : -1);
1823
+ } else {
1824
+ const contrastDirection = themeType === "light" ? -1 : 1;
1825
+ sideDirection = isStrongSide ? contrastDirection : -contrastDirection;
1826
+ }
1827
+ const foregroundDirection = Math.sign(
1828
+ foregroundOKLCH.l - defaultOKLCH.l
1829
+ );
1830
+ const isTowardForeground = sideDirection === foregroundDirection;
1831
+ let outermostDelta = 0;
1832
+ for (const v of sideVariants) {
1833
+ if (!scale[v]) continue;
1834
+ const vL = convert(parse(scale[v]), "oklch").l;
1835
+ const d = Math.abs(vL - defaultOKLCH.l);
1836
+ if (d > outermostDelta) outermostDelta = d;
1837
+ }
1838
+ let maxDelta;
1839
+ if (isTowardForeground) {
1840
+ const boundaryL = findContrastBoundaryLightness(
1841
+ parse(scale.DEFAULT),
1842
+ parse(scale.foreground),
1843
+ contrastTarget
1844
+ );
1845
+ const boundaryDelta = boundaryL !== null ? Math.abs(defaultOKLCH.l - boundaryL) : 0;
1846
+ maxDelta = Math.min(outermostDelta, boundaryDelta);
1847
+ } else {
1848
+ const gamutBound = sideDirection > 0 ? 1 : 0;
1849
+ const boundaryDelta = Math.min(
1850
+ Math.abs(gamutBound - defaultOKLCH.l),
1851
+ 0.2
1852
+ );
1853
+ maxDelta = Math.min(outermostDelta, boundaryDelta);
1854
+ }
1855
+ this.redistributeVariants(
1856
+ scale,
1857
+ sideVariants,
1858
+ defaultOKLCH,
1859
+ maxDelta * sideDirection,
1860
+ hueShiftPerStep,
1861
+ foregroundOKLCH,
1862
+ contrastTarget
1863
+ );
1864
+ }
1865
+ }
1866
+ }
1867
+ /**
1868
+ * Distributes variants evenly between DEFAULT and the lightness boundary.
1869
+ * Uses (i+1)/(n+1) spacing so variants never sit exactly at the boundary,
1870
+ * preserving AAA contrast margin.
1871
+ */
1872
+ redistributeVariants(scale, sideVariants, defaultOKLCH, totalDelta, hueShiftPerStep = 0, foregroundOKLCH, contrastTarget) {
1873
+ if (sideVariants.length < 1) return;
1874
+ const sourceChroma = Math.max(
1875
+ ...Object.entries(scale).filter(([k]) => k !== "foreground").map(([, v]) => convert(parse(v), "oklch").c)
1876
+ );
1877
+ const n = sideVariants.length;
1878
+ const sorted = [...sideVariants].sort((a, b) => {
1879
+ const aL = Math.abs(convert(parse(scale[a]), "oklch").l - defaultOKLCH.l);
1880
+ const bL = Math.abs(convert(parse(scale[b]), "oklch").l - defaultOKLCH.l);
1881
+ return aL - bL;
1882
+ });
1883
+ for (let i = 0; i < n; i++) {
1884
+ const variantName = sorted[i];
1885
+ let newL = defaultOKLCH.l + (i + 1) / (n + 1) * totalDelta;
1886
+ const newH = wrapHue(defaultOKLCH.h + hueShiftPerStep * (i + 1));
1887
+ let variant = {
1888
+ ...defaultOKLCH,
1889
+ l: Math.max(0, Math.min(1, newL)),
1890
+ c: sourceChroma,
1891
+ h: newH
1892
+ };
1893
+ if (hueShiftPerStep !== 0 && foregroundOKLCH && contrastTarget && calculateContrast(variant, foregroundOKLCH) < contrastTarget) {
1894
+ const direction = foregroundOKLCH.l < variant.l ? 1 : -1;
1895
+ let lo = direction === 1 ? variant.l : 0;
1896
+ let hi = direction === 1 ? 1 : variant.l;
1897
+ for (let j = 0; j < 20; j++) {
1898
+ const mid = (lo + hi) / 2;
1899
+ const test = { ...variant, l: mid };
1900
+ if (calculateContrast(test, foregroundOKLCH) >= contrastTarget) {
1901
+ if (direction === 1) hi = mid;
1902
+ else lo = mid;
1903
+ } else {
1904
+ if (direction === 1) lo = mid;
1905
+ else hi = mid;
1906
+ }
1907
+ }
1908
+ newL = (lo + hi) / 2;
1909
+ variant = { ...variant, l: Math.max(0, Math.min(1, newL)) };
1910
+ }
1911
+ scale[variantName] = variant;
1912
+ }
1913
+ }
1914
+ recomputeBetweenVariants() {
1915
+ for (const bv of this.betweenVariants) {
1916
+ for (const palette of [this.lightPalette, this.darkPalette]) {
1917
+ for (const role of Object.keys(palette)) {
1918
+ const scale = palette[role];
1919
+ scale[bv.name] = this.computeBetweenVariant(
1920
+ scale,
1921
+ bv.refs[0],
1922
+ bv.refs[1]
1923
+ );
1924
+ }
1925
+ }
1926
+ }
1927
+ }
1928
+ computeBeyondVariant(scale, edge) {
1929
+ const edgeColor = convert(parse(scale[edge]), "oklch");
1930
+ const defaultColor = convert(parse(scale.DEFAULT), "oklch");
1931
+ const delta = edgeColor.l - defaultColor.l;
1932
+ const newL = Math.max(0, Math.min(1, edgeColor.l + delta));
1933
+ return { ...edgeColor, l: newL };
1934
+ }
1935
+ computeBetweenVariant(scale, variantA, variantB) {
1936
+ const colorA = convert(parse(scale[variantA]), "oklch");
1937
+ const colorB = convert(parse(scale[variantB]), "oklch");
1938
+ const midL = (colorA.l + colorB.l) / 2;
1939
+ const midC = (colorA.c + colorB.c) / 2;
1940
+ const hueDiff = (colorB.h - colorA.h + 540) % 360 - 180;
1941
+ const midH = wrapHue(colorA.h + hueDiff / 2);
1942
+ return { ...colorA, l: midL, c: midC, h: midH };
1943
+ }
1944
+ resolvedOptions() {
1945
+ return { ...this.options };
1946
+ }
1947
+ };
1948
+
1949
+ // src/presets/demo.ts
1950
+ var demo = {
1951
+ generation: {
1952
+ minContrastRatio: "AA",
1953
+ baseHueShift: 180,
1954
+ baseMaxChroma: 0.025,
1955
+ hueShift: 10,
1956
+ light: { lightness: 0.55, maxChroma: 0.14 },
1957
+ dark: { lightness: 0.35, maxChroma: 0.12 }
1958
+ },
1959
+ roles: [
1960
+ { name: "cta", color: "#ff006e" },
1961
+ { name: "info", color: "#3a86ff" }
1962
+ ],
1963
+ variants: [
1964
+ { name: "muted", placement: { from: "weak" } },
1965
+ { name: "vivid", placement: { from: "strong" } }
1966
+ ],
1967
+ tokens: [
1968
+ { name: "surface", value: { from: "base.weak" } },
1969
+ { name: "surface-raised", value: { from: "base", emphasis: -0.04 } },
1970
+ { name: "border", value: { from: "base", emphasis: 0.1 } },
1971
+ { name: "border-subtle", value: { from: "base", emphasis: 0.05 } },
1972
+ { name: "ring", value: { from: "accent" } },
1973
+ { name: "cta-ring", value: { from: "cta", chroma: -0.05 } }
1974
+ ],
1975
+ format: {
1976
+ as: "css",
1977
+ colors: "hex",
1978
+ roleNames: {
1979
+ base: "bg",
1980
+ accent: "brand",
1981
+ positive: "success",
1982
+ negative: "danger"
1983
+ },
1984
+ variantNames: {
1985
+ foreground: "text"
1986
+ }
1987
+ }
1988
+ };
1989
+
1990
+ // src/presets/mui.ts
1991
+ var mui = {
1992
+ roles: [
1993
+ { name: "info", color: "#0288d1" },
1994
+ { name: "secondary", color: { from: "accent", hue: 180 } }
1995
+ ],
1996
+ tokens: [
1997
+ // background
1998
+ { name: "background-default", value: { from: "base" } },
1999
+ {
2000
+ name: "background-paper",
2001
+ value: { from: "base", emphasis: -0.025 }
2002
+ },
2003
+ // text
2004
+ { name: "text-primary", value: { from: "base.foreground" } },
2005
+ {
2006
+ name: "text-secondary",
2007
+ value: { from: "base.foreground", emphasis: -0.2 }
2008
+ },
2009
+ {
2010
+ name: "text-disabled",
2011
+ value: { from: "base.foreground", emphasis: -0.4 }
2012
+ },
2013
+ // divider
2014
+ { name: "divider", value: { from: "base", emphasis: 0.12 } },
2015
+ // action tokens
2016
+ { name: "action-hover", value: { from: "base", emphasis: 0.04 } },
2017
+ { name: "action-selected", value: { from: "base", emphasis: 0.08 } },
2018
+ { name: "action-disabled", value: { from: "base", emphasis: 0.15 } },
2019
+ {
2020
+ name: "action-disabledBackground",
2021
+ value: { from: "base", emphasis: 0.06 }
2022
+ },
2023
+ { name: "action-focus", value: { from: "base", emphasis: 0.06 } }
2024
+ ],
2025
+ format: {
2026
+ as: "object",
2027
+ colors: "hex",
2028
+ separator: "-",
2029
+ roleNames: {
2030
+ accent: "primary",
2031
+ negative: "error",
2032
+ positive: "success"
2033
+ },
2034
+ variantNames: {
2035
+ DEFAULT: "main",
2036
+ weak: "light",
2037
+ strong: "dark",
2038
+ foreground: "contrastText"
2039
+ }
2040
+ }
2041
+ };
2042
+
2043
+ // src/presets/shadcn.ts
2044
+ var shadcn = {
2045
+ tokens: [
2046
+ // shadcn uses bare --foreground (not --background-foreground)
2047
+ { name: "foreground", value: { from: "base.foreground" } },
2048
+ // card / popover — same as background
2049
+ { name: "card", value: { from: "base" } },
2050
+ { name: "card-foreground", value: { from: "base.foreground" } },
2051
+ { name: "popover", value: { from: "base" } },
2052
+ { name: "popover-foreground", value: { from: "base.foreground" } },
2053
+ // secondary — subtle contrast from background
2054
+ { name: "secondary", value: { from: "base" } },
2055
+ { name: "secondary-foreground", value: { from: "base.foreground" } },
2056
+ // muted — similar to secondary, but with dimmer foreground
2057
+ { name: "muted", value: { from: "base" } },
2058
+ {
2059
+ name: "muted-foreground",
2060
+ value: {
2061
+ from: "base.foreground",
2062
+ emphasis: -0.25
2063
+ }
2064
+ },
2065
+ // accent (shadcn meaning: subtle hover highlight, not the brand color)
2066
+ { name: "accent", value: { from: "base" } },
2067
+ { name: "accent-foreground", value: { from: "base.foreground" } },
2068
+ // border / input
2069
+ {
2070
+ name: "border",
2071
+ value: { from: "base", emphasis: 0.8 }
2072
+ },
2073
+ {
2074
+ name: "input",
2075
+ value: {
2076
+ from: "base",
2077
+ emphasis: 0.8
2078
+ }
2079
+ },
2080
+ // ring — uses the brand/primary color
2081
+ { name: "ring", value: { from: "accent" } },
2082
+ // chart tokens colors use brand/priamry color
2083
+ { name: "chart-1", value: { from: "accent" } },
2084
+ { name: "chart-2", value: { from: "chart-1", emphasis: -0.04 } },
2085
+ { name: "chart-3", value: { from: "chart-2", emphasis: -0.04 } },
2086
+ { name: "chart-4", value: { from: "chart-3", emphasis: -0.04 } },
2087
+ { name: "chart-5", value: { from: "chart-4", emphasis: -0.04 } }
2088
+ ],
2089
+ format: {
2090
+ as: "css",
2091
+ colors: "oklch",
2092
+ roleNames: {
2093
+ base: "background",
2094
+ accent: "primary",
2095
+ negative: "destructive",
2096
+ positive: "success"
2097
+ },
2098
+ variantNames: {
2099
+ foreground: "foreground"
2100
+ }
2101
+ }
2102
+ };
2103
+
2104
+ // src/index.ts
2105
+ var HextimateError = class extends Error {
2106
+ constructor(input, message) {
2107
+ super(message ?? `Failed to hextimate color: ${String(input)}`);
2108
+ this.input = input;
2109
+ this.name = "HextimateError";
2110
+ }
2111
+ };
2112
+ function hextimate(color, options) {
2113
+ try {
2114
+ const parsedColor = parse(color);
2115
+ return new HextimatePaletteBuilder(parsedColor, options);
2116
+ } catch (e) {
2117
+ if (e instanceof HextimateError) {
2118
+ throw e;
2119
+ }
2120
+ throw new HextimateError(
2121
+ color,
2122
+ e instanceof Error ? e.message : "Unknown error"
2123
+ );
2124
+ }
2125
+ }
2126
+
2127
+ // src/cli.ts
2128
+ var __dirname = dirname(fileURLToPath(import.meta.url));
2129
+ var PKG_VERSION = JSON.parse(
2130
+ readFileSync(join(__dirname, "..", "package.json"), "utf8")
2131
+ ).version;
2132
+ var AVAILABLE_PRESETS = {
2133
+ shadcn,
2134
+ mui,
2135
+ demo
2136
+ };
2137
+ var HELP = `
2138
+ hextimator <color> [options]
2139
+
2140
+ Generate a perceptually uniform color palette from a single color.
2141
+
2142
+ Arguments:
2143
+ color Input color (quote hex values: '#ff6600')
2144
+
2145
+ Presets:
2146
+ -p, --preset <name> Apply a preset: shadcn, mui, demo
2147
+ Preset format defaults can be overridden with -f, -c, etc.
2148
+
2149
+ Format options:
2150
+ -f, --format <type> css | object | tailwind | tailwind-css | scss | json (default: css)
2151
+ -c, --colors <type> hex | rgb | hsl | oklch | p3 and -raw variants (default: hex)
2152
+ -t, --theme <type> light | dark | both (default: both)
2153
+ --separator <char> Token separator (default: -)
2154
+
2155
+ Generation options:
2156
+ --base-color <color> Override base/neutral color
2157
+ --base-hue-shift <deg> Rotate base hue relative to accent
2158
+ --hue-shift <deg> Per-variant hue shift in degrees
2159
+ --base-max-chroma <n> Max chroma for base colors (default: 0.01)
2160
+ --fg-max-chroma <n> Max chroma for foreground colors (default: 0.01)
2161
+ --light-lightness <n> Light theme lightness 0-1 (default: 0.7)
2162
+ --light-max-chroma <n> Light theme max chroma
2163
+ --dark-lightness <n> Dark theme lightness 0-1 (default: 0.6)
2164
+ --dark-max-chroma <n> Dark theme max chroma
2165
+ --min-contrast <value> AAA | AA | <number> (default: AAA)
2166
+ --invert-dark Swap base/accent hues in dark mode (requires --base-color)
2167
+
2168
+ Semantic colors:
2169
+ --positive <color> Override positive/success color (default: auto green)
2170
+ --negative <color> Override negative/error color (default: auto red)
2171
+ --warning <color> Override warning color (default: auto amber)
2172
+
2173
+ CVD (color vision deficiency):
2174
+ --simulate <type> Simulate CVD: protanopia | deuteranopia | tritanopia | achromatopsia
2175
+ --adapt <type> Adapt palette for CVD type
2176
+ --cvd-severity <n> CVD severity 0-1 (default: 1)
2177
+
2178
+ Roles & variants (repeatable):
2179
+ --role <name>=<color> Add a custom role
2180
+ --variant <spec> from: "name:from:edge"
2181
+ between: "name:between:a,b"
2182
+
2183
+ Output:
2184
+ -o, --output <path> Write to file instead of stdout
2185
+ -h, --help Show this help
2186
+ -v, --version Show version
2187
+
2188
+ Examples:
2189
+ hextimator '#ff6600'
2190
+ hextimator '#ff6600' --format tailwind-css --colors oklch
2191
+ hextimator '#3366cc' --format json --theme light
2192
+ hextimator '#6366F1' --preset shadcn
2193
+ hextimator '#6366F1' --preset shadcn --colors hsl-raw
2194
+ hextimator '#22aa44' --role cta=#ee2244 --variant hover:from:strong -o theme.css
2195
+ hextimator '#6A5ACD' --base-color '#FEBA5D' --invert-dark
2196
+ hextimator '#ff6600' --simulate deuteranopia
2197
+ hextimator '#ff6600' --adapt deuteranopia --cvd-severity 0.8
2198
+ `.trim();
2199
+ function run() {
2200
+ const { values, positionals } = parseArgs({
2201
+ allowPositionals: true,
2202
+ options: {
2203
+ preset: { type: "string", short: "p" },
2204
+ format: { type: "string", short: "f" },
2205
+ colors: { type: "string", short: "c" },
2206
+ theme: { type: "string", short: "t", default: "both" },
2207
+ separator: { type: "string" },
2208
+ "base-color": { type: "string" },
2209
+ "base-hue-shift": { type: "string" },
2210
+ "hue-shift": { type: "string" },
2211
+ "base-max-chroma": { type: "string" },
2212
+ "fg-max-chroma": { type: "string" },
2213
+ "light-lightness": { type: "string" },
2214
+ "light-max-chroma": { type: "string" },
2215
+ "dark-lightness": { type: "string" },
2216
+ "dark-max-chroma": { type: "string" },
2217
+ "min-contrast": { type: "string" },
2218
+ "invert-dark": { type: "boolean" },
2219
+ positive: { type: "string" },
2220
+ negative: { type: "string" },
2221
+ warning: { type: "string" },
2222
+ simulate: { type: "string" },
2223
+ adapt: { type: "string" },
2224
+ "cvd-severity": { type: "string" },
2225
+ role: { type: "string", multiple: true },
2226
+ variant: { type: "string", multiple: true },
2227
+ output: { type: "string", short: "o" },
2228
+ help: { type: "boolean", short: "h" },
2229
+ version: { type: "boolean", short: "v" }
2230
+ }
2231
+ });
2232
+ if (values.help) {
2233
+ console.log(HELP);
2234
+ process.exit(0);
2235
+ }
2236
+ if (values.version) {
2237
+ console.log(PKG_VERSION);
2238
+ process.exit(0);
2239
+ }
2240
+ const color = positionals[0];
2241
+ if (!color) {
2242
+ console.error("Error: missing color argument. Run with --help for usage.");
2243
+ process.exit(1);
2244
+ }
2245
+ const generationOptions = {};
2246
+ if (values["base-color"]) generationOptions.baseColor = values["base-color"];
2247
+ if (values["base-hue-shift"])
2248
+ generationOptions.baseHueShift = Number(values["base-hue-shift"]);
2249
+ if (values["hue-shift"])
2250
+ generationOptions.hueShift = Number(values["hue-shift"]);
2251
+ if (values["base-max-chroma"])
2252
+ generationOptions.baseMaxChroma = Number(values["base-max-chroma"]);
2253
+ if (values["fg-max-chroma"])
2254
+ generationOptions.foregroundMaxChroma = Number(values["fg-max-chroma"]);
2255
+ if (values["light-lightness"] || values["light-max-chroma"]) {
2256
+ generationOptions.light = {};
2257
+ if (values["light-lightness"])
2258
+ generationOptions.light.lightness = Number(values["light-lightness"]);
2259
+ if (values["light-max-chroma"])
2260
+ generationOptions.light.maxChroma = Number(values["light-max-chroma"]);
2261
+ }
2262
+ if (values["dark-lightness"] || values["dark-max-chroma"]) {
2263
+ generationOptions.dark = {};
2264
+ if (values["dark-lightness"])
2265
+ generationOptions.dark.lightness = Number(values["dark-lightness"]);
2266
+ if (values["dark-max-chroma"])
2267
+ generationOptions.dark.maxChroma = Number(values["dark-max-chroma"]);
2268
+ }
2269
+ if (values["min-contrast"]) {
2270
+ const mc = values["min-contrast"];
2271
+ if (mc === "AAA" || mc === "AA") {
2272
+ generationOptions.minContrastRatio = mc;
2273
+ } else {
2274
+ generationOptions.minContrastRatio = Number(mc);
2275
+ }
2276
+ }
2277
+ if (values["invert-dark"]) {
2278
+ generationOptions.invertDarkModeBaseAccent = true;
2279
+ }
2280
+ const semanticColors = {};
2281
+ if (values.positive) semanticColors.positive = values.positive;
2282
+ if (values.negative) semanticColors.negative = values.negative;
2283
+ if (values.warning) semanticColors.warning = values.warning;
2284
+ if (Object.keys(semanticColors).length > 0) {
2285
+ generationOptions.semanticColors = semanticColors;
2286
+ }
2287
+ const builder = hextimate(color, generationOptions);
2288
+ if (values.preset) {
2289
+ const preset = AVAILABLE_PRESETS[values.preset];
2290
+ if (!preset) {
2291
+ console.error(
2292
+ `Error: unknown preset "${values.preset}". Available: ${Object.keys(AVAILABLE_PRESETS).join(", ")}`
2293
+ );
2294
+ process.exit(1);
2295
+ }
2296
+ builder.preset(preset);
2297
+ }
2298
+ if (values.role) {
2299
+ for (const r of values.role) {
2300
+ const eq = r.indexOf("=");
2301
+ if (eq === -1) {
2302
+ console.error(
2303
+ `Error: invalid --role "${r}". Expected format: name=color`
2304
+ );
2305
+ process.exit(1);
2306
+ }
2307
+ builder.addRole(r.slice(0, eq), r.slice(eq + 1));
2308
+ }
2309
+ }
2310
+ if (values.variant) {
2311
+ for (const v of values.variant) {
2312
+ const parts = v.split(":");
2313
+ if (parts.length < 3) {
2314
+ console.error(
2315
+ `Error: invalid --variant "${v}". Expected "name:from:edge" or "name:between:a,b"`
2316
+ );
2317
+ process.exit(1);
2318
+ }
2319
+ const [name, type, ref] = parts;
2320
+ if (type === "from") {
2321
+ builder.addVariant(name, { from: ref });
2322
+ } else if (type === "between") {
2323
+ const refs = ref.split(",");
2324
+ if (refs.length !== 2) {
2325
+ console.error(
2326
+ `Error: invalid --variant between spec "${v}". Expected "name:between:a,b"`
2327
+ );
2328
+ process.exit(1);
2329
+ }
2330
+ builder.addVariant(name, { between: [refs[0], refs[1]] });
2331
+ } else {
2332
+ console.error(
2333
+ `Error: invalid --variant type "${type}". Expected "from" or "between"`
2334
+ );
2335
+ process.exit(1);
2336
+ }
2337
+ }
2338
+ }
2339
+ const cvdSeverity = values["cvd-severity"] ? Number(values["cvd-severity"]) : 1;
2340
+ if (values.simulate) {
2341
+ builder.simulate(
2342
+ values.simulate,
2343
+ cvdSeverity
2344
+ );
2345
+ }
2346
+ if (values.adapt) {
2347
+ builder.adaptFor(
2348
+ values.adapt,
2349
+ cvdSeverity
2350
+ );
2351
+ }
2352
+ const hasPreset = !!values.preset;
2353
+ const formatOptions = {};
2354
+ if (values.format) {
2355
+ formatOptions.as = values.format;
2356
+ } else if (!hasPreset) {
2357
+ formatOptions.as = "css";
2358
+ }
2359
+ if (values.colors) {
2360
+ formatOptions.colors = values.colors;
2361
+ } else if (!hasPreset) {
2362
+ formatOptions.colors = "hex";
2363
+ }
2364
+ if (values.separator) {
2365
+ formatOptions.separator = values.separator;
2366
+ } else if (!hasPreset) {
2367
+ formatOptions.separator = "-";
2368
+ }
2369
+ const result = builder.format(formatOptions);
2370
+ const themeFilter = values.theme;
2371
+ let output;
2372
+ if (themeFilter === "light") {
2373
+ output = serialize(result.light);
2374
+ } else if (themeFilter === "dark") {
2375
+ output = serialize(result.dark);
2376
+ } else {
2377
+ output = serialize({ light: result.light, dark: result.dark });
2378
+ }
2379
+ if (values.output) {
2380
+ writeFileSync(values.output, `${output}
2381
+ `);
2382
+ } else {
2383
+ console.log(output);
2384
+ }
2385
+ }
2386
+ function serialize(value) {
2387
+ if (typeof value === "string") return value;
2388
+ return JSON.stringify(value, null, 2);
2389
+ }
2390
+ try {
2391
+ run();
2392
+ } catch (err) {
2393
+ console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
2394
+ process.exit(1);
2395
+ }