@tenphi/glaze 0.10.1 → 0.11.1

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
@@ -558,6 +558,116 @@ function formatOklch(h, s, l) {
558
558
  return `oklch(${fmt$1(L, 4)} ${fmt$1(C, 4)} ${fmt$1(hh, 2)})`;
559
559
  }
560
560
 
561
+ //#endregion
562
+ //#region src/config.ts
563
+ /**
564
+ * Build a fresh defaults object. Called from module init and from
565
+ * `resetConfig()` so the two paths can't drift.
566
+ */
567
+ function defaultConfig() {
568
+ return {
569
+ lightLightness: [10, 100],
570
+ darkLightness: [15, 95],
571
+ darkDesaturation: .1,
572
+ darkCurve: .5,
573
+ states: {
574
+ dark: "@dark",
575
+ highContrast: "@high-contrast"
576
+ },
577
+ modes: {
578
+ dark: true,
579
+ highContrast: false
580
+ }
581
+ };
582
+ }
583
+ let globalConfig = defaultConfig();
584
+ /**
585
+ * Monotonic counter incremented on every `configure()` / `resetConfig()`
586
+ * call. Theme / palette caches read this to invalidate stale resolve
587
+ * results when the config changes between exports.
588
+ */
589
+ let configVersion = 0;
590
+ /** Live reference to the current config. Mutated by `configure()` / `resetConfig()`. */
591
+ function getConfig() {
592
+ return globalConfig;
593
+ }
594
+ function getConfigVersion() {
595
+ return configVersion;
596
+ }
597
+ /**
598
+ * Public-facing snapshot used by `glaze.getConfig()`. Returns a shallow
599
+ * copy so callers can't mutate the live config.
600
+ */
601
+ function snapshotConfig() {
602
+ return { ...globalConfig };
603
+ }
604
+ function configure(config) {
605
+ configVersion++;
606
+ globalConfig = {
607
+ lightLightness: config.lightLightness ?? globalConfig.lightLightness,
608
+ darkLightness: config.darkLightness ?? globalConfig.darkLightness,
609
+ darkDesaturation: config.darkDesaturation ?? globalConfig.darkDesaturation,
610
+ darkCurve: config.darkCurve ?? globalConfig.darkCurve,
611
+ states: {
612
+ dark: config.states?.dark ?? globalConfig.states.dark,
613
+ highContrast: config.states?.highContrast ?? globalConfig.states.highContrast
614
+ },
615
+ modes: {
616
+ dark: config.modes?.dark ?? globalConfig.modes.dark,
617
+ highContrast: config.modes?.highContrast ?? globalConfig.modes.highContrast
618
+ },
619
+ shadowTuning: config.shadowTuning ?? globalConfig.shadowTuning
620
+ };
621
+ }
622
+ function resetConfig() {
623
+ configVersion++;
624
+ globalConfig = defaultConfig();
625
+ }
626
+
627
+ //#endregion
628
+ //#region src/hc-pair.ts
629
+ function pairNormal(p) {
630
+ return Array.isArray(p) ? p[0] : p;
631
+ }
632
+ function pairHC(p) {
633
+ return Array.isArray(p) ? p[1] : p;
634
+ }
635
+ function clamp(v, min, max) {
636
+ return Math.max(min, Math.min(max, v));
637
+ }
638
+ /**
639
+ * Parse a value that can be absolute (number) or relative (signed string).
640
+ * Returns the numeric value and whether it's relative.
641
+ */
642
+ function parseRelativeOrAbsolute(value) {
643
+ if (typeof value === "number") return {
644
+ value,
645
+ relative: false
646
+ };
647
+ return {
648
+ value: parseFloat(value),
649
+ relative: true
650
+ };
651
+ }
652
+ /**
653
+ * Compute the effective hue for a color, given the theme seed hue
654
+ * and an optional per-color hue override.
655
+ */
656
+ function resolveEffectiveHue(seedHue, defHue) {
657
+ if (defHue === void 0) return seedHue;
658
+ const parsed = parseRelativeOrAbsolute(defHue);
659
+ if (parsed.relative) return ((seedHue + parsed.value) % 360 + 360) % 360;
660
+ return (parsed.value % 360 + 360) % 360;
661
+ }
662
+ /**
663
+ * Check whether a lightness value represents an absolute root definition
664
+ * (i.e. a number, not a relative string).
665
+ */
666
+ function isAbsoluteLightness(lightness) {
667
+ if (lightness === void 0) return false;
668
+ return typeof (Array.isArray(lightness) ? lightness[0] : lightness) === "number";
669
+ }
670
+
561
671
  //#endregion
562
672
  //#region src/contrast-solver.ts
563
673
  /**
@@ -888,107 +998,15 @@ function findValueForMixContrast(options) {
888
998
  }
889
999
 
890
1000
  //#endregion
891
- //#region src/glaze.ts
892
- /**
893
- * Glaze — OKHSL-based color theme generator.
894
- *
895
- * Generates robust light, dark, and high-contrast colors from a hue/saturation
896
- * seed, preserving contrast for UI pairs via explicit dependencies.
897
- */
898
- /** Internal name of the user-facing standalone color in the synthesized def map. */
899
- const STANDALONE_VALUE = "value";
900
- /** Internal name of the hidden static-anchor seed used for relative lightness / contrast. */
901
- const STANDALONE_SEED = "seed";
902
- /** Internal name of an externally-resolved `GlazeColorToken` injected as a base reference. */
903
- const STANDALONE_BASE = "externalBase";
1001
+ //#region src/shadow.ts
904
1002
  /**
905
- * Build the create-time scaling snapshot used when the caller did not
906
- * pass an explicit `scaling`. All windows are snapshotted from the
907
- * current `globalConfig` so later `glaze.configure()` calls don't
908
- * retroactively change the resolved variants of an already-created
909
- * token (matches the documented "frozen at create time" semantics).
1003
+ * Shadow color computation.
910
1004
  *
911
- * String value-shorthand inputs use an extended dark window
912
- * `[globalConfig.darkLightness[0], 100]` so a totally-black input can
913
- * Möbius-invert to totally-white in dark mode; object / tuple /
914
- * structured inputs use `globalConfig.darkLightness` verbatim.
915
- */
916
- function defaultStandaloneScaling(extendDark) {
917
- const [lo, hi] = globalConfig.darkLightness;
918
- return {
919
- lightLightness: false,
920
- darkLightness: extendDark ? [lo, 100] : [lo, hi]
921
- };
922
- }
923
- /** Reserved internal names that user-supplied `name` must not collide with. */
924
- const RESERVED_STANDALONE_NAMES = new Set([
925
- STANDALONE_VALUE,
926
- STANDALONE_SEED,
927
- STANDALONE_BASE
928
- ]);
929
- /**
930
- * Discriminate a `GlazeColorToken` from a raw `GlazeColorValue`.
931
- * Used to widen `base?` so it accepts either a token reference or a
932
- * raw value (auto-wrapped into `glaze.color(value)`).
933
- */
934
- function isGlazeColorToken(candidate) {
935
- return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate) && "resolve" in candidate && typeof candidate.resolve === "function";
936
- }
937
- let globalConfig = {
938
- lightLightness: [10, 100],
939
- darkLightness: [15, 95],
940
- darkDesaturation: .1,
941
- darkCurve: .5,
942
- states: {
943
- dark: "@dark",
944
- highContrast: "@high-contrast"
945
- },
946
- modes: {
947
- dark: true,
948
- highContrast: false
949
- }
950
- };
951
- function pairNormal(p) {
952
- return Array.isArray(p) ? p[0] : p;
953
- }
954
- function pairHC(p) {
955
- return Array.isArray(p) ? p[1] : p;
956
- }
957
- /**
958
- * Dedupe contrast warnings within a single process. The cache survives
959
- * the lifetime of a token because tokens memoize their resolution; the
960
- * limit is a soft cap to keep noise bounded across long-lived sessions
961
- * (e.g. dev servers with HMR re-resolving themes repeatedly).
962
- */
963
- const CONTRAST_WARN_CACHE_LIMIT = 256;
964
- const contrastWarnCache = /* @__PURE__ */ new Set();
965
- function schemeLabel(isDark, isHighContrast) {
966
- if (isDark && isHighContrast) return "darkContrast";
967
- if (isDark) return "dark";
968
- if (isHighContrast) return "lightContrast";
969
- return "light";
970
- }
971
- function formatContrastTarget(input, ratio) {
972
- return typeof input === "string" ? `"${input}" (${ratio.toFixed(2)})` : ratio.toFixed(2);
973
- }
974
- /**
975
- * Slack factor below the requested target before we emit a warning.
976
- * The contrast solver already overshoots by `OVERSHOOT` (currently 1%)
977
- * to absorb rounding noise (`see findLightnessForContrast` in
978
- * `contrast-solver.ts`), so an `actual` ratio within ~2x that overshoot
979
- * is effectively a pass and not worth nagging the user about.
1005
+ * Owns the shadow / mix def predicates, default tuning constants, the
1006
+ * tuning merge, and the actual `computeShadow` math (hue blend,
1007
+ * saturation cap, lightness clamp, alpha curve). The resolver consumes
1008
+ * this module per scheme variant.
980
1009
  */
