@tenphi/glaze 0.11.1 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +401 -254
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +172 -73
- package/dist/index.d.mts +172 -73
- package/dist/index.mjs +401 -254
- package/dist/index.mjs.map +1 -1
- package/docs/api.md +143 -71
- package/docs/methodology.md +12 -6
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -577,7 +577,8 @@ function defaultConfig() {
|
|
|
577
577
|
modes: {
|
|
578
578
|
dark: true,
|
|
579
579
|
highContrast: false
|
|
580
|
-
}
|
|
580
|
+
},
|
|
581
|
+
autoFlip: true
|
|
581
582
|
};
|
|
582
583
|
}
|
|
583
584
|
let globalConfig = defaultConfig();
|
|
@@ -616,13 +617,33 @@ function configure(config) {
|
|
|
616
617
|
dark: config.modes?.dark ?? globalConfig.modes.dark,
|
|
617
618
|
highContrast: config.modes?.highContrast ?? globalConfig.modes.highContrast
|
|
618
619
|
},
|
|
619
|
-
shadowTuning: config.shadowTuning ?? globalConfig.shadowTuning
|
|
620
|
+
shadowTuning: config.shadowTuning ?? globalConfig.shadowTuning,
|
|
621
|
+
autoFlip: config.autoFlip ?? globalConfig.autoFlip
|
|
620
622
|
};
|
|
621
623
|
}
|
|
622
624
|
function resetConfig() {
|
|
623
625
|
configVersion++;
|
|
624
626
|
globalConfig = defaultConfig();
|
|
625
627
|
}
|
|
628
|
+
/**
|
|
629
|
+
* Merge a per-instance config override over a base resolved config.
|
|
630
|
+
* Only fields present in `override` are replaced; others fall through
|
|
631
|
+
* from `base`. `false` for lightness windows passes through as-is
|
|
632
|
+
* (treated as `[0, 100]` by `lightnessWindow()` in scheme-mapping).
|
|
633
|
+
*/
|
|
634
|
+
function mergeConfig(base, override) {
|
|
635
|
+
if (!override) return base;
|
|
636
|
+
return {
|
|
637
|
+
lightLightness: override.lightLightness !== void 0 ? override.lightLightness : base.lightLightness,
|
|
638
|
+
darkLightness: override.darkLightness !== void 0 ? override.darkLightness : base.darkLightness,
|
|
639
|
+
darkDesaturation: override.darkDesaturation ?? base.darkDesaturation,
|
|
640
|
+
darkCurve: override.darkCurve ?? base.darkCurve,
|
|
641
|
+
states: base.states,
|
|
642
|
+
modes: base.modes,
|
|
643
|
+
shadowTuning: override.shadowTuning ?? base.shadowTuning,
|
|
644
|
+
autoFlip: override.autoFlip ?? base.autoFlip
|
|
645
|
+
};
|
|
646
|
+
}
|
|
626
647
|
|
|
627
648
|
//#endregion
|
|
628
649
|
//#region src/hc-pair.ts
|
|
@@ -825,47 +846,64 @@ function findLightnessForContrast(options) {
|
|
|
825
846
|
branch: "preferred"
|
|
826
847
|
};
|
|
827
848
|
const [minL, maxL] = lightnessRange;
|
|
828
|
-
const
|
|
829
|
-
const
|
|
830
|
-
|
|
831
|
-
if (
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
if (
|
|
835
|
-
if (Math.abs(darkerResult.lightness - preferredLightness) <= Math.abs(lighterResult.lightness - preferredLightness)) return {
|
|
836
|
-
...darkerResult,
|
|
837
|
-
branch: "darker"
|
|
838
|
-
};
|
|
839
|
-
return {
|
|
840
|
-
...lighterResult,
|
|
841
|
-
branch: "lighter"
|
|
842
|
-
};
|
|
843
|
-
}
|
|
844
|
-
if (darkerPasses) return {
|
|
845
|
-
...darkerResult,
|
|
846
|
-
branch: "darker"
|
|
847
|
-
};
|
|
848
|
-
if (lighterPasses) return {
|
|
849
|
-
...lighterResult,
|
|
850
|
-
branch: "lighter"
|
|
851
|
-
};
|
|
852
|
-
const candidates = [];
|
|
853
|
-
if (darkerResult) candidates.push({
|
|
854
|
-
...darkerResult,
|
|
855
|
-
branch: "darker"
|
|
856
|
-
});
|
|
857
|
-
if (lighterResult) candidates.push({
|
|
858
|
-
...lighterResult,
|
|
859
|
-
branch: "lighter"
|
|
860
|
-
});
|
|
861
|
-
if (candidates.length === 0) return {
|
|
849
|
+
const canDarker = preferredLightness > minL;
|
|
850
|
+
const canLighter = preferredLightness < maxL;
|
|
851
|
+
let initialIsDarker;
|
|
852
|
+
if (options.initialDirection !== void 0) initialIsDarker = options.initialDirection === "darker";
|
|
853
|
+
else if (canDarker && !canLighter) initialIsDarker = true;
|
|
854
|
+
else if (!canDarker && canLighter) initialIsDarker = false;
|
|
855
|
+
else if (!canDarker && !canLighter) return {
|
|
862
856
|
lightness: preferredLightness,
|
|
863
857
|
contrast: crPref,
|
|
864
858
|
met: false,
|
|
865
859
|
branch: "preferred"
|
|
866
860
|
};
|
|
867
|
-
|
|
868
|
-
|
|
861
|
+
else {
|
|
862
|
+
const yMinExt = cachedLuminance(hue, saturation, minL);
|
|
863
|
+
const yMaxExt = cachedLuminance(hue, saturation, maxL);
|
|
864
|
+
initialIsDarker = contrastRatioFromLuminance(yMinExt, yBase) >= contrastRatioFromLuminance(yMaxExt, yBase);
|
|
865
|
+
}
|
|
866
|
+
const searchInitial = () => initialIsDarker ? searchBranch(hue, saturation, minL, preferredLightness, yBase, searchTarget, epsilon, maxIterations, preferredLightness) : searchBranch(hue, saturation, preferredLightness, maxL, yBase, searchTarget, epsilon, maxIterations, preferredLightness);
|
|
867
|
+
const searchOpposite = () => initialIsDarker ? searchBranch(hue, saturation, preferredLightness, maxL, yBase, searchTarget, epsilon, maxIterations, preferredLightness) : searchBranch(hue, saturation, minL, preferredLightness, yBase, searchTarget, epsilon, maxIterations, preferredLightness);
|
|
868
|
+
const initialBranchName = initialIsDarker ? "darker" : "lighter";
|
|
869
|
+
const oppositeBranchName = initialIsDarker ? "lighter" : "darker";
|
|
870
|
+
const initialResult = searchInitial();
|
|
871
|
+
initialResult.met = initialResult.contrast >= target;
|
|
872
|
+
if (initialResult.met && !options.flip) return {
|
|
873
|
+
...initialResult,
|
|
874
|
+
branch: initialBranchName
|
|
875
|
+
};
|
|
876
|
+
if (options.flip) {
|
|
877
|
+
const oppositeResult = (initialIsDarker ? canLighter : canDarker) ? searchOpposite() : null;
|
|
878
|
+
if (oppositeResult) oppositeResult.met = oppositeResult.contrast >= target;
|
|
879
|
+
if (initialResult.met && oppositeResult?.met) {
|
|
880
|
+
if (Math.abs(initialResult.lightness - preferredLightness) <= Math.abs(oppositeResult.lightness - preferredLightness)) return {
|
|
881
|
+
...initialResult,
|
|
882
|
+
branch: initialBranchName
|
|
883
|
+
};
|
|
884
|
+
return {
|
|
885
|
+
...oppositeResult,
|
|
886
|
+
branch: oppositeBranchName,
|
|
887
|
+
flipped: true
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
if (initialResult.met) return {
|
|
891
|
+
...initialResult,
|
|
892
|
+
branch: initialBranchName
|
|
893
|
+
};
|
|
894
|
+
if (oppositeResult?.met) return {
|
|
895
|
+
...oppositeResult,
|
|
896
|
+
branch: oppositeBranchName,
|
|
897
|
+
flipped: true
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
const extreme = initialIsDarker ? minL : maxL;
|
|
901
|
+
return {
|
|
902
|
+
lightness: extreme,
|
|
903
|
+
contrast: contrastRatioFromLuminance(cachedLuminance(hue, saturation, extreme), yBase),
|
|
904
|
+
met: false,
|
|
905
|
+
branch: initialBranchName
|
|
906
|
+
};
|
|
869
907
|
}
|
|
870
908
|
/**
|
|
871
909
|
* Binary-search one branch [lo, hi] for the nearest passing mix value
|
|
@@ -947,53 +985,59 @@ function findValueForMixContrast(options) {
|
|
|
947
985
|
contrast: crPref,
|
|
948
986
|
met: true
|
|
949
987
|
};
|
|
950
|
-
const
|
|
951
|
-
const
|
|
952
|
-
|
|
953
|
-
if (
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
if (darkerPasses && lighterPasses) {
|
|
957
|
-
if (Math.abs(darkerResult.lightness - preferredValue) <= Math.abs(lighterResult.lightness - preferredValue)) return {
|
|
958
|
-
value: darkerResult.lightness,
|
|
959
|
-
contrast: darkerResult.contrast,
|
|
960
|
-
met: true
|
|
961
|
-
};
|
|
962
|
-
return {
|
|
963
|
-
value: lighterResult.lightness,
|
|
964
|
-
contrast: lighterResult.contrast,
|
|
965
|
-
met: true
|
|
966
|
-
};
|
|
967
|
-
}
|
|
968
|
-
if (darkerPasses) return {
|
|
969
|
-
value: darkerResult.lightness,
|
|
970
|
-
contrast: darkerResult.contrast,
|
|
971
|
-
met: true
|
|
972
|
-
};
|
|
973
|
-
if (lighterPasses) return {
|
|
974
|
-
value: lighterResult.lightness,
|
|
975
|
-
contrast: lighterResult.contrast,
|
|
976
|
-
met: true
|
|
977
|
-
};
|
|
978
|
-
const candidates = [];
|
|
979
|
-
if (darkerResult) candidates.push({
|
|
980
|
-
...darkerResult,
|
|
981
|
-
branch: "lower"
|
|
982
|
-
});
|
|
983
|
-
if (lighterResult) candidates.push({
|
|
984
|
-
...lighterResult,
|
|
985
|
-
branch: "upper"
|
|
986
|
-
});
|
|
987
|
-
if (candidates.length === 0) return {
|
|
988
|
+
const canLower = preferredValue > 0;
|
|
989
|
+
const canUpper = preferredValue < 1;
|
|
990
|
+
let initialIsLower;
|
|
991
|
+
if (canLower && !canUpper) initialIsLower = true;
|
|
992
|
+
else if (!canLower && canUpper) initialIsLower = false;
|
|
993
|
+
else if (!canLower && !canUpper) return {
|
|
988
994
|
value: preferredValue,
|
|
989
995
|
contrast: crPref,
|
|
990
996
|
met: false
|
|
991
997
|
};
|
|
992
|
-
|
|
998
|
+
else initialIsLower = contrastRatioFromLuminance(luminanceAtValue(0), yBase) >= contrastRatioFromLuminance(luminanceAtValue(1), yBase);
|
|
999
|
+
const searchInitial = () => initialIsLower ? searchMixBranch(0, preferredValue, yBase, searchTarget, epsilon, maxIterations, preferredValue, luminanceAtValue) : searchMixBranch(preferredValue, 1, yBase, searchTarget, epsilon, maxIterations, preferredValue, luminanceAtValue);
|
|
1000
|
+
const searchOpposite = () => initialIsLower ? searchMixBranch(preferredValue, 1, yBase, searchTarget, epsilon, maxIterations, preferredValue, luminanceAtValue) : searchMixBranch(0, preferredValue, yBase, searchTarget, epsilon, maxIterations, preferredValue, luminanceAtValue);
|
|
1001
|
+
const initialResult = searchInitial();
|
|
1002
|
+
initialResult.met = initialResult.contrast >= target;
|
|
1003
|
+
if (initialResult.met && !options.flip) return {
|
|
1004
|
+
value: initialResult.lightness,
|
|
1005
|
+
contrast: initialResult.contrast,
|
|
1006
|
+
met: true
|
|
1007
|
+
};
|
|
1008
|
+
if (options.flip) {
|
|
1009
|
+
const oppositeResult = (initialIsLower ? canUpper : canLower) ? searchOpposite() : null;
|
|
1010
|
+
if (oppositeResult) oppositeResult.met = oppositeResult.contrast >= target;
|
|
1011
|
+
if (initialResult.met && oppositeResult?.met) {
|
|
1012
|
+
if (Math.abs(initialResult.lightness - preferredValue) <= Math.abs(oppositeResult.lightness - preferredValue)) return {
|
|
1013
|
+
value: initialResult.lightness,
|
|
1014
|
+
contrast: initialResult.contrast,
|
|
1015
|
+
met: true
|
|
1016
|
+
};
|
|
1017
|
+
return {
|
|
1018
|
+
value: oppositeResult.lightness,
|
|
1019
|
+
contrast: oppositeResult.contrast,
|
|
1020
|
+
met: true,
|
|
1021
|
+
flipped: true
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
if (initialResult.met) return {
|
|
1025
|
+
value: initialResult.lightness,
|
|
1026
|
+
contrast: initialResult.contrast,
|
|
1027
|
+
met: true
|
|
1028
|
+
};
|
|
1029
|
+
if (oppositeResult?.met) return {
|
|
1030
|
+
value: oppositeResult.lightness,
|
|
1031
|
+
contrast: oppositeResult.contrast,
|
|
1032
|
+
met: true,
|
|
1033
|
+
flipped: true
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
const extreme = initialIsLower ? 0 : 1;
|
|
993
1037
|
return {
|
|
994
|
-
value:
|
|
995
|
-
contrast:
|
|
996
|
-
met:
|
|
1038
|
+
value: extreme,
|
|
1039
|
+
contrast: contrastRatioFromLuminance(luminanceAtValue(extreme), yBase),
|
|
1040
|
+
met: false
|
|
997
1041
|
};
|
|
998
1042
|
}
|
|
999
1043
|
|
|
@@ -1022,8 +1066,7 @@ const DEFAULT_SHADOW_TUNING = {
|
|
|
1022
1066
|
alphaMax: 1,
|
|
1023
1067
|
bgHueBlend: .2
|
|
1024
1068
|
};
|
|
1025
|
-
function resolveShadowTuning(perColor) {
|
|
1026
|
-
const globalTuning = getConfig().shadowTuning;
|
|
1069
|
+
function resolveShadowTuning(perColor, globalTuning) {
|
|
1027
1070
|
return {
|
|
1028
1071
|
...DEFAULT_SHADOW_TUNING,
|
|
1029
1072
|
...globalTuning,
|
|
@@ -1075,61 +1118,58 @@ function computeShadow(bg, fg, intensity, tuning) {
|
|
|
1075
1118
|
/**
|
|
1076
1119
|
* Light / dark scheme lightness mappings.
|
|
1077
1120
|
*
|
|
1078
|
-
* Owns the active lightness window selection (
|
|
1079
|
-
*
|
|
1080
|
-
*
|
|
1081
|
-
*
|
|
1121
|
+
* Owns the active lightness window selection (from a resolved effective
|
|
1122
|
+
* config passed in), the Möbius curve used by the `'auto'` dark
|
|
1123
|
+
* adaptation, and the saturation-desaturation reducer for dark mode.
|
|
1124
|
+
*
|
|
1125
|
+
* All functions take a `GlazeConfigResolved` so the full config
|
|
1126
|
+
* (including per-instance overrides) is available without re-reading
|
|
1127
|
+
* the global singleton inside the resolver.
|
|
1082
1128
|
*/
|
|
1083
1129
|
/**
|
|
1084
1130
|
* Resolve the active lightness window for a scheme.
|
|
1085
|
-
* - HC variants always return `[0, 100]` (
|
|
1086
|
-
* -
|
|
1087
|
-
*
|
|
1131
|
+
* - HC variants always return `[0, 100]` (no clamping in high-contrast).
|
|
1132
|
+
* - `false` (= "no clamping") is treated as `[0, 100]`.
|
|
1133
|
+
* - Otherwise uses the window from the resolved effective config.
|
|
1088
1134
|
*/
|
|
1089
|
-
function lightnessWindow(isHighContrast, kind,
|
|
1135
|
+
function lightnessWindow(isHighContrast, kind, config) {
|
|
1090
1136
|
if (isHighContrast) return [0, 100];
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
if (override !== void 0) return override;
|
|
1095
|
-
}
|
|
1096
|
-
const cfg = getConfig();
|
|
1097
|
-
return kind === "dark" ? cfg.darkLightness : cfg.lightLightness;
|
|
1137
|
+
const win = kind === "dark" ? config.darkLightness : config.lightLightness;
|
|
1138
|
+
if (win === false) return [0, 100];
|
|
1139
|
+
return win;
|
|
1098
1140
|
}
|
|
1099
|
-
function mapLightnessLight(l, mode, isHighContrast,
|
|
1141
|
+
function mapLightnessLight(l, mode, isHighContrast, config) {
|
|
1100
1142
|
if (mode === "static") return l;
|
|
1101
|
-
const [lo, hi] = lightnessWindow(isHighContrast, "light",
|
|
1143
|
+
const [lo, hi] = lightnessWindow(isHighContrast, "light", config);
|
|
1102
1144
|
return l * (hi - lo) / 100 + lo;
|
|
1103
1145
|
}
|
|
1104
1146
|
function mobiusCurve(t, beta) {
|
|
1105
1147
|
if (beta >= 1) return t;
|
|
1106
1148
|
return t / (t + beta * (1 - t));
|
|
1107
1149
|
}
|
|
1108
|
-
function mapLightnessDark(l, mode, isHighContrast,
|
|
1150
|
+
function mapLightnessDark(l, mode, isHighContrast, config) {
|
|
1109
1151
|
if (mode === "static") return l;
|
|
1110
|
-
const
|
|
1111
|
-
const
|
|
1112
|
-
const [darkLo, darkHi] = lightnessWindow(isHighContrast, "dark", scaling);
|
|
1152
|
+
const beta = isHighContrast ? pairHC(config.darkCurve) : pairNormal(config.darkCurve);
|
|
1153
|
+
const [darkLo, darkHi] = lightnessWindow(isHighContrast, "dark", config);
|
|
1113
1154
|
if (mode === "fixed") return l * (darkHi - darkLo) / 100 + darkLo;
|
|
1114
|
-
const [lightLo, lightHi] = lightnessWindow(isHighContrast, "light",
|
|
1155
|
+
const [lightLo, lightHi] = lightnessWindow(isHighContrast, "light", config);
|
|
1115
1156
|
const t = (lightHi - (l * (lightHi - lightLo) / 100 + lightLo)) / (lightHi - lightLo);
|
|
1116
1157
|
return darkLo + (darkHi - darkLo) * mobiusCurve(t, beta);
|
|
1117
1158
|
}
|
|
1118
|
-
function lightMappedToDark(lightL, isHighContrast,
|
|
1119
|
-
const
|
|
1120
|
-
const
|
|
1121
|
-
const [
|
|
1122
|
-
const [darkLo, darkHi] = lightnessWindow(isHighContrast, "dark", scaling);
|
|
1159
|
+
function lightMappedToDark(lightL, isHighContrast, config) {
|
|
1160
|
+
const beta = isHighContrast ? pairHC(config.darkCurve) : pairNormal(config.darkCurve);
|
|
1161
|
+
const [lightLo, lightHi] = lightnessWindow(isHighContrast, "light", config);
|
|
1162
|
+
const [darkLo, darkHi] = lightnessWindow(isHighContrast, "dark", config);
|
|
1123
1163
|
const t = (lightHi - clamp(lightL, lightLo, lightHi)) / (lightHi - lightLo);
|
|
1124
1164
|
return darkLo + (darkHi - darkLo) * mobiusCurve(t, beta);
|
|
1125
1165
|
}
|
|
1126
|
-
function mapSaturationDark(s, mode) {
|
|
1166
|
+
function mapSaturationDark(s, mode, config) {
|
|
1127
1167
|
if (mode === "static") return s;
|
|
1128
|
-
return s * (1 -
|
|
1168
|
+
return s * (1 - config.darkDesaturation);
|
|
1129
1169
|
}
|
|
1130
|
-
function schemeLightnessRange(isDark, mode, isHighContrast,
|
|
1170
|
+
function schemeLightnessRange(isDark, mode, isHighContrast, config) {
|
|
1131
1171
|
if (mode === "static") return [0, 1];
|
|
1132
|
-
const [lo, hi] = lightnessWindow(isHighContrast, isDark ? "dark" : "light",
|
|
1172
|
+
const [lo, hi] = lightnessWindow(isHighContrast, isDark ? "dark" : "light", config);
|
|
1133
1173
|
return [lo / 100, hi / 100];
|
|
1134
1174
|
}
|
|
1135
1175
|
|
|
@@ -1267,6 +1307,10 @@ function warnContrastUnmet(name, isDark, isHighContrast, target, actual) {
|
|
|
1267
1307
|
* turns a `ColorMap` into a fully resolved `ResolvedColor` per name.
|
|
1268
1308
|
* Owns the per-scheme resolve helpers for regular, shadow, and mix
|
|
1269
1309
|
* color defs.
|
|
1310
|
+
*
|
|
1311
|
+
* Every function receives a single `GlazeConfigResolved` so the full
|
|
1312
|
+
* per-instance config (including overrides) is available without
|
|
1313
|
+
* re-reading the global singleton mid-resolve.
|
|
1270
1314
|
*/
|
|
1271
1315
|
function getSchemeVariant(color, isDark, isHighContrast) {
|
|
1272
1316
|
if (isDark && isHighContrast) return color.darkContrast;
|
|
@@ -1296,24 +1340,29 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
|
|
|
1296
1340
|
const parsed = parseRelativeOrAbsolute(isHighContrast ? pairHC(rawLightness) : pairNormal(rawLightness));
|
|
1297
1341
|
if (parsed.relative) {
|
|
1298
1342
|
const delta = parsed.value;
|
|
1299
|
-
if (isDark && mode === "auto") preferredL = lightMappedToDark(clamp(getSchemeVariant(baseResolved, false, isHighContrast).l * 100 + delta, 0, 100), isHighContrast, ctx.
|
|
1343
|
+
if (isDark && mode === "auto") preferredL = lightMappedToDark(clamp(getSchemeVariant(baseResolved, false, isHighContrast).l * 100 + delta, 0, 100), isHighContrast, ctx.config);
|
|
1300
1344
|
else preferredL = clamp(baseL + delta, 0, 100);
|
|
1301
|
-
} else if (isDark) preferredL = mapLightnessDark(parsed.value, mode, isHighContrast, ctx.
|
|
1302
|
-
else preferredL = mapLightnessLight(parsed.value, mode, isHighContrast, ctx.
|
|
1345
|
+
} else if (isDark) preferredL = mapLightnessDark(parsed.value, mode, isHighContrast, ctx.config);
|
|
1346
|
+
else preferredL = mapLightnessLight(parsed.value, mode, isHighContrast, ctx.config);
|
|
1303
1347
|
}
|
|
1304
1348
|
const rawContrast = def.contrast;
|
|
1305
1349
|
if (rawContrast !== void 0) {
|
|
1306
1350
|
const minCr = isHighContrast ? pairHC(rawContrast) : pairNormal(rawContrast);
|
|
1307
|
-
const effectiveSat = isDark ? mapSaturationDark(satFactor * ctx.saturation / 100, mode) : satFactor * ctx.saturation / 100;
|
|
1351
|
+
const effectiveSat = isDark ? mapSaturationDark(satFactor * ctx.saturation / 100, mode, ctx.config) : satFactor * ctx.saturation / 100;
|
|
1308
1352
|
const baseLinearRgb = okhslToLinearSrgb(baseVariant.h, baseVariant.s, baseVariant.l);
|
|
1309
|
-
const windowRange = schemeLightnessRange(isDark, mode, isHighContrast, ctx.
|
|
1353
|
+
const windowRange = schemeLightnessRange(isDark, mode, isHighContrast, ctx.config);
|
|
1354
|
+
let initialDirection;
|
|
1355
|
+
if (preferredL < baseL) initialDirection = "darker";
|
|
1356
|
+
else if (preferredL > baseL) initialDirection = "lighter";
|
|
1310
1357
|
const result = findLightnessForContrast({
|
|
1311
1358
|
hue: effectiveHue,
|
|
1312
1359
|
saturation: effectiveSat,
|
|
1313
1360
|
preferredLightness: clamp(preferredL / 100, windowRange[0], windowRange[1]),
|
|
1314
1361
|
baseLinearRgb,
|
|
1315
1362
|
contrast: minCr,
|
|
1316
|
-
lightnessRange: [0, 1]
|
|
1363
|
+
lightnessRange: [0, 1],
|
|
1364
|
+
initialDirection,
|
|
1365
|
+
flip: ctx.config.autoFlip
|
|
1317
1366
|
});
|
|
1318
1367
|
if (!result.met) warnContrastUnmet(name, isDark, isHighContrast, minCr, result.contrast);
|
|
1319
1368
|
return {
|
|
@@ -1347,13 +1396,13 @@ function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
|
1347
1396
|
let finalL;
|
|
1348
1397
|
let finalSat;
|
|
1349
1398
|
if (isDark && isRoot) {
|
|
1350
|
-
finalL = mapLightnessDark(lightL, mode, isHighContrast, ctx.
|
|
1351
|
-
finalSat = mapSaturationDark(satFactor * ctx.saturation / 100, mode);
|
|
1399
|
+
finalL = mapLightnessDark(lightL, mode, isHighContrast, ctx.config);
|
|
1400
|
+
finalSat = mapSaturationDark(satFactor * ctx.saturation / 100, mode, ctx.config);
|
|
1352
1401
|
} else if (isDark && !isRoot) {
|
|
1353
1402
|
finalL = lightL;
|
|
1354
|
-
finalSat = mapSaturationDark(satFactor * ctx.saturation / 100, mode);
|
|
1403
|
+
finalSat = mapSaturationDark(satFactor * ctx.saturation / 100, mode, ctx.config);
|
|
1355
1404
|
} else if (isRoot) {
|
|
1356
|
-
finalL = mapLightnessLight(lightL, mode, isHighContrast, ctx.
|
|
1405
|
+
finalL = mapLightnessLight(lightL, mode, isHighContrast, ctx.config);
|
|
1357
1406
|
finalSat = satFactor * ctx.saturation / 100;
|
|
1358
1407
|
} else {
|
|
1359
1408
|
finalL = lightL;
|
|
@@ -1371,7 +1420,7 @@ function resolveShadowForScheme(def, ctx, isDark, isHighContrast) {
|
|
|
1371
1420
|
let fgVariant;
|
|
1372
1421
|
if (def.fg) fgVariant = getSchemeVariant(ctx.resolved.get(def.fg), isDark, isHighContrast);
|
|
1373
1422
|
const intensity = isHighContrast ? pairHC(def.intensity) : pairNormal(def.intensity);
|
|
1374
|
-
const tuning = resolveShadowTuning(def.tuning);
|
|
1423
|
+
const tuning = resolveShadowTuning(def.tuning, ctx.config.shadowTuning);
|
|
1375
1424
|
return computeShadow(bgVariant, fgVariant, intensity, tuning);
|
|
1376
1425
|
}
|
|
1377
1426
|
function variantToLinearRgb(v) {
|
|
@@ -1434,7 +1483,8 @@ function resolveMixForScheme(def, ctx, isDark, isHighContrast) {
|
|
|
1434
1483
|
baseLinearRgb: baseLinear,
|
|
1435
1484
|
targetLinearRgb: targetLinear,
|
|
1436
1485
|
contrast: minCr,
|
|
1437
|
-
luminanceAtValue: luminanceAt
|
|
1486
|
+
luminanceAtValue: luminanceAt,
|
|
1487
|
+
flip: ctx.config.autoFlip
|
|
1438
1488
|
}).value;
|
|
1439
1489
|
}
|
|
1440
1490
|
if (blend === "transparent") return {
|
|
@@ -1495,7 +1545,7 @@ function seedField(order, ctx, field, source) {
|
|
|
1495
1545
|
});
|
|
1496
1546
|
}
|
|
1497
1547
|
}
|
|
1498
|
-
function resolveAllColors(hue, saturation, defs,
|
|
1548
|
+
function resolveAllColors(hue, saturation, defs, config, externalBases) {
|
|
1499
1549
|
validateColorDefs(defs, externalBases);
|
|
1500
1550
|
const order = topoSort(defs);
|
|
1501
1551
|
const ctx = {
|
|
@@ -1503,7 +1553,7 @@ function resolveAllColors(hue, saturation, defs, scaling, externalBases) {
|
|
|
1503
1553
|
saturation,
|
|
1504
1554
|
defs,
|
|
1505
1555
|
resolved: /* @__PURE__ */ new Map(),
|
|
1506
|
-
|
|
1556
|
+
config
|
|
1507
1557
|
};
|
|
1508
1558
|
if (externalBases) for (const [name, color] of externalBases) ctx.resolved.set(name, color);
|
|
1509
1559
|
const lightMap = runPass(order, defs, ctx, false, false, "light");
|
|
@@ -1625,15 +1675,16 @@ function buildCssMap(resolved, prefix, suffix, format) {
|
|
|
1625
1675
|
* Standalone single-color tokens (`glaze.color()` / `glaze.colorFrom()`).
|
|
1626
1676
|
*
|
|
1627
1677
|
* Owns the value-shorthand parser (hex, `rgb()` / `hsl()` / `okhsl()` /
|
|
1628
|
-
* `oklch()`,
|
|
1678
|
+
* `oklch()`, `{ r, g, b }`, `{ h, s, l }`, `{ l, c, h }`), the structured-input
|
|
1629
1679
|
* validator, the two factory paths (value vs structured), and the
|
|
1630
1680
|
* JSON-safe export / rehydration round-trip.
|
|
1631
1681
|
*
|
|
1632
|
-
* Standalone tokens snapshot the
|
|
1633
|
-
*
|
|
1634
|
-
*
|
|
1635
|
-
* `
|
|
1636
|
-
*
|
|
1682
|
+
* Standalone tokens snapshot the full effective config at create time
|
|
1683
|
+
* so later `configure()` calls do not retroactively change exported
|
|
1684
|
+
* tokens. The snapshot is built eagerly in
|
|
1685
|
+
* `buildValueFormConfigOverride()` / `buildStructuredConfigOverride()`.
|
|
1686
|
+
* The token's resolved variants are then memoized on first
|
|
1687
|
+
* `.resolve()` / `.token()` / ... call.
|
|
1637
1688
|
*/
|
|
1638
1689
|
/** Internal name of the user-facing standalone color in the synthesized def map. */
|
|
1639
1690
|
const STANDALONE_VALUE = "value";
|
|
@@ -1648,44 +1699,47 @@ const RESERVED_STANDALONE_NAMES = new Set([
|
|
|
1648
1699
|
STANDALONE_BASE
|
|
1649
1700
|
]);
|
|
1650
1701
|
/**
|
|
1651
|
-
* Build the
|
|
1652
|
-
* pass an explicit `scaling`. All windows are snapshotted from the
|
|
1653
|
-
* current `globalConfig` so later `glaze.configure()` calls don't
|
|
1654
|
-
* retroactively change the resolved variants of an already-created
|
|
1655
|
-
* token (matches the documented "frozen at create time" semantics).
|
|
1702
|
+
* Build the per-token effective config override for a value-form color.
|
|
1656
1703
|
*
|
|
1657
|
-
*
|
|
1658
|
-
*
|
|
1659
|
-
*
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
* so they behave like an ordinary theme color (auto-adapted on both
|
|
1663
|
-
* sides).
|
|
1664
|
-
*/
|
|
1665
|
-
function defaultStandaloneScaling(isString) {
|
|
1704
|
+
* Light window defaults to `false` (preserve input lightness exactly).
|
|
1705
|
+
* All other fields snapshot from global at create time. User override
|
|
1706
|
+
* fields win over all defaults.
|
|
1707
|
+
*/
|
|
1708
|
+
function buildValueFormConfigOverride(userOverride) {
|
|
1666
1709
|
const cfg = getConfig();
|
|
1667
|
-
if (isString) {
|
|
1668
|
-
const [darkLo] = cfg.darkLightness;
|
|
1669
|
-
return {
|
|
1670
|
-
lightLightness: false,
|
|
1671
|
-
darkLightness: [darkLo, 100]
|
|
1672
|
-
};
|
|
1673
|
-
}
|
|
1674
1710
|
return {
|
|
1675
|
-
lightLightness:
|
|
1676
|
-
darkLightness: cfg.darkLightness
|
|
1711
|
+
lightLightness: userOverride?.lightLightness !== void 0 ? userOverride.lightLightness : false,
|
|
1712
|
+
darkLightness: userOverride?.darkLightness !== void 0 ? userOverride.darkLightness : cfg.darkLightness,
|
|
1713
|
+
darkDesaturation: userOverride?.darkDesaturation ?? cfg.darkDesaturation,
|
|
1714
|
+
darkCurve: userOverride?.darkCurve ?? cfg.darkCurve,
|
|
1715
|
+
autoFlip: userOverride?.autoFlip ?? cfg.autoFlip,
|
|
1716
|
+
shadowTuning: userOverride?.shadowTuning ?? cfg.shadowTuning
|
|
1677
1717
|
};
|
|
1678
1718
|
}
|
|
1679
1719
|
/**
|
|
1680
|
-
*
|
|
1681
|
-
*
|
|
1682
|
-
*
|
|
1720
|
+
* Build the per-token effective config override for a structured-form color.
|
|
1721
|
+
*
|
|
1722
|
+
* Both light and dark windows snapshot from global at create time.
|
|
1723
|
+
* User override fields win.
|
|
1683
1724
|
*/
|
|
1684
|
-
function
|
|
1685
|
-
|
|
1725
|
+
function buildStructuredConfigOverride(userOverride) {
|
|
1726
|
+
const cfg = getConfig();
|
|
1727
|
+
return {
|
|
1728
|
+
lightLightness: userOverride?.lightLightness !== void 0 ? userOverride.lightLightness : cfg.lightLightness,
|
|
1729
|
+
darkLightness: userOverride?.darkLightness !== void 0 ? userOverride.darkLightness : cfg.darkLightness,
|
|
1730
|
+
darkDesaturation: userOverride?.darkDesaturation ?? cfg.darkDesaturation,
|
|
1731
|
+
darkCurve: userOverride?.darkCurve ?? cfg.darkCurve,
|
|
1732
|
+
autoFlip: userOverride?.autoFlip ?? cfg.autoFlip,
|
|
1733
|
+
shadowTuning: userOverride?.shadowTuning ?? cfg.shadowTuning
|
|
1734
|
+
};
|
|
1686
1735
|
}
|
|
1687
|
-
|
|
1688
|
-
|
|
1736
|
+
/**
|
|
1737
|
+
* Build the `GlazeConfigResolved` to pass to `resolveAllColors` from a
|
|
1738
|
+
* snapshot override. Uses `defaultConfig()` as the base so all required
|
|
1739
|
+
* fields are present; the snapshot fields win.
|
|
1740
|
+
*/
|
|
1741
|
+
function resolvedConfigFromOverride(override) {
|
|
1742
|
+
return mergeConfig(defaultConfig(), override);
|
|
1689
1743
|
}
|
|
1690
1744
|
/**
|
|
1691
1745
|
* Matches the CSS color functions Glaze itself emits (`rgb()`, `hsl()`,
|
|
@@ -1804,9 +1858,41 @@ function validateOkhslColor(value) {
|
|
|
1804
1858
|
if (!Number.isFinite(h) || !Number.isFinite(s) || !Number.isFinite(l)) throw new Error("glaze.color: OkhslColor h/s/l must be finite numbers.");
|
|
1805
1859
|
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)?");
|
|
1806
1860
|
}
|
|
1807
|
-
/** Validate a user-supplied `
|
|
1808
|
-
function
|
|
1809
|
-
for (const
|
|
1861
|
+
/** Validate a user-supplied `{ r, g, b }` object in 0–255. */
|
|
1862
|
+
function validateRgbColor(value) {
|
|
1863
|
+
for (const key of [
|
|
1864
|
+
"r",
|
|
1865
|
+
"g",
|
|
1866
|
+
"b"
|
|
1867
|
+
]) {
|
|
1868
|
+
const n = value[key];
|
|
1869
|
+
if (!Number.isFinite(n) || n < 0 || n > 255) throw new Error(`glaze.color: RgbColor ${key} must be a finite number in 0–255 (got ${n}).`);
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
/** Validate a user-supplied `{ l, c, h }` OKLCh object. */
|
|
1873
|
+
function validateOklchColor(value) {
|
|
1874
|
+
const { l, c, h } = value;
|
|
1875
|
+
if (!Number.isFinite(l) || !Number.isFinite(c) || !Number.isFinite(h)) throw new Error("glaze.color: OklchColor l/c/h must be finite numbers.");
|
|
1876
|
+
if (l > 1.5 || c > 1.5) throw new Error("glaze.color: OklchColor l/c must be in 0–1 range (matching oklch() strings).");
|
|
1877
|
+
}
|
|
1878
|
+
function oklchComponentsToOkhsl(l, c, hDeg) {
|
|
1879
|
+
const hRad = hDeg * Math.PI / 180;
|
|
1880
|
+
const [h, s, outL] = oklabToOkhsl([
|
|
1881
|
+
l,
|
|
1882
|
+
c * Math.cos(hRad),
|
|
1883
|
+
c * Math.sin(hRad)
|
|
1884
|
+
]);
|
|
1885
|
+
return {
|
|
1886
|
+
h,
|
|
1887
|
+
s,
|
|
1888
|
+
l: outL
|
|
1889
|
+
};
|
|
1890
|
+
}
|
|
1891
|
+
function isRgbColorObject(value) {
|
|
1892
|
+
return "r" in value && "g" in value && "b" in value;
|
|
1893
|
+
}
|
|
1894
|
+
function isOklchColorObject(value) {
|
|
1895
|
+
return "c" in value && "l" in value && "h" in value;
|
|
1810
1896
|
}
|
|
1811
1897
|
/**
|
|
1812
1898
|
* Validate a user-supplied `opacity` override on `glaze.color()`.
|
|
@@ -1852,18 +1938,17 @@ function validateStandaloneName(name) {
|
|
|
1852
1938
|
/**
|
|
1853
1939
|
* Extract an OKHSL color from any `GlazeColorValue` form. Also used by
|
|
1854
1940
|
* `glaze.shadow()` so all shadow inputs (hex, color functions, OKHSL,
|
|
1855
|
-
*
|
|
1941
|
+
* literal objects) go through one parser.
|
|
1856
1942
|
*/
|
|
1857
1943
|
function extractOkhslFromValue(value) {
|
|
1858
1944
|
if (typeof value === "string") return parseColorString(value);
|
|
1859
|
-
if (Array.isArray(value)) {
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
const [r, g, b] = tuple;
|
|
1945
|
+
if (Array.isArray(value)) throw new Error("glaze.color: RGB tuple [r, g, b] is no longer supported — use { r, g, b } instead.");
|
|
1946
|
+
if (isRgbColorObject(value)) {
|
|
1947
|
+
validateRgbColor(value);
|
|
1863
1948
|
const [h, s, l] = srgbToOkhsl([
|
|
1864
|
-
r / 255,
|
|
1865
|
-
g / 255,
|
|
1866
|
-
b / 255
|
|
1949
|
+
value.r / 255,
|
|
1950
|
+
value.g / 255,
|
|
1951
|
+
value.b / 255
|
|
1867
1952
|
]);
|
|
1868
1953
|
return {
|
|
1869
1954
|
h,
|
|
@@ -1871,6 +1956,10 @@ function extractOkhslFromValue(value) {
|
|
|
1871
1956
|
l
|
|
1872
1957
|
};
|
|
1873
1958
|
}
|
|
1959
|
+
if (isOklchColorObject(value)) {
|
|
1960
|
+
validateOklchColor(value);
|
|
1961
|
+
return oklchComponentsToOkhsl(value.l, value.c, value.h);
|
|
1962
|
+
}
|
|
1874
1963
|
validateOkhslColor(value);
|
|
1875
1964
|
return value;
|
|
1876
1965
|
}
|
|
@@ -1878,11 +1967,7 @@ function extractOkhslFromValue(value) {
|
|
|
1878
1967
|
* Build the `ColorMap` for a value-shorthand `glaze.color()` call.
|
|
1879
1968
|
*
|
|
1880
1969
|
* The user-facing color (`STANDALONE_VALUE`) defaults to `mode: 'auto'`
|
|
1881
|
-
* across every value-shorthand form.
|
|
1882
|
-
* extended dark window so a totally-black input renders as totally-white
|
|
1883
|
-
* in dark mode; `OkhslColor` / RGB-tuple inputs auto-adapt into the
|
|
1884
|
-
* snapshotted `globalConfig.lightLightness` / `globalConfig.darkLightness`
|
|
1885
|
-
* windows.
|
|
1970
|
+
* across every value-shorthand form.
|
|
1886
1971
|
*
|
|
1887
1972
|
* When the user requests `contrast` or relative `lightness`, a hidden
|
|
1888
1973
|
* `STANDALONE_SEED` def is synthesized at `mode: 'static'`. That keeps
|
|
@@ -1923,11 +2008,11 @@ function buildStandaloneValueDefs(main, options) {
|
|
|
1923
2008
|
primary
|
|
1924
2009
|
};
|
|
1925
2010
|
}
|
|
1926
|
-
function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary,
|
|
2011
|
+
function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effectiveConfig, baseToken, exportData) {
|
|
1927
2012
|
let cached;
|
|
1928
2013
|
const resolveOnce = () => {
|
|
1929
2014
|
if (cached) return cached;
|
|
1930
|
-
cached = resolveAllColors(seedHue, seedSaturation, defs,
|
|
2015
|
+
cached = resolveAllColors(seedHue, seedSaturation, defs, effectiveConfig, baseToken ? new Map([[STANDALONE_BASE, baseToken.resolve()]]) : void 0);
|
|
1931
2016
|
return cached;
|
|
1932
2017
|
};
|
|
1933
2018
|
const resolveStates = (options) => {
|
|
@@ -1956,17 +2041,43 @@ function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effect
|
|
|
1956
2041
|
};
|
|
1957
2042
|
}
|
|
1958
2043
|
/**
|
|
2044
|
+
* When a value/`from` color links to a base that was created via the
|
|
2045
|
+
* structured form (with explicit `hue`/`saturation`/`lightness`), resolve
|
|
2046
|
+
* that base with `lightLightness: false` for the linking math so the
|
|
2047
|
+
* contrast/lightness anchor matches the input lightness — not the
|
|
2048
|
+
* windowed output. The original base token's `.resolve()` is unaffected.
|
|
2049
|
+
*/
|
|
2050
|
+
function toLinkingBase(base) {
|
|
2051
|
+
if (!base) return void 0;
|
|
2052
|
+
const exp = base.export();
|
|
2053
|
+
if (exp.form !== "structured") return base;
|
|
2054
|
+
const linkingConfig = {
|
|
2055
|
+
...exp.config ?? {},
|
|
2056
|
+
lightLightness: false
|
|
2057
|
+
};
|
|
2058
|
+
return colorFromExport({
|
|
2059
|
+
...exp,
|
|
2060
|
+
config: linkingConfig
|
|
2061
|
+
});
|
|
2062
|
+
}
|
|
2063
|
+
/**
|
|
1959
2064
|
* Resolve `base` (which may be a token reference or a raw color value)
|
|
1960
2065
|
* into a `GlazeColorToken`. Raw values are auto-wrapped via
|
|
1961
|
-
* `
|
|
1962
|
-
* an explicit wrap. Returns `undefined` when no base is provided.
|
|
2066
|
+
* `createColorTokenFromValue` so they pick up the same auto-invert
|
|
2067
|
+
* defaults as an explicit wrap. Returns `undefined` when no base is provided.
|
|
1963
2068
|
*/
|
|
1964
2069
|
function resolveBaseToken(base) {
|
|
1965
2070
|
if (base === void 0) return void 0;
|
|
1966
2071
|
if (isGlazeColorToken(base)) return base;
|
|
1967
2072
|
return createColorTokenFromValue(base, void 0, void 0);
|
|
1968
2073
|
}
|
|
1969
|
-
|
|
2074
|
+
/**
|
|
2075
|
+
* Discriminate a `GlazeColorToken` from a raw `GlazeColorValue`.
|
|
2076
|
+
*/
|
|
2077
|
+
function isGlazeColorToken(candidate) {
|
|
2078
|
+
return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate) && "resolve" in candidate && typeof candidate.resolve === "function";
|
|
2079
|
+
}
|
|
2080
|
+
function createColorToken(input, configOverride) {
|
|
1970
2081
|
validateStructuredInput(input);
|
|
1971
2082
|
const userName = input.name;
|
|
1972
2083
|
if (userName !== void 0) validateStandaloneName(userName);
|
|
@@ -1987,27 +2098,28 @@ function createColorToken(input, scaling) {
|
|
|
1987
2098
|
saturation: 1,
|
|
1988
2099
|
mode: "static"
|
|
1989
2100
|
};
|
|
1990
|
-
const
|
|
2101
|
+
const effectiveConfigOverride = buildStructuredConfigOverride(configOverride);
|
|
2102
|
+
const effectiveConfig = resolvedConfigFromOverride(effectiveConfigOverride);
|
|
1991
2103
|
const exportData = () => ({
|
|
1992
2104
|
form: "structured",
|
|
1993
2105
|
input: buildStructuredInputExport(input),
|
|
1994
|
-
|
|
2106
|
+
config: effectiveConfigOverride
|
|
1995
2107
|
});
|
|
1996
|
-
return createColorTokenFromDefs(input.hue, input.saturation, defs, primary,
|
|
2108
|
+
return createColorTokenFromDefs(input.hue, input.saturation, defs, primary, effectiveConfig, baseToken, exportData);
|
|
1997
2109
|
}
|
|
1998
|
-
function createColorTokenFromValue(value, options,
|
|
1999
|
-
const inputIsString = typeof value === "string";
|
|
2110
|
+
function createColorTokenFromValue(value, options, configOverride) {
|
|
2000
2111
|
const main = extractOkhslFromValue(value);
|
|
2001
|
-
const
|
|
2112
|
+
const linkingBase = toLinkingBase(resolveBaseToken(options?.base));
|
|
2002
2113
|
const { seedHue, seedSaturation, defs, primary } = buildStandaloneValueDefs(main, options);
|
|
2003
|
-
const
|
|
2114
|
+
const effectiveConfigOverride = buildValueFormConfigOverride(configOverride);
|
|
2115
|
+
const effectiveConfig = resolvedConfigFromOverride(effectiveConfigOverride);
|
|
2004
2116
|
const exportData = () => ({
|
|
2005
2117
|
form: "value",
|
|
2006
2118
|
input: value,
|
|
2007
2119
|
...options !== void 0 ? { overrides: buildOverridesExport(options) } : {},
|
|
2008
|
-
|
|
2120
|
+
config: effectiveConfigOverride
|
|
2009
2121
|
});
|
|
2010
|
-
return createColorTokenFromDefs(seedHue, seedSaturation, defs, primary,
|
|
2122
|
+
return createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effectiveConfig, linkingBase, exportData);
|
|
2011
2123
|
}
|
|
2012
2124
|
/**
|
|
2013
2125
|
* Build a JSON-safe snapshot of `GlazeColorOverrides`. `base` is
|
|
@@ -2043,8 +2155,6 @@ function buildStructuredInputExport(input) {
|
|
|
2043
2155
|
}
|
|
2044
2156
|
/**
|
|
2045
2157
|
* Discriminate a `GlazeColorTokenExport` from a raw `GlazeColorValue`.
|
|
2046
|
-
* `GlazeColorTokenExport` always has a `form` field set to either
|
|
2047
|
-
* `'value'` or `'structured'`; raw values never do.
|
|
2048
2158
|
*/
|
|
2049
2159
|
function isExportedToken(candidate) {
|
|
2050
2160
|
return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate) && "form" in candidate && (candidate.form === "value" || candidate.form === "structured");
|
|
@@ -2079,6 +2189,10 @@ function rehydrateStructuredInput(data) {
|
|
|
2079
2189
|
/**
|
|
2080
2190
|
* Rehydrate a token from its `.export()` snapshot. Recursively rebuilds
|
|
2081
2191
|
* any base dependency. Inverse of `GlazeColorToken.export()`.
|
|
2192
|
+
*
|
|
2193
|
+
* The stored `config` field contains the full effective config override
|
|
2194
|
+
* snapshotted at creation time, so the rehydrated token is deterministic
|
|
2195
|
+
* regardless of subsequent `glaze.configure()` calls.
|
|
2082
2196
|
*/
|
|
2083
2197
|
function colorFromExport(data) {
|
|
2084
2198
|
if (data === null || typeof data !== "object") throw new Error(`glaze.colorFrom: expected an object from token.export(), got ${data === null ? "null" : typeof data}.`);
|
|
@@ -2086,9 +2200,9 @@ function colorFromExport(data) {
|
|
|
2086
2200
|
if (data.input === void 0) throw new Error(`glaze.colorFrom: missing "input" field — expected the original ${data.form === "value" ? "GlazeColorValue" : "GlazeColorInput"}.`);
|
|
2087
2201
|
if (data.form === "value") {
|
|
2088
2202
|
const value = data.input;
|
|
2089
|
-
return createColorTokenFromValue(value, data.overrides ? rehydrateOverrides(data.overrides) : void 0, data.
|
|
2203
|
+
return createColorTokenFromValue(value, data.overrides ? rehydrateOverrides(data.overrides) : void 0, data.config);
|
|
2090
2204
|
}
|
|
2091
|
-
return createColorToken(rehydrateStructuredInput(data.input), data.
|
|
2205
|
+
return createColorToken(rehydrateStructuredInput(data.input), data.config);
|
|
2092
2206
|
}
|
|
2093
2207
|
|
|
2094
2208
|
//#endregion
|
|
@@ -2217,21 +2331,32 @@ function createPalette(themes, paletteOptions) {
|
|
|
2217
2331
|
/**
|
|
2218
2332
|
* Theme factory.
|
|
2219
2333
|
*
|
|
2220
|
-
* Wraps a hue/saturation seed
|
|
2221
|
-
*
|
|
2222
|
-
* / `
|
|
2223
|
-
*
|
|
2334
|
+
* Wraps a hue/saturation seed, a mutable `ColorMap`, and an optional
|
|
2335
|
+
* per-theme `GlazeConfigOverride`. Exposes `tokens()` / `tasty()` /
|
|
2336
|
+
* `json()` / `css()` / `resolve()` / `export()` / `extend()`.
|
|
2337
|
+
*
|
|
2338
|
+
* The per-theme config override is **merged over the live global config at
|
|
2339
|
+
* resolve time** so the theme still reacts to later `configure()` calls
|
|
2340
|
+
* for fields it didn't override. The merged config is memoized by
|
|
2341
|
+
* `configVersion` to avoid rebuilding it on every export call.
|
|
2224
2342
|
*/
|
|
2225
|
-
function createTheme(hue, saturation, initialColors) {
|
|
2343
|
+
function createTheme(hue, saturation, initialColors, configOverride) {
|
|
2226
2344
|
let colorDefs = initialColors ? { ...initialColors } : {};
|
|
2227
2345
|
let cache = null;
|
|
2346
|
+
function getEffectiveConfig() {
|
|
2347
|
+
const version = getConfigVersion();
|
|
2348
|
+
if (cache && cache.version === version) return cache.effectiveConfig;
|
|
2349
|
+
return mergeConfig(getConfig(), configOverride);
|
|
2350
|
+
}
|
|
2228
2351
|
function resolveCached() {
|
|
2229
2352
|
const version = getConfigVersion();
|
|
2230
2353
|
if (cache && cache.version === version) return cache.map;
|
|
2231
|
-
const
|
|
2354
|
+
const effectiveConfig = mergeConfig(getConfig(), configOverride);
|
|
2355
|
+
const map = resolveAllColors(hue, saturation, colorDefs, effectiveConfig);
|
|
2232
2356
|
cache = {
|
|
2233
2357
|
map,
|
|
2234
|
-
version
|
|
2358
|
+
version,
|
|
2359
|
+
effectiveConfig
|
|
2235
2360
|
};
|
|
2236
2361
|
return map;
|
|
2237
2362
|
}
|
|
@@ -2273,11 +2398,13 @@ function createTheme(hue, saturation, initialColors) {
|
|
|
2273
2398
|
invalidate();
|
|
2274
2399
|
},
|
|
2275
2400
|
export() {
|
|
2276
|
-
|
|
2401
|
+
const out = {
|
|
2277
2402
|
hue,
|
|
2278
2403
|
saturation,
|
|
2279
2404
|
colors: { ...colorDefs }
|
|
2280
2405
|
};
|
|
2406
|
+
if (configOverride !== void 0) out.config = configOverride;
|
|
2407
|
+
return out;
|
|
2281
2408
|
},
|
|
2282
2409
|
extend(options) {
|
|
2283
2410
|
const newHue = options.hue ?? hue;
|
|
@@ -2287,7 +2414,10 @@ function createTheme(hue, saturation, initialColors) {
|
|
|
2287
2414
|
return createTheme(newHue, newSat, options.colors ? {
|
|
2288
2415
|
...inheritedColors,
|
|
2289
2416
|
...options.colors
|
|
2290
|
-
} : { ...inheritedColors }
|
|
2417
|
+
} : { ...inheritedColors }, configOverride || options.config ? {
|
|
2418
|
+
...configOverride ?? {},
|
|
2419
|
+
...options.config ?? {}
|
|
2420
|
+
} : void 0);
|
|
2291
2421
|
},
|
|
2292
2422
|
resolve() {
|
|
2293
2423
|
return new Map(resolveCached());
|
|
@@ -2297,7 +2427,7 @@ function createTheme(hue, saturation, initialColors) {
|
|
|
2297
2427
|
return buildFlatTokenMap(resolveCached(), "", modes, options?.format);
|
|
2298
2428
|
},
|
|
2299
2429
|
tasty(options) {
|
|
2300
|
-
const cfg =
|
|
2430
|
+
const cfg = getEffectiveConfig();
|
|
2301
2431
|
const states = {
|
|
2302
2432
|
dark: options?.states?.dark ?? cfg.states.dark,
|
|
2303
2433
|
highContrast: options?.states?.highContrast ?? cfg.states.highContrast
|
|
@@ -2332,16 +2462,24 @@ function createTheme(hue, saturation, initialColors) {
|
|
|
2332
2462
|
/**
|
|
2333
2463
|
* Create a single-hue glaze theme.
|
|
2334
2464
|
*
|
|
2465
|
+
* An optional `config` override can be supplied to customize the resolve
|
|
2466
|
+
* behavior for this theme (lightness windows, dark curve, etc.). The
|
|
2467
|
+
* override is **merged over the live global config at resolve time** —
|
|
2468
|
+
* the theme still reacts to later `configure()` calls for fields it
|
|
2469
|
+
* didn't override.
|
|
2470
|
+
*
|
|
2335
2471
|
* @example
|
|
2336
2472
|
* ```ts
|
|
2337
|
-
* const primary = glaze({ hue: 280, saturation: 80 });
|
|
2338
|
-
* // or shorthand:
|
|
2339
2473
|
* const primary = glaze(280, 80);
|
|
2474
|
+
* // or shorthand:
|
|
2475
|
+
* const primary = glaze({ hue: 280, saturation: 80 });
|
|
2476
|
+
* // with config override:
|
|
2477
|
+
* const raw = glaze(280, 80, { lightLightness: false });
|
|
2340
2478
|
* ```
|
|
2341
2479
|
*/
|
|
2342
|
-
function glaze(hueOrOptions, saturation) {
|
|
2343
|
-
if (typeof hueOrOptions === "number") return createTheme(hueOrOptions, saturation ?? 100);
|
|
2344
|
-
return createTheme(hueOrOptions.hue, hueOrOptions.saturation);
|
|
2480
|
+
function glaze(hueOrOptions, saturation, config) {
|
|
2481
|
+
if (typeof hueOrOptions === "number") return createTheme(hueOrOptions, saturation ?? 100, void 0, config);
|
|
2482
|
+
return createTheme(hueOrOptions.hue, hueOrOptions.saturation, void 0, config);
|
|
2345
2483
|
}
|
|
2346
2484
|
/** Configure global glaze settings. */
|
|
2347
2485
|
glaze.configure = function configure$1(config) {
|
|
@@ -2353,63 +2491,72 @@ glaze.palette = function palette(themes, options) {
|
|
|
2353
2491
|
};
|
|
2354
2492
|
/** Create a theme from a serialized export. */
|
|
2355
2493
|
glaze.from = function from(data) {
|
|
2356
|
-
return createTheme(data.hue, data.saturation, data.colors);
|
|
2494
|
+
return createTheme(data.hue, data.saturation, data.colors, data.config);
|
|
2357
2495
|
};
|
|
2358
2496
|
/**
|
|
2359
2497
|
* Create a standalone single-color token.
|
|
2360
2498
|
*
|
|
2361
|
-
*
|
|
2362
|
-
* - `glaze.color(input, scaling?)` — structured form:
|
|
2363
|
-
* `{ hue, saturation, lightness, ... }` plus an optional per-call
|
|
2364
|
-
* lightness-window override.
|
|
2365
|
-
* - `glaze.color(value, overrides?, scaling?)` — value-shorthand: a hex
|
|
2366
|
-
* string (3/6/8 digits), one of the CSS color functions Glaze itself
|
|
2367
|
-
* emits (`rgb()`, `hsl()`, `okhsl()`, `oklch()`), an `OkhslColor`
|
|
2368
|
-
* object `{ h, s, l }` (0–1 ranges), or an `[r, g, b]` (0–255) tuple.
|
|
2499
|
+
* **arg1 — the color** (four accepted shapes, discriminated by structure):
|
|
2369
2500
|
*
|
|
2370
|
-
*
|
|
2371
|
-
*
|
|
2372
|
-
*
|
|
2373
|
-
*
|
|
2374
|
-
*
|
|
2375
|
-
*
|
|
2376
|
-
* exactly; dark Möbius-inverts up to 100, so `glaze.color('#000')`
|
|
2377
|
-
* renders as `#fff` in dark mode (and `glaze.color('#fff')` falls to
|
|
2378
|
-
* the dark `lo` floor).
|
|
2379
|
-
* - `OkhslColor` object / RGB-tuple / structured value-shorthand:
|
|
2380
|
-
* `{ lightLightness: globalConfig.lightLightness, darkLightness:
|
|
2381
|
-
* globalConfig.darkLightness }` — both windows come straight from
|
|
2382
|
-
* `globalConfig`, so the resulting token behaves like a theme color.
|
|
2501
|
+
* | Shape | Example | Notes |
|
|
2502
|
+
* |---|---|---|
|
|
2503
|
+
* | Bare string | `'#26fcb2'`, `'rgb(38 252 178)'` | Hex or CSS color function |
|
|
2504
|
+
* | Value object | `{ h: 152, s: 0.95, l: 0.74 }` | OKHSL, `{r,g,b}`, `{l,c,h}` |
|
|
2505
|
+
* | `{ from, ...overrides }` | `{ from: '#fff', base: bg, contrast: 'AA' }` | Value + color overrides |
|
|
2506
|
+
* | Structured | `{ hue: 152, saturation: 95, lightness: 74 }` | Full theme-style token |
|
|
2383
2507
|
*
|
|
2384
|
-
*
|
|
2385
|
-
*
|
|
2386
|
-
*
|
|
2508
|
+
* **arg2 — config override** (optional, all shapes):
|
|
2509
|
+
* Overrides the resolve-relevant global config fields for this token.
|
|
2510
|
+
* Fields that are omitted fall through to the live global config at
|
|
2511
|
+
* create time (and are snapshotted). Pass `false` for a lightness window
|
|
2512
|
+
* to disable clamping entirely.
|
|
2387
2513
|
*
|
|
2388
|
-
*
|
|
2389
|
-
*
|
|
2390
|
-
*
|
|
2391
|
-
* `GlazeColorToken`) to anchor `contrast` and relative `lightness`
|
|
2392
|
-
* against another color's resolved variant per scheme instead. Relative
|
|
2393
|
-
* `hue: '+N'` always anchors to the seed.
|
|
2514
|
+
* ```ts
|
|
2515
|
+
* // Bare string — no overrides
|
|
2516
|
+
* glaze.color('#26fcb2')
|
|
2394
2517
|
*
|
|
2395
|
-
*
|
|
2396
|
-
*
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2518
|
+
* // From form — value + color overrides
|
|
2519
|
+
* glaze.color({ from: '#fff', base: bg, contrast: 'AA' })
|
|
2520
|
+
*
|
|
2521
|
+
* // Structured form — full theme-style token
|
|
2522
|
+
* glaze.color({ hue: 152, saturation: 95, lightness: 74 })
|
|
2523
|
+
*
|
|
2524
|
+
* // Config override on any form
|
|
2525
|
+
* glaze.color('#26fcb2', { darkLightness: false, autoFlip: false })
|
|
2526
|
+
* glaze.color({ from: '#fff', base: bg }, { darkCurve: 0.3 })
|
|
2527
|
+
* ```
|
|
2528
|
+
*
|
|
2529
|
+
* Defaults: every form defaults to `mode: 'auto'`. Value-shorthand forms
|
|
2530
|
+
* (bare strings and value objects) preserve light lightness exactly
|
|
2531
|
+
* (`lightLightness: false` internally). Structured form snapshots both
|
|
2532
|
+
* lightness windows from `globalConfig` at create time.
|
|
2533
|
+
*
|
|
2534
|
+
* Relative `lightness: '+N'` and `contrast` anchor to the literal seed by
|
|
2535
|
+
* default; when `base` is set they anchor to the base's resolved variant
|
|
2536
|
+
* per scheme. Relative `hue: '+N'` always anchors to the seed, not the base.
|
|
2537
|
+
*/
|
|
2538
|
+
glaze.color = function color(input, config) {
|
|
2539
|
+
if (typeof input === "string") return createColorTokenFromValue(input, void 0, config);
|
|
2540
|
+
const obj = input;
|
|
2541
|
+
if ("from" in obj) {
|
|
2542
|
+
const { from, ...overrides } = input;
|
|
2543
|
+
return createColorTokenFromValue(from, overrides, config);
|
|
2544
|
+
}
|
|
2545
|
+
if ("hue" in obj) return createColorToken(input, config);
|
|
2546
|
+
return createColorTokenFromValue(input, void 0, config);
|
|
2401
2547
|
};
|
|
2402
2548
|
/**
|
|
2403
2549
|
* Compute a shadow color from a bg/fg pair and intensity.
|
|
2404
2550
|
*
|
|
2405
2551
|
* Both `bg` and `fg` accept any `GlazeColorValue` form: hex (`#rgb` /
|
|
2406
2552
|
* `#rrggbb` / `#rrggbbaa`), `rgb()` / `hsl()` / `okhsl()` / `oklch()`
|
|
2407
|
-
* strings, `
|
|
2553
|
+
* strings, or `{ r, g, b }` / `{ h, s, l }` / `{ l, c, h }` objects.
|
|
2408
2554
|
*/
|
|
2409
2555
|
glaze.shadow = function shadow(input) {
|
|
2410
2556
|
const bg = extractOkhslFromValue(input.bg);
|
|
2411
2557
|
const fg = input.fg ? extractOkhslFromValue(input.fg) : void 0;
|
|
2412
|
-
const
|
|
2558
|
+
const cfg = getConfig();
|
|
2559
|
+
const tuning = resolveShadowTuning(input.tuning, cfg.shadowTuning);
|
|
2413
2560
|
return computeShadow({
|
|
2414
2561
|
...bg,
|
|
2415
2562
|
alpha: 1
|
|
@@ -2449,12 +2596,12 @@ glaze.fromRgb = function fromRgb(r, g, b) {
|
|
|
2449
2596
|
*
|
|
2450
2597
|
* The snapshot is a plain JSON-safe object containing the original
|
|
2451
2598
|
* input value, overrides (with any `base` token recursively serialized),
|
|
2452
|
-
* and the
|
|
2453
|
-
* behavior to the original at the time of export.
|
|
2599
|
+
* and the effective config snapshot. The reconstructed token is identical
|
|
2600
|
+
* in behavior to the original at the time of export.
|
|
2454
2601
|
*
|
|
2455
2602
|
* @example
|
|
2456
2603
|
* ```ts
|
|
2457
|
-
* const text = glaze.color('#1a1a1a',
|
|
2604
|
+
* const text = glaze.color({ from: '#1a1a1a', contrast: 'AA' });
|
|
2458
2605
|
* const data = text.export(); // JSON-safe
|
|
2459
2606
|
* localStorage.setItem('text', JSON.stringify(data));
|
|
2460
2607
|
* // ...later...
|