@tenphi/glaze 0.3.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 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
@@ -70,7 +71,7 @@ const success = primary.extend({ hue: 157 });
70
71
  // Compose into a palette and export
71
72
  const palette = glaze.palette({ primary, danger, success });
72
73
  const tokens = palette.tokens({ prefix: true });
73
- // → { '#primary-surface': { '': 'okhsl(...)', '@dark': 'okhsl(...)' }, ... }
74
+ // → { light: { 'primary-surface': 'okhsl(...)', ... }, dark: { 'primary-surface': 'okhsl(...)', ... } }
74
75
  ```
75
76
 
76
77
  ## Core Concepts
@@ -262,7 +263,8 @@ Create a single color token without a full theme:
262
263
  const accent = glaze.color({ hue: 280, saturation: 80, lightness: 52, mode: 'fixed' });
263
264
 
264
265
  accent.resolve(); // → ResolvedColor with light/dark/lightContrast/darkContrast
265
- accent.token(); // → { '': 'okhsl(...)', '@dark': 'okhsl(...)' }
266
+ accent.token(); // → { '': 'okhsl(...)', '@dark': 'okhsl(...)' } (tasty format)
267
+ accent.tasty(); // → { '': 'okhsl(...)', '@dark': 'okhsl(...)' } (same as token)
266
268
  accent.json(); // → { light: 'okhsl(...)', dark: 'okhsl(...)' }
267
269
  ```
268
270
 
@@ -289,34 +291,165 @@ brand.colors({
289
291
  });
290
292
  ```
291
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
+
292
416
  ## Output Formats
293
417
 
294
418
  Control the color format in exports with the `format` option:
295
419
 
296
420
  ```ts
297
421
  // Default: OKHSL
298
- theme.tokens(); // → 'okhsl(280.0 60.0% 97.0%)'
422
+ theme.tokens(); // → 'okhsl(280 60% 97%)'
299
423
 
300
- // RGB with fractional precision
301
- theme.tokens({ format: 'rgb' }); // → 'rgb(244.123, 240.456, 249.789)'
424
+ // RGB (modern space syntax, rounded integers)
425
+ theme.tokens({ format: 'rgb' }); // → 'rgb(244 240 250)'
302
426
 
303
- // HSL
304
- theme.tokens({ format: 'hsl' }); // → 'hsl(270.5, 45.2%, 95.8%)'
427
+ // HSL (modern space syntax)
428
+ theme.tokens({ format: 'hsl' }); // → 'hsl(270.5 45.2% 95.8%)'
305
429
 
306
430
  // OKLCH
307
- theme.tokens({ format: 'oklch' }); // → 'oklch(96.5% 0.0123 280.0)'
431
+ theme.tokens({ format: 'oklch' }); // → 'oklch(0.965 0.0123 280)'
308
432
  ```
309
433
 
310
- The `format` option works on all export methods: `theme.tokens()`, `theme.json()`, `theme.css()`, `palette.tokens()`, `palette.json()`, `palette.css()`, and standalone `glaze.color().token()` / `.json()`.
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()`.
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
+ ```
311
442
 
312
443
  Available formats:
313
444
 
314
- | Format | Output | Notes |
315
- |---|---|---|
316
- | `'okhsl'` (default) | `okhsl(H S% L%)` | Native format, perceptually uniform |
317
- | `'rgb'` | `rgb(R, G, B)` | Fractional 0–255 values (3 decimals) |
318
- | `'hsl'` | `hsl(H, S%, L%)` | Standard CSS HSL |
319
- | `'oklch'` | `oklch(L% C H)` | OKLab-based LCH |
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`).
320
453
 
321
454
  ## Adaptation Modes
322
455
 
@@ -410,18 +543,93 @@ const palette = glaze.palette({ primary, danger, success, warning });
410
543
 
411
544
  ### Token Export
412
545
 