981
- const CONTRAST_WARN_SLACK = .98;
982
- function warnContrastUnmet(name, isDark, isHighContrast, target, actual) {
983
- const targetRatio = resolveMinContrast(target);
984
- if (actual >= targetRatio * CONTRAST_WARN_SLACK) return;
985
- const scheme = schemeLabel(isDark, isHighContrast);
986
- const key = `${name}|${scheme}|${targetRatio.toFixed(3)}|${actual.toFixed(2)}`;
987
- if (contrastWarnCache.has(key)) return;
988
- if (contrastWarnCache.size >= CONTRAST_WARN_CACHE_LIMIT) contrastWarnCache.clear();
989
- contrastWarnCache.add(key);
990
- console.warn(`glaze: color "${name}" cannot meet contrast ${formatContrastTarget(target, targetRatio)} in ${scheme} scheme (got ${actual.toFixed(2)}). Try widening the lightness window, lowering the contrast target, or picking a base color further from this color's lightness.`);
991
- }
992
1010
  function isShadowDef(def) {
993
1011
  return def.type === "shadow";
994
1012
  }
@@ -1005,11 +1023,12 @@ const DEFAULT_SHADOW_TUNING = {
1005
1023
  bgHueBlend: .2
1006
1024
  };
1007
1025
  function resolveShadowTuning(perColor) {
1026
+ const globalTuning = getConfig().shadowTuning;
1008
1027
  return {
1009
1028
  ...DEFAULT_SHADOW_TUNING,
1010
- ...globalConfig.shadowTuning,
1029
+ ...globalTuning,
1011
1030
  ...perColor,
1012
- lightnessBounds: perColor?.lightnessBounds ?? globalConfig.shadowTuning?.lightnessBounds ?? DEFAULT_SHADOW_TUNING.lightnessBounds
1031
+ lightnessBounds: perColor?.lightnessBounds ?? globalTuning?.lightnessBounds ?? DEFAULT_SHADOW_TUNING.lightnessBounds
1013
1032
  };
1014
1033
  }
1015
1034
  function circularLerp(a, b, t) {
@@ -1050,6 +1069,80 @@ function computeShadow(bg, fg, intensity, tuning) {
1050
1069
  alpha
1051
1070
  };
1052
1071
  }
