@tenphi/glaze 0.8.0 → 0.9.1

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
@@ -70,8 +70,8 @@ const danger = primary.extend({ hue: 23 });
70
70
  const success = primary.extend({ hue: 157 });
71
71
 
72
72
  // Compose into a palette and export
73
- const palette = glaze.palette({ primary, danger, success });
74
- const tokens = palette.tokens({ primary: 'primary' });
73
+ const palette = glaze.palette({ primary, danger, success }, { primary: 'primary' });
74
+ const tokens = palette.tokens();
75
75
  // → { light: { 'primary-surface': 'okhsl(...)', 'surface': 'okhsl(...)', ... }, dark: { ... } }
76
76
  ```
77
77
 
@@ -647,7 +647,7 @@ const t = (100 - lightness) / 100;
647
647
  const invertedL = lo + (hi - lo) * t / (t + darkCurve * (1 - t)); // darkCurve default: 0.5
648
648
  ```
649
649
 
650
- The `darkCurve` parameter (default `0.5`, range 0–1) controls how much the dark-mode inversion expands lightness deltas. Lower values produce stronger expansion; `1` gives linear (legacy) behavior. Unlike a power curve, the Möbius transformation provides **proportional expansion** — small and large deltas are scaled by similar ratios, preserving the visual hierarchy of the light theme.
650
+ The `darkCurve` parameter (default `0.5`, range 0–1) controls how much the dark-mode inversion expands lightness deltas. Lower values produce stronger expansion; `1` gives linear (legacy) behavior. Accepts a `[normal, highContrast]` pair for separate HC tuning (e.g. `darkCurve: [0.5, 0.3]`); a single number applies to both. Unlike a power curve, the Möbius transformation provides **proportional expansion** — small and large deltas are scaled by similar ratios, preserving the visual hierarchy of the light theme.
651
651
 
652
652
  **`fixed`** — mapped without inversion (not affected by `darkCurve`):
653
653
 
@@ -661,7 +661,7 @@ const mappedL = (lightness * (hi - lo)) / 100 + lo;
661
661
  | accent-fill (L=52) | 52 | 66.9 | 53.4 | 56.6 |
662
662
  | accent-text (L=100) | 100 | 15 | 15 | 95 |
663
663
 
664
- In high-contrast variants, the `darkLightness` window is bypassed. Auto uses the same Möbius curve over the full [0, 100] range. Fixed uses identity (`L`). This allows HC colors to reach the full 0–100 range.
664
+ In high-contrast variants, the `darkLightness` window is bypassed auto uses the Möbius curve over the full [0, 100] range, and fixed uses identity (`L`). To use a different curve shape for HC, pass a `[normal, hc]` pair to `darkCurve` (e.g. `darkCurve: [0.5, 0.3]`).
665
665
 
666
666
  ### Saturation
667
667
 
@@ -701,6 +701,15 @@ Combine multiple themes into a single palette:
701
701
  const palette = glaze.palette({ primary, danger, success, warning });
702
702
  ```
703
703
 
704
+ Optionally designate a primary theme at creation time:
705
+
706
+ ```ts
707
+ const palette = glaze.palette(
708
+ { primary, danger, success, warning },
709
+ { primary: 'primary' },
710
+ );
711
+ ```
712
+
704
713
  ### Prefix Behavior
705
714
 
706
715
  Palette export methods (`tokens()`, `tasty()`, `css()`) default to `prefix: true` — all tokens are automatically prefixed with the theme name to avoid collisions:
@@ -719,15 +728,28 @@ Custom prefix mapping:
719
728
  palette.tokens({ prefix: { primary: 'brand-', danger: 'error-' } });
720
729
  ```
721
730
 
722
- To disable prefixing entirely, pass `prefix: false` explicitly. Note that tokens with the same name will overwrite each other (last theme wins).
731
+ To disable prefixing entirely, pass `prefix: false` explicitly.
732
+
733
+ ### Collision Detection
734
+
735
+ When two themes produce the same output key (via `prefix: false`, custom prefix maps, or primary unprefixed aliases), the first-written value wins and a `console.warn` is emitted:
736
+
737
+ ```ts
738
+ const palette = glaze.palette({ a, b });
739
+ palette.tokens({ prefix: false });
740
+ // ⚠ glaze: token "surface" from theme "b" collides with theme "a" — skipping.
741
+ ```
723
742
 
