@charcuterie/ui 3.30.0 → 3.32.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 (69) hide show
  1. package/README.md +14 -0
  2. package/dist/ActionTiles/ActionTiles.d.ts +115 -0
  3. package/dist/ActionTiles/ActionTiles.d.ts.map +1 -0
  4. package/dist/ActionTiles/ActionTiles.js +115 -0
  5. package/dist/ActionTiles/ActionTiles.js.map +1 -0
  6. package/dist/RadioGroup/RadioGroup.d.ts +10 -0
  7. package/dist/RadioGroup/RadioGroup.d.ts.map +1 -1
  8. package/dist/RadioGroup/RadioGroup.js +12 -39
  9. package/dist/RadioGroup/RadioGroup.js.map +1 -1
  10. package/dist/RadioGroup/RadioGroupOption.d.ts.map +1 -1
  11. package/dist/RadioGroup/RadioGroupOption.js +3 -33
  12. package/dist/RadioGroup/RadioGroupOption.js.map +1 -1
  13. package/dist/Tabs/PanelTabs.d.ts +56 -0
  14. package/dist/Tabs/PanelTabs.d.ts.map +1 -0
  15. package/dist/Tabs/PanelTabs.js +163 -0
  16. package/dist/Tabs/PanelTabs.js.map +1 -0
  17. package/dist/Tabs/RoutedTabs.d.ts +19 -0
  18. package/dist/Tabs/RoutedTabs.d.ts.map +1 -0
  19. package/dist/Tabs/RoutedTabs.js +28 -0
  20. package/dist/Tabs/RoutedTabs.js.map +1 -0
  21. package/dist/Tabs/TabLink.d.ts +34 -0
  22. package/dist/Tabs/TabLink.d.ts.map +1 -0
  23. package/dist/Tabs/TabLink.js +66 -0
  24. package/dist/Tabs/TabLink.js.map +1 -0
  25. package/dist/Tabs/TabTrigger.d.ts +1 -1
  26. package/dist/Tabs/TabTrigger.d.ts.map +1 -1
  27. package/dist/Tabs/TabTrigger.js +6 -30
  28. package/dist/Tabs/TabTrigger.js.map +1 -1
  29. package/dist/Tabs/Tabs.d.ts +80 -64
  30. package/dist/Tabs/Tabs.d.ts.map +1 -1
  31. package/dist/Tabs/Tabs.js +53 -182
  32. package/dist/Tabs/Tabs.js.map +1 -1
  33. package/dist/Tabs/tabItems.d.ts +36 -0
  34. package/dist/Tabs/tabItems.d.ts.map +1 -0
  35. package/dist/Tabs/tabItems.js +2 -0
  36. package/dist/Tabs/tabItems.js.map +1 -0
  37. package/dist/Tabs/tabStyles.d.ts +41 -0
  38. package/dist/Tabs/tabStyles.d.ts.map +1 -0
  39. package/dist/Tabs/tabStyles.js +79 -0
  40. package/dist/Tabs/tabStyles.js.map +1 -0
  41. package/dist/index.d.ts +4 -1
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +1 -0
  44. package/dist/index.js.map +1 -1
  45. package/dist/tileStyles.d.ts +83 -0
  46. package/dist/tileStyles.d.ts.map +1 -0
  47. package/dist/tileStyles.js +98 -0
  48. package/dist/tileStyles.js.map +1 -0
  49. package/package.json +1 -1
  50. package/src/ActionTiles/ActionTiles.mdx +131 -0
  51. package/src/ActionTiles/ActionTiles.stories.tsx +311 -0
  52. package/src/ActionTiles/ActionTiles.test.tsx +292 -0
  53. package/src/ActionTiles/ActionTiles.tsx +292 -0
  54. package/src/RadioGroup/RadioGroup.mdx +7 -0
  55. package/src/RadioGroup/RadioGroup.tsx +16 -42
  56. package/src/RadioGroup/RadioGroupOption.tsx +8 -38
  57. package/src/Tabs/PanelTabs.tsx +260 -0
  58. package/src/Tabs/RoutedTabs.tsx +58 -0
  59. package/src/Tabs/TabLink.tsx +132 -0
  60. package/src/Tabs/TabTrigger.tsx +10 -39
  61. package/src/Tabs/Tabs.mdx +70 -0
  62. package/src/Tabs/Tabs.stories.tsx +133 -1
  63. package/src/Tabs/Tabs.test.tsx +169 -0
  64. package/src/Tabs/Tabs.tsx +94 -296
  65. package/src/Tabs/tabItems.ts +40 -0
  66. package/src/Tabs/tabStyles.ts +107 -0
  67. package/src/index.ts +13 -3
  68. package/src/sourceRules.test.ts +9 -1
  69. package/src/tileStyles.ts +118 -0
