@tenphi/glaze 0.0.0-snapshot.75f81fa → 0.0.0-snapshot.78261ef

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
 
@@ -390,6 +390,7 @@ Available tuning parameters:
390
390
  | `minGapTarget` | 0.05 | Target minimum gap between pigment and bg lightness |
391
391
  | `alphaMax` | 0.6 | Asymptotic maximum alpha |
392
392
  | `bgHueBlend` | 0.2 | Blend weight pulling pigment hue toward bg hue |
393
+ | `darkShadowCurve` | 0.4 | Power curve for dark-scheme alpha (0-1). Lower = more dampening; 1 = no dampening |
393
394
 
394
395
  ### Standalone Shadow Computation
395
396
 
@@ -647,7 +648,7 @@ const t = (100 - lightness) / 100;
647
648
  const invertedL = lo + (hi - lo) * t / (t + darkCurve * (1 - t)); // darkCurve default: 0.5
648
649
  ```
649
650
 
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.
651
+ 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
652
 
652
653
  **`fixed`** — mapped without inversion (not affected by `darkCurve`):
653
654
 
@@ -661,7 +662,7 @@ const mappedL = (lightness * (hi - lo)) / 100 + lo;
661
662
  | accent-fill (L=52) | 52 | 66.9 | 53.4 | 56.6 |
662
663
  | accent-text (L=100) | 100 | 15 | 15 | 95 |
663
664
 
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.
665
+ 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
666
 
666
667
  ### Saturation
667
668
 
@@ -701,6 +702,15 @@ Combine multiple themes into a single palette:
701
702
  const palette = glaze.palette({ primary, danger, success, warning });
702
703
  ```
703
704
 
705
+ Optionally designate a primary theme at creation time:
706
+
707
+ ```ts
708
+ const palette = glaze.palette(
709
+ { primary, danger, success, warning },
710
+ { primary: 'primary' },
711
+ );
712
+ ```
713
+
704
714
  ### Prefix Behavior
705
715
 
706
716
  Palette export methods (`tokens()`, `tasty()`, `css()`) default to `prefix: true` — all tokens are automatically prefixed with the theme name to avoid collisions:
@@ -719,15 +729,28 @@ Custom prefix mapping:
719
729
  palette.tokens({ prefix: { primary: 'brand-', danger: 'error-' } });
720
730
  ```
721
731
 
722
- To disable prefixing entirely, pass `prefix: false` explicitly. Note that tokens with the same name will overwrite each other (last theme wins).
732
+ To disable prefixing entirely, pass `prefix: false` explicitly.
733
+
734
+ ### Collision Detection
735
+
736
+ 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:
737
+
738
+ ```ts
739
+ const palette = glaze.palette({ a, b });
740
+ palette.tokens({ prefix: false });
741
+ // ⚠ glaze: token "surface" from theme "b" collides with theme "a" — skipping.
742
+ ```
723
743
 
724
744
  ### Primary Theme
725
745
 
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:
746
+ 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
747
 
728
748
  ```ts
729
- const palette = glaze.palette({ primary, danger, success });
730
- const tokens = palette.tokens({ primary: 'primary' });
749
+ const palette = glaze.palette(
750
+ { primary, danger, success },
751
+ { primary: 'primary' },
752
+ );
753
+ const tokens = palette.tokens();
731
754
  // → {
732
755
  // light: {
733
756
  // 'primary-surface': 'okhsl(...)', // prefixed (all themes)
@@ -738,11 +761,18 @@ const tokens = palette.tokens({ primary: 'primary' });
738
761
  // }
739
762
  ```
740
763
 
764
+ Override or disable per-export:
765
+
766
+ ```ts
767
+ palette.tokens({ primary: 'danger' }); // use danger as primary for this call
768
+ palette.tokens({ primary: false }); // no primary for this call
769
+ ```
770
+
741
771
  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
772
 
743
773
  ```ts
744
- palette.tokens({ prefix: { primary: 'p-', danger: 'd-' }, primary: 'primary' });
745
- // → 'p-surface' + 'surface' (alias) + 'd-surface'
774
+ palette.tokens({ prefix: { primary: 'p-', danger: 'd-' } });
775
+ // → 'p-surface' + 'surface' (alias from palette-level primary) + 'd-surface'
746
776
  ```
747
777
 
748
778
  An error is thrown if the primary name doesn't match any theme in the palette.
@@ -752,7 +782,11 @@ An error is thrown if the primary name doesn't match any theme in the palette.
752
782
  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
783
 
754
784
  ```ts
