@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 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 darkerResult = preferredLightness > minL ? searchBranch(hue, saturation, minL, preferredLightness, yBase, searchTarget, epsilon, maxIterations, preferredLightness) : null;
831
- const lighterResult = preferredLightness < maxL ? searchBranch(hue, saturation, preferredLightness, maxL, yBase, searchTarget, epsilon, maxIterations, preferredLightness) : null;
832
- if (darkerResult) darkerResult.met = darkerResult.contrast >= target;
833
- if (lighterResult) lighterResult.met = lighterResult.contrast >= target;
834
- const darkerPasses = darkerResult?.met ?? false;
835
- const lighterPasses = lighterResult?.met ?? false;
836
- if (darkerPasses && lighterPasses) {
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
- candidates.sort((a, b) => b.contrast - a.contrast);
870
- return candidates[0];
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 darkerResult = preferredValue > 0 ? searchMixBranch(0, preferredValue, yBase, searchTarget, epsilon, maxIterations, preferredValue, luminanceAtValue) : null;
953
- const lighterResult = preferredValue < 1 ? searchMixBranch(preferredValue, 1, yBase, searchTarget, epsilon, maxIterations, preferredValue, luminanceAtValue) : null;
954
- if (darkerResult) darkerResult.met = darkerResult.contrast >= target;
955
- if (lighterResult) lighterResult.met = lighterResult.contrast >= target;
956
- const darkerPasses = darkerResult?.met ?? false;
957
- const lighterPasses = lighterResult?.met ?? false;
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
- candidates.sort((a, b) => b.contrast - a.contrast);
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: candidates[0].lightness,
997
- contrast: candidates[0].contrast,
998
- met: candidates[0].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 (with per-call scaling
1081
- * overrides and high-contrast handling), the Möbius curve used by the
1082
- * `'auto'` dark adaptation, and the saturation-desaturation reducer
1083
- * for dark mode.
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]` (existing behavior, predates per-call overrides).
1088
- * - Otherwise, per-call `scaling` (e.g. from `glaze.color()`'s third arg) wins;
1089
- * `false` is interpreted as `[0, 100]` (no remap). Falls back to `globalConfig.*Lightness`.
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, scaling) {
1137
+ function lightnessWindow(isHighContrast, kind, config) {
1092
1138
  if (isHighContrast) return [0, 100];
1093
- if (scaling) {
1094
- const override = kind === "dark" ? scaling.darkLightness : scaling.lightLightness;
1095
- if (override === false) return [0, 100];
1096
- if (override !== void 0) return override;
1097
- }
1098
- const cfg = getConfig();
1099
- return kind === "dark" ? cfg.darkLightness : cfg.lightLightness;
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, scaling) {
1143
+ function mapLightnessLight(l, mode, isHighContrast, config) {
1102
1144
  if (mode === "static") return l;
1103
- const [lo, hi] = lightnessWindow(isHighContrast, "light", scaling);
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, scaling) {
1152
+ function mapLightnessDark(l, mode, isHighContrast, config) {
1111
1153
  if (mode === "static") return l;
1112
- const cfg = getConfig();
1113
- const beta = isHighContrast ? pairHC(cfg.darkCurve) : pairNormal(cfg.darkCurve);
1114
- const [darkLo, darkHi] = lightnessWindow(isHighContrast, "dark", scaling);
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", scaling);
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, scaling) {
1121
- const cfg = getConfig();
1122
- const beta = isHighContrast ? pairHC(cfg.darkCurve) : pairNormal(cfg.darkCurve);
1123
- const [lightLo, lightHi] = lightnessWindow(isHighContrast, "light", scaling);
1124
- const [darkLo, darkHi] = lightnessWindow(isHighContrast, "dark", scaling);
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 - getConfig().darkDesaturation);
1170
+ return s * (1 - config.darkDesaturation);
1131
1171
  }
1132
- function schemeLightnessRange(isDark, mode, isHighContrast, scaling) {
1172
+ function schemeLightnessRange(isDark, mode, isHighContrast, config) {
1133
1173
  if (mode === "static") return [0, 1];
1134
- const [lo, hi] = lightnessWindow(isHighContrast, isDark ? "dark" : "light", scaling);
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.scaling);
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.scaling);
1304
- else preferredL = mapLightnessLight(parsed.value, mode, isHighContrast, ctx.scaling);
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.scaling);
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.scaling);
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.scaling);
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, scaling, externalBases) {
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
- scaling
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()`, OkhslColor object, [r, g, b] tuple), the structured-input
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 relevant `globalConfig` fields at
1635
- * create time so later `configure()` calls do not retroactively change
1636
- * exported tokens the snapshot is captured eagerly in
1637
- * `defaultStandaloneScaling()`. The token's resolved variants are then
1638
- * memoized on first `.resolve()` / `.token()` / ... call.
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 create-time scaling snapshot used when the caller did not
1654
- * pass an explicit `scaling`. All windows are snapshotted from the
1655
- * current `globalConfig` so later `glaze.configure()` calls don't
1656
- * retroactively change the resolved variants of an already-created
1657
- * token (matches the documented "frozen at create time" semantics).
1704
+ * Build the per-token effective config override for a value-form color.
1658
1705
  *
