@tenphi/glaze 0.0.0-snapshot.e26a2e0 → 0.0.0-snapshot.e7185f1
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 +116 -34
- package/dist/index.cjs +147 -34
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +80 -21
- package/dist/index.d.mts +80 -21
- package/dist/index.mjs +147 -34
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -70,9 +70,9 @@ 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(
|
|
75
|
-
// → { light: { 'primary-surface': 'okhsl(...)',
|
|
73
|
+
const palette = glaze.palette({ primary, danger, success }, { primary: 'primary' });
|
|
74
|
+
const tokens = palette.tokens();
|
|
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
|
|
@@ -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.5 | 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
|
|
|
@@ -618,14 +621,14 @@ Modes control how colors adapt across schemes:
|
|
|
618
621
|
|
|
619
622
|
### Lightness
|
|
620
623
|
|
|
621
|
-
|
|
624
|
+
Absolute lightness values (both root colors and dependent colors with absolute lightness) are mapped linearly within the configured `lightLightness` window:
|
|
622
625
|
|
|
623
626
|
```ts
|
|
624
627
|
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,12 +702,21 @@ Combine multiple themes into a single palette:
|
|
|
694
702
|
const palette = glaze.palette({ primary, danger, success, warning });
|
|
695
703
|
```
|
|
696
704
|
|
|
697
|
-
|
|
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
|
+
|
|
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:
|
|
700
717
|
|
|
701
718
|
```ts
|
|
702
|
-
const tokens = palette.tokens(
|
|
719
|
+
const tokens = palette.tokens();
|
|
703
720
|
// → {
|
|
704
721
|
// light: { 'primary-surface': 'okhsl(...)', 'danger-surface': 'okhsl(...)' },
|
|
705
722
|
// dark: { 'primary-surface': 'okhsl(...)', 'danger-surface': 'okhsl(...)' },
|
|
@@ -712,15 +729,68 @@ Custom prefix mapping:
|
|
|
712
729
|
palette.tokens({ prefix: { primary: 'brand-', danger: 'error-' } });
|
|
713
730
|
```
|
|
714
731
|
|
|
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
|
+
```
|
|
743
|
+
|
|
744
|
+
### Primary Theme
|
|
745
|
+
|
|
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:
|
|
747
|
+
|
|
748
|
+
```ts
|
|
749
|
+
const palette = glaze.palette(
|
|
750
|
+
{ primary, danger, success },
|
|
751
|
+
{ primary: 'primary' },
|
|
752
|
+
);
|
|
753
|
+
const tokens = palette.tokens();
|
|
754
|
+
// → {
|
|
755
|
+
// light: {
|
|
756
|
+
// 'primary-surface': 'okhsl(...)', // prefixed (all themes)
|
|
757
|
+
// 'danger-surface': 'okhsl(...)',
|
|
758
|
+
// 'success-surface': 'okhsl(...)',
|
|
759
|
+
// 'surface': 'okhsl(...)', // unprefixed alias (primary only)
|
|
760
|
+
// },
|
|
761
|
+
// }
|
|
762
|
+
```
|
|
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
|
+
|
|
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:
|
|
772
|
+
|
|
773
|
+
```ts
|
|
774
|
+
palette.tokens({ prefix: { primary: 'p-', danger: 'd-' } });
|
|
775
|
+
// → 'p-surface' + 'surface' (alias from palette-level primary) + 'd-surface'
|
|
776
|
+
```
|
|
777
|
+
|
|
778
|
+
An error is thrown if the primary name doesn't match any theme in the palette.
|
|
779
|
+
|
|
715
780
|
### Tasty Export (for [Tasty](https://cube-ui-kit.vercel.app/?path=/docs/tasty-documentation--docs) style system)
|
|
716
781
|
|
|
717
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.):
|
|
718
783
|
|
|
719
784
|
```ts
|
|
720
|
-
const
|
|
785
|
+
const palette = glaze.palette(
|
|
786
|
+
{ primary, danger, success },
|
|
787
|
+
{ primary: 'primary' },
|
|
788
|
+
);
|
|
789
|
+
const tastyTokens = palette.tasty();
|
|
721
790
|
// → {
|
|
722
791
|
// '#primary-surface': { '': 'okhsl(...)', '@dark': 'okhsl(...)' },
|
|
723
792
|
// '#danger-surface': { '': 'okhsl(...)', '@dark': 'okhsl(...)' },
|
|
793
|
+
// '#surface': { '': 'okhsl(...)', '@dark': 'okhsl(...)' }, // alias
|
|
724
794
|
// }
|
|
725
795
|
```
|
|
726
796
|
|
|
@@ -787,8 +857,10 @@ palette.tasty({ states: { dark: '@dark', highContrast: '@hc' } });
|
|
|
787
857
|
|
|
788
858
|
### JSON Export (Framework-Agnostic)
|
|
789
859
|
|
|
860
|
+
JSON export groups by theme name (no prefix needed):
|
|
861
|
+
|
|
790
862
|
```ts
|
|
791
|
-
const data = palette.json(
|
|
863
|
+
const data = palette.json();
|
|
792
864
|
// → {
|
|
793
865
|
// primary: { surface: { light: 'okhsl(...)', dark: 'okhsl(...)' } },
|
|
794
866
|
// danger: { surface: { light: 'okhsl(...)', dark: 'okhsl(...)' } },
|
|
@@ -810,7 +882,11 @@ const css = theme.css();
|
|
|
810
882
|
Use in a stylesheet:
|
|
811
883
|
|
|
812
884
|
```ts
|
|
813
|
-
const
|
|
885
|
+
const palette = glaze.palette(
|
|
886
|
+
{ primary, danger, success },
|
|
887
|
+
{ primary: 'primary' },
|
|
888
|
+
);
|
|
889
|
+
const css = palette.css();
|
|
814
890
|
|
|
815
891
|
const stylesheet = `
|
|
816
892
|
:root { ${css.light} }
|
|
@@ -826,7 +902,8 @@ Options:
|
|
|
826
902
|
|---|---|---|
|
|
827
903
|
| `format` | `'rgb'` | Color format (`'rgb'`, `'hsl'`, `'okhsl'`, `'oklch'`) |
|
|
828
904
|
| `suffix` | `'-color'` | Suffix appended to each CSS property name |
|
|
829
|
-
| `prefix` |
|
|
905
|
+
| `prefix` | `true` (palette) | (palette only) `true` uses `"<themeName>-"`, or provide a custom map |
|
|
906
|
+
| `primary` | inherited | (palette only) Override or disable (`false`) the palette-level primary for this call |
|
|
830
907
|
|
|
831
908
|
```ts
|
|
832
909
|
// Custom suffix
|
|
@@ -837,9 +914,9 @@ theme.css({ suffix: '' });
|
|
|
837
914
|
theme.css({ format: 'hsl' });
|
|
838
915
|
// → "--surface-color: hsl(...);"
|
|
839
916
|
|
|
840
|
-
// Palette with
|
|
841
|
-
palette.css(
|
|
842
|
-
// → "--primary-surface-color: rgb(...);\n--danger-surface-color: rgb(...);"
|
|
917
|
+
// Palette with primary (inherited from palette creation)
|
|
918
|
+
palette.css();
|
|
919
|
+
// → "--primary-surface-color: rgb(...);\n--surface-color: rgb(...);\n--danger-surface-color: rgb(...);"
|
|
843
920
|
```
|
|
844
921
|
|
|
845
922
|
## Output Modes
|
|
@@ -872,9 +949,10 @@ Resolution priority (highest first):
|
|
|
872
949
|
|
|
873
950
|
```ts
|
|
874
951
|
glaze.configure({
|
|
875
|
-
lightLightness: [10, 100], // Light scheme lightness window [lo, hi]
|
|
876
|
-
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)
|
|
877
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
|
|
878
956
|
states: {
|
|
879
957
|
dark: '@dark', // State alias for dark mode tokens
|
|
880
958
|
highContrast: '@high-contrast',
|
|
@@ -886,6 +964,7 @@ glaze.configure({
|
|
|
886
964
|
shadowTuning: { // Default tuning for all shadow colors
|
|
887
965
|
alphaMax: 0.6,
|
|
888
966
|
bgHueBlend: 0.2,
|
|
967
|
+
darkShadowCurve: 0.5, // Power curve for dark-scheme alpha dampening (0-1)
|
|
889
968
|
},
|
|
890
969
|
});
|
|
891
970
|
```
|
|
@@ -1009,18 +1088,21 @@ const success = primary.extend({ hue: 157 });
|
|
|
1009
1088
|
const warning = primary.extend({ hue: 84 });
|
|
1010
1089
|
const note = primary.extend({ hue: 302 });
|
|
1011
1090
|
|
|
1012
|
-
const palette = glaze.palette(
|
|
1091
|
+
const palette = glaze.palette(
|
|
1092
|
+
{ primary, danger, success, warning, note },
|
|
1093
|
+
{ primary: 'primary' },
|
|
1094
|
+
);
|
|
1013
1095
|
|
|
1014
|
-
// Export as flat token map grouped by variant
|
|
1015
|
-
const tokens = palette.tokens(
|
|
1016
|
-
// tokens.light → { 'primary-surface': '
|
|
1096
|
+
// Export as flat token map grouped by variant (prefix defaults to true)
|
|
1097
|
+
const tokens = palette.tokens();
|
|
1098
|
+
// tokens.light → { 'primary-surface': '...', 'surface': '...', 'danger-surface': '...' }
|
|
1017
1099
|
|
|
1018
1100
|
// Export as tasty style-to-state bindings (for Tasty style system)
|
|
1019
|
-
const tastyTokens = palette.tasty(
|
|
1101
|
+
const tastyTokens = palette.tasty();
|
|
1020
1102
|
|
|
1021
1103
|
// Export as CSS custom properties (rgb format by default)
|
|
1022
|
-
const css = palette.css(
|
|
1023
|
-
// css.light → "--primary-surface-color: rgb(...);\n--
|
|
1104
|
+
const css = palette.css();
|
|
1105
|
+
// css.light → "--primary-surface-color: rgb(...);\n--surface-color: rgb(...);\n--danger-surface-color: rgb(...);"
|
|
1024
1106
|
|
|
1025
1107
|
// Standalone shadow computation
|
|
1026
1108
|
const v = glaze.shadow({ bg: '#f0eef5', fg: '#1a1a2e', intensity: 10 });
|
|
@@ -1075,7 +1157,7 @@ brand.colors({ surface: { lightness: 97 }, text: { base: 'surface', lightness: '
|
|
|
1075
1157
|
| Method | Description |
|
|
1076
1158
|
|---|---|
|
|
1077
1159
|
| `glaze.configure(config)` | Set global configuration |
|
|
1078
|
-
| `glaze.palette(themes)` | Compose themes into a palette |
|
|
1160
|
+
| `glaze.palette(themes, options?)` | Compose themes into a palette (options: `{ primary? }`) |
|
|
1079
1161
|
| `glaze.getConfig()` | Get current global config |
|
|
1080
1162
|
| `glaze.resetConfig()` | Reset to defaults |
|
|
1081
1163
|
|
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
|
|
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(${
|
|
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,
|
|
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 * 1.
|
|
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 {
|
|
@@ -813,6 +814,7 @@ let globalConfig = {
|
|
|
813
814
|
lightLightness: [10, 100],
|
|
814
815
|
darkLightness: [15, 95],
|
|
815
816
|
darkDesaturation: .1,
|
|
817
|
+
darkCurve: .5,
|
|
816
818
|
states: {
|
|
817
819
|
dark: "@dark",
|
|
818
820
|
highContrast: "@high-contrast"
|
|
@@ -841,7 +843,8 @@ const DEFAULT_SHADOW_TUNING = {
|
|
|
841
843
|
lightnessBounds: [.05, .2],
|
|
842
844
|
minGapTarget: .05,
|
|
843
845
|
alphaMax: 1,
|
|
844
|
-
bgHueBlend: .2
|
|
846
|
+
bgHueBlend: .2,
|
|
847
|
+
darkShadowCurve: .5
|
|
845
848
|
};
|
|
846
849
|
function resolveShadowTuning(perColor) {
|
|
847
850
|
return {
|
|
@@ -960,21 +963,44 @@ function topoSort(defs) {
|
|
|
960
963
|
for (const name of Object.keys(defs)) visit(name);
|
|
961
964
|
return result;
|
|
962
965
|
}
|
|
963
|
-
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) {
|
|
964
971
|
if (mode === "static") return l;
|
|
965
|
-
const [lo, hi] =
|
|
972
|
+
const [lo, hi] = lightnessWindow(isHighContrast, "light");
|
|
966
973
|
return l * (hi - lo) / 100 + lo;
|
|
967
974
|
}
|
|
968
|
-
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) {
|
|
969
980
|
if (mode === "static") return l;
|
|
970
|
-
const
|
|
971
|
-
|
|
972
|
-
|
|
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);
|
|
973
994
|
}
|
|
974
995
|
function mapSaturationDark(s, mode) {
|
|
975
996
|
if (mode === "static") return s;
|
|
976
997
|
return s * (1 - globalConfig.darkDesaturation);
|
|
977
998
|
}
|
|
999
|
+
function schemeLightnessRange(isDark, mode, isHighContrast) {
|
|
1000
|
+
if (mode === "static") return [0, 1];
|
|
1001
|
+
const [lo, hi] = lightnessWindow(isHighContrast, isDark ? "dark" : "light");
|
|
1002
|
+
return [lo / 100, hi / 100];
|
|
1003
|
+
}
|
|
978
1004
|
function clamp(v, min, max) {
|
|
979
1005
|
return Math.max(min, Math.min(max, v));
|
|
980
1006
|
}
|
|
@@ -1031,24 +1057,26 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
|
|
|
1031
1057
|
else {
|
|
1032
1058
|
const parsed = parseRelativeOrAbsolute(isHighContrast ? pairHC(rawLightness) : pairNormal(rawLightness));
|
|
1033
1059
|
if (parsed.relative) {
|
|
1034
|
-
|
|
1035
|
-
if (isDark && mode === "auto")
|
|
1036
|
-
preferredL = clamp(baseL + delta, 0, 100);
|
|
1037
|
-
} else if (isDark) preferredL = mapLightnessDark(parsed.value, mode);
|
|
1038
|
-
else preferredL =
|
|
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);
|
|
1039
1065
|
}
|
|
1040
1066
|
const rawContrast = def.contrast;
|
|
1041
1067
|
if (rawContrast !== void 0) {
|
|
1042
1068
|
const minCr = isHighContrast ? pairHC(rawContrast) : pairNormal(rawContrast);
|
|
1043
1069
|
const effectiveSat = isDark ? mapSaturationDark(satFactor * ctx.saturation / 100, mode) : satFactor * ctx.saturation / 100;
|
|
1044
1070
|
const baseLinearRgb = okhslToLinearSrgb(baseVariant.h, baseVariant.s, baseVariant.l);
|
|
1071
|
+
const windowRange = schemeLightnessRange(isDark, mode, isHighContrast);
|
|
1045
1072
|
return {
|
|
1046
1073
|
l: findLightnessForContrast({
|
|
1047
1074
|
hue: effectiveHue,
|
|
1048
1075
|
saturation: effectiveSat,
|
|
1049
|
-
preferredLightness: preferredL / 100,
|
|
1076
|
+
preferredLightness: clamp(preferredL / 100, windowRange[0], windowRange[1]),
|
|
1050
1077
|
baseLinearRgb,
|
|
1051
|
-
contrast: minCr
|
|
1078
|
+
contrast: minCr,
|
|
1079
|
+
lightnessRange: [0, 1]
|
|
1052
1080
|
}).lightness * 100,
|
|
1053
1081
|
satFactor
|
|
1054
1082
|
};
|
|
@@ -1085,13 +1113,13 @@ function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
|
1085
1113
|
let finalL;
|
|
1086
1114
|
let finalSat;
|
|
1087
1115
|
if (isDark && isRoot) {
|
|
1088
|
-
finalL = mapLightnessDark(lightL, mode);
|
|
1116
|
+
finalL = mapLightnessDark(lightL, mode, isHighContrast);
|
|
1089
1117
|
finalSat = mapSaturationDark(satFactor * ctx.saturation / 100, mode);
|
|
1090
1118
|
} else if (isDark && !isRoot) {
|
|
1091
1119
|
finalL = lightL;
|
|
1092
1120
|
finalSat = mapSaturationDark(satFactor * ctx.saturation / 100, mode);
|
|
1093
1121
|
} else if (isRoot) {
|
|
1094
|
-
finalL = mapLightnessLight(lightL, mode);
|
|
1122
|
+
finalL = mapLightnessLight(lightL, mode, isHighContrast);
|
|
1095
1123
|
finalSat = satFactor * ctx.saturation / 100;
|
|
1096
1124
|
} else {
|
|
1097
1125
|
finalL = lightL;
|
|
@@ -1110,7 +1138,13 @@ function resolveShadowForScheme(def, ctx, isDark, isHighContrast) {
|
|
|
1110
1138
|
if (def.fg) fgVariant = getSchemeVariant(ctx.resolved.get(def.fg), isDark, isHighContrast);
|
|
1111
1139
|
const intensity = isHighContrast ? pairHC(def.intensity) : pairNormal(def.intensity);
|
|
1112
1140
|
const tuning = resolveShadowTuning(def.tuning);
|
|
1113
|
-
|
|
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;
|
|
1114
1148
|
}
|
|
1115
1149
|
function variantToLinearRgb(v) {
|
|
1116
1150
|
return okhslToLinearSrgb(v.h, v.s, v.l);
|
|
@@ -1388,10 +1422,14 @@ function createTheme(hue, saturation, initialColors) {
|
|
|
1388
1422
|
};
|
|
1389
1423
|
},
|
|
1390
1424
|
extend(options) {
|
|
1391
|
-
|
|
1392
|
-
|
|
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,
|
|
1393
1431
|
...options.colors
|
|
1394
|
-
} : { ...
|
|
1432
|
+
} : { ...inheritedColors });
|
|
1395
1433
|
},
|
|
1396
1434
|
resolve() {
|
|
1397
1435
|
return resolveAllColors(hue, saturation, colorDefs);
|
|
@@ -1413,35 +1451,88 @@ function createTheme(hue, saturation, initialColors) {
|
|
|
1413
1451
|
}
|
|
1414
1452
|
};
|
|
1415
1453
|
}
|
|
1416
|
-
function resolvePrefix(options, themeName) {
|
|
1417
|
-
|
|
1418
|
-
if (
|
|
1454
|
+
function resolvePrefix(options, themeName, defaultPrefix = false) {
|
|
1455
|
+
const prefix = options?.prefix ?? defaultPrefix;
|
|
1456
|
+
if (prefix === true) return `${themeName}-`;
|
|
1457
|
+
if (typeof prefix === "object" && prefix !== null) return prefix[themeName] ?? `${themeName}-`;
|
|
1419
1458
|
return "";
|
|
1420
1459
|
}
|
|
1421
|
-
function
|
|
1460
|
+
function validatePrimaryTheme(primary, themes) {
|
|
1461
|
+
if (primary !== void 0 && !(primary in themes)) {
|
|
1462
|
+
const available = Object.keys(themes).join(", ");
|
|
1463
|
+
throw new Error(`glaze: primary theme "${primary}" not found in palette. Available: ${available}.`);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
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);
|
|
1422
1495
|
return {
|
|
1423
1496
|
tokens(options) {
|
|
1497
|
+
const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
|
|
1498
|
+
if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
|
|
1424
1499
|
const modes = resolveModes(options?.modes);
|
|
1425
1500
|
const allTokens = {};
|
|
1501
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1426
1502
|
for (const [themeName, theme] of Object.entries(themes)) {
|
|
1427
|
-
const
|
|
1503
|
+
const resolved = theme.resolve();
|
|
1504
|
+
const prefix = resolvePrefix(options, themeName, true);
|
|
1505
|
+
const tokens = buildFlatTokenMap(filterCollisions(resolved, prefix, seen, themeName), prefix, modes, options?.format);
|
|
1428
1506
|
for (const variant of Object.keys(tokens)) {
|
|
1429
1507
|
if (!allTokens[variant]) allTokens[variant] = {};
|
|
1430
1508
|
Object.assign(allTokens[variant], tokens[variant]);
|
|
1431
1509
|
}
|
|
1510
|
+
if (themeName === effectivePrimary) {
|
|
1511
|
+
const unprefixed = buildFlatTokenMap(filterCollisions(resolved, "", seen, themeName, true), "", modes, options?.format);
|
|
1512
|
+
for (const variant of Object.keys(unprefixed)) Object.assign(allTokens[variant], unprefixed[variant]);
|
|
1513
|
+
}
|
|
1432
1514
|
}
|
|
1433
1515
|
return allTokens;
|
|
1434
1516
|
},
|
|
1435
1517
|
tasty(options) {
|
|
1518
|
+
const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
|
|
1519
|
+
if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
|
|
1436
1520
|
const states = {
|
|
1437
1521
|
dark: options?.states?.dark ?? globalConfig.states.dark,
|
|
1438
1522
|
highContrast: options?.states?.highContrast ?? globalConfig.states.highContrast
|
|
1439
1523
|
};
|
|
1440
1524
|
const modes = resolveModes(options?.modes);
|
|
1441
1525
|
const allTokens = {};
|
|
1526
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1442
1527
|
for (const [themeName, theme] of Object.entries(themes)) {
|
|
1443
|
-
const
|
|
1528
|
+
const resolved = theme.resolve();
|
|
1529
|
+
const prefix = resolvePrefix(options, themeName, true);
|
|
1530
|
+
const tokens = buildTokenMap(filterCollisions(resolved, prefix, seen, themeName), prefix, states, modes, options?.format);
|
|
1444
1531
|
Object.assign(allTokens, tokens);
|
|
1532
|
+
if (themeName === effectivePrimary) {
|
|
1533
|
+
const unprefixed = buildTokenMap(filterCollisions(resolved, "", seen, themeName, true), "", states, modes, options?.format);
|
|
1534
|
+
Object.assign(allTokens, unprefixed);
|
|
1535
|
+
}
|
|
1445
1536
|
}
|
|
1446
1537
|
return allTokens;
|
|
1447
1538
|
},
|
|
@@ -1452,6 +1543,8 @@ function createPalette(themes) {
|
|
|
1452
1543
|
return result;
|
|
1453
1544
|
},
|
|
1454
1545
|
css(options) {
|
|
1546
|
+
const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
|
|
1547
|
+
if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
|
|
1455
1548
|
const suffix = options?.suffix ?? "-color";
|
|
1456
1549
|
const format = options?.format ?? "rgb";
|
|
1457
1550
|
const allLines = {
|
|
@@ -1460,14 +1553,26 @@ function createPalette(themes) {
|
|
|
1460
1553
|
lightContrast: [],
|
|
1461
1554
|
darkContrast: []
|
|
1462
1555
|
};
|
|
1556
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1463
1557
|
for (const [themeName, theme] of Object.entries(themes)) {
|
|
1464
|
-
const
|
|
1558
|
+
const resolved = theme.resolve();
|
|
1559
|
+
const prefix = resolvePrefix(options, themeName, true);
|
|
1560
|
+
const css = buildCssMap(filterCollisions(resolved, prefix, seen, themeName), prefix, suffix, format);
|
|
1465
1561
|
for (const key of [
|
|
1466
1562
|
"light",
|
|
1467
1563
|
"dark",
|
|
1468
1564
|
"lightContrast",
|
|
1469
1565
|
"darkContrast"
|
|
1470
1566
|
]) if (css[key]) allLines[key].push(css[key]);
|
|
1567
|
+
if (themeName === effectivePrimary) {
|
|
1568
|
+
const unprefixed = buildCssMap(filterCollisions(resolved, "", seen, themeName, true), "", suffix, format);
|
|
1569
|
+
for (const key of [
|
|
1570
|
+
"light",
|
|
1571
|
+
"dark",
|
|
1572
|
+
"lightContrast",
|
|
1573
|
+
"darkContrast"
|
|
1574
|
+
]) if (unprefixed[key]) allLines[key].push(unprefixed[key]);
|
|
1575
|
+
}
|
|
1471
1576
|
}
|
|
1472
1577
|
return {
|
|
1473
1578
|
light: allLines.light.join("\n"),
|
|
@@ -1527,6 +1632,7 @@ glaze.configure = function configure(config) {
|
|
|
1527
1632
|
lightLightness: config.lightLightness ?? globalConfig.lightLightness,
|
|
1528
1633
|
darkLightness: config.darkLightness ?? globalConfig.darkLightness,
|
|
1529
1634
|
darkDesaturation: config.darkDesaturation ?? globalConfig.darkDesaturation,
|
|
1635
|
+
darkCurve: config.darkCurve ?? globalConfig.darkCurve,
|
|
1530
1636
|
states: {
|
|
1531
1637
|
dark: config.states?.dark ?? globalConfig.states.dark,
|
|
1532
1638
|
highContrast: config.states?.highContrast ?? globalConfig.states.highContrast
|
|
@@ -1541,8 +1647,8 @@ glaze.configure = function configure(config) {
|
|
|
1541
1647
|
/**
|
|
1542
1648
|
* Compose multiple themes into a palette.
|
|
1543
1649
|
*/
|
|
1544
|
-
glaze.palette = function palette(themes) {
|
|
1545
|
-
return createPalette(themes);
|
|
1650
|
+
glaze.palette = function palette(themes, options) {
|
|
1651
|
+
return createPalette(themes, options);
|
|
1546
1652
|
};
|
|
1547
1653
|
/**
|
|
1548
1654
|
* Create a theme from a serialized export.
|
|
@@ -1563,13 +1669,19 @@ glaze.shadow = function shadow(input) {
|
|
|
1563
1669
|
const bg = parseOkhslInput(input.bg);
|
|
1564
1670
|
const fg = input.fg ? parseOkhslInput(input.fg) : void 0;
|
|
1565
1671
|
const tuning = resolveShadowTuning(input.tuning);
|
|
1566
|
-
|
|
1672
|
+
const result = computeShadow({
|
|
1567
1673
|
...bg,
|
|
1568
1674
|
alpha: 1
|
|
1569
1675
|
}, fg ? {
|
|
1570
1676
|
...fg,
|
|
1571
1677
|
alpha: 1
|
|
1572
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;
|
|
1573
1685
|
};
|
|
1574
1686
|
/**
|
|
1575
1687
|
* Format a resolved color variant as a CSS string.
|
|
@@ -1626,6 +1738,7 @@ glaze.resetConfig = function resetConfig() {
|
|
|
1626
1738
|
lightLightness: [10, 100],
|
|
1627
1739
|
darkLightness: [15, 95],
|
|
1628
1740
|
darkDesaturation: .1,
|
|
1741
|
+
darkCurve: .5,
|
|
1629
1742
|
states: {
|
|
1630
1743
|
dark: "@dark",
|
|
1631
1744
|
highContrast: "@high-contrast"
|