1072
+
1073
+ //#endregion
1074
+ //#region src/scheme-mapping.ts
1075
+ /**
1076
+ * Light / dark scheme lightness mappings.
1077
+ *
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.
1082
+ */
1083
+ /**
1084
+ * 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`.
1088
+ */
1089
+ function lightnessWindow(isHighContrast, kind, scaling) {
1090
+ 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;
1098
+ }
1099
+ function mapLightnessLight(l, mode, isHighContrast, scaling) {
1100
+ if (mode === "static") return l;
1101
+ const [lo, hi] = lightnessWindow(isHighContrast, "light", scaling);
1102
+ return l * (hi - lo) / 100 + lo;
1103
+ }
1104
+ function mobiusCurve(t, beta) {
1105
+ if (beta >= 1) return t;
1106
+ return t / (t + beta * (1 - t));
1107
+ }
1108
+ function mapLightnessDark(l, mode, isHighContrast, scaling) {
1109
+ 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);
1113
+ if (mode === "fixed") return l * (darkHi - darkLo) / 100 + darkLo;
1114
+ const [lightLo, lightHi] = lightnessWindow(isHighContrast, "light", scaling);
1115
+ const t = (lightHi - (l * (lightHi - lightLo) / 100 + lightLo)) / (lightHi - lightLo);
1116
+ return darkLo + (darkHi - darkLo) * mobiusCurve(t, beta);
1117
+ }
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);
1123
+ const t = (lightHi - clamp(lightL, lightLo, lightHi)) / (lightHi - lightLo);
1124
+ return darkLo + (darkHi - darkLo) * mobiusCurve(t, beta);
1125
+ }
1126
+ function mapSaturationDark(s, mode) {
1127
+ if (mode === "static") return s;
1128
+ return s * (1 - getConfig().darkDesaturation);
1129
+ }
1130
+ function schemeLightnessRange(isDark, mode, isHighContrast, scaling) {
1131
+ if (mode === "static") return [0, 1];
1132
+ const [lo, hi] = lightnessWindow(isHighContrast, isDark ? "dark" : "light", scaling);
1133
+ return [lo / 100, hi / 100];
1134
+ }
1135
+
1136
+ //#endregion
1137
+ //#region src/validation.ts
1138
+ /**
1139
+ * Color graph validation and topological sort.
1140
+ *
1141
+ * `validateColorDefs` rejects bad references (missing / shadow-referencing /
1142
+ * base/contrast/lightness mismatches) and detects cycles before the
1143
+ * resolver runs. `topoSort` orders defs so each color is processed after
1144
+ * its base / bg / fg / target dependencies.
1145
+ */
1053
1146
  function validateColorDefs(defs, externalBases) {
1054
1147
  const localNames = new Set(Object.keys(defs));
1055
1148
  const allNames = new Set([...localNames, ...externalBases ? externalBases.keys() : []]);
@@ -1124,89 +1217,62 @@ function topoSort(defs) {
1124
1217
  for (const name of Object.keys(defs)) visit(name);
1125
1218
  return result;
1126
1219
  }
1220
+
1221
+ //#endregion
1222
+ //#region src/warnings.ts
1127
1223
  /**
1128
- * Resolve the active lightness window for a scheme.
1129
- * - HC variants always return `[0, 100]` (existing behavior, predates per-call overrides).
1130
- * - Otherwise, per-call `scaling` (e.g. from `glaze.color()`'s third arg) wins;
1131
- * `false` is interpreted as `[0, 100]` (no remap). Falls back to `globalConfig.*Lightness`.
1224
+ * Contrast-warning dispatcher.
1225
+ *
1226
+ * Tokens memoize their resolution, but a long-lived process (e.g. a dev
1227
+ * server with HMR) can re-resolve the same theme many times. The cache
1228
+ * here dedupes warnings within a session with a soft cap to keep noise
1229
+ * bounded.
1132
1230
  */
1133
- function lightnessWindow(isHighContrast, kind, scaling) {
1134
- if (isHighContrast) return [0, 100];
1135
- if (scaling) {
1136
- const override = kind === "dark" ? scaling.darkLightness : scaling.lightLightness;
1137
- if (override === false) return [0, 100];
1138
- if (override !== void 0) return override;
1139
- }
1140
- return kind === "dark" ? globalConfig.darkLightness : globalConfig.lightLightness;
1141
- }
1142
- function mapLightnessLight(l, mode, isHighContrast, scaling) {
1143
- if (mode === "static") return l;
1144
- const [lo, hi] = lightnessWindow(isHighContrast, "light", scaling);
1145
- return l * (hi - lo) / 100 + lo;
1231
+ const CONTRAST_WARN_CACHE_LIMIT = 256;
1232
+ const contrastWarnCache = /* @__PURE__ */ new Set();
1233
+ /**
1234
+ * Slack factor below the requested target before we emit a warning.
1235
+ * The contrast solver already overshoots by `OVERSHOOT` (currently 1%)
1236
+ * to absorb rounding noise (`see findLightnessForContrast` in
1237
+ * `contrast-solver.ts`), so an `actual` ratio within ~2x that overshoot
1238
+ * is effectively a pass and not worth nagging the user about.
1239
+ */
1240
+ const CONTRAST_WARN_SLACK = .98;
1241
+ function schemeLabel(isDark, isHighContrast) {
1242
+ if (isDark && isHighContrast) return "darkContrast";
1243
+ if (isDark) return "dark";
1244
+ if (isHighContrast) return "lightContrast";
1245
+ return "light";
1146
1246
  }
1147
- function mobiusCurve(t, beta) {
1148
- if (beta >= 1) return t;
1149
- return t / (t + beta * (1 - t));
1247
+ function formatContrastTarget(input, ratio) {
1248
+ return typeof input === "string" ? `"${input}" (${ratio.toFixed(2)})` : ratio.toFixed(2);
1150
1249
  }
1151
- function mapLightnessDark(l, mode, isHighContrast, scaling) {
1152
- if (mode === "static") return l;
1153
- const beta = isHighContrast ? pairHC(globalConfig.darkCurve) : pairNormal(globalConfig.darkCurve);
1154
- const [darkLo, darkHi] = lightnessWindow(isHighContrast, "dark", scaling);
1155
- if (mode === "fixed") return l * (darkHi - darkLo) / 100 + darkLo;
1156
- const [lightLo, lightHi] = lightnessWindow(isHighContrast, "light", scaling);
1157
- const t = (lightHi - (l * (lightHi - lightLo) / 100 + lightLo)) / (lightHi - lightLo);
1158
- return darkLo + (darkHi - darkLo) * mobiusCurve(t, beta);
1159
- }
1160
- function lightMappedToDark(lightL, isHighContrast, scaling) {
1161
- const beta = isHighContrast ? pairHC(globalConfig.darkCurve) : pairNormal(globalConfig.darkCurve);
1162
- const [lightLo, lightHi] = lightnessWindow(isHighContrast, "light", scaling);
1163
- const [darkLo, darkHi] = lightnessWindow(isHighContrast, "dark", scaling);
1164
- const t = (lightHi - clamp(lightL, lightLo, lightHi)) / (lightHi - lightLo);
1165
- return darkLo + (darkHi - darkLo) * mobiusCurve(t, beta);
1166
- }
1167
- function mapSaturationDark(s, mode) {
1168
- if (mode === "static") return s;
1169
- return s * (1 - globalConfig.darkDesaturation);
1170
- }
1171
- function schemeLightnessRange(isDark, mode, isHighContrast, scaling) {
1172
- if (mode === "static") return [0, 1];
1173
- const [lo, hi] = lightnessWindow(isHighContrast, isDark ? "dark" : "light", scaling);
1174
- return [lo / 100, hi / 100];
1175
- }
1176
- function clamp(v, min, max) {
1177
- return Math.max(min, Math.min(max, v));
1178
- }
1179
- /**
1180
- * Parse a value that can be absolute (number) or relative (signed string).
1181
- * Returns the numeric value and whether it's relative.
1182
- */
1183
- function parseRelativeOrAbsolute(value) {
1184
- if (typeof value === "number") return {
1185
- value,
1186
- relative: false
1187
- };
1188
- return {
1189
- value: parseFloat(value),
1190
- relative: true
1191
- };
1192
- }
1193
- /**
1194
- * Compute the effective hue for a color, given the theme seed hue
1195
- * and an optional per-color hue override.
1196
- */
1197
- function resolveEffectiveHue(seedHue, defHue) {
1198
- if (defHue === void 0) return seedHue;
1199
- const parsed = parseRelativeOrAbsolute(defHue);
1200
- if (parsed.relative) return ((seedHue + parsed.value) % 360 + 360) % 360;
1201
- return (parsed.value % 360 + 360) % 360;
1250
+ function warnContrastUnmet(name, isDark, isHighContrast, target, actual) {
1251
+ const targetRatio = resolveMinContrast(target);
1252
+ if (actual >= targetRatio * CONTRAST_WARN_SLACK) return;
1253
+ const scheme = schemeLabel(isDark, isHighContrast);
1254
+ const key = `${name}|${scheme}|${targetRatio.toFixed(3)}|${actual.toFixed(2)}`;
1255
+ if (contrastWarnCache.has(key)) return;
1256
+ if (contrastWarnCache.size >= CONTRAST_WARN_CACHE_LIMIT) contrastWarnCache.clear();
1257
+ contrastWarnCache.add(key);
1258
+ console.warn(`glaze: color "${name}" cannot meet contrast ${formatContrastTarget(target, targetRatio)} in ${scheme} scheme (got ${actual.toFixed(2)}). Try widening the lightness window, lowering the contrast target, or picking a base color further from this color's lightness.`);
1202
1259
  }
1260
+
1261
+ //#endregion
1262
+ //#region src/resolver.ts
1203
1263
  /**
1204
- * Check whether a lightness value represents an absolute root definition
1205
- * (i.e. a number, not a relative string).
1264
+ * Color resolution engine.
1265
+ *
1266
+ * Runs the four-pass solver (light → light-HC → dark → dark-HC) that
1267
+ * turns a `ColorMap` into a fully resolved `ResolvedColor` per name.
1268
+ * Owns the per-scheme resolve helpers for regular, shadow, and mix
1269
+ * color defs.
1206
1270
  */
1207
- function isAbsoluteLightness(lightness) {
1208
- if (lightness === void 0) return false;
1209
- return typeof (Array.isArray(lightness) ? lightness[0] : lightness) === "number";
1271
+ function getSchemeVariant(color, isDark, isHighContrast) {
1272
+ if (isDark && isHighContrast) return color.darkContrast;
1273
+ if (isDark) return color.dark;
1274
+ if (isHighContrast) return color.lightContrast;
1275
+ return color.light;
1210
1276
  }
1211
1277
  function resolveRootColor(_name, def, _ctx, isHighContrast) {
1212
1278
  const rawL = def.lightness;
@@ -1260,12 +1326,6 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
1260
1326
  satFactor
1261
1327
  };
1262
1328
  }
1263
- function getSchemeVariant(color, isDark, isHighContrast) {
1264
- if (isDark && isHighContrast) return color.darkContrast;
1265
- if (isDark) return color.dark;
1266
- if (isHighContrast) return color.lightContrast;
1267
- return color.light;
1268
- }
1269
1329
  function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
1270
1330
  if (isShadowDef(def)) return resolveShadowForScheme(def, ctx, isDark, isHighContrast);
1271
1331
  if (isMixDef(def)) return resolveMixForScheme(def, ctx, isDark, isHighContrast);
@@ -1391,26 +1451,27 @@ function resolveMixForScheme(def, ctx, isDark, isHighContrast) {
1391
1451
  alpha: 1
1392
1452
  };
1393
1453
  }
1394
- function resolveAllColors(hue, saturation, defs, scaling, externalBases) {
1395
- validateColorDefs(defs, externalBases);
1396
- const order = topoSort(defs);
1397
- const ctx = {
1398
- hue,
1399
- saturation,
1400
- defs,
1401
- resolved: /* @__PURE__ */ new Map(),
1402
- scaling
1403
- };
1404
- if (externalBases) for (const [name, color] of externalBases) ctx.resolved.set(name, color);
1405
- function defMode(def) {
1406
- if (isShadowDef(def) || isMixDef(def)) return void 0;
1407
- return def.mode ?? "auto";
1408
- }
1409
- const lightMap = /* @__PURE__ */ new Map();
1454
+ function defMode(def) {
1455
+ if (isShadowDef(def) || isMixDef(def)) return void 0;
1456
+ return def.mode ?? "auto";
1457
+ }
1458
+ /**
1459
+ * Run a single resolve pass over all local names. Pass 1 lazily creates
1460
+ * each `ResolvedColor` (all four slots seeded with the just-resolved
1461
+ * variant) the first time it sees a name; later passes update the
1462
+ * `target` slot on the existing record.
1463
+ */
1464
+ function runPass(order, defs, ctx, isDark, isHighContrast, target) {
1465
+ const out = /* @__PURE__ */ new Map();
1410
1466
  for (const name of order) {
1411
- const variant = resolveColorForScheme(name, defs[name], ctx, false, false);
1412
- lightMap.set(name, variant);
1413
- ctx.resolved.set(name, {
1467
+ const variant = resolveColorForScheme(name, defs[name], ctx, isDark, isHighContrast);
1468
+ out.set(name, variant);
1469
+ const existing = ctx.resolved.get(name);
1470
+ if (existing) ctx.resolved.set(name, {
1471
+ ...existing,
1472
+ [target]: variant
1473
+ });
1474
+ else ctx.resolved.set(name, {
1414
1475
  name,
1415
1476
  light: variant,
1416
1477
  dark: variant,
@@ -1419,49 +1480,40 @@ function resolveAllColors(hue, saturation, defs, scaling, externalBases) {
1419
1480
  mode: defMode(defs[name])
1420
1481
  });
1421
1482
  }
1422
- const lightHCMap = /* @__PURE__ */ new Map();
1423
- for (const name of order) ctx.resolved.set(name, {
1424
- ...ctx.resolved.get(name),
1425
- lightContrast: lightMap.get(name)
1426
- });
1427
- for (const name of order) {
1428
- const variant = resolveColorForScheme(name, defs[name], ctx, false, true);
1429
- lightHCMap.set(name, variant);
1430
- ctx.resolved.set(name, {
1431
- ...ctx.resolved.get(name),
1432
- lightContrast: variant
1433
- });
1434
- }
1435
- const darkMap = /* @__PURE__ */ new Map();
1436
- for (const name of order) ctx.resolved.set(name, {
1437
- name,
1438
- light: lightMap.get(name),
1439
- dark: lightMap.get(name),
1440
- lightContrast: lightHCMap.get(name),
1441
- darkContrast: lightHCMap.get(name),
1442
- mode: defMode(defs[name])
1443
- });
1444
- for (const name of order) {
1445
- const variant = resolveColorForScheme(name, defs[name], ctx, true, false);
1446
- darkMap.set(name, variant);
1447
- ctx.resolved.set(name, {
1448
- ...ctx.resolved.get(name),
1449
- dark: variant
1450
- });
1451
- }
1452
- const darkHCMap = /* @__PURE__ */ new Map();
1453
- for (const name of order) ctx.resolved.set(name, {
1454
- ...ctx.resolved.get(name),
1455
- darkContrast: darkMap.get(name)
1456
- });
1483
+ return out;
1484
+ }
1485
+ /**
1486
+ * Re-seed a single variant slot with a previously-resolved map so the
1487
+ * upcoming pass reads sensible fallbacks via `getSchemeVariant`.
1488
+ */
1489
+ function seedField(order, ctx, field, source) {
1457
1490
  for (const name of order) {
1458
- const variant = resolveColorForScheme(name, defs[name], ctx, true, true);
1459
- darkHCMap.set(name, variant);
1491
+ const existing = ctx.resolved.get(name);
1460
1492
  ctx.resolved.set(name, {
1461
- ...ctx.resolved.get(name),
1462
- darkContrast: variant
1493
+ ...existing,
1494
+ [field]: source.get(name)
1463
1495
  });
1464
1496
  }
1497
+ }
1498
+ function resolveAllColors(hue, saturation, defs, scaling, externalBases) {
1499
+ validateColorDefs(defs, externalBases);
1500
+ const order = topoSort(defs);
1501
+ const ctx = {
1502
+ hue,
1503
+ saturation,
1504
+ defs,
1505
+ resolved: /* @__PURE__ */ new Map(),
1506
+ scaling
1507
+ };
1508
+ if (externalBases) for (const [name, color] of externalBases) ctx.resolved.set(name, color);
1509
+ const lightMap = runPass(order, defs, ctx, false, false, "light");
1510
+ seedField(order, ctx, "lightContrast", lightMap);
1511
+ const lightHCMap = runPass(order, defs, ctx, false, true, "lightContrast");
1512
+ seedField(order, ctx, "dark", lightMap);
1513
+ seedField(order, ctx, "darkContrast", lightHCMap);
1514
+ const darkMap = runPass(order, defs, ctx, true, false, "dark");
1515
+ seedField(order, ctx, "darkContrast", darkMap);
1516
+ const darkHCMap = runPass(order, defs, ctx, true, true, "darkContrast");
1465
1517
  const result = /* @__PURE__ */ new Map();
1466
1518
  for (const name of order) result.set(name, {
1467
1519
  name,
@@ -1473,6 +1525,19 @@ function resolveAllColors(hue, saturation, defs, scaling, externalBases) {
1473
1525
  });
1474
1526
  return result;
1475
1527
  }
1528
+
1529
+ //#endregion
1530
+ //#region src/formatters.ts
1531
+ /**
1532
+ * Output formatting for resolved color maps.
1533
+ *
1534
+ * Owns the CSS-string formatter dispatch table (`okhsl` / `rgb` / `hsl` /
1535
+ * `oklch`) and the four token-map shapes Glaze emits:
1536
+ * - `buildTokenMap` — Tasty style-to-state bindings (`#name` keys, state aliases).
1537
+ * - `buildFlatTokenMap` — `{ light, dark, ... }` per-variant maps.
1538
+ * - `buildJsonMap` — `{ name: { light, dark, ... } }` per-color JSON.
1539
+ * - `buildCssMap` — CSS custom property declaration strings per variant.
1540
+ */
1476
1541
  const formatters = {
1477
1542
  okhsl: formatOkhsl,
1478
1543
  rgb: formatRgb,
@@ -1489,9 +1554,10 @@ function formatVariant(v, format = "okhsl") {
1489
1554
  return `${base.slice(0, closing)} / ${fmt(v.alpha, 4)})`;
1490
1555
  }
1491
1556
  function resolveModes(override) {
1557
+ const cfg = getConfig();
1492
1558
  return {
1493
- dark: override?.dark ?? globalConfig.modes.dark,
1494
- highContrast: override?.highContrast ?? globalConfig.modes.highContrast
1559
+ dark: override?.dark ?? cfg.modes.dark,
1560
+ highContrast: override?.highContrast ?? cfg.modes.highContrast
1495
1561
  };
1496
1562
  }
1497
1563
  function buildTokenMap(resolved, prefix, states, modes, format = "okhsl") {
@@ -1552,220 +1618,88 @@ function buildCssMap(resolved, prefix, suffix, format) {
1552
1618
  darkContrast: lines.darkContrast.join("\n")
1553
1619
  };
1554
1620
  }
1555
- function createTheme(hue, saturation, initialColors) {
1556
- let colorDefs = initialColors ? { ...initialColors } : {};
1621
+
1622
+ //#endregion
1623
+ //#region src/color-token.ts
1624
+ /**
1625
+ * Standalone single-color tokens (`glaze.color()` / `glaze.colorFrom()`).
1626
+ *
1627
+ * Owns the value-shorthand parser (hex, `rgb()` / `hsl()` / `okhsl()` /
1628
+ * `oklch()`, OkhslColor object, [r, g, b] tuple), the structured-input
1629
+ * validator, the two factory paths (value vs structured), and the
1630
+ * JSON-safe export / rehydration round-trip.
1631
+ *
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.
1637
+ */
1638
+ /** Internal name of the user-facing standalone color in the synthesized def map. */
1639
+ const STANDALONE_VALUE = "value";
1640
+ /** Internal name of the hidden static-anchor seed used for relative lightness / contrast. */
1641
+ const STANDALONE_SEED = "seed";
1642
+ /** Internal name of an externally-resolved `GlazeColorToken` injected as a base reference. */
1643
+ const STANDALONE_BASE = "externalBase";
1644
+ /** Reserved internal names that user-supplied `name` must not collide with. */
1645
+ const RESERVED_STANDALONE_NAMES = new Set([
1646
+ STANDALONE_VALUE,
1647
+ STANDALONE_SEED,
1648
+ STANDALONE_BASE
1649
+ ]);
1650
+ /**
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).
1656
+ *
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) {
1666
+ const cfg = getConfig();
1667
+ if (isString) {
1668
+ const [darkLo] = cfg.darkLightness;
1669
+ return {
1670
+ lightLightness: false,
1671
+ darkLightness: [darkLo, 100]
1672
+ };
1673
+ }
1557
1674
  return {
1558
- get hue() {
1559
- return hue;
1560
- },
1561
- get saturation() {
1562
- return saturation;
1563
- },
1564
- colors(defs) {
1565
- colorDefs = {
1566
- ...colorDefs,
1567
- ...defs
1568
- };
1569
- },
1570
- color(name, def) {
1571
- if (def === void 0) return colorDefs[name];
1572
- colorDefs[name] = def;
1573
- },
1574
- remove(names) {
1575
- const list = Array.isArray(names) ? names : [names];
1576
- for (const name of list) delete colorDefs[name];
1577
- },
1578
- has(name) {
1579
- return name in colorDefs;
1580
- },
1581
- list() {
1582
- return Object.keys(colorDefs);
1583
- },
1584
- reset() {
1585
- colorDefs = {};
1586
- },
1587
- export() {
1588
- return {
1589
- hue,
1590
- saturation,
1591
- colors: { ...colorDefs }
1592
- };
1593
- },
1594
- extend(options) {
1595
- const newHue = options.hue ?? hue;
1596
- const newSat = options.saturation ?? saturation;
1597
- const inheritedColors = {};
1598
- for (const [name, def] of Object.entries(colorDefs)) if (def.inherit !== false) inheritedColors[name] = def;
1599
- return createTheme(newHue, newSat, options.colors ? {
1600
- ...inheritedColors,
1601
- ...options.colors
1602
- } : { ...inheritedColors });
1603
- },
1604
- resolve() {
1605
- return resolveAllColors(hue, saturation, colorDefs);
1606
- },
1607
- tokens(options) {
1608
- return buildFlatTokenMap(resolveAllColors(hue, saturation, colorDefs), "", resolveModes(options?.modes), options?.format);
1609
- },
1610
- tasty(options) {
1611
- return buildTokenMap(resolveAllColors(hue, saturation, colorDefs), "", {
1612
- dark: options?.states?.dark ?? globalConfig.states.dark,
1613
- highContrast: options?.states?.highContrast ?? globalConfig.states.highContrast
1614
- }, resolveModes(options?.modes), options?.format);
1615
- },
1616
- json(options) {
1617
- return buildJsonMap(resolveAllColors(hue, saturation, colorDefs), resolveModes(options?.modes), options?.format);
1618
- },
1619
- css(options) {
1620
- return buildCssMap(resolveAllColors(hue, saturation, colorDefs), "", options?.suffix ?? "-color", options?.format ?? "rgb");
1621
- }
1675
+ lightLightness: cfg.lightLightness,
1676
+ darkLightness: cfg.darkLightness
1622
1677
  };
1623
1678
  }
1624
- function resolvePrefix(options, themeName, defaultPrefix = false) {
1625
- const prefix = options?.prefix ?? defaultPrefix;
1626
- if (prefix === true) return `${themeName}-`;
1627
- if (typeof prefix === "object" && prefix !== null) return prefix[themeName] ?? `${themeName}-`;
1628
- return "";
1679
+ /**
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)`).
1683
+ */
1684
+ function isGlazeColorToken(candidate) {
1685
+ return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate) && "resolve" in candidate && typeof candidate.resolve === "function";
1629
1686
  }
1630
- function validatePrimaryTheme(primary, themes) {
1631
- if (primary !== void 0 && !(primary in themes)) {
1632
- const available = Object.keys(themes).join(", ");
1633
- throw new Error(`glaze: primary theme "${primary}" not found in palette. Available: ${available}.`);
1634
- }
1687
+ function isStructuredColorInput(input) {
1688
+ return typeof input === "object" && input !== null && !Array.isArray(input) && "hue" in input && "lightness" in input;
1635
1689
  }
1636
1690
  /**
1637
- * Resolve the effective primary for an export call.
1638
- * `false` disables, a string overrides, `undefined` inherits from palette.
1691
+ * Matches the CSS color functions Glaze itself emits (`rgb()`, `hsl()`,
1692
+ * `okhsl()`, `oklch()`) plus their legacy alpha aliases (`rgba()`, `hsla()`).
1693
+ *
1694
+ * Only bare numeric components are supported. Named colors (`red`),
1695
+ * relative-color syntax (`from <color> ...`), and angle units other
1696
+ * than bare degrees (`deg` is the only suffix tolerated by `parseFloat`)
1697
+ * are out of scope.
1639
1698
  */
1640
- function resolveEffectivePrimary(exportPrimary, palettePrimary) {
1641
- if (exportPrimary === false) return void 0;
1642
- return exportPrimary ?? palettePrimary;
1643
- }
1644
- /**
1645
- * Filter a resolved color map, skipping keys already in `seen`.
1646
- * Warns on collision and keeps the first-written value (first-write-wins).
1647
- * Returns a new map containing only non-colliding entries.
1648
- */
1649
- function filterCollisions(resolved, prefix, seen, themeName, isPrimary) {
1650
- const filtered = /* @__PURE__ */ new Map();
1651
- const label = isPrimary ? `${themeName} (primary)` : themeName;
1652
- for (const [name, color] of resolved) {
1653
- const key = `${prefix}${name}`;
1654
- if (seen.has(key)) {
1655
- console.warn(`glaze: token "${key}" from theme "${label}" collides with theme "${seen.get(key)}" — skipping.`);
1656
- continue;
1657
- }
1658
- seen.set(key, label);
1659
- filtered.set(name, color);
1660
- }
1661
- return filtered;
1662
- }
1663
- function createPalette(themes, paletteOptions) {
1664
- validatePrimaryTheme(paletteOptions?.primary, themes);
1665
- return {
1666
- tokens(options) {
1667
- const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
1668
- if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
1669
- const modes = resolveModes(options?.modes);
1670
- const allTokens = {};
1671
- const seen = /* @__PURE__ */ new Map();
1672
- for (const [themeName, theme] of Object.entries(themes)) {
1673
- const resolved = theme.resolve();
1674
- const prefix = resolvePrefix(options, themeName, true);
1675
- const tokens = buildFlatTokenMap(filterCollisions(resolved, prefix, seen, themeName), prefix, modes, options?.format);
1676
- for (const variant of Object.keys(tokens)) {
1677
- if (!allTokens[variant]) allTokens[variant] = {};
1678
- Object.assign(allTokens[variant], tokens[variant]);
1679
- }
1680
- if (themeName === effectivePrimary) {
1681
- const unprefixed = buildFlatTokenMap(filterCollisions(resolved, "", seen, themeName, true), "", modes, options?.format);
1682
- for (const variant of Object.keys(unprefixed)) Object.assign(allTokens[variant], unprefixed[variant]);
1683
- }
1684
- }
1685
- return allTokens;
1686
- },
1687
- tasty(options) {
1688
- const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
1689
- if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
1690
- const states = {
1691
- dark: options?.states?.dark ?? globalConfig.states.dark,
1692
- highContrast: options?.states?.highContrast ?? globalConfig.states.highContrast
1693
- };
1694
- const modes = resolveModes(options?.modes);
1695
- const allTokens = {};
1696
- const seen = /* @__PURE__ */ new Map();
1697
- for (const [themeName, theme] of Object.entries(themes)) {
1698
- const resolved = theme.resolve();
1699
- const prefix = resolvePrefix(options, themeName, true);
1700
- const tokens = buildTokenMap(filterCollisions(resolved, prefix, seen, themeName), prefix, states, modes, options?.format);
1701
- Object.assign(allTokens, tokens);
1702
- if (themeName === effectivePrimary) {
1703
- const unprefixed = buildTokenMap(filterCollisions(resolved, "", seen, themeName, true), "", states, modes, options?.format);
1704
- Object.assign(allTokens, unprefixed);
1705
- }
1706
- }
1707
- return allTokens;
1708
- },
1709
- json(options) {
1710
- const modes = resolveModes(options?.modes);
1711
- const result = {};
1712
- for (const [themeName, theme] of Object.entries(themes)) result[themeName] = buildJsonMap(theme.resolve(), modes, options?.format);
1713
- return result;
1714
- },
1715
- css(options) {
1716
- const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
1717
- if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
1718
- const suffix = options?.suffix ?? "-color";
1719
- const format = options?.format ?? "rgb";
1720
- const allLines = {
1721
- light: [],
1722
- dark: [],
1723
- lightContrast: [],
1724
- darkContrast: []
1725
- };
1726
- const seen = /* @__PURE__ */ new Map();
1727
- for (const [themeName, theme] of Object.entries(themes)) {
1728
- const resolved = theme.resolve();
1729
- const prefix = resolvePrefix(options, themeName, true);
1730
- const css = buildCssMap(filterCollisions(resolved, prefix, seen, themeName), prefix, suffix, format);
1731
- for (const key of [
1732
- "light",
1733
- "dark",
1734
- "lightContrast",
1735
- "darkContrast"
1736
- ]) if (css[key]) allLines[key].push(css[key]);
1737
- if (themeName === effectivePrimary) {
1738
- const unprefixed = buildCssMap(filterCollisions(resolved, "", seen, themeName, true), "", suffix, format);
1739
- for (const key of [
1740
- "light",
1741
- "dark",
1742
- "lightContrast",
1743
- "darkContrast"
1744
- ]) if (unprefixed[key]) allLines[key].push(unprefixed[key]);
1745
- }
1746
- }
1747
- return {
1748
- light: allLines.light.join("\n"),
1749
- dark: allLines.dark.join("\n"),
1750
- lightContrast: allLines.lightContrast.join("\n"),
1751
- darkContrast: allLines.darkContrast.join("\n")
1752
- };
1753
- }
1754
- };
1755
- }
1756
- /**
1757
- * Matches the CSS color functions Glaze itself emits (`rgb()`, `hsl()`,
1758
- * `okhsl()`, `oklch()`) plus their legacy alpha aliases (`rgba()`, `hsla()`).
1759
- *
1760
- * Only bare numeric components are supported. Named colors (`red`),
1761
- * relative-color syntax (`from <color> ...`), and angle units other
1762
- * than bare degrees (`deg` is the only suffix tolerated by `parseFloat`)
1763
- * are out of scope.
1764
- */
1765
- const COLOR_FN_RE = /^(rgba?|hsla?|okhsl|oklch)\(\s*([^)]*)\s*\)$/i;
1766
- function parseNumberOrPercent(raw, percentScale) {
1767
- if (raw.endsWith("%")) return parseFloat(raw) / 100 * percentScale;
1768
- return parseFloat(raw);
1699
+ const COLOR_FN_RE = /^(rgba?|hsla?|okhsl|oklch)\(\s*([^)]*)\s*\)$/i;
1700
+ function parseNumberOrPercent(raw, percentScale) {
1701
+ if (raw.endsWith("%")) return parseFloat(raw) / 100 * percentScale;
1702
+ return parseFloat(raw);
1769
1703
  }
1770
1704
  /**
1771
1705
  * Split the body of a CSS color function into its components and detect
@@ -1870,9 +1804,7 @@ function validateOkhslColor(value) {
1870
1804
  if (!Number.isFinite(h) || !Number.isFinite(s) || !Number.isFinite(l)) throw new Error("glaze.color: OkhslColor h/s/l must be finite numbers.");
1871
1805
  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)?");
1872
1806
  }
1873
- /**
1874
- * Validate a user-supplied `[r, g, b]` tuple in 0-255.
1875
- */
1807
+ /** Validate a user-supplied `[r, g, b]` tuple in 0-255. */
1876
1808
  function validateRgbTuple(value) {
1877
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(", ")}]).`);
1878
1810
  }
@@ -1946,17 +1878,18 @@ function extractOkhslFromValue(value) {
1946
1878
  * Build the `ColorMap` for a value-shorthand `glaze.color()` call.
1947
1879
  *
1948
1880
  * The user-facing color (`STANDALONE_VALUE`) defaults to `mode: 'auto'`
1949
- * for string inputs (Möbius-inverted dark variant pairs with the
1881
+ * across every value-shorthand form. String inputs pair with the
1950
1882
  * extended dark window so a totally-black input renders as totally-white
1951
- * in dark mode) and `mode: 'fixed'` for `OkhslColor` / RGB-tuple inputs
1952
- * (linear, no inversion).
1883
+ * in dark mode; `OkhslColor` / RGB-tuple inputs auto-adapt into the
1884
+ * snapshotted `globalConfig.lightLightness` / `globalConfig.darkLightness`
1885
+ * windows.
1953
1886
  *
1954
1887
  * When the user requests `contrast` or relative `lightness`, a hidden
1955
1888
  * `STANDALONE_SEED` def is synthesized at `mode: 'static'`. That keeps
1956
1889
  * the seed pinned to the literal user-provided color across all four
1957
1890
  * variants, so the contrast solver always anchors against it.
1958
1891
  */
1959
- function buildStandaloneValueDefs(main, options, inputIsString) {
1892
+ function buildStandaloneValueDefs(main, options) {
1960
1893
  const seedHue = typeof options?.hue === "number" ? options.hue : main.h;
1961
1894
  const seedSaturation = options?.saturation ?? main.s * 100;
1962
1895
  const relativeHue = typeof options?.hue === "string" ? options.hue : void 0;
@@ -1972,7 +1905,7 @@ function buildStandaloneValueDefs(main, options, inputIsString) {
1972
1905
  saturation: options?.saturationFactor,
1973
1906
  lightness: lightnessOption ?? main.l * 100,
1974
1907
  contrast: options?.contrast,
1975
- mode: options?.mode ?? (inputIsString ? "auto" : "fixed"),
1908
+ mode: options?.mode ?? "auto",
1976
1909
  opacity: options?.opacity,
1977
1910
  base: hasExternalBase ? STANDALONE_BASE : needsSeedAnchor ? STANDALONE_SEED : void 0
1978
1911
  };
@@ -1997,10 +1930,13 @@ function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effect
1997
1930
  cached = resolveAllColors(seedHue, seedSaturation, defs, effectiveScaling, baseToken ? new Map([[STANDALONE_BASE, baseToken.resolve()]]) : void 0);
1998
1931
  return cached;
1999
1932
  };
2000
- const resolveStates = (options) => ({
2001
- dark: options?.states?.dark ?? globalConfig.states.dark,
2002
- highContrast: options?.states?.highContrast ?? globalConfig.states.highContrast
2003
- });
1933
+ const resolveStates = (options) => {
1934
+ const cfg = getConfig();
1935
+ return {
1936
+ dark: options?.states?.dark ?? cfg.states.dark,
1937
+ highContrast: options?.states?.highContrast ?? cfg.states.highContrast
1938
+ };
1939
+ };
2004
1940
  const tokenLike = (options) => {
2005
1941
  return buildTokenMap(resolveOnce(), "", resolveStates(options), resolveModes(options?.modes), options?.format)[`#${primary}`];
2006
1942
  };
@@ -2030,38 +1966,6 @@ function resolveBaseToken(base) {
2030
1966
  if (isGlazeColorToken(base)) return base;
2031
1967
  return createColorTokenFromValue(base, void 0, void 0);
2032
1968
  }
2033
- /**
2034
- * Build a JSON-safe snapshot of `GlazeColorOverrides`. `base` is
2035
- * recursively serialized when it was originally a token; raw values are
2036
- * preserved as-is so `glaze.colorFrom(...)` round-trips them.
2037
- */
2038
- function buildOverridesExport(options) {
2039
- const out = {};
2040
- if (options.hue !== void 0) out.hue = options.hue;
2041
- if (options.saturation !== void 0) out.saturation = options.saturation;
2042
- if (options.lightness !== void 0) out.lightness = options.lightness;
2043
- if (options.saturationFactor !== void 0) out.saturationFactor = options.saturationFactor;
2044
- if (options.mode !== void 0) out.mode = options.mode;
2045
- if (options.contrast !== void 0) out.contrast = options.contrast;
2046
- if (options.opacity !== void 0) out.opacity = options.opacity;
2047
- if (options.name !== void 0) out.name = options.name;
2048
- if (options.base !== void 0) out.base = isGlazeColorToken(options.base) ? options.base.export() : options.base;
2049
- return out;
2050
- }
2051
- function buildStructuredInputExport(input) {
2052
- const out = {
2053
- hue: input.hue,
2054
- saturation: input.saturation,
2055
- lightness: input.lightness
2056
- };
2057
- if (input.saturationFactor !== void 0) out.saturationFactor = input.saturationFactor;
2058
- if (input.mode !== void 0) out.mode = input.mode;
2059
- if (input.opacity !== void 0) out.opacity = input.opacity;
2060
- if (input.contrast !== void 0) out.contrast = input.contrast;
2061
- if (input.name !== void 0) out.name = input.name;
2062
- if (input.base !== void 0) out.base = isGlazeColorToken(input.base) ? input.base.export() : input.base;
2063
- return out;
2064
- }
2065
1969
  function createColorToken(input, scaling) {
2066
1970
  validateStructuredInput(input);
2067
1971
  const userName = input.name;
@@ -2073,7 +1977,7 @@ function createColorToken(input, scaling) {
2073
1977
  const defs = { [primary]: {
2074
1978
  lightness: input.lightness,
2075
1979
  saturation: input.saturationFactor,
2076
- mode: input.mode ?? "fixed",
1980
+ mode: input.mode ?? "auto",
2077
1981
  contrast: input.contrast,
2078
1982
  opacity: input.opacity,
2079
1983
  base: hasExternalBase ? STANDALONE_BASE : needsSeedAnchor ? STANDALONE_SEED : void 0
@@ -2095,7 +1999,7 @@ function createColorTokenFromValue(value, options, scaling) {
2095
1999
  const inputIsString = typeof value === "string";
2096
2000
  const main = extractOkhslFromValue(value);
2097
2001
  const baseToken = resolveBaseToken(options?.base);
2098
- const { seedHue, seedSaturation, defs, primary } = buildStandaloneValueDefs(main, options, inputIsString);
2002
+ const { seedHue, seedSaturation, defs, primary } = buildStandaloneValueDefs(main, options);
2099
2003
  const effectiveScaling = scaling ?? defaultStandaloneScaling(inputIsString);
2100
2004
  const exportData = () => ({
2101
2005
  form: "value",
@@ -2106,18 +2010,44 @@ function createColorTokenFromValue(value, options, scaling) {
2106
2010
  return createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effectiveScaling, baseToken, exportData);
2107
2011
  }
2108
2012
  /**
2109
- * Rehydrate a token from its `.export()` snapshot. Recursively rebuilds
2110
- * any base dependency. Inverse of `GlazeColorToken.export()`.
2013
+ * Build a JSON-safe snapshot of `GlazeColorOverrides`. `base` is
2014
+ * recursively serialized when it was originally a token; raw values are
2015
+ * preserved as-is so `glaze.colorFrom(...)` round-trips them.
2111
2016
  */
2112
- function colorFromExport(data) {
2113
- if (data === null || typeof data !== "object") throw new Error(`glaze.colorFrom: expected an object from token.export(), got ${data === null ? "null" : typeof data}.`);
2114
- if (data.form !== "value" && data.form !== "structured") throw new Error(`glaze.colorFrom: invalid "form" field — expected "value" or "structured" (got ${JSON.stringify(data.form)}).`);
2115
- if (data.input === void 0) throw new Error(`glaze.colorFrom: missing "input" field — expected the original ${data.form === "value" ? "GlazeColorValue" : "GlazeColorInput"}.`);
2116
- if (data.form === "value") {
2117
- const value = data.input;
2118
- return createColorTokenFromValue(value, data.overrides ? rehydrateOverrides(data.overrides) : void 0, data.scaling);
2119
- }
2120
- return createColorToken(rehydrateStructuredInput(data.input), data.scaling);
2017
+ function buildOverridesExport(options) {
2018
+ const out = {};
2019
+ if (options.hue !== void 0) out.hue = options.hue;
2020
+ if (options.saturation !== void 0) out.saturation = options.saturation;
2021
+ if (options.lightness !== void 0) out.lightness = options.lightness;
2022
+ if (options.saturationFactor !== void 0) out.saturationFactor = options.saturationFactor;
2023
+ if (options.mode !== void 0) out.mode = options.mode;
2024
+ if (options.contrast !== void 0) out.contrast = options.contrast;
2025
+ if (options.opacity !== void 0) out.opacity = options.opacity;
2026
+ if (options.name !== void 0) out.name = options.name;
2027
+ if (options.base !== void 0) out.base = isGlazeColorToken(options.base) ? options.base.export() : options.base;
2028
+ return out;
2029
+ }
2030
+ function buildStructuredInputExport(input) {
2031
+ const out = {
2032
+ hue: input.hue,
2033
+ saturation: input.saturation,
2034
+ lightness: input.lightness
2035
+ };
2036
+ if (input.saturationFactor !== void 0) out.saturationFactor = input.saturationFactor;
2037
+ if (input.mode !== void 0) out.mode = input.mode;
2038
+ if (input.opacity !== void 0) out.opacity = input.opacity;
2039
+ if (input.contrast !== void 0) out.contrast = input.contrast;
2040
+ if (input.name !== void 0) out.name = input.name;
2041
+ if (input.base !== void 0) out.base = isGlazeColorToken(input.base) ? input.base.export() : input.base;
2042
+ return out;
2043
+ }
2044
+ /**
2045
+ * 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
+ */
2049
+ function isExportedToken(candidate) {
2050
+ return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate) && "form" in candidate && (candidate.form === "value" || candidate.form === "structured");
2121
2051
  }
2122
2052
  function rehydrateOverrides(data) {
2123
2053
  const out = {};
@@ -2147,13 +2077,258 @@ function rehydrateStructuredInput(data) {
2147
2077
  return out;
2148
2078
  }
2149
2079
  /**
2150
- * Discriminate a `GlazeColorTokenExport` from a raw `GlazeColorValue`.
2151
- * `GlazeColorTokenExport` always has a `form` field set to either
2152
- * `'value'` or `'structured'`; raw values never do.
2080
+ * Rehydrate a token from its `.export()` snapshot. Recursively rebuilds
2081
+ * any base dependency. Inverse of `GlazeColorToken.export()`.
2153
2082
  */
2154
- function isExportedToken(candidate) {
2155
- return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate) && "form" in candidate && (candidate.form === "value" || candidate.form === "structured");
2083
+ function colorFromExport(data) {
2084
+ if (data === null || typeof data !== "object") throw new Error(`glaze.colorFrom: expected an object from token.export(), got ${data === null ? "null" : typeof data}.`);
2085
+ if (data.form !== "value" && data.form !== "structured") throw new Error(`glaze.colorFrom: invalid "form" field — expected "value" or "structured" (got ${JSON.stringify(data.form)}).`);
2086
+ if (data.input === void 0) throw new Error(`glaze.colorFrom: missing "input" field — expected the original ${data.form === "value" ? "GlazeColorValue" : "GlazeColorInput"}.`);
2087
+ if (data.form === "value") {
2088
+ const value = data.input;
2089
+ return createColorTokenFromValue(value, data.overrides ? rehydrateOverrides(data.overrides) : void 0, data.scaling);
2090
+ }
2091
+ return createColorToken(rehydrateStructuredInput(data.input), data.scaling);
2156
2092
  }
2093
+
2094
+ //#endregion
2095
+ //#region src/palette.ts
2096
+ /**
2097
+ * Palette factory.
2098
+ *
2099
+ * Composes multiple themes into a single token namespace with optional
2100
+ * theme-name prefixes and a "primary theme" that also surfaces an
2101
+ * unprefixed copy of its tokens. All four export methods (`tokens` /
2102
+ * `tasty` / `json` / `css`) share a `buildPaletteOutput` driver that
2103
+ * handles validation, per-theme iteration, prefix resolution, collision
2104
+ * filtering, and primary duplication.
2105
+ */
2106
+ function resolvePrefix(options, themeName, defaultPrefix = false) {
2107
+ const prefix = options?.prefix ?? defaultPrefix;
2108
+ if (prefix === true) return `${themeName}-`;
2109
+ if (typeof prefix === "object" && prefix !== null) return prefix[themeName] ?? `${themeName}-`;
2110
+ return "";
2111
+ }
2112
+ function validatePrimaryTheme(primary, themes) {
2113
+ if (primary !== void 0 && !(primary in themes)) {
2114
+ const available = Object.keys(themes).join(", ");
2115
+ throw new Error(`glaze: primary theme "${primary}" not found in palette. Available: ${available}.`);
2116
+ }
2117
+ }
2118
+ /**
2119
+ * Resolve the effective primary for an export call.
2120
+ * `false` disables, a string overrides, `undefined` inherits from palette.
2121
+ */
2122
+ function resolveEffectivePrimary(exportPrimary, palettePrimary) {
2123
+ if (exportPrimary === false) return void 0;
2124
+ return exportPrimary ?? palettePrimary;
2125
+ }
2126
+ /**
2127
+ * Filter a resolved color map, skipping keys already in `seen`.
2128
+ * Warns on collision and keeps the first-written value (first-write-wins).
2129
+ * Returns a new map containing only non-colliding entries.
2130
+ */
2131
+ function filterCollisions(resolved, prefix, seen, themeName, isPrimary) {
2132
+ const filtered = /* @__PURE__ */ new Map();
2133
+ const label = isPrimary ? `${themeName} (primary)` : themeName;
2134
+ for (const [name, color] of resolved) {
2135
+ const key = `${prefix}${name}`;
2136
+ if (seen.has(key)) {
2137
+ console.warn(`glaze: token "${key}" from theme "${label}" collides with theme "${seen.get(key)}" — skipping.`);
2138
+ continue;
2139
+ }
2140
+ seen.set(key, label);
2141
+ filtered.set(name, color);
2142
+ }
2143
+ return filtered;
2144
+ }
2145
+ /**
2146
+ * Shared per-theme driver for `tokens` / `tasty` / `css`. `json` skips
2147
+ * this because it doesn't do collision filtering or primary duplication.
2148
+ */
2149
+ function buildPaletteOutput(themes, paletteOptions, options, buildOne, merge, empty) {
2150
+ const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
2151
+ if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
2152
+ const acc = empty();
2153
+ const seen = /* @__PURE__ */ new Map();
2154
+ for (const [themeName, theme] of Object.entries(themes)) {
2155
+ const resolved = theme.resolve();
2156
+ const prefix = resolvePrefix(options, themeName, true);
2157
+ merge(acc, buildOne(filterCollisions(resolved, prefix, seen, themeName), prefix));
2158
+ if (themeName === effectivePrimary) merge(acc, buildOne(filterCollisions(resolved, "", seen, themeName, true), ""));
2159
+ }
2160
+ return acc;
2161
+ }
2162
+ function createPalette(themes, paletteOptions) {
2163
+ validatePrimaryTheme(paletteOptions?.primary, themes);
2164
+ return {
2165
+ tokens(options) {
2166
+ const modes = resolveModes(options?.modes);
2167
+ return buildPaletteOutput(themes, paletteOptions, options, (filtered, prefix) => buildFlatTokenMap(filtered, prefix, modes, options?.format), (acc, part) => {
2168
+ for (const variant of Object.keys(part)) {
2169
+ if (!acc[variant]) acc[variant] = {};
2170
+ Object.assign(acc[variant], part[variant]);
2171
+ }
2172
+ }, () => ({}));
2173
+ },
2174
+ tasty(options) {
2175
+ const cfg = getConfig();
2176
+ const states = {
2177
+ dark: options?.states?.dark ?? cfg.states.dark,
2178
+ highContrast: options?.states?.highContrast ?? cfg.states.highContrast
2179
+ };
2180
+ const modes = resolveModes(options?.modes);
2181
+ return buildPaletteOutput(themes, paletteOptions, options, (filtered, prefix) => buildTokenMap(filtered, prefix, states, modes, options?.format), (acc, part) => Object.assign(acc, part), () => ({}));
2182
+ },
2183
+ json(options) {
2184
+ const modes = resolveModes(options?.modes);
2185
+ const result = {};
2186
+ for (const [themeName, theme] of Object.entries(themes)) result[themeName] = buildJsonMap(theme.resolve(), modes, options?.format);
2187
+ return result;
2188
+ },
2189
+ css(options) {
2190
+ const suffix = options?.suffix ?? "-color";
2191
+ const format = options?.format ?? "rgb";
2192
+ const lines = buildPaletteOutput(themes, paletteOptions, options, (filtered, prefix) => buildCssMap(filtered, prefix, suffix, format), (acc, part) => {
2193
+ for (const key of [
2194
+ "light",
2195
+ "dark",
2196
+ "lightContrast",
2197
+ "darkContrast"
2198
+ ]) if (part[key]) acc[key].push(part[key]);
2199
+ }, () => ({
2200
+ light: [],
2201
+ dark: [],
2202
+ lightContrast: [],
2203
+ darkContrast: []
2204
+ }));
2205
+ return {
2206
+ light: lines.light.join("\n"),
2207
+ dark: lines.dark.join("\n"),
2208
+ lightContrast: lines.lightContrast.join("\n"),
2209
+ darkContrast: lines.darkContrast.join("\n")
2210
+ };
2211
+ }
2212
+ };
2213
+ }
2214
+
2215
+ //#endregion
2216
+ //#region src/theme.ts
2217
+ /**
2218
+ * Theme factory.
2219
+ *
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.
2224
+ */
2225
+ function createTheme(hue, saturation, initialColors) {
2226
+ let colorDefs = initialColors ? { ...initialColors } : {};
2227
+ let cache = null;
2228
+ function resolveCached() {
2229
+ const version = getConfigVersion();
2230
+ if (cache && cache.version === version) return cache.map;
2231
+ const map = resolveAllColors(hue, saturation, colorDefs);
2232
+ cache = {
2233
+ map,
2234
+ version
2235
+ };
2236
+ return map;
2237
+ }
2238
+ function invalidate() {
2239
+ cache = null;
2240
+ }
2241
+ return {
2242
+ get hue() {
2243
+ return hue;
2244
+ },
2245
+ get saturation() {
2246
+ return saturation;
2247
+ },
2248
+ colors(defs) {
2249
+ colorDefs = {
2250
+ ...colorDefs,
2251
+ ...defs
2252
+ };
2253
+ invalidate();
2254
+ },
2255
+ color(name, def) {
2256
+ if (def === void 0) return colorDefs[name];
2257
+ colorDefs[name] = def;
2258
+ invalidate();
2259
+ },
2260
+ remove(names) {
2261
+ const list = Array.isArray(names) ? names : [names];
2262
+ for (const name of list) delete colorDefs[name];
2263
+ invalidate();
2264
+ },
2265
+ has(name) {
2266
+ return name in colorDefs;
2267
+ },
2268
+ list() {
2269
+ return Object.keys(colorDefs);
2270
+ },
2271
+ reset() {
2272
+ colorDefs = {};
2273
+ invalidate();
2274
+ },
2275
+ export() {
2276
+ return {
2277
+ hue,
2278
+ saturation,
2279
+ colors: { ...colorDefs }
2280
+ };
2281
+ },
2282
+ extend(options) {
2283
+ const newHue = options.hue ?? hue;
2284
+ const newSat = options.saturation ?? saturation;
2285
+ const inheritedColors = {};
2286
+ for (const [name, def] of Object.entries(colorDefs)) if (def.inherit !== false) inheritedColors[name] = def;
2287
+ return createTheme(newHue, newSat, options.colors ? {
2288
+ ...inheritedColors,
2289
+ ...options.colors
2290
+ } : { ...inheritedColors });
2291
+ },
2292
+ resolve() {
2293
+ return new Map(resolveCached());
2294
+ },
2295
+ tokens(options) {
2296
+ const modes = resolveModes(options?.modes);
2297
+ return buildFlatTokenMap(resolveCached(), "", modes, options?.format);
2298
+ },
2299
+ tasty(options) {
2300
+ const cfg = getConfig();
2301
+ const states = {
2302
+ dark: options?.states?.dark ?? cfg.states.dark,
2303
+ highContrast: options?.states?.highContrast ?? cfg.states.highContrast
2304
+ };
2305
+ const modes = resolveModes(options?.modes);
2306
+ return buildTokenMap(resolveCached(), "", states, modes, options?.format);
2307
+ },
2308
+ json(options) {
2309
+ const modes = resolveModes(options?.modes);
2310
+ return buildJsonMap(resolveCached(), modes, options?.format);
2311
+ },
2312
+ css(options) {
2313
+ return buildCssMap(resolveCached(), "", options?.suffix ?? "-color", options?.format ?? "rgb");
2314
+ }
2315
+ };
2316
+ }
2317
+
2318
+ //#endregion
2319
+ //#region src/glaze.ts
2320
+ /**
2321
+ * Glaze — OKHSL-based color theme generator.
2322
+ *
2323
+ * Public API entry. Wires `glaze()` and its attached static methods to
2324
+ * the focused modules in this folder:
2325
+ * - `theme.ts` — single-theme factory
2326
+ * - `palette.ts` — multi-theme composition
2327
+ * - `color-token.ts` — standalone single-color tokens (`glaze.color`)
2328
+ * - `shadow.ts` — standalone shadow factory (`glaze.shadow`)
2329
+ * - `formatters.ts` — variant → string (`glaze.format`)
2330
+ * - `config.ts` — global config singleton
2331
+ */
2157
2332
  /**
2158
2333
  * Create a single-hue glaze theme.
2159
2334
  *
@@ -2168,41 +2343,18 @@ function glaze(hueOrOptions, saturation) {
2168
2343
  if (typeof hueOrOptions === "number") return createTheme(hueOrOptions, saturation ?? 100);
2169
2344
  return createTheme(hueOrOptions.hue, hueOrOptions.saturation);
2170
2345
  }
2171
- /**
2172
- * Configure global glaze settings.
2173
- */
2174
- glaze.configure = function configure(config) {
2175
- globalConfig = {
2176
- lightLightness: config.lightLightness ?? globalConfig.lightLightness,
2177
- darkLightness: config.darkLightness ?? globalConfig.darkLightness,
2178
- darkDesaturation: config.darkDesaturation ?? globalConfig.darkDesaturation,
2179
- darkCurve: config.darkCurve ?? globalConfig.darkCurve,
2180
- states: {
2181
- dark: config.states?.dark ?? globalConfig.states.dark,
2182
- highContrast: config.states?.highContrast ?? globalConfig.states.highContrast
2183
- },
2184
- modes: {
2185
- dark: config.modes?.dark ?? globalConfig.modes.dark,
2186
- highContrast: config.modes?.highContrast ?? globalConfig.modes.highContrast
2187
- },
2188
- shadowTuning: config.shadowTuning ?? globalConfig.shadowTuning
2189
- };
2346
+ /** Configure global glaze settings. */
2347
+ glaze.configure = function configure$1(config) {
2348
+ configure(config);
2190
2349
  };
2191
- /**
2192
- * Compose multiple themes into a palette.
2193
- */
2350
+ /** Compose multiple themes into a palette. */
2194
2351
  glaze.palette = function palette(themes, options) {
2195
2352
  return createPalette(themes, options);
2196
2353
  };
2197
- /**
2198
- * Create a theme from a serialized export.
2199
- */
2354
+ /** Create a theme from a serialized export. */
2200
2355
  glaze.from = function from(data) {
2201
2356
  return createTheme(data.hue, data.saturation, data.colors);
2202
2357
  };
2203
- function isStructuredColorInput(input) {
2204
- return typeof input === "object" && input !== null && !Array.isArray(input) && "hue" in input && "lightness" in input;
2205
- }
2206
2358
  /**
2207
2359
  * Create a standalone single-color token.
2208
2360
  *
@@ -2215,17 +2367,23 @@ function isStructuredColorInput(input) {
2215
2367
  * emits (`rgb()`, `hsl()`, `okhsl()`, `oklch()`), an `OkhslColor`
2216
2368
  * object `{ h, s, l }` (0–1 ranges), or an `[r, g, b]` (0–255) tuple.
2217
2369
  *
2218
- * Defaults vary by input form:
2219
- * - String value-shorthand: `mode: 'auto'` with snapshotted scaling
2220
- * `{ lightLightness: false, darkLightness: [globalConfig.darkLightness[0], 100] }`.
2221
- * Light preserves the input exactly; dark Möbius-inverts up to 100, so
2222
- * `glaze.color('#000')` renders as `#fff` in dark mode (and
2223
- * `glaze.color('#fff')` falls to the dark `lo` floor).
2224
- * - `OkhslColor` object / RGB-tuple value-shorthand: `mode: 'fixed'`
2225
- * with `scaling: { lightLightness: false }` light preserves the
2226
- * input; dark linearly maps into `globalConfig.darkLightness`.
2227
- * - Structured form (`{ hue, saturation, lightness, ... }`):
2228
- * `mode: 'fixed'`; both windows come from `globalConfig`.
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.
2383
+ *
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.
2229
2387
  *
2230
2388
  * Relative `lightness: '+N'` and `contrast: <ratio>` are anchored to
2231
2389
  * the literal seed (the value passed in) by default, pinned at
@@ -2260,9 +2418,7 @@ glaze.shadow = function shadow(input) {
2260
2418
  alpha: 1
2261
2419
  } : void 0, input.intensity, tuning);
2262
2420
  };
2263
- /**
2264
- * Format a resolved color variant as a CSS string.
2265
- */
2421
+ /** Format a resolved color variant as a CSS string. */
2266
2422
  glaze.format = function format(variant, colorFormat) {
2267
2423
  return formatVariant(variant, colorFormat);
2268
2424
  };
@@ -2308,30 +2464,13 @@ glaze.fromRgb = function fromRgb(r, g, b) {
2308
2464
  glaze.colorFrom = function colorFrom(data) {
2309
2465
  return colorFromExport(data);
2310
2466
  };
2311
- /**
2312
- * Get the current global configuration (for testing/debugging).
2313
- */
2467
+ /** Get the current global configuration (for testing/debugging). */
2314
2468
  glaze.getConfig = function getConfig() {
2315
- return { ...globalConfig };
2469
+ return snapshotConfig();
2316
2470
  };
2317
- /**
2318
- * Reset global configuration to defaults.
2319
- */
2320
- glaze.resetConfig = function resetConfig() {
2321
- globalConfig = {
2322
- lightLightness: [10, 100],
2323
- darkLightness: [15, 95],
2324
- darkDesaturation: .1,
2325
- darkCurve: .5,
2326
- states: {
2327
- dark: "@dark",
2328
- highContrast: "@high-contrast"
2329
- },
2330
- modes: {
2331
- dark: true,
2332
- highContrast: false
2333
- }
2334
- };
2471
+ /** Reset global configuration to defaults. */
2472
+ glaze.resetConfig = function resetConfig$1() {
2473
+ resetConfig();
2335
2474
  };
2336
2475
 
2337
2476
  //#endregion