@tenphi/glaze 1.1.1 → 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
@@ -1992,24 +1992,64 @@ function applyToneFlip(delta, baseTone, flip) {
1992
1992
  if (mirrored >= 0 && mirrored <= 100) return -delta;
1993
1993
  return delta;
1994
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);
2008
+ }
1995
2009
  function resolveRootColor(def, isHighContrast) {
1996
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;
1997
2037
  return {
1998
- authorTone: clamp(parseToneValue(isHighContrast ? pairHC(rawT) : pairNormal(rawT)).value, 0, 100),
1999
- 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)
2000
2040
  };
2001
2041
  }
2002
- function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effectiveHue, polarity, effectivePastel) {
2042
+ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, channels, polarity, effectivePastel) {
2003
2043
  const baseName = def.base;
2004
2044
  const baseResolved = ctx.resolved.get(baseName);
2005
2045
  if (!baseResolved) throw new Error(`glaze: base "${baseName}" not yet resolved for "${name}".`);
2006
2046
  const mode = def.mode ?? "auto";
2007
- const satFactor = clamp(def.saturation ?? 1, 0, 1);
2008
2047
  const flip = def.autoFlip ?? ctx.config.autoFlip;
2009
2048
  const pastel = effectivePastel;
2010
2049
  const baseVariant = getSchemeVariant(baseResolved, isDark, isHighContrast);
2011
2050
  const baseTone = baseVariant.t * 100;
2012
2051
  let preferredTone;
2052
+ let isExtreme = false;
2013
2053
  const rawTone = def.tone;
2014
2054
  if (rawTone === void 0) preferredTone = baseTone;
2015
2055
  else {
@@ -2018,22 +2058,28 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
2018
2058
  const baseLightTone = getSchemeVariant(baseResolved, false, isHighContrast).t * 100;
2019
2059
  preferredTone = mapToneForScheme(clamp(baseLightTone + applyToneFlip(parsed.value, baseLightTone, flip), 0, 100), "auto", true, isHighContrast, ctx.config);
2020
2060
  } else preferredTone = clamp(baseTone + applyToneFlip(parsed.value, baseTone, flip), 0, 100);
2021
- 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
+ }
2022
2068
  }
2023
2069
  const rawContrast = def.contrast;
2024
2070
  if (rawContrast !== void 0) {
2025
2071
  const resolvedContrast = resolveContrastSpec(rawContrast, isHighContrast, polarity);
2026
- const effectiveSat = isDark ? mapSaturationDark(satFactor * ctx.saturation / 100, mode, ctx.config) : satFactor * ctx.saturation / 100;
2027
2072
  const baseOkhsl = toOkhslVariant(baseVariant);
2028
2073
  const baseLinearRgb = okhslToLinearSrgb(baseOkhsl.h, baseOkhsl.s, baseOkhsl.l, baseVariant.pastel ?? ctx.config.pastel);
2029
2074
  const toneRange = schemeToneRange(isDark, mode, isHighContrast, ctx.config);
2030
2075
  let initialDirection;
2031
2076
  if (preferredTone < baseTone) initialDirection = "darker";
2032
2077
  else if (preferredTone > baseTone) initialDirection = "lighter";
2078
+ const seedTone = isExtreme ? clamp(preferredTone / 100, 0, 1) : clamp(preferredTone / 100, toneRange[0], toneRange[1]);
2033
2079
  const result = findToneForContrast({
2034
- hue: effectiveHue,
2035
- saturation: effectiveSat,
2036
- preferredTone: clamp(preferredTone / 100, toneRange[0], toneRange[1]),
2080
+ hue: channels.hue,
2081
+ saturation: channels.saturation,
2082
+ preferredTone: seedTone,
2037
2083
  baseLinearRgb,
2038
2084
  contrast: resolvedContrast,
2039
2085
  toneRange: [0, 1],
@@ -2042,15 +2088,9 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
2042
2088
  pastel
2043
2089
  });
2044
2090
  if (!result.met) warnContrastUnmet(name, isDark, isHighContrast, resolvedContrast, result.contrast);
2045
- return {
2046
- tone: result.tone * 100,
2047
- satFactor
2048
- };
2091
+ return result.tone * 100;
2049
2092
  }
