@tenphi/glaze 0.0.0-snapshot.bccc213 → 0.0.0-snapshot.c8281e2

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
@@ -71,8 +71,8 @@ const success = primary.extend({ hue: 157 });
71
71
 
72
72
  // Compose into a palette and export
73
73
  const palette = glaze.palette({ primary, danger, success });
74
- const tokens = palette.tokens({ prefix: true });
75
- // → { light: { 'primary-surface': 'okhsl(...)', ... }, dark: { 'primary-surface': 'okhsl(...)', ... } }
74
+ const tokens = palette.tokens({ primary: 'primary' });
75
+ // → { light: { 'primary-surface': 'okhsl(...)', 'surface': 'okhsl(...)', ... }, dark: { ... } }
76
76
  ```
77
77
 
78
78
  ## Core Concepts
@@ -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
@@ -618,14 +620,14 @@ Modes control how colors adapt across schemes:
618
620
 
619
621
  ### Lightness
620
622
 
621
- Root color lightness is mapped linearly within the configured `lightLightness` window:
623
+ Absolute lightness values (both root colors and dependent colors with absolute lightness) are mapped linearly within the configured `lightLightness` window:
622
624
 
623
625
  ```ts
624
626
  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
  |---|---|---|
@@ -656,6 +658,8 @@ const mappedL = (lightness * (hi - lo)) / 100 + lo;
656
658
  | accent-fill (L=52) | 52 | 53.4 | 56.6 |
657
659
  | accent-text (L=100) | 100 | 15 | 95 |
658
660
 
661
+ 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.
662
+
659
663
  ### Saturation
660
664
 
661
665
  `darkDesaturation` reduces saturation for all colors in dark scheme:
@@ -694,12 +698,12 @@ Combine multiple themes into a single palette:
694
698
  const palette = glaze.palette({ primary, danger, success, warning });
695
699
  ```
696
700
 
697
- ### Token Export
701
+ ### Prefix Behavior
698
702
 
699
- Tokens are grouped by scheme variant, with plain color names as keys:
703
+ Palette export methods (`tokens()`, `tasty()`, `css()`) default to `prefix: true` — all tokens are automatically prefixed with the theme name to avoid collisions:
700
704
 
701
705
  ```ts
702
- const tokens = palette.tokens({ prefix: true });
706
+ const tokens = palette.tokens();
703
707
  // → {
704
708
  // light: { 'primary-surface': 'okhsl(...)', 'danger-surface': 'okhsl(...)' },
705
709
  // dark: { 'primary-surface': 'okhsl(...)', 'danger-surface': 'okhsl(...)' },
@@ -712,15 +716,44 @@ Custom prefix mapping:
712
716
  palette.tokens({ prefix: { primary: 'brand-', danger: 'error-' } });
713
717
  ```
714
718
 
719
+ To disable prefixing entirely, pass `prefix: false` explicitly. Note that tokens with the same name will overwrite each other (last theme wins).
720
+
721
+ ### Primary Theme
722
+
723
+ Use the `primary` option to designate one theme as the primary. Its tokens are duplicated without prefix, providing convenient short aliases alongside the prefixed versions:
724
+
725
+ ```ts
726
+ const palette = glaze.palette({ primary, danger, success });
727
+ const tokens = palette.tokens({ primary: 'primary' });
728
+ // → {
729
+ // light: {
730
+ // 'primary-surface': 'okhsl(...)', // prefixed (all themes)
731
+ // 'danger-surface': 'okhsl(...)',
732
+ // 'success-surface': 'okhsl(...)',
733
+ // 'surface': 'okhsl(...)', // unprefixed alias (primary only)
734
+ // },
735
+ // }
736
+ ```
737
+
738
+ The `primary` option works on `tokens()`, `tasty()`, and `css()`. It combines with any prefix mode — when using a custom prefix map, primary tokens are still duplicated without prefix:
739
+
740
+ ```ts
741
+ palette.tokens({ prefix: { primary: 'p-', danger: 'd-' }, primary: 'primary' });
742
+ // → 'p-surface' + 'surface' (alias) + 'd-surface'
743
+ ```
744
+
745
+ An error is thrown if the primary name doesn't match any theme in the palette.
746
+
715
747
  ### Tasty Export (for [Tasty](https://cube-ui-kit.vercel.app/?path=/docs/tasty-documentation--docs) style system)
716
748
 
717
749
  The `tasty()` method exports tokens in the [Tasty](https://cube-ui-kit.vercel.app/?path=/docs/tasty-documentation--docs) style-to-state binding format — `#name` color token keys with state aliases (`''`, `@dark`, etc.):
