@blakesteve/roster 4.8.2 → 4.10.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
@@ -186,13 +186,44 @@ they wrap your content and that text is not Roster's to restyle. `Eyebrow`,
186
186
 
187
187
  `solid` is the one variant where a component picks both the background and the
188
188
  text on it, so it can fail contrast on its own with no help from your app. Every
189
- solid fill in `Badge`, `Pill` and `Button` is measured against WCAG AA (4.5:1)
189
+ solid fill in `Badge`, `Pill`, `Chip`, `Toast` and `Button` is measured against WCAG AA
190
+ (4.5:1)
190
191
  by `src/contrast.test.ts`, at rest **and** on hover, in both themes.
191
192
 
192
193
  `Checkbox` is measured too, at 3:1 rather than 4.5:1 — its tick is a graphical
193
194
  object under WCAG 1.4.11, not text. It used to be excluded from the suite
194
195
  entirely, which measured it against nothing.
195
196
 
197
+ `CallToAction` is **not** in that suite, and its light surface changed: the
198
+ fills moved a ramp step and the borders moved four, because the old surface
199
+ measured 1.03:1 against a light page with a 1.30:1 border — a banner you could
200
+ not see. **Installing this version changes how your existing CTAs look**, the
201
+ same way the 1.4.11 pass changed every field in 4.8.2. Its `info` variant also
202
+ moves from Tailwind's stock `blue-*` to Roster's `info-*`, so it now answers to
203
+ `--roster-info-*` like every other scheme.
204
+
205
+ Worth knowing what that border step is and is not. It is a **prominence**
206
+ decision, not a 1.4.11 one: a tinted banner is not a control, so the criterion
207
+ no longer reaches it once the surface has a fill. A call to action is the one
208
+ component whose job is to be looked at first, so it is deliberately the most
209
+ defined bordered block in the library. The filled surfaces it sits beside are
210
+ much fainter — `Card` soft at 1.04:1, `ErrorState` at 1.39:1, `Toast` and
211
+ `Pill` between 1.34:1 and 1.82:1. `Alert` is the closest, and only because its
212
+ 4px left stripe is already a `-500`, at 6.10:1 for primary.
213
+
214
+ `CallToAction` and `Countdown` also lay themselves out with **CSS container
215
+ queries** rather than `md:` and `sm:`. The switch is now the CARD's width, not
216
+ the window's — a 228px card on a 1200px page used to lay out as a row and
217
+ squeeze its own text until it overflowed. Three things follow:
218
+
219
+ - **The breakpoint number changed** from a 768px viewport to a 512px card. A
220
+ CTA in a narrow page column will stack where it did not before.
221
+ - **`container-type: inline-size` collapses inside a content-sized ancestor.**
222
+ Give any `width: fit-content`, `inline-block` or table-cell ancestor a
223
+ definite width, or the card measures zero.
224
+ - **Container queries are required**: Chrome 105+, Safari 16+, Firefox 110+.
225
+ Without them a `CallToAction` renders in its narrow form at every width.
226
+
196
227
  #### Ink follows the fill
197
228
 
198
229
  The foreground is a token per fill, not per family:
@@ -550,6 +581,340 @@ shared size, so they cannot drift apart. `Button` also has `xs` and `icon`,
550
581
  which neither field has an equivalent for. `Textarea` is not on this scale — it
551
582
  is multi-line, so the answer there is a min-height rather than a height.
552
583
 