2050
- return {
2051
- tone: clamp(preferredTone, 0, 100),
2052
- satFactor
2053
- };
2093
+ return clamp(preferredTone, 0, 100);
2054
2094
  }
2055
2095
  function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
2056
2096
  if (isShadowDef(def)) return resolveShadowForScheme(def, ctx, isDark, isHighContrast);
@@ -2058,27 +2098,14 @@ function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
2058
2098
  const regDef = def;
2059
2099
  const mode = regDef.mode ?? "auto";
2060
2100
  const isRoot = isAbsoluteTone(regDef.tone) && !regDef.base;
2061
- const effectiveHue = resolveEffectiveHue(ctx.hue, regDef.hue);
2101
+ const channels = resolveChannels(regDef, ctx, isDark);
2062
2102
  const polarity = roleToPolarity(resolveRole(name, def, ctx));
2063
2103
  const pastel = regDef.pastel ?? ctx.config.pastel;
2064
- let finalTone;
2065
- let satFactor;
2066
- if (isRoot) {
2067
- const root = resolveRootColor(regDef, isHighContrast);
2068
- finalTone = mapToneForScheme(root.authorTone, mode, isDark, isHighContrast, ctx.config);
2069
- satFactor = root.satFactor;
2070
- } else {
2071
- const dep = resolveDependentColor(name, regDef, ctx, isHighContrast, isDark, effectiveHue, polarity, pastel);
2072
- finalTone = dep.tone;
2073
- satFactor = dep.satFactor;
2074
- }
2075
- const baseSat = satFactor * ctx.saturation / 100;
2076
- const finalSat = isDark ? mapSaturationDark(baseSat, mode, ctx.config) : baseSat;
2077
- 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);
2078
2105
  return {
2079
- h: effectiveHue,
2080
- s: clamp(finalSat, 0, 1),
2081
- t: toneFraction,
2106
+ h: channels.hue,
2107
+ s: channels.saturation,
2108
+ t: clamp(finalTone / 100, 0, 1),
2082
2109
  alpha: regDef.opacity ?? 1,
2083
2110
  pastel
2084
2111
  };
@@ -2278,12 +2305,11 @@ function verifyContrastDrift(order, defs, result, config) {
2278
2305
  }
2279
2306
  }
2280
2307
  }
