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