@waveso/ui 0.16.0 → 0.18.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/dist/styles.css CHANGED
@@ -247,21 +247,21 @@
247
247
  * `--contrast`…).
248
248
  * ──────────────────────────────────────────────────────────────────────── */
249
249
 
250
- /* Graphite — the mid-tone grey (default). Built around its card colour, the
251
- * 900 (#2F3036): the page (950) and menus (800) one even contrast step
252
- * either side, the steps above evenly spaced up to the text colour at the
253
- * 50. One hue, 278.2; chroma arcs to 0.014 through the middle. */
254
- --graphite-50: oklch(0.964 0.001 278.2); /* #F3F3F4 */
255
- --graphite-100: oklch(0.911 0.004 278.2); /* #E1E1E4 */
256
- --graphite-200: oklch(0.840 0.007 278.2); /* #C9CACF */
257
- --graphite-300: oklch(0.757 0.009 278.2); /* #AEB0B6 */
258
- --graphite-400: oklch(0.678 0.011 278.2); /* #96979F */
259
- --graphite-500: oklch(0.589 0.013 278.2); /* #7B7C85 */
260
- --graphite-600: oklch(0.498 0.014 278.2); /* #61626B */
261
- --graphite-700: oklch(0.415 0.013 278.2); /* #4A4B53 */
262
- --graphite-800: oklch(0.348 0.012 278.2); /* #383940 */
263
- --graphite-900: oklch(0.311 0.011 278.2); /* #2F3036 */
264
- --graphite-950: oklch(0.268 0.009 278.2); /* #25262A */
250
+ /* Graphite — the mid-tone grey (default). Built around its original card
251
+ * colour, the 900 (#323339): the page (950) and menus (800) one 1.15
252
+ * contrast step either side, the steps above evenly spaced up to the text
253
+ * colour at the 50. One hue, 278.3; chroma arcs to 0.014 through the middle. */
254
+ --graphite-50: oklch(0.964 0.001 278.3); /* #F3F3F4 */
255
+ --graphite-100: oklch(0.912 0.003 278.3); /* #E1E2E4 */
256
+ --graphite-200: oklch(0.842 0.007 278.3); /* #CACBD0 */
257
+ --graphite-300: oklch(0.761 0.009 278.3); /* #B0B1B7 */
258
+ --graphite-400: oklch(0.683 0.011 278.3); /* #9799A0 */
259
+ --graphite-500: oklch(0.595 0.013 278.3); /* #7D7E87 */
260
+ --graphite-600: oklch(0.506 0.014 278.3); /* #63646D */
261
+ --graphite-700: oklch(0.424 0.014 278.3); /* #4C4E56 */
262
+ --graphite-800: oklch(0.359 0.012 278.3); /* #3B3C43 */
263
+ --graphite-900: oklch(0.322 0.011 278.3); /* #323339 */
264
+ --graphite-950: oklch(0.283 0.009 278.3); /* #28292E */
265
265
 
266
266
  /* Ink — deep navy surfaces, neutral-cool structure (page #020812, surface #0B1627).
267
267
  * Graphite's even lightness steps between Ink's 50 and its page. Chroma fades
@@ -318,6 +318,11 @@
318
318
  /* Rings = brand themed */
319
319
  --focus: var(--primary);
320
320
 
321
+ /* Selected — chosen, current or on: the current tab, the sidebar's current
322
+ * page, a pressed toggle, a checked box, a selected row. Primary by default,
323
+ * so it looks as it always has; set it to restyle every chosen state at once. */
324
+ --selected: var(--primary);
325
+
321
326
  /* Scrim — the dim behind modal overlays (dialog / drawer / alert backdrop). */
322
327
  --scrim: oklch(0 0 0 / 0.1);
323
328
 
@@ -498,17 +503,17 @@
498
503
  --wave-teal-900: oklch(0.251 0.049 219.9);
499
504
  --wave-teal-950: oklch(0.197 0.037 222.5);
500
505
  /* Graphite */
501
- --graphite-50: oklch(0.964 0.001 278.2);
502
- --graphite-100: oklch(0.911 0.004 278.2);
503
- --graphite-200: oklch(0.840 0.007 278.2);
504
- --graphite-300: oklch(0.757 0.009 278.2);
505
- --graphite-400: oklch(0.678 0.011 278.2);
506
- --graphite-500: oklch(0.589 0.013 278.2);
507
- --graphite-600: oklch(0.498 0.014 278.2);
508
- --graphite-700: oklch(0.415 0.013 278.2);
509
- --graphite-800: oklch(0.348 0.012 278.2);
510
- --graphite-900: oklch(0.311 0.011 278.2);
511
- --graphite-950: oklch(0.268 0.009 278.2);
506
+ --graphite-50: oklch(0.964 0.001 278.3);
507
+ --graphite-100: oklch(0.912 0.003 278.3);
508
+ --graphite-200: oklch(0.842 0.007 278.3);
509
+ --graphite-300: oklch(0.761 0.009 278.3);
510
+ --graphite-400: oklch(0.683 0.011 278.3);
511
+ --graphite-500: oklch(0.595 0.013 278.3);
512
+ --graphite-600: oklch(0.506 0.014 278.3);
513
+ --graphite-700: oklch(0.424 0.014 278.3);
514
+ --graphite-800: oklch(0.359 0.012 278.3);
515
+ --graphite-900: oklch(0.322 0.011 278.3);
516
+ --graphite-950: oklch(0.283 0.009 278.3);
512
517
  /* Ink */