1659
- * String value-shorthand inputs preserve their light lightness exactly
1660
- * (`lightLightness: false`) and use an extended dark window
1661
- * `[globalConfig.darkLightness[0], 100]` so a totally-black input can
1662
- * Möbius-invert to totally-white in dark mode. Object / tuple /
1663
- * structured inputs snapshot both windows from `globalConfig` verbatim
1664
- * so they behave like an ordinary theme color (auto-adapted on both
1665
- * sides).
1666
- */
1667
- function defaultStandaloneScaling(isString) {
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: cfg.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
- * Discriminate a `GlazeColorToken` from a raw `GlazeColorValue`.
1683
- * Used to widen `base?` so it accepts either a token reference or a
1684
- * raw value (auto-wrapped into `glaze.color(value)`).
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 isGlazeColorToken(candidate) {
1687
- return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate) && "resolve" in candidate && typeof candidate.resolve === "function";
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
- function isStructuredColorInput(input) {
1690
- return typeof input === "object" && input !== null && !Array.isArray(input) && "hue" in input && "lightness" in input;
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 `[r, g, b]` tuple in 0-255. */
1810
- function validateRgbTuple(value) {
1811
- for (const n of value) if (!Number.isFinite(n) || n < 0 || n > 255) throw new Error(`glaze.color: RGB tuple components must be finite numbers in 0–255 (got [${value.join(", ")}]).`);
1863
+ /** Validate a user-supplied `{ r, g, b }` object in 0255. */
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
- * RGB tuple) go through one parser.
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
- const tuple = value;
1863
- validateRgbTuple(tuple);
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. String inputs pair with the
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, effectiveScaling, baseToken, exportData) {
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, effectiveScaling, baseToken ? new Map([[STANDALONE_BASE, baseToken.resolve()]]) : void 0);
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
- * `glaze.color(value)` so they pick up the same auto-invert defaults as
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
- function createColorToken(input, scaling) {
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 effectiveScaling = scaling ?? defaultStandaloneScaling(false);
2103
+ const effectiveConfigOverride = buildStructuredConfigOverride(configOverride);
2104
+ const effectiveConfig = resolvedConfigFromOverride(effectiveConfigOverride);
1993
2105
  const exportData = () => ({
1994
2106
  form: "structured",
1995
2107
  input: buildStructuredInputExport(input),
1996
- scaling: effectiveScaling
2108
+ config: effectiveConfigOverride
1997
2109
  });
1998
- return createColorTokenFromDefs(input.hue, input.saturation, defs, primary, effectiveScaling, baseToken, exportData);
2110
+ return createColorTokenFromDefs(input.hue, input.saturation, defs, primary, effectiveConfig, baseToken, exportData);
1999
2111
  }
2000
- function createColorTokenFromValue(value, options, scaling) {
2001
- const inputIsString = typeof value === "string";
2112
+ function createColorTokenFromValue(value, options, configOverride) {
2002
2113
  const main = extractOkhslFromValue(value);
2003
- const baseToken = resolveBaseToken(options?.base);
2114
+ const linkingBase = toLinkingBase(resolveBaseToken(options?.base));
2004
2115
  const { seedHue, seedSaturation, defs, primary } = buildStandaloneValueDefs(main, options);
2005
- const effectiveScaling = scaling ?? defaultStandaloneScaling(inputIsString);
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
- scaling: effectiveScaling
2122
+ config: effectiveConfigOverride
2011
2123
  });
2012
- return createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effectiveScaling, baseToken, exportData);
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.scaling);
2205
+ return createColorTokenFromValue(value, data.overrides ? rehydrateOverrides(data.overrides) : void 0, data.config);
2092
2206
  }
2093
- return createColorToken(rehydrateStructuredInput(data.input), data.scaling);
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 and a mutable `ColorMap`, and exposes
2223
- * `tokens()` / `tasty()` / `json()` / `css()` / `resolve()` / `export()`
2224
- * / `extend()`. Caches the last resolve result so successive exports
2225
- * with the same defs and config don't re-run the four-pass resolver.
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 map = resolveAllColors(hue, saturation, colorDefs);
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
- return {
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 = getConfig();
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
- * Two overloads:
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
- * Defaults: every input form defaults to `mode: 'auto'` so colors
2373
- * automatically adapt between light and dark like an ordinary theme
2374
- * color. The scaling snapshot taken at create time differs by input
2375
- * form:
2376
- * - String value-shorthand: `{ lightLightness: false, darkLightness:
2377
- * [globalConfig.darkLightness[0], 100] }`. Light preserves the input
2378
- * exactly; dark Möbius-inverts up to 100, so `glaze.color('#000')`
2379
- * renders as `#fff` in dark mode (and `glaze.color('#fff')` falls to
2380
- * the dark `lo` floor).
2381
- * - `OkhslColor` object / RGB-tuple / structured value-shorthand:
2382
- * `{ lightLightness: globalConfig.lightLightness, darkLightness:
2383
- * globalConfig.darkLightness }` — both windows come straight from
2384
- * `globalConfig`, so the resulting token behaves like a theme color.
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
- * Pass `{ mode: 'fixed' }` to opt back into the legacy linear, non-
2387
- * inverting mapping, or `{ mode: 'static' }` to pin the same lightness
2388
- * across every variant.
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
- * Relative `lightness: '+N'` and `contrast: <ratio>` are anchored to
2391
- * the literal seed (the value passed in) by default, pinned at
2392
- * `mode: 'static'` across all four variants. Pass `overrides.base` (a
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
- * Alpha components in `rgba()` / `hsla()` / slash-alpha syntax and
2398
- * 8-digit hex are parsed but dropped with a `console.warn`.
2399
- */
2400
- glaze.color = function color(input, arg2, arg3) {
2401
- if (isStructuredColorInput(input)) return createColorToken(input, arg2);
2402
- return createColorTokenFromValue(input, arg2, arg3);
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, `OkhslColor` objects, or `[r, g, b]` (0–255) tuples.
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 tuning = resolveShadowTuning(input.tuning);
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 captured scaling. The reconstructed token is identical in
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', { contrast: 'AA' });
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...