@tenphi/glaze 0.0.0-snapshot.718119b → 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 +80 -30
- package/dist/index.cjs +115 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +61 -22
- package/dist/index.d.mts +61 -22
- package/dist/index.mjs +115 -40
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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(
|
|
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
|
|
|
@@ -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
|
|
@@ -388,6 +390,7 @@ Available tuning parameters:
|
|
|
388
390
|
| `minGapTarget` | 0.05 | Target minimum gap between pigment and bg lightness |
|
|
389
391
|
| `alphaMax` | 0.6 | Asymptotic maximum alpha |
|
|
390
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 |
|
|
391
394
|
|
|
392
395
|
### Standalone Shadow Computation
|
|
393
396
|
|
|
@@ -601,7 +604,7 @@ Modes control how colors adapt across schemes:
|
|
|
601
604
|
|
|
602
605
|
```ts
|
|
603
606
|
// Light: surface L=97, text lightness='-52' → L=45 (dark text on light bg)
|
|
604
|
-
// Dark: surface inverts to L≈
|
|
607
|
+
// Dark: surface inverts to L≈20 (Möbius curve), sign flips → L=20+52=72
|
|
605
608
|
// contrast solver may push further (light text on dark bg)
|
|
606
609
|
```
|
|
607
610
|
|
|
@@ -625,7 +628,7 @@ const [lo, hi] = lightLightness; // default: [10, 100]
|
|
|
625
628
|
const mappedL = (lightness * (hi - lo)) / 100 + lo;
|
|
626
629
|
```
|
|
627
630
|
|
|
628
|
-
Both `auto` and `fixed` modes use the same linear formula. `static` mode
|
|
631
|
+
Both `auto` and `fixed` modes use the same linear formula. `static` mode and high-contrast variants bypass the mapping entirely (identity: `mappedL = l`).
|
|
629
632
|
|
|
630
633
|
| Color | Raw L | Mapped L (default [10, 100]) |
|
|
631
634
|
|---|---|---|
|
|
@@ -637,24 +640,29 @@ Both `auto` and `fixed` modes use the same linear formula. `static` mode bypasse
|
|
|
637
640
|
|
|
638
641
|
### Lightness
|
|
639
642
|
|
|
640
|
-
**`auto`** — inverted within the configured window:
|
|
643
|
+
**`auto`** — inverted with a Möbius transformation within the configured window:
|
|
641
644
|
|
|
642
645
|
```ts
|
|
643
646
|
const [lo, hi] = darkLightness; // default: [15, 95]
|
|
644
|
-
const
|
|
647
|
+
const t = (100 - lightness) / 100;
|
|
648
|
+
const invertedL = lo + (hi - lo) * t / (t + darkCurve * (1 - t)); // darkCurve default: 0.5
|
|
645
649
|
```
|
|
646
650
|
|
|
647
|
-
|
|
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.
|
|
652
|
+
|
|
653
|
+
**`fixed`** — mapped without inversion (not affected by `darkCurve`):
|
|
648
654
|
|
|
649
655
|
```ts
|
|
650
656
|
const mappedL = (lightness * (hi - lo)) / 100 + lo;
|
|
651
657
|
```
|
|
652
658
|
|
|
653
|
-
| Color | Light L | Auto (
|
|
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 |
|
|
659
|
+
| Color | Light L | Auto (curve=0.5) | Auto (curve=1, linear) | Fixed (mapped) |
|
|
660
|
+
|---|---|---|---|---|
|
|
661
|
+
| surface (L=97) | 97 | 19.7 | 17.4 | 92.6 |
|
|
662
|
+
| accent-fill (L=52) | 52 | 66.9 | 53.4 | 56.6 |
|
|
663
|
+
| accent-text (L=100) | 100 | 15 | 15 | 95 |
|
|
664
|
+
|
|
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]`).
|
|
658
666
|
|
|
659
667
|
### Saturation
|
|
660
668
|
|
|
@@ -694,6 +702,15 @@ Combine multiple themes into a single palette:
|
|
|
694
702
|
const palette = glaze.palette({ primary, danger, success, warning });
|
|
695
703
|
```
|
|
696
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
|
+
|
|
697
714
|
### Prefix Behavior
|
|
698
715
|
|
|
699
716
|
Palette export methods (`tokens()`, `tasty()`, `css()`) default to `prefix: true` — all tokens are automatically prefixed with the theme name to avoid collisions:
|
|
@@ -712,15 +729,28 @@ Custom prefix mapping:
|
|
|
712
729
|
palette.tokens({ prefix: { primary: 'brand-', danger: 'error-' } });
|
|
713
730
|
```
|
|
714
731
|
|
|
715
|
-
To disable prefixing entirely, pass `prefix: false` explicitly.
|
|
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
|
+
```
|
|
716
743
|
|
|
717
744
|
### Primary Theme
|
|
718
745
|
|
|
719
|
-
|
|
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:
|
|
720
747
|
|
|
721
748
|
```ts
|
|
722
|
-
const palette = glaze.palette(
|
|
723
|
-
|
|
749
|
+
const palette = glaze.palette(
|
|
750
|
+
{ primary, danger, success },
|
|
751
|
+
{ primary: 'primary' },
|
|
752
|
+
);
|
|
753
|
+
const tokens = palette.tokens();
|
|
724
754
|
// → {
|
|
725
755
|
// light: {
|
|
726
756
|
// 'primary-surface': 'okhsl(...)', // prefixed (all themes)
|
|
@@ -731,11 +761,18 @@ const tokens = palette.tokens({ primary: 'primary' });
|
|
|
731
761
|
// }
|
|
732
762
|
```
|
|
733
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
|
+
|
|
734
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:
|
|
735
772
|
|
|
736
773
|
```ts
|
|
737
|
-
palette.tokens({ prefix: { primary: 'p-', danger: 'd-' }
|
|
738
|
-
// → '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'
|
|
739
776
|
```
|
|
740
777
|
|
|
741
778
|
An error is thrown if the primary name doesn't match any theme in the palette.
|
|
@@ -745,7 +782,11 @@ An error is thrown if the primary name doesn't match any theme in the palette.
|
|
|
745
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.):
|
|
746
783
|
|
|
747
784
|
```ts
|
|
748
|
-
const
|
|
785
|
+
const palette = glaze.palette(
|
|
786
|
+
{ primary, danger, success },
|
|
787
|
+
{ primary: 'primary' },
|
|
788
|
+
);
|
|
789
|
+
const tastyTokens = palette.tasty();
|
|
749
790
|
// → {
|
|
750
791
|
// '#primary-surface': { '': 'okhsl(...)', '@dark': 'okhsl(...)' },
|
|
751
792
|
// '#danger-surface': { '': 'okhsl(...)', '@dark': 'okhsl(...)' },
|
|
@@ -841,7 +882,11 @@ const css = theme.css();
|
|
|
841
882
|
Use in a stylesheet:
|
|
842
883
|
|
|
843
884
|
```ts
|
|
844
|
-
const
|
|
885
|
+
const palette = glaze.palette(
|
|
886
|
+
{ primary, danger, success },
|
|
887
|
+
{ primary: 'primary' },
|
|
888
|
+
);
|
|
889
|
+
const css = palette.css();
|
|
845
890
|
|
|
846
891
|
const stylesheet = `
|
|
847
892
|
:root { ${css.light} }
|
|
@@ -858,7 +903,7 @@ Options:
|
|
|
858
903
|
| `format` | `'rgb'` | Color format (`'rgb'`, `'hsl'`, `'okhsl'`, `'oklch'`) |
|
|
859
904
|
| `suffix` | `'-color'` | Suffix appended to each CSS property name |
|
|
860
905
|
| `prefix` | `true` (palette) | (palette only) `true` uses `"<themeName>-"`, or provide a custom map |
|
|
861
|
-
| `primary` |
|
|
906
|
+
| `primary` | inherited | (palette only) Override or disable (`false`) the palette-level primary for this call |
|
|
862
907
|
|
|
863
908
|
```ts
|
|
864
909
|
// Custom suffix
|
|
@@ -869,8 +914,8 @@ theme.css({ suffix: '' });
|
|
|
869
914
|
theme.css({ format: 'hsl' });
|
|
870
915
|
// → "--surface-color: hsl(...);"
|
|
871
916
|
|
|
872
|
-
// Palette with primary
|
|
873
|
-
palette.css(
|
|
917
|
+
// Palette with primary (inherited from palette creation)
|
|
918
|
+
palette.css();
|
|
874
919
|
// → "--primary-surface-color: rgb(...);\n--surface-color: rgb(...);\n--danger-surface-color: rgb(...);"
|
|
875
920
|
```
|
|
876
921
|
|
|
@@ -904,9 +949,10 @@ Resolution priority (highest first):
|
|
|
904
949
|
|
|
905
950
|
```ts
|
|
906
951
|
glaze.configure({
|
|
907
|
-
lightLightness: [10, 100], // Light scheme lightness window [lo, hi]
|
|
908
|
-
darkLightness: [15, 95], // Dark scheme lightness window [lo, hi]
|
|
952
|
+
lightLightness: [10, 100], // Light scheme lightness window [lo, hi] (bypassed in HC)
|
|
953
|
+
darkLightness: [15, 95], // Dark scheme lightness window [lo, hi] (bypassed in HC)
|
|
909
954
|
darkDesaturation: 0.1, // Saturation reduction in dark scheme (0–1)
|
|
955
|
+
darkCurve: 0.5, // Möbius beta for dark auto-inversion (0–1); or [normal, hc] pair
|
|
910
956
|
states: {
|
|
911
957
|
dark: '@dark', // State alias for dark mode tokens
|
|
912
958
|
highContrast: '@high-contrast',
|
|
@@ -918,6 +964,7 @@ glaze.configure({
|
|
|
918
964
|
shadowTuning: { // Default tuning for all shadow colors
|
|
919
965
|
alphaMax: 0.6,
|
|
920
966
|
bgHueBlend: 0.2,
|
|
967
|
+
darkShadowCurve: 0.4, // Power curve for dark-scheme alpha dampening (0-1)
|
|
921
968
|
},
|
|
922
969
|
});
|
|
923
970
|
```
|
|
@@ -1041,17 +1088,20 @@ const success = primary.extend({ hue: 157 });
|
|
|
1041
1088
|
const warning = primary.extend({ hue: 84 });
|
|
1042
1089
|
const note = primary.extend({ hue: 302 });
|
|
1043
1090
|
|
|
1044
|
-
const palette = glaze.palette(
|
|
1091
|
+
const palette = glaze.palette(
|
|
1092
|
+
{ primary, danger, success, warning, note },
|
|
1093
|
+
{ primary: 'primary' },
|
|
1094
|
+
);
|
|
1045
1095
|
|
|
1046
1096
|
// Export as flat token map grouped by variant (prefix defaults to true)
|
|
1047
|
-
const tokens = palette.tokens(
|
|
1097
|
+
const tokens = palette.tokens();
|
|
1048
1098
|
// tokens.light → { 'primary-surface': '...', 'surface': '...', 'danger-surface': '...' }
|
|
1049
1099
|
|
|
1050
1100
|
// Export as tasty style-to-state bindings (for Tasty style system)
|
|
1051
|
-
const tastyTokens = palette.tasty(
|
|
1101
|
+
const tastyTokens = palette.tasty();
|
|
1052
1102
|
|
|
1053
1103
|
// Export as CSS custom properties (rgb format by default)
|
|
1054
|
-
const css = palette.css(
|
|
1104
|
+
const css = palette.css();
|
|
1055
1105
|
// css.light → "--primary-surface-color: rgb(...);\n--surface-color: rgb(...);\n--danger-surface-color: rgb(...);"
|
|
1056
1106
|
|
|
1057
1107
|
// Standalone shadow computation
|
|
@@ -1107,7 +1157,7 @@ brand.colors({ surface: { lightness: 97 }, text: { base: 'surface', lightness: '
|
|
|
1107
1157
|
| Method | Description |
|
|
1108
1158
|
|---|---|
|
|
1109
1159
|
| `glaze.configure(config)` | Set global configuration |
|
|
1110
|
-
| `glaze.palette(themes)` | Compose themes into a palette |
|
|
1160
|
+
| `glaze.palette(themes, options?)` | Compose themes into a palette (options: `{ primary? }`) |
|
|
1111
1161
|
| `glaze.getConfig()` | Get current global config |
|
|
1112
1162
|
| `glaze.resetConfig()` | Reset to defaults |
|
|
1113
1163
|
|
package/dist/index.cjs
CHANGED
|
@@ -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.
|
|
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 {
|
|
@@ -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"
|
|
@@ -842,7 +843,8 @@ const DEFAULT_SHADOW_TUNING = {
|
|
|
842
843
|
lightnessBounds: [.05, .2],
|
|
843
844
|
minGapTarget: .05,
|
|
844
845
|
alphaMax: 1,
|
|
845
|
-
bgHueBlend: .2
|
|
846
|
+
bgHueBlend: .2,
|
|
847
|
+
darkShadowCurve: .4
|
|
846
848
|
};
|
|
847
849
|
function resolveShadowTuning(perColor) {
|
|
848
850
|
return {
|
|
@@ -961,24 +963,42 @@ function topoSort(defs) {
|
|
|
961
963
|
for (const name of Object.keys(defs)) visit(name);
|
|
962
964
|
return result;
|
|
963
965
|
}
|
|
964
|
-
function
|
|
966
|
+
function lightnessWindow(isHighContrast, kind) {
|
|
967
|
+
if (isHighContrast) return [0, 100];
|
|
968
|
+
return kind === "dark" ? globalConfig.darkLightness : globalConfig.lightLightness;
|
|
969
|
+
}
|
|
970
|
+
function mapLightnessLight(l, mode, isHighContrast) {
|
|
965
971
|
if (mode === "static") return l;
|
|
966
|
-
const [lo, hi] =
|
|
972
|
+
const [lo, hi] = lightnessWindow(isHighContrast, "light");
|
|
967
973
|
return l * (hi - lo) / 100 + lo;
|
|
968
974
|
}
|
|
969
|
-
function
|
|
975
|
+
function mobiusCurve(t, beta) {
|
|
976
|
+
if (beta >= 1) return t;
|
|
977
|
+
return t / (t + beta * (1 - t));
|
|
978
|
+
}
|
|
979
|
+
function mapLightnessDark(l, mode, isHighContrast) {
|
|
970
980
|
if (mode === "static") return l;
|
|
971
|
-
const
|
|
972
|
-
|
|
973
|
-
|
|
981
|
+
const beta = isHighContrast ? pairHC(globalConfig.darkCurve) : pairNormal(globalConfig.darkCurve);
|
|
982
|
+
const [darkLo, darkHi] = lightnessWindow(isHighContrast, "dark");
|
|
983
|
+
if (mode === "fixed") return l * (darkHi - darkLo) / 100 + darkLo;
|
|
984
|
+
const [lightLo, lightHi] = lightnessWindow(isHighContrast, "light");
|
|
985
|
+
const t = (lightHi - (l * (lightHi - lightLo) / 100 + lightLo)) / (lightHi - lightLo);
|
|
986
|
+
return darkLo + (darkHi - darkLo) * mobiusCurve(t, beta);
|
|
987
|
+
}
|
|
988
|
+
function lightMappedToDark(lightL, isHighContrast) {
|
|
989
|
+
const beta = isHighContrast ? pairHC(globalConfig.darkCurve) : pairNormal(globalConfig.darkCurve);
|
|
990
|
+
const [lightLo, lightHi] = lightnessWindow(isHighContrast, "light");
|
|
991
|
+
const [darkLo, darkHi] = lightnessWindow(isHighContrast, "dark");
|
|
992
|
+
const t = (lightHi - clamp(lightL, lightLo, lightHi)) / (lightHi - lightLo);
|
|
993
|
+
return darkLo + (darkHi - darkLo) * mobiusCurve(t, beta);
|
|
974
994
|
}
|
|
975
995
|
function mapSaturationDark(s, mode) {
|
|
976
996
|
if (mode === "static") return s;
|
|
977
997
|
return s * (1 - globalConfig.darkDesaturation);
|
|
978
998
|
}
|
|
979
|
-
function schemeLightnessRange(isDark, mode) {
|
|
999
|
+
function schemeLightnessRange(isDark, mode, isHighContrast) {
|
|
980
1000
|
if (mode === "static") return [0, 1];
|
|
981
|
-
const [lo, hi] = isDark ?
|
|
1001
|
+
const [lo, hi] = lightnessWindow(isHighContrast, isDark ? "dark" : "light");
|
|
982
1002
|
return [lo / 100, hi / 100];
|
|
983
1003
|
}
|
|
984
1004
|
function clamp(v, min, max) {
|
|
@@ -1037,26 +1057,26 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
|
|
|
1037
1057
|
else {
|
|
1038
1058
|
const parsed = parseRelativeOrAbsolute(isHighContrast ? pairHC(rawLightness) : pairNormal(rawLightness));
|
|
1039
1059
|
if (parsed.relative) {
|
|
1040
|
-
|
|
1041
|
-
if (isDark && mode === "auto")
|
|
1042
|
-
preferredL = clamp(baseL + delta, 0, 100);
|
|
1043
|
-
} else if (isDark) preferredL = mapLightnessDark(parsed.value, mode);
|
|
1044
|
-
else preferredL = mapLightnessLight(parsed.value, mode);
|
|
1060
|
+
const delta = parsed.value;
|
|
1061
|
+
if (isDark && mode === "auto") preferredL = lightMappedToDark(clamp(getSchemeVariant(baseResolved, false, isHighContrast).l * 100 + delta, 0, 100), isHighContrast);
|
|
1062
|
+
else preferredL = clamp(baseL + delta, 0, 100);
|
|
1063
|
+
} else if (isDark) preferredL = mapLightnessDark(parsed.value, mode, isHighContrast);
|
|
1064
|
+
else preferredL = mapLightnessLight(parsed.value, mode, isHighContrast);
|
|
1045
1065
|
}
|
|
1046
1066
|
const rawContrast = def.contrast;
|
|
1047
1067
|
if (rawContrast !== void 0) {
|
|
1048
1068
|
const minCr = isHighContrast ? pairHC(rawContrast) : pairNormal(rawContrast);
|
|
1049
1069
|
const effectiveSat = isDark ? mapSaturationDark(satFactor * ctx.saturation / 100, mode) : satFactor * ctx.saturation / 100;
|
|
1050
1070
|
const baseLinearRgb = okhslToLinearSrgb(baseVariant.h, baseVariant.s, baseVariant.l);
|
|
1051
|
-
const
|
|
1071
|
+
const windowRange = schemeLightnessRange(isDark, mode, isHighContrast);
|
|
1052
1072
|
return {
|
|
1053
1073
|
l: findLightnessForContrast({
|
|
1054
1074
|
hue: effectiveHue,
|
|
1055
1075
|
saturation: effectiveSat,
|
|
1056
|
-
preferredLightness: clamp(preferredL / 100,
|
|
1076
|
+
preferredLightness: clamp(preferredL / 100, windowRange[0], windowRange[1]),
|
|
1057
1077
|
baseLinearRgb,
|
|
1058
1078
|
contrast: minCr,
|
|
1059
|
-
lightnessRange
|
|
1079
|
+
lightnessRange: [0, 1]
|
|
1060
1080
|
}).lightness * 100,
|
|
1061
1081
|
satFactor
|
|
1062
1082
|
};
|
|
@@ -1093,13 +1113,13 @@ function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
|
1093
1113
|
let finalL;
|
|
1094
1114
|
let finalSat;
|
|
1095
1115
|
if (isDark && isRoot) {
|
|
1096
|
-
finalL = mapLightnessDark(lightL, mode);
|
|
1116
|
+
finalL = mapLightnessDark(lightL, mode, isHighContrast);
|
|
1097
1117
|
finalSat = mapSaturationDark(satFactor * ctx.saturation / 100, mode);
|
|
1098
1118
|
} else if (isDark && !isRoot) {
|
|
1099
1119
|
finalL = lightL;
|
|
1100
1120
|
finalSat = mapSaturationDark(satFactor * ctx.saturation / 100, mode);
|
|
1101
1121
|
} else if (isRoot) {
|
|
1102
|
-
finalL = mapLightnessLight(lightL, mode);
|
|
1122
|
+
finalL = mapLightnessLight(lightL, mode, isHighContrast);
|
|
1103
1123
|
finalSat = satFactor * ctx.saturation / 100;
|
|
1104
1124
|
} else {
|
|
1105
1125
|
finalL = lightL;
|
|
@@ -1118,7 +1138,13 @@ function resolveShadowForScheme(def, ctx, isDark, isHighContrast) {
|
|
|
1118
1138
|
if (def.fg) fgVariant = getSchemeVariant(ctx.resolved.get(def.fg), isDark, isHighContrast);
|
|
1119
1139
|
const intensity = isHighContrast ? pairHC(def.intensity) : pairNormal(def.intensity);
|
|
1120
1140
|
const tuning = resolveShadowTuning(def.tuning);
|
|
1121
|
-
|
|
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;
|
|
1122
1148
|
}
|
|
1123
1149
|
function variantToLinearRgb(v) {
|
|
1124
1150
|
return okhslToLinearSrgb(v.h, v.s, v.l);
|
|
@@ -1396,10 +1422,14 @@ function createTheme(hue, saturation, initialColors) {
|
|
|
1396
1422
|
};
|
|
1397
1423
|
},
|
|
1398
1424
|
extend(options) {
|
|
1399
|
-
|
|
1400
|
-
|
|
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,
|
|
1401
1431
|
...options.colors
|
|
1402
|
-
} : { ...
|
|
1432
|
+
} : { ...inheritedColors });
|
|
1403
1433
|
},
|
|
1404
1434
|
resolve() {
|
|
1405
1435
|
return resolveAllColors(hue, saturation, colorDefs);
|
|
@@ -1433,40 +1463,74 @@ function validatePrimaryTheme(primary, themes) {
|
|
|
1433
1463
|
throw new Error(`glaze: primary theme "${primary}" not found in palette. Available: ${available}.`);
|
|
1434
1464
|
}
|
|
1435
1465
|
}
|
|
1436
|
-
|
|
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);
|
|
1437
1495
|
return {
|
|
1438
1496
|
tokens(options) {
|
|
1439
|
-
|
|
1497
|
+
const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
|
|
1498
|
+
if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
|
|
1440
1499
|
const modes = resolveModes(options?.modes);
|
|
1441
1500
|
const allTokens = {};
|
|
1501
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1442
1502
|
for (const [themeName, theme] of Object.entries(themes)) {
|
|
1443
1503
|
const resolved = theme.resolve();
|
|
1444
|
-
const
|
|
1504
|
+
const prefix = resolvePrefix(options, themeName, true);
|
|
1505
|
+
const tokens = buildFlatTokenMap(filterCollisions(resolved, prefix, seen, themeName), prefix, modes, options?.format);
|
|
1445
1506
|
for (const variant of Object.keys(tokens)) {
|
|
1446
1507
|
if (!allTokens[variant]) allTokens[variant] = {};
|
|
1447
1508
|
Object.assign(allTokens[variant], tokens[variant]);
|
|
1448
1509
|
}
|
|
1449
|
-
if (themeName ===
|
|
1450
|
-
const unprefixed = buildFlatTokenMap(resolved, "", modes, options?.format);
|
|
1510
|
+
if (themeName === effectivePrimary) {
|
|
1511
|
+
const unprefixed = buildFlatTokenMap(filterCollisions(resolved, "", seen, themeName, true), "", modes, options?.format);
|
|
1451
1512
|
for (const variant of Object.keys(unprefixed)) Object.assign(allTokens[variant], unprefixed[variant]);
|
|
1452
1513
|
}
|
|
1453
1514
|
}
|
|
1454
1515
|
return allTokens;
|
|
1455
1516
|
},
|
|
1456
1517
|
tasty(options) {
|
|
1457
|
-
|
|
1518
|
+
const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
|
|
1519
|
+
if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
|
|
1458
1520
|
const states = {
|
|
1459
1521
|
dark: options?.states?.dark ?? globalConfig.states.dark,
|
|
1460
1522
|
highContrast: options?.states?.highContrast ?? globalConfig.states.highContrast
|
|
1461
1523
|
};
|
|
1462
1524
|
const modes = resolveModes(options?.modes);
|
|
1463
1525
|
const allTokens = {};
|
|
1526
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1464
1527
|
for (const [themeName, theme] of Object.entries(themes)) {
|
|
1465
1528
|
const resolved = theme.resolve();
|
|
1466
|
-
const
|
|
1529
|
+
const prefix = resolvePrefix(options, themeName, true);
|
|
1530
|
+
const tokens = buildTokenMap(filterCollisions(resolved, prefix, seen, themeName), prefix, states, modes, options?.format);
|
|
1467
1531
|
Object.assign(allTokens, tokens);
|
|
1468
|
-
if (themeName ===
|
|
1469
|
-
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);
|
|
1470
1534
|
Object.assign(allTokens, unprefixed);
|
|
1471
1535
|
}
|
|
1472
1536
|
}
|
|
@@ -1479,7 +1543,8 @@ function createPalette(themes) {
|
|
|
1479
1543
|
return result;
|
|
1480
1544
|
},
|
|
1481
1545
|
css(options) {
|
|
1482
|
-
|
|
1546
|
+
const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
|
|
1547
|
+
if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
|
|
1483
1548
|
const suffix = options?.suffix ?? "-color";
|
|
1484
1549
|
const format = options?.format ?? "rgb";
|
|
1485
1550
|
const allLines = {
|
|
@@ -1488,17 +1553,19 @@ function createPalette(themes) {
|
|
|
1488
1553
|
lightContrast: [],
|
|
1489
1554
|
darkContrast: []
|
|
1490
1555
|
};
|
|
1556
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1491
1557
|
for (const [themeName, theme] of Object.entries(themes)) {
|
|
1492
1558
|
const resolved = theme.resolve();
|
|
1493
|
-
const
|
|
1559
|
+
const prefix = resolvePrefix(options, themeName, true);
|
|
1560
|
+
const css = buildCssMap(filterCollisions(resolved, prefix, seen, themeName), prefix, suffix, format);
|
|
1494
1561
|
for (const key of [
|
|
1495
1562
|
"light",
|
|
1496
1563
|
"dark",
|
|
1497
1564
|
"lightContrast",
|
|
1498
1565
|
"darkContrast"
|
|
1499
1566
|
]) if (css[key]) allLines[key].push(css[key]);
|
|
1500
|
-
if (themeName ===
|
|
1501
|
-
const unprefixed = buildCssMap(resolved, "", suffix, format);
|
|
1567
|
+
if (themeName === effectivePrimary) {
|
|
1568
|
+
const unprefixed = buildCssMap(filterCollisions(resolved, "", seen, themeName, true), "", suffix, format);
|
|
1502
1569
|
for (const key of [
|
|
1503
1570
|
"light",
|
|
1504
1571
|
"dark",
|
|
@@ -1565,6 +1632,7 @@ glaze.configure = function configure(config) {
|
|
|
1565
1632
|
lightLightness: config.lightLightness ?? globalConfig.lightLightness,
|
|
1566
1633
|
darkLightness: config.darkLightness ?? globalConfig.darkLightness,
|
|
1567
1634
|
darkDesaturation: config.darkDesaturation ?? globalConfig.darkDesaturation,
|
|
1635
|
+
darkCurve: config.darkCurve ?? globalConfig.darkCurve,
|
|
1568
1636
|
states: {
|
|
1569
1637
|
dark: config.states?.dark ?? globalConfig.states.dark,
|
|
1570
1638
|
highContrast: config.states?.highContrast ?? globalConfig.states.highContrast
|
|
@@ -1579,8 +1647,8 @@ glaze.configure = function configure(config) {
|
|
|
1579
1647
|
/**
|
|
1580
1648
|
* Compose multiple themes into a palette.
|
|
1581
1649
|
*/
|
|
1582
|
-
glaze.palette = function palette(themes) {
|
|
1583
|
-
return createPalette(themes);
|
|
1650
|
+
glaze.palette = function palette(themes, options) {
|
|
1651
|
+
return createPalette(themes, options);
|
|
1584
1652
|
};
|
|
1585
1653
|
/**
|
|
1586
1654
|
* Create a theme from a serialized export.
|
|
@@ -1601,13 +1669,19 @@ glaze.shadow = function shadow(input) {
|
|
|
1601
1669
|
const bg = parseOkhslInput(input.bg);
|
|
1602
1670
|
const fg = input.fg ? parseOkhslInput(input.fg) : void 0;
|
|
1603
1671
|
const tuning = resolveShadowTuning(input.tuning);
|
|
1604
|
-
|
|
1672
|
+
const result = computeShadow({
|
|
1605
1673
|
...bg,
|
|
1606
1674
|
alpha: 1
|
|
1607
1675
|
}, fg ? {
|
|
1608
1676
|
...fg,
|
|
1609
1677
|
alpha: 1
|
|
1610
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;
|
|
1611
1685
|
};
|
|
1612
1686
|
/**
|
|
1613
1687
|
* Format a resolved color variant as a CSS string.
|
|
@@ -1664,6 +1738,7 @@ glaze.resetConfig = function resetConfig() {
|
|
|
1664
1738
|
lightLightness: [10, 100],
|
|
1665
1739
|
darkLightness: [15, 95],
|
|
1666
1740
|
darkDesaturation: .1,
|
|
1741
|
+
darkCurve: .5,
|
|
1667
1742
|
states: {
|
|
1668
1743
|
dark: "@dark",
|
|
1669
1744
|
highContrast: "@high-contrast"
|