718
750
 
719
751
  ```ts
720
- const tastyTokens = palette.tasty({ prefix: true });
752
+ const tastyTokens = palette.tasty({ primary: 'primary' });
721
753
  // → {
722
754
  // '#primary-surface': { '': 'okhsl(...)', '@dark': 'okhsl(...)' },
723
755
  // '#danger-surface': { '': 'okhsl(...)', '@dark': 'okhsl(...)' },
756
+ // '#surface': { '': 'okhsl(...)', '@dark': 'okhsl(...)' }, // alias
724
757
  // }
725
758
  ```
726
759
 
@@ -787,8 +820,10 @@ palette.tasty({ states: { dark: '@dark', highContrast: '@hc' } });
787
820
 
788
821
  ### JSON Export (Framework-Agnostic)
789
822
 
823
+ JSON export groups by theme name (no prefix needed):
824
+
790
825
  ```ts
791
- const data = palette.json({ prefix: true });
826
+ const data = palette.json();
792
827
  // → {
793
828
  // primary: { surface: { light: 'okhsl(...)', dark: 'okhsl(...)' } },
794
829
  // danger: { surface: { light: 'okhsl(...)', dark: 'okhsl(...)' } },
@@ -810,7 +845,7 @@ const css = theme.css();
810
845
  Use in a stylesheet:
811
846
 
812
847
  ```ts
813
- const css = palette.css({ prefix: true });
848
+ const css = palette.css({ primary: 'primary' });
814
849
 
815
850
  const stylesheet = `
816
851
  :root { ${css.light} }
@@ -826,7 +861,8 @@ Options:
826
861
  |---|---|---|
827
862
  | `format` | `'rgb'` | Color format (`'rgb'`, `'hsl'`, `'okhsl'`, `'oklch'`) |
828
863
  | `suffix` | `'-color'` | Suffix appended to each CSS property name |
829
- | `prefix` | | (palette only) Same prefix behavior as `tokens()` |
864
+ | `prefix` | `true` (palette) | (palette only) `true` uses `"<themeName>-"`, or provide a custom map |
865
+ | `primary` | — | (palette only) Theme name to duplicate without prefix |
830
866
 
831
867
  ```ts
832
868
  // Custom suffix
@@ -837,9 +873,9 @@ theme.css({ suffix: '' });
837
873
  theme.css({ format: 'hsl' });
838
874
  // → "--surface-color: hsl(...);"
839
875
 
840
- // Palette with prefix
841
- palette.css({ prefix: true });
842
- // → "--primary-surface-color: rgb(...);\n--danger-surface-color: rgb(...);"
876
+ // Palette with primary
877
+ palette.css({ primary: 'primary' });
878
+ // → "--primary-surface-color: rgb(...);\n--surface-color: rgb(...);\n--danger-surface-color: rgb(...);"
843
879
  ```
844
880
 
845
881
  ## Output Modes
@@ -872,8 +908,8 @@ Resolution priority (highest first):
872
908
 
873
909
  ```ts
