@tenphi/glaze 0.0.0-snapshot.4e8eab7 → 0.0.0-snapshot.52e0dd6
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 -1390
- package/dist/index.cjs +739 -563
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +37 -8
- package/dist/index.d.mts +37 -8
- package/dist/index.mjs +739 -563
- package/dist/index.mjs.map +1 -1
- package/docs/api.md +1074 -0
- package/docs/methodology.md +336 -0
- package/docs/migration.md +237 -0
- package/package.json +3 -2
package/dist/index.cjs
CHANGED
|
@@ -560,6 +560,118 @@ 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
|
+
autoFlip: true
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
let globalConfig = defaultConfig();
|
|
587
|
+
/**
|
|
588
|
+
* Monotonic counter incremented on every `configure()` / `resetConfig()`
|
|
589
|
+
* call. Theme / palette caches read this to invalidate stale resolve
|
|
590
|
+
* results when the config changes between exports.
|
|
591
|
+
*/
|
|
592
|
+
let configVersion = 0;
|
|
593
|
+
/** Live reference to the current config. Mutated by `configure()` / `resetConfig()`. */
|
|
594
|
+
function getConfig() {
|
|
595
|
+
return globalConfig;
|
|
596
|
+
}
|
|
597
|
+
function getConfigVersion() {
|
|
598
|
+
return configVersion;
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Public-facing snapshot used by `glaze.getConfig()`. Returns a shallow
|
|
602
|
+
* copy so callers can't mutate the live config.
|
|
603
|
+
*/
|
|
604
|
+
function snapshotConfig() {
|
|
605
|
+
return { ...globalConfig };
|
|
606
|
+
}
|
|
607
|
+
function configure(config) {
|
|
608
|
+
configVersion++;
|
|
609
|
+
globalConfig = {
|
|
610
|
+
lightLightness: config.lightLightness ?? globalConfig.lightLightness,
|
|
611
|
+
darkLightness: config.darkLightness ?? globalConfig.darkLightness,
|
|
612
|
+
darkDesaturation: config.darkDesaturation ?? globalConfig.darkDesaturation,
|
|
613
|
+
darkCurve: config.darkCurve ?? globalConfig.darkCurve,
|
|
614
|
+
states: {
|
|
615
|
+
dark: config.states?.dark ?? globalConfig.states.dark,
|
|
616
|
+
highContrast: config.states?.highContrast ?? globalConfig.states.highContrast
|
|
617
|
+
},
|
|
618
|
+
modes: {
|
|
619
|
+
dark: config.modes?.dark ?? globalConfig.modes.dark,
|
|
620
|
+
highContrast: config.modes?.highContrast ?? globalConfig.modes.highContrast
|
|
621
|
+
},
|
|
622
|
+
shadowTuning: config.shadowTuning ?? globalConfig.shadowTuning,
|
|
623
|
+
autoFlip: config.autoFlip ?? globalConfig.autoFlip
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
function resetConfig() {
|
|
627
|
+
configVersion++;
|
|
628
|
+
globalConfig = defaultConfig();
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
//#endregion
|
|
632
|
+
//#region src/hc-pair.ts
|
|
633
|
+
function pairNormal(p) {
|
|
634
|
+
return Array.isArray(p) ? p[0] : p;
|
|
635
|
+
}
|
|
636
|
+
function pairHC(p) {
|
|
637
|
+
return Array.isArray(p) ? p[1] : p;
|
|
638
|
+
}
|
|
639
|
+
function clamp(v, min, max) {
|
|
640
|
+
return Math.max(min, Math.min(max, v));
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Parse a value that can be absolute (number) or relative (signed string).
|
|
644
|
+
* Returns the numeric value and whether it's relative.
|
|
645
|
+
*/
|
|
646
|
+
function parseRelativeOrAbsolute(value) {
|
|
647
|
+
if (typeof value === "number") return {
|
|
648
|
+
value,
|
|
649
|
+
relative: false
|
|
650
|
+
};
|
|
651
|
+
return {
|
|
652
|
+
value: parseFloat(value),
|
|
653
|
+
relative: true
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Compute the effective hue for a color, given the theme seed hue
|
|
658
|
+
* and an optional per-color hue override.
|
|
659
|
+
*/
|
|
660
|
+
function resolveEffectiveHue(seedHue, defHue) {
|
|
661
|
+
if (defHue === void 0) return seedHue;
|
|
662
|
+
const parsed = parseRelativeOrAbsolute(defHue);
|
|
663
|
+
if (parsed.relative) return ((seedHue + parsed.value) % 360 + 360) % 360;
|
|
664
|
+
return (parsed.value % 360 + 360) % 360;
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Check whether a lightness value represents an absolute root definition
|
|
668
|
+
* (i.e. a number, not a relative string).
|
|
669
|
+
*/
|
|
670
|
+
function isAbsoluteLightness(lightness) {
|
|
671
|
+
if (lightness === void 0) return false;
|
|
672
|
+
return typeof (Array.isArray(lightness) ? lightness[0] : lightness) === "number";
|
|
673
|
+
}
|
|
674
|
+
|
|
563
675
|
//#endregion
|
|
564
676
|
//#region src/contrast-solver.ts
|
|
565
677
|
/**
|
|
@@ -757,6 +869,25 @@ function findLightnessForContrast(options) {
|
|
|
757
869
|
branch: "preferred"
|
|
758
870
|
};
|
|
759
871
|
candidates.sort((a, b) => b.contrast - a.contrast);
|
|
872
|
+
if (candidates.length > 0 && options.flip) {
|
|
873
|
+
const best = candidates[0];
|
|
874
|
+
const center = (minL + maxL) / 2;
|
|
875
|
+
const flippedResult = findLightnessForContrast({
|
|
876
|
+
hue,
|
|
877
|
+
saturation,
|
|
878
|
+
preferredLightness: Math.max(minL, Math.min(maxL, 2 * center - best.lightness)),
|
|
879
|
+
baseLinearRgb,
|
|
880
|
+
contrast: contrastInput,
|
|
881
|
+
lightnessRange: [minL, maxL],
|
|
882
|
+
epsilon,
|
|
883
|
+
maxIterations
|
|
884
|
+
});
|
|
885
|
+
if (flippedResult.met) return {
|
|
886
|
+
...flippedResult,
|
|
887
|
+
branch: flippedResult.branch,
|
|
888
|
+
flipped: true
|
|
889
|
+
};
|
|
890
|
+
}
|
|
760
891
|
return candidates[0];
|
|
761
892
|
}
|
|
762
893
|
/**
|
|
@@ -829,7 +960,7 @@ function searchMixBranch(lo, hi, yBase, target, epsilon, maxIter, preferred, lum
|
|
|
829
960
|
* target against a base color, staying as close to `preferredValue` as possible.
|
|
830
961
|
*/
|
|
831
962
|
function findValueForMixContrast(options) {
|
|
832
|
-
const { preferredValue, baseLinearRgb, contrast: contrastInput, luminanceAtValue, epsilon = 1e-4, maxIterations = 20 } = options;
|
|
963
|
+
const { preferredValue, baseLinearRgb, targetLinearRgb, contrast: contrastInput, luminanceAtValue, epsilon = 1e-4, maxIterations = 20 } = options;
|
|
833
964
|
const target = resolveMinContrast(contrastInput);
|
|
834
965
|
const searchTarget = target * 1.01;
|
|
835
966
|
const yBase = gamutClampedLuminance(baseLinearRgb);
|
|
@@ -882,6 +1013,22 @@ function findValueForMixContrast(options) {
|
|
|
882
1013
|
met: false
|
|
883
1014
|
};
|
|
884
1015
|
candidates.sort((a, b) => b.contrast - a.contrast);
|
|
1016
|
+
if (candidates.length > 0 && options.flip) {
|
|
1017
|
+
const best = candidates[0];
|
|
1018
|
+
const flippedResult = findValueForMixContrast({
|
|
1019
|
+
preferredValue: Math.max(0, Math.min(1, 2 * .5 - best.lightness)),
|
|
1020
|
+
baseLinearRgb,
|
|
1021
|
+
targetLinearRgb,
|
|
1022
|
+
contrast: contrastInput,
|
|
1023
|
+
luminanceAtValue,
|
|
1024
|
+
epsilon,
|
|
1025
|
+
maxIterations
|
|
1026
|
+
});
|
|
1027
|
+
if (flippedResult.met) return {
|
|
1028
|
+
...flippedResult,
|
|
1029
|
+
flipped: true
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
885
1032
|
return {
|
|
886
1033
|
value: candidates[0].lightness,
|
|
887
1034
|
contrast: candidates[0].contrast,
|
|
@@ -890,116 +1037,15 @@ function findValueForMixContrast(options) {
|
|
|
890
1037
|
}
|
|
891
1038
|
|
|
892
1039
|
//#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";
|
|
1040
|
+
//#region src/shadow.ts
|
|
906
1041
|
/**
|
|
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).
|
|
1042
|
+
* Shadow color computation.
|
|
912
1043
|
*
|
|
913
|
-
*
|
|
914
|
-
*
|
|
915
|
-
*
|
|
916
|
-
*
|
|
917
|
-
* structured inputs snapshot both windows from `globalConfig` verbatim
|
|
918
|
-
* so they behave like an ordinary theme color (auto-adapted on both
|
|
919
|
-
* sides).
|
|
920
|
-
*/
|
|
921
|
-
function defaultStandaloneScaling(isString) {
|
|
922
|
-
if (isString) {
|
|
923
|
-
const [darkLo] = globalConfig.darkLightness;
|
|
924
|
-
return {
|
|
925
|
-
lightLightness: false,
|
|
926
|
-
darkLightness: [darkLo, 100]
|
|
927
|
-
};
|
|
928
|
-
}
|
|
929
|
-
return {
|
|
930
|
-
lightLightness: globalConfig.lightLightness,
|
|
931
|
-
darkLightness: globalConfig.darkLightness
|
|
932
|
-
};
|
|
933
|
-
}
|
|
934
|
-
/** Reserved internal names that user-supplied `name` must not collide with. */
|
|
935
|
-
const RESERVED_STANDALONE_NAMES = new Set([
|
|
936
|
-
STANDALONE_VALUE,
|
|
937
|
-
STANDALONE_SEED,
|
|
938
|
-
STANDALONE_BASE
|
|
939
|
-
]);
|
|
940
|
-
/**
|
|
941
|
-
* Discriminate a `GlazeColorToken` from a raw `GlazeColorValue`.
|
|
942
|
-
* Used to widen `base?` so it accepts either a token reference or a
|
|
943
|
-
* raw value (auto-wrapped into `glaze.color(value)`).
|
|
944
|
-
*/
|
|
945
|
-
function isGlazeColorToken(candidate) {
|
|
946
|
-
return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate) && "resolve" in candidate && typeof candidate.resolve === "function";
|
|
947
|
-
}
|
|
948
|
-
let globalConfig = {
|
|
949
|
-
lightLightness: [10, 100],
|
|
950
|
-
darkLightness: [15, 95],
|
|
951
|
-
darkDesaturation: .1,
|
|
952
|
-
darkCurve: .5,
|
|
953
|
-
states: {
|
|
954
|
-
dark: "@dark",
|
|
955
|
-
highContrast: "@high-contrast"
|
|
956
|
-
},
|
|
957
|
-
modes: {
|
|
958
|
-
dark: true,
|
|
959
|
-
highContrast: false
|
|
960
|
-
}
|
|
961
|
-
};
|
|
962
|
-
function pairNormal(p) {
|
|
963
|
-
return Array.isArray(p) ? p[0] : p;
|
|
964
|
-
}
|
|
965
|
-
function pairHC(p) {
|
|
966
|
-
return Array.isArray(p) ? p[1] : p;
|
|
967
|
-
}
|
|
968
|
-
/**
|
|
969
|
-
* Dedupe contrast warnings within a single process. The cache survives
|
|
970
|
-
* the lifetime of a token because tokens memoize their resolution; the
|
|
971
|
-
* limit is a soft cap to keep noise bounded across long-lived sessions
|
|
972
|
-
* (e.g. dev servers with HMR re-resolving themes repeatedly).
|
|
973
|
-
*/
|
|
974
|
-
const CONTRAST_WARN_CACHE_LIMIT = 256;
|
|
975
|
-
const contrastWarnCache = /* @__PURE__ */ new Set();
|
|
976
|
-
function schemeLabel(isDark, isHighContrast) {
|
|
977
|
-
if (isDark && isHighContrast) return "darkContrast";
|
|
978
|
-
if (isDark) return "dark";
|
|
979
|
-
if (isHighContrast) return "lightContrast";
|
|
980
|
-
return "light";
|
|
981
|
-
}
|
|
982
|
-
function formatContrastTarget(input, ratio) {
|
|
983
|
-
return typeof input === "string" ? `"${input}" (${ratio.toFixed(2)})` : ratio.toFixed(2);
|
|
984
|
-
}
|
|
985
|
-
/**
|
|
986
|
-
* Slack factor below the requested target before we emit a warning.
|
|
987
|
-
* The contrast solver already overshoots by `OVERSHOOT` (currently 1%)
|
|
988
|
-
* to absorb rounding noise (`see findLightnessForContrast` in
|
|
989
|
-
* `contrast-solver.ts`), so an `actual` ratio within ~2x that overshoot
|
|
990
|
-
* is effectively a pass and not worth nagging the user about.
|
|
1044
|
+
* Owns the shadow / mix def predicates, default tuning constants, the
|
|
1045
|
+
* tuning merge, and the actual `computeShadow` math (hue blend,
|
|
1046
|
+
* saturation cap, lightness clamp, alpha curve). The resolver consumes
|
|
1047
|
+
* this module per scheme variant.
|
|
991
1048
|
*/
|
|
992
|
-
const CONTRAST_WARN_SLACK = .98;
|
|
993
|
-
function warnContrastUnmet(name, isDark, isHighContrast, target, actual) {
|
|
994
|
-
const targetRatio = resolveMinContrast(target);
|
|
995
|
-
if (actual >= targetRatio * CONTRAST_WARN_SLACK) return;
|
|
996
|
-
const scheme = schemeLabel(isDark, isHighContrast);
|
|
997
|
-
const key = `${name}|${scheme}|${targetRatio.toFixed(3)}|${actual.toFixed(2)}`;
|
|
998
|
-
if (contrastWarnCache.has(key)) return;
|
|
999
|
-
if (contrastWarnCache.size >= CONTRAST_WARN_CACHE_LIMIT) contrastWarnCache.clear();
|
|
1000
|
-
contrastWarnCache.add(key);
|
|
1001
|
-
console.warn(`glaze: color "${name}" cannot meet contrast ${formatContrastTarget(target, targetRatio)} in ${scheme} scheme (got ${actual.toFixed(2)}). Try widening the lightness window, lowering the contrast target, or picking a base color further from this color's lightness.`);
|
|
1002
|
-
}
|
|
1003
1049
|
function isShadowDef(def) {
|
|
1004
1050
|
return def.type === "shadow";
|
|
1005
1051
|
}
|
|
@@ -1016,11 +1062,12 @@ const DEFAULT_SHADOW_TUNING = {
|
|
|
1016
1062
|
bgHueBlend: .2
|
|
1017
1063
|
};
|
|
1018
1064
|
function resolveShadowTuning(perColor) {
|
|
1065
|
+
const globalTuning = getConfig().shadowTuning;
|
|
1019
1066
|
return {
|
|
1020
1067
|
...DEFAULT_SHADOW_TUNING,
|
|
1021
|
-
...
|
|
1068
|
+
...globalTuning,
|
|
1022
1069
|
...perColor,
|
|
1023
|
-
lightnessBounds: perColor?.lightnessBounds ??
|
|
1070
|
+
lightnessBounds: perColor?.lightnessBounds ?? globalTuning?.lightnessBounds ?? DEFAULT_SHADOW_TUNING.lightnessBounds
|
|
1024
1071
|
};
|
|
1025
1072
|
}
|
|
1026
1073
|
function circularLerp(a, b, t) {
|
|
@@ -1061,6 +1108,80 @@ function computeShadow(bg, fg, intensity, tuning) {
|
|
|
1061
1108
|
alpha
|
|
1062
1109
|
};
|
|
1063
1110
|
}
|
|
1111
|
+
|
|
1112
|
+
//#endregion
|
|
1113
|
+
//#region src/scheme-mapping.ts
|
|
1114
|
+
/**
|
|
1115
|
+
* Light / dark scheme lightness mappings.
|
|
1116
|
+
*
|
|
1117
|
+
* Owns the active lightness window selection (with per-call scaling
|
|
1118
|
+
* overrides and high-contrast handling), the Möbius curve used by the
|
|
1119
|
+
* `'auto'` dark adaptation, and the saturation-desaturation reducer
|
|
1120
|
+
* for dark mode.
|
|
1121
|
+
*/
|
|
1122
|
+
/**
|
|
1123
|
+
* Resolve the active lightness window for a scheme.
|
|
1124
|
+
* - HC variants always return `[0, 100]` (existing behavior, predates per-call overrides).
|
|
1125
|
+
* - Otherwise, per-call `scaling` (e.g. from `glaze.color()`'s third arg) wins;
|
|
1126
|
+
* `false` is interpreted as `[0, 100]` (no remap). Falls back to `globalConfig.*Lightness`.
|
|
1127
|
+
*/
|
|
1128
|
+
function lightnessWindow(isHighContrast, kind, scaling) {
|
|
1129
|
+
if (isHighContrast) return [0, 100];
|
|
1130
|
+
if (scaling) {
|
|
1131
|
+
const override = kind === "dark" ? scaling.darkLightness : scaling.lightLightness;
|
|
1132
|
+
if (override === false) return [0, 100];
|
|
1133
|
+
if (override !== void 0) return override;
|
|
1134
|
+
}
|
|
1135
|
+
const cfg = getConfig();
|
|
1136
|
+
return kind === "dark" ? cfg.darkLightness : cfg.lightLightness;
|
|
1137
|
+
}
|
|
1138
|
+
function mapLightnessLight(l, mode, isHighContrast, scaling) {
|
|
1139
|
+
if (mode === "static") return l;
|
|
1140
|
+
const [lo, hi] = lightnessWindow(isHighContrast, "light", scaling);
|
|
1141
|
+
return l * (hi - lo) / 100 + lo;
|
|
1142
|
+
}
|
|
1143
|
+
function mobiusCurve(t, beta) {
|
|
1144
|
+
if (beta >= 1) return t;
|
|
1145
|
+
return t / (t + beta * (1 - t));
|
|
1146
|
+
}
|
|
1147
|
+
function mapLightnessDark(l, mode, isHighContrast, scaling) {
|
|
1148
|
+
if (mode === "static") return l;
|
|
1149
|
+
const cfg = getConfig();
|
|
1150
|
+
const beta = isHighContrast ? pairHC(cfg.darkCurve) : pairNormal(cfg.darkCurve);
|
|
1151
|
+
const [darkLo, darkHi] = lightnessWindow(isHighContrast, "dark", scaling);
|
|
1152
|
+
if (mode === "fixed") return l * (darkHi - darkLo) / 100 + darkLo;
|
|
1153
|
+
const [lightLo, lightHi] = lightnessWindow(isHighContrast, "light", scaling);
|
|
1154
|
+
const t = (lightHi - (l * (lightHi - lightLo) / 100 + lightLo)) / (lightHi - lightLo);
|
|
1155
|
+
return darkLo + (darkHi - darkLo) * mobiusCurve(t, beta);
|
|
1156
|
+
}
|
|
1157
|
+
function lightMappedToDark(lightL, isHighContrast, scaling) {
|
|
1158
|
+
const cfg = getConfig();
|
|
1159
|
+
const beta = isHighContrast ? pairHC(cfg.darkCurve) : pairNormal(cfg.darkCurve);
|
|
1160
|
+
const [lightLo, lightHi] = lightnessWindow(isHighContrast, "light", scaling);
|
|
1161
|
+
const [darkLo, darkHi] = lightnessWindow(isHighContrast, "dark", scaling);
|
|
1162
|
+
const t = (lightHi - clamp(lightL, lightLo, lightHi)) / (lightHi - lightLo);
|
|
1163
|
+
return darkLo + (darkHi - darkLo) * mobiusCurve(t, beta);
|
|
1164
|
+
}
|
|
1165
|
+
function mapSaturationDark(s, mode) {
|
|
1166
|
+
if (mode === "static") return s;
|
|
1167
|
+
return s * (1 - getConfig().darkDesaturation);
|
|
1168
|
+
}
|
|
1169
|
+
function schemeLightnessRange(isDark, mode, isHighContrast, scaling) {
|
|
1170
|
+
if (mode === "static") return [0, 1];
|
|
1171
|
+
const [lo, hi] = lightnessWindow(isHighContrast, isDark ? "dark" : "light", scaling);
|
|
1172
|
+
return [lo / 100, hi / 100];
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
//#endregion
|
|
1176
|
+
//#region src/validation.ts
|
|
1177
|
+
/**
|
|
1178
|
+
* Color graph validation and topological sort.
|
|
1179
|
+
*
|
|
1180
|
+
* `validateColorDefs` rejects bad references (missing / shadow-referencing /
|
|
1181
|
+
* base/contrast/lightness mismatches) and detects cycles before the
|
|
1182
|
+
* resolver runs. `topoSort` orders defs so each color is processed after
|
|
1183
|
+
* its base / bg / fg / target dependencies.
|
|
1184
|
+
*/
|
|
1064
1185
|
function validateColorDefs(defs, externalBases) {
|
|
1065
1186
|
const localNames = new Set(Object.keys(defs));
|
|
1066
1187
|
const allNames = new Set([...localNames, ...externalBases ? externalBases.keys() : []]);
|
|
@@ -1135,89 +1256,62 @@ function topoSort(defs) {
|
|
|
1135
1256
|
for (const name of Object.keys(defs)) visit(name);
|
|
1136
1257
|
return result;
|
|
1137
1258
|
}
|
|
1259
|
+
|
|
1260
|
+
//#endregion
|
|
1261
|
+
//#region src/warnings.ts
|
|
1138
1262
|
/**
|
|
1139
|
-
*
|
|
1140
|
-
*
|
|
1141
|
-
*
|
|
1142
|
-
*
|
|
1263
|
+
* Contrast-warning dispatcher.
|
|
1264
|
+
*
|
|
1265
|
+
* Tokens memoize their resolution, but a long-lived process (e.g. a dev
|
|
1266
|
+
* server with HMR) can re-resolve the same theme many times. The cache
|
|
1267
|
+
* here dedupes warnings within a session with a soft cap to keep noise
|
|
1268
|
+
* bounded.
|
|
1143
1269
|
*/
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
if (scaling) {
|
|
1147
|
-
const override = kind === "dark" ? scaling.darkLightness : scaling.lightLightness;
|
|
1148
|
-
if (override === false) return [0, 100];
|
|
1149
|
-
if (override !== void 0) return override;
|
|
1150
|
-
}
|
|
1151
|
-
return kind === "dark" ? globalConfig.darkLightness : globalConfig.lightLightness;
|
|
1152
|
-
}
|
|
1153
|
-
function mapLightnessLight(l, mode, isHighContrast, scaling) {
|
|
1154
|
-
if (mode === "static") return l;
|
|
1155
|
-
const [lo, hi] = lightnessWindow(isHighContrast, "light", scaling);
|
|
1156
|
-
return l * (hi - lo) / 100 + lo;
|
|
1157
|
-
}
|
|
1158
|
-
function mobiusCurve(t, beta) {
|
|
1159
|
-
if (beta >= 1) return t;
|
|
1160
|
-
return t / (t + beta * (1 - t));
|
|
1161
|
-
}
|
|
1162
|
-
function mapLightnessDark(l, mode, isHighContrast, scaling) {
|
|
1163
|
-
if (mode === "static") return l;
|
|
1164
|
-
const beta = isHighContrast ? pairHC(globalConfig.darkCurve) : pairNormal(globalConfig.darkCurve);
|
|
1165
|
-
const [darkLo, darkHi] = lightnessWindow(isHighContrast, "dark", scaling);
|
|
1166
|
-
if (mode === "fixed") return l * (darkHi - darkLo) / 100 + darkLo;
|
|
1167
|
-
const [lightLo, lightHi] = lightnessWindow(isHighContrast, "light", scaling);
|
|
1168
|
-
const t = (lightHi - (l * (lightHi - lightLo) / 100 + lightLo)) / (lightHi - lightLo);
|
|
1169
|
-
return darkLo + (darkHi - darkLo) * mobiusCurve(t, beta);
|
|
1170
|
-
}
|
|
1171
|
-
function lightMappedToDark(lightL, isHighContrast, scaling) {
|
|
1172
|
-
const beta = isHighContrast ? pairHC(globalConfig.darkCurve) : pairNormal(globalConfig.darkCurve);
|
|
1173
|
-
const [lightLo, lightHi] = lightnessWindow(isHighContrast, "light", scaling);
|
|
1174
|
-
const [darkLo, darkHi] = lightnessWindow(isHighContrast, "dark", scaling);
|
|
1175
|
-
const t = (lightHi - clamp(lightL, lightLo, lightHi)) / (lightHi - lightLo);
|
|
1176
|
-
return darkLo + (darkHi - darkLo) * mobiusCurve(t, beta);
|
|
1177
|
-
}
|
|
1178
|
-
function mapSaturationDark(s, mode) {
|
|
1179
|
-
if (mode === "static") return s;
|
|
1180
|
-
return s * (1 - globalConfig.darkDesaturation);
|
|
1181
|
-
}
|
|
1182
|
-
function schemeLightnessRange(isDark, mode, isHighContrast, scaling) {
|
|
1183
|
-
if (mode === "static") return [0, 1];
|
|
1184
|
-
const [lo, hi] = lightnessWindow(isHighContrast, isDark ? "dark" : "light", scaling);
|
|
1185
|
-
return [lo / 100, hi / 100];
|
|
1186
|
-
}
|
|
1187
|
-
function clamp(v, min, max) {
|
|
1188
|
-
return Math.max(min, Math.min(max, v));
|
|
1189
|
-
}
|
|
1270
|
+
const CONTRAST_WARN_CACHE_LIMIT = 256;
|
|
1271
|
+
const contrastWarnCache = /* @__PURE__ */ new Set();
|
|
1190
1272
|
/**
|
|
1191
|
-
*
|
|
1192
|
-
*
|
|
1273
|
+
* Slack factor below the requested target before we emit a warning.
|
|
1274
|
+
* The contrast solver already overshoots by `OVERSHOOT` (currently 1%)
|
|
1275
|
+
* to absorb rounding noise (`see findLightnessForContrast` in
|
|
1276
|
+
* `contrast-solver.ts`), so an `actual` ratio within ~2x that overshoot
|
|
1277
|
+
* is effectively a pass and not worth nagging the user about.
|
|
1193
1278
|
*/
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
return
|
|
1200
|
-
value: parseFloat(value),
|
|
1201
|
-
relative: true
|
|
1202
|
-
};
|
|
1279
|
+
const CONTRAST_WARN_SLACK = .98;
|
|
1280
|
+
function schemeLabel(isDark, isHighContrast) {
|
|
1281
|
+
if (isDark && isHighContrast) return "darkContrast";
|
|
1282
|
+
if (isDark) return "dark";
|
|
1283
|
+
if (isHighContrast) return "lightContrast";
|
|
1284
|
+
return "light";
|
|
1203
1285
|
}
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
if (
|
|
1210
|
-
const
|
|
1211
|
-
|
|
1212
|
-
|
|
1286
|
+
function formatContrastTarget(input, ratio) {
|
|
1287
|
+
return typeof input === "string" ? `"${input}" (${ratio.toFixed(2)})` : ratio.toFixed(2);
|
|
1288
|
+
}
|
|
1289
|
+
function warnContrastUnmet(name, isDark, isHighContrast, target, actual) {
|
|
1290
|
+
const targetRatio = resolveMinContrast(target);
|
|
1291
|
+
if (actual >= targetRatio * CONTRAST_WARN_SLACK) return;
|
|
1292
|
+
const scheme = schemeLabel(isDark, isHighContrast);
|
|
1293
|
+
const key = `${name}|${scheme}|${targetRatio.toFixed(3)}|${actual.toFixed(2)}`;
|
|
1294
|
+
if (contrastWarnCache.has(key)) return;
|
|
1295
|
+
if (contrastWarnCache.size >= CONTRAST_WARN_CACHE_LIMIT) contrastWarnCache.clear();
|
|
1296
|
+
contrastWarnCache.add(key);
|
|
1297
|
+
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.`);
|
|
1213
1298
|
}
|
|
1299
|
+
|
|
1300
|
+
//#endregion
|
|
1301
|
+
//#region src/resolver.ts
|
|
1214
1302
|
/**
|
|
1215
|
-
*
|
|
1216
|
-
*
|
|
1303
|
+
* Color resolution engine.
|
|
1304
|
+
*
|
|
1305
|
+
* Runs the four-pass solver (light → light-HC → dark → dark-HC) that
|
|
1306
|
+
* turns a `ColorMap` into a fully resolved `ResolvedColor` per name.
|
|
1307
|
+
* Owns the per-scheme resolve helpers for regular, shadow, and mix
|
|
1308
|
+
* color defs.
|
|
1217
1309
|
*/
|
|
1218
|
-
function
|
|
1219
|
-
if (
|
|
1220
|
-
|
|
1310
|
+
function getSchemeVariant(color, isDark, isHighContrast) {
|
|
1311
|
+
if (isDark && isHighContrast) return color.darkContrast;
|
|
1312
|
+
if (isDark) return color.dark;
|
|
1313
|
+
if (isHighContrast) return color.lightContrast;
|
|
1314
|
+
return color.light;
|
|
1221
1315
|
}
|
|
1222
1316
|
function resolveRootColor(_name, def, _ctx, isHighContrast) {
|
|
1223
1317
|
const rawL = def.lightness;
|
|
@@ -1252,13 +1346,15 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
|
|
|
1252
1346
|
const effectiveSat = isDark ? mapSaturationDark(satFactor * ctx.saturation / 100, mode) : satFactor * ctx.saturation / 100;
|
|
1253
1347
|
const baseLinearRgb = okhslToLinearSrgb(baseVariant.h, baseVariant.s, baseVariant.l);
|
|
1254
1348
|
const windowRange = schemeLightnessRange(isDark, mode, isHighContrast, ctx.scaling);
|
|
1349
|
+
const autoFlip = ctx.autoFlip ?? getConfig().autoFlip;
|
|
1255
1350
|
const result = findLightnessForContrast({
|
|
1256
1351
|
hue: effectiveHue,
|
|
1257
1352
|
saturation: effectiveSat,
|
|
1258
1353
|
preferredLightness: clamp(preferredL / 100, windowRange[0], windowRange[1]),
|
|
1259
1354
|
baseLinearRgb,
|
|
1260
1355
|
contrast: minCr,
|
|
1261
|
-
lightnessRange: [0, 1]
|
|
1356
|
+
lightnessRange: [0, 1],
|
|
1357
|
+
flip: autoFlip
|
|
1262
1358
|
});
|
|
1263
1359
|
if (!result.met) warnContrastUnmet(name, isDark, isHighContrast, minCr, result.contrast);
|
|
1264
1360
|
return {
|
|
@@ -1271,12 +1367,6 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
|
|
|
1271
1367
|
satFactor
|
|
1272
1368
|
};
|
|
1273
1369
|
}
|
|
1274
|
-
function getSchemeVariant(color, isDark, isHighContrast) {
|
|
1275
|
-
if (isDark && isHighContrast) return color.darkContrast;
|
|
1276
|
-
if (isDark) return color.dark;
|
|
1277
|
-
if (isHighContrast) return color.lightContrast;
|
|
1278
|
-
return color.light;
|
|
1279
|
-
}
|
|
1280
1370
|
function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
1281
1371
|
if (isShadowDef(def)) return resolveShadowForScheme(def, ctx, isDark, isHighContrast);
|
|
1282
1372
|
if (isMixDef(def)) return resolveMixForScheme(def, ctx, isDark, isHighContrast);
|
|
@@ -1380,12 +1470,14 @@ function resolveMixForScheme(def, ctx, isDark, isHighContrast) {
|
|
|
1380
1470
|
else luminanceAt = (v) => {
|
|
1381
1471
|
return gamutClampedLuminance(okhslToLinearSrgb(mixHue(baseVariant, targetVariant, v), baseVariant.s + (targetVariant.s - baseVariant.s) * v, baseVariant.l + (targetVariant.l - baseVariant.l) * v));
|
|
1382
1472
|
};
|
|
1473
|
+
const autoFlip = ctx.autoFlip ?? getConfig().autoFlip;
|
|
1383
1474
|
t = findValueForMixContrast({
|
|
1384
1475
|
preferredValue: t,
|
|
1385
1476
|
baseLinearRgb: baseLinear,
|
|
1386
1477
|
targetLinearRgb: targetLinear,
|
|
1387
1478
|
contrast: minCr,
|
|
1388
|
-
luminanceAtValue: luminanceAt
|
|
1479
|
+
luminanceAtValue: luminanceAt,
|
|
1480
|
+
flip: autoFlip
|
|
1389
1481
|
}).value;
|
|
1390
1482
|
}
|
|
1391
1483
|
if (blend === "transparent") return {
|
|
@@ -1402,26 +1494,27 @@ function resolveMixForScheme(def, ctx, isDark, isHighContrast) {
|
|
|
1402
1494
|
alpha: 1
|
|
1403
1495
|
};
|
|
1404
1496
|
}
|
|
1405
|
-
function
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
if (isShadowDef(def) || isMixDef(def)) return void 0;
|
|
1418
|
-
return def.mode ?? "auto";
|
|
1419
|
-
}
|
|
1420
|
-
const lightMap = /* @__PURE__ */ new Map();
|
|
1497
|
+
function defMode(def) {
|
|
1498
|
+
if (isShadowDef(def) || isMixDef(def)) return void 0;
|
|
1499
|
+
return def.mode ?? "auto";
|
|
1500
|
+
}
|
|
1501
|
+
/**
|
|
1502
|
+
* Run a single resolve pass over all local names. Pass 1 lazily creates
|
|
1503
|
+
* each `ResolvedColor` (all four slots seeded with the just-resolved
|
|
1504
|
+
* variant) the first time it sees a name; later passes update the
|
|
1505
|
+
* `target` slot on the existing record.
|
|
1506
|
+
*/
|
|
1507
|
+
function runPass(order, defs, ctx, isDark, isHighContrast, target) {
|
|
1508
|
+
const out = /* @__PURE__ */ new Map();
|
|
1421
1509
|
for (const name of order) {
|
|
1422
|
-
const variant = resolveColorForScheme(name, defs[name], ctx,
|
|
1423
|
-
|
|
1424
|
-
ctx.resolved.
|
|
1510
|
+
const variant = resolveColorForScheme(name, defs[name], ctx, isDark, isHighContrast);
|
|
1511
|
+
out.set(name, variant);
|
|
1512
|
+
const existing = ctx.resolved.get(name);
|
|
1513
|
+
if (existing) ctx.resolved.set(name, {
|
|
1514
|
+
...existing,
|
|
1515
|
+
[target]: variant
|
|
1516
|
+
});
|
|
1517
|
+
else ctx.resolved.set(name, {
|
|
1425
1518
|
name,
|
|
1426
1519
|
light: variant,
|
|
1427
1520
|
dark: variant,
|
|
@@ -1430,49 +1523,42 @@ function resolveAllColors(hue, saturation, defs, scaling, externalBases) {
|
|
|
1430
1523
|
mode: defMode(defs[name])
|
|
1431
1524
|
});
|
|
1432
1525
|
}
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
lightHCMap.set(name, variant);
|
|
1441
|
-
ctx.resolved.set(name, {
|
|
1442
|
-
...ctx.resolved.get(name),
|
|
1443
|
-
lightContrast: variant
|
|
1444
|
-
});
|
|
1445
|
-
}
|
|
1446
|
-
const darkMap = /* @__PURE__ */ new Map();
|
|
1447
|
-
for (const name of order) ctx.resolved.set(name, {
|
|
1448
|
-
name,
|
|
1449
|
-
light: lightMap.get(name),
|
|
1450
|
-
dark: lightMap.get(name),
|
|
1451
|
-
lightContrast: lightHCMap.get(name),
|
|
1452
|
-
darkContrast: lightHCMap.get(name),
|
|
1453
|
-
mode: defMode(defs[name])
|
|
1454
|
-
});
|
|
1455
|
-
for (const name of order) {
|
|
1456
|
-
const variant = resolveColorForScheme(name, defs[name], ctx, true, false);
|
|
1457
|
-
darkMap.set(name, variant);
|
|
1458
|
-
ctx.resolved.set(name, {
|
|
1459
|
-
...ctx.resolved.get(name),
|
|
1460
|
-
dark: variant
|
|
1461
|
-
});
|
|
1462
|
-
}
|
|
1463
|
-
const darkHCMap = /* @__PURE__ */ new Map();
|
|
1464
|
-
for (const name of order) ctx.resolved.set(name, {
|
|
1465
|
-
...ctx.resolved.get(name),
|
|
1466
|
-
darkContrast: darkMap.get(name)
|
|
1467
|
-
});
|
|
1526
|
+
return out;
|
|
1527
|
+
}
|
|
1528
|
+
/**
|
|
1529
|
+
* Re-seed a single variant slot with a previously-resolved map so the
|
|
1530
|
+
* upcoming pass reads sensible fallbacks via `getSchemeVariant`.
|
|
1531
|
+
*/
|
|
1532
|
+
function seedField(order, ctx, field, source) {
|
|
1468
1533
|
for (const name of order) {
|
|
1469
|
-
const
|
|
1470
|
-
darkHCMap.set(name, variant);
|
|
1534
|
+
const existing = ctx.resolved.get(name);
|
|
1471
1535
|
ctx.resolved.set(name, {
|
|
1472
|
-
...
|
|
1473
|
-
|
|
1536
|
+
...existing,
|
|
1537
|
+
[field]: source.get(name)
|
|
1474
1538
|
});
|
|
1475
1539
|
}
|
|
1540
|
+
}
|
|
1541
|
+
function resolveAllColors(hue, saturation, defs, scaling, externalBases, overrideAutoFlip) {
|
|
1542
|
+
validateColorDefs(defs, externalBases);
|
|
1543
|
+
const order = topoSort(defs);
|
|
1544
|
+
const cfg = getConfig();
|
|
1545
|
+
const ctx = {
|
|
1546
|
+
hue,
|
|
1547
|
+
saturation,
|
|
1548
|
+
defs,
|
|
1549
|
+
resolved: /* @__PURE__ */ new Map(),
|
|
1550
|
+
scaling,
|
|
1551
|
+
autoFlip: overrideAutoFlip ?? cfg.autoFlip
|
|
1552
|
+
};
|
|
1553
|
+
if (externalBases) for (const [name, color] of externalBases) ctx.resolved.set(name, color);
|
|
1554
|
+
const lightMap = runPass(order, defs, ctx, false, false, "light");
|
|
1555
|
+
seedField(order, ctx, "lightContrast", lightMap);
|
|
1556
|
+
const lightHCMap = runPass(order, defs, ctx, false, true, "lightContrast");
|
|
1557
|
+
seedField(order, ctx, "dark", lightMap);
|
|
1558
|
+
seedField(order, ctx, "darkContrast", lightHCMap);
|
|
1559
|
+
const darkMap = runPass(order, defs, ctx, true, false, "dark");
|
|
1560
|
+
seedField(order, ctx, "darkContrast", darkMap);
|
|
1561
|
+
const darkHCMap = runPass(order, defs, ctx, true, true, "darkContrast");
|
|
1476
1562
|
const result = /* @__PURE__ */ new Map();
|
|
1477
1563
|
for (const name of order) result.set(name, {
|
|
1478
1564
|
name,
|
|
@@ -1484,6 +1570,19 @@ function resolveAllColors(hue, saturation, defs, scaling, externalBases) {
|
|
|
1484
1570
|
});
|
|
1485
1571
|
return result;
|
|
1486
1572
|
}
|
|
1573
|
+
|
|
1574
|
+
//#endregion
|
|
1575
|
+
//#region src/formatters.ts
|
|
1576
|
+
/**
|
|
1577
|
+
* Output formatting for resolved color maps.
|
|
1578
|
+
*
|
|
1579
|
+
* Owns the CSS-string formatter dispatch table (`okhsl` / `rgb` / `hsl` /
|
|
1580
|
+
* `oklch`) and the four token-map shapes Glaze emits:
|
|
1581
|
+
* - `buildTokenMap` — Tasty style-to-state bindings (`#name` keys, state aliases).
|
|
1582
|
+
* - `buildFlatTokenMap` — `{ light, dark, ... }` per-variant maps.
|
|
1583
|
+
* - `buildJsonMap` — `{ name: { light, dark, ... } }` per-color JSON.
|
|
1584
|
+
* - `buildCssMap` — CSS custom property declaration strings per variant.
|
|
1585
|
+
*/
|
|
1487
1586
|
const formatters = {
|
|
1488
1587
|
okhsl: formatOkhsl,
|
|
1489
1588
|
rgb: formatRgb,
|
|
@@ -1500,9 +1599,10 @@ function formatVariant(v, format = "okhsl") {
|
|
|
1500
1599
|
return `${base.slice(0, closing)} / ${fmt(v.alpha, 4)})`;
|
|
1501
1600
|
}
|
|
1502
1601
|
function resolveModes(override) {
|
|
1602
|
+
const cfg = getConfig();
|
|
1503
1603
|
return {
|
|
1504
|
-
dark: override?.dark ??
|
|
1505
|
-
highContrast: override?.highContrast ??
|
|
1604
|
+
dark: override?.dark ?? cfg.modes.dark,
|
|
1605
|
+
highContrast: override?.highContrast ?? cfg.modes.highContrast
|
|
1506
1606
|
};
|
|
1507
1607
|
}
|
|
1508
1608
|
function buildTokenMap(resolved, prefix, states, modes, format = "okhsl") {
|
|
@@ -1563,206 +1663,74 @@ function buildCssMap(resolved, prefix, suffix, format) {
|
|
|
1563
1663
|
darkContrast: lines.darkContrast.join("\n")
|
|
1564
1664
|
};
|
|
1565
1665
|
}
|
|
1566
|
-
|
|
1567
|
-
|
|
1666
|
+
|
|
1667
|
+
//#endregion
|
|
1668
|
+
//#region src/color-token.ts
|
|
1669
|
+
/**
|
|
1670
|
+
* Standalone single-color tokens (`glaze.color()` / `glaze.colorFrom()`).
|
|
1671
|
+
*
|
|
1672
|
+
* Owns the value-shorthand parser (hex, `rgb()` / `hsl()` / `okhsl()` /
|
|
1673
|
+
* `oklch()`, OkhslColor object, [r, g, b] tuple), the structured-input
|
|
1674
|
+
* validator, the two factory paths (value vs structured), and the
|
|
1675
|
+
* JSON-safe export / rehydration round-trip.
|
|
1676
|
+
*
|
|
1677
|
+
* Standalone tokens snapshot the relevant `globalConfig` fields at
|
|
1678
|
+
* create time so later `configure()` calls do not retroactively change
|
|
1679
|
+
* exported tokens — the snapshot is captured eagerly in
|
|
1680
|
+
* `defaultStandaloneScaling()`. The token's resolved variants are then
|
|
1681
|
+
* memoized on first `.resolve()` / `.token()` / ... call.
|
|
1682
|
+
*/
|
|
1683
|
+
/** Internal name of the user-facing standalone color in the synthesized def map. */
|
|
1684
|
+
const STANDALONE_VALUE = "value";
|
|
1685
|
+
/** Internal name of the hidden static-anchor seed used for relative lightness / contrast. */
|
|
1686
|
+
const STANDALONE_SEED = "seed";
|
|
1687
|
+
/** Internal name of an externally-resolved `GlazeColorToken` injected as a base reference. */
|
|
1688
|
+
const STANDALONE_BASE = "externalBase";
|
|
1689
|
+
/** Reserved internal names that user-supplied `name` must not collide with. */
|
|
1690
|
+
const RESERVED_STANDALONE_NAMES = new Set([
|
|
1691
|
+
STANDALONE_VALUE,
|
|
1692
|
+
STANDALONE_SEED,
|
|
1693
|
+
STANDALONE_BASE
|
|
1694
|
+
]);
|
|
1695
|
+
/**
|
|
1696
|
+
* Build the create-time scaling snapshot used when the caller did not
|
|
1697
|
+
* pass an explicit `scaling`. All windows are snapshotted from the
|
|
1698
|
+
* current `globalConfig` so later `glaze.configure()` calls don't
|
|
1699
|
+
* retroactively change the resolved variants of an already-created
|
|
1700
|
+
* token (matches the documented "frozen at create time" semantics).
|
|
1701
|
+
*
|
|
1702
|
+
* String value-shorthand inputs preserve their light lightness exactly
|
|
1703
|
+
* (`lightLightness: false`) and use an extended dark window
|
|
1704
|
+
* `[globalConfig.darkLightness[0], 100]` so a totally-black input can
|
|
1705
|
+
* Möbius-invert to totally-white in dark mode. Object / tuple /
|
|
1706
|
+
* structured inputs snapshot both windows from `globalConfig` verbatim
|
|
1707
|
+
* so they behave like an ordinary theme color (auto-adapted on both
|
|
1708
|
+
* sides).
|
|
1709
|
+
*/
|
|
1710
|
+
function defaultStandaloneScaling(isString) {
|
|
1711
|
+
const cfg = getConfig();
|
|
1712
|
+
if (isString) {
|
|
1713
|
+
const [darkLo] = cfg.darkLightness;
|
|
1714
|
+
return {
|
|
1715
|
+
lightLightness: false,
|
|
1716
|
+
darkLightness: [darkLo, 100]
|
|
1717
|
+
};
|
|
1718
|
+
}
|
|
1568
1719
|
return {
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
},
|
|
1572
|
-
get saturation() {
|
|
1573
|
-
return saturation;
|
|
1574
|
-
},
|
|
1575
|
-
colors(defs) {
|
|
1576
|
-
colorDefs = {
|
|
1577
|
-
...colorDefs,
|
|
1578
|
-
...defs
|
|
1579
|
-
};
|
|
1580
|
-
},
|
|
1581
|
-
color(name, def) {
|
|
1582
|
-
if (def === void 0) return colorDefs[name];
|
|
1583
|
-
colorDefs[name] = def;
|
|
1584
|
-
},
|
|
1585
|
-
remove(names) {
|
|
1586
|
-
const list = Array.isArray(names) ? names : [names];
|
|
1587
|
-
for (const name of list) delete colorDefs[name];
|
|
1588
|
-
},
|
|
1589
|
-
has(name) {
|
|
1590
|
-
return name in colorDefs;
|
|
1591
|
-
},
|
|
1592
|
-
list() {
|
|
1593
|
-
return Object.keys(colorDefs);
|
|
1594
|
-
},
|
|
1595
|
-
reset() {
|
|
1596
|
-
colorDefs = {};
|
|
1597
|
-
},
|
|
1598
|
-
export() {
|
|
1599
|
-
return {
|
|
1600
|
-
hue,
|
|
1601
|
-
saturation,
|
|
1602
|
-
colors: { ...colorDefs }
|
|
1603
|
-
};
|
|
1604
|
-
},
|
|
1605
|
-
extend(options) {
|
|
1606
|
-
const newHue = options.hue ?? hue;
|
|
1607
|
-
const newSat = options.saturation ?? saturation;
|
|
1608
|
-
const inheritedColors = {};
|
|
1609
|
-
for (const [name, def] of Object.entries(colorDefs)) if (def.inherit !== false) inheritedColors[name] = def;
|
|
1610
|
-
return createTheme(newHue, newSat, options.colors ? {
|
|
1611
|
-
...inheritedColors,
|
|
1612
|
-
...options.colors
|
|
1613
|
-
} : { ...inheritedColors });
|
|
1614
|
-
},
|
|
1615
|
-
resolve() {
|
|
1616
|
-
return resolveAllColors(hue, saturation, colorDefs);
|
|
1617
|
-
},
|
|
1618
|
-
tokens(options) {
|
|
1619
|
-
return buildFlatTokenMap(resolveAllColors(hue, saturation, colorDefs), "", resolveModes(options?.modes), options?.format);
|
|
1620
|
-
},
|
|
1621
|
-
tasty(options) {
|
|
1622
|
-
return buildTokenMap(resolveAllColors(hue, saturation, colorDefs), "", {
|
|
1623
|
-
dark: options?.states?.dark ?? globalConfig.states.dark,
|
|
1624
|
-
highContrast: options?.states?.highContrast ?? globalConfig.states.highContrast
|
|
1625
|
-
}, resolveModes(options?.modes), options?.format);
|
|
1626
|
-
},
|
|
1627
|
-
json(options) {
|
|
1628
|
-
return buildJsonMap(resolveAllColors(hue, saturation, colorDefs), resolveModes(options?.modes), options?.format);
|
|
1629
|
-
},
|
|
1630
|
-
css(options) {
|
|
1631
|
-
return buildCssMap(resolveAllColors(hue, saturation, colorDefs), "", options?.suffix ?? "-color", options?.format ?? "rgb");
|
|
1632
|
-
}
|
|
1720
|
+
lightLightness: cfg.lightLightness,
|
|
1721
|
+
darkLightness: cfg.darkLightness
|
|
1633
1722
|
};
|
|
1634
1723
|
}
|
|
1635
|
-
function resolvePrefix(options, themeName, defaultPrefix = false) {
|
|
1636
|
-
const prefix = options?.prefix ?? defaultPrefix;
|
|
1637
|
-
if (prefix === true) return `${themeName}-`;
|
|
1638
|
-
if (typeof prefix === "object" && prefix !== null) return prefix[themeName] ?? `${themeName}-`;
|
|
1639
|
-
return "";
|
|
1640
|
-
}
|
|
1641
|
-
function validatePrimaryTheme(primary, themes) {
|
|
1642
|
-
if (primary !== void 0 && !(primary in themes)) {
|
|
1643
|
-
const available = Object.keys(themes).join(", ");
|
|
1644
|
-
throw new Error(`glaze: primary theme "${primary}" not found in palette. Available: ${available}.`);
|
|
1645
|
-
}
|
|
1646
|
-
}
|
|
1647
1724
|
/**
|
|
1648
|
-
*
|
|
1649
|
-
* `
|
|
1725
|
+
* Discriminate a `GlazeColorToken` from a raw `GlazeColorValue`.
|
|
1726
|
+
* Used to widen `base?` so it accepts either a token reference or a
|
|
1727
|
+
* raw value (auto-wrapped into `glaze.color(value)`).
|
|
1650
1728
|
*/
|
|
1651
|
-
function
|
|
1652
|
-
|
|
1653
|
-
return exportPrimary ?? palettePrimary;
|
|
1654
|
-
}
|
|
1655
|
-
/**
|
|
1656
|
-
* Filter a resolved color map, skipping keys already in `seen`.
|
|
1657
|
-
* Warns on collision and keeps the first-written value (first-write-wins).
|
|
1658
|
-
* Returns a new map containing only non-colliding entries.
|
|
1659
|
-
*/
|
|
1660
|
-
function filterCollisions(resolved, prefix, seen, themeName, isPrimary) {
|
|
1661
|
-
const filtered = /* @__PURE__ */ new Map();
|
|
1662
|
-
const label = isPrimary ? `${themeName} (primary)` : themeName;
|
|
1663
|
-
for (const [name, color] of resolved) {
|
|
1664
|
-
const key = `${prefix}${name}`;
|
|
1665
|
-
if (seen.has(key)) {
|
|
1666
|
-
console.warn(`glaze: token "${key}" from theme "${label}" collides with theme "${seen.get(key)}" — skipping.`);
|
|
1667
|
-
continue;
|
|
1668
|
-
}
|
|
1669
|
-
seen.set(key, label);
|
|
1670
|
-
filtered.set(name, color);
|
|
1671
|
-
}
|
|
1672
|
-
return filtered;
|
|
1729
|
+
function isGlazeColorToken(candidate) {
|
|
1730
|
+
return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate) && "resolve" in candidate && typeof candidate.resolve === "function";
|
|
1673
1731
|
}
|
|
1674
|
-
function
|
|
1675
|
-
|
|
1676
|
-
return {
|
|
1677
|
-
tokens(options) {
|
|
1678
|
-
const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
|
|
1679
|
-
if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
|
|
1680
|
-
const modes = resolveModes(options?.modes);
|
|
1681
|
-
const allTokens = {};
|
|
1682
|
-
const seen = /* @__PURE__ */ new Map();
|
|
1683
|
-
for (const [themeName, theme] of Object.entries(themes)) {
|
|
1684
|
-
const resolved = theme.resolve();
|
|
1685
|
-
const prefix = resolvePrefix(options, themeName, true);
|
|
1686
|
-
const tokens = buildFlatTokenMap(filterCollisions(resolved, prefix, seen, themeName), prefix, modes, options?.format);
|
|
1687
|
-
for (const variant of Object.keys(tokens)) {
|
|
1688
|
-
if (!allTokens[variant]) allTokens[variant] = {};
|
|
1689
|
-
Object.assign(allTokens[variant], tokens[variant]);
|
|
1690
|
-
}
|
|
1691
|
-
if (themeName === effectivePrimary) {
|
|
1692
|
-
const unprefixed = buildFlatTokenMap(filterCollisions(resolved, "", seen, themeName, true), "", modes, options?.format);
|
|
1693
|
-
for (const variant of Object.keys(unprefixed)) Object.assign(allTokens[variant], unprefixed[variant]);
|
|
1694
|
-
}
|
|
1695
|
-
}
|
|
1696
|
-
return allTokens;
|
|
1697
|
-
},
|
|
1698
|
-
tasty(options) {
|
|
1699
|
-
const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
|
|
1700
|
-
if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
|
|
1701
|
-
const states = {
|
|
1702
|
-
dark: options?.states?.dark ?? globalConfig.states.dark,
|
|
1703
|
-
highContrast: options?.states?.highContrast ?? globalConfig.states.highContrast
|
|
1704
|
-
};
|
|
1705
|
-
const modes = resolveModes(options?.modes);
|
|
1706
|
-
const allTokens = {};
|
|
1707
|
-
const seen = /* @__PURE__ */ new Map();
|
|
1708
|
-
for (const [themeName, theme] of Object.entries(themes)) {
|
|
1709
|
-
const resolved = theme.resolve();
|
|
1710
|
-
const prefix = resolvePrefix(options, themeName, true);
|
|
1711
|
-
const tokens = buildTokenMap(filterCollisions(resolved, prefix, seen, themeName), prefix, states, modes, options?.format);
|
|
1712
|
-
Object.assign(allTokens, tokens);
|
|
1713
|
-
if (themeName === effectivePrimary) {
|
|
1714
|
-
const unprefixed = buildTokenMap(filterCollisions(resolved, "", seen, themeName, true), "", states, modes, options?.format);
|
|
1715
|
-
Object.assign(allTokens, unprefixed);
|
|
1716
|
-
}
|
|
1717
|
-
}
|
|
1718
|
-
return allTokens;
|
|
1719
|
-
},
|
|
1720
|
-
json(options) {
|
|
1721
|
-
const modes = resolveModes(options?.modes);
|
|
1722
|
-
const result = {};
|
|
1723
|
-
for (const [themeName, theme] of Object.entries(themes)) result[themeName] = buildJsonMap(theme.resolve(), modes, options?.format);
|
|
1724
|
-
return result;
|
|
1725
|
-
},
|
|
1726
|
-
css(options) {
|
|
1727
|
-
const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
|
|
1728
|
-
if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
|
|
1729
|
-
const suffix = options?.suffix ?? "-color";
|
|
1730
|
-
const format = options?.format ?? "rgb";
|
|
1731
|
-
const allLines = {
|
|
1732
|
-
light: [],
|
|
1733
|
-
dark: [],
|
|
1734
|
-
lightContrast: [],
|
|
1735
|
-
darkContrast: []
|
|
1736
|
-
};
|
|
1737
|
-
const seen = /* @__PURE__ */ new Map();
|
|
1738
|
-
for (const [themeName, theme] of Object.entries(themes)) {
|
|
1739
|
-
const resolved = theme.resolve();
|
|
1740
|
-
const prefix = resolvePrefix(options, themeName, true);
|
|
1741
|
-
const css = buildCssMap(filterCollisions(resolved, prefix, seen, themeName), prefix, suffix, format);
|
|
1742
|
-
for (const key of [
|
|
1743
|
-
"light",
|
|
1744
|
-
"dark",
|
|
1745
|
-
"lightContrast",
|
|
1746
|
-
"darkContrast"
|
|
1747
|
-
]) if (css[key]) allLines[key].push(css[key]);
|
|
1748
|
-
if (themeName === effectivePrimary) {
|
|
1749
|
-
const unprefixed = buildCssMap(filterCollisions(resolved, "", seen, themeName, true), "", suffix, format);
|
|
1750
|
-
for (const key of [
|
|
1751
|
-
"light",
|
|
1752
|
-
"dark",
|
|
1753
|
-
"lightContrast",
|
|
1754
|
-
"darkContrast"
|
|
1755
|
-
]) if (unprefixed[key]) allLines[key].push(unprefixed[key]);
|
|
1756
|
-
}
|
|
1757
|
-
}
|
|
1758
|
-
return {
|
|
1759
|
-
light: allLines.light.join("\n"),
|
|
1760
|
-
dark: allLines.dark.join("\n"),
|
|
1761
|
-
lightContrast: allLines.lightContrast.join("\n"),
|
|
1762
|
-
darkContrast: allLines.darkContrast.join("\n")
|
|
1763
|
-
};
|
|
1764
|
-
}
|
|
1765
|
-
};
|
|
1732
|
+
function isStructuredColorInput(input) {
|
|
1733
|
+
return typeof input === "object" && input !== null && !Array.isArray(input) && "hue" in input && "lightness" in input;
|
|
1766
1734
|
}
|
|
1767
1735
|
/**
|
|
1768
1736
|
* Matches the CSS color functions Glaze itself emits (`rgb()`, `hsl()`,
|
|
@@ -1881,9 +1849,7 @@ function validateOkhslColor(value) {
|
|
|
1881
1849
|
if (!Number.isFinite(h) || !Number.isFinite(s) || !Number.isFinite(l)) throw new Error("glaze.color: OkhslColor h/s/l must be finite numbers.");
|
|
1882
1850
|
if (s > 1.5 || l > 1.5) throw new Error("glaze.color: OkhslColor s/l must be in 0–1 range. Did you mean the structured form { hue, saturation, lightness } (which uses 0–100)?");
|
|
1883
1851
|
}
|
|
1884
|
-
/**
|
|
1885
|
-
* Validate a user-supplied `[r, g, b]` tuple in 0-255.
|
|
1886
|
-
*/
|
|
1852
|
+
/** Validate a user-supplied `[r, g, b]` tuple in 0-255. */
|
|
1887
1853
|
function validateRgbTuple(value) {
|
|
1888
1854
|
for (const n of value) if (!Number.isFinite(n) || n < 0 || n > 255) throw new Error(`glaze.color: RGB tuple components must be finite numbers in 0–255 (got [${value.join(", ")}]).`);
|
|
1889
1855
|
}
|
|
@@ -2002,17 +1968,20 @@ function buildStandaloneValueDefs(main, options) {
|
|
|
2002
1968
|
primary
|
|
2003
1969
|
};
|
|
2004
1970
|
}
|
|
2005
|
-
function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effectiveScaling, baseToken, exportData) {
|
|
1971
|
+
function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effectiveScaling, baseToken, exportData, autoFlip) {
|
|
2006
1972
|
let cached;
|
|
2007
1973
|
const resolveOnce = () => {
|
|
2008
1974
|
if (cached) return cached;
|
|
2009
|
-
cached = resolveAllColors(seedHue, seedSaturation, defs, effectiveScaling, baseToken ? new Map([[STANDALONE_BASE, baseToken.resolve()]]) : void 0);
|
|
1975
|
+
cached = resolveAllColors(seedHue, seedSaturation, defs, effectiveScaling, baseToken ? new Map([[STANDALONE_BASE, baseToken.resolve()]]) : void 0, autoFlip);
|
|
2010
1976
|
return cached;
|
|
2011
1977
|
};
|
|
2012
|
-
const resolveStates = (options) =>
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
1978
|
+
const resolveStates = (options) => {
|
|
1979
|
+
const cfg = getConfig();
|
|
1980
|
+
return {
|
|
1981
|
+
dark: options?.states?.dark ?? cfg.states.dark,
|
|
1982
|
+
highContrast: options?.states?.highContrast ?? cfg.states.highContrast
|
|
1983
|
+
};
|
|
1984
|
+
};
|
|
2016
1985
|
const tokenLike = (options) => {
|
|
2017
1986
|
return buildTokenMap(resolveOnce(), "", resolveStates(options), resolveModes(options?.modes), options?.format)[`#${primary}`];
|
|
2018
1987
|
};
|
|
@@ -2042,39 +2011,7 @@ function resolveBaseToken(base) {
|
|
|
2042
2011
|
if (isGlazeColorToken(base)) return base;
|
|
2043
2012
|
return createColorTokenFromValue(base, void 0, void 0);
|
|
2044
2013
|
}
|
|
2045
|
-
|
|
2046
|
-
* Build a JSON-safe snapshot of `GlazeColorOverrides`. `base` is
|
|
2047
|
-
* recursively serialized when it was originally a token; raw values are
|
|
2048
|
-
* preserved as-is so `glaze.colorFrom(...)` round-trips them.
|
|
2049
|
-
*/
|
|
2050
|
-
function buildOverridesExport(options) {
|
|
2051
|
-
const out = {};
|
|
2052
|
-
if (options.hue !== void 0) out.hue = options.hue;
|
|
2053
|
-
if (options.saturation !== void 0) out.saturation = options.saturation;
|
|
2054
|
-
if (options.lightness !== void 0) out.lightness = options.lightness;
|
|
2055
|
-
if (options.saturationFactor !== void 0) out.saturationFactor = options.saturationFactor;
|
|
2056
|
-
if (options.mode !== void 0) out.mode = options.mode;
|
|
2057
|
-
if (options.contrast !== void 0) out.contrast = options.contrast;
|
|
2058
|
-
if (options.opacity !== void 0) out.opacity = options.opacity;
|
|
2059
|
-
if (options.name !== void 0) out.name = options.name;
|
|
2060
|
-
if (options.base !== void 0) out.base = isGlazeColorToken(options.base) ? options.base.export() : options.base;
|
|
2061
|
-
return out;
|
|
2062
|
-
}
|
|
2063
|
-
function buildStructuredInputExport(input) {
|
|
2064
|
-
const out = {
|
|
2065
|
-
hue: input.hue,
|
|
2066
|
-
saturation: input.saturation,
|
|
2067
|
-
lightness: input.lightness
|
|
2068
|
-
};
|
|
2069
|
-
if (input.saturationFactor !== void 0) out.saturationFactor = input.saturationFactor;
|
|
2070
|
-
if (input.mode !== void 0) out.mode = input.mode;
|
|
2071
|
-
if (input.opacity !== void 0) out.opacity = input.opacity;
|
|
2072
|
-
if (input.contrast !== void 0) out.contrast = input.contrast;
|
|
2073
|
-
if (input.name !== void 0) out.name = input.name;
|
|
2074
|
-
if (input.base !== void 0) out.base = isGlazeColorToken(input.base) ? input.base.export() : input.base;
|
|
2075
|
-
return out;
|
|
2076
|
-
}
|
|
2077
|
-
function createColorToken(input, scaling) {
|
|
2014
|
+
function createColorToken(input, scaling, overrideAutoFlip) {
|
|
2078
2015
|
validateStructuredInput(input);
|
|
2079
2016
|
const userName = input.name;
|
|
2080
2017
|
if (userName !== void 0) validateStandaloneName(userName);
|
|
@@ -2096,40 +2033,70 @@ function createColorToken(input, scaling) {
|
|
|
2096
2033
|
mode: "static"
|
|
2097
2034
|
};
|
|
2098
2035
|
const effectiveScaling = scaling ?? defaultStandaloneScaling(false);
|
|
2036
|
+
const autoFlip = overrideAutoFlip ?? getConfig().autoFlip;
|
|
2099
2037
|
const exportData = () => ({
|
|
2100
2038
|
form: "structured",
|
|
2101
2039
|
input: buildStructuredInputExport(input),
|
|
2102
|
-
scaling: effectiveScaling
|
|
2040
|
+
scaling: effectiveScaling,
|
|
2041
|
+
autoFlip
|
|
2103
2042
|
});
|
|
2104
|
-
return createColorTokenFromDefs(input.hue, input.saturation, defs, primary, effectiveScaling, baseToken, exportData);
|
|
2043
|
+
return createColorTokenFromDefs(input.hue, input.saturation, defs, primary, effectiveScaling, baseToken, exportData, autoFlip);
|
|
2105
2044
|
}
|
|
2106
|
-
function createColorTokenFromValue(value, options, scaling) {
|
|
2045
|
+
function createColorTokenFromValue(value, options, scaling, overrideAutoFlip) {
|
|
2107
2046
|
const inputIsString = typeof value === "string";
|
|
2108
2047
|
const main = extractOkhslFromValue(value);
|
|
2109
2048
|
const baseToken = resolveBaseToken(options?.base);
|
|
2110
2049
|
const { seedHue, seedSaturation, defs, primary } = buildStandaloneValueDefs(main, options);
|
|
2111
2050
|
const effectiveScaling = scaling ?? defaultStandaloneScaling(inputIsString);
|
|
2051
|
+
const autoFlip = overrideAutoFlip ?? getConfig().autoFlip;
|
|
2112
2052
|
const exportData = () => ({
|
|
2113
2053
|
form: "value",
|
|
2114
2054
|
input: value,
|
|
2115
2055
|
...options !== void 0 ? { overrides: buildOverridesExport(options) } : {},
|
|
2116
|
-
scaling: effectiveScaling
|
|
2056
|
+
scaling: effectiveScaling,
|
|
2057
|
+
autoFlip
|
|
2117
2058
|
});
|
|
2118
|
-
return createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effectiveScaling, baseToken, exportData);
|
|
2059
|
+
return createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effectiveScaling, baseToken, exportData, autoFlip);
|
|
2119
2060
|
}
|
|
2120
2061
|
/**
|
|
2121
|
-
*
|
|
2122
|
-
*
|
|
2062
|
+
* Build a JSON-safe snapshot of `GlazeColorOverrides`. `base` is
|
|
2063
|
+
* recursively serialized when it was originally a token; raw values are
|
|
2064
|
+
* preserved as-is so `glaze.colorFrom(...)` round-trips them.
|
|
2123
2065
|
*/
|
|
2124
|
-
function
|
|
2125
|
-
|
|
2126
|
-
if (
|
|
2127
|
-
if (
|
|
2128
|
-
if (
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2066
|
+
function buildOverridesExport(options) {
|
|
2067
|
+
const out = {};
|
|
2068
|
+
if (options.hue !== void 0) out.hue = options.hue;
|
|
2069
|
+
if (options.saturation !== void 0) out.saturation = options.saturation;
|
|
2070
|
+
if (options.lightness !== void 0) out.lightness = options.lightness;
|
|
2071
|
+
if (options.saturationFactor !== void 0) out.saturationFactor = options.saturationFactor;
|
|
2072
|
+
if (options.mode !== void 0) out.mode = options.mode;
|
|
2073
|
+
if (options.contrast !== void 0) out.contrast = options.contrast;
|
|
2074
|
+
if (options.opacity !== void 0) out.opacity = options.opacity;
|
|
2075
|
+
if (options.name !== void 0) out.name = options.name;
|
|
2076
|
+
if (options.base !== void 0) out.base = isGlazeColorToken(options.base) ? options.base.export() : options.base;
|
|
2077
|
+
return out;
|
|
2078
|
+
}
|
|
2079
|
+
function buildStructuredInputExport(input) {
|
|
2080
|
+
const out = {
|
|
2081
|
+
hue: input.hue,
|
|
2082
|
+
saturation: input.saturation,
|
|
2083
|
+
lightness: input.lightness
|
|
2084
|
+
};
|
|
2085
|
+
if (input.saturationFactor !== void 0) out.saturationFactor = input.saturationFactor;
|
|
2086
|
+
if (input.mode !== void 0) out.mode = input.mode;
|
|
2087
|
+
if (input.opacity !== void 0) out.opacity = input.opacity;
|
|
2088
|
+
if (input.contrast !== void 0) out.contrast = input.contrast;
|
|
2089
|
+
if (input.name !== void 0) out.name = input.name;
|
|
2090
|
+
if (input.base !== void 0) out.base = isGlazeColorToken(input.base) ? input.base.export() : input.base;
|
|
2091
|
+
return out;
|
|
2092
|
+
}
|
|
2093
|
+
/**
|
|
2094
|
+
* Discriminate a `GlazeColorTokenExport` from a raw `GlazeColorValue`.
|
|
2095
|
+
* `GlazeColorTokenExport` always has a `form` field set to either
|
|
2096
|
+
* `'value'` or `'structured'`; raw values never do.
|
|
2097
|
+
*/
|
|
2098
|
+
function isExportedToken(candidate) {
|
|
2099
|
+
return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate) && "form" in candidate && (candidate.form === "value" || candidate.form === "structured");
|
|
2133
2100
|
}
|
|
2134
2101
|
function rehydrateOverrides(data) {
|
|
2135
2102
|
const out = {};
|
|
@@ -2159,13 +2126,264 @@ function rehydrateStructuredInput(data) {
|
|
|
2159
2126
|
return out;
|
|
2160
2127
|
}
|
|
2161
2128
|
/**
|
|
2162
|
-
*
|
|
2163
|
-
*
|
|
2164
|
-
* `'value'` or `'structured'`; raw values never do.
|
|
2129
|
+
* Rehydrate a token from its `.export()` snapshot. Recursively rebuilds
|
|
2130
|
+
* any base dependency. Inverse of `GlazeColorToken.export()`.
|
|
2165
2131
|
*/
|
|
2166
|
-
function
|
|
2167
|
-
|
|
2132
|
+
function colorFromExport(data) {
|
|
2133
|
+
if (data === null || typeof data !== "object") throw new Error(`glaze.colorFrom: expected an object from token.export(), got ${data === null ? "null" : typeof data}.`);
|
|
2134
|
+
if (data.form !== "value" && data.form !== "structured") throw new Error(`glaze.colorFrom: invalid "form" field — expected "value" or "structured" (got ${JSON.stringify(data.form)}).`);
|
|
2135
|
+
if (data.input === void 0) throw new Error(`glaze.colorFrom: missing "input" field — expected the original ${data.form === "value" ? "GlazeColorValue" : "GlazeColorInput"}.`);
|
|
2136
|
+
if (data.form === "value") {
|
|
2137
|
+
const value = data.input;
|
|
2138
|
+
const overrides = data.overrides ? rehydrateOverrides(data.overrides) : void 0;
|
|
2139
|
+
const cfg = getConfig();
|
|
2140
|
+
const effectiveAutoFlip = data.autoFlip ?? cfg.autoFlip;
|
|
2141
|
+
return createColorTokenFromValue(value, overrides, data.scaling, effectiveAutoFlip);
|
|
2142
|
+
}
|
|
2143
|
+
const input = rehydrateStructuredInput(data.input);
|
|
2144
|
+
const cfg = getConfig();
|
|
2145
|
+
const effectiveAutoFlip = data.autoFlip ?? cfg.autoFlip;
|
|
2146
|
+
return createColorToken(input, data.scaling, effectiveAutoFlip);
|
|
2168
2147
|
}
|
|
2148
|
+
|
|
2149
|
+
//#endregion
|
|
2150
|
+
//#region src/palette.ts
|
|
2151
|
+
/**
|
|
2152
|
+
* Palette factory.
|
|
2153
|
+
*
|
|
2154
|
+
* Composes multiple themes into a single token namespace with optional
|
|
2155
|
+
* theme-name prefixes and a "primary theme" that also surfaces an
|
|
2156
|
+
* unprefixed copy of its tokens. All four export methods (`tokens` /
|
|
2157
|
+
* `tasty` / `json` / `css`) share a `buildPaletteOutput` driver that
|
|
2158
|
+
* handles validation, per-theme iteration, prefix resolution, collision
|
|
2159
|
+
* filtering, and primary duplication.
|
|
2160
|
+
*/
|
|
2161
|
+
function resolvePrefix(options, themeName, defaultPrefix = false) {
|
|
2162
|
+
const prefix = options?.prefix ?? defaultPrefix;
|
|
2163
|
+
if (prefix === true) return `${themeName}-`;
|
|
2164
|
+
if (typeof prefix === "object" && prefix !== null) return prefix[themeName] ?? `${themeName}-`;
|
|
2165
|
+
return "";
|
|
2166
|
+
}
|
|
2167
|
+
function validatePrimaryTheme(primary, themes) {
|
|
2168
|
+
if (primary !== void 0 && !(primary in themes)) {
|
|
2169
|
+
const available = Object.keys(themes).join(", ");
|
|
2170
|
+
throw new Error(`glaze: primary theme "${primary}" not found in palette. Available: ${available}.`);
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
/**
|
|
2174
|
+
* Resolve the effective primary for an export call.
|
|
2175
|
+
* `false` disables, a string overrides, `undefined` inherits from palette.
|
|
2176
|
+
*/
|
|
2177
|
+
function resolveEffectivePrimary(exportPrimary, palettePrimary) {
|
|
2178
|
+
if (exportPrimary === false) return void 0;
|
|
2179
|
+
return exportPrimary ?? palettePrimary;
|
|
2180
|
+
}
|
|
2181
|
+
/**
|
|
2182
|
+
* Filter a resolved color map, skipping keys already in `seen`.
|
|
2183
|
+
* Warns on collision and keeps the first-written value (first-write-wins).
|
|
2184
|
+
* Returns a new map containing only non-colliding entries.
|
|
2185
|
+
*/
|
|
2186
|
+
function filterCollisions(resolved, prefix, seen, themeName, isPrimary) {
|
|
2187
|
+
const filtered = /* @__PURE__ */ new Map();
|
|
2188
|
+
const label = isPrimary ? `${themeName} (primary)` : themeName;
|
|
2189
|
+
for (const [name, color] of resolved) {
|
|
2190
|
+
const key = `${prefix}${name}`;
|
|
2191
|
+
if (seen.has(key)) {
|
|
2192
|
+
console.warn(`glaze: token "${key}" from theme "${label}" collides with theme "${seen.get(key)}" — skipping.`);
|
|
2193
|
+
continue;
|
|
2194
|
+
}
|
|
2195
|
+
seen.set(key, label);
|
|
2196
|
+
filtered.set(name, color);
|
|
2197
|
+
}
|
|
2198
|
+
return filtered;
|
|
2199
|
+
}
|
|
2200
|
+
/**
|
|
2201
|
+
* Shared per-theme driver for `tokens` / `tasty` / `css`. `json` skips
|
|
2202
|
+
* this because it doesn't do collision filtering or primary duplication.
|
|
2203
|
+
*/
|
|
2204
|
+
function buildPaletteOutput(themes, paletteOptions, options, buildOne, merge, empty) {
|
|
2205
|
+
const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
|
|
2206
|
+
if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
|
|
2207
|
+
const acc = empty();
|
|
2208
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2209
|
+
for (const [themeName, theme] of Object.entries(themes)) {
|
|
2210
|
+
const resolved = theme.resolve();
|
|
2211
|
+
const prefix = resolvePrefix(options, themeName, true);
|
|
2212
|
+
merge(acc, buildOne(filterCollisions(resolved, prefix, seen, themeName), prefix));
|
|
2213
|
+
if (themeName === effectivePrimary) merge(acc, buildOne(filterCollisions(resolved, "", seen, themeName, true), ""));
|
|
2214
|
+
}
|
|
2215
|
+
return acc;
|
|
2216
|
+
}
|
|
2217
|
+
function createPalette(themes, paletteOptions) {
|
|
2218
|
+
validatePrimaryTheme(paletteOptions?.primary, themes);
|
|
2219
|
+
return {
|
|
2220
|
+
tokens(options) {
|
|
2221
|
+
const modes = resolveModes(options?.modes);
|
|
2222
|
+
return buildPaletteOutput(themes, paletteOptions, options, (filtered, prefix) => buildFlatTokenMap(filtered, prefix, modes, options?.format), (acc, part) => {
|
|
2223
|
+
for (const variant of Object.keys(part)) {
|
|
2224
|
+
if (!acc[variant]) acc[variant] = {};
|
|
2225
|
+
Object.assign(acc[variant], part[variant]);
|
|
2226
|
+
}
|
|
2227
|
+
}, () => ({}));
|
|
2228
|
+
},
|
|
2229
|
+
tasty(options) {
|
|
2230
|
+
const cfg = getConfig();
|
|
2231
|
+
const states = {
|
|
2232
|
+
dark: options?.states?.dark ?? cfg.states.dark,
|
|
2233
|
+
highContrast: options?.states?.highContrast ?? cfg.states.highContrast
|
|
2234
|
+
};
|
|
2235
|
+
const modes = resolveModes(options?.modes);
|
|
2236
|
+
return buildPaletteOutput(themes, paletteOptions, options, (filtered, prefix) => buildTokenMap(filtered, prefix, states, modes, options?.format), (acc, part) => Object.assign(acc, part), () => ({}));
|
|
2237
|
+
},
|
|
2238
|
+
json(options) {
|
|
2239
|
+
const modes = resolveModes(options?.modes);
|
|
2240
|
+
const result = {};
|
|
2241
|
+
for (const [themeName, theme] of Object.entries(themes)) result[themeName] = buildJsonMap(theme.resolve(), modes, options?.format);
|
|
2242
|
+
return result;
|
|
2243
|
+
},
|
|
2244
|
+
css(options) {
|
|
2245
|
+
const suffix = options?.suffix ?? "-color";
|
|
2246
|
+
const format = options?.format ?? "rgb";
|
|
2247
|
+
const lines = buildPaletteOutput(themes, paletteOptions, options, (filtered, prefix) => buildCssMap(filtered, prefix, suffix, format), (acc, part) => {
|
|
2248
|
+
for (const key of [
|
|
2249
|
+
"light",
|
|
2250
|
+
"dark",
|
|
2251
|
+
"lightContrast",
|
|
2252
|
+
"darkContrast"
|
|
2253
|
+
]) if (part[key]) acc[key].push(part[key]);
|
|
2254
|
+
}, () => ({
|
|
2255
|
+
light: [],
|
|
2256
|
+
dark: [],
|
|
2257
|
+
lightContrast: [],
|
|
2258
|
+
darkContrast: []
|
|
2259
|
+
}));
|
|
2260
|
+
return {
|
|
2261
|
+
light: lines.light.join("\n"),
|
|
2262
|
+
dark: lines.dark.join("\n"),
|
|
2263
|
+
lightContrast: lines.lightContrast.join("\n"),
|
|
2264
|
+
darkContrast: lines.darkContrast.join("\n")
|
|
2265
|
+
};
|
|
2266
|
+
}
|
|
2267
|
+
};
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
//#endregion
|
|
2271
|
+
//#region src/theme.ts
|
|
2272
|
+
/**
|
|
2273
|
+
* Theme factory.
|
|
2274
|
+
*
|
|
2275
|
+
* Wraps a hue/saturation seed and a mutable `ColorMap`, and exposes
|
|
2276
|
+
* `tokens()` / `tasty()` / `json()` / `css()` / `resolve()` / `export()`
|
|
2277
|
+
* / `extend()`. Caches the last resolve result so successive exports
|
|
2278
|
+
* with the same defs and config don't re-run the four-pass resolver.
|
|
2279
|
+
*/
|
|
2280
|
+
function createTheme(hue, saturation, initialColors) {
|
|
2281
|
+
let colorDefs = initialColors ? { ...initialColors } : {};
|
|
2282
|
+
let cache = null;
|
|
2283
|
+
function resolveCached() {
|
|
2284
|
+
const version = getConfigVersion();
|
|
2285
|
+
if (cache && cache.version === version) return cache.map;
|
|
2286
|
+
const map = resolveAllColors(hue, saturation, colorDefs);
|
|
2287
|
+
cache = {
|
|
2288
|
+
map,
|
|
2289
|
+
version
|
|
2290
|
+
};
|
|
2291
|
+
return map;
|
|
2292
|
+
}
|
|
2293
|
+
function invalidate() {
|
|
2294
|
+
cache = null;
|
|
2295
|
+
}
|
|
2296
|
+
return {
|
|
2297
|
+
get hue() {
|
|
2298
|
+
return hue;
|
|
2299
|
+
},
|
|
2300
|
+
get saturation() {
|
|
2301
|
+
return saturation;
|
|
2302
|
+
},
|
|
2303
|
+
colors(defs) {
|
|
2304
|
+
colorDefs = {
|
|
2305
|
+
...colorDefs,
|
|
2306
|
+
...defs
|
|
2307
|
+
};
|
|
2308
|
+
invalidate();
|
|
2309
|
+
},
|
|
2310
|
+
color(name, def) {
|
|
2311
|
+
if (def === void 0) return colorDefs[name];
|
|
2312
|
+
colorDefs[name] = def;
|
|
2313
|
+
invalidate();
|
|
2314
|
+
},
|
|
2315
|
+
remove(names) {
|
|
2316
|
+
const list = Array.isArray(names) ? names : [names];
|
|
2317
|
+
for (const name of list) delete colorDefs[name];
|
|
2318
|
+
invalidate();
|
|
2319
|
+
},
|
|
2320
|
+
has(name) {
|
|
2321
|
+
return name in colorDefs;
|
|
2322
|
+
},
|
|
2323
|
+
list() {
|
|
2324
|
+
return Object.keys(colorDefs);
|
|
2325
|
+
},
|
|
2326
|
+
reset() {
|
|
2327
|
+
colorDefs = {};
|
|
2328
|
+
invalidate();
|
|
2329
|
+
},
|
|
2330
|
+
export() {
|
|
2331
|
+
return {
|
|
2332
|
+
hue,
|
|
2333
|
+
saturation,
|
|
2334
|
+
colors: { ...colorDefs }
|
|
2335
|
+
};
|
|
2336
|
+
},
|
|
2337
|
+
extend(options) {
|
|
2338
|
+
const newHue = options.hue ?? hue;
|
|
2339
|
+
const newSat = options.saturation ?? saturation;
|
|
2340
|
+
const inheritedColors = {};
|
|
2341
|
+
for (const [name, def] of Object.entries(colorDefs)) if (def.inherit !== false) inheritedColors[name] = def;
|
|
2342
|
+
return createTheme(newHue, newSat, options.colors ? {
|
|
2343
|
+
...inheritedColors,
|
|
2344
|
+
...options.colors
|
|
2345
|
+
} : { ...inheritedColors });
|
|
2346
|
+
},
|
|
2347
|
+
resolve() {
|
|
2348
|
+
return new Map(resolveCached());
|
|
2349
|
+
},
|
|
2350
|
+
tokens(options) {
|
|
2351
|
+
const modes = resolveModes(options?.modes);
|
|
2352
|
+
return buildFlatTokenMap(resolveCached(), "", modes, options?.format);
|
|
2353
|
+
},
|
|
2354
|
+
tasty(options) {
|
|
2355
|
+
const cfg = getConfig();
|
|
2356
|
+
const states = {
|
|
2357
|
+
dark: options?.states?.dark ?? cfg.states.dark,
|
|
2358
|
+
highContrast: options?.states?.highContrast ?? cfg.states.highContrast
|
|
2359
|
+
};
|
|
2360
|
+
const modes = resolveModes(options?.modes);
|
|
2361
|
+
return buildTokenMap(resolveCached(), "", states, modes, options?.format);
|
|
2362
|
+
},
|
|
2363
|
+
json(options) {
|
|
2364
|
+
const modes = resolveModes(options?.modes);
|
|
2365
|
+
return buildJsonMap(resolveCached(), modes, options?.format);
|
|
2366
|
+
},
|
|
2367
|
+
css(options) {
|
|
2368
|
+
return buildCssMap(resolveCached(), "", options?.suffix ?? "-color", options?.format ?? "rgb");
|
|
2369
|
+
}
|
|
2370
|
+
};
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
//#endregion
|
|
2374
|
+
//#region src/glaze.ts
|
|
2375
|
+
/**
|
|
2376
|
+
* Glaze — OKHSL-based color theme generator.
|
|
2377
|
+
*
|
|
2378
|
+
* Public API entry. Wires `glaze()` and its attached static methods to
|
|
2379
|
+
* the focused modules in this folder:
|
|
2380
|
+
* - `theme.ts` — single-theme factory
|
|
2381
|
+
* - `palette.ts` — multi-theme composition
|
|
2382
|
+
* - `color-token.ts` — standalone single-color tokens (`glaze.color`)
|
|
2383
|
+
* - `shadow.ts` — standalone shadow factory (`glaze.shadow`)
|
|
2384
|
+
* - `formatters.ts` — variant → string (`glaze.format`)
|
|
2385
|
+
* - `config.ts` — global config singleton
|
|
2386
|
+
*/
|
|
2169
2387
|
/**
|
|
2170
2388
|
* Create a single-hue glaze theme.
|
|
2171
2389
|
*
|
|
@@ -2180,41 +2398,18 @@ function glaze(hueOrOptions, saturation) {
|
|
|
2180
2398
|
if (typeof hueOrOptions === "number") return createTheme(hueOrOptions, saturation ?? 100);
|
|
2181
2399
|
return createTheme(hueOrOptions.hue, hueOrOptions.saturation);
|
|
2182
2400
|
}
|
|
2183
|
-
/**
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
glaze.configure = function configure(config) {
|
|
2187
|
-
globalConfig = {
|
|
2188
|
-
lightLightness: config.lightLightness ?? globalConfig.lightLightness,
|
|
2189
|
-
darkLightness: config.darkLightness ?? globalConfig.darkLightness,
|
|
2190
|
-
darkDesaturation: config.darkDesaturation ?? globalConfig.darkDesaturation,
|
|
2191
|
-
darkCurve: config.darkCurve ?? globalConfig.darkCurve,
|
|
2192
|
-
states: {
|
|
2193
|
-
dark: config.states?.dark ?? globalConfig.states.dark,
|
|
2194
|
-
highContrast: config.states?.highContrast ?? globalConfig.states.highContrast
|
|
2195
|
-
},
|
|
2196
|
-
modes: {
|
|
2197
|
-
dark: config.modes?.dark ?? globalConfig.modes.dark,
|
|
2198
|
-
highContrast: config.modes?.highContrast ?? globalConfig.modes.highContrast
|
|
2199
|
-
},
|
|
2200
|
-
shadowTuning: config.shadowTuning ?? globalConfig.shadowTuning
|
|
2201
|
-
};
|
|
2401
|
+
/** Configure global glaze settings. */
|
|
2402
|
+
glaze.configure = function configure$1(config) {
|
|
2403
|
+
configure(config);
|
|
2202
2404
|
};
|
|
2203
|
-
/**
|
|
2204
|
-
* Compose multiple themes into a palette.
|
|
2205
|
-
*/
|
|
2405
|
+
/** Compose multiple themes into a palette. */
|
|
2206
2406
|
glaze.palette = function palette(themes, options) {
|
|
2207
2407
|
return createPalette(themes, options);
|
|
2208
2408
|
};
|
|
2209
|
-
/**
|
|
2210
|
-
* Create a theme from a serialized export.
|
|
2211
|
-
*/
|
|
2409
|
+
/** Create a theme from a serialized export. */
|
|
2212
2410
|
glaze.from = function from(data) {
|
|
2213
2411
|
return createTheme(data.hue, data.saturation, data.colors);
|
|
2214
2412
|
};
|
|
2215
|
-
function isStructuredColorInput(input) {
|
|
2216
|
-
return typeof input === "object" && input !== null && !Array.isArray(input) && "hue" in input && "lightness" in input;
|
|
2217
|
-
}
|
|
2218
2413
|
/**
|
|
2219
2414
|
* Create a standalone single-color token.
|
|
2220
2415
|
*
|
|
@@ -2278,9 +2473,7 @@ glaze.shadow = function shadow(input) {
|
|
|
2278
2473
|
alpha: 1
|
|
2279
2474
|
} : void 0, input.intensity, tuning);
|
|
2280
2475
|
};
|
|
2281
|
-
/**
|
|
2282
|
-
* Format a resolved color variant as a CSS string.
|
|
2283
|
-
*/
|
|
2476
|
+
/** Format a resolved color variant as a CSS string. */
|
|
2284
2477
|
glaze.format = function format(variant, colorFormat) {
|
|
2285
2478
|
return formatVariant(variant, colorFormat);
|
|
2286
2479
|
};
|
|
@@ -2326,30 +2519,13 @@ glaze.fromRgb = function fromRgb(r, g, b) {
|
|
|
2326
2519
|
glaze.colorFrom = function colorFrom(data) {
|
|
2327
2520
|
return colorFromExport(data);
|
|
2328
2521
|
};
|
|
2329
|
-
/**
|
|
2330
|
-
* Get the current global configuration (for testing/debugging).
|
|
2331
|
-
*/
|
|
2522
|
+
/** Get the current global configuration (for testing/debugging). */
|
|
2332
2523
|
glaze.getConfig = function getConfig() {
|
|
2333
|
-
return
|
|
2524
|
+
return snapshotConfig();
|
|
2334
2525
|
};
|
|
2335
|
-
/**
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
glaze.resetConfig = function resetConfig() {
|
|
2339
|
-
globalConfig = {
|
|
2340
|
-
lightLightness: [10, 100],
|
|
2341
|
-
darkLightness: [15, 95],
|
|
2342
|
-
darkDesaturation: .1,
|
|
2343
|
-
darkCurve: .5,
|
|
2344
|
-
states: {
|
|
2345
|
-
dark: "@dark",
|
|
2346
|
-
highContrast: "@high-contrast"
|
|
2347
|
-
},
|
|
2348
|
-
modes: {
|
|
2349
|
-
dark: true,
|
|
2350
|
-
highContrast: false
|
|
2351
|
-
}
|
|
2352
|
-
};
|
|
2526
|
+
/** Reset global configuration to defaults. */
|
|
2527
|
+
glaze.resetConfig = function resetConfig$1() {
|
|
2528
|
+
resetConfig();
|
|
2353
2529
|
};
|
|
2354
2530
|
|
|
2355
2531
|
//#endregion
|