@tenphi/glaze 0.0.0-snapshot.432b23c → 0.0.0-snapshot.575cb1c

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/README.md CHANGED
@@ -194,6 +194,8 @@ A single value applies to both modes. All control is local and explicit.
194
194
  'muted': { base: 'surface', lightness: ['-35', '-50'], contrast: ['AA-large', 'AA'] }
195
195
  ```
196
196
 
197
+ **Full lightness spectrum in HC mode:** In high-contrast variants, the `lightLightness` and `darkLightness` window constraints are bypassed entirely. Colors can reach the full 0–100 lightness range, maximizing perceivable contrast. Normal (non-HC) variants continue to use the configured windows.
198
+
197
199
  ## Theme Color Management
198
200
 
199
201
  ### Adding Colors
@@ -601,7 +603,7 @@ Modes control how colors adapt across schemes:
601
603
 
602
604
  ```ts
603
605
  // Light: surface L=97, text lightness='-52' → L=45 (dark text on light bg)
604
- // Dark: surface inverts to L≈14, sign flips → L=14+52=66
606
+ // Dark: surface inverts to L≈29 (power curve), sign flips → L=29+52=81
605
607
  // contrast solver may push further (light text on dark bg)
606
608
  ```
607
609
 
@@ -625,7 +627,7 @@ const [lo, hi] = lightLightness; // default: [10, 100]
625
627
  const mappedL = (lightness * (hi - lo)) / 100 + lo;
626
628
  ```
627
629
 
628
- Both `auto` and `fixed` modes use the same linear formula. `static` mode bypasses the mapping entirely.
630
+ Both `auto` and `fixed` modes use the same linear formula. `static` mode and high-contrast variants bypass the mapping entirely (identity: `mappedL = l`).
629
631
 
630
632
  | Color | Raw L | Mapped L (default [10, 100]) |
631
633
  |---|---|---|
@@ -637,24 +639,29 @@ Both `auto` and `fixed` modes use the same linear formula. `static` mode bypasse
637
639
 
638
640
  ### Lightness
639
641
 
640
- **`auto`** — inverted within the configured window:
642
+ **`auto`** — inverted with a power curve within the configured window:
641
643
 
642
644
  ```ts
643
645
  const [lo, hi] = darkLightness; // default: [15, 95]
644
- const invertedL = ((100 - lightness) * (hi - lo)) / 100 + lo;
646
+ const d = (100 - lightness) / 100;
647
+ const invertedL = lo + (hi - lo) * Math.pow(d, darkCurve); // darkCurve default: 0.5
645
648
  ```
646
649
 
647
- **`fixed`** mapped without inversion:
650
+ The `darkCurve` exponent (default `0.5`) expands small light-theme deltas near white into larger usable deltas in dark mode. This preserves subtle surface hierarchy (e.g. L=97 vs L=95) that would otherwise collapse to near-identical dark values. Set `darkCurve: 1` for linear (legacy) behavior.
651
+
652
+ **`fixed`** — mapped without inversion (not affected by `darkCurve`):
648
653
 
649
654
  ```ts
650
655
  const mappedL = (lightness * (hi - lo)) / 100 + lo;
651
656
  ```
652
657
 
653
- | Color | Light L | Auto (inverted) | Fixed (mapped) |
654
- |---|---|---|---|
655
- | surface (L=97) | 97 | 17.4 | 92.6 |
656
- | accent-fill (L=52) | 52 | 53.4 | 56.6 |
657
- | accent-text (L=100) | 100 | 15 | 95 |
658
+ | Color | Light L | Auto (curve=0.5) | Auto (curve=1, linear) | Fixed (mapped) |
659
+ |---|---|---|---|---|
660
+ | surface (L=97) | 97 | 28.9 | 17.4 | 92.6 |
661
+ | accent-fill (L=52) | 52 | 70.4 | 53.4 | 56.6 |
662
+ | accent-text (L=100) | 100 | 15 | 15 | 95 |
663
+
664
+ In high-contrast variants, the `darkLightness` window is bypassed. Auto uses pure inversion (`100 - L`), fixed uses identity (`L`). This allows HC colors to reach the full 0–100 range.
658
665
 
659
666
  ### Saturation
660
667
 
@@ -904,9 +911,10 @@ Resolution priority (highest first):
904
911
 
