@tenphi/glaze 0.0.0-snapshot.4c063ef → 0.0.0-snapshot.575cb1c

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
@@ -193,6 +194,8 @@ A single value applies to both modes. All control is local and explicit.
193
194
  'muted': { base: 'surface', lightness: ['-35', '-50'], contrast: ['AA-large', 'AA'] }
194
195
  ```
195
196
 
197
+ **Full lightness spectrum in HC mode:** In high-contrast variants, the `lightLightness` and `darkLightness` window constraints are bypassed entirely. Colors can reach the full 0–100 lightness range, maximizing perceivable contrast. Normal (non-HC) variants continue to use the configured windows.
198
+
196
199
  ## Theme Color Management
197
200
 
198
201
  ### Adding Colors
@@ -413,6 +416,139 @@ const css = glaze.format(v, 'oklch');
413
416
  }
414
417
  ```
415
418
 
419
+ ## Mix Colors
420
+
421
+ Mix colors blend two existing colors together. Use them for hover overlays, tints, shades, and any derived color that sits between two reference colors.
422
+
423
+ ### Opaque Mix
424
+
425
+ Produces a solid color by interpolating between `base` and `target`:
426
+
427
+ ```ts
428
+ theme.colors({
429
+ surface: { lightness: 95 },
430
+ accent: { lightness: 30 },
431
+
432
+ // 30% of the way from surface toward accent
433
+ tint: { type: 'mix', base: 'surface', target: 'accent', value: 30 },
434
+ });
435
+ ```
436
+
437
+ - `value` — mix ratio 0–100 (0 = pure base, 100 = pure target)
438
+ - The result is a fully opaque color (alpha = 1)
439
+ - Adapts to light/dark/HC schemes automatically via the resolved base and target
440
+
441
+ ### Transparent Mix
442
+
443
+ Produces the target color with a controlled opacity — useful for hover overlays:
444
+
445
+ ```ts
446
+ theme.colors({
447
+ surface: { lightness: 95 },
448
+ black: { lightness: 0, saturation: 0 },
449
+
450
+ hover: {
451
+ type: 'mix',
452
+ base: 'surface',
453
+ target: 'black',
454
+ value: 8,
455
+ blend: 'transparent',
456
+ },
457
+ });
458
+ // hover → target color (black) with alpha = 0.08
459
+ ```
460
+
461
+ The output color has `h`, `s`, `l` from the target and `alpha = value / 100`.
462
+
463
+ ### Blend Space
464
+
465
+ 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:
466
+
467
+ ```ts
468
+ theme.colors({
469
+ surface: { lightness: 95 },
470
+ accent: { lightness: 30 },
471
+
472
+ // sRGB blend — matches what the browser would render
473
+ hover: { type: 'mix', base: 'surface', target: 'accent', value: 20, space: 'srgb' },
474
+ });
475
+ ```
476
+
477
+ | Space | Behavior | Best for |
478
+ |---|---|---|
479
+ | `'okhsl'` (default) | Perceptually uniform OKHSL interpolation | Design token derivation |
480
+ | `'srgb'` | Linear sRGB channel interpolation | Matching browser compositing |
481
+
482
+ The `space` option only affects opaque blending. Transparent blending always composites in linear sRGB (matching browser alpha compositing).
483
+
484
+ ### Contrast Solving
485
+
486
+ 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:
487
+
488
+ ```ts
489
+ theme.colors({
490
+ surface: { lightness: 95 },
491
+ accent: { lightness: 30 },
492
+
493
+ // Ensure the mixed color has at least AA contrast against surface
494
+ tint: {
495
+ type: 'mix',
496
+ base: 'surface',
497
+ target: 'accent',
498
+ value: 10,
499
+ contrast: 'AA',
500
+ },
501
+
502
+ // Ensure the transparent overlay has at least 3:1 contrast
503
+ overlay: {
504
+ type: 'mix',
505
+ base: 'surface',
506
+ target: 'accent',
507
+ value: 5,
508
+ blend: 'transparent',
509
+ contrast: 3,
510
+ },
511
+ });
512
+ ```
513
+
514
+ ### High-Contrast Pairs
515
+
516
+ Both `value` and `contrast` support `[normal, highContrast]` pairs:
517
+
518
+ ```ts
519
+ theme.colors({
520
+ surface: { lightness: 95 },
521
+ accent: { lightness: 30 },
522
+
523
+ tint: {
524
+ type: 'mix',
525
+ base: 'surface',
526
+ target: 'accent',
527
+ value: [20, 40], // stronger mix in high-contrast mode
528
+ contrast: [3, 'AAA'], // stricter contrast in high-contrast mode
529
+ },
530
+ });
531
+ ```
532
+
533
+ ### Achromatic Colors
534
+
535
+ 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.
536
+
537
+ ### Mix Chaining
538
+
539
+ Mix colors can reference other mix colors, enabling multi-step derivations:
540
+
541
+ ```ts
542
+ theme.colors({
543
+ white: { lightness: 100, saturation: 0 },
544
+ black: { lightness: 0, saturation: 0 },
545
+ gray: { type: 'mix', base: 'white', target: 'black', value: 50, space: 'srgb' },
546
+ lightGray: { type: 'mix', base: 'white', target: 'gray', value: 50, space: 'srgb' },
547
+ });
548
+ ```
549
+
550
+ Mix colors cannot reference shadow colors (same restriction as regular dependent colors).
551
+
416
552
  ## Output Formats
