ionbase-ui 0.48.0 → 0.59.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 (101) hide show
  1. package/LICENSE +21 -0
  2. package/dist/components/Accordion.d.ts +54 -0
  3. package/dist/components/Accordion.d.ts.map +1 -0
  4. package/dist/components/Accordion.js +68 -0
  5. package/dist/components/Accordion.js.map +1 -0
  6. package/dist/components/Breadcrumb.d.ts +45 -0
  7. package/dist/components/Breadcrumb.d.ts.map +1 -0
  8. package/dist/components/Breadcrumb.js +31 -0
  9. package/dist/components/Breadcrumb.js.map +1 -0
  10. package/dist/components/Checkbox.d.ts +2 -1
  11. package/dist/components/Checkbox.d.ts.map +1 -1
  12. package/dist/components/Checkbox.js +4 -2
  13. package/dist/components/Checkbox.js.map +1 -1
  14. package/dist/components/Combobox.d.ts +86 -0
  15. package/dist/components/Combobox.d.ts.map +1 -0
  16. package/dist/components/Combobox.js +261 -0
  17. package/dist/components/Combobox.js.map +1 -0
  18. package/dist/components/Drawer.d.ts +52 -0
  19. package/dist/components/Drawer.d.ts.map +1 -0
  20. package/dist/components/Drawer.js +66 -0
  21. package/dist/components/Drawer.js.map +1 -0
  22. package/dist/components/FileUpload.d.ts +79 -0
  23. package/dist/components/FileUpload.d.ts.map +1 -0
  24. package/dist/components/FileUpload.js +218 -0
  25. package/dist/components/FileUpload.js.map +1 -0
  26. package/dist/components/ProgressBar.d.ts +46 -0
  27. package/dist/components/ProgressBar.d.ts.map +1 -0
  28. package/dist/components/ProgressBar.js +52 -0
  29. package/dist/components/ProgressBar.js.map +1 -0
  30. package/dist/components/Select.d.ts +9 -2
  31. package/dist/components/Select.d.ts.map +1 -1
  32. package/dist/components/Select.js.map +1 -1
  33. package/dist/components/Skeleton.d.ts +39 -0
  34. package/dist/components/Skeleton.d.ts.map +1 -0
  35. package/dist/components/Skeleton.js +50 -0
  36. package/dist/components/Skeleton.js.map +1 -0
  37. package/dist/components/Spinner.d.ts +45 -0
  38. package/dist/components/Spinner.d.ts.map +1 -0
  39. package/dist/components/Spinner.js +44 -0
  40. package/dist/components/Spinner.js.map +1 -0
  41. package/dist/components/Textarea.d.ts +50 -0
  42. package/dist/components/Textarea.d.ts.map +1 -0
  43. package/dist/components/Textarea.js +48 -0
  44. package/dist/components/Textarea.js.map +1 -0
  45. package/dist/components/Toggle.d.ts +2 -1
  46. package/dist/components/Toggle.d.ts.map +1 -1
  47. package/dist/components/Toggle.js +4 -2
  48. package/dist/components/Toggle.js.map +1 -1
  49. package/dist/components/index.d.ts +18 -0
  50. package/dist/components/index.d.ts.map +1 -1
  51. package/dist/components/index.js +9 -0
  52. package/dist/components/index.js.map +1 -1
  53. package/dist/components/resolve-selection.d.ts +36 -0
  54. package/dist/components/resolve-selection.d.ts.map +1 -0
  55. package/dist/components/resolve-selection.js +14 -0
  56. package/dist/components/resolve-selection.js.map +1 -0
  57. package/dist/figma-descriptions.json +204 -144
  58. package/dist/figma-map.json +2311 -1643
  59. package/dist/meta/Accordion.json +123 -0
  60. package/dist/meta/AccordionItem.json +90 -0
  61. package/dist/meta/Breadcrumb.json +87 -0
  62. package/dist/meta/BreadcrumbItem.json +78 -0
  63. package/dist/meta/Checkbox.json +18 -1
  64. package/dist/meta/Combobox.json +265 -0
  65. package/dist/meta/Drawer.json +234 -0
  66. package/dist/meta/EmptyState.json +1 -1
  67. package/dist/meta/FileUpload.json +246 -0
  68. package/dist/meta/Input.json +0 -1
  69. package/dist/meta/PhoneInput.json +39 -39
  70. package/dist/meta/Popover.json +3 -3
  71. package/dist/meta/ProgressBar.json +166 -0
  72. package/dist/meta/Select.json +4 -3
  73. package/dist/meta/Skeleton.json +120 -0
  74. package/dist/meta/Spinner.json +123 -0
  75. package/dist/meta/Textarea.json +520 -0
  76. package/dist/meta/Toggle.json +18 -1
  77. package/dist/meta/Tooltip.json +3 -3
  78. package/dist/meta/components.json +2447 -361
  79. package/dist/meta/contrast.json +3130 -227
  80. package/dist/meta/index.json +149 -7
  81. package/dist/meta/patterns/index.json +1 -1
  82. package/dist/styles/accordion.css +102 -0
  83. package/dist/styles/breadcrumb.css +72 -0
  84. package/dist/styles/combobox.css +154 -0
  85. package/dist/styles/drawer.css +181 -0
  86. package/dist/styles/empty-state.css +16 -4
  87. package/dist/styles/file-upload.css +236 -0
  88. package/dist/styles/index.css +9 -0
  89. package/dist/styles/input.css +34 -15
  90. package/dist/styles/progress-bar.css +107 -0
  91. package/dist/styles/skeleton.css +76 -0
  92. package/dist/styles/spinner.css +70 -0
  93. package/dist/styles/textarea.css +178 -0
  94. package/dist/styles/tokens/base.css +0 -1
  95. package/dist/styles/tokens/theme-dark.css +0 -1
  96. package/dist/tokens/index.d.ts +0 -6
  97. package/dist/tokens/index.d.ts.map +1 -1
  98. package/dist/tokens/index.js +0 -5
  99. package/dist/tokens/index.js.map +1 -1
  100. package/llms.txt +2 -2
  101. package/package.json +19 -19
