@blakesteve/roster 5.14.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 (27) hide show
  1. package/README.md +100 -37
  2. package/dist/components/molecules/LiquidTabs/LiquidCount.d.ts +2 -2
  3. package/dist/components/molecules/LiquidTabs/LiquidTabPanel.cjs +1 -1
  4. package/dist/components/molecules/LiquidTabs/LiquidTabPanel.d.ts +10 -49
  5. package/dist/components/molecules/LiquidTabs/LiquidTabPanel.js +5 -28
  6. package/dist/components/molecules/LiquidTabs/LiquidTabs.cjs +1 -1
  7. package/dist/components/molecules/LiquidTabs/LiquidTabs.d.ts +13 -40
  8. package/dist/components/molecules/LiquidTabs/LiquidTabs.js +5 -95
  9. package/dist/components/molecules/LiquidTabs/liquid-count.d.ts +7 -5
  10. package/dist/components/molecules/LiquidTabs/liquid-tab-id.cjs +1 -1
  11. package/dist/components/molecules/LiquidTabs/liquid-tab-id.d.ts +4 -9
  12. package/dist/components/molecules/LiquidTabs/liquid-tab-id.js +3 -4
  13. package/dist/components/molecules/Tabs/TabPanel.cjs +1 -0
  14. package/dist/components/molecules/Tabs/TabPanel.d.ts +53 -0
  15. package/dist/components/molecules/Tabs/TabPanel.js +32 -0
  16. package/dist/components/molecules/Tabs/Tabs.cjs +1 -0
  17. package/dist/components/molecules/Tabs/Tabs.d.ts +118 -0
  18. package/dist/components/molecules/Tabs/Tabs.js +145 -0
  19. package/dist/components/molecules/Tabs/tab-id.cjs +1 -0
  20. package/dist/components/molecules/Tabs/tab-id.d.ts +13 -0
  21. package/dist/components/molecules/Tabs/tab-id.js +6 -0
  22. package/dist/index.d.ts +3 -0
  23. package/dist/meta.json +3 -3
  24. package/dist/roster.cjs +1 -1
  25. package/dist/roster.css +1 -1
  26. package/dist/roster.js +116 -110
  27. 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.
@@ -1983,17 +1984,54 @@ clicks and focus are one listener each on the row. Measured at 1,000 options
1983
1984
  in Chromium: mounting takes about 30ms longer than the default mode, a key
1984
1985
  moves focus within a millisecond, and the frame after it lands in about 7ms.
1985
1986
 
1986
- ### LiquidTabs
1987
+ ### Tabs
1987
1988
 
1988
- A tab strip with a sliding pill, on the WAI tabs pattern: one tab stop, arrows
1989
- that wrap, Home and End, and `activation="manual"` for tabs that are costly to
1990
- switch. `LiquidNav` is the same strip as a `nav` of links.
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.
1991
1993
 
1992
- **Counts.** A tab or a nav link takes `count`, shown in a small mark after its
1993
- label:
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:
1994
2032
 
1995
2033
  ```tsx
1996
- <LiquidTabs tabs={[{ id: "queue", label: "Queue", count: queue.length }, …]} … />
2034
+ <Tabs tabs={[{ id: "queue", label: "Queue", count: queue.length }, …]} … />
1997
2035
  ```
1998
2036
 
1999
2037
  The count is part of the name, "Queue, 12 items" rather than "Queue12" or
@@ -2014,29 +2052,29 @@ labels={{ count: (n) => (n === 1 ? "1 game" : `${n.toLocaleString()} games`) }}
2014
2052
  shorten the mark, pass `labels.countMark` (`(n) => "12K"`); the name stays
2015
2053
  whole, since a screen reader has no width to save.
2016
2054
  - **The mark is drawn from the tab's own text color:** its number in that
2017
- color on a 12% tint of it. So it follows the tab through every state, both
2018
- variants and both themes, and through any palette set on the
2019
- `--roster-lt-*` tokens, and its number clears 4.5:1 in every state on
2020
- 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
2021
2059
  palette, which on the selected tab would be a second chip sitting on the