417
553
 
418
554
  Control the color format in exports with the `format` option:
@@ -467,7 +603,7 @@ Modes control how colors adapt across schemes:
467
603
 
468
604
  ```ts
469
605
  // Light: surface L=97, text lightness='-52' → L=45 (dark text on light bg)
470
- // Dark: surface inverts to L≈14, sign flips → L=14+52=66
606
+ // Dark: surface inverts to L≈29 (power curve), sign flips → L=29+52=81
471
607
  // contrast solver may push further (light text on dark bg)
472
608
  ```
473
609
 
@@ -484,14 +620,14 @@ Modes control how colors adapt across schemes:
484
620
 
485
621
  ### Lightness
486
622
 
487
- Root color lightness is mapped linearly within the configured `lightLightness` window:
623
+ Absolute lightness values (both root colors and dependent colors with absolute lightness) are mapped linearly within the configured `lightLightness` window:
488
624
 
489
625
  ```ts
490
626
  const [lo, hi] = lightLightness; // default: [10, 100]
491
627
  const mappedL = (lightness * (hi - lo)) / 100 + lo;
492
628
  ```
493
629
 
494
- Both `auto` and `fixed` modes use the same linear formula. `static` mode bypasses the mapping entirely.
630
+ Both `auto` and `fixed` modes use the same linear formula. `static` mode and high-contrast variants bypass the mapping entirely (identity: `mappedL = l`).
495
631
 
496
632
  | Color | Raw L | Mapped L (default [10, 100]) |
497
633
  |---|---|---|
@@ -503,24 +639,29 @@ Both `auto` and `fixed` modes use the same linear formula. `static` mode bypasse
503
639
 
504
640
  ### Lightness
505
641
 
506
- **`auto`** — inverted within the configured window:
642
+ **`auto`** — inverted with a power curve within the configured window:
507
643
 
508
644
  ```ts
509
645
  const [lo, hi] = darkLightness; // default: [15, 95]
510
- const invertedL = ((100 - lightness) * (hi - lo)) / 100 + lo;
646
+ const d = (100 - lightness) / 100;
647
+ const invertedL = lo + (hi - lo) * Math.pow(d, darkCurve); // darkCurve default: 0.5
511
648
  ```
512
649
 
513
- **`fixed`** mapped without inversion:
650
+ The `darkCurve` exponent (default `0.5`) expands small light-theme deltas near white into larger usable deltas in dark mode. This preserves subtle surface hierarchy (e.g. L=97 vs L=95) that would otherwise collapse to near-identical dark values. Set `darkCurve: 1` for linear (legacy) behavior.
651
+
652
+ **`fixed`** — mapped without inversion (not affected by `darkCurve`):
514
653
 