@@ -0,0 +1,163 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { selectTabIndex, useRovingFocus, useSinglePicker, useUniqueId, } from "@charcuterie/logic";
3
+ import { useCallback, useEffect, useRef } from "react";
4
+ import { toClassName } from "../toClassName.js";
5
+ import { TabTrigger } from "./TabTrigger.js";
6
+ import { toTabListClass } from "./tabStyles.js";
7
+ const ORIENTATION_KEYS = {
8
+ horizontal: {
9
+ next: "ArrowRight",
10
+ previous: "ArrowLeft",
11
+ },
12
+ vertical: { next: "ArrowDown", previous: "ArrowUp" },
13
+ };
14
+ /**
15
+ * The panel bar — every tab a `<button>`, every section a
16
+ * `role="tabpanel"` this component renders and hides. See `Tabs`
17
+ * for which of the two bars a screen wants.
18
+ *
19
+ * The composition is two hooks and one decision:
20
+ *
21
+ * ```ts
22
+ * const selection = useSinglePicker({ selectedValue: initialKey })
23
+ * const focus = useRovingFocus({ activeValue: initialKey })
24
+ *
25
+ * // `automatic` is this line, and `manual` is its absence.
26
+ * if (activation === "automatic") selection.select(focus.activeValue)
27
+ * ```
28
+ *
29
+ * ### Selection, not visibility — and the difference is not cosmetic
30
+ *
31
+ * M4 built this on `VisibilityGroup`, and the panels do end up
32
+ * one-at-a-time either way, because the two cores are the same
33
+ * shape. But the thing that **registers** here is the tab, not the
34
+ * panel, and what a tab bar is asking is *which one did you pick*.
35
+ * `SinglePicker` is the kind that answers that; the panel's
36
+ * `hidden` is derived from the answer.
37
+ *
38
+ * Reading it the other way round cost something concrete: it made
39
+ * `aria-selected` a report about a *panel's* visibility, so a
40
+ * consumer wanting a tab bar with no panels at all — a segmented
41
+ * filter, a view switcher that swaps a route — had to model its
42
+ * choice as a group of visibilities it did not have. `Alert`'s
43
+ * sibling `SegmentedControl` is exactly that consumer, and it now
44
+ * shares this component's core rather than hand-rolling a fourth
45
+ * one.
46
+ * [Decision](../../../../docs/decisions/2026-07-30-tab-selection-is-a-single-picker.md).
47
+ *
48
+ * ### What it does not use, and why that is the right answer
49
+ *
50
+ * The plan expected `createLinkedIds` here as well. It does not
51
+ * fit, and forcing it would have been the wrong call: that kind
52
+ * exists for a **dynamic** trigger↔target pair — a `Popover` whose
53
+ * panel mounts and unmounts, where the multiset is what stops
54
+ * `aria-controls` pointing at a node React has already removed. A
55
+ * tab bar's pairing is static and known at render, so the ids come
56
+ * from one `useUniqueId` and cannot get out of step at all.
57
+ *
58
+ * ### Focus is moved deliberately, and only then
59
+ *
60
+ * `RovingFocus` is state; something still has to call `.focus()`.
61
+ * That happens only while the tablist already contains the focused
62
+ * element — otherwise mounting a tab bar, or any parent re-render
63
+ * that moved `activeValue`, yanks the caret out of whatever the
64
+ * user was actually doing.
65
+ */
66
+ export const PanelTabs = ({ activation = "automatic", activeKey, className, label, onChange, orientation = "horizontal", tabs, }) => {
67
+ const baseId = useUniqueId();
68
+ const tablistRef = useRef(null);
69
+ const tabElements = useRef(new Map());
70
+ const initialKey = activeKey ??
71
+ tabs.find((one) => !one.isDisabled)?.key ??
72
+ null;
73
+ const selection = useSinglePicker({
74
+ onChange,
75
+ selectedValue: initialKey,
76
+ });
77
+ const focus = useRovingFocus({
78
+ activeValue: initialKey,
79
+ });
80
+ const { select } = selection;
81
+ const { activeValue, setActiveValue } = focus;
82
+ /**
83
+ * Selected **or** pending, and the fallback is load-bearing.
84
+ *
85
+ * Members register from an effect, so on the first paint
86
+ * `selectedValue` is still null and the intent lives in
87
+ * `pendingValue` — a tab bar reading only the former renders with
88
+ * no tab selected and no panel shown, then corrects itself a
89
+ * frame later. It is a flash on a fast machine and a visible
90
+ * blank on a kiosk Pi.
91
+ *
92
+ * The core distinguishes the two on purpose, which is why this
93
+ * belongs here rather than in the selector — unlike
94
+ * `RovingFocus`, where the equivalent first-paint hole was in
95
+ * `selectTabIndex` itself and got fixed there.
96
+ */
97
+ const shownKey = selection.selectedValue ?? selection.pendingValue;
98
+ const selectTab = useCallback((key) => {
99
+ // Both, because a pointer press moves focus *and* chooses.
100
+ // Only selecting would leave `automatic`'s effect below to
101
+ // put the old panel straight back.
102
+ setActiveValue(key);
103
+ select(key);
104
+ }, [select, setActiveValue]);
105
+ useEffect(() => {
106
+ // Automatic activation, and the only behaviour that differs
107
+ // between the two modes. `select` is idempotent, so this is a
108
+ // no-op on every render where focus did not actually move.
109
+ if (activation === "automatic" &&
110
+ activeValue !== null) {
111
+ select(activeValue);
112
+ }
113
+ }, [activation, activeValue, select]);
114
+ useEffect(() => {
115
+ const tablist = tablistRef.current;
116
+ if (!tablist ||
117
+ activeValue === null ||
118
+ !tablist.contains(document.activeElement)) {
119
+ return;
120
+ }
121
+ tabElements.current.get(activeValue)?.focus();
122
+ }, [activeValue]);
123
+ const keys = ORIENTATION_KEYS[orientation];
124
+ return (_jsxs("div", { className: toClassName("flex flex-col gap-3", className), children: [_jsx("div", { "aria-label": label, "aria-orientation": orientation, className: toTabListClass(orientation), onKeyDown: (keyEvent) => {
125
+ const commands = {
126
+ [keys.next]: focus.next,
127
+ [keys.previous]: focus.previous,
128
+ End: focus.last,
129
+ Home: focus.first,
130
+ };
131
+ const command = commands[keyEvent.key];
132
+ if (command) {
133
+ // Arrow keys scroll the page by default, and a tab bar
134
+ // that scrolls the window while it moves focus is the
135
+ // hand-rolled version of this component.
136
+ keyEvent.preventDefault();
137
+ command();
138
+ return;
139
+ }
140
+ // `manual`'s commit. In `automatic` the panel is showing
141
+ // already, so this is the no-op that makes Enter behave
142
+ // identically in both modes.
143
+ if ((keyEvent.key === "Enter" ||
144
+ keyEvent.key === " ") &&
145
+ activeValue !== null) {
146
+ keyEvent.preventDefault();
147
+ select(activeValue);
148
+ }
149
+ }, ref: tablistRef, role: "tablist", children: tabs.map((tab, index) => (_jsx(TabTrigger, { id: `${baseId}-tab-${index}`, isSelected: shownKey === tab.key, onSelect: selectTab, panelId: `${baseId}-panel-${index}`, registerFocus: focus.register, orientation: orientation, registerSelection: selection.register, tab: tab,
150
+ // The roving-tabindex rule read from the core rather
151
+ // than restated here: exactly one member is in the tab
152
+ // order, so Tab enters and leaves the bar once while
153
+ // the arrow keys move inside it.
154
+ tabIndex: selectTabIndex(focus, tab.key), trackElement: (key, element) => {
155
+ if (element) {
156
+ tabElements.current.set(key, element);
157
+ }
158
+ else {
159
+ tabElements.current.delete(key);
160
+ }
161
+ } }, tab.key))) }), tabs.map((tab, index) => (_jsx("div", { "aria-labelledby": `${baseId}-tab-${index}`, hidden: shownKey !== tab.key, id: `${baseId}-panel-${index}`, role: "tabpanel", children: tab.content }, tab.key)))] }));
162
+ };
163
+ //# sourceMappingURL=PanelTabs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PanelTabs.js","sourceRoot":"","sources":["../../src/Tabs/PanelTabs.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import type { ReactNode } from "react";
2
+ import type { TabsLinkProps } from "./Tabs.tsx";
3
+ /**
4
+ * The routed bar: a named `<nav>` of real links wearing the tab
5
+ * paint.
6
+ *
7
+ * There is **no state here at all**, and that is the difference that
8
+ * matters rather than a saving. A panel bar owns its choice because
9
+ * nothing else knows it; a routed bar's choice is the address, so
10
+ * owning a copy would give the app two answers that can disagree —
11
+ * the Back button being the first thing to prove it.
12
+ *
13
+ * `resolveActiveKey` is `Nav`'s rule, imported rather than restated:
14
+ * whole-segment matching, the deepest match wins, the root is exact,
15
+ * an external item is never current, and the query string is not
16
+ * part of the answer.
17
+ */
18
+ export declare const RoutedTabs: ({ activeHref, className, label, orientation, tabs, }: TabsLinkProps) => ReactNode;
19
+ //# sourceMappingURL=RoutedTabs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RoutedTabs.d.ts","sourceRoot":"","sources":["../../src/Tabs/RoutedTabs.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAQtC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAG/C;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,UAAU,GAAI,sDAMxB,aAAa,KAAG,SAyBlB,CAAA"}
@@ -0,0 +1,28 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { getNavItemKey, resolveActiveKey, } from "../Nav/navItems.js";
3
+ import { toClassName } from "../toClassName.js";
4
+ import { TabLink } from "./TabLink.js";
5
+ import { toTabListClass } from "./tabStyles.js";
6
+ /**
7
+ * The routed bar: a named `<nav>` of real links wearing the tab
8
+ * paint.
9
+ *
10
+ * There is **no state here at all**, and that is the difference that
11
+ * matters rather than a saving. A panel bar owns its choice because
12
+ * nothing else knows it; a routed bar's choice is the address, so
13
+ * owning a copy would give the app two answers that can disagree —
14
+ * the Back button being the first thing to prove it.
15
+ *
16
+ * `resolveActiveKey` is `Nav`'s rule, imported rather than restated:
17
+ * whole-segment matching, the deepest match wins, the root is exact,
18
+ * an external item is never current, and the query string is not
19
+ * part of the answer.
20
+ */
21
+ export const RoutedTabs = ({ activeHref, className, label, orientation = "horizontal", tabs, }) => {
22
+ const activeKey = resolveActiveKey(tabs, activeHref);
23
+ return (_jsx("nav", { "aria-label": label, className: toClassName(toTabListClass(orientation), className), children: tabs.map((tab) => {
24
+ const key = getNavItemKey(tab);
25
+ return (_jsx(TabLink, { isCurrent: key === activeKey, item: tab, orientation: orientation }, key));
26
+ }) }));
27
+ };
28
+ //# sourceMappingURL=RoutedTabs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RoutedTabs.js","sourceRoot":"","sources":["../../src/Tabs/RoutedTabs.tsx"],"names":[],"mappings":";AAEA,OAAO,EACL,aAAa,EACb,gBAAgB,GACjB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC/C,OAAO,EAAE,OAAO,EAAE,MAAM,cAAe,CAAA;AAEvC,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAE/C;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,EACzB,UAAU,EACV,SAAS,EACT,KAAK,EACL,WAAW,GAAG,YAAY,EAC1B,IAAI,GACU,EAAa,EAAE;IAC7B,MAAM,SAAS,GAAG,gBAAgB,CAAC,IAAI,EAAE,UAAU,CAAC,CAAA;IAEpD,OAAO,CACL,4BACc,KAAK,EACjB,SAAS,EAAE,WAAW,CACpB,cAAc,CAAC,WAAW,CAAC,EAC3B,SAAS,CACV,YAEA,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;YAChB,MAAM,GAAG,GAAG,aAAa,CAAC,GAAG,CAAC,CAAA;YAE9B,OAAO,CACL,KAAC,OAAO,IACN,SAAS,EAAE,GAAG,KAAK,SAAS,EAC5B,IAAI,EAAE,GAAG,EAET,WAAW,EAAE,WAAW,IADnB,GAAG,CAER,CACH,CAAA;QACH,CAAC,CAAC,GACE,CACP,CAAA;AACH,CAAC,CAAA"}
@@ -0,0 +1,34 @@
1
+ import type { ReactNode } from "react";
2
+ import type { TabLinkItem, TabsOrientation } from "./tabItems.ts";
3
+ export type TabLinkProps = {
4
+ isCurrent: boolean;
5
+ item: TabLinkItem;
6
+ orientation: TabsOrientation;
7
+ };
8
+ /**
9
+ * One routed tab — and it is a real `<a href>`, through the same
10
+ * `RouterLinkProvider` seam `NavLink` uses.
11
+ *
12
+ * ### It carries no `role="tab"`, and that is the whole point
13
+ *
14
+ * `role="tab"` on an anchor **overrides** the link role. A screen
15
+ * reader then announces "tab", the reader expects a panel to appear
16
+ * beside it, and instead the address changes and the page under it
17
+ * is replaced. The APG tab pattern describes a disclosure inside one
18
+ * document; it does not describe navigation, and `aria-controls`
19
+ * has nothing to point at here because the section's content comes
20
+ * from the router — an `<Outlet />` the app renders somewhere this
21
+ * component cannot see.
22
+ *
23
+ * So a routed bar is what it actually is: a list of links inside a
24
+ * named `<nav>`, with `aria-current="page"` on the one you are on.
25
+ * That is `Nav`'s answer, and reusing it rather than restating it is
26
+ * why `resolveActiveKey` decides the current item.
27
+ *
28
+ * What the tab pattern was *really* supplying here was the paint,
29
+ * and the paint is shared through `toTabTriggerClass` — so this
30
+ * looks identical to the `<button>` beside it in Storybook and is
31
+ * announced correctly.
32
+ */
33
+ export declare const TabLink: ({ isCurrent, item, orientation, }: TabLinkProps) => ReactNode;
34
+ //# sourceMappingURL=TabLink.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TabLink.d.ts","sourceRoot":"","sources":["../../src/Tabs/TabLink.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAMtC,OAAO,KAAK,EACV,WAAW,EACX,eAAe,EAChB,MAAM,eAAe,CAAA;AAGtB,MAAM,MAAM,YAAY,GAAG;IACzB,SAAS,EAAE,OAAO,CAAA;IAClB,IAAI,EAAE,WAAW,CAAA;IACjB,WAAW,EAAE,eAAe,CAAA;CAC7B,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,OAAO,GAAI,mCAIrB,YAAY,KAAG,SAoFjB,CAAA"}
@@ -0,0 +1,66 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useRouterLink } from "../RouterLink/RouterLinkProvider.js";
3
+ import { getIsRoutedHref } from "../RouterLink/routerLink.js";
4
+ import { toClassName } from "../toClassName.js";
5
+ import { VisuallyHidden } from "../VisuallyHidden/VisuallyHidden.js";
6
+ import { toTabTriggerClass } from "./tabStyles.js";
7
+ /**
8
+ * One routed tab — and it is a real `<a href>`, through the same
9
+ * `RouterLinkProvider` seam `NavLink` uses.
10
+ *
11
+ * ### It carries no `role="tab"`, and that is the whole point
12
+ *
13
+ * `role="tab"` on an anchor **overrides** the link role. A screen
14
+ * reader then announces "tab", the reader expects a panel to appear
15
+ * beside it, and instead the address changes and the page under it
16
+ * is replaced. The APG tab pattern describes a disclosure inside one
17
+ * document; it does not describe navigation, and `aria-controls`
18
+ * has nothing to point at here because the section's content comes
19
+ * from the router — an `<Outlet />` the app renders somewhere this
20
+ * component cannot see.
21
+ *
22
+ * So a routed bar is what it actually is: a list of links inside a
23
+ * named `<nav>`, with `aria-current="page"` on the one you are on.
24
+ * That is `Nav`'s answer, and reusing it rather than restating it is
25
+ * why `resolveActiveKey` decides the current item.
26
+ *
27
+ * What the tab pattern was *really* supplying here was the paint,
28
+ * and the paint is shared through `toTabTriggerClass` — so this
29
+ * looks identical to the `<button>` beside it in Storybook and is
30
+ * announced correctly.
31
+ */
32
+ export const TabLink = ({ isCurrent, item, orientation, }) => {
33
+ const RouterLink = useRouterLink();
34
+ const { href, icon, isDisabled = false, label } = item;
35
+ const isExternal = item.isExternal === true;
36
+ const className = toTabTriggerClass({
37
+ isDisabled,
38
+ isSelected: isCurrent,
39
+ orientation,
40
+ });
41
+ const content = (_jsxs(_Fragment, { children: [icon === undefined ? null : (_jsx("span", { "aria-hidden": "true", className: "flex shrink-0 text-content-secondary", children: icon })), label, isExternal ? (_jsxs(VisuallyHidden, { children: [" ", "(opens in a new tab)"] })) : null] }));
42
+ if (isDisabled) {
43
+ // Not a link at all, rather than a link with a `disabled`
44
+ // attribute an anchor does not have. `aria-disabled` alone on a
45
+ // real `<a href>` still navigates on Enter, which is the version
46
+ // of this that looks right and is not.
47
+ return (_jsx("span", { "aria-disabled": "true", className: toClassName("inline-flex shrink-0 items-center gap-2", className), children: content }));
48
+ }
49
+ const LinkElement = isExternal || !getIsRoutedHref(href) ? "a" : RouterLink;
50
+ return (_jsx(LinkElement
51
+ // The platform's own "you are here", and the reason the paint
52
+ // is never the only signal: an underline is invisible to a
53
+ // screen reader and to anyone who cannot tell the two colours
54
+ // apart.
55
+ , { "aria-current": isCurrent ? "page" : undefined, className: toClassName("inline-flex shrink-0 items-center gap-2 no-underline", className), href: href,
56
+ // No `onClick`, deliberately — unlike `NavLink`, which has a
57
+ // panel to close behind the reader. A routed tab bar stays on
58
+ // screen across the navigation, so there is nothing to react
59
+ // to and a handler here would only be something to keep.
60
+ //
61
+ // `noreferrer` beside `noopener` on purpose: one closes the
62
+ // `window.opener` hole, the other stops this app's address
63
+ // leaking as the referrer to a product it does not own.
64
+ rel: isExternal ? "noopener noreferrer" : undefined, target: isExternal ? "_blank" : undefined, children: content }));
65
+ };
66
+ //# sourceMappingURL=TabLink.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TabLink.js","sourceRoot":"","sources":["../../src/Tabs/TabLink.tsx"],"names":[],"mappings":";AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,qCAAsC,CAAA;AACpE,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAA;AAC7D,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC/C,OAAO,EAAE,cAAc,EAAE,MAAM,qCAAsC,CAAA;AAKrE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAQlD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,EACtB,SAAS,EACT,IAAI,EACJ,WAAW,GACE,EAAa,EAAE;IAC5B,MAAM,UAAU,GAAG,aAAa,EAAE,CAAA;IAElC,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,UAAU,GAAG,KAAK,EAAE,KAAK,EAAE,GAAG,IAAI,CAAA;IAEtD,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,KAAK,IAAI,CAAA;IAE3C,MAAM,SAAS,GAAG,iBAAiB,CAAC;QAClC,UAAU;QACV,UAAU,EAAE,SAAS;QACrB,WAAW;KACZ,CAAC,CAAA;IAEF,MAAM,OAAO,GAAG,CACd,8BACG,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAG3B,8BACc,MAAM,EAClB,SAAS,EAAC,sCAAsC,YAE/C,IAAI,GACA,CACR,EAEA,KAAK,EAEL,UAAU,CAAC,CAAC,CAAC,CACZ,MAAC,cAAc,eACZ,GAAG,4BAEW,CAClB,CAAC,CAAC,CAAC,IAAI,IACP,CACJ,CAAA;IAED,IAAI,UAAU,EAAE,CAAC;QACf,0DAA0D;QAC1D,gEAAgE;QAChE,iEAAiE;QACjE,uCAAuC;QACvC,OAAO,CACL,gCACgB,MAAM,EACpB,SAAS,EAAE,WAAW,CACpB,yCAAyC,EACzC,SAAS,CACV,YAEA,OAAO,GACH,CACR,CAAA;IACH,CAAC;IAED,MAAM,WAAW,GACf,UAAU,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,UAAU,CAAA;IAEzD,OAAO,CACL,KAAC,WAAW;IACV,8DAA8D;IAC9D,2DAA2D;IAC3D,8DAA8D;IAC9D,SAAS;wBACK,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,EAC5C,SAAS,EAAE,WAAW,CACpB,sDAAsD,EACtD,SAAS,CACV,EACD,IAAI,EAAE,IAAI;QACV,6DAA6D;QAC7D,8DAA8D;QAC9D,6DAA6D;QAC7D,yDAAyD;QACzD,EAAE;QACF,4DAA4D;QAC5D,2DAA2D;QAC3D,wDAAwD;QACxD,GAAG,EAAE,UAAU,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,SAAS,EACnD,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,YAExC,OAAO,GACI,CACf,CAAA;AACH,CAAC,CAAA"}
@@ -1,5 +1,5 @@
1
1
  import type { ReactNode } from "react";
