@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.mjs CHANGED
@@ -1990,24 +1990,64 @@ function applyToneFlip(delta, baseTone, flip) {
1990
1990
  if (mirrored >= 0 && mirrored <= 100) return -delta;
1991
1991
  return delta;
1992
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);
2006
+ }
1993
2007
  function resolveRootColor(def, isHighContrast) {
1994
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;
1995
2035
  return {
1996
- authorTone: clamp(parseToneValue(isHighContrast ? pairHC(rawT) : pairNormal(rawT)).value, 0, 100),
1997
- 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)
1998
2038
  };
1999
2039
  }
2000
- function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effectiveHue, polarity, effectivePastel) {
2040
+ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, channels, polarity, effectivePastel) {
2001
2041
  const baseName = def.base;
2002
2042
  const baseResolved = ctx.resolved.get(baseName);
2003
2043
  if (!baseResolved) throw new Error(`glaze: base "${baseName}" not yet resolved for "${name}".`);
2004
2044
  const mode = def.mode ?? "auto";
2005
- const satFactor = clamp(def.saturation ?? 1, 0, 1);
2006
2045
  const flip = def.autoFlip ?? ctx.config.autoFlip;
2007
2046
  const pastel = effectivePastel;
2008
2047
  const baseVariant = getSchemeVariant(baseResolved, isDark, isHighContrast);
2009
2048
  const baseTone = baseVariant.t * 100;
2010
2049
  let preferredTone;
2050
+ let isExtreme = false;
2011
2051
  const rawTone = def.tone;
2012
2052
  if (rawTone === void 0) preferredTone = baseTone;
2013
2053
  else {
@@ -2016,22 +2056,28 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
2016
2056
  const baseLightTone = getSchemeVariant(baseResolved, false, isHighContrast).t * 100;
2017
2057
  preferredTone = mapToneForScheme(clamp(baseLightTone + applyToneFlip(parsed.value, baseLightTone, flip), 0, 100), "auto", true, isHighContrast, ctx.config);
2018
2058
  } else preferredTone = clamp(baseTone + applyToneFlip(parsed.value, baseTone, flip), 0, 100);
2019
- 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
+ }
2020
2066
  }
2021
2067
  const rawContrast = def.contrast;
2022
2068
  if (rawContrast !== void 0) {
2023
2069
  const resolvedContrast = resolveContrastSpec(rawContrast, isHighContrast, polarity);
2024
- const effectiveSat = isDark ? mapSaturationDark(satFactor * ctx.saturation / 100, mode, ctx.config) : satFactor * ctx.saturation / 100;
2025
2070
  const baseOkhsl = toOkhslVariant(baseVariant);
2026
2071
  const baseLinearRgb = okhslToLinearSrgb(baseOkhsl.h, baseOkhsl.s, baseOkhsl.l, baseVariant.pastel ?? ctx.config.pastel);
2027
2072
  const toneRange = schemeToneRange(isDark, mode, isHighContrast, ctx.config);
2028
2073
  let initialDirection;
2029
2074
  if (preferredTone < baseTone) initialDirection = "darker";
2030
2075
  else if (preferredTone > baseTone) initialDirection = "lighter";
2076
+ const seedTone = isExtreme ? clamp(preferredTone / 100, 0, 1) : clamp(preferredTone / 100, toneRange[0], toneRange[1]);
2031
2077
  const result = findToneForContrast({
2032
- hue: effectiveHue,
2033
- saturation: effectiveSat,
2034
- preferredTone: clamp(preferredTone / 100, toneRange[0], toneRange[1]),
2078
+ hue: channels.hue,
2079
+ saturation: channels.saturation,
2080
+ preferredTone: seedTone,
2035
2081
  baseLinearRgb,
2036
2082
  contrast: resolvedContrast,
2037
2083
  toneRange: [0, 1],
@@ -2040,15 +2086,9 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
2040
2086
  pastel
2041
2087
  });
2042
2088
  if (!result.met) warnContrastUnmet(name, isDark, isHighContrast, resolvedContrast, result.contrast);
2043
- return {
2044
- tone: result.tone * 100,
2045
- satFactor
2046
- };
2089
+ return result.tone * 100;
2047
2090
  }
