@tenphi/glaze 0.0.0-snapshot.99c649d → 0.0.0-snapshot.a30ab54

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,6 +22,7 @@ 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
+ - **Mix colors** — blend two colors with OKHSL or sRGB interpolation, opaque or transparent, with optional contrast solving
25
26
  - **Shadow colors** — OKHSL-native shadow computation with automatic alpha, fg/bg tinting, and per-scheme adaptation
26
27
  - **Light + Dark + High-Contrast** — all schemes from one definition
27
28
  - **Per-color hue override** — absolute or relative hue shifts within a theme
@@ -70,8 +71,8 @@ const success = primary.extend({ hue: 157 });
70
71
 
71
72
  // Compose into a palette and export
72
73
  const palette = glaze.palette({ primary, danger, success });
73
- const tokens = palette.tokens({ prefix: true });
74
- // → { light: { 'primary-surface': 'okhsl(...)', ... }, dark: { 'primary-surface': 'okhsl(...)', ... } }
74
+ const tokens = palette.tokens({ primary: 'primary' });
75
+ // → { light: { 'primary-surface': 'okhsl(...)', 'surface': 'okhsl(...)', ... }, dark: { ... } }
75
76
  ```
76
77
 
77
78
  ## Core Concepts
@@ -413,6 +414,139 @@ const css = glaze.format(v, 'oklch');
413
414
  }
414
415
  ```
415
416
 