2
- import type { TabItem, TabsOrientation } from "./Tabs.tsx";
2
+ import type { TabItem, TabsOrientation } from "./tabItems.ts";
3
3
  export type TabTriggerProps = {
4
4
  id: string;
5
5
  isSelected: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"TabTrigger.d.ts","sourceRoot":"","sources":["../../src/Tabs/TabTrigger.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAKtC,OAAO,KAAK,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAyB1D,MAAM,MAAM,eAAe,GAAG;IAC5B,EAAE,EAAE,MAAM,CAAA;IACV,UAAU,EAAE,OAAO,CAAA;IACnB,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;IAC/B,WAAW,EAAE,eAAe,CAAA;IAC5B,OAAO,EAAE,MAAM,CAAA;IACf,kEAAkE;IAClE,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,IAAI,CAAA;IAC5C,0DAA0D;IAC1D,iBAAiB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,IAAI,CAAA;IAChD,GAAG,EAAE,OAAO,CAAA;IACZ,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,CACZ,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,iBAAiB,GAAG,IAAI,KAC9B,IAAI,CAAA;CACV,CAAA;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,UAAU,GAAI,oHAWxB,eAAe,KAAG,SAoDpB,CAAA"}
1
+ {"version":3,"file":"TabTrigger.d.ts","sourceRoot":"","sources":["../../src/Tabs/TabTrigger.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAItC,OAAO,KAAK,EACV,OAAO,EACP,eAAe,EAChB,MAAM,eAAe,CAAA;AAGtB,MAAM,MAAM,eAAe,GAAG;IAC5B,EAAE,EAAE,MAAM,CAAA;IACV,UAAU,EAAE,OAAO,CAAA;IACnB,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;IAC/B,WAAW,EAAE,eAAe,CAAA;IAC5B,OAAO,EAAE,MAAM,CAAA;IACf,kEAAkE;IAClE,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,IAAI,CAAA;IAC5C,0DAA0D;IAC1D,iBAAiB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,IAAI,CAAA;IAChD,GAAG,EAAE,OAAO,CAAA;IACZ,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,CACZ,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,iBAAiB,GAAG,IAAI,KAC9B,IAAI,CAAA;CACV,CAAA;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,UAAU,GAAI,oHAWxB,eAAe,KAAG,SA2CpB,CAAA"}
@@ -1,26 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useEffect } from "react";
3
- import { FOCUS_RING_CLASS } from "../intentStyles.js";
4
3
  import { toClassName } from "../toClassName.js";
5
- /**
6
- * The selected marker runs along the edge the tab list itself
7
- * sits on — under a horizontal bar, down the inline-end of a
8
- * vertical rail — and the negative margin pulls it over the
9
- * list's own border so the two read as one line rather than two.
10
- *
11
- * Logical properties throughout, which is the house rule and also
12
- * the only way a vertical rail lands on the correct side in RTL.
13
- */
14
- const ORIENTATION_EDGE_CLASS = {
15
- horizontal: {
16
- base: "-mb-px border-b-2",
17
- selected: "border-b-intent-accent-solid",
18
- },
19
- vertical: {
20
- base: "-me-px border-e-2 text-start",
21
- selected: "border-e-intent-accent-solid",
22
- },
23
- };
4
+ import { toTabTriggerClass } from "./tabStyles.js";
24
5
  /**
25
6
  * Its own file for a reason that is not style: **both
26
7
  * registrations are effects**, and an effect cannot run in a loop.
@@ -37,7 +18,6 @@ const ORIENTATION_EDGE_CLASS = {
37
18
  */
38
19
  export const TabTrigger = ({ id, isSelected, onSelect, orientation, panelId, registerFocus, registerSelection, tab, tabIndex, trackElement, }) => {
39
20
  const { isDisabled = false, key, label } = tab;
40
- const edge = ORIENTATION_EDGE_CLASS[orientation];
41
21
  useEffect(() => registerSelection(key), [key, registerSelection]);
42
22
  useEffect(() => {
43
23
  if (isDisabled) {
@@ -45,15 +25,11 @@ export const TabTrigger = ({ id, isSelected, onSelect, orientation, panelId, reg
45
25
  }
46
26
  return registerFocus(key);
47
27
  }, [isDisabled, key, registerFocus]);
48
- return (_jsx("button", { "aria-controls": panelId, "aria-selected": isSelected, className: toClassName("cursor-pointer border-transparent px-3 py-2 font-medium text-sm whitespace-nowrap transition-colors duration-(--duration-fast) ease-standard", edge.base,
49
- // Two entries rather than one interpolated string:
50
- // `tailwindCandidates.test.ts` rejects a template literal
51
- // in a className outright, because Tailwind's scanner
52
- // cannot see a class that only exists at runtime.
53
- isSelected && edge.selected, isSelected
54
- ? "text-content-primary"
55
- : "text-content-secondary hover:text-content-primary", isDisabled &&
56
- "cursor-not-allowed text-content-disabled hover:text-content-disabled", FOCUS_RING_CLASS), disabled: isDisabled, id: id, onClick: () => {
28
+ return (_jsx("button", { "aria-controls": panelId, "aria-selected": isSelected, className: toClassName("cursor-pointer", toTabTriggerClass({
29
+ isDisabled,
30
+ isSelected,
31
+ orientation,
32
+ })), disabled: isDisabled, id: id, onClick: () => {
57
33
  onSelect(key);
58
34
  }, ref: (element) => {
59
35
  trackElement(key, element);
@@ -1 +1 @@
1
- {"version":3,"file":"TabTrigger.js","sourceRoot":"","sources":["../../src/Tabs/TabTrigger.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEjC,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAG/C;;;;;;;;GAQG;AACH,MAAM,sBAAsB,GAGxB;IACF,UAAU,EAAE;QACV,IAAI,EAAE,mBAAmB;QACzB,QAAQ,EAAE,8BAA8B;KACzC;IACD,QAAQ,EAAE;QACR,IAAI,EAAE,8BAA8B;QACpC,QAAQ,EAAE,8BAA8B;KACzC;CACF,CAAA;AAoBD;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,EACzB,EAAE,EACF,UAAU,EACV,QAAQ,EACR,WAAW,EACX,OAAO,EACP,aAAa,EACb,iBAAiB,EACjB,GAAG,EACH,QAAQ,EACR,YAAY,GACI,EAAa,EAAE;IAC/B,MAAM,EAAE,UAAU,GAAG,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,GAAG,CAAA;IAE9C,MAAM,IAAI,GAAG,sBAAsB,CAAC,WAAW,CAAC,CAAA;IAEhD,SAAS,CACP,GAAG,EAAE,CAAC,iBAAiB,CAAC,GAAG,CAAC,EAC5B,CAAC,GAAG,EAAE,iBAAiB,CAAC,CACzB,CAAA;IAED,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,UAAU,EAAE,CAAC;YACf,OAAM;QACR,CAAC;QAED,OAAO,aAAa,CAAC,GAAG,CAAC,CAAA;IAC3B,CAAC,EAAE,CAAC,UAAU,EAAE,GAAG,EAAE,aAAa,CAAC,CAAC,CAAA;IAEpC,OAAO,CACL,kCACiB,OAAO,mBACP,UAAU,EACzB,SAAS,EAAE,WAAW,CACpB,8IAA8I,EAC9I,IAAI,CAAC,IAAI;QACT,mDAAmD;QACnD,0DAA0D;QAC1D,sDAAsD;QACtD,kDAAkD;QAClD,UAAU,IAAI,IAAI,CAAC,QAAQ,EAC3B,UAAU;YACR,CAAC,CAAC,sBAAsB;YACxB,CAAC,CAAC,mDAAmD,EACvD,UAAU;YACR,sEAAsE,EACxE,gBAAgB,CACjB,EACD,QAAQ,EAAE,UAAU,EACpB,EAAE,EAAE,EAAE,EACN,OAAO,EAAE,GAAG,EAAE;YACZ,QAAQ,CAAC,GAAG,CAAC,CAAA;QACf,CAAC,EACD,GAAG,EAAE,CAAC,OAAO,EAAE,EAAE;YACf,YAAY,CAAC,GAAG,EAAE,OAAO,CAAC,CAAA;QAC5B,CAAC,EACD,IAAI,EAAC,KAAK,EACV,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAC,QAAQ,YAEZ,KAAK,GACC,CACV,CAAA;AACH,CAAC,CAAA"}
1
+ {"version":3,"file":"TabTrigger.js","sourceRoot":"","sources":["../../src/Tabs/TabTrigger.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEjC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAK/C,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAoBlD;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,EACzB,EAAE,EACF,UAAU,EACV,QAAQ,EACR,WAAW,EACX,OAAO,EACP,aAAa,EACb,iBAAiB,EACjB,GAAG,EACH,QAAQ,EACR,YAAY,GACI,EAAa,EAAE;IAC/B,MAAM,EAAE,UAAU,GAAG,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,GAAG,CAAA;IAE9C,SAAS,CACP,GAAG,EAAE,CAAC,iBAAiB,CAAC,GAAG,CAAC,EAC5B,CAAC,GAAG,EAAE,iBAAiB,CAAC,CACzB,CAAA;IAED,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,UAAU,EAAE,CAAC;YACf,OAAM;QACR,CAAC;QAED,OAAO,aAAa,CAAC,GAAG,CAAC,CAAA;IAC3B,CAAC,EAAE,CAAC,UAAU,EAAE,GAAG,EAAE,aAAa,CAAC,CAAC,CAAA;IAEpC,OAAO,CACL,kCACiB,OAAO,mBACP,UAAU,EACzB,SAAS,EAAE,WAAW,CACpB,gBAAgB,EAChB,iBAAiB,CAAC;YAChB,UAAU;YACV,UAAU;YACV,WAAW;SACZ,CAAC,CACH,EACD,QAAQ,EAAE,UAAU,EACpB,EAAE,EAAE,EAAE,EACN,OAAO,EAAE,GAAG,EAAE;YACZ,QAAQ,CAAC,GAAG,CAAC,CAAA;QACf,CAAC,EACD,GAAG,EAAE,CAAC,OAAO,EAAE,EAAE;YACf,YAAY,CAAC,GAAG,EAAE,OAAO,CAAC,CAAA;QAC5B,CAAC,EACD,IAAI,EAAC,KAAK,EACV,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAC,QAAQ,YAEZ,KAAK,GACC,CACV,CAAA;AACH,CAAC,CAAA"}
@@ -1,19 +1,12 @@
1
1
  import type { ReactNode } from "react";
2
- export type TabsActivation = "automatic" | "manual";
3
- export type TabsOrientation = "horizontal" | "vertical";
4
- export type TabItem = {
5
- content: ReactNode;
6
- /**
7
- * A disabled tab is simply **not registered** with the roving
8
- * group. Registration is membership, so the arrow keys skip it
9
- * without any command in `RovingFocus` having to know what
10
- * "disabled" means.
11
- */
12
- isDisabled?: boolean;
13
- key: string;
14
- label: ReactNode;
2
+ import type { TabItem, TabLinkItem, TabsActivation, TabsOrientation } from "./tabItems.ts";
3
+ type TabsCommonProps = {
4
+ className?: string;
5
+ /** The tab bar's accessible name. Required. */
6
+ label: string;
7
+ orientation?: TabsOrientation;
15
8
  };
16
- export type TabsProps = {
9
+ export type TabsPanelProps = TabsCommonProps & {
17
10
  /** **Initial** only. Charcuterie owns it from then on. */
18
11
  activeKey?: string;
19
12
  /**
@@ -28,68 +21,91 @@ export type TabsProps = {
28
21
  * composed rather than one kind doing both jobs badly.
29
22
  */
30
23
  activation?: TabsActivation;
31
- className?: string;
32
- /** The tablist's accessible name. Required. */
33
- label: string;
34
24
  onChange?: (activeKey: string | null) => void;
35
- orientation?: TabsOrientation;
36
- tabs: TabItem[];
25
+ tabs: readonly TabItem[];
26
+ };
27
+ export type TabsLinkProps = TabsCommonProps & {
28
+ /**
29
+ * The address the app is at — usually `location.pathname`. The tab
30
+ * whose `href` matches wears `aria-current="page"`.
31
+ *
32
+ * **Required, and it is the discriminant.** `Nav` leaves the
33
+ * equivalent optional because a top-level bar is honest about
34
+ * naming nothing on a screen it does not contain. A routed tab bar
35
+ * is drawn *inside* the section it divides, so "none of these" is
36
+ * not a state it has; an app that wants it passes a path that
37
+ * matches no tab.
38
+ */
39
+ activeHref: string;
40
+ tabs: readonly TabLinkItem[];
37
41
  };
38
42
  /**
39
- * Kept at P0 as the **state layer's falsification test**, not on
40
- * duplication grounds — the fifteen ad-hoc tab bars that used to
41
- * justify it were in the withdrawn evidence. What it has to prove
42
- * is that two kinds compose: one says which tab is **chosen**, the
43
- * other says which tab is **tabbable**, and a tab bar needs both
44
- * at once *and* needs them to disagree on purpose while a user
45
- * arrows around in `manual` mode.
46
- *
47
- * The composition is two hooks and one decision:
43
+ * The union splits on `activeHref`, which only the routed member
44
+ * has. `tabs` alone could not carry the split — TypeScript does not
45
+ * narrow a union by the shape of an array's elements, so a mixed bar
46
+ * would type-check and then render half a navigation.
47
+ */
48
+ export type TabsProps = TabsLinkProps | TabsPanelProps;
49
+ /**
50
+ * One choice out of a handful, all of them on screen at once — and
51
+ * the choice is either a **panel** this component reveals or a
52
+ * **place** the router goes to.
48
53
  *
49
- * ```ts
50
- * const selection = useSinglePicker({ selectedValue: initialKey })
51
- * const focus = useRovingFocus({ activeValue: initialKey })
54
+ * ```tsx
55
+ * // A section of this screen. Nothing outside knows about it.
56
+ * <Tabs label="Bay 3" tabs={[{ key: "log", label: "Log", content: … }]} />
52
57
  *
53
- * // `automatic` is this line, and `manual` is its absence.
54
- * if (activation === "automatic") selection.select(focus.activeValue)
58
+ * // A section that has an address.
59
+ * <Tabs
60
+ * activeHref={pathname}
61
+ * label="Bay 3 sections"
62
+ * tabs={[{ href: "/projects/1/tasks", label: "Tasks" }]}
63
+ * />
64
+ * <Outlet />
55
65
  * ```
56
66
  *
57
- * ### Selection, not visibility — and the difference is not cosmetic
67
+ * ## Which one, and the question is not "how does it look"
68
+ *
69
+ * **Does this section have an address?** If reloading the page, or
70
+ * opening it in a second tab, or pressing Back has to land the
71
+ * reader back on the same section, it is routed and every tab is an
72
+ * `<a href>`. If the section is a disclosure inside one screen —
73
+ * Settings' groups, a card's detail — it is a panel and every tab is
74
+ * a `<button>`.
58
75
  *
59
- * M4 built this on `VisibilityGroup`, and the panels do end up
60
- * one-at-a-time either way, because the two cores are the same
61
- * shape. But the thing that **registers** here is the tab, not the
62
- * panel, and what a tab bar is asking is *which one did you pick*.
63
- * `SinglePicker` is the kind that answers that; the panel's
64
- * `hidden` is derived from the answer.
76
+ * The two are drawn identically on purpose, through
77
+ * `toTabTriggerClass`. Docket had one of each on adjacent screens
78
+ * wearing different paint, because the routed one had to be built
79
+ * out of `Nav` and `Nav`'s current item is a filled pill
80
+ * ([decision](../../../../docs/decisions/2026-08-31-a-routed-tab-is-a-tab-with-an-href.md)).
65
81
  *
66
- * Reading it the other way round cost something concrete: it made
67
- * `aria-selected` a report about a *panel's* visibility, so a
68
- * consumer wanting a tab bar with no panels at all — a segmented
69
- * filter, a view switcher that swaps a route — had to model its
70
- * choice as a group of visibilities it did not have. `Alert`'s
71
- * sibling `SegmentedControl` is exactly that consumer, and it now
72
- * shares this component's core rather than hand-rolling a fourth
73
- * one.
74
- * [Decision](../../../../docs/decisions/2026-07-30-tab-selection-is-a-single-picker.md).
82
+ * ## `Nav` is still the app's destinations
75
83
  *
76
- * ### What it does not use, and why that is the right answer
84
+ * This is **sub**-navigation: the sections of the screen you are
85
+ * already on. The app's own destinations — the header row, the side
86
+ * rail, the narrow view's menu — are `Nav` + `useNavLayout`, and
87
+ * that has not moved
88
+ * ([decision](../../../../docs/decisions/2026-08-27-nav-is-the-one-navigation-component-and-navbar-is-deprecated.md)).
89
+ * A routed `Tabs` renders no rail, no fold and no menu, because a
90
+ * bar dividing one screen has nowhere to fold to.
77
91
  *
78
- * The plan expected `createLinkedIds` here as well. It does not
79
- * fit, and forcing it would have been the wrong call: that kind
80
- * exists for a **dynamic** trigger↔target pair — a `Popover` whose
81
- * panel mounts and unmounts, where the multiset is what stops
82
- * `aria-controls` pointing at a node React has already removed. A
83
- * tab bar's pairing is static and known at render, so the ids come
84
- * from one `useUniqueId` and cannot get out of step at all.
92
+ * ## The routed bar is not a `tablist`, and that is deliberate
85
93
  *
86
- * ### Focus is moved deliberately, and only then
94
+ * `role="tab"` on an anchor overrides the link role, so a screen
95
+ * reader announces a disclosure and the address changes instead. See
96
+ * `TabLink`. The routed bar is a `<nav>` of links with
97
+ * `aria-current="page"`; the panel bar below keeps the full APG
98
+ * pattern, `aria-controls` and all.
87
99
  *
88
- * `RovingFocus` is state; something still has to call `.focus()`.
89
- * That happens only while the tablist already contains the focused
90
- * element — otherwise mounting a tab bar, or any parent re-render
91
- * that moved `activeValue`, yanks the caret out of whatever the
92
- * user was actually doing.
100
+ * ## The panel bar: kept at P0 as the state layer's falsification test
101
+ *
102
+ * Not on duplication grounds — the fifteen ad-hoc tab bars that used
103
+ * to justify it were in the withdrawn evidence. What it has to prove
104
+ * is that two kinds compose: one says which tab is **chosen**, the
105
+ * other says which tab is **tabbable**, and a tab bar needs both at
106
+ * once *and* needs them to disagree on purpose while a user arrows
107
+ * around in `manual` mode. `PanelTabs` is where that lives.
93
108
  */
94
- export declare const Tabs: ({ activation, activeKey, className, label, onChange, orientation, tabs, }: TabsProps) => ReactNode;
109
+ export declare const Tabs: (props: TabsProps) => ReactNode;
110
+ export {};
95
111
  //# sourceMappingURL=Tabs.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Tabs.d.ts","sourceRoot":"","sources":["../../src/Tabs/Tabs.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAMtC,MAAM,MAAM,cAAc,GAAG,WAAW,GAAG,QAAQ,CAAA;AAEnD,MAAM,MAAM,eAAe,GAAG,YAAY,GAAG,UAAU,CAAA;AAEvD,MAAM,MAAM,OAAO,GAAG;IACpB,OAAO,EAAE,SAAS,CAAA;IAClB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,SAAS,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,SAAS,GAAG;IACtB,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;;;;;;;OAUG;IACH,UAAU,CAAC,EAAE,cAAc,CAAA;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,+CAA+C;IAC/C,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IAC7C,WAAW,CAAC,EAAE,eAAe,CAAA;IAC7B,IAAI,EAAE,OAAO,EAAE,CAAA;CAChB,CAAA;AAaD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AACH,eAAO,MAAM,IAAI,GAAI,2EAQlB,SAAS,KAAG,SA6Md,CAAA"}
1
+ {"version":3,"file":"Tabs.d.ts","sourceRoot":"","sources":["../../src/Tabs/Tabs.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAItC,OAAO,KAAK,EACV,OAAO,EACP,WAAW,EACX,cAAc,EACd,eAAe,EAChB,MAAM,eAAe,CAAA;AAEtB,KAAK,eAAe,GAAG;IACrB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,+CAA+C;IAC/C,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,eAAe,CAAA;CAC9B,CAAA;AAED,MAAM,MAAM,cAAc,GAAG,eAAe,GAAG;IAC7C,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;;;;;;;OAUG;IACH,UAAU,CAAC,EAAE,cAAc,CAAA;IAC3B,QAAQ,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IAC7C,IAAI,EAAE,SAAS,OAAO,EAAE,CAAA;CACzB,CAAA;AAED,MAAM,MAAM,aAAa,GAAG,eAAe,GAAG;IAC5C;;;;;;;;;;OAUG;IACH,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,EAAE,SAAS,WAAW,EAAE,CAAA;CAC7B,CAAA;AAED;;;;;GAKG;AACH,MAAM,MAAM,SAAS,GAAG,aAAa,GAAG,cAAc,CAAA;AAMtD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2DG;AACH,eAAO,MAAM,IAAI,GAAI,OAAO,SAAS,KAAG,SAKrC,CAAA"}