@tenphi/glaze 0.1.1 → 0.2.0
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 +109 -71
- package/dist/index.cjs +76 -43
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +23 -13
- package/dist/index.d.mts +23 -13
- package/dist/index.mjs +76 -43
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,6 +23,7 @@ Glaze generates robust **light**, **dark**, and **high-contrast** color schemes
|
|
|
23
23
|
- **OKHSL color space** — perceptually uniform hue and saturation
|
|
24
24
|
- **WCAG 2 contrast solving** — automatic lightness adjustment to meet AA/AAA targets
|
|
25
25
|
- **Light + Dark + High-Contrast** — all schemes from one definition
|
|
26
|
+
- **Per-color hue override** — absolute or relative hue shifts within a theme
|
|
26
27
|
- **Multi-format output** — `okhsl`, `rgb`, `hsl`, `oklch`
|
|
27
28
|
- **Import/Export** — serialize and restore theme configurations
|
|
28
29
|
- **Create from hex/RGB** — start from an existing brand color
|
|
@@ -54,11 +55,11 @@ const primary = glaze(280, 80);
|
|
|
54
55
|
|
|
55
56
|
// Define colors with explicit lightness and contrast relationships
|
|
56
57
|
primary.colors({
|
|
57
|
-
surface:
|
|
58
|
-
text:
|
|
59
|
-
border:
|
|
60
|
-
'accent-fill': {
|
|
61
|
-
'accent-text': { base: 'accent-fill',
|
|
58
|
+
surface: { lightness: 97, saturation: 0.75 },
|
|
59
|
+
text: { base: 'surface', lightness: '-52', contrast: 'AAA' },
|
|
60
|
+
border: { base: 'surface', lightness: ['-7', '-20'], contrast: 'AA-large' },
|
|
61
|
+
'accent-fill': { lightness: 52, mode: 'fixed' },
|
|
62
|
+
'accent-text': { base: 'accent-fill', lightness: '+48', contrast: 'AA', mode: 'fixed' },
|
|
62
63
|
});
|
|
63
64
|
|
|
64
65
|
// Create status themes by rotating the hue
|
|
@@ -77,6 +78,8 @@ const tokens = palette.tokens({ prefix: true });
|
|
|
77
78
|
|
|
78
79
|
A single `glaze` theme is tied to one hue/saturation seed. Status colors (danger, success, warning) are derived via `extend`, which inherits all color definitions and replaces the seed.
|
|
79
80
|
|
|
81
|
+
Individual colors can override the hue via the `hue` prop (see [Per-Color Hue Override](#per-color-hue-override)), but the primary purpose of a theme is to scope colors with the same hue.
|
|
82
|
+
|
|
80
83
|
### Color Definitions
|
|
81
84
|
|
|
82
85
|
Every color is defined explicitly. No implicit roles — every value is stated.
|
|
@@ -85,48 +88,73 @@ Every color is defined explicitly. No implicit roles — every value is stated.
|
|
|
85
88
|
|
|
86
89
|
```ts
|
|
87
90
|
primary.colors({
|
|
88
|
-
surface: {
|
|
89
|
-
border: {
|
|
91
|
+
surface: { lightness: 97, saturation: 0.75 },
|
|
92
|
+
border: { lightness: 90, saturation: 0.20 },
|
|
90
93
|
});
|
|
91
94
|
```
|
|
92
95
|
|
|
93
|
-
- `
|
|
94
|
-
- `
|
|
96
|
+
- `lightness` — lightness in the light scheme (0–100)
|
|
97
|
+
- `saturation` — saturation factor applied to the seed saturation (0–1, default: `1`)
|
|
95
98
|
|
|
96
99
|
#### Dependent Colors (relative to base)
|
|
97
100
|
|
|
98
101
|
```ts
|
|
99
102
|
primary.colors({
|
|
100
|
-
surface: {
|
|
101
|
-
text: { base: 'surface',
|
|
103
|
+
surface: { lightness: 97, saturation: 0.75 },
|
|
104
|
+
text: { base: 'surface', lightness: '-52', contrast: 'AAA' },
|
|
102
105
|
});
|
|
103
106
|
```
|
|
104
107
|
|
|
105
108
|
- `base` — name of another color in the same theme
|
|
106
|
-
- `
|
|
107
|
-
- `
|
|
109
|
+
- `lightness` — position of this color (see [Lightness Values](#lightness-values))
|
|
110
|
+
- `contrast` — ensures the WCAG contrast ratio meets a target floor against the base
|
|
108
111
|
|
|
109
|
-
|
|
112
|
+
### Lightness Values
|
|
110
113
|
|
|
111
|
-
|
|
114
|
+
The `lightness` prop accepts two forms:
|
|
112
115
|
|
|
113
|
-
|
|
|
114
|
-
|
|
115
|
-
|
|
|
116
|
-
|
|
|
117
|
-
|
|
116
|
+
| Form | Example | Meaning |
|
|
117
|
+
|---|---|---|
|
|
118
|
+
| Number (absolute) | `lightness: 45` | Absolute lightness 0–100 |
|
|
119
|
+
| String (relative) | `lightness: '-52'` | Relative to base color's lightness |
|
|
120
|
+
|
|
121
|
+
**Absolute lightness** on a dependent color (with `base`) positions the color independently. In dark mode, it is dark-mapped on its own. The `contrast` WCAG solver acts as a safety net.
|
|
122
|
+
|
|
123
|
+
**Relative lightness** applies a signed delta to the base color's resolved lightness. In dark mode with `auto` adaptation, the sign flips automatically.
|
|
118
124
|
|
|
119
125
|
```ts
|
|
120
|
-
//
|
|
121
|
-
'text': { base: 'surface',
|
|
122
|
-
|
|
126
|
+
// Relative: 97 - 52 = 45 in light mode
|
|
127
|
+
'text': { base: 'surface', lightness: '-52' }
|
|
128
|
+
|
|
129
|
+
// Absolute: lightness 45 in light mode, dark-mapped independently
|
|
130
|
+
'text': { base: 'surface', lightness: 45 }
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
A dependent color with `base` but no `lightness` inherits the base's lightness (equivalent to a delta of 0).
|
|
134
|
+
|
|
135
|
+
### Per-Color Hue Override
|
|
123
136
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
137
|
+
Individual colors can override the theme's hue. The `hue` prop accepts:
|
|
138
|
+
|
|
139
|
+
| Form | Example | Meaning |
|
|
140
|
+
|---|---|---|
|
|
141
|
+
| Number (absolute) | `hue: 120` | Absolute hue 0–360 |
|
|
142
|
+
| String (relative) | `hue: '+20'` | Relative to the **theme seed** hue |
|
|
143
|
+
|
|
144
|
+
**Important:** Relative hue is always relative to the **theme seed hue**, not to a base color's hue.
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
const theme = glaze(280, 80);
|
|
148
|
+
theme.colors({
|
|
149
|
+
surface: { lightness: 97 },
|
|
150
|
+
// Gradient end — slight hue shift from seed (280 + 20 = 300)
|
|
151
|
+
gradientEnd: { lightness: 90, hue: '+20' },
|
|
152
|
+
// Entirely different hue
|
|
153
|
+
warning: { lightness: 60, hue: 40 },
|
|
154
|
+
});
|
|
127
155
|
```
|
|
128
156
|
|
|
129
|
-
###
|
|
157
|
+
### contrast (WCAG Floor)
|
|
130
158
|
|
|
131
159
|
Ensures the WCAG contrast ratio meets a target floor. Accepts a numeric ratio or a preset string:
|
|
132
160
|
|
|
@@ -141,26 +169,26 @@ type MinContrast = number | 'AA' | 'AAA' | 'AA-large' | 'AAA-large';
|
|
|
141
169
|
| `'AA-large'` | 3 |
|
|
142
170
|
| `'AAA-large'` | 4.5 |
|
|
143
171
|
|
|
144
|
-
You can also pass any numeric ratio directly (e.g., `
|
|
172
|
+
You can also pass any numeric ratio directly (e.g., `contrast: 4.5`, `contrast: 7`, `contrast: 11`).
|
|
145
173
|
|
|
146
|
-
The constraint is applied independently for each scheme. If the `
|
|
174
|
+
The constraint is applied independently for each scheme. If the `lightness` already satisfies the floor, it's kept. Otherwise, the solver adjusts lightness until the target is met.
|
|
147
175
|
|
|
148
176
|
### High-Contrast via Array Values
|
|
149
177
|
|
|
150
|
-
`
|
|
178
|
+
`lightness` and `contrast` accept a `[normal, high-contrast]` pair:
|
|
151
179
|
|
|
152
180
|
```ts
|
|
153
|
-
'border': { base: 'surface',
|
|
154
|
-
//
|
|
155
|
-
//
|
|
181
|
+
'border': { base: 'surface', lightness: ['-7', '-20'], contrast: 'AA-large' }
|
|
182
|
+
// ↑ ↑
|
|
183
|
+
// normal high-contrast
|
|
156
184
|
```
|
|
157
185
|
|
|
158
186
|
A single value applies to both modes. All control is local and explicit.
|
|
159
187
|
|
|
160
188
|
```ts
|
|
161
|
-
'text': { base: 'surface',
|
|
162
|
-
'border': { base: 'surface',
|
|
163
|
-
'muted': { base: 'surface',
|
|
189
|
+
'text': { base: 'surface', lightness: '-52', contrast: 'AAA' }
|
|
190
|
+
'border': { base: 'surface', lightness: ['-7', '-20'], contrast: 'AA-large' }
|
|
191
|
+
'muted': { base: 'surface', lightness: ['-35', '-50'], contrast: ['AA-large', 'AA'] }
|
|
164
192
|
```
|
|
165
193
|
|
|
166
194
|
## Theme Color Management
|
|
@@ -171,8 +199,8 @@ A single value applies to both modes. All control is local and explicit.
|
|
|
171
199
|
|
|
172
200
|
```ts
|
|
173
201
|
const theme = glaze(280, 80);
|
|
174
|
-
theme.colors({ surface: {
|
|
175
|
-
theme.colors({ text: {
|
|
202
|
+
theme.colors({ surface: { lightness: 97 } });
|
|
203
|
+
theme.colors({ text: { lightness: 30 } });
|
|
176
204
|
// Both 'surface' and 'text' are now defined
|
|
177
205
|
```
|
|
178
206
|
|
|
@@ -181,8 +209,8 @@ theme.colors({ text: { l: 30 } });
|
|
|
181
209
|
`.color(name)` returns the definition, `.color(name, def)` sets it:
|
|
182
210
|
|
|
183
211
|
```ts
|
|
184
|
-
theme.color('surface', {
|
|
185
|
-
const def = theme.color('surface');
|
|
212
|
+
theme.color('surface', { lightness: 97, saturation: 0.75 }); // set
|
|
213
|
+
const def = theme.color('surface'); // get → { lightness: 97, saturation: 0.75 }
|
|
186
214
|
```
|
|
187
215
|
|
|
188
216
|
### Removing Colors
|
|
@@ -214,7 +242,7 @@ Serialize a theme's configuration (hue, saturation, color definitions) to a plai
|
|
|
214
242
|
```ts
|
|
215
243
|
// Export
|
|
216
244
|
const snapshot = theme.export();
|
|
217
|
-
// → { hue: 280, saturation: 80, colors: { surface: {
|
|
245
|
+
// → { hue: 280, saturation: 80, colors: { surface: { lightness: 97, saturation: 0.75 }, ... } }
|
|
218
246
|
|
|
219
247
|
const jsonString = JSON.stringify(snapshot);
|
|
220
248
|
|
|
@@ -230,14 +258,14 @@ The export contains only the configuration — not resolved color values. Resolv
|
|
|
230
258
|
Create a single color token without a full theme:
|
|
231
259
|
|
|
232
260
|
```ts
|
|
233
|
-
const accent = glaze.color({ hue: 280, saturation: 80,
|
|
261
|
+
const accent = glaze.color({ hue: 280, saturation: 80, lightness: 52, mode: 'fixed' });
|
|
234
262
|
|
|
235
263
|
accent.resolve(); // → ResolvedColor with light/dark/lightContrast/darkContrast
|
|
236
264
|
accent.token(); // → { '': 'okhsl(...)', '@dark': 'okhsl(...)' }
|
|
237
265
|
accent.json(); // → { light: 'okhsl(...)', dark: 'okhsl(...)' }
|
|
238
266
|
```
|
|
239
267
|
|
|
240
|
-
Standalone colors are always root colors (no `base`/`contrast
|
|
268
|
+
Standalone colors are always root colors (no `base`/`contrast`).
|
|
241
269
|
|
|
242
270
|
## From Existing Colors
|
|
243
271
|
|
|
@@ -255,8 +283,8 @@ The resulting theme has the extracted hue and saturation. Add colors as usual:
|
|
|
255
283
|
|
|
256
284
|
```ts
|
|
257
285
|
brand.colors({
|
|
258
|
-
surface: {
|
|
259
|
-
text: { base: 'surface',
|
|
286
|
+
surface: { lightness: 97, saturation: 0.75 },
|
|
287
|
+
text: { base: 'surface', lightness: '-52', contrast: 'AAA' },
|
|
260
288
|
});
|
|
261
289
|
```
|
|
262
290
|
|
|
@@ -299,20 +327,20 @@ Modes control how colors adapt across schemes:
|
|
|
299
327
|
| `'fixed'` | Color stays recognizable. Only safety corrections. For brand buttons, CTAs. |
|
|
300
328
|
| `'static'` | No adaptation. Same value in every scheme. |
|
|
301
329
|
|
|
302
|
-
### How
|
|
330
|
+
### How Relative Lightness Adapts
|
|
303
331
|
|
|
304
|
-
**`auto` mode** —
|
|
332
|
+
**`auto` mode** — relative lightness sign flips in dark scheme:
|
|
305
333
|
|
|
306
334
|
```ts
|
|
307
|
-
// Light: surface L=97, text
|
|
335
|
+
// Light: surface L=97, text lightness='-52' → L=45 (dark text on light bg)
|
|
308
336
|
// Dark: surface inverts to L≈14, sign flips → L=14+52=66
|
|
309
|
-
//
|
|
337
|
+
// contrast solver may push further (light text on dark bg)
|
|
310
338
|
```
|
|
311
339
|
|
|
312
|
-
**`fixed` mode** — lightness is mapped (not inverted),
|
|
340
|
+
**`fixed` mode** — lightness is mapped (not inverted), relative sign preserved:
|
|
313
341
|
|
|
314
342
|
```ts
|
|
315
|
-
// Light: accent-fill L=52, accent-text
|
|
343
|
+
// Light: accent-fill L=52, accent-text lightness='+48' → L=100 (white on brand)
|
|
316
344
|
// Dark: accent-fill maps to L≈51.6, sign preserved → L≈99.6
|
|
317
345
|
```
|
|
318
346
|
|
|
@@ -367,7 +395,7 @@ Override individual colors (additive merge):
|
|
|
367
395
|
```ts
|
|
368
396
|
const danger = primary.extend({
|
|
369
397
|
hue: 23,
|
|
370
|
-
colors: { 'accent-fill': {
|
|
398
|
+
colors: { 'accent-fill': { lightness: 48, mode: 'fixed' } },
|
|
371
399
|
});
|
|
372
400
|
```
|
|
373
401
|
|
|
@@ -446,33 +474,43 @@ glaze.configure({
|
|
|
446
474
|
## Color Definition Shape
|
|
447
475
|
|
|
448
476
|
```ts
|
|
477
|
+
type RelativeValue = `+${number}` | `-${number}`;
|
|
449
478
|
type HCPair<T> = T | [T, T]; // [normal, high-contrast]
|
|
450
479
|
|
|
451
480
|
interface ColorDef {
|
|
452
|
-
//
|
|
453
|
-
|
|
454
|
-
|
|
481
|
+
// Lightness
|
|
482
|
+
lightness?: HCPair<number | RelativeValue>;
|
|
483
|
+
// Number: absolute (0–100)
|
|
484
|
+
// String: relative to base ('+N' / '-N')
|
|
485
|
+
|
|
486
|
+
// Hue override
|
|
487
|
+
hue?: number | RelativeValue;
|
|
488
|
+
// Number: absolute (0–360)
|
|
489
|
+
// String: relative to theme seed ('+N' / '-N')
|
|
490
|
+
|
|
491
|
+
// Saturation factor (0–1, default: 1)
|
|
492
|
+
saturation?: number;
|
|
455
493
|
|
|
456
|
-
//
|
|
494
|
+
// Dependency
|
|
457
495
|
base?: string; // name of another color
|
|
458
|
-
contrast?: HCPair<
|
|
459
|
-
ensureContrast?: HCPair<MinContrast>; // ensures WCAG contrast ratio meets target floor
|
|
496
|
+
contrast?: HCPair<MinContrast>; // WCAG contrast ratio floor against base
|
|
460
497
|
|
|
461
498
|
// Adaptation mode
|
|
462
499
|
mode?: 'auto' | 'fixed' | 'static'; // default: 'auto'
|
|
463
500
|
}
|
|
464
501
|
```
|
|
465
502
|
|
|
466
|
-
|
|
503
|
+
A root color must have absolute `lightness` (a number). A dependent color must have `base`. Relative `lightness` (a string) requires `base`.
|
|
467
504
|
|
|
468
505
|
## Validation
|
|
469
506
|
|
|
470
507
|
| Condition | Behavior |
|
|
471
508
|
|---|---|
|
|
472
|
-
| Both `
|
|
509
|
+
| Both absolute `lightness` and `base` on same color | Warning, `lightness` takes precedence |
|
|
473
510
|
| `contrast` without `base` | Validation error |
|
|
474
|
-
| `
|
|
475
|
-
| `
|
|
511
|
+
| Relative `lightness` without `base` | Validation error |
|
|
512
|
+
| `lightness` resolves outside 0–100 | Clamp silently |
|
|
513
|
+
| `saturation` outside 0–1 | Clamp silently |
|
|
476
514
|
| Circular `base` references | Validation error |
|
|
477
515
|
| `base` references non-existent name | Validation error |
|
|
478
516
|
|
|
@@ -506,14 +544,14 @@ import { glaze } from '@tenphi/glaze';
|
|
|
506
544
|
const primary = glaze(280, 80);
|
|
507
545
|
|
|
508
546
|
primary.colors({
|
|
509
|
-
surface:
|
|
510
|
-
text:
|
|
511
|
-
border:
|
|
512
|
-
bg:
|
|
513
|
-
icon:
|
|
514
|
-
'accent-fill': {
|
|
515
|
-
'accent-text': { base: 'accent-fill',
|
|
516
|
-
disabled:
|
|
547
|
+
surface: { lightness: 97, saturation: 0.75 },
|
|
548
|
+
text: { base: 'surface', lightness: '-52', contrast: 'AAA' },
|
|
549
|
+
border: { base: 'surface', lightness: ['-7', '-20'], contrast: 'AA-large' },
|
|
550
|
+
bg: { lightness: 97, saturation: 0.75 },
|
|
551
|
+
icon: { lightness: 60, saturation: 0.94 },
|
|
552
|
+
'accent-fill': { lightness: 52, mode: 'fixed' },
|
|
553
|
+
'accent-text': { base: 'accent-fill', lightness: '+48', contrast: 'AA', mode: 'fixed' },
|
|
554
|
+
disabled: { lightness: 81, saturation: 0.4 },
|
|
517
555
|
});
|
|
518
556
|
|
|
519
557
|
const danger = primary.extend({ hue: 23 });
|
|
@@ -535,7 +573,7 @@ const restored = glaze.from(snapshot);
|
|
|
535
573
|
|
|
536
574
|
// Create from an existing brand color
|
|
537
575
|
const brand = glaze.fromHex('#7a4dbf');
|
|
538
|
-
brand.colors({ surface: {
|
|
576
|
+
brand.colors({ surface: { lightness: 97 }, text: { base: 'surface', lightness: '-52' } });
|
|
539
577
|
```
|
|
540
578
|
|
|
541
579
|
## API Reference
|
package/dist/index.cjs
CHANGED
|
@@ -495,7 +495,7 @@ function formatOklch(h, s, l) {
|
|
|
495
495
|
*
|
|
496
496
|
* Finds the closest OKHSL lightness that satisfies a WCAG 2 contrast target
|
|
497
497
|
* against a base color. Used by glaze when resolving dependent colors
|
|
498
|
-
* with `
|
|
498
|
+
* with `contrast`.
|
|
499
499
|
*/
|
|
500
500
|
const CONTRAST_PRESETS = {
|
|
501
501
|
AA: 4.5,
|
|
@@ -633,8 +633,8 @@ function coarseScan(h, s, lo, hi, yBase, target, epsilon, maxIter) {
|
|
|
633
633
|
* against a base color, staying as close to `preferredLightness` as possible.
|
|
634
634
|
*/
|
|
635
635
|
function findLightnessForContrast(options) {
|
|
636
|
-
const { hue, saturation, preferredLightness, baseLinearRgb,
|
|
637
|
-
const target = resolveMinContrast(
|
|
636
|
+
const { hue, saturation, preferredLightness, baseLinearRgb, contrast: contrastInput, lightnessRange = [0, 1], epsilon = 1e-4, maxIterations = 14 } = options;
|
|
637
|
+
const target = resolveMinContrast(contrastInput);
|
|
638
638
|
const yBase = relativeLuminanceFromLinearRgb(baseLinearRgb);
|
|
639
639
|
const crPref = contrastRatioFromLuminance(cachedLuminance(hue, saturation, preferredLightness), yBase);
|
|
640
640
|
if (crPref >= target) return {
|
|
@@ -715,9 +715,10 @@ function validateColorDefs(defs) {
|
|
|
715
715
|
const names = new Set(Object.keys(defs));
|
|
716
716
|
for (const [name, def] of Object.entries(defs)) {
|
|
717
717
|
if (def.contrast !== void 0 && !def.base) throw new Error(`glaze: color "${name}" has "contrast" without "base".`);
|
|
718
|
-
if (def.
|
|
718
|
+
if (def.lightness !== void 0 && !isAbsoluteLightness(def.lightness) && !def.base) throw new Error(`glaze: color "${name}" has relative "lightness" without "base".`);
|
|
719
|
+
if (isAbsoluteLightness(def.lightness) && def.base !== void 0) console.warn(`glaze: color "${name}" has absolute "lightness" and "base". Absolute lightness takes precedence.`);
|
|
719
720
|
if (def.base && !names.has(def.base)) throw new Error(`glaze: color "${name}" references non-existent base "${def.base}".`);
|
|
720
|
-
if (def.
|
|
721
|
+
if (!isAbsoluteLightness(def.lightness) && def.base === void 0) throw new Error(`glaze: color "${name}" must have either absolute "lightness" (root) or "base" (dependent).`);
|
|
721
722
|
}
|
|
722
723
|
const visited = /* @__PURE__ */ new Set();
|
|
723
724
|
const inStack = /* @__PURE__ */ new Set();
|
|
@@ -726,7 +727,7 @@ function validateColorDefs(defs) {
|
|
|
726
727
|
if (visited.has(name)) return;
|
|
727
728
|
inStack.add(name);
|
|
728
729
|
const def = defs[name];
|
|
729
|
-
if (def.base && def.
|
|
730
|
+
if (def.base && !isAbsoluteLightness(def.lightness)) dfs(def.base);
|
|
730
731
|
inStack.delete(name);
|
|
731
732
|
visited.add(name);
|
|
732
733
|
}
|
|
@@ -739,7 +740,7 @@ function topoSort(defs) {
|
|
|
739
740
|
if (visited.has(name)) return;
|
|
740
741
|
visited.add(name);
|
|
741
742
|
const def = defs[name];
|
|
742
|
-
if (def.base && def.
|
|
743
|
+
if (def.base && !isAbsoluteLightness(def.lightness)) visit(def.base);
|
|
743
744
|
result.push(name);
|
|
744
745
|
}
|
|
745
746
|
for (const name of Object.keys(defs)) visit(name);
|
|
@@ -755,44 +756,75 @@ function mapSaturationDark(s, mode) {
|
|
|
755
756
|
if (mode === "static") return s;
|
|
756
757
|
return s * (1 - globalConfig.darkDesaturation);
|
|
757
758
|
}
|
|
759
|
+
function clamp(v, min, max) {
|
|
760
|
+
return Math.max(min, Math.min(max, v));
|
|
761
|
+
}
|
|
758
762
|
/**
|
|
759
|
-
*
|
|
763
|
+
* Parse a value that can be absolute (number) or relative (signed string).
|
|
764
|
+
* Returns the numeric value and whether it's relative.
|
|
760
765
|
*/
|
|
761
|
-
function
|
|
762
|
-
if (
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
+
function parseRelativeOrAbsolute(value) {
|
|
767
|
+
if (typeof value === "number") return {
|
|
768
|
+
value,
|
|
769
|
+
relative: false
|
|
770
|
+
};
|
|
771
|
+
return {
|
|
772
|
+
value: parseFloat(value),
|
|
773
|
+
relative: true
|
|
774
|
+
};
|
|
766
775
|
}
|
|
767
|
-
|
|
768
|
-
|
|
776
|
+
/**
|
|
777
|
+
* Compute the effective hue for a color, given the theme seed hue
|
|
778
|
+
* and an optional per-color hue override.
|
|
779
|
+
*/
|
|
780
|
+
function resolveEffectiveHue(seedHue, defHue) {
|
|
781
|
+
if (defHue === void 0) return seedHue;
|
|
782
|
+
const parsed = parseRelativeOrAbsolute(defHue);
|
|
783
|
+
if (parsed.relative) return ((seedHue + parsed.value) % 360 + 360) % 360;
|
|
784
|
+
return (parsed.value % 360 + 360) % 360;
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* Check whether a lightness value represents an absolute root definition
|
|
788
|
+
* (i.e. a number, not a relative string).
|
|
789
|
+
*/
|
|
790
|
+
function isAbsoluteLightness(lightness) {
|
|
791
|
+
if (lightness === void 0) return false;
|
|
792
|
+
return typeof (Array.isArray(lightness) ? lightness[0] : lightness) === "number";
|
|
769
793
|
}
|
|
770
794
|
function resolveRootColor(_name, def, _ctx, isHighContrast) {
|
|
771
|
-
const rawL = def.
|
|
795
|
+
const rawL = def.lightness;
|
|
772
796
|
return {
|
|
773
|
-
lightL: clamp(isHighContrast ? pairHC(rawL) : pairNormal(rawL), 0, 100),
|
|
774
|
-
|
|
797
|
+
lightL: clamp(parseRelativeOrAbsolute(isHighContrast ? pairHC(rawL) : pairNormal(rawL)).value, 0, 100),
|
|
798
|
+
satFactor: clamp(def.saturation ?? 1, 0, 1)
|
|
775
799
|
};
|
|
776
800
|
}
|
|
777
|
-
function resolveDependentColor(name, def, ctx, isHighContrast, isDark) {
|
|
801
|
+
function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effectiveHue) {
|
|
778
802
|
const baseName = def.base;
|
|
779
803
|
const baseResolved = ctx.resolved.get(baseName);
|
|
780
804
|
if (!baseResolved) throw new Error(`glaze: base "${baseName}" not yet resolved for "${name}".`);
|
|
781
805
|
const mode = def.mode ?? "auto";
|
|
782
|
-
const
|
|
806
|
+
const satFactor = clamp(def.saturation ?? 1, 0, 1);
|
|
783
807
|
let baseL;
|
|
784
808
|
if (isDark && isHighContrast) baseL = baseResolved.darkContrast.l * 100;
|
|
785
809
|
else if (isDark) baseL = baseResolved.dark.l * 100;
|
|
786
810
|
else if (isHighContrast) baseL = baseResolved.lightContrast.l * 100;
|
|
787
811
|
else baseL = baseResolved.light.l * 100;
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
if (
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
812
|
+
let preferredL;
|
|
813
|
+
const rawLightness = def.lightness;
|
|
814
|
+
if (rawLightness === void 0) preferredL = baseL;
|
|
815
|
+
else {
|
|
816
|
+
const parsed = parseRelativeOrAbsolute(isHighContrast ? pairHC(rawLightness) : pairNormal(rawLightness));
|
|
817
|
+
if (parsed.relative) {
|
|
818
|
+
let delta = parsed.value;
|
|
819
|
+
if (isDark && mode === "auto") delta = -delta;
|
|
820
|
+
preferredL = clamp(baseL + delta, 0, 100);
|
|
821
|
+
} else if (isDark) preferredL = mapLightnessDark(parsed.value, mode);
|
|
822
|
+
else preferredL = clamp(parsed.value, 0, 100);
|
|
823
|
+
}
|
|
824
|
+
const rawContrast = def.contrast;
|
|
825
|
+
if (rawContrast !== void 0) {
|
|
826
|
+
const minCr = isHighContrast ? pairHC(rawContrast) : pairNormal(rawContrast);
|
|
827
|
+
const effectiveSat = isDark ? mapSaturationDark(satFactor * ctx.saturation / 100, mode) : satFactor * ctx.saturation / 100;
|
|
796
828
|
let baseH;
|
|
797
829
|
let baseS;
|
|
798
830
|
let baseLNorm;
|
|
@@ -816,48 +848,49 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark) {
|
|
|
816
848
|
const baseLinearRgb = okhslToLinearSrgb(baseH, baseS, baseLNorm);
|
|
817
849
|
return {
|
|
818
850
|
l: findLightnessForContrast({
|
|
819
|
-
hue:
|
|
851
|
+
hue: effectiveHue,
|
|
820
852
|
saturation: effectiveSat,
|
|
821
853
|
preferredLightness: preferredL / 100,
|
|
822
854
|
baseLinearRgb,
|
|
823
|
-
|
|
855
|
+
contrast: minCr
|
|
824
856
|
}).lightness * 100,
|
|
825
|
-
|
|
857
|
+
satFactor
|
|
826
858
|
};
|
|
827
859
|
}
|
|
828
860
|
return {
|
|
829
861
|
l: clamp(preferredL, 0, 100),
|
|
830
|
-
|
|
862
|
+
satFactor
|
|
831
863
|
};
|
|
832
864
|
}
|
|
833
865
|
function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
834
866
|
const mode = def.mode ?? "auto";
|
|
835
|
-
const isRoot = def.
|
|
867
|
+
const isRoot = isAbsoluteLightness(def.lightness) && !def.base;
|
|
868
|
+
const effectiveHue = resolveEffectiveHue(ctx.hue, def.hue);
|
|
836
869
|
let lightL;
|
|
837
|
-
let
|
|
870
|
+
let satFactor;
|
|
838
871
|
if (isRoot) {
|
|
839
872
|
const root = resolveRootColor(name, def, ctx, isHighContrast);
|
|
840
873
|
lightL = root.lightL;
|
|
841
|
-
|
|
874
|
+
satFactor = root.satFactor;
|
|
842
875
|
} else {
|
|
843
|
-
const dep = resolveDependentColor(name, def, ctx, isHighContrast, isDark);
|
|
876
|
+
const dep = resolveDependentColor(name, def, ctx, isHighContrast, isDark, effectiveHue);
|
|
844
877
|
lightL = dep.l;
|
|
845
|
-
|
|
878
|
+
satFactor = dep.satFactor;
|
|
846
879
|
}
|
|
847
880
|
let finalL;
|
|
848
881
|
let finalSat;
|
|
849
882
|
if (isDark && isRoot) {
|
|
850
883
|
finalL = mapLightnessDark(lightL, mode);
|
|
851
|
-
finalSat = mapSaturationDark(
|
|
884
|
+
finalSat = mapSaturationDark(satFactor * ctx.saturation / 100, mode);
|
|
852
885
|
} else if (isDark && !isRoot) {
|
|
853
886
|
finalL = lightL;
|
|
854
|
-
finalSat = mapSaturationDark(
|
|
887
|
+
finalSat = mapSaturationDark(satFactor * ctx.saturation / 100, mode);
|
|
855
888
|
} else {
|
|
856
889
|
finalL = lightL;
|
|
857
|
-
finalSat =
|
|
890
|
+
finalSat = satFactor * ctx.saturation / 100;
|
|
858
891
|
}
|
|
859
892
|
return {
|
|
860
|
-
h:
|
|
893
|
+
h: effectiveHue,
|
|
861
894
|
s: clamp(finalSat, 0, 1),
|
|
862
895
|
l: clamp(finalL / 100, 0, 1)
|
|
863
896
|
};
|
|
@@ -1064,8 +1097,8 @@ function createPalette(themes) {
|
|
|
1064
1097
|
}
|
|
1065
1098
|
function createColorToken(input) {
|
|
1066
1099
|
const defs = { __color__: {
|
|
1067
|
-
|
|
1068
|
-
|
|
1100
|
+
lightness: input.lightness,
|
|
1101
|
+
saturation: input.saturationFactor,
|
|
1069
1102
|
mode: input.mode
|
|
1070
1103
|
} };
|
|
1071
1104
|
return {
|