2022
2060
  pill.
2023
- - **The pill follows,** re-measuring when a count widens its tab or one
2024
- before it.
2061
+ - **The liquid pill follows,** re-measuring when a count widens its tab or
2062
+ one before it.
2025
2063
 
2026
- **The panel.** `LiquidTabPanel` is the `role="tabpanel"` a tab controls, paired
2027
- with it by construction: its `id` is the tab's `panelId`, and its
2028
- `aria-labelledby` points at the tab through `liquidTabId`, so the panel takes
2029
- the tab's name, count included. Give the `LiquidTabs` an `id` to pair it with.
2030
- 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:
2031
2069
 
2032
2070
  ```tsx
2033
2071
  // One panel: every tab carries the same panelId, and `tab` is the selected one.
2034
- <LiquidTabs id="lists" tabs={tabs.map((t) => ({ ...t, panelId: "lists-panel" }))} activeTab={list} onChange={setList} />
2035
- <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>
2036
2074
 
2037
2075
  // A panel per tab: each its own panelId, and every panel gets activeTab.
2038
- <LiquidTabPanel tabsId="views" id="views-board" tab="board" activeTab={view}>…</LiquidTabPanel>
2039
- <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>
2040
2078
  ```
2041
2079
 
2042
2080
  Per tab, a panel that isn't selected stays in the page, hidden, so every tab's
@@ -2048,6 +2086,22 @@ can reach what they just selected; it leaves the tab order once its content
2048
2086
  holds a link, a button or a field, rechecked as the content changes. Pass
2049
2087
  `tabIndex` to decide it yourself.
2050
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
+
2051
2105
  ### Components that render links
2052
2106
 
2053
2107
  `Breadcrumbs` renders a plain `<a>` by default, which is right for a static
@@ -2098,8 +2152,8 @@ Dialog or Sheet), alongside `Pagination`, the loading system and DataTable's
2098
2152
  instead, and 5.8.0 adds the ones that were missing: MultiSelect's
2099
2153
  `countLabel`, DataTable's `emptyMessage` and MediaPreview's `newTabLabel`, next
2100
2154
  to the existing `dismissLabel`, `placeholder`, `removeLabel`, `prevLabel` and
2101
- the like. `LiquidTabs` and `LiquidNav` take `labels` for the words a count
2102
- 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
2103
2157
  whose word changes with the count can say so. An entry passed as `undefined`
2104
2158
  (a catalog missing a key) keeps its English rather than leaving a control
2105
2159
  unnamed.
@@ -2210,9 +2264,9 @@ function App() {
2210
2264
  | `DescriptionList` | Label and value pairs as a real `<dl>`: inline, stacked, or split |
2211
2265
  | `EmptyState` | Zero-data placeholder with icon and action slot; `headingLevel` sets its title's `h1` to `h6` |
2212
2266
  | `ErrorState` | Error display with retry action |
2213
- | `LiquidNav` | `LiquidTabs`' strip as a `nav` of links; takes a router's link component |
2214
- | `LiquidTabs` | Tab strip with a sliding pill; arrow keys, roving focus, counts, 44px `lg` size (see [LiquidTabs](#liquidtabs)) |
2215
- | `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` |
2216
2270
  | `MatchupCard` | Head-to-head comparison card |
2217
2271
  | `MediaPreview` | Thumbnail that falls back to "Preview unavailable, open original" when it can't render |
2218
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)) |
@@ -2225,6 +2279,8 @@ function App() {
2225
2279
  | `RadioGroup` | Radio set reporting one `string`; descriptions, error, 44px targets |
2226
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)) |
2227
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 |
2228
2284
  | `Toast` | Transient floating notice; `Toaster` + the imperative `toast` handle |
2229
2285
 
2230
2286
  ### Organisms
@@ -2390,6 +2446,13 @@ npm run check:prefix
2390
2446
  npm run check:classes-emit