584
+ ### Toasts
585
+
586
+ Mount `Toaster` once near the root, then call `toast` from anywhere:
587
+
588
+ ```tsx
589
+ import { Toaster, toast } from "@blakesteve/roster";
590
+
591
+ <Toaster position="bottom-right" />;
592
+
593
+ toast.success("Saved");
594
+ toast.error("That name is taken");
595
+ toast.info("Week 4 matchups are up");
596
+ toast.warning("Picks close in ten minutes");
597
+ ```
598
+
599
+ It wraps [`react-hot-toast`](https://react-hot-toast.com), which is a **peer
600
+ dependency** — install it alongside Roster. It has to be external rather than
601
+ bundled: its queue lives in module scope, so a bundled copy would give you two
602
+ stores, and toasts fired from your own `import { toast } from "react-hot-toast"`
603
+ would silently never appear.
604
+
605
+ An app already using it keeps its call sites and only changes the import. What changes is
606
+ the styling: the toast body is a real `Toast` component reading `--roster-*`
607
+ rather than the inline hex each app was pinning.
608
+
609
+ Two of those four helpers are Roster's own. `react-hot-toast` ships `success`,
610
+ `error`, `loading` and `blank` — there is no `info` and no `warning`, so an app
611
+ that wants one improvises, and at least one improvised by calling `toast.error`
612
+ for `info`.
613
+
614
+ **Errors are announced assertively, everything else politely.** That is also
615
+ Roster's decision rather than the library's: `react-hot-toast` marks every
616
+ toast `status` / `polite`, and a polite live region is read when the reader
617
+ next pauses — which for a message that disappears in four seconds can mean
618
+ never.
619
+
620
+ `variant` on `Toaster` sets the fill for the whole queue:
621
+
622
+ - **`soft`** (default) — the tinted fill `Alert` uses. Quiet enough that a stack
623
+ does not shout, which is the common case.
624
+ - **`solid`** — the `-500` fill and ink token, step for step with `Pill` and
625
+ `Chip`. The one to reach for when toasts land over photography, video, or any
626
+ surface the app does not own: `bg-success-50` is a whisper on a white page
627
+ and illegible over an image.
628
+ - **`glass`** — a neutral translucent surface at 60% with a backdrop blur,
629
+ matching `Dialog`'s variant of the same name. The tone lives on the **border
630
+ only**; the surface and the text are neutral for every scheme.
631
+
632
+ Glass is neutral for a reason, and the border-only tone is the cost. Anything
633
+ whose contrast depends on the backdrop is a guess: a colored fill measured
634
+ 2.60:1, and colored text on a neutral fill measured 2.81:1 — worse. Only the
635
+ neutral ink is safe in both directions, at 17.49 over white, 6.14 over black
636
+ and 9.22 over a brand color — 60% is as far as that allows, with 4.69 the worst
637
+ case in dark. If the tone needs to be unmistakable, use `solid`.
638
+
639
+ `src/contrast.test.ts` measures the `solid` fills and deliberately does not
640
+ measure `glass` — a number there would be a number for one background.
641
+
642
+ ### Anchored popups share their internals
643
+
644
+ `Select`, `Combobox` and `MultiSelect` open the same panel, and deliberately
645
+ so: a combobox that opened a different-looking menu than a select on the same
646
+ form is the bug worth preventing. The panel classes, the option rows and the dark-mode carry
647
+ live in `src/internal/`, which is **not exported** — its contract is
648
+ with those components, not with you. The escape hatch you reach for is
649
+ `optionsClassName` on the component itself.
650
+
651
+ Two things in there are worth knowing about even from outside, because both
652
+ look like oversights until you know why:
653
+
654
+ - **The panel sets no `max-height` and no `overflow`.** Headless UI's `size`
655
+ middleware writes both inline whenever `anchor` is set, so a utility would
656
+ lose to it anyway. To cap a menu shorter than the viewport allows, set the
657
+ variable that inline rule reads:
658
+ `optionsClassName="rst:[--anchor-max-height:20rem]"`.
659
+ - **The panel is portaled to `<body>`**, so a `.dark` scoped to part of your
660
+ page does not reach it. All three components copy the nearest `.dark` onto
661
+ the panel to fix that. Custom properties are not carried the same way — set
662
+ `--roster-popover-*` at `:root`.
663
+ - **`variant="slate"` opens on the dark palette**, in light mode too. That
664
+ variant paints a dark trigger, and its panel was rendering white: a dark
665
+ control opening a white sheet, which is the mismatch `--roster-popover-*`
666
+ exists to prevent. The token family only ever covered the variant that reads
667
+ tokens, and the ones that name a surface were left behind. It reuses the dark
668
+ palette rather than introducing a slate one, because every option state there
669
+ already exists and has already been measured.
670
+
671
+ ### Two ways to select more than one thing
672
+
673
+ `MultiSelect` and `CheckboxGroup` both hold a set. They are not a fallback for
674
+ each other:
675
+
676
+ - **`MultiSelect`** when the field is one row of a form and the options do not
677
+ need to be read at a glance. It costs a click to see the list, which is what
678
+ makes it the right control for a dialog. Note that the default trigger still
679
+ grows as chips wrap — `maxChips` or `display="count"` is what actually pins
680
+ it to one row.
681
+ - **`CheckboxGroup`** when scanning every option matters more than saving the
682
+ space, or when the options arrive in labeled categories. Nothing is hidden
683
+ and nothing needs a click to be read.
684
+
685
+ Density is the axis, not correctness. The same eight sports are a good
686
+ `MultiSelect` in a cramped creation dialog and a good `CheckboxGroup` on a
687
+ settings page.
688
+
689
+ Swapping one for the other is not free, though, and it is worth saying which
690
+ parts move. `label`, `helperText` and `errorMessage` are the same three names
691
+ in both. The value types are not: `CheckboxGroup` is `string[]`, `MultiSelect`
692
+ is `(string | number)[]`, so state typed for one does not assign to the other.
693
+ Neither do the options — `CheckboxGroupOption` carries a `description` that
694
+ `SelectOption` has no field for, and `CheckboxGroup` additionally accepts
695
+ `{ category, options }` groups that `MultiSelect` cannot take at all. Headless
696
+ UI's `Listbox` has no grouping primitive, so option groups in the dropdown
697
+ would be Roster's to build.
698
+
699
+ #### MultiSelect
700
+
701
+ The same Headless UI `Listbox` as `Select`, with `multiple` set, opening the
702
+ same panel from `src/internal/popup.ts`. The panel stays open across picks;
703
+ clicking a selected option again removes it.
704
+
705
+ ```tsx
706
+ <MultiSelect
707
+ label="Sports"
708
+ options={SPORTS}
709
+ value={sports}
710
+ onChange={setSports}
711
+ maxChips={3}
712
+ clearable
713
+ />
714
+ ```
715
+
716
+ Selections render as chips in the trigger, and each chip is individually
717
+ dismissible without opening the menu.
718
+
719
+ Getting that required a specific structure, and it is the interesting part of
720
+ the component. A dismiss control is a `<button>` and `ListboxButton` is a
721
+ button, so the chips cannot be its children — that pair is invalid HTML, and
722
+ browsers do not rescue it either: React builds the DOM through the DOM API
723
+ rather than the parser, which leaves it nested, so it would ship as a focusable
724
+ control whose every click also opens the listbox. So the trigger is a **shell**
725
+ with the `ListboxButton` stretched `inset-0` across it, and the chips are the
726
+ button's siblings drawn on top.
727
+
728
+ That keeps the button the full-width element, which matters more than it
729
+ sounds: Headless UI anchors the panel to the button and sizes it from
730
+ `--button-width`, and a trigger that stops being full width is how `Combobox`
731
+ once shipped a 20px unreadable menu. Measured after the change, the panel still
732
+ matches the field exactly.
733
+
734
+ Everything decorative in the shell is `pointer-events-none`, so a click on a
735
+ chip's label falls through and opens the menu like the rest of the field. Only
736
+ the dismiss controls take their events back. Each is named after its own chip —
737
+ a row of buttons all called "Remove" is a row a screen reader user cannot tell
738
+ apart — and `removeLabel` takes a function if "Remove NFL" is not the phrasing
739
+ you want. The chips are hidden from the trigger's accessible name, which
740
+ carries a punctuated summary instead: they are `inline-flex` spans, and name
741
+ computation joins them with no whitespace, so "NCAA Football" followed by "NBA"
742
+ announced as "FootballNBA".
743
+
744
+ The chips default to `outline` rather than a soft fill. The default trigger
745
+ paints `--roster-control-bg`, which is `transparent`, so a chip sits on
746
+ whatever the page is: a `soft neutral` chip is `gray-100`, which on this
747
+ library's own `gray-50` page is a 1.04:1 rectangle. It rendered, it measured
748
+ correct, and it could not be seen.
749
+
750
+ `outline`'s identity is a border rather than a fill, but that alone was not
751
+ enough either. Chip holds its border to 3:1 against a *page*, and this
752
+ component can be told to paint its own surface — `variant="slate"` paints
753
+ `gray-700`, byte-identical to the neutral chip's own `gray-700` label, which
754
+ rendered chip-shaped holes at 1.00:1. So the chips take `text-inherit` and
755
+ `border-current`, in both schemes, and cannot disagree with the surface they
756
+ were placed on. The dismiss and clear controls inherit the same way, because
757
+ the shell is what carries the trigger's color and they all sit inside it.
758
+ Measured across all five variants in both schemes: chip label 9.42 to 18.11,
759
+ chip border 4.33 to 8.99, glyphs 5.59 to 8.99.
760
+
761
+ Each dismiss control's hit area is grown to 31x28 with an invisible `::after`.
762
+ The glyph's own box is 15x12, and WCAG 2.5.8 asks 24x24 — the spacing exception
763
+ does not rescue it, because the trigger is another target sitting directly
764
+ underneath. That fix landed in `Chip` rather than here, so every dismissible
765
+ chip gets it.
766
+
767
+ `maxChips` collapses the tail into a `+N` chip and `display="count"` replaces
768
+ the chips with `N selected`, so a field with eight selections does not become
769
+ four rows tall. A selected value with no matching option renders as its own id
770
+ rather than vanishing from a field that still reports it.
771
+
772
+ #### CheckboxGroup
773
+
774
+ Three things it adds over the version mega-squad's squad form hand-rolled:
775
+
776
+ - **Each option is a Headless UI `Field`**, so its label is wired to its own
777
+ checkbox and clicking the text toggles it. The pointer cursor sits on the
778
+ label rather than on the row, because the label *is* the hit area: at two
779
+ columns, a short option like `NFL` fills about a quarter of its grid cell,
780
+ and a row-wide affordance would advertise the other three quarters as
781
+ clickable.
782
+ - **Each category is a nested fieldset**, so `Football` is the accessible name
783
+ of the group under it rather than a styled `div` a screen reader walks past.
784
+ The heading is still a `<span>`, not a heading element, so it does not appear
785
+ in a screen reader's heading list — the name is reachable by entering the
786
+ group, which is what the nesting buys.
787
+ - **`errorMessage` reaches the fieldset** as `aria-describedby`, wired by hand
788
+ rather than through Headless UI's `Description`. `Fieldset` collects
789
+ descendant labels but never calls `useDescriptions` and provides no
790
+ description context, so a `Description` dropped inside one throws rather than
791
+ degrading quietly. Per-option `description` text does go through `Field`,
792
+ which wires it properly — the distinction is real: a group description is
793
+ announced on entering the group, an option's on focusing that checkbox.
794
+
795
+ Both `aria-describedby` and `aria-invalid` are set **after** `...props` is
796
+ spread, and the describedby is merged with any the caller passes. Set before,
797
+ a caller's own `aria-describedby` would replace the error association with no
798
+ type error and no warning: a group rendering red text and reporting itself
799
+ valid.
800
+
801
+ `variant="panel"` draws the enclosing box for the long-list case. It
802
+ deliberately does not read `--roster-popover-*`: that family is for surfaces
803
+ that float over the page, and this one is in the flow, so it takes `Card`'s
804
+ `soft` fill. Its border is two ramp steps heavier than that card's, and that
805
+ part is not shared: a card's hairline separates content from the page, while
806
+ this one has to read as the wall a scroll region ends at.
807
+
808
+ `maxHeight` is applied inline rather than as a class, because the useful values
809
+ are arbitrary and a Tailwind class built from a prop is a class that does not
810
+ exist at build time. A capped panel takes a tab stop of its own — Chrome and
811
+ Firefox focus scrollers by themselves now, Safari does not, and a `disabled`
812
+ group has no focusable option to tab to at all, so the region would be
813
+ unscrollable from the keyboard.
814
+
815
+ `errorMessage` repaints less here than it does on `Input` or `Select`, on
816
+ purpose. `panel` gets an error border; `plain` has no boundary to repaint, so
817
+ the message is the whole error state. A checkbox is not the thing that is
818
+ invalid — the selection is — so nothing rings the individual controls.
819
+
820
+ ```tsx
821
+ <CheckboxGroup
822
+ label="Select supported sports"
823
+ helperText="You can always add more later."
824
+ variant="panel"
825
+ columns={2}
826
+ maxHeight={240}
827
+ options={[
828
+ { category: "Football", options: [{ value: "nfl", label: "NFL" }] },
829
+ { category: "Hockey", options: [{ value: "nhl", label: "NHL" }] },
830
+ ]}
831
+ value={sports}
832
+ onChange={setSports}
833
+ />
834
+ ```
835
+
836
+ `onChange` appends and filters rather than rebuilding the array from `options`.
837
+ A rebuild sorts the result into declaration order, which reads tidier and
838
+ silently drops any selected value whose option has not loaded yet.
839
+
840
+ ### Multi-selection is a checklist, not a menu
841
+
842
+ `CheckboxGroup` exists instead of a multi-select `Select`, and the reason is
843
+ that neither multi-selection UI in the portfolio was ever a dropdown.
844
+ mega-squad's squad form picks sports from a grouped, scrollable checklist with
845
+ category headings; bb-memorial's share form picks tags from a flat row of
846
+ toggles. Collapsing either into a menu would have hidden the options behind a
847
+ click to save vertical space neither screen was short of, so the component
848
+ follows the shape that already worked. A multi-select dropdown is still the
849
+ right answer for a list too long to show — when something in the portfolio has
850
+ one, that is when to build it.
851
+
852
+ Three things it adds over the hand-rolled version it replaces:
853
+
854
+ - **Each option is a Headless UI `Field`**, so its label is wired to its own
855
+ checkbox and clicking the text toggles it. The pointer cursor sits on the
856
+ label rather than on the row, because the label *is* the hit area: at two
857
+ columns, a short option like `NFL` fills about a quarter of its grid cell,
858
+ and a row-wide affordance would advertise the other three quarters as
859
+ clickable.
860
+ - **Each category is a nested fieldset**, so `Football` is the accessible name
861
+ of the group under it rather than a styled `div` a screen reader walks past.
862
+ The heading is still a `<span>`, not a heading element, so it does not appear
863
+ in a screen reader's heading list — the name is reachable by entering the
864
+ group, which is what the nesting buys.
865
+ - **`errorMessage` reaches the fieldset** as `aria-describedby`, wired by hand
866
+ rather than through Headless UI's `Description`. `Fieldset` collects
867
+ descendant labels but never calls `useDescriptions` and provides no
868
+ description context, so a `Description` dropped inside one throws rather than
869
+ degrading quietly. Per-option `description` text does go through `Field`,
870
+ which wires it properly — the distinction is real: a group description is
871
+ announced on entering the group, an option's on focusing that checkbox.
872
+
873
+ Both `aria-describedby` and `aria-invalid` are set **after** `...props` is
874
+ spread, and the describedby is merged with any the caller passes. Set before,
875
+ a caller's own `aria-describedby` would replace the error association with no
876
+ type error and no warning: a group rendering red text and reporting itself
877
+ valid.
878
+
879
+ `variant="panel"` draws the enclosing box for the long-list case. It
880
+ deliberately does not read `--roster-popover-*`: that family is for surfaces
881
+ that float over the page, and this one is in the flow, so it takes `Card`'s
882
+ `soft` fill. Its border is two ramp steps heavier than that card's, and that
883
+ part is not shared: a card's hairline separates content from the page, while
884
+ this one has to read as the wall a scroll region ends at.
885
+
886
+ `maxHeight` is applied inline rather than as a class, because the useful values
887
+ are arbitrary and a Tailwind class built from a prop is a class that does not
888
+ exist at build time. A capped panel takes a tab stop of its own — Chrome and
889
+ Firefox focus scrollers by themselves now, Safari does not, and a `disabled`
890
+ group has no focusable option to tab to at all, so the region would be
891
+ unscrollable from the keyboard.
892
+
893
+ `errorMessage` repaints less here than it does on `Input` or `Select`, on
894
+ purpose. `panel` gets an error border; `plain` has no boundary to repaint, so
895
+ the message is the whole error state. A checkbox is not the thing that is
896
+ invalid — the selection is — so nothing rings the individual controls.
897
+
898
+ ```tsx
899
+ <CheckboxGroup
900
+ label="Select supported sports"
901
+ helperText="You can always add more later."
902
+ variant="panel"
903
+ columns={2}
904
+ maxHeight={240}
905
+ options={[
906
+ { category: "Football", options: [{ value: "nfl", label: "NFL" }] },
907
+ { category: "Hockey", options: [{ value: "nhl", label: "NHL" }] },
908
+ ]}
909
+ value={sports}
910
+ onChange={setSports}
911
+ />
912
+ ```
913
+
914
+ `onChange` appends and filters rather than rebuilding the array from `options`.
915
+ A rebuild sorts the result into declaration order, which reads tidier and
916
+ silently drops any selected value whose option has not loaded yet.
917
+
553
918
  ### Components that render links
554
919
 
555
920
  `Breadcrumbs` renders a plain `<a>` by default, which is right for a static
@@ -606,12 +971,15 @@ function App() {
606
971
  | `Button` | Primary interactive element: solid, soft, outline, ghost, link variants |
607
972
  | `Card` | Bordered surface container |
608
973
  | `Checkbox` | Accessible checkbox with label support |
974
+ | `Combobox` | A `Select` you can type into: filters as you type, same panel and size scale |
975
+ | `Chip` | The interactive one of Badge / Pill / Chip: removable, selectable, or both |
609
976
  | `Disclosure` | Show/hide toggle using HeadlessUI |
610
977
  | `Eyebrow` | Small tracked-out uppercase label above a heading or beside a rule; polymorphic via `as` |
611
978
  | `InlineCode` | Inline `<code>` for identifiers in running prose |
612
979
  | `Input` | Text input with label, error state, icon slots, and a size scale matching `Button` |
613
980
  | `LabeledDivider` | Horizontal rule carrying a label, with an optional trailing count |
614
981
  | `Link` | Styled anchor with variant support |
982
+ | `MultiSelect` | A `Select` that holds more than one value: dismissible chips, same panel |
615
983
  | `PasswordInput` | Password field with a show/hide reveal toggle |
616
984
  | `Pill` | Inline phrase chrome: social proof, live state, applied filters |
617
985
  | `AvatarStrip` | Stacked avatar row with overflow chip, dismiss button, trailing slot, and label area |
@@ -633,12 +1001,14 @@ function App() {
633
1001
  | `Accordion` | Collapsible content sections (single or multi-expand) |
634
1002
  | `Alert` | Inline notice strip with optional title and dismiss |
635
1003
  | `Breadcrumbs` | Navigation trail; pass `linkComponent` to keep navigation client-side |
1004
+ | `CheckboxGroup` | Checkbox set reporting one `string[]`; groups, columns, scroll, error |
636
1005
  | `CallToAction` | Prominent hero-style CTA block |
637
1006
  | `DescriptionList` | Label and value pairs as a real `<dl>`: inline, stacked, or split |
638
1007
  | `EmptyState` | Zero-data placeholder with icon and action slot |
639
1008
  | `ErrorState` | Error display with retry action |
640
1009
  | `MatchupCard` | Head-to-head comparison card |
641
1010
  | `Pullquote` | A line lifted out of prose, as `<figure>` + `<blockquote>` + `<figcaption>` |
1011
+ | `Toast` | Transient floating notice; `Toaster` + the imperative `toast` handle |
642
1012
 
643
1013
  ### Organisms
644
1014
 
@@ -4880,8 +4880,8 @@ export {
4880
4880
  ss as $,
4881
4881
  cs as A,
4882
4882
  Rs as B,
4883
- ds as C,
4884
- Df as D,
4883
+ Df as C,
4884
+ ds as D,
4885
4885
  ks as E,
4886
4886
  xf as F,
4887
4887
  He as G,
@@ -4896,22 +4896,22 @@ export {
4896
4896
  Hf as P,
4897
4897
  su as Q,
4898
4898
  Uf as R,
4899
- Yf as S,
4899
+ Jf as S,
4900
4900
  Pf as T,
4901
- Qf as U,
4901
+ Yf as U,
4902
4902
  Ns as V,
4903
- fu as W,
4904
- ru as X,
4903
+ Qf as W,
4904
+ fu as X,
4905
4905
  Ps as Y,
4906
- Zf as Z,
4906
+ ru as Z,
4907
4907
  Bf as _,
4908
4908
  Tf as a,
4909
- Jf as a0,
4910
- qf as a1,
4911
- tu as a2,
4912
- Kf as a3,
4913
- iu as a4,
4914
- _s as a5,
4909
+ Zf as a0,
4910
+ _s as a1,
4911
+ qf as a2,
4912
+ tu as a3,
4913
+ Kf as a4,
4914
+ iu as a5,
4915
4915
  _f as a6,
4916
4916
  If as a7,
4917
4917
  js as a8,
@@ -0,0 +1,57 @@
1
+ import { VariantProps } from 'class-variance-authority';
2
+ import { chipVariants } from './chip-variants';
3
+ import * as React from "react";
4
+ export interface ChipProps extends Omit<React.HTMLAttributes<HTMLSpanElement>, "onSelect">, Omit<VariantProps<typeof chipVariants>, "disabled"> {
5
+ children: React.ReactNode;
6
+ /** Leading icon or avatar. */
7
+ leadingIcon?: React.ReactNode;
8
+ /**
9
+ * Presence makes the chip removable, adding a dismiss control.
10
+ *
11
+ * This is what Multi-select consumes for its selected values, and the reason
12
+ * Chip exists as a component rather than a prop on Badge.
13
+ */
14
+ onRemove?: () => void;
15
+ /**
16
+ * Accessible name for the dismiss control.
17
+ *
18
+ * Defaults to `Remove {children}` when `children` is a string, because eight
19
+ * chips in a row all named "Remove" is a list a screen reader user cannot
20
+ * navigate. Pass this explicitly whenever the label is not plain text.
21
+ */
22
+ removeLabel?: string;
23
+ /** Presence makes the body of the chip a toggle. */
24
+ onSelectedChange?: (selected: boolean) => void;
25
+ /**
26
+ * Pressed state for a selectable chip.
27
+ *
28
+ * Requires `onSelectedChange`. On its own it does nothing at all — the chip
29
+ * stays a plain span with no `aria-pressed` — because a chip that reports a
30
+ * pressed state and cannot be pressed is worse than one that reports
31
+ * nothing. Chip never styles this itself either; color is the consumer's
32
+ * call, as in the `Selectable` story.
33
+ */
34
+ selected?: boolean;
35
+ disabled?: boolean;
36
+ }
37
+ /**
38
+ * A chip: a label that does something. Removable, selectable, or both.
39
+ *
40
+ * The element it renders depends on what it can do, because that is what
41
+ * decides how it is announced and whether it can be reached:
42
+ *
43
+ * - neither -> `<span>`, and you probably wanted `Pill`
44
+ * - select -> the whole chip is a `<button aria-pressed>`
45
+ * - remove -> a `<span>` wrapping the label and a dismiss `<button>`
46
+ * - both -> a `<span>` wrapping TWO sibling buttons
47
+ *
48
+ * The last case is why the body is not simply made clickable when a dismiss
49
+ * button is present: a button inside a button is invalid HTML. Browsers do not
50
+ * rescue it either — measured, the HTML parser splits the pair into siblings,
51
+ * while the DOM API that React uses leaves it nested, so what actually ships is
52
+ * a focusable control whose every click also fires the button around it. Two
53
+ * siblings in a styled wrapper is the only structure that gives both actions a
54
+ * name, a focus ring and a tab stop.
55
+ */
56
+ declare const Chip: React.ForwardRefExoticComponent<ChipProps & React.RefAttributes<HTMLElement>>;
57
+ export { Chip };
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Chip vs Pill vs Badge.
3
+ *
4
+ * Badge is a compact label attached to something else: a count on an avatar, a
5
+ * status on a table row. Pill is standalone inline chrome carrying a short
6
+ * phrase: social proof, live state, a filter that reads as a sentence fragment.
7
+ * Neither does anything when you click it.
8
+ *
9
+ * Chip is the interactive one. It is removable, selectable, or both, and it is
10
+ * the only one of the three that is a control rather than decoration. That is
11
+ * also why it is a component and not a `interactive` prop on Badge: it renders
12
+ * real buttons with real names and real focus rings, and bolting that onto a
13
+ * `<span>` would either lie to assistive tech or turn Badge into two
14
+ * components wearing one name.
15
+ *
16
+ * The visual language deliberately matches Pill — fully rounded, roomy
17
+ * horizontal padding — because a selected filter and a filter that merely
18
+ * reads as one should not look like different species. The difference the
19
+ * viewer should notice is that this one responds.
20
+ */
21
+ export declare const chipVariants: (props?: ({
22
+ colorScheme?: "primary" | "amber" | "success" | "error" | "neutral" | "info" | null | undefined;
23
+ variant?: "soft" | "solid" | "outline" | null | undefined;
24
+ size?: "sm" | "md" | null | undefined;
25
+ disabled?: boolean | null | undefined;
26
+ } & import('class-variance-authority/types').ClassProp) | undefined) => string;
@@ -0,0 +1,52 @@
1
+ import { VariantProps } from 'class-variance-authority';
2
+ import { inputVariants } from '../Input/input-variants';
3
+ import { SelectOption } from '../Select/Select';
4
+ import * as React from "react";
5
+ export interface ComboboxProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChange">, VariantProps<typeof inputVariants> {
6
+ options: SelectOption[];
7
+ value: string | number | null;
8
+ /**
9
+ * Receives `null` when the field is cleared.
10
+ *
11
+ * Headless UI reports a cleared input as `null`, and swallowing that made
12
+ * the selection unclearable: emptying the input fired nothing and Escape
13
+ * silently restored the old label.
14
+ */
15
+ onChange: (value: string | number | null) => void;
16
+ placeholder?: string;
17
+ disabled?: boolean;
18
+ label?: string;
19
+ helperText?: string;
20
+ errorMessage?: string;
21
+ /** Classes for the text input itself. `className` lands on the wrapper. */
22
+ inputClassName?: string;
23
+ /** Classes for the popup panel. */
24
+ optionsClassName?: string;
25
+ /** Shown in the panel when the query matches nothing. */
26
+ emptyMessage?: string;
27
+ /**
28
+ * Replaces the match rule.
29
+ *
30
+ * The default is a case-insensitive substring on `label`, which is what
31
+ * every hand-rolled version in the portfolio did. Pass this for fuzzy
32
+ * matching, matching on `value`, or filtering server-side by ignoring the
33
+ * query here entirely.
34
+ */
35
+ filter?: (options: SelectOption[], query: string) => SelectOption[];
36
+ }
37
+ /**
38
+ * A `Select` you can type into.
39
+ *
40
+ * Built on Headless UI's `Combobox` rather than on a Roster primitive, for the
41
+ * same reason `Select` is built on its `Listbox`: that IS the primitive, and
42
+ * it already handles the parts worth not rewriting — active-option tracking,
43
+ * the keyboard model, and the input's `aria-activedescendant` wiring.
44
+ *
45
+ * What Roster adds is the presentation, and it is deliberately the SAME
46
+ * presentation `Select` uses: the panel, its surface tokens, the `.dark` carry
47
+ * across the portal, and the option rows all come from shared internals. A
48
+ * combobox that opened a different-looking menu than a select on the same form
49
+ * would be the bug this component is meant to prevent.
50
+ */
51
+ declare const Combobox: ({ options, value, onChange, placeholder, disabled, variant, size, error, label, helperText, errorMessage, className, inputClassName, optionsClassName, emptyMessage, filter, ...props }: ComboboxProps) => import("react/jsx-runtime").JSX.Element;
52
+ export { Combobox };