@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.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,116 +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 preserve their light lightness exactly
912
- * (`lightLightness: false`) and use an extended dark window
913
- * `[globalConfig.darkLightness[0], 100]` so a totally-black input can
914
- * Möbius-invert to totally-white in dark mode. Object / tuple /
915
- * structured inputs snapshot both windows from `globalConfig` verbatim
916
- * so they behave like an ordinary theme color (auto-adapted on both
917
- * sides).
918
- */
919
- function defaultStandaloneScaling(isString) {
920
- if (isString) {
921
- const [darkLo] = globalConfig.darkLightness;
922
- return {
923
- lightLightness: false,
924
- darkLightness: [darkLo, 100]
925
- };
926
- }
927
- return {
928
- lightLightness: globalConfig.lightLightness,
929
- darkLightness: globalConfig.darkLightness
930
- };
931
- }
932
- /** Reserved internal names that user-supplied `name` must not collide with. */
933
- const RESERVED_STANDALONE_NAMES = new Set([
934
- STANDALONE_VALUE,
935
- STANDALONE_SEED,
936
- STANDALONE_BASE
937
- ]);
938
- /**
939
- * Discriminate a `GlazeColorToken` from a raw `GlazeColorValue`.
940
- * Used to widen `base?` so it accepts either a token reference or a
941
- * raw value (auto-wrapped into `glaze.color(value)`).
942
- */
943
- function isGlazeColorToken(candidate) {
944
- return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate) && "resolve" in candidate && typeof candidate.resolve === "function";
945
- }
946
- let globalConfig = {
947
- lightLightness: [10, 100],
948
- darkLightness: [15, 95],
949
- darkDesaturation: .1,
950
- darkCurve: .5,
951
- states: {
952
- dark: "@dark",
953
- highContrast: "@high-contrast"
954
- },
955
- modes: {
956
- dark: true,
957
- highContrast: false
958
- }
959
- };
960
- function pairNormal(p) {
961
- return Array.isArray(p) ? p[0] : p;
962
- }
963
- function pairHC(p) {
964
- return Array.isArray(p) ? p[1] : p;
965
- }
966
- /**
967
- * Dedupe contrast warnings within a single process. The cache survives
968
- * the lifetime of a token because tokens memoize their resolution; the
969
- * limit is a soft cap to keep noise bounded across long-lived sessions
970
- * (e.g. dev servers with HMR re-resolving themes repeatedly).
971
- */
972
- const CONTRAST_WARN_CACHE_LIMIT = 256;
973
- const contrastWarnCache = /* @__PURE__ */ new Set();
974
- function schemeLabel(isDark, isHighContrast) {
975
- if (isDark && isHighContrast) return "darkContrast";
976
- if (isDark) return "dark";
977
- if (isHighContrast) return "lightContrast";
978
- return "light";
979
- }
980
- function formatContrastTarget(input, ratio) {
981
- return typeof input === "string" ? `"${input}" (${ratio.toFixed(2)})` : ratio.toFixed(2);
982
- }
983
- /**
984
- * Slack factor below the requested target before we emit a warning.
985
- * The contrast solver already overshoots by `OVERSHOOT` (currently 1%)
986
- * to absorb rounding noise (`see findLightnessForContrast` in
987
- * `contrast-solver.ts`), so an `actual` ratio within ~2x that overshoot
988
- * 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.
989
1009
  */