2391
2447
  ```
2392
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
+
2393
2456
  Three guards are worth knowing about, because the bugs they catch cannot be
2394
2457
  reproduced from inside this repo.
2395
2458
 
@@ -1,5 +1,5 @@
1
1
  import { ReactNode } from 'react';
2
- import { LiquidTabsLabels } from './liquid-count';
2
+ import { TabsLabels } from './liquid-count';
3
3
  /**
4
4
  * A tab's or a link's label, with its count beside it when it has one.
5
5
  *
@@ -25,5 +25,5 @@ import { LiquidTabsLabels } from './liquid-count';
25
25
  export declare function LiquidCount({ label, count, labels }: {
26
26
  label: ReactNode;
27
27
  count: number | undefined;
28
- labels: LiquidTabsLabels;
28
+ labels: TabsLabels;
29
29
  }): import("react/jsx-runtime").JSX.Element;
@@ -1 +1 @@
1
- "use client";"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const v=require("react/jsx-runtime"),t=require("react"),y=require("./liquid-tab-id.cjs"),p='a[href], area[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), iframe, summary, audio[controls], video[controls], [contenteditable]:not([contenteditable="false"]), [tabindex]:not([tabindex="-1"])';function x({tabsId:d,id:l,tab:n,activeTab:a,keepMounted:u=!1,tabIndex:b,children:c,...f}){const i=t.useRef(null),r=a===void 0||a===n,[h,q]=t.useState(!1);return t.useLayoutEffect(()=>{const e=i.current;if(!e)return;const o=()=>q(!!e.querySelector(p));if(o(),typeof MutationObserver>"u")return;const s=new MutationObserver(o);return s.observe(e,{childList:!0,subtree:!0,attributes:!0,attributeFilter:["disabled","tabindex","href","contenteditable"]}),()=>s.disconnect()},[]),v.jsx("div",{...f,ref:i,role:"tabpanel",id:l,"aria-labelledby":y.liquidTabId(d,n),hidden:!r,tabIndex:b??(h?void 0:0),"data-tab-panel":"",children:r||u?c:null})}exports.LiquidTabPanel=x;
1
+ "use client";"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const n=require("react/jsx-runtime"),i=require("../Tabs/TabPanel.cjs");function t(e){return n.jsx(i.TabPanel,{...e})}exports.LiquidTabPanel=t;
@@ -1,52 +1,13 @@
1
- import { HTMLAttributes, ReactNode } from 'react';
2
- export interface LiquidTabPanelProps extends Omit<HTMLAttributes<HTMLDivElement>, "id" | "role" | "hidden" | "children"> {
3
- /** The `id` of the `LiquidTabs` this panel belongs to (its `id` prop). */
4
- tabsId: string;
5
- /** This panel's `id`: the `panelId` its tab carries, or every tab, for one panel that swaps its content. */
6
- id: string;
7
- /**
8
- * The tab that names this panel. For one panel that swaps its content, the
9
- * selected tab: the panel's name follows the selection. For a panel per tab,
10
- * its own tab.
11
- */
12
- tab: string;
13
- /**
14
- * For a panel per tab: the selected tab. The panel shows when it is `tab`,
15
- * and is hidden otherwise. Leave it out for one panel that swaps its
16
- * content, which always shows.
17
- */
18
- activeTab?: string;
19
- /**
20
- * For a panel per tab: keep the content mounted while the panel is hidden,
21
- * so what's in it (a half-filled form, a scroll position) survives a switch
22
- * away and back. By default a hidden panel renders no content. The panel
23
- * element itself always stays, hidden, so its tab's `aria-controls` points
24
- * at something.
25
- */
26
- keepMounted?: boolean;
27
- children?: ReactNode;
28
- }
1
+ import { TabPanelProps } from '../Tabs/TabPanel';
29
2
  /**
30
- * The panel a `LiquidTabs` tab controls, paired with it by construction:
31
- * `role="tabpanel"`, its `id` the tab's `panelId`, and `aria-labelledby`
32
- * pointing at the tab through `liquidTabId`, so it's named for its tab.
33
- *
34
- * ```tsx
35
- * <LiquidTabs id="views" tabs={[{ id: "open", label: "Open", panelId: "views-panel" }, …]} … />
36
- * <LiquidTabPanel tabsId="views" id="views-panel" tab={view}>…</LiquidTabPanel>
37
- * ```
38
- *
39
- * Two ways to lay it out. **One panel that swaps its content:** every tab
40
- * carries the same `panelId`, and `tab` is the selected tab. **A panel per
41
- * tab:** each tab has its own `panelId`, each panel its own `tab`, and every
42
- * panel gets `activeTab`, so only the selected one shows.
3
+ * @deprecated Renamed `TabPanelProps`; the props are the same.
4
+ */
5
+ export type LiquidTabPanelProps = TabPanelProps;
6
+ /**
7
+ * The panel a tab controls, named for its tab by construction.
43
8
  *
44
- * The WAI tabs pattern puts the panel in the tab order when nothing inside it
45
- * can take focus, so a keyboard user can reach and scroll what was just
46
- * selected. Whether the content holds anything focusable isn't known until it
47
- * renders, and it can change as content loads, so the panel starts reachable
48
- * (`tabIndex={0}`) and leaves the tab order once its content holds something
49
- * that can be Tabbed to, rechecking as the content changes. Pass `tabIndex`
50
- * to decide it yourself.
9
+ * @deprecated Renamed `TabPanel`, which this renders, for `Tabs` of every
10
+ * variant and for `LiquidTabs`. Replace the name; nothing else changes. This
11
+ * name keeps working.
51
12
  */
52
- export declare function LiquidTabPanel({ tabsId, id, tab, activeTab, keepMounted, tabIndex, children, ...rest }: LiquidTabPanelProps): import("react/jsx-runtime").JSX.Element;
13
+ export declare function LiquidTabPanel(props: LiquidTabPanelProps): import("react/jsx-runtime").JSX.Element;
@@ -1,32 +1,9 @@
1
1
  "use client";
2
- import { jsx as p } from "react/jsx-runtime";
3
- import { useRef as m, useState as v, useLayoutEffect as x } from "react";
4
- import { liquidTabId as y } from "./liquid-tab-id.js";
5
- const L = 'a[href], area[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), iframe, summary, audio[controls], video[controls], [contenteditable]:not([contenteditable="false"]), [tabindex]:not([tabindex="-1"])';
6
- function w({ tabsId: d, id: s, tab: t, activeTab: n, keepMounted: l = !1, tabIndex: b, children: u, ...c }) {
7
- const r = m(null), a = n === void 0 || n === t, [f, h] = v(!1);
8
- return x(() => {
9
- const e = r.current;
10
- if (!e) return;
11
- const o = () => h(!!e.querySelector(L));
12
- if (o(), typeof MutationObserver > "u") return;
13
- const i = new MutationObserver(o);
14
- return i.observe(e, { childList: !0, subtree: !0, attributes: !0, attributeFilter: ["disabled", "tabindex", "href", "contenteditable"] }), () => i.disconnect();
15
- }, []), /* @__PURE__ */ p(
16
- "div",
17
- {
18
- ...c,
19
- ref: r,
20
- role: "tabpanel",
21
- id: s,
22
- "aria-labelledby": y(d, t),
23
- hidden: !a,
24
- tabIndex: b ?? (f ? void 0 : 0),
25
- "data-tab-panel": "",
26
- children: a || l ? u : null
27
- }
28
- );
2
+ import { jsx as e } from "react/jsx-runtime";
3
+ import { TabPanel as i } from "../Tabs/TabPanel.js";
4
+ function t(r) {
5
+ return /* @__PURE__ */ e(i, { ...r });
29
6
  }
30
7
  export {
31
- w as LiquidTabPanel
8
+ t as LiquidTabPanel
32
9
  };
@@ -1 +1 @@
1
- "use client";"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const u=require("react/jsx-runtime"),d=require("react"),o=require("./liquid-pill.cjs"),U=require("./liquid-tab-id.cjs"),O=require("./LiquidCount.cjs"),m=require("./liquid-count.cjs"),Q=require("../../../lib/labels.cjs"),B=require("../../../internal/key-focus.cjs");function K({tabs:t,activeTab:r,onChange:C,variant:c="pill",fullWidth:y=!1,size:A="md",activation:R="automatic",id:h,"aria-label":w,"aria-labelledby":N,className:j,labels:k}){B.useKeyFocus();const L=Q.withLabels(m.LIQUID_COUNT_LABELS,k),a=d.useRef(null),I=d.useRef(null),x=d.useId(),p=h??x;o.useLiquidPill(a,I,r,t.map(e=>e.id).join("\0"));const b=e=>{e!==r&&C(e)},[f,S]=d.useState(null),T=t.some(e=>e.id===r),P=f!==null&&t.some(e=>e.id===f)?f:T?r:t[0]?.id,_=e=>{e.currentTarget.contains(e.relatedTarget)||S(null)},D=e=>{const i=a.current;if(!i)return;const l=Array.from(i.querySelectorAll('[role="tab"]')),n=l.indexOf(e.target);if(n===-1)return;const q=l.length-1,g=getComputedStyle(i).direction==="rtl",E=g?"ArrowLeft":"ArrowRight",F=g?"ArrowRight":"ArrowLeft";let s;if(e.key===E)s=n===q?0:n+1;else if(e.key===F)s=n===0?q:n-1;else if(e.key==="Home")s=0;else if(e.key==="End")s=q;else return;e.preventDefault(),l[s].focus(),R==="automatic"&&b(t[s].id)};return u.jsxs("div",{ref:a,id:p,role:"tablist","aria-label":w,"aria-labelledby":N,"data-testid":"liquid-tabs",style:o.LIQUID_STRIP_STYLE,className:o.liquidStripClassName(c,y,j),onKeyDown:D,onBlur:_,children:[u.jsx("div",{ref:I,"aria-hidden":!0,"data-testid":"liquid-tabs-pill",className:o.liquidPillClassName(c),style:o.LIQUID_PILL_STYLE}),t.map(e=>{const i=e.id===r,l=typeof e.label=="function"?e.label(i):e.label;return u.jsx("button",{type:"button",role:"tab",id:U.liquidTabId(p,e.id),"aria-selected":i,"aria-controls":e.panelId,"aria-label":m.countName(l,e.count,L),tabIndex:e.id===P?0:-1,"data-tab-id":e.id,"data-testid":`liquid-tab-${e.id}`,onClick:()=>b(e.id),onFocus:()=>S(e.id),className:o.liquidItemClassName({variant:c,fullWidth:y,size:A,isActive:i}),children:m.shownCount(e.count)?u.jsx(O.LiquidCount,{label:l,count:e.count,labels:L}):l},e.id)})]})}exports.LiquidTabs=K;
1
+ "use client";"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const t=require("react/jsx-runtime"),u=require("../Tabs/Tabs.cjs");function r({variant:i="pill",...e}){return t.jsx(u.Tabs,{...e,variant:"liquid",liquidStyle:i})}exports.LiquidTabs=r;
@@ -1,29 +1,7 @@
1
- import { ReactNode } from 'react';
2
1
  import { LiquidSize, LiquidVariant } from './liquid-pill';
3
- import { LiquidTabsLabels } from './liquid-count';
4
- export type { LiquidTabsLabels };
5
- export interface TabItem {
6
- id: string;
7
- /** Static label, or a render function that receives whether this tab is active. */
8
- label: ReactNode | ((isActive: boolean) => ReactNode);
9
- /**
10
- * The `id` of the panel this tab controls, which sets `aria-controls`. Give
11
- * every tab the same id when one panel swaps its content.
12
- */
13
- panelId?: string;
14
- /**
15
- * A number in a small mark beside the label, read as part of the tab's
16
- * name: "Open, 12 items" (`labels.count` says it).
17
- *
18
- * **Zero shows**, as "0" and "0 items": a count that vanished at zero would
19
- * change the tab's width and move every tab after it, and an empty list is
20
- * worth saying. To hide it, pass `undefined` for zero. `undefined` means no
21
- * count and no mark. A negative or fractional count isn't shown at all,
22
- * never rounded or clamped. Large counts aren't abbreviated: 12,345 shows
23
- * as "12,345" unless `labels.countMark` writes it shorter.
24
- */
25
- count?: number;
26
- }
2
+ import { TabItem, TabsLabels } from '../Tabs/Tabs';
3
+ export type { TabItem };
4
+ export type { LiquidTabsLabels } from './liquid-count';
27
5
  export interface LiquidTabsProps {
28
6
  tabs: TabItem[];
29
7
  /** The `id` of the currently active tab. */
@@ -64,25 +42,20 @@ export interface LiquidTabsProps {
64
42
  /** Extra classes applied to the outer container. */
65
43
  className?: string;
66
44
  /** Every word a count says, with English defaults: see `TabItem.count`. */
67
- labels?: Partial<LiquidTabsLabels>;
45
+ labels?: Partial<TabsLabels>;
68
46
  }
69
47
  /**
70
48
  * A controlled tab strip with a liquid sliding indicator.
71
49
  *
72
- * Follows the WAI tabs pattern: only the selected tab is in the tab order,
73
- * Left and Right move between tabs and wrap, Home and End go to the ends. If
74
- * `activeTab` matches no tab, the first tab takes the tab stop and no pill
75
- * shows.
76
- *
77
- * Until script has measured the pill, the active tab paints the pill's color
78
- * itself, so server-rendered HTML shows the selection too.
79
- *
80
- * The pill follows `activeTab`, never the click, so a change the consumer
81
- * vetoes, or one that waits on navigation, never leaves it on the wrong tab.
50
+ * @deprecated Use `Tabs` with `variant="liquid"`, which renders exactly this:
51
+ * the same markup, styles, keys and pill. `variant` becomes `liquidStyle`, and
52
+ * every other prop is the same:
82
53
  *
83
- * A tab can carry a `count`, and a panel pairs with its tab by construction
84
- * through `LiquidTabPanel`.
54
+ * ```tsx
55
+ * <LiquidTabs variant="filled" … /> // before
56
+ * <Tabs variant="liquid" liquidStyle="filled" … /> // after
57
+ * ```
85
58
  *
86
- * For navigation between routes, use `LiquidNav`, which renders links.
59
+ * `LiquidTabs` keeps working, unchanged.
87
60
  */
88
- export declare function LiquidTabs({ tabs, activeTab, onChange, variant, fullWidth, size, activation, id, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, className, labels, }: LiquidTabsProps): import("react/jsx-runtime").JSX.Element;
61
+ export declare function LiquidTabs({ variant, ...rest }: LiquidTabsProps): import("react/jsx-runtime").JSX.Element;
@@ -1,99 +1,9 @@
1
1
  "use client";
2
- import { jsxs as U, jsx as c } from "react/jsx-runtime";
3
- import { useRef as A, useId as j, useState as F } from "react";
4
- import { useLiquidPill as Q, LIQUID_STRIP_STYLE as B, LIQUID_PILL_STYLE as K, liquidPillClassName as O, liquidItemClassName as Y, liquidStripClassName as H } from "./liquid-pill.js";
5
- import { liquidTabId as $ } from "./liquid-tab-id.js";
6
- import { LiquidCount as z } from "./LiquidCount.js";
7
- import { shownCount as G, countName as J, LIQUID_COUNT_LABELS as M } from "./liquid-count.js";
8
- import { withLabels as V } from "../../../lib/labels.js";
9
- import { useKeyFocus as X } from "../../../internal/key-focus.js";
10
- function re({
11
- tabs: l,
12
- activeTab: r,
13
- onChange: S,
14
- variant: n = "pill",
15
- fullWidth: m = !1,
16
- size: g = "md",
17
- activation: h = "automatic",
18
- id: w,
19
- "aria-label": C,
20
- "aria-labelledby": N,
21
- className: k,
22
- labels: R
23
- }) {
24
- X();
25
- const f = V(M, R), d = A(null), p = A(null), _ = j(), y = w ?? _;
26
- Q(d, p, r, l.map((e) => e.id).join("\0"));
27
- const I = (e) => {
28
- e !== r && S(e);
29
- }, [u, L] = F(null), b = l.some((e) => e.id === r), x = u !== null && l.some((e) => e.id === u) ? u : b ? r : l[0]?.id, D = (e) => {
30
- e.currentTarget.contains(e.relatedTarget) || L(null);
31
- }, E = (e) => {
32
- const i = d.current;
33
- if (!i) return;
34
- const t = Array.from(
35
- i.querySelectorAll('[role="tab"]')
36
- ), s = t.indexOf(e.target);
37
- if (s === -1) return;
38
- const a = t.length - 1, q = getComputedStyle(i).direction === "rtl", P = q ? "ArrowLeft" : "ArrowRight", T = q ? "ArrowRight" : "ArrowLeft";
39
- let o;
40
- if (e.key === P) o = s === a ? 0 : s + 1;
41
- else if (e.key === T) o = s === 0 ? a : s - 1;
42
- else if (e.key === "Home") o = 0;
43
- else if (e.key === "End") o = a;
44
- else return;
45
- e.preventDefault(), t[o].focus(), h === "automatic" && I(l[o].id);
46
- };
47
- return /* @__PURE__ */ U(
48
- "div",
49
- {
50
- ref: d,
51
- id: y,
52
- role: "tablist",
53
- "aria-label": C,
54
- "aria-labelledby": N,
55
- "data-testid": "liquid-tabs",
56
- style: B,
57
- className: H(n, m, k),
58
- onKeyDown: E,
59
- onBlur: D,
60
- children: [
61
- /* @__PURE__ */ c(
62
- "div",
63
- {
64
- ref: p,
65
- "aria-hidden": !0,
66
- "data-testid": "liquid-tabs-pill",
67
- className: O(n),
68
- style: K
69
- }
70
- ),
71
- l.map((e) => {
72
- const i = e.id === r, t = typeof e.label == "function" ? e.label(i) : e.label;
73
- return /* @__PURE__ */ c(
74
- "button",
75
- {
76
- type: "button",
77
- role: "tab",
78
- id: $(y, e.id),
79
- "aria-selected": i,
80
- "aria-controls": e.panelId,
81
- "aria-label": J(t, e.count, f),
82
- tabIndex: e.id === x ? 0 : -1,
83
- "data-tab-id": e.id,
84
- "data-testid": `liquid-tab-${e.id}`,
85
- onClick: () => I(e.id),
86
- onFocus: () => L(e.id),
87
- className: Y({ variant: n, fullWidth: m, size: g, isActive: i }),
88
- children: G(e.count) ? /* @__PURE__ */ c(z, { label: t, count: e.count, labels: f }) : t
89
- },
90
- e.id
91
- );
92
- })
93
- ]
94
- }
95
- );
2
+ import { jsx as t } from "react/jsx-runtime";
3
+ import { Tabs as l } from "../Tabs/Tabs.js";
4
+ function e({ variant: i = "pill", ...r }) {
5
+ return /* @__PURE__ */ t(l, { ...r, variant: "liquid", liquidStyle: i });
96
6
  }
97
7
  export {
98
- re as LiquidTabs
8
+ e as LiquidTabs
99
9
  };
@@ -1,9 +1,9 @@
1
1
  /**
2
- * Counts on a tab or a nav link: what `LiquidTabs` and `LiquidNav` share
3
- * about them. Internal; the type is exported from `LiquidTabs`.
2
+ * Counts on a tab or a nav link: what `Tabs`, `LiquidTabs` and `LiquidNav`
3
+ * share about them. Internal; the type is exported from `Tabs`.
4
4
  */
5
5
  /** Every word a count says. Pass any of them in `labels` to replace it. */
6
- export interface LiquidTabsLabels {
6
+ export interface TabsLabels {
7
7
  /**
8
8
  * What a count says after the label, as part of the tab's accessible name:
9
9
  * "12 items", so the tab reads "Open, 12 items". Name your own noun, with
@@ -19,7 +19,9 @@ export interface LiquidTabsLabels {
19
19
  */
20
20
  countMark: (count: number) => string;
21
21
  }
22
- export declare const LIQUID_COUNT_LABELS: LiquidTabsLabels;
22
+ /** @deprecated Renamed `TabsLabels`; the words are the same. */
23
+ export type LiquidTabsLabels = TabsLabels;
24
+ export declare const LIQUID_COUNT_LABELS: TabsLabels;
23
25
  /**
24
26
  * Whether a count gets a mark: a whole number, zero or more. `undefined`
25
27
  * means no count. A negative or fractional count, or `NaN`, isn't shown at
@@ -34,4 +36,4 @@ export declare const shownCount: (count: number | undefined) => count is number;
34
36
  * visually hidden part, so the hidden ", 12 items" reads "Open , 12 items".
35
37
  * A label that's a node keeps that content name, which is spoken the same.
36
38
  */
37
- export declare const countName: (label: unknown, count: number | undefined, labels: LiquidTabsLabels) => string | undefined;
39
+ export declare const countName: (label: unknown, count: number | undefined, labels: TabsLabels) => string | undefined;
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});function i(t,e){return`${t}-tab-${e}`}exports.liquidTabId=i;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const t=require("../Tabs/tab-id.cjs"),e=t.tabId;exports.liquidTabId=e;
@@ -1,12 +1,7 @@
1
1
  /**
2
- * The `id` a `LiquidTabs` tab renders with, for a panel's `aria-labelledby`.
2
+ * The `id` a tab renders with, for a panel's `aria-labelledby`.
3
3
  *
4
- * ```tsx
5
- * <LiquidTabs id="views" tabs={tabs} activeTab={view} onChange={setView} />
6
- * <div role="tabpanel" id="views-panel" aria-labelledby={liquidTabId("views", view)}>
7
- * ```
8
- *
9
- * The tab's own `id` is used verbatim, so keep it free of whitespace. A module
10
- * of its own, with no React in it, so a server component can call it too.
4
+ * @deprecated Renamed `tabId`, which is the same function: `Tabs` and
5
+ * `LiquidTabs` give their tabs the same ids. This name keeps working.
11
6
  */
12
- export declare function liquidTabId(listId: string, tabId: string): string;
7
+ export declare const liquidTabId: (listId: string, tab: string) => string;
@@ -1,6 +1,5 @@
1
- function n(t, i) {
2
- return `${t}-tab-${i}`;
3
- }
1
+ import { tabId as o } from "../Tabs/tab-id.js";
2
+ const d = o;
4
3
  export {
5
- n as liquidTabId
4
+ d as liquidTabId
6
5
  };
@@ -0,0 +1 @@
1
+ "use client";"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const y=require("react/jsx-runtime"),t=require("react"),p=require("./tab-id.cjs"),x='a[href], area[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), iframe, summary, audio[controls], video[controls], [contenteditable]:not([contenteditable="false"]), [tabindex]:not([tabindex="-1"])';function m({tabsId:d,id:l,tab:n,activeTab:a,keepMounted:b=!1,tabIndex:u,children:c,...f}){const r=t.useRef(null),o=a===void 0||a===n,[h,v]=t.useState(!1);return t.useLayoutEffect(()=>{const e=r.current;if(!e)return;const s=()=>v(!!e.querySelector(x));if(s(),typeof MutationObserver>"u")return;const i=new MutationObserver(s);return i.observe(e,{childList:!0,subtree:!0,attributes:!0,attributeFilter:["disabled","tabindex","href","contenteditable"]}),()=>i.disconnect()},[]),y.jsx("div",{...f,ref:r,role:"tabpanel",id:l,"aria-labelledby":p.tabId(d,n),hidden:!o,tabIndex:u??(h?void 0:0),"data-tab-panel":"",children:o||b?c:null})}exports.TabPanel=m;