417
+ ## Mix Colors
418
+
419
+ Mix colors blend two existing colors together. Use them for hover overlays, tints, shades, and any derived color that sits between two reference colors.
420
+
421
+ ### Opaque Mix
422
+
423
+ Produces a solid color by interpolating between `base` and `target`:
424
+
425
+ ```ts
426
+ theme.colors({
427
+ surface: { lightness: 95 },
428
+ accent: { lightness: 30 },
429
+
430
+ // 30% of the way from surface toward accent
431
+ tint: { type: 'mix', base: 'surface', target: 'accent', value: 30 },
432
+ });
433
+ ```
434
+
435
+ - `value` — mix ratio 0–100 (0 = pure base, 100 = pure target)
436
+ - The result is a fully opaque color (alpha = 1)
437
+ - Adapts to light/dark/HC schemes automatically via the resolved base and target
438
+
439
+ ### Transparent Mix
440
+
441
+ Produces the target color with a controlled opacity — useful for hover overlays:
442
+
443
+ ```ts
444
+ theme.colors({
445
+ surface: { lightness: 95 },
446
+ black: { lightness: 0, saturation: 0 },
447
+
448
+ hover: {
449
+ type: 'mix',
450
+ base: 'surface',
451
+ target: 'black',
452
+ value: 8,
453
+ blend: 'transparent',
454
+ },
455
+ });
456
+ // hover → target color (black) with alpha = 0.08
457
+ ```
458
+
459
+ The output color has `h`, `s`, `l` from the target and `alpha = value / 100`.
460
+
461
+ ### Blend Space
462
+
463
+ By default, opaque mixing interpolates in OKHSL (perceptually uniform, consistent with Glaze's model). Use `space: 'srgb'` for linear sRGB interpolation, which matches browser compositing:
464
+
465
+ ```ts
466
+ theme.colors({
467
+ surface: { lightness: 95 },
468
+ accent: { lightness: 30 },
469
+
470
+ // sRGB blend — matches what the browser would render
471
+ hover: { type: 'mix', base: 'surface', target: 'accent', value: 20, space: 'srgb' },
472
+ });
473
+ ```
474
+
475
+ | Space | Behavior | Best for |
476
+ |---|---|---|
477
+ | `'okhsl'` (default) | Perceptually uniform OKHSL interpolation | Design token derivation |
478
+ | `'srgb'` | Linear sRGB channel interpolation | Matching browser compositing |
479
+
480
+ The `space` option only affects opaque blending. Transparent blending always composites in linear sRGB (matching browser alpha compositing).
481
+
482
+ ### Contrast Solving
483
+
484
+ Mix colors support the same `contrast` prop as regular colors. The solver adjusts the mix ratio (opaque) or opacity (transparent) to meet the WCAG target:
485
+
486
+ ```ts
487
+ theme.colors({
488
+ surface: { lightness: 95 },
489
+ accent: { lightness: 30 },
490
+
491
+ // Ensure the mixed color has at least AA contrast against surface
492
+ tint: {
493
+ type: 'mix',
494
+ base: 'surface',
495
+ target: 'accent',
496
+ value: 10,
497
+ contrast: 'AA',
498
+ },
499
+
500
+ // Ensure the transparent overlay has at least 3:1 contrast
501
+ overlay: {
502
+ type: 'mix',
503
+ base: 'surface',
504
+ target: 'accent',
505
+ value: 5,
506
+ blend: 'transparent',
507
+ contrast: 3,
508
+ },
509
+ });
510
+ ```
511
+
512
+ ### High-Contrast Pairs
513
+
514
+ Both `value` and `contrast` support `[normal, highContrast]` pairs:
515
+
516
+ ```ts
517
+ theme.colors({
518
+ surface: { lightness: 95 },
519
+ accent: { lightness: 30 },
520
+
521
+ tint: {
522
+ type: 'mix',
523
+ base: 'surface',
524
+ target: 'accent',
525
+ value: [20, 40], // stronger mix in high-contrast mode
526
+ contrast: [3, 'AAA'], // stricter contrast in high-contrast mode
527
+ },
528
+ });
529
+ ```
530
+
531
+ ### Achromatic Colors
532
+
533
+ When mixing with achromatic colors (saturation near zero, e.g., white or black) in `okhsl` space, the hue comes from whichever color has saturation. This prevents meaningless hue artifacts and matches CSS `color-mix()` "missing component" behavior. For purely achromatic mixes, prefer `space: 'srgb'` where hue is irrelevant.
534
+
535
+ ### Mix Chaining
536
+
537
+ Mix colors can reference other mix colors, enabling multi-step derivations:
538
+
539
+ ```ts
540
+ theme.colors({
541
+ white: { lightness: 100, saturation: 0 },
542
+ black: { lightness: 0, saturation: 0 },
543
+ gray: { type: 'mix', base: 'white', target: 'black', value: 50, space: 'srgb' },
544
+ lightGray: { type: 'mix', base: 'white', target: 'gray', value: 50, space: 'srgb' },
545
+ });
546
+ ```
547
+
548
+ Mix colors cannot reference shadow colors (same restriction as regular dependent colors).
549
+
416
550
  ## Output Formats
417
551
 
418
552
  Control the color format in exports with the `format` option:
@@ -484,7 +618,7 @@ Modes control how colors adapt across schemes:
484
618
 
485
619
  ### Lightness
486
620
 
487
- Root color lightness is mapped linearly within the configured `lightLightness` window:
621
+ Absolute lightness values (both root colors and dependent colors with absolute lightness) are mapped linearly within the configured `lightLightness` window:
488
622
 
489
623
  ```ts
490
624
  const [lo, hi] = lightLightness; // default: [10, 100]
@@ -560,12 +694,12 @@ Combine multiple themes into a single palette:
560
694
  const palette = glaze.palette({ primary, danger, success, warning });
561
695
  ```
562
696
 
563
- ### Token Export
697
+ ### Prefix Behavior
564
698
 
565
- Tokens are grouped by scheme variant, with plain color names as keys:
699
+ Palette export methods (`tokens()`, `tasty()`, `css()`) default to `prefix: true` — all tokens are automatically prefixed with the theme name to avoid collisions:
566
700
 
567
701
  ```ts
568
- const tokens = palette.tokens({ prefix: true });
702
+ const tokens = palette.tokens();
569
703
  // → {
570
704
  // light: { 'primary-surface': 'okhsl(...)', 'danger-surface': 'okhsl(...)' },
571
705
  // dark: { 'primary-surface': 'okhsl(...)', 'danger-surface': 'okhsl(...)' },
@@ -578,15 +712,44 @@ Custom prefix mapping:
578
712
  palette.tokens({ prefix: { primary: 'brand-', danger: 'error-' } });
579
713
  ```
580
714
 
715
+ To disable prefixing entirely, pass `prefix: false` explicitly. Note that tokens with the same name will overwrite each other (last theme wins).
716
+
717
+ ### Primary Theme
718
+
719
+ Use the `primary` option to designate one theme as the primary. Its tokens are duplicated without prefix, providing convenient short aliases alongside the prefixed versions:
720
+
721
+ ```ts
722
+ const palette = glaze.palette({ primary, danger, success });
723
+ const tokens = palette.tokens({ primary: 'primary' });
724
+ // → {
725
+ // light: {
726
+ // 'primary-surface': 'okhsl(...)', // prefixed (all themes)
727
+ // 'danger-surface': 'okhsl(...)',
728
+ // 'success-surface': 'okhsl(...)',
729
+ // 'surface': 'okhsl(...)', // unprefixed alias (primary only)
730
+ // },
731
+ // }
732
+ ```
733
+
734
+ The `primary` option works on `tokens()`, `tasty()`, and `css()`. It combines with any prefix mode — when using a custom prefix map, primary tokens are still duplicated without prefix:
735
+
736
+ ```ts
737
+ palette.tokens({ prefix: { primary: 'p-', danger: 'd-' }, primary: 'primary' });
738
+ // → 'p-surface' + 'surface' (alias) + 'd-surface'
739
+ ```
740
+
741
+ An error is thrown if the primary name doesn't match any theme in the palette.
742
+
581
743
  ### Tasty Export (for [Tasty](https://cube-ui-kit.vercel.app/?path=/docs/tasty-documentation--docs) style system)
582
744
 
583
745
  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.):