990
- const CONTRAST_WARN_SLACK = .98;
991
- function warnContrastUnmet(name, isDark, isHighContrast, target, actual) {
992
- const targetRatio = resolveMinContrast(target);
993
- if (actual >= targetRatio * CONTRAST_WARN_SLACK) return;
994
- const scheme = schemeLabel(isDark, isHighContrast);
995
- const key = `${name}|${scheme}|${targetRatio.toFixed(3)}|${actual.toFixed(2)}`;
996
- if (contrastWarnCache.has(key)) return;
997
- if (contrastWarnCache.size >= CONTRAST_WARN_CACHE_LIMIT) contrastWarnCache.clear();
998
- contrastWarnCache.add(key);
999
- 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.`);
1000
- }
1001
1010
  function isShadowDef(def) {
1002
1011
  return def.type === "shadow";
1003
1012
  }
@@ -1014,11 +1023,12 @@ const DEFAULT_SHADOW_TUNING = {
1014
1023
  bgHueBlend: .2
1015
1024
  };
1016
1025
  function resolveShadowTuning(perColor) {
1026
+ const globalTuning = getConfig().shadowTuning;
1017
1027
  return {
1018
1028
  ...DEFAULT_SHADOW_TUNING,
1019
- ...globalConfig.shadowTuning,
1029
+ ...globalTuning,
1020
1030
  ...perColor,
1021
- lightnessBounds: perColor?.lightnessBounds ?? globalConfig.shadowTuning?.lightnessBounds ?? DEFAULT_SHADOW_TUNING.lightnessBounds
1031
+ lightnessBounds: perColor?.lightnessBounds ?? globalTuning?.lightnessBounds ?? DEFAULT_SHADOW_TUNING.lightnessBounds
1022
1032
  };
1023
1033
  }
1024
1034
  function circularLerp(a, b, t) {
@@ -1059,6 +1069,80 @@ function computeShadow(bg, fg, intensity, tuning) {
1059
1069
  alpha
1060
1070
  };
1061
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
+ */
1062
1146
  function validateColorDefs(defs, externalBases) {
1063
1147
  const localNames = new Set(Object.keys(defs));
1064
1148
  const allNames = new Set([...localNames, ...externalBases ? externalBases.keys() : []]);
@@ -1133,89 +1217,62 @@ function topoSort(defs) {
1133
1217
  for (const name of Object.keys(defs)) visit(name);
1134
1218
  return result;
1135
1219
  }
1220
+
1221
+ //#endregion
1222
+ //#region src/warnings.ts
1136
1223
  /**
1137
- * Resolve the active lightness window for a scheme.
1138
- * - HC variants always return `[0, 100]` (existing behavior, predates per-call overrides).
1139
- * - Otherwise, per-call `scaling` (e.g. from `glaze.color()`'s third arg) wins;
1140
- * `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.
1141
1230
  */
1142
- function lightnessWindow(isHighContrast, kind, scaling) {
1143
- if (isHighContrast) return [0, 100];
1144
- if (scaling) {
1145
- const override = kind === "dark" ? scaling.darkLightness : scaling.lightLightness;
1146
- if (override === false) return [0, 100];
1147
- if (override !== void 0) return override;
1148
- }
1149
- return kind === "dark" ? globalConfig.darkLightness : globalConfig.lightLightness;
1150
- }
1151
- function mapLightnessLight(l, mode, isHighContrast, scaling) {
1152
- if (mode === "static") return l;
1153
- const [lo, hi] = lightnessWindow(isHighContrast, "light", scaling);
1154
- return l * (hi - lo) / 100 + lo;
1155
- }
1156
- function mobiusCurve(t, beta) {
1157
- if (beta >= 1) return t;
1158
- return t / (t + beta * (1 - t));
1159
- }
1160
- function mapLightnessDark(l, mode, isHighContrast, scaling) {
1161
- if (mode === "static") return l;
1162
- const beta = isHighContrast ? pairHC(globalConfig.darkCurve) : pairNormal(globalConfig.darkCurve);
1163
- const [darkLo, darkHi] = lightnessWindow(isHighContrast, "dark", scaling);
1164
- if (mode === "fixed") return l * (darkHi - darkLo) / 100 + darkLo;
1165
- const [lightLo, lightHi] = lightnessWindow(isHighContrast, "light", scaling);
1166
- const t = (lightHi - (l * (lightHi - lightLo) / 100 + lightLo)) / (lightHi - lightLo);
1167
- return darkLo + (darkHi - darkLo) * mobiusCurve(t, beta);
1168
- }
1169
- function lightMappedToDark(lightL, isHighContrast, scaling) {
1170
- const beta = isHighContrast ? pairHC(globalConfig.darkCurve) : pairNormal(globalConfig.darkCurve);
1171
- const [lightLo, lightHi] = lightnessWindow(isHighContrast, "light", scaling);
1172
- const [darkLo, darkHi] = lightnessWindow(isHighContrast, "dark", scaling);
1173
- const t = (lightHi - clamp(lightL, lightLo, lightHi)) / (lightHi - lightLo);
1174
- return darkLo + (darkHi - darkLo) * mobiusCurve(t, beta);
1175
- }
1176
- function mapSaturationDark(s, mode) {
1177
- if (mode === "static") return s;
1178
- return s * (1 - globalConfig.darkDesaturation);
1179
- }
1180
- function schemeLightnessRange(isDark, mode, isHighContrast, scaling) {
1181
- if (mode === "static") return [0, 1];
1182
- const [lo, hi] = lightnessWindow(isHighContrast, isDark ? "dark" : "light", scaling);
1183
- return [lo / 100, hi / 100];
1184
- }
1185
- function clamp(v, min, max) {
1186
- return Math.max(min, Math.min(max, v));
1187
- }
1231
+ const CONTRAST_WARN_CACHE_LIMIT = 256;
1232
+ const contrastWarnCache = /* @__PURE__ */ new Set();
1188
1233
  /**
1189
- * Parse a value that can be absolute (number) or relative (signed string).
1190
- * Returns the numeric value and whether it's relative.
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.
1191
1239
  */
1192
- function parseRelativeOrAbsolute(value) {
1193
- if (typeof value === "number") return {
1194
- value,
1195
- relative: false
1196
- };
1197
- return {
1198
- value: parseFloat(value),
1199
- relative: true
1200
- };
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";
1201
1246
  }
1202
- /**
1203
- * Compute the effective hue for a color, given the theme seed hue
1204
- * and an optional per-color hue override.
1205
- */
1206
- function resolveEffectiveHue(seedHue, defHue) {
1207
- if (defHue === void 0) return seedHue;
1208
- const parsed = parseRelativeOrAbsolute(defHue);
1209
- if (parsed.relative) return ((seedHue + parsed.value) % 360 + 360) % 360;
1210
- return (parsed.value % 360 + 360) % 360;
1247
+ function formatContrastTarget(input, ratio) {
1248
+ return typeof input === "string" ? `"${input}" (${ratio.toFixed(2)})` : ratio.toFixed(2);
1249
+ }
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.`);
1211
1259
  }
1260
+
1261
+ //#endregion
1262
+ //#region src/resolver.ts
1212
1263
  /**
1213
- * Check whether a lightness value represents an absolute root definition
1214
- * (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.
1215
1270
  */
1216
- function isAbsoluteLightness(lightness) {
1217
- if (lightness === void 0) return false;
1218
- 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;
1219
1276
  }
1220
1277
  function resolveRootColor(_name, def, _ctx, isHighContrast) {
1221
1278
  const rawL = def.lightness;
@@ -1269,12 +1326,6 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
1269
1326
  satFactor
1270
1327
  };
1271
1328
  }
1272
- function getSchemeVariant(color, isDark, isHighContrast) {
1273
- if (isDark && isHighContrast) return color.darkContrast;
1274
- if (isDark) return color.dark;
1275
- if (isHighContrast) return color.lightContrast;
1276
- return color.light;
1277
- }
1278
1329
  function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
1279
1330
  if (isShadowDef(def)) return resolveShadowForScheme(def, ctx, isDark, isHighContrast);
1280
1331
  if (isMixDef(def)) return resolveMixForScheme(def, ctx, isDark, isHighContrast);
@@ -1400,26 +1451,27 @@ function resolveMixForScheme(def, ctx, isDark, isHighContrast) {
1400
1451
  alpha: 1
1401
1452
  };
1402
1453
  }
1403
- function resolveAllColors(hue, saturation, defs, scaling, externalBases) {
1404
- validateColorDefs(defs, externalBases);
1405
- const order = topoSort(defs);
1406
- const ctx = {
1407
- hue,
1408
- saturation,
1409
- defs,
1410
- resolved: /* @__PURE__ */ new Map(),
1411
- scaling
1412
- };
1413
- if (externalBases) for (const [name, color] of externalBases) ctx.resolved.set(name, color);
1414
- function defMode(def) {
1415
- if (isShadowDef(def) || isMixDef(def)) return void 0;
1416
- return def.mode ?? "auto";
1417
- }
1418
- 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();
1419
1466
  for (const name of order) {
1420
- const variant = resolveColorForScheme(name, defs[name], ctx, false, false);
1421
- lightMap.set(name, variant);
1422
- 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, {
1423
1475
  name,
1424
1476
  light: variant,
1425
1477
  dark: variant,
@@ -1428,49 +1480,40 @@ function resolveAllColors(hue, saturation, defs, scaling, externalBases) {
1428
1480
  mode: defMode(defs[name])
1429
1481
  });
1430
1482
  }
1431
- const lightHCMap = /* @__PURE__ */ new Map();
1432
- for (const name of order) ctx.resolved.set(name, {
1433
- ...ctx.resolved.get(name),
1434
- lightContrast: lightMap.get(name)
1435
- });
1436
- for (const name of order) {
1437
- const variant = resolveColorForScheme(name, defs[name], ctx, false, true);
1438
- lightHCMap.set(name, variant);
1439
- ctx.resolved.set(name, {
1440
- ...ctx.resolved.get(name),
1441
- lightContrast: variant
1442
- });
1443
- }
1444
- const darkMap = /* @__PURE__ */ new Map();
1445
- for (const name of order) ctx.resolved.set(name, {
1446
- name,
1447
- light: lightMap.get(name),
1448
- dark: lightMap.get(name),
1449
- lightContrast: lightHCMap.get(name),
1450
- darkContrast: lightHCMap.get(name),
1451
- mode: defMode(defs[name])
1452
- });
1453
- for (const name of order) {
1454
- const variant = resolveColorForScheme(name, defs[name], ctx, true, false);
1455
- darkMap.set(name, variant);
1456
- ctx.resolved.set(name, {
1457
- ...ctx.resolved.get(name),
1458
- dark: variant
1459
- });
1460
- }
1461
- const darkHCMap = /* @__PURE__ */ new Map();
1462
- for (const name of order) ctx.resolved.set(name, {
1463
- ...ctx.resolved.get(name),
1464
- darkContrast: darkMap.get(name)
1465
- });
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) {
1466
1490
  for (const name of order) {
1467
- const variant = resolveColorForScheme(name, defs[name], ctx, true, true);
1468
- darkHCMap.set(name, variant);
1491
+ const existing = ctx.resolved.get(name);
1469
1492
  ctx.resolved.set(name, {
1470
- ...ctx.resolved.get(name),
1471
- darkContrast: variant
1493
+ ...existing,
1494
+ [field]: source.get(name)
1472
1495
  });
1473
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");
1474
1517
  const result = /* @__PURE__ */ new Map();
1475
1518
  for (const name of order) result.set(name, {
1476
1519
  name,
@@ -1482,6 +1525,19 @@ function resolveAllColors(hue, saturation, defs, scaling, externalBases) {
1482
1525
  });
1483
1526
  return result;
1484
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
+ */
1485
1541
  const formatters = {
1486
1542
  okhsl: formatOkhsl,
1487
1543
  rgb: formatRgb,
@@ -1498,9 +1554,10 @@ function formatVariant(v, format = "okhsl") {
1498
1554
  return `${base.slice(0, closing)} / ${fmt(v.alpha, 4)})`;
1499
1555
  }
1500
1556
  function resolveModes(override) {
1557
+ const cfg = getConfig();
1501
1558
  return {
1502
- dark: override?.dark ?? globalConfig.modes.dark,
1503
- highContrast: override?.highContrast ?? globalConfig.modes.highContrast
1559
+ dark: override?.dark ?? cfg.modes.dark,
1560
+ highContrast: override?.highContrast ?? cfg.modes.highContrast
1504
1561
  };
1505
1562
  }
1506
1563
  function buildTokenMap(resolved, prefix, states, modes, format = "okhsl") {
@@ -1561,206 +1618,74 @@ function buildCssMap(resolved, prefix, suffix, format) {
1561
1618
  darkContrast: lines.darkContrast.join("\n")
1562
1619
  };
1563
1620
  }
1564
- function createTheme(hue, saturation, initialColors) {
1565
- 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
+ }
1566
1674
  return {
1567
- get hue() {
1568
- return hue;
1569
- },
1570
- get saturation() {
1571
- return saturation;
1572
- },
1573
- colors(defs) {
1574
- colorDefs = {
1575
- ...colorDefs,
1576
- ...defs
1577
- };
1578
- },
1579
- color(name, def) {
1580
- if (def === void 0) return colorDefs[name];
1581
- colorDefs[name] = def;
1582
- },
1583
- remove(names) {
1584
- const list = Array.isArray(names) ? names : [names];
1585
- for (const name of list) delete colorDefs[name];
1586
- },
1587
- has(name) {
1588
- return name in colorDefs;
1589
- },
1590
- list() {
1591
- return Object.keys(colorDefs);
1592
- },
1593
- reset() {
1594
- colorDefs = {};
1595
- },
1596
- export() {
1597
- return {
1598
- hue,
1599
- saturation,
1600
- colors: { ...colorDefs }
1601
- };
1602
- },
1603
- extend(options) {
1604
- const newHue = options.hue ?? hue;
1605
- const newSat = options.saturation ?? saturation;
1606
- const inheritedColors = {};
1607
- for (const [name, def] of Object.entries(colorDefs)) if (def.inherit !== false) inheritedColors[name] = def;
1608
- return createTheme(newHue, newSat, options.colors ? {
1609
- ...inheritedColors,
1610
- ...options.colors
1611
- } : { ...inheritedColors });
1612
- },
1613
- resolve() {
1614
- return resolveAllColors(hue, saturation, colorDefs);
1615
- },
1616
- tokens(options) {
1617
- return buildFlatTokenMap(resolveAllColors(hue, saturation, colorDefs), "", resolveModes(options?.modes), options?.format);
1618
- },
1619
- tasty(options) {
1620
- return buildTokenMap(resolveAllColors(hue, saturation, colorDefs), "", {
1621
- dark: options?.states?.dark ?? globalConfig.states.dark,
1622
- highContrast: options?.states?.highContrast ?? globalConfig.states.highContrast
1623
- }, resolveModes(options?.modes), options?.format);
1624
- },
1625
- json(options) {
1626
- return buildJsonMap(resolveAllColors(hue, saturation, colorDefs), resolveModes(options?.modes), options?.format);
1627
- },
1628
- css(options) {
1629
- return buildCssMap(resolveAllColors(hue, saturation, colorDefs), "", options?.suffix ?? "-color", options?.format ?? "rgb");
1630
- }
1675
+ lightLightness: cfg.lightLightness,
1676
+ darkLightness: cfg.darkLightness
1631
1677
  };
1632
1678
  }
1633
- function resolvePrefix(options, themeName, defaultPrefix = false) {
1634
- const prefix = options?.prefix ?? defaultPrefix;
1635
- if (prefix === true) return `${themeName}-`;
1636
- if (typeof prefix === "object" && prefix !== null) return prefix[themeName] ?? `${themeName}-`;
1637
- 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";
1638
1686
  }
1639
- function validatePrimaryTheme(primary, themes) {
1640
- if (primary !== void 0 && !(primary in themes)) {
1641
- const available = Object.keys(themes).join(", ");
1642
- throw new Error(`glaze: primary theme "${primary}" not found in palette. Available: ${available}.`);
1643
- }
1644
- }
1645
- /**
1646
- * Resolve the effective primary for an export call.
1647
- * `false` disables, a string overrides, `undefined` inherits from palette.
1648
- */
1649
- function resolveEffectivePrimary(exportPrimary, palettePrimary) {
1650
- if (exportPrimary === false) return void 0;
1651
- return exportPrimary ?? palettePrimary;
1652
- }
1653
- /**
1654
- * Filter a resolved color map, skipping keys already in `seen`.
1655
- * Warns on collision and keeps the first-written value (first-write-wins).
1656
- * Returns a new map containing only non-colliding entries.
1657
- */
1658
- function filterCollisions(resolved, prefix, seen, themeName, isPrimary) {
1659
- const filtered = /* @__PURE__ */ new Map();
1660
- const label = isPrimary ? `${themeName} (primary)` : themeName;
1661
- for (const [name, color] of resolved) {
1662
- const key = `${prefix}${name}`;
1663
- if (seen.has(key)) {
1664
- console.warn(`glaze: token "${key}" from theme "${label}" collides with theme "${seen.get(key)}" — skipping.`);
1665
- continue;
1666
- }
1667
- seen.set(key, label);
1668
- filtered.set(name, color);
1669
- }
1670
- return filtered;
1671
- }
1672
- function createPalette(themes, paletteOptions) {
1673
- validatePrimaryTheme(paletteOptions?.primary, themes);
1674
- return {
1675
- tokens(options) {
1676
- const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
1677
- if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
1678
- const modes = resolveModes(options?.modes);
1679
- const allTokens = {};
1680
- const seen = /* @__PURE__ */ new Map();
1681
- for (const [themeName, theme] of Object.entries(themes)) {
1682
- const resolved = theme.resolve();
1683
- const prefix = resolvePrefix(options, themeName, true);
1684
- const tokens = buildFlatTokenMap(filterCollisions(resolved, prefix, seen, themeName), prefix, modes, options?.format);
1685
- for (const variant of Object.keys(tokens)) {
1686
- if (!allTokens[variant]) allTokens[variant] = {};
1687
- Object.assign(allTokens[variant], tokens[variant]);
1688
- }
1689
- if (themeName === effectivePrimary) {
1690
- const unprefixed = buildFlatTokenMap(filterCollisions(resolved, "", seen, themeName, true), "", modes, options?.format);
1691
- for (const variant of Object.keys(unprefixed)) Object.assign(allTokens[variant], unprefixed[variant]);
1692
- }
1693
- }
1694
- return allTokens;
1695
- },
1696
- tasty(options) {
1697
- const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
1698
- if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
1699
- const states = {
1700
- dark: options?.states?.dark ?? globalConfig.states.dark,
1701
- highContrast: options?.states?.highContrast ?? globalConfig.states.highContrast
1702
- };
1703
- const modes = resolveModes(options?.modes);
1704
- const allTokens = {};
1705
- const seen = /* @__PURE__ */ new Map();
1706
- for (const [themeName, theme] of Object.entries(themes)) {
1707
- const resolved = theme.resolve();
1708
- const prefix = resolvePrefix(options, themeName, true);
1709
- const tokens = buildTokenMap(filterCollisions(resolved, prefix, seen, themeName), prefix, states, modes, options?.format);
1710
- Object.assign(allTokens, tokens);
1711
- if (themeName === effectivePrimary) {
1712
- const unprefixed = buildTokenMap(filterCollisions(resolved, "", seen, themeName, true), "", states, modes, options?.format);
1713
- Object.assign(allTokens, unprefixed);
1714
- }
1715
- }
1716
- return allTokens;
1717
- },
1718
- json(options) {
1719
- const modes = resolveModes(options?.modes);
1720
- const result = {};
1721
- for (const [themeName, theme] of Object.entries(themes)) result[themeName] = buildJsonMap(theme.resolve(), modes, options?.format);
1722
- return result;
1723
- },
1724
- css(options) {
1725
- const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
1726
- if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
1727
- const suffix = options?.suffix ?? "-color";
1728
- const format = options?.format ?? "rgb";
1729
- const allLines = {
1730
- light: [],
1731
- dark: [],
1732
- lightContrast: [],
1733
- darkContrast: []
1734
- };
1735
- const seen = /* @__PURE__ */ new Map();
1736
- for (const [themeName, theme] of Object.entries(themes)) {
1737
- const resolved = theme.resolve();
1738
- const prefix = resolvePrefix(options, themeName, true);
1739
- const css = buildCssMap(filterCollisions(resolved, prefix, seen, themeName), prefix, suffix, format);
1740
- for (const key of [
1741
- "light",
1742
- "dark",
1743
- "lightContrast",
1744
- "darkContrast"
1745
- ]) if (css[key]) allLines[key].push(css[key]);
1746
- if (themeName === effectivePrimary) {
1747
- const unprefixed = buildCssMap(filterCollisions(resolved, "", seen, themeName, true), "", suffix, format);
1748
- for (const key of [
1749
- "light",
1750
- "dark",
1751
- "lightContrast",
1752
- "darkContrast"
1753
- ]) if (unprefixed[key]) allLines[key].push(unprefixed[key]);
1754
- }
1755
- }
1756
- return {
1757
- light: allLines.light.join("\n"),
1758
- dark: allLines.dark.join("\n"),
1759
- lightContrast: allLines.lightContrast.join("\n"),
1760
- darkContrast: allLines.darkContrast.join("\n")
1761
- };
1762
- }
1763
- };
1687
+ function isStructuredColorInput(input) {
1688
+ return typeof input === "object" && input !== null && !Array.isArray(input) && "hue" in input && "lightness" in input;
1764
1689
  }
1765
1690
  /**
1766
1691
  * Matches the CSS color functions Glaze itself emits (`rgb()`, `hsl()`,
@@ -1879,9 +1804,7 @@ function validateOkhslColor(value) {
1879
1804
  if (!Number.isFinite(h) || !Number.isFinite(s) || !Number.isFinite(l)) throw new Error("glaze.color: OkhslColor h/s/l must be finite numbers.");
1880
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)?");
1881
1806
  }
1882
- /**
1883
- * Validate a user-supplied `[r, g, b]` tuple in 0-255.
1884
- */
1807
+ /** Validate a user-supplied `[r, g, b]` tuple in 0-255. */
1885
1808
  function validateRgbTuple(value) {
1886
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(", ")}]).`);
1887
1810
  }
