@charcuterie/ui 0.1.1 → 0.2.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.
- package/README.md +9 -0
- package/dist/Accordion/Accordion.d.ts +82 -0
- package/dist/Accordion/Accordion.d.ts.map +1 -0
- package/dist/Accordion/Accordion.js +108 -0
- package/dist/Accordion/Accordion.js.map +1 -0
- package/dist/Accordion/AccordionSection.d.ts +28 -0
- package/dist/Accordion/AccordionSection.d.ts.map +1 -0
- package/dist/Accordion/AccordionSection.js +53 -0
- package/dist/Accordion/AccordionSection.js.map +1 -0
- package/dist/Field/Field.d.ts +69 -0
- package/dist/Field/Field.d.ts.map +1 -0
- package/dist/Field/Field.js +73 -0
- package/dist/Field/Field.js.map +1 -0
- package/dist/FileDropZone/FileDropZone.d.ts +63 -0
- package/dist/FileDropZone/FileDropZone.d.ts.map +1 -0
- package/dist/FileDropZone/FileDropZone.js +122 -0
- package/dist/FileDropZone/FileDropZone.js.map +1 -0
- package/dist/LogViewer/LogViewer.d.ts +88 -0
- package/dist/LogViewer/LogViewer.d.ts.map +1 -0
- package/dist/LogViewer/LogViewer.js +104 -0
- package/dist/LogViewer/LogViewer.js.map +1 -0
- package/dist/Menu/Menu.d.ts +81 -0
- package/dist/Menu/Menu.d.ts.map +1 -0
- package/dist/Menu/Menu.js +199 -0
- package/dist/Menu/Menu.js.map +1 -0
- package/dist/Menu/MenuAction.d.ts +24 -0
- package/dist/Menu/MenuAction.d.ts.map +1 -0
- package/dist/Menu/MenuAction.js +37 -0
- package/dist/Menu/MenuAction.js.map +1 -0
- package/dist/Select/Select.d.ts +87 -0
- package/dist/Select/Select.d.ts.map +1 -0
- package/dist/Select/Select.js +51 -0
- package/dist/Select/Select.js.map +1 -0
- package/dist/SortableTableHeader/SortableTableHeader.d.ts +61 -0
- package/dist/SortableTableHeader/SortableTableHeader.d.ts.map +1 -0
- package/dist/SortableTableHeader/SortableTableHeader.js +56 -0
- package/dist/SortableTableHeader/SortableTableHeader.js.map +1 -0
- package/dist/Toast/Toast.d.ts +54 -0
- package/dist/Toast/Toast.d.ts.map +1 -0
- package/dist/Toast/Toast.js +121 -0
- package/dist/Toast/Toast.js.map +1 -0
- package/dist/Toast/ToastRegion.d.ts +57 -0
- package/dist/Toast/ToastRegion.d.ts.map +1 -0
- package/dist/Toast/ToastRegion.js +41 -0
- package/dist/Toast/ToastRegion.js.map +1 -0
- package/dist/Toast/toastLifecycle.d.ts +34 -0
- package/dist/Toast/toastLifecycle.d.ts.map +1 -0
- package/dist/Toast/toastLifecycle.js +25 -0
- package/dist/Toast/toastLifecycle.js.map +1 -0
- package/dist/Tooltip/Tooltip.d.ts +68 -0
- package/dist/Tooltip/Tooltip.d.ts.map +1 -0
- package/dist/Tooltip/Tooltip.js +104 -0
- package/dist/Tooltip/Tooltip.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -1
- package/dist/mountStory.testHelpers.d.ts +0 -11
- package/dist/mountStory.testHelpers.d.ts.map +1 -1
- package/dist/mountStory.testHelpers.js +27 -0
- package/dist/mountStory.testHelpers.js.map +1 -1
- package/package.json +1 -1
- package/src/Accordion/Accordion.mdx +83 -0
- package/src/Accordion/Accordion.stories.tsx +146 -0
- package/src/Accordion/Accordion.test.tsx +163 -0
- package/src/Accordion/Accordion.tsx +182 -0
- package/src/Accordion/AccordionSection.tsx +163 -0
- package/src/Field/Field.mdx +74 -0
- package/src/Field/Field.stories.tsx +144 -0
- package/src/Field/Field.test.tsx +142 -0
- package/src/Field/Field.tsx +163 -0
- package/src/FileDropZone/FileDropZone.mdx +78 -0
- package/src/FileDropZone/FileDropZone.stories.tsx +136 -0
- package/src/FileDropZone/FileDropZone.test.tsx +230 -0
- package/src/FileDropZone/FileDropZone.tsx +234 -0
- package/src/LogViewer/LogViewer.mdx +88 -0
- package/src/LogViewer/LogViewer.stories.tsx +135 -0
- package/src/LogViewer/LogViewer.test.tsx +173 -0
- package/src/LogViewer/LogViewer.tsx +195 -0
- package/src/Menu/Menu.mdx +103 -0
- package/src/Menu/Menu.stories.tsx +161 -0
- package/src/Menu/Menu.test.tsx +202 -0
- package/src/Menu/Menu.tsx +300 -0
- package/src/Menu/MenuAction.tsx +95 -0
- package/src/Select/Select.mdx +74 -0
- package/src/Select/Select.stories.tsx +126 -0
- package/src/Select/Select.test.tsx +129 -0
- package/src/Select/Select.tsx +200 -0
- package/src/SortableTableHeader/SortableTableHeader.mdx +67 -0
- package/src/SortableTableHeader/SortableTableHeader.stories.tsx +207 -0
- package/src/SortableTableHeader/SortableTableHeader.test.tsx +180 -0
- package/src/SortableTableHeader/SortableTableHeader.tsx +131 -0
- package/src/Toast/Toast.mdx +84 -0
- package/src/Toast/Toast.stories.tsx +192 -0
- package/src/Toast/Toast.test.tsx +217 -0
- package/src/Toast/Toast.tsx +224 -0
- package/src/Toast/ToastRegion.tsx +98 -0
- package/src/Toast/toastLifecycle.ts +45 -0
- package/src/Tooltip/Tooltip.mdx +69 -0
- package/src/Tooltip/Tooltip.stories.tsx +104 -0
- package/src/Tooltip/Tooltip.test.tsx +138 -0
- package/src/Tooltip/Tooltip.tsx +169 -0
- package/src/index.ts +43 -0
- package/src/mountStory.testHelpers.ts +30 -0
- package/src/sourceRules.test.ts +221 -30
|
@@ -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"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useStatus } from "@charcuterie/logic";
|
|
3
|
+
import { useEffect, useRef } from "react";
|
|
4
|
+
import { IconButton } from "../IconButton/IconButton.js";
|
|
5
|
+
import { INTENT_APPEARANCE_CLASS, INTENT_CONTENT_CLASS, } from "../intentStyles.js";
|
|
6
|
+
import { toClassName } from "../toClassName.js";
|
|
7
|
+
import { toastTransitions } from "./toastLifecycle.js";
|
|
8
|
+
const ENTER_CLASS = {
|
|
9
|
+
entering: "translate-y-2 opacity-0",
|
|
10
|
+
exiting: "translate-y-1 opacity-0",
|
|
11
|
+
removed: "opacity-0",
|
|
12
|
+
visible: "translate-y-0 opacity-100",
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* One toast, and its whole job is the four-state machine the plan
|
|
16
|
+
* specified and M2 left unbuilt.
|
|
17
|
+
*
|
|
18
|
+
* ### Why a `Status`, when a boolean and a timeout would do
|
|
19
|
+
*
|
|
20
|
+
* Because a boolean cannot express *why* the node is still mounted.
|
|
21
|
+
* A toast that is leaving is still painted, still occupies its slot,
|
|
22
|
+
* and must not be announced again — and `isVisible === false` says
|
|
23
|
+
* none of that. The states are what a reader needs to know the
|
|
24
|
+
* difference between "arriving", "leaving", and "gone but not yet
|
|
25
|
+
* unmounted", and `createStatus` makes the illegal ones throw:
|
|
26
|
+
* `visible → removed` skips the exit and is rejected, and `removed`
|
|
27
|
+
* is terminal so a late timer cannot remove the same toast twice.
|
|
28
|
+
*
|
|
29
|
+
* That last one is not hypothetical. Hover-to-pause plus a dismiss
|
|
30
|
+
* button means two independent timers can be in flight, and the
|
|
31
|
+
* hand-rolled version of this component reconciles them with a
|
|
32
|
+
* `clearTimeout` and a ref. Here the second one throws.
|
|
33
|
+
*
|
|
34
|
+
* ### It does not announce itself
|
|
35
|
+
*
|
|
36
|
+
* No `role="status"` and no `aria-live` on this element — the
|
|
37
|
+
* `ToastRegion` around it owns that, and a live region inside a live
|
|
38
|
+
* region is a message announced twice. See that component for the
|
|
39
|
+
* reason there is only ever one, and it is polite.
|
|
40
|
+
*/
|
|
41
|
+
export const Toast = ({ children, className, duration = 5000, exitDuration = 200, intent = "neutral", onRemove, title, }) => {
|
|
42
|
+
const { is, status, transitionTo } = useStatus({
|
|
43
|
+
initialState: "entering",
|
|
44
|
+
transitions: toastTransitions,
|
|
45
|
+
});
|
|
46
|
+
const onRemoveRef = useRef(onRemove);
|
|
47
|
+
onRemoveRef.current = onRemove;
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (!is("entering")) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// A frame, not zero. The element has to be painted in its
|
|
53
|
+
// `entering` position before the class changes, or the browser
|
|
54
|
+
// coalesces both into one style recalculation and there is no
|
|
55
|
+
// transition to see — the single most common reason a CSS
|
|
56
|
+
// enter-animation "does nothing".
|
|
57
|
+
const frame = globalThis.requestAnimationFrame(() => {
|
|
58
|
+
// Guarded, because `is` and `transitionTo` are stable — this
|
|
59
|
+
// effect runs once, so the frame is *not* cancelled when the
|
|
60
|
+
// status changes underneath it. A dismiss during the enter
|
|
61
|
+
// frame moves the toast to `exiting`, and `exiting → visible`
|
|
62
|
+
// is a legal transition (it is how hover-to-pause works), so
|
|
63
|
+
// an unguarded write here brings a dismissed toast straight
|
|
64
|
+
// back and restarts its duration timer.
|
|
65
|
+
if (is("entering")) {
|
|
66
|
+
transitionTo("visible");
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
return () => {
|
|
70
|
+
globalThis.cancelAnimationFrame(frame);
|
|
71
|
+
};
|
|
72
|
+
}, [is, transitionTo]);
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (status !== "visible" || duration === 0) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const timer = globalThis.setTimeout(() => {
|
|
78
|
+
transitionTo("exiting");
|
|
79
|
+
}, duration);
|
|
80
|
+
return () => {
|
|
81
|
+
globalThis.clearTimeout(timer);
|
|
82
|
+
};
|
|
83
|
+
}, [duration, status, transitionTo]);
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
if (status !== "exiting") {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const timer = globalThis.setTimeout(() => {
|
|
89
|
+
transitionTo("removed");
|
|
90
|
+
}, exitDuration);
|
|
91
|
+
return () => {
|
|
92
|
+
globalThis.clearTimeout(timer);
|
|
93
|
+
};
|
|
94
|
+
}, [exitDuration, status, transitionTo]);
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
if (status === "removed") {
|
|
97
|
+
onRemoveRef.current();
|
|
98
|
+
}
|
|
99
|
+
}, [status]);
|
|
100
|
+
return (_jsxs("li", { className: toClassName("pointer-events-auto flex items-start gap-3 rounded-md border p-3 shadow-medium transition-[opacity,transform] duration-(--duration-normal) ease-standard", INTENT_APPEARANCE_CLASS[intent].soft, ENTER_CLASS[status], className),
|
|
101
|
+
// Pausing on hover is WCAG 2.2.1's "pause" for a moving
|
|
102
|
+
// deadline, and it is the reason `exiting → visible` is a
|
|
103
|
+
// legal transition rather than an oddity in the table.
|
|
104
|
+
onMouseEnter: () => {
|
|
105
|
+
if (status === "exiting") {
|
|
106
|
+
transitionTo("visible");
|
|
107
|
+
}
|
|
108
|
+
}, children: [_jsxs("div", { className: "flex min-w-0 flex-col gap-0.5", children: [_jsx("span", { className: toClassName("font-medium text-sm", INTENT_CONTENT_CLASS[intent]), children: title }), children === undefined ? null : (_jsx("span", { className: "text-content-secondary text-xs", children: children }))] }), _jsx(IconButton, { appearance: "ghost", className: "ms-auto shrink-0",
|
|
109
|
+
// The name has to be distinct per toast, or a stack of three
|
|
110
|
+
// gives `getByRole("button", { name: "Dismiss" })` three
|
|
111
|
+
// matches and `expectAgentDrivable` fails the story — which
|
|
112
|
+
// is the point of that helper: an agent picking one of three
|
|
113
|
+
// at random is worse than finding none.
|
|
114
|
+
label: `Dismiss ${title}`, onClick: () => {
|
|
115
|
+
if (status !== "exiting" &&
|
|
116
|
+
status !== "removed") {
|
|
117
|
+
transitionTo("exiting");
|
|
118
|
+
}
|
|
119
|
+
}, size: "sm", children: _jsx("svg", { "aria-hidden": "true", className: "size-[1.15em] shrink-0", 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 6 12 12M18 6 6 18" }) }) })] }));
|
|
120
|
+
};
|
|
121
|
+
//# sourceMappingURL=Toast.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Toast.js","sourceRoot":"","sources":["../../src/Toast/Toast.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { IntentName } from "@charcuterie/tokens";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
export type ToastRecord = {
|
|
4
|
+
description?: ReactNode;
|
|
5
|
+
/** `0` pins this one open. See `Toast`'s `duration`. */
|
|
6
|
+
duration?: number;
|
|
7
|
+
intent?: IntentName;
|
|
8
|
+
key: string;
|
|
9
|
+
title: string;
|
|
10
|
+
};
|
|
11
|
+
export type ToastRegionProps = {
|
|
12
|
+
className?: string;
|
|
13
|
+
/**
|
|
14
|
+
* The region's accessible name. `role="status"` takes **none from
|
|
15
|
+
* its content** — the trap `Spinner` walked into in M3 and
|
|
16
|
+
* `Popover` in M4.
|
|
17
|
+
*/
|
|
18
|
+
label?: string;
|
|
19
|
+
onDismiss: (key: string) => void;
|
|
20
|
+
toasts: ToastRecord[];
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* The live region, and the list is the caller's.
|
|
24
|
+
*
|
|
25
|
+
* ### One region, and it is polite
|
|
26
|
+
*
|
|
27
|
+
* Not two. The usual arrangement — a polite region and an assertive
|
|
28
|
+
* one, so a danger toast interrupts — was rejected because the
|
|
29
|
+
* interrupting case does not belong in a toast at all. A message
|
|
30
|
+
* that must be heard *now* is one the user has to be able to go back
|
|
31
|
+
* and read, and a toast is gone in five seconds; if it matters that
|
|
32
|
+
* much it is an `Alert` on the page, which this library already
|
|
33
|
+
* ships and which stays put.
|
|
34
|
+
*
|
|
35
|
+
* That is the same line `Field` draws for its error text, and it is
|
|
36
|
+
* worth keeping the two consistent: **this library announces once,
|
|
37
|
+
* in the place the user can act on it.**
|
|
38
|
+
*
|
|
39
|
+
* ### The stack does not own the toasts
|
|
40
|
+
*
|
|
41
|
+
* `toasts` comes in and `onDismiss` goes out — there is no
|
|
42
|
+
* `toast()` function, no imperative queue, and no context provider.
|
|
43
|
+
* An app already has a store (mux-magic has Jotai, rip-deck has its
|
|
44
|
+
* own), and a component library that ships a second one is asking
|
|
45
|
+
* every consumer to keep two lists of the same notifications in
|
|
46
|
+
* sync. The lifecycle *inside* one toast is state a store cannot
|
|
47
|
+
* see, which is why `Toast` owns that and nothing more.
|
|
48
|
+
*
|
|
49
|
+
* ### `pointer-events-none` on the container, `auto` on each toast
|
|
50
|
+
*
|
|
51
|
+
* A fixed overlay across the bottom of the viewport swallows every
|
|
52
|
+
* click in that band, including on the app behind it, and it does so
|
|
53
|
+
* invisibly. The container passes clicks through and each toast
|
|
54
|
+
* takes them back.
|
|
55
|
+
*/
|
|
56
|
+
export declare const ToastRegion: ({ className, label, onDismiss, toasts, }: ToastRegionProps) => ReactNode;
|
|
57
|
+
//# sourceMappingURL=ToastRegion.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ToastRegion.d.ts","sourceRoot":"","sources":["../../src/Toast/ToastRegion.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AACrD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAKtC,MAAM,MAAM,WAAW,GAAG;IACxB,WAAW,CAAC,EAAE,SAAS,CAAA;IACvB,wDAAwD;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE,UAAU,CAAA;IACnB,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;IAChC,MAAM,EAAE,WAAW,EAAE,CAAA;CACtB,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,eAAO,MAAM,WAAW,GAAI,0CAKzB,gBAAgB,KAAG,SA+BrB,CAAA"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { toClassName } from "../toClassName.js";
|
|
3
|
+
import { Toast } from "./Toast.js";
|
|
4
|
+
/**
|
|
5
|
+
* The live region, and the list is the caller's.
|
|
6
|
+
*
|
|
7
|
+
* ### One region, and it is polite
|
|
8
|
+
*
|
|
9
|
+
* Not two. The usual arrangement — a polite region and an assertive
|
|
10
|
+
* one, so a danger toast interrupts — was rejected because the
|
|
11
|
+
* interrupting case does not belong in a toast at all. A message
|
|
12
|
+
* that must be heard *now* is one the user has to be able to go back
|
|
13
|
+
* and read, and a toast is gone in five seconds; if it matters that
|
|
14
|
+
* much it is an `Alert` on the page, which this library already
|
|
15
|
+
* ships and which stays put.
|
|
16
|
+
*
|
|
17
|
+
* That is the same line `Field` draws for its error text, and it is
|
|
18
|
+
* worth keeping the two consistent: **this library announces once,
|
|
19
|
+
* in the place the user can act on it.**
|
|
20
|
+
*
|
|
21
|
+
* ### The stack does not own the toasts
|
|
22
|
+
*
|
|
23
|
+
* `toasts` comes in and `onDismiss` goes out — there is no
|
|
24
|
+
* `toast()` function, no imperative queue, and no context provider.
|
|
25
|
+
* An app already has a store (mux-magic has Jotai, rip-deck has its
|
|
26
|
+
* own), and a component library that ships a second one is asking
|
|
27
|
+
* every consumer to keep two lists of the same notifications in
|
|
28
|
+
* sync. The lifecycle *inside* one toast is state a store cannot
|
|
29
|
+
* see, which is why `Toast` owns that and nothing more.
|
|
30
|
+
*
|
|
31
|
+
* ### `pointer-events-none` on the container, `auto` on each toast
|
|
32
|
+
*
|
|
33
|
+
* A fixed overlay across the bottom of the viewport swallows every
|
|
34
|
+
* click in that band, including on the app behind it, and it does so
|
|
35
|
+
* invisibly. The container passes clicks through and each toast
|
|
36
|
+
* takes them back.
|
|
37
|
+
*/
|
|
38
|
+
export const ToastRegion = ({ className, label = "Notifications", onDismiss, toasts, }) => (_jsx("div", { "aria-label": label, "aria-atomic": "false", className: toClassName("pointer-events-none fixed inset-x-0 bottom-0 z-50 flex flex-col items-center gap-2 p-4", className), role: "status", children: _jsx("ul", { className: "pointer-events-none flex w-full max-w-sm list-none flex-col gap-2 p-0", children: toasts.map((toast) => (_jsx(Toast, { duration: toast.duration, intent: toast.intent, onRemove: () => {
|
|
39
|
+
onDismiss(toast.key);
|
|
40
|
+
}, title: toast.title, children: toast.description }, toast.key))) }) }));
|
|
41
|
+
//# sourceMappingURL=ToastRegion.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ToastRegion.js","sourceRoot":"","sources":["../../src/Toast/ToastRegion.tsx"],"names":[],"mappings":";AAGA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC/C,OAAO,EAAE,KAAK,EAAE,MAAM,YAAa,CAAA;AAuBnC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,EAC1B,SAAS,EACT,KAAK,GAAG,eAAe,EACvB,SAAS,EACT,MAAM,GACW,EAAa,EAAE,CAAC,CACjC,4BACc,KAAK,iBAML,OAAO,EACnB,SAAS,EAAE,WAAW,CACpB,wFAAwF,EACxF,SAAS,CACV,EACD,IAAI,EAAC,QAAQ,YAEb,aAAI,SAAS,EAAC,uEAAuE,YAClF,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CACrB,KAAC,KAAK,IACJ,QAAQ,EAAE,KAAK,CAAC,QAAQ,EACxB,MAAM,EAAE,KAAK,CAAC,MAAM,EAEpB,QAAQ,EAAE,GAAG,EAAE;gBACb,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;YACtB,CAAC,EACD,KAAK,EAAE,KAAK,CAAC,KAAK,YAEjB,KAAK,CAAC,WAAW,IANb,KAAK,CAAC,GAAG,CAOR,CACT,CAAC,GACC,GACD,CACP,CAAA"}
|