@tenphi/glaze 1.1.0 → 1.2.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
@@ -1977,34 +1977,77 @@ function resolveContrastSpec(spec, isHighContrast, polarity) {
1977
1977
  /**
1978
1978
  * Apply the relative-tone delta against a base, honoring `flip`.
1979
1979
  *
1980
- * When `flip` is on and `base + delta` falls outside `[0, 100]`, mirror the
1981
- * delta to the other side of the base (so an offset that would clamp instead
1982
- * reflects back into range). When off, the caller clamps as usual.
1980
+ * When `flip` is on and `base + delta` falls outside `[0, 100]`, try mirroring
1981
+ * the delta to the other side of the base. If the mirrored target is also out
1982
+ * of range, keep the original delta so the caller clamps on the authored side.
1983
+ * When off, the caller clamps as usual.
1983
1984
  */
1984
1985
  function applyToneFlip(delta, baseTone, flip) {
1985
1986
  if (!flip) return delta;
1986
1987
  const target = baseTone + delta;
1987
1988
  if (target >= 0 && target <= 100) return delta;
1988
- return -delta;
1989
+ const mirrored = baseTone - delta;
1990
+ if (mirrored >= 0 && mirrored <= 100) return -delta;
1991
+ return delta;
1992
+ }
1993
+ /**
1994
+ * Dark position for an extreme tone (`'max'` / `'min'`) anchored to a base.
1995
+ *
1996
+ * A windowed dark remap compresses the base→extreme span, so the pair loses
1997
+ * contrast in dark. Instead, measure the tone shift the light scheme applied
1998
+ * between the base and the extreme, then replay it against the base's dark
1999
+ * tone: `auto` mirrors the sign (both ends invert), `fixed` keeps it. The
2000
+ * result is clamped to `[0, 100]` only — crossing the dark tone window is
2001
+ * intended, since the author asked for the extreme.
2002
+ */
2003
+ function extremeDarkTone(authorTone, mode, isHighContrast, baseResolved, baseDarkTone, config) {
2004
+ const delta = mapToneForScheme(authorTone, mode, false, isHighContrast, config) - getSchemeVariant(baseResolved, false, isHighContrast).t * 100;
2005
+ return clamp(baseDarkTone + (mode === "auto" ? -delta : delta), 0, 100);
1989
2006
  }
1990
2007
  function resolveRootColor(def, isHighContrast) {
1991
2008
  const rawT = def.tone;
2009
+ return clamp(parseToneValue(isHighContrast ? pairHC(rawT) : pairNormal(rawT)).value, 0, 100);
2010
+ }
2011
+ /**
2012
+ * Effective hue and saturation for one color in one scheme.
2013
+ *
2014
+ * Dark schemes read the dark seed pair and the color's `darkHue` /
2015
+ * `darkSaturation`, each falling back to its light counterpart. An explicit
2016
+ * dark saturation — on the seed or the def — is taken literally; the global
2017
+ * `darkDesaturation` reduction applies only when neither is authored.
2018
+ * `mode: 'static'` pins one value across every scheme, so it skips all of it.
2019
+ *
2020
+ * Shared by `resolveColorForScheme` (the emitted variant) and
2021
+ * `resolveDependentColor` (the contrast solver's inputs) so the solver always
2022
+ * measures the color it is actually going to produce.
2023
+ */
2024
+ function resolveChannels(def, ctx, isDark) {
2025
+ const mode = def.mode ?? "auto";
2026
+ const satFactor = clamp(def.saturation ?? 1, 0, 1);
2027
+ if (!isDark || mode === "static") return {
2028
+ hue: resolveEffectiveHue(ctx.hue, def.hue),
2029
+ saturation: clamp(satFactor * ctx.saturation / 100, 0, 1)
2030
+ };
2031
+ const darkSeedSaturation = ctx.darkSaturation ?? ctx.saturation;
2032
+ const darkFactor = clamp(def.darkSaturation ?? satFactor, 0, 1);
2033
+ const explicitDark = def.darkSaturation !== void 0 || ctx.darkSaturation !== void 0;
2034
+ const raw = darkFactor * darkSeedSaturation / 100;
1992
2035
  return {
1993
- authorTone: clamp(parseToneValue(isHighContrast ? pairHC(rawT) : pairNormal(rawT)).value, 0, 100),
1994
- satFactor: clamp(def.saturation ?? 1, 0, 1)
2036
+ hue: resolveEffectiveHue(ctx.darkHue ?? ctx.hue, def.darkHue ?? def.hue),
2037
+ saturation: clamp(explicitDark ? raw : mapSaturationDark(raw, mode, ctx.config), 0, 1)
1995
2038
  };
1996
2039
  }
1997
- function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effectiveHue, polarity, effectivePastel) {
2040
+ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, channels, polarity, effectivePastel) {
1998
2041
  const baseName = def.base;
1999
2042
  const baseResolved = ctx.resolved.get(baseName);
2000
2043
  if (!baseResolved) throw new Error(`glaze: base "${baseName}" not yet resolved for "${name}".`);
2001
2044
  const mode = def.mode ?? "auto";
2002
- const satFactor = clamp(def.saturation ?? 1, 0, 1);
2003
2045
  const flip = def.autoFlip ?? ctx.config.autoFlip;
2004
2046
  const pastel = effectivePastel;
2005
2047
  const baseVariant = getSchemeVariant(baseResolved, isDark, isHighContrast);
2006
2048
  const baseTone = baseVariant.t * 100;
2007
2049
  let preferredTone;
2050
+ let isExtreme = false;
2008
2051
  const rawTone = def.tone;
2009
2052
  if (rawTone === void 0) preferredTone = baseTone;
2010
2053
  else {
@@ -2013,22 +2056,28 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
2013
2056
  const baseLightTone = getSchemeVariant(baseResolved, false, isHighContrast).t * 100;
2014
2057
  preferredTone = mapToneForScheme(clamp(baseLightTone + applyToneFlip(parsed.value, baseLightTone, flip), 0, 100), "auto", true, isHighContrast, ctx.config);
2015
2058
  } else preferredTone = clamp(baseTone + applyToneFlip(parsed.value, baseTone, flip), 0, 100);
2016
- else preferredTone = mapToneForScheme(parsed.value, mode, isDark, isHighContrast, ctx.config);
2059
+ else if (parsed.kind === "extreme" && isDark && mode !== "static") {
2060
+ isExtreme = true;
2061
+ preferredTone = extremeDarkTone(parsed.value, mode, isHighContrast, baseResolved, baseTone, ctx.config);
2062
+ } else {
2063
+ isExtreme = parsed.kind === "extreme";
2064
+ preferredTone = mapToneForScheme(parsed.value, mode, isDark, isHighContrast, ctx.config);
2065
+ }
2017
2066
  }
