@blakesteve/roster 5.13.0 → 5.15.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.
Files changed (39) hide show
  1. package/README.md +187 -41
  2. package/dist/components/molecules/Carousel/Carousel.cjs +1 -1
  3. package/dist/components/molecules/Carousel/Carousel.d.ts +65 -3
  4. package/dist/components/molecules/Carousel/Carousel.js +376 -225
  5. package/dist/components/molecules/Carousel/carousel-math.cjs +1 -1
  6. package/dist/components/molecules/Carousel/carousel-math.d.ts +16 -0
  7. package/dist/components/molecules/Carousel/carousel-math.js +63 -44
  8. package/dist/components/molecules/Carousel/carousel-selection.cjs +1 -0
  9. package/dist/components/molecules/Carousel/carousel-selection.d.ts +28 -0
  10. package/dist/components/molecules/Carousel/carousel-selection.js +23 -0
  11. package/dist/components/molecules/LiquidTabs/LiquidCount.d.ts +2 -2
  12. package/dist/components/molecules/LiquidTabs/LiquidTabPanel.cjs +1 -1
  13. package/dist/components/molecules/LiquidTabs/LiquidTabPanel.d.ts +10 -49
  14. package/dist/components/molecules/LiquidTabs/LiquidTabPanel.js +5 -28
  15. package/dist/components/molecules/LiquidTabs/LiquidTabs.cjs +1 -1
  16. package/dist/components/molecules/LiquidTabs/LiquidTabs.d.ts +13 -40
  17. package/dist/components/molecules/LiquidTabs/LiquidTabs.js +5 -95
  18. package/dist/components/molecules/LiquidTabs/liquid-count.d.ts +7 -5
  19. package/dist/components/molecules/LiquidTabs/liquid-tab-id.cjs +1 -1
  20. package/dist/components/molecules/LiquidTabs/liquid-tab-id.d.ts +4 -9
  21. package/dist/components/molecules/LiquidTabs/liquid-tab-id.js +3 -4
  22. package/dist/components/molecules/Tabs/TabPanel.cjs +1 -0
  23. package/dist/components/molecules/Tabs/TabPanel.d.ts +53 -0
  24. package/dist/components/molecules/Tabs/TabPanel.js +32 -0
  25. package/dist/components/molecules/Tabs/Tabs.cjs +1 -0
  26. package/dist/components/molecules/Tabs/Tabs.d.ts +118 -0
  27. package/dist/components/molecules/Tabs/Tabs.js +145 -0
  28. package/dist/components/molecules/Tabs/tab-id.cjs +1 -0
  29. package/dist/components/molecules/Tabs/tab-id.d.ts +13 -0
  30. package/dist/components/molecules/Tabs/tab-id.js +6 -0
  31. package/dist/index.d.ts +3 -0
  32. package/dist/internal/dev.cjs +1 -0
  33. package/dist/internal/dev.d.ts +10 -0
  34. package/dist/internal/dev.js +10 -0
  35. package/dist/meta.json +3 -3
  36. package/dist/roster.cjs +1 -1
  37. package/dist/roster.css +1 -1
  38. package/dist/roster.js +116 -110
  39. package/package.json +1 -1
package/README.md CHANGED
@@ -268,9 +268,9 @@ changes nothing.
268
268
  | token | default | reshapes |
269
269
  | ----- | ------- | -------- |
270
270
  | `--roster-radius-sm` | `0.25rem` | `Button` `xs`, `Checkbox` `sm` and `md`, `InlineCode`, `Link`, `MultiSelect`'s chip dismiss, the dismiss and reveal buttons in `Alert`, `Toast` and `PasswordInput` |
271
- | `--roster-radius-md` | `0.375rem` | `Button`, `Input`, `Textarea`, `Select`, `MultiSelect`, `Badge`, `Avatar` and its popover, `Checkbox` `lg`, `CheckboxGroup`'s panel, `Disclosure`, `Dialog`'s and `Sheet`'s close buttons, `Navbar`'s links and menu, popup panels, `Table` |
272
- | `--roster-radius-lg` | `0.5rem` | `Alert`, `Toast`, `Tooltip`, `CallToAction`, `EmptyState`, `ErrorState`, `LiquidTabs`, `MatchupCard`, `Navbar`'s mobile panel, `Skeleton` blocks (unless `--roster-skeleton-radius` is set) |
273
- | `--roster-radius-xl` | `0.75rem` | `Card`, `LiquidTabs`' `pill` strip |
271
+ | `--roster-radius-md` | `0.375rem` | `Button`, `Input`, `Textarea`, `Select`, `MultiSelect`, `Badge`, `Avatar` and its popover, `Checkbox` `lg`, `CheckboxGroup`'s panel, `Disclosure`, `Dialog`'s and `Sheet`'s close buttons, `Navbar`'s links and menu, popup panels, `Table`, a plain `Tabs`' focus ring |
272
+ | `--roster-radius-lg` | `0.5rem` | `Alert`, `Toast`, `Tooltip`, `CallToAction`, `EmptyState`, `ErrorState`, `Tabs`' liquid variant, `MatchupCard`, `Navbar`'s mobile panel, `Skeleton` blocks (unless `--roster-skeleton-radius` is set) |
273
+ | `--roster-radius-xl` | `0.75rem` | `Card`, the liquid `Tabs`' `pill` strip |
274
274
  | `--roster-radius-2xl` | `1rem` | `Dialog`, the top of `Sheet` |
275
275
  | `--roster-radius-3xl` | `1.5rem` | nothing yet; wired for the first component that uses it |
276
276
  | `--roster-border-width` | `1px` | every 1px edge: borders, `divide-y`, the ring `Select`'s trigger and the anchored panels draw their edge with, and the rules in `Accordion` and `LabeledDivider` |
