@tenphi/glaze 0.9.2 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +310 -13
- package/dist/index.cjs +679 -83
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +269 -10
- package/dist/index.d.mts +269 -10
- package/dist/index.mjs +677 -84
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -344,6 +344,11 @@ function gamutClampedLuminance(linearRgb) {
|
|
|
344
344
|
const linearSrgbToOklab = (rgb) => {
|
|
345
345
|
return transform(cbrt3(transform(rgb, linear_sRGB_to_LMS_M)), LMS_to_OKLab_M);
|
|
346
346
|
};
|
|
347
|
+
/**
|
|
348
|
+
* Convert OKLab to OKHSL.
|
|
349
|
+
* Input: [L, a, b] where L: 0–1, a/b: roughly -0.5 to 0.5.
|
|
350
|
+
* Returns [h, s, l] where h: 0–360, s: 0–1, l: 0–1.
|
|
351
|
+
*/
|
|
347
352
|
const oklabToOkhsl = (lab) => {
|
|
348
353
|
const L = lab[0];
|
|
349
354
|
const a = lab[1];
|
|
@@ -391,32 +396,108 @@ function srgbToOkhsl(rgb) {
|
|
|
391
396
|
]));
|
|
392
397
|
}
|
|
393
398
|
/**
|
|
399
|
+
* Convert CSS HSL (sRGB-based) to gamma-encoded sRGB [r, g, b] in 0–1 range.
|
|
400
|
+
* h: 0–360, s: 0–1, l: 0–1.
|
|
401
|
+
*
|
|
402
|
+
* Note: CSS HSL is not the same as OKHSL — it's HSL in the sRGB color space.
|
|
403
|
+
* Use this when parsing `hsl(...)` strings before passing to `srgbToOkhsl`.
|
|
404
|
+
*/
|
|
405
|
+
function hslToSrgb(h, s, l) {
|
|
406
|
+
const hh = (h % 360 + 360) % 360 / 360;
|
|
407
|
+
const ss = clampVal(s, 0, 1);
|
|
408
|
+
const ll = clampVal(l, 0, 1);
|
|
409
|
+
if (ss === 0) return [
|
|
410
|
+
ll,
|
|
411
|
+
ll,
|
|
412
|
+
ll
|
|
413
|
+
];
|
|
414
|
+
const q = ll < .5 ? ll * (1 + ss) : ll + ss - ll * ss;
|
|
415
|
+
const p = 2 * ll - q;
|
|
416
|
+
const hueToChannel = (t) => {
|
|
417
|
+
let tt = t;
|
|
418
|
+
if (tt < 0) tt += 1;
|
|
419
|
+
if (tt > 1) tt -= 1;
|
|
420
|
+
if (tt < 1 / 6) return p + (q - p) * 6 * tt;
|
|
421
|
+
if (tt < 1 / 2) return q;
|
|
422
|
+
if (tt < 2 / 3) return p + (q - p) * (2 / 3 - tt) * 6;
|
|
423
|
+
return p;
|
|
424
|
+
};
|
|
425
|
+
return [
|
|
426
|
+
hueToChannel(hh + 1 / 3),
|
|
427
|
+
hueToChannel(hh),
|
|
428
|
+
hueToChannel(hh - 1 / 3)
|
|
429
|
+
];
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
394
432
|
* Parse a hex color string (#rgb or #rrggbb) to sRGB [r, g, b] in 0–1 range.
|
|
395
433
|
* Returns null if the string is not a valid hex color.
|
|
434
|
+
*
|
|
435
|
+
* For 8-digit hex (`#rrggbbaa`) and 4-digit hex (`#rgba`) with alpha,
|
|
436
|
+
* use {@link parseHexAlpha}.
|
|
396
437
|
*/
|
|
397
438
|
function parseHex(hex) {
|
|
439
|
+
const result = parseHexAlpha(hex);
|
|
440
|
+
if (!result || result.alpha !== void 0) return null;
|
|
441
|
+
return result.rgb;
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Parse a hex color string (#rgb, #rrggbb, #rgba, or #rrggbbaa) to
|
|
445
|
+
* sRGB [r, g, b] in 0–1 range plus an optional alpha (0–1).
|
|
446
|
+
* Returns null if the string is not a valid hex color.
|
|
447
|
+
*/
|
|
448
|
+
function parseHexAlpha(hex) {
|
|
398
449
|
const h = hex.startsWith("#") ? hex.slice(1) : hex;
|
|
399
450
|
if (h.length === 3) {
|
|
400
451
|
const r = parseInt(h[0] + h[0], 16);
|
|
401
452
|
const g = parseInt(h[1] + h[1], 16);
|
|
402
453
|
const b = parseInt(h[2] + h[2], 16);
|
|
403
454
|
if (isNaN(r) || isNaN(g) || isNaN(b)) return null;
|
|
404
|
-
return [
|
|
455
|
+
return { rgb: [
|
|
405
456
|
r / 255,
|
|
406
457
|
g / 255,
|
|
407
458
|
b / 255
|
|
408
|
-
];
|
|
459
|
+
] };
|
|
460
|
+
}
|
|
461
|
+
if (h.length === 4) {
|
|
462
|
+
const r = parseInt(h[0] + h[0], 16);
|
|
463
|
+
const g = parseInt(h[1] + h[1], 16);
|
|
464
|
+
const b = parseInt(h[2] + h[2], 16);
|
|
465
|
+
const a = parseInt(h[3] + h[3], 16);
|
|
466
|
+
if (isNaN(r) || isNaN(g) || isNaN(b) || isNaN(a)) return null;
|
|
467
|
+
return {
|
|
468
|
+
rgb: [
|
|
469
|
+
r / 255,
|
|
470
|
+
g / 255,
|
|
471
|
+
b / 255
|
|
472
|
+
],
|
|
473
|
+
alpha: a / 255
|
|
474
|
+
};
|
|
409
475
|
}
|
|
410
476
|
if (h.length === 6) {
|
|
411
477
|
const r = parseInt(h.slice(0, 2), 16);
|
|
412
478
|
const g = parseInt(h.slice(2, 4), 16);
|
|
413
479
|
const b = parseInt(h.slice(4, 6), 16);
|
|
414
480
|
if (isNaN(r) || isNaN(g) || isNaN(b)) return null;
|
|
415
|
-
return [
|
|
481
|
+
return { rgb: [
|
|
416
482
|
r / 255,
|
|
417
483
|
g / 255,
|
|
418
484
|
b / 255
|
|
419
|
-
];
|
|
485
|
+
] };
|
|
486
|
+
}
|
|
487
|
+
if (h.length === 8) {
|
|
488
|
+
const r = parseInt(h.slice(0, 2), 16);
|
|
489
|
+
const g = parseInt(h.slice(2, 4), 16);
|
|
490
|
+
const b = parseInt(h.slice(4, 6), 16);
|
|
491
|
+
const a = parseInt(h.slice(6, 8), 16);
|
|
492
|
+
if (isNaN(r) || isNaN(g) || isNaN(b) || isNaN(a)) return null;
|
|
493
|
+
return {
|
|
494
|
+
rgb: [
|
|
495
|
+
r / 255,
|
|
496
|
+
g / 255,
|
|
497
|
+
b / 255
|
|
498
|
+
],
|
|
499
|
+
alpha: a / 255
|
|
500
|
+
};
|
|
420
501
|
}
|
|
421
502
|
return null;
|
|
422
503
|
}
|
|
@@ -618,7 +699,7 @@ function coarseScan(h, s, lo, hi, yBase, target, epsilon, maxIter) {
|
|
|
618
699
|
function findLightnessForContrast(options) {
|
|
619
700
|
const { hue, saturation, preferredLightness, baseLinearRgb, contrast: contrastInput, lightnessRange = [0, 1], epsilon = 1e-4, maxIterations = 14 } = options;
|
|
620
701
|
const target = resolveMinContrast(contrastInput);
|
|
621
|
-
const searchTarget = target * 1.
|
|
702
|
+
const searchTarget = target * 1.01;
|
|
622
703
|
const yBase = gamutClampedLuminance(baseLinearRgb);
|
|
623
704
|
const crPref = contrastRatioFromLuminance(cachedLuminance(hue, saturation, preferredLightness), yBase);
|
|
624
705
|
if (crPref >= searchTarget) return {
|
|
@@ -808,6 +889,45 @@ function findValueForMixContrast(options) {
|
|
|
808
889
|
* Generates robust light, dark, and high-contrast colors from a hue/saturation
|
|
809
890
|
* seed, preserving contrast for UI pairs via explicit dependencies.
|
|
810
891
|
*/
|
|
892
|
+
/** Internal name of the user-facing standalone color in the synthesized def map. */
|
|
893
|
+
const STANDALONE_VALUE = "value";
|
|
894
|
+
/** Internal name of the hidden static-anchor seed used for relative lightness / contrast. */
|
|
895
|
+
const STANDALONE_SEED = "seed";
|
|
896
|
+
/** Internal name of an externally-resolved `GlazeColorToken` injected as a base reference. */
|
|
897
|
+
const STANDALONE_BASE = "externalBase";
|
|
898
|
+
/**
|
|
899
|
+
* Build the create-time scaling snapshot used when the caller did not
|
|
900
|
+
* pass an explicit `scaling`. All windows are snapshotted from the
|
|
901
|
+
* current `globalConfig` so later `glaze.configure()` calls don't
|
|
902
|
+
* retroactively change the resolved variants of an already-created
|
|
903
|
+
* token (matches the documented "frozen at create time" semantics).
|
|
904
|
+
*
|
|
905
|
+
* String value-shorthand inputs use an extended dark window
|
|
906
|
+
* `[globalConfig.darkLightness[0], 100]` so a totally-black input can
|
|
907
|
+
* Möbius-invert to totally-white in dark mode; object / tuple /
|
|
908
|
+
* structured inputs use `globalConfig.darkLightness` verbatim.
|
|
909
|
+
*/
|
|
910
|
+
function defaultStandaloneScaling(extendDark) {
|
|
911
|
+
const [lo, hi] = globalConfig.darkLightness;
|
|
912
|
+
return {
|
|
913
|
+
lightLightness: false,
|
|
914
|
+
darkLightness: extendDark ? [lo, 100] : [lo, hi]
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
/** Reserved internal names that user-supplied `name` must not collide with. */
|
|
918
|
+
const RESERVED_STANDALONE_NAMES = new Set([
|
|
919
|
+
STANDALONE_VALUE,
|
|
920
|
+
STANDALONE_SEED,
|
|
921
|
+
STANDALONE_BASE
|
|
922
|
+
]);
|
|
923
|
+
/**
|
|
924
|
+
* Discriminate a `GlazeColorToken` from a raw `GlazeColorValue`.
|
|
925
|
+
* Used to widen `base?` so it accepts either a token reference or a
|
|
926
|
+
* raw value (auto-wrapped into `glaze.color(value)`).
|
|
927
|
+
*/
|
|
928
|
+
function isGlazeColorToken(candidate) {
|
|
929
|
+
return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate) && "resolve" in candidate && typeof candidate.resolve === "function";
|
|
930
|
+
}
|
|
811
931
|
let globalConfig = {
|
|
812
932
|
lightLightness: [10, 100],
|
|
813
933
|
darkLightness: [15, 95],
|
|
@@ -828,6 +948,41 @@ function pairNormal(p) {
|
|
|
828
948
|
function pairHC(p) {
|
|
829
949
|
return Array.isArray(p) ? p[1] : p;
|
|
830
950
|
}
|
|
951
|
+
/**
|
|
952
|
+
* Dedupe contrast warnings within a single process. The cache survives
|
|
953
|
+
* the lifetime of a token because tokens memoize their resolution; the
|
|
954
|
+
* limit is a soft cap to keep noise bounded across long-lived sessions
|
|
955
|
+
* (e.g. dev servers with HMR re-resolving themes repeatedly).
|
|
956
|
+
*/
|
|
957
|
+
const CONTRAST_WARN_CACHE_LIMIT = 256;
|
|
958
|
+
const contrastWarnCache = /* @__PURE__ */ new Set();
|
|
959
|
+
function schemeLabel(isDark, isHighContrast) {
|
|
960
|
+
if (isDark && isHighContrast) return "darkContrast";
|
|
961
|
+
if (isDark) return "dark";
|
|
962
|
+
if (isHighContrast) return "lightContrast";
|
|
963
|
+
return "light";
|
|
964
|
+
}
|
|
965
|
+
function formatContrastTarget(input, ratio) {
|
|
966
|
+
return typeof input === "string" ? `"${input}" (${ratio.toFixed(2)})` : ratio.toFixed(2);
|
|
967
|
+
}
|
|
968
|
+
/**
|
|
969
|
+
* Slack factor below the requested target before we emit a warning.
|
|
970
|
+
* The contrast solver already overshoots by `OVERSHOOT` (currently 1%)
|
|
971
|
+
* to absorb rounding noise (`see findLightnessForContrast` in
|
|
972
|
+
* `contrast-solver.ts`), so an `actual` ratio within ~2x that overshoot
|
|
973
|
+
* is effectively a pass and not worth nagging the user about.
|
|
974
|
+
*/
|
|
975
|
+
const CONTRAST_WARN_SLACK = .98;
|
|
976
|
+
function warnContrastUnmet(name, isDark, isHighContrast, target, actual) {
|
|
977
|
+
const targetRatio = resolveMinContrast(target);
|
|
978
|
+
if (actual >= targetRatio * CONTRAST_WARN_SLACK) return;
|
|
979
|
+
const scheme = schemeLabel(isDark, isHighContrast);
|
|
980
|
+
const key = `${name}|${scheme}|${targetRatio.toFixed(3)}|${actual.toFixed(2)}`;
|
|
981
|
+
if (contrastWarnCache.has(key)) return;
|
|
982
|
+
if (contrastWarnCache.size >= CONTRAST_WARN_CACHE_LIMIT) contrastWarnCache.clear();
|
|
983
|
+
contrastWarnCache.add(key);
|
|
984
|
+
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.`);
|
|
985
|
+
}
|
|
831
986
|
function isShadowDef(def) {
|
|
832
987
|
return def.type === "shadow";
|
|
833
988
|
}
|
|
@@ -889,36 +1044,38 @@ function computeShadow(bg, fg, intensity, tuning) {
|
|
|
889
1044
|
alpha
|
|
890
1045
|
};
|
|
891
1046
|
}
|
|
892
|
-
function validateColorDefs(defs) {
|
|
893
|
-
const
|
|
1047
|
+
function validateColorDefs(defs, externalBases) {
|
|
1048
|
+
const localNames = new Set(Object.keys(defs));
|
|
1049
|
+
const allNames = new Set([...localNames, ...externalBases ? externalBases.keys() : []]);
|
|
894
1050
|
for (const [name, def] of Object.entries(defs)) {
|
|
895
1051
|
if (isShadowDef(def)) {
|
|
896
|
-
if (!
|
|
897
|
-
if (isShadowDef(defs[def.bg])) throw new Error(`glaze: shadow "${name}" bg "${def.bg}" references another shadow color.`);
|
|
1052
|
+
if (!allNames.has(def.bg)) throw new Error(`glaze: shadow "${name}" references non-existent bg "${def.bg}".`);
|
|
1053
|
+
if (localNames.has(def.bg) && isShadowDef(defs[def.bg])) throw new Error(`glaze: shadow "${name}" bg "${def.bg}" references another shadow color.`);
|
|
898
1054
|
if (def.fg !== void 0) {
|
|
899
|
-
if (!
|
|
900
|
-
if (isShadowDef(defs[def.fg])) throw new Error(`glaze: shadow "${name}" fg "${def.fg}" references another shadow color.`);
|
|
1055
|
+
if (!allNames.has(def.fg)) throw new Error(`glaze: shadow "${name}" references non-existent fg "${def.fg}".`);
|
|
1056
|
+
if (localNames.has(def.fg) && isShadowDef(defs[def.fg])) throw new Error(`glaze: shadow "${name}" fg "${def.fg}" references another shadow color.`);
|
|
901
1057
|
}
|
|
902
1058
|
continue;
|
|
903
1059
|
}
|
|
904
1060
|
if (isMixDef(def)) {
|
|
905
|
-
if (!
|
|
906
|
-
if (!
|
|
907
|
-
if (isShadowDef(defs[def.base])) throw new Error(`glaze: mix "${name}" base "${def.base}" references a shadow color.`);
|
|
908
|
-
if (isShadowDef(defs[def.target])) throw new Error(`glaze: mix "${name}" target "${def.target}" references a shadow color.`);
|
|
1061
|
+
if (!allNames.has(def.base)) throw new Error(`glaze: mix "${name}" references non-existent base "${def.base}".`);
|
|
1062
|
+
if (!allNames.has(def.target)) throw new Error(`glaze: mix "${name}" references non-existent target "${def.target}".`);
|
|
1063
|
+
if (localNames.has(def.base) && isShadowDef(defs[def.base])) throw new Error(`glaze: mix "${name}" base "${def.base}" references a shadow color.`);
|
|
1064
|
+
if (localNames.has(def.target) && isShadowDef(defs[def.target])) throw new Error(`glaze: mix "${name}" target "${def.target}" references a shadow color.`);
|
|
909
1065
|
continue;
|
|
910
1066
|
}
|
|
911
1067
|
const regDef = def;
|
|
912
1068
|
if (regDef.contrast !== void 0 && !regDef.base) throw new Error(`glaze: color "${name}" has "contrast" without "base".`);
|
|
913
1069
|
if (regDef.lightness !== void 0 && !isAbsoluteLightness(regDef.lightness) && !regDef.base) throw new Error(`glaze: color "${name}" has relative "lightness" without "base".`);
|
|
914
|
-
if (regDef.base && !
|
|
915
|
-
if (regDef.base && isShadowDef(defs[regDef.base])) throw new Error(`glaze: color "${name}" base "${regDef.base}" references a shadow color.`);
|
|
1070
|
+
if (regDef.base && !allNames.has(regDef.base)) throw new Error(`glaze: color "${name}" references non-existent base "${regDef.base}".`);
|
|
1071
|
+
if (regDef.base && localNames.has(regDef.base) && isShadowDef(defs[regDef.base])) throw new Error(`glaze: color "${name}" base "${regDef.base}" references a shadow color.`);
|
|
916
1072
|
if (!isAbsoluteLightness(regDef.lightness) && regDef.base === void 0) throw new Error(`glaze: color "${name}" must have either absolute "lightness" (root) or "base" (dependent).`);
|
|
917
1073
|
if (regDef.contrast !== void 0 && regDef.opacity !== void 0) console.warn(`glaze: color "${name}" has both "contrast" and "opacity". Opacity makes perceived lightness unpredictable.`);
|
|
918
1074
|
}
|
|
919
1075
|
const visited = /* @__PURE__ */ new Set();
|
|
920
1076
|
const inStack = /* @__PURE__ */ new Set();
|
|
921
1077
|
function dfs(name) {
|
|
1078
|
+
if (!localNames.has(name)) return;
|
|
922
1079
|
if (inStack.has(name)) throw new Error(`glaze: circular base reference detected involving "${name}".`);
|
|
923
1080
|
if (visited.has(name)) return;
|
|
924
1081
|
inStack.add(name);
|
|
@@ -936,7 +1093,7 @@ function validateColorDefs(defs) {
|
|
|
936
1093
|
inStack.delete(name);
|
|
937
1094
|
visited.add(name);
|
|
938
1095
|
}
|
|
939
|
-
for (const name of
|
|
1096
|
+
for (const name of localNames) dfs(name);
|
|
940
1097
|
}
|
|
941
1098
|
function topoSort(defs) {
|
|
942
1099
|
const result = [];
|
|
@@ -945,6 +1102,7 @@ function topoSort(defs) {
|
|
|
945
1102
|
if (visited.has(name)) return;
|
|
946
1103
|
visited.add(name);
|
|
947
1104
|
const def = defs[name];
|
|
1105
|
+
if (def === void 0) return;
|
|
948
1106
|
if (isShadowDef(def)) {
|
|
949
1107
|
visit(def.bg);
|
|
950
1108
|
if (def.fg) visit(def.fg);
|
|
@@ -960,32 +1118,43 @@ function topoSort(defs) {
|
|
|
960
1118
|
for (const name of Object.keys(defs)) visit(name);
|
|
961
1119
|
return result;
|
|
962
1120
|
}
|
|
963
|
-
|
|
1121
|
+
/**
|
|
1122
|
+
* Resolve the active lightness window for a scheme.
|
|
1123
|
+
* - HC variants always return `[0, 100]` (existing behavior, predates per-call overrides).
|
|
1124
|
+
* - Otherwise, per-call `scaling` (e.g. from `glaze.color()`'s third arg) wins;
|
|
1125
|
+
* `false` is interpreted as `[0, 100]` (no remap). Falls back to `globalConfig.*Lightness`.
|
|
1126
|
+
*/
|
|
1127
|
+
function lightnessWindow(isHighContrast, kind, scaling) {
|
|
964
1128
|
if (isHighContrast) return [0, 100];
|
|
1129
|
+
if (scaling) {
|
|
1130
|
+
const override = kind === "dark" ? scaling.darkLightness : scaling.lightLightness;
|
|
1131
|
+
if (override === false) return [0, 100];
|
|
1132
|
+
if (override !== void 0) return override;
|
|
1133
|
+
}
|
|
965
1134
|
return kind === "dark" ? globalConfig.darkLightness : globalConfig.lightLightness;
|
|
966
1135
|
}
|
|
967
|
-
function mapLightnessLight(l, mode, isHighContrast) {
|
|
1136
|
+
function mapLightnessLight(l, mode, isHighContrast, scaling) {
|
|
968
1137
|
if (mode === "static") return l;
|
|
969
|
-
const [lo, hi] = lightnessWindow(isHighContrast, "light");
|
|
1138
|
+
const [lo, hi] = lightnessWindow(isHighContrast, "light", scaling);
|
|
970
1139
|
return l * (hi - lo) / 100 + lo;
|
|
971
1140
|
}
|
|
972
1141
|
function mobiusCurve(t, beta) {
|
|
973
1142
|
if (beta >= 1) return t;
|
|
974
1143
|
return t / (t + beta * (1 - t));
|
|
975
1144
|
}
|
|
976
|
-
function mapLightnessDark(l, mode, isHighContrast) {
|
|
1145
|
+
function mapLightnessDark(l, mode, isHighContrast, scaling) {
|
|
977
1146
|
if (mode === "static") return l;
|
|
978
1147
|
const beta = isHighContrast ? pairHC(globalConfig.darkCurve) : pairNormal(globalConfig.darkCurve);
|
|
979
|
-
const [darkLo, darkHi] = lightnessWindow(isHighContrast, "dark");
|
|
1148
|
+
const [darkLo, darkHi] = lightnessWindow(isHighContrast, "dark", scaling);
|
|
980
1149
|
if (mode === "fixed") return l * (darkHi - darkLo) / 100 + darkLo;
|
|
981
|
-
const [lightLo, lightHi] = lightnessWindow(isHighContrast, "light");
|
|
1150
|
+
const [lightLo, lightHi] = lightnessWindow(isHighContrast, "light", scaling);
|
|
982
1151
|
const t = (lightHi - (l * (lightHi - lightLo) / 100 + lightLo)) / (lightHi - lightLo);
|
|
983
1152
|
return darkLo + (darkHi - darkLo) * mobiusCurve(t, beta);
|
|
984
1153
|
}
|
|
985
|
-
function lightMappedToDark(lightL, isHighContrast) {
|
|
1154
|
+
function lightMappedToDark(lightL, isHighContrast, scaling) {
|
|
986
1155
|
const beta = isHighContrast ? pairHC(globalConfig.darkCurve) : pairNormal(globalConfig.darkCurve);
|
|
987
|
-
const [lightLo, lightHi] = lightnessWindow(isHighContrast, "light");
|
|
988
|
-
const [darkLo, darkHi] = lightnessWindow(isHighContrast, "dark");
|
|
1156
|
+
const [lightLo, lightHi] = lightnessWindow(isHighContrast, "light", scaling);
|
|
1157
|
+
const [darkLo, darkHi] = lightnessWindow(isHighContrast, "dark", scaling);
|
|
989
1158
|
const t = (lightHi - clamp(lightL, lightLo, lightHi)) / (lightHi - lightLo);
|
|
990
1159
|
return darkLo + (darkHi - darkLo) * mobiusCurve(t, beta);
|
|
991
1160
|
}
|
|
@@ -993,9 +1162,9 @@ function mapSaturationDark(s, mode) {
|
|
|
993
1162
|
if (mode === "static") return s;
|
|
994
1163
|
return s * (1 - globalConfig.darkDesaturation);
|
|
995
1164
|
}
|
|
996
|
-
function schemeLightnessRange(isDark, mode, isHighContrast) {
|
|
1165
|
+
function schemeLightnessRange(isDark, mode, isHighContrast, scaling) {
|
|
997
1166
|
if (mode === "static") return [0, 1];
|
|
998
|
-
const [lo, hi] = lightnessWindow(isHighContrast, isDark ? "dark" : "light");
|
|
1167
|
+
const [lo, hi] = lightnessWindow(isHighContrast, isDark ? "dark" : "light", scaling);
|
|
999
1168
|
return [lo / 100, hi / 100];
|
|
1000
1169
|
}
|
|
1001
1170
|
function clamp(v, min, max) {
|
|
@@ -1055,26 +1224,28 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
|
|
|
1055
1224
|
const parsed = parseRelativeOrAbsolute(isHighContrast ? pairHC(rawLightness) : pairNormal(rawLightness));
|
|
1056
1225
|
if (parsed.relative) {
|
|
1057
1226
|
const delta = parsed.value;
|
|
1058
|
-
if (isDark && mode === "auto") preferredL = lightMappedToDark(clamp(getSchemeVariant(baseResolved, false, isHighContrast).l * 100 + delta, 0, 100), isHighContrast);
|
|
1227
|
+
if (isDark && mode === "auto") preferredL = lightMappedToDark(clamp(getSchemeVariant(baseResolved, false, isHighContrast).l * 100 + delta, 0, 100), isHighContrast, ctx.scaling);
|
|
1059
1228
|
else preferredL = clamp(baseL + delta, 0, 100);
|
|
1060
|
-
} else if (isDark) preferredL = mapLightnessDark(parsed.value, mode, isHighContrast);
|
|
1061
|
-
else preferredL = mapLightnessLight(parsed.value, mode, isHighContrast);
|
|
1229
|
+
} else if (isDark) preferredL = mapLightnessDark(parsed.value, mode, isHighContrast, ctx.scaling);
|
|
1230
|
+
else preferredL = mapLightnessLight(parsed.value, mode, isHighContrast, ctx.scaling);
|
|
1062
1231
|
}
|
|
1063
1232
|
const rawContrast = def.contrast;
|
|
1064
1233
|
if (rawContrast !== void 0) {
|
|
1065
1234
|
const minCr = isHighContrast ? pairHC(rawContrast) : pairNormal(rawContrast);
|
|
1066
1235
|
const effectiveSat = isDark ? mapSaturationDark(satFactor * ctx.saturation / 100, mode) : satFactor * ctx.saturation / 100;
|
|
1067
1236
|
const baseLinearRgb = okhslToLinearSrgb(baseVariant.h, baseVariant.s, baseVariant.l);
|
|
1068
|
-
const windowRange = schemeLightnessRange(isDark, mode, isHighContrast);
|
|
1237
|
+
const windowRange = schemeLightnessRange(isDark, mode, isHighContrast, ctx.scaling);
|
|
1238
|
+
const result = findLightnessForContrast({
|
|
1239
|
+
hue: effectiveHue,
|
|
1240
|
+
saturation: effectiveSat,
|
|
1241
|
+
preferredLightness: clamp(preferredL / 100, windowRange[0], windowRange[1]),
|
|
1242
|
+
baseLinearRgb,
|
|
1243
|
+
contrast: minCr,
|
|
1244
|
+
lightnessRange: [0, 1]
|
|
1245
|
+
});
|
|
1246
|
+
if (!result.met) warnContrastUnmet(name, isDark, isHighContrast, minCr, result.contrast);
|
|
1069
1247
|
return {
|
|
1070
|
-
l:
|
|
1071
|
-
hue: effectiveHue,
|
|
1072
|
-
saturation: effectiveSat,
|
|
1073
|
-
preferredLightness: clamp(preferredL / 100, windowRange[0], windowRange[1]),
|
|
1074
|
-
baseLinearRgb,
|
|
1075
|
-
contrast: minCr,
|
|
1076
|
-
lightnessRange: [0, 1]
|
|
1077
|
-
}).lightness * 100,
|
|
1248
|
+
l: result.lightness * 100,
|
|
1078
1249
|
satFactor
|
|
1079
1250
|
};
|
|
1080
1251
|
}
|
|
@@ -1110,13 +1281,13 @@ function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
|
1110
1281
|
let finalL;
|
|
1111
1282
|
let finalSat;
|
|
1112
1283
|
if (isDark && isRoot) {
|
|
1113
|
-
finalL = mapLightnessDark(lightL, mode, isHighContrast);
|
|
1284
|
+
finalL = mapLightnessDark(lightL, mode, isHighContrast, ctx.scaling);
|
|
1114
1285
|
finalSat = mapSaturationDark(satFactor * ctx.saturation / 100, mode);
|
|
1115
1286
|
} else if (isDark && !isRoot) {
|
|
1116
1287
|
finalL = lightL;
|
|
1117
1288
|
finalSat = mapSaturationDark(satFactor * ctx.saturation / 100, mode);
|
|
1118
1289
|
} else if (isRoot) {
|
|
1119
|
-
finalL = mapLightnessLight(lightL, mode, isHighContrast);
|
|
1290
|
+
finalL = mapLightnessLight(lightL, mode, isHighContrast, ctx.scaling);
|
|
1120
1291
|
finalSat = satFactor * ctx.saturation / 100;
|
|
1121
1292
|
} else {
|
|
1122
1293
|
finalL = lightL;
|
|
@@ -1214,15 +1385,17 @@ function resolveMixForScheme(def, ctx, isDark, isHighContrast) {
|
|
|
1214
1385
|
alpha: 1
|
|
1215
1386
|
};
|
|
1216
1387
|
}
|
|
1217
|
-
function resolveAllColors(hue, saturation, defs) {
|
|
1218
|
-
validateColorDefs(defs);
|
|
1388
|
+
function resolveAllColors(hue, saturation, defs, scaling, externalBases) {
|
|
1389
|
+
validateColorDefs(defs, externalBases);
|
|
1219
1390
|
const order = topoSort(defs);
|
|
1220
1391
|
const ctx = {
|
|
1221
1392
|
hue,
|
|
1222
1393
|
saturation,
|
|
1223
1394
|
defs,
|
|
1224
|
-
resolved: /* @__PURE__ */ new Map()
|
|
1395
|
+
resolved: /* @__PURE__ */ new Map(),
|
|
1396
|
+
scaling
|
|
1225
1397
|
};
|
|
1398
|
+
if (externalBases) for (const [name, color] of externalBases) ctx.resolved.set(name, color);
|
|
1226
1399
|
function defMode(def) {
|
|
1227
1400
|
if (isShadowDef(def) || isMixDef(def)) return void 0;
|
|
1228
1401
|
return def.mode ?? "auto";
|
|
@@ -1574,34 +1747,408 @@ function createPalette(themes, paletteOptions) {
|
|
|
1574
1747
|
}
|
|
1575
1748
|
};
|
|
1576
1749
|
}
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1750
|
+
/**
|
|
1751
|
+
* Matches the CSS color functions Glaze itself emits (`rgb()`, `hsl()`,
|
|
1752
|
+
* `okhsl()`, `oklch()`) plus their legacy alpha aliases (`rgba()`, `hsla()`).
|
|
1753
|
+
*
|
|
1754
|
+
* Only bare numeric components are supported. Named colors (`red`),
|
|
1755
|
+
* relative-color syntax (`from <color> ...`), and angle units other
|
|
1756
|
+
* than bare degrees (`deg` is the only suffix tolerated by `parseFloat`)
|
|
1757
|
+
* are out of scope.
|
|
1758
|
+
*/
|
|
1759
|
+
const COLOR_FN_RE = /^(rgba?|hsla?|okhsl|oklch)\(\s*([^)]*)\s*\)$/i;
|
|
1760
|
+
function parseNumberOrPercent(raw, percentScale) {
|
|
1761
|
+
if (raw.endsWith("%")) return parseFloat(raw) / 100 * percentScale;
|
|
1762
|
+
return parseFloat(raw);
|
|
1763
|
+
}
|
|
1764
|
+
/**
|
|
1765
|
+
* Split the body of a CSS color function into its components and detect
|
|
1766
|
+
* whether an alpha channel was present.
|
|
1767
|
+
*
|
|
1768
|
+
* Handles both modern slash syntax (`R G B / A` or `R, G, B / A`) and
|
|
1769
|
+
* legacy comma syntax (`R, G, B, A`). The alpha value itself is discarded
|
|
1770
|
+
* by the caller — standalone Glaze colors have no opacity field.
|
|
1771
|
+
*/
|
|
1772
|
+
function splitColorBody(body) {
|
|
1773
|
+
const slashIdx = body.indexOf("/");
|
|
1774
|
+
if (slashIdx !== -1) return {
|
|
1775
|
+
components: body.slice(0, slashIdx).trim().split(/[\s,]+/).filter(Boolean),
|
|
1776
|
+
hadAlpha: body.slice(slashIdx + 1).trim().length > 0
|
|
1777
|
+
};
|
|
1778
|
+
const components = body.split(/[\s,]+/).filter(Boolean);
|
|
1779
|
+
if (components.length === 4) {
|
|
1780
|
+
components.pop();
|
|
1781
|
+
return {
|
|
1782
|
+
components,
|
|
1783
|
+
hadAlpha: true
|
|
1784
|
+
};
|
|
1785
|
+
}
|
|
1786
|
+
return {
|
|
1787
|
+
components,
|
|
1788
|
+
hadAlpha: false
|
|
1789
|
+
};
|
|
1790
|
+
}
|
|
1791
|
+
function warnDroppedAlpha(input) {
|
|
1792
|
+
console.warn(`glaze: alpha component dropped from "${input}" (standalone color has no opacity field).`);
|
|
1793
|
+
}
|
|
1794
|
+
function parseColorString(input) {
|
|
1795
|
+
if (input.startsWith("#")) {
|
|
1796
|
+
const parsed = parseHexAlpha(input);
|
|
1797
|
+
if (!parsed) throw new Error(`glaze: invalid hex color "${input}".`);
|
|
1798
|
+
if (parsed.alpha !== void 0) warnDroppedAlpha(input);
|
|
1799
|
+
const [h, s, l] = srgbToOkhsl(parsed.rgb);
|
|
1800
|
+
return {
|
|
1801
|
+
h,
|
|
1802
|
+
s,
|
|
1803
|
+
l
|
|
1804
|
+
};
|
|
1805
|
+
}
|
|
1806
|
+
const m = input.match(COLOR_FN_RE);
|
|
1807
|
+
if (!m) throw new Error(`glaze: unsupported color string "${input}".`);
|
|
1808
|
+
const fn = m[1].toLowerCase();
|
|
1809
|
+
const { components, hadAlpha } = splitColorBody(m[2].trim());
|
|
1810
|
+
if (hadAlpha) warnDroppedAlpha(input);
|
|
1811
|
+
if (components.length !== 3) throw new Error(`glaze: expected 3 components in "${input}".`);
|
|
1812
|
+
switch (fn) {
|
|
1813
|
+
case "rgb":
|
|
1814
|
+
case "rgba": {
|
|
1815
|
+
const [h, s, l] = srgbToOkhsl([
|
|
1816
|
+
parseNumberOrPercent(components[0], 255) / 255,
|
|
1817
|
+
parseNumberOrPercent(components[1], 255) / 255,
|
|
1818
|
+
parseNumberOrPercent(components[2], 255) / 255
|
|
1819
|
+
]);
|
|
1820
|
+
return {
|
|
1821
|
+
h,
|
|
1822
|
+
s,
|
|
1823
|
+
l
|
|
1824
|
+
};
|
|
1825
|
+
}
|
|
1826
|
+
case "hsl":
|
|
1827
|
+
case "hsla": {
|
|
1828
|
+
const [oh, os, ol] = srgbToOkhsl(hslToSrgb(parseFloat(components[0]), parseNumberOrPercent(components[1], 1), parseNumberOrPercent(components[2], 1)));
|
|
1829
|
+
return {
|
|
1830
|
+
h: oh,
|
|
1831
|
+
s: os,
|
|
1832
|
+
l: ol
|
|
1833
|
+
};
|
|
1834
|
+
}
|
|
1835
|
+
case "okhsl": return {
|
|
1836
|
+
h: parseFloat(components[0]),
|
|
1837
|
+
s: parseNumberOrPercent(components[1], 1),
|
|
1838
|
+
l: parseNumberOrPercent(components[2], 1)
|
|
1839
|
+
};
|
|
1840
|
+
case "oklch": {
|
|
1841
|
+
const L = parseNumberOrPercent(components[0], 1);
|
|
1842
|
+
const C = parseNumberOrPercent(components[1], .4);
|
|
1843
|
+
const hRad = parseFloat(components[2]) * Math.PI / 180;
|
|
1844
|
+
const [h, s, l] = oklabToOkhsl([
|
|
1845
|
+
L,
|
|
1846
|
+
C * Math.cos(hRad),
|
|
1847
|
+
C * Math.sin(hRad)
|
|
1848
|
+
]);
|
|
1849
|
+
return {
|
|
1850
|
+
h,
|
|
1851
|
+
s,
|
|
1852
|
+
l
|
|
1853
|
+
};
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
throw new Error(`glaze: unsupported color function "${fn}".`);
|
|
1857
|
+
}
|
|
1858
|
+
/**
|
|
1859
|
+
* Validate a user-supplied `OkhslColor`. Catches the common 0-100 vs 0-1
|
|
1860
|
+
* confusion (the structured form uses 0-100, OKHSL objects use 0-1).
|
|
1861
|
+
*/
|
|
1862
|
+
function validateOkhslColor(value) {
|
|
1863
|
+
const { h, s, l } = value;
|
|
1864
|
+
if (!Number.isFinite(h) || !Number.isFinite(s) || !Number.isFinite(l)) throw new Error("glaze.color: OkhslColor h/s/l must be finite numbers.");
|
|
1865
|
+
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)?");
|
|
1866
|
+
}
|
|
1867
|
+
/**
|
|
1868
|
+
* Validate a user-supplied `[r, g, b]` tuple in 0-255.
|
|
1869
|
+
*/
|
|
1870
|
+
function validateRgbTuple(value) {
|
|
1871
|
+
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(", ")}]).`);
|
|
1872
|
+
}
|
|
1873
|
+
/**
|
|
1874
|
+
* Validate a user-supplied `opacity` override on `glaze.color()`.
|
|
1875
|
+
* Must be a finite number in `0..=1`.
|
|
1876
|
+
*/
|
|
1877
|
+
function validateStandaloneOpacity(value) {
|
|
1878
|
+
if (!Number.isFinite(value) || value < 0 || value > 1) throw new Error(`glaze.color: opacity must be a finite number in 0–1 (got ${value}).`);
|
|
1879
|
+
}
|
|
1880
|
+
/**
|
|
1881
|
+
* Validate a structured `GlazeColorInput`. Range-checks the `hue` /
|
|
1882
|
+
* `saturation` / `lightness` numerics (and any HC-pair second value)
|
|
1883
|
+
* before the resolver sees them so out-of-range or non-finite inputs
|
|
1884
|
+
* fail with a helpful, top-level error rather than producing a
|
|
1885
|
+
* NaN-laden token. `opacity` is checked here too so all input
|
|
1886
|
+
* validation lives in one place.
|
|
1887
|
+
*/
|
|
1888
|
+
function validateStructuredInput(input) {
|
|
1889
|
+
if (!Number.isFinite(input.hue)) throw new Error(`glaze.color: structured hue must be a finite number (got ${input.hue}).`);
|
|
1890
|
+
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}).`);
|
|
1891
|
+
const checkLightness = (value, label) => {
|
|
1892
|
+
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}).`);
|
|
1893
|
+
};
|
|
1894
|
+
if (Array.isArray(input.lightness)) {
|
|
1895
|
+
checkLightness(input.lightness[0], "lightness[normal]");
|
|
1896
|
+
checkLightness(input.lightness[1], "lightness[hc]");
|
|
1897
|
+
} else checkLightness(input.lightness, "lightness");
|
|
1898
|
+
if (input.saturationFactor !== void 0) {
|
|
1899
|
+
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}).`);
|
|
1900
|
+
}
|
|
1901
|
+
if (input.opacity !== void 0) validateStandaloneOpacity(input.opacity);
|
|
1902
|
+
}
|
|
1903
|
+
/**
|
|
1904
|
+
* Validate a user-supplied `name` override. Rejects empty / whitespace-only
|
|
1905
|
+
* strings and names colliding with `glaze`'s reserved internal sentinels.
|
|
1906
|
+
*/
|
|
1907
|
+
function validateStandaloneName(name) {
|
|
1908
|
+
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.");
|
|
1909
|
+
if (RESERVED_STANDALONE_NAMES.has(name)) {
|
|
1910
|
+
const reserved = [...RESERVED_STANDALONE_NAMES].map((n) => `"${n}"`).join(", ");
|
|
1911
|
+
throw new Error(`glaze.color: name "${name}" is reserved (used internally). Reserved names are: ${reserved}. Pick a different name.`);
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
/**
|
|
1915
|
+
* Extract an OKHSL color from any `GlazeColorValue` form. Also used by
|
|
1916
|
+
* `glaze.shadow()` so all shadow inputs (hex, color functions, OKHSL,
|
|
1917
|
+
* RGB tuple) go through one parser.
|
|
1918
|
+
*/
|
|
1919
|
+
function extractOkhslFromValue(value) {
|
|
1920
|
+
if (typeof value === "string") return parseColorString(value);
|
|
1921
|
+
if (Array.isArray(value)) {
|
|
1922
|
+
const tuple = value;
|
|
1923
|
+
validateRgbTuple(tuple);
|
|
1924
|
+
const [r, g, b] = tuple;
|
|
1925
|
+
const [h, s, l] = srgbToOkhsl([
|
|
1926
|
+
r / 255,
|
|
1927
|
+
g / 255,
|
|
1928
|
+
b / 255
|
|
1929
|
+
]);
|
|
1930
|
+
return {
|
|
1931
|
+
h,
|
|
1932
|
+
s,
|
|
1933
|
+
l
|
|
1934
|
+
};
|
|
1935
|
+
}
|
|
1936
|
+
validateOkhslColor(value);
|
|
1937
|
+
return value;
|
|
1938
|
+
}
|
|
1939
|
+
/**
|
|
1940
|
+
* Build the `ColorMap` for a value-shorthand `glaze.color()` call.
|
|
1941
|
+
*
|
|
1942
|
+
* The user-facing color (`STANDALONE_VALUE`) defaults to `mode: 'auto'`
|
|
1943
|
+
* for string inputs (Möbius-inverted dark variant — pairs with the
|
|
1944
|
+
* extended dark window so a totally-black input renders as totally-white
|
|
1945
|
+
* in dark mode) and `mode: 'fixed'` for `OkhslColor` / RGB-tuple inputs
|
|
1946
|
+
* (linear, no inversion).
|
|
1947
|
+
*
|
|
1948
|
+
* When the user requests `contrast` or relative `lightness`, a hidden
|
|
1949
|
+
* `STANDALONE_SEED` def is synthesized at `mode: 'static'`. That keeps
|
|
1950
|
+
* the seed pinned to the literal user-provided color across all four
|
|
1951
|
+
* variants, so the contrast solver always anchors against it.
|
|
1952
|
+
*/
|
|
1953
|
+
function buildStandaloneValueDefs(main, options, inputIsString) {
|
|
1954
|
+
const seedHue = typeof options?.hue === "number" ? options.hue : main.h;
|
|
1955
|
+
const seedSaturation = options?.saturation ?? main.s * 100;
|
|
1956
|
+
const relativeHue = typeof options?.hue === "string" ? options.hue : void 0;
|
|
1957
|
+
const lightnessOption = options?.lightness;
|
|
1958
|
+
const hasExternalBase = options?.base !== void 0;
|
|
1959
|
+
const needsSeedAnchor = !hasExternalBase && (options?.contrast !== void 0 || lightnessOption !== void 0 && !isAbsoluteLightness(lightnessOption));
|
|
1960
|
+
if (options?.opacity !== void 0) validateStandaloneOpacity(options.opacity);
|
|
1961
|
+
const userName = options?.name;
|
|
1962
|
+
if (userName !== void 0) validateStandaloneName(userName);
|
|
1963
|
+
const primary = userName ?? STANDALONE_VALUE;
|
|
1964
|
+
const valueDef = {
|
|
1965
|
+
hue: relativeHue,
|
|
1966
|
+
saturation: options?.saturationFactor,
|
|
1967
|
+
lightness: lightnessOption ?? main.l * 100,
|
|
1968
|
+
contrast: options?.contrast,
|
|
1969
|
+
mode: options?.mode ?? (inputIsString ? "auto" : "fixed"),
|
|
1970
|
+
opacity: options?.opacity,
|
|
1971
|
+
base: hasExternalBase ? STANDALONE_BASE : needsSeedAnchor ? STANDALONE_SEED : void 0
|
|
1972
|
+
};
|
|
1973
|
+
const defs = { [primary]: valueDef };
|
|
1974
|
+
if (needsSeedAnchor) defs[STANDALONE_SEED] = {
|
|
1975
|
+
hue: main.h,
|
|
1976
|
+
saturation: 1,
|
|
1977
|
+
lightness: main.l * 100,
|
|
1978
|
+
mode: "static"
|
|
1979
|
+
};
|
|
1980
|
+
return {
|
|
1981
|
+
seedHue,
|
|
1982
|
+
seedSaturation,
|
|
1983
|
+
defs,
|
|
1984
|
+
primary
|
|
1985
|
+
};
|
|
1986
|
+
}
|
|
1987
|
+
function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effectiveScaling, baseToken, exportData) {
|
|
1988
|
+
let cached;
|
|
1989
|
+
const resolveOnce = () => {
|
|
1990
|
+
if (cached) return cached;
|
|
1991
|
+
cached = resolveAllColors(seedHue, seedSaturation, defs, effectiveScaling, baseToken ? new Map([[STANDALONE_BASE, baseToken.resolve()]]) : void 0);
|
|
1992
|
+
return cached;
|
|
1993
|
+
};
|
|
1994
|
+
const resolveStates = (options) => ({
|
|
1995
|
+
dark: options?.states?.dark ?? globalConfig.states.dark,
|
|
1996
|
+
highContrast: options?.states?.highContrast ?? globalConfig.states.highContrast
|
|
1997
|
+
});
|
|
1998
|
+
const tokenLike = (options) => {
|
|
1999
|
+
return buildTokenMap(resolveOnce(), "", resolveStates(options), resolveModes(options?.modes), options?.format)[`#${primary}`];
|
|
2000
|
+
};
|
|
1583
2001
|
return {
|
|
1584
2002
|
resolve() {
|
|
1585
|
-
return
|
|
2003
|
+
return resolveOnce().get(primary);
|
|
1586
2004
|
},
|
|
1587
|
-
token
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
}, resolveModes(options?.modes), options?.format)["#__color__"];
|
|
2005
|
+
token: tokenLike,
|
|
2006
|
+
tasty: tokenLike,
|
|
2007
|
+
json(options) {
|
|
2008
|
+
return buildJsonMap(resolveOnce(), resolveModes(options?.modes), options?.format)[primary];
|
|
1592
2009
|
},
|
|
1593
|
-
|
|
1594
|
-
return
|
|
1595
|
-
dark: options?.states?.dark ?? globalConfig.states.dark,
|
|
1596
|
-
highContrast: options?.states?.highContrast ?? globalConfig.states.highContrast
|
|
1597
|
-
}, resolveModes(options?.modes), options?.format)["#__color__"];
|
|
2010
|
+
css(options) {
|
|
2011
|
+
return buildCssMap(new Map([[options.name, resolveOnce().get(primary)]]), "", options.suffix ?? "-color", options.format ?? "rgb");
|
|
1598
2012
|
},
|
|
1599
|
-
|
|
1600
|
-
return buildJsonMap(resolveAllColors(input.hue, input.saturation, defs), resolveModes(options?.modes), options?.format)["__color__"];
|
|
1601
|
-
}
|
|
2013
|
+
export: exportData
|
|
1602
2014
|
};
|
|
1603
2015
|
}
|
|
1604
2016
|
/**
|
|
2017
|
+
* Resolve `base` (which may be a token reference or a raw color value)
|
|
2018
|
+
* into a `GlazeColorToken`. Raw values are auto-wrapped via
|
|
2019
|
+
* `glaze.color(value)` so they pick up the same auto-invert defaults as
|
|
2020
|
+
* an explicit wrap. Returns `undefined` when no base is provided.
|
|
2021
|
+
*/
|
|
2022
|
+
function resolveBaseToken(base) {
|
|
2023
|
+
if (base === void 0) return void 0;
|
|
2024
|
+
if (isGlazeColorToken(base)) return base;
|
|
2025
|
+
return createColorTokenFromValue(base, void 0, void 0);
|
|
2026
|
+
}
|
|
2027
|
+
/**
|
|
2028
|
+
* Build a JSON-safe snapshot of `GlazeColorOverrides`. `base` is
|
|
2029
|
+
* recursively serialized when it was originally a token; raw values are
|
|
2030
|
+
* preserved as-is so `glaze.colorFrom(...)` round-trips them.
|
|
2031
|
+
*/
|
|
2032
|
+
function buildOverridesExport(options) {
|
|
2033
|
+
const out = {};
|
|
2034
|
+
if (options.hue !== void 0) out.hue = options.hue;
|
|
2035
|
+
if (options.saturation !== void 0) out.saturation = options.saturation;
|
|
2036
|
+
if (options.lightness !== void 0) out.lightness = options.lightness;
|
|
2037
|
+
if (options.saturationFactor !== void 0) out.saturationFactor = options.saturationFactor;
|
|
2038
|
+
if (options.mode !== void 0) out.mode = options.mode;
|
|
2039
|
+
if (options.contrast !== void 0) out.contrast = options.contrast;
|
|
2040
|
+
if (options.opacity !== void 0) out.opacity = options.opacity;
|
|
2041
|
+
if (options.name !== void 0) out.name = options.name;
|
|
2042
|
+
if (options.base !== void 0) out.base = isGlazeColorToken(options.base) ? options.base.export() : options.base;
|
|
2043
|
+
return out;
|
|
2044
|
+
}
|
|
2045
|
+
function buildStructuredInputExport(input) {
|
|
2046
|
+
const out = {
|
|
2047
|
+
hue: input.hue,
|
|
2048
|
+
saturation: input.saturation,
|
|
2049
|
+
lightness: input.lightness
|
|
2050
|
+
};
|
|
2051
|
+
if (input.saturationFactor !== void 0) out.saturationFactor = input.saturationFactor;
|
|
2052
|
+
if (input.mode !== void 0) out.mode = input.mode;
|
|
2053
|
+
if (input.opacity !== void 0) out.opacity = input.opacity;
|
|
2054
|
+
if (input.contrast !== void 0) out.contrast = input.contrast;
|
|
2055
|
+
if (input.name !== void 0) out.name = input.name;
|
|
2056
|
+
if (input.base !== void 0) out.base = isGlazeColorToken(input.base) ? input.base.export() : input.base;
|
|
2057
|
+
return out;
|
|
2058
|
+
}
|
|
2059
|
+
function createColorToken(input, scaling) {
|
|
2060
|
+
validateStructuredInput(input);
|
|
2061
|
+
const userName = input.name;
|
|
2062
|
+
if (userName !== void 0) validateStandaloneName(userName);
|
|
2063
|
+
const primary = userName ?? STANDALONE_VALUE;
|
|
2064
|
+
const baseToken = resolveBaseToken(input.base);
|
|
2065
|
+
const hasExternalBase = baseToken !== void 0;
|
|
2066
|
+
const needsSeedAnchor = !hasExternalBase && input.contrast !== void 0;
|
|
2067
|
+
const defs = { [primary]: {
|
|
2068
|
+
lightness: input.lightness,
|
|
2069
|
+
saturation: input.saturationFactor,
|
|
2070
|
+
mode: input.mode ?? "fixed",
|
|
2071
|
+
contrast: input.contrast,
|
|
2072
|
+
opacity: input.opacity,
|
|
2073
|
+
base: hasExternalBase ? STANDALONE_BASE : needsSeedAnchor ? STANDALONE_SEED : void 0
|
|
2074
|
+
} };
|
|
2075
|
+
if (needsSeedAnchor) defs[STANDALONE_SEED] = {
|
|
2076
|
+
lightness: pairNormal(input.lightness),
|
|
2077
|
+
saturation: 1,
|
|
2078
|
+
mode: "static"
|
|
2079
|
+
};
|
|
2080
|
+
const effectiveScaling = scaling ?? defaultStandaloneScaling(false);
|
|
2081
|
+
const exportData = () => ({
|
|
2082
|
+
form: "structured",
|
|
2083
|
+
input: buildStructuredInputExport(input),
|
|
2084
|
+
scaling: effectiveScaling
|
|
2085
|
+
});
|
|
2086
|
+
return createColorTokenFromDefs(input.hue, input.saturation, defs, primary, effectiveScaling, baseToken, exportData);
|
|
2087
|
+
}
|
|
2088
|
+
function createColorTokenFromValue(value, options, scaling) {
|
|
2089
|
+
const inputIsString = typeof value === "string";
|
|
2090
|
+
const main = extractOkhslFromValue(value);
|
|
2091
|
+
const baseToken = resolveBaseToken(options?.base);
|
|
2092
|
+
const { seedHue, seedSaturation, defs, primary } = buildStandaloneValueDefs(main, options, inputIsString);
|
|
2093
|
+
const effectiveScaling = scaling ?? defaultStandaloneScaling(inputIsString);
|
|
2094
|
+
const exportData = () => ({
|
|
2095
|
+
form: "value",
|
|
2096
|
+
input: value,
|
|
2097
|
+
...options !== void 0 ? { overrides: buildOverridesExport(options) } : {},
|
|
2098
|
+
scaling: effectiveScaling
|
|
2099
|
+
});
|
|
2100
|
+
return createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effectiveScaling, baseToken, exportData);
|
|
2101
|
+
}
|
|
2102
|
+
/**
|
|
2103
|
+
* Rehydrate a token from its `.export()` snapshot. Recursively rebuilds
|
|
2104
|
+
* any base dependency. Inverse of `GlazeColorToken.export()`.
|
|
2105
|
+
*/
|
|
2106
|
+
function colorFromExport(data) {
|
|
2107
|
+
if (data === null || typeof data !== "object") throw new Error(`glaze.colorFrom: expected an object from token.export(), got ${data === null ? "null" : typeof data}.`);
|
|
2108
|
+
if (data.form !== "value" && data.form !== "structured") throw new Error(`glaze.colorFrom: invalid "form" field — expected "value" or "structured" (got ${JSON.stringify(data.form)}).`);
|
|
2109
|
+
if (data.input === void 0) throw new Error(`glaze.colorFrom: missing "input" field — expected the original ${data.form === "value" ? "GlazeColorValue" : "GlazeColorInput"}.`);
|
|
2110
|
+
if (data.form === "value") {
|
|
2111
|
+
const value = data.input;
|
|
2112
|
+
return createColorTokenFromValue(value, data.overrides ? rehydrateOverrides(data.overrides) : void 0, data.scaling);
|
|
2113
|
+
}
|
|
2114
|
+
return createColorToken(rehydrateStructuredInput(data.input), data.scaling);
|
|
2115
|
+
}
|
|
2116
|
+
function rehydrateOverrides(data) {
|
|
2117
|
+
const out = {};
|
|
2118
|
+
if (data.hue !== void 0) out.hue = data.hue;
|
|
2119
|
+
if (data.saturation !== void 0) out.saturation = data.saturation;
|
|
2120
|
+
if (data.lightness !== void 0) out.lightness = data.lightness;
|
|
2121
|
+
if (data.saturationFactor !== void 0) out.saturationFactor = data.saturationFactor;
|
|
2122
|
+
if (data.mode !== void 0) out.mode = data.mode;
|
|
2123
|
+
if (data.contrast !== void 0) out.contrast = data.contrast;
|
|
2124
|
+
if (data.opacity !== void 0) out.opacity = data.opacity;
|
|
2125
|
+
if (data.name !== void 0) out.name = data.name;
|
|
2126
|
+
if (data.base !== void 0) out.base = isExportedToken(data.base) ? colorFromExport(data.base) : data.base;
|
|
2127
|
+
return out;
|
|
2128
|
+
}
|
|
2129
|
+
function rehydrateStructuredInput(data) {
|
|
2130
|
+
const out = {
|
|
2131
|
+
hue: data.hue,
|
|
2132
|
+
saturation: data.saturation,
|
|
2133
|
+
lightness: data.lightness
|
|
2134
|
+
};
|
|
2135
|
+
if (data.saturationFactor !== void 0) out.saturationFactor = data.saturationFactor;
|
|
2136
|
+
if (data.mode !== void 0) out.mode = data.mode;
|
|
2137
|
+
if (data.opacity !== void 0) out.opacity = data.opacity;
|
|
2138
|
+
if (data.contrast !== void 0) out.contrast = data.contrast;
|
|
2139
|
+
if (data.name !== void 0) out.name = data.name;
|
|
2140
|
+
if (data.base !== void 0) out.base = isExportedToken(data.base) ? colorFromExport(data.base) : data.base;
|
|
2141
|
+
return out;
|
|
2142
|
+
}
|
|
2143
|
+
/**
|
|
2144
|
+
* Discriminate a `GlazeColorTokenExport` from a raw `GlazeColorValue`.
|
|
2145
|
+
* `GlazeColorTokenExport` always has a `form` field set to either
|
|
2146
|
+
* `'value'` or `'structured'`; raw values never do.
|
|
2147
|
+
*/
|
|
2148
|
+
function isExportedToken(candidate) {
|
|
2149
|
+
return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate) && "form" in candidate && (candidate.form === "value" || candidate.form === "structured");
|
|
2150
|
+
}
|
|
2151
|
+
/**
|
|
1605
2152
|
* Create a single-hue glaze theme.
|
|
1606
2153
|
*
|
|
1607
2154
|
* @example
|
|
@@ -1647,18 +2194,57 @@ glaze.palette = function palette(themes, options) {
|
|
|
1647
2194
|
glaze.from = function from(data) {
|
|
1648
2195
|
return createTheme(data.hue, data.saturation, data.colors);
|
|
1649
2196
|
};
|
|
2197
|
+
function isStructuredColorInput(input) {
|
|
2198
|
+
return typeof input === "object" && input !== null && !Array.isArray(input) && "hue" in input && "lightness" in input;
|
|
2199
|
+
}
|
|
1650
2200
|
/**
|
|
1651
2201
|
* Create a standalone single-color token.
|
|
2202
|
+
*
|
|
2203
|
+
* Two overloads:
|
|
2204
|
+
* - `glaze.color(input, scaling?)` — structured form:
|
|
2205
|
+
* `{ hue, saturation, lightness, ... }` plus an optional per-call
|
|
2206
|
+
* lightness-window override.
|
|
2207
|
+
* - `glaze.color(value, overrides?, scaling?)` — value-shorthand: a hex
|
|
2208
|
+
* string (3/6/8 digits), one of the CSS color functions Glaze itself
|
|
2209
|
+
* emits (`rgb()`, `hsl()`, `okhsl()`, `oklch()`), an `OkhslColor`
|
|
2210
|
+
* object `{ h, s, l }` (0–1 ranges), or an `[r, g, b]` (0–255) tuple.
|
|
2211
|
+
*
|
|
2212
|
+
* Defaults vary by input form:
|
|
2213
|
+
* - String value-shorthand: `mode: 'auto'` with snapshotted scaling
|
|
2214
|
+
* `{ lightLightness: false, darkLightness: [globalConfig.darkLightness[0], 100] }`.
|
|
2215
|
+
* Light preserves the input exactly; dark Möbius-inverts up to 100, so
|
|
2216
|
+
* `glaze.color('#000')` renders as `#fff` in dark mode (and
|
|
2217
|
+
* `glaze.color('#fff')` falls to the dark `lo` floor).
|
|
2218
|
+
* - `OkhslColor` object / RGB-tuple value-shorthand: `mode: 'fixed'`
|
|
2219
|
+
* with `scaling: { lightLightness: false }` — light preserves the
|
|
2220
|
+
* input; dark linearly maps into `globalConfig.darkLightness`.
|
|
2221
|
+
* - Structured form (`{ hue, saturation, lightness, ... }`):
|
|
2222
|
+
* `mode: 'fixed'`; both windows come from `globalConfig`.
|
|
2223
|
+
*
|
|
2224
|
+
* Relative `lightness: '+N'` and `contrast: <ratio>` are anchored to
|
|
2225
|
+
* the literal seed (the value passed in) by default, pinned at
|
|
2226
|
+
* `mode: 'static'` across all four variants. Pass `overrides.base` (a
|
|
2227
|
+
* `GlazeColorToken`) to anchor `contrast` and relative `lightness`
|
|
2228
|
+
* against another color's resolved variant per scheme instead. Relative
|
|
2229
|
+
* `hue: '+N'` always anchors to the seed.
|
|
2230
|
+
*
|
|
2231
|
+
* Alpha components in `rgba()` / `hsla()` / slash-alpha syntax and
|
|
2232
|
+
* 8-digit hex are parsed but dropped with a `console.warn`.
|
|
1652
2233
|
*/
|
|
1653
|
-
glaze.color = function color(input) {
|
|
1654
|
-
return createColorToken(input);
|
|
2234
|
+
glaze.color = function color(input, arg2, arg3) {
|
|
2235
|
+
if (isStructuredColorInput(input)) return createColorToken(input, arg2);
|
|
2236
|
+
return createColorTokenFromValue(input, arg2, arg3);
|
|
1655
2237
|
};
|
|
1656
2238
|
/**
|
|
1657
2239
|
* Compute a shadow color from a bg/fg pair and intensity.
|
|
2240
|
+
*
|
|
2241
|
+
* Both `bg` and `fg` accept any `GlazeColorValue` form: hex (`#rgb` /
|
|
2242
|
+
* `#rrggbb` / `#rrggbbaa`), `rgb()` / `hsl()` / `okhsl()` / `oklch()`
|
|
2243
|
+
* strings, `OkhslColor` objects, or `[r, g, b]` (0–255) tuples.
|
|
1658
2244
|
*/
|
|
1659
2245
|
glaze.shadow = function shadow(input) {
|
|
1660
|
-
const bg =
|
|
1661
|
-
const fg = input.fg ?
|
|
2246
|
+
const bg = extractOkhslFromValue(input.bg);
|
|
2247
|
+
const fg = input.fg ? extractOkhslFromValue(input.fg) : void 0;
|
|
1662
2248
|
const tuning = resolveShadowTuning(input.tuning);
|
|
1663
2249
|
return computeShadow({
|
|
1664
2250
|
...bg,
|
|
@@ -1674,19 +2260,6 @@ glaze.shadow = function shadow(input) {
|
|
|
1674
2260
|
glaze.format = function format(variant, colorFormat) {
|
|
1675
2261
|
return formatVariant(variant, colorFormat);
|
|
1676
2262
|
};
|
|
1677
|
-
function parseOkhslInput(input) {
|
|
1678
|
-
if (typeof input === "string") {
|
|
1679
|
-
const rgb = parseHex(input);
|
|
1680
|
-
if (!rgb) throw new Error(`glaze: invalid hex color "${input}".`);
|
|
1681
|
-
const [h, s, l] = srgbToOkhsl(rgb);
|
|
1682
|
-
return {
|
|
1683
|
-
h,
|
|
1684
|
-
s,
|
|
1685
|
-
l
|
|
1686
|
-
};
|
|
1687
|
-
}
|
|
1688
|
-
return input;
|
|
1689
|
-
}
|
|
1690
2263
|
/**
|
|
1691
2264
|
* Create a theme from a hex color string.
|
|
1692
2265
|
* Extracts hue and saturation from the color.
|
|
@@ -1710,6 +2283,26 @@ glaze.fromRgb = function fromRgb(r, g, b) {
|
|
|
1710
2283
|
return createTheme(h, s * 100);
|
|
1711
2284
|
};
|
|
1712
2285
|
/**
|
|
2286
|
+
* Rehydrate a `glaze.color()` token from a `.export()` snapshot.
|
|
2287
|
+
*
|
|
2288
|
+
* The snapshot is a plain JSON-safe object containing the original
|
|
2289
|
+
* input value, overrides (with any `base` token recursively serialized),
|
|
2290
|
+
* and the captured scaling. The reconstructed token is identical in
|
|
2291
|
+
* behavior to the original at the time of export.
|
|
2292
|
+
*
|
|
2293
|
+
* @example
|
|
2294
|
+
* ```ts
|
|
2295
|
+
* const text = glaze.color('#1a1a1a', { contrast: 'AA' });
|
|
2296
|
+
* const data = text.export(); // JSON-safe
|
|
2297
|
+
* localStorage.setItem('text', JSON.stringify(data));
|
|
2298
|
+
* // ...later...
|
|
2299
|
+
* const restored = glaze.colorFrom(JSON.parse(localStorage.getItem('text')!));
|
|
2300
|
+
* ```
|
|
2301
|
+
*/
|
|
2302
|
+
glaze.colorFrom = function colorFrom(data) {
|
|
2303
|
+
return colorFromExport(data);
|
|
2304
|
+
};
|
|
2305
|
+
/**
|
|
1713
2306
|
* Get the current global configuration (for testing/debugging).
|
|
1714
2307
|
*/
|
|
1715
2308
|
glaze.getConfig = function getConfig() {
|
|
@@ -1736,5 +2329,5 @@ glaze.resetConfig = function resetConfig() {
|
|
|
1736
2329
|
};
|
|
1737
2330
|
|
|
1738
2331
|
//#endregion
|
|
1739
|
-
export { contrastRatioFromLuminance, findLightnessForContrast, findValueForMixContrast, formatHsl, formatOkhsl, formatOklch, formatRgb, gamutClampedLuminance, glaze, okhslToLinearSrgb, okhslToOklab, okhslToSrgb, parseHex, relativeLuminanceFromLinearRgb, resolveMinContrast, srgbToOkhsl };
|
|
2332
|
+
export { contrastRatioFromLuminance, findLightnessForContrast, findValueForMixContrast, formatHsl, formatOkhsl, formatOklch, formatRgb, gamutClampedLuminance, glaze, hslToSrgb, okhslToLinearSrgb, okhslToOklab, okhslToSrgb, oklabToOkhsl, parseHex, parseHexAlpha, relativeLuminanceFromLinearRgb, resolveMinContrast, srgbToOkhsl };
|
|
1740
2333
|
//# sourceMappingURL=index.mjs.map
|