2018
2067
  const rawContrast = def.contrast;
2019
2068
  if (rawContrast !== void 0) {
2020
2069
  const resolvedContrast = resolveContrastSpec(rawContrast, isHighContrast, polarity);
2021
- const effectiveSat = isDark ? mapSaturationDark(satFactor * ctx.saturation / 100, mode, ctx.config) : satFactor * ctx.saturation / 100;
2022
2070
  const baseOkhsl = toOkhslVariant(baseVariant);
2023
2071
  const baseLinearRgb = okhslToLinearSrgb(baseOkhsl.h, baseOkhsl.s, baseOkhsl.l, baseVariant.pastel ?? ctx.config.pastel);
2024
2072
  const toneRange = schemeToneRange(isDark, mode, isHighContrast, ctx.config);
2025
2073
  let initialDirection;
2026
2074
  if (preferredTone < baseTone) initialDirection = "darker";
2027
2075
  else if (preferredTone > baseTone) initialDirection = "lighter";
2076
+ const seedTone = isExtreme ? clamp(preferredTone / 100, 0, 1) : clamp(preferredTone / 100, toneRange[0], toneRange[1]);
2028
2077
  const result = findToneForContrast({
2029
- hue: effectiveHue,
2030
- saturation: effectiveSat,
2031
- preferredTone: clamp(preferredTone / 100, toneRange[0], toneRange[1]),
2078
+ hue: channels.hue,
2079
+ saturation: channels.saturation,
2080
+ preferredTone: seedTone,
2032
2081
  baseLinearRgb,
2033
2082
  contrast: resolvedContrast,
2034
2083
  toneRange: [0, 1],
@@ -2037,15 +2086,9 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
2037
2086
  pastel
2038
2087
  });
2039
2088
  if (!result.met) warnContrastUnmet(name, isDark, isHighContrast, resolvedContrast, result.contrast);
2040
- return {
2041
- tone: result.tone * 100,
2042
- satFactor
2043
- };
2089
+ return result.tone * 100;
2044
2090
  }
2045
- return {
2046
- tone: clamp(preferredTone, 0, 100),
2047
- satFactor
2048
- };
2091
+ return clamp(preferredTone, 0, 100);
2049
2092
  }
2050
2093
  function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
2051
2094
  if (isShadowDef(def)) return resolveShadowForScheme(def, ctx, isDark, isHighContrast);
@@ -2053,27 +2096,14 @@ function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
2053
2096
  const regDef = def;
2054
2097
  const mode = regDef.mode ?? "auto";
2055
2098
  const isRoot = isAbsoluteTone(regDef.tone) && !regDef.base;
2056
- const effectiveHue = resolveEffectiveHue(ctx.hue, regDef.hue);
2099
+ const channels = resolveChannels(regDef, ctx, isDark);
2057
2100
  const polarity = roleToPolarity(resolveRole(name, def, ctx));
2058
2101
  const pastel = regDef.pastel ?? ctx.config.pastel;
2059
- let finalTone;
2060
- let satFactor;
2061
- if (isRoot) {
2062
- const root = resolveRootColor(regDef, isHighContrast);
2063
- finalTone = mapToneForScheme(root.authorTone, mode, isDark, isHighContrast, ctx.config);
2064
- satFactor = root.satFactor;
2065
- } else {
2066
- const dep = resolveDependentColor(name, regDef, ctx, isHighContrast, isDark, effectiveHue, polarity, pastel);
2067
- finalTone = dep.tone;
2068
- satFactor = dep.satFactor;
2069
- }
2070
- const baseSat = satFactor * ctx.saturation / 100;
2071
- const finalSat = isDark ? mapSaturationDark(baseSat, mode, ctx.config) : baseSat;
2072
- const toneFraction = clamp(finalTone / 100, 0, 1);
2102
+ const finalTone = isRoot ? mapToneForScheme(resolveRootColor(regDef, isHighContrast), mode, isDark, isHighContrast, ctx.config) : resolveDependentColor(name, regDef, ctx, isHighContrast, isDark, channels, polarity, pastel);
2073
2103
  return {
2074
- h: effectiveHue,
2075
- s: clamp(finalSat, 0, 1),
2076
- t: toneFraction,
2104
+ h: channels.hue,
2105
+ s: channels.saturation,
2106
+ t: clamp(finalTone / 100, 0, 1),
2077
2107
  alpha: regDef.opacity ?? 1,
2078
2108
  pastel
2079
2109
  };
