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