515
654
  ```ts
516
655
  const mappedL = (lightness * (hi - lo)) / 100 + lo;
517
656
  ```
518
657
 
519
- | Color | Light L | Auto (inverted) | Fixed (mapped) |
520
- |---|---|---|---|
521
- | surface (L=97) | 97 | 17.4 | 92.6 |
522
- | accent-fill (L=52) | 52 | 53.4 | 56.6 |
523
- | accent-text (L=100) | 100 | 15 | 95 |
658
+ | Color | Light L | Auto (curve=0.5) | Auto (curve=1, linear) | Fixed (mapped) |
659
+ |---|---|---|---|---|
660
+ | surface (L=97) | 97 | 28.9 | 17.4 | 92.6 |
661
+ | accent-fill (L=52) | 52 | 70.4 | 53.4 | 56.6 |
662
+ | accent-text (L=100) | 100 | 15 | 15 | 95 |
663
+
664
+ In high-contrast variants, the `darkLightness` window is bypassed. Auto uses pure inversion (`100 - L`), fixed uses identity (`L`). This allows HC colors to reach the full 0–100 range.
524
665
 
525
666
  ### Saturation
526
667
 
@@ -560,12 +701,12 @@ Combine multiple themes into a single palette:
560
701
  const palette = glaze.palette({ primary, danger, success, warning });
561
702
  ```
562
703
 
563
- ### Token Export
704
+ ### Prefix Behavior
564
705
 
565
- Tokens are grouped by scheme variant, with plain color names as keys:
706
+ Palette export methods (`tokens()`, `tasty()`, `css()`) default to `prefix: true` — all tokens are automatically prefixed with the theme name to avoid collisions:
566
707
 
567
708
  ```ts
568
- const tokens = palette.tokens({ prefix: true });
709
+ const tokens = palette.tokens();
569
710
  // → {
570
711
  // light: { 'primary-surface': 'okhsl(...)', 'danger-surface': 'okhsl(...)' },
571
712
  // dark: { 'primary-surface': 'okhsl(...)', 'danger-surface': 'okhsl(...)' },
@@ -578,15 +719,44 @@ Custom prefix mapping:
578
719
  palette.tokens({ prefix: { primary: 'brand-', danger: 'error-' } });
579
720
  ```
580
721
 
722
+ To disable prefixing entirely, pass `prefix: false` explicitly. Note that tokens with the same name will overwrite each other (last theme wins).
723
+
724
+ ### Primary Theme
725
+
726
+ 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:
727
+
728
+ ```ts
729
+ const palette = glaze.palette({ primary, danger, success });
730
+ const tokens = palette.tokens({ primary: 'primary' });
731
+ // → {
732
+ // light: {
733
+ // 'primary-surface': 'okhsl(...)', // prefixed (all themes)
734
+ // 'danger-surface': 'okhsl(...)',
735
+ // 'success-surface': 'okhsl(...)',
736
+ // 'surface': 'okhsl(...)', // unprefixed alias (primary only)
737
+ // },
738
+ // }
739
+ ```
740
+
741
+ 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:
742
+
743
+ ```ts
744
+ palette.tokens({ prefix: { primary: 'p-', danger: 'd-' }, primary: 'primary' });
745
+ // → 'p-surface' + 'surface' (alias) + 'd-surface'
746
+ ```
747
+
748
+ An error is thrown if the primary name doesn't match any theme in the palette.
749
+
581
750
  ### Tasty Export (for [Tasty](https://cube-ui-kit.vercel.app/?path=/docs/tasty-documentation--docs) style system)
582
751
 
583
752
  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
753
 
585
754
  ```ts
586
- const tastyTokens = palette.tasty({ prefix: true });
755
+ const tastyTokens = palette.tasty({ primary: 'primary' });
587
756
  // → {
588
757
  // '#primary-surface': { '': 'okhsl(...)', '@dark': 'okhsl(...)' },
589
758
  // '#danger-surface': { '': 'okhsl(...)', '@dark': 'okhsl(...)' },