546
+ Tokens are grouped by scheme variant, with plain color names as keys:
547
+
413
548
  ```ts
414
549
  const tokens = palette.tokens({ prefix: true });
415
550
  // → {
551
+ // light: { 'primary-surface': 'okhsl(...)', 'danger-surface': 'okhsl(...)' },
552
+ // dark: { 'primary-surface': 'okhsl(...)', 'danger-surface': 'okhsl(...)' },
553
+ // }
554
+ ```
555
+
556
+ Custom prefix mapping:
557
+
558
+ ```ts
559
+ palette.tokens({ prefix: { primary: 'brand-', danger: 'error-' } });
560
+ ```
561
+
562
+ ### Tasty Export (for [Tasty](https://cube-ui-kit.vercel.app/?path=/docs/tasty-documentation--docs) style system)
563
+
564
+ 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.):
565
+
566
+ ```ts
567
+ const tastyTokens = palette.tasty({ prefix: true });
568
+ // → {
416
569
  // '#primary-surface': { '': 'okhsl(...)', '@dark': 'okhsl(...)' },
417
570
  // '#danger-surface': { '': 'okhsl(...)', '@dark': 'okhsl(...)' },
418
571
  // }
419
572
  ```
420
573
 
574
+ Apply as global styles to make color tokens available app-wide:
575
+
576
+ ```ts
577
+ import { useGlobalStyles } from '@cube-dev/ui-kit';
578
+
579
+ // In your root component
580
+ useGlobalStyles('body', tastyTokens);
581
+ ```
582
+
583
+ For zero-runtime builds, use `tastyStatic` to generate the CSS at build time:
584
+
585
+ ```ts
586
+ import { tastyStatic } from '@cube-dev/ui-kit';
587
+
588
+ tastyStatic('body', tastyTokens);
589
+ ```
590
+
591
+ Alternatively, register as a recipe via `configure()`:
592
+
593
+ ```ts
594
+ import { configure, tasty } from '@cube-dev/ui-kit';
595
+
596
+ configure({
597
+ recipes: {
598
+ 'all-themes': tastyTokens,
599
+ },
600
+ });
601
+
602
+ const Page = tasty({
603
+ styles: {
604
+ recipe: 'all-themes',
605
+ fill: '#primary-surface',
606
+ color: '#primary-text',
607
+ },
608
+ });
609
+ ```
610
+
611
+ Or spread directly into component styles:
612
+
613
+ ```ts
614
+ const Card = tasty({
615
+ styles: {
616
+ ...tastyTokens,
617
+ fill: '#primary-surface',
618
+ color: '#primary-text',
619
+ },
620
+ });
621
+ ```
622
+
421
623
  Custom prefix mapping:
422
624
 
423
625
  ```ts
424
- palette.tokens({ prefix: { primary: 'brand-', danger: 'error-' } });
626
+ palette.tasty({ prefix: { primary: 'brand-', danger: 'error-' } });
627
+ ```
628
+
629
+ Custom state aliases:
630
+
631
+ ```ts
632
+ palette.tasty({ states: { dark: '@dark', highContrast: '@hc' } });
425
633
  ```
426
634
 
427
635
  ### JSON Export (Framework-Agnostic)
@@ -488,17 +696,22 @@ Control which scheme variants appear in exports:
488
696
  ```ts
489
697
  // Light only
490
698
  palette.tokens({ modes: { dark: false, highContrast: false } });
699
+ // → { light: { ... } }
491
700
 
492
701
  // Light + dark (default)
493
702
  palette.tokens({ modes: { highContrast: false } });
703
+ // → { light: { ... }, dark: { ... } }
494
704
 
495
705
  // All four variants
496
706
  palette.tokens({ modes: { dark: true, highContrast: true } });
707
+ // → { light: { ... }, dark: { ... }, lightContrast: { ... }, darkContrast: { ... } }
497
708
  ```
498
709
 
710
+ The `modes` option works the same way on `tokens()`, `tasty()`, `json()`, and `css()`.
711
+
499
712
  Resolution priority (highest first):
500
713
 
501
- 1. `tokens({ modes })` / `json({ modes })` / `css({ ... })` — per-call override
714
+ 1. `tokens({ modes })` / `tasty({ modes })` / `json({ modes })` / `css({ ... })` — per-call override
502
715
  2. `glaze.configure({ modes })` — global config
503
716
  3. Built-in default: `{ dark: true, highContrast: false }`
504
717
 
@@ -516,39 +729,40 @@ glaze.configure({
516
729
  dark: true, // Include dark variants in exports
517
730
  highContrast: false, // Include high-contrast variants
518
731
  },
732
+ shadowTuning: { // Default tuning for all shadow colors
733
+ alphaMax: 0.6,
734
+ bgHueBlend: 0.2,
735
+ },
519
736
  });
520
737
  ```
521
738
 
522
739
  ## Color Definition Shape
523
740
 
741
+ `ColorDef` is a discriminated union of regular colors and shadow colors:
742
+
524
743
  ```ts