874
910
  glaze.configure({
875
- lightLightness: [10, 100], // Light scheme lightness window [lo, hi]
876
- darkLightness: [15, 95], // Dark scheme lightness window [lo, hi]
911
+ lightLightness: [10, 100], // Light scheme lightness window [lo, hi] (bypassed in HC)
912
+ darkLightness: [15, 95], // Dark scheme lightness window [lo, hi] (bypassed in HC)
877
913
  darkDesaturation: 0.1, // Saturation reduction in dark scheme (0–1)
878
914
  states: {
879
915
  dark: '@dark', // State alias for dark mode tokens
@@ -1011,16 +1047,16 @@ const note = primary.extend({ hue: 302 });
1011
1047
 
1012
1048
  const palette = glaze.palette({ primary, danger, success, warning, note });
1013
1049
 
1014
- // Export as flat token map grouped by variant
1015
- const tokens = palette.tokens({ prefix: true });
1016
- // tokens.light → { 'primary-surface': 'okhsl(...)', 'primary-shadow-md': 'okhsl(... / 0.1)' }
1050
+ // Export as flat token map grouped by variant (prefix defaults to true)
1051
+ const tokens = palette.tokens({ primary: 'primary' });
1052
+ // tokens.light → { 'primary-surface': '...', 'surface': '...', 'danger-surface': '...' }
1017
1053
 
1018
1054
  // Export as tasty style-to-state bindings (for Tasty style system)
1019
- const tastyTokens = palette.tasty({ prefix: true });
1055
+ const tastyTokens = palette.tasty({ primary: 'primary' });
1020
1056
 
1021
1057
  // Export as CSS custom properties (rgb format by default)
1022
- const css = palette.css({ prefix: true });
1023
- // css.light → "--primary-surface-color: rgb(...);\n--primary-shadow-md-color: rgb(... / 0.1);"
1058
+ const css = palette.css({ primary: 'primary' });
1059
+ // css.light → "--primary-surface-color: rgb(...);\n--surface-color: rgb(...);\n--danger-surface-color: rgb(...);"
1024
1060
 
1025
1061
  // Standalone shadow computation
1026
1062
  const v = glaze.shadow({ bg: '#f0eef5', fg: '#1a1a2e', intensity: 10 });
package/dist/index.cjs CHANGED
@@ -433,12 +433,13 @@ function formatOkhsl(h, s, l) {
433
433
  return `okhsl(${fmt$1(h, 2)} ${fmt$1(s, 2)}% ${fmt$1(l, 2)}%)`;
434
434
  }
435
435
  /**
436
- * Format OKHSL values as a CSS `rgb(R G B)` string with rounded integer values.
436
+ * Format OKHSL values as a CSS `rgb(R G B)` string.
437
+ * Uses 2 decimal places to avoid 8-bit quantization contrast loss.
437
438
  * h: 0–360, s: 0–100, l: 0–100 (percentage scale for s and l).
438
439
  */
439
440
  function formatRgb(h, s, l) {
440
441
  const [r, g, b] = okhslToSrgb(h, s / 100, l / 100);
441
- return `rgb(${Math.round(r * 255)} ${Math.round(g * 255)} ${Math.round(b * 255)})`;
442
+ return `rgb(${parseFloat((r * 255).toFixed(2))} ${parseFloat((g * 255).toFixed(2))} ${parseFloat((b * 255).toFixed(2))})`;
442
443
  }
443
444
  /**
444
445
  * Format OKHSL values as a CSS `hsl(H S% L%)` string.
@@ -469,7 +470,7 @@ function formatOklch(h, s, l) {
469
470
  const C = Math.sqrt(a * a + b * b);
470
471
  let hh = Math.atan2(b, a) * (180 / Math.PI);
471
472
  hh = constrainAngle(hh);
472
- 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)})`;
473
474
  }
474
475
 
475
476
  //#endregion
@@ -619,7 +620,7 @@ function coarseScan(h, s, lo, hi, yBase, target, epsilon, maxIter) {
619
620
  function findLightnessForContrast(options) {
620
621
  const { hue, saturation, preferredLightness, baseLinearRgb, contrast: contrastInput, lightnessRange = [0, 1], epsilon = 1e-4, maxIterations = 14 } = options;
621
622
  const target = resolveMinContrast(contrastInput);
622
- const searchTarget = target + .02;
623
+ const searchTarget = target * 1.007;
623
624
  const yBase = gamutClampedLuminance(baseLinearRgb);
624
625
  const crPref = contrastRatioFromLuminance(cachedLuminance(hue, saturation, preferredLightness), yBase);
625
626
  if (crPref >= searchTarget) return {
@@ -743,7 +744,7 @@ function searchMixBranch(lo, hi, yBase, target, epsilon, maxIter, preferred, lum
743
744
  function findValueForMixContrast(options) {
744
745
  const { preferredValue, baseLinearRgb, contrast: contrastInput, luminanceAtValue, epsilon = 1e-4, maxIterations = 20 } = options;
745
746
  const target = resolveMinContrast(contrastInput);
746
- const searchTarget = target + .02;
747
+ const searchTarget = target * 1.01;
747
748
  const yBase = gamutClampedLuminance(baseLinearRgb);
748
749
  const crPref = contrastRatioFromLuminance(luminanceAtValue(preferredValue), yBase);
749
750
  if (crPref >= searchTarget) return {
@@ -960,13 +961,14 @@ function topoSort(defs) {
960
961
  for (const name of Object.keys(defs)) visit(name);
961
962
  return result;
962
963
  }
963
- function mapLightnessLight(l, mode) {
964
- if (mode === "static") return l;
964
+ function mapLightnessLight(l, mode, isHighContrast) {
965
+ if (mode === "static" || isHighContrast) return l;
965
966
  const [lo, hi] = globalConfig.lightLightness;
966
967
  return l * (hi - lo) / 100 + lo;
967
968
  }
968
- function mapLightnessDark(l, mode) {
969
+ function mapLightnessDark(l, mode, isHighContrast) {
969
970
  if (mode === "static") return l;
971
+ if (isHighContrast) return mode === "fixed" ? l : 100 - l;
970
972
  const [lo, hi] = globalConfig.darkLightness;
971
973
  if (mode === "fixed") return l * (hi - lo) / 100 + lo;
972
974
  return (100 - l) * (hi - lo) / 100 + lo;
@@ -975,6 +977,11 @@ function mapSaturationDark(s, mode) {
975
977
  if (mode === "static") return s;
976
978
  return s * (1 - globalConfig.darkDesaturation);
977
979
  }
980
+ function schemeLightnessRange(isDark, mode, isHighContrast) {
981
+ if (mode === "static" || isHighContrast) return [0, 1];
982
+ const [lo, hi] = isDark ? globalConfig.darkLightness : globalConfig.lightLightness;
983
+ return [lo / 100, hi / 100];
984
+ }
978
985
  function clamp(v, min, max) {
979
986
  return Math.max(min, Math.min(max, v));
980
987
  }
@@ -1034,21 +1041,23 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
1034
1041
  let delta = parsed.value;
1035
1042
  if (isDark && mode === "auto") delta = -delta;
1036
1043
  preferredL = clamp(baseL + delta, 0, 100);
1037
- } else if (isDark) preferredL = mapLightnessDark(parsed.value, mode);
1038
- else preferredL = clamp(parsed.value, 0, 100);
1044
+ } else if (isDark) preferredL = mapLightnessDark(parsed.value, mode, isHighContrast);
1045
+ else preferredL = mapLightnessLight(parsed.value, mode, isHighContrast);
1039
1046
  }
1040
1047
  const rawContrast = def.contrast;
1041
1048
  if (rawContrast !== void 0) {
1042
1049
  const minCr = isHighContrast ? pairHC(rawContrast) : pairNormal(rawContrast);
1043
1050
  const effectiveSat = isDark ? mapSaturationDark(satFactor * ctx.saturation / 100, mode) : satFactor * ctx.saturation / 100;
1044
1051
  const baseLinearRgb = okhslToLinearSrgb(baseVariant.h, baseVariant.s, baseVariant.l);
1052
+ const lightnessRange = schemeLightnessRange(isDark, mode, isHighContrast);
1045
1053
  return {
1046
1054
  l: findLightnessForContrast({
1047
1055
  hue: effectiveHue,
1048
1056
  saturation: effectiveSat,
1049
- preferredLightness: preferredL / 100,
1057
+ preferredLightness: clamp(preferredL / 100, lightnessRange[0], lightnessRange[1]),
1050
1058
  baseLinearRgb,
1051
- contrast: minCr
1059
+ contrast: minCr,
1060
+ lightnessRange
1052
1061
  }).lightness * 100,
1053
1062
  satFactor
1054
1063
  };
@@ -1085,13 +1094,13 @@ function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
1085
1094
  let finalL;
1086
1095
  let finalSat;
1087
1096
  if (isDark && isRoot) {
1088
- finalL = mapLightnessDark(lightL, mode);
1097
+ finalL = mapLightnessDark(lightL, mode, isHighContrast);
1089
1098
  finalSat = mapSaturationDark(satFactor * ctx.saturation / 100, mode);
1090
1099
  } else if (isDark && !isRoot) {
1091
1100
  finalL = lightL;
1092
1101
  finalSat = mapSaturationDark(satFactor * ctx.saturation / 100, mode);
1093
1102
  } else if (isRoot) {
1094
- finalL = mapLightnessLight(lightL, mode);
1103
+ finalL = mapLightnessLight(lightL, mode, isHighContrast);
1095
1104
  finalSat = satFactor * ctx.saturation / 100;
1096
1105
  } else {
1097
1106
  finalL = lightL;
@@ -1413,26 +1422,40 @@ function createTheme(hue, saturation, initialColors) {
1413
1422
  }
1414
1423
  };
1415
1424
  }
1416
- function resolvePrefix(options, themeName) {
1417
- if (options?.prefix === true) return `${themeName}-`;
1418
- if (typeof options?.prefix === "object" && options.prefix !== null) return options.prefix[themeName] ?? `${themeName}-`;
1425
+ function resolvePrefix(options, themeName, defaultPrefix = false) {
1426
+ const prefix = options?.prefix ?? defaultPrefix;
1427
+ if (prefix === true) return `${themeName}-`;
1428
+ if (typeof prefix === "object" && prefix !== null) return prefix[themeName] ?? `${themeName}-`;
1419
1429
  return "";
1420
1430
  }
1431
+ function validatePrimaryTheme(primary, themes) {
1432
+ if (primary !== void 0 && !(primary in themes)) {
1433
+ const available = Object.keys(themes).join(", ");
1434
+ throw new Error(`glaze: primary theme "${primary}" not found in palette. Available: ${available}.`);
1435
+ }
1436
+ }
1421
1437
  function createPalette(themes) {
1422
1438
  return {
1423
1439
  tokens(options) {
1440
+ validatePrimaryTheme(options?.primary, themes);
1424
1441
  const modes = resolveModes(options?.modes);
1425
1442
  const allTokens = {};
1426
1443
  for (const [themeName, theme] of Object.entries(themes)) {
1427
- const tokens = buildFlatTokenMap(theme.resolve(), resolvePrefix(options, themeName), modes, options?.format);
1444
+ const resolved = theme.resolve();
1445
+ const tokens = buildFlatTokenMap(resolved, resolvePrefix(options, themeName, true), modes, options?.format);
1428
1446
  for (const variant of Object.keys(tokens)) {
1429
1447
  if (!allTokens[variant]) allTokens[variant] = {};
1430
1448
  Object.assign(allTokens[variant], tokens[variant]);
1431
1449
  }
1450
+ if (themeName === options?.primary) {
1451
+ const unprefixed = buildFlatTokenMap(resolved, "", modes, options?.format);
1452
+ for (const variant of Object.keys(unprefixed)) Object.assign(allTokens[variant], unprefixed[variant]);
1453
+ }
1432
1454
  }
1433
1455
  return allTokens;
1434
1456
  },
1435
1457
  tasty(options) {
1458
+ validatePrimaryTheme(options?.primary, themes);
1436
1459
  const states = {
1437
1460
  dark: options?.states?.dark ?? globalConfig.states.dark,
1438
1461
  highContrast: options?.states?.highContrast ?? globalConfig.states.highContrast
@@ -1440,8 +1463,13 @@ function createPalette(themes) {
1440
1463
  const modes = resolveModes(options?.modes);
1441
1464
  const allTokens = {};
1442
1465
  for (const [themeName, theme] of Object.entries(themes)) {
1443
- const tokens = buildTokenMap(theme.resolve(), resolvePrefix(options, themeName), states, modes, options?.format);
1466
+ const resolved = theme.resolve();
1467
+ const tokens = buildTokenMap(resolved, resolvePrefix(options, themeName, true), states, modes, options?.format);
1444
1468
  Object.assign(allTokens, tokens);
1469
+ if (themeName === options?.primary) {
1470
+ const unprefixed = buildTokenMap(resolved, "", states, modes, options?.format);
1471
+ Object.assign(allTokens, unprefixed);
1472
+ }
1445
1473
  }
1446
1474
  return allTokens;
1447
1475
  },
@@ -1452,6 +1480,7 @@ function createPalette(themes) {
1452
1480
  return result;
1453
1481
  },
1454
1482
  css(options) {
1483
+ validatePrimaryTheme(options?.primary, themes);
1455
1484
  const suffix = options?.suffix ?? "-color";
1456
1485
  const format = options?.format ?? "rgb";
1457
1486
  const allLines = {
@@ -1461,13 +1490,23 @@ function createPalette(themes) {
1461
1490
  darkContrast: []
1462
1491
  };
1463
1492
  for (const [themeName, theme] of Object.entries(themes)) {
1464
- const css = buildCssMap(theme.resolve(), resolvePrefix(options, themeName), suffix, format);
1493
+ const resolved = theme.resolve();
1494
+ const css = buildCssMap(resolved, resolvePrefix(options, themeName, true), suffix, format);
1465
1495
  for (const key of [
1466
1496
  "light",
1467
1497
  "dark",
1468
1498
  "lightContrast",
1469
1499
  "darkContrast"
1470
1500
  ]) if (css[key]) allLines[key].push(css[key]);
1501
+ if (themeName === options?.primary) {
1502
+ const unprefixed = buildCssMap(resolved, "", suffix, format);
1503
+ for (const key of [
1504
+ "light",
1505
+ "dark",
1506
+ "lightContrast",
1507
+ "darkContrast"
1508
+ ]) if (unprefixed[key]) allLines[key].push(unprefixed[key]);
1509
+ }
1471
1510
  }
1472
1511
  return {
1473
1512
  light: allLines.light.join("\n"),