584
746
 
585
747
  ```ts
586
- const tastyTokens = palette.tasty({ prefix: true });
748
+ const tastyTokens = palette.tasty({ primary: 'primary' });
587
749
  // → {
588
750
  // '#primary-surface': { '': 'okhsl(...)', '@dark': 'okhsl(...)' },
589
751
  // '#danger-surface': { '': 'okhsl(...)', '@dark': 'okhsl(...)' },
752
+ // '#surface': { '': 'okhsl(...)', '@dark': 'okhsl(...)' }, // alias
590
753
  // }
591
754
  ```
592
755
 
@@ -653,8 +816,10 @@ palette.tasty({ states: { dark: '@dark', highContrast: '@hc' } });
653
816
 
654
817
  ### JSON Export (Framework-Agnostic)
655
818
 
819
+ JSON export groups by theme name (no prefix needed):
820
+
656
821
  ```ts
657
- const data = palette.json({ prefix: true });
822
+ const data = palette.json();
658
823
  // → {
659
824
  // primary: { surface: { light: 'okhsl(...)', dark: 'okhsl(...)' } },
660
825
  // danger: { surface: { light: 'okhsl(...)', dark: 'okhsl(...)' } },
@@ -676,7 +841,7 @@ const css = theme.css();
676
841
  Use in a stylesheet:
677
842
 
678
843
  ```ts
679
- const css = palette.css({ prefix: true });
844
+ const css = palette.css({ primary: 'primary' });
680
845
 
681
846
  const stylesheet = `
682
847
  :root { ${css.light} }
@@ -692,7 +857,8 @@ Options:
692
857
  |---|---|---|
693
858
  | `format` | `'rgb'` | Color format (`'rgb'`, `'hsl'`, `'okhsl'`, `'oklch'`) |
694
859
  | `suffix` | `'-color'` | Suffix appended to each CSS property name |
695
- | `prefix` | | (palette only) Same prefix behavior as `tokens()` |
860
+ | `prefix` | `true` (palette) | (palette only) `true` uses `"<themeName>-"`, or provide a custom map |
861
+ | `primary` | — | (palette only) Theme name to duplicate without prefix |
696
862
 
697
863
  ```ts
698
864
  // Custom suffix
@@ -703,9 +869,9 @@ theme.css({ suffix: '' });
703
869
  theme.css({ format: 'hsl' });
704
870
  // → "--surface-color: hsl(...);"
705
871
 
706
- // Palette with prefix
707
- palette.css({ prefix: true });
708
- // → "--primary-surface-color: rgb(...);\n--danger-surface-color: rgb(...);"
872
+ // Palette with primary
873
+ palette.css({ primary: 'primary' });
874
+ // → "--primary-surface-color: rgb(...);\n--surface-color: rgb(...);\n--danger-surface-color: rgb(...);"
709
875
  ```
710
876
 
711
877
  ## Output Modes
@@ -758,10 +924,10 @@ glaze.configure({
758
924
 
759
925
  ## Color Definition Shape
760
926
 
761
- `ColorDef` is a discriminated union of regular colors and shadow colors:
927
+ `ColorDef` is a discriminated union of regular colors, shadow colors, and mix colors:
762
928
 
763
929
  ```ts