759
+ // '#surface': { '': 'okhsl(...)', '@dark': 'okhsl(...)' }, // alias
590
760
  // }
591
761
  ```
592
762
 
@@ -653,8 +823,10 @@ palette.tasty({ states: { dark: '@dark', highContrast: '@hc' } });
653
823
 
654
824
  ### JSON Export (Framework-Agnostic)
655
825
 
826
+ JSON export groups by theme name (no prefix needed):
827
+
656
828
  ```ts
657
- const data = palette.json({ prefix: true });
829
+ const data = palette.json();
658
830
  // → {
659
831
  // primary: { surface: { light: 'okhsl(...)', dark: 'okhsl(...)' } },
660
832
  // danger: { surface: { light: 'okhsl(...)', dark: 'okhsl(...)' } },
@@ -676,7 +848,7 @@ const css = theme.css();
676
848
  Use in a stylesheet:
677
849
 
678
850
  ```ts
679
- const css = palette.css({ prefix: true });
851
+ const css = palette.css({ primary: 'primary' });
680
852
 
681
853
  const stylesheet = `
682
854
  :root { ${css.light} }
@@ -692,7 +864,8 @@ Options:
692
864
  |---|---|---|
693
865
  | `format` | `'rgb'` | Color format (`'rgb'`, `'hsl'`, `'okhsl'`, `'oklch'`) |
694
866
  | `suffix` | `'-color'` | Suffix appended to each CSS property name |
695
- | `prefix` | | (palette only) Same prefix behavior as `tokens()` |
867
+ | `prefix` | `true` (palette) | (palette only) `true` uses `"<themeName>-"`, or provide a custom map |
868
+ | `primary` | — | (palette only) Theme name to duplicate without prefix |
696
869
 
697
870
  ```ts
698
871
  // Custom suffix
@@ -703,9 +876,9 @@ theme.css({ suffix: '' });
703
876
  theme.css({ format: 'hsl' });
704
877
  // → "--surface-color: hsl(...);"
705
878
 
706
- // Palette with prefix
707
- palette.css({ prefix: true });
708
- // → "--primary-surface-color: rgb(...);\n--danger-surface-color: rgb(...);"
879
+ // Palette with primary
880
+ palette.css({ primary: 'primary' });
881
+ // → "--primary-surface-color: rgb(...);\n--surface-color: rgb(...);\n--danger-surface-color: rgb(...);"
709
882
  ```
710
883
 
711
884
  ## Output Modes
@@ -738,9 +911,10 @@ Resolution priority (highest first):
738
911
 
739
912
  ```ts
740
913
  glaze.configure({
741
- lightLightness: [10, 100], // Light scheme lightness window [lo, hi]
742
- darkLightness: [15, 95], // Dark scheme lightness window [lo, hi]
914
+ lightLightness: [10, 100], // Light scheme lightness window [lo, hi] (bypassed in HC)
915
+ darkLightness: [15, 95], // Dark scheme lightness window [lo, hi] (bypassed in HC)
743
916
  darkDesaturation: 0.1, // Saturation reduction in dark scheme (0–1)
917
+ darkCurve: 0.5, // Power-curve exponent for dark auto-inversion (0–1)
744
918
  states: {
745
919
  dark: '@dark', // State alias for dark mode tokens
746
920
  highContrast: '@high-contrast',
@@ -758,10 +932,10 @@ glaze.configure({
758
932
 
759
933
  ## Color Definition Shape
760
934
 
761
- `ColorDef` is a discriminated union of regular colors and shadow colors:
935
+ `ColorDef` is a discriminated union of regular colors, shadow colors, and mix colors:
762
936
 
763
937
  ```ts
764
- type ColorDef = RegularColorDef | ShadowColorDef;
938
+ type ColorDef = RegularColorDef | ShadowColorDef | MixColorDef;
765
939
 