@@ -2007,10 +1930,13 @@ function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effect
2007
1930
  cached = resolveAllColors(seedHue, seedSaturation, defs, effectiveScaling, baseToken ? new Map([[STANDALONE_BASE, baseToken.resolve()]]) : void 0);
2008
1931
  return cached;
2009
1932
  };
2010
- const resolveStates = (options) => ({
2011
- dark: options?.states?.dark ?? globalConfig.states.dark,
2012
- highContrast: options?.states?.highContrast ?? globalConfig.states.highContrast
2013
- });
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
+ };
2014
1940
  const tokenLike = (options) => {
2015
1941
  return buildTokenMap(resolveOnce(), "", resolveStates(options), resolveModes(options?.modes), options?.format)[`#${primary}`];
2016
1942
  };
@@ -2040,38 +1966,6 @@ function resolveBaseToken(base) {
2040
1966
  if (isGlazeColorToken(base)) return base;
2041
1967
  return createColorTokenFromValue(base, void 0, void 0);
2042
1968
  }
2043
- /**
2044
- * Build a JSON-safe snapshot of `GlazeColorOverrides`. `base` is
2045
- * recursively serialized when it was originally a token; raw values are
2046
- * preserved as-is so `glaze.colorFrom(...)` round-trips them.
2047
- */
2048
- function buildOverridesExport(options) {
2049
- const out = {};
2050
- if (options.hue !== void 0) out.hue = options.hue;
2051
- if (options.saturation !== void 0) out.saturation = options.saturation;
2052
- if (options.lightness !== void 0) out.lightness = options.lightness;
2053
- if (options.saturationFactor !== void 0) out.saturationFactor = options.saturationFactor;
2054
- if (options.mode !== void 0) out.mode = options.mode;
2055
- if (options.contrast !== void 0) out.contrast = options.contrast;
2056
- if (options.opacity !== void 0) out.opacity = options.opacity;
2057
- if (options.name !== void 0) out.name = options.name;
2058
- if (options.base !== void 0) out.base = isGlazeColorToken(options.base) ? options.base.export() : options.base;
2059
- return out;
2060
- }
2061
- function buildStructuredInputExport(input) {
2062
- const out = {
2063
- hue: input.hue,
2064
- saturation: input.saturation,
2065
- lightness: input.lightness
2066
- };
2067
- if (input.saturationFactor !== void 0) out.saturationFactor = input.saturationFactor;
2068
- if (input.mode !== void 0) out.mode = input.mode;
2069
- if (input.opacity !== void 0) out.opacity = input.opacity;
2070
- if (input.contrast !== void 0) out.contrast = input.contrast;
2071
- if (input.name !== void 0) out.name = input.name;
2072
- if (input.base !== void 0) out.base = isGlazeColorToken(input.base) ? input.base.export() : input.base;
2073
- return out;
2074
- }
2075
1969
  function createColorToken(input, scaling) {
2076
1970
  validateStructuredInput(input);
2077
1971
  const userName = input.name;
@@ -2116,18 +2010,44 @@ function createColorTokenFromValue(value, options, scaling) {
2116
2010
  return createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effectiveScaling, baseToken, exportData);
2117
2011
  }