724
743
  ### Primary Theme
725
744
 
726
- 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:
745
+ The primary theme's tokens are duplicated without prefix, providing convenient short aliases alongside the prefixed versions. Set at palette creation to apply to all exports automatically:
727
746
 
728
747
  ```ts
729
- const palette = glaze.palette({ primary, danger, success });
730
- const tokens = palette.tokens({ primary: 'primary' });
748
+ const palette = glaze.palette(
749
+ { primary, danger, success },
750
+ { primary: 'primary' },
751
+ );
752
+ const tokens = palette.tokens();
731
753
  // → {
732
754
  // light: {
733
755
  // 'primary-surface': 'okhsl(...)', // prefixed (all themes)
@@ -738,11 +760,18 @@ const tokens = palette.tokens({ primary: 'primary' });
738
760
  // }
739
761
  ```
740
762
 
763
+ Override or disable per-export:
764
+
765
+ ```ts
766
+ palette.tokens({ primary: 'danger' }); // use danger as primary for this call
767
+ palette.tokens({ primary: false }); // no primary for this call
768
+ ```
769
+
741
770
  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:
742
771
 
743
772
  ```ts
744
- palette.tokens({ prefix: { primary: 'p-', danger: 'd-' }, primary: 'primary' });
745
- // → 'p-surface' + 'surface' (alias) + 'd-surface'
773
+ palette.tokens({ prefix: { primary: 'p-', danger: 'd-' } });
774
+ // → 'p-surface' + 'surface' (alias from palette-level primary) + 'd-surface'
746
775
  ```
747
776
 
748
777
  An error is thrown if the primary name doesn't match any theme in the palette.
@@ -752,7 +781,11 @@ An error is thrown if the primary name doesn't match any theme in the palette.
752
781
  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.):
753
782
 
754
783
  ```ts
755
- const tastyTokens = palette.tasty({ primary: 'primary' });
784
+ const palette = glaze.palette(
785
+ { primary, danger, success },
786
+ { primary: 'primary' },
787
+ );
788
+ const tastyTokens = palette.tasty();
756
789
  // → {
757
790
  // '#primary-surface': { '': 'okhsl(...)', '@dark': 'okhsl(...)' },
758
791
  // '#danger-surface': { '': 'okhsl(...)', '@dark': 'okhsl(...)' },
@@ -848,7 +881,11 @@ const css = theme.css();
848
881
  Use in a stylesheet:
849
882
 
850
883
  ```ts
851
- const css = palette.css({ primary: 'primary' });
884
+ const palette = glaze.palette(
885
+ { primary, danger, success },
886
+ { primary: 'primary' },
887
+ );
888
+ const css = palette.css();
852
889
 
853
890
  const stylesheet = `
854
891
  :root { ${css.light} }
@@ -865,7 +902,7 @@ Options:
865
902
  | `format` | `'rgb'` | Color format (`'rgb'`, `'hsl'`, `'okhsl'`, `'oklch'`) |
866
903
  | `suffix` | `'-color'` | Suffix appended to each CSS property name |
867
904
  | `prefix` | `true` (palette) | (palette only) `true` uses `"<themeName>-"`, or provide a custom map |
868
- | `primary` | | (palette only) Theme name to duplicate without prefix |
905
+ | `primary` | inherited | (palette only) Override or disable (`false`) the palette-level primary for this call |
869
906
 
870
907
  ```ts
871
908
  // Custom suffix
@@ -876,8 +913,8 @@ theme.css({ suffix: '' });
876
913
  theme.css({ format: 'hsl' });
877
914
  // → "--surface-color: hsl(...);"
878
915
 
879
- // Palette with primary
880
- palette.css({ primary: 'primary' });
916
+ // Palette with primary (inherited from palette creation)
917
+ palette.css();
881
918
  // → "--primary-surface-color: rgb(...);\n--surface-color: rgb(...);\n--danger-surface-color: rgb(...);"
