@tenphi/glaze 0.0.0-snapshot.432b23c → 0.0.0-snapshot.4e8eab7
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/README.md +394 -43
- package/dist/index.cjs +782 -102
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +328 -31
- package/dist/index.d.mts +328 -31
- package/dist/index.mjs +780 -103
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -7
package/dist/index.cjs
CHANGED
|
@@ -346,6 +346,11 @@ function gamutClampedLuminance(linearRgb) {
|
|
|
346
346
|
const linearSrgbToOklab = (rgb) => {
|
|
347
347
|
return transform(cbrt3(transform(rgb, linear_sRGB_to_LMS_M)), LMS_to_OKLab_M);
|
|
348
348
|
};
|
|
349
|
+
/**
|
|
350
|
+
* Convert OKLab to OKHSL.
|
|
351
|
+
* Input: [L, a, b] where L: 0–1, a/b: roughly -0.5 to 0.5.
|
|
352
|
+
* Returns [h, s, l] where h: 0–360, s: 0–1, l: 0–1.
|
|
353
|
+
*/
|
|
349
354
|
const oklabToOkhsl = (lab) => {
|
|
350
355
|
const L = lab[0];
|
|
351
356
|
const a = lab[1];
|
|
@@ -356,6 +361,12 @@ const oklabToOkhsl = (lab) => {
|
|
|
356
361
|
0,
|
|
357
362
|
toe(L)
|
|
358
363
|
];
|
|
364
|
+
const L_EXTREME_EPSILON = 1e-6;
|
|
365
|
+
if (L >= 1 - L_EXTREME_EPSILON || L <= L_EXTREME_EPSILON) return [
|
|
366
|
+
0,
|
|
367
|
+
0,
|
|
368
|
+
toe(L)
|
|
369
|
+
];
|
|
359
370
|
const a_ = a / C;
|
|
360
371
|
const b_ = b / C;
|
|
361
372
|
let h = Math.atan2(b, a) * (180 / Math.PI);
|
|
@@ -393,32 +404,108 @@ function srgbToOkhsl(rgb) {
|
|
|
393
404
|
]));
|
|
394
405
|
}
|
|
395
406
|
/**
|
|
407
|
+
* Convert CSS HSL (sRGB-based) to gamma-encoded sRGB [r, g, b] in 0–1 range.
|
|
408
|
+
* h: 0–360, s: 0–1, l: 0–1.
|
|
409
|
+
*
|
|
410
|
+
* Note: CSS HSL is not the same as OKHSL — it's HSL in the sRGB color space.
|
|
411
|
+
* Use this when parsing `hsl(...)` strings before passing to `srgbToOkhsl`.
|
|
412
|
+
*/
|
|
413
|
+
function hslToSrgb(h, s, l) {
|
|
414
|
+
const hh = (h % 360 + 360) % 360 / 360;
|
|
415
|
+
const ss = clampVal(s, 0, 1);
|
|
416
|
+
const ll = clampVal(l, 0, 1);
|
|
417
|
+
if (ss === 0) return [
|
|
418
|
+
ll,
|
|
419
|
+
ll,
|
|
420
|
+
ll
|
|
421
|
+
];
|
|
422
|
+
const q = ll < .5 ? ll * (1 + ss) : ll + ss - ll * ss;
|
|
423
|
+
const p = 2 * ll - q;
|
|
424
|
+
const hueToChannel = (t) => {
|
|
425
|
+
let tt = t;
|
|
426
|
+
if (tt < 0) tt += 1;
|
|
427
|
+
if (tt > 1) tt -= 1;
|
|
428
|
+
if (tt < 1 / 6) return p + (q - p) * 6 * tt;
|
|
429
|
+
if (tt < 1 / 2) return q;
|
|
430
|
+
if (tt < 2 / 3) return p + (q - p) * (2 / 3 - tt) * 6;
|
|
431
|
+
return p;
|
|
432
|
+
};
|
|
433
|
+
return [
|
|
434
|
+
hueToChannel(hh + 1 / 3),
|
|
435
|
+
hueToChannel(hh),
|
|
436
|
+
hueToChannel(hh - 1 / 3)
|
|
437
|
+
];
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
396
440
|
* Parse a hex color string (#rgb or #rrggbb) to sRGB [r, g, b] in 0–1 range.
|
|
397
441
|
* Returns null if the string is not a valid hex color.
|
|
442
|
+
*
|
|
443
|
+
* For 8-digit hex (`#rrggbbaa`) and 4-digit hex (`#rgba`) with alpha,
|
|
444
|
+
* use {@link parseHexAlpha}.
|
|
398
445
|
*/
|
|
399
446
|
function parseHex(hex) {
|
|
447
|
+
const result = parseHexAlpha(hex);
|
|
448
|
+
if (!result || result.alpha !== void 0) return null;
|
|
449
|
+
return result.rgb;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Parse a hex color string (#rgb, #rrggbb, #rgba, or #rrggbbaa) to
|
|
453
|
+
* sRGB [r, g, b] in 0–1 range plus an optional alpha (0–1).
|
|
454
|
+
* Returns null if the string is not a valid hex color.
|
|
455
|
+
*/
|
|
456
|
+
function parseHexAlpha(hex) {
|
|
400
457
|
const h = hex.startsWith("#") ? hex.slice(1) : hex;
|
|
401
458
|
if (h.length === 3) {
|
|
402
459
|
const r = parseInt(h[0] + h[0], 16);
|
|
403
460
|
const g = parseInt(h[1] + h[1], 16);
|
|
404
461
|
const b = parseInt(h[2] + h[2], 16);
|
|
405
462
|
if (isNaN(r) || isNaN(g) || isNaN(b)) return null;
|
|
406
|
-
return [
|
|
463
|
+
return { rgb: [
|
|
407
464
|
r / 255,
|
|
408
465
|
g / 255,
|
|
409
466
|
b / 255
|
|
410
|
-
];
|
|
467
|
+
] };
|
|
468
|
+
}
|
|
469
|
+
if (h.length === 4) {
|
|
470
|
+
const r = parseInt(h[0] + h[0], 16);
|
|
471
|
+
const g = parseInt(h[1] + h[1], 16);
|
|
472
|
+
const b = parseInt(h[2] + h[2], 16);
|
|
473
|
+
const a = parseInt(h[3] + h[3], 16);
|
|
474
|
+
if (isNaN(r) || isNaN(g) || isNaN(b) || isNaN(a)) return null;
|
|
475
|
+
return {
|
|
476
|
+
rgb: [
|
|
477
|
+
r / 255,
|
|
478
|
+
g / 255,
|
|
479
|
+
b / 255
|
|
480
|
+
],
|
|
481
|
+
alpha: a / 255
|
|
482
|
+
};
|
|
411
483
|
}
|
|
412
484
|
if (h.length === 6) {
|
|
413
485
|
const r = parseInt(h.slice(0, 2), 16);
|
|
414
486
|
const g = parseInt(h.slice(2, 4), 16);
|
|
415
487
|
const b = parseInt(h.slice(4, 6), 16);
|
|
416
488
|
if (isNaN(r) || isNaN(g) || isNaN(b)) return null;
|
|
417
|
-
return [
|
|
489
|
+
return { rgb: [
|
|
418
490
|
r / 255,
|
|
419
491
|
g / 255,
|
|
420
492
|
b / 255
|
|
421
|
-
];
|
|
493
|
+
] };
|
|
494
|
+
}
|
|
495
|
+
if (h.length === 8) {
|
|
496
|
+
const r = parseInt(h.slice(0, 2), 16);
|
|
497
|
+
const g = parseInt(h.slice(2, 4), 16);
|
|
498
|
+
const b = parseInt(h.slice(4, 6), 16);
|
|
499
|
+
const a = parseInt(h.slice(6, 8), 16);
|
|
500
|
+
if (isNaN(r) || isNaN(g) || isNaN(b) || isNaN(a)) return null;
|
|
501
|
+
return {
|
|
502
|
+
rgb: [
|
|
503
|
+
r / 255,
|
|
504
|
+
g / 255,
|
|
505
|
+
b / 255
|
|
506
|
+
],
|
|
507
|
+
alpha: a / 255
|
|
508
|
+
};
|
|
422
509
|
}
|
|
423
510
|
return null;
|
|
424
511
|
}
|
|
@@ -470,7 +557,7 @@ function formatOklch(h, s, l) {
|
|
|
470
557
|
const C = Math.sqrt(a * a + b * b);
|
|
471
558
|
let hh = Math.atan2(b, a) * (180 / Math.PI);
|
|
472
559
|
hh = constrainAngle(hh);
|
|
473
|
-
return `oklch(${fmt$1(L, 4)} ${fmt$1(C, 4)} ${fmt$1(hh,
|
|
560
|
+
return `oklch(${fmt$1(L, 4)} ${fmt$1(C, 4)} ${fmt$1(hh, 2)})`;
|
|
474
561
|
}
|
|
475
562
|
|
|
476
563
|
//#endregion
|
|
@@ -620,7 +707,7 @@ function coarseScan(h, s, lo, hi, yBase, target, epsilon, maxIter) {
|
|
|
620
707
|
function findLightnessForContrast(options) {
|
|
621
708
|
const { hue, saturation, preferredLightness, baseLinearRgb, contrast: contrastInput, lightnessRange = [0, 1], epsilon = 1e-4, maxIterations = 14 } = options;
|
|
622
709
|
const target = resolveMinContrast(contrastInput);
|
|
623
|
-
const searchTarget = target * 1.
|
|
710
|
+
const searchTarget = target * 1.01;
|
|
624
711
|
const yBase = gamutClampedLuminance(baseLinearRgb);
|
|
625
712
|
const crPref = contrastRatioFromLuminance(cachedLuminance(hue, saturation, preferredLightness), yBase);
|
|
626
713
|
if (crPref >= searchTarget) return {
|
|
@@ -744,7 +831,7 @@ function searchMixBranch(lo, hi, yBase, target, epsilon, maxIter, preferred, lum
|
|
|
744
831
|
function findValueForMixContrast(options) {
|
|
745
832
|
const { preferredValue, baseLinearRgb, contrast: contrastInput, luminanceAtValue, epsilon = 1e-4, maxIterations = 20 } = options;
|
|
746
833
|
const target = resolveMinContrast(contrastInput);
|
|
747
|
-
const searchTarget = target * 1.
|
|
834
|
+
const searchTarget = target * 1.01;
|
|
748
835
|
const yBase = gamutClampedLuminance(baseLinearRgb);
|
|
749
836
|
const crPref = contrastRatioFromLuminance(luminanceAtValue(preferredValue), yBase);
|
|
750
837
|
if (crPref >= searchTarget) return {
|
|
@@ -810,10 +897,59 @@ function findValueForMixContrast(options) {
|
|
|
810
897
|
* Generates robust light, dark, and high-contrast colors from a hue/saturation
|
|
811
898
|
* seed, preserving contrast for UI pairs via explicit dependencies.
|
|
812
899
|
*/
|
|
900
|
+
/** Internal name of the user-facing standalone color in the synthesized def map. */
|
|
901
|
+
const STANDALONE_VALUE = "value";
|
|
902
|
+
/** Internal name of the hidden static-anchor seed used for relative lightness / contrast. */
|
|
903
|
+
const STANDALONE_SEED = "seed";
|
|
904
|
+
/** Internal name of an externally-resolved `GlazeColorToken` injected as a base reference. */
|
|
905
|
+
const STANDALONE_BASE = "externalBase";
|
|
906
|
+
/**
|
|
907
|
+
* Build the create-time scaling snapshot used when the caller did not
|
|
908
|
+
* pass an explicit `scaling`. All windows are snapshotted from the
|
|
909
|
+
* current `globalConfig` so later `glaze.configure()` calls don't
|
|
910
|
+
* retroactively change the resolved variants of an already-created
|
|
911
|
+
* token (matches the documented "frozen at create time" semantics).
|
|
912
|
+
*
|
|
913
|
+
* String value-shorthand inputs preserve their light lightness exactly
|
|
914
|
+
* (`lightLightness: false`) and use an extended dark window
|
|
915
|
+
* `[globalConfig.darkLightness[0], 100]` so a totally-black input can
|
|
916
|
+
* Möbius-invert to totally-white in dark mode. Object / tuple /
|
|
917
|
+
* structured inputs snapshot both windows from `globalConfig` verbatim
|
|
918
|
+
* so they behave like an ordinary theme color (auto-adapted on both
|
|
919
|
+
* sides).
|
|
920
|
+
*/
|
|
921
|
+
function defaultStandaloneScaling(isString) {
|
|
922
|
+
if (isString) {
|
|
923
|
+
const [darkLo] = globalConfig.darkLightness;
|
|
924
|
+
return {
|
|
925
|
+
lightLightness: false,
|
|
926
|
+
darkLightness: [darkLo, 100]
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
return {
|
|
930
|
+
lightLightness: globalConfig.lightLightness,
|
|
931
|
+
darkLightness: globalConfig.darkLightness
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
/** Reserved internal names that user-supplied `name` must not collide with. */
|
|
935
|
+
const RESERVED_STANDALONE_NAMES = new Set([
|
|
936
|
+
STANDALONE_VALUE,
|
|
937
|
+
STANDALONE_SEED,
|
|
938
|
+
STANDALONE_BASE
|
|
939
|
+
]);
|
|
940
|
+
/**
|
|
941
|
+
* Discriminate a `GlazeColorToken` from a raw `GlazeColorValue`.
|
|
942
|
+
* Used to widen `base?` so it accepts either a token reference or a
|
|
943
|
+
* raw value (auto-wrapped into `glaze.color(value)`).
|
|
944
|
+
*/
|
|
945
|
+
function isGlazeColorToken(candidate) {
|
|
946
|
+
return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate) && "resolve" in candidate && typeof candidate.resolve === "function";
|
|
947
|
+
}
|
|
813
948
|
let globalConfig = {
|
|
814
949
|
lightLightness: [10, 100],
|
|
815
950
|
darkLightness: [15, 95],
|
|
816
951
|
darkDesaturation: .1,
|
|
952
|
+
darkCurve: .5,
|
|
817
953
|
states: {
|
|
818
954
|
dark: "@dark",
|
|
819
955
|
highContrast: "@high-contrast"
|
|
@@ -829,6 +965,41 @@ function pairNormal(p) {
|
|
|
829
965
|
function pairHC(p) {
|
|
830
966
|
return Array.isArray(p) ? p[1] : p;
|
|
831
967
|
}
|
|
968
|
+
/**
|
|
969
|
+
* Dedupe contrast warnings within a single process. The cache survives
|
|
970
|
+
* the lifetime of a token because tokens memoize their resolution; the
|
|
971
|
+
* limit is a soft cap to keep noise bounded across long-lived sessions
|
|
972
|
+
* (e.g. dev servers with HMR re-resolving themes repeatedly).
|
|
973
|
+
*/
|
|
974
|
+
const CONTRAST_WARN_CACHE_LIMIT = 256;
|
|
975
|
+
const contrastWarnCache = /* @__PURE__ */ new Set();
|
|
976
|
+
function schemeLabel(isDark, isHighContrast) {
|
|
977
|
+
if (isDark && isHighContrast) return "darkContrast";
|
|
978
|
+
if (isDark) return "dark";
|
|
979
|
+
if (isHighContrast) return "lightContrast";
|
|
980
|
+
return "light";
|
|
981
|
+
}
|
|
982
|
+
function formatContrastTarget(input, ratio) {
|
|
983
|
+
return typeof input === "string" ? `"${input}" (${ratio.toFixed(2)})` : ratio.toFixed(2);
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Slack factor below the requested target before we emit a warning.
|
|
987
|
+
* The contrast solver already overshoots by `OVERSHOOT` (currently 1%)
|
|
988
|
+
* to absorb rounding noise (`see findLightnessForContrast` in
|
|
989
|
+
* `contrast-solver.ts`), so an `actual` ratio within ~2x that overshoot
|
|
990
|
+
* is effectively a pass and not worth nagging the user about.
|
|
991
|
+
*/
|
|
992
|
+
const CONTRAST_WARN_SLACK = .98;
|
|
993
|
+
function warnContrastUnmet(name, isDark, isHighContrast, target, actual) {
|
|
994
|
+
const targetRatio = resolveMinContrast(target);
|
|
995
|
+
if (actual >= targetRatio * CONTRAST_WARN_SLACK) return;
|
|
996
|
+
const scheme = schemeLabel(isDark, isHighContrast);
|
|
997
|
+
const key = `${name}|${scheme}|${targetRatio.toFixed(3)}|${actual.toFixed(2)}`;
|
|
998
|
+
if (contrastWarnCache.has(key)) return;
|
|
999
|
+
if (contrastWarnCache.size >= CONTRAST_WARN_CACHE_LIMIT) contrastWarnCache.clear();
|
|
1000
|
+
contrastWarnCache.add(key);
|
|
1001
|
+
console.warn(`glaze: color "${name}" cannot meet contrast ${formatContrastTarget(target, targetRatio)} in ${scheme} scheme (got ${actual.toFixed(2)}). Try widening the lightness window, lowering the contrast target, or picking a base color further from this color's lightness.`);
|
|
1002
|
+
}
|
|
832
1003
|
function isShadowDef(def) {
|
|
833
1004
|
return def.type === "shadow";
|
|
834
1005
|
}
|
|
@@ -890,36 +1061,38 @@ function computeShadow(bg, fg, intensity, tuning) {
|
|
|
890
1061
|
alpha
|
|
891
1062
|
};
|
|
892
1063
|
}
|
|
893
|
-
function validateColorDefs(defs) {
|
|
894
|
-
const
|
|
1064
|
+
function validateColorDefs(defs, externalBases) {
|
|
1065
|
+
const localNames = new Set(Object.keys(defs));
|
|
1066
|
+
const allNames = new Set([...localNames, ...externalBases ? externalBases.keys() : []]);
|
|
895
1067
|
for (const [name, def] of Object.entries(defs)) {
|
|
896
1068
|
if (isShadowDef(def)) {
|
|
897
|
-
if (!
|
|
898
|
-
if (isShadowDef(defs[def.bg])) throw new Error(`glaze: shadow "${name}" bg "${def.bg}" references another shadow color.`);
|
|
1069
|
+
if (!allNames.has(def.bg)) throw new Error(`glaze: shadow "${name}" references non-existent bg "${def.bg}".`);
|
|
1070
|
+
if (localNames.has(def.bg) && isShadowDef(defs[def.bg])) throw new Error(`glaze: shadow "${name}" bg "${def.bg}" references another shadow color.`);
|
|
899
1071
|
if (def.fg !== void 0) {
|
|
900
|
-
if (!
|
|
901
|
-
if (isShadowDef(defs[def.fg])) throw new Error(`glaze: shadow "${name}" fg "${def.fg}" references another shadow color.`);
|
|
1072
|
+
if (!allNames.has(def.fg)) throw new Error(`glaze: shadow "${name}" references non-existent fg "${def.fg}".`);
|
|
1073
|
+
if (localNames.has(def.fg) && isShadowDef(defs[def.fg])) throw new Error(`glaze: shadow "${name}" fg "${def.fg}" references another shadow color.`);
|
|
902
1074
|
}
|
|
903
1075
|
continue;
|
|
904
1076
|
}
|
|
905
1077
|
if (isMixDef(def)) {
|
|
906
|
-
if (!
|
|
907
|
-
if (!
|
|
908
|
-
if (isShadowDef(defs[def.base])) throw new Error(`glaze: mix "${name}" base "${def.base}" references a shadow color.`);
|
|
909
|
-
if (isShadowDef(defs[def.target])) throw new Error(`glaze: mix "${name}" target "${def.target}" references a shadow color.`);
|
|
1078
|
+
if (!allNames.has(def.base)) throw new Error(`glaze: mix "${name}" references non-existent base "${def.base}".`);
|
|
1079
|
+
if (!allNames.has(def.target)) throw new Error(`glaze: mix "${name}" references non-existent target "${def.target}".`);
|
|
1080
|
+
if (localNames.has(def.base) && isShadowDef(defs[def.base])) throw new Error(`glaze: mix "${name}" base "${def.base}" references a shadow color.`);
|
|
1081
|
+
if (localNames.has(def.target) && isShadowDef(defs[def.target])) throw new Error(`glaze: mix "${name}" target "${def.target}" references a shadow color.`);
|
|
910
1082
|
continue;
|
|
911
1083
|
}
|
|
912
1084
|
const regDef = def;
|
|
913
1085
|
if (regDef.contrast !== void 0 && !regDef.base) throw new Error(`glaze: color "${name}" has "contrast" without "base".`);
|
|
914
1086
|
if (regDef.lightness !== void 0 && !isAbsoluteLightness(regDef.lightness) && !regDef.base) throw new Error(`glaze: color "${name}" has relative "lightness" without "base".`);
|
|
915
|
-
if (regDef.base && !
|
|
916
|
-
if (regDef.base && isShadowDef(defs[regDef.base])) throw new Error(`glaze: color "${name}" base "${regDef.base}" references a shadow color.`);
|
|
1087
|
+
if (regDef.base && !allNames.has(regDef.base)) throw new Error(`glaze: color "${name}" references non-existent base "${regDef.base}".`);
|
|
1088
|
+
if (regDef.base && localNames.has(regDef.base) && isShadowDef(defs[regDef.base])) throw new Error(`glaze: color "${name}" base "${regDef.base}" references a shadow color.`);
|
|
917
1089
|
if (!isAbsoluteLightness(regDef.lightness) && regDef.base === void 0) throw new Error(`glaze: color "${name}" must have either absolute "lightness" (root) or "base" (dependent).`);
|
|
918
1090
|
if (regDef.contrast !== void 0 && regDef.opacity !== void 0) console.warn(`glaze: color "${name}" has both "contrast" and "opacity". Opacity makes perceived lightness unpredictable.`);
|
|
919
1091
|
}
|
|
920
1092
|
const visited = /* @__PURE__ */ new Set();
|
|
921
1093
|
const inStack = /* @__PURE__ */ new Set();
|
|
922
1094
|
function dfs(name) {
|
|
1095
|
+
if (!localNames.has(name)) return;
|
|
923
1096
|
if (inStack.has(name)) throw new Error(`glaze: circular base reference detected involving "${name}".`);
|
|
924
1097
|
if (visited.has(name)) return;
|
|
925
1098
|
inStack.add(name);
|
|
@@ -937,7 +1110,7 @@ function validateColorDefs(defs) {
|
|
|
937
1110
|
inStack.delete(name);
|
|
938
1111
|
visited.add(name);
|
|
939
1112
|
}
|
|
940
|
-
for (const name of
|
|
1113
|
+
for (const name of localNames) dfs(name);
|
|
941
1114
|
}
|
|
942
1115
|
function topoSort(defs) {
|
|
943
1116
|
const result = [];
|
|
@@ -946,6 +1119,7 @@ function topoSort(defs) {
|
|
|
946
1119
|
if (visited.has(name)) return;
|
|
947
1120
|
visited.add(name);
|
|
948
1121
|
const def = defs[name];
|
|
1122
|
+
if (def === void 0) return;
|
|
949
1123
|
if (isShadowDef(def)) {
|
|
950
1124
|
visit(def.bg);
|
|
951
1125
|
if (def.fg) visit(def.fg);
|
|
@@ -961,24 +1135,53 @@ function topoSort(defs) {
|
|
|
961
1135
|
for (const name of Object.keys(defs)) visit(name);
|
|
962
1136
|
return result;
|
|
963
1137
|
}
|
|
964
|
-
|
|
1138
|
+
/**
|
|
1139
|
+
* Resolve the active lightness window for a scheme.
|
|
1140
|
+
* - HC variants always return `[0, 100]` (existing behavior, predates per-call overrides).
|
|
1141
|
+
* - Otherwise, per-call `scaling` (e.g. from `glaze.color()`'s third arg) wins;
|
|
1142
|
+
* `false` is interpreted as `[0, 100]` (no remap). Falls back to `globalConfig.*Lightness`.
|
|
1143
|
+
*/
|
|
1144
|
+
function lightnessWindow(isHighContrast, kind, scaling) {
|
|
1145
|
+
if (isHighContrast) return [0, 100];
|
|
1146
|
+
if (scaling) {
|
|
1147
|
+
const override = kind === "dark" ? scaling.darkLightness : scaling.lightLightness;
|
|
1148
|
+
if (override === false) return [0, 100];
|
|
1149
|
+
if (override !== void 0) return override;
|
|
1150
|
+
}
|
|
1151
|
+
return kind === "dark" ? globalConfig.darkLightness : globalConfig.lightLightness;
|
|
1152
|
+
}
|
|
1153
|
+
function mapLightnessLight(l, mode, isHighContrast, scaling) {
|
|
965
1154
|
if (mode === "static") return l;
|
|
966
|
-
const [lo, hi] =
|
|
1155
|
+
const [lo, hi] = lightnessWindow(isHighContrast, "light", scaling);
|
|
967
1156
|
return l * (hi - lo) / 100 + lo;
|
|
968
1157
|
}
|
|
969
|
-
function
|
|
1158
|
+
function mobiusCurve(t, beta) {
|
|
1159
|
+
if (beta >= 1) return t;
|
|
1160
|
+
return t / (t + beta * (1 - t));
|
|
1161
|
+
}
|
|
1162
|
+
function mapLightnessDark(l, mode, isHighContrast, scaling) {
|
|
970
1163
|
if (mode === "static") return l;
|
|
971
|
-
const
|
|
972
|
-
|
|
973
|
-
|
|
1164
|
+
const beta = isHighContrast ? pairHC(globalConfig.darkCurve) : pairNormal(globalConfig.darkCurve);
|
|
1165
|
+
const [darkLo, darkHi] = lightnessWindow(isHighContrast, "dark", scaling);
|
|
1166
|
+
if (mode === "fixed") return l * (darkHi - darkLo) / 100 + darkLo;
|
|
1167
|
+
const [lightLo, lightHi] = lightnessWindow(isHighContrast, "light", scaling);
|
|
1168
|
+
const t = (lightHi - (l * (lightHi - lightLo) / 100 + lightLo)) / (lightHi - lightLo);
|
|
1169
|
+
return darkLo + (darkHi - darkLo) * mobiusCurve(t, beta);
|
|
1170
|
+
}
|
|
1171
|
+
function lightMappedToDark(lightL, isHighContrast, scaling) {
|
|
1172
|
+
const beta = isHighContrast ? pairHC(globalConfig.darkCurve) : pairNormal(globalConfig.darkCurve);
|
|
1173
|
+
const [lightLo, lightHi] = lightnessWindow(isHighContrast, "light", scaling);
|
|
1174
|
+
const [darkLo, darkHi] = lightnessWindow(isHighContrast, "dark", scaling);
|
|
1175
|
+
const t = (lightHi - clamp(lightL, lightLo, lightHi)) / (lightHi - lightLo);
|
|
1176
|
+
return darkLo + (darkHi - darkLo) * mobiusCurve(t, beta);
|
|
974
1177
|
}
|
|
975
1178
|
function mapSaturationDark(s, mode) {
|
|
976
1179
|
if (mode === "static") return s;
|
|
977
1180
|
return s * (1 - globalConfig.darkDesaturation);
|
|
978
1181
|
}
|
|
979
|
-
function schemeLightnessRange(isDark, mode) {
|
|
1182
|
+
function schemeLightnessRange(isDark, mode, isHighContrast, scaling) {
|
|
980
1183
|
if (mode === "static") return [0, 1];
|
|
981
|
-
const [lo, hi] = isDark ?
|
|
1184
|
+
const [lo, hi] = lightnessWindow(isHighContrast, isDark ? "dark" : "light", scaling);
|
|
982
1185
|
return [lo / 100, hi / 100];
|
|
983
1186
|
}
|
|
984
1187
|
function clamp(v, min, max) {
|
|
@@ -1037,27 +1240,29 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
|
|
|
1037
1240
|
else {
|
|
1038
1241
|
const parsed = parseRelativeOrAbsolute(isHighContrast ? pairHC(rawLightness) : pairNormal(rawLightness));
|
|
1039
1242
|
if (parsed.relative) {
|
|
1040
|
-
|
|
1041
|
-
if (isDark && mode === "auto")
|
|
1042
|
-
preferredL = clamp(baseL + delta, 0, 100);
|
|
1043
|
-
} else if (isDark) preferredL = mapLightnessDark(parsed.value, mode);
|
|
1044
|
-
else preferredL = mapLightnessLight(parsed.value, mode);
|
|
1243
|
+
const delta = parsed.value;
|
|
1244
|
+
if (isDark && mode === "auto") preferredL = lightMappedToDark(clamp(getSchemeVariant(baseResolved, false, isHighContrast).l * 100 + delta, 0, 100), isHighContrast, ctx.scaling);
|
|
1245
|
+
else preferredL = clamp(baseL + delta, 0, 100);
|
|
1246
|
+
} else if (isDark) preferredL = mapLightnessDark(parsed.value, mode, isHighContrast, ctx.scaling);
|
|
1247
|
+
else preferredL = mapLightnessLight(parsed.value, mode, isHighContrast, ctx.scaling);
|
|
1045
1248
|
}
|
|
1046
1249
|
const rawContrast = def.contrast;
|
|
1047
1250
|
if (rawContrast !== void 0) {
|
|
1048
1251
|
const minCr = isHighContrast ? pairHC(rawContrast) : pairNormal(rawContrast);
|
|
1049
1252
|
const effectiveSat = isDark ? mapSaturationDark(satFactor * ctx.saturation / 100, mode) : satFactor * ctx.saturation / 100;
|
|
1050
1253
|
const baseLinearRgb = okhslToLinearSrgb(baseVariant.h, baseVariant.s, baseVariant.l);
|
|
1051
|
-
const
|
|
1254
|
+
const windowRange = schemeLightnessRange(isDark, mode, isHighContrast, ctx.scaling);
|
|
1255
|
+
const result = findLightnessForContrast({
|
|
1256
|
+
hue: effectiveHue,
|
|
1257
|
+
saturation: effectiveSat,
|
|
1258
|
+
preferredLightness: clamp(preferredL / 100, windowRange[0], windowRange[1]),
|
|
1259
|
+
baseLinearRgb,
|
|
1260
|
+
contrast: minCr,
|
|
1261
|
+
lightnessRange: [0, 1]
|
|
1262
|
+
});
|
|
1263
|
+
if (!result.met) warnContrastUnmet(name, isDark, isHighContrast, minCr, result.contrast);
|
|
1052
1264
|
return {
|
|
1053
|
-
l:
|
|
1054
|
-
hue: effectiveHue,
|
|
1055
|
-
saturation: effectiveSat,
|
|
1056
|
-
preferredLightness: clamp(preferredL / 100, lightnessRange[0], lightnessRange[1]),
|
|
1057
|
-
baseLinearRgb,
|
|
1058
|
-
contrast: minCr,
|
|
1059
|
-
lightnessRange
|
|
1060
|
-
}).lightness * 100,
|
|
1265
|
+
l: result.lightness * 100,
|
|
1061
1266
|
satFactor
|
|
1062
1267
|
};
|
|
1063
1268
|
}
|
|
@@ -1093,13 +1298,13 @@ function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
|
1093
1298
|
let finalL;
|
|
1094
1299
|
let finalSat;
|
|
1095
1300
|
if (isDark && isRoot) {
|
|
1096
|
-
finalL = mapLightnessDark(lightL, mode);
|
|
1301
|
+
finalL = mapLightnessDark(lightL, mode, isHighContrast, ctx.scaling);
|
|
1097
1302
|
finalSat = mapSaturationDark(satFactor * ctx.saturation / 100, mode);
|
|
1098
1303
|
} else if (isDark && !isRoot) {
|
|
1099
1304
|
finalL = lightL;
|
|
1100
1305
|
finalSat = mapSaturationDark(satFactor * ctx.saturation / 100, mode);
|
|
1101
1306
|
} else if (isRoot) {
|
|
1102
|
-
finalL = mapLightnessLight(lightL, mode);
|
|
1307
|
+
finalL = mapLightnessLight(lightL, mode, isHighContrast, ctx.scaling);
|
|
1103
1308
|
finalSat = satFactor * ctx.saturation / 100;
|
|
1104
1309
|
} else {
|
|
1105
1310
|
finalL = lightL;
|
|
@@ -1197,15 +1402,17 @@ function resolveMixForScheme(def, ctx, isDark, isHighContrast) {
|
|
|
1197
1402
|
alpha: 1
|
|
1198
1403
|
};
|
|
1199
1404
|
}
|
|
1200
|
-
function resolveAllColors(hue, saturation, defs) {
|
|
1201
|
-
validateColorDefs(defs);
|
|
1405
|
+
function resolveAllColors(hue, saturation, defs, scaling, externalBases) {
|
|
1406
|
+
validateColorDefs(defs, externalBases);
|
|
1202
1407
|
const order = topoSort(defs);
|
|
1203
1408
|
const ctx = {
|
|
1204
1409
|
hue,
|
|
1205
1410
|
saturation,
|
|
1206
1411
|
defs,
|
|
1207
|
-
resolved: /* @__PURE__ */ new Map()
|
|
1412
|
+
resolved: /* @__PURE__ */ new Map(),
|
|
1413
|
+
scaling
|
|
1208
1414
|
};
|
|
1415
|
+
if (externalBases) for (const [name, color] of externalBases) ctx.resolved.set(name, color);
|
|
1209
1416
|
function defMode(def) {
|
|
1210
1417
|
if (isShadowDef(def) || isMixDef(def)) return void 0;
|
|
1211
1418
|
return def.mode ?? "auto";
|
|
@@ -1396,10 +1603,14 @@ function createTheme(hue, saturation, initialColors) {
|
|
|
1396
1603
|
};
|
|
1397
1604
|
},
|
|
1398
1605
|
extend(options) {
|
|
1399
|
-
|
|
1400
|
-
|
|
1606
|
+
const newHue = options.hue ?? hue;
|
|
1607
|
+
const newSat = options.saturation ?? saturation;
|
|
1608
|
+
const inheritedColors = {};
|
|
1609
|
+
for (const [name, def] of Object.entries(colorDefs)) if (def.inherit !== false) inheritedColors[name] = def;
|
|
1610
|
+
return createTheme(newHue, newSat, options.colors ? {
|
|
1611
|
+
...inheritedColors,
|
|
1401
1612
|
...options.colors
|
|
1402
|
-
} : { ...
|
|
1613
|
+
} : { ...inheritedColors });
|
|
1403
1614
|
},
|
|
1404
1615
|
resolve() {
|
|
1405
1616
|
return resolveAllColors(hue, saturation, colorDefs);
|
|
@@ -1433,40 +1644,74 @@ function validatePrimaryTheme(primary, themes) {
|
|
|
1433
1644
|
throw new Error(`glaze: primary theme "${primary}" not found in palette. Available: ${available}.`);
|
|
1434
1645
|
}
|
|
1435
1646
|
}
|
|
1436
|
-
|
|
1647
|
+
/**
|
|
1648
|
+
* Resolve the effective primary for an export call.
|
|
1649
|
+
* `false` disables, a string overrides, `undefined` inherits from palette.
|
|
1650
|
+
*/
|
|
1651
|
+
function resolveEffectivePrimary(exportPrimary, palettePrimary) {
|
|
1652
|
+
if (exportPrimary === false) return void 0;
|
|
1653
|
+
return exportPrimary ?? palettePrimary;
|
|
1654
|
+
}
|
|
1655
|
+
/**
|
|
1656
|
+
* Filter a resolved color map, skipping keys already in `seen`.
|
|
1657
|
+
* Warns on collision and keeps the first-written value (first-write-wins).
|
|
1658
|
+
* Returns a new map containing only non-colliding entries.
|
|
1659
|
+
*/
|
|
1660
|
+
function filterCollisions(resolved, prefix, seen, themeName, isPrimary) {
|
|
1661
|
+
const filtered = /* @__PURE__ */ new Map();
|
|
1662
|
+
const label = isPrimary ? `${themeName} (primary)` : themeName;
|
|
1663
|
+
for (const [name, color] of resolved) {
|
|
1664
|
+
const key = `${prefix}${name}`;
|
|
1665
|
+
if (seen.has(key)) {
|
|
1666
|
+
console.warn(`glaze: token "${key}" from theme "${label}" collides with theme "${seen.get(key)}" — skipping.`);
|
|
1667
|
+
continue;
|
|
1668
|
+
}
|
|
1669
|
+
seen.set(key, label);
|
|
1670
|
+
filtered.set(name, color);
|
|
1671
|
+
}
|
|
1672
|
+
return filtered;
|
|
1673
|
+
}
|
|
1674
|
+
function createPalette(themes, paletteOptions) {
|
|
1675
|
+
validatePrimaryTheme(paletteOptions?.primary, themes);
|
|
1437
1676
|
return {
|
|
1438
1677
|
tokens(options) {
|
|
1439
|
-
|
|
1678
|
+
const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
|
|
1679
|
+
if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
|
|
1440
1680
|
const modes = resolveModes(options?.modes);
|
|
1441
1681
|
const allTokens = {};
|
|
1682
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1442
1683
|
for (const [themeName, theme] of Object.entries(themes)) {
|
|
1443
1684
|
const resolved = theme.resolve();
|
|
1444
|
-
const
|
|
1685
|
+
const prefix = resolvePrefix(options, themeName, true);
|
|
1686
|
+
const tokens = buildFlatTokenMap(filterCollisions(resolved, prefix, seen, themeName), prefix, modes, options?.format);
|
|
1445
1687
|
for (const variant of Object.keys(tokens)) {
|
|
1446
1688
|
if (!allTokens[variant]) allTokens[variant] = {};
|
|
1447
1689
|
Object.assign(allTokens[variant], tokens[variant]);
|
|
1448
1690
|
}
|
|
1449
|
-
if (themeName ===
|
|
1450
|
-
const unprefixed = buildFlatTokenMap(resolved, "", modes, options?.format);
|
|
1691
|
+
if (themeName === effectivePrimary) {
|
|
1692
|
+
const unprefixed = buildFlatTokenMap(filterCollisions(resolved, "", seen, themeName, true), "", modes, options?.format);
|
|
1451
1693
|
for (const variant of Object.keys(unprefixed)) Object.assign(allTokens[variant], unprefixed[variant]);
|
|
1452
1694
|
}
|
|
1453
1695
|
}
|
|
1454
1696
|
return allTokens;
|
|
1455
1697
|
},
|
|
1456
1698
|
tasty(options) {
|
|
1457
|
-
|
|
1699
|
+
const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
|
|
1700
|
+
if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
|
|
1458
1701
|
const states = {
|
|
1459
1702
|
dark: options?.states?.dark ?? globalConfig.states.dark,
|
|
1460
1703
|
highContrast: options?.states?.highContrast ?? globalConfig.states.highContrast
|
|
1461
1704
|
};
|
|
1462
1705
|
const modes = resolveModes(options?.modes);
|
|
1463
1706
|
const allTokens = {};
|
|
1707
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1464
1708
|
for (const [themeName, theme] of Object.entries(themes)) {
|
|
1465
1709
|
const resolved = theme.resolve();
|
|
1466
|
-
const
|
|
1710
|
+
const prefix = resolvePrefix(options, themeName, true);
|
|
1711
|
+
const tokens = buildTokenMap(filterCollisions(resolved, prefix, seen, themeName), prefix, states, modes, options?.format);
|
|
1467
1712
|
Object.assign(allTokens, tokens);
|
|
1468
|
-
if (themeName ===
|
|
1469
|
-
const unprefixed = buildTokenMap(resolved, "", states, modes, options?.format);
|
|
1713
|
+
if (themeName === effectivePrimary) {
|
|
1714
|
+
const unprefixed = buildTokenMap(filterCollisions(resolved, "", seen, themeName, true), "", states, modes, options?.format);
|
|
1470
1715
|
Object.assign(allTokens, unprefixed);
|
|
1471
1716
|
}
|
|
1472
1717
|
}
|
|
@@ -1479,7 +1724,8 @@ function createPalette(themes) {
|
|
|
1479
1724
|
return result;
|
|
1480
1725
|
},
|
|
1481
1726
|
css(options) {
|
|
1482
|
-
|
|
1727
|
+
const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
|
|
1728
|
+
if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
|
|
1483
1729
|
const suffix = options?.suffix ?? "-color";
|
|
1484
1730
|
const format = options?.format ?? "rgb";
|
|
1485
1731
|
const allLines = {
|
|
@@ -1488,17 +1734,19 @@ function createPalette(themes) {
|
|
|
1488
1734
|
lightContrast: [],
|
|
1489
1735
|
darkContrast: []
|
|
1490
1736
|
};
|
|
1737
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1491
1738
|
for (const [themeName, theme] of Object.entries(themes)) {
|
|
1492
1739
|
const resolved = theme.resolve();
|
|
1493
|
-
const
|
|
1740
|
+
const prefix = resolvePrefix(options, themeName, true);
|
|
1741
|
+
const css = buildCssMap(filterCollisions(resolved, prefix, seen, themeName), prefix, suffix, format);
|
|
1494
1742
|
for (const key of [
|
|
1495
1743
|
"light",
|
|
1496
1744
|
"dark",
|
|
1497
1745
|
"lightContrast",
|
|
1498
1746
|
"darkContrast"
|
|
1499
1747
|
]) if (css[key]) allLines[key].push(css[key]);
|
|
1500
|
-
if (themeName ===
|
|
1501
|
-
const unprefixed = buildCssMap(resolved, "", suffix, format);
|
|
1748
|
+
if (themeName === effectivePrimary) {
|
|
1749
|
+
const unprefixed = buildCssMap(filterCollisions(resolved, "", seen, themeName, true), "", suffix, format);
|
|
1502
1750
|
for (const key of [
|
|
1503
1751
|
"light",
|
|
1504
1752
|
"dark",
|
|
@@ -1516,32 +1764,407 @@ function createPalette(themes) {
|
|
|
1516
1764
|
}
|
|
1517
1765
|
};
|
|
1518
1766
|
}
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1767
|
+
/**
|
|
1768
|
+
* Matches the CSS color functions Glaze itself emits (`rgb()`, `hsl()`,
|
|
1769
|
+
* `okhsl()`, `oklch()`) plus their legacy alpha aliases (`rgba()`, `hsla()`).
|
|
1770
|
+
*
|
|
1771
|
+
* Only bare numeric components are supported. Named colors (`red`),
|
|
1772
|
+
* relative-color syntax (`from <color> ...`), and angle units other
|
|
1773
|
+
* than bare degrees (`deg` is the only suffix tolerated by `parseFloat`)
|
|
1774
|
+
* are out of scope.
|
|
1775
|
+
*/
|
|
1776
|
+
const COLOR_FN_RE = /^(rgba?|hsla?|okhsl|oklch)\(\s*([^)]*)\s*\)$/i;
|
|
1777
|
+
function parseNumberOrPercent(raw, percentScale) {
|
|
1778
|
+
if (raw.endsWith("%")) return parseFloat(raw) / 100 * percentScale;
|
|
1779
|
+
return parseFloat(raw);
|
|
1780
|
+
}
|
|
1781
|
+
/**
|
|
1782
|
+
* Split the body of a CSS color function into its components and detect
|
|
1783
|
+
* whether an alpha channel was present.
|
|
1784
|
+
*
|
|
1785
|
+
* Handles both modern slash syntax (`R G B / A` or `R, G, B / A`) and
|
|
1786
|
+
* legacy comma syntax (`R, G, B, A`). The alpha value itself is discarded
|
|
1787
|
+
* by the caller — standalone Glaze colors have no opacity field.
|
|
1788
|
+
*/
|
|
1789
|
+
function splitColorBody(body) {
|
|
1790
|
+
const slashIdx = body.indexOf("/");
|
|
1791
|
+
if (slashIdx !== -1) return {
|
|
1792
|
+
components: body.slice(0, slashIdx).trim().split(/[\s,]+/).filter(Boolean),
|
|
1793
|
+
hadAlpha: body.slice(slashIdx + 1).trim().length > 0
|
|
1794
|
+
};
|
|
1795
|
+
const components = body.split(/[\s,]+/).filter(Boolean);
|
|
1796
|
+
if (components.length === 4) {
|
|
1797
|
+
components.pop();
|
|
1798
|
+
return {
|
|
1799
|
+
components,
|
|
1800
|
+
hadAlpha: true
|
|
1801
|
+
};
|
|
1802
|
+
}
|
|
1803
|
+
return {
|
|
1804
|
+
components,
|
|
1805
|
+
hadAlpha: false
|
|
1806
|
+
};
|
|
1807
|
+
}
|
|
1808
|
+
function warnDroppedAlpha(input) {
|
|
1809
|
+
console.warn(`glaze: alpha component dropped from "${input}" (standalone color has no opacity field).`);
|
|
1810
|
+
}
|
|
1811
|
+
function parseColorString(input) {
|
|
1812
|
+
if (input.startsWith("#")) {
|
|
1813
|
+
const parsed = parseHexAlpha(input);
|
|
1814
|
+
if (!parsed) throw new Error(`glaze: invalid hex color "${input}".`);
|
|
1815
|
+
if (parsed.alpha !== void 0) warnDroppedAlpha(input);
|
|
1816
|
+
const [h, s, l] = srgbToOkhsl(parsed.rgb);
|
|
1817
|
+
return {
|
|
1818
|
+
h,
|
|
1819
|
+
s,
|
|
1820
|
+
l
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1823
|
+
const m = input.match(COLOR_FN_RE);
|
|
1824
|
+
if (!m) throw new Error(`glaze: unsupported color string "${input}".`);
|
|
1825
|
+
const fn = m[1].toLowerCase();
|
|
1826
|
+
const { components, hadAlpha } = splitColorBody(m[2].trim());
|
|
1827
|
+
if (hadAlpha) warnDroppedAlpha(input);
|
|
1828
|
+
if (components.length !== 3) throw new Error(`glaze: expected 3 components in "${input}".`);
|
|
1829
|
+
switch (fn) {
|
|
1830
|
+
case "rgb":
|
|
1831
|
+
case "rgba": {
|
|
1832
|
+
const [h, s, l] = srgbToOkhsl([
|
|
1833
|
+
parseNumberOrPercent(components[0], 255) / 255,
|
|
1834
|
+
parseNumberOrPercent(components[1], 255) / 255,
|
|
1835
|
+
parseNumberOrPercent(components[2], 255) / 255
|
|
1836
|
+
]);
|
|
1837
|
+
return {
|
|
1838
|
+
h,
|
|
1839
|
+
s,
|
|
1840
|
+
l
|
|
1841
|
+
};
|
|
1842
|
+
}
|
|
1843
|
+
case "hsl":
|
|
1844
|
+
case "hsla": {
|
|
1845
|
+
const [oh, os, ol] = srgbToOkhsl(hslToSrgb(parseFloat(components[0]), parseNumberOrPercent(components[1], 1), parseNumberOrPercent(components[2], 1)));
|
|
1846
|
+
return {
|
|
1847
|
+
h: oh,
|
|
1848
|
+
s: os,
|
|
1849
|
+
l: ol
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
case "okhsl": return {
|
|
1853
|
+
h: parseFloat(components[0]),
|
|
1854
|
+
s: parseNumberOrPercent(components[1], 1),
|
|
1855
|
+
l: parseNumberOrPercent(components[2], 1)
|
|
1856
|
+
};
|
|
1857
|
+
case "oklch": {
|
|
1858
|
+
const L = parseNumberOrPercent(components[0], 1);
|
|
1859
|
+
const C = parseNumberOrPercent(components[1], .4);
|
|
1860
|
+
const hRad = parseFloat(components[2]) * Math.PI / 180;
|
|
1861
|
+
const [h, s, l] = oklabToOkhsl([
|
|
1862
|
+
L,
|
|
1863
|
+
C * Math.cos(hRad),
|
|
1864
|
+
C * Math.sin(hRad)
|
|
1865
|
+
]);
|
|
1866
|
+
return {
|
|
1867
|
+
h,
|
|
1868
|
+
s,
|
|
1869
|
+
l
|
|
1870
|
+
};
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
throw new Error(`glaze: unsupported color function "${fn}".`);
|
|
1874
|
+
}
|
|
1875
|
+
/**
|
|
1876
|
+
* Validate a user-supplied `OkhslColor`. Catches the common 0-100 vs 0-1
|
|
1877
|
+
* confusion (the structured form uses 0-100, OKHSL objects use 0-1).
|
|
1878
|
+
*/
|
|
1879
|
+
function validateOkhslColor(value) {
|
|
1880
|
+
const { h, s, l } = value;
|
|
1881
|
+
if (!Number.isFinite(h) || !Number.isFinite(s) || !Number.isFinite(l)) throw new Error("glaze.color: OkhslColor h/s/l must be finite numbers.");
|
|
1882
|
+
if (s > 1.5 || l > 1.5) throw new Error("glaze.color: OkhslColor s/l must be in 0–1 range. Did you mean the structured form { hue, saturation, lightness } (which uses 0–100)?");
|
|
1883
|
+
}
|
|
1884
|
+
/**
|
|
1885
|
+
* Validate a user-supplied `[r, g, b]` tuple in 0-255.
|
|
1886
|
+
*/
|
|
1887
|
+
function validateRgbTuple(value) {
|
|
1888
|
+
for (const n of value) if (!Number.isFinite(n) || n < 0 || n > 255) throw new Error(`glaze.color: RGB tuple components must be finite numbers in 0–255 (got [${value.join(", ")}]).`);
|
|
1889
|
+
}
|
|
1890
|
+
/**
|
|
1891
|
+
* Validate a user-supplied `opacity` override on `glaze.color()`.
|
|
1892
|
+
* Must be a finite number in `0..=1`.
|
|
1893
|
+
*/
|
|
1894
|
+
function validateStandaloneOpacity(value) {
|
|
1895
|
+
if (!Number.isFinite(value) || value < 0 || value > 1) throw new Error(`glaze.color: opacity must be a finite number in 0–1 (got ${value}).`);
|
|
1896
|
+
}
|
|
1897
|
+
/**
|
|
1898
|
+
* Validate a structured `GlazeColorInput`. Range-checks the `hue` /
|
|
1899
|
+
* `saturation` / `lightness` numerics (and any HC-pair second value)
|
|
1900
|
+
* before the resolver sees them so out-of-range or non-finite inputs
|
|
1901
|
+
* fail with a helpful, top-level error rather than producing a
|
|
1902
|
+
* NaN-laden token. `opacity` is checked here too so all input
|
|
1903
|
+
* validation lives in one place.
|
|
1904
|
+
*/
|
|
1905
|
+
function validateStructuredInput(input) {
|
|
1906
|
+
if (!Number.isFinite(input.hue)) throw new Error(`glaze.color: structured hue must be a finite number (got ${input.hue}).`);
|
|
1907
|
+
if (!Number.isFinite(input.saturation) || input.saturation < 0 || input.saturation > 100) throw new Error(`glaze.color: structured saturation must be a finite number in 0–100 (got ${input.saturation}).`);
|
|
1908
|
+
const checkLightness = (value, label) => {
|
|
1909
|
+
if (!Number.isFinite(value) || value < 0 || value > 100) throw new Error(`glaze.color: structured ${label} must be a finite number in 0–100 (got ${value}).`);
|
|
1910
|
+
};
|
|
1911
|
+
if (Array.isArray(input.lightness)) {
|
|
1912
|
+
checkLightness(input.lightness[0], "lightness[normal]");
|
|
1913
|
+
checkLightness(input.lightness[1], "lightness[hc]");
|
|
1914
|
+
} else checkLightness(input.lightness, "lightness");
|
|
1915
|
+
if (input.saturationFactor !== void 0) {
|
|
1916
|
+
if (!Number.isFinite(input.saturationFactor) || input.saturationFactor < 0 || input.saturationFactor > 1) throw new Error(`glaze.color: structured saturationFactor must be a finite number in 0–1 (got ${input.saturationFactor}).`);
|
|
1917
|
+
}
|
|
1918
|
+
if (input.opacity !== void 0) validateStandaloneOpacity(input.opacity);
|
|
1919
|
+
}
|
|
1920
|
+
/**
|
|
1921
|
+
* Validate a user-supplied `name` override. Rejects empty / whitespace-only
|
|
1922
|
+
* strings and names colliding with `glaze`'s reserved internal sentinels.
|
|
1923
|
+
*/
|
|
1924
|
+
function validateStandaloneName(name) {
|
|
1925
|
+
if (typeof name !== "string" || name.trim() === "") throw new Error("glaze.color: name must be a non-empty string. Omit `name` if you do not want to set a debug label.");
|
|
1926
|
+
if (RESERVED_STANDALONE_NAMES.has(name)) {
|
|
1927
|
+
const reserved = [...RESERVED_STANDALONE_NAMES].map((n) => `"${n}"`).join(", ");
|
|
1928
|
+
throw new Error(`glaze.color: name "${name}" is reserved (used internally). Reserved names are: ${reserved}. Pick a different name.`);
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
/**
|
|
1932
|
+
* Extract an OKHSL color from any `GlazeColorValue` form. Also used by
|
|
1933
|
+
* `glaze.shadow()` so all shadow inputs (hex, color functions, OKHSL,
|
|
1934
|
+
* RGB tuple) go through one parser.
|
|
1935
|
+
*/
|
|
1936
|
+
function extractOkhslFromValue(value) {
|
|
1937
|
+
if (typeof value === "string") return parseColorString(value);
|
|
1938
|
+
if (Array.isArray(value)) {
|
|
1939
|
+
const tuple = value;
|
|
1940
|
+
validateRgbTuple(tuple);
|
|
1941
|
+
const [r, g, b] = tuple;
|
|
1942
|
+
const [h, s, l] = srgbToOkhsl([
|
|
1943
|
+
r / 255,
|
|
1944
|
+
g / 255,
|
|
1945
|
+
b / 255
|
|
1946
|
+
]);
|
|
1947
|
+
return {
|
|
1948
|
+
h,
|
|
1949
|
+
s,
|
|
1950
|
+
l
|
|
1951
|
+
};
|
|
1952
|
+
}
|
|
1953
|
+
validateOkhslColor(value);
|
|
1954
|
+
return value;
|
|
1955
|
+
}
|
|
1956
|
+
/**
|
|
1957
|
+
* Build the `ColorMap` for a value-shorthand `glaze.color()` call.
|
|
1958
|
+
*
|
|
1959
|
+
* The user-facing color (`STANDALONE_VALUE`) defaults to `mode: 'auto'`
|
|
1960
|
+
* across every value-shorthand form. String inputs pair with the
|
|
1961
|
+
* extended dark window so a totally-black input renders as totally-white
|
|
1962
|
+
* in dark mode; `OkhslColor` / RGB-tuple inputs auto-adapt into the
|
|
1963
|
+
* snapshotted `globalConfig.lightLightness` / `globalConfig.darkLightness`
|
|
1964
|
+
* windows.
|
|
1965
|
+
*
|
|
1966
|
+
* When the user requests `contrast` or relative `lightness`, a hidden
|
|
1967
|
+
* `STANDALONE_SEED` def is synthesized at `mode: 'static'`. That keeps
|
|
1968
|
+
* the seed pinned to the literal user-provided color across all four
|
|
1969
|
+
* variants, so the contrast solver always anchors against it.
|
|
1970
|
+
*/
|
|
1971
|
+
function buildStandaloneValueDefs(main, options) {
|
|
1972
|
+
const seedHue = typeof options?.hue === "number" ? options.hue : main.h;
|
|
1973
|
+
const seedSaturation = options?.saturation ?? main.s * 100;
|
|
1974
|
+
const relativeHue = typeof options?.hue === "string" ? options.hue : void 0;
|
|
1975
|
+
const lightnessOption = options?.lightness;
|
|
1976
|
+
const hasExternalBase = options?.base !== void 0;
|
|
1977
|
+
const needsSeedAnchor = !hasExternalBase && (options?.contrast !== void 0 || lightnessOption !== void 0 && !isAbsoluteLightness(lightnessOption));
|
|
1978
|
+
if (options?.opacity !== void 0) validateStandaloneOpacity(options.opacity);
|
|
1979
|
+
const userName = options?.name;
|
|
1980
|
+
if (userName !== void 0) validateStandaloneName(userName);
|
|
1981
|
+
const primary = userName ?? STANDALONE_VALUE;
|
|
1982
|
+
const valueDef = {
|
|
1983
|
+
hue: relativeHue,
|
|
1984
|
+
saturation: options?.saturationFactor,
|
|
1985
|
+
lightness: lightnessOption ?? main.l * 100,
|
|
1986
|
+
contrast: options?.contrast,
|
|
1987
|
+
mode: options?.mode ?? "auto",
|
|
1988
|
+
opacity: options?.opacity,
|
|
1989
|
+
base: hasExternalBase ? STANDALONE_BASE : needsSeedAnchor ? STANDALONE_SEED : void 0
|
|
1990
|
+
};
|
|
1991
|
+
const defs = { [primary]: valueDef };
|
|
1992
|
+
if (needsSeedAnchor) defs[STANDALONE_SEED] = {
|
|
1993
|
+
hue: main.h,
|
|
1994
|
+
saturation: 1,
|
|
1995
|
+
lightness: main.l * 100,
|
|
1996
|
+
mode: "static"
|
|
1997
|
+
};
|
|
1998
|
+
return {
|
|
1999
|
+
seedHue,
|
|
2000
|
+
seedSaturation,
|
|
2001
|
+
defs,
|
|
2002
|
+
primary
|
|
2003
|
+
};
|
|
2004
|
+
}
|
|
2005
|
+
function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effectiveScaling, baseToken, exportData) {
|
|
2006
|
+
let cached;
|
|
2007
|
+
const resolveOnce = () => {
|
|
2008
|
+
if (cached) return cached;
|
|
2009
|
+
cached = resolveAllColors(seedHue, seedSaturation, defs, effectiveScaling, baseToken ? new Map([[STANDALONE_BASE, baseToken.resolve()]]) : void 0);
|
|
2010
|
+
return cached;
|
|
2011
|
+
};
|
|
2012
|
+
const resolveStates = (options) => ({
|
|
2013
|
+
dark: options?.states?.dark ?? globalConfig.states.dark,
|
|
2014
|
+
highContrast: options?.states?.highContrast ?? globalConfig.states.highContrast
|
|
2015
|
+
});
|
|
2016
|
+
const tokenLike = (options) => {
|
|
2017
|
+
return buildTokenMap(resolveOnce(), "", resolveStates(options), resolveModes(options?.modes), options?.format)[`#${primary}`];
|
|
2018
|
+
};
|
|
1525
2019
|
return {
|
|
1526
2020
|
resolve() {
|
|
1527
|
-
return
|
|
2021
|
+
return resolveOnce().get(primary);
|
|
1528
2022
|
},
|
|
1529
|
-
token
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
}, resolveModes(options?.modes), options?.format)["#__color__"];
|
|
2023
|
+
token: tokenLike,
|
|
2024
|
+
tasty: tokenLike,
|
|
2025
|
+
json(options) {
|
|
2026
|
+
return buildJsonMap(resolveOnce(), resolveModes(options?.modes), options?.format)[primary];
|
|
1534
2027
|
},
|
|
1535
|
-
|
|
1536
|
-
return
|
|
1537
|
-
dark: options?.states?.dark ?? globalConfig.states.dark,
|
|
1538
|
-
highContrast: options?.states?.highContrast ?? globalConfig.states.highContrast
|
|
1539
|
-
}, resolveModes(options?.modes), options?.format)["#__color__"];
|
|
2028
|
+
css(options) {
|
|
2029
|
+
return buildCssMap(new Map([[options.name, resolveOnce().get(primary)]]), "", options.suffix ?? "-color", options.format ?? "rgb");
|
|
1540
2030
|
},
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
2031
|
+
export: exportData
|
|
2032
|
+
};
|
|
2033
|
+
}
|
|
2034
|
+
/**
|
|
2035
|
+
* Resolve `base` (which may be a token reference or a raw color value)
|
|
2036
|
+
* into a `GlazeColorToken`. Raw values are auto-wrapped via
|
|
2037
|
+
* `glaze.color(value)` so they pick up the same auto-invert defaults as
|
|
2038
|
+
* an explicit wrap. Returns `undefined` when no base is provided.
|
|
2039
|
+
*/
|
|
2040
|
+
function resolveBaseToken(base) {
|
|
2041
|
+
if (base === void 0) return void 0;
|
|
2042
|
+
if (isGlazeColorToken(base)) return base;
|
|
2043
|
+
return createColorTokenFromValue(base, void 0, void 0);
|
|
2044
|
+
}
|
|
2045
|
+
/**
|
|
2046
|
+
* Build a JSON-safe snapshot of `GlazeColorOverrides`. `base` is
|
|
2047
|
+
* recursively serialized when it was originally a token; raw values are
|
|
2048
|
+
* preserved as-is so `glaze.colorFrom(...)` round-trips them.
|
|
2049
|
+
*/
|
|
2050
|
+
function buildOverridesExport(options) {
|
|
2051
|
+
const out = {};
|
|
2052
|
+
if (options.hue !== void 0) out.hue = options.hue;
|
|
2053
|
+
if (options.saturation !== void 0) out.saturation = options.saturation;
|
|
2054
|
+
if (options.lightness !== void 0) out.lightness = options.lightness;
|
|
2055
|
+
if (options.saturationFactor !== void 0) out.saturationFactor = options.saturationFactor;
|
|
2056
|
+
if (options.mode !== void 0) out.mode = options.mode;
|
|
2057
|
+
if (options.contrast !== void 0) out.contrast = options.contrast;
|
|
2058
|
+
if (options.opacity !== void 0) out.opacity = options.opacity;
|
|
2059
|
+
if (options.name !== void 0) out.name = options.name;
|
|
2060
|
+
if (options.base !== void 0) out.base = isGlazeColorToken(options.base) ? options.base.export() : options.base;
|
|
2061
|
+
return out;
|
|
2062
|
+
}
|
|
2063
|
+
function buildStructuredInputExport(input) {
|
|
2064
|
+
const out = {
|
|
2065
|
+
hue: input.hue,
|
|
2066
|
+
saturation: input.saturation,
|
|
2067
|
+
lightness: input.lightness
|
|
2068
|
+
};
|
|
2069
|
+
if (input.saturationFactor !== void 0) out.saturationFactor = input.saturationFactor;
|
|
2070
|
+
if (input.mode !== void 0) out.mode = input.mode;
|
|
2071
|
+
if (input.opacity !== void 0) out.opacity = input.opacity;
|
|
2072
|
+
if (input.contrast !== void 0) out.contrast = input.contrast;
|
|
2073
|
+
if (input.name !== void 0) out.name = input.name;
|
|
2074
|
+
if (input.base !== void 0) out.base = isGlazeColorToken(input.base) ? input.base.export() : input.base;
|
|
2075
|
+
return out;
|
|
2076
|
+
}
|
|
2077
|
+
function createColorToken(input, scaling) {
|
|
2078
|
+
validateStructuredInput(input);
|
|
2079
|
+
const userName = input.name;
|
|
2080
|
+
if (userName !== void 0) validateStandaloneName(userName);
|
|
2081
|
+
const primary = userName ?? STANDALONE_VALUE;
|
|
2082
|
+
const baseToken = resolveBaseToken(input.base);
|
|
2083
|
+
const hasExternalBase = baseToken !== void 0;
|
|
2084
|
+
const needsSeedAnchor = !hasExternalBase && input.contrast !== void 0;
|
|
2085
|
+
const defs = { [primary]: {
|
|
2086
|
+
lightness: input.lightness,
|
|
2087
|
+
saturation: input.saturationFactor,
|
|
2088
|
+
mode: input.mode ?? "auto",
|
|
2089
|
+
contrast: input.contrast,
|
|
2090
|
+
opacity: input.opacity,
|
|
2091
|
+
base: hasExternalBase ? STANDALONE_BASE : needsSeedAnchor ? STANDALONE_SEED : void 0
|
|
2092
|
+
} };
|
|
2093
|
+
if (needsSeedAnchor) defs[STANDALONE_SEED] = {
|
|
2094
|
+
lightness: pairNormal(input.lightness),
|
|
2095
|
+
saturation: 1,
|
|
2096
|
+
mode: "static"
|
|
2097
|
+
};
|
|
2098
|
+
const effectiveScaling = scaling ?? defaultStandaloneScaling(false);
|
|
2099
|
+
const exportData = () => ({
|
|
2100
|
+
form: "structured",
|
|
2101
|
+
input: buildStructuredInputExport(input),
|
|
2102
|
+
scaling: effectiveScaling
|
|
2103
|
+
});
|
|
2104
|
+
return createColorTokenFromDefs(input.hue, input.saturation, defs, primary, effectiveScaling, baseToken, exportData);
|
|
2105
|
+
}
|
|
2106
|
+
function createColorTokenFromValue(value, options, scaling) {
|
|
2107
|
+
const inputIsString = typeof value === "string";
|
|
2108
|
+
const main = extractOkhslFromValue(value);
|
|
2109
|
+
const baseToken = resolveBaseToken(options?.base);
|
|
2110
|
+
const { seedHue, seedSaturation, defs, primary } = buildStandaloneValueDefs(main, options);
|
|
2111
|
+
const effectiveScaling = scaling ?? defaultStandaloneScaling(inputIsString);
|
|
2112
|
+
const exportData = () => ({
|
|
2113
|
+
form: "value",
|
|
2114
|
+
input: value,
|
|
2115
|
+
...options !== void 0 ? { overrides: buildOverridesExport(options) } : {},
|
|
2116
|
+
scaling: effectiveScaling
|
|
2117
|
+
});
|
|
2118
|
+
return createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effectiveScaling, baseToken, exportData);
|
|
2119
|
+
}
|
|
2120
|
+
/**
|
|
2121
|
+
* Rehydrate a token from its `.export()` snapshot. Recursively rebuilds
|
|
2122
|
+
* any base dependency. Inverse of `GlazeColorToken.export()`.
|
|
2123
|
+
*/
|
|
2124
|
+
function colorFromExport(data) {
|
|
2125
|
+
if (data === null || typeof data !== "object") throw new Error(`glaze.colorFrom: expected an object from token.export(), got ${data === null ? "null" : typeof data}.`);
|
|
2126
|
+
if (data.form !== "value" && data.form !== "structured") throw new Error(`glaze.colorFrom: invalid "form" field — expected "value" or "structured" (got ${JSON.stringify(data.form)}).`);
|
|
2127
|
+
if (data.input === void 0) throw new Error(`glaze.colorFrom: missing "input" field — expected the original ${data.form === "value" ? "GlazeColorValue" : "GlazeColorInput"}.`);
|
|
2128
|
+
if (data.form === "value") {
|
|
2129
|
+
const value = data.input;
|
|
2130
|
+
return createColorTokenFromValue(value, data.overrides ? rehydrateOverrides(data.overrides) : void 0, data.scaling);
|
|
2131
|
+
}
|
|
2132
|
+
return createColorToken(rehydrateStructuredInput(data.input), data.scaling);
|
|
2133
|
+
}
|
|
2134
|
+
function rehydrateOverrides(data) {
|
|
2135
|
+
const out = {};
|
|
2136
|
+
if (data.hue !== void 0) out.hue = data.hue;
|
|
2137
|
+
if (data.saturation !== void 0) out.saturation = data.saturation;
|
|
2138
|
+
if (data.lightness !== void 0) out.lightness = data.lightness;
|
|
2139
|
+
if (data.saturationFactor !== void 0) out.saturationFactor = data.saturationFactor;
|
|
2140
|
+
if (data.mode !== void 0) out.mode = data.mode;
|
|
2141
|
+
if (data.contrast !== void 0) out.contrast = data.contrast;
|
|
2142
|
+
if (data.opacity !== void 0) out.opacity = data.opacity;
|
|
2143
|
+
if (data.name !== void 0) out.name = data.name;
|
|
2144
|
+
if (data.base !== void 0) out.base = isExportedToken(data.base) ? colorFromExport(data.base) : data.base;
|
|
2145
|
+
return out;
|
|
2146
|
+
}
|
|
2147
|
+
function rehydrateStructuredInput(data) {
|
|
2148
|
+
const out = {
|
|
2149
|
+
hue: data.hue,
|
|
2150
|
+
saturation: data.saturation,
|
|
2151
|
+
lightness: data.lightness
|
|
1544
2152
|
};
|
|
2153
|
+
if (data.saturationFactor !== void 0) out.saturationFactor = data.saturationFactor;
|
|
2154
|
+
if (data.mode !== void 0) out.mode = data.mode;
|
|
2155
|
+
if (data.opacity !== void 0) out.opacity = data.opacity;
|
|
2156
|
+
if (data.contrast !== void 0) out.contrast = data.contrast;
|
|
2157
|
+
if (data.name !== void 0) out.name = data.name;
|
|
2158
|
+
if (data.base !== void 0) out.base = isExportedToken(data.base) ? colorFromExport(data.base) : data.base;
|
|
2159
|
+
return out;
|
|
2160
|
+
}
|
|
2161
|
+
/**
|
|
2162
|
+
* Discriminate a `GlazeColorTokenExport` from a raw `GlazeColorValue`.
|
|
2163
|
+
* `GlazeColorTokenExport` always has a `form` field set to either
|
|
2164
|
+
* `'value'` or `'structured'`; raw values never do.
|
|
2165
|
+
*/
|
|
2166
|
+
function isExportedToken(candidate) {
|
|
2167
|
+
return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate) && "form" in candidate && (candidate.form === "value" || candidate.form === "structured");
|
|
1545
2168
|
}
|
|
1546
2169
|
/**
|
|
1547
2170
|
* Create a single-hue glaze theme.
|
|
@@ -1565,6 +2188,7 @@ glaze.configure = function configure(config) {
|
|
|
1565
2188
|
lightLightness: config.lightLightness ?? globalConfig.lightLightness,
|
|
1566
2189
|
darkLightness: config.darkLightness ?? globalConfig.darkLightness,
|
|
1567
2190
|
darkDesaturation: config.darkDesaturation ?? globalConfig.darkDesaturation,
|
|
2191
|
+
darkCurve: config.darkCurve ?? globalConfig.darkCurve,
|
|
1568
2192
|
states: {
|
|
1569
2193
|
dark: config.states?.dark ?? globalConfig.states.dark,
|
|
1570
2194
|
highContrast: config.states?.highContrast ?? globalConfig.states.highContrast
|
|
@@ -1579,8 +2203,8 @@ glaze.configure = function configure(config) {
|
|
|
1579
2203
|
/**
|
|
1580
2204
|
* Compose multiple themes into a palette.
|
|
1581
2205
|
*/
|
|
1582
|
-
glaze.palette = function palette(themes) {
|
|
1583
|
-
return createPalette(themes);
|
|
2206
|
+
glaze.palette = function palette(themes, options) {
|
|
2207
|
+
return createPalette(themes, options);
|
|
1584
2208
|
};
|
|
1585
2209
|
/**
|
|
1586
2210
|
* Create a theme from a serialized export.
|
|
@@ -1588,18 +2212,63 @@ glaze.palette = function palette(themes) {
|
|
|
1588
2212
|
glaze.from = function from(data) {
|
|
1589
2213
|
return createTheme(data.hue, data.saturation, data.colors);
|
|
1590
2214
|
};
|
|
2215
|
+
function isStructuredColorInput(input) {
|
|
2216
|
+
return typeof input === "object" && input !== null && !Array.isArray(input) && "hue" in input && "lightness" in input;
|
|
2217
|
+
}
|
|
1591
2218
|
/**
|
|
1592
2219
|
* Create a standalone single-color token.
|
|
2220
|
+
*
|
|
2221
|
+
* Two overloads:
|
|
2222
|
+
* - `glaze.color(input, scaling?)` — structured form:
|
|
2223
|
+
* `{ hue, saturation, lightness, ... }` plus an optional per-call
|
|
2224
|
+
* lightness-window override.
|
|
2225
|
+
* - `glaze.color(value, overrides?, scaling?)` — value-shorthand: a hex
|
|
2226
|
+
* string (3/6/8 digits), one of the CSS color functions Glaze itself
|
|
2227
|
+
* emits (`rgb()`, `hsl()`, `okhsl()`, `oklch()`), an `OkhslColor`
|
|
2228
|
+
* object `{ h, s, l }` (0–1 ranges), or an `[r, g, b]` (0–255) tuple.
|
|
2229
|
+
*
|
|
2230
|
+
* Defaults: every input form defaults to `mode: 'auto'` so colors
|
|
2231
|
+
* automatically adapt between light and dark like an ordinary theme
|
|
2232
|
+
* color. The scaling snapshot taken at create time differs by input
|
|
2233
|
+
* form:
|
|
2234
|
+
* - String value-shorthand: `{ lightLightness: false, darkLightness:
|
|
2235
|
+
* [globalConfig.darkLightness[0], 100] }`. Light preserves the input
|
|
2236
|
+
* exactly; dark Möbius-inverts up to 100, so `glaze.color('#000')`
|
|
2237
|
+
* renders as `#fff` in dark mode (and `glaze.color('#fff')` falls to
|
|
2238
|
+
* the dark `lo` floor).
|
|
2239
|
+
* - `OkhslColor` object / RGB-tuple / structured value-shorthand:
|
|
2240
|
+
* `{ lightLightness: globalConfig.lightLightness, darkLightness:
|
|
2241
|
+
* globalConfig.darkLightness }` — both windows come straight from
|
|
2242
|
+
* `globalConfig`, so the resulting token behaves like a theme color.
|
|
2243
|
+
*
|
|
2244
|
+
* Pass `{ mode: 'fixed' }` to opt back into the legacy linear, non-
|
|
2245
|
+
* inverting mapping, or `{ mode: 'static' }` to pin the same lightness
|
|
2246
|
+
* across every variant.
|
|
2247
|
+
*
|
|
2248
|
+
* Relative `lightness: '+N'` and `contrast: <ratio>` are anchored to
|
|
2249
|
+
* the literal seed (the value passed in) by default, pinned at
|
|
2250
|
+
* `mode: 'static'` across all four variants. Pass `overrides.base` (a
|
|
2251
|
+
* `GlazeColorToken`) to anchor `contrast` and relative `lightness`
|
|
2252
|
+
* against another color's resolved variant per scheme instead. Relative
|
|
2253
|
+
* `hue: '+N'` always anchors to the seed.
|
|
2254
|
+
*
|
|
2255
|
+
* Alpha components in `rgba()` / `hsla()` / slash-alpha syntax and
|
|
2256
|
+
* 8-digit hex are parsed but dropped with a `console.warn`.
|
|
1593
2257
|
*/
|
|
1594
|
-
glaze.color = function color(input) {
|
|
1595
|
-
return createColorToken(input);
|
|
2258
|
+
glaze.color = function color(input, arg2, arg3) {
|
|
2259
|
+
if (isStructuredColorInput(input)) return createColorToken(input, arg2);
|
|
2260
|
+
return createColorTokenFromValue(input, arg2, arg3);
|
|
1596
2261
|
};
|
|
1597
2262
|
/**
|
|
1598
2263
|
* Compute a shadow color from a bg/fg pair and intensity.
|
|
2264
|
+
*
|
|
2265
|
+
* Both `bg` and `fg` accept any `GlazeColorValue` form: hex (`#rgb` /
|
|
2266
|
+
* `#rrggbb` / `#rrggbbaa`), `rgb()` / `hsl()` / `okhsl()` / `oklch()`
|
|
2267
|
+
* strings, `OkhslColor` objects, or `[r, g, b]` (0–255) tuples.
|
|
1599
2268
|
*/
|
|
1600
2269
|
glaze.shadow = function shadow(input) {
|
|
1601
|
-
const bg =
|
|
1602
|
-
const fg = input.fg ?
|
|
2270
|
+
const bg = extractOkhslFromValue(input.bg);
|
|
2271
|
+
const fg = input.fg ? extractOkhslFromValue(input.fg) : void 0;
|
|
1603
2272
|
const tuning = resolveShadowTuning(input.tuning);
|
|
1604
2273
|
return computeShadow({
|
|
1605
2274
|
...bg,
|
|
@@ -1615,19 +2284,6 @@ glaze.shadow = function shadow(input) {
|
|
|
1615
2284
|
glaze.format = function format(variant, colorFormat) {
|
|
1616
2285
|
return formatVariant(variant, colorFormat);
|
|
1617
2286
|
};
|
|
1618
|
-
function parseOkhslInput(input) {
|
|
1619
|
-
if (typeof input === "string") {
|
|
1620
|
-
const rgb = parseHex(input);
|
|
1621
|
-
if (!rgb) throw new Error(`glaze: invalid hex color "${input}".`);
|
|
1622
|
-
const [h, s, l] = srgbToOkhsl(rgb);
|
|
1623
|
-
return {
|
|
1624
|
-
h,
|
|
1625
|
-
s,
|
|
1626
|
-
l
|
|
1627
|
-
};
|
|
1628
|
-
}
|
|
1629
|
-
return input;
|
|
1630
|
-
}
|
|
1631
2287
|
/**
|
|
1632
2288
|
* Create a theme from a hex color string.
|
|
1633
2289
|
* Extracts hue and saturation from the color.
|
|
@@ -1651,6 +2307,26 @@ glaze.fromRgb = function fromRgb(r, g, b) {
|
|
|
1651
2307
|
return createTheme(h, s * 100);
|
|
1652
2308
|
};
|
|
1653
2309
|
/**
|
|
2310
|
+
* Rehydrate a `glaze.color()` token from a `.export()` snapshot.
|
|
2311
|
+
*
|
|
2312
|
+
* The snapshot is a plain JSON-safe object containing the original
|
|
2313
|
+
* input value, overrides (with any `base` token recursively serialized),
|
|
2314
|
+
* and the captured scaling. The reconstructed token is identical in
|
|
2315
|
+
* behavior to the original at the time of export.
|
|
2316
|
+
*
|
|
2317
|
+
* @example
|
|
2318
|
+
* ```ts
|
|
2319
|
+
* const text = glaze.color('#1a1a1a', { contrast: 'AA' });
|
|
2320
|
+
* const data = text.export(); // JSON-safe
|
|
2321
|
+
* localStorage.setItem('text', JSON.stringify(data));
|
|
2322
|
+
* // ...later...
|
|
2323
|
+
* const restored = glaze.colorFrom(JSON.parse(localStorage.getItem('text')!));
|
|
2324
|
+
* ```
|
|
2325
|
+
*/
|
|
2326
|
+
glaze.colorFrom = function colorFrom(data) {
|
|
2327
|
+
return colorFromExport(data);
|
|
2328
|
+
};
|
|
2329
|
+
/**
|
|
1654
2330
|
* Get the current global configuration (for testing/debugging).
|
|
1655
2331
|
*/
|
|
1656
2332
|
glaze.getConfig = function getConfig() {
|
|
@@ -1664,6 +2340,7 @@ glaze.resetConfig = function resetConfig() {
|
|
|
1664
2340
|
lightLightness: [10, 100],
|
|
1665
2341
|
darkLightness: [15, 95],
|
|
1666
2342
|
darkDesaturation: .1,
|
|
2343
|
+
darkCurve: .5,
|
|
1667
2344
|
states: {
|
|
1668
2345
|
dark: "@dark",
|
|
1669
2346
|
highContrast: "@high-contrast"
|
|
@@ -1685,10 +2362,13 @@ exports.formatOklch = formatOklch;
|
|
|
1685
2362
|
exports.formatRgb = formatRgb;
|
|
1686
2363
|
exports.gamutClampedLuminance = gamutClampedLuminance;
|
|
1687
2364
|
exports.glaze = glaze;
|
|
2365
|
+
exports.hslToSrgb = hslToSrgb;
|
|
1688
2366
|
exports.okhslToLinearSrgb = okhslToLinearSrgb;
|
|
1689
2367
|
exports.okhslToOklab = okhslToOklab;
|
|
1690
2368
|
exports.okhslToSrgb = okhslToSrgb;
|
|
2369
|
+
exports.oklabToOkhsl = oklabToOkhsl;
|
|
1691
2370
|
exports.parseHex = parseHex;
|
|
2371
|
+
exports.parseHexAlpha = parseHexAlpha;
|
|
1692
2372
|
exports.relativeLuminanceFromLinearRgb = relativeLuminanceFromLinearRgb;
|
|
1693
2373
|
exports.resolveMinContrast = resolveMinContrast;
|
|
1694
2374
|
exports.srgbToOkhsl = srgbToOkhsl;
|