@@ -477,11 +477,12 @@ Replace the rule with the token:
477
477
  :root { --roster-primary-600-ink: #10142e; }
478
478
  ```
479
479
 
480
- `LiquidTabs` followed later: its active label reads `--roster-lt-text-active`,
481
- which defaults to `--roster-primary-500-ink`, the ink of the pill's default
482
- fill. A rule naming `rst:text-white` on a tab stops matching; a rule on
483
- `[role="tab"][aria-selected="true"]` still does. If you repaint the pill with
484
- `--roster-lt-pill`, set `--roster-lt-text-active` to match it.
480
+ `LiquidTabs` followed later (now `Tabs`' liquid variant): its active label
481
+ reads `--roster-lt-text-active`, which defaults to `--roster-primary-500-ink`,
482
+ the ink of the pill's default fill. A rule naming `rst:text-white` on a tab
483
+ stops matching; a rule on `[role="tab"][aria-selected="true"]` still does. If
484
+ you repaint the pill with `--roster-lt-pill`, set `--roster-lt-text-active` to
485
+ match it.
485
486
 
486
487
  **If you pass a foreground through `className`, add the hover modifier.** Solid
487
488
  variants now carry `hover:text-*`, and `tailwind-merge` resolves conflicts per
@@ -565,7 +566,7 @@ a click. A small tracker marks those elements: keyboard mode starts on a navigat
565
566
  arrows, Escape, Enter, Space, Home, End, PageUp, PageDown) and ends on pointer
566
567
  input, and it marks only what Chromium's own `:focus-visible` already matches,
567
568
  so Chromium's rings are unchanged. It runs while a component that moves focus
568
- is mounted (Menu, the select family, Dialog, Sheet, RadioGroup, LiquidTabs,
569
+ is mounted (Menu, the select family, Dialog, Sheet, RadioGroup, Tabs,
569
570
  Slider and the rest), on one set of document listeners.
570
571
 
571
572
  Your own components' rings come from your Tailwind, not Roster's, so they
@@ -1382,7 +1383,7 @@ What it promises:
1382
1383
  sideways at 320px. Nothing animates as an item leaves.
1383
1384
 
1384
1385
  Counts for a tab need no API: the app holds the items, so it's
1385
- `items.length`, passed as a `LiquidTabs` tab's `count`. `MediaPreview` is the
1386
+ `items.length`, passed as a `Tabs` tab's `count`. `MediaPreview` is the
1386
1387
  thumbnail to use in an item: when the image can't render (an expired link, a
1387
1388
  format the browser can't draw), it falls back to a link to the original in the
1388
1389
  same footprint.
@@ -1861,8 +1862,8 @@ Things to know:
1861
1862
 
1862
1863
  ### Carousel
1863
1864
 
1864
- A sideways row: a row of cards, a selection strip, or a gallery of one per
1865
- view. It is a real scroll container with CSS scroll snap, so touch, trackpad,
1865
+ A sideways row: a row of cards, a selection strip (`selectable`, below), or a
1866
+ gallery of one per view. It is a real scroll container with CSS scroll snap, so touch, trackpad,
1866
1867
  Shift with the wheel and keyboard focus all scroll it without script, and every
1867
1868
  item is in the server render. No clones, no virtualization, no loop, and
1868
1869
  nothing moves on its own.
@@ -1895,22 +1896,142 @@ Script adds what a mouse is missing:
1895
1896
 
1896
1897
  A vertical wheel over the row scrolls the page, and sideways overscroll is
1897
1898
  contained. Under reduced motion every scroll the component makes is instant.
1898
- `ref` gives `scrollToIndex(i)`, `scrollPrev()` and `scrollNext()`;
1899
+ `ref` gives `scrollToIndex(i)`, `scrollPrev()`, `scrollNext()` and `focus()`;
1899
1900
  `onIndexChange` reports the first item wholly in view. For a gallery,
1900
1901
  `showPosition` shows "2 of 5", which is also each item's name and is announced
1901
1902
  politely when it changes.
1902
1903
 
1903
- ### LiquidTabs
1904
+ #### Selection mode
1904
1905
 
1905
- A tab strip with a sliding pill, on the WAI tabs pattern: one tab stop, arrows
1906
- that wrap, Home and End, and `activation="manual"` for tabs that are costly to
1907
- switch. `LiquidNav` is the same strip as a `nav` of links.
1906
+ For a strip of choices, one of which is picked, pass `selectable`. The row
1907
+ becomes a horizontal listbox: one tab stop, the keys moving focus between
1908
+ options, and one option selected. A row of buttons with `aria-pressed` is a tab
1909
+ stop per button (300 options, 300 Tab presses), and a screen reader hears a
1910
+ list of unrelated buttons. Without `selectable` nothing changes.
1908
1911
 
1909
- **Counts.** A tab or a nav link takes `count`, shown in a small mark after its
1910
- label:
1912
+ ```tsx
1913
+ <Carousel
1914
+ aria-label="Choose a record"
1915
+ aria-describedby="how"
1916
+ selectable
1917
+ selectedKey={chosen}
1918
+ onSelectionChange={setChosen}
1919
+ onActivate={play}
1920
+ clearable
1921
+ itemWidth={150}
1922
+ >
1923
+ {records.map((r) => <RecordCell key={r.id} record={r} selected={r.id === chosen} />)}
1924
+ </Carousel>
1925
+ ```
1926
+
1927
+ - **Tab** lands on the selected option, or the first when nothing is. Leaving
1928
+ the row hands the tab stop back to the selected option.
1929
+ - **Left and Right** move by one, without wrapping; **Home and End** go to the
1930
+ ends; **Page Up and Page Down** move by as many options as are wholly in
1931
+ view.
1932
+ - **Type-ahead:** typing jumps to the next option whose name starts with what
1933
+ was typed. A pause of 700ms starts over, and the same letter again steps on
1934
+ to the next option starting with it.
1935
+ - **Enter or Space selects. Selection doesn't follow focus:** arrowing only
1936
+ moves focus, so whatever a selection sets off (a pan, a fetch, an
1937
+ announcement) happens once, on purpose.
1938
+ - **Enter, a click or a tap on the selected option** calls `onActivate`, the
1939
+ step after choosing: open it, read it, play it. A click or tap on any other
1940
+ option selects it.
1941
+ - **Escape** clears the selection when `clearable` is set, and calls
1942
+ `onSelectionChange(null)`.
1943
+
1944
+ Moving focus with the keys scrolls the option into view, smoothly, or at once
1945
+ under reduced motion; a click doesn't move the row. A selection doesn't scroll
1946
+ the row at all, so a `scrollToIndex` of your own (centering the choice, say)
1947
+ isn't undone. The keyboard-mode tracker is mounted, so the ring shows in Safari
1948
+ after a click too (see [Focus](#focus)).
1949
+
1950
+ **Identity is the child's `key`.** `selectedKey`, `defaultSelectedKey` and both
1951
+ callbacks deal in it, as React stores it: a string. Keys, not positions,
1952
+ because a consumer that filters the row changes every position after the first
1953
+ option it hides. A development build warns about a child without a key.
1954
+ Controlled (`selectedKey` with `onSelectionChange`) or uncontrolled
1955
+ (`defaultSelectedKey`); where focus is stays internal, and `focus()` on the
1956
+ `ref` moves focus onto the tab stop, from a find field's Down Arrow, say.
1957
+
1958
+ **Names.** An option is named by its content, or by `getOptionLabel(key,
1959
+ index)`, which is also the text type-ahead matches. Name the listbox with
1960
+ `aria-label` or `aria-labelledby`, and put a sentence saying how it works in
1961
+ `aria-describedby`.
1962
+
1963
+ **Nothing interactive inside an option.** An option is one choice, reached with
1964
+ the arrow keys; a link or a button inside it is a tab stop the keys skip, and
1965
+ a screen reader can't reach it as anything but text. Put actions outside the
1966
+ row, or behind `onActivate`. A development build warns when an option holds
1967
+ something that takes focus.
1968
+
1969
+ **Filtering.** Render fewer children and `aria-setsize` and `aria-posinset`
1970
+ describe what's shown. The tab stop stays on its option if that's still shown;
1971
+ otherwise it moves to the nearest option still shown, in the order before the
1972
+ change, after it if there is one, else before it. A selection made elsewhere
1973
+ moves the tab stop to the selected option while focus is outside the row, and
1974
+ leaves it where the keys put it while focus is inside.
1975
+
1976
+ **The selected look is yours.** Roster draws the focus ring, an outline 2px out,
1977
+ so a selected look drawn with a ring or a border sits inside it. Draw the
1978
+ selection in the child, from `selectedKey`, or on the option with
1979
+ `itemClassName` and the `aria-selected:` variant.
1980
+
1981
+ **Scale.** Every option is in the DOM, for the keys and for screen readers;
1982
+ draw expensive content lazily inside it. Options are memoized and the keys,
1983
+ clicks and focus are one listener each on the row. Measured at 1,000 options
1984
+ in Chromium: mounting takes about 30ms longer than the default mode, a key
1985
+ moves focus within a millisecond, and the frame after it lands in about 7ms.
1986
+
1987
+ ### Tabs
1988
+
1989
+ A controlled tab strip, on the WAI tabs pattern in every variant: one tab
1990
+ stop, arrows that wrap, Home and End, and `activation="manual"` for tabs that
1991
+ are costly to switch. Every tab is `type="button"`, so a strip inside a form
1992
+ never submits it.
1993
+
1994
+ ```tsx
1995
+ import { Tabs } from "@blakesteve/roster";
1996
+
1997
+ <Tabs aria-label="Project" tabs={[{ id: "overview", label: "Overview" }, …]} activeTab={tab} onChange={setTab} />
1998
+ ```
1999
+
2000
+ **Plain, by default.** The selected tab is underlined, 2px, on a 1px rule
2001
+ that runs the strip's width. The other tabs are muted and darken under the
2002
+ pointer, with a lighter underline as a hint. The weight never changes, so
2003
+ selecting a tab never moves the row. Keyboard focus is Roster's ring. The
2004
+ underline is a border, so it stays in forced colors, and only the selected
2005
+ tab draws one there. Its colors are tokens, each set in light and dark:
2006
+
2007
+ | token | light | dark | paints |
2008
+ | ----- | ----- | ---- | ------ |
2009
+ | `--roster-tabs-indicator` | `primary-500` | `primary-400` | the selected tab's underline |
2010
+ | `--roster-tabs-indicator-hover` | `gray-300` | `gray-600` | the underline under the pointer |
2011
+ | `--roster-tabs-border` | `gray-200` | `gray-800` | the rule along the strip |
2012
+ | `--roster-tabs-text` | `gray-500` | `gray-400` | a tab at rest |
2013
+ | `--roster-tabs-text-hover` | `gray-900` | `gray-100` | a tab under the pointer |
2014
+ | `--roster-tabs-text-active` | `gray-900` | `gray-100` | the selected tab |
2015
+
2016
+ On Roster's own surfaces the underline clears 3:1 against the page (6.37:1 in
2017
+ light, 7.31:1 in dark), a label at rest 4.5:1 (5.99:1 and 7.83:1), and a
2018
+ count's number 4.5:1 against its mark (5.07:1 at the least). `fullWidth` gives
2019
+ every tab an equal share of the row, and `size="lg"` makes each at least 44px
2020
+ tall.
2021
+
2022
+ **Liquid.** `variant="liquid"` draws the sliding pill instead:
2023
+ `liquidStyle="pill"` (the default), a floating pill in a padded strip, or
2024
+ `"filled"`, where the selected tab fills its cell. The pill follows
2025
+ `activeTab`, never the click, and moves in two phases, a stretch and a
2026
+ settle, or not at all under reduced motion. Its colors are the
2027
+ `--roster-lt-*` tokens. `liquidStyle` is a type error without
2028
+ `variant="liquid"`.
2029
+
2030
+ **Counts.** A tab takes `count`, in every variant, and so does a `LiquidNav` link, shown
2031
+ in a small mark after its label:
1911
2032
 
1912
2033
  ```tsx
1913
- <LiquidTabs tabs={[{ id: "queue", label: "Queue", count: queue.length }, …]} … />
2034
+ <Tabs tabs={[{ id: "queue", label: "Queue", count: queue.length }, …]} … />
1914
2035
  ```
1915
2036
 
1916
2037
  The count is part of the name, "Queue, 12 items" rather than "Queue12" or
@@ -1931,29 +2052,29 @@ labels={{ count: (n) => (n === 1 ? "1 game" : `${n.toLocaleString()} games`) }}
1931
2052
  shorten the mark, pass `labels.countMark` (`(n) => "12K"`); the name stays
1932
2053
  whole, since a screen reader has no width to save.
1933
2054
  - **The mark is drawn from the tab's own text color:** its number in that
1934
- color on a 12% tint of it. So it follows the tab through every state, both
1935
- variants and both themes, and through any palette set on the
1936
- `--roster-lt-*` tokens, and its number clears 4.5:1 in every state on
1937
- Roster's own (4.88:1 at the least). `Badge` and `Pill` each draw from a fixed
2055
+ color on a 12% tint of it. So it follows the tab through every state, every
2056
+ variant and both themes, and through any palette set on the tabs' tokens,
2057
+ and its number clears 4.5:1 in every state on Roster's own (4.88:1 at the
2058
+ least, on the liquid pill). `Badge` and `Pill` each draw from a fixed
1938
2059
  palette, which on the selected tab would be a second chip sitting on the
1939
2060
  pill.
1940
- - **The pill follows,** re-measuring when a count widens its tab or one
1941
- before it.
2061
+ - **The liquid pill follows,** re-measuring when a count widens its tab or
2062
+ one before it.
1942
2063
 
1943
- **The panel.** `LiquidTabPanel` is the `role="tabpanel"` a tab controls, paired
1944
- with it by construction: its `id` is the tab's `panelId`, and its
1945
- `aria-labelledby` points at the tab through `liquidTabId`, so the panel takes
1946
- the tab's name, count included. Give the `LiquidTabs` an `id` to pair it with.
1947
- One panel that swaps its content, or a panel per tab:
2064
+ **The panel.** `TabPanel` is the `role="tabpanel"` a tab controls, in every
2065
+ variant, paired with it by construction: its `id` is the tab's `panelId`, and
2066
+ its `aria-labelledby` points at the tab through `tabId`, so the panel takes the
2067
+ tab's name, count included. Give the `Tabs` an `id` to pair it with. One panel
2068
+ that swaps its content, or a panel per tab:
1948
2069
 
1949
2070
  ```tsx
1950
2071
  // One panel: every tab carries the same panelId, and `tab` is the selected one.
1951
- <LiquidTabs id="lists" tabs={tabs.map((t) => ({ ...t, panelId: "lists-panel" }))} activeTab={list} onChange={setList} />
1952
- <LiquidTabPanel tabsId="lists" id="lists-panel" tab={list}>…</LiquidTabPanel>
2072
+ <Tabs id="lists" tabs={tabs.map((t) => ({ ...t, panelId: "lists-panel" }))} activeTab={list} onChange={setList} />
2073
+ <TabPanel tabsId="lists" id="lists-panel" tab={list}>…</TabPanel>
1953
2074
 
1954
2075
  // A panel per tab: each its own panelId, and every panel gets activeTab.
1955
- <LiquidTabPanel tabsId="views" id="views-board" tab="board" activeTab={view}>…</LiquidTabPanel>
1956
- <LiquidTabPanel tabsId="views" id="views-list" tab="list" activeTab={view} keepMounted>…</LiquidTabPanel>
2076
+ <TabPanel tabsId="views" id="views-board" tab="board" activeTab={view}>…</TabPanel>
2077
+ <TabPanel tabsId="views" id="views-list" tab="list" activeTab={view} keepMounted>…</TabPanel>
1957
2078
  ```
1958
2079
 
1959
2080
  Per tab, a panel that isn't selected stays in the page, hidden, so every tab's
@@ -1965,6 +2086,22 @@ can reach what they just selected; it leaves the tab order once its content
1965
2086
  holds a link, a button or a field, rechecked as the content changes. Pass
1966
2087
  `tabIndex` to decide it yourself.
1967
2088
 
2089
+ **From `LiquidTabs`.** `LiquidTabs` is deprecated, and keeps working,
2090
+ unchanged: it renders `Tabs` with the liquid variant, the same markup, styles
2091
+ and test ids it always has. To move, rename it and add the variant; a
2092
+ `variant="filled"` moves to `liquidStyle`:
2093
+
2094
+ ```tsx
2095
+ <LiquidTabs … /> → <Tabs variant="liquid" … />
2096
+ <LiquidTabs variant="filled" … /> → <Tabs variant="liquid" liquidStyle="filled" … />
2097
+ <LiquidTabPanel … /> → <TabPanel … />
2098
+ liquidTabId(id, tab) → tabId(id, tab)
2099
+ ```
2100
+
2101
+ Nothing else changes: the props, ids, keys, pill and `data-testid` hooks are
2102
+ the same. `LiquidTabsLabels` is now `TabsLabels`, and the old name still works.
2103
+ `LiquidNav`, the liquid strip as a `nav` of links, is unchanged.
2104
+
1968
2105
  ### Components that render links
1969
2106
 
1970
2107
  `Breadcrumbs` renders a plain `<a>` by default, which is right for a static
@@ -2015,8 +2152,8 @@ Dialog or Sheet), alongside `Pagination`, the loading system and DataTable's
2015
2152
  instead, and 5.8.0 adds the ones that were missing: MultiSelect's
2016
2153
  `countLabel`, DataTable's `emptyMessage` and MediaPreview's `newTabLabel`, next
2017
2154
  to the existing `dismissLabel`, `placeholder`, `removeLabel`, `prevLabel` and
2018
- the like. `LiquidTabs` and `LiquidNav` take `labels` for the words a count
2019
- says. Words that take a number or a name are functions, so a language
2155
+ the like. `Tabs`, `LiquidTabs` and `LiquidNav` take `labels` for the words a
2156
+ count says. Words that take a number or a name are functions, so a language
2020
2157
  whose word changes with the count can say so. An entry passed as `undefined`
2021
2158
  (a catalog missing a key) keeps its English rather than leaving a control
2022
2159
  unnamed.
@@ -2123,13 +2260,13 @@ function App() {
2123
2260
  | `Breadcrumbs` | Navigation trail; pass `linkComponent` to keep navigation client-side |
2124
2261
  | `CheckboxGroup` | Checkbox set reporting one `string[]`; groups, columns, scroll, error |
2125
2262
  | `CallToAction` | Prominent hero-style CTA block |
2126
- | `Carousel` | Sideways row of items on native scroll snap; mouse drag, paging arrows, no autoplay |
2263
+ | `Carousel` | Sideways row of items on native scroll snap; mouse drag, paging arrows, no autoplay; a selection mode for a strip of choices (see [Selection mode](#selection-mode)) |
2127
2264
  | `DescriptionList` | Label and value pairs as a real `<dl>`: inline, stacked, or split |
2128
2265
  | `EmptyState` | Zero-data placeholder with icon and action slot; `headingLevel` sets its title's `h1` to `h6` |
2129
2266
  | `ErrorState` | Error display with retry action |
2130
- | `LiquidNav` | `LiquidTabs`' strip as a `nav` of links; takes a router's link component |
2131
- | `LiquidTabs` | Tab strip with a sliding pill; arrow keys, roving focus, counts, 44px `lg` size (see [LiquidTabs](#liquidtabs)) |
2132
- | `LiquidTabPanel` | The panel a `LiquidTabs` tab controls, named for its tab by construction; one swapping panel or one per tab |
2267
+ | `LiquidNav` | The liquid tab strip as a `nav` of links; takes a router's link component |
2268
+ | `LiquidTabs` | Deprecated: `Tabs` with `variant="liquid"`, which it renders (see [Tabs](#tabs)) |
2269
+ | `LiquidTabPanel` | Deprecated: the old name of `TabPanel` |
2133
2270
  | `MatchupCard` | Head-to-head comparison card |
2134
2271
  | `MediaPreview` | Thumbnail that falls back to "Preview unavailable, open original" when it can't render |
2135
2272
  | `Menu` | A button's menu of actions: icons, shortcuts, checkbox and radio items, links, groups, submenus; the full keyboard pattern, 44px targets (see [Menu](#menu)) |
@@ -2142,6 +2279,8 @@ function App() {
2142
2279
  | `RadioGroup` | Radio set reporting one `string`; descriptions, error, 44px targets |
2143
2280
  | `Slider` | A value along a track, starting in the middle or not set on request; named snap-point chips, a visible number, the full keyboard, 44px targets (see [Slider](#slider)) |
2144
2281
  | `RangeSlider` | A low and a high on one track: two thumbs that meet and never cross, each its own slider to the keyboard (see [RangeSlider](#rangeslider)) |
2282
+ | `Tabs` | Tab strip, plain (an underline) or liquid (a sliding pill); arrow keys, one tab stop, counts, 44px `lg` size (see [Tabs](#tabs)) |
2283
+ | `TabPanel` | The panel a `Tabs` tab controls, named for its tab by construction; one swapping panel or one per tab |
2145
2284
  | `Toast` | Transient floating notice; `Toaster` + the imperative `toast` handle |
2146
2285
 
2147
2286
  ### Organisms
@@ -2307,6 +2446,13 @@ npm run check:prefix
2307
2446
  npm run check:classes-emit
2308
2447
  ```
2309
2448
 
2449
+ In CI the Storybook projects run inside Playwright's own image for the locked
2450
+ version (`mcr.microsoft.com/playwright:v<version>-noble` in `ci.yml`), so no
2451
+ browser or system library is installed from Ubuntu's mirrors. Bumping
2452
+ `playwright` means bumping that tag too: `src/ci-workflow.test.ts` fails until
2453
+ they match. Every job and test step has a time limit, so a stall fails with
2454
+ GitHub's "has timed out" message instead of running for hours.
2455
+
2310
2456
  Three guards are worth knowing about, because the bugs they catch cannot be
2311
2457
  reproduced from inside this repo.
2312
2458
 
@@ -1 +1 @@
1
- "use client";"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const c=require("react/jsx-runtime"),Be=require("react"),$e=require("../../atoms/Button/Button.cjs"),T=require("../../../lib/utils.cjs"),ze=require("../../../lib/labels.cjs"),He=require("../../../internal/motion.cjs"),d=require("./carousel-math.cjs"),U=require("./carousel-layout.cjs"),Ue=require("../../../internal/key-focus.cjs");function Ve(o){const a=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(o){for(const f in o)if(f!=="default"){const x=Object.getOwnPropertyDescriptor(o,f);Object.defineProperty(a,f,x.get?x:{enumerable:!0,get:()=>o[f]})}}return a.default=o,Object.freeze(a)}const n=Ve(Be),We={position:(o,a)=>`${o} of ${a}`},Xe=o=>Math.min(3e3,500+o*.35),Ge='a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"]), [contenteditable]:not([contenteditable="false"])',V=o=>typeof o=="number"?`${o}px`:o;function se(o){const a=parseFloat(o);return Number.isFinite(a)?a:0}const Je="rst:hidden rst:[@media(any-hover:hover)]:inline-flex",Ke="rst:hidden rst:[@media(any-hover:hover)]:flex";function oe({direction:o}){return c.jsx("svg",{"aria-hidden":"true",focusable:"false",viewBox:"0 0 20 20",className:"rst:h-5 rst:w-5",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:c.jsx("path",{d:o==="prev"?"M12.5 4.5 7 10l5.5 5.5":"M7.5 4.5 13 10l-5.5 5.5"})})}const le={overflowing:null,canScrollPrev:!1,canScrollNext:!1,index:0},Qe=n.forwardRef(function({"aria-label":a,"aria-labelledby":f,children:x,itemWidth:I,perView:C,gap:W=12,gutter:X=16,bleed:ce=!1,snap:R="start",snapStrictness:ie="mandatory",oneAtATime:ae=!1,arrows:j="hover",arrowPlacement:M="controls",renderArrows:L,prevLabel:ue,nextLabel:de,prevIcon:fe,nextIcon:ge,arrowButtonProps:ve,fade:pe=!1,scrollbar:me=!1,showPosition:G=!1,labels:be,onIndexChange:J,onScrollStateChange:K,itemClassName:xe,listClassName:he,className:ye,style:Se,...we},Ne){Ue.useKeyFocus();const Q=ze.withLabels(We,be),p=n.useRef(null),Y=`${n.useId()}-list`,Z=n.Children.toArray(x),E=Z.length,[l,Te]=n.useState(le),ee=n.useRef(le),[Ie,Le]=n.useState(""),[Ee,Pe]=n.useState(!1),O=n.useRef({onIndexChange:J,onScrollStateChange:K});n.useEffect(()=>{O.current={onIndexChange:J,onScrollStateChange:K}});const g=n.useCallback(()=>{const e=p.current;if(!e)return null;const t=e.getBoundingClientRect(),r=getComputedStyle(e),s=t.left+e.clientLeft-e.scrollLeft;return{items:Array.from(e.children).map(u=>{const b=u.getBoundingClientRect();return{start:b.left-s,end:b.right-s}}),scrollLeft:e.scrollLeft,clientWidth:e.clientWidth,scrollWidth:e.scrollWidth,paddingStart:se(r.scrollPaddingInlineStart||r.scrollPaddingLeft),paddingEnd:se(r.scrollPaddingInlineEnd||r.scrollPaddingRight),align:R}},[R]),h=n.useCallback(()=>{const e=g();if(!e)return;const t={overflowing:d.overflows(e),canScrollPrev:d.canScrollPrev(e),canScrollNext:d.canScrollNext(e),index:e.items.length?d.currentIndex(e):0},r=ee.current;r.overflowing===t.overflowing&&r.canScrollPrev===t.canScrollPrev&&r.canScrollNext===t.canScrollNext&&r.index===t.index||(ee.current=t,Te(t),r.index!==t.index&&O.current.onIndexChange?.(t.index),(r.canScrollPrev!==t.canScrollPrev||r.canScrollNext!==t.canScrollNext)&&O.current.onScrollStateChange?.({canScrollPrev:t.canScrollPrev,canScrollNext:t.canScrollNext}))},[g]),ke=JSON.stringify([W,X,I,C]);n.useLayoutEffect(()=>{const e=p.current;if(!e)return;h();let t=0;const r=()=>{t||(t=requestAnimationFrame(()=>{t=0,h()}))};e.addEventListener("scroll",r,{passive:!0});const s=typeof ResizeObserver>"u"?null:new ResizeObserver(()=>h()),i=()=>{s?.observe(e),Array.from(e.children).forEach(b=>s?.observe(b))};i();const u=typeof MutationObserver>"u"?null:new MutationObserver(()=>{i(),h()});return u?.observe(e,{childList:!0}),()=>{e.removeEventListener("scroll",r),s?.disconnect(),u?.disconnect(),t&&cancelAnimationFrame(t)}},[h,E,ke]),n.useLayoutEffect(()=>{const e=p.current;if(e&&(Pe(!e.querySelector(Ge)),f)){const t=f.split(/\s+/).map(r=>document.getElementById(r)?.textContent?.trim()??"").filter(Boolean).join(" ");Le(t)}},[x,f]);const y=n.useRef(null),v=n.useCallback(()=>{const e=p.current,t=y.current;t&&(y.current=null,window.clearTimeout(t.timer),e?.removeEventListener("scrollend",t.done),e&&(e.style.scrollSnapType="",delete e.dataset.settling))},[]),m=n.useCallback((e,t)=>{const r=p.current;if(!r)return;if(v(),t!=="smooth"||He.prefersReducedMotion()||Math.abs(r.scrollLeft-e)<1){r.style.scrollSnapType="none",r.scrollTo({left:e,behavior:"instant"}),r.style.scrollSnapType="";return}r.style.scrollSnapType="none",r.dataset.settling="";const i=r.scrollLeft,u=()=>{Math.abs(r.scrollLeft-i)<.5||v()};y.current={timer:window.setTimeout(v,Xe(Math.abs(r.scrollLeft-e))),done:u,target:e},r.addEventListener("scrollend",u),r.scrollTo({left:e,behavior:"smooth"})},[v]),P=n.useCallback(e=>{const t=g();if(!t)return;const r=y.current?{...t,scrollLeft:y.current.target}:t;m(d.pageTarget(r,e),"smooth")},[g,m]);n.useImperativeHandle(Ne,()=>({scrollToIndex:(e,t)=>{const r=g();if(!r||!r.items.length)return;const s=Math.min(Math.max(0,Math.round(e)),r.items.length-1);m(d.snapPosition(r,s),t?.behavior??"smooth")},scrollPrev:()=>P(-1),scrollNext:()=>P(1)}),[g,P,m]),n.useEffect(()=>v,[v]);const D=n.useRef(null),F=n.useRef(null),_=n.useRef(null);n.useLayoutEffect(()=>{const e=_.current;if(!e)return;const t=e==="prev"?D.current:F.current,r=e==="prev"?F.current:D.current,s=e==="prev"?!l.canScrollPrev:!l.canScrollNext,i=document.activeElement;s&&(i!==t&&i!==document.body&&i!==null||r&&!r.disabled&&r.focus())},[l.canScrollPrev,l.canScrollNext]);const S=n.useRef(null),w=n.useRef(!1),Ce=e=>{e.pointerType!=="mouse"||e.button!==0||l.overflowing&&(v(),w.current=!1,S.current={pointerId:e.pointerId,x0:e.clientX,scroll0:e.currentTarget.scrollLeft,dragging:!1})},Re=e=>{const t=S.current;if(!t||e.pointerId!==t.pointerId)return;const r=e.currentTarget,s=e.clientX-t.x0;if(!t.dragging){if(Math.abs(s)<=d.CAROUSEL_DRAG_THRESHOLD)return;t.dragging=!0,r.dataset.dragging="",r.style.scrollSnapType="none",window.getSelection?.()?.removeAllRanges();try{r.setPointerCapture(e.pointerId)}catch{}}r.scrollLeft=t.scroll0-s},A=e=>{const t=S.current;if(!t||e.pointerId!==t.pointerId||(S.current=null,!t.dragging))return;const r=e.currentTarget;delete r.dataset.dragging,w.current=e.type==="pointerup",window.setTimeout(()=>{w.current=!1},0);const s=g();s?m(d.snapPosition(s,d.nearestIndex(s)),"smooth"):r.style.scrollSnapType=""},je=e=>{!w.current||e.detail===0||(w.current=!1,e.preventDefault(),e.stopPropagation())},Me=e=>{l.overflowing&&e.preventDefault()},Oe=e=>{const t=e.currentTarget,r=e.target;if(r===t||S.current?.dragging)return;let s=!1;try{s=r.matches(":focus-visible, [data-key-focus]")}catch{s=!1}if(!s)return;const i=r.closest("[data-carousel-item]");if(!i||i.parentElement!==t)return;const u=g();if(!u)return;const b=Array.prototype.indexOf.call(t.children,i);m(d.snapPosition(u,b),"instant")},[De,Fe]=n.useState(0);n.useEffect(()=>{const e=window.setTimeout(()=>Fe(l.index),150);return()=>window.clearTimeout(e)},[l.index]);const te=a??Ie,re=e=>te?`${e} ${te}`:e,q=j!=="none"&&l.overflowing!==!1,_e=j==="hover"?Je:"rst:inline-flex",ne=e=>{const t=e==="prev",{className:r,...s}=ve??{};return c.jsx($e.Button,{type:"button",variant:"outline",colorScheme:"neutral",size:"icon",...s,"aria-label":t?ue??re("Previous"):de??re("Next"),"aria-controls":Y,disabled:t?!l.canScrollPrev:!l.canScrollNext,onClick:()=>P(t?-1:1),ref:t?D:F,onFocus:()=>{_.current=e},onBlur:i=>{i.relatedTarget&&(_.current=null)},"data-carousel-arrow":e,className:T.cn("rst:size-11 rst:shrink-0 rst:rounded-full",M==="overlay"&&!L&&"rst:bg-[var(--roster-popover-bg)] rst:elevation-raised rst:hover:bg-[var(--roster-popover-bg)] rst:disabled:invisible",_e,r),children:t?fe??c.jsx(oe,{direction:"prev"}):ge??c.jsx(oe,{direction:"next"})})},k=q?ne("prev"):null,B=q?ne("next"):null,N=G&&E?c.jsx("p",{"aria-live":"polite",className:"rst:font-ui rst:m-0 rst:text-sm rst:tabular-nums rst:opacity-75","data-carousel-position":"",children:Q.position(De+1,E)}):null;let $=null;L?$=L({prev:k,next:B,position:N}):M==="controls"&&(k||N)&&($=c.jsxs("div",{className:T.cn("rst:mb-2 rst:items-center rst:justify-end rst:gap-2",!N&&j==="hover"?Ke:"rst:flex"),"data-carousel-controls":"",children:[N&&c.jsx("div",{className:"rst:mr-auto",children:N}),k,B]}));const z=pe&&l.overflowing&&(l.canScrollPrev||l.canScrollNext)?`linear-gradient(to right, ${l.canScrollPrev?"transparent, black 48px":"black"}, ${l.canScrollNext?"black calc(100% - 48px), transparent":"black"})`:void 0,H=I===void 0&&C!==void 0,Ae={"--rst-cv-gap":V(W),"--rst-cv-gutter":V(X),...H?U.perViewVars(C):{},...z?{maskImage:z,WebkitMaskImage:z}:{}},qe=I!==void 0?{width:V(I)}:H?{width:U.FLUID_ITEM_WIDTH}:void 0;return c.jsxs("div",{className:T.cn("rst:min-w-0",ye),style:Se,...we,children:[$,c.jsxs("div",{className:"rst:relative",children:[c.jsx("ul",{ref:p,id:Y,role:"list","aria-label":a,"aria-labelledby":a?void 0:f,tabIndex:Ee&&l.overflowing?0:void 0,"data-overflowing":l.overflowing?"":void 0,onPointerDown:Ce,onPointerMove:Re,onPointerUp:A,onPointerCancel:A,onLostPointerCapture:A,onClickCapture:je,onDragStart:Me,onFocus:Oe,style:Ae,className:T.cn("rst:m-0 rst:flex rst:list-none rst:overflow-x-auto rst:overflow-y-hidden","rst:gap-(--rst-cv-gap) rst:px-(--rst-cv-gutter) rst:py-1 rst:scroll-px-(--rst-cv-gutter)","rst:overscroll-x-contain",ie==="mandatory"?"rst:snap-x rst:snap-mandatory":"rst:snap-x rst:snap-proximity",ce&&"rst:-mx-(--rst-cv-gutter)",!me&&"rst:[scrollbar-width:none] rst:[&::-webkit-scrollbar]:hidden",H&&U.FLUID_CLASSES,"rst:data-overflowing:cursor-grab rst:data-dragging:cursor-grabbing rst:data-dragging:select-none","rst:focus-visible:outline-hidden rst:focus-visible:ring-2 rst:focus-visible:ring-ring rst:focus-visible:ring-inset",he),children:Z.map((e,t)=>c.jsx("li",{"data-carousel-item":"","aria-label":G?Q.position(t+1,E):void 0,style:qe,className:T.cn("rst:shrink-0",R==="start"?"rst:snap-start":"rst:snap-center",ae&&"rst:snap-always",xe),children:e},n.isValidElement(e)&&e.key!=null?e.key:t))}),!L&&M==="overlay"&&q&&c.jsxs(c.Fragment,{children:[c.jsx("div",{className:"rst:pointer-events-none rst:absolute rst:inset-y-0 rst:start-1 rst:flex rst:items-center",children:c.jsx("div",{className:"rst:pointer-events-auto",children:k})}),c.jsx("div",{className:"rst:pointer-events-none rst:absolute rst:inset-y-0 rst:end-1 rst:flex rst:items-center",children:c.jsx("div",{className:"rst:pointer-events-auto",children:B})})]})]})]})});exports.Carousel=Qe;
1
+ "use client";"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const a=require("react/jsx-runtime"),Ee=require("react"),Te=require("../../atoms/Button/Button.cjs"),_=require("../../../lib/utils.cjs"),Ie=require("../../../lib/labels.cjs"),Ne=require("../../../internal/motion.cjs"),v=require("./carousel-math.cjs"),ct=require("./carousel-layout.cjs"),at=require("./carousel-selection.cjs"),Ce=require("../../../internal/key-focus.cjs"),Re=require("../../../internal/dev.cjs");function Pe(l){const c=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(l){for(const u in l)if(u!=="default"){const x=Object.getOwnPropertyDescriptor(l,u);Object.defineProperty(c,u,x.get?x:{enumerable:!0,get:()=>l[u]})}}return c.default=l,Object.freeze(c)}const s=Pe(Ee),Le={position:(l,c)=>`${l} of ${c}`},je=l=>Math.min(3e3,500+l*.35),ut='a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"]), [contenteditable]:not([contenteditable="false"])',dt=l=>typeof l=="number"?`${l}px`:l;function Lt(l){const c=parseFloat(l);return Number.isFinite(c)?c:0}const Oe="rst:hidden rst:[@media(any-hover:hover)]:inline-flex",De="rst:hidden rst:[@media(any-hover:hover)]:flex";function jt({direction:l}){return a.jsx("svg",{"aria-hidden":"true",focusable:"false",viewBox:"0 0 20 20",className:"rst:h-5 rst:w-5",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:a.jsx("path",{d:l==="prev"?"M12.5 4.5 7 10l5.5 5.5":"M7.5 4.5 13 10l-5.5 5.5"})})}const Me="rst:cursor-pointer rst:in-data-dragging:cursor-grabbing rst:outline-hidden rst:focus-visible:outline-solid rst:focus-visible:outline-2 rst:focus-visible:outline-offset-2 rst:focus-visible:outline-ring",Ae=s.memo(function({child:c,index:u,count:x,selected:C,active:k,label:P,style:q,className:F}){return a.jsx("li",{role:"option","aria-selected":C,"aria-setsize":x,"aria-posinset":u+1,"aria-label":P,tabIndex:k?0:-1,"data-carousel-item":"","data-selected":C?"":void 0,style:q,className:F,children:c})});function _e(l){const c=[];return s.Children.forEach(l,u=>{if(u==null||typeof u=="boolean")return;const x=s.isValidElement(u)&&u.key!==null;c.push({child:u,key:x?String(u.key):`#${c.length}`,keyed:x})}),c}const qe={active:null,keys:[],sig:"",selected:null,init:!1},W=(l,c)=>Array.prototype.indexOf.call(l.children,c),Ot={overflowing:null,canScrollPrev:!1,canScrollNext:!1,index:0},Fe=s.forwardRef(function({"aria-label":c,"aria-labelledby":u,"aria-describedby":x,children:C,itemWidth:k,perView:P,gap:q=12,gutter:F=16,bleed:Dt=!1,snap:G="start",snapStrictness:Mt="mandatory",oneAtATime:At=!1,arrows:X="hover",arrowPlacement:J="controls",renderArrows:$,prevLabel:_t,nextLabel:qt,prevIcon:Ft,nextIcon:$t,arrowButtonProps:Bt,fade:zt=!1,scrollbar:Ht=!1,showPosition:ft=!1,labels:Vt,onIndexChange:gt,onScrollStateChange:pt,selectable:p=!1,selectedKey:vt,defaultSelectedKey:Kt=null,onSelectionChange:Ut,onActivate:mt,clearable:Wt=!1,getOptionLabel:Y,itemClassName:Gt,listClassName:Xt,className:Jt,style:Yt,...Qt},Zt){Ce.useKeyFocus();const yt=Ie.withLabels(Le,Vt),h=s.useRef(null),xt=`${s.useId()}-list`,ht=s.Children.toArray(C),w=p?_e(C):null,L=w?w.length:ht.length,bt=vt!==void 0,[te,ee]=s.useState(Kt),d=bt?vt:te,[re,St]=s.useState(!1),m=w?w.map(t=>t.key):[],kt=m.join("\0"),[E,B]=s.useState(qe);let T=E.active;if(p&&(!E.init||E.sig!==kt||E.selected!==d)){const t=new Set(m);E.init&&E.selected!==d&&!re&&d!==null&&t.has(d)&&(T=d),(T===null||!t.has(T))&&(T=at.survivingNeighbor(E.keys,t,T)??(d!==null&&t.has(d)?d:m[0]??null)),B({active:T,keys:m,sig:kt,selected:d,init:!0})}const[f,ne]=s.useState(Ot),wt=s.useRef(Ot),[se,oe]=s.useState(""),[ie,le]=s.useState(!1),Q=s.useRef({onIndexChange:gt,onScrollStateChange:pt});s.useEffect(()=>{Q.current={onIndexChange:gt,onScrollStateChange:pt}});const y=s.useCallback(()=>{const t=h.current;if(!t)return null;const e=t.getBoundingClientRect(),r=getComputedStyle(t),n=e.left+t.clientLeft-t.scrollLeft;return{items:Array.from(t.children).map(i=>{const g=i.getBoundingClientRect();return{start:g.left-n,end:g.right-n}}),scrollLeft:t.scrollLeft,clientWidth:t.clientWidth,scrollWidth:t.scrollWidth,paddingStart:Lt(r.scrollPaddingInlineStart||r.scrollPaddingLeft),paddingEnd:Lt(r.scrollPaddingInlineEnd||r.scrollPaddingRight),align:G}},[G]),j=s.useCallback(()=>{const t=y();if(!t)return;const e={overflowing:v.overflows(t),canScrollPrev:v.canScrollPrev(t),canScrollNext:v.canScrollNext(t),index:t.items.length?v.currentIndex(t):0},r=wt.current;r.overflowing===e.overflowing&&r.canScrollPrev===e.canScrollPrev&&r.canScrollNext===e.canScrollNext&&r.index===e.index||(wt.current=e,ne(e),r.index!==e.index&&Q.current.onIndexChange?.(e.index),(r.canScrollPrev!==e.canScrollPrev||r.canScrollNext!==e.canScrollNext)&&Q.current.onScrollStateChange?.({canScrollPrev:e.canScrollPrev,canScrollNext:e.canScrollNext}))},[y]),ce=JSON.stringify([q,F,k,P]);s.useLayoutEffect(()=>{const t=h.current;if(!t)return;j();let e=0;const r=()=>{e||(e=requestAnimationFrame(()=>{e=0,j()}))};t.addEventListener("scroll",r,{passive:!0});const n=typeof ResizeObserver>"u"?null:new ResizeObserver(()=>j()),o=()=>{n?.observe(t),Array.from(t.children).forEach(g=>n?.observe(g))};o();const i=typeof MutationObserver>"u"?null:new MutationObserver(()=>{o(),j()});return i?.observe(t,{childList:!0}),()=>{t.removeEventListener("scroll",r),n?.disconnect(),i?.disconnect(),e&&cancelAnimationFrame(e)}},[j,L,ce]),s.useLayoutEffect(()=>{const t=h.current;if(t&&(le(!t.querySelector(ut)),u)){const e=u.split(/\s+/).map(r=>document.getElementById(r)?.textContent?.trim()??"").filter(Boolean).join(" ");oe(e)}},[C,u]);const I=s.useRef(null),N=s.useCallback(()=>{const t=h.current,e=I.current;e&&(I.current=null,window.clearTimeout(e.timer),t?.removeEventListener("scrollend",e.done),t&&(t.style.scrollSnapType="",delete t.dataset.settling))},[]),b=s.useCallback((t,e)=>{const r=h.current;if(!r)return;if(N(),e!=="smooth"||Ne.prefersReducedMotion()||Math.abs(r.scrollLeft-t)<1){r.style.scrollSnapType="none",r.scrollTo({left:t,behavior:"instant"}),r.style.scrollSnapType="";return}r.style.scrollSnapType="none",r.dataset.settling="";const o=r.scrollLeft,i=()=>{Math.abs(r.scrollLeft-o)<.5||N()};I.current={timer:window.setTimeout(N,je(Math.abs(r.scrollLeft-t))),done:i,target:t},r.addEventListener("scrollend",i),r.scrollTo({left:t,behavior:"smooth"})},[N]),z=s.useCallback(t=>{const e=y();if(!e)return;const r=I.current?{...e,scrollLeft:I.current.target}:e;b(v.pageTarget(r,t),"smooth")},[y,b]);s.useImperativeHandle(Zt,()=>({scrollToIndex:(t,e)=>{const r=y();if(!r||!r.items.length)return;const n=Math.min(Math.max(0,Math.round(t)),r.items.length-1);b(v.snapPosition(r,n),e?.behavior??"smooth")},scrollPrev:()=>z(-1),scrollNext:()=>z(1),focus:()=>{const t=h.current;if(!t)return;const e=p?t.querySelector(':scope > [tabindex="0"]'):t.querySelector(ut)??(t.tabIndex===0?t:null);if(!e)return;e.focus({preventScroll:!0});const r=e.closest("[data-carousel-item]"),n=y(),o=r&&r.parentElement===t?W(t,r):-1,i=n&&o>=0?v.revealTarget(n,o,I.current?.target??n.scrollLeft):null;i!==null&&b(i,"smooth")}}),[y,z,b,p]),s.useEffect(()=>N,[N]);const Z=s.useRef(null),tt=s.useRef(null),et=s.useRef(null);s.useLayoutEffect(()=>{const t=et.current;if(!t)return;const e=t==="prev"?Z.current:tt.current,r=t==="prev"?tt.current:Z.current,n=t==="prev"?!f.canScrollPrev:!f.canScrollNext,o=document.activeElement;n&&(o!==e&&o!==document.body&&o!==null||r&&!r.disabled&&r.focus())},[f.canScrollPrev,f.canScrollNext]);const R=s.useRef(null),O=s.useRef(!1),ae=t=>{t.pointerType!=="mouse"||t.button!==0||f.overflowing&&(N(),O.current=!1,R.current={pointerId:t.pointerId,x0:t.clientX,scroll0:t.currentTarget.scrollLeft,dragging:!1})},ue=t=>{const e=R.current;if(!e||t.pointerId!==e.pointerId)return;const r=t.currentTarget,n=t.clientX-e.x0;if(!e.dragging){if(Math.abs(n)<=v.CAROUSEL_DRAG_THRESHOLD)return;e.dragging=!0,r.dataset.dragging="",r.style.scrollSnapType="none",window.getSelection?.()?.removeAllRanges();try{r.setPointerCapture(t.pointerId)}catch{}}r.scrollLeft=e.scroll0-n},rt=t=>{const e=R.current;if(!e||t.pointerId!==e.pointerId||(R.current=null,!e.dragging))return;const r=t.currentTarget;delete r.dataset.dragging,O.current=t.type==="pointerup",window.setTimeout(()=>{O.current=!1},0);const n=y();n?b(v.snapPosition(n,v.nearestIndex(n)),"smooth"):r.style.scrollSnapType=""},de=t=>{!O.current||t.detail===0||(O.current=!1,t.preventDefault(),t.stopPropagation())},fe=t=>{f.overflowing&&t.preventDefault()},ge=t=>{if(p){ve(t);return}const e=t.currentTarget,r=t.target;if(r===e||R.current?.dragging)return;let n=!1;try{n=r.matches(":focus-visible, [data-key-focus]")}catch{n=!1}if(!n)return;const o=r.closest("[data-carousel-item]");if(!o||o.parentElement!==e)return;const i=y();if(!i)return;const g=Array.prototype.indexOf.call(e.children,o);b(v.snapPosition(i,g),"instant")},H=t=>{bt||ee(t),Ut?.(t)},pe=t=>{const e=y();if(!e)return;const r=v.revealTarget(e,t,I.current?.target??e.scrollLeft);r!==null&&b(r,"smooth")},ve=t=>{const e=t.currentTarget,r=t.target;if(r.parentElement!==e)return;const n=W(e,r),o=m[n];if(o===void 0||(B(g=>g.active===o?g:{...g,active:o}),St(!0),R.current?.dragging))return;let i=!1;try{i=r.matches(":focus-visible, [data-key-focus]")}catch{i=!1}i&&pe(n)},me=t=>{const e=t.relatedTarget;e&&t.currentTarget.contains(e)||(St(!1),d!==null&&m.includes(d)&&B(r=>r.active===d?r:{...r,active:d}))},ye=t=>{const e=t.currentTarget,r=t.target.closest("[data-carousel-item]");if(!r||r.parentElement!==e)return;const n=m[W(e,r)];n!==void 0&&(B(o=>o.active===n?o:{...o,active:n}),n===d?mt?.(n):H(n))},D=s.useRef({text:"",at:-1/0}),xe=t=>{const e=t.currentTarget,r=t.target;if(r.parentElement!==e||t.nativeEvent.isComposing||t.ctrlKey||t.metaKey||t.altKey)return;const n=m.length-1,o=W(e,r),i=m[o];if(i===void 0)return;const g=S=>{t.preventDefault();const A=Math.min(n,Math.max(0,S));A!==o&&e.children[A].focus({preventScroll:!0})},Rt=t.timeStamp-D.current.at<at.TYPEAHEAD_RESET_MS&&D.current.text!=="";switch(t.key.length!==1&&(D.current={text:"",at:-1/0}),t.key){case"ArrowRight":return g(o+1);case"ArrowLeft":return g(o-1);case"Home":return g(0);case"End":return g(n);case"PageDown":case"PageUp":{const S=y(),A=S?v.pageStep(S):1;return g(t.key==="PageDown"?o+A:o-A)}case"Enter":t.preventDefault(),i===d?mt?.(i):H(i);return;case" ":if(Rt)break;t.preventDefault(),i!==d&&H(i);return;case"Escape":if(!Wt||d===null)return;t.preventDefault(),t.stopPropagation(),H(null);return}if(t.key.length!==1)return;const Pt=(Rt?D.current.text:"")+t.key.toLowerCase();D.current={text:Pt,at:t.timeStamp},t.preventDefault();const we=S=>Y?Y(m[S],S):e.children[S]?.textContent??"",lt=at.typeaheadIndex(m.length,o,Pt,we);lt>=0&&lt!==o&&e.children[lt].focus({preventScroll:!0})},V=s.useRef({keys:!1,interactive:!1});s.useEffect(()=>{if(!p||!Re.isDevelopment())return;const t=h.current;!V.current.keys&&w?.some(e=>!e.keyed)&&(V.current.keys=!0,console.warn("Roster Carousel: selection mode needs a key on every child; it's the option's identity.")),!V.current.interactive&&t?.querySelector(`[data-carousel-item] :is(${ut})`)&&(V.current.interactive=!0,console.warn("Roster Carousel: an option holds something that takes focus. In selection mode the keys can't reach it; put it outside the row, or use onActivate."))});const[he,be]=s.useState(0);s.useEffect(()=>{const t=window.setTimeout(()=>be(f.index),150);return()=>window.clearTimeout(t)},[f.index]);const Et=c??se,Tt=t=>Et?`${t} ${Et}`:t,nt=X!=="none"&&f.overflowing!==!1,Se=X==="hover"?Oe:"rst:inline-flex",It=t=>{const e=t==="prev",{className:r,...n}=Bt??{};return a.jsx(Te.Button,{type:"button",variant:"outline",colorScheme:"neutral",size:"icon",...n,"aria-label":e?_t??Tt("Previous"):qt??Tt("Next"),"aria-controls":xt,disabled:e?!f.canScrollPrev:!f.canScrollNext,onClick:()=>z(e?-1:1),ref:e?Z:tt,onFocus:()=>{et.current=t},onBlur:o=>{o.relatedTarget&&(et.current=null)},"data-carousel-arrow":t,className:_.cn("rst:size-11 rst:shrink-0 rst:rounded-full",J==="overlay"&&!$&&"rst:bg-[var(--roster-popover-bg)] rst:elevation-raised rst:hover:bg-[var(--roster-popover-bg)] rst:disabled:invisible",Se,r),children:e?Ft??a.jsx(jt,{direction:"prev"}):$t??a.jsx(jt,{direction:"next"})})},K=nt?It("prev"):null,st=nt?It("next"):null,M=ft&&L?a.jsx("p",{"aria-live":"polite",className:"rst:font-ui rst:m-0 rst:text-sm rst:tabular-nums rst:opacity-75","data-carousel-position":"",children:yt.position(he+1,L)}):null;let ot=null;$?ot=$({prev:K,next:st,position:M}):J==="controls"&&(K||M)&&(ot=a.jsxs("div",{className:_.cn("rst:mb-2 rst:items-center rst:justify-end rst:gap-2",!M&&X==="hover"?De:"rst:flex"),"data-carousel-controls":"",children:[M&&a.jsx("div",{className:"rst:mr-auto",children:M}),K,st]}));const it=zt&&f.overflowing&&(f.canScrollPrev||f.canScrollNext)?`linear-gradient(to right, ${f.canScrollPrev?"transparent, black 48px":"black"}, ${f.canScrollNext?"black calc(100% - 48px), transparent":"black"})`:void 0,U=k===void 0&&P!==void 0,ke={"--rst-cv-gap":dt(q),"--rst-cv-gutter":dt(F),...U?ct.perViewVars(P):{},...it?{maskImage:it,WebkitMaskImage:it}:{}},Nt=s.useMemo(()=>k!==void 0?{width:dt(k)}:U?{width:ct.FLUID_ITEM_WIDTH}:void 0,[k,U]),Ct=_.cn("rst:shrink-0",G==="start"?"rst:snap-start":"rst:snap-center",At&&"rst:snap-always",p&&Me,Gt);return a.jsxs("div",{className:_.cn("rst:min-w-0",Jt),style:Yt,...Qt,"aria-describedby":p?void 0:x,children:[ot,a.jsxs("div",{className:"rst:relative",children:[a.jsx("ul",{ref:h,id:xt,role:p?"listbox":"list","aria-label":c,"aria-labelledby":c?void 0:u,"aria-orientation":p?"horizontal":void 0,"aria-describedby":p?x:void 0,tabIndex:!p&&ie&&f.overflowing?0:void 0,"data-overflowing":f.overflowing?"":void 0,onPointerDown:ae,onPointerMove:ue,onPointerUp:rt,onPointerCancel:rt,onLostPointerCapture:rt,onClickCapture:de,onClick:p?ye:void 0,onKeyDown:p?xe:void 0,onBlur:p?me:void 0,onDragStart:fe,onFocus:ge,style:ke,className:_.cn("rst:m-0 rst:flex rst:list-none rst:overflow-x-auto rst:overflow-y-hidden","rst:gap-(--rst-cv-gap) rst:px-(--rst-cv-gutter) rst:py-1 rst:scroll-px-(--rst-cv-gutter)","rst:overscroll-x-contain",Mt==="mandatory"?"rst:snap-x rst:snap-mandatory":"rst:snap-x rst:snap-proximity",Dt&&"rst:-mx-(--rst-cv-gutter)",!Ht&&"rst:[scrollbar-width:none] rst:[&::-webkit-scrollbar]:hidden",U&&ct.FLUID_CLASSES,"rst:data-overflowing:cursor-grab rst:data-dragging:cursor-grabbing rst:data-dragging:select-none","rst:focus-visible:outline-hidden rst:focus-visible:ring-2 rst:focus-visible:ring-ring rst:focus-visible:ring-inset",Xt),children:w?w.map((t,e)=>a.jsx(Ae,{child:t.child,index:e,count:L,selected:t.key===d,active:t.key===T,label:Y?.(t.key,e),style:Nt,className:Ct},t.key)):ht.map((t,e)=>a.jsx("li",{"data-carousel-item":"","aria-label":ft?yt.position(e+1,L):void 0,style:Nt,className:Ct,children:t},s.isValidElement(t)&&t.key!=null?t.key:e))}),!$&&J==="overlay"&&nt&&a.jsxs(a.Fragment,{children:[a.jsx("div",{className:"rst:pointer-events-none rst:absolute rst:inset-y-0 rst:start-1 rst:flex rst:items-center",children:a.jsx("div",{className:"rst:pointer-events-auto",children:K})}),a.jsx("div",{className:"rst:pointer-events-none rst:absolute rst:inset-y-0 rst:end-1 rst:flex rst:items-center",children:a.jsx("div",{className:"rst:pointer-events-auto",children:st})})]})]})]})});exports.Carousel=Fe;
@@ -17,6 +17,13 @@ export interface CarouselHandle {
17
17
  scrollPrev: () => void;
18
18
  /** What the next arrow does: on one page. */
19
19
  scrollNext: () => void;
20
+ /**
21
+ * Moves focus into the row. In selection mode, onto the option a Tab would
22
+ * land on: the selected one, or where the keys last left off. Otherwise onto
23
+ * the first item that can take focus, or the list when it's the tab stop.
24
+ * Scrolls it into view.
25
+ */
26
+ focus: () => void;
20
27
  }
