@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.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 darkerResult = preferredLightness > minL ? searchBranch(hue, saturation, minL, preferredLightness, yBase, searchTarget, epsilon, maxIterations, preferredLightness) : null;
829
- const lighterResult = preferredLightness < maxL ? searchBranch(hue, saturation, preferredLightness, maxL, yBase, searchTarget, epsilon, maxIterations, preferredLightness) : null;
830
- if (darkerResult) darkerResult.met = darkerResult.contrast >= target;
831
- if (lighterResult) lighterResult.met = lighterResult.contrast >= target;
832
- const darkerPasses = darkerResult?.met ?? false;
833
- const lighterPasses = lighterResult?.met ?? false;
834
- if (darkerPasses && lighterPasses) {
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
- candidates.sort((a, b) => b.contrast - a.contrast);
868
- return candidates[0];
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 darkerResult = preferredValue > 0 ? searchMixBranch(0, preferredValue, yBase, searchTarget, epsilon, maxIterations, preferredValue, luminanceAtValue) : null;
951
- const lighterResult = preferredValue < 1 ? searchMixBranch(preferredValue, 1, yBase, searchTarget, epsilon, maxIterations, preferredValue, luminanceAtValue) : null;
952
- if (darkerResult) darkerResult.met = darkerResult.contrast >= target;
953
- if (lighterResult) lighterResult.met = lighterResult.contrast >= target;
954
- const darkerPasses = darkerResult?.met ?? false;
955
- const lighterPasses = lighterResult?.met ?? false;
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
- candidates.sort((a, b) => b.contrast - a.contrast);
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: candidates[0].lightness,
995
- contrast: candidates[0].contrast,
996
- met: candidates[0].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 (with per-call scaling
1079
- * overrides and high-contrast handling), the Möbius curve used by the
1080
- * `'auto'` dark adaptation, and the saturation-desaturation reducer
1081
- * for dark mode.
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]` (existing behavior, predates per-call overrides).
1086
- * - Otherwise, per-call `scaling` (e.g. from `glaze.color()`'s third arg) wins;
1087
- * `false` is interpreted as `[0, 100]` (no remap). Falls back to `globalConfig.*Lightness`.
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, scaling) {
1135
+ function lightnessWindow(isHighContrast, kind, config) {
1090
1136
  if (isHighContrast) return [0, 100];
1091
- if (scaling) {
1092
- const override = kind === "dark" ? scaling.darkLightness : scaling.lightLightness;
1093
- if (override === false) return [0, 100];
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, scaling) {
1141
+ function mapLightnessLight(l, mode, isHighContrast, config) {
1100
1142
  if (mode === "static") return l;
1101
- const [lo, hi] = lightnessWindow(isHighContrast, "light", scaling);
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, scaling) {
1150
+ function mapLightnessDark(l, mode, isHighContrast, config) {
1109
1151
  if (mode === "static") return l;
1110
- const cfg = getConfig();
1111
- const beta = isHighContrast ? pairHC(cfg.darkCurve) : pairNormal(cfg.darkCurve);
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", scaling);
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, scaling) {
1119
- const cfg = getConfig();
1120
- const beta = isHighContrast ? pairHC(cfg.darkCurve) : pairNormal(cfg.darkCurve);
1121
- const [lightLo, lightHi] = lightnessWindow(isHighContrast, "light", scaling);
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 - getConfig().darkDesaturation);
1168
+ return s * (1 - config.darkDesaturation);
1129
1169
  }
1130
- function schemeLightnessRange(isDark, mode, isHighContrast, scaling) {
1170
+ function schemeLightnessRange(isDark, mode, isHighContrast, config) {
1131
1171
  if (mode === "static") return [0, 1];
1132
- const [lo, hi] = lightnessWindow(isHighContrast, isDark ? "dark" : "light", scaling);
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.scaling);
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.scaling);
1302
- else preferredL = mapLightnessLight(parsed.value, mode, isHighContrast, ctx.scaling);
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.scaling);
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.scaling);
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.scaling);
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, scaling, externalBases) {
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
- scaling
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()`, OkhslColor object, [r, g, b] tuple), the structured-input
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 relevant `globalConfig` fields at
1633
- * create time so later `configure()` calls do not retroactively change
1634
- * exported tokens the snapshot is captured eagerly in
1635
- * `defaultStandaloneScaling()`. The token's resolved variants are then
1636
- * memoized on first `.resolve()` / `.token()` / ... call.
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 create-time scaling snapshot used when the caller did not
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
- * String value-shorthand inputs preserve their light lightness exactly
1658
- * (`lightLightness: false`) and use an extended dark window
1659
- * `[globalConfig.darkLightness[0], 100]` so a totally-black input can
1660
- * Möbius-invert to totally-white in dark mode. Object / tuple /
1661
- * structured inputs snapshot both windows from `globalConfig` verbatim
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: cfg.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
- * Discriminate a `GlazeColorToken` from a raw `GlazeColorValue`.
1681
- * Used to widen `base?` so it accepts either a token reference or a
1682
- * raw value (auto-wrapped into `glaze.color(value)`).
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 isGlazeColorToken(candidate) {
1685
- return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate) && "resolve" in candidate && typeof candidate.resolve === "function";
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
- function isStructuredColorInput(input) {
1688
- return typeof input === "object" && input !== null && !Array.isArray(input) && "hue" in input && "lightness" in input;
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 `[r, g, b]` tuple in 0-255. */
1808
- function validateRgbTuple(value) {
1809
- for (const n of value) if (!Number.isFinite(n) || n < 0 || n > 255) throw new Error(`glaze.color: RGB tuple components must be finite numbers in 0–255 (got [${value.join(", ")}]).`);
1861
+ /** Validate a user-supplied `{ r, g, b }` object in 0255. */
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
- * RGB tuple) go through one parser.
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
- const tuple = value;
1861
- validateRgbTuple(tuple);
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. String inputs pair with the
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, effectiveScaling, baseToken, exportData) {
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, effectiveScaling, baseToken ? new Map([[STANDALONE_BASE, baseToken.resolve()]]) : void 0);
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
- * `glaze.color(value)` so they pick up the same auto-invert defaults as
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
- function createColorToken(input, scaling) {
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 effectiveScaling = scaling ?? defaultStandaloneScaling(false);
2101
+ const effectiveConfigOverride = buildStructuredConfigOverride(configOverride);
2102
+ const effectiveConfig = resolvedConfigFromOverride(effectiveConfigOverride);
1991
2103
  const exportData = () => ({
1992
2104
  form: "structured",
1993
2105
  input: buildStructuredInputExport(input),
1994
- scaling: effectiveScaling
2106
+ config: effectiveConfigOverride
1995
2107
  });
1996
- return createColorTokenFromDefs(input.hue, input.saturation, defs, primary, effectiveScaling, baseToken, exportData);
2108
+ return createColorTokenFromDefs(input.hue, input.saturation, defs, primary, effectiveConfig, baseToken, exportData);
1997
2109
  }
