@tenphi/glaze 1.4.0 → 2.0.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.d.cts CHANGED
@@ -234,9 +234,10 @@ interface GlazeOutputModes {
234
234
  /**
235
235
  * Include high-contrast variants (both light-HC and dark-HC). Default: false.
236
236
  *
237
- * Inert while a global `contrastLevel` is set: a manual level has no separate
238
- * high-contrast tier, so this reads as "emit high-contrast variants when
239
- * contrast is automatic".
237
+ * Independent of `contrastLevel` — the level positions the normal variants
238
+ * while these stay the true high-contrast resolution, so the two compose. The
239
+ * exception is a global level of 100, where the normal variants already *are*
240
+ * the high-contrast ones: the tier would duplicate them and is dropped.
240
241
  * @see GlazeConfig.contrastLevel
241
242
  */
242
243
  highContrast?: boolean;
@@ -572,15 +573,15 @@ interface GlazeConfig {
572
573
  shadowTuning?: ShadowTuning;
573
574
  /**
574
575
  * Manual contrast level — a 0–100 slider from normal contrast (`0`) to high
575
- * contrast (`100`), or `'auto'` (the default) to keep the two-tier
576
- * normal + high-contrast model.
576
+ * contrast (`100`), or `'auto'` (the default) to take the authored normal
577
+ * entries as-is.
577
578
  *
578
- * With a number, Glaze resolves the `light` / `dark` variants *at* that
579
- * level and stops emitting a separate high-contrast tier:
580
- * `lightContrast` / `darkContrast` mirror their normal counterparts and
581
- * `modes.highContrast` defaults to `false`. Level `0` is therefore "normal
582
- * contrast, no high-contrast tier"; `100` is "the high-contrast scheme as
583
- * the only scheme".
579
+ * With a number, Glaze resolves the `light` / `dark` variants *at* that level.
580
+ * That is all it does: `lightContrast` / `darkContrast` stay the true
581
+ * high-contrast resolution at every level, and `modes.highContrast` alone
582
+ * decides whether they are emitted. Level `0` therefore reproduces `'auto'`
583
+ * bit for bit. At `100` the normal variants *are* the high-contrast ones, so a
584
+ * separate tier would duplicate them and a global level of 100 drops it.
584
585
  *
585
586
  * The level interpolates all three things that make high contrast differ:
586
587
  * authored `[normal, highContrast]` pairs, the tone-window widening, and
@@ -630,8 +631,8 @@ interface GlazeConfigResolved {
630
631
  shadowTuning?: ShadowTuning;
631
632
  autoFlip: boolean;
632
633
  /**
633
- * Manual contrast level (0–100), or `'auto'` for the two-tier
634
- * normal + high-contrast model.
634
+ * Manual contrast level (0–100) for the normal variants, or `'auto'` to take
635
+ * the authored normal entries as-is.
635
636
  * @see GlazeConfig.contrastLevel
636
637
  */
637
638
  contrastLevel: number | 'auto';
@@ -661,8 +662,10 @@ interface GlazeConfigOverride {
661
662
  /** Whether to auto-flip tone when contrast can't be met. */
662
663
  autoFlip?: boolean;
663
664
  /**
664
- * Manual contrast level (0–100) for this instance, or `'auto'` to opt out
665
- * of a global level and keep the two-tier normal + high-contrast model.
665
+ * Manual contrast level (0–100) for this instance, or `'auto'` to opt out of a
666
+ * global level and take the authored normal entries as-is. Either way the
667
+ * instance keeps its high-contrast tier; a per-instance level never changes
668
+ * which modes are emitted.
666
669
  *
667
670
  * Only an instance-authored level is frozen into `.export()` snapshots — a
668
671
  * level inherited from the global config is treated as a live preference and
@@ -1695,7 +1698,11 @@ declare function okhslToOkhst(c: {
1695
1698
  };
1696
1699
  /**
1697
1700
  * Edge adapter: a resolved variant stores canonical tone `t` (0–1). Convert
1698
- * it to the OKHSL `{ h, s, l }` the formatters and luminance pipeline expect.
1701
+ * it to the OKHSL `{ h, s, l }` the formatters and luminance pipeline expect —
1702
+ * `s` and `l` on the same 0–1 factor scale, so `const { h, s, l } =
1703
+ * variantToOkhsl(v)` destructures straight into `formatOkhsl` / `formatRgb` /
1704
+ * `okhslToLinearSrgb` with no rescaling. `alpha` stays on the variant; use
1705
+ * `glaze.format(v, format)` for a string that carries it.
1699
1706
  */
1700
1707
  declare function variantToOkhsl(v: {
1701
1708
  h: number;
@@ -1750,6 +1757,7 @@ declare function relativeLuminanceFromLinearRgb(rgb: [number, number, number]):
1750
1757
  declare function contrastRatioFromLuminance(yA: number, yB: number): number;
1751
1758
  /**
1752
1759
  * Convert OKHSL to gamma-encoded sRGB (clamped to 0–1).
1760
+ * h: 0–360, s: 0–1, l: 0–1. Returns [r, g, b] with each channel 0–1.
1753
1761
  */
1754
1762
  declare function okhslToSrgb(h: number, s: number, l: number, pastel?: boolean): [number, number, number];
1755
1763
  /**
@@ -1796,12 +1804,12 @@ declare function parseHexAlpha(hex: string): {
1796
1804
  } | null;
1797
1805
  /**
1798
1806
  * Format OKHSL values as a CSS `okhsl(H S% L%)` string.
1799
- * h: 0–360, s: 0–100, l: 0–100 (percentage scale for s and l).
1807
+ * h: 0–360, s: 0–1, l: 0–1 (factor scale, as returned by every converter).
1800
1808
  */
1801
1809
  declare function formatOkhsl(h: number, s: number, l: number, pastel?: boolean): string;
1802
1810
  /**
1803
1811
  * Format OKHST values as a CSS `okhst(H S% T%)` string.
1804
- * h: 0–360, s: 0–100, t: 0–100 (percentage scale for s and t).
1812
+ * h: 0–360, s: 0–1, t: 0–1 (factor scale, matching `ResolvedColorVariant`).
1805
1813
  *
1806
1814
  * Pastel recompute matches `formatOkhsl`: convert via OKLab so external
1807
1815
  * parsers that only understand non-pastel OKHST render identically.
@@ -1810,17 +1818,17 @@ declare function formatOkhst(h: number, s: number, t: number, pastel?: boolean):
1810
1818
  /**
1811
1819
  * Format OKHSL values as a CSS `rgb(R G B)` string.
1812
1820
  * Uses 2 decimal places to avoid 8-bit quantization contrast loss.
1813
- * h: 0–360, s: 0–100, l: 0–100 (percentage scale for s and l).
1821
+ * h: 0–360, s: 0–1, l: 0–1 (factor scale, as returned by every converter).
1814
1822
  */
1815
1823
  declare function formatRgb(h: number, s: number, l: number, pastel?: boolean): string;
1816
1824
  /**
1817
1825
  * Format OKHSL values as a CSS `hsl(H S% L%)` string.
1818
- * h: 0–360, s: 0–100, l: 0–100 (percentage scale for s and l).
1826
+ * h: 0–360, s: 0–1, l: 0–1 (factor scale, as returned by every converter).
1819
1827
  */
1820
1828
  declare function formatHsl(h: number, s: number, l: number, pastel?: boolean): string;
1821
1829
  /**
1822
1830
  * Format OKHSL values as a CSS `oklch(L C H)` string.
1823
- * h: 0–360, s: 0–100, l: 0–100 (percentage scale for s and l).
1831
+ * h: 0–360, s: 0–1, l: 0–1 (factor scale, as returned by every converter).
1824
1832
  */
1825
1833
  declare function formatOklch(h: number, s: number, l: number, pastel?: boolean): string;
1826
1834
  /**
package/dist/index.d.mts CHANGED
@@ -234,9 +234,10 @@ interface GlazeOutputModes {
234
234
  /**
235
235
  * Include high-contrast variants (both light-HC and dark-HC). Default: false.
236
236
  *
237
- * Inert while a global `contrastLevel` is set: a manual level has no separate
238
- * high-contrast tier, so this reads as "emit high-contrast variants when
239
- * contrast is automatic".
237
+ * Independent of `contrastLevel` — the level positions the normal variants
238
+ * while these stay the true high-contrast resolution, so the two compose. The
239
+ * exception is a global level of 100, where the normal variants already *are*
240
+ * the high-contrast ones: the tier would duplicate them and is dropped.
240
241
  * @see GlazeConfig.contrastLevel
241
242
  */
242
243
  highContrast?: boolean;
@@ -572,15 +573,15 @@ interface GlazeConfig {
572
573
  shadowTuning?: ShadowTuning;
573
574
  /**
574
575
  * Manual contrast level — a 0–100 slider from normal contrast (`0`) to high
575
- * contrast (`100`), or `'auto'` (the default) to keep the two-tier
576
- * normal + high-contrast model.
576
+ * contrast (`100`), or `'auto'` (the default) to take the authored normal
577
+ * entries as-is.
577
578
  *
578
- * With a number, Glaze resolves the `light` / `dark` variants *at* that
579
- * level and stops emitting a separate high-contrast tier:
580
- * `lightContrast` / `darkContrast` mirror their normal counterparts and
581
- * `modes.highContrast` defaults to `false`. Level `0` is therefore "normal
582
- * contrast, no high-contrast tier"; `100` is "the high-contrast scheme as
583
- * the only scheme".
579
+ * With a number, Glaze resolves the `light` / `dark` variants *at* that level.
580
+ * That is all it does: `lightContrast` / `darkContrast` stay the true
581
+ * high-contrast resolution at every level, and `modes.highContrast` alone
582
+ * decides whether they are emitted. Level `0` therefore reproduces `'auto'`
583
+ * bit for bit. At `100` the normal variants *are* the high-contrast ones, so a
584
+ * separate tier would duplicate them and a global level of 100 drops it.
584
585
  *
585
586
  * The level interpolates all three things that make high contrast differ:
586
587
  * authored `[normal, highContrast]` pairs, the tone-window widening, and
@@ -630,8 +631,8 @@ interface GlazeConfigResolved {
630
631
  shadowTuning?: ShadowTuning;
631
632
  autoFlip: boolean;
632
633
  /**
633
- * Manual contrast level (0–100), or `'auto'` for the two-tier
634
- * normal + high-contrast model.
634
+ * Manual contrast level (0–100) for the normal variants, or `'auto'` to take
635
+ * the authored normal entries as-is.
635
636
  * @see GlazeConfig.contrastLevel
636
637
  */
637
638
  contrastLevel: number | 'auto';
@@ -661,8 +662,10 @@ interface GlazeConfigOverride {
661
662
  /** Whether to auto-flip tone when contrast can't be met. */
662
663
  autoFlip?: boolean;
663
664
  /**
664
- * Manual contrast level (0–100) for this instance, or `'auto'` to opt out
665
- * of a global level and keep the two-tier normal + high-contrast model.
665
+ * Manual contrast level (0–100) for this instance, or `'auto'` to opt out of a
666
+ * global level and take the authored normal entries as-is. Either way the
667
+ * instance keeps its high-contrast tier; a per-instance level never changes
668
+ * which modes are emitted.
666
669
  *
667
670
  * Only an instance-authored level is frozen into `.export()` snapshots — a
668
671
  * level inherited from the global config is treated as a live preference and
@@ -1695,7 +1698,11 @@ declare function okhslToOkhst(c: {
1695
1698
  };
1696
1699
  /**
1697
1700
  * Edge adapter: a resolved variant stores canonical tone `t` (0–1). Convert
1698
- * it to the OKHSL `{ h, s, l }` the formatters and luminance pipeline expect.
1701
+ * it to the OKHSL `{ h, s, l }` the formatters and luminance pipeline expect —
1702
+ * `s` and `l` on the same 0–1 factor scale, so `const { h, s, l } =
1703
+ * variantToOkhsl(v)` destructures straight into `formatOkhsl` / `formatRgb` /
1704
+ * `okhslToLinearSrgb` with no rescaling. `alpha` stays on the variant; use
1705
+ * `glaze.format(v, format)` for a string that carries it.
1699
1706
  */
1700
1707
  declare function variantToOkhsl(v: {
1701
1708
  h: number;
@@ -1750,6 +1757,7 @@ declare function relativeLuminanceFromLinearRgb(rgb: [number, number, number]):
1750
1757
  declare function contrastRatioFromLuminance(yA: number, yB: number): number;
1751
1758
  /**
1752
1759
  * Convert OKHSL to gamma-encoded sRGB (clamped to 0–1).
1760
+ * h: 0–360, s: 0–1, l: 0–1. Returns [r, g, b] with each channel 0–1.
1753
1761
  */
1754
1762
  declare function okhslToSrgb(h: number, s: number, l: number, pastel?: boolean): [number, number, number];
1755
1763
  /**
@@ -1796,12 +1804,12 @@ declare function parseHexAlpha(hex: string): {
1796
1804
  } | null;
1797
1805
  /**
1798
1806
  * Format OKHSL values as a CSS `okhsl(H S% L%)` string.
1799
- * h: 0–360, s: 0–100, l: 0–100 (percentage scale for s and l).
1807
+ * h: 0–360, s: 0–1, l: 0–1 (factor scale, as returned by every converter).
1800
1808
  */
1801
1809
  declare function formatOkhsl(h: number, s: number, l: number, pastel?: boolean): string;
1802
1810
  /**
1803
1811
  * Format OKHST values as a CSS `okhst(H S% T%)` string.
1804
- * h: 0–360, s: 0–100, t: 0–100 (percentage scale for s and t).
1812
+ * h: 0–360, s: 0–1, t: 0–1 (factor scale, matching `ResolvedColorVariant`).
1805
1813
  *
1806
1814
  * Pastel recompute matches `formatOkhsl`: convert via OKLab so external
1807
1815
  * parsers that only understand non-pastel OKHST render identically.
@@ -1810,17 +1818,17 @@ declare function formatOkhst(h: number, s: number, t: number, pastel?: boolean):
1810
1818
  /**
1811
1819
  * Format OKHSL values as a CSS `rgb(R G B)` string.
1812
1820
  * Uses 2 decimal places to avoid 8-bit quantization contrast loss.
1813
- * h: 0–360, s: 0–100, l: 0–100 (percentage scale for s and l).
1821
+ * h: 0–360, s: 0–1, l: 0–1 (factor scale, as returned by every converter).
1814
1822
  */
1815
1823
  declare function formatRgb(h: number, s: number, l: number, pastel?: boolean): string;
1816
1824
  /**
1817
1825
  * Format OKHSL values as a CSS `hsl(H S% L%)` string.
1818
- * h: 0–360, s: 0–100, l: 0–100 (percentage scale for s and l).
1826
+ * h: 0–360, s: 0–1, l: 0–1 (factor scale, as returned by every converter).
1819
1827
  */
1820
1828
  declare function formatHsl(h: number, s: number, l: number, pastel?: boolean): string;
1821
1829
  /**
1822
1830
  * Format OKHSL values as a CSS `oklch(L C H)` string.
1823
- * h: 0–360, s: 0–100, l: 0–100 (percentage scale for s and l).
1831
+ * h: 0–360, s: 0–1, l: 0–1 (factor scale, as returned by every converter).
1824
1832
  */
1825
1833
  declare function formatOklch(h: number, s: number, l: number, pastel?: boolean): string;
1826
1834
  /**
package/dist/index.mjs CHANGED
@@ -370,6 +370,7 @@ const sRGBGammaToLinear = (val) => {
370
370
  };
371
371
  /**
372
372
  * Convert OKHSL to gamma-encoded sRGB (clamped to 0–1).
373
+ * h: 0–360, s: 0–1, l: 0–1. Returns [r, g, b] with each channel 0–1.
373
374
  */
374
375
  function okhslToSrgb(h, s, l, pastel = false) {
375
376
  const lin = okhslToLinearSrgb(h, s, l, pastel);
@@ -579,47 +580,72 @@ function fmt$1(value, decimals) {
579
580
  return parseFloat(value.toFixed(decimals)).toString();
580
581
  }
581
582
  /**
583
+ * Every `format*` writer takes `s` / `l` / `t` on the **0–1 factor scale** —
584
+ * the same scale every producer in the library hands back (`resolve()`,
585
+ * `variantToOkhsl`, `srgbToOkhsl`, `oklabToOkhsl`, `okhslToOklab`,
586
+ * `okhslToSrgb`). The percentages are an output detail: the writers multiply
587
+ * by 100 themselves when the CSS syntax asks for a percentage.
588
+ *
589
+ * Before 2.0 the writers took 0–100 while everything producing those values
590
+ * returned 0–1, so the obvious composition was off by 100x and failed
591
+ * silently — `0.7` is a legal percentage, so the result was a valid CSS string
592
+ * naming a near-black color. A value above 1 can only be that old
593
+ * percentage-scale input, so warn about it loudly rather than emit a wrong
594
+ * color quietly.
595
+ */
596
+ const scaleWarnCache = /* @__PURE__ */ new Set();
597
+ function warnPercentScale(fn, ...values) {
598
+ if (!values.some((v) => v > 1.000001)) return;
599
+ if (!scaleWarnCache.has(fn)) {
600
+ scaleWarnCache.add(fn);
601
+ console.warn(`glaze: ${fn}() got a value above 1 — it takes the 0-1 factors every Glaze producer returns (resolve(), variantToOkhsl, srgbToOkhsl), not 0-100 percentages. Divide by 100, or drop the "* 100" left over from the pre-2.0 signature.`);
602
+ }
603
+ }
604
+ /**
582
605
  * Format OKHSL values as a CSS `okhsl(H S% L%)` string.
583
- * h: 0–360, s: 0–100, l: 0–100 (percentage scale for s and l).
606
+ * h: 0–360, s: 0–1, l: 0–1 (factor scale, as returned by every converter).
584
607
  */
585
608
  function formatOkhsl(h, s, l, pastel = false) {
609
+ warnPercentScale("formatOkhsl", s, l);
586
610
  let outS = s;
587
- if (pastel) outS = oklabToOkhsl(okhslToOklab(h, s / 100, l / 100, true), false)[1] * 100;
588
- return `okhsl(${fmt$1(h, 2)} ${fmt$1(outS, 2)}% ${fmt$1(l, 2)}%)`;
611
+ if (pastel) outS = oklabToOkhsl(okhslToOklab(h, s, l, true), false)[1];
612
+ return `okhsl(${fmt$1(h, 2)} ${fmt$1(outS * 100, 2)}% ${fmt$1(l * 100, 2)}%)`;
589
613
  }
590
614
  /**
591
615
  * Format OKHST values as a CSS `okhst(H S% T%)` string.
592
- * h: 0–360, s: 0–100, t: 0–100 (percentage scale for s and t).
616
+ * h: 0–360, s: 0–1, t: 0–1 (factor scale, matching `ResolvedColorVariant`).
593
617
  *
594
618
  * Pastel recompute matches `formatOkhsl`: convert via OKLab so external
595
619
  * parsers that only understand non-pastel OKHST render identically.
596
620
  */
597
621
  function formatOkhst(h, s, t, pastel = false) {
622
+ warnPercentScale("formatOkhst", s, t);
598
623
  let outS = s;
599
624
  if (pastel) {
600
625
  const REF_EPS = .05;
601
626
  const den = Math.log(1 + REF_EPS) - Math.log(REF_EPS);
602
- const y = Math.exp(t / 100 * den + Math.log(REF_EPS)) - REF_EPS;
603
- const l = toe(Math.cbrt(Math.max(0, y)));
604
- outS = oklabToOkhsl(okhslToOklab(h, s / 100, l, true), false)[1] * 100;
627
+ const y = Math.exp(t * den + Math.log(REF_EPS)) - REF_EPS;
628
+ outS = oklabToOkhsl(okhslToOklab(h, s, toe(Math.cbrt(Math.max(0, y))), true), false)[1];
605
629
  }
606
- return `okhst(${fmt$1(h, 2)} ${fmt$1(outS, 2)}% ${fmt$1(t, 2)}%)`;
630
+ return `okhst(${fmt$1(h, 2)} ${fmt$1(outS * 100, 2)}% ${fmt$1(t * 100, 2)}%)`;
607
631
  }
608
632
  /**
609
633
  * Format OKHSL values as a CSS `rgb(R G B)` string.
610
634
  * Uses 2 decimal places to avoid 8-bit quantization contrast loss.
611
- * h: 0–360, s: 0–100, l: 0–100 (percentage scale for s and l).
635
+ * h: 0–360, s: 0–1, l: 0–1 (factor scale, as returned by every converter).
612
636
  */
613
637
  function formatRgb(h, s, l, pastel = false) {
614
- const [r, g, b] = okhslToSrgb(h, s / 100, l / 100, pastel);
638
+ warnPercentScale("formatRgb", s, l);
639
+ const [r, g, b] = okhslToSrgb(h, s, l, pastel);
615
640
  return `rgb(${parseFloat((r * 255).toFixed(2))} ${parseFloat((g * 255).toFixed(2))} ${parseFloat((b * 255).toFixed(2))})`;
616
641
  }
617
642
  /**
618
643
  * Format OKHSL values as a CSS `hsl(H S% L%)` string.
619
- * h: 0–360, s: 0–100, l: 0–100 (percentage scale for s and l).
644
+ * h: 0–360, s: 0–1, l: 0–1 (factor scale, as returned by every converter).
620
645
  */
621
646
  function formatHsl(h, s, l, pastel = false) {
622
- const [r, g, b] = okhslToSrgb(h, s / 100, l / 100, pastel);
647
+ warnPercentScale("formatHsl", s, l);
648
+ const [r, g, b] = okhslToSrgb(h, s, l, pastel);
623
649
  const max = Math.max(r, g, b);
624
650
  const min = Math.min(r, g, b);
625
651
  const delta = max - min;
@@ -636,10 +662,11 @@ function formatHsl(h, s, l, pastel = false) {
636
662
  }
637
663
  /**
638
664
  * Format OKHSL values as a CSS `oklch(L C H)` string.
639
- * h: 0–360, s: 0–100, l: 0–100 (percentage scale for s and l).
665
+ * h: 0–360, s: 0–1, l: 0–1 (factor scale, as returned by every converter).
640
666
  */
641
667
  function formatOklch(h, s, l, pastel = false) {
642
- const [L, C, hh] = okhslToOklch(h, s / 100, l / 100, pastel);
668
+ warnPercentScale("formatOklch", s, l);
669
+ const [L, C, hh] = okhslToOklch(h, s, l, pastel);
643
670
  return `oklch(${fmt$1(L, 4)} ${fmt$1(C, 4)} ${fmt$1(hh, 2)})`;
644
671
  }
645
672
  /**
@@ -904,10 +931,14 @@ function levelFraction(level) {
904
931
  }
905
932
  /**
906
933
  * Manual-contrast blend fraction (0–1) for a resolved config, or `undefined`
907
- * in `'auto'` mode (the two-tier normal + high-contrast model).
934
+ * in `'auto'` mode, where the normal variants take the authored normal entries
935
+ * as-is.
908
936
  *
909
- * Note `contrastLevel: 0` yields `0`, not `undefined`: "pinned to normal
910
- * contrast with no high-contrast tier" is a distinct state from `'auto'`.
937
+ * Note `contrastLevel: 0` yields `0`, not `undefined`. The two select different
938
+ * branches, and an authored `0` is a slider position that `.export()` freezes
939
+ * where an absent level is not — but they are not an *output* distinction: at
940
+ * fraction 0 every interpolation returns its normal entry verbatim, so level 0
941
+ * reproduces `'auto'` bit for bit.
911
942
  */
912
943
  function contrastFraction(config) {
913
944
  const level = config.contrastLevel;
@@ -915,6 +946,20 @@ function contrastFraction(config) {
915
946
  return levelFraction(level);
916
947
  }
917
948
  /**
949
+ * Whether the high-contrast variants are exact duplicates of the normal ones —
950
+ * true only at `contrastLevel: 100`, where the normal passes already resolve at
951
+ * full contrast.
952
+ *
953
+ * The level otherwise says nothing about high contrast: it positions the normal
954
+ * variants, while `lightContrast` / `darkContrast` stay the true high-contrast
955
+ * resolution at every level. This predicate marks the one level where that tier
956
+ * carries no new information, so callers can skip the two high-contrast passes,
957
+ * their drift checks, and the redundant output tier.
958
+ */
959
+ function hcMirrorsNormal(config) {
960
+ return contrastFraction(config) === 1;
961
+ }
962
+ /**
918
963
  * A numeric HC pair at a blend fraction (shadow `intensity`, mix `value`).
919
964
  *
920
965
  * The endpoints return the authored entry *by identity* rather than through
@@ -1099,7 +1144,11 @@ function okhslToOkhst(c) {
1099
1144
  }
1100
1145
  /**
1101
1146
  * Edge adapter: a resolved variant stores canonical tone `t` (0–1). Convert
1102
- * it to the OKHSL `{ h, s, l }` the formatters and luminance pipeline expect.
1147
+ * it to the OKHSL `{ h, s, l }` the formatters and luminance pipeline expect —
1148
+ * `s` and `l` on the same 0–1 factor scale, so `const { h, s, l } =
1149
+ * variantToOkhsl(v)` destructures straight into `formatOkhsl` / `formatRgb` /
1150
+ * `okhslToLinearSrgb` with no rescaling. `alpha` stays on the variant; use
1151
+ * `glaze.format(v, format)` for a string that carries it.
1103
1152
  */
1104
1153
  function variantToOkhsl(v) {
1105
1154
  return {
@@ -2274,12 +2323,14 @@ function warnContrastDrift(name, isDark, isHighContrast, contrast, yColor, yBase
2274
2323
  * Color resolution engine.
2275
2324
  *
2276
2325
  * Runs the four-pass solver (light → light-HC → dark → dark-HC) that
2277
- * turns a `ColorMap` into a fully resolved `ResolvedColor` per name — or,
2278
- * under a manual `contrastLevel`, a two-pass solver (light → dark) whose
2279
- * authored HC pairs, tone window, and contrast targets are interpolated at
2280
- * that level and whose high-contrast slots mirror the normal ones.
2281
- * Owns the per-scheme resolve helpers for regular, shadow, and mix
2282
- * color defs.
2326
+ * turns a `ColorMap` into a fully resolved `ResolvedColor` per name. A manual
2327
+ * `contrastLevel` interpolates the authored HC pairs, tone window, and contrast
2328
+ * targets at that level for the two *normal* passes only; the high-contrast
2329
+ * passes bypass the level entirely, so the high-contrast slots are the same
2330
+ * values `'auto'` resolves. At level 100 the normal passes already are the
2331
+ * high-contrast ones, so the solver drops to two passes (light → dark) and
2332
+ * mirrors the slots. Owns the per-scheme resolve helpers for regular, shadow,
2333
+ * and mix color defs.
2283
2334
  *
2284
2335
  * Variants are stored in OKHST: `h` / `s` are OKHSL hue/saturation and
2285
2336
  * `t` is the canonical contrast-uniform tone (0–1, reference eps). The
@@ -2536,7 +2587,7 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, channels,
2536
2587
  flip,
2537
2588
  pastel
2538
2589
  };
2539
- const level = contrastFraction(ctx.config);
2590
+ const level = isHighContrast ? void 0 : contrastFraction(config);
2540
2591
  let preferInitial = false;
2541
2592
  if (level !== void 0 && level > 0 && level < 1) {
2542
2593
  initialDirection = findToneForContrast({
@@ -2725,10 +2776,11 @@ function seedField(order, ctx, field, source) {
2725
2776
  * resolved with a `base` + `contrast` may land slightly under the contrast
2726
2777
  * its tone implies because chromatic luminance drifts from the gray tone.
2727
2778
  *
2728
- * Under a manual `contrastLevel` only the two emitted variants are checked
2729
- * (the high-contrast slots are mirrors), and the spec is resolved at the level
2730
- * so the check measures the output against the target it was actually solved
2731
- * for.
2779
+ * Under a manual `contrastLevel` the spec is resolved at the level, so the check
2780
+ * measures each normal variant against the target it was actually solved for. At
2781
+ * level 100 the high-contrast slots are mirrors and are skipped: the normal
2782
+ * schemes already carry that check, and repeating it would emit a byte-identical
2783
+ * advisory under a different scheme label, which dedupes as a separate key.
2732
2784
  */
2733
2785
  function verifyContrastDrift(order, defs, result, config) {
2734
2786
  const roles = /* @__PURE__ */ new Map();
@@ -2763,9 +2815,9 @@ function verifyContrastDrift(order, defs, result, config) {
2763
2815
  field: "darkContrast"
2764
2816
  }
2765
2817
  ];
2766
- const manual = contrastFraction(config) !== void 0;
2818
+ const mirrored = hcMirrorsNormal(config);
2767
2819
  for (const s of schemes) {
2768
- if (manual && s.isHighContrast) continue;
2820
+ if (mirrored && s.isHighContrast) continue;
2769
2821
  const spec = resolveContrastSpec(regDef.contrast, s.isHighContrast, config, polarity);
2770
2822
  const cVariant = color[s.field];
2771
2823
  const bVariant = base[s.field];
@@ -2790,10 +2842,10 @@ function resolveAllColors(seed, defs, config, externalBases) {
2790
2842
  roles: /* @__PURE__ */ new Map()
2791
2843
  };
2792
2844
  if (externalBases) for (const [name, color] of externalBases) ctx.resolved.set(name, color);
2793
- const manual = contrastFraction(config) !== void 0;
2845
+ const mirrored = hcMirrorsNormal(config);
2794
2846
  const lightMap = runPass(order, defs, ctx, false, false, "light");
2795
2847
  let lightHCMap = lightMap;
2796
- if (!manual) {
2848
+ if (!mirrored) {
2797
2849
  seedField(order, ctx, "lightContrast", lightMap);
2798
2850
  lightHCMap = runPass(order, defs, ctx, false, true, "lightContrast");
2799
2851
  }
@@ -2801,7 +2853,7 @@ function resolveAllColors(seed, defs, config, externalBases) {
2801
2853
  seedField(order, ctx, "darkContrast", lightHCMap);
2802
2854
  const darkMap = runPass(order, defs, ctx, true, false, "dark");
2803
2855
  let darkHCMap = darkMap;
2804
- if (!manual) {
2856
+ if (!mirrored) {
2805
2857
  seedField(order, ctx, "darkContrast", darkMap);
2806
2858
  darkHCMap = runPass(order, defs, ctx, true, true, "darkContrast");
2807
2859
  }
@@ -3003,10 +3055,10 @@ function fmt(value, decimals) {
3003
3055
  function formatVariant(v, format = "oklch", pastel = false) {
3004
3056
  const effectivePastel = v.pastel ?? pastel;
3005
3057
  let base;
3006
- if (format === "okhst") base = formatOkhst(v.h, v.s * 100, v.t * 100, effectivePastel);
3058
+ if (format === "okhst") base = formatOkhst(v.h, v.s, v.t, effectivePastel);
3007
3059
  else {
3008
3060
  const { l } = variantToOkhsl(v);
3009
- base = formatters[format](v.h, v.s * 100, l * 100, effectivePastel);
3061
+ base = formatters[format](v.h, v.s, l, effectivePastel);
3010
3062
  }
3011
3063
  if (v.alpha >= 1) return base;
3012
3064
  const closing = base.lastIndexOf(")");
@@ -3022,7 +3074,7 @@ function formatVariantHue(v, plan, pastel = false) {
3022
3074
  const [L, C] = okhslToOklch(v.h, v.s, l, effectivePastel);
3023
3075
  let base;
3024
3076
  if (plan.inline) if (v.s <= 1e-6) base = `oklch(${fmt(L, 4)} 0 0)`;
3025
- else base = formatOklch(v.h, v.s * 100, l * 100, effectivePastel);
3077
+ else base = formatOklch(v.h, v.s, l, effectivePastel);
3026
3078
  else base = `oklch(${fmt(L, 4)} ${fmt(C, 4)} ${plan.hueVar})`;
3027
3079
  if (v.alpha >= 1) return base;
3028
3080
  const closing = base.lastIndexOf(")");
@@ -3035,23 +3087,28 @@ function formatColorValue(v, format, pastel, huePlan) {
3035
3087
  /**
3036
3088
  * Resolve the effective output modes for an export.
3037
3089
  *
3038
- * A global manual `contrastLevel` turns off high-contrast output outright: the
3039
- * level *is* the contrast preference, so a separate high-contrast tier has no
3040
- * meaning alongside it. `modes.highContrast` therefore reads as "emit a separate
3041
- * high-contrast set **when** contrast is automatic" — it goes inert under a
3042
- * manual level rather than fighting it, and silently, since switching a
3043
- * preference from auto to manual is normal use and not a mistake to report.
3090
+ * `modes.highContrast` and `contrastLevel` are independent: the level positions
3091
+ * the normal variants, the high-contrast tier stays the true high-contrast
3092
+ * resolution, and the two compose — a slider raises the baseline while a
3093
+ * `prefers-contrast: more` block still escalates on top of it.
3094
+ *
3095
+ * The one exception is a **global** level of 100, where the normal variants
3096
+ * already *are* the high-contrast ones. The tier would be an exact duplicate, so
3097
+ * it is dropped and a single light/dark set is emitted — including against an
3098
+ * explicit `highContrast: true` override, which has nothing different left to
3099
+ * ask for.
3044
3100
  *
3045
3101
  * A level set on a single theme or token does not change which modes are
3046
- * emitted: sibling themes in a palette may still have a real high-contrast
3047
- * tier, and the manual one correctly reports its own resolved values there —
3048
- * the alternative would drop its colors from that tier entirely.
3102
+ * emitted: `modes` is global-only, and a palette must not have one sibling
3103
+ * collapse the shared token structure. A per-instance level of 100 therefore
3104
+ * still reports into the tier, with values equal to its own normal ones.
3049
3105
  */
3050
3106
  function resolveModes(override) {
3051
3107
  const cfg = getConfig();
3108
+ const highContrast = override?.highContrast ?? cfg.modes.highContrast;
3052
3109
  return {
3053
3110
  dark: override?.dark ?? cfg.modes.dark,
3054
- highContrast: contrastFraction(cfg) !== void 0 ? false : override?.highContrast ?? cfg.modes.highContrast
3111
+ highContrast: highContrast && !hcMirrorsNormal(cfg)
3055
3112
  };
3056
3113
  }
3057
3114
  function buildTokenMap(resolved, prefix, states, modes, format = "oklch", pastel = false, channelCtx) {