21
28
  export interface CarouselArrowsRender {
22
29
  /** The previous arrow, or `null` while the row fits and there is nothing to page. */
@@ -35,7 +42,17 @@ export interface CarouselProps extends Omit<React.HTMLAttributes<HTMLDivElement>
35
42
  "aria-label"?: string;
36
43
  /** The id of the heading that names the row. */
37
44
  "aria-labelledby"?: string;
38
- /** One child per item. Each is wrapped in the list item the row snaps to. */
45
+ /**
46
+ * In selection mode, describes the listbox: say how it works, as "Enter
47
+ * selects. Enter again opens it." Otherwise it describes the outer wrapper,
48
+ * as it always has.
49
+ */
50
+ "aria-describedby"?: string;
51
+ /**
52
+ * One child per item. Each is wrapped in the list item the row snaps to. In
53
+ * selection mode each child is an option, and its React `key` is its
54
+ * identity: give every child one.
55
+ */
39
56
  children: React.ReactNode;
40
57
  /**
41
58
  * Fixed mode: every item is this wide (a number is px). The row overflows
@@ -117,14 +134,53 @@ export interface CarouselProps extends Omit<React.HTMLAttributes<HTMLDivElement>
117
134
  canScrollPrev: boolean;
118
135
  canScrollNext: boolean;
119
136
  }) => void;
137
+ /**
138
+ * Selection mode: the row becomes a listbox of options, one of which is
139
+ * selected. One tab stop, the arrow keys, Home, End and the Page keys move
140
+ * between options, type-ahead jumps to one, and Enter or Space selects.
141
+ * Selection doesn't follow focus: arrowing only moves focus. See the README
142
+ * for when to use it. Off by default, and off changes nothing.
143
+ *
144
+ * **Nothing interactive inside an option.** An option is one choice; a link
145
+ * or a button inside it would be a second tab stop the keys can't reach.
146
+ * Put actions outside the row (or use `onActivate`). A development build
147
+ * warns when an option holds something that takes focus.
148
+ */
149
+ selectable?: boolean;
150
+ /**
151
+ * Selection mode, controlled: the selected option's `key`, as React stores
152
+ * it (a string), or `null` for none.
153
+ */
154
+ selectedKey?: string | null;
155
+ /** Selection mode, uncontrolled: the option selected at first. */
156
+ defaultSelectedKey?: string | null;
157
+ /**
158
+ * Selection mode: an option was selected (Enter, Space, a click or a tap on
159
+ * one that isn't), with its key; or the selection was cleared (Escape, with
160
+ * `clearable`), with `null`.
161
+ */
162
+ onSelectionChange?: (key: string | null) => void;
163
+ /**
164
+ * Selection mode: Enter, or a click or a tap, on the option that's already
165
+ * selected. The second step after choosing, such as opening it.
166
+ */
167
+ onActivate?: (key: string) => void;
168
+ /** Selection mode: Escape clears the selection. */
169
+ clearable?: boolean;
170
+ /**
171
+ * Selection mode: each option's accessible name, which is also the text
172
+ * type-ahead matches. Without it an option is named by its content, and
173
+ * type-ahead reads its text.
174
+ */
175
+ getOptionLabel?: (key: string, index: number) => string;
120
176
  /** Classes for each list item. */
121
177
  itemClassName?: string;
122
178
  /** Classes for the scrolling list. `className` is the outer wrapper. */
123
179
  listClassName?: string;
124
180
  }