2118
2012
  /**
2119
- * Rehydrate a token from its `.export()` snapshot. Recursively rebuilds
2120
- * 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.
2121
2016
  */
2122
- function colorFromExport(data) {
2123
- if (data === null || typeof data !== "object") throw new Error(`glaze.colorFrom: expected an object from token.export(), got ${data === null ? "null" : typeof data}.`);
2124
- if (data.form !== "value" && data.form !== "structured") throw new Error(`glaze.colorFrom: invalid "form" field — expected "value" or "structured" (got ${JSON.stringify(data.form)}).`);
2125
- if (data.input === void 0) throw new Error(`glaze.colorFrom: missing "input" field — expected the original ${data.form === "value" ? "GlazeColorValue" : "GlazeColorInput"}.`);
2126
- if (data.form === "value") {
2127
- const value = data.input;
2128
- return createColorTokenFromValue(value, data.overrides ? rehydrateOverrides(data.overrides) : void 0, data.scaling);
2129
- }
2130
- 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");
2131
2051
  }
2132
2052
  function rehydrateOverrides(data) {
2133
2053
  const out = {};
@@ -2157,13 +2077,258 @@ function rehydrateStructuredInput(data) {
2157
2077
  return out;
2158
2078
  }
2159
2079
  /**
2160
- * Discriminate a `GlazeColorTokenExport` from a raw `GlazeColorValue`.
2161
- * `GlazeColorTokenExport` always has a `form` field set to either
2162
- * `'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()`.
2163
2082
  */
2164
- function isExportedToken(candidate) {
2165
- 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);
2166
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
+ */
2167
2332
  /**
2168
2333
  * Create a single-hue glaze theme.
2169
2334
  *
@@ -2178,41 +2343,18 @@ function glaze(hueOrOptions, saturation) {
2178
2343
  if (typeof hueOrOptions === "number") return createTheme(hueOrOptions, saturation ?? 100);
2179
2344
  return createTheme(hueOrOptions.hue, hueOrOptions.saturation);
2180
2345
  }
2181
- /**
2182
- * Configure global glaze settings.
2183
- */
2184
- glaze.configure = function configure(config) {
2185
- globalConfig = {
2186
- lightLightness: config.lightLightness ?? globalConfig.lightLightness,
2187
- darkLightness: config.darkLightness ?? globalConfig.darkLightness,
2188
- darkDesaturation: config.darkDesaturation ?? globalConfig.darkDesaturation,
2189
- darkCurve: config.darkCurve ?? globalConfig.darkCurve,
2190
- states: {
2191
- dark: config.states?.dark ?? globalConfig.states.dark,
2192
- highContrast: config.states?.highContrast ?? globalConfig.states.highContrast
2193
- },
2194
- modes: {
2195
- dark: config.modes?.dark ?? globalConfig.modes.dark,
2196
- highContrast: config.modes?.highContrast ?? globalConfig.modes.highContrast
2197
- },
2198
- shadowTuning: config.shadowTuning ?? globalConfig.shadowTuning
2199
- };
2346
+ /** Configure global glaze settings. */
2347
+ glaze.configure = function configure$1(config) {
2348
+ configure(config);
2200
2349
  };
2201
- /**
2202
- * Compose multiple themes into a palette.
2203
- */
2350
+ /** Compose multiple themes into a palette. */
2204
2351
  glaze.palette = function palette(themes, options) {
2205
2352
  return createPalette(themes, options);
2206
2353
  };
2207
- /**
2208
- * Create a theme from a serialized export.
2209
- */
2354
+ /** Create a theme from a serialized export. */
2210
2355
  glaze.from = function from(data) {
2211
2356
  return createTheme(data.hue, data.saturation, data.colors);
2212
2357
  };
2213
- function isStructuredColorInput(input) {
2214
- return typeof input === "object" && input !== null && !Array.isArray(input) && "hue" in input && "lightness" in input;
2215
- }
2216
2358
  /**
2217
2359
  * Create a standalone single-color token.
2218
2360
  *
@@ -2276,9 +2418,7 @@ glaze.shadow = function shadow(input) {
2276
2418
  alpha: 1
2277
2419
  } : void 0, input.intensity, tuning);
2278
2420
  };
2279
- /**
2280
- * Format a resolved color variant as a CSS string.
2281
- */
2421
+ /** Format a resolved color variant as a CSS string. */
2282
2422
  glaze.format = function format(variant, colorFormat) {
2283
2423
  return formatVariant(variant, colorFormat);
2284
2424
  };
@@ -2324,30 +2464,13 @@ glaze.fromRgb = function fromRgb(r, g, b) {
2324
2464
  glaze.colorFrom = function colorFrom(data) {
2325
2465
  return colorFromExport(data);
2326
2466
  };
2327
- /**
2328
- * Get the current global configuration (for testing/debugging).
2329
- */
2467
+ /** Get the current global configuration (for testing/debugging). */
2330
2468
  glaze.getConfig = function getConfig() {
2331
- return { ...globalConfig };
2469
+ return snapshotConfig();
2332
2470
  };
2333
- /**
2334
- * Reset global configuration to defaults.
2335
- */
2336
- glaze.resetConfig = function resetConfig() {
2337
- globalConfig = {
2338
- lightLightness: [10, 100],
2339
- darkLightness: [15, 95],
2340
- darkDesaturation: .1,
2341
- darkCurve: .5,
2342
- states: {
2343
- dark: "@dark",
2344
- highContrast: "@high-contrast"
2345
- },
2346
- modes: {
2347
- dark: true,
2348
- highContrast: false
2349
- }
2350
- };
2471
+ /** Reset global configuration to defaults. */
2472
+ glaze.resetConfig = function resetConfig$1() {
2473
+ resetConfig();
2351
2474
  };
2352
2475
 
2353
2476
  //#endregion