513
518
  --ink-50: oklch(0.966 0.003 248);
514
519
  --ink-100: oklch(0.906 0.007 265);
@@ -546,8 +551,8 @@
546
551
  * dark system as on a light one. A pinned theme class still beats both.
547
552
  *
548
553
  * The page and popups are white; cards and the sidebar sit on the 50, a faint
549
- * warm tint, so the contour does most of the separating. The marks are its
550
- * text colour, the 950, made translucent, as in every theme.
554
+ * warm tint, so their edge does most of the separating. Lines and fills are
555
+ * its text colour, the 950, made translucent, as in every theme.
551
556
  * ------------------------------------------------------------------------- */
552
557
  :where(:root),
553
558
  .paper {
@@ -556,11 +561,13 @@
556
561
  --elevated: white;
557
562
  --contrast: var(--paper-950);
558
563
  --muted: var(--paper-600);
559
- --soft: var(--paper-500);
564
+ --subtle: var(--paper-500);
560
565
  --secondary: var(--paper-100);
561
- --contour: color-mix(in srgb, var(--paper-950) 5%, transparent);
562
- --highlight: color-mix(in srgb, var(--paper-950) 10%, transparent);
563
- --shade: color-mix(in srgb, var(--paper-950) 15%, transparent);
566
+ --line: color-mix(in srgb, var(--paper-950) 5%, transparent);
567
+ --edge: color-mix(in srgb, var(--paper-950) 10%, transparent);
568
+ --contour: color-mix(in srgb, var(--paper-950) 15%, transparent);
569
+ --highlighted: color-mix(in srgb, var(--paper-950) 10%, transparent);
570
+ --neutral: color-mix(in srgb, var(--paper-950) 15%, transparent);
564
571
  --shadow-strength: 0.055;
565
572
  --danger: var(--wave-red-500);
566
573
  --danger-text: var(--wave-red-600);
@@ -589,20 +596,22 @@
589
596
  --elevated: var(--graphite-800);
590
597
  --contrast: var(--graphite-50);
591
598
  --muted: var(--graphite-300);
592
- --soft: var(--graphite-500);
599
+ --subtle: var(--graphite-500);
593
600
  --secondary: var(--graphite-700);
594
- --contour: color-mix(in srgb, var(--graphite-50) 5%, transparent);
595
- --highlight: color-mix(in srgb, var(--graphite-50) 10%, transparent);
596
- --shade: color-mix(in srgb, var(--graphite-50) 15%, transparent);
601
+ --line: color-mix(in srgb, var(--graphite-50) 5%, transparent);
602
+ --edge: color-mix(in srgb, var(--graphite-50) 10%, transparent);
603
+ --contour: color-mix(in srgb, var(--graphite-50) 15%, transparent);
604
+ --highlighted: color-mix(in srgb, var(--graphite-50) 10%, transparent);
605
+ --neutral: color-mix(in srgb, var(--graphite-50) 15%, transparent);
597
606
  --shadow-strength: 0.045;
598
607
  --danger: var(--wave-red-500);
599
- --danger-text: oklch(0.738 0.158 16.8); /* #FE7C86, see below */
608
+ --danger-text: oklch(0.749 0.152 16.8); /* #FF828B, see below */
600
609
  --success: var(--wave-green-500);
601
- --success-text: oklch(0.700 0.155 160); /* #14BB7C, see below */
610
+ --success-text: oklch(0.712 0.161 160); /* #00C07E, see below */
602
611
  --warning: var(--wave-orange-500);
603
- --warning-text: oklch(0.732 0.173 57.3); /* #F78711, see below */
612
+ --warning-text: oklch(0.745 0.179 57.3); /* #FE8A04, see below */
604
613
  --info: var(--wave-blue-500);
605
- --info-text: oklch(0.717 0.150 250.4); /* #52A8FD, see below */
614
+ --info-text: oklch(0.730 0.145 250.4); /* #5AADFF, see below */
606
615
  }
607
616
  }
608
617
 
@@ -612,20 +621,22 @@
612
621
  --elevated: var(--graphite-800);
613
622
  --contrast: var(--graphite-50);
614
623
  --muted: var(--graphite-300);
615
- --soft: var(--graphite-500);
624
+ --subtle: var(--graphite-500);
616
625
  --secondary: var(--graphite-700);
