@charcuterie/ui 2.11.0 → 2.12.1

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 (36) hide show
  1. package/dist/Toolbar/Toolbar.d.ts +156 -0
  2. package/dist/Toolbar/Toolbar.d.ts.map +1 -0
  3. package/dist/Toolbar/Toolbar.js +169 -0
  4. package/dist/Toolbar/Toolbar.js.map +1 -0
  5. package/dist/Toolbar/ToolbarSlot.d.ts +44 -0
  6. package/dist/Toolbar/ToolbarSlot.d.ts.map +1 -0
  7. package/dist/Toolbar/ToolbarSlot.js +69 -0
  8. package/dist/Toolbar/ToolbarSlot.js.map +1 -0
  9. package/dist/Toolbar/chooseVisibleCount.d.ts +38 -0
  10. package/dist/Toolbar/chooseVisibleCount.d.ts.map +1 -0
  11. package/dist/Toolbar/chooseVisibleCount.js +69 -0
  12. package/dist/Toolbar/chooseVisibleCount.js.map +1 -0
  13. package/dist/Toolbar/useToolbarOverflow.d.ts +49 -0
  14. package/dist/Toolbar/useToolbarOverflow.d.ts.map +1 -0
  15. package/dist/Toolbar/useToolbarOverflow.js +157 -0
  16. package/dist/Toolbar/useToolbarOverflow.js.map +1 -0
  17. package/dist/index.d.ts +3 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +2 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/testing/expectAgentDrivable.d.ts.map +1 -1
  22. package/dist/testing/expectAgentDrivable.js +32 -7
  23. package/dist/testing/expectAgentDrivable.js.map +1 -1
  24. package/package.json +2 -2
  25. package/src/Toolbar/Toolbar.mdx +126 -0
  26. package/src/Toolbar/Toolbar.stories.tsx +347 -0
  27. package/src/Toolbar/Toolbar.test.tsx +286 -0
  28. package/src/Toolbar/Toolbar.tsx +461 -0
  29. package/src/Toolbar/ToolbarSlot.tsx +122 -0
  30. package/src/Toolbar/chooseVisibleCount.test.ts +92 -0
  31. package/src/Toolbar/chooseVisibleCount.ts +92 -0
  32. package/src/Toolbar/useToolbarOverflow.ts +229 -0
  33. package/src/icons.storyHelpers.tsx +23 -0
  34. package/src/index.ts +8 -0
  35. package/src/sourceRules.test.ts +18 -1
  36. package/src/testing/expectAgentDrivable.ts +50 -20
