@tenphi/glaze 0.4.0 → 0.5.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 +190 -42
- package/dist/index.cjs +160 -51
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +85 -6
- package/dist/index.d.mts +85 -6
- package/dist/index.mjs +160 -51
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,9 +22,10 @@ Glaze generates robust **light**, **dark**, and **high-contrast** color schemes
|
|
|
22
22
|
|
|
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
|
+
- **Shadow colors** — OKHSL-native shadow computation with automatic alpha, fg/bg tinting, and per-scheme adaptation
|
|
25
26
|
- **Light + Dark + High-Contrast** — all schemes from one definition
|
|
26
27
|
- **Per-color hue override** — absolute or relative hue shifts within a theme
|
|
27
|
-
- **Multi-format output** — `okhsl`, `rgb`, `hsl`, `oklch`
|
|
28
|
+
- **Multi-format output** — `okhsl`, `rgb`, `hsl`, `oklch` with modern CSS space syntax
|
|
28
29
|
- **CSS custom properties export** — ready-to-use `--var: value;` declarations per scheme
|
|
29
30
|
- **Import/Export** — serialize and restore theme configurations
|
|
30
31
|
- **Create from hex/RGB** — start from an existing brand color
|
|
@@ -290,34 +291,165 @@ brand.colors({
|
|
|
290
291
|
});
|
|
291
292
|
```
|
|
292
293
|
|
|
294
|
+
## Shadow Colors
|
|
295
|
+
|
|
296
|
+
Shadow colors are colors with computed alpha. Instead of a parallel shadow system, they extend the existing color pipeline. All math is done natively in OKHSL.
|
|
297
|
+
|
|
298
|
+
### Defining Shadow Colors
|
|
299
|
+
|
|
300
|
+
Shadow colors use `type: 'shadow'` and reference a `bg` (background) color and optionally an `fg` (foreground) color for tinting and intensity modulation:
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
theme.colors({
|
|
304
|
+
surface: { lightness: 95 },
|
|
305
|
+
text: { base: 'surface', lightness: '-52', contrast: 'AAA' },
|
|
306
|
+
|
|
307
|
+
'shadow-sm': { type: 'shadow', bg: 'surface', fg: 'text', intensity: 5 },
|
|
308
|
+
'shadow-md': { type: 'shadow', bg: 'surface', fg: 'text', intensity: 10 },
|
|
309
|
+
'shadow-lg': { type: 'shadow', bg: 'surface', fg: 'text', intensity: 20 },
|
|
310
|
+
});
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Shadow colors are included in all output methods (`tokens()`, `tasty()`, `css()`, `json()`) alongside regular colors:
|
|
314
|
+
|
|
315
|
+
```ts
|
|
316
|
+
theme.tokens({ format: 'oklch' });
|
|
317
|
+
// light: { 'shadow-md': 'oklch(0.15 0.009 282 / 0.1)', ... }
|
|
318
|
+
// dark: { 'shadow-md': 'oklch(0.06 0.004 0 / 0.49)', ... }
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### How Shadows Work
|
|
322
|
+
|
|
323
|
+
The shadow algorithm computes a dark, low-saturation pigment color and an alpha value that produces the desired visual intensity:
|
|
324
|
+
|
|
325
|
+
1. **Contrast weight** — when `fg` is provided, shadow strength scales with `|l_bg - l_fg|`. Dark text on a light background produces a strong shadow; near-background-lightness elements produce barely visible shadows.
|
|
326
|
+
2. **Pigment color** — hue blended between fg and bg, low saturation, dark lightness.
|
|
327
|
+
3. **Alpha** — computed via a `tanh` curve that saturates smoothly toward `alphaMax` (default 0.6), ensuring well-separated shadow levels even on dark backgrounds.
|
|
328
|
+
|
|
329
|
+
### Achromatic Shadows
|
|
330
|
+
|
|
331
|
+
Omit `fg` for a pure achromatic shadow at full user-specified intensity:
|
|
332
|
+
|
|
333
|
+
```ts
|
|
334
|
+
theme.colors({
|
|
335
|
+
'drop-shadow': { type: 'shadow', bg: 'surface', intensity: 12 },
|
|
336
|
+
});
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### High-Contrast Intensity
|
|
340
|
+
|
|
341
|
+
`intensity` supports `[normal, highContrast]` pairs:
|
|
342
|
+
|
|
343
|
+
```ts
|
|
344
|
+
theme.colors({
|
|
345
|
+
'shadow-card': { type: 'shadow', bg: 'surface', fg: 'text', intensity: [10, 20] },
|
|
346
|
+
});
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### Fixed Opacity (Regular Colors)
|
|
350
|
+
|
|
351
|
+
For a simple fixed-alpha color (no shadow algorithm), use `opacity` on a regular color:
|
|
352
|
+
|
|
353
|
+
```ts
|
|
354
|
+
theme.colors({
|
|
355
|
+
overlay: { lightness: 0, opacity: 0.5 },
|
|
356
|
+
});
|
|
357
|
+
// → 'oklch(0 0 0 / 0.5)'
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### Shadow Tuning
|
|
361
|
+
|
|
362
|
+
Fine-tune shadow behavior per-color or globally:
|
|
363
|
+
|
|
364
|
+
```ts
|
|
365
|
+
// Per-color tuning
|
|
366
|
+
theme.colors({
|
|
367
|
+
'shadow-soft': {
|
|
368
|
+
type: 'shadow', bg: 'surface', intensity: 10,
|
|
369
|
+
tuning: { alphaMax: 0.3, saturationFactor: 0.1 },
|
|
370
|
+
},
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// Global tuning
|
|
374
|
+
glaze.configure({
|
|
375
|
+
shadowTuning: { alphaMax: 0.5, bgHueBlend: 0.3 },
|
|
376
|
+
});
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
Available tuning parameters:
|
|
380
|
+
|
|
381
|
+
| Parameter | Default | Description |
|
|
382
|
+
|---|---|---|
|
|
383
|
+
| `saturationFactor` | 0.18 | Fraction of fg saturation kept in pigment |
|
|
384
|
+
| `maxSaturation` | 0.25 | Upper clamp on pigment saturation |
|
|
385
|
+
| `lightnessFactor` | 0.25 | Multiplier for bg lightness to pigment lightness |
|
|
386
|
+
| `lightnessBounds` | [0.05, 0.20] | Clamp range for pigment lightness |
|
|
387
|
+
| `minGapTarget` | 0.05 | Target minimum gap between pigment and bg lightness |
|
|
388
|
+
| `alphaMax` | 0.6 | Asymptotic maximum alpha |
|
|
389
|
+
| `bgHueBlend` | 0.2 | Blend weight pulling pigment hue toward bg hue |
|
|
390
|
+
|
|
391
|
+
### Standalone Shadow Computation
|
|
392
|
+
|
|
393
|
+
Compute a shadow outside of a theme:
|
|
394
|
+
|
|
395
|
+
```ts
|
|
396
|
+
const v = glaze.shadow({
|
|
397
|
+
bg: '#f0eef5',
|
|
398
|
+
fg: '#1a1a2e',
|
|
399
|
+
intensity: 10,
|
|
400
|
+
});
|
|
401
|
+
// → { h: 280, s: 0.14, l: 0.2, alpha: 0.1 }
|
|
402
|
+
|
|
403
|
+
const css = glaze.format(v, 'oklch');
|
|
404
|
+
// → 'oklch(0.15 0.014 280 / 0.1)'
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### Consuming in CSS
|
|
408
|
+
|
|
409
|
+
```css
|
|
410
|
+
.card {
|
|
411
|
+
box-shadow: 0 2px 6px var(--shadow-sm-color),
|
|
412
|
+
0 8px 24px var(--shadow-md-color);
|
|
413
|
+
}
|
|
414
|
+
```
|
|
415
|
+
|
|
293
416
|
## Output Formats
|
|
294
417
|
|
|
295
418
|
Control the color format in exports with the `format` option:
|
|
296
419
|
|
|
297
420
|
```ts
|
|
298
421
|
// Default: OKHSL
|
|
299
|
-
theme.tokens(); // → 'okhsl(280
|
|
422
|
+
theme.tokens(); // → 'okhsl(280 60% 97%)'
|
|
300
423
|
|
|
301
|
-
// RGB
|
|
302
|
-
theme.tokens({ format: 'rgb' }); // → 'rgb(244
|
|
424
|
+
// RGB (modern space syntax, rounded integers)
|
|
425
|
+
theme.tokens({ format: 'rgb' }); // → 'rgb(244 240 250)'
|
|
303
426
|
|
|
304
|
-
// HSL
|
|
305
|
-
theme.tokens({ format: 'hsl' }); // → 'hsl(270.5
|
|
427
|
+
// HSL (modern space syntax)
|
|
428
|
+
theme.tokens({ format: 'hsl' }); // → 'hsl(270.5 45.2% 95.8%)'
|
|
306
429
|
|
|
307
430
|
// OKLCH
|
|
308
|
-
theme.tokens({ format: 'oklch' }); // → 'oklch(
|
|
431
|
+
theme.tokens({ format: 'oklch' }); // → 'oklch(0.965 0.0123 280)'
|
|
309
432
|
```
|
|
310
433
|
|
|
311
434
|
The `format` option works on all export methods: `theme.tokens()`, `theme.tasty()`, `theme.json()`, `theme.css()`, `palette.tokens()`, `palette.tasty()`, `palette.json()`, `palette.css()`, and standalone `glaze.color().token()` / `.tasty()` / `.json()`.
|
|
312
435
|
|
|
436
|
+
Colors with `alpha < 1` (shadow colors, or regular colors with `opacity`) include an alpha component:
|
|
437
|
+
|
|
438
|
+
```ts
|
|
439
|
+
// → 'oklch(0.15 0.009 282 / 0.1)'
|
|
440
|
+
// → 'rgb(34 28 42 / 0.1)'
|
|
441
|
+
```
|
|
442
|
+
|
|
313
443
|
Available formats:
|
|
314
444
|
|
|
315
|
-
| Format | Output | Notes |
|
|
316
|
-
|
|
317
|
-
| `'okhsl'` (default) | `okhsl(H S% L%)` | Native format,
|
|
318
|
-
| `'rgb'` | `rgb(R
|
|
319
|
-
| `'hsl'` | `hsl(H
|
|
320
|
-
| `'oklch'` | `oklch(L
|
|
445
|
+
| Format | Output (alpha = 1) | Output (alpha < 1) | Notes |
|
|
446
|
+
|---|---|---|---|
|
|
447
|
+
| `'okhsl'` (default) | `okhsl(H S% L%)` | `okhsl(H S% L% / A)` | Native format, not a CSS function |
|
|
448
|
+
| `'rgb'` | `rgb(R G B)` | `rgb(R G B / A)` | Rounded integers, space syntax |
|
|
449
|
+
| `'hsl'` | `hsl(H S% L%)` | `hsl(H S% L% / A)` | Modern space syntax |
|
|
450
|
+
| `'oklch'` | `oklch(L C H)` | `oklch(L C H / A)` | OKLab-based LCH |
|
|
451
|
+
|
|
452
|
+
All numeric output strips trailing zeros for cleaner CSS (e.g., `95` not `95.0`).
|
|
321
453
|
|
|
322
454
|
## Adaptation Modes
|
|
323
455
|
|
|
@@ -597,39 +729,40 @@ glaze.configure({
|
|
|
597
729
|
dark: true, // Include dark variants in exports
|
|
598
730
|
highContrast: false, // Include high-contrast variants
|
|
599
731
|
},
|
|
732
|
+
shadowTuning: { // Default tuning for all shadow colors
|
|
733
|
+
alphaMax: 0.6,
|
|
734
|
+
bgHueBlend: 0.2,
|
|
735
|
+
},
|
|
600
736
|
});
|
|
601
737
|
```
|
|
602
738
|
|
|
603
739
|
## Color Definition Shape
|
|
604
740
|
|
|
741
|
+
`ColorDef` is a discriminated union of regular colors and shadow colors:
|
|
742
|
+
|
|
605
743
|
```ts
|
|
606
|
-
type
|
|
607
|
-
type HCPair<T> = T | [T, T]; // [normal, high-contrast]
|
|
744
|
+
type ColorDef = RegularColorDef | ShadowColorDef;
|
|
608
745
|
|
|
609
|
-
interface
|
|
610
|
-
// Lightness
|
|
746
|
+
interface RegularColorDef {
|
|
611
747
|
lightness?: HCPair<number | RelativeValue>;
|
|
612
|
-
// Number: absolute (0–100)
|
|
613
|
-
// String: relative to base ('+N' / '-N')
|
|
614
|
-
|
|
615
|
-
// Hue override
|
|
616
|
-
hue?: number | RelativeValue;
|
|
617
|
-
// Number: absolute (0–360)
|
|
618
|
-
// String: relative to theme seed ('+N' / '-N')
|
|
619
|
-
|
|
620
|
-
// Saturation factor (0–1, default: 1)
|
|
621
748
|
saturation?: number;
|
|
749
|
+
hue?: number | RelativeValue;
|
|
750
|
+
base?: string;
|
|
751
|
+
contrast?: HCPair<MinContrast>;
|
|
752
|
+
mode?: 'auto' | 'fixed' | 'static';
|
|
753
|
+
opacity?: number; // fixed alpha (0–1)
|
|
754
|
+
}
|
|
622
755
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
//
|
|
628
|
-
|
|
756
|
+
interface ShadowColorDef {
|
|
757
|
+
type: 'shadow';
|
|
758
|
+
bg: string; // background color name (non-shadow)
|
|
759
|
+
fg?: string; // foreground color name (non-shadow)
|
|
760
|
+
intensity: HCPair<number>; // 0–100
|
|
761
|
+
tuning?: ShadowTuning;
|
|
629
762
|
}
|
|
630
763
|
```
|
|
631
764
|
|
|
632
|
-
A root color must have absolute `lightness` (a number). A dependent color must have `base`. Relative `lightness` (a string) requires `base`.
|
|
765
|
+
A root color must have absolute `lightness` (a number). A dependent color must have `base`. Relative `lightness` (a string) requires `base`. Shadow colors use `type: 'shadow'` and must reference a non-shadow `bg` color.
|
|
633
766
|
|
|
634
767
|
## Validation
|
|
635
768
|
|
|
@@ -642,6 +775,13 @@ A root color must have absolute `lightness` (a number). A dependent color must h
|
|
|
642
775
|
| `saturation` outside 0–1 | Clamp silently |
|
|
643
776
|
| Circular `base` references | Validation error |
|
|
644
777
|
| `base` references non-existent name | Validation error |
|
|
778
|
+
| Shadow `bg` references non-existent color | Validation error |
|
|
779
|
+
| Shadow `fg` references non-existent color | Validation error |
|
|
780
|
+
| Shadow `bg` references another shadow color | Validation error |
|
|
781
|
+
| Shadow `fg` references another shadow color | Validation error |
|
|
782
|
+
| Regular color `base` references a shadow color | Validation error |
|
|
783
|
+
| Shadow `intensity` outside 0–100 | Clamp silently |
|
|
784
|
+
| `contrast` + `opacity` combined | Warning |
|
|
645
785
|
|
|
646
786
|
## Advanced: Color Math Utilities
|
|
647
787
|
|
|
@@ -681,6 +821,14 @@ primary.colors({
|
|
|
681
821
|
'accent-fill': { lightness: 52, mode: 'fixed' },
|
|
682
822
|
'accent-text': { base: 'accent-fill', lightness: '+48', contrast: 'AA', mode: 'fixed' },
|
|
683
823
|
disabled: { lightness: 81, saturation: 0.4 },
|
|
824
|
+
|
|
825
|
+
// Shadow colors — computed alpha, automatic dark-mode adaptation
|
|
826
|
+
'shadow-sm': { type: 'shadow', bg: 'surface', fg: 'text', intensity: 5 },
|
|
827
|
+
'shadow-md': { type: 'shadow', bg: 'surface', fg: 'text', intensity: 10 },
|
|
828
|
+
'shadow-lg': { type: 'shadow', bg: 'surface', fg: 'text', intensity: 20 },
|
|
829
|
+
|
|
830
|
+
// Fixed-alpha overlay
|
|
831
|
+
overlay: { lightness: 0, opacity: 0.5 },
|
|
684
832
|
});
|
|
685
833
|
|
|
686
834
|
const danger = primary.extend({ hue: 23 });
|
|
@@ -692,21 +840,19 @@ const palette = glaze.palette({ primary, danger, success, warning, note });
|
|
|
692
840
|
|
|
693
841
|
// Export as flat token map grouped by variant
|
|
694
842
|
const tokens = palette.tokens({ prefix: true });
|
|
695
|
-
// tokens.light → { 'primary-surface': 'okhsl(...)', '
|
|
696
|
-
// tokens.dark → { 'primary-surface': 'okhsl(...)', 'danger-surface': 'okhsl(...)' }
|
|
843
|
+
// tokens.light → { 'primary-surface': 'okhsl(...)', 'primary-shadow-md': 'okhsl(... / 0.1)' }
|
|
697
844
|
|
|
698
845
|
// Export as tasty style-to-state bindings (for Tasty style system)
|
|
699
846
|
const tastyTokens = palette.tasty({ prefix: true });
|
|
700
|
-
// tastyTokens['#primary-surface'] → { '': 'okhsl(...)', '@dark': 'okhsl(...)' }
|
|
701
|
-
// Use as a recipe or spread into component styles (see Tasty Export section)
|
|
702
|
-
|
|
703
|
-
// Export as RGB for broader CSS compatibility
|
|
704
|
-
const rgbTokens = palette.tokens({ prefix: true, format: 'rgb' });
|
|
705
847
|
|
|
706
848
|
// Export as CSS custom properties (rgb format by default)
|
|
707
849
|
const css = palette.css({ prefix: true });
|
|
708
|
-
// css.light → "--primary-surface-color: rgb(...);\n--
|
|
709
|
-
|
|
850
|
+
// css.light → "--primary-surface-color: rgb(...);\n--primary-shadow-md-color: rgb(... / 0.1);"
|
|
851
|
+
|
|
852
|
+
// Standalone shadow computation
|
|
853
|
+
const v = glaze.shadow({ bg: '#f0eef5', fg: '#1a1a2e', intensity: 10 });
|
|
854
|
+
const shadowCss = glaze.format(v, 'oklch');
|
|
855
|
+
// → 'oklch(0.15 0.014 280 / 0.1)'
|
|
710
856
|
|
|
711
857
|
// Save and restore a theme
|
|
712
858
|
const snapshot = primary.export();
|
|
@@ -729,6 +875,8 @@ brand.colors({ surface: { lightness: 97 }, text: { base: 'surface', lightness: '
|
|
|
729
875
|
| `glaze.fromHex(hex)` | Create a theme from a hex color (`#rgb` or `#rrggbb`) |
|
|
730
876
|
| `glaze.fromRgb(r, g, b)` | Create a theme from RGB values (0–255) |
|
|
731
877
|
| `glaze.color(input)` | Create a standalone color token |
|
|
878
|
+
| `glaze.shadow(input)` | Compute a standalone shadow color (returns `ResolvedColorVariant`) |
|
|
879
|
+
| `glaze.format(variant, format?)` | Format any `ResolvedColorVariant` as a CSS string |
|
|
732
880
|
|
|
733
881
|
### Theme Methods
|
|
734
882
|
|
package/dist/index.cjs
CHANGED
|
@@ -441,23 +441,26 @@ function parseHex(hex) {
|
|
|
441
441
|
}
|
|
442
442
|
return null;
|
|
443
443
|
}
|
|
444
|
+
function fmt$1(value, decimals) {
|
|
445
|
+
return parseFloat(value.toFixed(decimals)).toString();
|
|
446
|
+
}
|
|
444
447
|
/**
|
|
445
448
|
* Format OKHSL values as a CSS `okhsl(H S% L%)` string.
|
|
446
449
|
* h: 0–360, s: 0–100, l: 0–100 (percentage scale for s and l).
|
|
447
450
|
*/
|
|
448
451
|
function formatOkhsl(h, s, l) {
|
|
449
|
-
return `okhsl(${h
|
|
452
|
+
return `okhsl(${fmt$1(h, 1)} ${fmt$1(s, 1)}% ${fmt$1(l, 1)}%)`;
|
|
450
453
|
}
|
|
451
454
|
/**
|
|
452
|
-
* Format OKHSL values as a CSS `rgb(R
|
|
455
|
+
* Format OKHSL values as a CSS `rgb(R G B)` string with rounded integer values.
|
|
453
456
|
* h: 0–360, s: 0–100, l: 0–100 (percentage scale for s and l).
|
|
454
457
|
*/
|
|
455
458
|
function formatRgb(h, s, l) {
|
|
456
459
|
const [r, g, b] = okhslToSrgb(h, s / 100, l / 100);
|
|
457
|
-
return `rgb(${(r * 255)
|
|
460
|
+
return `rgb(${Math.round(r * 255)} ${Math.round(g * 255)} ${Math.round(b * 255)})`;
|
|
458
461
|
}
|
|
459
462
|
/**
|
|
460
|
-
* Format OKHSL values as a CSS `hsl(H
|
|
463
|
+
* Format OKHSL values as a CSS `hsl(H S% L%)` string.
|
|
461
464
|
* h: 0–360, s: 0–100, l: 0–100 (percentage scale for s and l).
|
|
462
465
|
*/
|
|
463
466
|
function formatHsl(h, s, l) {
|
|
@@ -474,10 +477,10 @@ function formatHsl(h, s, l) {
|
|
|
474
477
|
else if (max === g) hh = ((b - r) / delta + 2) * 60;
|
|
475
478
|
else hh = ((r - g) / delta + 4) * 60;
|
|
476
479
|
}
|
|
477
|
-
return `hsl(${hh
|
|
480
|
+
return `hsl(${fmt$1(hh, 1)} ${fmt$1(ss * 100, 1)}% ${fmt$1(ll * 100, 1)}%)`;
|
|
478
481
|
}
|
|
479
482
|
/**
|
|
480
|
-
* Format OKHSL values as a CSS `oklch(L
|
|
483
|
+
* Format OKHSL values as a CSS `oklch(L C H)` string.
|
|
481
484
|
* h: 0–360, s: 0–100, l: 0–100 (percentage scale for s and l).
|
|
482
485
|
*/
|
|
483
486
|
function formatOklch(h, s, l) {
|
|
@@ -485,7 +488,7 @@ function formatOklch(h, s, l) {
|
|
|
485
488
|
const C = Math.sqrt(a * a + b * b);
|
|
486
489
|
let hh = Math.atan2(b, a) * (180 / Math.PI);
|
|
487
490
|
hh = constrainAngle(hh);
|
|
488
|
-
return `oklch(${(L
|
|
491
|
+
return `oklch(${fmt$1(L, 4)} ${fmt$1(C, 4)} ${fmt$1(hh, 1)})`;
|
|
489
492
|
}
|
|
490
493
|
|
|
491
494
|
//#endregion
|
|
@@ -711,14 +714,70 @@ function pairNormal(p) {
|
|
|
711
714
|
function pairHC(p) {
|
|
712
715
|
return Array.isArray(p) ? p[1] : p;
|
|
713
716
|
}
|
|
717
|
+
function isShadowDef(def) {
|
|
718
|
+
return def.type === "shadow";
|
|
719
|
+
}
|
|
720
|
+
const DEFAULT_SHADOW_TUNING = {
|
|
721
|
+
saturationFactor: .18,
|
|
722
|
+
maxSaturation: .25,
|
|
723
|
+
lightnessFactor: .25,
|
|
724
|
+
lightnessBounds: [.05, .2],
|
|
725
|
+
minGapTarget: .05,
|
|
726
|
+
alphaMax: .6,
|
|
727
|
+
bgHueBlend: .2
|
|
728
|
+
};
|
|
729
|
+
function resolveShadowTuning(perColor) {
|
|
730
|
+
return {
|
|
731
|
+
...DEFAULT_SHADOW_TUNING,
|
|
732
|
+
...globalConfig.shadowTuning,
|
|
733
|
+
...perColor,
|
|
734
|
+
lightnessBounds: perColor?.lightnessBounds ?? globalConfig.shadowTuning?.lightnessBounds ?? DEFAULT_SHADOW_TUNING.lightnessBounds
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
function circularLerp(a, b, t) {
|
|
738
|
+
let diff = b - a;
|
|
739
|
+
if (diff > 180) diff -= 360;
|
|
740
|
+
else if (diff < -180) diff += 360;
|
|
741
|
+
return ((a + diff * t) % 360 + 360) % 360;
|
|
742
|
+
}
|
|
743
|
+
function computeShadow(bg, fg, intensity, tuning) {
|
|
744
|
+
const EPSILON = 1e-6;
|
|
745
|
+
const clampedIntensity = clamp(intensity, 0, 100);
|
|
746
|
+
const contrastWeight = fg ? Math.abs(bg.l - fg.l) : 1;
|
|
747
|
+
const deltaL = clampedIntensity / 100 * contrastWeight;
|
|
748
|
+
const h = fg ? circularLerp(fg.h, bg.h, tuning.bgHueBlend) : bg.h;
|
|
749
|
+
const s = fg ? Math.min(fg.s * tuning.saturationFactor, tuning.maxSaturation) : 0;
|
|
750
|
+
let lSh = clamp(bg.l * tuning.lightnessFactor, tuning.lightnessBounds[0], tuning.lightnessBounds[1]);
|
|
751
|
+
lSh = Math.max(Math.min(lSh, bg.l - tuning.minGapTarget), 0);
|
|
752
|
+
const t = deltaL / Math.max(bg.l - lSh, EPSILON);
|
|
753
|
+
const alpha = tuning.alphaMax * Math.tanh(t / tuning.alphaMax);
|
|
754
|
+
return {
|
|
755
|
+
h,
|
|
756
|
+
s,
|
|
757
|
+
l: lSh,
|
|
758
|
+
alpha
|
|
759
|
+
};
|
|
760
|
+
}
|
|
714
761
|
function validateColorDefs(defs) {
|
|
715
762
|
const names = new Set(Object.keys(defs));
|
|
716
763
|
for (const [name, def] of Object.entries(defs)) {
|
|
717
|
-
if (def
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
764
|
+
if (isShadowDef(def)) {
|
|
765
|
+
if (!names.has(def.bg)) throw new Error(`glaze: shadow "${name}" references non-existent bg "${def.bg}".`);
|
|
766
|
+
if (isShadowDef(defs[def.bg])) throw new Error(`glaze: shadow "${name}" bg "${def.bg}" references another shadow color.`);
|
|
767
|
+
if (def.fg !== void 0) {
|
|
768
|
+
if (!names.has(def.fg)) throw new Error(`glaze: shadow "${name}" references non-existent fg "${def.fg}".`);
|
|
769
|
+
if (isShadowDef(defs[def.fg])) throw new Error(`glaze: shadow "${name}" fg "${def.fg}" references another shadow color.`);
|
|
770
|
+
}
|
|
771
|
+
continue;
|
|
772
|
+
}
|
|
773
|
+
const regDef = def;
|
|
774
|
+
if (regDef.contrast !== void 0 && !regDef.base) throw new Error(`glaze: color "${name}" has "contrast" without "base".`);
|
|
775
|
+
if (regDef.lightness !== void 0 && !isAbsoluteLightness(regDef.lightness) && !regDef.base) throw new Error(`glaze: color "${name}" has relative "lightness" without "base".`);
|
|
776
|
+
if (isAbsoluteLightness(regDef.lightness) && regDef.base !== void 0) console.warn(`glaze: color "${name}" has absolute "lightness" and "base". Absolute lightness takes precedence.`);
|
|
777
|
+
if (regDef.base && !names.has(regDef.base)) throw new Error(`glaze: color "${name}" references non-existent base "${regDef.base}".`);
|
|
778
|
+
if (regDef.base && isShadowDef(defs[regDef.base])) throw new Error(`glaze: color "${name}" base "${regDef.base}" references a shadow color.`);
|
|
779
|
+
if (!isAbsoluteLightness(regDef.lightness) && regDef.base === void 0) throw new Error(`glaze: color "${name}" must have either absolute "lightness" (root) or "base" (dependent).`);
|
|
780
|
+
if (regDef.contrast !== void 0 && regDef.opacity !== void 0) console.warn(`glaze: color "${name}" has both "contrast" and "opacity". Opacity makes perceived lightness unpredictable.`);
|
|
722
781
|
}
|
|
723
782
|
const visited = /* @__PURE__ */ new Set();
|
|
724
783
|
const inStack = /* @__PURE__ */ new Set();
|
|
@@ -727,7 +786,13 @@ function validateColorDefs(defs) {
|
|
|
727
786
|
if (visited.has(name)) return;
|
|
728
787
|
inStack.add(name);
|
|
729
788
|
const def = defs[name];
|
|
730
|
-
if (
|
|
789
|
+
if (isShadowDef(def)) {
|
|
790
|
+
dfs(def.bg);
|
|
791
|
+
if (def.fg) dfs(def.fg);
|
|
792
|
+
} else {
|
|
793
|
+
const regDef = def;
|
|
794
|
+
if (regDef.base && !isAbsoluteLightness(regDef.lightness)) dfs(regDef.base);
|
|
795
|
+
}
|
|
731
796
|
inStack.delete(name);
|
|
732
797
|
visited.add(name);
|
|
733
798
|
}
|
|
@@ -740,7 +805,13 @@ function topoSort(defs) {
|
|
|
740
805
|
if (visited.has(name)) return;
|
|
741
806
|
visited.add(name);
|
|
742
807
|
const def = defs[name];
|
|
743
|
-
if (
|
|
808
|
+
if (isShadowDef(def)) {
|
|
809
|
+
visit(def.bg);
|
|
810
|
+
if (def.fg) visit(def.fg);
|
|
811
|
+
} else {
|
|
812
|
+
const regDef = def;
|
|
813
|
+
if (regDef.base && !isAbsoluteLightness(regDef.lightness)) visit(regDef.base);
|
|
814
|
+
}
|
|
744
815
|
result.push(name);
|
|
745
816
|
}
|
|
746
817
|
for (const name of Object.keys(defs)) visit(name);
|
|
@@ -804,11 +875,8 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
|
|
|
804
875
|
if (!baseResolved) throw new Error(`glaze: base "${baseName}" not yet resolved for "${name}".`);
|
|
805
876
|
const mode = def.mode ?? "auto";
|
|
806
877
|
const satFactor = clamp(def.saturation ?? 1, 0, 1);
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
else if (isDark) baseL = baseResolved.dark.l * 100;
|
|
810
|
-
else if (isHighContrast) baseL = baseResolved.lightContrast.l * 100;
|
|
811
|
-
else baseL = baseResolved.light.l * 100;
|
|
878
|
+
const baseVariant = getSchemeVariant(baseResolved, isDark, isHighContrast);
|
|
879
|
+
const baseL = baseVariant.l * 100;
|
|
812
880
|
let preferredL;
|
|
813
881
|
const rawLightness = def.lightness;
|
|
814
882
|
if (rawLightness === void 0) preferredL = baseL;
|
|
@@ -825,27 +893,7 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
|
|
|
825
893
|
if (rawContrast !== void 0) {
|
|
826
894
|
const minCr = isHighContrast ? pairHC(rawContrast) : pairNormal(rawContrast);
|
|
827
895
|
const effectiveSat = isDark ? mapSaturationDark(satFactor * ctx.saturation / 100, mode) : satFactor * ctx.saturation / 100;
|
|
828
|
-
|
|
829
|
-
let baseS;
|
|
830
|
-
let baseLNorm;
|
|
831
|
-
if (isDark && isHighContrast) {
|
|
832
|
-
baseH = baseResolved.darkContrast.h;
|
|
833
|
-
baseS = baseResolved.darkContrast.s;
|
|
834
|
-
baseLNorm = baseResolved.darkContrast.l;
|
|
835
|
-
} else if (isDark) {
|
|
836
|
-
baseH = baseResolved.dark.h;
|
|
837
|
-
baseS = baseResolved.dark.s;
|
|
838
|
-
baseLNorm = baseResolved.dark.l;
|
|
839
|
-
} else if (isHighContrast) {
|
|
840
|
-
baseH = baseResolved.lightContrast.h;
|
|
841
|
-
baseS = baseResolved.lightContrast.s;
|
|
842
|
-
baseLNorm = baseResolved.lightContrast.l;
|
|
843
|
-
} else {
|
|
844
|
-
baseH = baseResolved.light.h;
|
|
845
|
-
baseS = baseResolved.light.s;
|
|
846
|
-
baseLNorm = baseResolved.light.l;
|
|
847
|
-
}
|
|
848
|
-
const baseLinearRgb = okhslToLinearSrgb(baseH, baseS, baseLNorm);
|
|
896
|
+
const baseLinearRgb = okhslToLinearSrgb(baseVariant.h, baseVariant.s, baseVariant.l);
|
|
849
897
|
return {
|
|
850
898
|
l: findLightnessForContrast({
|
|
851
899
|
hue: effectiveHue,
|
|
@@ -862,18 +910,26 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
|
|
|
862
910
|
satFactor
|
|
863
911
|
};
|
|
864
912
|
}
|
|
913
|
+
function getSchemeVariant(color, isDark, isHighContrast) {
|
|
914
|
+
if (isDark && isHighContrast) return color.darkContrast;
|
|
915
|
+
if (isDark) return color.dark;
|
|
916
|
+
if (isHighContrast) return color.lightContrast;
|
|
917
|
+
return color.light;
|
|
918
|
+
}
|
|
865
919
|
function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
866
|
-
|
|
867
|
-
const
|
|
868
|
-
const
|
|
920
|
+
if (isShadowDef(def)) return resolveShadowForScheme(def, ctx, isDark, isHighContrast);
|
|
921
|
+
const regDef = def;
|
|
922
|
+
const mode = regDef.mode ?? "auto";
|
|
923
|
+
const isRoot = isAbsoluteLightness(regDef.lightness) && !regDef.base;
|
|
924
|
+
const effectiveHue = resolveEffectiveHue(ctx.hue, regDef.hue);
|
|
869
925
|
let lightL;
|
|
870
926
|
let satFactor;
|
|
871
927
|
if (isRoot) {
|
|
872
|
-
const root = resolveRootColor(name,
|
|
928
|
+
const root = resolveRootColor(name, regDef, ctx, isHighContrast);
|
|
873
929
|
lightL = root.lightL;
|
|
874
930
|
satFactor = root.satFactor;
|
|
875
931
|
} else {
|
|
876
|
-
const dep = resolveDependentColor(name,
|
|
932
|
+
const dep = resolveDependentColor(name, regDef, ctx, isHighContrast, isDark, effectiveHue);
|
|
877
933
|
lightL = dep.l;
|
|
878
934
|
satFactor = dep.satFactor;
|
|
879
935
|
}
|
|
@@ -892,9 +948,18 @@ function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
|
892
948
|
return {
|
|
893
949
|
h: effectiveHue,
|
|
894
950
|
s: clamp(finalSat, 0, 1),
|
|
895
|
-
l: clamp(finalL / 100, 0, 1)
|
|
951
|
+
l: clamp(finalL / 100, 0, 1),
|
|
952
|
+
alpha: regDef.opacity ?? 1
|
|
896
953
|
};
|
|
897
954
|
}
|
|
955
|
+
function resolveShadowForScheme(def, ctx, isDark, isHighContrast) {
|
|
956
|
+
const bgVariant = getSchemeVariant(ctx.resolved.get(def.bg), isDark, isHighContrast);
|
|
957
|
+
let fgVariant;
|
|
958
|
+
if (def.fg) fgVariant = getSchemeVariant(ctx.resolved.get(def.fg), isDark, isHighContrast);
|
|
959
|
+
const intensity = isHighContrast ? pairHC(def.intensity) : pairNormal(def.intensity);
|
|
960
|
+
const tuning = resolveShadowTuning(def.tuning);
|
|
961
|
+
return computeShadow(bgVariant, fgVariant, intensity, tuning);
|
|
962
|
+
}
|
|
898
963
|
function resolveAllColors(hue, saturation, defs) {
|
|
899
964
|
validateColorDefs(defs);
|
|
900
965
|
const order = topoSort(defs);
|
|
@@ -904,6 +969,9 @@ function resolveAllColors(hue, saturation, defs) {
|
|
|
904
969
|
defs,
|
|
905
970
|
resolved: /* @__PURE__ */ new Map()
|
|
906
971
|
};
|
|
972
|
+
function defMode(def) {
|
|
973
|
+
return isShadowDef(def) ? void 0 : def.mode ?? "auto";
|
|
974
|
+
}
|
|
907
975
|
const lightMap = /* @__PURE__ */ new Map();
|
|
908
976
|
for (const name of order) {
|
|
909
977
|
const variant = resolveColorForScheme(name, defs[name], ctx, false, false);
|
|
@@ -914,7 +982,7 @@ function resolveAllColors(hue, saturation, defs) {
|
|
|
914
982
|
dark: variant,
|
|
915
983
|
lightContrast: variant,
|
|
916
984
|
darkContrast: variant,
|
|
917
|
-
mode: defs[name]
|
|
985
|
+
mode: defMode(defs[name])
|
|
918
986
|
});
|
|
919
987
|
}
|
|
920
988
|
const lightHCMap = /* @__PURE__ */ new Map();
|
|
@@ -937,7 +1005,7 @@ function resolveAllColors(hue, saturation, defs) {
|
|
|
937
1005
|
dark: lightMap.get(name),
|
|
938
1006
|
lightContrast: lightHCMap.get(name),
|
|
939
1007
|
darkContrast: lightHCMap.get(name),
|
|
940
|
-
mode: defs[name]
|
|
1008
|
+
mode: defMode(defs[name])
|
|
941
1009
|
});
|
|
942
1010
|
for (const name of order) {
|
|
943
1011
|
const variant = resolveColorForScheme(name, defs[name], ctx, true, false);
|
|
@@ -967,7 +1035,7 @@ function resolveAllColors(hue, saturation, defs) {
|
|
|
967
1035
|
dark: darkMap.get(name),
|
|
968
1036
|
lightContrast: lightHCMap.get(name),
|
|
969
1037
|
darkContrast: darkHCMap.get(name),
|
|
970
|
-
mode: defs[name]
|
|
1038
|
+
mode: defMode(defs[name])
|
|
971
1039
|
});
|
|
972
1040
|
return result;
|
|
973
1041
|
}
|
|
@@ -977,8 +1045,14 @@ const formatters = {
|
|
|
977
1045
|
hsl: formatHsl,
|
|
978
1046
|
oklch: formatOklch
|
|
979
1047
|
};
|
|
1048
|
+
function fmt(value, decimals) {
|
|
1049
|
+
return parseFloat(value.toFixed(decimals)).toString();
|
|
1050
|
+
}
|
|
980
1051
|
function formatVariant(v, format = "okhsl") {
|
|
981
|
-
|
|
1052
|
+
const base = formatters[format](v.h, v.s * 100, v.l * 100);
|
|
1053
|
+
if (v.alpha >= 1) return base;
|
|
1054
|
+
const closing = base.lastIndexOf(")");
|
|
1055
|
+
return `${base.slice(0, closing)} / ${fmt(v.alpha, 4)})`;
|
|
982
1056
|
}
|
|
983
1057
|
function resolveModes(override) {
|
|
984
1058
|
return {
|
|
@@ -1229,7 +1303,8 @@ glaze.configure = function configure(config) {
|
|
|
1229
1303
|
modes: {
|
|
1230
1304
|
dark: config.modes?.dark ?? globalConfig.modes.dark,
|
|
1231
1305
|
highContrast: config.modes?.highContrast ?? globalConfig.modes.highContrast
|
|
1232
|
-
}
|
|
1306
|
+
},
|
|
1307
|
+
shadowTuning: config.shadowTuning ?? globalConfig.shadowTuning
|
|
1233
1308
|
};
|
|
1234
1309
|
};
|
|
1235
1310
|
/**
|
|
@@ -1251,6 +1326,40 @@ glaze.color = function color(input) {
|
|
|
1251
1326
|
return createColorToken(input);
|
|
1252
1327
|
};
|
|
1253
1328
|
/**
|
|
1329
|
+
* Compute a shadow color from a bg/fg pair and intensity.
|
|
1330
|
+
*/
|
|
1331
|
+
glaze.shadow = function shadow(input) {
|
|
1332
|
+
const bg = parseOkhslInput(input.bg);
|
|
1333
|
+
const fg = input.fg ? parseOkhslInput(input.fg) : void 0;
|
|
1334
|
+
const tuning = resolveShadowTuning(input.tuning);
|
|
1335
|
+
return computeShadow({
|
|
1336
|
+
...bg,
|
|
1337
|
+
alpha: 1
|
|
1338
|
+
}, fg ? {
|
|
1339
|
+
...fg,
|
|
1340
|
+
alpha: 1
|
|
1341
|
+
} : void 0, input.intensity, tuning);
|
|
1342
|
+
};
|
|
1343
|
+
/**
|
|
1344
|
+
* Format a resolved color variant as a CSS string.
|
|
1345
|
+
*/
|
|
1346
|
+
glaze.format = function format(variant, colorFormat) {
|
|
1347
|
+
return formatVariant(variant, colorFormat);
|
|
1348
|
+
};
|
|
1349
|
+
function parseOkhslInput(input) {
|
|
1350
|
+
if (typeof input === "string") {
|
|
1351
|
+
const rgb = parseHex(input);
|
|
1352
|
+
if (!rgb) throw new Error(`glaze: invalid hex color "${input}".`);
|
|
1353
|
+
const [h, s, l] = srgbToOkhsl(rgb);
|
|
1354
|
+
return {
|
|
1355
|
+
h,
|
|
1356
|
+
s,
|
|
1357
|
+
l
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
return input;
|
|
1361
|
+
}
|
|
1362
|
+
/**
|
|
1254
1363
|
* Create a theme from a hex color string.
|
|
1255
1364
|
* Extracts hue and saturation from the color.
|
|
1256
1365
|
*/
|