525
- type RelativeValue = `+${number}` | `-${number}`;
526
- type HCPair<T> = T | [T, T]; // [normal, high-contrast]
744
+ type ColorDef = RegularColorDef | ShadowColorDef;
527
745
 
528
- interface ColorDef {
529
- // Lightness
746
+ interface RegularColorDef {
530
747
  lightness?: HCPair<number | RelativeValue>;
531
- // Number: absolute (0–100)
532
- // String: relative to base ('+N' / '-N')
533
-
534
- // Hue override
535
- hue?: number | RelativeValue;
536
- // Number: absolute (0–360)
537
- // String: relative to theme seed ('+N' / '-N')
538
-
539
- // Saturation factor (0–1, default: 1)
540
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
+ }
541
755
 
542
- // Dependency
543
- base?: string; // name of another color
544
- contrast?: HCPair<MinContrast>; // WCAG contrast ratio floor against base
545
-
546
- // Adaptation mode
547
- mode?: 'auto' | 'fixed' | 'static'; // default: 'auto'
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;
548
762
  }
549
763
  ```
550
764
 
551
- 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.
552
766
 
553
767
  ## Validation
554
768
 
@@ -561,6 +775,13 @@ A root color must have absolute `lightness` (a number). A dependent color must h
561
775
  | `saturation` outside 0–1 | Clamp silently |
562
776
  | Circular `base` references | Validation error |
563
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 |
564
785
 
565
786
  ## Advanced: Color Math Utilities
566
787
 
@@ -600,6 +821,14 @@ primary.colors({
600
821
  'accent-fill': { lightness: 52, mode: 'fixed' },
601
822
  'accent-text': { base: 'accent-fill', lightness: '+48', contrast: 'AA', mode: 'fixed' },
602
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 },
603
832
  });
604
833
 
605
834
  const danger = primary.extend({ hue: 23 });
@@ -609,16 +838,21 @@ const note = primary.extend({ hue: 302 });
609
838
 
610
839
  const palette = glaze.palette({ primary, danger, success, warning, note });
611
840
 
612
- // Export as OKHSL tokens (default)
841
+ // Export as flat token map grouped by variant
613
842
  const tokens = palette.tokens({ prefix: true });
843
+ // tokens.light → { 'primary-surface': 'okhsl(...)', 'primary-shadow-md': 'okhsl(... / 0.1)' }
614
844
 
615
- // Export as RGB for broader CSS compatibility
616
- const rgbTokens = palette.tokens({ prefix: true, format: 'rgb' });
845
+ // Export as tasty style-to-state bindings (for Tasty style system)
846
+ const tastyTokens = palette.tasty({ prefix: true });
617
847
 
618
848
  // Export as CSS custom properties (rgb format by default)
619
849
  const css = palette.css({ prefix: true });
620
- // css.light → "--primary-surface-color: rgb(...);\n--danger-surface-color: rgb(...);"
621
- // css.dark → "--primary-surface-color: rgb(...);\n--danger-surface-color: rgb(...);"
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)'
622
856
 
623
857
  // Save and restore a theme
624
858
  const snapshot = primary.export();
@@ -641,6 +875,8 @@ brand.colors({ surface: { lightness: 97 }, text: { base: 'surface', lightness: '
641
875
  | `glaze.fromHex(hex)` | Create a theme from a hex color (`#rgb` or `#rrggbb`) |
642
876
  | `glaze.fromRgb(r, g, b)` | Create a theme from RGB values (0–255) |
643
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 |
644
880
 
645
881
  ### Theme Methods
646
882
 
@@ -656,7 +892,8 @@ brand.colors({ surface: { lightness: 97 }, text: { base: 'surface', lightness: '
656
892
  | `theme.export()` | Export configuration as JSON-safe object |
657
893
  | `theme.extend(options)` | Create a child theme |
658
894
  | `theme.resolve()` | Resolve all colors |
659
- | `theme.tokens(options?)` | Export as token map |
895
+ | `theme.tokens(options?)` | Export as flat token map grouped by variant |
896
+ | `theme.tasty(options?)` | Export as [Tasty](https://cube-ui-kit.vercel.app/?path=/docs/tasty-documentation--docs) style-to-state bindings |
660
897
  | `theme.json(options?)` | Export as plain JSON |
661
898
  | `theme.css(options?)` | Export as CSS custom property declarations |
662
899