2048
- return {
2049
- tone: clamp(preferredTone, 0, 100),
2050
- satFactor
2051
- };
2091
+ return clamp(preferredTone, 0, 100);
2052
2092
  }
2053
2093
  function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
2054
2094
  if (isShadowDef(def)) return resolveShadowForScheme(def, ctx, isDark, isHighContrast);
@@ -2056,27 +2096,14 @@ function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
2056
2096
  const regDef = def;
2057
2097
  const mode = regDef.mode ?? "auto";
2058
2098
  const isRoot = isAbsoluteTone(regDef.tone) && !regDef.base;
2059
- const effectiveHue = resolveEffectiveHue(ctx.hue, regDef.hue);
2099
+ const channels = resolveChannels(regDef, ctx, isDark);
2060
2100
  const polarity = roleToPolarity(resolveRole(name, def, ctx));
2061
2101
  const pastel = regDef.pastel ?? ctx.config.pastel;
2062
- let finalTone;
2063
- let satFactor;
2064
- if (isRoot) {
2065
- const root = resolveRootColor(regDef, isHighContrast);
2066
- finalTone = mapToneForScheme(root.authorTone, mode, isDark, isHighContrast, ctx.config);
2067
- satFactor = root.satFactor;
2068
- } else {
2069
- const dep = resolveDependentColor(name, regDef, ctx, isHighContrast, isDark, effectiveHue, polarity, pastel);
2070
- finalTone = dep.tone;
2071
- satFactor = dep.satFactor;
2072
- }
2073
- const baseSat = satFactor * ctx.saturation / 100;
2074
- const finalSat = isDark ? mapSaturationDark(baseSat, mode, ctx.config) : baseSat;
2075
- 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);
2076
2103
  return {
2077
- h: effectiveHue,
2078
- s: clamp(finalSat, 0, 1),
2079
- t: toneFraction,
2104
+ h: channels.hue,
2105
+ s: channels.saturation,
2106
+ t: clamp(finalTone / 100, 0, 1),
2080
2107
  alpha: regDef.opacity ?? 1,
2081
2108
  pastel
2082
2109
  };
@@ -2276,12 +2303,11 @@ function verifyContrastDrift(order, defs, result, config) {
2276
2303
  }
2277
2304
  }
2278
2305
  }