2281
- function resolveAllColors(hue, saturation, defs, config, externalBases) {
2308
+ function resolveAllColors(seed, defs, config, externalBases) {
2282
2309
  validateColorDefs(defs, externalBases);
2283
2310
  const order = topoSort(defs);
2284
2311
  const ctx = {
2285
- hue,
2286
- saturation,
2312
+ ...seed,
2287
2313
  defs,
2288
2314
  resolved: /* @__PURE__ */ new Map(),
2289
2315
  config,
@@ -2316,8 +2342,12 @@ function resolveAllColors(hue, saturation, defs, config, externalBases) {
2316
2342
  /**
2317
2343
  * Hue channel planning for `splitHue` exports.
2318
2344
  *
2319
- * Builds per-color hue var references and scheme-independent `--*-hue`
2320
- * 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`.
2321
2351
  */
2322
2352
  const ACHROMATIC_EPSILON = 1e-6;
2323
2353
  function cssProp(prefix, name, suffix) {
@@ -2326,7 +2356,11 @@ function cssProp(prefix, name, suffix) {
2326
2356
  function isAchromatic(v) {
2327
2357
  return v.s <= ACHROMATIC_EPSILON;
2328
2358
  }
2329
- 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) {
2330
2364
  if (def === void 0 || isShadowDef(def) || isMixDef(def) || isAchromatic(variant)) return {
2331
2365
  hueVar: "",
2332
2366
  inline: true,
@@ -2334,13 +2368,25 @@ function themeHuePlan(name, def, variant, ctx) {
2334
2368
  };
2335
2369
  const regDef = def;
2336
2370
  const baseHueVar = `var(--${ctx.baseName}-hue)`;
2337
- 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 {
2338
2385
  hueVar: baseHueVar,
2339
2386
  inline: false,
2340
2387
  declarations: []
2341
2388
  };
2342
- const parsed = parseRelativeOrAbsolute(regDef.hue);
2343
- const prop = cssProp(ctx.prefix, name, "-hue");
2389
+ const parsed = parseRelativeOrAbsolute(authored);
2344
2390
  if (parsed.relative) {
2345
2391
  const sign = parsed.value >= 0 ? "+" : "-";
2346
2392
  const magnitude = Math.abs(parsed.value);
@@ -2364,13 +2410,13 @@ function themeHuePlan(name, def, variant, ctx) {
2364
2410
  }]
2365
2411
  };
2366
2412
  }
2367
- function standaloneHuePlan(name, variant, ctx) {
2413
+ function standaloneHuePlan(name, variant, ctx, scheme) {
2368
2414
  if (isAchromatic(variant)) return {
2369
2415
  hueVar: "",
2370
2416
  inline: true,
2371
2417
  declarations: []
2372
2418
  };
2373
- const hue = ctx.resolvedHue ?? variant.h;
2419
+ const hue = scheme === "dark" ? variant.h : ctx.resolvedHue ?? variant.h;
2374
2420
  const prop = cssProp(ctx.prefix, name, "-hue");
2375
2421
  return {
2376
2422
  hueVar: `var(${prop})`,
@@ -2381,12 +2427,12 @@ function standaloneHuePlan(name, variant, ctx) {
2381
2427
  }]
2382
2428
  };
2383
2429
  }
2384
- function buildHuePlan(name, def, variant, ctx) {
2385
- if (ctx.mode === "standalone") return standaloneHuePlan(name, variant, ctx);
2386
- 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);
2387
2433
  }
2388
2434
  /** Collect unique hue declarations across all colors (theme + per-color). */
2389
- function collectHueDeclarations(resolved, ctx) {
2435
+ function collectHueDeclarations(resolved, ctx, scheme = "light") {
2390
2436
  if (ctx.emitDeclarations === false) return [];
2391
2437
  const seen = /* @__PURE__ */ new Set();
2392
2438
  const out = [];
@@ -2397,15 +2443,32 @@ function collectHueDeclarations(resolved, ctx) {
2397
2443
  };
2398
2444
  if (ctx.mode === "theme") push({
2399
2445
  prop: `--${ctx.baseName}-hue`,
2400
- value: String(ctx.seedHue)
2446
+ value: String(schemeSeedHue(ctx, scheme))
2401
2447
  });
2402
2448
  for (const [name, color] of resolved) {
2403
2449
  const def = ctx.defs[name];
2404
- const plan = buildHuePlan(name, def, color.light, ctx);
2450
+ const plan = buildHuePlan(name, def, scheme === "dark" ? color.dark : color.light, ctx, scheme);
2405
2451
  for (const decl of plan.declarations) push(decl);
2406
2452
  }
2407
2453
  return out;
2408
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
+ }
2409
2472
  function buildHuePlans(resolved, ctx) {
2410
2473
  const plans = /* @__PURE__ */ new Map();
2411
2474
  for (const [name, color] of resolved) plans.set(name, buildHuePlan(name, ctx.defs[name], color.light, ctx));
@@ -2479,17 +2542,15 @@ function buildTokenMap(resolved, prefix, states, modes, format = "oklch", pastel
2479
2542
  const tokens = {};
2480
2543
  const huePlans = channelCtx !== void 0 && format === "oklch" ? buildHuePlans(resolved, channelCtx) : void 0;
2481
2544
  if (huePlans !== void 0 && channelCtx !== void 0) {
2482
- const emitDecls = channelCtx.emitDeclarations !== false;
2483
- if (emitDecls && channelCtx.mode === "theme") tokens[`$${channelCtx.baseName}-hue`] = { "": String(channelCtx.seedHue) };
2484
- for (const [name, color] of resolved) {
2485
- const plan = huePlans.get(name);
2486
- if (emitDecls) for (const decl of plan.declarations) {
2487
- const key = `$${decl.prop.slice(2)}`;
2488
- if (!(key in tokens)) tokens[key] = { "": decl.value };
2489
- }
2490
- const colorKey = `#${prefix}${name}`;
2491
- 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
+ };
2492
2552
  }
2553
+ for (const [name, color] of resolved) tokens[`#${prefix}${name}`] = buildTokenEntry(color, states, modes, format, pastel, huePlans.get(name));
2493
2554
  return tokens;
2494
2555
  }
2495
2556
  for (const [name, color] of resolved) {
@@ -2538,7 +2599,13 @@ function buildCssMap(resolved, prefix, suffix, format, pastel = false, channelCt
2538
2599
  darkContrast: []
2539
2600
  };
2540
2601
  const huePlans = channelCtx !== void 0 && format === "oklch" ? buildHuePlans(resolved, channelCtx) : void 0;
2541
- 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
+ }
2542
2609
  for (const [name, color] of resolved) {
2543
2610
  const prop = `--${prefix}${name}${suffix}`;
2544
2611
  const plan = huePlans?.get(name);
@@ -2975,6 +3042,13 @@ function validateStructuredInput(input) {
2975
3042
  if (input.saturationFactor !== void 0) {
2976
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}).`);
2977
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
+ }
2978
3052
  if (input.opacity !== void 0) validateStandaloneOpacity(input.opacity);
2979
3053
  }
2980
3054
  /**
@@ -3021,11 +3095,27 @@ function extractOkhslFromValue(value) {
3021
3095
  return value;
3022
3096
  }
3023
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
+ /**
3024
3110
  * Build the `ColorMap` for a value-shorthand `glaze.color()` call.
3025
3111
  *
3026
3112
  * The user-facing color (`STANDALONE_VALUE`) defaults to `mode: 'auto'`
3027
3113
  * across every value-shorthand form.
3028
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
+ *
3029
3119
  * When the user requests `contrast` or relative `tone`, a hidden
3030
3120
  * `STANDALONE_SEED` def is synthesized at `mode: 'static'`. That keeps
3031
3121
  * the seed pinned to the literal user-provided color across all four
@@ -3043,10 +3133,13 @@ function buildStandaloneValueDefs(main, options) {
3043
3133
  if (userName !== void 0) validateStandaloneName(userName);
3044
3134
  const primary = userName ?? STANDALONE_VALUE;
3045
3135
  const seedTone = toTone(main.l);
3136
+ const primaryTone = toneOption ?? seedTone;
3046
3137
  const valueDef = {
3047
3138
  hue: relativeHue,
3048
3139
  saturation: options?.saturationFactor,
3049
- tone: toneOption ?? seedTone,
3140
+ darkHue: options?.darkHue,
3141
+ darkSaturation: options?.darkSaturationFactor,
3142
+ tone: needsSeedAnchor ? pinExtremeTone(primaryTone) : primaryTone,
3050
3143
  contrast: options?.contrast,
3051
3144
  mode: options?.mode ?? "auto",
3052
3145
  autoFlip: options?.autoFlip,
@@ -3063,13 +3156,16 @@ function buildStandaloneValueDefs(main, options) {
3063
3156
  mode: "static"
3064
3157
  };
3065
3158
  return {
3066
- seedHue,
3067
- seedSaturation,
3159
+ seed: {
3160
+ hue: seedHue,
3161
+ saturation: seedSaturation,
3162
+ darkSaturation: options?.darkSaturation
3163
+ },
3068
3164
  defs,
3069
3165
  primary
3070
3166
  };
3071
3167
  }
3072
- function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, configOverride, baseToken, buildExport) {
3168
+ function createColorTokenFromDefs(seed, defs, primary, configOverride, baseToken, buildExport) {
3073
3169
  let cache = null;
3074
3170
  function getEffectiveConfig() {
3075
3171
  const version = getConfigVersion();
@@ -3086,7 +3182,7 @@ function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, config
3086
3182
  const version = getConfigVersion();
3087
3183
  if (cache && cache.version === version && cache.map) return cache.map;
3088
3184
  const effectiveConfig = getEffectiveConfig();
3089
- 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);
3090
3186
  cache = {
3091
3187
  map,
3092
3188
  version,
@@ -3124,7 +3220,7 @@ function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, config
3124
3220
  if (options.splitHue && format === "oklch") {
3125
3221
  assertAllPastel(renamed, resolveModes());
3126
3222
  channelCtx = {
3127
- seedHue,
3223
+ seedHue: seed.hue,
3128
3224
  baseName: options.name,
3129
3225
  prefix: "",
3130
3226
  defs: { [options.name]: defs[primary] },
@@ -3208,8 +3304,10 @@ function createColorToken(input, configOverride) {
3208
3304
  const hasExternalBase = baseToken !== void 0;
3209
3305
  const needsSeedAnchor = !hasExternalBase && input.contrast !== void 0;
3210
3306
  const defs = { [primary]: {
3211
- tone: input.tone,
3307
+ tone: needsSeedAnchor ? pinExtremeTone(input.tone) : input.tone,
3212
3308
  saturation: input.saturationFactor,
3309
+ darkHue: input.darkHue,
3310
+ darkSaturation: input.darkSaturationFactor,
3213
3311
  mode: input.mode ?? "auto",
3214
3312
  autoFlip: input.autoFlip,
3215
3313
  contrast: input.contrast,
@@ -3218,16 +3316,17 @@ function createColorToken(input, configOverride) {
3218
3316
  role: input.role,
3219
3317
  base: hasExternalBase ? STANDALONE_BASE : needsSeedAnchor ? STANDALONE_SEED : void 0
3220
3318
  } };
3221
- if (needsSeedAnchor) {
3222
- const seedTone = pairNormal(input.tone);
3223
- defs[STANDALONE_SEED] = {
3224
- tone: seedTone === "max" ? 100 : seedTone === "min" ? 0 : seedTone,
3225
- saturation: 1,
3226
- mode: "static"
3227
- };
3228
- }
3319
+ if (needsSeedAnchor) defs[STANDALONE_SEED] = {
3320
+ tone: pinExtremeTone(pairNormal(input.tone)),
3321
+ saturation: 1,
3322
+ mode: "static"
3323
+ };
3229
3324
  const localOverride = configOverride;
3230
- 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) => ({
3231
3330
  kind: "color",
3232
3331
  version: GLAZE_EXPORT_VERSION,
3233
3332
  form: "structured",
@@ -3238,9 +3337,9 @@ function createColorToken(input, configOverride) {
3238
3337
  function createColorTokenFromValue(value, options, configOverride) {
3239
3338
  const main = extractOkhslFromValue(value);
3240
3339
  const linkingBase = toLinkingBase(resolveBaseToken(options?.base));
3241
- const { seedHue, seedSaturation, defs, primary } = buildStandaloneValueDefs(main, options);
3340
+ const { seed, defs, primary } = buildStandaloneValueDefs(main, options);
3242
3341
  const localOverride = sparseValueFormLocal(configOverride);
3243
- return createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, localOverride, linkingBase, (exportArg) => ({
3342
+ return createColorTokenFromDefs(seed, defs, primary, localOverride, linkingBase, (exportArg) => ({
3244
3343
  kind: "color",
3245
3344
  version: GLAZE_EXPORT_VERSION,
3246
3345
  form: "value",
@@ -3260,6 +3359,9 @@ function buildOverridesExport(options, exportArg) {
3260
3359
  if (options.saturation !== void 0) out.saturation = options.saturation;
3261
3360
  if (options.tone !== void 0) out.tone = options.tone;
3262
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;
3263
3365
  if (options.mode !== void 0) out.mode = options.mode;
3264
3366
  if (options.autoFlip !== void 0) out.autoFlip = options.autoFlip;
3265
3367
  if (options.contrast !== void 0) out.contrast = options.contrast;
@@ -3277,6 +3379,9 @@ function buildStructuredInputExport(input, exportArg) {
3277
3379
  tone: input.tone
3278
3380
  };
3279
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;
3280
3385
  if (input.mode !== void 0) out.mode = input.mode;
3281
3386
  if (input.autoFlip !== void 0) out.autoFlip = input.autoFlip;
3282
3387
  if (input.opacity !== void 0) out.opacity = input.opacity;
@@ -3299,6 +3404,9 @@ function rehydrateOverrides(data) {
3299
3404
  if (data.saturation !== void 0) out.saturation = data.saturation;
3300
3405
  if (data.tone !== void 0) out.tone = data.tone;
3301
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;
3302
3410
  if (data.mode !== void 0) out.mode = data.mode;
3303
3411
  if (data.autoFlip !== void 0) out.autoFlip = data.autoFlip;
3304
3412
  if (data.contrast !== void 0) out.contrast = data.contrast;
@@ -3316,6 +3424,9 @@ function rehydrateStructuredInput(data) {
3316
3424
  tone: data.tone
3317
3425
  };
3318
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;
3319
3430
  if (data.mode !== void 0) out.mode = data.mode;
3320
3431
  if (data.autoFlip !== void 0) out.autoFlip = data.autoFlip;
3321
3432
  if (data.opacity !== void 0) out.opacity = data.opacity;
@@ -3349,7 +3460,8 @@ function colorFromExport(data) {
3349
3460
 
3350
3461
  //#endregion
3351
3462
  //#region src/theme.ts
3352
- function createTheme(hue, saturation, initialColors, configOverride) {
3463
+ function createTheme(seed, initialColors, configOverride) {
3464
+ const { hue, saturation, darkHue, darkSaturation } = seed;
3353
3465
  let colorDefs = initialColors ? { ...initialColors } : {};
3354
3466
  let cache = null;
3355
3467
  function getEffectiveConfig() {
@@ -3367,7 +3479,7 @@ function createTheme(hue, saturation, initialColors, configOverride) {
3367
3479
  const version = getConfigVersion();
3368
3480
  if (cache && cache.version === version && cache.map) return cache.map;
3369
3481
  const effectiveConfig = getEffectiveConfig();
3370
- const map = resolveAllColors(hue, saturation, colorDefs, effectiveConfig);
3482
+ const map = resolveAllColors(seed, colorDefs, effectiveConfig);
3371
3483
  cache = {
3372
3484
  map,
3373
3485
  version,
@@ -3384,6 +3496,7 @@ function createTheme(hue, saturation, initialColors, configOverride) {
3384
3496
  assertAllPastel(resolveCached(), resolveModes(options?.modes));
3385
3497
  return {
3386
3498
  seedHue: hue,
3499
+ darkSeedHue: darkHue,
3387
3500
  baseName: options.name ?? "theme",
3388
3501
  prefix,
3389
3502
  defs: colorDefs,
@@ -3397,6 +3510,12 @@ function createTheme(hue, saturation, initialColors, configOverride) {
3397
3510
  get saturation() {
3398
3511
  return saturation;
3399
3512
  },
3513
+ get darkHue() {
3514
+ return darkHue;
3515
+ },
3516
+ get darkSaturation() {
3517
+ return darkSaturation;
3518
+ },
3400
3519
  getConfig() {
3401
3520
  return getEffectiveConfig();
3402
3521
  },
@@ -3433,16 +3552,22 @@ function createTheme(hue, saturation, initialColors, configOverride) {
3433
3552
  version: GLAZE_EXPORT_VERSION,
3434
3553
  hue,
3435
3554
  saturation,
3555
+ ...darkHue !== void 0 ? { darkHue } : {},
3556
+ ...darkSaturation !== void 0 ? { darkSaturation } : {},
3436
3557
  colors: structuredClone(colorDefs),
3437
3558
  config: freezeConfigForExport(configOverride, override)
3438
3559
  };
3439
3560
  },
3440
3561
  extend(options) {
3441
- const newHue = options.hue ?? hue;
3442
- 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
+ };
3443
3568
  const inheritedColors = {};
3444
3569
  for (const [name, def] of Object.entries(colorDefs)) if (def.inherit !== false) inheritedColors[name] = def;
3445
- return createTheme(newHue, newSat, options.colors ? {
3570
+ return createTheme(childSeed, options.colors ? {
3446
3571
  ...inheritedColors,
3447
3572
  ...options.colors
3448
3573
  } : { ...inheritedColors }, configOverride || options.config ? {
@@ -3552,6 +3677,7 @@ function channelCtxForTheme(theme, themeName, passPrefix, themedPrefix, splitHue
3552
3677
  assertAllPastel(filtered, modes);
3553
3678
  return {
3554
3679
  seedHue: theme.hue,
3680
+ darkSeedHue: theme.darkHue,
3555
3681
  baseName: themeName,
3556
3682
  prefix: themedPrefix,
3557
3683
  defs: colorMapFromTheme(theme),
@@ -3583,7 +3709,13 @@ function createThemeFromExport(data, factory = "glaze.themeFrom") {
3583
3709
  assertExportKind(data, "theme", factory);
3584
3710
  assertExportVersion(data, factory);
3585
3711
  if (typeof data.hue !== "number" || typeof data.saturation !== "number") throw new Error(`${factory}: expected numeric "hue" and "saturation" fields.`);
3586
- 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);
3587
3719
  }
3588
3720
  /**
3589
3721
  * Rebuild a palette from a `palette.export()` snapshot.
@@ -3745,18 +3877,28 @@ function createPalette(themes, paletteOptions) {
3745
3877
  * the theme still reacts to later `configure()` calls for fields it
3746
3878
  * didn't override.
3747
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
+ *
3748
3885
  * @example
3749
3886
  * ```ts
3750
3887
  * const primary = glaze(280, 80);
3751
3888
  * // or shorthand:
3752
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 });
3753
3892
  * // with config override:
3754
3893
  * const raw = glaze(280, 80, { lightTone: false });
3755
3894
  * ```
3756
3895
  */
3757
3896
  function glaze(hueOrOptions, saturation, config) {
3758
- if (typeof hueOrOptions === "number") return createTheme(hueOrOptions, saturation ?? 100, void 0, config);
3759
- 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);
3760
3902
  }
3761
3903
  /** Configure global glaze settings. */
3762
3904
  glaze.configure = function configure$1(config) {
@@ -3870,7 +4012,10 @@ glaze.fromHex = function fromHex(hex) {
3870
4012
  const rgb = parseHex(hex);
3871
4013
  if (!rgb) throw new Error(`glaze: invalid hex color "${hex}".`);
3872
4014
  const [h, s] = srgbToOkhsl(rgb);
3873
- return createTheme(h, s * 100);
4015
+ return createTheme({
4016
+ hue: h,
4017
+ saturation: s * 100
4018
+ });
3874
4019
  };
3875
4020
  /**
3876
4021
  * Create a theme from RGB values (0–255).
@@ -3882,7 +4027,10 @@ glaze.fromRgb = function fromRgb(r, g, b) {
3882
4027
  g / 255,
3883
4028
  b / 255
3884
4029
  ]);
3885
- return createTheme(h, s * 100);
4030
+ return createTheme({
4031
+ hue: h,
4032
+ saturation: s * 100
4033
+ });
3886
4034
  };
3887
4035
  /**
3888
4036
  * Rehydrate a `glaze.color()` token from a `.export()` snapshot.