@charcuterie/ui 0.1.1 → 1.0.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 (115) hide show
  1. package/README.md +9 -0
  2. package/dist/Accordion/Accordion.d.ts +82 -0
  3. package/dist/Accordion/Accordion.d.ts.map +1 -0
  4. package/dist/Accordion/Accordion.js +108 -0
  5. package/dist/Accordion/Accordion.js.map +1 -0
  6. package/dist/Accordion/AccordionSection.d.ts +28 -0
  7. package/dist/Accordion/AccordionSection.d.ts.map +1 -0
  8. package/dist/Accordion/AccordionSection.js +53 -0
  9. package/dist/Accordion/AccordionSection.js.map +1 -0
  10. package/dist/Field/Field.d.ts +104 -0
  11. package/dist/Field/Field.d.ts.map +1 -0
  12. package/dist/Field/Field.js +115 -0
  13. package/dist/Field/Field.js.map +1 -0
  14. package/dist/Field/FieldGroup.d.ts +58 -0
  15. package/dist/Field/FieldGroup.d.ts.map +1 -0
  16. package/dist/Field/FieldGroup.js +65 -0
  17. package/dist/Field/FieldGroup.js.map +1 -0
  18. package/dist/FileDropZone/FileDropZone.d.ts +63 -0
  19. package/dist/FileDropZone/FileDropZone.d.ts.map +1 -0
  20. package/dist/FileDropZone/FileDropZone.js +122 -0
  21. package/dist/FileDropZone/FileDropZone.js.map +1 -0
  22. package/dist/LogViewer/LogViewer.d.ts +90 -0
  23. package/dist/LogViewer/LogViewer.d.ts.map +1 -0
  24. package/dist/LogViewer/LogViewer.js +165 -0
  25. package/dist/LogViewer/LogViewer.js.map +1 -0
  26. package/dist/Menu/Menu.d.ts +81 -0
  27. package/dist/Menu/Menu.d.ts.map +1 -0
  28. package/dist/Menu/Menu.js +199 -0
  29. package/dist/Menu/Menu.js.map +1 -0
  30. package/dist/Menu/MenuAction.d.ts +24 -0
  31. package/dist/Menu/MenuAction.d.ts.map +1 -0
  32. package/dist/Menu/MenuAction.js +37 -0
  33. package/dist/Menu/MenuAction.js.map +1 -0
  34. package/dist/Select/Select.d.ts +87 -0
  35. package/dist/Select/Select.d.ts.map +1 -0
  36. package/dist/Select/Select.js +51 -0
  37. package/dist/Select/Select.js.map +1 -0
  38. package/dist/SortableTableHeader/SortableTableHeader.d.ts +61 -0
  39. package/dist/SortableTableHeader/SortableTableHeader.d.ts.map +1 -0
  40. package/dist/SortableTableHeader/SortableTableHeader.js +56 -0
  41. package/dist/SortableTableHeader/SortableTableHeader.js.map +1 -0
  42. package/dist/Toast/Toast.d.ts +54 -0
  43. package/dist/Toast/Toast.d.ts.map +1 -0
  44. package/dist/Toast/Toast.js +121 -0
  45. package/dist/Toast/Toast.js.map +1 -0
  46. package/dist/Toast/ToastRegion.d.ts +57 -0
  47. package/dist/Toast/ToastRegion.d.ts.map +1 -0
  48. package/dist/Toast/ToastRegion.js +41 -0
  49. package/dist/Toast/ToastRegion.js.map +1 -0
  50. package/dist/Toast/toastLifecycle.d.ts +34 -0
  51. package/dist/Toast/toastLifecycle.d.ts.map +1 -0
  52. package/dist/Toast/toastLifecycle.js +25 -0
  53. package/dist/Toast/toastLifecycle.js.map +1 -0
  54. package/dist/Tooltip/Tooltip.d.ts +82 -0
  55. package/dist/Tooltip/Tooltip.d.ts.map +1 -0
  56. package/dist/Tooltip/Tooltip.js +124 -0
  57. package/dist/Tooltip/Tooltip.js.map +1 -0
  58. package/dist/index.d.ts +26 -0
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +13 -0
  61. package/dist/index.js.map +1 -1
  62. package/dist/mountStory.testHelpers.d.ts +0 -11
  63. package/dist/mountStory.testHelpers.d.ts.map +1 -1
  64. package/dist/mountStory.testHelpers.js +27 -0
  65. package/dist/mountStory.testHelpers.js.map +1 -1
  66. package/dist/slotProps.d.ts +110 -0
  67. package/dist/slotProps.d.ts.map +1 -0
  68. package/dist/slotProps.js +39 -0
  69. package/dist/slotProps.js.map +1 -0
  70. package/package.json +3 -3
  71. package/src/Accordion/Accordion.mdx +83 -0
  72. package/src/Accordion/Accordion.stories.tsx +146 -0
  73. package/src/Accordion/Accordion.test.tsx +163 -0
  74. package/src/Accordion/Accordion.tsx +182 -0
  75. package/src/Accordion/AccordionSection.tsx +163 -0
  76. package/src/Field/Field.mdx +144 -0
  77. package/src/Field/Field.stories.tsx +280 -0
  78. package/src/Field/Field.test.tsx +344 -0
  79. package/src/Field/Field.tsx +212 -0
  80. package/src/Field/FieldGroup.tsx +138 -0
  81. package/src/FileDropZone/FileDropZone.mdx +78 -0
  82. package/src/FileDropZone/FileDropZone.stories.tsx +136 -0
  83. package/src/FileDropZone/FileDropZone.test.tsx +230 -0
  84. package/src/FileDropZone/FileDropZone.tsx +234 -0
  85. package/src/LogViewer/LogViewer.mdx +123 -0
  86. package/src/LogViewer/LogViewer.stories.tsx +169 -0
  87. package/src/LogViewer/LogViewer.test.tsx +236 -0
  88. package/src/LogViewer/LogViewer.tsx +261 -0
  89. package/src/Menu/Menu.mdx +103 -0
  90. package/src/Menu/Menu.stories.tsx +161 -0
  91. package/src/Menu/Menu.test.tsx +202 -0
  92. package/src/Menu/Menu.tsx +300 -0
  93. package/src/Menu/MenuAction.tsx +95 -0
  94. package/src/Select/Select.mdx +74 -0
  95. package/src/Select/Select.stories.tsx +126 -0
  96. package/src/Select/Select.test.tsx +129 -0
  97. package/src/Select/Select.tsx +200 -0
  98. package/src/SortableTableHeader/SortableTableHeader.mdx +67 -0
  99. package/src/SortableTableHeader/SortableTableHeader.stories.tsx +207 -0
  100. package/src/SortableTableHeader/SortableTableHeader.test.tsx +180 -0
  101. package/src/SortableTableHeader/SortableTableHeader.tsx +131 -0
  102. package/src/Toast/Toast.mdx +84 -0
  103. package/src/Toast/Toast.stories.tsx +192 -0
  104. package/src/Toast/Toast.test.tsx +217 -0
  105. package/src/Toast/Toast.tsx +224 -0
  106. package/src/Toast/ToastRegion.tsx +98 -0
  107. package/src/Toast/toastLifecycle.ts +45 -0
  108. package/src/Tooltip/Tooltip.mdx +69 -0
  109. package/src/Tooltip/Tooltip.stories.tsx +104 -0
  110. package/src/Tooltip/Tooltip.test.tsx +138 -0
  111. package/src/Tooltip/Tooltip.tsx +193 -0
  112. package/src/index.ts +47 -0
  113. package/src/mountStory.testHelpers.ts +30 -0
  114. package/src/slotProps.ts +143 -0
  115. package/src/sourceRules.test.ts +221 -30