@@ -0,0 +1,156 @@
1
+ import type { ControlSize } from "@charcuterie/tokens";
2
+ import type { ReactElement, ReactNode } from "react";
3
+ /**
4
+ * Something the toolbar renders itself: a button that **does**
5
+ * something. The same shape as `MenuItem`, because a collapsed one
6
+ * becomes exactly that.
7
+ */
8
+ export type ToolbarAction = {
9
+ icon?: ReactNode;
10
+ isDisabled?: boolean;
11
+ key: string;
12
+ /** The accessible name, and the label when it collapses. */
13
+ label: string;
14
+ onSelect: () => void;
15
+ /**
16
+ * The default kind, and optional so the common
17
+ * `{ key, label, onSelect }` still type-checks. Present only to
18
+ * discriminate this from a `control`.
19
+ */
20
+ type?: "action";
21
+ };
22
+ /**
23
+ * Something the **app** renders: a `Switch`, a `SegmentedControl`,
24
+ * a `ColorSchemeSwitcher` — a control with state, not an action.
25
+ *
26
+ * Exactly one instance of `element` is mounted at any width. It
27
+ * moves between the bar and the overflow panel; it is never
28
+ * rendered in both and hidden in one.
29
+ */
30
+ export type ToolbarControl = {
31
+ element: ReactElement;
32
+ key: string;
33
+ /**
34
+ * Optional row label, used **only** in the overflow panel and
35
+ * only when the control does not already show its own text. A
36
+ * `Switch` renders its label; an icon-only toggle does not.
37
+ */
38
+ label?: string;
39
+ type: "control";
40
+ };
41
+ export type ToolbarItem = ToolbarAction | ToolbarControl;
42
+ type ToolbarSharedProps = {
43
+ className?: string;
44
+ /**
45
+ * **Initial** only, and the toolbar owns it from then on — the
46
+ * same uncontrolled contract as every other component here. It
47
+ * exists so a story (and a screenshot) can show the panel open;
48
+ * an app almost never passes it.
49
+ */
50
+ isOverflowVisible?: boolean;
51
+ /**
52
+ * Required, and it becomes `aria-label`. A `role="toolbar"` takes
53
+ * **no accessible name from its content** — the same trap
54
+ * `Popover`'s `heading` and `Spinner`'s label exist for. mux-magic
55
+ * gets this one right and then gives the toolbar nothing else.
56
+ */
57
+ label: string;
58
+ /**
59
+ * The overflow trigger's glyph. **No default** — the library
60
+ * ships no icons, and `⋮` renders as nothing where the font lacks
61
+ * it. With no icon the trigger is a text button reading
62
+ * `overflowLabel`, which works everywhere.
63
+ */
64
+ overflowIcon?: ReactNode;
65
+ /**
66
+ * Names the overflow trigger, and through it the panel it opens —
67
+ * a menu is named by its trigger, and `Popover`'s `aria-label`
68
+ * takes the same string.
69
+ */
70
+ overflowLabel?: string;
71
+ size?: ControlSize;
72
+ };
73
+ /**
74
+ * **The overflow's role is a type, not a flag.**
75
+ *
76
+ * `role="menu"` permits only `menuitem`, `menuitemradio`,
77
+ * `menuitemcheckbox`, `group` and `separator` — so a header's
78
+ * scheme switcher inside one is invalid, which is what
79
+ * plex-channels ships today. The fix is not a runtime warning and
80
+ * not a boolean: with `overflow="menu"` the item type narrows to
81
+ * `ToolbarAction`, and a `control` in the array is a **compile
82
+ * error**. Mixed content declares `overflow="panel"` and gets a
83
+ * `Popover` — `role="dialog"`, `aria-haspopup="dialog"` — where a
84
+ * switch is perfectly legal content.
85
+ *
86
+ * Same instinct as
87
+ * [`Menu`'s items being a discriminated union](../../../../docs/decisions/2026-08-05-menu-items-is-a-discriminated-union.md):
88
+ * the kinds are told apart by the type system, in the data.
89
+ */
90
+ export type ToolbarProps = ToolbarSharedProps & ({
91
+ items: readonly ToolbarAction[];
92
+ overflow: "menu";
93
+ } | {
94
+ items: readonly ToolbarItem[];
95
+ overflow: "panel";
96
+ });
97
+ /**
98
+ * A row of controls that collapses into an overflow when it runs
99
+ * out of room — the fleet's fourth-largest duplication, and the one
100
+ * where the copies are worst.
101
+ *
102
+ * Four repos have a toolbar-with-overflow; mux-magic's `PageHeader`
103
+ * is the original and plex-channels' `Header` says in its own
104
+ * comments that it copied it. What they copied:
105
+ *
106
+ * - **`role="toolbar"` with no toolbar keyboard behaviour.** Six
107
+ * sequential tab stops where the APG requires one, with the
108
+ * arrow keys moving between members. Here that is `RovingFocus`,
109
+ * the same kind `Menu`, `Tabs` and `SegmentedControl` use, and
110
+ * the tab stop is read from `selectTabIndex` rather than
111
+ * restated.
112
+ * - **Triggers that advertise nothing.** No `aria-expanded`, no
113
+ * `aria-haspopup`, no `aria-controls` anywhere in the file. Here
114
+ * they come from `useRole` inside `Menu`/`Popover`, which cannot
115
+ * be forgotten because nothing writes them by hand.
116
+ * - **No focus management.** `Menu` moves focus to the first item
117
+ * on opening and `FloatingFocusManager` returns it to the
118
+ * trigger on close.
119
+ * - **Collapse by DOM duplication.** Both repos render every
120
+ * action twice and hide one set with a media query — mux-magic's
121
+ * is a documented CSS specificity coin-flip ("source order
122
+ * wins"). Every action here is mounted **exactly once** and
123
+ * moves.
124
+ * - **Nothing decides what collapses.** Two hardcoded media
125
+ * queries at 480/481px, and the ⋮ shows at every width, so it is
126
+ * not progressive collapse at all. Here the row is measured, the
127
+ * items are in priority order, and the trigger exists only when
128
+ * something actually overflowed.
129
+ *
130
+ * The toggles' missing `aria-pressed` is the one defect this
131
+ * component does not fix, because it cannot: a `control`'s state is
132
+ * the control's own, and `Switch` has carried `role="switch"` since
133
+ * M6a. The bar's job is to hold it once.
134
+ *
135
+ * ## Priority is the array order
136
+ *
137
+ * `items[0]` is the last thing to collapse. One axis, and it is
138
+ * also paint order — see `chooseVisibleCount`.
139
+ *
140
+ * ## Measured, not breakpointed
141
+ *
142
+ * There is no `collapseAt` prop and no media query. The bar reads
143
+ * the widths its items actually have, so a longer label or a
144
+ * `data-density="kiosk"` collapses at the width it should rather
145
+ * than at a number someone typed in 2024. `useMediaQuery` is still
146
+ * the right tool one level up — for **relocating** a whole toolbar
147
+ * out of a header on a phone, which is a layout decision and not a
148
+ * fit — and `Toolbar.stories.tsx` shows that composition.
149
+ *
150
+ * ⚠️ The root is `flex-1 min-w-0`: it must be sized by its
151
+ * container, never by its own contents, or collapsing an item
152
+ * narrows the box that decides whether to collapse.
153
+ */
154
+ export declare const Toolbar: ({ className, isOverflowVisible, items, label, overflow, overflowIcon, overflowLabel, size, }: ToolbarProps) => ReactNode;
155
+ export {};
156
+ //# sourceMappingURL=Toolbar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Toolbar.d.ts","sourceRoot":"","sources":["../../src/Toolbar/Toolbar.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAWpD;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,GAAG,EAAE,MAAM,CAAA;IACX,4DAA4D;IAC5D,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,IAAI,CAAA;IACpB;;;;OAIG;IACH,IAAI,CAAC,EAAE,QAAQ,CAAA;CAChB,CAAA;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,OAAO,EAAE,YAAY,CAAA;IACrB,GAAG,EAAE,MAAM,CAAA;IACX;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,SAAS,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,WAAW,GAAG,aAAa,GAAG,cAAc,CAAA;AAExD,KAAK,kBAAkB,GAAG;IACxB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B;;;;;OAKG;IACH,KAAK,EAAE,MAAM,CAAA;IACb;;;;;OAKG;IACH,YAAY,CAAC,EAAE,SAAS,CAAA;IACxB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,IAAI,CAAC,EAAE,WAAW,CAAA;CACnB,CAAA;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,YAAY,GAAG,kBAAkB,GAC3C,CACI;IACE,KAAK,EAAE,SAAS,aAAa,EAAE,CAAA;IAC/B,QAAQ,EAAE,MAAM,CAAA;CACjB,GACD;IACE,KAAK,EAAE,SAAS,WAAW,EAAE,CAAA;IAC7B,QAAQ,EAAE,OAAO,CAAA;CAClB,CACJ,CAAA;AAaH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AACH,eAAO,MAAM,OAAO,GAAI,8FASrB,YAAY,KAAG,SAuQjB,CAAA"}
@@ -0,0 +1,169 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { selectTabIndex, useRovingFocus, useVisibility, } from "@charcuterie/logic";
3
+ import { useCallback, useEffect, useRef } from "react";
4
+ import { Button } from "../Button/Button.js";
5
+ import { IconButton } from "../IconButton/IconButton.js";
6
+ import { Menu } from "../Menu/Menu.js";
7
+ import { Popover } from "../Popover/Popover.js";
8
+ import { toClassName } from "../toClassName.js";
9
+ import { ToolbarSlot } from "./ToolbarSlot.js";
10
+ import { useToolbarOverflow } from "./useToolbarOverflow.js";
11
+ /**
12
+ * The roving group's last member. Not a key any item can collide
13
+ * with, because a caller's keys are its own strings and this one
14
+ * is namespaced.
15
+ */
16
+ const OVERFLOW_KEY = "charcuterie-toolbar-overflow";
17
+ const isToolbarAction = (item) => item.type !== "control";
18
+ /**
19
+ * A row of controls that collapses into an overflow when it runs
20
+ * out of room — the fleet's fourth-largest duplication, and the one
21
+ * where the copies are worst.
22
+ *
23
+ * Four repos have a toolbar-with-overflow; mux-magic's `PageHeader`
24
+ * is the original and plex-channels' `Header` says in its own
25
+ * comments that it copied it. What they copied:
26
+ *
27
+ * - **`role="toolbar"` with no toolbar keyboard behaviour.** Six
28
+ * sequential tab stops where the APG requires one, with the
29
+ * arrow keys moving between members. Here that is `RovingFocus`,
30
+ * the same kind `Menu`, `Tabs` and `SegmentedControl` use, and
31
+ * the tab stop is read from `selectTabIndex` rather than
32
+ * restated.
33
+ * - **Triggers that advertise nothing.** No `aria-expanded`, no
34
+ * `aria-haspopup`, no `aria-controls` anywhere in the file. Here
35
+ * they come from `useRole` inside `Menu`/`Popover`, which cannot
36
+ * be forgotten because nothing writes them by hand.
37
+ * - **No focus management.** `Menu` moves focus to the first item
38
+ * on opening and `FloatingFocusManager` returns it to the
39
+ * trigger on close.
40
+ * - **Collapse by DOM duplication.** Both repos render every
41
+ * action twice and hide one set with a media query — mux-magic's
42
+ * is a documented CSS specificity coin-flip ("source order
43
+ * wins"). Every action here is mounted **exactly once** and
44
+ * moves.
45
+ * - **Nothing decides what collapses.** Two hardcoded media
46
+ * queries at 480/481px, and the ⋮ shows at every width, so it is
47
+ * not progressive collapse at all. Here the row is measured, the
48
+ * items are in priority order, and the trigger exists only when
49
+ * something actually overflowed.
50
+ *
51
+ * The toggles' missing `aria-pressed` is the one defect this
52
+ * component does not fix, because it cannot: a `control`'s state is
53
+ * the control's own, and `Switch` has carried `role="switch"` since
54
+ * M6a. The bar's job is to hold it once.
55
+ *
56
+ * ## Priority is the array order
57
+ *
58
+ * `items[0]` is the last thing to collapse. One axis, and it is
59
+ * also paint order — see `chooseVisibleCount`.
60
+ *
61
+ * ## Measured, not breakpointed
62
+ *
63
+ * There is no `collapseAt` prop and no media query. The bar reads
64
+ * the widths its items actually have, so a longer label or a
65
+ * `data-density="kiosk"` collapses at the width it should rather
66
+ * than at a number someone typed in 2024. `useMediaQuery` is still
67
+ * the right tool one level up — for **relocating** a whole toolbar
68
+ * out of a header on a phone, which is a layout decision and not a
69
+ * fit — and `Toolbar.stories.tsx` shows that composition.
70
+ *
71
+ * ⚠️ The root is `flex-1 min-w-0`: it must be sized by its
72
+ * container, never by its own contents, or collapsing an item
73
+ * narrows the box that decides whether to collapse.
74
+ */
75
+ export const Toolbar = ({ className, isOverflowVisible = false, items, label, overflow, overflowIcon, overflowLabel = "More actions", size = "sm", }) => {
76
+ const focusableElements = useRef(new Map());
77
+ const focus = useRovingFocus();
78
+ const { activeValue, register, setActiveValue } = focus;
79
+ const itemKeys = items.map((item) => item.key);
80
+ const { containerRef, trackItem, trackTrigger, visibleCount, } = useToolbarOverflow({ itemKeys });
81
+ const visibleItems = items.slice(0, visibleCount);
82
+ const collapsedItems = items.slice(visibleCount);
83
+ const hasOverflow = collapsedItems.length > 0;
84
+ const { hide, isVisible, toggle } = useVisibility({
85
+ isVisible: isOverflowVisible,
86
+ });
87
+ const trackFocusable = useCallback((key, element) => {
88
+ if (element) {
89
+ focusableElements.current.set(key, element);
90
+ }
91
+ else {
92
+ focusableElements.current.delete(key);
93
+ }
94
+ }, []);
95
+ const trackOverflowElement = useCallback((_key, element) => {
96
+ trackTrigger(element);
97
+ }, [trackTrigger]);
98
+ /**
99
+ * Move the caret only when the toolbar already has it.
100
+ *
101
+ * Same guard as `SegmentedControl`: the active member changes
102
+ * whenever the row re-measures, and a bar that stole focus every
103
+ * time the window resized would be worse than one with no
104
+ * keyboard model at all.
105
+ */
106
+ useEffect(() => {
107
+ const container = containerRef.current;
108
+ if (!container ||
109
+ activeValue === null ||
110
+ !container.contains(document.activeElement)) {
111
+ return;
112
+ }
113
+ focusableElements.current.get(activeValue)?.focus();
114
+ }, [activeValue, containerRef]);
115
+ const renderAction = (action) => action.icon === undefined ? (_jsx(Button, { appearance: "ghost", intent: "neutral", isDisabled: action.isDisabled, onClick: action.onSelect, size: size, children: action.label })) : (_jsx(IconButton, { appearance: "ghost", intent: "neutral", isDisabled: action.isDisabled, label: action.label, onClick: action.onSelect, size: size, children: action.icon }));
116
+ const trigger = overflowIcon === undefined ? (_jsx(Button, { appearance: "ghost", intent: "neutral", onClick: toggle, size: size, children: overflowLabel })) : (_jsx(IconButton, { appearance: "ghost", intent: "neutral", label: overflowLabel, onClick: toggle, size: size, children: overflowIcon }));
117
+ const overflowElement = overflow === "menu" ? (_jsx(Menu, { isVisible: isVisible, items: collapsedItems
118
+ .filter(isToolbarAction)
119
+ .map((action) => ({
120
+ icon: action.icon,
121
+ isDisabled: action.isDisabled,
122
+ key: action.key,
123
+ label: action.label,
124
+ onSelect: action.onSelect,
125
+ })), onDismiss: hide, placement: "bottom-end", trigger: trigger })) : (_jsx(Popover, { heading: overflowLabel, isVisible: isVisible, onDismiss: hide, placement: "bottom-end", trigger: trigger, children: _jsx("div", { className: "flex min-w-48 flex-col gap-1", children: collapsedItems.map((item) => isToolbarAction(item) ? (_jsx(Button, { appearance: "ghost", className: "justify-start", iconStart: item.icon, intent: "neutral", isDisabled: item.isDisabled, isFullWidth: true, onClick: () => {
126
+ item.onSelect();
127
+ // Choosing dismisses, exactly as it does in a
128
+ // `Menu`. A panel that stays open after an action
129
+ // has fired is the one users read as "it didn't
130
+ // work".
131
+ hide();
132
+ }, size: size, children: item.label }, item.key)) : (_jsxs("div", { className: "flex items-center justify-between gap-3", children: [item.label === undefined ? null : (_jsx("span", { className: "text-content-secondary text-sm", children: item.label })), item.element] }, item.key))) }) }));
133
+ return (_jsxs("div", { "aria-label": label, className: toClassName("flex min-w-0 flex-1 items-center gap-1", className),
134
+ /**
135
+ * Focus arriving from **outside** the keyboard model — a
136
+ * click, or Tab landing on the one member in the tab order —
137
+ * becomes the active member, so the next arrow key continues
138
+ * from where the user actually is rather than from wherever
139
+ * the group was last left. React's `onFocus` is `focusin`, so
140
+ * this hears a descendant.
141
+ */
142
+ onFocus: (focusEvent) => {
143
+ for (const [key, element,] of focusableElements.current) {
144
+ if (element.contains(focusEvent.target)) {
145
+ setActiveValue(key);
146
+ return;
147
+ }
148
+ }
149
+ }, onKeyDown: (keyEvent) => {
150
+ const commands = {
151
+ ArrowLeft: focus.previous,
152
+ ArrowRight: focus.next,
153
+ End: focus.last,
154
+ Home: focus.first,
155
+ };
156
+ const command = commands[keyEvent.key];
157
+ if (command) {
158
+ // Arrow keys scroll the page by default, and a toolbar
159
+ // that scrolls the document while moving focus is the
160
+ // hand-rolled version of this component.
161
+ keyEvent.preventDefault();
162
+ command();
163
+ }
164
+ }, ref: containerRef, role: "toolbar", children: [visibleItems.map((item) => (_jsx(ToolbarSlot, { isDisabled: isToolbarAction(item) &&
165
+ item.isDisabled === true, itemKey: item.key, register: register, tabIndex: selectTabIndex(focus, item.key), trackElement: trackItem, trackFocusable: trackFocusable, children: isToolbarAction(item)
166
+ ? renderAction(item)
167
+ : item.element }, item.key))), hasOverflow ? (_jsx(ToolbarSlot, { itemKey: OVERFLOW_KEY, register: register, tabIndex: selectTabIndex(focus, OVERFLOW_KEY), trackElement: trackOverflowElement, trackFocusable: trackFocusable, children: overflowElement })) : null] }));
168
+ };
169
+ //# sourceMappingURL=Toolbar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Toolbar.js","sourceRoot":"","sources":["../../src/Toolbar/Toolbar.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,44 @@
1
+ import type { ReactNode } from "react";
2
+ export type ToolbarSlotProps = {
3
+ children: ReactNode;
4
+ /** A disabled member never registers, so the arrows skip it. */
5
+ isDisabled?: boolean;
6
+ itemKey: string;
7
+ /** `RovingFocus.register` — membership of the arrow-key group. */
8
+ register: (value: string) => () => void;
9
+ tabIndex: number;
10
+ /** Hands the slot's box to the measuring hook. */
11
+ trackElement: (key: string, element: HTMLElement | null) => void;
12
+ /** Hands the slot's focusable to the toolbar, for `.focus()`. */
13
+ trackFocusable: (key: string, element: HTMLElement | null) => void;
14
+ };
15
+ /**
16
+ * One member of the toolbar's roving group: an action the toolbar
17
+ * rendered, or a control the caller handed in.
18
+ *
19
+ * Its own file because **registration is an effect**, and an effect
20
+ * cannot run in a loop — the same reason `MenuAction`, `TabTrigger`
21
+ * and `AccordionSection` are separate files.
22
+ *
23
+ * ### Why the tab stop is written imperatively
24
+ *
25
+ * Every other roving group in this library passes `tabIndex` as a
26
+ * prop, because it renders its own members. A toolbar does not: a
27
+ * `control` item is whatever the app already has — a `Switch`, a
28
+ * `ColorSchemeSwitcher`, a `SegmentedControl` — and most components
29
+ * in this library do not forward a `tabIndex` prop at all. Cloning
30
+ * one onto them would compile, do nothing, and leave the bar with
31
+ * one tab stop per control, which is precisely the APG violation
32
+ * this component exists to fix (mux-magic's `role="toolbar"` is six
33
+ * sequential tab stops).
34
+ *
35
+ * So the slot finds the one focusable in its subtree and writes
36
+ * `tabIndex` onto it. React never sets that attribute on these
37
+ * elements, so there is no second writer to fight with.
38
+ *
39
+ * A control holding **more than one** focusable is a nested
40
+ * composite the toolbar does not manage; only the first is given
41
+ * the tab stop, and `Toolbar.mdx` says so.
42
+ */
43
+ export declare const ToolbarSlot: ({ children, isDisabled, itemKey, register, tabIndex, trackElement, trackFocusable, }: ToolbarSlotProps) => ReactNode;
44
+ //# sourceMappingURL=ToolbarSlot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ToolbarSlot.d.ts","sourceRoot":"","sources":["../../src/Toolbar/ToolbarSlot.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAetC,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,EAAE,SAAS,CAAA;IACnB,gEAAgE;IAChE,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,OAAO,EAAE,MAAM,CAAA;IACf,kEAAkE;IAClE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,IAAI,CAAA;IACvC,QAAQ,EAAE,MAAM,CAAA;IAChB,kDAAkD;IAClD,YAAY,EAAE,CACZ,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,WAAW,GAAG,IAAI,KACxB,IAAI,CAAA;IACT,iEAAiE;IACjE,cAAc,EAAE,CACd,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,WAAW,GAAG,IAAI,KACxB,IAAI,CAAA;CACV,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,eAAO,MAAM,WAAW,GAAI,sFAQzB,gBAAgB,KAAG,SAkDrB,CAAA"}
@@ -0,0 +1,69 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useRef } from "react";
3
+ /**
4
+ * Every focusable a toolbar knows how to hand a tab stop to.
5
+ *
6
+ * `[tabindex]` carries **no** `:not([tabindex="-1"])` clause, unlike
7
+ * the usual copy of this selector: the whole job here is to move a
8
+ * tab stop between elements, so an element this slot has already
9
+ * pushed out of the tab order still has to be findable to be let
10
+ * back in.
11
+ */
12
+ const FOCUSABLE_SELECTOR = "a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]";
13
+ /**
14
+ * One member of the toolbar's roving group: an action the toolbar
15
+ * rendered, or a control the caller handed in.
16
+ *
17
+ * Its own file because **registration is an effect**, and an effect
18
+ * cannot run in a loop — the same reason `MenuAction`, `TabTrigger`
19
+ * and `AccordionSection` are separate files.
20
+ *
21
+ * ### Why the tab stop is written imperatively
22
+ *
23
+ * Every other roving group in this library passes `tabIndex` as a
24
+ * prop, because it renders its own members. A toolbar does not: a
25
+ * `control` item is whatever the app already has — a `Switch`, a
26
+ * `ColorSchemeSwitcher`, a `SegmentedControl` — and most components
27
+ * in this library do not forward a `tabIndex` prop at all. Cloning
28
+ * one onto them would compile, do nothing, and leave the bar with
29
+ * one tab stop per control, which is precisely the APG violation
30
+ * this component exists to fix (mux-magic's `role="toolbar"` is six
31
+ * sequential tab stops).
32
+ *
33
+ * So the slot finds the one focusable in its subtree and writes
34
+ * `tabIndex` onto it. React never sets that attribute on these
35
+ * elements, so there is no second writer to fight with.
36
+ *
37
+ * A control holding **more than one** focusable is a nested
38
+ * composite the toolbar does not manage; only the first is given
39
+ * the tab stop, and `Toolbar.mdx` says so.
40
+ */
41
+ export const ToolbarSlot = ({ children, isDisabled = false, itemKey, register, tabIndex, trackElement, trackFocusable, }) => {
42
+ const slotRef = useRef(null);
43
+ useEffect(() => {
44
+ if (isDisabled) {
45
+ return;
46
+ }
47
+ return register(itemKey);
48
+ }, [isDisabled, itemKey, register]);
49
+ useEffect(() => {
50
+ // The first focusable that is not an `aria-hidden` prop —
51
+ // floating-ui plants focus guards (`<span tabindex="0">`) either
52
+ // side of an overlay's reference element, and the overflow
53
+ // trigger *is* one, so a bare `querySelector` can hand back a
54
+ // guard and give the tab stop to something no user can see.
55
+ const focusable = Array.from(slotRef.current?.querySelectorAll(FOCUSABLE_SELECTOR) ?? []).find((candidate) => candidate.getAttribute("aria-hidden") !== "true") ?? null;
56
+ trackFocusable(itemKey, focusable);
57
+ if (focusable && !isDisabled) {
58
+ focusable.tabIndex = tabIndex;
59
+ }
60
+ return () => {
61
+ trackFocusable(itemKey, null);
62
+ };
63
+ }, [isDisabled, itemKey, tabIndex, trackFocusable]);
64
+ return (_jsx("div", { className: "flex shrink-0 items-center", ref: (element) => {
65
+ slotRef.current = element;
66
+ trackElement(itemKey, element);
67
+ }, children: children }));
68
+ };
69
+ //# sourceMappingURL=ToolbarSlot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ToolbarSlot.js","sourceRoot":"","sources":["../../src/Toolbar/ToolbarSlot.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,OAAO,CAAA;AAEzC;;;;;;;;GAQG;AACH,MAAM,kBAAkB,GACtB,sHAAsH,CAAA;AAsBxH;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,EAC1B,QAAQ,EACR,UAAU,GAAG,KAAK,EAClB,OAAO,EACP,QAAQ,EACR,QAAQ,EACR,YAAY,EACZ,cAAc,GACG,EAAa,EAAE;IAChC,MAAM,OAAO,GAAG,MAAM,CAAiB,IAAI,CAAC,CAAA;IAE5C,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,UAAU,EAAE,CAAC;YACf,OAAM;QACR,CAAC;QAED,OAAO,QAAQ,CAAC,OAAO,CAAC,CAAA;IAC1B,CAAC,EAAE,CAAC,UAAU,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAA;IAEnC,SAAS,CAAC,GAAG,EAAE;QACb,0DAA0D;QAC1D,iEAAiE;QACjE,2DAA2D;QAC3D,8DAA8D;QAC9D,4DAA4D;QAC5D,MAAM,SAAS,GACb,KAAK,CAAC,IAAI,CACR,OAAO,CAAC,OAAO,EAAE,gBAAgB,CAC/B,kBAAkB,CACnB,IAAI,EAAE,CACR,CAAC,IAAI,CACJ,CAAC,SAAS,EAAE,EAAE,CACZ,SAAS,CAAC,YAAY,CAAC,aAAa,CAAC,KAAK,MAAM,CACnD,IAAI,IAAI,CAAA;QAEX,cAAc,CAAC,OAAO,EAAE,SAAS,CAAC,CAAA;QAElC,IAAI,SAAS,IAAI,CAAC,UAAU,EAAE,CAAC;YAC7B,SAAS,CAAC,QAAQ,GAAG,QAAQ,CAAA;QAC/B,CAAC;QAED,OAAO,GAAG,EAAE;YACV,cAAc,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;QAC/B,CAAC,CAAA;IACH,CAAC,EAAE,CAAC,UAAU,EAAE,OAAO,EAAE,QAAQ,EAAE,cAAc,CAAC,CAAC,CAAA;IAEnD,OAAO,CACL,cACE,SAAS,EAAC,4BAA4B,EACtC,GAAG,EAAE,CAAC,OAAO,EAAE,EAAE;YACf,OAAO,CAAC,OAAO,GAAG,OAAO,CAAA;YAEzB,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;QAChC,CAAC,YAEA,QAAQ,GACL,CACP,CAAA;AACH,CAAC,CAAA"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The collapse rule: how many of a toolbar's items fit, given what
3
+ * they measured.
4
+ *
5
+ * Pure arithmetic, in its own file for the same reason
6
+ * `chooseColumns` is — the rule is a table of numbers anyone can
7
+ * check in Node, and everything that needs a browser is quarantined
8
+ * in `useToolbarOverflow`.
9
+ *
10
+ * ## Priority is the array order
11
+ *
12
+ * `itemInlineSizes` is in **priority order, highest first**, which
13
+ * is also paint order: the toolbar collapses from the end. One axis
14
+ * rather than two, because a separate `priority` number and a
15
+ * separate display order is a pair a caller can put out of step,
16
+ * and neither mux-magic nor plex-channels has *any* ordering — they
17
+ * hardcode which controls live in the bar and which live in the
18
+ * popover, at every width.
19
+ *
20
+ * ## Why the trigger's width is an input
21
+ *
22
+ * The overflow trigger only exists once something has collapsed, so
23
+ * the question is not "how many fit" but "how many fit **beside a
24
+ * trigger** — unless all of them fit without one". Ignoring that is
25
+ * how an overflow control ends up wrapping to a second line at
26
+ * exactly the width where it first appears.
27
+ */
28
+ export declare const chooseVisibleCount: ({ availableInlineSize, gapInlineSize, itemInlineSizes, triggerInlineSize, }: {
29
+ /** The room the bar has, in CSS px. */
30
+ availableInlineSize: number;
31
+ /** The row's `column-gap`, in CSS px. */
32
+ gapInlineSize: number;
33
+ /** Each item's measured inline size, priority order, in CSS px. */
34
+ itemInlineSizes: readonly number[];
35
+ /** The overflow trigger's inline size, in CSS px. */
36
+ triggerInlineSize: number;
37
+ }) => number;
38
+ //# sourceMappingURL=chooseVisibleCount.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chooseVisibleCount.d.ts","sourceRoot":"","sources":["../../src/Toolbar/chooseVisibleCount.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,eAAO,MAAM,kBAAkB,GAAI,6EAKhC;IACD,uCAAuC;IACvC,mBAAmB,EAAE,MAAM,CAAA;IAC3B,yCAAyC;IACzC,aAAa,EAAE,MAAM,CAAA;IACrB,mEAAmE;IACnE,eAAe,EAAE,SAAS,MAAM,EAAE,CAAA;IAClC,qDAAqD;IACrD,iBAAiB,EAAE,MAAM,CAAA;CAC1B,KAAG,MAiDH,CAAA"}
@@ -0,0 +1,69 @@
1
+ /**
2
+ * The collapse rule: how many of a toolbar's items fit, given what
3
+ * they measured.
4
+ *
5
+ * Pure arithmetic, in its own file for the same reason
6
+ * `chooseColumns` is — the rule is a table of numbers anyone can
7
+ * check in Node, and everything that needs a browser is quarantined
8
+ * in `useToolbarOverflow`.
9
+ *
10
+ * ## Priority is the array order
11
+ *
12
+ * `itemInlineSizes` is in **priority order, highest first**, which
13
+ * is also paint order: the toolbar collapses from the end. One axis
14
+ * rather than two, because a separate `priority` number and a
15
+ * separate display order is a pair a caller can put out of step,
16
+ * and neither mux-magic nor plex-channels has *any* ordering — they
17
+ * hardcode which controls live in the bar and which live in the
18
+ * popover, at every width.
19
+ *
20
+ * ## Why the trigger's width is an input
21
+ *
22
+ * The overflow trigger only exists once something has collapsed, so
23
+ * the question is not "how many fit" but "how many fit **beside a
24
+ * trigger** — unless all of them fit without one". Ignoring that is
25
+ * how an overflow control ends up wrapping to a second line at
26
+ * exactly the width where it first appears.
27
+ */
28
+ export const chooseVisibleCount = ({ availableInlineSize, gapInlineSize, itemInlineSizes, triggerInlineSize, }) => {
29
+ const itemCount = itemInlineSizes.length;
30
+ if (itemCount === 0) {
31
+ return 0;
32
+ }
33
+ /**
34
+ * Nothing measured yet — show everything.
35
+ *
36
+ * The safe direction to be wrong in for one frame, and the same
37
+ * one `useAdaptiveColumns` argues from the other end: a bar that
38
+ * starts whole and collapses over-draws, where a bar that starts
39
+ * collapsed hides controls that were there. It is also what makes
40
+ * the first measurement possible at all, because an item that was
41
+ * never mounted has no width to read.
42
+ */
43
+ if (availableInlineSize <= 0) {
44
+ return itemCount;
45
+ }
46
+ const measure = (count, extra) => itemInlineSizes
47
+ .slice(0, count)
48
+ .reduce((total, size) => total + size, 0) +
49
+ gapInlineSize *
50
+ Math.max(count + (extra > 0 ? 1 : 0) - 1, 0) +
51
+ extra;
52
+ if (measure(itemCount, 0) <= availableInlineSize) {
53
+ return itemCount;
54
+ }
55
+ // Everything below here has an overflow trigger in the row, so
56
+ // every candidate pays for it. Counting down rather than up
57
+ // because the answer is usually near the top.
58
+ for (let count = itemCount - 1; count > 0; count -= 1) {
59
+ if (measure(count, triggerInlineSize) <=
60
+ availableInlineSize) {
61
+ return count;
62
+ }
63
+ }
64
+ // Not even one item and the trigger fit. Everything collapses;
65
+ // the trigger alone may still overflow, and a bar narrower than
66
+ // one control is a layout problem no collapse rule can solve.
67
+ return 0;
68
+ };
69
+ //# sourceMappingURL=chooseVisibleCount.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chooseVisibleCount.js","sourceRoot":"","sources":["../../src/Toolbar/chooseVisibleCount.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,EACjC,mBAAmB,EACnB,aAAa,EACb,eAAe,EACf,iBAAiB,GAUlB,EAAU,EAAE;IACX,MAAM,SAAS,GAAG,eAAe,CAAC,MAAM,CAAA;IAExC,IAAI,SAAS,KAAK,CAAC,EAAE,CAAC;QACpB,OAAO,CAAC,CAAA;IACV,CAAC;IAED;;;;;;;;;OASG;IACH,IAAI,mBAAmB,IAAI,CAAC,EAAE,CAAC;QAC7B,OAAO,SAAS,CAAA;IAClB,CAAC;IAED,MAAM,OAAO,GAAG,CAAC,KAAa,EAAE,KAAa,EAAE,EAAE,CAC/C,eAAe;SACZ,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC;SACf,MAAM,CAAC,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,KAAK,GAAG,IAAI,EAAE,CAAC,CAAC;QAC3C,aAAa;YACX,IAAI,CAAC,GAAG,CAAC,KAAK,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC9C,KAAK,CAAA;IAEP,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC,IAAI,mBAAmB,EAAE,CAAC;QACjD,OAAO,SAAS,CAAA;IAClB,CAAC;IAED,+DAA+D;IAC/D,4DAA4D;IAC5D,8CAA8C;IAC9C,KAAK,IAAI,KAAK,GAAG,SAAS,GAAG,CAAC,EAAE,KAAK,GAAG,CAAC,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;QACtD,IACE,OAAO,CAAC,KAAK,EAAE,iBAAiB,CAAC;YACjC,mBAAmB,EACnB,CAAC;YACD,OAAO,KAAK,CAAA;QACd,CAAC;IACH,CAAC;IAED,+DAA+D;IAC/D,gEAAgE;IAChE,8DAA8D;IAC9D,OAAO,CAAC,CAAA;AACV,CAAC,CAAA"}
@@ -0,0 +1,49 @@
1
+ import type { RefObject } from "react";
2
+ /**
3
+ * The measuring half of progressive overflow — `chooseVisibleCount`
4
+ * is the decision, this is the tape measure.
5
+ *
6
+ * ## Measured, not breakpointed
7
+ *
8
+ * mux-magic collapses at a hardcoded `480px` and plex-channels at a
9
+ * hardcoded `760px`, and both numbers are wrong the moment a label
10
+ * gets longer, the density axis changes, or a rail takes half the
11
+ * row. A measured bar has no number to be wrong: it asks the
12
+ * elements how wide they actually are, in the font and density they
13
+ * are actually rendered in.
14
+ *
15
+ * ## One instance, and cached widths
16
+ *
17
+ * An item that has collapsed is **not in the DOM** — that is the
18
+ * whole point, against a mechanism that renders every action twice
19
+ * and hides one with a CSS specificity coin-flip. So its width
20
+ * cannot be re-read while it is away, and it is remembered from
21
+ * when it was last in the bar instead.
22
+ *
23
+ * The first paint is what seeds the cache: every item starts in the
24
+ * bar (`chooseVisibleCount` returns all of them for an unmeasured
25
+ * container), gets measured, and the row collapses **before paint**
26
+ * because the measuring runs in a layout effect. Any item whose
27
+ * width is still unknown forces one more all-visible pass rather
28
+ * than being guessed at — one extra layout pass, and it converges,
29
+ * because a mounted item always measures.
30
+ *
31
+ * ⚠️ **The measured element must not be sized by its own contents.**
32
+ * If the bar shrink-wraps its items, collapsing one narrows the box,
33
+ * which collapses another — a loop with no error anywhere.
34
+ * `Toolbar`'s root is `flex-1 min-w-0` for exactly this reason, the
35
+ * same trap `useAdaptiveColumns` documents about a container capped
36
+ * by its own answer.
37
+ */
38
+ export declare const useToolbarOverflow: ({ itemKeys, }: {
39
+ /** Priority order, highest first. */
40
+ itemKeys: readonly string[];
41
+ }) => {
42
+ containerRef: RefObject<HTMLDivElement | null>;
43
+ /** Callback ref for each item's box. `null` on unmount. */
44
+ trackItem: (key: string, element: HTMLElement | null) => void;
45
+ /** Callback ref for the overflow trigger's box. */
46
+ trackTrigger: (element: HTMLElement | null) => void;
47
+ visibleCount: number;
48
+ };
49
+ //# sourceMappingURL=useToolbarOverflow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useToolbarOverflow.d.ts","sourceRoot":"","sources":["../../src/Toolbar/useToolbarOverflow.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAUtC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,eAAO,MAAM,kBAAkB,GAAI,eAEhC;IACD,qCAAqC;IACrC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;CAC5B,KAAG;IACF,YAAY,EAAE,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,CAAA;IAC9C,2DAA2D;IAC3D,SAAS,EAAE,CACT,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,WAAW,GAAG,IAAI,KACxB,IAAI,CAAA;IACT,mDAAmD;IACnD,YAAY,EAAE,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAA;IACnD,YAAY,EAAE,MAAM,CAAA;CAwKrB,CAAA"}