617
- --contour: color-mix(in srgb, var(--graphite-50) 5%, transparent);
618
- --highlight: color-mix(in srgb, var(--graphite-50) 10%, transparent);
619
- --shade: color-mix(in srgb, var(--graphite-50) 15%, transparent);
626
+ --line: color-mix(in srgb, var(--graphite-50) 5%, transparent);
627
+ --edge: color-mix(in srgb, var(--graphite-50) 10%, transparent);
628
+ --contour: color-mix(in srgb, var(--graphite-50) 15%, transparent);
629
+ --highlighted: color-mix(in srgb, var(--graphite-50) 10%, transparent);
630
+ --neutral: color-mix(in srgb, var(--graphite-50) 15%, transparent);
620
631
  --shadow-strength: 0.045;
621
632
  --danger: var(--wave-red-500);
622
- --danger-text: oklch(0.738 0.158 16.8); /* #FE7C86, see below */
633
+ --danger-text: oklch(0.749 0.152 16.8); /* #FF828B, see below */
623
634
  --success: var(--wave-green-500);
624
- --success-text: oklch(0.700 0.155 160); /* #14BB7C, see below */
635
+ --success-text: oklch(0.712 0.161 160); /* #00C07E, see below */
625
636
  --warning: var(--wave-orange-500);
626
- --warning-text: oklch(0.732 0.173 57.3); /* #F78711, see below */
637
+ --warning-text: oklch(0.745 0.179 57.3); /* #FE8A04, see below */
627
638
  --info: var(--wave-blue-500);
628
- --info-text: oklch(0.717 0.150 250.4); /* #52A8FD, see below */
639
+ --info-text: oklch(0.730 0.145 250.4); /* #5AADFF, see below */
629
640
  }
630
641
 
631
642
  /* Graphite's status text. Its mid-tone menus need each status a touch lighter
@@ -636,19 +647,19 @@
636
647
  * palette steps. */
637
648
  @media (color-gamut: p3) {
638
649
  .graphite {
639
- --danger-text: oklch(0.743 0.198 16.8);
640
- --success-text: oklch(0.693 0.212 160);
641
- --warning-text: oklch(0.734 0.198 57.3);
642
- --info-text: oklch(0.717 0.162 250.4);
650
+ --danger-text: oklch(0.755 0.190 16.8);
651
+ --success-text: oklch(0.705 0.219 160);
652
+ --warning-text: oklch(0.748 0.205 57.3);
653
+ --info-text: oklch(0.730 0.157 250.4);
643
654
  }
644
655
  }
645
656
 
646
657
  @media (prefers-color-scheme: dark) and (color-gamut: p3) {
647
658
  :where(:root:not(.paper):not(.graphite):not(.ink)) {
648
- --danger-text: oklch(0.743 0.198 16.8);
649
- --success-text: oklch(0.693 0.212 160);
650
- --warning-text: oklch(0.734 0.198 57.3);
651
- --info-text: oklch(0.717 0.162 250.4);
659
+ --danger-text: oklch(0.755 0.190 16.8);
660
+ --success-text: oklch(0.705 0.219 160);
661
+ --warning-text: oklch(0.748 0.205 57.3);
662
+ --info-text: oklch(0.730 0.157 250.4);
652
663
  }
653
664
  }
654
665
 
@@ -664,11 +675,13 @@
664
675
  --elevated: var(--ink-800);
665
676
  --contrast: var(--ink-50);
666
677
  --muted: var(--ink-400);
667
- --soft: var(--ink-600);
678
+ --subtle: var(--ink-600);
668
679
  --secondary: var(--ink-700);
669
- --contour: color-mix(in srgb, var(--ink-50) 5%, transparent);
670
- --highlight: color-mix(in srgb, var(--ink-50) 10%, transparent);
671
- --shade: color-mix(in srgb, var(--ink-50) 15%, transparent);
680
+ --line: color-mix(in srgb, var(--ink-50) 5%, transparent);
681
+ --edge: color-mix(in srgb, var(--ink-50) 10%, transparent);
682
+ --contour: color-mix(in srgb, var(--ink-50) 15%, transparent);
683
+ --highlighted: color-mix(in srgb, var(--ink-50) 10%, transparent);
684
+ --neutral: color-mix(in srgb, var(--ink-50) 15%, transparent);
672
685
  --shadow-strength: 0.045;
673
686
  --danger: var(--wave-red-500);
674
687
  --danger-text: var(--wave-red-400);
@@ -683,7 +696,7 @@
683
696
  /* ---------------------------------------------------------------------------
684
697
  * Tailwind v4 theme mapping — maps CSS variables to Tailwind color tokens.
685
698
  * Consumers MUST include this for Tailwind classes like `bg-primary`,
686
- * `text-muted`, `border-contour` etc. to work.
699
+ * `text-muted`, `border-line` etc. to work.
687
700
  * ------------------------------------------------------------------------- */
688
701
 