764
- type ColorDef = RegularColorDef | ShadowColorDef;
930
+ type ColorDef = RegularColorDef | ShadowColorDef | MixColorDef;
765
931
 
766
932
  interface RegularColorDef {
767
933
  lightness?: HCPair<number | RelativeValue>;
@@ -780,9 +946,19 @@ interface ShadowColorDef {
780
946
  intensity: HCPair<number>; // 0–100
781
947
  tuning?: ShadowTuning;
782
948
  }
949
+
950
+ interface MixColorDef {
951
+ type: 'mix';
952
+ base: string; // "from" color name
953
+ target: string; // "to" color name
954
+ value: HCPair<number>; // 0–100 (mix ratio or opacity)
955
+ blend?: 'opaque' | 'transparent'; // default: 'opaque'
956
+ space?: 'okhsl' | 'srgb'; // default: 'okhsl'
957
+ contrast?: HCPair<MinContrast>;
958
+ }
783
959
  ```
784
960
 
785
- 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.
961
+ 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. Mix colors use `type: 'mix'` and must reference two non-shadow colors.
786
962
 
787
963
  ## Validation
788
964
 
@@ -801,6 +977,12 @@ A root color must have absolute `lightness` (a number). A dependent color must h
801
977
  | Regular color `base` references a shadow color | Validation error |
802
978
  | Shadow `intensity` outside 0–100 | Clamp silently |
803
979
  | `contrast` + `opacity` combined | Warning |
980
+ | Mix `base` references non-existent color | Validation error |
981
+ | Mix `target` references non-existent color | Validation error |
982
+ | Mix `base` references a shadow color | Validation error |
983
+ | Mix `target` references a shadow color | Validation error |
984
+ | Mix `value` outside 0–100 | Clamp silently |
985
+ | Circular references involving mix colors | Validation error |
804
986
 
805
987
  ## Advanced: Color Math Utilities
806
988
 
@@ -846,6 +1028,10 @@ primary.colors({
846
1028
  'shadow-md': { type: 'shadow', bg: 'surface', fg: 'text', intensity: 10 },
847
1029
  'shadow-lg': { type: 'shadow', bg: 'surface', fg: 'text', intensity: 20 },
848
1030
 
1031
+ // Mix colors — hover overlays and tints
1032
+ 'hover': { type: 'mix', base: 'surface', target: 'accent-fill', value: 8, blend: 'transparent' },
1033
+ 'tint': { type: 'mix', base: 'surface', target: 'accent-fill', value: 20 },
1034
+
849
1035
  // Fixed-alpha overlay
850
1036
  overlay: { lightness: 0, opacity: 0.5 },
851
1037
  });
@@ -857,16 +1043,16 @@ const note = primary.extend({ hue: 302 });
857
1043
 
858
1044
  const palette = glaze.palette({ primary, danger, success, warning, note });
859
1045
 
860
- // Export as flat token map grouped by variant
861
- const tokens = palette.tokens({ prefix: true });
862
- // tokens.light → { 'primary-surface': 'okhsl(...)', 'primary-shadow-md': 'okhsl(... / 0.1)' }
1046
+ // Export as flat token map grouped by variant (prefix defaults to true)
1047
+ const tokens = palette.tokens({ primary: 'primary' });
1048
+ // tokens.light → { 'primary-surface': '...', 'surface': '...', 'danger-surface': '...' }
863
1049
 
864
1050
  // Export as tasty style-to-state bindings (for Tasty style system)
865
- const tastyTokens = palette.tasty({ prefix: true });
1051
+ const tastyTokens = palette.tasty({ primary: 'primary' });
866
1052
 
867
1053
  // Export as CSS custom properties (rgb format by default)
868
- const css = palette.css({ prefix: true });
869
- // css.light → "--primary-surface-color: rgb(...);\n--primary-shadow-md-color: rgb(... / 0.1);"
1054
+ const css = palette.css({ primary: 'primary' });
1055
+ // css.light → "--primary-surface-color: rgb(...);\n--surface-color: rgb(...);\n--danger-surface-color: rgb(...);"
870
1056
 
871
1057
  // Standalone shadow computation
872
1058
  const v = glaze.shadow({ bg: '#f0eef5', fg: '#1a1a2e', intensity: 10 });