@@ -0,0 +1,261 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useMemo, useRef, useState } from 'react';
4
+ import { Overlay, usePopover, useComboBox, useListBox, useOption, useButton, useFilter, DismissButton, mergeProps, } from 'react-aria';
5
+ import { useComboBoxState, Item } from 'react-stately';
6
+ const ChevronDown = () => (_jsx("svg", { viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", focusable: "false", children: _jsx("path", { d: "m6 9 6 6 6-6", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }));
7
+ /* ------------------------------------------------------------------ option */
8
+ function ComboboxOptionRow({ item, state, }) {
9
+ const ref = useRef(null);
10
+ const { optionProps, isSelected, isFocused, isDisabled } = useOption({ key: item.key }, state, ref);
11
+ const option = item.value;
12
+ return (_jsxs("li", { ...optionProps, ref: ref, className: [
13
+ 'ion-combobox-menu__option',
14
+ isSelected ? 'ion-combobox-menu__option--selected' : '',
15
+ isFocused ? 'ion-combobox-menu__option--focused' : '',
16
+ isDisabled ? 'ion-combobox-menu__option--disabled' : '',
17
+ ]
18
+ .filter(Boolean)
19
+ .join(' '), "data-focused": isFocused || undefined, "data-selected": isSelected || undefined, "data-disabled": isDisabled || undefined, children: [_jsx("span", { className: "ion-combobox-menu__label", children: item.rendered }), option?.description && (_jsx("span", { className: "ion-combobox-menu__description", children: option.description }))] }));
20
+ }
21
+ /** Breathing room kept between the menu and the viewport edge, in px. */
22
+ const GUTTER = 8;
23
+ /**
24
+ * The listbox, in its own component because it is mounted only while open.
25
+ *
26
+ * Same split as Popover and Drawer, and for the same reason: `usePopover`
27
+ * positions against a ref in an effect that runs when the component CALLING it
28
+ * mounts. Called beside `useComboBoxState` in the parent, that is the moment
29
+ * the input mounts, and the panel is then positioned once, at the origin,
30
+ * before it exists.
31
+ */
32
+ function ComboboxListBox({ state, listBoxRef, popoverRef, triggerRef, listBoxProps, emptyLabel, }) {
33
+ const { popoverProps } = usePopover({
34
+ triggerRef,
35
+ popoverRef,
36
+ placement: 'bottom start',
37
+ offset: 4,
38
+ /*
39
+ * Non-modal, unlike every other overlay in this system. A modal popover
40
+ * moves focus into itself and hides the rest of the page — correct for a
41
+ * dialog, fatal here, because the user is still typing in the input and
42
+ * focus must never leave it. The input keeps focus and drives the list
43
+ * through `aria-activedescendant`.
44
+ */
45
+ isNonModal: true,
46
+ }, state);
47
+ const { listBoxProps: innerListBoxProps } = useListBox({ ...listBoxProps, disallowEmptySelection: true }, state, listBoxRef);
48
+ /*
49
+ * The menu matches the field's width rather than hugging its content.
50
+ * `usePopover` sizes to the panel, and a list narrower or wider than the
51
+ * control it belongs to reads as a different element. Measured rather than
52
+ * assumed, because the field is fluid.
53
+ *
54
+ * READ DURING RENDER, NOT FROM A ResizeObserver — and this cost an afternoon.
55
+ *
56
+ * The first version kept the width in state and synced it from a
57
+ * ResizeObserver on the field. It measured correctly and closed the menu:
58
+ * every open flashed a listbox that was gone about a hundred milliseconds
59
+ * later, before any option could take virtual focus. Writing a width onto a
60
+ * positioned popover feeds back into `usePopover`'s own position pass, and
61
+ * its scroll handling closes the overlay. Bisecting the panel down to nothing
62
+ * was the only way to see it — the symptom looked like a keyboard bug.
63
+ *
64
+ * The panel mounts only while open, so the trigger is already laid out by the
65
+ * time this runs. One read, no state, no observer, nothing to feed back.
66
+ */
67
+ const rect = triggerRef.current?.getBoundingClientRect();
68
+ /*
69
+ * Clamped to what is actually to the right of the trigger. A field running
70
+ * the full bleed of the page sits flush to the viewport edge, and a menu that
71
+ * matched its width exactly would start at the field's left and end past the
72
+ * edge — a horizontal scrollbar, which `useCloseOnScroll` reads as a scroll
73
+ * and closes the menu on. The failure mode was a combobox that could not be
74
+ * opened at all, so the clamp trades a slightly narrow menu for one that
75
+ * exists.
76
+ */
77
+ const width = rect
78
+ ? Math.min(rect.width, document.documentElement.clientWidth - rect.left - GUTTER)
79
+ : undefined;
80
+ const isEmpty = state.collection.size === 0;
81
+ return (_jsx(Overlay, { children: _jsxs("div", { ...popoverProps, ref: popoverRef, className: "ion-combobox-menu", style: { ...popoverProps.style, width }, children: [_jsx(DismissButton, { onDismiss: () => state.close() }), _jsx("ul", { ...mergeProps(innerListBoxProps), ref: listBoxRef, className: "ion-combobox-menu__list", children: [...state.collection].map((item) => (_jsx(ComboboxOptionRow, { item: item, state: state }, item.key))) }), isEmpty && (_jsx("div", { className: "ion-combobox-menu__empty", role: "presentation", children: emptyLabel })), _jsx(DismissButton, { onDismiss: () => state.close() })] }) }));
82
+ }
83
+ /* ---------------------------------------------------------------- combobox */
84
+ /**
85
+ * Combobox — a text field that filters a list, with one selected value.
86
+ *
87
+ * Drawn in Figma as `Combobox` (1370:2359) — three sizes by seven states, the
88
+ * same axes `Input` carries, because the field IS Input's box. The open list is
89
+ * a second Figma component, `Combobox Menu`, with no React export behind it.
90
+ *
91
+ * WHY THIS IS NOT `Select` WITH A SEARCH BOX
92
+ *
93
+ * `Select` wraps a native `<select>`, whose list the browser owns: it cannot be
94
+ * filtered, and its rows can only hold text. Everything above about twenty
95
+ * options needs filtering, and `Select`'s own contract has pointed at "a
96
+ * combobox — not yet in this system" since it was written. This is it.
97
+ *
98
+ * The cost is that everything the native control gave away for free — keyboard
99
+ * handling, the mobile picker, screen-reader semantics — now has to be built.
100
+ * React Aria's `useComboBox` builds it: `role="combobox"` with `aria-expanded`,
101
+ * `aria-controls` and `aria-activedescendant`, arrow keys that move a virtual
102
+ * focus while the real focus stays in the input, and Escape that reverts.
103
+ *
104
+ * FILTERING IS LOCALE-AWARE, NOT `toLowerCase().includes()`
105
+ *
106
+ * `useFilter({ sensitivity: 'base' })` is `Intl.Collator` underneath, so
107
+ * "resume" matches "résumé" and Turkish dotted/dotless I behave the way a
108
+ * Turkish reader expects. The lowercase-and-includes version fails both, and
109
+ * fails them silently in exactly the locales least likely to be tested.
110
+ */
111
+ export function Combobox({ options, label, description, errorMessage, isInvalid, isDisabled, isReadOnly, size = 'md', placeholder, selectedKey, defaultSelectedKey, onSelectionChange, inputValue, defaultInputValue, onInputChange, allowsCustomValue, menuTrigger = 'input', emptyLabel = 'No matches', buttonLabel = 'Show suggestions', name, className, wrapperClassName, id, }) {
112
+ const { contains } = useFilter({ sensitivity: 'base' });
113
+ /*
114
+ * The description is searchable, but it is NOT part of `textValue`.
115
+ *
116
+ * That distinction cost a test to find. `textValue` does double duty in
117
+ * react-stately: it is what the filter reads AND what the input is set to
118
+ * once an option is chosen. Folding the description into it made "Australia"
119
+ * searchable by "Oceania" and then wrote `Australia Oceania` into the field.
120
+ *
121
+ * So `textValue` stays the label, and the filter is widened instead —
122
+ * keyed by label, which is what a Node carries back to it.
123
+ */
124
+ const searchable = useMemo(() => {
125
+ const map = new Map();
126
+ for (const o of options) {
127
+ map.set(o.label, [o.label, o.description].filter(Boolean).join(' '));
128
+ }
129
+ return map;
130
+ }, [options]);
131
+ /*
132
+ * Filtering is done HERE, not handed to react-stately's `defaultFilter`.
133
+ *
134
+ * `defaultFilter` is only consulted for an UNCONTROLLED collection — one
135
+ * passed as `defaultItems`. Passed as `items`, as it is here, react-stately
136
+ * takes the list as final and the filter is silently ignored: no error, no
137
+ * warning, and a combobox that lists every option no matter what is typed.
138
+ * Three tests failed on it before the cause was obvious.
139
+ *
140
+ * `items` is the right half of that trade anyway. `defaultItems` snapshots
141
+ * the list on first render, so a caller whose options arrive from a fetch
142
+ * would get an empty combobox forever.
143
+ */
144
+ const [typed, setTyped] = useState(defaultInputValue ?? '');
145
+ const text = inputValue ?? typed;
146
+ const filtered = useMemo(() => {
147
+ if (!text)
148
+ return options;
149
+ /*
150
+ * An exact label match shows the WHOLE list, which looks like a bug and is
151
+ * the opposite. Selecting an option writes its label into the input, and a
152
+ * filter applied to that would narrow the list to the one row already
153
+ * chosen — so reopening it to change your mind would offer nothing else.
154
+ */
155
+ if (options.some((o) => o.label === text))
156
+ return options;
157
+ return options.filter((o) => contains(searchable.get(o.label) ?? o.label, text));
158
+ }, [contains, options, searchable, text]);
159
+ const disabledKeys = useMemo(() => options.filter((o) => o.isDisabled).map((o) => o.value), [options]);
160
+ const state = useComboBoxState({
161
+ label: typeof label === 'string' ? label : undefined,
162
+ items: filtered,
163
+ children: (item) => (_jsx(Item, { textValue: item.label, children: item.label }, item.value)),
164
+ disabledKeys,
165
+ menuTrigger,
166
+ /*
167
+ * The list stays mounted with nothing in it so the empty message can be
168
+ * shown. Left off, react-aria closes the popover the moment the filter
169
+ * matches nothing, and the user sees the list vanish with no explanation.
170
+ */
171
+ allowsEmptyCollection: true,
172
+ allowsCustomValue,
173
+ isDisabled,
174
+ isReadOnly,
175
+ selectedKey,
176
+ defaultSelectedKey,
177
+ onSelectionChange: onSelectionChange
178
+ ? (key) => onSelectionChange(key === null ? null : String(key))
179
+ : undefined,
180
+ inputValue,
181
+ defaultInputValue,
182
+ onInputChange: (value) => {
183
+ setTyped(value);
184
+ onInputChange?.(value);
185
+ },
186
+ });
187
+ const inputRef = useRef(null);
188
+ const listBoxRef = useRef(null);
189
+ const popoverRef = useRef(null);
190
+ const buttonRef = useRef(null);
191
+ const boxRef = useRef(null);
192
+ const { buttonProps: triggerProps, inputProps, listBoxProps, labelProps, descriptionProps, errorMessageProps, } = useComboBox({
193
+ /*
194
+ * `id` goes to useComboBox, not to useComboBoxState. The state owns the
195
+ * collection; the hook owns the DOM wiring — the input's id, the label's
196
+ * htmlFor, and the aria-controls that points at the listbox. Setting it
197
+ * on the state is accepted by neither and silently does nothing.
198
+ */
199
+ id,
200
+ label: typeof label === 'string' ? label : undefined,
201
+ placeholder,
202
+ isDisabled,
203
+ isReadOnly,
204
+ isInvalid,
205
+ inputRef,
206
+ listBoxRef,
207
+ popoverRef,
208
+ buttonRef,
209
+ }, state);
210
+ const { buttonProps } = useButton({
211
+ ...triggerProps,
212
+ 'aria-label': buttonLabel,
213
+ }, buttonRef);
214
+ const helper = isInvalid && errorMessage ? errorMessage : description;
215
+ const boxClassNames = [
216
+ 'ion-input',
217
+ 'ion-combobox',
218
+ size !== 'md' ? `ion-input--${size}` : '',
219
+ isInvalid ? 'ion-input--invalid' : '',
220
+ isDisabled ? 'ion-input--disabled' : '',
221
+ className || '',
222
+ ]
223
+ .filter(Boolean)
224
+ .join(' ');
225
+ return (_jsxs("div", { className: [
226
+ 'ion-field',
227
+ isInvalid ? 'ion-field--error' : '',
228
+ wrapperClassName || '',
229
+ ]
230
+ .filter(Boolean)
231
+ .join(' '), children: [label && (_jsx("label", { ...labelProps, className: "ion-field__label", children: label })), _jsxs("div", { ref: boxRef, className: boxClassNames, "data-open": state.isOpen || undefined, "data-invalid": isInvalid || undefined, "data-disabled": isDisabled || undefined, children: [_jsx("input", { ...inputProps, ref: inputRef, className: "ion-input__field" }), name && (_jsx("input", { type: "hidden", name: name, value: state.selectedKey == null ? '' : String(state.selectedKey) })), _jsx("button", { ...buttonProps, "aria-labelledby": undefined, "aria-label": buttonLabel,
232
+ /*
233
+ * The press must not move focus to the button.
234
+ *
235
+ * `useComboBox` closes the list the moment focus leaves the input, so
236
+ * a disclosure button that takes focus opens the list and closes it
237
+ * again within the same gesture. A test caught exactly that: the
238
+ * listbox was found and was gone by the next assertion. Chromium
239
+ * focuses a button on mousedown, so this is not hypothetical.
240
+ *
241
+ * `preventDefault` on mousedown is what stops that — and the caret
242
+ * goes to the field instead, which is where someone pressing a
243
+ * chevron to browse a list wants to type next anyway.
244
+ */
245
+ onMouseDown: (e) => {
246
+ e.preventDefault();
247
+ inputRef.current?.focus();
248
+ }, ref: buttonRef, type: "button",
249
+ /*
250
+ * Out of the tab order on purpose, and this is the ARIA pattern
251
+ * rather than an oversight. The input already reaches the list with
252
+ * ArrowDown, so a tab stop on the disclosure button would make every
253
+ * combobox in a form cost two tabs to pass and offer nothing the
254
+ * input does not.
255
+ */
256
+ tabIndex: -1, className: "ion-combobox__button", children: _jsx(ChevronDown, {}) })] }), state.isOpen && (_jsx(ComboboxListBox, { state: state, listBoxRef: listBoxRef, popoverRef: popoverRef, triggerRef: boxRef, listBoxProps: listBoxProps, emptyLabel: emptyLabel })), helper && (_jsx("span", { ...(isInvalid && errorMessage
257
+ ? errorMessageProps
258
+ : descriptionProps), className: "ion-field__helper", children: helper }))] }));
259
+ }
260
+ Combobox.displayName = 'Combobox';
261
+ //# sourceMappingURL=Combobox.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Combobox.js","sourceRoot":"","sources":["../../src/components/Combobox.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,52 @@
1
+ import React from 'react';
2
+ import type { AriaModalOverlayProps } from 'react-aria';
3
+ export type DrawerPlacement = 'start' | 'end' | 'top' | 'bottom';
4
+ export type DrawerSize = 'sm' | 'md' | 'lg';
5
+ export interface DrawerProps extends AriaModalOverlayProps {
6
+ /** Whether the drawer is shown. */
7
+ isOpen?: boolean;
8
+ /** Called with the next open state — `false` when the drawer asks to close. */
9
+ onOpenChange?: (isOpen: boolean) => void;
10
+ /**
11
+ * Which edge it enters from. `start` and `end` follow writing direction, so
12
+ * a right-hand drawer in English is a left-hand one in Arabic without a
13
+ * second variant.
14
+ */
15
+ placement?: DrawerPlacement;
16
+ size?: DrawerSize;
17
+ /** Required: the dialog's accessible name, wired via `aria-labelledby`. */
18
+ title: React.ReactNode;
19
+ /** Supporting copy under the title. */
20
+ description?: React.ReactNode;
21
+ /** Action row, pinned to the bottom edge. */
22
+ footer?: React.ReactNode;
23
+ showClose?: boolean;
24
+ closeLabel?: string;
25
+ children?: React.ReactNode;
26
+ className?: string;
27
+ }
28
+ /**
29
+ * Drawer — a modal panel anchored to an edge.
30
+ *
31
+ * WHY IT IS A MODAL AND NOT A SIDEBAR
32
+ *
33
+ * It takes focus, traps it, closes on Escape and on an outside click, and marks
34
+ * the rest of the page inert — all of `Modal`'s behaviour, differing only in
35
+ * where the panel sits and how it enters. A persistent side panel that does not
36
+ * do those things is layout, not a drawer, and belongs in the page rather than
37
+ * in an overlay.
38
+ *
39
+ * WHEN TO REACH FOR MODAL INSTEAD
40
+ *
41
+ * A drawer suits work that is long or list-shaped — filters, a record's detail,
42
+ * a sequence of settings — because an edge panel can be tall without becoming a
43
+ * square that fights the viewport. A decision, especially a destructive one,
44
+ * belongs in a Modal: it is centred, it is smaller, and it does not invite
45
+ * scrolling past the thing being confirmed.
46
+ *
47
+ * RENDERS NOTHING WHEN CLOSED, and portals when open, for the reasons recorded
48
+ * on Modal: a closed overlay left in the tree is still keyboard-focusable, and
49
+ * an inline one inherits whatever `overflow` and stacking context surrounds it.
50
+ */
51
+ export declare const Drawer: React.ForwardRefExoticComponent<DrawerProps & React.RefAttributes<HTMLDivElement>>;
52
+ //# sourceMappingURL=Drawer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Drawer.d.ts","sourceRoot":"","sources":["../../src/components/Drawer.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAkD,MAAM,OAAO,CAAC;AASvE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAExD,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,KAAK,GAAG,KAAK,GAAG,QAAQ,CAAC;AACjE,MAAM,MAAM,UAAU,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAE5C,MAAM,WAAW,WAAY,SAAQ,qBAAqB;IACxD,mCAAmC;IACnC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,+EAA+E;IAC/E,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IACzC;;;;OAIG;IACH,SAAS,CAAC,EAAE,eAAe,CAAC;IAC5B,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,2EAA2E;IAC3E,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,uCAAuC;IACvC,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC9B,6CAA6C;IAC7C,MAAM,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACzB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAkGD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,MAAM,oFAUjB,CAAC"}
@@ -0,0 +1,66 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { forwardRef, useRef, useImperativeHandle } from 'react';
4
+ import { Overlay, useModalOverlay, useDialog, useButton, mergeProps, } from 'react-aria';
5
+ import { useOverlayTriggerState } from 'react-stately';
6
+ const CloseGlyph = () => (_jsx("svg", { viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", focusable: "false", children: _jsx("path", { d: "M6 6l12 12M18 6L6 18", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }) }));
7
+ /**
8
+ * The panel. Mounted only while open, which is what makes the hooks work.
9
+ *
10
+ * `useDialog` resolves the title id and moves focus in effects keyed on the
11
+ * panel ref — effects that run when the component CALLING them mounts. Called
12
+ * beside `useOverlayTriggerState` in `Drawer` below, that moment would be when
13
+ * the trigger mounts, with this panel not yet in the DOM: no `aria-labelledby`,
14
+ * and focus never entering the drawer, so Escape never reaches the key handler.
15
+ * Silent in every case. AGENTS.md records the same split for Modal and Popover.
16
+ */
17
+ function DrawerDialog(props, forwardedRef) {
18
+ const { state, placement = 'end', size = 'md', title, description, footer, showClose = true, closeLabel = 'Close drawer', children, className, } = props;
19
+ const panelRef = useRef(null);
20
+ useImperativeHandle(forwardedRef, () => panelRef.current);
21
+ const { modalProps, underlayProps } = useModalOverlay(props, state, panelRef);
22
+ const { dialogProps, titleProps } = useDialog({ role: 'dialog' }, panelRef);
23
+ const closeRef = useRef(null);
24
+ const { buttonProps } = useButton({ onPress: () => state.close(), 'aria-label': closeLabel }, closeRef);
25
+ const panelClassNames = [
26
+ 'ion-drawer',
27
+ `ion-drawer--${placement}`,
28
+ `ion-drawer--${size}`,
29
+ className || '',
30
+ ]
31
+ .filter(Boolean)
32
+ .join(' ');
33
+ return (_jsx("div", { ...underlayProps, className: "ion-drawer__scrim", children: _jsxs("div", { ...mergeProps(modalProps, dialogProps), ref: panelRef, className: panelClassNames, children: [_jsxs("div", { className: "ion-drawer__header", children: [_jsxs("div", { className: "ion-drawer__heading", children: [_jsx("h2", { ...titleProps, className: "ion-drawer__title", children: title }), description && (_jsx("p", { className: "ion-drawer__description", children: description }))] }), showClose && (_jsx("button", { ...buttonProps, ref: closeRef, type: "button", className: "ion-drawer__close", children: _jsx(CloseGlyph, {}) }))] }), children && _jsx("div", { className: "ion-drawer__body", children: children }), footer && _jsx("div", { className: "ion-drawer__footer", children: footer })] }) }));
34
+ }
35
+ const DrawerDialogWithRef = forwardRef(DrawerDialog);
36
+ /**
37
+ * Drawer — a modal panel anchored to an edge.
38
+ *
39
+ * WHY IT IS A MODAL AND NOT A SIDEBAR
40
+ *
41
+ * It takes focus, traps it, closes on Escape and on an outside click, and marks
42
+ * the rest of the page inert — all of `Modal`'s behaviour, differing only in
43
+ * where the panel sits and how it enters. A persistent side panel that does not
44
+ * do those things is layout, not a drawer, and belongs in the page rather than
45
+ * in an overlay.
46
+ *
47
+ * WHEN TO REACH FOR MODAL INSTEAD
48
+ *
49
+ * A drawer suits work that is long or list-shaped — filters, a record's detail,
50
+ * a sequence of settings — because an edge panel can be tall without becoming a
51
+ * square that fights the viewport. A decision, especially a destructive one,
52
+ * belongs in a Modal: it is centred, it is smaller, and it does not invite
53
+ * scrolling past the thing being confirmed.
54
+ *
55
+ * RENDERS NOTHING WHEN CLOSED, and portals when open, for the reasons recorded
56
+ * on Modal: a closed overlay left in the tree is still keyboard-focusable, and
57
+ * an inline one inherits whatever `overflow` and stacking context surrounds it.
58
+ */
59
+ export const Drawer = forwardRef((props, ref) => {
60
+ const state = useOverlayTriggerState(props);
61
+ if (!state.isOpen)
62
+ return null;
63
+ return (_jsx(Overlay, { children: _jsx(DrawerDialogWithRef, { ...props, state: state, ref: ref }) }));
64
+ });
65
+ Drawer.displayName = 'Drawer';
66
+ //# sourceMappingURL=Drawer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Drawer.js","sourceRoot":"","sources":["../../src/components/Drawer.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,79 @@
1
+ import React from 'react';
2
+ export type FileUploadSize = 'sm' | 'md';
3
+ /** A file the control refused, and the reason, so the caller can say so. */
4
+ export interface RejectedFile {
5
+ file: File;
6
+ reason: 'type' | 'size' | 'count';
7
+ message: string;
8
+ }
9
+ export interface FileUploadProps {
10
+ /** Field label. Also the accessible name of the file input. */
11
+ label?: React.ReactNode;
12
+ /** Helper text below the control. */
13
+ description?: React.ReactNode;
14
+ /** Replaces the helper text when `isInvalid` is set. */
15
+ errorMessage?: React.ReactNode;
16
+ isInvalid?: boolean;
17
+ isDisabled?: boolean;
18
+ /** Matches the Figma `Size` variant when drawn. Height of the drop target. */
19
+ size?: FileUploadSize;
20
+ /** Native `accept` — a comma-separated list of extensions or MIME types. */
21
+ accept?: string;
22
+ /** Allow more than one file. */
23
+ multiple?: boolean;
24
+ /** Reject anything larger, in bytes. */
25
+ maxSize?: number;
26
+ /** Reject once this many files are held. Only meaningful with `multiple`. */
27
+ maxFiles?: number;
28
+ /**
29
+ * The files held. Pass it to control the list; omit and the component keeps
30
+ * its own. A controlled list is the usual case, because the caller is the
31
+ * only thing that knows which uploads have since succeeded or failed.
32
+ */
33
+ files?: readonly File[];
34
+ /** Fires with the full list after every add or remove, never with a delta. */
35
+ onChange?: (files: File[]) => void;
36
+ /** Fires with anything refused by `accept`, `maxSize` or `maxFiles`. */
37
+ onReject?: (rejected: RejectedFile[]) => void;
38
+ /** Call-to-action inside the drop target. */
39
+ prompt?: React.ReactNode;
40
+ /** Second line inside the drop target — the constraints, in words. */
41
+ hint?: React.ReactNode;
42
+ /** Accessible label for each file's remove button. Receives the file name. */
43
+ removeLabel?: (name: string) => string;
44
+ className?: string;
45
+ wrapperClassName?: string;
46
+ id?: string;
47
+ name?: string;
48
+ }
49
+ /**
50
+ * FileUpload — a drop target wrapped around a real `<input type="file">`.
51
+ *
52
+ * Drawn in Figma as `File Upload` (1367:2333) — two sizes by five states. The
53
+ * measurements came from `Input` and `EmptyState` rather than being invented:
54
+ * the border, radius and disabled treatment are Input's; the centred
55
+ * icon-over-text stack is EmptyState's. Figma draws the drop zone alone, the
56
+ * same split `Input` makes against `Form Field`.
57
+ *
58
+ * THE INPUT IS THE CONTROL. THE DROP ZONE IS DECORATION.
59
+ *
60
+ * The usual build of this component is a `<div>` with drag handlers and a
61
+ * click that calls `input.click()`. That version cannot be reached by keyboard,
62
+ * has no accessible name, does not participate in a form, and does not work in
63
+ * any environment without a pointer — which includes switch access, voice
64
+ * control and most screen-reader browse modes.
65
+ *
66
+ * Here the file input is a real, focusable, labelled control that is visually
67
+ * hidden but NOT `display: none` — it keeps its place in the tab order and its
68
+ * label. Drag-and-drop is layered on top as an enhancement, and every path it
69
+ * offers is also reachable without it. That ordering is the whole component.
70
+ *
71
+ * VALIDATION IS ADVISORY, NOT SECURITY. `accept` and `maxSize` are checked here
72
+ * so the user finds out immediately instead of after an upload. A server that
73
+ * trusts either one is trusting a value the client chose.
74
+ */
75
+ export declare function FileUpload({ label, description, errorMessage, isInvalid, isDisabled: isDisabledProp, size, accept, multiple, maxSize, maxFiles, files: controlledFiles, onChange, onReject, prompt, hint, removeLabel, className, wrapperClassName, id: providedId, name: inputName, }: FileUploadProps): React.JSX.Element;
76
+ export declare namespace FileUpload {
77
+ var displayName: string;
78
+ }
79
+ //# sourceMappingURL=FileUpload.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FileUpload.d.ts","sourceRoot":"","sources":["../../src/components/FileUpload.tsx"],"names":[],"mappings":"AAEA,OAAO,KAA+C,MAAM,OAAO,CAAC;AAEpE,MAAM,MAAM,cAAc,GAAG,IAAI,GAAG,IAAI,CAAC;AAEzC,4EAA4E;AAC5E,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,IAAI,CAAC;IACX,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;IAClC,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,eAAe;IAC9B,+DAA+D;IAC/D,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACxB,qCAAqC;IACrC,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC9B,wDAAwD;IACxD,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC/B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,8EAA8E;IAC9E,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB,4EAA4E;IAC5E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gCAAgC;IAChC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,wCAAwC;IACxC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,KAAK,CAAC,EAAE,SAAS,IAAI,EAAE,CAAC;IACxB,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,IAAI,CAAC;IACnC,wEAAwE;IACxE,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,YAAY,EAAE,KAAK,IAAI,CAAC;IAC9C,6CAA6C;IAC7C,MAAM,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACzB,sEAAsE;IACtE,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACvC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAmED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,UAAU,CAAC,EACzB,KAAK,EACL,WAAW,EACX,YAAY,EACZ,SAAS,EACT,UAAU,EAAE,cAAc,EAC1B,IAAW,EACX,MAAM,EACN,QAAQ,EACR,OAAO,EACP,QAAQ,EACR,KAAK,EAAE,eAAe,EACtB,QAAQ,EACR,QAAQ,EACR,MAAM,EACN,IAAI,EACJ,WAAwC,EACxC,SAAS,EACT,gBAAgB,EAChB,EAAE,EAAE,UAAU,EACd,IAAI,EAAE,SAAS,GAChB,EAAE,eAAe,qBAuPjB;yBA5Qe,UAAU"}