689
702
  @theme inline {
@@ -692,10 +705,10 @@
692
705
  --color-surface: var(--surface);
693
706
  --color-elevated: var(--elevated);
694
707
 
695
- /* Contrast / text — `contrast` (highest emphasis) → `muted` → `soft`. */
708
+ /* Contrast / text, strongest to faintest — `contrast` → `muted` → `subtle`. */
696
709
  --color-contrast: var(--contrast);
697
710
  --color-muted: var(--muted);
698
- --color-soft: var(--soft);
711
+ --color-subtle: var(--subtle);
699
712
 
700
713
  /* Inverse — maps the `:root` inverse aliases to utilities. */
701
714
  --color-surface-inverse: var(--surface-inverse);
@@ -706,20 +719,27 @@
706
719
  --color-primary: var(--primary);
707
720
  --color-secondary: var(--secondary);
708
721
  --color-accent: var(--accent);
709
-
710
- /* Form — the theme's foreground (its text colour) made translucent, at
711
- * three strengths, the way a form is drawn: `contour` marks where a shape
712
- * ends: hairline borders and separators. `highlight` marks where attention
713
- * lands: hovered, keyboard-highlighted and open items. `shade` gives a
714
- * control its body: field and track fills and outline-control borders.
715
- * Every theme uses the same strengths: contour 5%, highlight 10%, shade
716
- * 15% of its text colour (Graphite's and Ink's 50, Paper's 950).
717
- * All three are translucent, so each reads the same on any surface beneath
718
- * it, and `shade` doubles as the intense border (layout regions, control
719
- * outlines). `focus` is the brand-themed ring (= primary). */
722
+ --color-selected: var(--selected);
723
+
724
+ /* Lines and fills — the theme's text colour made translucent (Graphite's
725
+ * and Ink's 50, Paper's 950), so each reads the same on any surface beneath
726
+ * it. Every theme uses the same strengths.
727
+ * Lines, one job each: `line` (5%) divides: separators, table rows,
728
+ * hairlines. `edge` (10%) bounds a surface: cards, dialogs, popups, toasts,
729
+ * always as a border (rings are kept for focus and invalid halos).
730
+ * `contour` (15%) outlines a control or a region: outline buttons, badges,
731
+ * toggles, the slider thumb, menubars.
732
+ * Fills, one job each: `highlighted` (10%) marks where attention lands:
733
+ * hovered, keyboard-highlighted and open items. `neutral` (15%) gives a
734
+ * control its body, its look before anything is chosen: field and track
735
+ * fills, unchecked boxes. (Tailwind's own `neutral-50` … `neutral-950`
736
+ * greys are a separate palette Wave doesn't use.) `focus` is the
737
+ * brand-themed ring (= primary). */
738
+ --color-line: var(--line);
739
+ --color-edge: var(--edge);
720
740
  --color-contour: var(--contour);
721
- --color-highlight: var(--highlight);
722
- --color-shade: var(--shade);
741
+ --color-highlighted: var(--highlighted);
742
+ --color-neutral: var(--neutral);
723
743
  --color-focus: var(--focus);
724
744
  --color-scrim: var(--scrim);
725
745
 
@@ -813,6 +833,81 @@
813
833
  }
814
834
  }
815
835
 