2279
- function resolveAllColors(hue, saturation, defs, config, externalBases) {
2306
+ function resolveAllColors(seed, defs, config, externalBases) {
2280
2307
  validateColorDefs(defs, externalBases);
2281
2308
  const order = topoSort(defs);
2282
2309
  const ctx = {
2283
- hue,
2284
- saturation,
2310
+ ...seed,
2285
2311
  defs,
2286
2312
  resolved: /* @__PURE__ */ new Map(),
2287
2313
  config,
@@ -2314,8 +2340,12 @@ function resolveAllColors(hue, saturation, defs, config, externalBases) {
2314
2340
  /**
2315
2341
  * Hue channel planning for `splitHue` exports.
2316
2342
  *
2317
- * Builds per-color hue var references and scheme-independent `--*-hue`
2318
- * 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`.
2319
2349
  */
2320
2350
  const ACHROMATIC_EPSILON = 1e-6;
2321
2351
  function cssProp(prefix, name, suffix) {
@@ -2324,7 +2354,11 @@ function cssProp(prefix, name, suffix) {
2324
2354
  function isAchromatic(v) {
2325
2355
  return v.s <= ACHROMATIC_EPSILON;
2326
2356
  }
2327
- 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) {
2328
2362
  if (def === void 0 || isShadowDef(def) || isMixDef(def) || isAchromatic(variant)) return {
2329
2363
  hueVar: "",
2330
2364
  inline: true,
@@ -2332,13 +2366,25 @@ function themeHuePlan(name, def, variant, ctx) {
2332
2366
  };
2333
2367
  const regDef = def;
2334
2368
  const baseHueVar = `var(--${ctx.baseName}-hue)`;
2335
- 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 {
2336
2383
  hueVar: baseHueVar,
2337
2384
  inline: false,
2338
2385
  declarations: []
2339
2386
  };
2340
- const parsed = parseRelativeOrAbsolute(regDef.hue);
2341
- const prop = cssProp(ctx.prefix, name, "-hue");
2387
+ const parsed = parseRelativeOrAbsolute(authored);
2342
2388
  if (parsed.relative) {
2343
2389
  const sign = parsed.value >= 0 ? "+" : "-";
2344
2390
  const magnitude = Math.abs(parsed.value);
@@ -2362,13 +2408,13 @@ function themeHuePlan(name, def, variant, ctx) {
2362
2408
  }]
2363
2409
  };
2364
2410
  }
2365
- function standaloneHuePlan(name, variant, ctx) {
2411
+ function standaloneHuePlan(name, variant, ctx, scheme) {
2366
2412
  if (isAchromatic(variant)) return {
2367
2413
  hueVar: "",
2368
2414
  inline: true,
2369
2415
  declarations: []
2370
2416
  };
2371
- const hue = ctx.resolvedHue ?? variant.h;
2417
+ const hue = scheme === "dark" ? variant.h : ctx.resolvedHue ?? variant.h;
2372
2418
  const prop = cssProp(ctx.prefix, name, "-hue");
2373
2419
  return {
2374
2420
  hueVar: `var(${prop})`,
@@ -2379,12 +2425,12 @@ function standaloneHuePlan(name, variant, ctx) {
2379
2425
  }]
2380
2426
  };
2381
2427
  }
2382
- function buildHuePlan(name, def, variant, ctx) {
2383
- if (ctx.mode === "standalone") return standaloneHuePlan(name, variant, ctx);
2384
- 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);
2385
2431
  }
2386
2432
  /** Collect unique hue declarations across all colors (theme + per-color). */
2387
- function collectHueDeclarations(resolved, ctx) {
2433
+ function collectHueDeclarations(resolved, ctx, scheme = "light") {
2388
2434
  if (ctx.emitDeclarations === false) return [];
2389
2435
  const seen = /* @__PURE__ */ new Set();
2390
2436
  const out = [];
@@ -2395,15 +2441,32 @@ function collectHueDeclarations(resolved, ctx) {
2395
2441
  };
2396
2442
  if (ctx.mode === "theme") push({
2397
2443
  prop: `--${ctx.baseName}-hue`,
2398
- value: String(ctx.seedHue)
2444
+ value: String(schemeSeedHue(ctx, scheme))
2399
2445
  });
2400
2446
  for (const [name, color] of resolved) {
2401
2447
  const def = ctx.defs[name];
2402
- const plan = buildHuePlan(name, def, color.light, ctx);
2448
+ const plan = buildHuePlan(name, def, scheme === "dark" ? color.dark : color.light, ctx, scheme);
2403
2449
  for (const decl of plan.declarations) push(decl);
2404
2450
  }
2405
2451
  return out;
2406
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
+ }
2407
2470
  function buildHuePlans(resolved, ctx) {
2408
2471
  const plans = /* @__PURE__ */ new Map();
2409
2472
  for (const [name, color] of resolved) plans.set(name, buildHuePlan(name, ctx.defs[name], color.light, ctx));
@@ -2477,17 +2540,15 @@ function buildTokenMap(resolved, prefix, states, modes, format = "oklch", pastel
2477
2540
  const tokens = {};
2478
2541
  const huePlans = channelCtx !== void 0 && format === "oklch" ? buildHuePlans(resolved, channelCtx) : void 0;
2479
2542
  if (huePlans !== void 0 && channelCtx !== void 0) {
2480
- const emitDecls = channelCtx.emitDeclarations !== false;
2481
- if (emitDecls && channelCtx.mode === "theme") tokens[`$${channelCtx.baseName}-hue`] = { "": String(channelCtx.seedHue) };
2482
- for (const [name, color] of resolved) {
2483
- const plan = huePlans.get(name);
2484
- if (emitDecls) for (const decl of plan.declarations) {
2485
- const key = `$${decl.prop.slice(2)}`;
2486
- if (!(key in tokens)) tokens[key] = { "": decl.value };
2487
- }
2488
- const colorKey = `#${prefix}${name}`;
2489
- 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
+ };
2490
2550
  }
2551
+ for (const [name, color] of resolved) tokens[`#${prefix}${name}`] = buildTokenEntry(color, states, modes, format, pastel, huePlans.get(name));
2491
2552
  return tokens;
2492
2553
  }
2493
2554
  for (const [name, color] of resolved) {
@@ -2536,7 +2597,13 @@ function buildCssMap(resolved, prefix, suffix, format, pastel = false, channelCt
2536
2597
  darkContrast: []
2537
2598
  };
2538
2599
  const huePlans = channelCtx !== void 0 && format === "oklch" ? buildHuePlans(resolved, channelCtx) : void 0;
2539
- 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
+ }
2540
2607
  for (const [name, color] of resolved) {
2541
2608
  const prop = `--${prefix}${name}${suffix}`;
2542
2609
  const plan = huePlans?.get(name);
@@ -2973,6 +3040,13 @@ function validateStructuredInput(input) {
2973
3040
  if (input.saturationFactor !== void 0) {
2974
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}).`);
2975
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
+ }
2976
3050
  if (input.opacity !== void 0) validateStandaloneOpacity(input.opacity);
2977
3051
  }
2978
3052
  /**
@@ -3019,11 +3093,27 @@ function extractOkhslFromValue(value) {
3019
3093
  return value;
3020
3094
  }
3021
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
+ /**
3022
3108
  * Build the `ColorMap` for a value-shorthand `glaze.color()` call.
3023
3109
  *
3024
3110
  * The user-facing color (`STANDALONE_VALUE`) defaults to `mode: 'auto'`
3025
3111
  * across every value-shorthand form.
3026
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
+ *
3027
3117
  * When the user requests `contrast` or relative `tone`, a hidden
3028
3118
  * `STANDALONE_SEED` def is synthesized at `mode: 'static'`. That keeps
3029
3119
  * the seed pinned to the literal user-provided color across all four
@@ -3041,10 +3131,13 @@ function buildStandaloneValueDefs(main, options) {
3041
3131
  if (userName !== void 0) validateStandaloneName(userName);
3042
3132
  const primary = userName ?? STANDALONE_VALUE;
3043
3133
  const seedTone = toTone(main.l);
3134
+ const primaryTone = toneOption ?? seedTone;
3044
3135
  const valueDef = {
3045
3136
  hue: relativeHue,
3046
3137
  saturation: options?.saturationFactor,
3047
- tone: toneOption ?? seedTone,
3138
+ darkHue: options?.darkHue,
3139
+ darkSaturation: options?.darkSaturationFactor,
3140
+ tone: needsSeedAnchor ? pinExtremeTone(primaryTone) : primaryTone,
3048
3141
  contrast: options?.contrast,
3049
3142
  mode: options?.mode ?? "auto",
3050
3143
  autoFlip: options?.autoFlip,
@@ -3061,13 +3154,16 @@ function buildStandaloneValueDefs(main, options) {
3061
3154
  mode: "static"
3062
3155
  };
3063
3156
  return {
3064
- seedHue,
3065
- seedSaturation,
3157
+ seed: {
3158
+ hue: seedHue,
3159
+ saturation: seedSaturation,
3160
+ darkSaturation: options?.darkSaturation
3161
+ },
3066
3162
  defs,
3067
3163
  primary
3068
3164
  };
3069
3165
  }
3070
- function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, configOverride, baseToken, buildExport) {
3166
+ function createColorTokenFromDefs(seed, defs, primary, configOverride, baseToken, buildExport) {
3071
3167
  let cache = null;
3072
3168
  function getEffectiveConfig() {
3073
3169
  const version = getConfigVersion();
@@ -3084,7 +3180,7 @@ function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, config
3084
3180
  const version = getConfigVersion();
3085
3181
  if (cache && cache.version === version && cache.map) return cache.map;
3086
3182
  const effectiveConfig = getEffectiveConfig();
3087
- 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);
3088
3184
  cache = {
3089
3185
  map,
3090
3186
  version,
@@ -3122,7 +3218,7 @@ function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, config
3122
3218
  if (options.splitHue && format === "oklch") {
3123
3219
  assertAllPastel(renamed, resolveModes());
3124
3220
  channelCtx = {
3125
- seedHue,
3221
+ seedHue: seed.hue,
3126
3222
  baseName: options.name,
3127
3223
  prefix: "",
3128
3224
  defs: { [options.name]: defs[primary] },
@@ -3206,8 +3302,10 @@ function createColorToken(input, configOverride) {
3206
3302
  const hasExternalBase = baseToken !== void 0;
3207
3303
  const needsSeedAnchor = !hasExternalBase && input.contrast !== void 0;
3208
3304
  const defs = { [primary]: {
3209
- tone: input.tone,
3305
+ tone: needsSeedAnchor ? pinExtremeTone(input.tone) : input.tone,
3210
3306
  saturation: input.saturationFactor,
3307
+ darkHue: input.darkHue,
3308
+ darkSaturation: input.darkSaturationFactor,
3211
3309
  mode: input.mode ?? "auto",
3212
3310
  autoFlip: input.autoFlip,
3213
3311
  contrast: input.contrast,
@@ -3216,16 +3314,17 @@ function createColorToken(input, configOverride) {
3216
3314
  role: input.role,
3217
3315
  base: hasExternalBase ? STANDALONE_BASE : needsSeedAnchor ? STANDALONE_SEED : void 0
3218
3316
  } };
3219
- if (needsSeedAnchor) {
3220
- const seedTone = pairNormal(input.tone);
3221
- defs[STANDALONE_SEED] = {
3222
- tone: seedTone === "max" ? 100 : seedTone === "min" ? 0 : seedTone,
3223
- saturation: 1,
3224
- mode: "static"
3225
- };
3226
- }
3317
+ if (needsSeedAnchor) defs[STANDALONE_SEED] = {
3318
+ tone: pinExtremeTone(pairNormal(input.tone)),
3319
+ saturation: 1,
3320
+ mode: "static"
3321
+ };
3227
3322
  const localOverride = configOverride;
3228
- 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) => ({
3229
3328
  kind: "color",
3230
3329
  version: GLAZE_EXPORT_VERSION,
3231
3330
  form: "structured",
@@ -3236,9 +3335,9 @@ function createColorToken(input, configOverride) {
3236
3335
  function createColorTokenFromValue(value, options, configOverride) {
3237
3336
  const main = extractOkhslFromValue(value);
3238
3337
  const linkingBase = toLinkingBase(resolveBaseToken(options?.base));
3239
- const { seedHue, seedSaturation, defs, primary } = buildStandaloneValueDefs(main, options);
3338
+ const { seed, defs, primary } = buildStandaloneValueDefs(main, options);
3240
3339
  const localOverride = sparseValueFormLocal(configOverride);
3241
- return createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, localOverride, linkingBase, (exportArg) => ({
3340
+ return createColorTokenFromDefs(seed, defs, primary, localOverride, linkingBase, (exportArg) => ({
3242
3341
  kind: "color",
3243
3342
  version: GLAZE_EXPORT_VERSION,
3244
3343
  form: "value",
@@ -3258,6 +3357,9 @@ function buildOverridesExport(options, exportArg) {
3258
3357
  if (options.saturation !== void 0) out.saturation = options.saturation;
3259
3358
  if (options.tone !== void 0) out.tone = options.tone;
3260
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;
3261
3363
  if (options.mode !== void 0) out.mode = options.mode;
3262
3364
  if (options.autoFlip !== void 0) out.autoFlip = options.autoFlip;
3263
3365
  if (options.contrast !== void 0) out.contrast = options.contrast;
@@ -3275,6 +3377,9 @@ function buildStructuredInputExport(input, exportArg) {
3275
3377
  tone: input.tone
3276
3378
  };
3277
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;
3278
3383
  if (input.mode !== void 0) out.mode = input.mode;
3279
3384
  if (input.autoFlip !== void 0) out.autoFlip = input.autoFlip;
3280
3385
  if (input.opacity !== void 0) out.opacity = input.opacity;
@@ -3297,6 +3402,9 @@ function rehydrateOverrides(data) {
3297
3402
  if (data.saturation !== void 0) out.saturation = data.saturation;
3298
3403
  if (data.tone !== void 0) out.tone = data.tone;
3299
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;
3300
3408
  if (data.mode !== void 0) out.mode = data.mode;
3301
3409
  if (data.autoFlip !== void 0) out.autoFlip = data.autoFlip;
3302
3410
  if (data.contrast !== void 0) out.contrast = data.contrast;
@@ -3314,6 +3422,9 @@ function rehydrateStructuredInput(data) {
3314
3422
  tone: data.tone
3315
3423
  };
3316
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;
3317
3428
  if (data.mode !== void 0) out.mode = data.mode;
3318
3429
  if (data.autoFlip !== void 0) out.autoFlip = data.autoFlip;
3319
3430
  if (data.opacity !== void 0) out.opacity = data.opacity;
@@ -3347,7 +3458,8 @@ function colorFromExport(data) {
3347
3458
 
3348
3459
  //#endregion
3349
3460
  //#region src/theme.ts
3350
- function createTheme(hue, saturation, initialColors, configOverride) {
3461
+ function createTheme(seed, initialColors, configOverride) {
3462
+ const { hue, saturation, darkHue, darkSaturation } = seed;
3351
3463
  let colorDefs = initialColors ? { ...initialColors } : {};
3352
3464
  let cache = null;
3353
3465
  function getEffectiveConfig() {
@@ -3365,7 +3477,7 @@ function createTheme(hue, saturation, initialColors, configOverride) {
3365
3477
  const version = getConfigVersion();
3366
3478
  if (cache && cache.version === version && cache.map) return cache.map;
3367
3479
  const effectiveConfig = getEffectiveConfig();
3368
- const map = resolveAllColors(hue, saturation, colorDefs, effectiveConfig);
3480
+ const map = resolveAllColors(seed, colorDefs, effectiveConfig);
3369
3481
  cache = {
3370
3482
  map,
3371
3483
  version,
@@ -3382,6 +3494,7 @@ function createTheme(hue, saturation, initialColors, configOverride) {
3382
3494
  assertAllPastel(resolveCached(), resolveModes(options?.modes));
3383
3495
  return {
3384
3496
  seedHue: hue,
3497
+ darkSeedHue: darkHue,
3385
3498
  baseName: options.name ?? "theme",
3386
3499
  prefix,
3387
3500
  defs: colorDefs,
@@ -3395,6 +3508,12 @@ function createTheme(hue, saturation, initialColors, configOverride) {
3395
3508
  get saturation() {
3396
3509
  return saturation;
3397
3510
  },
3511
+ get darkHue() {
3512
+ return darkHue;
3513
+ },
3514
+ get darkSaturation() {
3515
+ return darkSaturation;
3516
+ },
3398
3517
  getConfig() {
3399
3518
  return getEffectiveConfig();
3400
3519
  },
@@ -3431,16 +3550,22 @@ function createTheme(hue, saturation, initialColors, configOverride) {
3431
3550
  version: GLAZE_EXPORT_VERSION,
3432
3551
  hue,
3433
3552
  saturation,
3553
+ ...darkHue !== void 0 ? { darkHue } : {},
3554
+ ...darkSaturation !== void 0 ? { darkSaturation } : {},
3434
3555
  colors: structuredClone(colorDefs),
3435
3556
  config: freezeConfigForExport(configOverride, override)
3436
3557
  };
3437
3558
  },
3438
3559
  extend(options) {
3439
- const newHue = options.hue ?? hue;
3440
- 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
+ };
3441
3566
  const inheritedColors = {};
3442
3567
  for (const [name, def] of Object.entries(colorDefs)) if (def.inherit !== false) inheritedColors[name] = def;
3443
- return createTheme(newHue, newSat, options.colors ? {
3568
+ return createTheme(childSeed, options.colors ? {
3444
3569
  ...inheritedColors,
3445
3570
  ...options.colors
3446
3571
  } : { ...inheritedColors }, configOverride || options.config ? {
@@ -3550,6 +3675,7 @@ function channelCtxForTheme(theme, themeName, passPrefix, themedPrefix, splitHue
3550
3675
  assertAllPastel(filtered, modes);
3551
3676
  return {
3552
3677
  seedHue: theme.hue,
3678
+ darkSeedHue: theme.darkHue,
3553
3679
  baseName: themeName,
3554
3680
  prefix: themedPrefix,
3555
3681
  defs: colorMapFromTheme(theme),
@@ -3581,7 +3707,13 @@ function createThemeFromExport(data, factory = "glaze.themeFrom") {
3581
3707
  assertExportKind(data, "theme", factory);
3582
3708
  assertExportVersion(data, factory);
3583
3709
  if (typeof data.hue !== "number" || typeof data.saturation !== "number") throw new Error(`${factory}: expected numeric "hue" and "saturation" fields.`);
3584
- 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);
3585
3717
  }
3586
3718
  /**
3587
3719
  * Rebuild a palette from a `palette.export()` snapshot.
@@ -3743,18 +3875,28 @@ function createPalette(themes, paletteOptions) {
3743
3875
  * the theme still reacts to later `configure()` calls for fields it
3744
3876
  * didn't override.
3745
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
+ *
3746
3883
  * @example
3747
3884
  * ```ts
3748
3885
  * const primary = glaze(280, 80);
3749
3886
  * // or shorthand:
3750
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 });
3751
3890
  * // with config override:
3752
3891
  * const raw = glaze(280, 80, { lightTone: false });
3753
3892
  * ```
3754
3893
  */
3755
3894
  function glaze(hueOrOptions, saturation, config) {
3756
- if (typeof hueOrOptions === "number") return createTheme(hueOrOptions, saturation ?? 100, void 0, config);
3757
- 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);
3758
3900
  }
3759
3901
  /** Configure global glaze settings. */
3760
3902
  glaze.configure = function configure$1(config) {
@@ -3868,7 +4010,10 @@ glaze.fromHex = function fromHex(hex) {
3868
4010
  const rgb = parseHex(hex);
3869
4011
  if (!rgb) throw new Error(`glaze: invalid hex color "${hex}".`);
3870
4012
  const [h, s] = srgbToOkhsl(rgb);
3871
- return createTheme(h, s * 100);
4013
+ return createTheme({
4014
+ hue: h,
4015
+ saturation: s * 100
4016
+ });
3872
4017
  };
3873
4018
  /**
3874
4019
  * Create a theme from RGB values (0–255).
@@ -3880,7 +4025,10 @@ glaze.fromRgb = function fromRgb(r, g, b) {
3880
4025
  g / 255,
3881
4026
  b / 255
3882
4027
  ]);
3883
- return createTheme(h, s * 100);
4028
+ return createTheme({
4029
+ hue: h,
4030
+ saturation: s * 100
4031
+ });
3884
4032
  };
3885
4033
  /**
3886
4034
  * Rehydrate a `glaze.color()` token from a `.export()` snapshot.