@@ -2273,12 +2303,11 @@ function verifyContrastDrift(order, defs, result, config) {
2273
2303
  }
2274
2304
  }
2275
2305
  }
2276
- function resolveAllColors(hue, saturation, defs, config, externalBases) {
2306
+ function resolveAllColors(seed, defs, config, externalBases) {
2277
2307
  validateColorDefs(defs, externalBases);
2278
2308
  const order = topoSort(defs);
2279
2309
  const ctx = {
2280
- hue,
2281
- saturation,
2310
+ ...seed,
2282
2311
  defs,
2283
2312
  resolved: /* @__PURE__ */ new Map(),
2284
2313
  config,
@@ -2311,8 +2340,12 @@ function resolveAllColors(hue, saturation, defs, config, externalBases) {
2311
2340
  /**
2312
2341
  * Hue channel planning for `splitHue` exports.
2313
2342
  *
2314
- * Builds per-color hue var references and scheme-independent `--*-hue`
2315
- * declarations for oklch CSS / Tasty output when every color is pastel.
2343
+ * Builds per-color hue var references and the `--*-hue` declarations backing
2344
+ * them for oklch CSS / Tasty output when every color is pastel.
2345
+ *
2346
+ * Hue is scheme-independent unless the theme or a color authors a dark hue,
2347
+ * so declarations are collected per scheme (`'light'` / `'dark'`) and the dark
2348
+ * set is emitted only when it actually differs — see `darkHueDeclarations`.
2316
2349
  */
2317
2350
  const ACHROMATIC_EPSILON = 1e-6;
2318
2351
  function cssProp(prefix, name, suffix) {
@@ -2321,7 +2354,11 @@ function cssProp(prefix, name, suffix) {
2321
2354
  function isAchromatic(v) {
2322
2355
  return v.s <= ACHROMATIC_EPSILON;
2323
2356
  }
2324
- function themeHuePlan(name, def, variant, ctx) {
2357
+ /** Seed hue backing `--{baseName}-hue` in a given scheme. */
2358
+ function schemeSeedHue(ctx, scheme) {
2359
+ return scheme === "dark" ? ctx.darkSeedHue ?? ctx.seedHue : ctx.seedHue;
2360
+ }
2361
+ function themeHuePlan(name, def, variant, ctx, scheme) {
2325
2362
  if (def === void 0 || isShadowDef(def) || isMixDef(def) || isAchromatic(variant)) return {
2326
2363
  hueVar: "",
2327
2364
  inline: true,
@@ -2329,13 +2366,25 @@ function themeHuePlan(name, def, variant, ctx) {
2329
2366
  };
2330
2367
  const regDef = def;
2331
2368
  const baseHueVar = `var(--${ctx.baseName}-hue)`;
2332
- if (regDef.hue === void 0) return {
2369
+ const prop = cssProp(ctx.prefix, name, "-hue");
2370
+ if ((regDef.mode ?? "auto") === "static" && schemeSeedHue(ctx, "dark") !== ctx.seedHue) {
2371
+ const pinned = resolveEffectiveHue(ctx.seedHue, regDef.hue);
2372
+ return {
2373
+ hueVar: `var(${prop})`,
2374
+ inline: false,
2375
+ declarations: [{
2376
+ prop,
2377
+ value: String(pinned)
2378
+ }]
2379
+ };
2380
+ }
2381
+ const authored = scheme === "dark" ? regDef.darkHue ?? regDef.hue : regDef.hue;
2382
+ if (authored === void 0) return {
2333
2383
  hueVar: baseHueVar,
2334
2384
  inline: false,
2335
2385
  declarations: []
2336
2386
  };
2337
- const parsed = parseRelativeOrAbsolute(regDef.hue);
2338
- const prop = cssProp(ctx.prefix, name, "-hue");
2387
+ const parsed = parseRelativeOrAbsolute(authored);
2339
2388
  if (parsed.relative) {
2340
2389
  const sign = parsed.value >= 0 ? "+" : "-";
2341
2390
  const magnitude = Math.abs(parsed.value);
@@ -2359,13 +2408,13 @@ function themeHuePlan(name, def, variant, ctx) {
2359
2408
  }]
2360
2409
  };
2361
2410
  }
2362
- function standaloneHuePlan(name, variant, ctx) {
2411
+ function standaloneHuePlan(name, variant, ctx, scheme) {
2363
2412
  if (isAchromatic(variant)) return {
2364
2413
  hueVar: "",
2365
2414
  inline: true,
2366
2415
  declarations: []
2367
2416
  };
2368
- const hue = ctx.resolvedHue ?? variant.h;
2417
+ const hue = scheme === "dark" ? variant.h : ctx.resolvedHue ?? variant.h;
2369
2418
  const prop = cssProp(ctx.prefix, name, "-hue");
2370
2419
  return {
2371
2420
  hueVar: `var(${prop})`,
@@ -2376,12 +2425,12 @@ function standaloneHuePlan(name, variant, ctx) {
2376
2425
  }]
2377
2426
  };
2378
2427
  }
2379
- function buildHuePlan(name, def, variant, ctx) {
2380
- if (ctx.mode === "standalone") return standaloneHuePlan(name, variant, ctx);
2381
- return themeHuePlan(name, def, variant, ctx);
2428
+ function buildHuePlan(name, def, variant, ctx, scheme = "light") {
2429
+ if (ctx.mode === "standalone") return standaloneHuePlan(name, variant, ctx, scheme);
2430
+ return themeHuePlan(name, def, variant, ctx, scheme);
2382
2431
  }
2383
2432
  /** Collect unique hue declarations across all colors (theme + per-color). */
2384
- function collectHueDeclarations(resolved, ctx) {
2433
+ function collectHueDeclarations(resolved, ctx, scheme = "light") {
2385
2434
  if (ctx.emitDeclarations === false) return [];
2386
2435
  const seen = /* @__PURE__ */ new Set();
2387
2436
  const out = [];
@@ -2392,15 +2441,32 @@ function collectHueDeclarations(resolved, ctx) {
2392
2441
  };
2393
2442
  if (ctx.mode === "theme") push({
2394
2443
  prop: `--${ctx.baseName}-hue`,
2395
- value: String(ctx.seedHue)
2444
+ value: String(schemeSeedHue(ctx, scheme))
2396
2445
  });
2397
2446
  for (const [name, color] of resolved) {
2398
2447
  const def = ctx.defs[name];
2399
- const plan = buildHuePlan(name, def, color.light, ctx);
2448
+ const plan = buildHuePlan(name, def, scheme === "dark" ? color.dark : color.light, ctx, scheme);
2400
2449
  for (const decl of plan.declarations) push(decl);
2401
2450
  }
2402
2451
  return out;
2403
2452
  }
2453
+ /**
2454
+ * Dark-scheme hue declarations, or `[]` when dark resolves to the same hues as
2455
+ * light (the common case — no `darkHue` anywhere).
2456
+ *
2457
+ * When they differ, the **whole** set is returned rather than only the entries
2458
+ * whose text changed. A relative declaration reads
2459
+ * `calc(var(--theme-hue) + N)` in both schemes even though its resolved value
2460
+ * moves with the seed, and custom properties substitute `var()` at
2461
+ * computed-value time on the declaring element — so re-declaring
2462
+ * `--{baseName}-hue` alone would leave inherited per-color vars still holding
2463
+ * the light-substituted value.
2464
+ */
2465
+ function darkHueDeclarations(resolved, ctx) {
2466
+ const dark = collectHueDeclarations(resolved, ctx, "dark");
2467
+ const light = collectHueDeclarations(resolved, ctx, "light");
2468
+ return dark.length === light.length && dark.every((decl, i) => decl.prop === light[i].prop && decl.value === light[i].value) ? [] : dark;
2469
+ }
2404
2470
  function buildHuePlans(resolved, ctx) {
2405
2471
  const plans = /* @__PURE__ */ new Map();
2406
2472
  for (const [name, color] of resolved) plans.set(name, buildHuePlan(name, ctx.defs[name], color.light, ctx));
@@ -2474,17 +2540,15 @@ function buildTokenMap(resolved, prefix, states, modes, format = "oklch", pastel
2474
2540
  const tokens = {};
2475
2541
  const huePlans = channelCtx !== void 0 && format === "oklch" ? buildHuePlans(resolved, channelCtx) : void 0;
2476
2542
  if (huePlans !== void 0 && channelCtx !== void 0) {
2477
- const emitDecls = channelCtx.emitDeclarations !== false;
2478
- if (emitDecls && channelCtx.mode === "theme") tokens[`$${channelCtx.baseName}-hue`] = { "": String(channelCtx.seedHue) };
2479
- for (const [name, color] of resolved) {
2480
- const plan = huePlans.get(name);
2481
- if (emitDecls) for (const decl of plan.declarations) {
2482
- const key = `$${decl.prop.slice(2)}`;
2483
- if (!(key in tokens)) tokens[key] = { "": decl.value };
2484
- }
2485
- const colorKey = `#${prefix}${name}`;
2486
- tokens[colorKey] = buildTokenEntry(color, states, modes, format, pastel, huePlans.get(name));
2543
+ for (const decl of collectHueDeclarations(resolved, channelCtx)) tokens[`$${decl.prop.slice(2)}`] = { "": decl.value };
2544
+ if (modes.dark) for (const decl of darkHueDeclarations(resolved, channelCtx)) {
2545
+ const key = `$${decl.prop.slice(2)}`;
2546
+ tokens[key] = {
2547
+ ...tokens[key],
2548
+ [states.dark]: decl.value
2549
+ };
2487
2550
  }
2551
+ for (const [name, color] of resolved) tokens[`#${prefix}${name}`] = buildTokenEntry(color, states, modes, format, pastel, huePlans.get(name));
2488
2552
  return tokens;
2489
2553
  }
2490
2554
  for (const [name, color] of resolved) {
@@ -2533,7 +2597,13 @@ function buildCssMap(resolved, prefix, suffix, format, pastel = false, channelCt
2533
2597
  darkContrast: []
2534
2598
  };
2535
2599
  const huePlans = channelCtx !== void 0 && format === "oklch" ? buildHuePlans(resolved, channelCtx) : void 0;
2536
- if (huePlans !== void 0 && channelCtx !== void 0) for (const decl of collectHueDeclarations(resolved, channelCtx)) lines.light.push(`${decl.prop}: ${decl.value};`);
2600
+ if (huePlans !== void 0 && channelCtx !== void 0) {
2601
+ for (const decl of collectHueDeclarations(resolved, channelCtx)) lines.light.push(`${decl.prop}: ${decl.value};`);
2602
+ for (const decl of darkHueDeclarations(resolved, channelCtx)) {
2603
+ lines.dark.push(`${decl.prop}: ${decl.value};`);
2604
+ lines.darkContrast.push(`${decl.prop}: ${decl.value};`);
2605
+ }
2606
+ }
2537
2607
  for (const [name, color] of resolved) {
2538
2608
  const prop = `--${prefix}${name}${suffix}`;
2539
2609
  const plan = huePlans?.get(name);
@@ -2970,6 +3040,13 @@ function validateStructuredInput(input) {
2970
3040
  if (input.saturationFactor !== void 0) {
2971
3041
  if (!Number.isFinite(input.saturationFactor) || input.saturationFactor < 0 || input.saturationFactor > 1) throw new Error(`glaze.color: structured saturationFactor must be a finite number in 0–1 (got ${input.saturationFactor}).`);
2972
3042
  }
3043
+ if (typeof input.darkHue === "number" && !Number.isFinite(input.darkHue)) throw new Error(`glaze.color: structured darkHue must be a finite number (got ${input.darkHue}).`);
3044
+ if (input.darkSaturation !== void 0) {
3045
+ if (!Number.isFinite(input.darkSaturation) || input.darkSaturation < 0 || input.darkSaturation > 100) throw new Error(`glaze.color: structured darkSaturation must be a finite number in 0–100 (got ${input.darkSaturation}).`);
3046
+ }
3047
+ if (input.darkSaturationFactor !== void 0) {
3048
+ if (!Number.isFinite(input.darkSaturationFactor) || input.darkSaturationFactor < 0 || input.darkSaturationFactor > 1) throw new Error(`glaze.color: structured darkSaturationFactor must be a finite number in 0–1 (got ${input.darkSaturationFactor}).`);
3049
+ }
2973
3050
  if (input.opacity !== void 0) validateStandaloneOpacity(input.opacity);
2974
3051
  }
2975
3052
  /**
@@ -3016,11 +3093,27 @@ function extractOkhslFromValue(value) {
3016
3093
  return value;
3017
3094
  }
3018
3095
  /**
3096
+ * Resolve `'max'` / `'min'` to their literal author tones (100 / 0).
3097
+ *
3098
+ * The hidden `STANDALONE_SEED` anchor exists only so `contrast` has something
3099
+ * to measure against — it is not an authored base. Pinning the extreme keeps
3100
+ * the token on the plain scheme mapping instead of the base-anchored dark
3101
+ * replay the resolver applies to real `base` references.
3102
+ */
3103
+ function pinExtremeTone(tone) {
3104
+ const pin = (value) => value === "max" ? 100 : value === "min" ? 0 : value;
3105
+ return Array.isArray(tone) ? [pin(tone[0]), pin(tone[1])] : pin(tone);
3106
+ }
3107
+ /**
3019
3108
  * Build the `ColorMap` for a value-shorthand `glaze.color()` call.
3020
3109
  *
3021
3110
  * The user-facing color (`STANDALONE_VALUE`) defaults to `mode: 'auto'`
3022
3111
  * across every value-shorthand form.
3023
3112
  *
3113
+ * An absolute `hue` override becomes the seed while a relative one becomes a
3114
+ * def field; `darkHue` always lands on the def in either form, so an absolute
3115
+ * `darkHue` wins outright instead of being re-offset by a relative `hue`.
3116
+ *
3024
3117
  * When the user requests `contrast` or relative `tone`, a hidden
3025
3118
  * `STANDALONE_SEED` def is synthesized at `mode: 'static'`. That keeps
3026
3119
  * the seed pinned to the literal user-provided color across all four
@@ -3038,10 +3131,13 @@ function buildStandaloneValueDefs(main, options) {
3038
3131
  if (userName !== void 0) validateStandaloneName(userName);
3039
3132
  const primary = userName ?? STANDALONE_VALUE;
3040
3133
  const seedTone = toTone(main.l);
3134
+ const primaryTone = toneOption ?? seedTone;
3041
3135
  const valueDef = {
3042
3136
  hue: relativeHue,
3043
3137
  saturation: options?.saturationFactor,
3044
- tone: toneOption ?? seedTone,
3138
+ darkHue: options?.darkHue,
3139
+ darkSaturation: options?.darkSaturationFactor,
3140
+ tone: needsSeedAnchor ? pinExtremeTone(primaryTone) : primaryTone,
3045
3141
  contrast: options?.contrast,
3046
3142
  mode: options?.mode ?? "auto",
3047
3143
  autoFlip: options?.autoFlip,
@@ -3058,13 +3154,16 @@ function buildStandaloneValueDefs(main, options) {
3058
3154
  mode: "static"
3059
3155
  };
3060
3156
  return {
3061
- seedHue,
3062
- seedSaturation,
3157
+ seed: {
3158
+ hue: seedHue,
3159
+ saturation: seedSaturation,
3160
+ darkSaturation: options?.darkSaturation
3161
+ },
3063
3162
  defs,
3064
3163
  primary
3065
3164
  };
3066
3165
  }
3067
- function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, configOverride, baseToken, buildExport) {
3166
+ function createColorTokenFromDefs(seed, defs, primary, configOverride, baseToken, buildExport) {
3068
3167
  let cache = null;
3069
3168
  function getEffectiveConfig() {
3070
3169
  const version = getConfigVersion();
@@ -3081,7 +3180,7 @@ function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, config
3081
3180
  const version = getConfigVersion();
3082
3181
  if (cache && cache.version === version && cache.map) return cache.map;
3083
3182
  const effectiveConfig = getEffectiveConfig();
3084
- const map = resolveAllColors(seedHue, seedSaturation, defs, effectiveConfig, baseToken ? new Map([[STANDALONE_BASE, baseToken.resolve()]]) : void 0);
3183
+ const map = resolveAllColors(seed, defs, effectiveConfig, baseToken ? new Map([[STANDALONE_BASE, baseToken.resolve()]]) : void 0);
3085
3184
  cache = {
3086
3185
  map,
3087
3186
  version,
@@ -3119,7 +3218,7 @@ function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, config
3119
3218
  if (options.splitHue && format === "oklch") {
3120
3219
  assertAllPastel(renamed, resolveModes());
3121
3220
  channelCtx = {
3122
- seedHue,
3221
+ seedHue: seed.hue,
3123
3222
  baseName: options.name,
3124
3223
  prefix: "",
3125
3224
  defs: { [options.name]: defs[primary] },
@@ -3203,8 +3302,10 @@ function createColorToken(input, configOverride) {
3203
3302
  const hasExternalBase = baseToken !== void 0;
3204
3303
  const needsSeedAnchor = !hasExternalBase && input.contrast !== void 0;
3205
3304
  const defs = { [primary]: {
3206
- tone: input.tone,
3305
+ tone: needsSeedAnchor ? pinExtremeTone(input.tone) : input.tone,
3207
3306
  saturation: input.saturationFactor,
3307
+ darkHue: input.darkHue,
3308
+ darkSaturation: input.darkSaturationFactor,
3208
3309
  mode: input.mode ?? "auto",
3209
3310
  autoFlip: input.autoFlip,
3210
3311
  contrast: input.contrast,
@@ -3213,16 +3314,17 @@ function createColorToken(input, configOverride) {
3213
3314
  role: input.role,
3214
3315
  base: hasExternalBase ? STANDALONE_BASE : needsSeedAnchor ? STANDALONE_SEED : void 0
3215
3316
  } };
3216
- if (needsSeedAnchor) {
3217
- const seedTone = pairNormal(input.tone);
3218
- defs[STANDALONE_SEED] = {
3219
- tone: seedTone === "max" ? 100 : seedTone === "min" ? 0 : seedTone,
3220
- saturation: 1,
3221
- mode: "static"
3222
- };
3223
- }
3317
+ if (needsSeedAnchor) defs[STANDALONE_SEED] = {
3318
+ tone: pinExtremeTone(pairNormal(input.tone)),
3319
+ saturation: 1,
3320
+ mode: "static"
3321
+ };
3224
3322
  const localOverride = configOverride;
3225
- return createColorTokenFromDefs(input.hue, input.saturation, defs, primary, localOverride, baseToken, (exportArg) => ({
3323
+ return createColorTokenFromDefs({
3324
+ hue: input.hue,
3325
+ saturation: input.saturation,
3326
+ darkSaturation: input.darkSaturation
3327
+ }, defs, primary, localOverride, baseToken, (exportArg) => ({
3226
3328
  kind: "color",
3227
3329
  version: GLAZE_EXPORT_VERSION,
3228
3330
  form: "structured",
@@ -3233,9 +3335,9 @@ function createColorToken(input, configOverride) {
3233
3335
  function createColorTokenFromValue(value, options, configOverride) {
3234
3336
  const main = extractOkhslFromValue(value);
3235
3337
  const linkingBase = toLinkingBase(resolveBaseToken(options?.base));
3236
- const { seedHue, seedSaturation, defs, primary } = buildStandaloneValueDefs(main, options);
3338
+ const { seed, defs, primary } = buildStandaloneValueDefs(main, options);
3237
3339
  const localOverride = sparseValueFormLocal(configOverride);
3238
- return createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, localOverride, linkingBase, (exportArg) => ({
3340
+ return createColorTokenFromDefs(seed, defs, primary, localOverride, linkingBase, (exportArg) => ({
3239
3341
  kind: "color",
3240
3342
  version: GLAZE_EXPORT_VERSION,
3241
3343
  form: "value",
@@ -3255,6 +3357,9 @@ function buildOverridesExport(options, exportArg) {
3255
3357
  if (options.saturation !== void 0) out.saturation = options.saturation;
3256
3358
  if (options.tone !== void 0) out.tone = options.tone;
3257
3359
  if (options.saturationFactor !== void 0) out.saturationFactor = options.saturationFactor;
3360
+ if (options.darkHue !== void 0) out.darkHue = options.darkHue;
3361
+ if (options.darkSaturation !== void 0) out.darkSaturation = options.darkSaturation;
3362
+ if (options.darkSaturationFactor !== void 0) out.darkSaturationFactor = options.darkSaturationFactor;
3258
3363
  if (options.mode !== void 0) out.mode = options.mode;
3259
3364
  if (options.autoFlip !== void 0) out.autoFlip = options.autoFlip;
3260
3365
  if (options.contrast !== void 0) out.contrast = options.contrast;
@@ -3272,6 +3377,9 @@ function buildStructuredInputExport(input, exportArg) {
3272
3377
  tone: input.tone
3273
3378
  };
3274
3379
  if (input.saturationFactor !== void 0) out.saturationFactor = input.saturationFactor;
3380
+ if (input.darkHue !== void 0) out.darkHue = input.darkHue;
3381
+ if (input.darkSaturation !== void 0) out.darkSaturation = input.darkSaturation;
3382
+ if (input.darkSaturationFactor !== void 0) out.darkSaturationFactor = input.darkSaturationFactor;
3275
3383
  if (input.mode !== void 0) out.mode = input.mode;
3276
3384
  if (input.autoFlip !== void 0) out.autoFlip = input.autoFlip;
3277
3385
  if (input.opacity !== void 0) out.opacity = input.opacity;
@@ -3294,6 +3402,9 @@ function rehydrateOverrides(data) {
3294
3402
  if (data.saturation !== void 0) out.saturation = data.saturation;
3295
3403
  if (data.tone !== void 0) out.tone = data.tone;
3296
3404
  if (data.saturationFactor !== void 0) out.saturationFactor = data.saturationFactor;
3405
+ if (data.darkHue !== void 0) out.darkHue = data.darkHue;
3406
+ if (data.darkSaturation !== void 0) out.darkSaturation = data.darkSaturation;
3407
+ if (data.darkSaturationFactor !== void 0) out.darkSaturationFactor = data.darkSaturationFactor;
3297
3408
  if (data.mode !== void 0) out.mode = data.mode;
3298
3409
  if (data.autoFlip !== void 0) out.autoFlip = data.autoFlip;
3299
3410
  if (data.contrast !== void 0) out.contrast = data.contrast;
@@ -3311,6 +3422,9 @@ function rehydrateStructuredInput(data) {
3311
3422
  tone: data.tone
3312
3423
  };
3313
3424
  if (data.saturationFactor !== void 0) out.saturationFactor = data.saturationFactor;
3425
+ if (data.darkHue !== void 0) out.darkHue = data.darkHue;
3426
+ if (data.darkSaturation !== void 0) out.darkSaturation = data.darkSaturation;
3427
+ if (data.darkSaturationFactor !== void 0) out.darkSaturationFactor = data.darkSaturationFactor;
3314
3428
  if (data.mode !== void 0) out.mode = data.mode;
3315
3429
  if (data.autoFlip !== void 0) out.autoFlip = data.autoFlip;
3316
3430
  if (data.opacity !== void 0) out.opacity = data.opacity;
@@ -3344,7 +3458,8 @@ function colorFromExport(data) {
3344
3458
 
3345
3459
  //#endregion
3346
3460
  //#region src/theme.ts
3347
- function createTheme(hue, saturation, initialColors, configOverride) {
3461
+ function createTheme(seed, initialColors, configOverride) {
3462
+ const { hue, saturation, darkHue, darkSaturation } = seed;
3348
3463
  let colorDefs = initialColors ? { ...initialColors } : {};
3349
3464
  let cache = null;
3350
3465
  function getEffectiveConfig() {
@@ -3362,7 +3477,7 @@ function createTheme(hue, saturation, initialColors, configOverride) {
3362
3477
  const version = getConfigVersion();
3363
3478
  if (cache && cache.version === version && cache.map) return cache.map;
3364
3479
  const effectiveConfig = getEffectiveConfig();
3365
- const map = resolveAllColors(hue, saturation, colorDefs, effectiveConfig);
3480
+ const map = resolveAllColors(seed, colorDefs, effectiveConfig);
3366
3481
  cache = {
3367
3482
  map,
3368
3483
  version,
@@ -3379,6 +3494,7 @@ function createTheme(hue, saturation, initialColors, configOverride) {
3379
3494
  assertAllPastel(resolveCached(), resolveModes(options?.modes));
3380
3495
  return {
3381
3496
  seedHue: hue,
3497
+ darkSeedHue: darkHue,
3382
3498
  baseName: options.name ?? "theme",
3383
3499
  prefix,
3384
3500
  defs: colorDefs,
@@ -3392,6 +3508,12 @@ function createTheme(hue, saturation, initialColors, configOverride) {
3392
3508
  get saturation() {
3393
3509
  return saturation;
3394
3510
  },
3511
+ get darkHue() {
3512
+ return darkHue;
3513
+ },
3514
+ get darkSaturation() {
3515
+ return darkSaturation;
3516
+ },
3395
3517
  getConfig() {
3396
3518
  return getEffectiveConfig();
3397
3519
  },
@@ -3428,16 +3550,22 @@ function createTheme(hue, saturation, initialColors, configOverride) {
3428
3550
  version: GLAZE_EXPORT_VERSION,
3429
3551
  hue,
3430
3552
  saturation,
3553
+ ...darkHue !== void 0 ? { darkHue } : {},
3554
+ ...darkSaturation !== void 0 ? { darkSaturation } : {},
3431
3555
  colors: structuredClone(colorDefs),
3432
3556
  config: freezeConfigForExport(configOverride, override)
3433
3557
  };
3434
3558
  },
3435
3559
  extend(options) {
3436
- const newHue = options.hue ?? hue;
3437
- const newSat = options.saturation ?? saturation;
3560
+ const childSeed = {
3561
+ hue: options.hue ?? hue,
3562
+ saturation: options.saturation ?? saturation,
3563
+ darkHue: options.darkHue ?? darkHue,
3564
+ darkSaturation: options.darkSaturation ?? darkSaturation
3565
+ };
3438
3566
  const inheritedColors = {};
3439
3567
  for (const [name, def] of Object.entries(colorDefs)) if (def.inherit !== false) inheritedColors[name] = def;
3440
- return createTheme(newHue, newSat, options.colors ? {
3568
+ return createTheme(childSeed, options.colors ? {
3441
3569
  ...inheritedColors,
3442
3570
  ...options.colors
3443
3571
  } : { ...inheritedColors }, configOverride || options.config ? {
@@ -3547,6 +3675,7 @@ function channelCtxForTheme(theme, themeName, passPrefix, themedPrefix, splitHue
3547
3675
  assertAllPastel(filtered, modes);
3548
3676
  return {
3549
3677
  seedHue: theme.hue,
3678
+ darkSeedHue: theme.darkHue,
3550
3679
  baseName: themeName,
3551
3680
  prefix: themedPrefix,
3552
3681
  defs: colorMapFromTheme(theme),
@@ -3578,7 +3707,13 @@ function createThemeFromExport(data, factory = "glaze.themeFrom") {
3578
3707
  assertExportKind(data, "theme", factory);
3579
3708
  assertExportVersion(data, factory);
3580
3709
  if (typeof data.hue !== "number" || typeof data.saturation !== "number") throw new Error(`${factory}: expected numeric "hue" and "saturation" fields.`);
3581
- return createTheme(data.hue, data.saturation, data.colors, data.config);
3710
+ for (const field of ["darkHue", "darkSaturation"]) if (data[field] !== void 0 && typeof data[field] !== "number") throw new Error(`${factory}: expected "${field}" to be a number when present (got ${JSON.stringify(data[field])}).`);
3711
+ return createTheme({
3712
+ hue: data.hue,
3713
+ saturation: data.saturation,
3714
+ darkHue: data.darkHue,
3715
+ darkSaturation: data.darkSaturation
3716
+ }, data.colors, data.config);
3582
3717
  }
3583
3718
  /**
3584
3719
  * Rebuild a palette from a `palette.export()` snapshot.
@@ -3740,18 +3875,28 @@ function createPalette(themes, paletteOptions) {
3740
3875
  * the theme still reacts to later `configure()` calls for fields it
3741
3876
  * didn't override.
3742
3877
  *
3878
+ * The object form also accepts `darkHue` / `darkSaturation` — a separate
3879
+ * seed pair for the dark and dark-high-contrast schemes. Both default to
3880
+ * their light counterparts, and an explicit `darkSaturation` opts the theme
3881
+ * out of the global `darkDesaturation` reduction.
3882
+ *
3743
3883
  * @example
3744
3884
  * ```ts
3745
3885
  * const primary = glaze(280, 80);
3746
3886
  * // or shorthand:
3747
3887
  * const primary = glaze({ hue: 280, saturation: 80 });
3888
+ * // with a retuned dark seed:
3889
+ * const primary = glaze({ hue: 280, saturation: 80, darkHue: 268, darkSaturation: 65 });
3748
3890
  * // with config override:
3749
3891
  * const raw = glaze(280, 80, { lightTone: false });
3750
3892
  * ```
3751
3893
  */
3752
3894
  function glaze(hueOrOptions, saturation, config) {
3753
- if (typeof hueOrOptions === "number") return createTheme(hueOrOptions, saturation ?? 100, void 0, config);
3754
- return createTheme(hueOrOptions.hue, hueOrOptions.saturation, void 0, config);
3895
+ if (typeof hueOrOptions === "number") return createTheme({
3896
+ hue: hueOrOptions,
3897
+ saturation: saturation ?? 100
3898
+ }, void 0, config);
3899
+ return createTheme(hueOrOptions, void 0, config);
3755
3900
  }
3756
3901
  /** Configure global glaze settings. */
3757
3902
  glaze.configure = function configure$1(config) {
@@ -3865,7 +4010,10 @@ glaze.fromHex = function fromHex(hex) {
3865
4010
  const rgb = parseHex(hex);
3866
4011
  if (!rgb) throw new Error(`glaze: invalid hex color "${hex}".`);
3867
4012
  const [h, s] = srgbToOkhsl(rgb);
3868
- return createTheme(h, s * 100);
4013
+ return createTheme({
4014
+ hue: h,
4015
+ saturation: s * 100
4016
+ });
3869
4017
  };
3870
4018
  /**
3871
4019
  * Create a theme from RGB values (0–255).
@@ -3877,7 +4025,10 @@ glaze.fromRgb = function fromRgb(r, g, b) {
3877
4025
  g / 255,
3878
4026
  b / 255
3879
4027
  ]);
3880
- return createTheme(h, s * 100);
4028
+ return createTheme({
4029
+ hue: h,
4030
+ saturation: s * 100
4031
+ });
3881
4032
  };
3882
4033
  /**
3883
4034
  * Rehydrate a `glaze.color()` token from a `.export()` snapshot.