ux-color-engine 1.0.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.
@@ -0,0 +1,970 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __export = (target, all) => {
6
+ for (var name in all)
7
+ __defProp(target, name, { get: all[name], enumerable: true });
8
+ };
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
18
+
19
+ // src/index.js
20
+ var index_exports = {};
21
+ __export(index_exports, {
22
+ applyCvd: () => applyCvd,
23
+ contrastRatio: () => contrastRatio,
24
+ deltaEoklab: () => deltaEoklab,
25
+ hexToOklch: () => hexToOklch,
26
+ hexToRgb: () => hexToRgb,
27
+ linearRgbToOklab: () => linearRgbToOklab,
28
+ linearRgbToRgb: () => linearRgbToRgb,
29
+ linearToSrgb: () => linearToSrgb,
30
+ oklabToLinearRgb: () => oklabToLinearRgb,
31
+ oklabToOklch: () => oklabToOklch,
32
+ oklchToHex: () => oklchToHex,
33
+ oklchToOklab: () => oklchToOklab,
34
+ recommendTokensDual: () => recommendTokensDual,
35
+ recommendTokensDualAsJson: () => recommendTokensDualAsJson,
36
+ relativeLuminance: () => relativeLuminance,
37
+ rgbToHex: () => rgbToHex,
38
+ rgbToLinearRgb: () => rgbToLinearRgb,
39
+ srgbToLinear: () => srgbToLinear,
40
+ validateTokens: () => validateTokens
41
+ });
42
+ module.exports = __toCommonJS(index_exports);
43
+ var RNG = class {
44
+ constructor(seed = 123456789) {
45
+ this.state = seed >>> 0;
46
+ }
47
+ next() {
48
+ let x = this.state;
49
+ x ^= x << 13;
50
+ x ^= x >>> 17;
51
+ x ^= x << 5;
52
+ this.state = x >>> 0;
53
+ return (this.state >>> 0) / 4294967295;
54
+ }
55
+ int(lo, hi) {
56
+ return Math.floor(this.next() * (hi - lo + 1)) + lo;
57
+ }
58
+ float(lo, hi) {
59
+ return this.next() * (hi - lo) + lo;
60
+ }
61
+ pick(arr) {
62
+ return arr[this.int(0, arr.length - 1)];
63
+ }
64
+ };
65
+ function clamp(x, lo, hi) {
66
+ return Math.min(hi, Math.max(lo, x));
67
+ }
68
+ function mod360(h) {
69
+ let x = h % 360;
70
+ if (x < 0) x += 360;
71
+ return x;
72
+ }
73
+ function hueDistance(a, b) {
74
+ const d = Math.abs(mod360(a) - mod360(b));
75
+ return Math.min(d, 360 - d);
76
+ }
77
+ function hexToRgb(hex) {
78
+ const h = hex.trim().replace(/^#/, "");
79
+ if (h.length !== 6) throw new Error(`Invalid hex: ${hex}`);
80
+ const r = parseInt(h.slice(0, 2), 16) / 255;
81
+ const g = parseInt(h.slice(2, 4), 16) / 255;
82
+ const b = parseInt(h.slice(4, 6), 16) / 255;
83
+ return { r, g, b };
84
+ }
85
+ function rgbToHex(rgb) {
86
+ const r = clamp(rgb.r, 0, 1);
87
+ const g = clamp(rgb.g, 0, 1);
88
+ const b = clamp(rgb.b, 0, 1);
89
+ const ri = Math.round(r * 255);
90
+ const gi = Math.round(g * 255);
91
+ const bi = Math.round(b * 255);
92
+ return ("#" + ri.toString(16).padStart(2, "0") + gi.toString(16).padStart(2, "0") + bi.toString(16).padStart(2, "0")).toUpperCase();
93
+ }
94
+ function srgbToLinear(c) {
95
+ if (c <= 0.04045) return c / 12.92;
96
+ return Math.pow((c + 0.055) / 1.055, 2.4);
97
+ }
98
+ function linearToSrgb(c) {
99
+ if (c <= 31308e-7) return 12.92 * c;
100
+ return 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
101
+ }
102
+ function rgbToLinearRgb(rgb) {
103
+ return { r: srgbToLinear(rgb.r), g: srgbToLinear(rgb.g), b: srgbToLinear(rgb.b) };
104
+ }
105
+ function linearRgbToRgb(rgb) {
106
+ return { r: linearToSrgb(rgb.r), g: linearToSrgb(rgb.g), b: linearToSrgb(rgb.b) };
107
+ }
108
+ function linearRgbToOklab(rgb) {
109
+ const r = rgb.r, g = rgb.g, b = rgb.b;
110
+ const l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;
111
+ const m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b;
112
+ const s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b;
113
+ const l_ = Math.cbrt(l);
114
+ const m_ = Math.cbrt(m);
115
+ const s_ = Math.cbrt(s);
116
+ const L = 0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_;
117
+ const a = 1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_;
118
+ const b2 = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_;
119
+ return { L, a, b: b2 };
120
+ }
121
+ function oklabToLinearRgb(lab) {
122
+ const L = lab.L, a = lab.a, b = lab.b;
123
+ const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
124
+ const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
125
+ const s_ = L - 0.0894841775 * a - 1.291485548 * b;
126
+ const l = l_ * l_ * l_;
127
+ const m = m_ * m_ * m_;
128
+ const s = s_ * s_ * s_;
129
+ const r = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s;
130
+ const g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s;
131
+ const b2 = -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s;
132
+ return { r, g, b: b2 };
133
+ }
134
+ function oklabToOklch(lab) {
135
+ const C = Math.sqrt(lab.a * lab.a + lab.b * lab.b);
136
+ const H = mod360(Math.atan2(lab.b, lab.a) * 180 / Math.PI);
137
+ return { L: lab.L, C, H };
138
+ }
139
+ function oklchToOklab(lch2) {
140
+ const a = lch2.C * Math.cos(lch2.H * Math.PI / 180);
141
+ const b = lch2.C * Math.sin(lch2.H * Math.PI / 180);
142
+ return { L: lch2.L, a, b };
143
+ }
144
+ function hexToOklch(hex) {
145
+ const rgb = hexToRgb(hex);
146
+ const lin = rgbToLinearRgb(rgb);
147
+ const lab = linearRgbToOklab(lin);
148
+ return oklabToOklch(lab);
149
+ }
150
+ function oklchToHex(lch2) {
151
+ const lab = oklchToOklab(lch2);
152
+ const lin = oklabToLinearRgb(lab);
153
+ const rgb = linearRgbToRgb(lin);
154
+ const inGamut = rgb.r >= 0 && rgb.r <= 1 && rgb.g >= 0 && rgb.g <= 1 && rgb.b >= 0 && rgb.b <= 1;
155
+ return { hex: rgbToHex(rgb), inGamut };
156
+ }
157
+ function relativeLuminance(rgb) {
158
+ const r = srgbToLinear(rgb.r);
159
+ const g = srgbToLinear(rgb.g);
160
+ const b = srgbToLinear(rgb.b);
161
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
162
+ }
163
+ function contrastRatio(fgHex, bgHex) {
164
+ const fg = hexToRgb(fgHex);
165
+ const bg = hexToRgb(bgHex);
166
+ const L1 = relativeLuminance(fg);
167
+ const L2 = relativeLuminance(bg);
168
+ const lighter = Math.max(L1, L2);
169
+ const darker = Math.min(L1, L2);
170
+ return (lighter + 0.05) / (darker + 0.05);
171
+ }
172
+ function targetContrast(target) {
173
+ return {
174
+ normal: target === "AAA" ? 7 : 4.5,
175
+ large: target === "AAA" ? 4.5 : 3
176
+ };
177
+ }
178
+ var CVD_MATS = {
179
+ protan: [
180
+ [0.56667, 0.43333, 0],
181
+ [0.55833, 0.44167, 0],
182
+ [0, 0.24167, 0.75833]
183
+ ],
184
+ deutan: [
185
+ [0.625, 0.375, 0],
186
+ [0.7, 0.3, 0],
187
+ [0, 0.3, 0.7]
188
+ ],
189
+ tritan: [
190
+ [0.95, 0.05, 0],
191
+ [0, 0.43333, 0.56667],
192
+ [0, 0.475, 0.525]
193
+ ]
194
+ };
195
+ function applyCvd(hex, mode) {
196
+ if (mode === "none") return hex.toUpperCase();
197
+ const mat = CVD_MATS[mode];
198
+ const rgb = hexToRgb(hex);
199
+ const r = clamp(mat[0][0] * rgb.r + mat[0][1] * rgb.g + mat[0][2] * rgb.b, 0, 1);
200
+ const g = clamp(mat[1][0] * rgb.r + mat[1][1] * rgb.g + mat[1][2] * rgb.b, 0, 1);
201
+ const b = clamp(mat[2][0] * rgb.r + mat[2][1] * rgb.g + mat[2][2] * rgb.b, 0, 1);
202
+ return rgbToHex({ r, g, b });
203
+ }
204
+ function deltaEoklab(hex1, hex2) {
205
+ const lch1 = hexToOklch(hex1);
206
+ const lch2 = hexToOklch(hex2);
207
+ const lab1 = oklchToOklab(lch1);
208
+ const lab2 = oklchToOklab(lch2);
209
+ const dL = lab1.L - lab2.L;
210
+ const da = lab1.a - lab2.a;
211
+ const db = lab1.b - lab2.b;
212
+ return Math.sqrt(dL * dL + da * da + db * db);
213
+ }
214
+ function defaultWeights(overrides) {
215
+ const base = {
216
+ contrast: 3.2,
217
+ toneSystem: 1.1,
218
+ emphasis: 1.3,
219
+ harmony: 0.7,
220
+ cvdRobust: 1.4,
221
+ semanticSeparation: 1,
222
+ gamutPenalty: 2,
223
+ stateContrast: 1.4
224
+ };
225
+ return { ...base, ...overrides || {} };
226
+ }
227
+ function lch(L, C, H) {
228
+ return { L: clamp(L, 0, 1), C: Math.max(0, C), H: mod360(H) };
229
+ }
230
+ function pickTextOn(fillHex) {
231
+ const lum = relativeLuminance(hexToRgb(fillHex));
232
+ return lum > 0.5 ? "#000000" : "#FFFFFF";
233
+ }
234
+ function deriveFillStates(baseHex, mode) {
235
+ const base = hexToOklch(baseHex);
236
+ const dir = mode === "light" ? -1 : 1;
237
+ const hoverL = clamp(base.L + dir * 0.04, 0, 1);
238
+ const pressedL = clamp(base.L + dir * 0.08, 0, 1);
239
+ const disabledL = clamp(mode === "light" ? 0.85 : 0.25, 0, 1);
240
+ const disabledC = Math.min(base.C, 0.02);
241
+ const hover = oklchToHex(lch(hoverL, base.C, base.H)).hex;
242
+ const pressed = oklchToHex(lch(pressedL, base.C, base.H)).hex;
243
+ const disabled = oklchToHex(lch(disabledL, disabledC, base.H)).hex;
244
+ return { hover, pressed, disabled };
245
+ }
246
+ function deriveSubtleBg(baseHex, bgHex, mode) {
247
+ const base = hexToOklch(baseHex);
248
+ const bg = hexToOklch(bgHex);
249
+ const targetL = clamp(
250
+ mode === "light" ? Math.max(bg.L - 0.05, 0.88) : Math.min(bg.L + 0.1, 0.22),
251
+ 0,
252
+ 1
253
+ );
254
+ const C = Math.min(0.05, Math.max(0.02, base.C * 0.35));
255
+ return oklchToHex(lch(targetL, C, base.H)).hex;
256
+ }
257
+ function deriveBorderFrom(bgOrSurfaceHex, mode, deltaL, hue) {
258
+ const ref = hexToOklch(bgOrSurfaceHex);
259
+ const L = clamp(ref.L + (mode === "light" ? -deltaL : deltaL), 0, 1);
260
+ const C = Math.min(0.02, Math.max(4e-3, ref.C));
261
+ return oklchToHex(lch(L, C, hue)).hex;
262
+ }
263
+ function deriveFocusRing(fromHex, mode, cBoost, targetL) {
264
+ const src = hexToOklch(fromHex);
265
+ const C = clamp(src.C + cBoost, 0.08, 0.26);
266
+ const L = clamp(targetL, 0.25, 0.85);
267
+ return oklchToHex(lch(L, C, src.H)).hex;
268
+ }
269
+ function buildTokens(params, mode, primaryHexFixed) {
270
+ let gamutCount = 0;
271
+ const bg = oklchToHex(lch(params.bgL, params.neutralC, params.neutralHue));
272
+ const surface = oklchToHex(
273
+ lch(
274
+ clamp(params.bgL + (mode === "light" ? -params.surfaceDeltaL : params.surfaceDeltaL), 0, 1),
275
+ params.neutralC * 1.05,
276
+ params.neutralHue
277
+ )
278
+ );
279
+ const surface2 = oklchToHex(
280
+ lch(
281
+ clamp(
282
+ params.bgL + (mode === "light" ? -params.surface2DeltaL : params.surface2DeltaL),
283
+ 0,
284
+ 1
285
+ ),
286
+ params.neutralC * 1.1,
287
+ params.neutralHue
288
+ )
289
+ );
290
+ const textPrimary = oklchToHex(
291
+ lch(params.textPrimaryL, params.neutralC * 0.12, params.neutralHue)
292
+ );
293
+ const textSecondary = oklchToHex(
294
+ lch(params.textSecondaryL, params.neutralC * 0.14, params.neutralHue)
295
+ );
296
+ const textTertiary = oklchToHex(
297
+ lch(params.textTertiaryL, params.neutralC * 0.16, params.neutralHue)
298
+ );
299
+ const secondary = oklchToHex(lch(params.secondaryL, params.secondaryC, params.secondaryHue));
300
+ const accent = oklchToHex(lch(params.accentL, params.accentC, params.accentHue));
301
+ const primaryHex = normalizeHex(primaryHexFixed);
302
+ const primaryLch = hexToOklch(primaryHex);
303
+ const border = deriveBorderFrom(surface.hex, mode, params.borderDeltaL, params.neutralHue);
304
+ const divider = deriveBorderFrom(surface2.hex, mode, params.dividerDeltaL, params.neutralHue);
305
+ const focusSourceHex = params.focusFrom === "primary" ? primaryHex : accent.hex;
306
+ const focusRing = deriveFocusRing(focusSourceHex, mode, params.focusCBoost, params.focusL);
307
+ const primaryText = pickTextOn(primaryHex);
308
+ const primaryStates = deriveFillStates(primaryHex, mode);
309
+ const primaryDisabledText = mode === "light" ? "#9AA0A6" : "#6B7280";
310
+ const secondaryTextOn = pickTextOn(secondary.hex);
311
+ const secondaryStates = deriveFillStates(secondary.hex, mode);
312
+ const secondaryDisabledText = primaryDisabledText;
313
+ const buttonPrimary = {
314
+ bg: primaryHex,
315
+ text: primaryText,
316
+ hoverBg: primaryStates.hover,
317
+ pressedBg: primaryStates.pressed,
318
+ disabledBg: primaryStates.disabled,
319
+ disabledText: secondaryDisabledText
320
+ };
321
+ const buttonSecondary = {
322
+ bg: secondary.hex,
323
+ text: secondaryTextOn,
324
+ hoverBg: secondaryStates.hover,
325
+ pressedBg: secondaryStates.pressed,
326
+ disabledBg: secondaryStates.disabled,
327
+ disabledText: secondaryDisabledText
328
+ };
329
+ const successBase = oklchToHex(lch(params.successL, params.semanticC, params.successHue));
330
+ const warningBase = oklchToHex(lch(params.warningL, params.semanticC, params.warningHue));
331
+ const dangerBase = oklchToHex(lch(params.dangerL, params.semanticC, params.dangerHue));
332
+ const infoBase = oklchToHex(lch(params.infoL, params.semanticC, params.infoHue));
333
+ const semList = [
334
+ bg,
335
+ surface,
336
+ surface2,
337
+ textPrimary,
338
+ textSecondary,
339
+ textTertiary,
340
+ secondary,
341
+ accent,
342
+ successBase,
343
+ warningBase,
344
+ dangerBase,
345
+ infoBase
346
+ ];
347
+ for (const e of semList) if (!e.inGamut) gamutCount += 1;
348
+ const sem = {
349
+ success: buildSemanticStates(successBase.hex, bg.hex, mode),
350
+ warning: buildSemanticStates(warningBase.hex, bg.hex, mode),
351
+ danger: buildSemanticStates(dangerBase.hex, bg.hex, mode),
352
+ info: buildSemanticStates(infoBase.hex, bg.hex, mode)
353
+ };
354
+ const secondaryText = secondaryTextOn;
355
+ const accentText = pickTextOn(accent.hex);
356
+ const tokens = {
357
+ mode,
358
+ background: bg.hex,
359
+ surface: surface.hex,
360
+ surface2: surface2.hex,
361
+ textPrimary: textPrimary.hex,
362
+ textSecondary: textSecondary.hex,
363
+ textTertiary: textTertiary.hex,
364
+ primary: primaryHex,
365
+ primaryText,
366
+ secondary: secondary.hex,
367
+ secondaryText,
368
+ accent: accent.hex,
369
+ accentText,
370
+ border,
371
+ divider,
372
+ focusRing,
373
+ buttonPrimary,
374
+ buttonSecondary,
375
+ semantic: sem
376
+ };
377
+ const Linfo = {
378
+ background: params.bgL,
379
+ surface: clamp(
380
+ params.bgL + (mode === "light" ? -params.surfaceDeltaL : params.surfaceDeltaL),
381
+ 0,
382
+ 1
383
+ ),
384
+ surface2: clamp(
385
+ params.bgL + (mode === "light" ? -params.surface2DeltaL : params.surface2DeltaL),
386
+ 0,
387
+ 1
388
+ ),
389
+ textPrimary: params.textPrimaryL,
390
+ textSecondary: params.textSecondaryL,
391
+ textTertiary: params.textTertiaryL,
392
+ primary: primaryLch.L,
393
+ secondary: params.secondaryL,
394
+ accent: params.accentL
395
+ };
396
+ return { tokens, gamutCount, Linfo };
397
+ }
398
+ function buildSemanticStates(baseHex, bgHex, mode) {
399
+ const base = normalizeHex(baseHex);
400
+ const onBaseText = pickTextOn(base);
401
+ const subtleBg = deriveSubtleBg(base, bgHex, mode);
402
+ const subtleText = normalizeHex(base);
403
+ const border = deriveBorderFrom(subtleBg, mode, 0.04, hexToOklch(base).H);
404
+ const states = deriveFillStates(base, mode);
405
+ return {
406
+ base,
407
+ onBaseText,
408
+ subtleBg,
409
+ subtleText,
410
+ border,
411
+ hover: states.hover,
412
+ pressed: states.pressed
413
+ };
414
+ }
415
+ function normalizeHex(hex) {
416
+ const h = hex.trim().toUpperCase();
417
+ if (!h.startsWith("#")) return ("#" + h).toUpperCase();
418
+ return h;
419
+ }
420
+ function scoreContrastBase(tokens, target, cvdMode) {
421
+ const { normal, large } = targetContrast(target);
422
+ const checks = [];
423
+ const add = (pair, fg, bg, min) => {
424
+ const ratio = contrastRatio(applyCvd(fg, cvdMode), applyCvd(bg, cvdMode));
425
+ const pass = ratio >= min;
426
+ checks.push({ pair, ratio, pass, mode: cvdMode });
427
+ };
428
+ add("textPrimary/background (normal)", tokens.textPrimary, tokens.background, normal);
429
+ add("textPrimary/surface (normal)", tokens.textPrimary, tokens.surface, normal);
430
+ add("textSecondary/background (normal)", tokens.textSecondary, tokens.background, normal);
431
+ add("textSecondary/surface (normal)", tokens.textSecondary, tokens.surface, normal);
432
+ add("textTertiary/surface (normal)", tokens.textTertiary, tokens.surface, large);
433
+ add("primaryText/primary (normal)", tokens.primaryText, tokens.primary, normal);
434
+ add("secondaryText/secondary (normal)", tokens.secondaryText, tokens.secondary, normal);
435
+ add("accentText/accent (normal)", tokens.accentText, tokens.accent, normal);
436
+ add("textPrimary/background (large)", tokens.textPrimary, tokens.background, large);
437
+ const passAll = checks.every((c) => c.pass);
438
+ const worstRatio = checks.reduce((m, c) => Math.min(m, c.ratio), Infinity);
439
+ return { target, normalTextMin: normal, largeTextMin: large, checks, passAll, worstRatio };
440
+ }
441
+ function scoreContrastStates(tokens, target, cvdMode) {
442
+ const { normal } = targetContrast(target);
443
+ const checks = [];
444
+ const add = (pair, fg, bg, min) => {
445
+ const ratio = contrastRatio(applyCvd(fg, cvdMode), applyCvd(bg, cvdMode));
446
+ const pass = ratio >= min;
447
+ checks.push({ pair, ratio, pass, mode: cvdMode });
448
+ };
449
+ add("btnPrimary/text:hoverBg", tokens.buttonPrimary.text, tokens.buttonPrimary.hoverBg, normal);
450
+ add(
451
+ "btnPrimary/text:pressedBg",
452
+ tokens.buttonPrimary.text,
453
+ tokens.buttonPrimary.pressedBg,
454
+ normal
455
+ );
456
+ add(
457
+ "btnSecondary/text:hoverBg",
458
+ tokens.buttonSecondary.text,
459
+ tokens.buttonSecondary.hoverBg,
460
+ normal
461
+ );
462
+ add(
463
+ "btnSecondary/text:pressedBg",
464
+ tokens.buttonSecondary.text,
465
+ tokens.buttonSecondary.pressedBg,
466
+ normal
467
+ );
468
+ add(
469
+ "semantic.success/subtleText:subtleBg",
470
+ tokens.semantic.success.subtleText,
471
+ tokens.semantic.success.subtleBg,
472
+ normal
473
+ );
474
+ add(
475
+ "semantic.warning/subtleText:subtleBg",
476
+ tokens.semantic.warning.subtleText,
477
+ tokens.semantic.warning.subtleBg,
478
+ normal
479
+ );
480
+ add(
481
+ "semantic.danger/subtleText:subtleBg",
482
+ tokens.semantic.danger.subtleText,
483
+ tokens.semantic.danger.subtleBg,
484
+ normal
485
+ );
486
+ add(
487
+ "semantic.info/subtleText:subtleBg",
488
+ tokens.semantic.info.subtleText,
489
+ tokens.semantic.info.subtleBg,
490
+ normal
491
+ );
492
+ const passAll = checks.every((c) => c.pass);
493
+ const worstRatio = checks.reduce((m, c) => Math.min(m, c.ratio), Infinity);
494
+ return { checks, passAll, worstRatio };
495
+ }
496
+ function scoreToneSystem(mode, Linfo) {
497
+ const notes = [];
498
+ let ok = true;
499
+ const bg = Linfo.background;
500
+ const sf = Linfo.surface;
501
+ const sf2 = Linfo.surface2;
502
+ const tp = Linfo.textPrimary;
503
+ const ts = Linfo.textSecondary;
504
+ const tt = Linfo.textTertiary;
505
+ if (mode === "light") {
506
+ if (bg < 0.85) {
507
+ ok = false;
508
+ notes.push("\uB77C\uC774\uD2B8: background L \uAD8C\uC7A5 >= 0.85");
509
+ }
510
+ if (!(sf < bg && bg - sf >= 0.03 && bg - sf <= 0.14)) {
511
+ ok = false;
512
+ notes.push("\uB77C\uC774\uD2B8: surface\uB294 bg\uBCF4\uB2E4 \uC0B4\uC9DD \uC5B4\uB450\uC6CC\uC57C \uD568(\u0394L 0.03~0.14)");
513
+ }
514
+ if (!(sf2 <= sf && sf - sf2 >= 0.01 && sf - sf2 <= 0.08)) {
515
+ ok = false;
516
+ notes.push("\uB77C\uC774\uD2B8: surface2\uB294 surface\uBCF4\uB2E4 \uC870\uAE08 \uB354 \uC5B4\uB450\uC6B4 \uCE35 \uAD8C\uC7A5");
517
+ }
518
+ if (tp > 0.26) {
519
+ ok = false;
520
+ notes.push("\uB77C\uC774\uD2B8: textPrimary L \uAD8C\uC7A5 <= 0.26");
521
+ }
522
+ if (!(ts > tp && ts <= 0.45)) {
523
+ ok = false;
524
+ notes.push("\uB77C\uC774\uD2B8: textSecondary\uB294 primary\uBCF4\uB2E4 \uBC1D\uACE0 \uB108\uBB34 \uBC1D\uC9C0 \uC54A\uAC8C");
525
+ }
526
+ if (!(tt >= ts && tt <= 0.6)) {
527
+ ok = false;
528
+ notes.push("\uB77C\uC774\uD2B8: textTertiary\uB294 secondary\uBCF4\uB2E4 \uBC1D\uACE0 \uB108\uBB34 \uBC1D\uC9C0 \uC54A\uAC8C");
529
+ }
530
+ } else {
531
+ if (bg > 0.18) {
532
+ ok = false;
533
+ notes.push("\uB2E4\uD06C: background L \uAD8C\uC7A5 <= 0.18");
534
+ }
535
+ if (!(sf > bg && sf - bg >= 0.03 && sf - bg <= 0.14)) {
536
+ ok = false;
537
+ notes.push("\uB2E4\uD06C: surface\uB294 bg\uBCF4\uB2E4 \uC0B4\uC9DD \uBC1D\uC544\uC57C \uD568(\u0394L 0.03~0.14)");
538
+ }
539
+ if (!(sf2 >= sf && sf2 - sf >= 0.01 && sf2 - sf <= 0.08)) {
540
+ ok = false;
541
+ notes.push("\uB2E4\uD06C: surface2\uB294 surface\uBCF4\uB2E4 \uC870\uAE08 \uB354 \uBC1D\uC740 \uCE35 \uAD8C\uC7A5");
542
+ }
543
+ if (tp < 0.78) {
544
+ ok = false;
545
+ notes.push("\uB2E4\uD06C: textPrimary L \uAD8C\uC7A5 >= 0.78");
546
+ }
547
+ if (!(ts < tp && ts >= 0.6)) {
548
+ ok = false;
549
+ notes.push("\uB2E4\uD06C: textSecondary\uB294 primary\uBCF4\uB2E4 \uC5B4\uB461\uB418 \uB108\uBB34 \uC5B4\uB461\uC9C0 \uC54A\uAC8C");
550
+ }
551
+ if (!(tt <= ts && tt >= 0.45)) {
552
+ ok = false;
553
+ notes.push("\uB2E4\uD06C: textTertiary\uB294 secondary\uBCF4\uB2E4 \uC5B4\uB461\uB418 \uB108\uBB34 \uC5B4\uB461\uC9C0 \uC54A\uAC8C");
554
+ }
555
+ }
556
+ return { mode, L: { ...Linfo }, ok, notes };
557
+ }
558
+ function scoreEmphasis(tokens) {
559
+ const p = hexToOklch(tokens.primary);
560
+ const s = hexToOklch(tokens.surface);
561
+ const dL = Math.abs(p.L - s.L);
562
+ const dC = Math.abs(p.C - s.C);
563
+ const dH = hueDistance(p.H, s.H);
564
+ const score = clamp(dL / 0.2 * 0.35 + dC / 0.14 * 0.55 + dH / 90 * 0.1, 0, 1);
565
+ const ok = dL >= 0.1 || dC >= 0.1;
566
+ return { primaryVsSurface: { dL, dC, dH, score }, ok };
567
+ }
568
+ function classifyHarmony(seedHue, primaryHue, accentHue) {
569
+ if (seedHue == null) {
570
+ const d = hueDistance(primaryHue, accentHue);
571
+ if (d < 15) return "mono";
572
+ if (d < 45) return "analogous";
573
+ if (Math.abs(d - 180) < 15) return "complementary";
574
+ if (Math.abs(d - 150) < 20 || Math.abs(d - 210) < 20) return "split";
575
+ if (Math.abs(d - 120) < 15) return "triadic";
576
+ return "other";
577
+ } else {
578
+ const d3 = hueDistance(primaryHue, accentHue);
579
+ if (d3 < 15) return "mono";
580
+ if (d3 < 45) return "analogous";
581
+ if (Math.abs(d3 - 180) < 20) return "complementary";
582
+ if (Math.abs(d3 - 150) < 25 || Math.abs(d3 - 210) < 25) return "split";
583
+ if (Math.abs(d3 - 120) < 20) return "triadic";
584
+ return "other";
585
+ }
586
+ }
587
+ function scoreHarmony(seedHue, tokens) {
588
+ const p = hexToOklch(tokens.primary);
589
+ const a = hexToOklch(tokens.accent);
590
+ const rel = classifyHarmony(seedHue, p.H, a.H);
591
+ const scoreMap = {
592
+ mono: 0.9,
593
+ analogous: 1,
594
+ complementary: 0.9,
595
+ split: 0.95,
596
+ triadic: 0.85,
597
+ other: 0.75
598
+ };
599
+ return {
600
+ hueRelations: {
601
+ seedHue,
602
+ primaryHue: p.H,
603
+ accentHue: a.H,
604
+ relation: rel
605
+ },
606
+ score: scoreMap[rel]
607
+ };
608
+ }
609
+ function scoreSemanticSeparation(tokens, cvdMode) {
610
+ const roles = [
611
+ ["success", tokens.semantic.success.base],
612
+ ["warning", tokens.semantic.warning.base],
613
+ ["danger", tokens.semantic.danger.base],
614
+ ["info", tokens.semantic.info.base]
615
+ ];
616
+ const pairs = [];
617
+ let minDE = Infinity;
618
+ for (let i = 0; i < roles.length; i++) {
619
+ for (let j = i + 1; j < roles.length; j++) {
620
+ const n1 = roles[i][0];
621
+ const n2 = roles[j][0];
622
+ const c1 = applyCvd(roles[i][1], cvdMode);
623
+ const c2 = applyCvd(roles[j][1], cvdMode);
624
+ const dE = deltaEoklab(c1, c2);
625
+ pairs.push({ pair: `${n1}-${n2}`, dE, mode: cvdMode });
626
+ minDE = Math.min(minDE, dE);
627
+ }
628
+ }
629
+ const ok = minDE >= 0.06;
630
+ return { pairs, minDE, ok };
631
+ }
632
+ function aggregateScore(tokens, mode, target, weights, gamutCount, Linfo, cvdModes, seedHue) {
633
+ const contrastReports = cvdModes.map((m) => scoreContrastBase(tokens, target, m));
634
+ const worstContrast = Math.min(...contrastReports.map((r) => r.worstRatio));
635
+ const passAllAllModes = contrastReports.every((r) => r.passAll);
636
+ const stateReports = cvdModes.map((m) => scoreContrastStates(tokens, target, m));
637
+ const worstState = Math.min(...stateReports.map((r) => r.worstRatio));
638
+ const passAllStates = stateReports.every((r) => r.passAll);
639
+ const tone = scoreToneSystem(mode, Linfo);
640
+ const emphasis = scoreEmphasis(tokens);
641
+ const harmony = scoreHarmony(seedHue, tokens);
642
+ const primaryDist = cvdModes.map((m) => {
643
+ const p = applyCvd(tokens.primary, m);
644
+ const s = applyCvd(tokens.surface, m);
645
+ return { mode: m, dE: deltaEoklab(p, s) };
646
+ });
647
+ const semanticDist = cvdModes.map((m) => {
648
+ const sem = scoreSemanticSeparation(tokens, m);
649
+ let minPair = "";
650
+ let minDE = Infinity;
651
+ for (const p of sem.pairs) {
652
+ if (p.dE < minDE) {
653
+ minDE = p.dE;
654
+ minPair = p.pair;
655
+ }
656
+ }
657
+ return { mode: m, minPair, minDE };
658
+ });
659
+ const semanticNone = scoreSemanticSeparation(tokens, "none");
660
+ const { normal } = targetContrast(target);
661
+ const contrastScore = clamp((worstContrast - normal) / (normal * 0.6), 0, 1);
662
+ const stateContrastScore = clamp((worstState - normal) / (normal * 0.6), 0, 1);
663
+ const toneScore = tone.ok ? 1 : 0.65;
664
+ const emphasisScore = emphasis.primaryVsSurface.score;
665
+ const harmonyScore = clamp(harmony.score, 0, 1);
666
+ const primaryDEmin = Math.min(...primaryDist.map((x) => x.dE));
667
+ const semDEmin = Math.min(...semanticDist.map((x) => x.minDE));
668
+ const cvdPrimaryScore = clamp((primaryDEmin - 0.05) / 0.1, 0, 1);
669
+ const cvdSemanticScore = clamp((semDEmin - 0.05) / 0.1, 0, 1);
670
+ const cvdScore = 0.55 * cvdPrimaryScore + 0.45 * cvdSemanticScore;
671
+ const semanticScore = semanticNone.ok ? clamp((semanticNone.minDE - 0.06) / 0.08, 0, 1) : 0;
672
+ const outOfGamutCount = gamutCount;
673
+ const penaltyGamut = outOfGamutCount > 0 ? clamp(outOfGamutCount / 8, 0, 1) : 0;
674
+ const hardContrastPenalty = passAllAllModes ? 0 : 0.85;
675
+ const hardStatePenalty = passAllStates ? 0 : 0.65;
676
+ const total = weights.contrast * contrastScore + weights.stateContrast * stateContrastScore + weights.toneSystem * toneScore + weights.emphasis * emphasisScore + weights.harmony * harmonyScore + weights.cvdRobust * cvdScore + weights.semanticSeparation * semanticScore - weights.gamutPenalty * penaltyGamut - 4 * hardContrastPenalty - 2.8 * hardStatePenalty;
677
+ const mergedContrast = {
678
+ target,
679
+ normalTextMin: targetContrast(target).normal,
680
+ largeTextMin: targetContrast(target).large,
681
+ checks: contrastReports.flatMap((r) => r.checks),
682
+ passAll: passAllAllModes,
683
+ worstRatio: worstContrast
684
+ };
685
+ const mergedStates = {
686
+ checks: stateReports.flatMap((r) => r.checks),
687
+ passAll: passAllStates,
688
+ worstRatio: worstState
689
+ };
690
+ return {
691
+ mode,
692
+ total,
693
+ contrast: mergedContrast,
694
+ tone,
695
+ emphasis,
696
+ harmony,
697
+ cvd: { modes: cvdModes, primaryDist, semanticDist },
698
+ semantic: semanticNone,
699
+ states: mergedStates,
700
+ penalties: {
701
+ outOfGamutCount,
702
+ penalty: weights.gamutPenalty * penaltyGamut + 4 * hardContrastPenalty + 2.8 * hardStatePenalty
703
+ }
704
+ };
705
+ }
706
+ function initParams(rng, mode, seedHex, preferVibrant, semanticConventional) {
707
+ let seedHue = rng.float(0, 360);
708
+ if (seedHex) {
709
+ try {
710
+ seedHue = hexToOklch(seedHex).H;
711
+ } catch {
712
+ }
713
+ }
714
+ const isLight = mode === "light";
715
+ const neutralHue = seedHex ? mod360(seedHue + rng.float(-10, 10)) : rng.float(0, 360);
716
+ const neutralC = rng.float(4e-3, 0.028);
717
+ const bgL = isLight ? rng.float(0.88, 0.98) : rng.float(0.06, 0.16);
718
+ const surfaceDeltaL = rng.float(0.04, 0.11);
719
+ const surface2DeltaL = clamp(surfaceDeltaL + rng.float(0.01, 0.06), 0.05, 0.18);
720
+ const textPrimaryL = isLight ? rng.float(0.1, 0.23) : rng.float(0.82, 0.95);
721
+ const textSecondaryL = isLight ? rng.float(0.22, 0.4) : rng.float(0.6, 0.8);
722
+ const textTertiaryL = isLight ? rng.float(0.36, 0.58) : rng.float(0.48, 0.68);
723
+ const secondaryHue = mod360(seedHue + rng.float(-25, 25));
724
+ const secondaryC = preferVibrant ? rng.float(0.05, 0.11) : rng.float(0.04, 0.09);
725
+ const secondaryL = isLight ? rng.float(0.7, 0.88) : rng.float(0.18, 0.34);
726
+ const accentStrategy = rng.int(0, 3);
727
+ let accentHue = seedHue;
728
+ if (accentStrategy === 0) accentHue = mod360(seedHue + rng.float(-12, 12));
729
+ if (accentStrategy === 1) accentHue = mod360(seedHue + rng.float(20, 45));
730
+ if (accentStrategy === 2) accentHue = mod360(seedHue + 180 + rng.float(-12, 12));
731
+ if (accentStrategy === 3)
732
+ accentHue = mod360(seedHue + rng.pick([150, 210]) + rng.float(-12, 12));
733
+ const accentC = preferVibrant ? rng.float(0.1, 0.2) : rng.float(0.08, 0.16);
734
+ const accentL = isLight ? rng.float(0.45, 0.66) : rng.float(0.45, 0.72);
735
+ const successHue = semanticConventional ? mod360(145 + rng.float(-10, 10)) : rng.float(0, 360);
736
+ const warningHue = semanticConventional ? mod360(85 + rng.float(-12, 12)) : rng.float(0, 360);
737
+ const dangerHue = semanticConventional ? mod360(25 + rng.float(-12, 12)) : rng.float(0, 360);
738
+ const infoHue = semanticConventional ? mod360(245 + rng.float(-12, 12)) : rng.float(0, 360);
739
+ const semanticC = preferVibrant ? rng.float(0.12, 0.2) : rng.float(0.1, 0.18);
740
+ const successL = isLight ? rng.float(0.45, 0.62) : rng.float(0.45, 0.62);
741
+ const warningL = isLight ? rng.float(0.55, 0.72) : rng.float(0.55, 0.74);
742
+ const dangerL = isLight ? rng.float(0.45, 0.62) : rng.float(0.45, 0.62);
743
+ const infoL = isLight ? rng.float(0.45, 0.62) : rng.float(0.45, 0.62);
744
+ const borderDeltaL = rng.float(0.02, 0.06);
745
+ const dividerDeltaL = rng.float(0.03, 0.08);
746
+ const focusFrom = rng.pick(["primary", "accent"]);
747
+ const focusCBoost = preferVibrant ? rng.float(0.05, 0.1) : rng.float(0.04, 0.08);
748
+ const focusL = isLight ? rng.float(0.45, 0.65) : rng.float(0.55, 0.75);
749
+ return {
750
+ seedHue,
751
+ neutralHue,
752
+ neutralC,
753
+ bgL,
754
+ surfaceDeltaL,
755
+ surface2DeltaL,
756
+ textPrimaryL,
757
+ textSecondaryL,
758
+ textTertiaryL,
759
+ secondaryHue,
760
+ secondaryC,
761
+ secondaryL,
762
+ accentHue,
763
+ accentC,
764
+ accentL,
765
+ successHue,
766
+ warningHue,
767
+ dangerHue,
768
+ infoHue,
769
+ semanticC,
770
+ successL,
771
+ warningL,
772
+ dangerL,
773
+ infoL,
774
+ borderDeltaL,
775
+ dividerDeltaL,
776
+ focusFrom,
777
+ focusCBoost,
778
+ focusL
779
+ };
780
+ }
781
+ function mutateParams(rng, p) {
782
+ const q = { ...p };
783
+ const jitter = (x, amt) => x + rng.float(-amt, amt);
784
+ const pick = rng.int(0, 9);
785
+ switch (pick) {
786
+ case 0:
787
+ q.bgL = clamp(jitter(q.bgL, 0.03), 0, 1);
788
+ q.surfaceDeltaL = clamp(jitter(q.surfaceDeltaL, 0.02), 0.02, 0.16);
789
+ q.surface2DeltaL = clamp(jitter(q.surface2DeltaL, 0.03), 0.03, 0.2);
790
+ break;
791
+ case 1:
792
+ q.textPrimaryL = clamp(jitter(q.textPrimaryL, 0.04), 0, 1);
793
+ q.textSecondaryL = clamp(jitter(q.textSecondaryL, 0.05), 0, 1);
794
+ q.textTertiaryL = clamp(jitter(q.textTertiaryL, 0.06), 0, 1);
795
+ break;
796
+ case 2:
797
+ q.secondaryHue = mod360(jitter(q.secondaryHue, 25));
798
+ q.secondaryC = clamp(jitter(q.secondaryC, 0.03), 0.01, 0.16);
799
+ q.secondaryL = clamp(jitter(q.secondaryL, 0.06), 0.12, 0.92);
800
+ break;
801
+ case 3:
802
+ q.accentHue = mod360(jitter(q.accentHue, 30));
803
+ q.accentC = clamp(jitter(q.accentC, 0.04), 0.02, 0.24);
804
+ q.accentL = clamp(jitter(q.accentL, 0.07), 0.16, 0.92);
805
+ break;
806
+ case 4:
807
+ q.neutralHue = mod360(jitter(q.neutralHue, 15));
808
+ q.neutralC = clamp(jitter(q.neutralC, 0.01), 2e-3, 0.06);
809
+ break;
810
+ case 5:
811
+ q.semanticC = clamp(jitter(q.semanticC, 0.03), 0.06, 0.26);
812
+ q.successL = clamp(jitter(q.successL, 0.05), 0.2, 0.85);
813
+ q.warningL = clamp(jitter(q.warningL, 0.05), 0.2, 0.9);
814
+ q.dangerL = clamp(jitter(q.dangerL, 0.05), 0.2, 0.85);
815
+ q.infoL = clamp(jitter(q.infoL, 0.05), 0.2, 0.85);
816
+ break;
817
+ case 6:
818
+ q.successHue = mod360(jitter(q.successHue, 15));
819
+ q.warningHue = mod360(jitter(q.warningHue, 15));
820
+ q.dangerHue = mod360(jitter(q.dangerHue, 15));
821
+ q.infoHue = mod360(jitter(q.infoHue, 15));
822
+ break;
823
+ case 7:
824
+ q.borderDeltaL = clamp(jitter(q.borderDeltaL, 0.02), 0.01, 0.1);
825
+ q.dividerDeltaL = clamp(jitter(q.dividerDeltaL, 0.02), 0.02, 0.14);
826
+ break;
827
+ case 8:
828
+ q.focusFrom = rng.pick(["primary", "accent"]);
829
+ q.focusCBoost = clamp(jitter(q.focusCBoost, 0.03), 0.02, 0.16);
830
+ q.focusL = clamp(jitter(q.focusL, 0.08), 0.25, 0.85);
831
+ break;
832
+ case 9:
833
+ q.seedHue = mod360(jitter(q.seedHue, 15));
834
+ break;
835
+ default:
836
+ break;
837
+ }
838
+ return q;
839
+ }
840
+ function accept(delta, temperature, rng) {
841
+ if (delta >= 0) return true;
842
+ const prob = Math.exp(delta / Math.max(1e-9, temperature));
843
+ return rng.next() < prob;
844
+ }
845
+ function optimizeTheme(mode, primaryHexFixed, opts, rng, seedHue) {
846
+ const contrastTarget = opts.contrastTarget ?? "AA";
847
+ const iterations = opts.iterations ?? 3500;
848
+ let temperature = opts.temperature ?? 1;
849
+ const cooling = opts.cooling ?? 0.985;
850
+ const preferVibrant = opts.preferVibrant ?? true;
851
+ const semanticConventional = opts.semanticConventional ?? true;
852
+ const cvdModes = opts.cvdModes && opts.cvdModes.length > 0 ? opts.cvdModes : ["none", "protan", "deutan", "tritan"];
853
+ const weights = defaultWeights(opts.weights);
854
+ let curParams = initParams(rng, mode, opts.seedHex, preferVibrant, semanticConventional);
855
+ let built = buildTokens(curParams, mode, primaryHexFixed);
856
+ let curReport = aggregateScore(
857
+ built.tokens,
858
+ mode,
859
+ contrastTarget,
860
+ weights,
861
+ built.gamutCount,
862
+ built.Linfo,
863
+ cvdModes,
864
+ seedHue
865
+ );
866
+ let curScore = curReport.total;
867
+ let bestParams = curParams;
868
+ let bestTokens = built.tokens;
869
+ let bestReport = curReport;
870
+ let bestScore = curScore;
871
+ for (let i = 0; i < iterations; i++) {
872
+ const nextParams = mutateParams(rng, curParams);
873
+ const nextBuilt = buildTokens(nextParams, mode, primaryHexFixed);
874
+ const nextReport = aggregateScore(
875
+ nextBuilt.tokens,
876
+ mode,
877
+ contrastTarget,
878
+ weights,
879
+ nextBuilt.gamutCount,
880
+ nextBuilt.Linfo,
881
+ cvdModes,
882
+ seedHue
883
+ );
884
+ const nextScore = nextReport.total;
885
+ const delta = nextScore - curScore;
886
+ if (accept(delta, temperature, rng)) {
887
+ curParams = nextParams;
888
+ built = nextBuilt;
889
+ curReport = nextReport;
890
+ curScore = nextScore;
891
+ }
892
+ if (curScore > bestScore) {
893
+ bestScore = curScore;
894
+ bestParams = curParams;
895
+ bestTokens = built.tokens;
896
+ bestReport = curReport;
897
+ }
898
+ temperature *= cooling;
899
+ if ((i + 1) % 900 === 0) temperature = Math.min(1, temperature * 1.18);
900
+ }
901
+ return { tokens: bestTokens, score: bestScore, report: bestReport };
902
+ }
903
+ function recommendTokensDual(options) {
904
+ if (!options || !options.primaryHex) {
905
+ throw new Error("primaryHex is required (brand primary fixed).");
906
+ }
907
+ const randomSeed = options.randomSeed ?? Math.floor(Math.random() * 1e9);
908
+ const rng = new RNG(randomSeed);
909
+ const primaryHex = normalizeHex(options.primaryHex);
910
+ const primaryDarkHex = normalizeHex(options.primaryDarkHex ?? options.primaryHex);
911
+ const cvdModes = options.cvdModes && options.cvdModes.length > 0 ? options.cvdModes : ["none", "protan", "deutan", "tritan"];
912
+ const seedHue = safeSeedHue(options.seedHex);
913
+ const light = optimizeTheme("light", primaryHex, options, rng, seedHue);
914
+ const dark = optimizeTheme("dark", primaryDarkHex, options, rng, seedHue);
915
+ return {
916
+ light,
917
+ dark,
918
+ meta: {
919
+ primaryHex,
920
+ primaryDarkHex,
921
+ contrastTarget: options.contrastTarget ?? "AA",
922
+ cvdModes,
923
+ randomSeed
924
+ }
925
+ };
926
+ }
927
+ function safeSeedHue(seedHex) {
928
+ if (!seedHex) return void 0;
929
+ try {
930
+ return hexToOklch(seedHex).H;
931
+ } catch {
932
+ return void 0;
933
+ }
934
+ }
935
+ function validateTokens(tokens, target = "AA", cvdModes = ["none", "protan", "deutan", "tritan"]) {
936
+ const base = cvdModes.map((m) => scoreContrastBase(tokens, target, m));
937
+ const states = cvdModes.map((m) => scoreContrastStates(tokens, target, m));
938
+ const pass = base.every((r) => r.passAll) && states.every((r) => r.passAll);
939
+ const worst = Math.min(
940
+ ...base.map((r) => r.worstRatio),
941
+ ...states.map((r) => r.worstRatio)
942
+ );
943
+ return { pass, worst, base, states };
944
+ }
945
+ function recommendTokensDualAsJson(options) {
946
+ const res = recommendTokensDual(options);
947
+ return JSON.stringify(
948
+ {
949
+ meta: res.meta,
950
+ light: {
951
+ score: res.light.score,
952
+ contrastPass: res.light.report.contrast.passAll,
953
+ statePass: res.light.report.states.passAll,
954
+ worstContrast: res.light.report.contrast.worstRatio,
955
+ worstState: res.light.report.states.worstRatio,
956
+ tokens: res.light.tokens
957
+ },
958
+ dark: {
959
+ score: res.dark.score,
960
+ contrastPass: res.dark.report.contrast.passAll,
961
+ statePass: res.dark.report.states.passAll,
962
+ worstContrast: res.dark.report.contrast.worstRatio,
963
+ worstState: res.dark.report.states.worstRatio,
964
+ tokens: res.dark.tokens
965
+ }
966
+ },
967
+ null,
968
+ 2
969
+ );
970
+ }