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