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