836
+ /* `scroll-fade` — fades the edges of anything that scrolls, where there is
837
+ * more to scroll. It's a mask, so the content itself turns transparent and
838
+ * whatever is behind shows through: no colour to match, in any theme. Each
839
+ * fade grows with the first 2.5rem of scroll and shrinks over the last, so it
840
+ * never pops in. Two things drive it, so it works on any scroller:
841
+ * - the element's own scroll position, through scroll-driven animations
842
+ * (Chrome, Edge, Safari 26+): a Combobox list, a plain `overflow-auto` box;
843
+ * - ScrollArea, which reports how far each edge is (`--scroll-area-overflow-*`),
844
+ * so its viewport fades in every browser, Firefox included.
845
+ * A plain scroller in a browser without scroll-driven animations (Firefox,
846
+ * for now) simply doesn't fade. `--scroll-fade-size` sets the length, up to a
847
+ * quarter of the box; the horizontal fade follows the text direction. */
848
+ @property --scroll-fade-top { syntax: "<length>"; inherits: false; initial-value: 0px; }
849
+ @property --scroll-fade-bottom { syntax: "<length>"; inherits: false; initial-value: 0px; }
850
+ @property --scroll-fade-start { syntax: "<length>"; inherits: false; initial-value: 0px; }
851
+ @property --scroll-fade-end { syntax: "<length>"; inherits: false; initial-value: 0px; }
852
+ @utility scroll-fade {
853
+ --scroll-fade-x: to right;
854
+ --scroll-fade-top: min(var(--scroll-fade-size, 2.5rem), var(--scroll-area-overflow-y-start, 0px));
855
+ --scroll-fade-bottom: min(var(--scroll-fade-size, 2.5rem), var(--scroll-area-overflow-y-end, 0px));
856
+ --scroll-fade-start: min(var(--scroll-fade-size, 2.5rem), var(--scroll-area-overflow-x-start, 0px));
857
+ --scroll-fade-end: min(var(--scroll-fade-size, 2.5rem), var(--scroll-area-overflow-x-end, 0px));
858
+ /* Controls laid over an edge (Select's scroll arrows) set
859
+ * `--scroll-fade-inset` to their height: the fade then starts where they
860
+ * end, only while that edge is fading. */
861
+ --scroll-fade-inset-top: min(var(--scroll-fade-inset, 0px), var(--scroll-fade-top) * 1000);
862
+ --scroll-fade-inset-bottom: min(var(--scroll-fade-inset, 0px), var(--scroll-fade-bottom) * 1000);
863
+ /* Each fade at most a quarter of the box, so a short list still keeps a
864
+ * row clear between its two fades. */
865
+ --scroll-fade-top-size: min(var(--scroll-fade-top), 25%);
866
+ --scroll-fade-bottom-size: min(var(--scroll-fade-bottom), 25%);
867
+ --scroll-fade-start-size: min(var(--scroll-fade-start), 25%);
868
+ --scroll-fade-end-size: min(var(--scroll-fade-end), 25%);
869
+ mask-image:
870
+ linear-gradient(to bottom, transparent var(--scroll-fade-inset-top), #000 calc(var(--scroll-fade-inset-top) + var(--scroll-fade-top-size)), #000 calc(100% - var(--scroll-fade-inset-bottom) - var(--scroll-fade-bottom-size)), transparent calc(100% - var(--scroll-fade-inset-bottom))),
871
+ linear-gradient(var(--scroll-fade-x), transparent, #000 var(--scroll-fade-start-size), #000 calc(100% - var(--scroll-fade-end-size)), transparent);
872
+ mask-composite: intersect;
873
+ /* What's scrolled into view (an option picked with the arrow keys) stops
874
+ * clear of whatever is faded at that moment, and nowhere is padded where
875
+ * nothing fades (Firefox's plain lists, an edge scrolled to its end). */
876
+ scroll-padding-block:
877
+ calc(var(--scroll-fade-inset-top) + var(--scroll-fade-top-size))
878
+ calc(var(--scroll-fade-inset-bottom) + var(--scroll-fade-bottom-size));
879
+ @variant rtl {
880
+ --scroll-fade-x: to left;
881
+ }
882
+ @supports (animation-timeline: scroll()) {
883
+ animation-name: scroll-fade-top, scroll-fade-bottom, scroll-fade-start, scroll-fade-end;
884
+ animation-timing-function: linear;
885
+ animation-fill-mode: both;
886
+ animation-timeline: scroll(self block), scroll(self block), scroll(self inline), scroll(self inline);
887
+ animation-range:
888
+ 0 var(--scroll-fade-size, 2.5rem),
889
+ calc(100% - var(--scroll-fade-size, 2.5rem)) 100%,
890
+ 0 var(--scroll-fade-size, 2.5rem),
891
+ calc(100% - var(--scroll-fade-size, 2.5rem)) 100%;
892
+ }
893
+ }
894
+ @keyframes scroll-fade-top {
895
+ from { --scroll-fade-top: 0px; }
896
+ to { --scroll-fade-top: var(--scroll-fade-size, 2.5rem); }
897
+ }
898
+ @keyframes scroll-fade-bottom {
899
+ from { --scroll-fade-bottom: var(--scroll-fade-size, 2.5rem); }
900
+ to { --scroll-fade-bottom: 0px; }
901
+ }
902
+ @keyframes scroll-fade-start {
903
+ from { --scroll-fade-start: 0px; }
904
+ to { --scroll-fade-start: var(--scroll-fade-size, 2.5rem); }
905
+ }
906
+ @keyframes scroll-fade-end {
907
+ from { --scroll-fade-end: var(--scroll-fade-size, 2.5rem); }
908
+ to { --scroll-fade-end: 0px; }
909
+ }
910
+
816
911
  /* `motion-color` — interaction-state transition for controls: smoothly
817
912
  * animates color / background / border / ring (box-shadow) / backdrop blur on hover, focus,
818
913
  * pressed, checked, etc. Runs at the `sm` tier (controls are small + fast). A
@@ -842,8 +937,12 @@
842
937
  * cascade, with NO `!important` (banned — see feedback_security). The
843
938
  * resting/enter/exit declarations stay layered so `group-has` still wins.
844
939
  *
845
- * `0.001px` resting blur defeats Lightning CSS's `blur(0) → blur()`
846
- * minification (which emits invalid CSS); it is visually equivalent to 0. */
940
+ * The resting state declares NO `filter`. It stays `none`, and `none` blends
941
+ * with the starting/ending `blur()` as a zero blur (how CSS interpolates
942
+ * `none` with a filter list), so the transitions still run. Don't add a
943
+ * resting `blur()`: Lightning CSS minifies `blur(0)` to the invalid `blur()`,
944
+ * and a stand-in like `blur(0.001px)` is not invisible in WebKit, which draws
945
+ * any non-zero blur at a minimum radius and softens the whole surface. */
847
946
 
848
947
  /* `motion-scrim` — opacity only, no blur. For full-screen backdrops: animating
849
948
  * blur across the whole viewport janks (a flat dim shows none anyway). The
@@ -865,7 +964,6 @@
865
964
  * tier per surface (don't compose — each declares its own unlayered transition).
866
965
  * Companion idioms: `motion-color` (controls) and `motion-scrim` (backdrops). */
867
966
  @utility motion-scale-sm {
868
- filter: blur(0.001px);
869
967
  scale: 1;
870
968
  &[data-starting-style],
871
969
  &[data-ending-style] {
@@ -875,7 +973,6 @@
875
973
  }
876
974
  }
877
975
  @utility motion-scale-md {
878
- filter: blur(0.001px);
879
976
  scale: 1;
880
977
  &[data-starting-style],
881
978
  &[data-ending-style] {
@@ -885,7 +982,6 @@
885
982
  }
886
983
  }
887
984
  @utility motion-scale-lg {
888
- filter: blur(0.001px);
889
985
  scale: 1;
890
986
  &[data-starting-style],
891
987
  &[data-ending-style] {
@@ -899,7 +995,6 @@
899
995
  * trigger's `data-[side]` (Base UI sets it) and settles toward it; default /
900
996
  * side=bottom rises up. Opacity + blur + translate (`--offset-*`), NO scale. */
901
997
  @utility motion-slide-sm {
902
- filter: blur(0.001px);
903
998
  translate: 0;
904
999
  &[data-starting-style],
905
1000
  &[data-ending-style] {
@@ -919,7 +1014,6 @@
919
1014
  &[data-side=inline-end][data-ending-style] { translate: var(--offset-sm) 0; }
920
1015
  }
921
1016
  @utility motion-slide-md {
922
- filter: blur(0.001px);
923
1017
  translate: 0;
924
1018
  &[data-starting-style],
925
1019
  &[data-ending-style] {
@@ -939,7 +1033,6 @@
939
1033
  &[data-side=inline-end][data-ending-style] { translate: var(--offset-md) 0; }
940
1034
  }
941
1035
  @utility motion-slide-lg {
942
- filter: blur(0.001px);
943
1036
  translate: 0;
944
1037
  &[data-starting-style],
945
1038
  &[data-ending-style] {
@@ -962,7 +1055,6 @@
962
1055
  /* `motion-fade-*` — fade in place: opacity + blur, no transform. The signature
963
1056
  * blur-fade for content that should appear without moving or scaling. */
964
1057
  @utility motion-fade-sm {
965
- filter: blur(0.001px);
966
1058
  &[data-starting-style],
967
1059
  &[data-ending-style] {
968
1060
  opacity: 0;
@@ -970,7 +1062,6 @@
970
1062
  }
971
1063
  }
972
1064
  @utility motion-fade-md {
973
- filter: blur(0.001px);
974
1065
  &[data-starting-style],
975
1066
  &[data-ending-style] {
976
1067
  opacity: 0;
@@ -978,7 +1069,6 @@
978
1069
  }
979
1070
  }
980
1071
  @utility motion-fade-lg {
981
- filter: blur(0.001px);
982
1072
  &[data-starting-style],
983
1073
  &[data-ending-style] {
984
1074
  opacity: 0;
@@ -991,7 +1081,6 @@
991
1081
  * recipe with one transition. Don't try `motion-scale-md motion-slide-md`: their
992
1082
  * transitions collide and scale won't animate. md only (no sm/lg consumer). */
993
1083
  @utility motion-pop-md {
994
- filter: blur(0.001px);
995
1084
  scale: 1;
996
1085
  translate: 0;
997
1086
  /* Pop from the anchored EDGE, centered along it — symmetric left/right for
@@ -1166,3 +1255,22 @@
1166
1255
  0%, 70%, 100% { opacity: 1; }
1167
1256
  20%, 50% { opacity: 0; }
1168
1257
  }
1258
+
1259
+ /* ---------------------------------------------------------------------------
1260
+ * SwipeDeck — the numbers each card works out its place from change every
1261
+ * frame of a drag (the cards behind lift toward the front). Unregistered,
1262
+ * custom properties inherit, so each change would restyle everything inside
1263
+ * the cards in sight; registered as not inheriting, it stops at the card.
1264
+ * Measured with heavy cards: about 2.5x less style work in Chromium, and
1265
+ * WebKit back to 60 fps from about 30. The card's public hooks
1266
+ * (`--swipe-deck-progress`, `-movement-x`/`-y`, `-place`, `-index`) still
1267
+ * inherit, so what's inside a card can read them. */
1268
+ @property --swipe-deck-lift { syntax: "*"; inherits: false; }
1269
+ @property --swipe-deck-slot { syntax: "*"; inherits: false; }
1270
+ @property --swipe-deck-layout-slot { syntax: "*"; inherits: false; }
1271
+ @property --swipe-deck-layout-x { syntax: "*"; inherits: false; }
1272
+ @property --swipe-deck-layout-y { syntax: "*"; inherits: false; }
1273
+ @property --swipe-deck-layout-rotate { syntax: "*"; inherits: false; }
1274
+ @property --swipe-deck-layout-scale { syntax: "*"; inherits: false; }
1275
+ @property --swipe-deck-enter-scale { syntax: "*"; inherits: false; }
1276
+ @property --swipe-deck-pose { syntax: "*"; inherits: false; }
@@ -0,0 +1,154 @@
1
+ import { ButtonProps } from "./button.js";
2
+ import * as React from "react";
3
+ //#region src/swipe-deck.d.ts
4
+ type SwipeDeckDirection = "left" | "right" | "up" | "down";
5
+ type SwipeDeckLayout = "stack" | "fan" | "cascade" | "pile";
6
+ interface SwipeDeckSwipeDetails {
7
+ /** The `value` of the card. */
8
+ value: string;
9
+ /** The direction it leaves in. */
10
+ direction: SwipeDeckDirection;
11
+ }
12
+ /** What sent a card off: a drag or throw, an arrow key, or a `SwipeDeckAction`. */
13
+ type SwipeDeckSwipeEventReason = SwipeDeckSwipeCause["reason"];
14
+ type SwipeDeckSwipeCause = {
15
+ reason: "swipe";
16
+ event: PointerEvent;
17
+ } | {
18
+ reason: "keyboard";
19
+ event: KeyboardEvent;
20
+ } | {
21
+ reason: "action-press";
22
+ event: MouseEvent;
23
+ };
24
+ /**
25
+ * `onSwipe`'s second argument: the reason and native event behind a swipe, and
26
+ * a way to cancel it. The parts of Base UI's own event details that apply.
27
+ */
28
+ type SwipeDeckSwipeEventDetails = SwipeDeckSwipeCause & {
29
+ /**
30
+ * Keeps the card in front: a dragged card springs back, and a key or
31
+ * action does nothing.
32
+ */
33
+ cancel: () => void;
34
+ /** Whether `cancel` has been called. */
35
+ readonly isCanceled: boolean;
36
+ };
37
+ type SwipeDeckProps = React.ComponentProps<"div"> & {
38
+ /**
39
+ * Called the moment a card is sent off, before it leaves: count it here, or
40
+ * call `eventDetails.cancel()` to keep it in front. To cycle the deck, move
41
+ * the card to the end of your list here, and it slides back under the pile.
42
+ * Don't remove it here: it would vanish mid-exit, and `onSwipeComplete`
43
+ * would never come.
44
+ */
45
+ onSwipe?: (details: SwipeDeckSwipeDetails, eventDetails: SwipeDeckSwipeEventDetails) => void;
46
+ /**
47
+ * Called once a card is done leaving: faded out, or (moved to the back in
48
+ * `onSwipe`) on its way under the pile. Remove it from your list here; a
49
+ * card your list keeps comes back, in front or wherever you moved it.
50
+ * Return a promise, such as a save, to keep a faded card out until it
51
+ * settles; a card sliding under the pile is back already.
52
+ */
53
+ onSwipeComplete?: (details: SwipeDeckSwipeDetails) => void | Promise<unknown>;
54
+ /** The directions a card can leave in. Defaults to left and right. */
55
+ directions?: readonly SwipeDeckDirection[];
56
+ /** Stops cards leaving by pointer, keyboard or action. */
57
+ disabled?: boolean;
58
+ /**
59
+ * How the cards behind the front one sit: `stack` peeks them above it, `fan`
60
+ * turns them out like a hand of cards, `cascade` steps them up and to the
61
+ * side, and `pile` scatters them. Defaults to `fan`. `fan` and `cascade`
62
+ * open toward the end of the line: right in left-to-right text, left in
63
+ * right-to-left.
64
+ */
65
+ layout?: SwipeDeckLayout;
66
+ /** How far the cards behind spread out, from 0 (not at all) to 2. Defaults to 0.5. */
67
+ spread?: number;
68
+ /** How much each card behind shrinks, from 0 (not at all) to 1. Defaults to 0.5. */
69
+ depth?: number;
70
+ /** How many cards show, the front one included; at least 1. Defaults to 4. */
71
+ visible?: number;
72
+ };
73
+ type SwipeDeckItemProps = React.ComponentProps<"div"> & {
74
+ /**
75
+ * Names the card in `onSwipe`. Use the same id as its `key`, unique among
76
+ * the deck's cards, those still leaving included: a card dealt again is a
77
+ * new card and needs a new value.
78
+ */
79
+ value: string;
80
+ };
81
+ type SwipeDeckEmptyProps = React.ComponentProps<"div">;
82
+ type SwipeDeckActionProps = ButtonProps & {
83
+ /** The direction the front card leaves in when pressed. */
84
+ direction: SwipeDeckDirection;
85
+ };
86
+ /**
87
+ * A pile of cards people swipe away: drag or throw the front card, press an
88
+ * arrow key while the deck is focused, or use a `SwipeDeckAction`. A let-go
89
+ * card whose position and throw carry it past a quarter of its width (or of
90
+ * its height, up and down) leaves:
91
+ * it slides out the way it is heading until clear of the pile, held by a
92
+ * spring that a hard throw runs out against, then fades. Short of that, it
93
+ * springs back. Pressed, a card lifts under the pointer; dragged, it tilts
94
+ * with the throw.
95
+ *
96
+ * Your list stays the source of truth. `onSwipe` comes the moment a card is
97
+ * sent off: count it there, or cancel to keep it. `onSwipeComplete` comes once
98
+ * it has gone: remove it from your list then, not in `onSwipe`, where removing
99
+ * it would cut its exit short. To cycle the deck instead, move the card to
100
+ * the end of your list in `onSwipe`, and it slides back under the pile. To
101
+ * undo a swipe, put the card back at the start of your list.
102
+ *
103
+ * The cards are the deck's own children, in your list's order. A long list
104
+ * needn't be rendered whole: the deck shows `visible` cards, so the next few
105
+ * dozen are plenty.
106
+ *
107
+ * End the deck with a `SwipeDeckEmpty`: it lies under the last card, so
108
+ * dragging that card shows what comes next.
109
+ *
110
+ * Styling hooks: `data-layout`, `data-disabled`, and `data-empty` once no
111
+ * card is in front (the last one leaving, or none left). CSS variables:
112
+ * `--swipe-deck-card-width` / `-height`, the size of the largest card.
113
+ *
114
+ * Give the deck an `aria-label`. Screen readers hear the next card after each
115
+ * swipe: its `aria-label` when it has one, otherwise all of its text.
116
+ */
117
+ declare function SwipeDeck({ onSwipe, onSwipeComplete, directions, disabled, layout, spread, depth, visible, className, style, onKeyDown, children, ref, ...props }: SwipeDeckProps): React.JSX.Element;
118
+ /**
119
+ * One card in the deck, a direct child of it. It sits in the deck's first grid
120
+ * cell with every other card, so the deck takes the size of its largest card.
121
+ *
122
+ * Styling hooks: `data-front` on the top card, `data-swiping` while it is
123
+ * dragged, `data-leaving` from the moment it leaves, `data-starting-style`
124
+ * while it comes in and `data-ending-style` once it is clear of the pile and
125
+ * fading (the `motion-fade-lg` recipe), and `data-swipe-direction` for the
126
+ * way it leans or leaves. CSS variables, which what's inside the card can
127
+ * read too: `--swipe-deck-index` (its place in the pile, 0 at the front),
128
+ * `--swipe-deck-place` (that place as it springs from one to the next),
129
+ * `--swipe-deck-movement-x` / `-y` in px, and `--swipe-deck-progress` from 0
130
+ * at rest to 1 where letting go makes the card leave.
131
+ *
132
+ * The card owns its translate, rotate, scale and transform, so its motion
133
+ * recipe can only be a `motion-fade-*` one: a recipe that transitions those
134
+ * would lag every drag.
135
+ */
136
+ declare function SwipeDeckItem({ value, className, style, ref, onPointerDown, onClickCapture, onDragStart, ...props }: SwipeDeckItemProps): React.JSX.Element;
137
+ /**
138
+ * What the deck shows once its cards run out: a message, an action (start
139
+ * over, carry on), anything you put in it. It sits where the cards sit, at
140
+ * their size, inside a dashed outline of their shape (`data-slot`
141
+ * `swipe-deck-empty-outline`, to restyle or hide). It is drawn as soon as one
142
+ * card is left and has come to rest, under that card, so dragging it shows
143
+ * what comes next; it can be reached (Tab, clicks) and screen readers hear it
144
+ * (its `aria-label`, or its text) once that last card has gone too.
145
+ */
146
+ declare function SwipeDeckEmpty({ className, children, ref, ...props }: SwipeDeckEmptyProps): React.JSX.Element;
147
+ /**
148
+ * A button that sends the front card off in `direction`: the way to swipe
149
+ * without dragging. Disabled (and still focusable) when the deck is disabled,
150
+ * has no card in front, or can't go that way.
151
+ */
152
+ declare function SwipeDeckAction({ direction, disabled, onClick, onKeyDown, ...props }: SwipeDeckActionProps): React.JSX.Element;
153
+ //#endregion
154
+ export { SwipeDeck, SwipeDeckAction, type SwipeDeckActionProps, type SwipeDeckDirection, SwipeDeckEmpty, type SwipeDeckEmptyProps, SwipeDeckItem, type SwipeDeckItemProps, type SwipeDeckLayout, type SwipeDeckProps, type SwipeDeckSwipeDetails, type SwipeDeckSwipeEventDetails, type SwipeDeckSwipeEventReason };