1998
- function createColorTokenFromValue(value, options, scaling) {
1999
- const inputIsString = typeof value === "string";
2110
+ function createColorTokenFromValue(value, options, configOverride) {
2000
2111
  const main = extractOkhslFromValue(value);
2001
- const baseToken = resolveBaseToken(options?.base);
2112
+ const linkingBase = toLinkingBase(resolveBaseToken(options?.base));
2002
2113
  const { seedHue, seedSaturation, defs, primary } = buildStandaloneValueDefs(main, options);
2003
- const effectiveScaling = scaling ?? defaultStandaloneScaling(inputIsString);
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
- scaling: effectiveScaling
2120
+ config: effectiveConfigOverride
2009
2121
  });
2010
- return createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effectiveScaling, baseToken, exportData);
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.scaling);
2203
+ return createColorTokenFromValue(value, data.overrides ? rehydrateOverrides(data.overrides) : void 0, data.config);
2090
2204
  }
2091
- return createColorToken(rehydrateStructuredInput(data.input), data.scaling);
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 and a mutable `ColorMap`, and exposes
2221
- * `tokens()` / `tasty()` / `json()` / `css()` / `resolve()` / `export()`
2222
- * / `extend()`. Caches the last resolve result so successive exports
2223
- * with the same defs and config don't re-run the four-pass resolver.
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 map = resolveAllColors(hue, saturation, colorDefs);
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
- return {
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 = getConfig();
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
- * Two overloads:
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
- * Defaults: every input form defaults to `mode: 'auto'` so colors
2371
- * automatically adapt between light and dark like an ordinary theme
2372
- * color. The scaling snapshot taken at create time differs by input
2373
- * form:
2374
- * - String value-shorthand: `{ lightLightness: false, darkLightness:
2375
- * [globalConfig.darkLightness[0], 100] }`. Light preserves the input
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
- * Pass `{ mode: 'fixed' }` to opt back into the legacy linear, non-
2385
- * inverting mapping, or `{ mode: 'static' }` to pin the same lightness
2386
- * across every variant.
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
- * Relative `lightness: '+N'` and `contrast: <ratio>` are anchored to
2389
- * the literal seed (the value passed in) by default, pinned at
2390
- * `mode: 'static'` across all four variants. Pass `overrides.base` (a
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
- * Alpha components in `rgba()` / `hsla()` / slash-alpha syntax and
2396
- * 8-digit hex are parsed but dropped with a `console.warn`.
2397
- */
2398
- glaze.color = function color(input, arg2, arg3) {
2399
- if (isStructuredColorInput(input)) return createColorToken(input, arg2);
2400
- return createColorTokenFromValue(input, arg2, arg3);
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, `OkhslColor` objects, or `[r, g, b]` (0–255) tuples.
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 tuning = resolveShadowTuning(input.tuning);
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 captured scaling. The reconstructed token is identical in
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', { contrast: 'AA' });
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...