@tenphi/glaze 0.10.1 → 0.11.1
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 +19 -1384
- package/dist/index.cjs +714 -575
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -20
- package/dist/index.d.mts +20 -20
- package/dist/index.mjs +714 -575
- package/dist/index.mjs.map +1 -1
- package/docs/api.md +1074 -0
- package/docs/methodology.md +330 -0
- package/docs/migration.md +237 -0
- package/package.json +3 -2
package/dist/index.cjs
CHANGED
|
@@ -560,6 +560,116 @@ function formatOklch(h, s, l) {
|
|
|
560
560
|
return `oklch(${fmt$1(L, 4)} ${fmt$1(C, 4)} ${fmt$1(hh, 2)})`;
|
|
561
561
|
}
|
|
562
562
|
|
|
563
|
+
//#endregion
|
|
564
|
+
//#region src/config.ts
|
|
565
|
+
/**
|
|
566
|
+
* Build a fresh defaults object. Called from module init and from
|
|
567
|
+
* `resetConfig()` so the two paths can't drift.
|
|
568
|
+
*/
|
|
569
|
+
function defaultConfig() {
|
|
570
|
+
return {
|
|
571
|
+
lightLightness: [10, 100],
|
|
572
|
+
darkLightness: [15, 95],
|
|
573
|
+
darkDesaturation: .1,
|
|
574
|
+
darkCurve: .5,
|
|
575
|
+
states: {
|
|
576
|
+
dark: "@dark",
|
|
577
|
+
highContrast: "@high-contrast"
|
|
578
|
+
},
|
|
579
|
+
modes: {
|
|
580
|
+
dark: true,
|
|
581
|
+
highContrast: false
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
let globalConfig = defaultConfig();
|
|
586
|
+
/**
|
|
587
|
+
* Monotonic counter incremented on every `configure()` / `resetConfig()`
|
|
588
|
+
* call. Theme / palette caches read this to invalidate stale resolve
|
|
589
|
+
* results when the config changes between exports.
|
|
590
|
+
*/
|
|
591
|
+
let configVersion = 0;
|
|
592
|
+
/** Live reference to the current config. Mutated by `configure()` / `resetConfig()`. */
|
|
593
|
+
function getConfig() {
|
|
594
|
+
return globalConfig;
|
|
595
|
+
}
|
|
596
|
+
function getConfigVersion() {
|
|
597
|
+
return configVersion;
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Public-facing snapshot used by `glaze.getConfig()`. Returns a shallow
|
|
601
|
+
* copy so callers can't mutate the live config.
|
|
602
|
+
*/
|
|
603
|
+
function snapshotConfig() {
|
|
604
|
+
return { ...globalConfig };
|
|
605
|
+
}
|
|
606
|
+
function configure(config) {
|
|
607
|
+
configVersion++;
|
|
608
|
+
globalConfig = {
|
|
609
|
+
lightLightness: config.lightLightness ?? globalConfig.lightLightness,
|
|
610
|
+
darkLightness: config.darkLightness ?? globalConfig.darkLightness,
|
|
611
|
+
darkDesaturation: config.darkDesaturation ?? globalConfig.darkDesaturation,
|
|
612
|
+
darkCurve: config.darkCurve ?? globalConfig.darkCurve,
|
|
613
|
+
states: {
|
|
614
|
+
dark: config.states?.dark ?? globalConfig.states.dark,
|
|
615
|
+
highContrast: config.states?.highContrast ?? globalConfig.states.highContrast
|
|
616
|
+
},
|
|
617
|
+
modes: {
|
|
618
|
+
dark: config.modes?.dark ?? globalConfig.modes.dark,
|
|
619
|
+
highContrast: config.modes?.highContrast ?? globalConfig.modes.highContrast
|
|
620
|
+
},
|
|
621
|
+
shadowTuning: config.shadowTuning ?? globalConfig.shadowTuning
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
function resetConfig() {
|
|
625
|
+
configVersion++;
|
|
626
|
+
globalConfig = defaultConfig();
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
//#endregion
|
|
630
|
+
//#region src/hc-pair.ts
|
|
631
|
+
function pairNormal(p) {
|
|
632
|
+
return Array.isArray(p) ? p[0] : p;
|
|
633
|
+
}
|
|
634
|
+
function pairHC(p) {
|
|
635
|
+
return Array.isArray(p) ? p[1] : p;
|
|
636
|
+
}
|
|
637
|
+
function clamp(v, min, max) {
|
|
638
|
+
return Math.max(min, Math.min(max, v));
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Parse a value that can be absolute (number) or relative (signed string).
|
|
642
|
+
* Returns the numeric value and whether it's relative.
|
|
643
|
+
*/
|
|
644
|
+
function parseRelativeOrAbsolute(value) {
|
|
645
|
+
if (typeof value === "number") return {
|
|
646
|
+
value,
|
|
647
|
+
relative: false
|
|
648
|
+
};
|
|
649
|
+
return {
|
|
650
|
+
value: parseFloat(value),
|
|
651
|
+
relative: true
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Compute the effective hue for a color, given the theme seed hue
|
|
656
|
+
* and an optional per-color hue override.
|
|
657
|
+
*/
|
|
658
|
+
function resolveEffectiveHue(seedHue, defHue) {
|
|
659
|
+
if (defHue === void 0) return seedHue;
|
|
660
|
+
const parsed = parseRelativeOrAbsolute(defHue);
|
|
661
|
+
if (parsed.relative) return ((seedHue + parsed.value) % 360 + 360) % 360;
|
|
662
|
+
return (parsed.value % 360 + 360) % 360;
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Check whether a lightness value represents an absolute root definition
|
|
666
|
+
* (i.e. a number, not a relative string).
|
|
667
|
+
*/
|
|
668
|
+
function isAbsoluteLightness(lightness) {
|
|
669
|
+
if (lightness === void 0) return false;
|
|
670
|
+
return typeof (Array.isArray(lightness) ? lightness[0] : lightness) === "number";
|
|
671
|
+
}
|
|
672
|
+
|
|
563
673
|
//#endregion
|
|
564
674
|
//#region src/contrast-solver.ts
|
|
565
675
|
/**
|
|
@@ -890,107 +1000,15 @@ function findValueForMixContrast(options) {
|
|
|
890
1000
|
}
|
|
891
1001
|
|
|
892
1002
|
//#endregion
|
|
893
|
-
//#region src/
|
|
894
|
-
/**
|
|
895
|
-
* Glaze — OKHSL-based color theme generator.
|
|
896
|
-
*
|
|
897
|
-
* Generates robust light, dark, and high-contrast colors from a hue/saturation
|
|
898
|
-
* seed, preserving contrast for UI pairs via explicit dependencies.
|
|
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";
|
|
1003
|
+
//#region src/shadow.ts
|
|
906
1004
|
/**
|
|
907
|
-
*
|
|
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).
|
|
1005
|
+
* Shadow color computation.
|
|
912
1006
|
*
|
|
913
|
-
*
|
|
914
|
-
*
|
|
915
|
-
*
|
|
916
|
-
*
|
|
917
|
-
*/
|
|
918
|
-
function defaultStandaloneScaling(extendDark) {
|
|
919
|
-
const [lo, hi] = globalConfig.darkLightness;
|
|
920
|
-
return {
|
|
921
|
-
lightLightness: false,
|
|
922
|
-
darkLightness: extendDark ? [lo, 100] : [lo, hi]
|
|
923
|
-
};
|
|
924
|
-
}
|
|
925
|
-
/** Reserved internal names that user-supplied `name` must not collide with. */
|
|
926
|
-
const RESERVED_STANDALONE_NAMES = new Set([
|
|
927
|
-
STANDALONE_VALUE,
|
|
928
|
-
STANDALONE_SEED,
|
|
929
|
-
STANDALONE_BASE
|
|
930
|
-
]);
|
|
931
|
-
/**
|
|
932
|
-
* Discriminate a `GlazeColorToken` from a raw `GlazeColorValue`.
|
|
933
|
-
* Used to widen `base?` so it accepts either a token reference or a
|
|
934
|
-
* raw value (auto-wrapped into `glaze.color(value)`).
|
|
935
|
-
*/
|
|
936
|
-
function isGlazeColorToken(candidate) {
|
|
937
|
-
return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate) && "resolve" in candidate && typeof candidate.resolve === "function";
|
|
938
|
-
}
|
|
939
|
-
let globalConfig = {
|
|
940
|
-
lightLightness: [10, 100],
|
|
941
|
-
darkLightness: [15, 95],
|
|
942
|
-
darkDesaturation: .1,
|
|
943
|
-
darkCurve: .5,
|
|
944
|
-
states: {
|
|
945
|
-
dark: "@dark",
|
|
946
|
-
highContrast: "@high-contrast"
|
|
947
|
-
},
|
|
948
|
-
modes: {
|
|
949
|
-
dark: true,
|
|
950
|
-
highContrast: false
|
|
951
|
-
}
|
|
952
|
-
};
|
|
953
|
-
function pairNormal(p) {
|
|
954
|
-
return Array.isArray(p) ? p[0] : p;
|
|
955
|
-
}
|
|
956
|
-
function pairHC(p) {
|
|
957
|
-
return Array.isArray(p) ? p[1] : p;
|
|
958
|
-
}
|
|
959
|
-
/**
|
|
960
|
-
* Dedupe contrast warnings within a single process. The cache survives
|
|
961
|
-
* the lifetime of a token because tokens memoize their resolution; the
|
|
962
|
-
* limit is a soft cap to keep noise bounded across long-lived sessions
|
|
963
|
-
* (e.g. dev servers with HMR re-resolving themes repeatedly).
|
|
964
|
-
*/
|
|
965
|
-
const CONTRAST_WARN_CACHE_LIMIT = 256;
|
|
966
|
-
const contrastWarnCache = /* @__PURE__ */ new Set();
|
|
967
|
-
function schemeLabel(isDark, isHighContrast) {
|
|
968
|
-
if (isDark && isHighContrast) return "darkContrast";
|
|
969
|
-
if (isDark) return "dark";
|
|
970
|
-
if (isHighContrast) return "lightContrast";
|
|
971
|
-
return "light";
|
|
972
|
-
}
|
|
973
|
-
function formatContrastTarget(input, ratio) {
|
|
974
|
-
return typeof input === "string" ? `"${input}" (${ratio.toFixed(2)})` : ratio.toFixed(2);
|
|
975
|
-
}
|
|
976
|
-
/**
|
|
977
|
-
* Slack factor below the requested target before we emit a warning.
|
|
978
|
-
* The contrast solver already overshoots by `OVERSHOOT` (currently 1%)
|
|
979
|
-
* to absorb rounding noise (`see findLightnessForContrast` in
|
|
980
|
-
* `contrast-solver.ts`), so an `actual` ratio within ~2x that overshoot
|
|
981
|
-
* is effectively a pass and not worth nagging the user about.
|
|
1007
|
+
* Owns the shadow / mix def predicates, default tuning constants, the
|
|
1008
|
+
* tuning merge, and the actual `computeShadow` math (hue blend,
|
|
1009
|
+
* saturation cap, lightness clamp, alpha curve). The resolver consumes
|
|
1010
|
+
* this module per scheme variant.
|
|
982
1011
|
*/
|
|
983
|
-
const CONTRAST_WARN_SLACK = .98;
|
|
984
|
-
function warnContrastUnmet(name, isDark, isHighContrast, target, actual) {
|
|
985
|
-
const targetRatio = resolveMinContrast(target);
|
|
986
|
-
if (actual >= targetRatio * CONTRAST_WARN_SLACK) return;
|
|
987
|
-
const scheme = schemeLabel(isDark, isHighContrast);
|
|
988
|
-
const key = `${name}|${scheme}|${targetRatio.toFixed(3)}|${actual.toFixed(2)}`;
|
|
989
|
-
if (contrastWarnCache.has(key)) return;
|
|
990
|
-
if (contrastWarnCache.size >= CONTRAST_WARN_CACHE_LIMIT) contrastWarnCache.clear();
|
|
991
|
-
contrastWarnCache.add(key);
|
|
992
|
-
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.`);
|
|
993
|
-
}
|
|
994
1012
|
function isShadowDef(def) {
|
|
995
1013
|
return def.type === "shadow";
|
|
996
1014
|
}
|
|
@@ -1007,11 +1025,12 @@ const DEFAULT_SHADOW_TUNING = {
|
|
|
1007
1025
|
bgHueBlend: .2
|
|
1008
1026
|
};
|
|
1009
1027
|
function resolveShadowTuning(perColor) {
|
|
1028
|
+
const globalTuning = getConfig().shadowTuning;
|
|
1010
1029
|
return {
|
|
1011
1030
|
...DEFAULT_SHADOW_TUNING,
|
|
1012
|
-
...
|
|
1031
|
+
...globalTuning,
|
|
1013
1032
|
...perColor,
|
|
1014
|
-
lightnessBounds: perColor?.lightnessBounds ??
|
|
1033
|
+
lightnessBounds: perColor?.lightnessBounds ?? globalTuning?.lightnessBounds ?? DEFAULT_SHADOW_TUNING.lightnessBounds
|
|
1015
1034
|
};
|
|
1016
1035
|
}
|
|
1017
1036
|
function circularLerp(a, b, t) {
|
|
@@ -1052,6 +1071,80 @@ function computeShadow(bg, fg, intensity, tuning) {
|
|
|
1052
1071
|
alpha
|
|
1053
1072
|
};
|
|
1054
1073
|
}
|
|
1074
|
+
|
|
1075
|
+
//#endregion
|
|
1076
|
+
//#region src/scheme-mapping.ts
|
|
1077
|
+
/**
|
|
1078
|
+
* Light / dark scheme lightness mappings.
|
|
1079
|
+
*
|
|
1080
|
+
* Owns the active lightness window selection (with per-call scaling
|
|
1081
|
+
* overrides and high-contrast handling), the Möbius curve used by the
|
|
1082
|
+
* `'auto'` dark adaptation, and the saturation-desaturation reducer
|
|
1083
|
+
* for dark mode.
|
|
1084
|
+
*/
|
|
1085
|
+
/**
|
|
1086
|
+
* Resolve the active lightness window for a scheme.
|
|
1087
|
+
* - HC variants always return `[0, 100]` (existing behavior, predates per-call overrides).
|
|
1088
|
+
* - Otherwise, per-call `scaling` (e.g. from `glaze.color()`'s third arg) wins;
|
|
1089
|
+
* `false` is interpreted as `[0, 100]` (no remap). Falls back to `globalConfig.*Lightness`.
|
|
1090
|
+
*/
|
|
1091
|
+
function lightnessWindow(isHighContrast, kind, scaling) {
|
|
1092
|
+
if (isHighContrast) return [0, 100];
|
|
1093
|
+
if (scaling) {
|
|
1094
|
+
const override = kind === "dark" ? scaling.darkLightness : scaling.lightLightness;
|
|
1095
|
+
if (override === false) return [0, 100];
|
|
1096
|
+
if (override !== void 0) return override;
|
|
1097
|
+
}
|
|
1098
|
+
const cfg = getConfig();
|
|
1099
|
+
return kind === "dark" ? cfg.darkLightness : cfg.lightLightness;
|
|
1100
|
+
}
|
|
1101
|
+
function mapLightnessLight(l, mode, isHighContrast, scaling) {
|
|
1102
|
+
if (mode === "static") return l;
|
|
1103
|
+
const [lo, hi] = lightnessWindow(isHighContrast, "light", scaling);
|
|
1104
|
+
return l * (hi - lo) / 100 + lo;
|
|
1105
|
+
}
|
|
1106
|
+
function mobiusCurve(t, beta) {
|
|
1107
|
+
if (beta >= 1) return t;
|
|
1108
|
+
return t / (t + beta * (1 - t));
|
|
1109
|
+
}
|
|
1110
|
+
function mapLightnessDark(l, mode, isHighContrast, scaling) {
|
|
1111
|
+
if (mode === "static") return l;
|
|
1112
|
+
const cfg = getConfig();
|
|
1113
|
+
const beta = isHighContrast ? pairHC(cfg.darkCurve) : pairNormal(cfg.darkCurve);
|
|
1114
|
+
const [darkLo, darkHi] = lightnessWindow(isHighContrast, "dark", scaling);
|
|
1115
|
+
if (mode === "fixed") return l * (darkHi - darkLo) / 100 + darkLo;
|
|
1116
|
+
const [lightLo, lightHi] = lightnessWindow(isHighContrast, "light", scaling);
|
|
1117
|
+
const t = (lightHi - (l * (lightHi - lightLo) / 100 + lightLo)) / (lightHi - lightLo);
|
|
1118
|
+
return darkLo + (darkHi - darkLo) * mobiusCurve(t, beta);
|
|
1119
|
+
}
|
|
1120
|
+
function lightMappedToDark(lightL, isHighContrast, scaling) {
|
|
1121
|
+
const cfg = getConfig();
|
|
1122
|
+
const beta = isHighContrast ? pairHC(cfg.darkCurve) : pairNormal(cfg.darkCurve);
|
|
1123
|
+
const [lightLo, lightHi] = lightnessWindow(isHighContrast, "light", scaling);
|
|
1124
|
+
const [darkLo, darkHi] = lightnessWindow(isHighContrast, "dark", scaling);
|
|
1125
|
+
const t = (lightHi - clamp(lightL, lightLo, lightHi)) / (lightHi - lightLo);
|
|
1126
|
+
return darkLo + (darkHi - darkLo) * mobiusCurve(t, beta);
|
|
1127
|
+
}
|
|
1128
|
+
function mapSaturationDark(s, mode) {
|
|
1129
|
+
if (mode === "static") return s;
|
|
1130
|
+
return s * (1 - getConfig().darkDesaturation);
|
|
1131
|
+
}
|
|
1132
|
+
function schemeLightnessRange(isDark, mode, isHighContrast, scaling) {
|
|
1133
|
+
if (mode === "static") return [0, 1];
|
|
1134
|
+
const [lo, hi] = lightnessWindow(isHighContrast, isDark ? "dark" : "light", scaling);
|
|
1135
|
+
return [lo / 100, hi / 100];
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
//#endregion
|
|
1139
|
+
//#region src/validation.ts
|
|
1140
|
+
/**
|
|
1141
|
+
* Color graph validation and topological sort.
|
|
1142
|
+
*
|
|
1143
|
+
* `validateColorDefs` rejects bad references (missing / shadow-referencing /
|
|
1144
|
+
* base/contrast/lightness mismatches) and detects cycles before the
|
|
1145
|
+
* resolver runs. `topoSort` orders defs so each color is processed after
|
|
1146
|
+
* its base / bg / fg / target dependencies.
|
|
1147
|
+
*/
|
|
1055
1148
|
function validateColorDefs(defs, externalBases) {
|
|
1056
1149
|
const localNames = new Set(Object.keys(defs));
|
|
1057
1150
|
const allNames = new Set([...localNames, ...externalBases ? externalBases.keys() : []]);
|
|
@@ -1126,89 +1219,62 @@ function topoSort(defs) {
|
|
|
1126
1219
|
for (const name of Object.keys(defs)) visit(name);
|
|
1127
1220
|
return result;
|
|
1128
1221
|
}
|
|
1222
|
+
|
|
1223
|
+
//#endregion
|
|
1224
|
+
//#region src/warnings.ts
|
|
1129
1225
|
/**
|
|
1130
|
-
*
|
|
1131
|
-
*
|
|
1132
|
-
*
|
|
1133
|
-
*
|
|
1226
|
+
* Contrast-warning dispatcher.
|
|
1227
|
+
*
|
|
1228
|
+
* Tokens memoize their resolution, but a long-lived process (e.g. a dev
|
|
1229
|
+
* server with HMR) can re-resolve the same theme many times. The cache
|
|
1230
|
+
* here dedupes warnings within a session with a soft cap to keep noise
|
|
1231
|
+
* bounded.
|
|
1134
1232
|
*/
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1233
|
+
const CONTRAST_WARN_CACHE_LIMIT = 256;
|
|
1234
|
+
const contrastWarnCache = /* @__PURE__ */ new Set();
|
|
1235
|
+
/**
|
|
1236
|
+
* Slack factor below the requested target before we emit a warning.
|
|
1237
|
+
* The contrast solver already overshoots by `OVERSHOOT` (currently 1%)
|
|
1238
|
+
* to absorb rounding noise (`see findLightnessForContrast` in
|
|
1239
|
+
* `contrast-solver.ts`), so an `actual` ratio within ~2x that overshoot
|
|
1240
|
+
* is effectively a pass and not worth nagging the user about.
|
|
1241
|
+
*/
|
|
1242
|
+
const CONTRAST_WARN_SLACK = .98;
|
|
1243
|
+
function schemeLabel(isDark, isHighContrast) {
|
|
1244
|
+
if (isDark && isHighContrast) return "darkContrast";
|
|
1245
|
+
if (isDark) return "dark";
|
|
1246
|
+
if (isHighContrast) return "lightContrast";
|
|
1247
|
+
return "light";
|
|
1148
1248
|
}
|
|
1149
|
-
function
|
|
1150
|
-
|
|
1151
|
-
return t / (t + beta * (1 - t));
|
|
1249
|
+
function formatContrastTarget(input, ratio) {
|
|
1250
|
+
return typeof input === "string" ? `"${input}" (${ratio.toFixed(2)})` : ratio.toFixed(2);
|
|
1152
1251
|
}
|
|
1153
|
-
function
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
const
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
}
|
|
1162
|
-
function lightMappedToDark(lightL, isHighContrast, scaling) {
|
|
1163
|
-
const beta = isHighContrast ? pairHC(globalConfig.darkCurve) : pairNormal(globalConfig.darkCurve);
|
|
1164
|
-
const [lightLo, lightHi] = lightnessWindow(isHighContrast, "light", scaling);
|
|
1165
|
-
const [darkLo, darkHi] = lightnessWindow(isHighContrast, "dark", scaling);
|
|
1166
|
-
const t = (lightHi - clamp(lightL, lightLo, lightHi)) / (lightHi - lightLo);
|
|
1167
|
-
return darkLo + (darkHi - darkLo) * mobiusCurve(t, beta);
|
|
1168
|
-
}
|
|
1169
|
-
function mapSaturationDark(s, mode) {
|
|
1170
|
-
if (mode === "static") return s;
|
|
1171
|
-
return s * (1 - globalConfig.darkDesaturation);
|
|
1172
|
-
}
|
|
1173
|
-
function schemeLightnessRange(isDark, mode, isHighContrast, scaling) {
|
|
1174
|
-
if (mode === "static") return [0, 1];
|
|
1175
|
-
const [lo, hi] = lightnessWindow(isHighContrast, isDark ? "dark" : "light", scaling);
|
|
1176
|
-
return [lo / 100, hi / 100];
|
|
1177
|
-
}
|
|
1178
|
-
function clamp(v, min, max) {
|
|
1179
|
-
return Math.max(min, Math.min(max, v));
|
|
1180
|
-
}
|
|
1181
|
-
/**
|
|
1182
|
-
* Parse a value that can be absolute (number) or relative (signed string).
|
|
1183
|
-
* Returns the numeric value and whether it's relative.
|
|
1184
|
-
*/
|
|
1185
|
-
function parseRelativeOrAbsolute(value) {
|
|
1186
|
-
if (typeof value === "number") return {
|
|
1187
|
-
value,
|
|
1188
|
-
relative: false
|
|
1189
|
-
};
|
|
1190
|
-
return {
|
|
1191
|
-
value: parseFloat(value),
|
|
1192
|
-
relative: true
|
|
1193
|
-
};
|
|
1194
|
-
}
|
|
1195
|
-
/**
|
|
1196
|
-
* Compute the effective hue for a color, given the theme seed hue
|
|
1197
|
-
* and an optional per-color hue override.
|
|
1198
|
-
*/
|
|
1199
|
-
function resolveEffectiveHue(seedHue, defHue) {
|
|
1200
|
-
if (defHue === void 0) return seedHue;
|
|
1201
|
-
const parsed = parseRelativeOrAbsolute(defHue);
|
|
1202
|
-
if (parsed.relative) return ((seedHue + parsed.value) % 360 + 360) % 360;
|
|
1203
|
-
return (parsed.value % 360 + 360) % 360;
|
|
1252
|
+
function warnContrastUnmet(name, isDark, isHighContrast, target, actual) {
|
|
1253
|
+
const targetRatio = resolveMinContrast(target);
|
|
1254
|
+
if (actual >= targetRatio * CONTRAST_WARN_SLACK) return;
|
|
1255
|
+
const scheme = schemeLabel(isDark, isHighContrast);
|
|
1256
|
+
const key = `${name}|${scheme}|${targetRatio.toFixed(3)}|${actual.toFixed(2)}`;
|
|
1257
|
+
if (contrastWarnCache.has(key)) return;
|
|
1258
|
+
if (contrastWarnCache.size >= CONTRAST_WARN_CACHE_LIMIT) contrastWarnCache.clear();
|
|
1259
|
+
contrastWarnCache.add(key);
|
|
1260
|
+
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.`);
|
|
1204
1261
|
}
|
|
1262
|
+
|
|
1263
|
+
//#endregion
|
|
1264
|
+
//#region src/resolver.ts
|
|
1205
1265
|
/**
|
|
1206
|
-
*
|
|
1207
|
-
*
|
|
1266
|
+
* Color resolution engine.
|
|
1267
|
+
*
|
|
1268
|
+
* Runs the four-pass solver (light → light-HC → dark → dark-HC) that
|
|
1269
|
+
* turns a `ColorMap` into a fully resolved `ResolvedColor` per name.
|
|
1270
|
+
* Owns the per-scheme resolve helpers for regular, shadow, and mix
|
|
1271
|
+
* color defs.
|
|
1208
1272
|
*/
|
|
1209
|
-
function
|
|
1210
|
-
if (
|
|
1211
|
-
|
|
1273
|
+
function getSchemeVariant(color, isDark, isHighContrast) {
|
|
1274
|
+
if (isDark && isHighContrast) return color.darkContrast;
|
|
1275
|
+
if (isDark) return color.dark;
|
|
1276
|
+
if (isHighContrast) return color.lightContrast;
|
|
1277
|
+
return color.light;
|
|
1212
1278
|
}
|
|
1213
1279
|
function resolveRootColor(_name, def, _ctx, isHighContrast) {
|
|
1214
1280
|
const rawL = def.lightness;
|
|
@@ -1262,12 +1328,6 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
|
|
|
1262
1328
|
satFactor
|
|
1263
1329
|
};
|
|
1264
1330
|
}
|
|
1265
|
-
function getSchemeVariant(color, isDark, isHighContrast) {
|
|
1266
|
-
if (isDark && isHighContrast) return color.darkContrast;
|
|
1267
|
-
if (isDark) return color.dark;
|
|
1268
|
-
if (isHighContrast) return color.lightContrast;
|
|
1269
|
-
return color.light;
|
|
1270
|
-
}
|
|
1271
1331
|
function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
1272
1332
|
if (isShadowDef(def)) return resolveShadowForScheme(def, ctx, isDark, isHighContrast);
|
|
1273
1333
|
if (isMixDef(def)) return resolveMixForScheme(def, ctx, isDark, isHighContrast);
|
|
@@ -1393,26 +1453,27 @@ function resolveMixForScheme(def, ctx, isDark, isHighContrast) {
|
|
|
1393
1453
|
alpha: 1
|
|
1394
1454
|
};
|
|
1395
1455
|
}
|
|
1396
|
-
function
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
if (isShadowDef(def) || isMixDef(def)) return void 0;
|
|
1409
|
-
return def.mode ?? "auto";
|
|
1410
|
-
}
|
|
1411
|
-
const lightMap = /* @__PURE__ */ new Map();
|
|
1456
|
+
function defMode(def) {
|
|
1457
|
+
if (isShadowDef(def) || isMixDef(def)) return void 0;
|
|
1458
|
+
return def.mode ?? "auto";
|
|
1459
|
+
}
|
|
1460
|
+
/**
|
|
1461
|
+
* Run a single resolve pass over all local names. Pass 1 lazily creates
|
|
1462
|
+
* each `ResolvedColor` (all four slots seeded with the just-resolved
|
|
1463
|
+
* variant) the first time it sees a name; later passes update the
|
|
1464
|
+
* `target` slot on the existing record.
|
|
1465
|
+
*/
|
|
1466
|
+
function runPass(order, defs, ctx, isDark, isHighContrast, target) {
|
|
1467
|
+
const out = /* @__PURE__ */ new Map();
|
|
1412
1468
|
for (const name of order) {
|
|
1413
|
-
const variant = resolveColorForScheme(name, defs[name], ctx,
|
|
1414
|
-
|
|
1415
|
-
ctx.resolved.
|
|
1469
|
+
const variant = resolveColorForScheme(name, defs[name], ctx, isDark, isHighContrast);
|
|
1470
|
+
out.set(name, variant);
|
|
1471
|
+
const existing = ctx.resolved.get(name);
|
|
1472
|
+
if (existing) ctx.resolved.set(name, {
|
|
1473
|
+
...existing,
|
|
1474
|
+
[target]: variant
|
|
1475
|
+
});
|
|
1476
|
+
else ctx.resolved.set(name, {
|
|
1416
1477
|
name,
|
|
1417
1478
|
light: variant,
|
|
1418
1479
|
dark: variant,
|
|
@@ -1421,49 +1482,40 @@ function resolveAllColors(hue, saturation, defs, scaling, externalBases) {
|
|
|
1421
1482
|
mode: defMode(defs[name])
|
|
1422
1483
|
});
|
|
1423
1484
|
}
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
lightHCMap.set(name, variant);
|
|
1432
|
-
ctx.resolved.set(name, {
|
|
1433
|
-
...ctx.resolved.get(name),
|
|
1434
|
-
lightContrast: variant
|
|
1435
|
-
});
|
|
1436
|
-
}
|
|
1437
|
-
const darkMap = /* @__PURE__ */ new Map();
|
|
1438
|
-
for (const name of order) ctx.resolved.set(name, {
|
|
1439
|
-
name,
|
|
1440
|
-
light: lightMap.get(name),
|
|
1441
|
-
dark: lightMap.get(name),
|
|
1442
|
-
lightContrast: lightHCMap.get(name),
|
|
1443
|
-
darkContrast: lightHCMap.get(name),
|
|
1444
|
-
mode: defMode(defs[name])
|
|
1445
|
-
});
|
|
1446
|
-
for (const name of order) {
|
|
1447
|
-
const variant = resolveColorForScheme(name, defs[name], ctx, true, false);
|
|
1448
|
-
darkMap.set(name, variant);
|
|
1449
|
-
ctx.resolved.set(name, {
|
|
1450
|
-
...ctx.resolved.get(name),
|
|
1451
|
-
dark: variant
|
|
1452
|
-
});
|
|
1453
|
-
}
|
|
1454
|
-
const darkHCMap = /* @__PURE__ */ new Map();
|
|
1455
|
-
for (const name of order) ctx.resolved.set(name, {
|
|
1456
|
-
...ctx.resolved.get(name),
|
|
1457
|
-
darkContrast: darkMap.get(name)
|
|
1458
|
-
});
|
|
1485
|
+
return out;
|
|
1486
|
+
}
|
|
1487
|
+
/**
|
|
1488
|
+
* Re-seed a single variant slot with a previously-resolved map so the
|
|
1489
|
+
* upcoming pass reads sensible fallbacks via `getSchemeVariant`.
|
|
1490
|
+
*/
|
|
1491
|
+
function seedField(order, ctx, field, source) {
|
|
1459
1492
|
for (const name of order) {
|
|
1460
|
-
const
|
|
1461
|
-
darkHCMap.set(name, variant);
|
|
1493
|
+
const existing = ctx.resolved.get(name);
|
|
1462
1494
|
ctx.resolved.set(name, {
|
|
1463
|
-
...
|
|
1464
|
-
|
|
1495
|
+
...existing,
|
|
1496
|
+
[field]: source.get(name)
|
|
1465
1497
|
});
|
|
1466
1498
|
}
|
|
1499
|
+
}
|
|
1500
|
+
function resolveAllColors(hue, saturation, defs, scaling, externalBases) {
|
|
1501
|
+
validateColorDefs(defs, externalBases);
|
|
1502
|
+
const order = topoSort(defs);
|
|
1503
|
+
const ctx = {
|
|
1504
|
+
hue,
|
|
1505
|
+
saturation,
|
|
1506
|
+
defs,
|
|
1507
|
+
resolved: /* @__PURE__ */ new Map(),
|
|
1508
|
+
scaling
|
|
1509
|
+
};
|
|
1510
|
+
if (externalBases) for (const [name, color] of externalBases) ctx.resolved.set(name, color);
|
|
1511
|
+
const lightMap = runPass(order, defs, ctx, false, false, "light");
|
|
1512
|
+
seedField(order, ctx, "lightContrast", lightMap);
|
|
1513
|
+
const lightHCMap = runPass(order, defs, ctx, false, true, "lightContrast");
|
|
1514
|
+
seedField(order, ctx, "dark", lightMap);
|
|
1515
|
+
seedField(order, ctx, "darkContrast", lightHCMap);
|
|
1516
|
+
const darkMap = runPass(order, defs, ctx, true, false, "dark");
|
|
1517
|
+
seedField(order, ctx, "darkContrast", darkMap);
|
|
1518
|
+
const darkHCMap = runPass(order, defs, ctx, true, true, "darkContrast");
|
|
1467
1519
|
const result = /* @__PURE__ */ new Map();
|
|
1468
1520
|
for (const name of order) result.set(name, {
|
|
1469
1521
|
name,
|
|
@@ -1475,6 +1527,19 @@ function resolveAllColors(hue, saturation, defs, scaling, externalBases) {
|
|
|
1475
1527
|
});
|
|
1476
1528
|
return result;
|
|
1477
1529
|
}
|
|
1530
|
+
|
|
1531
|
+
//#endregion
|
|
1532
|
+
//#region src/formatters.ts
|
|
1533
|
+
/**
|
|
1534
|
+
* Output formatting for resolved color maps.
|
|
1535
|
+
*
|
|
1536
|
+
* Owns the CSS-string formatter dispatch table (`okhsl` / `rgb` / `hsl` /
|
|
1537
|
+
* `oklch`) and the four token-map shapes Glaze emits:
|
|
1538
|
+
* - `buildTokenMap` — Tasty style-to-state bindings (`#name` keys, state aliases).
|
|
1539
|
+
* - `buildFlatTokenMap` — `{ light, dark, ... }` per-variant maps.
|
|
1540
|
+
* - `buildJsonMap` — `{ name: { light, dark, ... } }` per-color JSON.
|
|
1541
|
+
* - `buildCssMap` — CSS custom property declaration strings per variant.
|
|
1542
|
+
*/
|
|
1478
1543
|
const formatters = {
|
|
1479
1544
|
okhsl: formatOkhsl,
|
|
1480
1545
|
rgb: formatRgb,
|
|
@@ -1491,9 +1556,10 @@ function formatVariant(v, format = "okhsl") {
|
|
|
1491
1556
|
return `${base.slice(0, closing)} / ${fmt(v.alpha, 4)})`;
|
|
1492
1557
|
}
|
|
1493
1558
|
function resolveModes(override) {
|
|
1559
|
+
const cfg = getConfig();
|
|
1494
1560
|
return {
|
|
1495
|
-
dark: override?.dark ??
|
|
1496
|
-
highContrast: override?.highContrast ??
|
|
1561
|
+
dark: override?.dark ?? cfg.modes.dark,
|
|
1562
|
+
highContrast: override?.highContrast ?? cfg.modes.highContrast
|
|
1497
1563
|
};
|
|
1498
1564
|
}
|
|
1499
1565
|
function buildTokenMap(resolved, prefix, states, modes, format = "okhsl") {
|
|
@@ -1554,220 +1620,88 @@ function buildCssMap(resolved, prefix, suffix, format) {
|
|
|
1554
1620
|
darkContrast: lines.darkContrast.join("\n")
|
|
1555
1621
|
};
|
|
1556
1622
|
}
|
|
1557
|
-
|
|
1558
|
-
|
|
1623
|
+
|
|
1624
|
+
//#endregion
|
|
1625
|
+
//#region src/color-token.ts
|
|
1626
|
+
/**
|
|
1627
|
+
* Standalone single-color tokens (`glaze.color()` / `glaze.colorFrom()`).
|
|
1628
|
+
*
|
|
1629
|
+
* Owns the value-shorthand parser (hex, `rgb()` / `hsl()` / `okhsl()` /
|
|
1630
|
+
* `oklch()`, OkhslColor object, [r, g, b] tuple), the structured-input
|
|
1631
|
+
* validator, the two factory paths (value vs structured), and the
|
|
1632
|
+
* JSON-safe export / rehydration round-trip.
|
|
1633
|
+
*
|
|
1634
|
+
* Standalone tokens snapshot the relevant `globalConfig` fields at
|
|
1635
|
+
* create time so later `configure()` calls do not retroactively change
|
|
1636
|
+
* exported tokens — the snapshot is captured eagerly in
|
|
1637
|
+
* `defaultStandaloneScaling()`. The token's resolved variants are then
|
|
1638
|
+
* memoized on first `.resolve()` / `.token()` / ... call.
|
|
1639
|
+
*/
|
|
1640
|
+
/** Internal name of the user-facing standalone color in the synthesized def map. */
|
|
1641
|
+
const STANDALONE_VALUE = "value";
|
|
1642
|
+
/** Internal name of the hidden static-anchor seed used for relative lightness / contrast. */
|
|
1643
|
+
const STANDALONE_SEED = "seed";
|
|
1644
|
+
/** Internal name of an externally-resolved `GlazeColorToken` injected as a base reference. */
|
|
1645
|
+
const STANDALONE_BASE = "externalBase";
|
|
1646
|
+
/** Reserved internal names that user-supplied `name` must not collide with. */
|
|
1647
|
+
const RESERVED_STANDALONE_NAMES = new Set([
|
|
1648
|
+
STANDALONE_VALUE,
|
|
1649
|
+
STANDALONE_SEED,
|
|
1650
|
+
STANDALONE_BASE
|
|
1651
|
+
]);
|
|
1652
|
+
/**
|
|
1653
|
+
* Build the create-time scaling snapshot used when the caller did not
|
|
1654
|
+
* pass an explicit `scaling`. All windows are snapshotted from the
|
|
1655
|
+
* current `globalConfig` so later `glaze.configure()` calls don't
|
|
1656
|
+
* retroactively change the resolved variants of an already-created
|
|
1657
|
+
* token (matches the documented "frozen at create time" semantics).
|
|
1658
|
+
*
|
|
1659
|
+
* String value-shorthand inputs preserve their light lightness exactly
|
|
1660
|
+
* (`lightLightness: false`) and use an extended dark window
|
|
1661
|
+
* `[globalConfig.darkLightness[0], 100]` so a totally-black input can
|
|
1662
|
+
* Möbius-invert to totally-white in dark mode. Object / tuple /
|
|
1663
|
+
* structured inputs snapshot both windows from `globalConfig` verbatim
|
|
1664
|
+
* so they behave like an ordinary theme color (auto-adapted on both
|
|
1665
|
+
* sides).
|
|
1666
|
+
*/
|
|
1667
|
+
function defaultStandaloneScaling(isString) {
|
|
1668
|
+
const cfg = getConfig();
|
|
1669
|
+
if (isString) {
|
|
1670
|
+
const [darkLo] = cfg.darkLightness;
|
|
1671
|
+
return {
|
|
1672
|
+
lightLightness: false,
|
|
1673
|
+
darkLightness: [darkLo, 100]
|
|
1674
|
+
};
|
|
1675
|
+
}
|
|
1559
1676
|
return {
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
},
|
|
1563
|
-
get saturation() {
|
|
1564
|
-
return saturation;
|
|
1565
|
-
},
|
|
1566
|
-
colors(defs) {
|
|
1567
|
-
colorDefs = {
|
|
1568
|
-
...colorDefs,
|
|
1569
|
-
...defs
|
|
1570
|
-
};
|
|
1571
|
-
},
|
|
1572
|
-
color(name, def) {
|
|
1573
|
-
if (def === void 0) return colorDefs[name];
|
|
1574
|
-
colorDefs[name] = def;
|
|
1575
|
-
},
|
|
1576
|
-
remove(names) {
|
|
1577
|
-
const list = Array.isArray(names) ? names : [names];
|
|
1578
|
-
for (const name of list) delete colorDefs[name];
|
|
1579
|
-
},
|
|
1580
|
-
has(name) {
|
|
1581
|
-
return name in colorDefs;
|
|
1582
|
-
},
|
|
1583
|
-
list() {
|
|
1584
|
-
return Object.keys(colorDefs);
|
|
1585
|
-
},
|
|
1586
|
-
reset() {
|
|
1587
|
-
colorDefs = {};
|
|
1588
|
-
},
|
|
1589
|
-
export() {
|
|
1590
|
-
return {
|
|
1591
|
-
hue,
|
|
1592
|
-
saturation,
|
|
1593
|
-
colors: { ...colorDefs }
|
|
1594
|
-
};
|
|
1595
|
-
},
|
|
1596
|
-
extend(options) {
|
|
1597
|
-
const newHue = options.hue ?? hue;
|
|
1598
|
-
const newSat = options.saturation ?? saturation;
|
|
1599
|
-
const inheritedColors = {};
|
|
1600
|
-
for (const [name, def] of Object.entries(colorDefs)) if (def.inherit !== false) inheritedColors[name] = def;
|
|
1601
|
-
return createTheme(newHue, newSat, options.colors ? {
|
|
1602
|
-
...inheritedColors,
|
|
1603
|
-
...options.colors
|
|
1604
|
-
} : { ...inheritedColors });
|
|
1605
|
-
},
|
|
1606
|
-
resolve() {
|
|
1607
|
-
return resolveAllColors(hue, saturation, colorDefs);
|
|
1608
|
-
},
|
|
1609
|
-
tokens(options) {
|
|
1610
|
-
return buildFlatTokenMap(resolveAllColors(hue, saturation, colorDefs), "", resolveModes(options?.modes), options?.format);
|
|
1611
|
-
},
|
|
1612
|
-
tasty(options) {
|
|
1613
|
-
return buildTokenMap(resolveAllColors(hue, saturation, colorDefs), "", {
|
|
1614
|
-
dark: options?.states?.dark ?? globalConfig.states.dark,
|
|
1615
|
-
highContrast: options?.states?.highContrast ?? globalConfig.states.highContrast
|
|
1616
|
-
}, resolveModes(options?.modes), options?.format);
|
|
1617
|
-
},
|
|
1618
|
-
json(options) {
|
|
1619
|
-
return buildJsonMap(resolveAllColors(hue, saturation, colorDefs), resolveModes(options?.modes), options?.format);
|
|
1620
|
-
},
|
|
1621
|
-
css(options) {
|
|
1622
|
-
return buildCssMap(resolveAllColors(hue, saturation, colorDefs), "", options?.suffix ?? "-color", options?.format ?? "rgb");
|
|
1623
|
-
}
|
|
1677
|
+
lightLightness: cfg.lightLightness,
|
|
1678
|
+
darkLightness: cfg.darkLightness
|
|
1624
1679
|
};
|
|
1625
1680
|
}
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1681
|
+
/**
|
|
1682
|
+
* Discriminate a `GlazeColorToken` from a raw `GlazeColorValue`.
|
|
1683
|
+
* Used to widen `base?` so it accepts either a token reference or a
|
|
1684
|
+
* raw value (auto-wrapped into `glaze.color(value)`).
|
|
1685
|
+
*/
|
|
1686
|
+
function isGlazeColorToken(candidate) {
|
|
1687
|
+
return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate) && "resolve" in candidate && typeof candidate.resolve === "function";
|
|
1631
1688
|
}
|
|
1632
|
-
function
|
|
1633
|
-
|
|
1634
|
-
const available = Object.keys(themes).join(", ");
|
|
1635
|
-
throw new Error(`glaze: primary theme "${primary}" not found in palette. Available: ${available}.`);
|
|
1636
|
-
}
|
|
1689
|
+
function isStructuredColorInput(input) {
|
|
1690
|
+
return typeof input === "object" && input !== null && !Array.isArray(input) && "hue" in input && "lightness" in input;
|
|
1637
1691
|
}
|
|
1638
1692
|
/**
|
|
1639
|
-
*
|
|
1640
|
-
* `
|
|
1693
|
+
* Matches the CSS color functions Glaze itself emits (`rgb()`, `hsl()`,
|
|
1694
|
+
* `okhsl()`, `oklch()`) plus their legacy alpha aliases (`rgba()`, `hsla()`).
|
|
1695
|
+
*
|
|
1696
|
+
* Only bare numeric components are supported. Named colors (`red`),
|
|
1697
|
+
* relative-color syntax (`from <color> ...`), and angle units other
|
|
1698
|
+
* than bare degrees (`deg` is the only suffix tolerated by `parseFloat`)
|
|
1699
|
+
* are out of scope.
|
|
1641
1700
|
*/
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
return
|
|
1645
|
-
|
|
1646
|
-
/**
|
|
1647
|
-
* Filter a resolved color map, skipping keys already in `seen`.
|
|
1648
|
-
* Warns on collision and keeps the first-written value (first-write-wins).
|
|
1649
|
-
* Returns a new map containing only non-colliding entries.
|
|
1650
|
-
*/
|
|
1651
|
-
function filterCollisions(resolved, prefix, seen, themeName, isPrimary) {
|
|
1652
|
-
const filtered = /* @__PURE__ */ new Map();
|
|
1653
|
-
const label = isPrimary ? `${themeName} (primary)` : themeName;
|
|
1654
|
-
for (const [name, color] of resolved) {
|
|
1655
|
-
const key = `${prefix}${name}`;
|
|
1656
|
-
if (seen.has(key)) {
|
|
1657
|
-
console.warn(`glaze: token "${key}" from theme "${label}" collides with theme "${seen.get(key)}" — skipping.`);
|
|
1658
|
-
continue;
|
|
1659
|
-
}
|
|
1660
|
-
seen.set(key, label);
|
|
1661
|
-
filtered.set(name, color);
|
|
1662
|
-
}
|
|
1663
|
-
return filtered;
|
|
1664
|
-
}
|
|
1665
|
-
function createPalette(themes, paletteOptions) {
|
|
1666
|
-
validatePrimaryTheme(paletteOptions?.primary, themes);
|
|
1667
|
-
return {
|
|
1668
|
-
tokens(options) {
|
|
1669
|
-
const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
|
|
1670
|
-
if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
|
|
1671
|
-
const modes = resolveModes(options?.modes);
|
|
1672
|
-
const allTokens = {};
|
|
1673
|
-
const seen = /* @__PURE__ */ new Map();
|
|
1674
|
-
for (const [themeName, theme] of Object.entries(themes)) {
|
|
1675
|
-
const resolved = theme.resolve();
|
|
1676
|
-
const prefix = resolvePrefix(options, themeName, true);
|
|
1677
|
-
const tokens = buildFlatTokenMap(filterCollisions(resolved, prefix, seen, themeName), prefix, modes, options?.format);
|
|
1678
|
-
for (const variant of Object.keys(tokens)) {
|
|
1679
|
-
if (!allTokens[variant]) allTokens[variant] = {};
|
|
1680
|
-
Object.assign(allTokens[variant], tokens[variant]);
|
|
1681
|
-
}
|
|
1682
|
-
if (themeName === effectivePrimary) {
|
|
1683
|
-
const unprefixed = buildFlatTokenMap(filterCollisions(resolved, "", seen, themeName, true), "", modes, options?.format);
|
|
1684
|
-
for (const variant of Object.keys(unprefixed)) Object.assign(allTokens[variant], unprefixed[variant]);
|
|
1685
|
-
}
|
|
1686
|
-
}
|
|
1687
|
-
return allTokens;
|
|
1688
|
-
},
|
|
1689
|
-
tasty(options) {
|
|
1690
|
-
const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
|
|
1691
|
-
if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
|
|
1692
|
-
const states = {
|
|
1693
|
-
dark: options?.states?.dark ?? globalConfig.states.dark,
|
|
1694
|
-
highContrast: options?.states?.highContrast ?? globalConfig.states.highContrast
|
|
1695
|
-
};
|
|
1696
|
-
const modes = resolveModes(options?.modes);
|
|
1697
|
-
const allTokens = {};
|
|
1698
|
-
const seen = /* @__PURE__ */ new Map();
|
|
1699
|
-
for (const [themeName, theme] of Object.entries(themes)) {
|
|
1700
|
-
const resolved = theme.resolve();
|
|
1701
|
-
const prefix = resolvePrefix(options, themeName, true);
|
|
1702
|
-
const tokens = buildTokenMap(filterCollisions(resolved, prefix, seen, themeName), prefix, states, modes, options?.format);
|
|
1703
|
-
Object.assign(allTokens, tokens);
|
|
1704
|
-
if (themeName === effectivePrimary) {
|
|
1705
|
-
const unprefixed = buildTokenMap(filterCollisions(resolved, "", seen, themeName, true), "", states, modes, options?.format);
|
|
1706
|
-
Object.assign(allTokens, unprefixed);
|
|
1707
|
-
}
|
|
1708
|
-
}
|
|
1709
|
-
return allTokens;
|
|
1710
|
-
},
|
|
1711
|
-
json(options) {
|
|
1712
|
-
const modes = resolveModes(options?.modes);
|
|
1713
|
-
const result = {};
|
|
1714
|
-
for (const [themeName, theme] of Object.entries(themes)) result[themeName] = buildJsonMap(theme.resolve(), modes, options?.format);
|
|
1715
|
-
return result;
|
|
1716
|
-
},
|
|
1717
|
-
css(options) {
|
|
1718
|
-
const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
|
|
1719
|
-
if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
|
|
1720
|
-
const suffix = options?.suffix ?? "-color";
|
|
1721
|
-
const format = options?.format ?? "rgb";
|
|
1722
|
-
const allLines = {
|
|
1723
|
-
light: [],
|
|
1724
|
-
dark: [],
|
|
1725
|
-
lightContrast: [],
|
|
1726
|
-
darkContrast: []
|
|
1727
|
-
};
|
|
1728
|
-
const seen = /* @__PURE__ */ new Map();
|
|
1729
|
-
for (const [themeName, theme] of Object.entries(themes)) {
|
|
1730
|
-
const resolved = theme.resolve();
|
|
1731
|
-
const prefix = resolvePrefix(options, themeName, true);
|
|
1732
|
-
const css = buildCssMap(filterCollisions(resolved, prefix, seen, themeName), prefix, suffix, format);
|
|
1733
|
-
for (const key of [
|
|
1734
|
-
"light",
|
|
1735
|
-
"dark",
|
|
1736
|
-
"lightContrast",
|
|
1737
|
-
"darkContrast"
|
|
1738
|
-
]) if (css[key]) allLines[key].push(css[key]);
|
|
1739
|
-
if (themeName === effectivePrimary) {
|
|
1740
|
-
const unprefixed = buildCssMap(filterCollisions(resolved, "", seen, themeName, true), "", suffix, format);
|
|
1741
|
-
for (const key of [
|
|
1742
|
-
"light",
|
|
1743
|
-
"dark",
|
|
1744
|
-
"lightContrast",
|
|
1745
|
-
"darkContrast"
|
|
1746
|
-
]) if (unprefixed[key]) allLines[key].push(unprefixed[key]);
|
|
1747
|
-
}
|
|
1748
|
-
}
|
|
1749
|
-
return {
|
|
1750
|
-
light: allLines.light.join("\n"),
|
|
1751
|
-
dark: allLines.dark.join("\n"),
|
|
1752
|
-
lightContrast: allLines.lightContrast.join("\n"),
|
|
1753
|
-
darkContrast: allLines.darkContrast.join("\n")
|
|
1754
|
-
};
|
|
1755
|
-
}
|
|
1756
|
-
};
|
|
1757
|
-
}
|
|
1758
|
-
/**
|
|
1759
|
-
* Matches the CSS color functions Glaze itself emits (`rgb()`, `hsl()`,
|
|
1760
|
-
* `okhsl()`, `oklch()`) plus their legacy alpha aliases (`rgba()`, `hsla()`).
|
|
1761
|
-
*
|
|
1762
|
-
* Only bare numeric components are supported. Named colors (`red`),
|
|
1763
|
-
* relative-color syntax (`from <color> ...`), and angle units other
|
|
1764
|
-
* than bare degrees (`deg` is the only suffix tolerated by `parseFloat`)
|
|
1765
|
-
* are out of scope.
|
|
1766
|
-
*/
|
|
1767
|
-
const COLOR_FN_RE = /^(rgba?|hsla?|okhsl|oklch)\(\s*([^)]*)\s*\)$/i;
|
|
1768
|
-
function parseNumberOrPercent(raw, percentScale) {
|
|
1769
|
-
if (raw.endsWith("%")) return parseFloat(raw) / 100 * percentScale;
|
|
1770
|
-
return parseFloat(raw);
|
|
1701
|
+
const COLOR_FN_RE = /^(rgba?|hsla?|okhsl|oklch)\(\s*([^)]*)\s*\)$/i;
|
|
1702
|
+
function parseNumberOrPercent(raw, percentScale) {
|
|
1703
|
+
if (raw.endsWith("%")) return parseFloat(raw) / 100 * percentScale;
|
|
1704
|
+
return parseFloat(raw);
|
|
1771
1705
|
}
|
|
1772
1706
|
/**
|
|
1773
1707
|
* Split the body of a CSS color function into its components and detect
|
|
@@ -1872,9 +1806,7 @@ function validateOkhslColor(value) {
|
|
|
1872
1806
|
if (!Number.isFinite(h) || !Number.isFinite(s) || !Number.isFinite(l)) throw new Error("glaze.color: OkhslColor h/s/l must be finite numbers.");
|
|
1873
1807
|
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)?");
|
|
1874
1808
|
}
|
|
1875
|
-
/**
|
|
1876
|
-
* Validate a user-supplied `[r, g, b]` tuple in 0-255.
|
|
1877
|
-
*/
|
|
1809
|
+
/** Validate a user-supplied `[r, g, b]` tuple in 0-255. */
|
|
1878
1810
|
function validateRgbTuple(value) {
|
|
1879
1811
|
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(", ")}]).`);
|
|
1880
1812
|
}
|
|
@@ -1948,17 +1880,18 @@ function extractOkhslFromValue(value) {
|
|
|
1948
1880
|
* Build the `ColorMap` for a value-shorthand `glaze.color()` call.
|
|
1949
1881
|
*
|
|
1950
1882
|
* The user-facing color (`STANDALONE_VALUE`) defaults to `mode: 'auto'`
|
|
1951
|
-
*
|
|
1883
|
+
* across every value-shorthand form. String inputs pair with the
|
|
1952
1884
|
* extended dark window so a totally-black input renders as totally-white
|
|
1953
|
-
* in dark mode
|
|
1954
|
-
*
|
|
1885
|
+
* in dark mode; `OkhslColor` / RGB-tuple inputs auto-adapt into the
|
|
1886
|
+
* snapshotted `globalConfig.lightLightness` / `globalConfig.darkLightness`
|
|
1887
|
+
* windows.
|
|
1955
1888
|
*
|
|
1956
1889
|
* When the user requests `contrast` or relative `lightness`, a hidden
|
|
1957
1890
|
* `STANDALONE_SEED` def is synthesized at `mode: 'static'`. That keeps
|
|
1958
1891
|
* the seed pinned to the literal user-provided color across all four
|
|
1959
1892
|
* variants, so the contrast solver always anchors against it.
|
|
1960
1893
|
*/
|
|
1961
|
-
function buildStandaloneValueDefs(main, options
|
|
1894
|
+
function buildStandaloneValueDefs(main, options) {
|
|
1962
1895
|
const seedHue = typeof options?.hue === "number" ? options.hue : main.h;
|
|
1963
1896
|
const seedSaturation = options?.saturation ?? main.s * 100;
|
|
1964
1897
|
const relativeHue = typeof options?.hue === "string" ? options.hue : void 0;
|
|
@@ -1974,7 +1907,7 @@ function buildStandaloneValueDefs(main, options, inputIsString) {
|
|
|
1974
1907
|
saturation: options?.saturationFactor,
|
|
1975
1908
|
lightness: lightnessOption ?? main.l * 100,
|
|
1976
1909
|
contrast: options?.contrast,
|
|
1977
|
-
mode: options?.mode ??
|
|
1910
|
+
mode: options?.mode ?? "auto",
|
|
1978
1911
|
opacity: options?.opacity,
|
|
1979
1912
|
base: hasExternalBase ? STANDALONE_BASE : needsSeedAnchor ? STANDALONE_SEED : void 0
|
|
1980
1913
|
};
|
|
@@ -1999,10 +1932,13 @@ function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effect
|
|
|
1999
1932
|
cached = resolveAllColors(seedHue, seedSaturation, defs, effectiveScaling, baseToken ? new Map([[STANDALONE_BASE, baseToken.resolve()]]) : void 0);
|
|
2000
1933
|
return cached;
|
|
2001
1934
|
};
|
|
2002
|
-
const resolveStates = (options) =>
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
1935
|
+
const resolveStates = (options) => {
|
|
1936
|
+
const cfg = getConfig();
|
|
1937
|
+
return {
|
|
1938
|
+
dark: options?.states?.dark ?? cfg.states.dark,
|
|
1939
|
+
highContrast: options?.states?.highContrast ?? cfg.states.highContrast
|
|
1940
|
+
};
|
|
1941
|
+
};
|
|
2006
1942
|
const tokenLike = (options) => {
|
|
2007
1943
|
return buildTokenMap(resolveOnce(), "", resolveStates(options), resolveModes(options?.modes), options?.format)[`#${primary}`];
|
|
2008
1944
|
};
|
|
@@ -2032,38 +1968,6 @@ function resolveBaseToken(base) {
|
|
|
2032
1968
|
if (isGlazeColorToken(base)) return base;
|
|
2033
1969
|
return createColorTokenFromValue(base, void 0, void 0);
|
|
2034
1970
|
}
|
|
2035
|
-
/**
|
|
2036
|
-
* Build a JSON-safe snapshot of `GlazeColorOverrides`. `base` is
|
|
2037
|
-
* recursively serialized when it was originally a token; raw values are
|
|
2038
|
-
* preserved as-is so `glaze.colorFrom(...)` round-trips them.
|
|
2039
|
-
*/
|
|
2040
|
-
function buildOverridesExport(options) {
|
|
2041
|
-
const out = {};
|
|
2042
|
-
if (options.hue !== void 0) out.hue = options.hue;
|
|
2043
|
-
if (options.saturation !== void 0) out.saturation = options.saturation;
|
|
2044
|
-
if (options.lightness !== void 0) out.lightness = options.lightness;
|
|
2045
|
-
if (options.saturationFactor !== void 0) out.saturationFactor = options.saturationFactor;
|
|
2046
|
-
if (options.mode !== void 0) out.mode = options.mode;
|
|
2047
|
-
if (options.contrast !== void 0) out.contrast = options.contrast;
|
|
2048
|
-
if (options.opacity !== void 0) out.opacity = options.opacity;
|
|
2049
|
-
if (options.name !== void 0) out.name = options.name;
|
|
2050
|
-
if (options.base !== void 0) out.base = isGlazeColorToken(options.base) ? options.base.export() : options.base;
|
|
2051
|
-
return out;
|
|
2052
|
-
}
|
|
2053
|
-
function buildStructuredInputExport(input) {
|
|
2054
|
-
const out = {
|
|
2055
|
-
hue: input.hue,
|
|
2056
|
-
saturation: input.saturation,
|
|
2057
|
-
lightness: input.lightness
|
|
2058
|
-
};
|
|
2059
|
-
if (input.saturationFactor !== void 0) out.saturationFactor = input.saturationFactor;
|
|
2060
|
-
if (input.mode !== void 0) out.mode = input.mode;
|
|
2061
|
-
if (input.opacity !== void 0) out.opacity = input.opacity;
|
|
2062
|
-
if (input.contrast !== void 0) out.contrast = input.contrast;
|
|
2063
|
-
if (input.name !== void 0) out.name = input.name;
|
|
2064
|
-
if (input.base !== void 0) out.base = isGlazeColorToken(input.base) ? input.base.export() : input.base;
|
|
2065
|
-
return out;
|
|
2066
|
-
}
|
|
2067
1971
|
function createColorToken(input, scaling) {
|
|
2068
1972
|
validateStructuredInput(input);
|
|
2069
1973
|
const userName = input.name;
|
|
@@ -2075,7 +1979,7 @@ function createColorToken(input, scaling) {
|
|
|
2075
1979
|
const defs = { [primary]: {
|
|
2076
1980
|
lightness: input.lightness,
|
|
2077
1981
|
saturation: input.saturationFactor,
|
|
2078
|
-
mode: input.mode ?? "
|
|
1982
|
+
mode: input.mode ?? "auto",
|
|
2079
1983
|
contrast: input.contrast,
|
|
2080
1984
|
opacity: input.opacity,
|
|
2081
1985
|
base: hasExternalBase ? STANDALONE_BASE : needsSeedAnchor ? STANDALONE_SEED : void 0
|
|
@@ -2097,7 +2001,7 @@ function createColorTokenFromValue(value, options, scaling) {
|
|
|
2097
2001
|
const inputIsString = typeof value === "string";
|
|
2098
2002
|
const main = extractOkhslFromValue(value);
|
|
2099
2003
|
const baseToken = resolveBaseToken(options?.base);
|
|
2100
|
-
const { seedHue, seedSaturation, defs, primary } = buildStandaloneValueDefs(main, options
|
|
2004
|
+
const { seedHue, seedSaturation, defs, primary } = buildStandaloneValueDefs(main, options);
|
|
2101
2005
|
const effectiveScaling = scaling ?? defaultStandaloneScaling(inputIsString);
|
|
2102
2006
|
const exportData = () => ({
|
|
2103
2007
|
form: "value",
|
|
@@ -2108,18 +2012,44 @@ function createColorTokenFromValue(value, options, scaling) {
|
|
|
2108
2012
|
return createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effectiveScaling, baseToken, exportData);
|
|
2109
2013
|
}
|
|
2110
2014
|
/**
|
|
2111
|
-
*
|
|
2112
|
-
*
|
|
2015
|
+
* Build a JSON-safe snapshot of `GlazeColorOverrides`. `base` is
|
|
2016
|
+
* recursively serialized when it was originally a token; raw values are
|
|
2017
|
+
* preserved as-is so `glaze.colorFrom(...)` round-trips them.
|
|
2113
2018
|
*/
|
|
2114
|
-
function
|
|
2115
|
-
|
|
2116
|
-
if (
|
|
2117
|
-
if (
|
|
2118
|
-
if (
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2019
|
+
function buildOverridesExport(options) {
|
|
2020
|
+
const out = {};
|
|
2021
|
+
if (options.hue !== void 0) out.hue = options.hue;
|
|
2022
|
+
if (options.saturation !== void 0) out.saturation = options.saturation;
|
|
2023
|
+
if (options.lightness !== void 0) out.lightness = options.lightness;
|
|
2024
|
+
if (options.saturationFactor !== void 0) out.saturationFactor = options.saturationFactor;
|
|
2025
|
+
if (options.mode !== void 0) out.mode = options.mode;
|
|
2026
|
+
if (options.contrast !== void 0) out.contrast = options.contrast;
|
|
2027
|
+
if (options.opacity !== void 0) out.opacity = options.opacity;
|
|
2028
|
+
if (options.name !== void 0) out.name = options.name;
|
|
2029
|
+
if (options.base !== void 0) out.base = isGlazeColorToken(options.base) ? options.base.export() : options.base;
|
|
2030
|
+
return out;
|
|
2031
|
+
}
|
|
2032
|
+
function buildStructuredInputExport(input) {
|
|
2033
|
+
const out = {
|
|
2034
|
+
hue: input.hue,
|
|
2035
|
+
saturation: input.saturation,
|
|
2036
|
+
lightness: input.lightness
|
|
2037
|
+
};
|
|
2038
|
+
if (input.saturationFactor !== void 0) out.saturationFactor = input.saturationFactor;
|
|
2039
|
+
if (input.mode !== void 0) out.mode = input.mode;
|
|
2040
|
+
if (input.opacity !== void 0) out.opacity = input.opacity;
|
|
2041
|
+
if (input.contrast !== void 0) out.contrast = input.contrast;
|
|
2042
|
+
if (input.name !== void 0) out.name = input.name;
|
|
2043
|
+
if (input.base !== void 0) out.base = isGlazeColorToken(input.base) ? input.base.export() : input.base;
|
|
2044
|
+
return out;
|
|
2045
|
+
}
|
|
2046
|
+
/**
|
|
2047
|
+
* Discriminate a `GlazeColorTokenExport` from a raw `GlazeColorValue`.
|
|
2048
|
+
* `GlazeColorTokenExport` always has a `form` field set to either
|
|
2049
|
+
* `'value'` or `'structured'`; raw values never do.
|
|
2050
|
+
*/
|
|
2051
|
+
function isExportedToken(candidate) {
|
|
2052
|
+
return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate) && "form" in candidate && (candidate.form === "value" || candidate.form === "structured");
|
|
2123
2053
|
}
|
|
2124
2054
|
function rehydrateOverrides(data) {
|
|
2125
2055
|
const out = {};
|
|
@@ -2149,13 +2079,258 @@ function rehydrateStructuredInput(data) {
|
|
|
2149
2079
|
return out;
|
|
2150
2080
|
}
|
|
2151
2081
|
/**
|
|
2152
|
-
*
|
|
2153
|
-
*
|
|
2154
|
-
* `'value'` or `'structured'`; raw values never do.
|
|
2082
|
+
* Rehydrate a token from its `.export()` snapshot. Recursively rebuilds
|
|
2083
|
+
* any base dependency. Inverse of `GlazeColorToken.export()`.
|
|
2155
2084
|
*/
|
|
2156
|
-
function
|
|
2157
|
-
|
|
2085
|
+
function colorFromExport(data) {
|
|
2086
|
+
if (data === null || typeof data !== "object") throw new Error(`glaze.colorFrom: expected an object from token.export(), got ${data === null ? "null" : typeof data}.`);
|
|
2087
|
+
if (data.form !== "value" && data.form !== "structured") throw new Error(`glaze.colorFrom: invalid "form" field — expected "value" or "structured" (got ${JSON.stringify(data.form)}).`);
|
|
2088
|
+
if (data.input === void 0) throw new Error(`glaze.colorFrom: missing "input" field — expected the original ${data.form === "value" ? "GlazeColorValue" : "GlazeColorInput"}.`);
|
|
2089
|
+
if (data.form === "value") {
|
|
2090
|
+
const value = data.input;
|
|
2091
|
+
return createColorTokenFromValue(value, data.overrides ? rehydrateOverrides(data.overrides) : void 0, data.scaling);
|
|
2092
|
+
}
|
|
2093
|
+
return createColorToken(rehydrateStructuredInput(data.input), data.scaling);
|
|
2158
2094
|
}
|
|
2095
|
+
|
|
2096
|
+
//#endregion
|
|
2097
|
+
//#region src/palette.ts
|
|
2098
|
+
/**
|
|
2099
|
+
* Palette factory.
|
|
2100
|
+
*
|
|
2101
|
+
* Composes multiple themes into a single token namespace with optional
|
|
2102
|
+
* theme-name prefixes and a "primary theme" that also surfaces an
|
|
2103
|
+
* unprefixed copy of its tokens. All four export methods (`tokens` /
|
|
2104
|
+
* `tasty` / `json` / `css`) share a `buildPaletteOutput` driver that
|
|
2105
|
+
* handles validation, per-theme iteration, prefix resolution, collision
|
|
2106
|
+
* filtering, and primary duplication.
|
|
2107
|
+
*/
|
|
2108
|
+
function resolvePrefix(options, themeName, defaultPrefix = false) {
|
|
2109
|
+
const prefix = options?.prefix ?? defaultPrefix;
|
|
2110
|
+
if (prefix === true) return `${themeName}-`;
|
|
2111
|
+
if (typeof prefix === "object" && prefix !== null) return prefix[themeName] ?? `${themeName}-`;
|
|
2112
|
+
return "";
|
|
2113
|
+
}
|
|
2114
|
+
function validatePrimaryTheme(primary, themes) {
|
|
2115
|
+
if (primary !== void 0 && !(primary in themes)) {
|
|
2116
|
+
const available = Object.keys(themes).join(", ");
|
|
2117
|
+
throw new Error(`glaze: primary theme "${primary}" not found in palette. Available: ${available}.`);
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
/**
|
|
2121
|
+
* Resolve the effective primary for an export call.
|
|
2122
|
+
* `false` disables, a string overrides, `undefined` inherits from palette.
|
|
2123
|
+
*/
|
|
2124
|
+
function resolveEffectivePrimary(exportPrimary, palettePrimary) {
|
|
2125
|
+
if (exportPrimary === false) return void 0;
|
|
2126
|
+
return exportPrimary ?? palettePrimary;
|
|
2127
|
+
}
|
|
2128
|
+
/**
|
|
2129
|
+
* Filter a resolved color map, skipping keys already in `seen`.
|
|
2130
|
+
* Warns on collision and keeps the first-written value (first-write-wins).
|
|
2131
|
+
* Returns a new map containing only non-colliding entries.
|
|
2132
|
+
*/
|
|
2133
|
+
function filterCollisions(resolved, prefix, seen, themeName, isPrimary) {
|
|
2134
|
+
const filtered = /* @__PURE__ */ new Map();
|
|
2135
|
+
const label = isPrimary ? `${themeName} (primary)` : themeName;
|
|
2136
|
+
for (const [name, color] of resolved) {
|
|
2137
|
+
const key = `${prefix}${name}`;
|
|
2138
|
+
if (seen.has(key)) {
|
|
2139
|
+
console.warn(`glaze: token "${key}" from theme "${label}" collides with theme "${seen.get(key)}" — skipping.`);
|
|
2140
|
+
continue;
|
|
2141
|
+
}
|
|
2142
|
+
seen.set(key, label);
|
|
2143
|
+
filtered.set(name, color);
|
|
2144
|
+
}
|
|
2145
|
+
return filtered;
|
|
2146
|
+
}
|
|
2147
|
+
/**
|
|
2148
|
+
* Shared per-theme driver for `tokens` / `tasty` / `css`. `json` skips
|
|
2149
|
+
* this because it doesn't do collision filtering or primary duplication.
|
|
2150
|
+
*/
|
|
2151
|
+
function buildPaletteOutput(themes, paletteOptions, options, buildOne, merge, empty) {
|
|
2152
|
+
const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
|
|
2153
|
+
if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
|
|
2154
|
+
const acc = empty();
|
|
2155
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2156
|
+
for (const [themeName, theme] of Object.entries(themes)) {
|
|
2157
|
+
const resolved = theme.resolve();
|
|
2158
|
+
const prefix = resolvePrefix(options, themeName, true);
|
|
2159
|
+
merge(acc, buildOne(filterCollisions(resolved, prefix, seen, themeName), prefix));
|
|
2160
|
+
if (themeName === effectivePrimary) merge(acc, buildOne(filterCollisions(resolved, "", seen, themeName, true), ""));
|
|
2161
|
+
}
|
|
2162
|
+
return acc;
|
|
2163
|
+
}
|
|
2164
|
+
function createPalette(themes, paletteOptions) {
|
|
2165
|
+
validatePrimaryTheme(paletteOptions?.primary, themes);
|
|
2166
|
+
return {
|
|
2167
|
+
tokens(options) {
|
|
2168
|
+
const modes = resolveModes(options?.modes);
|
|
2169
|
+
return buildPaletteOutput(themes, paletteOptions, options, (filtered, prefix) => buildFlatTokenMap(filtered, prefix, modes, options?.format), (acc, part) => {
|
|
2170
|
+
for (const variant of Object.keys(part)) {
|
|
2171
|
+
if (!acc[variant]) acc[variant] = {};
|
|
2172
|
+
Object.assign(acc[variant], part[variant]);
|
|
2173
|
+
}
|
|
2174
|
+
}, () => ({}));
|
|
2175
|
+
},
|
|
2176
|
+
tasty(options) {
|
|
2177
|
+
const cfg = getConfig();
|
|
2178
|
+
const states = {
|
|
2179
|
+
dark: options?.states?.dark ?? cfg.states.dark,
|
|
2180
|
+
highContrast: options?.states?.highContrast ?? cfg.states.highContrast
|
|
2181
|
+
};
|
|
2182
|
+
const modes = resolveModes(options?.modes);
|
|
2183
|
+
return buildPaletteOutput(themes, paletteOptions, options, (filtered, prefix) => buildTokenMap(filtered, prefix, states, modes, options?.format), (acc, part) => Object.assign(acc, part), () => ({}));
|
|
2184
|
+
},
|
|
2185
|
+
json(options) {
|
|
2186
|
+
const modes = resolveModes(options?.modes);
|
|
2187
|
+
const result = {};
|
|
2188
|
+
for (const [themeName, theme] of Object.entries(themes)) result[themeName] = buildJsonMap(theme.resolve(), modes, options?.format);
|
|
2189
|
+
return result;
|
|
2190
|
+
},
|
|
2191
|
+
css(options) {
|
|
2192
|
+
const suffix = options?.suffix ?? "-color";
|
|
2193
|
+
const format = options?.format ?? "rgb";
|
|
2194
|
+
const lines = buildPaletteOutput(themes, paletteOptions, options, (filtered, prefix) => buildCssMap(filtered, prefix, suffix, format), (acc, part) => {
|
|
2195
|
+
for (const key of [
|
|
2196
|
+
"light",
|
|
2197
|
+
"dark",
|
|
2198
|
+
"lightContrast",
|
|
2199
|
+
"darkContrast"
|
|
2200
|
+
]) if (part[key]) acc[key].push(part[key]);
|
|
2201
|
+
}, () => ({
|
|
2202
|
+
light: [],
|
|
2203
|
+
dark: [],
|
|
2204
|
+
lightContrast: [],
|
|
2205
|
+
darkContrast: []
|
|
2206
|
+
}));
|
|
2207
|
+
return {
|
|
2208
|
+
light: lines.light.join("\n"),
|
|
2209
|
+
dark: lines.dark.join("\n"),
|
|
2210
|
+
lightContrast: lines.lightContrast.join("\n"),
|
|
2211
|
+
darkContrast: lines.darkContrast.join("\n")
|
|
2212
|
+
};
|
|
2213
|
+
}
|
|
2214
|
+
};
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
//#endregion
|
|
2218
|
+
//#region src/theme.ts
|
|
2219
|
+
/**
|
|
2220
|
+
* Theme factory.
|
|
2221
|
+
*
|
|
2222
|
+
* Wraps a hue/saturation seed and a mutable `ColorMap`, and exposes
|
|
2223
|
+
* `tokens()` / `tasty()` / `json()` / `css()` / `resolve()` / `export()`
|
|
2224
|
+
* / `extend()`. Caches the last resolve result so successive exports
|
|
2225
|
+
* with the same defs and config don't re-run the four-pass resolver.
|
|
2226
|
+
*/
|
|
2227
|
+
function createTheme(hue, saturation, initialColors) {
|
|
2228
|
+
let colorDefs = initialColors ? { ...initialColors } : {};
|
|
2229
|
+
let cache = null;
|
|
2230
|
+
function resolveCached() {
|
|
2231
|
+
const version = getConfigVersion();
|
|
2232
|
+
if (cache && cache.version === version) return cache.map;
|
|
2233
|
+
const map = resolveAllColors(hue, saturation, colorDefs);
|
|
2234
|
+
cache = {
|
|
2235
|
+
map,
|
|
2236
|
+
version
|
|
2237
|
+
};
|
|
2238
|
+
return map;
|
|
2239
|
+
}
|
|
2240
|
+
function invalidate() {
|
|
2241
|
+
cache = null;
|
|
2242
|
+
}
|
|
2243
|
+
return {
|
|
2244
|
+
get hue() {
|
|
2245
|
+
return hue;
|
|
2246
|
+
},
|
|
2247
|
+
get saturation() {
|
|
2248
|
+
return saturation;
|
|
2249
|
+
},
|
|
2250
|
+
colors(defs) {
|
|
2251
|
+
colorDefs = {
|
|
2252
|
+
...colorDefs,
|
|
2253
|
+
...defs
|
|
2254
|
+
};
|
|
2255
|
+
invalidate();
|
|
2256
|
+
},
|
|
2257
|
+
color(name, def) {
|
|
2258
|
+
if (def === void 0) return colorDefs[name];
|
|
2259
|
+
colorDefs[name] = def;
|
|
2260
|
+
invalidate();
|
|
2261
|
+
},
|
|
2262
|
+
remove(names) {
|
|
2263
|
+
const list = Array.isArray(names) ? names : [names];
|
|
2264
|
+
for (const name of list) delete colorDefs[name];
|
|
2265
|
+
invalidate();
|
|
2266
|
+
},
|
|
2267
|
+
has(name) {
|
|
2268
|
+
return name in colorDefs;
|
|
2269
|
+
},
|
|
2270
|
+
list() {
|
|
2271
|
+
return Object.keys(colorDefs);
|
|
2272
|
+
},
|
|
2273
|
+
reset() {
|
|
2274
|
+
colorDefs = {};
|
|
2275
|
+
invalidate();
|
|
2276
|
+
},
|
|
2277
|
+
export() {
|
|
2278
|
+
return {
|
|
2279
|
+
hue,
|
|
2280
|
+
saturation,
|
|
2281
|
+
colors: { ...colorDefs }
|
|
2282
|
+
};
|
|
2283
|
+
},
|
|
2284
|
+
extend(options) {
|
|
2285
|
+
const newHue = options.hue ?? hue;
|
|
2286
|
+
const newSat = options.saturation ?? saturation;
|
|
2287
|
+
const inheritedColors = {};
|
|
2288
|
+
for (const [name, def] of Object.entries(colorDefs)) if (def.inherit !== false) inheritedColors[name] = def;
|
|
2289
|
+
return createTheme(newHue, newSat, options.colors ? {
|
|
2290
|
+
...inheritedColors,
|
|
2291
|
+
...options.colors
|
|
2292
|
+
} : { ...inheritedColors });
|
|
2293
|
+
},
|
|
2294
|
+
resolve() {
|
|
2295
|
+
return new Map(resolveCached());
|
|
2296
|
+
},
|
|
2297
|
+
tokens(options) {
|
|
2298
|
+
const modes = resolveModes(options?.modes);
|
|
2299
|
+
return buildFlatTokenMap(resolveCached(), "", modes, options?.format);
|
|
2300
|
+
},
|
|
2301
|
+
tasty(options) {
|
|
2302
|
+
const cfg = getConfig();
|
|
2303
|
+
const states = {
|
|
2304
|
+
dark: options?.states?.dark ?? cfg.states.dark,
|
|
2305
|
+
highContrast: options?.states?.highContrast ?? cfg.states.highContrast
|
|
2306
|
+
};
|
|
2307
|
+
const modes = resolveModes(options?.modes);
|
|
2308
|
+
return buildTokenMap(resolveCached(), "", states, modes, options?.format);
|
|
2309
|
+
},
|
|
2310
|
+
json(options) {
|
|
2311
|
+
const modes = resolveModes(options?.modes);
|
|
2312
|
+
return buildJsonMap(resolveCached(), modes, options?.format);
|
|
2313
|
+
},
|
|
2314
|
+
css(options) {
|
|
2315
|
+
return buildCssMap(resolveCached(), "", options?.suffix ?? "-color", options?.format ?? "rgb");
|
|
2316
|
+
}
|
|
2317
|
+
};
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
//#endregion
|
|
2321
|
+
//#region src/glaze.ts
|
|
2322
|
+
/**
|
|
2323
|
+
* Glaze — OKHSL-based color theme generator.
|
|
2324
|
+
*
|
|
2325
|
+
* Public API entry. Wires `glaze()` and its attached static methods to
|
|
2326
|
+
* the focused modules in this folder:
|
|
2327
|
+
* - `theme.ts` — single-theme factory
|
|
2328
|
+
* - `palette.ts` — multi-theme composition
|
|
2329
|
+
* - `color-token.ts` — standalone single-color tokens (`glaze.color`)
|
|
2330
|
+
* - `shadow.ts` — standalone shadow factory (`glaze.shadow`)
|
|
2331
|
+
* - `formatters.ts` — variant → string (`glaze.format`)
|
|
2332
|
+
* - `config.ts` — global config singleton
|
|
2333
|
+
*/
|
|
2159
2334
|
/**
|
|
2160
2335
|
* Create a single-hue glaze theme.
|
|
2161
2336
|
*
|
|
@@ -2170,41 +2345,18 @@ function glaze(hueOrOptions, saturation) {
|
|
|
2170
2345
|
if (typeof hueOrOptions === "number") return createTheme(hueOrOptions, saturation ?? 100);
|
|
2171
2346
|
return createTheme(hueOrOptions.hue, hueOrOptions.saturation);
|
|
2172
2347
|
}
|
|
2173
|
-
/**
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
glaze.configure = function configure(config) {
|
|
2177
|
-
globalConfig = {
|
|
2178
|
-
lightLightness: config.lightLightness ?? globalConfig.lightLightness,
|
|
2179
|
-
darkLightness: config.darkLightness ?? globalConfig.darkLightness,
|
|
2180
|
-
darkDesaturation: config.darkDesaturation ?? globalConfig.darkDesaturation,
|
|
2181
|
-
darkCurve: config.darkCurve ?? globalConfig.darkCurve,
|
|
2182
|
-
states: {
|
|
2183
|
-
dark: config.states?.dark ?? globalConfig.states.dark,
|
|
2184
|
-
highContrast: config.states?.highContrast ?? globalConfig.states.highContrast
|
|
2185
|
-
},
|
|
2186
|
-
modes: {
|
|
2187
|
-
dark: config.modes?.dark ?? globalConfig.modes.dark,
|
|
2188
|
-
highContrast: config.modes?.highContrast ?? globalConfig.modes.highContrast
|
|
2189
|
-
},
|
|
2190
|
-
shadowTuning: config.shadowTuning ?? globalConfig.shadowTuning
|
|
2191
|
-
};
|
|
2348
|
+
/** Configure global glaze settings. */
|
|
2349
|
+
glaze.configure = function configure$1(config) {
|
|
2350
|
+
configure(config);
|
|
2192
2351
|
};
|
|
2193
|
-
/**
|
|
2194
|
-
* Compose multiple themes into a palette.
|
|
2195
|
-
*/
|
|
2352
|
+
/** Compose multiple themes into a palette. */
|
|
2196
2353
|
glaze.palette = function palette(themes, options) {
|
|
2197
2354
|
return createPalette(themes, options);
|
|
2198
2355
|
};
|
|
2199
|
-
/**
|
|
2200
|
-
* Create a theme from a serialized export.
|
|
2201
|
-
*/
|
|
2356
|
+
/** Create a theme from a serialized export. */
|
|
2202
2357
|
glaze.from = function from(data) {
|
|
2203
2358
|
return createTheme(data.hue, data.saturation, data.colors);
|
|
2204
2359
|
};
|
|
2205
|
-
function isStructuredColorInput(input) {
|
|
2206
|
-
return typeof input === "object" && input !== null && !Array.isArray(input) && "hue" in input && "lightness" in input;
|
|
2207
|
-
}
|
|
2208
2360
|
/**
|
|
2209
2361
|
* Create a standalone single-color token.
|
|
2210
2362
|
*
|
|
@@ -2217,17 +2369,23 @@ function isStructuredColorInput(input) {
|
|
|
2217
2369
|
* emits (`rgb()`, `hsl()`, `okhsl()`, `oklch()`), an `OkhslColor`
|
|
2218
2370
|
* object `{ h, s, l }` (0–1 ranges), or an `[r, g, b]` (0–255) tuple.
|
|
2219
2371
|
*
|
|
2220
|
-
* Defaults
|
|
2221
|
-
*
|
|
2222
|
-
*
|
|
2223
|
-
*
|
|
2224
|
-
*
|
|
2225
|
-
*
|
|
2226
|
-
* -
|
|
2227
|
-
*
|
|
2228
|
-
*
|
|
2229
|
-
* -
|
|
2230
|
-
* `
|
|
2372
|
+
* Defaults: every input form defaults to `mode: 'auto'` so colors
|
|
2373
|
+
* automatically adapt between light and dark like an ordinary theme
|
|
2374
|
+
* color. The scaling snapshot taken at create time differs by input
|
|
2375
|
+
* form:
|
|
2376
|
+
* - String value-shorthand: `{ lightLightness: false, darkLightness:
|
|
2377
|
+
* [globalConfig.darkLightness[0], 100] }`. Light preserves the input
|
|
2378
|
+
* exactly; dark Möbius-inverts up to 100, so `glaze.color('#000')`
|
|
2379
|
+
* renders as `#fff` in dark mode (and `glaze.color('#fff')` falls to
|
|
2380
|
+
* the dark `lo` floor).
|
|
2381
|
+
* - `OkhslColor` object / RGB-tuple / structured value-shorthand:
|
|
2382
|
+
* `{ lightLightness: globalConfig.lightLightness, darkLightness:
|
|
2383
|
+
* globalConfig.darkLightness }` — both windows come straight from
|
|
2384
|
+
* `globalConfig`, so the resulting token behaves like a theme color.
|
|
2385
|
+
*
|
|
2386
|
+
* Pass `{ mode: 'fixed' }` to opt back into the legacy linear, non-
|
|
2387
|
+
* inverting mapping, or `{ mode: 'static' }` to pin the same lightness
|
|
2388
|
+
* across every variant.
|
|
2231
2389
|
*
|
|
2232
2390
|
* Relative `lightness: '+N'` and `contrast: <ratio>` are anchored to
|
|
2233
2391
|
* the literal seed (the value passed in) by default, pinned at
|
|
@@ -2262,9 +2420,7 @@ glaze.shadow = function shadow(input) {
|
|
|
2262
2420
|
alpha: 1
|
|
2263
2421
|
} : void 0, input.intensity, tuning);
|
|
2264
2422
|
};
|
|
2265
|
-
/**
|
|
2266
|
-
* Format a resolved color variant as a CSS string.
|
|
2267
|
-
*/
|
|
2423
|
+
/** Format a resolved color variant as a CSS string. */
|
|
2268
2424
|
glaze.format = function format(variant, colorFormat) {
|
|
2269
2425
|
return formatVariant(variant, colorFormat);
|
|
2270
2426
|
};
|
|
@@ -2310,30 +2466,13 @@ glaze.fromRgb = function fromRgb(r, g, b) {
|
|
|
2310
2466
|
glaze.colorFrom = function colorFrom(data) {
|
|
2311
2467
|
return colorFromExport(data);
|
|
2312
2468
|
};
|
|
2313
|
-
/**
|
|
2314
|
-
* Get the current global configuration (for testing/debugging).
|
|
2315
|
-
*/
|
|
2469
|
+
/** Get the current global configuration (for testing/debugging). */
|
|
2316
2470
|
glaze.getConfig = function getConfig() {
|
|
2317
|
-
return
|
|
2471
|
+
return snapshotConfig();
|
|
2318
2472
|
};
|
|
2319
|
-
/**
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
glaze.resetConfig = function resetConfig() {
|
|
2323
|
-
globalConfig = {
|
|
2324
|
-
lightLightness: [10, 100],
|
|
2325
|
-
darkLightness: [15, 95],
|
|
2326
|
-
darkDesaturation: .1,
|
|
2327
|
-
darkCurve: .5,
|
|
2328
|
-
states: {
|
|
2329
|
-
dark: "@dark",
|
|
2330
|
-
highContrast: "@high-contrast"
|
|
2331
|
-
},
|
|
2332
|
-
modes: {
|
|
2333
|
-
dark: true,
|
|
2334
|
-
highContrast: false
|
|
2335
|
-
}
|
|
2336
|
-
};
|
|
2473
|
+
/** Reset global configuration to defaults. */
|
|
2474
|
+
glaze.resetConfig = function resetConfig$1() {
|
|
2475
|
+
resetConfig();
|
|
2337
2476
|
};
|
|
2338
2477
|
|
|
2339
2478
|
//#endregion
|