766
940
  interface RegularColorDef {
767
941
  lightness?: HCPair<number | RelativeValue>;
@@ -780,15 +954,24 @@ interface ShadowColorDef {
780
954
  intensity: HCPair<number>; // 0–100
781
955
  tuning?: ShadowTuning;
782
956
  }
957
+
958
+ interface MixColorDef {
959
+ type: 'mix';
960
+ base: string; // "from" color name
961
+ target: string; // "to" color name
962
+ value: HCPair<number>; // 0–100 (mix ratio or opacity)
963
+ blend?: 'opaque' | 'transparent'; // default: 'opaque'
964
+ space?: 'okhsl' | 'srgb'; // default: 'okhsl'
965
+ contrast?: HCPair<MinContrast>;
966
+ }
783
967
  ```
784
968
 
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.
969
+ 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
970
 
787
971
  ## Validation
788
972
 
789
973
  | Condition | Behavior |
790
974
  |---|---|
791
- | Both absolute `lightness` and `base` on same color | Warning, `lightness` takes precedence |
792
975
  | `contrast` without `base` | Validation error |
793
976
  | Relative `lightness` without `base` | Validation error |
794
977
  | `lightness` resolves outside 0–100 | Clamp silently |
@@ -802,6 +985,12 @@ A root color must have absolute `lightness` (a number). A dependent color must h
802
985
  | Regular color `base` references a shadow color | Validation error |
803
986
  | Shadow `intensity` outside 0–100 | Clamp silently |
804
987
  | `contrast` + `opacity` combined | Warning |
988
+ | Mix `base` references non-existent color | Validation error |
989
+ | Mix `target` references non-existent color | Validation error |
990
+ | Mix `base` references a shadow color | Validation error |
991
+ | Mix `target` references a shadow color | Validation error |
992
+ | Mix `value` outside 0–100 | Clamp silently |
993
+ | Circular references involving mix colors | Validation error |
805
994
 
806
995
  ## Advanced: Color Math Utilities
807
996
 
@@ -847,6 +1036,10 @@ primary.colors({
847
1036
  'shadow-md': { type: 'shadow', bg: 'surface', fg: 'text', intensity: 10 },
848
1037
  'shadow-lg': { type: 'shadow', bg: 'surface', fg: 'text', intensity: 20 },
849
1038
 
1039
+ // Mix colors — hover overlays and tints
1040
+ 'hover': { type: 'mix', base: 'surface', target: 'accent-fill', value: 8, blend: 'transparent' },
1041
+ 'tint': { type: 'mix', base: 'surface', target: 'accent-fill', value: 20 },
1042
+
850
1043
  // Fixed-alpha overlay
851
1044
  overlay: { lightness: 0, opacity: 0.5 },
852
1045
  });
@@ -858,16 +1051,16 @@ const note = primary.extend({ hue: 302 });
858
1051
 
859
1052
  const palette = glaze.palette({ primary, danger, success, warning, note });
860
1053
 
861
- // Export as flat token map grouped by variant
862
- const tokens = palette.tokens({ prefix: true });
863
- // tokens.light → { 'primary-surface': 'okhsl(...)', 'primary-shadow-md': 'okhsl(... / 0.1)' }
1054
+ // Export as flat token map grouped by variant (prefix defaults to true)
1055
+ const tokens = palette.tokens({ primary: 'primary' });
1056
+ // tokens.light → { 'primary-surface': '...', 'surface': '...', 'danger-surface': '...' }
864
1057
 
865
1058
  // Export as tasty style-to-state bindings (for Tasty style system)
866
- const tastyTokens = palette.tasty({ prefix: true });
1059
+ const tastyTokens = palette.tasty({ primary: 'primary' });
867
1060
 
868
1061
  // Export as CSS custom properties (rgb format by default)
869
- const css = palette.css({ prefix: true });
870
- // css.light → "--primary-surface-color: rgb(...);\n--primary-shadow-md-color: rgb(... / 0.1);"
1062
+ const css = palette.css({ primary: 'primary' });
1063
+ // css.light → "--primary-surface-color: rgb(...);\n--surface-color: rgb(...);\n--danger-surface-color: rgb(...);"
871
1064
 
872
1065
  // Standalone shadow computation
873
1066
  const v = glaze.shadow({ bg: '#f0eef5', fg: '#1a1a2e', intensity: 10 });