755
- const tastyTokens = palette.tasty({ primary: 'primary' });
785
+ const palette = glaze.palette(
786
+ { primary, danger, success },
787
+ { primary: 'primary' },
788
+ );
789
+ const tastyTokens = palette.tasty();
756
790
  // → {
757
791
  // '#primary-surface': { '': 'okhsl(...)', '@dark': 'okhsl(...)' },
758
792
  // '#danger-surface': { '': 'okhsl(...)', '@dark': 'okhsl(...)' },
@@ -848,7 +882,11 @@ const css = theme.css();
848
882
  Use in a stylesheet:
849
883
 
850
884
  ```ts
851
- const css = palette.css({ primary: 'primary' });
885
+ const palette = glaze.palette(
886
+ { primary, danger, success },
887
+ { primary: 'primary' },
888
+ );
889
+ const css = palette.css();
852
890
 
853
891
  const stylesheet = `
854
892
  :root { ${css.light} }
@@ -865,7 +903,7 @@ Options:
865
903
  | `format` | `'rgb'` | Color format (`'rgb'`, `'hsl'`, `'okhsl'`, `'oklch'`) |
866
904
  | `suffix` | `'-color'` | Suffix appended to each CSS property name |
867
905
  | `prefix` | `true` (palette) | (palette only) `true` uses `"<themeName>-"`, or provide a custom map |
868
- | `primary` | | (palette only) Theme name to duplicate without prefix |
906
+ | `primary` | inherited | (palette only) Override or disable (`false`) the palette-level primary for this call |
869
907
 
870
908
  ```ts
871
909
  // Custom suffix
@@ -876,8 +914,8 @@ theme.css({ suffix: '' });
876
914
  theme.css({ format: 'hsl' });
877
915
  // → "--surface-color: hsl(...);"
878
916
 
879
- // Palette with primary
880
- palette.css({ primary: 'primary' });
917
+ // Palette with primary (inherited from palette creation)
918
+ palette.css();
881
919
  // → "--primary-surface-color: rgb(...);\n--surface-color: rgb(...);\n--danger-surface-color: rgb(...);"
882
920
  ```
883
921
 