882
919
  ```
883
920
 
@@ -914,7 +951,7 @@ glaze.configure({
914
951
  lightLightness: [10, 100], // Light scheme lightness window [lo, hi] (bypassed in HC)
915
952
  darkLightness: [15, 95], // Dark scheme lightness window [lo, hi] (bypassed in HC)
916
953
  darkDesaturation: 0.1, // Saturation reduction in dark scheme (0–1)
917
- darkCurve: 0.5, // Möbius beta for dark auto-inversion (0–1, lower = more expansion)
954
+ darkCurve: 0.5, // Möbius beta for dark auto-inversion (0–1); or [normal, hc] pair
918
955
  states: {
919
956
  dark: '@dark', // State alias for dark mode tokens
920
957
  highContrast: '@high-contrast',
@@ -1049,17 +1086,20 @@ const success = primary.extend({ hue: 157 });
1049
1086
  const warning = primary.extend({ hue: 84 });
1050
1087
  const note = primary.extend({ hue: 302 });
1051
1088
 
1052
- const palette = glaze.palette({ primary, danger, success, warning, note });
1089
+ const palette = glaze.palette(
1090
+ { primary, danger, success, warning, note },
1091
+ { primary: 'primary' },
1092
+ );
1053
1093
 
1054
1094
  // Export as flat token map grouped by variant (prefix defaults to true)
1055
- const tokens = palette.tokens({ primary: 'primary' });
1095
+ const tokens = palette.tokens();
1056
1096
  // tokens.light → { 'primary-surface': '...', 'surface': '...', 'danger-surface': '...' }
1057
1097
 
1058
1098
  // Export as tasty style-to-state bindings (for Tasty style system)
1059
- const tastyTokens = palette.tasty({ primary: 'primary' });
1099
+ const tastyTokens = palette.tasty();
1060
1100
 
1061
1101
  // Export as CSS custom properties (rgb format by default)
1062
- const css = palette.css({ primary: 'primary' });
1102
+ const css = palette.css();
1063
1103
  // css.light → "--primary-surface-color: rgb(...);\n--surface-color: rgb(...);\n--danger-surface-color: rgb(...);"
1064
1104
 
1065
1105
  // Standalone shadow computation
@@ -1115,7 +1155,7 @@ brand.colors({ surface: { lightness: 97 }, text: { base: 'surface', lightness: '
1115
1155
  | Method | Description |
1116
1156
  |---|---|
1117
1157
  | `glaze.configure(config)` | Set global configuration |
1118
- | `glaze.palette(themes)` | Compose themes into a palette |
1158
+ | `glaze.palette(themes, options?)` | Compose themes into a palette (options: `{ primary? }`) |
1119
1159
  | `glaze.getConfig()` | Get current global config |
1120
1160
  | `glaze.resetConfig()` | Reset to defaults |
1121
1161
 
package/dist/index.cjs CHANGED
@@ -962,9 +962,13 @@ function topoSort(defs) {
962
962
  for (const name of Object.keys(defs)) visit(name);
963
963
  return result;
964
964
  }
965
+ function lightnessWindow(isHighContrast, kind) {
966
+ if (isHighContrast) return [0, 100];
967
+ return kind === "dark" ? globalConfig.darkLightness : globalConfig.lightLightness;
968
+ }
965
969
  function mapLightnessLight(l, mode, isHighContrast) {
966
- if (mode === "static" || isHighContrast) return l;
967
- const [lo, hi] = globalConfig.lightLightness;
970
+ if (mode === "static") return l;
971
+ const [lo, hi] = lightnessWindow(isHighContrast, "light");
968
972
  return l * (hi - lo) / 100 + lo;
969
973
  }
970
974
  function mobiusCurve(t, beta) {
@@ -973,22 +977,17 @@ function mobiusCurve(t, beta) {
973
977
  }
974
978
  function mapLightnessDark(l, mode, isHighContrast) {
975
979
  if (mode === "static") return l;
976
- const beta = globalConfig.darkCurve;
977
- if (isHighContrast) {
978
- if (mode === "fixed") return l;
979
- return 100 * mobiusCurve((100 - l) / 100, beta);
980
- }
981
- const [darkLo, darkHi] = globalConfig.darkLightness;
980
+ const beta = isHighContrast ? pairHC(globalConfig.darkCurve) : pairNormal(globalConfig.darkCurve);
981
+ const [darkLo, darkHi] = lightnessWindow(isHighContrast, "dark");
982
982
  if (mode === "fixed") return l * (darkHi - darkLo) / 100 + darkLo;
983
- const [lightLo, lightHi] = globalConfig.lightLightness;
983
+ const [lightLo, lightHi] = lightnessWindow(isHighContrast, "light");
984
984
  const t = (lightHi - (l * (lightHi - lightLo) / 100 + lightLo)) / (lightHi - lightLo);
985
985
  return darkLo + (darkHi - darkLo) * mobiusCurve(t, beta);
986
986
  }
987
987
  function lightMappedToDark(lightL, isHighContrast) {
988
- const beta = globalConfig.darkCurve;
989
- if (isHighContrast) return 100 * mobiusCurve((100 - lightL) / 100, beta);
990
- const [lightLo, lightHi] = globalConfig.lightLightness;
991
- const [darkLo, darkHi] = globalConfig.darkLightness;
988
+ const beta = isHighContrast ? pairHC(globalConfig.darkCurve) : pairNormal(globalConfig.darkCurve);
989
+ const [lightLo, lightHi] = lightnessWindow(isHighContrast, "light");
990
+ const [darkLo, darkHi] = lightnessWindow(isHighContrast, "dark");
992
991
  const t = (lightHi - clamp(lightL, lightLo, lightHi)) / (lightHi - lightLo);
993
992
  return darkLo + (darkHi - darkLo) * mobiusCurve(t, beta);
994
993
  }
@@ -997,8 +996,8 @@ function mapSaturationDark(s, mode) {
997
996
  return s * (1 - globalConfig.darkDesaturation);
998
997
  }
999
998
  function schemeLightnessRange(isDark, mode, isHighContrast) {
1000
- if (mode === "static" || isHighContrast) return [0, 1];
1001
- const [lo, hi] = isDark ? globalConfig.darkLightness : globalConfig.lightLightness;
999
+ if (mode === "static") return [0, 1];
1000
+ const [lo, hi] = lightnessWindow(isHighContrast, isDark ? "dark" : "light");
1002
1001
  return [lo / 100, hi / 100];
1003
1002
  }
1004
1003
  function clamp(v, min, max) {
@@ -1453,40 +1452,74 @@ function validatePrimaryTheme(primary, themes) {
1453
1452
  throw new Error(`glaze: primary theme "${primary}" not found in palette. Available: ${available}.`);
1454
1453
  }
1455
1454
  }
1456
- function createPalette(themes) {
1455
+ /**
1456
+ * Resolve the effective primary for an export call.
1457
+ * `false` disables, a string overrides, `undefined` inherits from palette.
1458
+ */
1459
+ function resolveEffectivePrimary(exportPrimary, palettePrimary) {
1460
+ if (exportPrimary === false) return void 0;
1461
+ return exportPrimary ?? palettePrimary;
1462
+ }
1463
+ /**
1464
+ * Filter a resolved color map, skipping keys already in `seen`.
1465
+ * Warns on collision and keeps the first-written value (first-write-wins).
1466
+ * Returns a new map containing only non-colliding entries.
1467
+ */
1468
+ function filterCollisions(resolved, prefix, seen, themeName, isPrimary) {
1469
+ const filtered = /* @__PURE__ */ new Map();
1470
+ const label = isPrimary ? `${themeName} (primary)` : themeName;
1471
+ for (const [name, color] of resolved) {
1472
+ const key = `${prefix}${name}`;
1473
+ if (seen.has(key)) {
1474
+ console.warn(`glaze: token "${key}" from theme "${label}" collides with theme "${seen.get(key)}" — skipping.`);
1475
+ continue;
1476
+ }
1477
+ seen.set(key, label);
1478
+ filtered.set(name, color);
1479
+ }
1480
+ return filtered;
1481
+ }
1482
+ function createPalette(themes, paletteOptions) {
1483
+ validatePrimaryTheme(paletteOptions?.primary, themes);
1457
1484
  return {
1458
1485
  tokens(options) {
1459
- validatePrimaryTheme(options?.primary, themes);
1486
+ const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
1487
+ if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
1460
1488
  const modes = resolveModes(options?.modes);
1461
1489
  const allTokens = {};
1490
+ const seen = /* @__PURE__ */ new Map();
1462
1491
  for (const [themeName, theme] of Object.entries(themes)) {
1463
1492
  const resolved = theme.resolve();
1464
- const tokens = buildFlatTokenMap(resolved, resolvePrefix(options, themeName, true), modes, options?.format);
1493
+ const prefix = resolvePrefix(options, themeName, true);
1494
+ const tokens = buildFlatTokenMap(filterCollisions(resolved, prefix, seen, themeName), prefix, modes, options?.format);
1465
1495
  for (const variant of Object.keys(tokens)) {
1466
1496
  if (!allTokens[variant]) allTokens[variant] = {};
1467
1497
  Object.assign(allTokens[variant], tokens[variant]);
1468
1498
  }
1469
- if (themeName === options?.primary) {
1470
- const unprefixed = buildFlatTokenMap(resolved, "", modes, options?.format);
1499
+ if (themeName === effectivePrimary) {
1500
+ const unprefixed = buildFlatTokenMap(filterCollisions(resolved, "", seen, themeName, true), "", modes, options?.format);
1471
1501
  for (const variant of Object.keys(unprefixed)) Object.assign(allTokens[variant], unprefixed[variant]);
1472
1502
  }
1473
1503
  }
1474
1504
  return allTokens;
1475
1505
  },
1476
1506
  tasty(options) {
1477
- validatePrimaryTheme(options?.primary, themes);
1507
+ const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
1508
+ if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
1478
1509
  const states = {
1479
1510
  dark: options?.states?.dark ?? globalConfig.states.dark,
1480
1511
  highContrast: options?.states?.highContrast ?? globalConfig.states.highContrast
1481
1512
  };
1482
1513
  const modes = resolveModes(options?.modes);
1483
1514
  const allTokens = {};
1515
+ const seen = /* @__PURE__ */ new Map();
1484
1516
  for (const [themeName, theme] of Object.entries(themes)) {
1485
1517
  const resolved = theme.resolve();
1486
- const tokens = buildTokenMap(resolved, resolvePrefix(options, themeName, true), states, modes, options?.format);
1518
+ const prefix = resolvePrefix(options, themeName, true);
1519
+ const tokens = buildTokenMap(filterCollisions(resolved, prefix, seen, themeName), prefix, states, modes, options?.format);
1487
1520
  Object.assign(allTokens, tokens);
1488
- if (themeName === options?.primary) {
1489
- const unprefixed = buildTokenMap(resolved, "", states, modes, options?.format);
1521
+ if (themeName === effectivePrimary) {
1522
+ const unprefixed = buildTokenMap(filterCollisions(resolved, "", seen, themeName, true), "", states, modes, options?.format);
1490
1523
  Object.assign(allTokens, unprefixed);
1491
1524
  }
1492
1525
  }
@@ -1499,7 +1532,8 @@ function createPalette(themes) {
1499
1532
  return result;
1500
1533
  },
1501
1534
  css(options) {
1502
- validatePrimaryTheme(options?.primary, themes);
1535
+ const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
1536
+ if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
1503
1537
  const suffix = options?.suffix ?? "-color";
1504
1538
  const format = options?.format ?? "rgb";
1505
1539
  const allLines = {
@@ -1508,17 +1542,19 @@ function createPalette(themes) {
1508
1542
  lightContrast: [],
1509
1543
  darkContrast: []
1510
1544
  };
1545
+ const seen = /* @__PURE__ */ new Map();
1511
1546
  for (const [themeName, theme] of Object.entries(themes)) {
1512
1547
  const resolved = theme.resolve();
1513
- const css = buildCssMap(resolved, resolvePrefix(options, themeName, true), suffix, format);
1548
+ const prefix = resolvePrefix(options, themeName, true);
1549
+ const css = buildCssMap(filterCollisions(resolved, prefix, seen, themeName), prefix, suffix, format);
1514
1550
  for (const key of [
1515
1551
  "light",
1516
1552
  "dark",
1517
1553
  "lightContrast",
1518
1554
  "darkContrast"
1519
1555
  ]) if (css[key]) allLines[key].push(css[key]);
1520
- if (themeName === options?.primary) {
1521
- const unprefixed = buildCssMap(resolved, "", suffix, format);
1556
+ if (themeName === effectivePrimary) {
1557
+ const unprefixed = buildCssMap(filterCollisions(resolved, "", seen, themeName, true), "", suffix, format);
1522
1558
  for (const key of [
1523
1559
  "light",
1524
1560
  "dark",
@@ -1600,8 +1636,8 @@ glaze.configure = function configure(config) {
1600
1636
  /**
1601
1637
  * Compose multiple themes into a palette.
1602
1638
  */
1603
- glaze.palette = function palette(themes) {
1604
- return createPalette(themes);
1639
+ glaze.palette = function palette(themes, options) {
1640
+ return createPalette(themes, options);
1605
1641
  };
1606
1642
  /**
1607
1643
  * Create a theme from a serialized export.