905
912
  ```ts
906
913
  glaze.configure({
907
- lightLightness: [10, 100], // Light scheme lightness window [lo, hi]
908
- darkLightness: [15, 95], // Dark scheme lightness window [lo, hi]
914
+ lightLightness: [10, 100], // Light scheme lightness window [lo, hi] (bypassed in HC)
915
+ darkLightness: [15, 95], // Dark scheme lightness window [lo, hi] (bypassed in HC)
909
916
  darkDesaturation: 0.1, // Saturation reduction in dark scheme (0–1)
917
+ darkCurve: 0.5, // Power-curve exponent for dark auto-inversion (0–1)
910
918
  states: {
911
919
  dark: '@dark', // State alias for dark mode tokens
912
920
  highContrast: '@high-contrast',
package/dist/index.cjs CHANGED
@@ -470,7 +470,7 @@ function formatOklch(h, s, l) {
470
470
  const C = Math.sqrt(a * a + b * b);
471
471
  let hh = Math.atan2(b, a) * (180 / Math.PI);
472
472
  hh = constrainAngle(hh);
473
- return `oklch(${fmt$1(L, 4)} ${fmt$1(C, 4)} ${fmt$1(hh, 1)})`;
473
+ return `oklch(${fmt$1(L, 4)} ${fmt$1(C, 4)} ${fmt$1(hh, 2)})`;
474
474
  }
475
475
 
476
476
  //#endregion
@@ -620,7 +620,7 @@ function coarseScan(h, s, lo, hi, yBase, target, epsilon, maxIter) {
620
620
  function findLightnessForContrast(options) {
621
621
  const { hue, saturation, preferredLightness, baseLinearRgb, contrast: contrastInput, lightnessRange = [0, 1], epsilon = 1e-4, maxIterations = 14 } = options;
622
622
  const target = resolveMinContrast(contrastInput);
623
- const searchTarget = target * 1.005;
623
+ const searchTarget = target * 1.007;
624
624
  const yBase = gamutClampedLuminance(baseLinearRgb);
625
625
  const crPref = contrastRatioFromLuminance(cachedLuminance(hue, saturation, preferredLightness), yBase);
626
626
  if (crPref >= searchTarget) return {
@@ -744,7 +744,7 @@ function searchMixBranch(lo, hi, yBase, target, epsilon, maxIter, preferred, lum
744
744
  function findValueForMixContrast(options) {
745
745
  const { preferredValue, baseLinearRgb, contrast: contrastInput, luminanceAtValue, epsilon = 1e-4, maxIterations = 20 } = options;
746
746
  const target = resolveMinContrast(contrastInput);
747
- const searchTarget = target * 1.005;
747
+ const searchTarget = target * 1.01;
748
748
  const yBase = gamutClampedLuminance(baseLinearRgb);
749
749
  const crPref = contrastRatioFromLuminance(luminanceAtValue(preferredValue), yBase);
750
750
  if (crPref >= searchTarget) return {
@@ -814,6 +814,7 @@ let globalConfig = {
814
814
  lightLightness: [10, 100],
815
815
  darkLightness: [15, 95],
816
816
  darkDesaturation: .1,
817
+ darkCurve: .5,
817
818
  states: {
818
819
  dark: "@dark",
819
820
  highContrast: "@high-contrast"
@@ -961,23 +962,26 @@ function topoSort(defs) {
961
962
  for (const name of Object.keys(defs)) visit(name);
962
963
  return result;
963
964
  }
964
- function mapLightnessLight(l, mode) {
965
- if (mode === "static") return l;
965
+ function mapLightnessLight(l, mode, isHighContrast) {
966
+ if (mode === "static" || isHighContrast) return l;
966
967
  const [lo, hi] = globalConfig.lightLightness;
967
968
  return l * (hi - lo) / 100 + lo;
968
969
  }
969
- function mapLightnessDark(l, mode) {
970
+ function mapLightnessDark(l, mode, isHighContrast) {
970
971
  if (mode === "static") return l;
971
- const [lo, hi] = globalConfig.darkLightness;
972
- if (mode === "fixed") return l * (hi - lo) / 100 + lo;
973
- return (100 - l) * (hi - lo) / 100 + lo;
972
+ if (isHighContrast) return mode === "fixed" ? l : 100 - l;
973
+ const [darkLo, darkHi] = globalConfig.darkLightness;
974
+ if (mode === "fixed") return l * (darkHi - darkLo) / 100 + darkLo;
975
+ const [lightLo, lightHi] = globalConfig.lightLightness;
976
+ const t = (lightHi - (l * (lightHi - lightLo) / 100 + lightLo)) / (lightHi - lightLo);
977
+ return darkLo + (darkHi - darkLo) * Math.pow(t, globalConfig.darkCurve);
974
978
  }
975
979
  function mapSaturationDark(s, mode) {
976
980
  if (mode === "static") return s;
977
981
  return s * (1 - globalConfig.darkDesaturation);
978
982
  }
979
- function schemeLightnessRange(isDark, mode) {
980
- if (mode === "static") return [0, 1];
983
+ function schemeLightnessRange(isDark, mode, isHighContrast) {
984
+ if (mode === "static" || isHighContrast) return [0, 1];
981
985
  const [lo, hi] = isDark ? globalConfig.darkLightness : globalConfig.lightLightness;
982
986
  return [lo / 100, hi / 100];
983
987
  }
@@ -1040,23 +1044,23 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
1040
1044
  let delta = parsed.value;
1041
1045
  if (isDark && mode === "auto") delta = -delta;
1042
1046
  preferredL = clamp(baseL + delta, 0, 100);
1043
- } else if (isDark) preferredL = mapLightnessDark(parsed.value, mode);
1044
- else preferredL = mapLightnessLight(parsed.value, mode);
1047
+ } else if (isDark) preferredL = mapLightnessDark(parsed.value, mode, isHighContrast);
1048
+ else preferredL = mapLightnessLight(parsed.value, mode, isHighContrast);
1045
1049
  }
1046
1050
  const rawContrast = def.contrast;
1047
1051
  if (rawContrast !== void 0) {
1048
1052
  const minCr = isHighContrast ? pairHC(rawContrast) : pairNormal(rawContrast);
1049
1053
  const effectiveSat = isDark ? mapSaturationDark(satFactor * ctx.saturation / 100, mode) : satFactor * ctx.saturation / 100;
1050
1054
  const baseLinearRgb = okhslToLinearSrgb(baseVariant.h, baseVariant.s, baseVariant.l);
1051
- const lightnessRange = schemeLightnessRange(isDark, mode);
1055
+ const windowRange = schemeLightnessRange(isDark, mode, isHighContrast);
1052
1056
  return {
1053
1057
  l: findLightnessForContrast({
1054
1058
  hue: effectiveHue,
1055
1059
  saturation: effectiveSat,
1056
- preferredLightness: clamp(preferredL / 100, lightnessRange[0], lightnessRange[1]),
1060
+ preferredLightness: clamp(preferredL / 100, windowRange[0], windowRange[1]),
1057
1061
  baseLinearRgb,
1058
1062
  contrast: minCr,
1059
- lightnessRange
1063
+ lightnessRange: [0, 1]
1060
1064
  }).lightness * 100,
1061
1065
  satFactor
1062
1066
  };
@@ -1093,13 +1097,13 @@ function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
1093
1097
  let finalL;
1094
1098
  let finalSat;
1095
1099
  if (isDark && isRoot) {
1096
- finalL = mapLightnessDark(lightL, mode);
1100
+ finalL = mapLightnessDark(lightL, mode, isHighContrast);
1097
1101
  finalSat = mapSaturationDark(satFactor * ctx.saturation / 100, mode);
1098
1102
  } else if (isDark && !isRoot) {
1099
1103
  finalL = lightL;
1100
1104
  finalSat = mapSaturationDark(satFactor * ctx.saturation / 100, mode);
1101
1105
  } else if (isRoot) {
1102
- finalL = mapLightnessLight(lightL, mode);
1106
+ finalL = mapLightnessLight(lightL, mode, isHighContrast);
1103
1107
  finalSat = satFactor * ctx.saturation / 100;
1104
1108
  } else {
1105
1109
  finalL = lightL;
@@ -1565,6 +1569,7 @@ glaze.configure = function configure(config) {
1565
1569
  lightLightness: config.lightLightness ?? globalConfig.lightLightness,
1566
1570
  darkLightness: config.darkLightness ?? globalConfig.darkLightness,
1567
1571
  darkDesaturation: config.darkDesaturation ?? globalConfig.darkDesaturation,
1572
+ darkCurve: config.darkCurve ?? globalConfig.darkCurve,
1568
1573
  states: {
1569
1574
  dark: config.states?.dark ?? globalConfig.states.dark,
1570
1575
  highContrast: config.states?.highContrast ?? globalConfig.states.highContrast
@@ -1664,6 +1669,7 @@ glaze.resetConfig = function resetConfig() {
1664
1669
  lightLightness: [10, 100],
1665
1670
  darkLightness: [15, 95],
1666
1671
  darkDesaturation: .1,
1672
+ darkCurve: .5,
1667
1673
  states: {
1668
1674
  dark: "@dark",
1669
1675
  highContrast: "@high-contrast"