@@ -914,7 +952,7 @@ glaze.configure({
914
952
  lightLightness: [10, 100], // Light scheme lightness window [lo, hi] (bypassed in HC)
915
953
  darkLightness: [15, 95], // Dark scheme lightness window [lo, hi] (bypassed in HC)
916
954
  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)
955
+ darkCurve: 0.5, // Möbius beta for dark auto-inversion (0–1); or [normal, hc] pair
918
956
  states: {
919
957
  dark: '@dark', // State alias for dark mode tokens
920
958
  highContrast: '@high-contrast',
@@ -926,6 +964,7 @@ glaze.configure({
926
964
  shadowTuning: { // Default tuning for all shadow colors
927
965
  alphaMax: 0.6,
928
966
  bgHueBlend: 0.2,
967
+ darkShadowCurve: 0.4, // Power curve for dark-scheme alpha dampening (0-1)
929
968
  },
930
969
  });
931
970
  ```
@@ -1049,17 +1088,20 @@ const success = primary.extend({ hue: 157 });
1049
1088
  const warning = primary.extend({ hue: 84 });
1050
1089
  const note = primary.extend({ hue: 302 });
1051
1090
 
1052
- const palette = glaze.palette({ primary, danger, success, warning, note });
1091
+ const palette = glaze.palette(
1092
+ { primary, danger, success, warning, note },
1093
+ { primary: 'primary' },
1094
+ );
1053
1095
 
1054
1096
  // Export as flat token map grouped by variant (prefix defaults to true)
1055
- const tokens = palette.tokens({ primary: 'primary' });
1097
+ const tokens = palette.tokens();
1056
1098
  // tokens.light → { 'primary-surface': '...', 'surface': '...', 'danger-surface': '...' }
1057
1099
 
1058
1100
  // Export as tasty style-to-state bindings (for Tasty style system)
1059
- const tastyTokens = palette.tasty({ primary: 'primary' });
1101
+ const tastyTokens = palette.tasty();
1060
1102
 
1061
1103
  // Export as CSS custom properties (rgb format by default)
1062
- const css = palette.css({ primary: 'primary' });
1104
+ const css = palette.css();
1063
1105
  // css.light → "--primary-surface-color: rgb(...);\n--surface-color: rgb(...);\n--danger-surface-color: rgb(...);"
1064
1106
 
1065
1107
  // Standalone shadow computation
@@ -1115,7 +1157,7 @@ brand.colors({ surface: { lightness: 97 }, text: { base: 'surface', lightness: '
1115
1157
  | Method | Description |
1116
1158
  |---|---|
1117
1159
  | `glaze.configure(config)` | Set global configuration |
1118
- | `glaze.palette(themes)` | Compose themes into a palette |
1160
+ | `glaze.palette(themes, options?)` | Compose themes into a palette (options: `{ primary? }`) |
1119
1161
  | `glaze.getConfig()` | Get current global config |
1120
1162
  | `glaze.resetConfig()` | Reset to defaults |
1121
1163
 
package/dist/index.cjs CHANGED
@@ -843,7 +843,8 @@ const DEFAULT_SHADOW_TUNING = {
843
843
  lightnessBounds: [.05, .2],
844
844
  minGapTarget: .05,
845
845
  alphaMax: 1,
846
- bgHueBlend: .2
846
+ bgHueBlend: .2,
847
+ darkShadowCurve: .4
847
848
  };
848
849
  function resolveShadowTuning(perColor) {
849
850
  return {
@@ -977,7 +978,7 @@ function mobiusCurve(t, beta) {
977
978
  }
978
979
  function mapLightnessDark(l, mode, isHighContrast) {
979
980
  if (mode === "static") return l;
980
- const beta = globalConfig.darkCurve;
981
+ const beta = isHighContrast ? pairHC(globalConfig.darkCurve) : pairNormal(globalConfig.darkCurve);
981
982
  const [darkLo, darkHi] = lightnessWindow(isHighContrast, "dark");
982
983
  if (mode === "fixed") return l * (darkHi - darkLo) / 100 + darkLo;
983
984
  const [lightLo, lightHi] = lightnessWindow(isHighContrast, "light");
@@ -985,7 +986,7 @@ function mapLightnessDark(l, mode, isHighContrast) {
985
986
  return darkLo + (darkHi - darkLo) * mobiusCurve(t, beta);
986
987
  }
987
988
  function lightMappedToDark(lightL, isHighContrast) {
988
- const beta = globalConfig.darkCurve;
989
+ const beta = isHighContrast ? pairHC(globalConfig.darkCurve) : pairNormal(globalConfig.darkCurve);
989
990
  const [lightLo, lightHi] = lightnessWindow(isHighContrast, "light");
990
991
  const [darkLo, darkHi] = lightnessWindow(isHighContrast, "dark");
991
992
  const t = (lightHi - clamp(lightL, lightLo, lightHi)) / (lightHi - lightLo);
@@ -1137,7 +1138,13 @@ function resolveShadowForScheme(def, ctx, isDark, isHighContrast) {
1137
1138
  if (def.fg) fgVariant = getSchemeVariant(ctx.resolved.get(def.fg), isDark, isHighContrast);
1138
1139
  const intensity = isHighContrast ? pairHC(def.intensity) : pairNormal(def.intensity);
1139
1140
  const tuning = resolveShadowTuning(def.tuning);
1140
- return computeShadow(bgVariant, fgVariant, intensity, tuning);
1141
+ const result = computeShadow(bgVariant, fgVariant, intensity, tuning);
1142
+ if (isDark && tuning.darkShadowCurve < 1 && result.alpha > 0) {
1143
+ const normalized = result.alpha / tuning.alphaMax;
1144
+ const exponent = 1 / tuning.darkShadowCurve;
1145
+ result.alpha = tuning.alphaMax * Math.pow(normalized, exponent);
1146
+ }
1147
+ return result;
1141
1148
  }
1142
1149
  function variantToLinearRgb(v) {
1143
1150
  return okhslToLinearSrgb(v.h, v.s, v.l);
@@ -1415,10 +1422,14 @@ function createTheme(hue, saturation, initialColors) {
1415
1422
  };
1416
1423
  },
1417
1424
  extend(options) {
1418
- return createTheme(options.hue ?? hue, options.saturation ?? saturation, options.colors ? {
1419
- ...colorDefs,
1425
+ const newHue = options.hue ?? hue;
1426
+ const newSat = options.saturation ?? saturation;
1427
+ const inheritedColors = {};
1428
+ for (const [name, def] of Object.entries(colorDefs)) if (def.inherit !== false) inheritedColors[name] = def;
1429
+ return createTheme(newHue, newSat, options.colors ? {
1430
+ ...inheritedColors,
1420
1431
  ...options.colors
1421
- } : { ...colorDefs });
1432
+ } : { ...inheritedColors });
1422
1433
  },
1423
1434
  resolve() {
1424
1435
  return resolveAllColors(hue, saturation, colorDefs);
@@ -1452,40 +1463,74 @@ function validatePrimaryTheme(primary, themes) {
1452
1463
  throw new Error(`glaze: primary theme "${primary}" not found in palette. Available: ${available}.`);
1453
1464
  }
1454
1465
  }
1455
- function createPalette(themes) {
1466
+ /**
1467
+ * Resolve the effective primary for an export call.
1468
+ * `false` disables, a string overrides, `undefined` inherits from palette.
1469
+ */
1470
+ function resolveEffectivePrimary(exportPrimary, palettePrimary) {
1471
+ if (exportPrimary === false) return void 0;
1472
+ return exportPrimary ?? palettePrimary;
1473
+ }
1474
+ /**
1475
+ * Filter a resolved color map, skipping keys already in `seen`.
1476
+ * Warns on collision and keeps the first-written value (first-write-wins).
1477
+ * Returns a new map containing only non-colliding entries.
1478
+ */
1479
+ function filterCollisions(resolved, prefix, seen, themeName, isPrimary) {
1480
+ const filtered = /* @__PURE__ */ new Map();
1481
+ const label = isPrimary ? `${themeName} (primary)` : themeName;
1482
+ for (const [name, color] of resolved) {
1483
+ const key = `${prefix}${name}`;
1484
+ if (seen.has(key)) {
1485
+ console.warn(`glaze: token "${key}" from theme "${label}" collides with theme "${seen.get(key)}" — skipping.`);
1486
+ continue;
1487
+ }
1488
+ seen.set(key, label);
1489
+ filtered.set(name, color);
1490
+ }
1491
+ return filtered;
1492
+ }
1493
+ function createPalette(themes, paletteOptions) {
1494
+ validatePrimaryTheme(paletteOptions?.primary, themes);
1456
1495
  return {
1457
1496
  tokens(options) {
1458
- validatePrimaryTheme(options?.primary, themes);
1497
+ const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
1498
+ if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
1459
1499
  const modes = resolveModes(options?.modes);
1460
1500
  const allTokens = {};
1501
+ const seen = /* @__PURE__ */ new Map();
1461
1502
  for (const [themeName, theme] of Object.entries(themes)) {
1462
1503
  const resolved = theme.resolve();
1463
- const tokens = buildFlatTokenMap(resolved, resolvePrefix(options, themeName, true), modes, options?.format);
1504
+ const prefix = resolvePrefix(options, themeName, true);
1505
+ const tokens = buildFlatTokenMap(filterCollisions(resolved, prefix, seen, themeName), prefix, modes, options?.format);
1464
1506
  for (const variant of Object.keys(tokens)) {
1465
1507
  if (!allTokens[variant]) allTokens[variant] = {};
1466
1508
  Object.assign(allTokens[variant], tokens[variant]);
1467
1509
  }
1468
- if (themeName === options?.primary) {
1469
- const unprefixed = buildFlatTokenMap(resolved, "", modes, options?.format);
1510
+ if (themeName === effectivePrimary) {
1511
+ const unprefixed = buildFlatTokenMap(filterCollisions(resolved, "", seen, themeName, true), "", modes, options?.format);
1470
1512
  for (const variant of Object.keys(unprefixed)) Object.assign(allTokens[variant], unprefixed[variant]);
1471
1513
  }
1472
1514
  }
1473
1515
  return allTokens;
1474
1516
  },
1475
1517
  tasty(options) {
1476
- validatePrimaryTheme(options?.primary, themes);
1518
+ const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
1519
+ if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
1477
1520
  const states = {
1478
1521
  dark: options?.states?.dark ?? globalConfig.states.dark,
1479
1522
  highContrast: options?.states?.highContrast ?? globalConfig.states.highContrast
1480
1523
  };
1481
1524
  const modes = resolveModes(options?.modes);
1482
1525
  const allTokens = {};
1526
+ const seen = /* @__PURE__ */ new Map();
1483
1527
  for (const [themeName, theme] of Object.entries(themes)) {
1484
1528
  const resolved = theme.resolve();
1485
- const tokens = buildTokenMap(resolved, resolvePrefix(options, themeName, true), states, modes, options?.format);
1529
+ const prefix = resolvePrefix(options, themeName, true);
1530
+ const tokens = buildTokenMap(filterCollisions(resolved, prefix, seen, themeName), prefix, states, modes, options?.format);
1486
1531
  Object.assign(allTokens, tokens);
1487
- if (themeName === options?.primary) {
1488
- const unprefixed = buildTokenMap(resolved, "", states, modes, options?.format);
1532
+ if (themeName === effectivePrimary) {
1533
+ const unprefixed = buildTokenMap(filterCollisions(resolved, "", seen, themeName, true), "", states, modes, options?.format);
1489
1534
  Object.assign(allTokens, unprefixed);
1490
1535
  }
1491
1536
  }
@@ -1498,7 +1543,8 @@ function createPalette(themes) {
1498
1543
  return result;
1499
1544
  },
1500
1545
  css(options) {
1501
- validatePrimaryTheme(options?.primary, themes);
1546
+ const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
1547
+ if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
1502
1548
  const suffix = options?.suffix ?? "-color";
1503
1549
  const format = options?.format ?? "rgb";
1504
1550
  const allLines = {
@@ -1507,17 +1553,19 @@ function createPalette(themes) {
1507
1553
  lightContrast: [],
1508
1554
  darkContrast: []
1509
1555
  };
1556
+ const seen = /* @__PURE__ */ new Map();
1510
1557
  for (const [themeName, theme] of Object.entries(themes)) {
1511
1558
  const resolved = theme.resolve();
1512
- const css = buildCssMap(resolved, resolvePrefix(options, themeName, true), suffix, format);
1559
+ const prefix = resolvePrefix(options, themeName, true);
1560
+ const css = buildCssMap(filterCollisions(resolved, prefix, seen, themeName), prefix, suffix, format);
1513
1561
  for (const key of [
1514
1562
  "light",
1515
1563
  "dark",
1516
1564
  "lightContrast",
1517
1565
  "darkContrast"
1518
1566
  ]) if (css[key]) allLines[key].push(css[key]);
1519
- if (themeName === options?.primary) {
1520
- const unprefixed = buildCssMap(resolved, "", suffix, format);
1567
+ if (themeName === effectivePrimary) {
1568
+ const unprefixed = buildCssMap(filterCollisions(resolved, "", seen, themeName, true), "", suffix, format);
1521
1569
  for (const key of [
1522
1570
  "light",
1523
1571
  "dark",
@@ -1599,8 +1647,8 @@ glaze.configure = function configure(config) {
1599
1647
  /**
1600
1648
  * Compose multiple themes into a palette.
1601
1649
  */
1602
- glaze.palette = function palette(themes) {
1603
- return createPalette(themes);
1650
+ glaze.palette = function palette(themes, options) {
1651
+ return createPalette(themes, options);
1604
1652
  };
1605
1653
  /**
1606
1654
  * Create a theme from a serialized export.
@@ -1621,13 +1669,19 @@ glaze.shadow = function shadow(input) {
1621
1669
  const bg = parseOkhslInput(input.bg);
1622
1670
  const fg = input.fg ? parseOkhslInput(input.fg) : void 0;
1623
1671
  const tuning = resolveShadowTuning(input.tuning);
1624
- return computeShadow({
1672
+ const result = computeShadow({
1625
1673
  ...bg,
1626
1674
  alpha: 1
1627
1675
  }, fg ? {
1628
1676
  ...fg,
1629
1677
  alpha: 1
1630
1678
  } : void 0, input.intensity, tuning);
1679
+ if (input.dark && tuning.darkShadowCurve < 1 && result.alpha > 0) {
1680
+ const normalized = result.alpha / tuning.alphaMax;
1681
+ const exponent = 1 / tuning.darkShadowCurve;
1682
+ result.alpha = tuning.alphaMax * Math.pow(normalized, exponent);
1683
+ }
1684
+ return result;
1631
1685
  };
1632
1686
  /**
1633
1687
  * Format a resolved color variant as a CSS string.