@tenphi/glaze 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -625,6 +625,25 @@ function resetConfig() {
625
625
  configVersion++;
626
626
  globalConfig = defaultConfig();
627
627
  }
628
+ /**
629
+ * Merge a per-instance config override over a base resolved config.
630
+ * Only fields present in `override` are replaced; others fall through
631
+ * from `base`. `false` for lightness windows passes through as-is
632
+ * (treated as `[0, 100]` by `lightnessWindow()` in scheme-mapping).
633
+ */
634
+ function mergeConfig(base, override) {
635
+ if (!override) return base;
636
+ return {
637
+ lightLightness: override.lightLightness !== void 0 ? override.lightLightness : base.lightLightness,
638
+ darkLightness: override.darkLightness !== void 0 ? override.darkLightness : base.darkLightness,
639
+ darkDesaturation: override.darkDesaturation ?? base.darkDesaturation,
640
+ darkCurve: override.darkCurve ?? base.darkCurve,
641
+ states: base.states,
642
+ modes: base.modes,
643
+ shadowTuning: override.shadowTuning ?? base.shadowTuning,
644
+ autoFlip: override.autoFlip ?? base.autoFlip
645
+ };
646
+ }
628
647
 
629
648
  //#endregion
630
649
  //#region src/hc-pair.ts
@@ -1047,8 +1066,7 @@ const DEFAULT_SHADOW_TUNING = {
1047
1066
  alphaMax: 1,
1048
1067
  bgHueBlend: .2
1049
1068
  };
