@blakesteve/roster 5.14.0 → 5.16.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 (33) hide show
  1. package/README.md +153 -38
  2. package/dist/components/molecules/FileUpload/FileUpload.cjs +1 -1
  3. package/dist/components/molecules/FileUpload/FileUpload.d.ts +20 -3
  4. package/dist/components/molecules/FileUpload/FileUpload.js +266 -216
  5. package/dist/components/molecules/FileUpload/file-upload-rejection.cjs +1 -0
  6. package/dist/components/molecules/FileUpload/file-upload-rejection.d.ts +24 -0
  7. package/dist/components/molecules/FileUpload/file-upload-rejection.js +11 -0
  8. package/dist/components/molecules/LiquidTabs/LiquidCount.d.ts +2 -2
  9. package/dist/components/molecules/LiquidTabs/LiquidTabPanel.cjs +1 -1
  10. package/dist/components/molecules/LiquidTabs/LiquidTabPanel.d.ts +10 -49
  11. package/dist/components/molecules/LiquidTabs/LiquidTabPanel.js +5 -28
  12. package/dist/components/molecules/LiquidTabs/LiquidTabs.cjs +1 -1
  13. package/dist/components/molecules/LiquidTabs/LiquidTabs.d.ts +13 -40
  14. package/dist/components/molecules/LiquidTabs/LiquidTabs.js +5 -95
  15. package/dist/components/molecules/LiquidTabs/liquid-count.d.ts +7 -5
  16. package/dist/components/molecules/LiquidTabs/liquid-tab-id.cjs +1 -1
  17. package/dist/components/molecules/LiquidTabs/liquid-tab-id.d.ts +4 -9
  18. package/dist/components/molecules/LiquidTabs/liquid-tab-id.js +3 -4
  19. package/dist/components/molecules/Tabs/TabPanel.cjs +1 -0
  20. package/dist/components/molecules/Tabs/TabPanel.d.ts +53 -0
  21. package/dist/components/molecules/Tabs/TabPanel.js +32 -0
  22. package/dist/components/molecules/Tabs/Tabs.cjs +1 -0
  23. package/dist/components/molecules/Tabs/Tabs.d.ts +118 -0
  24. package/dist/components/molecules/Tabs/Tabs.js +145 -0
  25. package/dist/components/molecules/Tabs/tab-id.cjs +1 -0
  26. package/dist/components/molecules/Tabs/tab-id.d.ts +13 -0
  27. package/dist/components/molecules/Tabs/tab-id.js +6 -0
  28. package/dist/index.d.ts +4 -0
  29. package/dist/meta.json +3 -3
  30. package/dist/roster.cjs +1 -1
  31. package/dist/roster.css +1 -1
  32. package/dist/roster.js +150 -142
  33. package/package.json +1 -1
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const t=Symbol.for("@blakesteve/roster/file-upload-rejection");class o extends Error{constructor(i,l){super(i,l),this.name="FileUploadRejection",Object.defineProperty(this,t,{value:!0})}}const n=e=>e instanceof o||typeof e=="object"&&e!==null&&t in e;exports.FileUploadRejection=o;exports.isFileUploadRejection=n;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Throw this from FileUpload's `upload`, or reject with it, when the file can
3
+ * never be added: the server says it isn't a photo, its type is refused, it
4
+ * can't be decoded. The file ends `rejected`, as a file turned away on picking
5
+ * does: "Not added." with this message as the reason, no Try again, and its
6
+ * place in `maxFiles` freed. A plain `Error` still means the upload failed and
7
+ * can be tried again.
8
+ *
9
+ * ```ts
10
+ * upload={async (file, { signal }) => {
11
+ * const response = await fetch("/photos", { method: "POST", body: file, signal });
12
+ * if (response.status === 415) throw new FileUploadRejection("That isn't a photo.");
13
+ * if (!response.ok) throw new Error("The server is busy.");
14
+ * return (await response.json()).id;
15
+ * }}
16
+ * ```
17
+ *
18
+ * No React in this module, so a server-side helper can make one too.
19
+ */
20
+ export declare class FileUploadRejection extends Error {
21
+ constructor(reason: string, options?: ErrorOptions);
22
+ }
23
+ /** Whether `error` says the file can never be added: a `FileUploadRejection` from any copy of this module. */
24
+ export declare const isFileUploadRejection: (error: unknown) => boolean;
@@ -0,0 +1,11 @@
1
+ const t = /* @__PURE__ */ Symbol.for("@blakesteve/roster/file-upload-rejection");
2
+ class l extends Error {
3
+ constructor(o, i) {
4
+ super(o, i), this.name = "FileUploadRejection", Object.defineProperty(this, t, { value: !0 });
5
+ }
6
+ }
7
+ const n = (e) => e instanceof l || typeof e == "object" && e !== null && t in e;
8
+ export {
9
+ l as FileUploadRejection,
10
+ n as isFileUploadRejection
11
+ };
@@ -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;
@@ -0,0 +1,53 @@
1
+ import { HTMLAttributes, ReactNode } from 'react';
2
+ export interface TabPanelProps extends Omit<HTMLAttributes<HTMLDivElement>, "id" | "role" | "hidden" | "children"> {
3
+ /** The `id` of the `Tabs` 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
+ }
29
+ /**
30
+ * The panel a `Tabs` 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 `tabId`, so it's named for its tab. It works
33
+ * with every variant, and with `LiquidTabs`, whose tabs have the same ids.
34
+ *
35
+ * ```tsx
36
+ * <Tabs id="views" tabs={[{ id: "open", label: "Open", panelId: "views-panel" }, …]} … />
37
+ * <TabPanel tabsId="views" id="views-panel" tab={view}>…</TabPanel>
38
+ * ```
39
+ *
40
+ * Two ways to lay it out. **One panel that swaps its content:** every tab
41
+ * carries the same `panelId`, and `tab` is the selected tab. **A panel per
42
+ * tab:** each tab has its own `panelId`, each panel its own `tab`, and every
43
+ * panel gets `activeTab`, so only the selected one shows.
44
+ *
45
+ * The WAI tabs pattern puts the panel in the tab order when nothing inside it
46
+ * can take focus, so a keyboard user can reach and scroll what was just
47
+ * selected. Whether the content holds anything focusable isn't known until it
48
+ * renders, and it can change as content loads, so the panel starts reachable
49
+ * (`tabIndex={0}`) and leaves the tab order once its content holds something
50
+ * that can be Tabbed to, rechecking as the content changes. Pass `tabIndex`
51
+ * to decide it yourself.
52
+ */
53
+ export declare function TabPanel({ tabsId, id, tab, activeTab, keepMounted, tabIndex, children, ...rest }: TabPanelProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,32 @@
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 { tabId as y } from "./tab-id.js";
5
+ const B = '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 A({ tabsId: s, id: d, 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(B));
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: d,
22
+ "aria-labelledby": y(s, t),
23
+ hidden: !a,
24
+ tabIndex: b ?? (f ? void 0 : 0),
25
+ "data-tab-panel": "",
26
+ children: a || l ? u : null
27
+ }
28
+ );
29
+ }
30
+ export {
31
+ A as TabPanel
32
+ };
@@ -0,0 +1 @@
1
+ "use client";"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const n=require("react/jsx-runtime"),y=require("react"),o=require("../LiquidTabs/liquid-pill.cjs"),O=require("../LiquidTabs/LiquidCount.cjs"),S=require("../LiquidTabs/liquid-count.cjs"),Q=require("./tab-id.cjs"),Y=require("../../../lib/labels.cjs"),_=require("../../../lib/utils.cjs"),$=require("../../../internal/key-focus.cjs"),z=s=>_.cn("rst:font-ui rst:flex rst:gap-1 rst:border-b rst:border-(--roster-tabs-border)",s),H=({fullWidth:s,size:e,isActive:r})=>_.cn("rst:relative rst:cursor-pointer rst:rounded-md rst:border-0 rst:bg-transparent rst:px-3 rst:py-2 rst:text-sm rst:font-medium rst:transition-colors","rst:ring-offset-background rst:focus-visible:outline-hidden rst:focus-visible:ring-2 rst:focus-visible:ring-ring rst:focus-visible:ring-offset-2","rst:after:pointer-events-none rst:after:absolute rst:after:inset-x-0 rst:after:-bottom-px",e==="lg"&&"rst:min-h-11",s&&"rst:flex-1",r?"rst:text-(--roster-tabs-text-active) rst:after:border-b-2 rst:after:border-(--roster-tabs-indicator)":"rst:text-(--roster-tabs-text) rst:hover:text-(--roster-tabs-text-hover) rst:hover:after:border-b-2 rst:hover:after:border-(--roster-tabs-indicator-hover)");function M({stripRef:s,activeTab:e,itemsKey:r,liquidStyle:b,fullWidth:x,className:f,id:d,"aria-label":m,"aria-labelledby":g,onKeyDown:h,onBlur:p,children:q}){const c=y.useRef(null);return o.useLiquidPill(s,c,e,r),n.jsxs("div",{ref:s,id:d,role:"tablist","aria-label":m,"aria-labelledby":g,"data-testid":"liquid-tabs",style:o.LIQUID_STRIP_STYLE,className:o.liquidStripClassName(b,x,f),onKeyDown:h,onBlur:p,children:[n.jsx("div",{ref:c,"aria-hidden":!0,"data-testid":"liquid-tabs-pill",className:o.liquidPillClassName(b),style:o.LIQUID_PILL_STYLE}),q]})}function W(s){const{tabs:e,activeTab:r,onChange:b,variant:x="plain",liquidStyle:f="pill",fullWidth:d=!1,size:m="md",activation:g="automatic",id:h,"aria-label":p,"aria-labelledby":q,className:c,labels:D}=s;$.useKeyFocus();const T=Y.withLabels(S.LIQUID_COUNT_LABELS,D),L=y.useRef(null),E=y.useId(),v=h??E,I=x==="liquid",j=t=>{t!==r&&b(t)},[C,A]=y.useState(null),F=e.some(t=>t.id===r),U=C!==null&&e.some(t=>t.id===C)?C:F?r:e[0]?.id,R=t=>{t.currentTarget.contains(t.relatedTarget)||A(null)},k=t=>{const i=L.current;if(!i)return;const a=Array.from(i.querySelectorAll('[role="tab"]')),u=a.indexOf(t.target);if(u===-1)return;const N=a.length-1,P=getComputedStyle(i).direction==="rtl",B=P?"ArrowLeft":"ArrowRight",K=P?"ArrowRight":"ArrowLeft";let l;if(t.key===B)l=u===N?0:u+1;else if(t.key===K)l=u===0?N:u-1;else if(t.key==="Home")l=0;else if(t.key==="End")l=N;else return;t.preventDefault(),a[l].focus(),g==="automatic"&&j(e[l].id)},w=e.map(t=>{const i=t.id===r,a=typeof t.label=="function"?t.label(i):t.label;return n.jsx("button",{type:"button",role:"tab",id:Q.tabId(v,t.id),"aria-selected":i,"aria-controls":t.panelId,"aria-label":S.countName(a,t.count,T),tabIndex:t.id===U?0:-1,"data-tab-id":t.id,"data-testid":I?`liquid-tab-${t.id}`:void 0,onClick:()=>j(t.id),onFocus:()=>A(t.id),className:I?o.liquidItemClassName({variant:f,fullWidth:d,size:m,isActive:i}):H({fullWidth:d,size:m,isActive:i}),children:S.shownCount(t.count)?n.jsx(O.LiquidCount,{label:a,count:t.count,labels:T}):a},t.id)});return I?n.jsx(M,{stripRef:L,activeTab:r,itemsKey:e.map(t=>t.id).join("\0"),liquidStyle:f,fullWidth:d,className:c,id:v,"aria-label":p,"aria-labelledby":q,onKeyDown:k,onBlur:R,children:w}):n.jsx("div",{ref:L,id:v,role:"tablist","aria-label":p,"aria-labelledby":q,className:z(c),onKeyDown:k,onBlur:R,children:w})}exports.Tabs=W;
@@ -0,0 +1,118 @@
1
+ import { ReactNode } from 'react';
2
+ import { TabsLabels } from '../LiquidTabs/liquid-count';
3
+ export type { TabsLabels };
4
+ export interface TabItem {
5
+ id: string;
6
+ /** Static label, or a render function that receives whether this tab is active. */
7
+ label: ReactNode | ((isActive: boolean) => ReactNode);
8
+ /**
9
+ * The `id` of the panel this tab controls, which sets `aria-controls`. Give
10
+ * every tab the same id when one panel swaps its content.
11
+ */
12
+ panelId?: string;
13
+ /**
14
+ * A number in a small mark beside the label, read as part of the tab's
15
+ * name: "Open, 12 items" (`labels.count` says it).
16
+ *
17
+ * **Zero shows**, as "0" and "0 items": a count that vanished at zero would
18
+ * change the tab's width and move every tab after it, and an empty list is
19
+ * worth saying. To hide it, pass `undefined` for zero. `undefined` means no
20
+ * count and no mark. A negative or fractional count isn't shown at all,
21
+ * never rounded or clamped. Large counts aren't abbreviated: 12,345 shows
22
+ * as "12,345" unless `labels.countMark` writes it shorter.
23
+ */
24
+ count?: number;
25
+ }
26
+ /** How the strip looks: `plain` (an underline) or `liquid` (a sliding pill). */
27
+ export type TabsVariant = "plain" | "liquid";
28
+ /** The liquid variant's two styles: a floating `pill`, or `filled` cells. */
29
+ export type TabsLiquidStyle = "pill" | "filled";
30
+ /** `md`, or `lg` for a 44px target. */
31
+ export type TabsSize = "md" | "lg";
32
+ interface TabsBaseProps {
33
+ tabs: TabItem[];
34
+ /** The `id` of the currently active tab. */
35
+ activeTab: string;
36
+ /** Called with the new tab `id` when the user selects a different tab. */
37
+ onChange: (id: string) => void;
38
+ /**
39
+ * Stretch the tabs to fill the row, each an equal share. In `plain`, the
40
+ * underline then spans each tab's share; the liquid `filled` style is always
41
+ * full width.
42
+ */
43
+ fullWidth?: boolean;
44
+ /**
45
+ * `"md"` (default), or `"lg"`, which makes every tab at least 44px tall, the
46
+ * WCAG 2.5.5 target size.
47
+ */
48
+ size?: TabsSize;
49
+ /**
50
+ * `"automatic"` (default): arrowing onto a tab selects it.
51
+ * `"manual"`: arrows move focus only, and Enter or Space selects. Use it when
52
+ * selecting a tab is expensive, like swapping a whole form.
53
+ */
54
+ activation?: "automatic" | "manual";
55
+ /**
56
+ * The tablist's `id`, and the base of every tab's id: see `tabId`.
57
+ * Generated when omitted, which is fine unless a panel needs to point back at
58
+ * its tab: give it one to pair a `TabPanel`.
59
+ */
60
+ id?: string;
61
+ "aria-label"?: string;
62
+ "aria-labelledby"?: string;
63
+ /** Extra classes applied to the tablist. */
64
+ className?: string;
65
+ /** Every word a count says, with English defaults: see `TabItem.count`. */
66
+ labels?: Partial<TabsLabels>;
67
+ }
68
+ export type TabsProps = TabsBaseProps & ({
69
+ /**
70
+ * `"plain"` (default): the selected tab is underlined, on a rule that
71
+ * runs the strip's width. `"liquid"`: a pill slides to the selected
72
+ * tab; `liquidStyle` picks its look.
73
+ */
74
+ variant?: "plain";
75
+ /** The liquid variant's look; only with `variant="liquid"`. */
76
+ liquidStyle?: never;
77
+ } | {
78
+ variant: "liquid";
79
+ /**
80
+ * `"pill"` (default): a floating pill inside a padded strip, `w-fit`
81
+ * unless `fullWidth` is set.
82
+ *
83
+ * `"filled"`: the selected tab fills its whole cell, and the strip is
84
+ * always full width.
85
+ *
86
+ * Both squash the pill vertically (`scaleY`) while it stretches
87
+ * between tabs.
88
+ */
89
+ liquidStyle?: TabsLiquidStyle;
90
+ });
91
+ /**
92
+ * A controlled tab strip, plain or liquid.
93
+ *
94
+ * Follows the WAI tabs pattern in every variant: only the selected tab is in
95
+ * the tab order, Left and Right move between tabs and wrap, Home and End go
96
+ * to the ends, and `activation="manual"` moves focus without selecting. If
97
+ * `activeTab` matches no tab, the first tab takes the tab stop and nothing is
98
+ * marked selected. Every tab is `type="button"`, so a strip inside a form
99
+ * never submits it.
100
+ *
101
+ * **`variant="plain"`**, the default, underlines the selected tab in
102
+ * `--roster-tabs-indicator` on a rule in `--roster-tabs-border`. The other
103
+ * tabs read `--roster-tabs-text`, darkening to `--roster-tabs-text-hover`
104
+ * under the pointer with a lighter underline, `--roster-tabs-indicator-hover`.
105
+ *
106
+ * **`variant="liquid"`** is the sliding pill `LiquidTabs` has always drawn,
107
+ * with its `pill` and `filled` styles (`liquidStyle`) and its
108
+ * `--roster-lt-*` tokens. The pill follows `activeTab`, never the click, so a
109
+ * change the consumer vetoes, or one that waits on navigation, never leaves
110
+ * it on the wrong tab. Until script has measured it, the selected tab paints
111
+ * the pill's color itself, so server-rendered HTML shows the selection too.
112
+ *
113
+ * A tab can carry a `count`, and a panel pairs with its tab by construction
114
+ * through `TabPanel`, in either variant.
115
+ *
116
+ * For navigation between routes, use `LiquidNav`, which renders links.
117
+ */
118
+ export declare function Tabs(props: TabsProps): import("react/jsx-runtime").JSX.Element;