125
181
  /**
126
- * A sideways row of items: a row of cards, a selection strip, a gallery of one
127
- * per view.
182
+ * A sideways row of items: a row of cards, a selection strip (`selectable`), a
183
+ * gallery of one per view.
128
184
  *
129
185
  * Native first. The row is a real scroll container with CSS scroll snap, so
130
186
  * touch, trackpad, Shift with the wheel and focus all scroll it without
@@ -148,6 +204,12 @@ export interface CarouselProps extends Omit<React.HTMLAttributes<HTMLDivElement>
148
204
  *
149
205
  * Renders a `ul` named by `aria-label` or `aria-labelledby`, one `li` per
150
206
  * child. There is no carousel role and no slide role: nothing rotates.
207
+ *
208
+ * For a strip of choices, one of which is picked, use `selectable`: the row
209
+ * becomes a horizontal listbox with one tab stop, roving focus over its
210
+ * options, and a selection that changes only on Enter, Space or a tap. A row
211
+ * of buttons with `aria-pressed` is as many tab stops as there are buttons,
212
+ * and a screen reader hears a list of unrelated buttons.
151
213
  */
152
214
  declare const Carousel: React.ForwardRefExoticComponent<CarouselProps & React.RefAttributes<CarouselHandle>>;
153
215
  export { Carousel };