1050
- function resolveShadowTuning(perColor) {
1051
- const globalTuning = getConfig().shadowTuning;
1069
+ function resolveShadowTuning(perColor, globalTuning) {
1052
1070
  return {
1053
1071
  ...DEFAULT_SHADOW_TUNING,
1054
1072
  ...globalTuning,
@@ -1100,61 +1118,58 @@ function computeShadow(bg, fg, intensity, tuning) {
1100
1118
  /**
1101
1119
  * Light / dark scheme lightness mappings.
1102
1120
  *
1103
- * Owns the active lightness window selection (with per-call scaling
1104
- * overrides and high-contrast handling), the Möbius curve used by the
1105
- * `'auto'` dark adaptation, and the saturation-desaturation reducer
1106
- * for dark mode.
1121
+ * Owns the active lightness window selection (from a resolved effective
1122
+ * config passed in), the Möbius curve used by the `'auto'` dark
1123
+ * adaptation, and the saturation-desaturation reducer for dark mode.
1124
+ *
1125
+ * All functions take a `GlazeConfigResolved` so the full config
1126
+ * (including per-instance overrides) is available without re-reading
1127
+ * the global singleton inside the resolver.
1107
1128
  */
1108
1129
  /**
1109
1130
  * Resolve the active lightness window for a scheme.
1110
- * - HC variants always return `[0, 100]` (existing behavior, predates per-call overrides).
1111
- * - Otherwise, per-call `scaling` (e.g. from `glaze.color()`'s third arg) wins;
1112
- * `false` is interpreted as `[0, 100]` (no remap). Falls back to `globalConfig.*Lightness`.
1131
+ * - HC variants always return `[0, 100]` (no clamping in high-contrast).
1132
+ * - `false` (= "no clamping") is treated as `[0, 100]`.
1133
+ * - Otherwise uses the window from the resolved effective config.
1113
1134
  */
1114
- function lightnessWindow(isHighContrast, kind, scaling) {
1135
+ function lightnessWindow(isHighContrast, kind, config) {
1115
1136
  if (isHighContrast) return [0, 100];
1116
- if (scaling) {
1117
- const override = kind === "dark" ? scaling.darkLightness : scaling.lightLightness;
1118
- if (override === false) return [0, 100];
1119
- if (override !== void 0) return override;
1120
- }
1121
- const cfg = getConfig();
1122
- return kind === "dark" ? cfg.darkLightness : cfg.lightLightness;
1137
+ const win = kind === "dark" ? config.darkLightness : config.lightLightness;
1138
+ if (win === false) return [0, 100];
1139
+ return win;
1123
1140
  }
1124
- function mapLightnessLight(l, mode, isHighContrast, scaling) {
1141
+ function mapLightnessLight(l, mode, isHighContrast, config) {
1125
1142
  if (mode === "static") return l;
1126
- const [lo, hi] = lightnessWindow(isHighContrast, "light", scaling);
1143
+ const [lo, hi] = lightnessWindow(isHighContrast, "light", config);
1127
1144
  return l * (hi - lo) / 100 + lo;
1128
1145
  }
1129
1146
  function mobiusCurve(t, beta) {
1130
1147
  if (beta >= 1) return t;
1131
1148
  return t / (t + beta * (1 - t));
1132
1149
  }
1133
- function mapLightnessDark(l, mode, isHighContrast, scaling) {
1150
+ function mapLightnessDark(l, mode, isHighContrast, config) {
1134
1151
  if (mode === "static") return l;
1135
- const cfg = getConfig();
1136
- const beta = isHighContrast ? pairHC(cfg.darkCurve) : pairNormal(cfg.darkCurve);
1137
- const [darkLo, darkHi] = lightnessWindow(isHighContrast, "dark", scaling);
1152
+ const beta = isHighContrast ? pairHC(config.darkCurve) : pairNormal(config.darkCurve);
1153
+ const [darkLo, darkHi] = lightnessWindow(isHighContrast, "dark", config);
1138
1154
  if (mode === "fixed") return l * (darkHi - darkLo) / 100 + darkLo;
1139
- const [lightLo, lightHi] = lightnessWindow(isHighContrast, "light", scaling);
1155
+ const [lightLo, lightHi] = lightnessWindow(isHighContrast, "light", config);
1140
1156
  const t = (lightHi - (l * (lightHi - lightLo) / 100 + lightLo)) / (lightHi - lightLo);
1141
1157
  return darkLo + (darkHi - darkLo) * mobiusCurve(t, beta);
1142
1158
  }
1143
- function lightMappedToDark(lightL, isHighContrast, scaling) {
1144
- const cfg = getConfig();
1145
- const beta = isHighContrast ? pairHC(cfg.darkCurve) : pairNormal(cfg.darkCurve);
1146
- const [lightLo, lightHi] = lightnessWindow(isHighContrast, "light", scaling);
1147
- const [darkLo, darkHi] = lightnessWindow(isHighContrast, "dark", scaling);
1159
+ function lightMappedToDark(lightL, isHighContrast, config) {
1160
+ const beta = isHighContrast ? pairHC(config.darkCurve) : pairNormal(config.darkCurve);
1161
+ const [lightLo, lightHi] = lightnessWindow(isHighContrast, "light", config);
1162
+ const [darkLo, darkHi] = lightnessWindow(isHighContrast, "dark", config);
1148
1163
  const t = (lightHi - clamp(lightL, lightLo, lightHi)) / (lightHi - lightLo);
1149
1164
  return darkLo + (darkHi - darkLo) * mobiusCurve(t, beta);
1150
1165
  }
1151
- function mapSaturationDark(s, mode) {
1166
+ function mapSaturationDark(s, mode, config) {
1152
1167
  if (mode === "static") return s;
1153
- return s * (1 - getConfig().darkDesaturation);
1168
+ return s * (1 - config.darkDesaturation);
1154
1169
  }
1155
- function schemeLightnessRange(isDark, mode, isHighContrast, scaling) {
1170
+ function schemeLightnessRange(isDark, mode, isHighContrast, config) {
1156
1171
  if (mode === "static") return [0, 1];
1157
- const [lo, hi] = lightnessWindow(isHighContrast, isDark ? "dark" : "light", scaling);
1172
+ const [lo, hi] = lightnessWindow(isHighContrast, isDark ? "dark" : "light", config);
1158
1173
  return [lo / 100, hi / 100];
1159
1174
  }
1160
1175
 
@@ -1292,6 +1307,10 @@ function warnContrastUnmet(name, isDark, isHighContrast, target, actual) {
1292
1307
  * turns a `ColorMap` into a fully resolved `ResolvedColor` per name.
1293
1308
  * Owns the per-scheme resolve helpers for regular, shadow, and mix
1294
1309
  * color defs.
1310
+ *
1311
+ * Every function receives a single `GlazeConfigResolved` so the full
1312
+ * per-instance config (including overrides) is available without
1313
+ * re-reading the global singleton mid-resolve.
1295
1314
  */
1296
1315
  function getSchemeVariant(color, isDark, isHighContrast) {
1297
1316
  if (isDark && isHighContrast) return color.darkContrast;
@@ -1321,18 +1340,17 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
1321
1340
  const parsed = parseRelativeOrAbsolute(isHighContrast ? pairHC(rawLightness) : pairNormal(rawLightness));
1322
1341
  if (parsed.relative) {
1323
1342
  const delta = parsed.value;
1324
- if (isDark && mode === "auto") preferredL = lightMappedToDark(clamp(getSchemeVariant(baseResolved, false, isHighContrast).l * 100 + delta, 0, 100), isHighContrast, ctx.scaling);
1343
+ if (isDark && mode === "auto") preferredL = lightMappedToDark(clamp(getSchemeVariant(baseResolved, false, isHighContrast).l * 100 + delta, 0, 100), isHighContrast, ctx.config);
1325
1344
  else preferredL = clamp(baseL + delta, 0, 100);
1326
- } else if (isDark) preferredL = mapLightnessDark(parsed.value, mode, isHighContrast, ctx.scaling);
1327
- else preferredL = mapLightnessLight(parsed.value, mode, isHighContrast, ctx.scaling);
1345
+ } else if (isDark) preferredL = mapLightnessDark(parsed.value, mode, isHighContrast, ctx.config);
1346
+ else preferredL = mapLightnessLight(parsed.value, mode, isHighContrast, ctx.config);
1328
1347
  }
1329
1348
  const rawContrast = def.contrast;
1330
1349
  if (rawContrast !== void 0) {
1331
1350
  const minCr = isHighContrast ? pairHC(rawContrast) : pairNormal(rawContrast);
1332
- const effectiveSat = isDark ? mapSaturationDark(satFactor * ctx.saturation / 100, mode) : satFactor * ctx.saturation / 100;
1351
+ const effectiveSat = isDark ? mapSaturationDark(satFactor * ctx.saturation / 100, mode, ctx.config) : satFactor * ctx.saturation / 100;
1333
1352
  const baseLinearRgb = okhslToLinearSrgb(baseVariant.h, baseVariant.s, baseVariant.l);
1334
- const windowRange = schemeLightnessRange(isDark, mode, isHighContrast, ctx.scaling);
1335
- const autoFlip = ctx.autoFlip ?? getConfig().autoFlip;
1353
+ const windowRange = schemeLightnessRange(isDark, mode, isHighContrast, ctx.config);
1336
1354
  let initialDirection;
1337
1355
  if (preferredL < baseL) initialDirection = "darker";
1338
1356
  else if (preferredL > baseL) initialDirection = "lighter";
@@ -1344,7 +1362,7 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
1344
1362
  contrast: minCr,
1345
1363
  lightnessRange: [0, 1],
1346
1364
  initialDirection,
1347
- flip: autoFlip
1365
+ flip: ctx.config.autoFlip
1348
1366
  });
1349
1367
  if (!result.met) warnContrastUnmet(name, isDark, isHighContrast, minCr, result.contrast);
1350
1368
  return {
@@ -1378,13 +1396,13 @@ function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
1378
1396
  let finalL;
1379
1397
  let finalSat;
1380
1398
  if (isDark && isRoot) {
1381
- finalL = mapLightnessDark(lightL, mode, isHighContrast, ctx.scaling);
1382
- finalSat = mapSaturationDark(satFactor * ctx.saturation / 100, mode);
1399
+ finalL = mapLightnessDark(lightL, mode, isHighContrast, ctx.config);
1400
+ finalSat = mapSaturationDark(satFactor * ctx.saturation / 100, mode, ctx.config);
1383
1401
  } else if (isDark && !isRoot) {
1384
1402
  finalL = lightL;
1385
- finalSat = mapSaturationDark(satFactor * ctx.saturation / 100, mode);
1403
+ finalSat = mapSaturationDark(satFactor * ctx.saturation / 100, mode, ctx.config);
1386
1404
  } else if (isRoot) {
1387
- finalL = mapLightnessLight(lightL, mode, isHighContrast, ctx.scaling);
1405
+ finalL = mapLightnessLight(lightL, mode, isHighContrast, ctx.config);
1388
1406
  finalSat = satFactor * ctx.saturation / 100;
1389
1407
  } else {
1390
1408
  finalL = lightL;
@@ -1402,7 +1420,7 @@ function resolveShadowForScheme(def, ctx, isDark, isHighContrast) {
1402
1420
  let fgVariant;
1403
1421
  if (def.fg) fgVariant = getSchemeVariant(ctx.resolved.get(def.fg), isDark, isHighContrast);
1404
1422
  const intensity = isHighContrast ? pairHC(def.intensity) : pairNormal(def.intensity);
1405
- const tuning = resolveShadowTuning(def.tuning);
1423
+ const tuning = resolveShadowTuning(def.tuning, ctx.config.shadowTuning);
1406
1424
  return computeShadow(bgVariant, fgVariant, intensity, tuning);
1407
1425
  }
1408
1426
  function variantToLinearRgb(v) {
@@ -1460,14 +1478,13 @@ function resolveMixForScheme(def, ctx, isDark, isHighContrast) {
1460
1478
  else luminanceAt = (v) => {
1461
1479
  return gamutClampedLuminance(okhslToLinearSrgb(mixHue(baseVariant, targetVariant, v), baseVariant.s + (targetVariant.s - baseVariant.s) * v, baseVariant.l + (targetVariant.l - baseVariant.l) * v));
1462
1480
  };
1463
- const autoFlip = ctx.autoFlip ?? getConfig().autoFlip;
1464
1481
  t = findValueForMixContrast({
1465
1482
  preferredValue: t,
1466
1483
  baseLinearRgb: baseLinear,
1467
1484
  targetLinearRgb: targetLinear,
1468
1485
  contrast: minCr,
1469
1486
  luminanceAtValue: luminanceAt,
1470
- flip: autoFlip
1487
+ flip: ctx.config.autoFlip
1471
1488
  }).value;
1472
1489
  }
1473
1490
  if (blend === "transparent") return {
@@ -1528,17 +1545,15 @@ function seedField(order, ctx, field, source) {
1528
1545
  });
1529
1546
  }
1530
1547
  }
1531
- function resolveAllColors(hue, saturation, defs, scaling, externalBases, overrideAutoFlip) {
1548
+ function resolveAllColors(hue, saturation, defs, config, externalBases) {
1532
1549
  validateColorDefs(defs, externalBases);
1533
1550
  const order = topoSort(defs);
1534
- const cfg = getConfig();
1535
1551
  const ctx = {
1536
1552
  hue,
1537
1553
  saturation,
1538
1554
  defs,
1539
1555
  resolved: /* @__PURE__ */ new Map(),
1540
- scaling,
1541
- autoFlip: overrideAutoFlip ?? cfg.autoFlip
1556
+ config
1542
1557
  };
1543
1558
  if (externalBases) for (const [name, color] of externalBases) ctx.resolved.set(name, color);
1544
1559
  const lightMap = runPass(order, defs, ctx, false, false, "light");
@@ -1664,11 +1679,12 @@ function buildCssMap(resolved, prefix, suffix, format) {
1664
1679
  * validator, the two factory paths (value vs structured), and the
1665
1680
  * JSON-safe export / rehydration round-trip.
1666
1681
  *
1667
- * Standalone tokens snapshot the relevant `globalConfig` fields at
1668
- * create time so later `configure()` calls do not retroactively change
1669
- * exported tokens the snapshot is captured eagerly in
1670
- * `defaultStandaloneScaling()`. The token's resolved variants are then
1671
- * memoized on first `.resolve()` / `.token()` / ... call.
1682
+ * Standalone tokens snapshot the full effective config at create time
1683
+ * so later `configure()` calls do not retroactively change exported
1684
+ * tokens. The snapshot is built eagerly in
1685
+ * `buildValueFormConfigOverride()` / `buildStructuredConfigOverride()`.
1686
+ * The token's resolved variants are then memoized on first
1687
+ * `.resolve()` / `.token()` / ... call.
1672
1688
  */
1673
1689
  /** Internal name of the user-facing standalone color in the synthesized def map. */
1674
1690
  const STANDALONE_VALUE = "value";
@@ -1683,39 +1699,47 @@ const RESERVED_STANDALONE_NAMES = new Set([
1683
1699
  STANDALONE_BASE
1684
1700
  ]);
1685
1701
  /**
1686
- * Create-time scaling for all value-shorthand `glaze.color()` inputs.
1687
- * Light lightness is preserved (`lightLightness: false`); dark uses the
1688
- * theme window from `globalConfig.darkLightness`, snapshotted at create
1689
- * time so later `configure()` does not retroactively change tokens.
1702
+ * Build the per-token effective config override for a value-form color.
1703
+ *
1704
+ * Light window defaults to `false` (preserve input lightness exactly).
1705
+ * All other fields snapshot from global at create time. User override
1706
+ * fields win over all defaults.
1690
1707
  */
1691
- function defaultValueShorthandScaling() {
1708
+ function buildValueFormConfigOverride(userOverride) {
1709
+ const cfg = getConfig();
1692
1710
  return {
1693
- lightLightness: false,
1694
- darkLightness: getConfig().darkLightness
1711
+ lightLightness: userOverride?.lightLightness !== void 0 ? userOverride.lightLightness : false,
1712
+ darkLightness: userOverride?.darkLightness !== void 0 ? userOverride.darkLightness : cfg.darkLightness,
1713
+ darkDesaturation: userOverride?.darkDesaturation ?? cfg.darkDesaturation,
1714
+ darkCurve: userOverride?.darkCurve ?? cfg.darkCurve,
1715
+ autoFlip: userOverride?.autoFlip ?? cfg.autoFlip,
1716
+ shadowTuning: userOverride?.shadowTuning ?? cfg.shadowTuning
1695
1717
  };
1696
1718
  }
1697
1719
  /**
1698
- * Create-time scaling for structured `glaze.color({ hue, saturation,
1699
- * lightness, ... })`. Both windows come from `globalConfig` so the
1700
- * token behaves like an ordinary theme color on light and dark sides.
1720
+ * Build the per-token effective config override for a structured-form color.
1721
+ *
1722
+ * Both light and dark windows snapshot from global at create time.
1723
+ * User override fields win.
1701
1724
  */
1702
- function defaultStructuredScaling() {
1725
+ function buildStructuredConfigOverride(userOverride) {
1703
1726
  const cfg = getConfig();
1704
1727
  return {
1705
- lightLightness: cfg.lightLightness,
1706
- darkLightness: cfg.darkLightness
1728
+ lightLightness: userOverride?.lightLightness !== void 0 ? userOverride.lightLightness : cfg.lightLightness,
1729
+ darkLightness: userOverride?.darkLightness !== void 0 ? userOverride.darkLightness : cfg.darkLightness,
1730
+ darkDesaturation: userOverride?.darkDesaturation ?? cfg.darkDesaturation,
1731
+ darkCurve: userOverride?.darkCurve ?? cfg.darkCurve,
1732
+ autoFlip: userOverride?.autoFlip ?? cfg.autoFlip,
1733
+ shadowTuning: userOverride?.shadowTuning ?? cfg.shadowTuning
1707
1734
  };
1708
1735
  }
1709
1736
  /**
1710
- * Discriminate a `GlazeColorToken` from a raw `GlazeColorValue`.
1711
- * Used to widen `base?` so it accepts either a token reference or a
1712
- * raw value (auto-wrapped into `glaze.color(value)`).
1737
+ * Build the `GlazeConfigResolved` to pass to `resolveAllColors` from a
1738
+ * snapshot override. Uses `defaultConfig()` as the base so all required
1739
+ * fields are present; the snapshot fields win.
1713
1740
  */
1714
- function isGlazeColorToken(candidate) {
1715
- return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate) && "resolve" in candidate && typeof candidate.resolve === "function";
1716
- }
1717
- function isStructuredColorInput(input) {
1718
- return typeof input === "object" && input !== null && !Array.isArray(input) && "hue" in input && "lightness" in input;
1741
+ function resolvedConfigFromOverride(override) {
1742
+ return mergeConfig(defaultConfig(), override);
1719
1743
  }
1720
1744
  /**
1721
1745
  * Matches the CSS color functions Glaze itself emits (`rgb()`, `hsl()`,
@@ -1943,9 +1967,7 @@ function extractOkhslFromValue(value) {
1943
1967
  * Build the `ColorMap` for a value-shorthand `glaze.color()` call.
1944
1968
  *
1945
1969
  * The user-facing color (`STANDALONE_VALUE`) defaults to `mode: 'auto'`
1946
- * across every value-shorthand form, using the snapshotted
1947
- * `globalConfig.darkLightness` window (light lightness preserved via
1948
- * `lightLightness: false`).
1970
+ * across every value-shorthand form.
1949
1971
  *
1950
1972
  * When the user requests `contrast` or relative `lightness`, a hidden
1951
1973
  * `STANDALONE_SEED` def is synthesized at `mode: 'static'`. That keeps
@@ -1986,11 +2008,11 @@ function buildStandaloneValueDefs(main, options) {
1986
2008
  primary
1987
2009
  };
1988
2010
  }
1989
- function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effectiveScaling, baseToken, exportData, autoFlip) {
2011
+ function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effectiveConfig, baseToken, exportData) {
1990
2012
  let cached;
1991
2013
  const resolveOnce = () => {
1992
2014
  if (cached) return cached;
1993
- cached = resolveAllColors(seedHue, seedSaturation, defs, effectiveScaling, baseToken ? new Map([[STANDALONE_BASE, baseToken.resolve()]]) : void 0, autoFlip);
2015
+ cached = resolveAllColors(seedHue, seedSaturation, defs, effectiveConfig, baseToken ? new Map([[STANDALONE_BASE, baseToken.resolve()]]) : void 0);
1994
2016
  return cached;
1995
2017
  };
1996
2018
  const resolveStates = (options) => {
@@ -2019,17 +2041,43 @@ function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effect
2019
2041
  };
2020
2042
  }
2021
2043
  /**
2044
+ * When a value/`from` color links to a base that was created via the
2045
+ * structured form (with explicit `hue`/`saturation`/`lightness`), resolve
2046
+ * that base with `lightLightness: false` for the linking math so the
2047
+ * contrast/lightness anchor matches the input lightness — not the
2048
+ * windowed output. The original base token's `.resolve()` is unaffected.
2049
+ */
2050
+ function toLinkingBase(base) {
2051
+ if (!base) return void 0;
2052
+ const exp = base.export();
2053
+ if (exp.form !== "structured") return base;
2054
+ const linkingConfig = {
2055
+ ...exp.config ?? {},
2056
+ lightLightness: false
2057
+ };
2058
+ return colorFromExport({
2059
+ ...exp,
2060
+ config: linkingConfig
2061
+ });
2062
+ }
2063
+ /**
2022
2064
  * Resolve `base` (which may be a token reference or a raw color value)
2023
2065
  * into a `GlazeColorToken`. Raw values are auto-wrapped via
2024
- * `glaze.color(value)` so they pick up the same auto-invert defaults as
2025
- * an explicit wrap. Returns `undefined` when no base is provided.
2066
+ * `createColorTokenFromValue` so they pick up the same auto-invert
2067
+ * defaults as an explicit wrap. Returns `undefined` when no base is provided.
2026
2068
  */
2027
2069
  function resolveBaseToken(base) {
2028
2070
  if (base === void 0) return void 0;
2029
2071
  if (isGlazeColorToken(base)) return base;
2030
2072
  return createColorTokenFromValue(base, void 0, void 0);
2031
2073
  }
2032
- function createColorToken(input, scaling, overrideAutoFlip) {
2074
+ /**
2075
+ * Discriminate a `GlazeColorToken` from a raw `GlazeColorValue`.
2076
+ */
2077
+ function isGlazeColorToken(candidate) {
2078
+ return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate) && "resolve" in candidate && typeof candidate.resolve === "function";
2079
+ }
2080
+ function createColorToken(input, configOverride) {
2033
2081
  validateStructuredInput(input);
2034
2082
  const userName = input.name;
2035
2083
  if (userName !== void 0) validateStandaloneName(userName);
@@ -2050,30 +2098,28 @@ function createColorToken(input, scaling, overrideAutoFlip) {
2050
2098
  saturation: 1,
2051
2099
  mode: "static"
2052
2100
  };
2053
- const effectiveScaling = scaling ?? defaultStructuredScaling();
2054
- const autoFlip = overrideAutoFlip ?? getConfig().autoFlip;
2101
+ const effectiveConfigOverride = buildStructuredConfigOverride(configOverride);
2102
+ const effectiveConfig = resolvedConfigFromOverride(effectiveConfigOverride);
2055
2103
  const exportData = () => ({
2056
2104
  form: "structured",
2057
2105
  input: buildStructuredInputExport(input),
2058
- scaling: effectiveScaling,
2059
- autoFlip
2106
+ config: effectiveConfigOverride
2060
2107
  });
2061
- return createColorTokenFromDefs(input.hue, input.saturation, defs, primary, effectiveScaling, baseToken, exportData, autoFlip);
2108
+ return createColorTokenFromDefs(input.hue, input.saturation, defs, primary, effectiveConfig, baseToken, exportData);
2062
2109
  }
2063
- function createColorTokenFromValue(value, options, scaling, overrideAutoFlip) {
2110
+ function createColorTokenFromValue(value, options, configOverride) {
2064
2111
  const main = extractOkhslFromValue(value);
2065
- const baseToken = resolveBaseToken(options?.base);
2112
+ const linkingBase = toLinkingBase(resolveBaseToken(options?.base));
2066
2113
  const { seedHue, seedSaturation, defs, primary } = buildStandaloneValueDefs(main, options);
2067
- const effectiveScaling = scaling ?? defaultValueShorthandScaling();
2068
- const autoFlip = overrideAutoFlip ?? getConfig().autoFlip;
2114
+ const effectiveConfigOverride = buildValueFormConfigOverride(configOverride);
2115
+ const effectiveConfig = resolvedConfigFromOverride(effectiveConfigOverride);
2069
2116
  const exportData = () => ({
2070
2117
  form: "value",
2071
2118
  input: value,
2072
2119
  ...options !== void 0 ? { overrides: buildOverridesExport(options) } : {},
2073
- scaling: effectiveScaling,
2074
- autoFlip
2120
+ config: effectiveConfigOverride
2075
2121
  });
2076
- return createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effectiveScaling, baseToken, exportData, autoFlip);
2122
+ return createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effectiveConfig, linkingBase, exportData);
2077
2123
  }
2078
2124
  /**
2079
2125
  * Build a JSON-safe snapshot of `GlazeColorOverrides`. `base` is
@@ -2109,8 +2155,6 @@ function buildStructuredInputExport(input) {
2109
2155
  }
2110
2156
  /**
2111
2157
  * Discriminate a `GlazeColorTokenExport` from a raw `GlazeColorValue`.
2112
- * `GlazeColorTokenExport` always has a `form` field set to either
2113
- * `'value'` or `'structured'`; raw values never do.
2114
2158
  */
2115
2159
  function isExportedToken(candidate) {
2116
2160
  return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate) && "form" in candidate && (candidate.form === "value" || candidate.form === "structured");
@@ -2145,6 +2189,10 @@ function rehydrateStructuredInput(data) {
2145
2189
  /**
2146
2190
  * Rehydrate a token from its `.export()` snapshot. Recursively rebuilds
2147
2191
  * any base dependency. Inverse of `GlazeColorToken.export()`.
2192
+ *
2193
+ * The stored `config` field contains the full effective config override
2194
+ * snapshotted at creation time, so the rehydrated token is deterministic
2195
+ * regardless of subsequent `glaze.configure()` calls.
2148
2196
  */
2149
2197
  function colorFromExport(data) {
2150
2198
  if (data === null || typeof data !== "object") throw new Error(`glaze.colorFrom: expected an object from token.export(), got ${data === null ? "null" : typeof data}.`);
@@ -2152,15 +2200,9 @@ function colorFromExport(data) {
2152
2200
  if (data.input === void 0) throw new Error(`glaze.colorFrom: missing "input" field — expected the original ${data.form === "value" ? "GlazeColorValue" : "GlazeColorInput"}.`);
2153
2201
  if (data.form === "value") {
2154
2202
  const value = data.input;
2155
- const overrides = data.overrides ? rehydrateOverrides(data.overrides) : void 0;
2156
- const cfg = getConfig();
2157
- const effectiveAutoFlip = data.autoFlip ?? cfg.autoFlip;
2158
- return createColorTokenFromValue(value, overrides, data.scaling, effectiveAutoFlip);
2203
+ return createColorTokenFromValue(value, data.overrides ? rehydrateOverrides(data.overrides) : void 0, data.config);
2159
2204
  }
2160
- const input = rehydrateStructuredInput(data.input);
2161
- const cfg = getConfig();
2162
- const effectiveAutoFlip = data.autoFlip ?? cfg.autoFlip;
2163
- return createColorToken(input, data.scaling, effectiveAutoFlip);
2205
+ return createColorToken(rehydrateStructuredInput(data.input), data.config);
2164
2206
  }
2165
2207
 
2166
2208
  //#endregion
@@ -2289,21 +2331,32 @@ function createPalette(themes, paletteOptions) {
2289
2331
  /**
2290
2332
  * Theme factory.
2291
2333
  *
2292
- * Wraps a hue/saturation seed and a mutable `ColorMap`, and exposes
2293
- * `tokens()` / `tasty()` / `json()` / `css()` / `resolve()` / `export()`
2294
- * / `extend()`. Caches the last resolve result so successive exports
2295
- * with the same defs and config don't re-run the four-pass resolver.
2334
+ * Wraps a hue/saturation seed, a mutable `ColorMap`, and an optional
2335
+ * per-theme `GlazeConfigOverride`. Exposes `tokens()` / `tasty()` /
2336
+ * `json()` / `css()` / `resolve()` / `export()` / `extend()`.
2337
+ *
2338
+ * The per-theme config override is **merged over the live global config at
2339
+ * resolve time** so the theme still reacts to later `configure()` calls
2340
+ * for fields it didn't override. The merged config is memoized by
2341
+ * `configVersion` to avoid rebuilding it on every export call.
2296
2342
  */
2297
- function createTheme(hue, saturation, initialColors) {
2343
+ function createTheme(hue, saturation, initialColors, configOverride) {
2298
2344
  let colorDefs = initialColors ? { ...initialColors } : {};
2299
2345
  let cache = null;
2346
+ function getEffectiveConfig() {
2347
+ const version = getConfigVersion();
2348
+ if (cache && cache.version === version) return cache.effectiveConfig;
2349
+ return mergeConfig(getConfig(), configOverride);
2350
+ }
2300
2351
  function resolveCached() {
2301
2352
  const version = getConfigVersion();
2302
2353
  if (cache && cache.version === version) return cache.map;
2303
- const map = resolveAllColors(hue, saturation, colorDefs);
2354
+ const effectiveConfig = mergeConfig(getConfig(), configOverride);
2355
+ const map = resolveAllColors(hue, saturation, colorDefs, effectiveConfig);
2304
2356
  cache = {
2305
2357
  map,
2306
- version
2358
+ version,
2359
+ effectiveConfig
2307
2360
  };
2308
2361
  return map;
2309
2362
  }
@@ -2345,11 +2398,13 @@ function createTheme(hue, saturation, initialColors) {
2345
2398
  invalidate();
2346
2399
  },
2347
2400
  export() {
2348
- return {
2401
+ const out = {
2349
2402
  hue,
2350
2403
  saturation,
2351
2404
  colors: { ...colorDefs }
2352
2405
  };
2406
+ if (configOverride !== void 0) out.config = configOverride;
2407
+ return out;
2353
2408
  },
2354
2409
  extend(options) {
2355
2410
  const newHue = options.hue ?? hue;
@@ -2359,7 +2414,10 @@ function createTheme(hue, saturation, initialColors) {
2359
2414
  return createTheme(newHue, newSat, options.colors ? {
2360
2415
  ...inheritedColors,
2361
2416
  ...options.colors
2362
- } : { ...inheritedColors });
2417
+ } : { ...inheritedColors }, configOverride || options.config ? {
2418
+ ...configOverride ?? {},
2419
+ ...options.config ?? {}
2420
+ } : void 0);
2363
2421
  },
2364
2422
  resolve() {
2365
2423
  return new Map(resolveCached());
@@ -2369,7 +2427,7 @@ function createTheme(hue, saturation, initialColors) {
2369
2427
  return buildFlatTokenMap(resolveCached(), "", modes, options?.format);
2370
2428
  },
2371
2429
  tasty(options) {
2372
- const cfg = getConfig();
2430
+ const cfg = getEffectiveConfig();
2373
2431
  const states = {
2374
2432
  dark: options?.states?.dark ?? cfg.states.dark,
2375
2433
  highContrast: options?.states?.highContrast ?? cfg.states.highContrast
@@ -2404,16 +2462,24 @@ function createTheme(hue, saturation, initialColors) {
2404
2462
  /**
2405
2463
  * Create a single-hue glaze theme.
2406
2464
  *
2465
+ * An optional `config` override can be supplied to customize the resolve
2466
+ * behavior for this theme (lightness windows, dark curve, etc.). The
2467
+ * override is **merged over the live global config at resolve time** —
2468
+ * the theme still reacts to later `configure()` calls for fields it
2469
+ * didn't override.
2470
+ *
2407
2471
  * @example
2408
2472
  * ```ts
2409
- * const primary = glaze({ hue: 280, saturation: 80 });
2410
- * // or shorthand:
2411
2473
  * const primary = glaze(280, 80);
2474
+ * // or shorthand:
2475
+ * const primary = glaze({ hue: 280, saturation: 80 });
2476
+ * // with config override:
2477
+ * const raw = glaze(280, 80, { lightLightness: false });
2412
2478
  * ```
2413
2479
  */
2414
- function glaze(hueOrOptions, saturation) {
2415
- if (typeof hueOrOptions === "number") return createTheme(hueOrOptions, saturation ?? 100);
2416
- return createTheme(hueOrOptions.hue, hueOrOptions.saturation);
2480
+ function glaze(hueOrOptions, saturation, config) {
2481
+ if (typeof hueOrOptions === "number") return createTheme(hueOrOptions, saturation ?? 100, void 0, config);
2482
+ return createTheme(hueOrOptions.hue, hueOrOptions.saturation, void 0, config);
2417
2483
  }
2418
2484
  /** Configure global glaze settings. */
2419
2485
  glaze.configure = function configure$1(config) {
@@ -2425,45 +2491,59 @@ glaze.palette = function palette(themes, options) {
2425
2491
  };
2426
2492
  /** Create a theme from a serialized export. */
2427
2493
  glaze.from = function from(data) {
2428
- return createTheme(data.hue, data.saturation, data.colors);
2494
+ return createTheme(data.hue, data.saturation, data.colors, data.config);
2429
2495
  };
2430
2496
  /**
2431
2497
  * Create a standalone single-color token.
2432
2498
  *
2433
- * Two overloads:
2434
- * - `glaze.color(input, scaling?)` — structured form:
2435
- * `{ hue, saturation, lightness, ... }` plus an optional per-call
2436
- * lightness-window override.
2437
- * - `glaze.color(value, overrides?, scaling?)` — value-shorthand: a hex
2438
- * string (3/6/8 digits), one of the CSS color functions Glaze itself
2439
- * emits (`rgb()`, `hsl()`, `okhsl()`, `oklch()`), or literal objects
2440
- * `{ r, g, b }` (0–255), `{ h, s, l }` (OKHSL 0–1), `{ l, c, h }`
2441
- * (OKLCh, matching `oklch()` strings).
2499
+ * **arg1 — the color** (four accepted shapes, discriminated by structure):
2442
2500
  *
2443
- * Defaults: every input form defaults to `mode: 'auto'`. Value-shorthand
2444
- * (strings and literal objects) snapshots `{ lightLightness: false,
2445
- * darkLightness: globalConfig.darkLightness }` light preserves the
2446
- * input; dark uses the theme window. Structured `{ hue, saturation,
2447
- * lightness, ... }` snapshots both `globalConfig` windows like a theme
2448
- * color.
2501
+ * | Shape | Example | Notes |
2502
+ * |---|---|---|
2503
+ * | Bare string | `'#26fcb2'`, `'rgb(38 252 178)'` | Hex or CSS color function |
2504
+ * | Value object | `{ h: 152, s: 0.95, l: 0.74 }` | OKHSL, `{r,g,b}`, `{l,c,h}` |
2505
+ * | `{ from, ...overrides }` | `{ from: '#fff', base: bg, contrast: 'AA' }` | Value + color overrides |
2506
+ * | Structured | `{ hue: 152, saturation: 95, lightness: 74 }` | Full theme-style token |
2449
2507
  *
2450
- * Pass `{ mode: 'fixed' }` to opt back into the legacy linear, non-
2451
- * inverting mapping, or `{ mode: 'static' }` to pin the same lightness
2452
- * across every variant.
2508
+ * **arg2 config override** (optional, all shapes):
2509
+ * Overrides the resolve-relevant global config fields for this token.
2510
+ * Fields that are omitted fall through to the live global config at
2511
+ * create time (and are snapshotted). Pass `false` for a lightness window
2512
+ * to disable clamping entirely.
2453
2513
  *
2454
- * Relative `lightness: '+N'` and `contrast: <ratio>` are anchored to
2455
- * the literal seed (the value passed in) by default, pinned at
2456
- * `mode: 'static'` across all four variants. Pass `overrides.base` (a
2457
- * `GlazeColorToken`) to anchor `contrast` and relative `lightness`
2458
- * against another color's resolved variant per scheme instead. Relative
2459
- * `hue: '+N'` always anchors to the seed.
2514
+ * ```ts
2515
+ * // Bare string no overrides
2516
+ * glaze.color('#26fcb2')
2460
2517
  *
2461
- * Alpha components in `rgba()` / `hsla()` / slash-alpha syntax and
2462
- * 8-digit hex are parsed but dropped with a `console.warn`.
2463
- */
2464
- glaze.color = function color(input, arg2, arg3) {
2465
- if (isStructuredColorInput(input)) return createColorToken(input, arg2);
2466
- return createColorTokenFromValue(input, arg2, arg3);
2518
+ * // From form value + color overrides
2519
+ * glaze.color({ from: '#fff', base: bg, contrast: 'AA' })
2520
+ *
2521
+ * // Structured form full theme-style token
2522
+ * glaze.color({ hue: 152, saturation: 95, lightness: 74 })
2523
+ *
2524
+ * // Config override on any form
2525
+ * glaze.color('#26fcb2', { darkLightness: false, autoFlip: false })
2526
+ * glaze.color({ from: '#fff', base: bg }, { darkCurve: 0.3 })
2527
+ * ```
2528
+ *
2529
+ * Defaults: every form defaults to `mode: 'auto'`. Value-shorthand forms
2530
+ * (bare strings and value objects) preserve light lightness exactly
2531
+ * (`lightLightness: false` internally). Structured form snapshots both
2532
+ * lightness windows from `globalConfig` at create time.
2533
+ *
2534
+ * Relative `lightness: '+N'` and `contrast` anchor to the literal seed by
2535
+ * default; when `base` is set they anchor to the base's resolved variant
2536
+ * per scheme. Relative `hue: '+N'` always anchors to the seed, not the base.
2537
+ */
2538
+ glaze.color = function color(input, config) {
2539
+ if (typeof input === "string") return createColorTokenFromValue(input, void 0, config);
2540
+ const obj = input;
2541
+ if ("from" in obj) {
2542
+ const { from, ...overrides } = input;
2543
+ return createColorTokenFromValue(from, overrides, config);
2544
+ }
2545
+ if ("hue" in obj) return createColorToken(input, config);
2546
+ return createColorTokenFromValue(input, void 0, config);
2467
2547
  };
2468
2548
  /**
2469
2549
  * Compute a shadow color from a bg/fg pair and intensity.
@@ -2475,7 +2555,8 @@ glaze.color = function color(input, arg2, arg3) {
2475
2555
  glaze.shadow = function shadow(input) {
2476
2556
  const bg = extractOkhslFromValue(input.bg);
2477
2557
  const fg = input.fg ? extractOkhslFromValue(input.fg) : void 0;
2478
- const tuning = resolveShadowTuning(input.tuning);
2558
+ const cfg = getConfig();
2559
+ const tuning = resolveShadowTuning(input.tuning, cfg.shadowTuning);
2479
2560
  return computeShadow({
2480
2561
  ...bg,
2481
2562
  alpha: 1
@@ -2515,12 +2596,12 @@ glaze.fromRgb = function fromRgb(r, g, b) {
2515
2596
  *
2516
2597
  * The snapshot is a plain JSON-safe object containing the original
2517
2598
  * input value, overrides (with any `base` token recursively serialized),
2518
- * and the captured scaling. The reconstructed token is identical in
2519
- * behavior to the original at the time of export.
2599
+ * and the effective config snapshot. The reconstructed token is identical
2600
+ * in behavior to the original at the time of export.
2520
2601
  *
2521
2602
  * @example
2522
2603
  * ```ts
2523
- * const text = glaze.color('#1a1a1a', { contrast: 'AA' });
2604
+ * const text = glaze.color({ from: '#1a1a1a', contrast: 'AA' });
2524
2605
  * const data = text.export(); // JSON-safe
2525
2606
  * localStorage.setItem('text', JSON.stringify(data));
2526
2607
  * // ...later...