@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
package/README.md
CHANGED
|
@@ -18,6 +18,15 @@ M3 ships the **P0 pure-presentation** set: `Spinner`, `Skeleton`, `Button`, `Ico
|
|
|
18
18
|
is a composition that could not be expressed until
|
|
19
19
|
[`Tabs` moved onto `SinglePicker`](../../docs/decisions/2026-07-30-tab-selection-is-a-single-picker.md).
|
|
20
20
|
|
|
21
|
+
**M6a ships the nine P1 components**: `Select`, `Field`, `Accordion`, `LogViewer`, `Menu`,
|
|
22
|
+
`Tooltip`, `SortableTableHeader`, `Toast` (with `ToastRegion`), and `FileDropZone`. Twenty
|
|
23
|
+
five components in all. Four of them changed rules this package had already written down —
|
|
24
|
+
a menu is [named by its trigger](../../docs/decisions/2026-07-31-a-menu-is-named-by-its-trigger.md),
|
|
25
|
+
an accordion panel is [a `group` rather than a landmark](../../docs/decisions/2026-07-31-an-accordion-panel-is-a-group-not-a-landmark.md),
|
|
26
|
+
[`Select` owns no state at all](../../docs/decisions/2026-07-31-select-is-uncontrolled-because-the-platform-owns-the-value.md),
|
|
27
|
+
and [not every boolean is a state kind](../../docs/decisions/2026-07-31-not-every-boolean-is-a-state-kind.md).
|
|
28
|
+
Full write-up: [M6a handoff](../../docs/2026-07-31-m6a-the-p1-components.md).
|
|
29
|
+
|
|
21
30
|
## Install and wire
|
|
22
31
|
|
|
23
32
|
```ts
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export type AccordionHeadingLevel = 2 | 3 | 4 | 5 | 6;
|
|
3
|
+
export type AccordionItem = {
|
|
4
|
+
content: ReactNode;
|
|
5
|
+
isDisabled?: boolean;
|
|
6
|
+
key: string;
|
|
7
|
+
label: ReactNode;
|
|
8
|
+
};
|
|
9
|
+
export type AccordionProps = {
|
|
10
|
+
className?: string;
|
|
11
|
+
/** **Initial** only. Charcuterie owns it from then on. */
|
|
12
|
+
expandedKeys?: string[];
|
|
13
|
+
/**
|
|
14
|
+
* The heading level the triggers sit at. An accordion inside a
|
|
15
|
+
* `Card` that already has an `<h2>` needs `3`, and getting this
|
|
16
|
+
* wrong is a document-outline break that no gate here can see —
|
|
17
|
+
* it depends entirely on the page.
|
|
18
|
+
*/
|
|
19
|
+
headingLevel?: AccordionHeadingLevel;
|
|
20
|
+
/**
|
|
21
|
+
* Several sections open at once. The default is **exclusive**,
|
|
22
|
+
* because that is what every one of the fleet's disclosure groups
|
|
23
|
+
* does today.
|
|
24
|
+
*/
|
|
25
|
+
isMultiple?: boolean;
|
|
26
|
+
items: AccordionItem[];
|
|
27
|
+
onChange?: (expandedKeys: string[]) => void;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Eleven sites in the fleet, and the plan called it "nearly free
|
|
31
|
+
* once `Tabs` exists". It is — the exclusive mode is
|
|
32
|
+
* `VisibilityGroup` and nothing else — but the reason it is worth
|
|
33
|
+
* shipping is not the saving.
|
|
34
|
+
*
|
|
35
|
+
* ### The two-owners bug, live, in the app this milestone migrates
|
|
36
|
+
*
|
|
37
|
+
* mux-magic's `JobStepsDisclosure` is built on `<details>`, and
|
|
38
|
+
* `<details>` **owns `open`**. It also wants the state in a Jotai
|
|
39
|
+
* atom so a job can be expanded from elsewhere. Reconciling the two
|
|
40
|
+
* takes three separate mechanisms:
|
|
41
|
+
*
|
|
42
|
+
* ```tsx
|
|
43
|
+
* const detailsRef = useRef<HTMLDetailsElement>(null)
|
|
44
|
+
* const skipNextToggleRef = useRef(isOpen)
|
|
45
|
+
*
|
|
46
|
+
* useEffect(() => {
|
|
47
|
+
* if (detailsRef.current) detailsRef.current.open = isOpen
|
|
48
|
+
* }, [isOpen])
|
|
49
|
+
*
|
|
50
|
+
* const handleToggle = (event) => {
|
|
51
|
+
* if (skipNextToggleRef.current) {
|
|
52
|
+
* skipNextToggleRef.current = false
|
|
53
|
+
* return // ← swallow the toggle our own write just fired
|
|
54
|
+
* }
|
|
55
|
+
* setStepsOpen(…)
|
|
56
|
+
* }
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* That is the Radix argument from `Popover` pointed at the platform:
|
|
60
|
+
* a ref to reach past React, an effect to push state into the DOM,
|
|
61
|
+
* and a guard to stop the DOM's echo coming back. Every one of the
|
|
62
|
+
* three exists only because two things hold one fact.
|
|
63
|
+
*
|
|
64
|
+
* So this is a `<button aria-expanded>` and a `role="region"` — the
|
|
65
|
+
* ARIA Authoring Practices accordion — with `VisibilityGroup`
|
|
66
|
+
* holding the state alone. `<details name="…">` is the platform's
|
|
67
|
+
* own exclusive accordion and was rejected for the same reason, plus
|
|
68
|
+
* one it cannot fix: **a `<summary>` cannot be disabled**, and four
|
|
69
|
+
* of the fleet's eleven sites have a section that is unreachable
|
|
70
|
+
* until a job produces it.
|
|
71
|
+
*
|
|
72
|
+
* ### Two kinds, one component, and both hooks always run
|
|
73
|
+
*
|
|
74
|
+
* `isMultiple` picks between `VisibilityGroup` and `MultiplePicker`,
|
|
75
|
+
* and hooks cannot be conditional — so both are created and one is
|
|
76
|
+
* consulted. That is two empty stores and no DOM, which is cheaper
|
|
77
|
+
* than the alternative: reimplementing exclusivity on top of
|
|
78
|
+
* `MultiplePicker` by deselecting the others, which is precisely the
|
|
79
|
+
* logic `VisibilityGroup` exists to own once.
|
|
80
|
+
*/
|
|
81
|
+
export declare const Accordion: ({ className, expandedKeys, headingLevel, isMultiple, items, onChange, }: AccordionProps) => ReactNode;
|
|
82
|
+
//# sourceMappingURL=Accordion.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Accordion.d.ts","sourceRoot":"","sources":["../../src/Accordion/Accordion.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAKtC,MAAM,MAAM,qBAAqB,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;AAErD,MAAM,MAAM,aAAa,GAAG;IAC1B,OAAO,EAAE,SAAS,CAAA;IAClB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,SAAS,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,0DAA0D;IAC1D,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;IACvB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,qBAAqB,CAAA;IACpC;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,KAAK,EAAE,aAAa,EAAE,CAAA;IACtB,QAAQ,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,IAAI,CAAA;CAC5C,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AACH,eAAO,MAAM,SAAS,GAAI,yEAOvB,cAAc,KAAG,SAkFnB,CAAA"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useMultiplePicker, useUniqueId, useVisibilityGroup, } from "@charcuterie/logic";
|
|
3
|
+
import { toClassName } from "../toClassName.js";
|
|
4
|
+
import { AccordionSection } from "./AccordionSection.js";
|
|
5
|
+
/**
|
|
6
|
+
* Eleven sites in the fleet, and the plan called it "nearly free
|
|
7
|
+
* once `Tabs` exists". It is — the exclusive mode is
|
|
8
|
+
* `VisibilityGroup` and nothing else — but the reason it is worth
|
|
9
|
+
* shipping is not the saving.
|
|
10
|
+
*
|
|
11
|
+
* ### The two-owners bug, live, in the app this milestone migrates
|
|
12
|
+
*
|
|
13
|
+
* mux-magic's `JobStepsDisclosure` is built on `<details>`, and
|
|
14
|
+
* `<details>` **owns `open`**. It also wants the state in a Jotai
|
|
15
|
+
* atom so a job can be expanded from elsewhere. Reconciling the two
|
|
16
|
+
* takes three separate mechanisms:
|
|
17
|
+
*
|
|
18
|
+
* ```tsx
|
|
19
|
+
* const detailsRef = useRef<HTMLDetailsElement>(null)
|
|
20
|
+
* const skipNextToggleRef = useRef(isOpen)
|
|
21
|
+
*
|
|
22
|
+
* useEffect(() => {
|
|
23
|
+
* if (detailsRef.current) detailsRef.current.open = isOpen
|
|
24
|
+
* }, [isOpen])
|
|
25
|
+
*
|
|
26
|
+
* const handleToggle = (event) => {
|
|
27
|
+
* if (skipNextToggleRef.current) {
|
|
28
|
+
* skipNextToggleRef.current = false
|
|
29
|
+
* return // ← swallow the toggle our own write just fired
|
|
30
|
+
* }
|
|
31
|
+
* setStepsOpen(…)
|
|
32
|
+
* }
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* That is the Radix argument from `Popover` pointed at the platform:
|
|
36
|
+
* a ref to reach past React, an effect to push state into the DOM,
|
|
37
|
+
* and a guard to stop the DOM's echo coming back. Every one of the
|
|
38
|
+
* three exists only because two things hold one fact.
|
|
39
|
+
*
|
|
40
|
+
* So this is a `<button aria-expanded>` and a `role="region"` — the
|
|
41
|
+
* ARIA Authoring Practices accordion — with `VisibilityGroup`
|
|
42
|
+
* holding the state alone. `<details name="…">` is the platform's
|
|
43
|
+
* own exclusive accordion and was rejected for the same reason, plus
|
|
44
|
+
* one it cannot fix: **a `<summary>` cannot be disabled**, and four
|
|
45
|
+
* of the fleet's eleven sites have a section that is unreachable
|
|
46
|
+
* until a job produces it.
|
|
47
|
+
*
|
|
48
|
+
* ### Two kinds, one component, and both hooks always run
|
|
49
|
+
*
|
|
50
|
+
* `isMultiple` picks between `VisibilityGroup` and `MultiplePicker`,
|
|
51
|
+
* and hooks cannot be conditional — so both are created and one is
|
|
52
|
+
* consulted. That is two empty stores and no DOM, which is cheaper
|
|
53
|
+
* than the alternative: reimplementing exclusivity on top of
|
|
54
|
+
* `MultiplePicker` by deselecting the others, which is precisely the
|
|
55
|
+
* logic `VisibilityGroup` exists to own once.
|
|
56
|
+
*/
|
|
57
|
+
export const Accordion = ({ className, expandedKeys, headingLevel = 3, isMultiple = false, items, onChange, }) => {
|
|
58
|
+
const baseId = useUniqueId();
|
|
59
|
+
const [initialKey] = expandedKeys ?? [];
|
|
60
|
+
const group = useVisibilityGroup({
|
|
61
|
+
onChange: (visibleKey) => {
|
|
62
|
+
if (!isMultiple) {
|
|
63
|
+
onChange?.(visibleKey === null ? [] : [visibleKey]);
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
visibleKey: initialKey ?? null,
|
|
67
|
+
});
|
|
68
|
+
const picker = useMultiplePicker({
|
|
69
|
+
onChange: (selectedValues) => {
|
|
70
|
+
if (isMultiple) {
|
|
71
|
+
onChange?.([...selectedValues]);
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
selectedValues: expandedKeys,
|
|
75
|
+
});
|
|
76
|
+
/**
|
|
77
|
+
* Visible **or** pending, the same load-bearing fallback `Tabs`
|
|
78
|
+
* needs and for the same reason: members register from an effect,
|
|
79
|
+
* so on the first paint the intent still lives in the pending
|
|
80
|
+
* field. Reading only the settled one renders every section
|
|
81
|
+
* collapsed and corrects itself a frame later — a flash on a
|
|
82
|
+
* desktop and a visible jump on the kiosk Pi.
|
|
83
|
+
*
|
|
84
|
+
* **And only until something registers**, which `Tabs` never had
|
|
85
|
+
* to say because it seeds itself from the first *enabled* tab. A
|
|
86
|
+
* key that can never register — a disabled section's — stays
|
|
87
|
+
* pending forever, so an unqualified fallback renders it expanded
|
|
88
|
+
* permanently and `isDisabled` quietly stops meaning anything.
|
|
89
|
+
* Found by the story that passes a disabled key on purpose.
|
|
90
|
+
*
|
|
91
|
+
* Every member registers in one commit, so this really is "before
|
|
92
|
+
* the first effects ran" rather than a race between them.
|
|
93
|
+
*/
|
|
94
|
+
const hasRegistered = isMultiple
|
|
95
|
+
? picker.registeredValues.length > 0
|
|
96
|
+
: group.registeredKeys.length > 0;
|
|
97
|
+
const expanded = isMultiple
|
|
98
|
+
? new Set([
|
|
99
|
+
...picker.selectedValues,
|
|
100
|
+
...(hasRegistered ? [] : picker.pendingValues),
|
|
101
|
+
])
|
|
102
|
+
: new Set([
|
|
103
|
+
group.visibleKey ??
|
|
104
|
+
(hasRegistered ? null : group.pendingKey),
|
|
105
|
+
].filter((key) => key !== null));
|
|
106
|
+
return (_jsx("div", { className: toClassName("flex flex-col divide-y divide-border-subtle overflow-hidden rounded-md border border-border-subtle", className), children: items.map((item, index) => (_jsx(AccordionSection, { headingLevel: headingLevel, id: `${baseId}-trigger-${index}`, isExpanded: expanded.has(item.key), item: item, onToggle: isMultiple ? picker.toggle : group.toggle, panelId: `${baseId}-panel-${index}`, register: isMultiple ? picker.register : group.register }, item.key))) }));
|
|
107
|
+
};
|
|
108
|
+
//# sourceMappingURL=Accordion.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Accordion.js","sourceRoot":"","sources":["../../src/Accordion/Accordion.tsx"],"names":[],"mappings":";AAAA,OAAO,EACL,iBAAiB,EACjB,WAAW,EACX,kBAAkB,GACnB,MAAM,oBAAoB,CAAA;AAG3B,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC/C,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAwB,CAAA;AAgCzD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,EACxB,SAAS,EACT,YAAY,EACZ,YAAY,GAAG,CAAC,EAChB,UAAU,GAAG,KAAK,EAClB,KAAK,EACL,QAAQ,GACO,EAAa,EAAE;IAC9B,MAAM,MAAM,GAAG,WAAW,EAAE,CAAA;IAE5B,MAAM,CAAC,UAAU,CAAC,GAAG,YAAY,IAAI,EAAE,CAAA;IAEvC,MAAM,KAAK,GAAG,kBAAkB,CAAS;QACvC,QAAQ,EAAE,CAAC,UAAU,EAAE,EAAE;YACvB,IAAI,CAAC,UAAU,EAAE,CAAC;gBAChB,QAAQ,EAAE,CAAC,UAAU,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAA;YACrD,CAAC;QACH,CAAC;QACD,UAAU,EAAE,UAAU,IAAI,IAAI;KAC/B,CAAC,CAAA;IAEF,MAAM,MAAM,GAAG,iBAAiB,CAAS;QACvC,QAAQ,EAAE,CAAC,cAAc,EAAE,EAAE;YAC3B,IAAI,UAAU,EAAE,CAAC;gBACf,QAAQ,EAAE,CAAC,CAAC,GAAG,cAAc,CAAC,CAAC,CAAA;YACjC,CAAC;QACH,CAAC;QACD,cAAc,EAAE,YAAY;KAC7B,CAAC,CAAA;IAEF;;;;;;;;;;;;;;;;;OAiBG;IACH,MAAM,aAAa,GAAG,UAAU;QAC9B,CAAC,CAAC,MAAM,CAAC,gBAAgB,CAAC,MAAM,GAAG,CAAC;QACpC,CAAC,CAAC,KAAK,CAAC,cAAc,CAAC,MAAM,GAAG,CAAC,CAAA;IAEnC,MAAM,QAAQ,GAAG,UAAU;QACzB,CAAC,CAAC,IAAI,GAAG,CAAC;YACN,GAAG,MAAM,CAAC,cAAc;YACxB,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC;SAC/C,CAAC;QACJ,CAAC,CAAC,IAAI,GAAG,CACL;YACE,KAAK,CAAC,UAAU;gBACd,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC;SAC5C,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,KAAK,IAAI,CAAC,CAChC,CAAA;IAEL,OAAO,CACL,cACE,SAAS,EAAE,WAAW,CACpB,oGAAoG,EACpG,SAAS,CACV,YAEA,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,CAC1B,KAAC,gBAAgB,IACf,YAAY,EAAE,YAAY,EAC1B,EAAE,EAAE,GAAG,MAAM,YAAY,KAAK,EAAE,EAChC,UAAU,EAAE,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,EAClC,IAAI,EAAE,IAAI,EAEV,QAAQ,EACN,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,EAE3C,OAAO,EAAE,GAAG,MAAM,UAAU,KAAK,EAAE,EACnC,QAAQ,EACN,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,IAN1C,IAAI,CAAC,GAAG,CAQb,CACH,CAAC,GACE,CACP,CAAA;AACH,CAAC,CAAA"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { AccordionHeadingLevel, AccordionItem } from "./Accordion.tsx";
|
|
3
|
+
export type AccordionSectionProps = {
|
|
4
|
+
headingLevel: AccordionHeadingLevel;
|
|
5
|
+
id: string;
|
|
6
|
+
isExpanded: boolean;
|
|
7
|
+
item: AccordionItem;
|
|
8
|
+
onToggle: (key: string) => void;
|
|
9
|
+
panelId: string;
|
|
10
|
+
/**
|
|
11
|
+
* `VisibilityGroup.register` or `MultiplePicker.register`,
|
|
12
|
+
* whichever kind the accordion is running. Both have this shape,
|
|
13
|
+
* which is the point of the kinds being separate rather than one
|
|
14
|
+
* configurable one.
|
|
15
|
+
*/
|
|
16
|
+
register: (key: string) => () => void;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Its own file for the same reason `TabTrigger` is: **registration
|
|
20
|
+
* is an effect**, and an effect cannot run in a loop.
|
|
21
|
+
*
|
|
22
|
+
* A disabled section stays out of the group entirely — it is not a
|
|
23
|
+
* member, so nothing can expand it, including a consumer passing its
|
|
24
|
+
* key in `expandedKeys`. That is registration-as-membership again,
|
|
25
|
+
* and it is the behaviour a `<summary>` cannot have at all.
|
|
26
|
+
*/
|
|
27
|
+
export declare const AccordionSection: ({ headingLevel, id, isExpanded, item, onToggle, panelId, register, }: AccordionSectionProps) => ReactNode;
|
|
28
|
+
//# sourceMappingURL=AccordionSection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AccordionSection.d.ts","sourceRoot":"","sources":["../../src/Accordion/AccordionSection.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAKtC,OAAO,KAAK,EACV,qBAAqB,EACrB,aAAa,EACd,MAAM,iBAAiB,CAAA;AAmBxB,MAAM,MAAM,qBAAqB,GAAG;IAClC,YAAY,EAAE,qBAAqB,CAAA;IACnC,EAAE,EAAE,MAAM,CAAA;IACV,UAAU,EAAE,OAAO,CAAA;IACnB,IAAI,EAAE,aAAa,CAAA;IACnB,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;IAC/B,OAAO,EAAE,MAAM,CAAA;IACf;;;;;OAKG;IACH,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,IAAI,CAAA;CACtC,CAAA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,GAAI,sEAQ9B,qBAAqB,KAAG,SAsG1B,CAAA"}
|
|
@@ -0,0 +1,53 @@
|
|
|
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
|
+
* Written out rather than interpolated into a tag name. React
|
|
7
|
+
* accepts `` `h${level}` `` perfectly well; a reader and a grep do
|
|
8
|
+
* not, and "which heading levels does this component emit" should be
|
|
9
|
+
* answerable by looking.
|
|
10
|
+
*/
|
|
11
|
+
const HEADING_TAG = {
|
|
12
|
+
2: "h2",
|
|
13
|
+
3: "h3",
|
|
14
|
+
4: "h4",
|
|
15
|
+
5: "h5",
|
|
16
|
+
6: "h6",
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Its own file for the same reason `TabTrigger` is: **registration
|
|
20
|
+
* is an effect**, and an effect cannot run in a loop.
|
|
21
|
+
*
|
|
22
|
+
* A disabled section stays out of the group entirely — it is not a
|
|
23
|
+
* member, so nothing can expand it, including a consumer passing its
|
|
24
|
+
* key in `expandedKeys`. That is registration-as-membership again,
|
|
25
|
+
* and it is the behaviour a `<summary>` cannot have at all.
|
|
26
|
+
*/
|
|
27
|
+
export const AccordionSection = ({ headingLevel, id, isExpanded, item, onToggle, panelId, register, }) => {
|
|
28
|
+
const { isDisabled = false, key, label } = item;
|
|
29
|
+
const Heading = HEADING_TAG[headingLevel];
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (isDisabled) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
return register(key);
|
|
35
|
+
}, [isDisabled, key, register]);
|
|
36
|
+
return (_jsxs("div", { className: "bg-surface-raised", children: [_jsx(Heading, { className: "m-0 text-md", children: _jsxs("button", { "aria-controls": panelId, "aria-expanded": isExpanded, className: toClassName("flex w-full cursor-pointer items-center justify-between gap-3 bg-transparent px-4 py-3 text-start font-medium text-content-primary text-sm transition-colors duration-(--duration-fast) ease-standard", "hover:bg-intent-neutral-surface", isDisabled &&
|
|
37
|
+
"cursor-not-allowed text-content-disabled hover:bg-transparent", FOCUS_RING_CLASS), disabled: isDisabled, id: id, onClick: () => {
|
|
38
|
+
onToggle(key);
|
|
39
|
+
}, type: "button", children: [label, _jsx("svg", { "aria-hidden": "true", className: toClassName("size-4 shrink-0 text-content-secondary transition-transform duration-(--duration-fast) ease-standard", isExpanded && "rotate-180"), 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" }) })] }) }), _jsx("div", { "aria-labelledby": id, className: "px-4 pt-1 pb-4 text-content-secondary text-sm",
|
|
40
|
+
// `hidden` rather than unmounting, matching `Tabs`. A panel
|
|
41
|
+
// that unmounts loses a scroll position, a partially typed
|
|
42
|
+
// form, and any subscription its content opened — and the
|
|
43
|
+
// fleet's log panes are exactly that.
|
|
44
|
+
hidden: !isExpanded, id: panelId,
|
|
45
|
+
// `group`, not `region`. A `group` is explicitly "a set of
|
|
46
|
+
// UI objects **not** intended to be included in a page
|
|
47
|
+
// summary", so it carries `aria-labelledby` and the
|
|
48
|
+
// trigger↔panel relationship without joining the landmark
|
|
49
|
+
// list — which is exactly the distinction the APG's caveat
|
|
50
|
+
// is reaching for and does not have a role for.
|
|
51
|
+
role: "group", children: item.content })] }));
|
|
52
|
+
};
|
|
53
|
+
//# sourceMappingURL=AccordionSection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AccordionSection.js","sourceRoot":"","sources":["../../src/Accordion/AccordionSection.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;AAM/C;;;;;GAKG;AACH,MAAM,WAAW,GAGb;IACF,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,IAAI;CACR,CAAA;AAkBD;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,EAC/B,YAAY,EACZ,EAAE,EACF,UAAU,EACV,IAAI,EACJ,QAAQ,EACR,OAAO,EACP,QAAQ,GACc,EAAa,EAAE;IACrC,MAAM,EAAE,UAAU,GAAG,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,IAAI,CAAA;IAE/C,MAAM,OAAO,GAAG,WAAW,CAAC,YAAY,CAAC,CAAA;IAEzC,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,eAAK,SAAS,EAAC,mBAAmB,aAChC,KAAC,OAAO,IAAC,SAAS,EAAC,aAAa,YAC9B,mCACiB,OAAO,mBACP,UAAU,EACzB,SAAS,EAAE,WAAW,CACpB,uMAAuM,EACvM,iCAAiC,EACjC,UAAU;wBACR,+DAA+D,EACjE,gBAAgB,CACjB,EACD,QAAQ,EAAE,UAAU,EACpB,EAAE,EAAE,EAAE,EACN,OAAO,EAAE,GAAG,EAAE;wBACZ,QAAQ,CAAC,GAAG,CAAC,CAAA;oBACf,CAAC,EACD,IAAI,EAAC,QAAQ,aAEZ,KAAK,EAEN,6BAGc,MAAM,EAClB,SAAS,EAAE,WAAW,CACpB,sGAAsG,EACtG,UAAU,IAAI,YAAY,CAC3B,EACD,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,IACC,GACD,EA0BV,iCACmB,EAAE,EACnB,SAAS,EAAC,+CAA+C;gBACzD,4DAA4D;gBAC5D,2DAA2D;gBAC3D,0DAA0D;gBAC1D,sCAAsC;gBACtC,MAAM,EAAE,CAAC,UAAU,EACnB,EAAE,EAAE,OAAO;gBACX,2DAA2D;gBAC3D,uDAAuD;gBACvD,oDAAoD;gBACpD,0DAA0D;gBAC1D,2DAA2D;gBAC3D,gDAAgD;gBAChD,IAAI,EAAC,OAAO,YAEX,IAAI,CAAC,OAAO,GACT,IACF,CACP,CAAA;AACH,CAAC,CAAA"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from "react";
|
|
2
|
+
export type FieldProps = {
|
|
3
|
+
/**
|
|
4
|
+
* The control. **Cloned, not wrapped** — the same slot contract
|
|
5
|
+
* as `Popover`'s trigger, so a `Field` can go around an `<input>`,
|
|
6
|
+
* a `<textarea>`, a `Select`, or an app's own control without
|
|
7
|
+
* changing that control's layout.
|
|
8
|
+
*/
|
|
9
|
+
children: ReactElement;
|
|
10
|
+
className?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Standing help — units, a format, a warning about what the value
|
|
13
|
+
* does. Announced with the control, before any error.
|
|
14
|
+
*/
|
|
15
|
+
description?: ReactNode;
|
|
16
|
+
/**
|
|
17
|
+
* Present means invalid. There is no separate `isInvalid`, because
|
|
18
|
+
* two sources for one fact is how a control ends up
|
|
19
|
+
* `aria-invalid="true"` with nothing saying why — which is what
|
|
20
|
+
* `RegexWithFlagsField` does in mux-magic today.
|
|
21
|
+
*/
|
|
22
|
+
error?: ReactNode;
|
|
23
|
+
isRequired?: boolean;
|
|
24
|
+
label: ReactNode;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Sixteen files in the fleet spell a label, a control, and a hint,
|
|
28
|
+
* and no two spell the wiring the same way. `FieldLabel` in
|
|
29
|
+
* mux-magic renders a `<label>` with no `htmlFor` at all — which
|
|
30
|
+
* looks correct, reads correctly to a sighted user, and gives a
|
|
31
|
+
* screen reader an unnamed textbox.
|
|
32
|
+
*
|
|
33
|
+
* ### The ids are static, and that is why `createLinkedIds` is not here
|
|
34
|
+
*
|
|
35
|
+
* The plan expected `useLinkedIds` to do this wiring. It does not
|
|
36
|
+
* fit, for the same reason it did not fit `Tabs`: that kind exists
|
|
37
|
+
* for a **dynamic** pairing, where the registration multiset is what
|
|
38
|
+
* stops an `aria-describedby` naming a node React has already
|
|
39
|
+
* removed. A field renders its own description and its own error, so
|
|
40
|
+
* it knows at render which of them exist — the ids come from one
|
|
41
|
+
* `useUniqueId` and a conditional join, and they cannot get out of
|
|
42
|
+
* step at all.
|
|
43
|
+
*
|
|
44
|
+
* That leaves `createLinkedIds` with **no consumer in this package
|
|
45
|
+
* across twenty-five components**, which is worth saying plainly
|
|
46
|
+
* rather than manufacturing a use for. It is the one state kind the
|
|
47
|
+
* component layer has never needed.
|
|
48
|
+
*
|
|
49
|
+
* ### Order matters inside `aria-describedby`
|
|
50
|
+
*
|
|
51
|
+
* Description first, error second. A screen reader reads the list in
|
|
52
|
+
* order, and "millisecondsonly — required" is a different sentence
|
|
53
|
+
* from "required — milliseconds only". Nothing enforces this but the
|
|
54
|
+
* order of the array below, which is why it is not built from an
|
|
55
|
+
* object.
|
|
56
|
+
*
|
|
57
|
+
* ### The error is described, not announced
|
|
58
|
+
*
|
|
59
|
+
* No `role="alert"` and no live region here, deliberately. An error
|
|
60
|
+
* bound through `aria-describedby` is read when the control takes
|
|
61
|
+
* focus, which is when the user can act on it; a live region on the
|
|
62
|
+
* same text announces it a second time, from nowhere, while the
|
|
63
|
+
* caret is elsewhere. A form that fails on submit should say so
|
|
64
|
+
* once, at the top, with the `Alert` this library already ships —
|
|
65
|
+
* and then the user tabs into the field and hears the specific
|
|
66
|
+
* reason.
|
|
67
|
+
*/
|
|
68
|
+
export declare const Field: ({ children, className, description, error, isRequired, label, }: FieldProps) => ReactNode;
|
|
69
|
+
//# sourceMappingURL=Field.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Field.d.ts","sourceRoot":"","sources":["../../src/Field/Field.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAIpD,MAAM,MAAM,UAAU,GAAG;IACvB;;;;;OAKG;IACH,QAAQ,EAAE,YAAY,CAAA;IACtB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;OAGG;IACH,WAAW,CAAC,EAAE,SAAS,CAAA;IACvB;;;;;OAKG;IACH,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,KAAK,EAAE,SAAS,CAAA;CACjB,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,eAAO,MAAM,KAAK,GAAI,iEAOnB,UAAU,KAAG,SAgFf,CAAA"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useClonedChild, useUniqueId, } from "@charcuterie/logic";
|
|
3
|
+
import { toClassName } from "../toClassName.js";
|
|
4
|
+
/**
|
|
5
|
+
* Sixteen files in the fleet spell a label, a control, and a hint,
|
|
6
|
+
* and no two spell the wiring the same way. `FieldLabel` in
|
|
7
|
+
* mux-magic renders a `<label>` with no `htmlFor` at all — which
|
|
8
|
+
* looks correct, reads correctly to a sighted user, and gives a
|
|
9
|
+
* screen reader an unnamed textbox.
|
|
10
|
+
*
|
|
11
|
+
* ### The ids are static, and that is why `createLinkedIds` is not here
|
|
12
|
+
*
|
|
13
|
+
* The plan expected `useLinkedIds` to do this wiring. It does not
|
|
14
|
+
* fit, for the same reason it did not fit `Tabs`: that kind exists
|
|
15
|
+
* for a **dynamic** pairing, where the registration multiset is what
|
|
16
|
+
* stops an `aria-describedby` naming a node React has already
|
|
17
|
+
* removed. A field renders its own description and its own error, so
|
|
18
|
+
* it knows at render which of them exist — the ids come from one
|
|
19
|
+
* `useUniqueId` and a conditional join, and they cannot get out of
|
|
20
|
+
* step at all.
|
|
21
|
+
*
|
|
22
|
+
* That leaves `createLinkedIds` with **no consumer in this package
|
|
23
|
+
* across twenty-five components**, which is worth saying plainly
|
|
24
|
+
* rather than manufacturing a use for. It is the one state kind the
|
|
25
|
+
* component layer has never needed.
|
|
26
|
+
*
|
|
27
|
+
* ### Order matters inside `aria-describedby`
|
|
28
|
+
*
|
|
29
|
+
* Description first, error second. A screen reader reads the list in
|
|
30
|
+
* order, and "millisecondsonly — required" is a different sentence
|
|
31
|
+
* from "required — milliseconds only". Nothing enforces this but the
|
|
32
|
+
* order of the array below, which is why it is not built from an
|
|
33
|
+
* object.
|
|
34
|
+
*
|
|
35
|
+
* ### The error is described, not announced
|
|
36
|
+
*
|
|
37
|
+
* No `role="alert"` and no live region here, deliberately. An error
|
|
38
|
+
* bound through `aria-describedby` is read when the control takes
|
|
39
|
+
* focus, which is when the user can act on it; a live region on the
|
|
40
|
+
* same text announces it a second time, from nowhere, while the
|
|
41
|
+
* caret is elsewhere. A form that fails on submit should say so
|
|
42
|
+
* once, at the top, with the `Alert` this library already ships —
|
|
43
|
+
* and then the user tabs into the field and hears the specific
|
|
44
|
+
* reason.
|
|
45
|
+
*/
|
|
46
|
+
export const Field = ({ children, className, description, error, isRequired = false, label, }) => {
|
|
47
|
+
const baseId = useUniqueId();
|
|
48
|
+
const controlId = `${baseId}-control`;
|
|
49
|
+
const descriptionId = `${baseId}-description`;
|
|
50
|
+
const errorId = `${baseId}-error`;
|
|
51
|
+
const describedBy = [
|
|
52
|
+
description === undefined ? null : descriptionId,
|
|
53
|
+
error === undefined ? null : errorId,
|
|
54
|
+
]
|
|
55
|
+
.filter(Boolean)
|
|
56
|
+
.join(" ");
|
|
57
|
+
const clonedControl = useClonedChild(children, {
|
|
58
|
+
"aria-describedby": describedBy === "" ? undefined : describedBy,
|
|
59
|
+
// `aria-invalid` is derived from the error's presence, never
|
|
60
|
+
// passed. A control marked invalid with no described reason is
|
|
61
|
+
// a screen reader saying "invalid" and stopping.
|
|
62
|
+
"aria-invalid": error === undefined ? undefined : true,
|
|
63
|
+
// Both, and they are not redundant: `required` is the
|
|
64
|
+
// constraint the browser validates and `aria-required` is the
|
|
65
|
+
// one it announces on a control the browser does not validate
|
|
66
|
+
// — a `role="combobox"`, or anything composite.
|
|
67
|
+
"aria-required": isRequired || undefined,
|
|
68
|
+
id: controlId,
|
|
69
|
+
required: isRequired || undefined,
|
|
70
|
+
});
|
|
71
|
+
return (_jsxs("div", { className: toClassName("flex flex-col gap-1.5", className), children: [_jsxs("label", { className: "font-medium text-content-primary text-sm", htmlFor: controlId, children: [label, isRequired ? (_jsx("span", { "aria-hidden": "true", className: "ms-1 text-intent-danger-content", children: "*" })) : null] }), clonedControl, description === undefined ? null : (_jsx("p", { className: "text-content-secondary text-xs", id: descriptionId, children: description })), error === undefined ? null : (_jsx("p", { className: "text-intent-danger-content text-xs", id: errorId, children: error }))] }));
|
|
72
|
+
};
|
|
73
|
+
//# sourceMappingURL=Field.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Field.js","sourceRoot":"","sources":["../../src/Field/Field.tsx"],"names":[],"mappings":";AAAA,OAAO,EACL,cAAc,EACd,WAAW,GACZ,MAAM,oBAAoB,CAAA;AAG3B,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AA2B/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,CAAC,EACpB,QAAQ,EACR,SAAS,EACT,WAAW,EACX,KAAK,EACL,UAAU,GAAG,KAAK,EAClB,KAAK,GACM,EAAa,EAAE;IAC1B,MAAM,MAAM,GAAG,WAAW,EAAE,CAAA;IAE5B,MAAM,SAAS,GAAG,GAAG,MAAM,UAAU,CAAA;IAErC,MAAM,aAAa,GAAG,GAAG,MAAM,cAAc,CAAA;IAE7C,MAAM,OAAO,GAAG,GAAG,MAAM,QAAQ,CAAA;IAEjC,MAAM,WAAW,GAAG;QAClB,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,aAAa;QAChD,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO;KACrC;SACE,MAAM,CAAC,OAAO,CAAC;SACf,IAAI,CAAC,GAAG,CAAC,CAAA;IAEZ,MAAM,aAAa,GAAG,cAAc,CAAC,QAAQ,EAAE;QAC7C,kBAAkB,EAChB,WAAW,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW;QAC9C,6DAA6D;QAC7D,+DAA+D;QAC/D,iDAAiD;QACjD,cAAc,EAAE,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI;QACtD,sDAAsD;QACtD,8DAA8D;QAC9D,8DAA8D;QAC9D,gDAAgD;QAChD,eAAe,EAAE,UAAU,IAAI,SAAS;QACxC,EAAE,EAAE,SAAS;QACb,QAAQ,EAAE,UAAU,IAAI,SAAS;KAClC,CAAC,CAAA;IAEF,OAAO,CACL,eACE,SAAS,EAAE,WAAW,CACpB,uBAAuB,EACvB,SAAS,CACV,aAED,iBACE,SAAS,EAAC,0CAA0C,EACpD,OAAO,EAAE,SAAS,aAEjB,KAAK,EAEL,UAAU,CAAC,CAAC,CAAC,CACZ,8BAKc,MAAM,EAClB,SAAS,EAAC,iCAAiC,kBAGtC,CACR,CAAC,CAAC,CAAC,IAAI,IACF,EAEP,aAAa,EAEb,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAClC,YACE,SAAS,EAAC,gCAAgC,EAC1C,EAAE,EAAE,aAAa,YAEhB,WAAW,GACV,CACL,EAEA,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAC5B,YACE,SAAS,EAAC,oCAAoC,EAC9C,EAAE,EAAE,OAAO,YAEV,KAAK,GACJ,CACL,IACG,CACP,CAAA;AACH,CAAC,CAAA"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export type FileDropZoneProps = {
|
|
3
|
+
/** An `accept` list, passed straight to the input. */
|
|
4
|
+
accept?: string;
|
|
5
|
+
className?: string;
|
|
6
|
+
/** Secondary line — the formats taken, a size cap. */
|
|
7
|
+
description?: ReactNode;
|
|
8
|
+
isDisabled?: boolean;
|
|
9
|
+
isMultiple?: boolean;
|
|
10
|
+
/** The control's accessible name, and its visible prompt. */
|
|
11
|
+
label: string;
|
|
12
|
+
onDropFiles: (files: File[]) => void;
|
|
13
|
+
/**
|
|
14
|
+
* Dropped text, when there is any. A browser hands over a dragged
|
|
15
|
+
* link as `text/plain` and no files at all, which is what
|
|
16
|
+
* gallery-downloader's page is built entirely around — it takes
|
|
17
|
+
* `dataTransfer.getData("text")` and never looks at `.files`.
|
|
18
|
+
*/
|
|
19
|
+
onDropText?: (text: string) => void;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* The one drag target in the fleet is gallery-downloader's, and it
|
|
23
|
+
* gets the hard part right: `dragenter`/`dragleave` fire once per
|
|
24
|
+
* *element* the pointer crosses, not once per zone, so a naive
|
|
25
|
+
* implementation flickers over every child — and that page keeps a
|
|
26
|
+
* `dragDepth` counter, which is the correct fix. It is reproduced
|
|
27
|
+
* below with credit.
|
|
28
|
+
*
|
|
29
|
+
* What it has no answer for is the part that cannot be fixed by
|
|
30
|
+
* counting:
|
|
31
|
+
*
|
|
32
|
+
* ### Dragging is pointer-only, so the zone is a label around an input
|
|
33
|
+
*
|
|
34
|
+
* There is **no keyboard gesture for drag-and-drop**. WCAG 2.5.7
|
|
35
|
+
* (AA since 2.2) requires a single-pointer alternative for any
|
|
36
|
+
* dragging movement, and the honest one has existed since 1995: a
|
|
37
|
+
* real `<input type="file">`. So this component is a `<label>`
|
|
38
|
+
* wrapping a visually-hidden input — the whole zone is the label's
|
|
39
|
+
* click target, Tab reaches the input, Space and Enter open the
|
|
40
|
+
* picker, and the drop handlers are an *enhancement* on top of a
|
|
41
|
+
* control that already works without them.
|
|
42
|
+
*
|
|
43
|
+
* That ordering is the entire design. Building the drop zone first
|
|
44
|
+
* and bolting on a "or browse" link is how the fleet's version ended
|
|
45
|
+
* up with an `alert()` as its error channel and no keyboard path at
|
|
46
|
+
* all.
|
|
47
|
+
*
|
|
48
|
+
* ### `dragover` must be prevented, and the reason is a footgun
|
|
49
|
+
*
|
|
50
|
+
* A browser's default for `dragover` is *reject the drop*. Not
|
|
51
|
+
* preventing it means `drop` never fires — nothing errors, the
|
|
52
|
+
* cursor shows a "no entry" sign, and the zone looks like it has a
|
|
53
|
+
* broken handler when it in fact has no handler running at all.
|
|
54
|
+
*
|
|
55
|
+
* ### It reports text as well as files
|
|
56
|
+
*
|
|
57
|
+
* Dragging a link gives you `text/plain` and an empty `files` list.
|
|
58
|
+
* A zone that only reads `.files` silently ignores the single most
|
|
59
|
+
* common thing a user drags into a downloader, which is why
|
|
60
|
+
* `onDropText` is here rather than being someone's follow-up.
|
|
61
|
+
*/
|
|
62
|
+
export declare const FileDropZone: ({ accept, className, description, isDisabled, isMultiple, label, onDropFiles, onDropText, }: FileDropZoneProps) => ReactNode;
|
|
63
|
+
//# sourceMappingURL=FileDropZone.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FileDropZone.d.ts","sourceRoot":"","sources":["../../src/FileDropZone/FileDropZone.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAa,SAAS,EAAE,MAAM,OAAO,CAAA;AAKjD,MAAM,MAAM,iBAAiB,GAAG;IAC9B,sDAAsD;IACtD,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,sDAAsD;IACtD,WAAW,CAAC,EAAE,SAAS,CAAA;IACvB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,6DAA6D;IAC7D,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,IAAI,CAAA;IACpC;;;;;OAKG;IACH,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CACpC,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,eAAO,MAAM,YAAY,GAAI,6FAS1B,iBAAiB,KAAG,SA6JtB,CAAA"}
|