@@ -0,0 +1,199 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { selectTabIndex, useClonedChild, useRovingFocus, } from "@charcuterie/logic";
3
+ import { autoUpdate, FloatingFocusManager, flip, offset, shift, useDismiss, useFloating, useInteractions, useRole, } from "@floating-ui/react";
4
+ import { useEffect, useRef } from "react";
5
+ import { toClassName } from "../toClassName.js";
6
+ import { MenuAction } from "./MenuAction.js";
7
+ /**
8
+ * A menu is **not** a listbox, and mux-magic's `TypePicker` is the
9
+ * fleet's one attempt at telling them apart — it renders
10
+ * `role="menu"` over items that set a value, which is a listbox
11
+ * wearing the wrong role. The distinction is what the items *are*:
12
+ *
13
+ * - `menuitem` **does** something. Rename, Delete, Copy path.
14
+ * - `option` **is** something. It is a value you are choosing.
15
+ *
16
+ * A screen reader announces "menu, 4 items" for one and "listbox,
17
+ * selected, 2 of 4" for the other, and an agent driving the app
18
+ * needs the second to know what state it left behind. Getting it
19
+ * wrong is invisible in a browser and invisible to axe.
20
+ *
21
+ * ### There is no `label` prop, and that corrects a lesson
22
+ *
23
+ * `Spinner` (M3), `Popover` and `Modal` (M4) all take a **required**
24
+ * name, because a `role="status"` and a `role="dialog"` take none
25
+ * from their content. The obvious move here was a `label` prop doing
26
+ * the same job, and it was written that way first — then measured
27
+ * as doing nothing.
28
+ *
29
+ * `useRole(context, { role: "menu" })` puts **`aria-labelledby` on
30
+ * the panel pointing at the trigger**, and `aria-labelledby` beats
31
+ * `aria-label`. So the menu was already named — "Bay 3", from the
32
+ * button — and the prop was silently discarded. That is also the ARIA
33
+ * Authoring Practices menu-button pattern, where a menu is named by
34
+ * the control that opens it.
35
+ *
36
+ * The rule the other three teach is therefore narrower than it
37
+ * looked: **an overlay with no trigger relationship needs its own
38
+ * name.** A menu has one, so the requirement moves to the trigger —
39
+ * which `Button` and `IconButton` already enforce.
40
+ *
41
+ * ### Roving focus, and the composition `Tabs` already proved
42
+ *
43
+ * `RovingFocus` alone this time — there is no selection, because a
44
+ * menu item is not selected, it is invoked. That is the same
45
+ * two-kinds argument from the other side: `Tabs` needed both kinds
46
+ * because a tab is chosen *and* focused, and a menu needs one
47
+ * because a menu item is only ever focused.
48
+ *
49
+ * ### Focus moves in, once, and only on opening
50
+ *
51
+ * A menu is one of the few patterns where focus must jump on open —
52
+ * otherwise the keyboard user who pressed Enter on the trigger is
53
+ * still on the trigger with a menu they cannot reach. It happens in
54
+ * the panel's own effect rather than through
55
+ * `FloatingFocusManager`'s `initialFocus`, because the element to
56
+ * focus is whichever one `RovingFocus` says is active, and that is
57
+ * not known until the items have registered.
58
+ *
59
+ * ### No type-ahead, deliberately
60
+ *
61
+ * The APG lists type-ahead as optional for a menu and required for a
62
+ * listbox, and none of the fleet's menus are long enough to need it
63
+ * — the largest has six items. Adding it would mean a keystroke
64
+ * buffer and a timer in a component that currently has neither, for
65
+ * a menu you can cross in three arrow presses.
66
+ */
67
+ export const Menu = ({ className, isVisible, items, onDismiss, placement = "bottom-start", trigger, }) => {
68
+ const itemElements = useRef(new Map());
69
+ const panelElement = useRef(null);
70
+ const focus = useRovingFocus();
71
+ const { activeValue, registeredValues, setActiveValue } = focus;
72
+ const [firstValue] = registeredValues;
73
+ const { context, floatingStyles, refs } = useFloating({
74
+ open: isVisible,
75
+ onOpenChange: (isNextVisible) => {
76
+ if (!isNextVisible) {
77
+ onDismiss();
78
+ }
79
+ },
80
+ placement,
81
+ middleware: [offset(4), flip(), shift({ padding: 8 })],
82
+ strategy: "fixed",
83
+ whileElementsMounted: autoUpdate,
84
+ });
85
+ const { getFloatingProps, getReferenceProps } = useInteractions([
86
+ useDismiss(context),
87
+ useRole(context, { role: "menu" }),
88
+ ]);
89
+ const clonedTrigger = useClonedChild(trigger, {
90
+ ...getReferenceProps(),
91
+ ref: refs.setReference,
92
+ });
93
+ /**
94
+ * The other half of the `isConnected` guard below. If the ref
95
+ * fired while the node was still detached, nothing has shown the
96
+ * popover — and a `[popover]` that was never shown is
97
+ * `display: none`, so the menu renders, passes typecheck, and is
98
+ * invisible to `getByRole` and to a user alike.
99
+ *
100
+ * Effects run after the tree is connected, so this is the
101
+ * backstop. It is idempotent: `:popover-open` is the same question
102
+ * the ref asks.
103
+ */
104
+ useEffect(() => {
105
+ const panel = panelElement.current;
106
+ if (isVisible &&
107
+ panel?.isConnected &&
108
+ !panel.matches(":popover-open")) {
109
+ panel.showPopover();
110
+ }
111
+ }, [isVisible]);
112
+ /**
113
+ * Opening **makes** a member active, and this is the step the
114
+ * first version was missing.
115
+ *
116
+ * `RovingFocus` leaves `activeValue` at `null` until something
117
+ * sets it — `selectTabIndex` covers the first paint by giving the
118
+ * tab stop to `registeredValues[0]` while it is still null, which
119
+ * is exactly why nothing looked wrong: the menu had a correct tab
120
+ * stop, correct roles, and no focus. `Tabs` never hit it because a
121
+ * tab bar seeds itself from its selected key.
122
+ *
123
+ * Re-running on every open is deliberate. The APG says a menu
124
+ * opens on its first item, so a menu reopened after arrowing to
125
+ * the third one starts at the top again rather than resuming a
126
+ * position the user has forgotten.
127
+ */
128
+ useEffect(() => {
129
+ if (!isVisible || firstValue === undefined) {
130
+ return;
131
+ }
132
+ setActiveValue(firstValue);
133
+ }, [firstValue, isVisible, setActiveValue]);
134
+ useEffect(() => {
135
+ if (!isVisible || activeValue === null) {
136
+ return;
137
+ }
138
+ itemElements.current.get(activeValue)?.focus();
139
+ }, [activeValue, isVisible]);
140
+ return (_jsxs(_Fragment, { children: [clonedTrigger, isVisible ? (_jsx(FloatingFocusManager, { context: context,
141
+ // `false`, so Tab leaves the menu and lands on the next
142
+ // control in the page — which is what closes it, through
143
+ // `useDismiss`. A trapped menu is a dialog.
144
+ modal: false,
145
+ // Focus is moved by the effect above, to whichever item
146
+ // `RovingFocus` made active. Letting the manager also aim
147
+ // it is two owners for one caret.
148
+ initialFocus: -1, children: _jsx("div", { ...getFloatingProps(), className: toClassName("inset-auto m-0 flex min-w-48 flex-col gap-0.5 rounded-md border border-border-default bg-surface-overlay p-1 shadow-medium", className), onKeyDown: (keyEvent) => {
149
+ const commands = {
150
+ ArrowDown: focus.next,
151
+ ArrowUp: focus.previous,
152
+ End: focus.last,
153
+ Home: focus.first,
154
+ };
155
+ const command = commands[keyEvent.key];
156
+ if (command) {
157
+ // Arrow keys scroll the page by default, and a menu
158
+ // that scrolls the document behind it while moving
159
+ // focus is the hand-rolled version of this
160
+ // component.
161
+ keyEvent.preventDefault();
162
+ command();
163
+ }
164
+ }, popover: "manual", ref: (node) => {
165
+ refs.setFloating(node);
166
+ panelElement.current = node;
167
+ // A ref callback rather than an effect, for the same
168
+ // reason as `Popover`: a `[popover]` is
169
+ // `display: none` until shown, and every child effect
170
+ // — including the focus move above — would otherwise
171
+ // be aiming at a hidden element.
172
+ //
173
+ // `isConnected` is the guard `Popover` does not need
174
+ // and this does. `FloatingFocusManager` renders a pair
175
+ // of focus guards around its child, so React commits
176
+ // this subtree in an order where the ref can fire
177
+ // while the node is still detached — and
178
+ // `showPopover()` on a disconnected element throws
179
+ // `InvalidStateError`, which surfaces as an unhandled
180
+ // rejection rather than as a failing assertion.
181
+ if (node?.isConnected &&
182
+ !node.matches(":popover-open")) {
183
+ node.showPopover();
184
+ }
185
+ },
186
+ // Duplicated from `getFloatingProps()` on purpose, and
187
+ // the two cannot differ — `useRole(context, { role:
188
+ // "menu" })` above is where it comes from. Stated here
189
+ // because a linter cannot see a role through a spread.
190
+ role: "menu", style: floatingStyles, children: items.map((item) => (_jsx(MenuAction, { item: item, onDismiss: onDismiss, register: focus.register, tabIndex: selectTabIndex(focus, item.key), trackElement: (key, element) => {
191
+ if (element) {
192
+ itemElements.current.set(key, element);
193
+ }
194
+ else {
195
+ itemElements.current.delete(key);
196
+ }
197
+ } }, item.key))) }) })) : null] }));
198
+ };
199
+ //# sourceMappingURL=Menu.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Menu.js","sourceRoot":"","sources":["../../src/Menu/Menu.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ import type { ReactNode } from "react";
2
+ import type { MenuItem } from "./Menu.tsx";
3
+ export type MenuActionProps = {
4
+ item: MenuItem;
5
+ onDismiss: () => void;
6
+ /** `RovingFocus.register` — membership of the arrow-key group. */
7
+ register: (value: string) => () => void;
8
+ tabIndex: number;
9
+ trackElement: (key: string, element: HTMLButtonElement | null) => void;
10
+ };
11
+ /**
12
+ * Its own file because **registration is an effect**, and an effect
13
+ * cannot run in a loop — the same reason `TabTrigger` and
14
+ * `AccordionSection` are separate. Any member of a registering group
15
+ * ends up shaped like this.
16
+ *
17
+ * A disabled item is simply not registered, so the arrow keys skip
18
+ * it without any command in `RovingFocus` knowing what "disabled"
19
+ * means. It stays in the DOM and stays announced, which is the
20
+ * difference between "you cannot do this right now" and "this does
21
+ * not exist".
22
+ */
23
+ export declare const MenuAction: ({ item, onDismiss, register, tabIndex, trackElement, }: MenuActionProps) => ReactNode;
24
+ //# sourceMappingURL=MenuAction.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MenuAction.d.ts","sourceRoot":"","sources":["../../src/Menu/MenuAction.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAKtC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAE1C,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,EAAE,QAAQ,CAAA;IACd,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,kEAAkE;IAClE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,IAAI,CAAA;IACvC,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,CACZ,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,iBAAiB,GAAG,IAAI,KAC9B,IAAI,CAAA;CACV,CAAA;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,UAAU,GAAI,wDAMxB,eAAe,KAAG,SAyDpB,CAAA"}
@@ -0,0 +1,37 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect } from "react";
3
+ import { FOCUS_RING_CLASS } from "../intentStyles.js";
4
+ import { toClassName } from "../toClassName.js";
5
+ /**
6
+ * Its own file because **registration is an effect**, and an effect
7
+ * cannot run in a loop — the same reason `TabTrigger` and
8
+ * `AccordionSection` are separate. Any member of a registering group
9
+ * ends up shaped like this.
10
+ *
11
+ * A disabled item is simply not registered, so the arrow keys skip
12
+ * it without any command in `RovingFocus` knowing what "disabled"
13
+ * means. It stays in the DOM and stays announced, which is the
14
+ * difference between "you cannot do this right now" and "this does
15
+ * not exist".
16
+ */
17
+ export const MenuAction = ({ item, onDismiss, register, tabIndex, trackElement, }) => {
18
+ const { icon, isDisabled = false, key, label, onSelect, } = item;
19
+ useEffect(() => {
20
+ if (isDisabled) {
21
+ return;
22
+ }
23
+ return register(key);
24
+ }, [isDisabled, key, register]);
25
+ return (_jsxs("button", { className: toClassName("flex w-full cursor-pointer items-center gap-2 rounded-sm bg-transparent px-2 py-1.5 text-start text-content-primary text-sm transition-colors duration-(--duration-fast) ease-standard", "hover:bg-intent-neutral-surface", isDisabled &&
26
+ "cursor-not-allowed text-content-disabled hover:bg-transparent", FOCUS_RING_CLASS), disabled: isDisabled, onClick: () => {
27
+ onSelect();
28
+ // Choosing dismisses. A menu that stays open after an action
29
+ // has fired is the one interaction users read as "it didn't
30
+ // work", and every one of the fleet's menus closes here
31
+ // today by unmounting its whole parent.
32
+ onDismiss();
33
+ }, ref: (element) => {
34
+ trackElement(key, element);
35
+ }, role: "menuitem", tabIndex: tabIndex, type: "button", children: [icon === undefined ? null : (_jsx("span", { "aria-hidden": "true", className: "shrink-0 text-content-secondary", children: icon })), label] }));
36
+ };
37
+ //# sourceMappingURL=MenuAction.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MenuAction.js","sourceRoot":"","sources":["../../src/Menu/MenuAction.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;AAe/C;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,EACzB,IAAI,EACJ,SAAS,EACT,QAAQ,EACR,QAAQ,EACR,YAAY,GACI,EAAa,EAAE;IAC/B,MAAM,EACJ,IAAI,EACJ,UAAU,GAAG,KAAK,EAClB,GAAG,EACH,KAAK,EACL,QAAQ,GACT,GAAG,IAAI,CAAA;IAER,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,UAAU,EAAE,CAAC;YACf,OAAM;QACR,CAAC;QAED,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAA;IACtB,CAAC,EAAE,CAAC,UAAU,EAAE,GAAG,EAAE,QAAQ,CAAC,CAAC,CAAA;IAE/B,OAAO,CACL,kBACE,SAAS,EAAE,WAAW,CACpB,wLAAwL,EACxL,iCAAiC,EACjC,UAAU;YACR,+DAA+D,EACjE,gBAAgB,CACjB,EACD,QAAQ,EAAE,UAAU,EACpB,OAAO,EAAE,GAAG,EAAE;YACZ,QAAQ,EAAE,CAAA;YAEV,6DAA6D;YAC7D,4DAA4D;YAC5D,wDAAwD;YACxD,wCAAwC;YACxC,SAAS,EAAE,CAAA;QACb,CAAC,EACD,GAAG,EAAE,CAAC,OAAO,EAAE,EAAE;YACf,YAAY,CAAC,GAAG,EAAE,OAAO,CAAC,CAAA;QAC5B,CAAC,EACD,IAAI,EAAC,UAAU,EACf,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAC,QAAQ,aAEZ,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAC3B,8BAGc,MAAM,EAClB,SAAS,EAAC,iCAAiC,YAE1C,IAAI,GACA,CACR,EAEA,KAAK,IACC,CACV,CAAA;AACH,CAAC,CAAA"}
@@ -0,0 +1,87 @@
1
+ import type { ControlSize } from "@charcuterie/tokens";
2
+ import type { ComponentPropsWithRef, ReactNode } from "react";
3
+ export type SelectOption = {
4
+ isDisabled?: boolean;
5
+ label: string;
6
+ value: string;
7
+ };
8
+ export type SelectOptionGroup = {
9
+ label: string;
10
+ options: SelectOption[];
11
+ };
12
+ export type SelectItem = SelectOption | SelectOptionGroup;
13
+ /**
14
+ * The HTML attributes are passed through, and that is not
15
+ * convenience — it is what makes the `Field` slot work at all.
16
+ *
17
+ * `Field` clones `id`, `aria-describedby`, `aria-invalid` and
18
+ * `required` onto whatever control it is given. A component with a
19
+ * closed prop list **silently drops all four**: the field renders,
20
+ * the label still points at nothing, and every assertion about the
21
+ * control's own props passes because they never arrived. Found by
22
+ * the one test that asked the control what it had been handed.
23
+ *
24
+ * Same arrangement as `Button`, for the same reason.
25
+ */
26
+ export type SelectProps = Omit<ComponentPropsWithRef<"select">, "disabled" | "onChange" | "size" | "value"> & {
27
+ className?: string;
28
+ isDisabled?: boolean;
29
+ /**
30
+ * The accessible name, when this is **not** inside a `Field`. A
31
+ * `<select>` with neither a `<label for>` nor an `aria-label` is
32
+ * announced as "combobox" and is unfindable by
33
+ * `getByRole("combobox", { name })` — twelve of the fleet's
34
+ * fourteen native selects are in exactly that state.
35
+ */
36
+ label?: string;
37
+ onChange?: (value: string) => void;
38
+ options: SelectItem[];
39
+ /**
40
+ * A disabled first entry, not a real option. Reads as a prompt to
41
+ * a sighted user and cannot be submitted, which is what a
42
+ * placeholder should mean — unlike an empty-valued option, which
43
+ * is selectable and silently posts "".
44
+ */
45
+ placeholder?: string;
46
+ size?: ControlSize;
47
+ /** **Initial** only, like every other value prop in this library. */
48
+ value?: string;
49
+ };
50
+ /**
51
+ * A styled native `<select>`, and the fleet's fourteen hand-rolled
52
+ * ones collapse onto it.
53
+ *
54
+ * ### There is no state kind here, and that is the finding
55
+ *
56
+ * Every other interactive component in this package owns its state
57
+ * through `@charcuterie/logic`. This one owns none, because the
58
+ * platform already does: a `<select>` holds the chosen option in the
59
+ * DOM, and putting a `useSinglePicker` beside it would create the
60
+ * exact conflict the state layer exists to prevent — two things
61
+ * believing they hold `value`, which is the Radix argument from
62
+ * `Popover` pointed at ourselves. So `value` seeds `defaultValue`
63
+ * and changes are reported outward.
64
+ *
65
+ * The consequence worth knowing: **`Select` is uncontrolled and
66
+ * `Tabs` is not**, and a reader should be able to tell why without
67
+ * asking. A tab bar's selection has no DOM representation, so
68
+ * something has to hold it; a select's does.
69
+ *
70
+ * ### Native, and what that rules out
71
+ *
72
+ * A `<select>` cannot render a rich option — an icon, two lines, a
73
+ * badge — and cannot filter. mux-magic has six controls that need
74
+ * one of those (`CommandPicker`, `PathPicker`, `LinkPicker`,
75
+ * `EnumPicker`, `AssFieldPicker`, `RenameTargetPicker`), and every
76
+ * one of them is a **`Combobox`**: a text input that filters a
77
+ * listbox. That is a different pattern with a different ARIA
78
+ * contract, it is P2 in the plan, and building a bare `Listbox`
79
+ * here that none of the six could adopt would have shipped a
80
+ * component with no caller.
81
+ *
82
+ * What native buys, and what a hand-rolled listbox has to rebuild
83
+ * from nothing: type-ahead, Home/End, PageUp/PageDown, the mobile
84
+ * wheel picker, form submission, `:invalid`, and autofill.
85
+ */
86
+ export declare const Select: ({ className, isDisabled, label, onChange, options, placeholder, size, value, ...selectProps }: SelectProps) => ReactNode;
87
+ //# sourceMappingURL=Select.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Select.d.ts","sourceRoot":"","sources":["../../src/Select/Select.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AACtD,OAAO,KAAK,EAEV,qBAAqB,EACrB,SAAS,EACV,MAAM,OAAO,CAAA;AAYd,MAAM,MAAM,YAAY,GAAG;IACzB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,YAAY,EAAE,CAAA;CACxB,CAAA;AAED,MAAM,MAAM,UAAU,GAAG,YAAY,GAAG,iBAAiB,CAAA;AAEzD;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,WAAW,GAAG,IAAI,CAC5B,qBAAqB,CAAC,QAAQ,CAAC,EAC/B,UAAU,GAAG,UAAU,GAAG,MAAM,GAAG,OAAO,CAC3C,GAAG;IACF,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAClC,OAAO,EAAE,UAAU,EAAE,CAAA;IACrB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,IAAI,CAAC,EAAE,WAAW,CAAA;IAClB,qEAAqE;IACrE,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,eAAO,MAAM,MAAM,GAAI,+FAUpB,WAAW,KAAG,SA8EhB,CAAA"}
@@ -0,0 +1,51 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { CONTROL_SIZE_CLASS, MIN_TOUCH_TARGET_CLASS, } from "../controlStyles.js";
3
+ import { DISABLED_CLASS, FOCUS_RING_CLASS, } from "../intentStyles.js";
4
+ import { toClassName } from "../toClassName.js";
5
+ const getIsGroup = (item) => "options" in item;
6
+ /**
7
+ * A styled native `<select>`, and the fleet's fourteen hand-rolled
8
+ * ones collapse onto it.
9
+ *
10
+ * ### There is no state kind here, and that is the finding
11
+ *
12
+ * Every other interactive component in this package owns its state
13
+ * through `@charcuterie/logic`. This one owns none, because the
14
+ * platform already does: a `<select>` holds the chosen option in the
15
+ * DOM, and putting a `useSinglePicker` beside it would create the
16
+ * exact conflict the state layer exists to prevent — two things
17
+ * believing they hold `value`, which is the Radix argument from
18
+ * `Popover` pointed at ourselves. So `value` seeds `defaultValue`
19
+ * and changes are reported outward.
20
+ *
21
+ * The consequence worth knowing: **`Select` is uncontrolled and
22
+ * `Tabs` is not**, and a reader should be able to tell why without
23
+ * asking. A tab bar's selection has no DOM representation, so
24
+ * something has to hold it; a select's does.
25
+ *
26
+ * ### Native, and what that rules out
27
+ *
28
+ * A `<select>` cannot render a rich option — an icon, two lines, a
29
+ * badge — and cannot filter. mux-magic has six controls that need
30
+ * one of those (`CommandPicker`, `PathPicker`, `LinkPicker`,
31
+ * `EnumPicker`, `AssFieldPicker`, `RenameTargetPicker`), and every
32
+ * one of them is a **`Combobox`**: a text input that filters a
33
+ * listbox. That is a different pattern with a different ARIA
34
+ * contract, it is P2 in the plan, and building a bare `Listbox`
35
+ * here that none of the six could adopt would have shipped a
36
+ * component with no caller.
37
+ *
38
+ * What native buys, and what a hand-rolled listbox has to rebuild
39
+ * from nothing: type-ahead, Home/End, PageUp/PageDown, the mobile
40
+ * wheel picker, form submission, `:invalid`, and autofill.
41
+ */
42
+ export const Select = ({ className, isDisabled = false, label, onChange, options, placeholder, size = "md", value, ...selectProps }) => (_jsxs("div", { className: "relative inline-grid w-full items-center", children: [_jsxs("select", { ...selectProps, "aria-label": label, className: toClassName("w-full cursor-pointer appearance-none rounded-md border border-border-default bg-surface-raised text-content-primary transition-colors duration-(--duration-fast) ease-standard",
43
+ // Room for the chevron below, which sits in the same grid
44
+ // cell. A `<select>`'s own arrow is what `appearance-none`
45
+ // removes, and forgetting to put one back leaves a control
46
+ // that looks exactly like a text input.
47
+ "pe-9", CONTROL_SIZE_CLASS[size], MIN_TOUCH_TARGET_CLASS, "hover:border-border-strong", FOCUS_RING_CLASS, DISABLED_CLASS, className), defaultValue: value ??
48
+ (placeholder === undefined ? undefined : ""), disabled: isDisabled, onChange: (changeEvent) => {
49
+ onChange?.(changeEvent.currentTarget.value);
50
+ }, children: [placeholder === undefined ? null : (_jsx("option", { disabled: true, value: "", children: placeholder })), options.map((item) => getIsGroup(item) ? (_jsx("optgroup", { label: item.label, children: item.options.map((option) => (_jsx("option", { disabled: option.isDisabled, value: option.value, children: option.label }, option.value))) }, item.label)) : (_jsx("option", { disabled: item.isDisabled, value: item.value, children: item.label }, item.value)))] }), _jsx("svg", { "aria-hidden": "true", className: "pointer-events-none absolute end-3 size-4 text-content-secondary", fill: "none", focusable: false, stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 1.75, viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", children: _jsx("path", { d: "m6 9 6 6 6-6" }) })] }));
51
+ //# sourceMappingURL=Select.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Select.js","sourceRoot":"","sources":["../../src/Select/Select.tsx"],"names":[],"mappings":";AAOA,OAAO,EACL,kBAAkB,EAClB,sBAAsB,GACvB,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACL,cAAc,EACd,gBAAgB,GACjB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAwD/C,MAAM,UAAU,GAAG,CACjB,IAAgB,EACW,EAAE,CAAC,SAAS,IAAI,IAAI,CAAA;AAEjD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,EACrB,SAAS,EACT,UAAU,GAAG,KAAK,EAClB,KAAK,EACL,QAAQ,EACR,OAAO,EACP,WAAW,EACX,IAAI,GAAG,IAAI,EACX,KAAK,EACL,GAAG,WAAW,EACF,EAAa,EAAE,CAAC,CAC5B,eAAK,SAAS,EAAC,0CAA0C,aACvD,qBACM,WAAW,gBACH,KAAK,EACjB,SAAS,EAAE,WAAW,CACpB,iLAAiL;YACjL,0DAA0D;YAC1D,2DAA2D;YAC3D,2DAA2D;YAC3D,wCAAwC;YACxC,MAAM,EACN,kBAAkB,CAAC,IAAI,CAAC,EACxB,sBAAsB,EACtB,4BAA4B,EAC5B,gBAAgB,EAChB,cAAc,EACd,SAAS,CACV,EACD,YAAY,EACV,KAAK;gBACL,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,EAE9C,QAAQ,EAAE,UAAU,EACpB,QAAQ,EAAE,CACR,WAA2C,EAC3C,EAAE;gBACF,QAAQ,EAAE,CAAC,WAAW,CAAC,aAAa,CAAC,KAAK,CAAC,CAAA;YAC7C,CAAC,aAEA,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAClC,iBAAQ,QAAQ,QAAC,KAAK,EAAC,EAAE,YACtB,WAAW,GACL,CACV,EAEA,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CACpB,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CACjB,mBAA2B,KAAK,EAAE,IAAI,CAAC,KAAK,YACzC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAC5B,iBACE,QAAQ,EAAE,MAAM,CAAC,UAAU,EAE3B,KAAK,EAAE,MAAM,CAAC,KAAK,YAElB,MAAM,CAAC,KAAK,IAHR,MAAM,CAAC,KAAK,CAIV,CACV,CAAC,IATW,IAAI,CAAC,KAAK,CAUd,CACZ,CAAC,CAAC,CAAC,CACF,iBACE,QAAQ,EAAE,IAAI,CAAC,UAAU,EAEzB,KAAK,EAAE,IAAI,CAAC,KAAK,YAEhB,IAAI,CAAC,KAAK,IAHN,IAAI,CAAC,KAAK,CAIR,CACV,CACF,IACM,EAET,6BAGc,MAAM,EAClB,SAAS,EAAC,kEAAkE,EAC5E,IAAI,EAAC,MAAM,EACX,SAAS,EAAE,KAAK,EAChB,MAAM,EAAC,cAAc,EACrB,aAAa,EAAC,OAAO,EACrB,cAAc,EAAC,OAAO,EACtB,WAAW,EAAE,IAAI,EACjB,OAAO,EAAC,WAAW,EACnB,KAAK,EAAC,4BAA4B,YAElC,eAAM,CAAC,EAAC,cAAc,GAAG,GACrB,IACF,CACP,CAAA"}
@@ -0,0 +1,61 @@
1
+ import type { ReactNode } from "react";
2
+ export type SortDirection = "ascending" | "descending";
3
+ export type SortableTableHeaderProps = {
4
+ children: ReactNode;
5
+ className?: string;
6
+ /**
7
+ * The direction **this column** is sorted in, or `undefined` when
8
+ * it is not the sorted one. Undefined is not "ascending by
9
+ * default": a table has exactly one sorted column and every other
10
+ * header has to say `aria-sort="none"`, which is what tells a
11
+ * screen reader which one is live.
12
+ */
13
+ direction?: SortDirection;
14
+ /** Called with the direction the header is asking to move to. */
15
+ onSort: (direction: SortDirection) => void;
16
+ };
17
+ /**
18
+ * **The header cell, and nothing else.** No `DataTable`, no rows, no
19
+ * comparator — the plan is explicit about that, and the reason is
20
+ * that sorting *data* is app logic that differs per column type,
21
+ * while announcing *that* a column is sorted is markup every table
22
+ * gets wrong the same way.
23
+ *
24
+ * ### `aria-sort` does not exist anywhere in the fleet
25
+ *
26
+ * Not in mux-magic, not in rip-deck, not in castkit. mux-magic's
27
+ * `FileExplorerModal` — the one sortable table there is — renders
28
+ * the state as a character:
29
+ *
30
+ * ```tsx
31
+ * {sortDirection === "asc" ? "▲" : "▼"}
32
+ * ```
33
+ *
34
+ * A screen reader announces that as "black up-pointing triangle", if
35
+ * the font has it at all, and the sandbox's headless Chromium does
36
+ * not have it — so the same glyph measures blank in a screenshot.
37
+ * Sighted users see a direction, everyone else gets nothing, and no
38
+ * automated check anywhere reports it: axe has no rule for a missing
39
+ * `aria-sort`, because a table with none is a table that is simply
40
+ * not sorted as far as the accessibility tree knows.
41
+ *
42
+ * This is therefore the one component in M6 whose payoff is not
43
+ * duplication — there is a single site — but a class of failure
44
+ * that is invisible to every gate the fleet has.
45
+ *
46
+ * ### The cell announces, the button acts
47
+ *
48
+ * `aria-sort` belongs on the `<th>`; the click target is a `<button>`
49
+ * inside it. Putting the role on the button instead is the common
50
+ * mistake and it breaks the grid semantics — a `<th>` that is a
51
+ * button is no longer a column header to anything reading the table
52
+ * structure.
53
+ *
54
+ * The name stays the column's name. It is tempting to append the
55
+ * state — "Name, sorted ascending" — and that duplicates what
56
+ * `aria-sort` already says, so the column is announced twice and
57
+ * `getByRole("columnheader", { name: "Name" })` stops matching the
58
+ * moment somebody sorts it.
59
+ */
60
+ export declare const SortableTableHeader: ({ children, className, direction, onSort, }: SortableTableHeaderProps) => ReactNode;
61
+ //# sourceMappingURL=SortableTableHeader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SortableTableHeader.d.ts","sourceRoot":"","sources":["../../src/SortableTableHeader/SortableTableHeader.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAKtC,MAAM,MAAM,aAAa,GAAG,WAAW,GAAG,YAAY,CAAA;AAEtD,MAAM,MAAM,wBAAwB,GAAG;IACrC,QAAQ,EAAE,SAAS,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,aAAa,CAAA;IACzB,iEAAiE;IACjE,MAAM,EAAE,CAAC,SAAS,EAAE,aAAa,KAAK,IAAI,CAAA;CAC3C,CAAA;AAQD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,eAAO,MAAM,mBAAmB,GAAI,6CAKjC,wBAAwB,KAAG,SAsD7B,CAAA"}
@@ -0,0 +1,56 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { FOCUS_RING_CLASS } from "../intentStyles.js";
3
+ import { toClassName } from "../toClassName.js";
4
+ const NEXT_DIRECTION = {
5
+ ascending: "descending",
6
+ descending: "ascending",
7
+ };
8
+ /**
9
+ * **The header cell, and nothing else.** No `DataTable`, no rows, no
10
+ * comparator — the plan is explicit about that, and the reason is
11
+ * that sorting *data* is app logic that differs per column type,
12
+ * while announcing *that* a column is sorted is markup every table
13
+ * gets wrong the same way.
14
+ *
15
+ * ### `aria-sort` does not exist anywhere in the fleet
16
+ *
17
+ * Not in mux-magic, not in rip-deck, not in castkit. mux-magic's
18
+ * `FileExplorerModal` — the one sortable table there is — renders
19
+ * the state as a character:
20
+ *
21
+ * ```tsx
22
+ * {sortDirection === "asc" ? "▲" : "▼"}
23
+ * ```
24
+ *
25
+ * A screen reader announces that as "black up-pointing triangle", if
26
+ * the font has it at all, and the sandbox's headless Chromium does
27
+ * not have it — so the same glyph measures blank in a screenshot.
28
+ * Sighted users see a direction, everyone else gets nothing, and no
29
+ * automated check anywhere reports it: axe has no rule for a missing
30
+ * `aria-sort`, because a table with none is a table that is simply
31
+ * not sorted as far as the accessibility tree knows.
32
+ *
33
+ * This is therefore the one component in M6 whose payoff is not
34
+ * duplication — there is a single site — but a class of failure
35
+ * that is invisible to every gate the fleet has.
36
+ *
37
+ * ### The cell announces, the button acts
38
+ *
39
+ * `aria-sort` belongs on the `<th>`; the click target is a `<button>`
40
+ * inside it. Putting the role on the button instead is the common
41
+ * mistake and it breaks the grid semantics — a `<th>` that is a
42
+ * button is no longer a column header to anything reading the table
43
+ * structure.
44
+ *
45
+ * The name stays the column's name. It is tempting to append the
46
+ * state — "Name, sorted ascending" — and that duplicates what
47
+ * `aria-sort` already says, so the column is announced twice and
48
+ * `getByRole("columnheader", { name: "Name" })` stops matching the
49
+ * moment somebody sorts it.
50
+ */
51
+ export const SortableTableHeader = ({ children, className, direction, onSort, }) => (_jsx("th", { "aria-sort": direction ?? "none", className: toClassName("border-border-subtle border-b p-0 text-start", className), scope: "col", children: _jsxs("button", { className: toClassName("flex w-full cursor-pointer items-center gap-1.5 bg-transparent px-3 py-2 text-start font-medium text-content-secondary text-xs uppercase tracking-wide transition-colors duration-(--duration-fast) ease-standard", "hover:text-content-primary", FOCUS_RING_CLASS), onClick: () => {
52
+ onSort(direction === undefined
53
+ ? "ascending"
54
+ : NEXT_DIRECTION[direction]);
55
+ }, type: "button", children: [children, _jsx("svg", { "aria-hidden": "true", className: toClassName("size-3.5 shrink-0 transition-[transform,opacity] duration-(--duration-fast) ease-standard", direction === undefined && "opacity-0", direction === "descending" && "rotate-180"), fill: "none", focusable: false, stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", children: _jsx("path", { d: "m6 15 6-6 6 6" }) })] }) }));
56
+ //# sourceMappingURL=SortableTableHeader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SortableTableHeader.js","sourceRoot":"","sources":["../../src/SortableTableHeader/SortableTableHeader.tsx"],"names":[],"mappings":";AAEA,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAmB/C,MAAM,cAAc,GAClB;IACE,SAAS,EAAE,YAAY;IACvB,UAAU,EAAE,WAAW;CACxB,CAAA;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,EAClC,QAAQ,EACR,SAAS,EACT,SAAS,EACT,MAAM,GACmB,EAAa,EAAE,CAAC,CACzC,0BAMa,SAAS,IAAI,MAAM,EAC9B,SAAS,EAAE,WAAW,CACpB,8CAA8C,EAC9C,SAAS,CACV,EACD,KAAK,EAAC,KAAK,YAEX,kBACE,SAAS,EAAE,WAAW,CACpB,mNAAmN,EACnN,4BAA4B,EAC5B,gBAAgB,CACjB,EACD,OAAO,EAAE,GAAG,EAAE;YACZ,MAAM,CACJ,SAAS,KAAK,SAAS;gBACrB,CAAC,CAAC,WAAW;gBACb,CAAC,CAAC,cAAc,CAAC,SAAS,CAAC,CAC9B,CAAA;QACH,CAAC,EACD,IAAI,EAAC,QAAQ,aAEZ,QAAQ,EAET,6BAIc,MAAM,EAClB,SAAS,EAAE,WAAW,CACpB,2FAA2F,EAC3F,SAAS,KAAK,SAAS,IAAI,WAAW,EACtC,SAAS,KAAK,YAAY,IAAI,YAAY,CAC3C,EACD,IAAI,EAAC,MAAM,EACX,SAAS,EAAE,KAAK,EAChB,MAAM,EAAC,cAAc,EACrB,aAAa,EAAC,OAAO,EACrB,cAAc,EAAC,OAAO,EACtB,WAAW,EAAE,CAAC,EACd,OAAO,EAAC,WAAW,EACnB,KAAK,EAAC,4BAA4B,YAElC,eAAM,CAAC,EAAC,eAAe,GAAG,GACtB,IACC,GACN,CACN,CAAA"}
@@ -0,0 +1,54 @@
1
+ import type { IntentName } from "@charcuterie/tokens";
2
+ import type { ReactNode } from "react";
3
+ export type ToastProps = {
4
+ /** Detail under the title. Optional — many toasts are one line. */
5
+ children?: ReactNode;
6
+ className?: string;
7
+ /**
8
+ * Milliseconds at `visible` before it starts leaving. `0` pins it
9
+ * open, which is what a toast carrying an action has to do — WCAG
10
+ * 2.2.1 gives a user the right to finish reading.
11
+ */
12
+ duration?: number;
13
+ /** How long `exiting` lasts, matched to the transition below. */
14
+ exitDuration?: number;
15
+ intent?: IntentName;
16
+ /** Called once, when the machine reaches its terminal state. */
17
+ onRemove: () => void;
18
+ /**
19
+ * A string, not a `ReactNode`. It is what the region announces and
20
+ * what names this toast's own dismiss button — a stack of three
21
+ * toasts all offering "Dismiss" is three matches for one query,
22
+ * which `expectAgentDrivable` treats as a failure.
23
+ */
24
+ title: string;
25
+ };
26
+ /**
27
+ * One toast, and its whole job is the four-state machine the plan
28
+ * specified and M2 left unbuilt.
29
+ *
30
+ * ### Why a `Status`, when a boolean and a timeout would do
31
+ *
32
+ * Because a boolean cannot express *why* the node is still mounted.
33
+ * A toast that is leaving is still painted, still occupies its slot,
34
+ * and must not be announced again — and `isVisible === false` says
35
+ * none of that. The states are what a reader needs to know the
36
+ * difference between "arriving", "leaving", and "gone but not yet
37
+ * unmounted", and `createStatus` makes the illegal ones throw:
38
+ * `visible → removed` skips the exit and is rejected, and `removed`
39
+ * is terminal so a late timer cannot remove the same toast twice.
40
+ *
41
+ * That last one is not hypothetical. Hover-to-pause plus a dismiss
42
+ * button means two independent timers can be in flight, and the
43
+ * hand-rolled version of this component reconciles them with a
44
+ * `clearTimeout` and a ref. Here the second one throws.
45
+ *
46
+ * ### It does not announce itself
47
+ *
48
+ * No `role="status"` and no `aria-live` on this element — the
49
+ * `ToastRegion` around it owns that, and a live region inside a live
50
+ * region is a message announced twice. See that component for the
51
+ * reason there is only ever one, and it is polite.
52
+ */
53
+ export declare const Toast: ({ children, className, duration, exitDuration, intent, onRemove, title, }: ToastProps) => ReactNode;
54
+ //# sourceMappingURL=Toast.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Toast.d.ts","sourceRoot":"","sources":["../../src/Toast/Toast.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AACrD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAYtC,MAAM,MAAM,UAAU,GAAG;IACvB,mEAAmE;IACnE,QAAQ,CAAC,EAAE,SAAS,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,iEAAiE;IACjE,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,MAAM,CAAC,EAAE,UAAU,CAAA;IACnB,gEAAgE;IAChE,QAAQ,EAAE,MAAM,IAAI,CAAA;IACpB;;;;;OAKG;IACH,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AASD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,KAAK,GAAI,2EAQnB,UAAU,KAAG,SA+If,CAAA"}