@mistriwork/ui 0.0.0-stage → 1.0.0-rc.10

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 (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +50 -2
  3. package/dist/Avatar.d.ts +11 -0
  4. package/dist/Avatar.js +28 -0
  5. package/dist/Button.d.ts +36 -0
  6. package/dist/Button.js +22 -0
  7. package/dist/Carousel.d.ts +52 -0
  8. package/dist/Carousel.js +110 -0
  9. package/dist/Choices.d.ts +77 -0
  10. package/dist/Choices.js +88 -0
  11. package/dist/DateField.d.ts +66 -0
  12. package/dist/DateField.js +255 -0
  13. package/dist/Drawer.d.ts +33 -0
  14. package/dist/Drawer.js +12 -0
  15. package/dist/DropMenu.d.ts +59 -0
  16. package/dist/DropMenu.js +52 -0
  17. package/dist/Dropdown.d.ts +73 -0
  18. package/dist/Dropdown.js +85 -0
  19. package/dist/Field.d.ts +53 -0
  20. package/dist/Field.js +58 -0
  21. package/dist/FormParts.d.ts +68 -0
  22. package/dist/FormParts.js +53 -0
  23. package/dist/HeroBlocks.d.ts +37 -0
  24. package/dist/HeroBlocks.js +26 -0
  25. package/dist/Icon.d.ts +29 -0
  26. package/dist/Icon.js +78 -0
  27. package/dist/Modal.d.ts +48 -0
  28. package/dist/Modal.js +57 -0
  29. package/dist/Pagination.d.ts +33 -0
  30. package/dist/Pagination.js +25 -0
  31. package/dist/Popover.d.ts +21 -0
  32. package/dist/Popover.js +20 -0
  33. package/dist/Resizable.d.ts +48 -0
  34. package/dist/Resizable.js +122 -0
  35. package/dist/ScrollArea.d.ts +29 -0
  36. package/dist/ScrollArea.js +62 -0
  37. package/dist/Select.d.ts +26 -0
  38. package/dist/Select.js +20 -0
  39. package/dist/SensitiveField.d.ts +26 -0
  40. package/dist/SensitiveField.js +27 -0
  41. package/dist/Slider.d.ts +17 -0
  42. package/dist/Slider.js +10 -0
  43. package/dist/Switch.d.ts +28 -0
  44. package/dist/Switch.js +24 -0
  45. package/dist/Tabs.d.ts +25 -0
  46. package/dist/Tabs.js +5 -0
  47. package/dist/TextField.d.ts +44 -0
  48. package/dist/TextField.js +19 -0
  49. package/dist/Tooltip.d.ts +29 -0
  50. package/dist/Tooltip.js +39 -0
  51. package/dist/Typeahead.d.ts +63 -0
  52. package/dist/Typeahead.js +90 -0
  53. package/dist/fieldFrame.d.ts +37 -0
  54. package/dist/fieldFrame.js +16 -0
  55. package/dist/forms.css +192 -0
  56. package/dist/hero.css +23 -0
  57. package/dist/index.d.ts +38 -0
  58. package/dist/index.js +40 -0
  59. package/dist/internal/ComponentStyleScope.d.ts +1 -0
  60. package/dist/internal/ComponentStyleScope.js +4 -0
  61. package/dist/internal/anchor.d.ts +19 -0
  62. package/dist/internal/anchor.js +52 -0
  63. package/dist/internal/datePickerModel.d.ts +53 -0
  64. package/dist/internal/datePickerModel.js +175 -0
  65. package/dist/internal/popups.d.ts +13 -0
  66. package/dist/internal/popups.js +16 -0
  67. package/dist/internal/ruler.d.ts +37 -0
  68. package/dist/internal/ruler.js +55 -0
  69. package/dist/masks.d.ts +32 -0
  70. package/dist/masks.js +62 -0
  71. package/dist/preferences.css +26 -0
  72. package/dist/primitives.css +188 -0
  73. package/dist/standalone.css +413 -0
  74. package/dist/style.css +19 -0
  75. package/dist/tokens.css +233 -0
  76. package/mark.svg +1 -0
  77. package/package.json +84 -4
@@ -0,0 +1,255 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * The themed calendar field: editable ISO text beside a calendar popover,
4
+ * for a date, or with `withTime` for a date and a wall-clock time. The text
5
+ * is the value — typed drafts survive, the calendar only ever writes the
6
+ * date part — and nothing here converts a value through an instant. The
7
+ * arithmetic and the bounds are datePickerModel.ts's. Its words and the
8
+ * calendar button's icon are props: the kit lends its catalog's and its icon.
9
+ */
10
+ import { useCallback, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from 'react';
11
+ import { Popover } from '@base-ui/react/popover';
12
+ import { layoutViewport, zoomAnchor } from './internal/anchor.js';
13
+ import { addDateDays, addDateMonths, clampDate, dateBounds, dateFormat, dateParts, dateTimeWithinBounds, dateWithinBounds, isPickerDate, isPickerDateTime, monthGrid, monthLabel, splitDateTime, todayInZone, weekdayLabels, withDate, } from './internal/datePickerModel.js';
14
+ import { popupContainer } from './internal/popups.js';
15
+ /** How a locale writes a numeric date: the order of its parts and its separator ("MM/DD/YYYY" in en-US). */
16
+ function dateMask(locale) {
17
+ let parts;
18
+ try {
19
+ parts = dateFormat(locale, { year: 'numeric', month: '2-digit', day: '2-digit', timeZone: 'UTC', calendar: 'gregory' }).formatToParts(Date.UTC(2006, 10, 22));
20
+ }
21
+ catch {
22
+ parts = [];
23
+ }
24
+ const order = parts.filter((part) => ['year', 'month', 'day'].includes(part.type)).map((part) => part.type);
25
+ if (order.length !== 3)
26
+ return { order: ['year', 'month', 'day'], sep: '-', hint: 'YYYY-MM-DD' };
27
+ const sep = parts.find((part) => part.type === 'literal')?.value.trim() || '/';
28
+ const word = { year: 'YYYY', month: 'MM', day: 'DD' };
29
+ return { order, sep, hint: order.map((type) => word[type]).join(sep) };
30
+ }
31
+ /** An ISO date as the locale writes it in a box. */
32
+ function localeText(iso, mask) {
33
+ const { year, month, day } = dateParts(iso);
34
+ const of = {
35
+ year: String(year).padStart(4, '0'),
36
+ month: String(month).padStart(2, '0'),
37
+ day: String(day).padStart(2, '0'),
38
+ };
39
+ return mask.order.map((type) => of[type]).join(mask.sep);
40
+ }
41
+ const isoDate = (text) => isPickerDate(text);
42
+ /** What was typed, read as a whole date in the locale's order (or ISO), else undefined: a draft stays text. */
43
+ function readDate(text, mask) {
44
+ if (isoDate(text))
45
+ return text;
46
+ const numbers = text.trim().match(/^(\d{1,4})\D(\d{1,2})\D(\d{1,4})$/);
47
+ if (!numbers)
48
+ return undefined;
49
+ const at = {};
50
+ mask.order.forEach((type, index) => (at[type] = numbers[index + 1]));
51
+ if (at.year.length !== 4)
52
+ return undefined;
53
+ const iso = `${at.year}-${at.month.padStart(2, '0')}-${at.day.padStart(2, '0')}`;
54
+ return isPickerDate(iso) ? iso : undefined;
55
+ }
56
+ /** A date field, or a date and a wall-clock time. No input is parsed as an instant or shifted by a timezone. */
57
+ export function DateField({ id, words, calendarIcon, value, onChange, onBlur, onKeyDown, disabled, required, min, max, locale, timezone, firstDay = 1, className = '', name, inputRef, placeholder, withTime = false, popupRef, onOpenChange, 'aria-label': label, 'aria-invalid': invalid, 'aria-describedby': describedBy, }) {
58
+ // The calendar only ever sees the date; a wall time's clock stays in the box.
59
+ const datePart = withTime ? (splitDateTime(value)?.date ?? value.slice(0, 10)) : value;
60
+ // A range facet may temporarily supply an incomplete or reversed bound.
61
+ // Keep text editable but disable calendar selection until both ends agree.
62
+ const configuredBounds = useMemo(() => {
63
+ try {
64
+ return dateBounds(withTime ? min?.slice(0, 10) : min, withTime ? max?.slice(0, 10) : max);
65
+ }
66
+ catch {
67
+ return null;
68
+ }
69
+ }, [min, max, withTime]);
70
+ const bounds = configuredBounds ?? dateBounds();
71
+ const today = todayInZone(new Date(), timezone);
72
+ const seed = () => clampDate(isPickerDate(datePart) ? datePart : today, bounds);
73
+ const [active, setActive] = useState(seed);
74
+ const [open, setOpenState] = useState(false);
75
+ const openCallback = useRef(onOpenChange);
76
+ useLayoutEffect(() => {
77
+ openCallback.current = onOpenChange;
78
+ });
79
+ const setOpen = (next) => {
80
+ setOpenState(next);
81
+ openCallback.current?.(next);
82
+ };
83
+ const [container, setContainer] = useState(null);
84
+ const group = useRef(null);
85
+ const trigger = useRef(null);
86
+ const input = useRef(null);
87
+ useImperativeHandle(inputRef, () => input.current, []);
88
+ const popup = useRef(null);
89
+ const setPopup = useCallback((node) => {
90
+ popup.current = node;
91
+ popupRef?.(node);
92
+ }, [popupRef]);
93
+ const ownedFocus = useRef(false);
94
+ const moveFocus = useRef(false);
95
+ const blurTimer = useRef(undefined);
96
+ const restoreFocus = useRef(false);
97
+ const blurCallback = useRef(onBlur);
98
+ useLayoutEffect(() => {
99
+ blurCallback.current = onBlur;
100
+ });
101
+ const caption = useId();
102
+ const hint = useId();
103
+ /* A date alone is shown as the locale writes it and kept as ISO; what is being typed stays as typed until the
104
+ field is left, so a half-typed date is never rewritten under the caret. A date with a time stays ISO text. */
105
+ const mask = useMemo(() => dateMask(locale), [locale]);
106
+ const [draft, setDraft] = useState(null);
107
+ const shown = withTime ? value : (draft ?? (isPickerDate(value) ? localeText(value, mask) : value));
108
+ const formatHint = withTime ? words.dateTimePattern : mask.hint;
109
+ useEffect(() => {
110
+ if (draft !== null && value !== (readDate(draft, mask) ?? draft))
111
+ setDraft(null);
112
+ }, [value]); // eslint-disable-line react-hooks/exhaustive-deps
113
+ const days = monthGrid(active, firstDay, bounds);
114
+ const weekdays = weekdayLabels(locale, firstDay);
115
+ useEffect(() => {
116
+ setContainer(trigger.current?.closest('dialog, [role="dialog"]'));
117
+ return () => {
118
+ clearTimeout(blurTimer.current);
119
+ };
120
+ }, []);
121
+ const bad = !configuredBounds ||
122
+ (value !== '' &&
123
+ (withTime
124
+ ? !isPickerDateTime(value) || !dateWithinBounds(datePart, bounds) || !dateTimeWithinBounds(value, min, max)
125
+ : !dateWithinBounds(value, bounds)));
126
+ useEffect(() => {
127
+ input.current?.setCustomValidity(!bad ? '' : withTime ? words.enterDateAndTime : words.enterDate(mask.hint));
128
+ }, [bad, withTime, mask]); // eslint-disable-line react-hooks/exhaustive-deps
129
+ useLayoutEffect(() => {
130
+ if (!open)
131
+ return;
132
+ if (!configuredBounds || disabled) {
133
+ setOpen(false);
134
+ return;
135
+ }
136
+ const next = clampDate(isPickerDate(datePart) ? datePart : active, configuredBounds);
137
+ if (next !== active) {
138
+ moveFocus.current = Boolean(popup.current?.querySelector('[data-date]:focus'));
139
+ setActive(next);
140
+ }
141
+ // Synchronize external changes, not arrow-key movement within the calendar.
142
+ }, [value, min, max, disabled]);
143
+ useLayoutEffect(() => {
144
+ if (!open || !moveFocus.current)
145
+ return;
146
+ popup.current?.querySelector(`[data-date="${active}"]`)?.focus();
147
+ moveFocus.current = false;
148
+ }, [active, open]);
149
+ const within = (node) => Boolean(node && (group.current?.contains(node) || popup.current?.contains(node)));
150
+ const commitBlur = () => {
151
+ if (ownedFocus.current) {
152
+ ownedFocus.current = false;
153
+ setDraft(null);
154
+ blurCallback.current?.();
155
+ }
156
+ };
157
+ const checkBlur = () => {
158
+ clearTimeout(blurTimer.current);
159
+ // Let the portal's focus restoration finish before deciding the field was left.
160
+ blurTimer.current = setTimeout(() => {
161
+ if (!within(document.activeElement))
162
+ commitBlur();
163
+ }, 0);
164
+ };
165
+ const leave = (event) => {
166
+ if (!event.relatedTarget) {
167
+ checkBlur();
168
+ return;
169
+ }
170
+ clearTimeout(blurTimer.current);
171
+ // Pointer focus leaves before the destination's click/submit. Publish the
172
+ // draft now so a surrounding filter's Apply button reads the new value.
173
+ if (!within(event.relatedTarget))
174
+ commitBlur();
175
+ };
176
+ const changeOpen = (next) => {
177
+ if (next) {
178
+ restoreFocus.current = false;
179
+ setActive(seed());
180
+ }
181
+ setOpen(next);
182
+ if (!next)
183
+ checkBlur();
184
+ };
185
+ const returnToTrigger = () => {
186
+ const target = trigger.current;
187
+ if (target?.isConnected && (!container || (container.isConnected && !container.hasAttribute('data-closed')))) {
188
+ target.focus({ preventScroll: true });
189
+ }
190
+ };
191
+ const choose = (date) => {
192
+ restoreFocus.current = true;
193
+ setDraft(null);
194
+ // Move focus before removing the focused day. Otherwise an enclosing
195
+ // modal sees focus fall to body and starts its own recovery to the dialog.
196
+ returnToTrigger();
197
+ onChange(withTime && date ? withDate(value, date) : date);
198
+ changeOpen(false);
199
+ };
200
+ const move = (date) => {
201
+ moveFocus.current = true;
202
+ setActive(clampDate(date, bounds));
203
+ };
204
+ const onDayKey = (event) => {
205
+ const index = days.findIndex((day) => day.date === active) % 7;
206
+ const steps = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -7, ArrowDown: 7, Home: -index, End: 6 - index };
207
+ if (Object.hasOwn(steps, event.key)) {
208
+ event.preventDefault();
209
+ event.stopPropagation();
210
+ move(addDateDays(active, steps[event.key]));
211
+ }
212
+ else if (event.key === 'PageUp' || event.key === 'PageDown') {
213
+ event.preventDefault();
214
+ event.stopPropagation();
215
+ move(addDateMonths(active, (event.key === 'PageUp' ? -1 : 1) * (event.shiftKey ? 12 : 1)));
216
+ }
217
+ };
218
+ const dayName = (date) => {
219
+ const p = dateParts(date), ruler = new Date(0);
220
+ ruler.setUTCFullYear(p.year, p.month - 1, p.day);
221
+ return dateFormat(locale, { calendar: 'gregory', timeZone: 'UTC', dateStyle: 'full' }).format(ruler);
222
+ };
223
+ const previous = addDateMonths(active, -1), next = addDateMonths(active, 1);
224
+ return (_jsx(Popover.Root, { open: open && !disabled && Boolean(configuredBounds), onOpenChange: (next, details) => {
225
+ restoreFocus.current = !next && details.reason === 'escape-key';
226
+ if (restoreFocus.current)
227
+ returnToTrigger();
228
+ changeOpen(next);
229
+ }, children: _jsxs("span", { ref: group, className: withTime ? 'date-field with-time' : 'date-field', onFocus: () => {
230
+ ownedFocus.current = true;
231
+ }, onBlur: leave, children: [_jsx("input", { ref: input, id: id, name: name, type: "text", value: shown, className: `date-field-input ${className}`, placeholder: placeholder ?? (withTime ? words.dateTimePattern : mask.hint), autoComplete: "off", spellCheck: false, disabled: disabled, required: required, "aria-required": required, "aria-label": label, "aria-invalid": invalid || bad || undefined, "aria-describedby": [describedBy, hint].filter(Boolean).join(' '), onChange: (event) => {
232
+ const text = event.target.value;
233
+ if (withTime)
234
+ return onChange(text);
235
+ setDraft(text);
236
+ onChange(readDate(text, mask) ?? text);
237
+ }, onKeyDown: (event) => {
238
+ if (event.altKey && event.key === 'ArrowDown') {
239
+ event.preventDefault();
240
+ event.stopPropagation();
241
+ if (configuredBounds)
242
+ changeOpen(true);
243
+ }
244
+ else
245
+ onKeyDown?.(event);
246
+ } }), _jsxs("span", { id: hint, className: "date-field-hint", children: [withTime ? words.dateAndTimeFormat : words.dateFormat(formatHint), " ", words.openWithKeys] }), _jsx(Popover.Trigger, { render: _jsx("button", { ref: trigger, type: "button", className: "date-field-trigger", disabled: disabled || !configuredBounds, "aria-label": words.chooseDate(label || undefined) }), children: calendarIcon }), _jsx(Popover.Portal, { container: container ?? popupContainer(), children: _jsx(Popover.Positioner, { anchor: () => (trigger.current ? zoomAnchor(trigger.current) : null), positionMethod: "fixed", side: "bottom", align: "end", sideOffset: 6, collisionBoundary: layoutViewport(), collisionPadding: 8, style: { zIndex: 45 }, children: _jsxs(Popover.Popup, { ref: setPopup, finalFocus: () => {
247
+ const target = trigger.current, focused = document.activeElement;
248
+ return restoreFocus.current &&
249
+ target?.isConnected &&
250
+ (!container || (container.isConnected && !container.hasAttribute('data-closed'))) &&
251
+ (focused === target || focused === document.body || focused === container)
252
+ ? target
253
+ : false;
254
+ }, className: "date-field-popup", "aria-label": words.calendar(label || undefined), initialFocus: () => popup.current?.querySelector(`[data-date="${active}"]`) ?? true, children: [_jsxs("div", { className: "date-field-head", children: [_jsx("button", { type: "button", "aria-label": words.previousMonth, disabled: previous.slice(0, 7) === active.slice(0, 7) || previous.slice(0, 7) < bounds.min.slice(0, 7), onClick: () => setActive(clampDate(previous, bounds)), children: "\u2190" }), _jsx("span", { id: caption, "aria-live": "polite", "aria-atomic": "true", children: monthLabel(active, locale) }), _jsx("button", { type: "button", "aria-label": words.nextMonth, disabled: next.slice(0, 7) === active.slice(0, 7) || next.slice(0, 7) > bounds.max.slice(0, 7), onClick: () => setActive(clampDate(next, bounds)), children: "\u2192" })] }), _jsxs("table", { className: "date-field-calendar", role: "grid", "aria-labelledby": caption, children: [_jsx("thead", { children: _jsx("tr", { children: weekdays.map((day) => (_jsx("th", { scope: "col", "aria-label": day.long, children: day.short }, day.long))) }) }), _jsx("tbody", { children: Array.from({ length: 6 }, (_, week) => (_jsx("tr", { children: days.slice(week * 7, week * 7 + 7).map((day, index) => (_jsx("td", { "aria-selected": day.date === datePart, children: day.date && (_jsx("button", { type: "button", "data-date": day.date, "data-outside": !day.inMonth || undefined, "data-selected": day.date === datePart || undefined, "aria-current": day.date === today ? 'date' : undefined, disabled: day.disabled, tabIndex: day.date === active ? 0 : -1, "aria-label": dayName(day.date), onKeyDown: onDayKey, onClick: () => choose(day.date), children: Number(day.date.slice(8)) })) }, day.date ?? index))) }, week))) })] }), _jsxs("div", { className: "date-field-foot", children: [_jsx("button", { type: "button", onClick: () => choose(''), children: words.clear }), _jsx("button", { type: "button", disabled: !dateWithinBounds(today, bounds), onClick: () => choose(today), children: words.today })] })] }) }) })] }) }));
255
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * A drawer: a sheet that slides in from an edge, over Base UI's Drawer,
3
+ * which gives it the scrim, focus held inside, Escape and a click on the
4
+ * scrim to dismiss, focus handed back on close, and the swipe to dismiss
5
+ * on a phone. Controlled with `open`/`onOpenChange`; a closed drawer is
6
+ * dismissed (its exit animation aside, the sheet stays in the tree only
7
+ * while that runs), so nothing inside it is read while it is shut.
8
+ * `side` is the edge it comes from, right by default,
9
+ * and the panel is at most 420px wide (a left drawer the same), full
10
+ * width on a phone. The sheet is named by its `title` or, where it draws
11
+ * none, by `ariaLabel`; one of the two is required.
12
+ */
13
+ import type { ReactNode } from 'react';
14
+ export type DrawerSide = 'left' | 'right';
15
+ interface DrawerBase {
16
+ open: boolean;
17
+ onOpenChange?: (open: boolean) => void;
18
+ side?: DrawerSide;
19
+ children: ReactNode;
20
+ className?: string;
21
+ /** A non-cancellable operation is awaiting acknowledgement. */
22
+ closeDisabled?: boolean;
23
+ }
24
+ /** A sheet is named by a title it draws or by an `ariaLabel` it does not; one of the two is required, so none is announced unnamed. */
25
+ export type DrawerProps = DrawerBase & ({
26
+ title: ReactNode;
27
+ ariaLabel?: string;
28
+ } | {
29
+ title?: undefined;
30
+ ariaLabel: string;
31
+ });
32
+ export declare function Drawer({ open, onOpenChange, side, title, ariaLabel, children, className, closeDisabled }: DrawerProps): import("react").JSX.Element;
33
+ export {};
package/dist/Drawer.js ADDED
@@ -0,0 +1,12 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Drawer as BaseDrawer } from '@base-ui/react/drawer';
3
+ export function Drawer({ open, onOpenChange, side = 'right', title, ariaLabel, children, className, closeDisabled = false }) {
4
+ return (_jsx(BaseDrawer.Root, { open: open, disablePointerDismissal: closeDisabled, onOpenChange: (next) => {
5
+ /* A non-cancellable operation: a dismissal request (Escape, a
6
+ click outside) is refused here, as Modal's beforeClose refuses
7
+ one, and the controlled drawer stays open. */
8
+ if (!next && closeDisabled)
9
+ return;
10
+ onOpenChange?.(next);
11
+ }, children: _jsxs(BaseDrawer.Portal, { children: [_jsx(BaseDrawer.Backdrop, { className: "ui-drawer-backdrop" }), _jsx(BaseDrawer.Viewport, { className: ['ui-drawer-viewport', `ui-drawer-${side}`, className].filter(Boolean).join(' '), children: _jsxs(BaseDrawer.Popup, { className: "ui-drawer", "aria-label": ariaLabel ?? (typeof title === 'string' ? title : undefined), children: [title !== undefined ? _jsx(BaseDrawer.Title, { className: "ui-drawer-title", children: title }) : null, children] }) })] }) }));
12
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * A button that opens a menu, over Base UI's Menu — what the `<details>`
3
+ * menus here did with a summary and a hook (react to a click, close on
4
+ * Escape or a click outside, hand focus back), plus what they never did:
5
+ * arrows between the items, Home and End, typeahead, and the roles a
6
+ * screen reader expects of a menu.
7
+ *
8
+ * The wrapper keeps the caller's class (`export`, `pb-picker`) because the
9
+ * stylesheet places the control by it; the trigger and the popup wear the
10
+ * classes the summary and the panel wore. The popup is anchored on the
11
+ * zoom-aware ruler (anchor.ts) and fixed to the viewport, so a scrolling
12
+ * panel cannot clip it. The open state can be the caller's, for a menu
13
+ * that has to stay up while a click inside it fetches.
14
+ */
15
+ import { type ReactNode, type Ref } from 'react';
16
+ import { Menu } from '@base-ui/react/menu';
17
+ interface DropMenuProps {
18
+ /** The wrapper's class — where the stylesheet places the control. */
19
+ className?: string;
20
+ /** The trigger's classes; a secondary button's by default. */
21
+ triggerClassName?: string;
22
+ triggerTitle?: string;
23
+ /** The trigger's accessible name, when its visible label is shared with another menu on the page. */
24
+ triggerLabel?: string;
25
+ label: ReactNode;
26
+ /** The popup's classes; the house listbox surface (`dd-list`) by default. */
27
+ popupClassName?: string;
28
+ side?: 'top' | 'bottom';
29
+ align?: 'start' | 'end';
30
+ sideOffset?: number;
31
+ open?: boolean;
32
+ onOpenChange?: (open: boolean) => void;
33
+ /** Where the focus goes when the menu closes; absent, back to the trigger. */
34
+ finalFocus?: Menu.Popup.Props['finalFocus'];
35
+ children: ReactNode;
36
+ }
37
+ export declare function DropMenu({ className, triggerClassName, triggerTitle, triggerLabel, label, popupClassName, side, align, sideOffset, open, onOpenChange, finalFocus, children, }: DropMenuProps): import("react").JSX.Element;
38
+ /** A labelled run of items: a menu that changes several things says which each item changes. */
39
+ export declare function DropGroup({ className, labelClassName, label, children, }: {
40
+ className?: string;
41
+ labelClassName?: string;
42
+ label: string;
43
+ children: ReactNode;
44
+ }): import("react").JSX.Element;
45
+ /** A rule between two runs of items, a separator to a screen reader. */
46
+ export declare function DropSeparator({ className }: {
47
+ className?: string;
48
+ }): import("react").JSX.Element;
49
+ /** An item drawn as a button, so `disabled` and the button styles hold. Closes the menu unless told not to. */
50
+ export declare function DropItem({ className, disabled, closeOnClick, onClick, title, children, ref, }: {
51
+ className?: string;
52
+ disabled?: boolean;
53
+ closeOnClick?: boolean;
54
+ onClick: () => void;
55
+ title?: string;
56
+ children: ReactNode;
57
+ ref?: Ref<HTMLButtonElement>;
58
+ }): import("react").JSX.Element;
59
+ export {};
@@ -0,0 +1,52 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * A button that opens a menu, over Base UI's Menu — what the `<details>`
4
+ * menus here did with a summary and a hook (react to a click, close on
5
+ * Escape or a click outside, hand focus back), plus what they never did:
6
+ * arrows between the items, Home and End, typeahead, and the roles a
7
+ * screen reader expects of a menu.
8
+ *
9
+ * The wrapper keeps the caller's class (`export`, `pb-picker`) because the
10
+ * stylesheet places the control by it; the trigger and the popup wear the
11
+ * classes the summary and the panel wore. The popup is anchored on the
12
+ * zoom-aware ruler (anchor.ts) and fixed to the viewport, so a scrolling
13
+ * panel cannot clip it. The open state can be the caller's, for a menu
14
+ * that has to stay up while a click inside it fetches.
15
+ */
16
+ import { useEffect, useRef, useState } from 'react';
17
+ import { Menu } from '@base-ui/react/menu';
18
+ import { layoutViewport, zoomAnchor } from './internal/anchor.js';
19
+ import { popupContainer } from './internal/popups.js';
20
+ import { buttonClass } from './Button.js';
21
+ export function DropMenu({ className, triggerClassName = buttonClass(), triggerTitle, triggerLabel, label, popupClassName = 'dd-list', side = 'bottom', align = 'end', sideOffset = 4, open, onOpenChange, finalFocus, children, }) {
22
+ const trigger = useRef(null);
23
+ /* Inside a dialog the popup is drawn into that dialog, so it shares its
24
+ stacking context rather than landing behind it; elsewhere it goes to
25
+ the body the way every floating surface here does. */
26
+ const [container, setContainer] = useState(null);
27
+ useEffect(() => {
28
+ setContainer(trigger.current?.closest('dialog, [role="dialog"]'));
29
+ }, []);
30
+ const [own, setOwn] = useState(false);
31
+ const isOpen = open ?? own;
32
+ const change = (next, details) => {
33
+ // Return focus before unmount so a containing dialog does not reclaim it — unless the caller says where it goes.
34
+ if (!next && details.reason === 'escape-key' && finalFocus === undefined)
35
+ trigger.current?.focus({ preventScroll: true });
36
+ setOwn(next);
37
+ onOpenChange?.(next);
38
+ };
39
+ return (_jsx("div", { className: className || undefined, children: _jsxs(Menu.Root, { open: isOpen, onOpenChange: change, children: [_jsx(Menu.Trigger, { render: _jsx("button", { type: "button", className: triggerClassName, title: triggerTitle, "aria-label": triggerLabel, ref: trigger }), children: label }), _jsx(Menu.Portal, { container: container ?? popupContainer(), children: _jsx(Menu.Positioner, { anchor: () => (trigger.current ? zoomAnchor(trigger.current) : null), positionMethod: "fixed", side: side, align: align, sideOffset: sideOffset, collisionBoundary: layoutViewport(), collisionPadding: 8, style: { zIndex: 90 }, children: _jsx(Menu.Popup, { className: popupClassName, finalFocus: finalFocus, children: children }) }) })] }) }));
40
+ }
41
+ /** A labelled run of items: a menu that changes several things says which each item changes. */
42
+ export function DropGroup({ className, labelClassName, label, children, }) {
43
+ return (_jsxs(Menu.Group, { className: className, children: [_jsx(Menu.GroupLabel, { className: labelClassName, children: label }), children] }));
44
+ }
45
+ /** A rule between two runs of items, a separator to a screen reader. */
46
+ export function DropSeparator({ className = 'dd-divider' }) {
47
+ return _jsx(Menu.Separator, { className: className });
48
+ }
49
+ /** An item drawn as a button, so `disabled` and the button styles hold. Closes the menu unless told not to. */
50
+ export function DropItem({ className = 'dd-option', disabled, closeOnClick, onClick, title, children, ref, }) {
51
+ return (_jsx(Menu.Item, { render: _jsx("button", { type: "button", className: className, title: title, ref: ref }), nativeButton: true, disabled: disabled, closeOnClick: closeOnClick, onClick: onClick, children: children }));
52
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * A themed listbox, over Base UI's Select.
3
+ *
4
+ * A native `<select>` cannot be styled where it matters: the open menu is
5
+ * drawn by the operating system, in the system font, with the system
6
+ * highlight colour, ignoring the page entirely. Base UI gives back what
7
+ * the native one gave for free — arrows move, Home and End jump, Enter and
8
+ * Space choose, Escape closes and returns focus, Tab closes, typing jumps
9
+ * to a matching option, the roles and `aria-expanded` — and this draws it
10
+ * in the tool's own classes, so the stylesheet and the browser suite see
11
+ * the control they always saw.
12
+ *
13
+ * Actions ride under a divider and are walked with the same arrows, but
14
+ * run rather than become the value: the value stays what the caller says
15
+ * it is, so choosing "re-read the schema" cannot select it.
16
+ */
17
+ import { type Ref } from 'react';
18
+ export interface DropdownItem {
19
+ value: string;
20
+ label: string;
21
+ detail?: string;
22
+ badge?: string;
23
+ disabled?: boolean;
24
+ /** Colours the option's word; the five tones of a display rule. */
25
+ tone?: 'good' | 'warn' | 'bad' | 'info' | 'neutral';
26
+ swatches?: string[];
27
+ }
28
+ interface DropdownBase {
29
+ items: DropdownItem[];
30
+ /** What a reader hears the list called; the kit's Dropdown supplies its catalog's word where a caller names none. */
31
+ ariaLabel?: string;
32
+ ariaInvalid?: boolean;
33
+ ariaDescribedBy?: string;
34
+ /** The caption that names the button, where it is drawn beside it (a field's label, "(required)" and all); `ariaLabel` gives way to it. */
35
+ ariaLabelledBy?: string;
36
+ /** The button's id, so a field's `<label for>` names it (components/Field.tsx). */
37
+ id?: string;
38
+ /** The button, for a form that needs to put focus on this control. */
39
+ buttonRef?: Ref<HTMLButtonElement>;
40
+ popupRef?: Ref<HTMLDivElement>;
41
+ onOpenChange?: (open: boolean) => void;
42
+ disabled?: boolean;
43
+ readOnly?: boolean;
44
+ /** A class on the shown value — a page title that is also a picker. */
45
+ valueClassName?: string;
46
+ /** Things to do rather than pick, listed under a divider — "re-read the schema" beneath the databases. */
47
+ actions?: DropdownAction[];
48
+ /** Classes on the button beside `dd-button`: a form's field wears `field-control` through it. */
49
+ buttonClassName?: string;
50
+ /** Classes on the list beside `dd-list`. */
51
+ listClassName?: string;
52
+ /** The id of the shown value, so a form can name the button by its label and its value both. */
53
+ valueId?: string;
54
+ /** What the button says while nothing is chosen (a form's "Choose"); without it an empty list says a dash. */
55
+ placeholder?: string;
56
+ }
57
+ export type DropdownProps = DropdownBase & {
58
+ value: string;
59
+ onChange: (value: string) => void;
60
+ };
61
+ /** `MultiDropdown`'s: the list of values taken and given, kept in the items' order. */
62
+ export type MultiDropdownProps = DropdownBase & {
63
+ value: string[];
64
+ onChange: (value: string[]) => void;
65
+ };
66
+ export interface DropdownAction {
67
+ label: string;
68
+ onSelect: () => void;
69
+ }
70
+ export declare function Dropdown(props: DropdownProps): import("react").JSX.Element;
71
+ /** The same listbox choosing several: it stays open while it is chosen from, and the button says the choices. */
72
+ export declare function MultiDropdown(props: MultiDropdownProps): import("react").JSX.Element;
73
+ export {};
@@ -0,0 +1,85 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * A themed listbox, over Base UI's Select.
4
+ *
5
+ * A native `<select>` cannot be styled where it matters: the open menu is
6
+ * drawn by the operating system, in the system font, with the system
7
+ * highlight colour, ignoring the page entirely. Base UI gives back what
8
+ * the native one gave for free — arrows move, Home and End jump, Enter and
9
+ * Space choose, Escape closes and returns focus, Tab closes, typing jumps
10
+ * to a matching option, the roles and `aria-expanded` — and this draws it
11
+ * in the tool's own classes, so the stylesheet and the browser suite see
12
+ * the control they always saw.
13
+ *
14
+ * Actions ride under a divider and are walked with the same arrows, but
15
+ * run rather than become the value: the value stays what the caller says
16
+ * it is, so choosing "re-read the schema" cannot select it.
17
+ */
18
+ import { useEffect, useRef, useState } from 'react';
19
+ import { Select } from '@base-ui/react/select';
20
+ import { layoutViewport, zoomAnchor } from './internal/anchor.js';
21
+ import { popupContainer } from './internal/popups.js';
22
+ /* A value no option could carry, so an action's row is told apart from an option's. */
23
+ const ACTION = '~~action:';
24
+ export function Dropdown(props) {
25
+ return _jsx(DropdownList, { ...props });
26
+ }
27
+ /** The same listbox choosing several: it stays open while it is chosen from, and the button says the choices. */
28
+ export function MultiDropdown(props) {
29
+ return _jsx(DropdownList, { ...props, multiple: true });
30
+ }
31
+ function DropdownList(props) {
32
+ const { items, ariaLabel, ariaInvalid, ariaDescribedBy, ariaLabelledBy, id, buttonRef, popupRef, onOpenChange, disabled, readOnly, valueClassName, actions = [], buttonClassName, listClassName, valueId, placeholder, } = props;
33
+ const many = props.multiple === true;
34
+ const chosen = props.multiple === true ? props.value : [props.value];
35
+ const selected = items.find((i) => i.value === chosen[0]);
36
+ /* Nothing chosen: a value no item carries (a form's empty answer) reads as no selection, not as a stray one. */
37
+ const said = many
38
+ ? items
39
+ .filter((i) => chosen.includes(i.value))
40
+ .map((i) => i.label)
41
+ .join(', ')
42
+ : (selected?.label ?? '');
43
+ /* Where the list is drawn. A list portalled to the end of the body sits
44
+ behind a modal <dialog>'s top layer, and half the pickers here live in
45
+ one — so inside a dialog the list is drawn into that dialog, and keeps
46
+ its stacking context; elsewhere it goes to the body the way every
47
+ floating surface here does. */
48
+ const root = useRef(null);
49
+ const [container, setContainer] = useState(null);
50
+ useEffect(() => {
51
+ setContainer(root.current?.closest('dialog, [role="dialog"]'));
52
+ }, []);
53
+ /* The trigger, for the anchor measured with the zoom-aware ruler (anchor.ts). */
54
+ const trigger = useRef(null);
55
+ const setTrigger = (node) => {
56
+ trigger.current = node;
57
+ if (typeof buttonRef === 'function')
58
+ buttonRef(node);
59
+ else if (buttonRef)
60
+ buttonRef.current = node;
61
+ };
62
+ return (_jsx("div", { className: "dd", ref: root, children: _jsxs(Select.Root, { multiple: many || undefined, value: (many ? chosen : selected ? selected.value : null), disabled: disabled, readOnly: readOnly, onOpenChange: onOpenChange,
63
+ /* The labels, handed over: without them Base UI mounts the list
64
+ once, hidden, to read the selected label off an item. */
65
+ items: items.map((i) => ({ value: i.value, label: i.label })), onValueChange: (next) => {
66
+ if (props.multiple === true) {
67
+ if (!Array.isArray(next))
68
+ return;
69
+ const ran = next.find((v) => typeof v === 'string' && v.startsWith(ACTION));
70
+ if (ran)
71
+ actions[Number(ran.slice(ACTION.length))]?.onSelect();
72
+ else
73
+ props.onChange(items.map((i) => i.value).filter((v) => next.includes(v)));
74
+ return;
75
+ }
76
+ if (typeof next !== 'string')
77
+ return;
78
+ if (next.startsWith(ACTION)) {
79
+ actions[Number(next.slice(ACTION.length))]?.onSelect();
80
+ return;
81
+ }
82
+ if (next !== props.value)
83
+ props.onChange(next);
84
+ }, children: [_jsxs(Select.Trigger, { render: _jsx("button", { type: "button", role: "button", id: id, className: buttonClassName ? `dd-button ${buttonClassName}` : 'dd-button', ref: setTrigger, "aria-label": ariaLabelledBy ? undefined : ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-invalid": ariaInvalid || undefined, "aria-describedby": ariaDescribedBy, title: selected?.detail ?? '' }), children: [selected?.swatches && (_jsx("span", { className: "dd-swatches", "aria-hidden": "true", children: selected.swatches.map((color, index) => (_jsx("span", { style: { background: color } }, index))) })), _jsx("span", { id: valueId, className: `dd-value${valueClassName ? ` ${valueClassName}` : ''}${said ? '' : ' is-empty'}`, children: said || placeholder || '—' }), _jsx("span", { className: "dd-caret", "aria-hidden": "true", children: "\u25BE" })] }), _jsx(Select.Portal, { container: container ?? popupContainer(), children: _jsx(Select.Positioner, { anchor: () => (trigger.current ? zoomAnchor(trigger.current) : null), alignItemWithTrigger: false, positionMethod: "fixed", side: "bottom", align: "start", sideOffset: 4, collisionBoundary: layoutViewport(), collisionPadding: 8, style: { zIndex: 40 }, children: _jsx(Select.Popup, { ref: popupRef, className: "dd-popup", children: _jsxs(Select.List, { className: ['dd-list', many ? 'dd-list-multi' : '', listClassName].filter(Boolean).join(' '), "aria-label": ariaLabel, "aria-labelledby": ariaLabel ? undefined : ariaLabelledBy?.split(' ')[0], children: [items.map((item) => (_jsxs(Select.Item, { value: item.value, label: item.label, disabled: item.disabled, className: (s) => `dd-option${s.selected ? ' selected' : ''}${item.disabled ? ' disabled' : ''}${item.tone ? ` dd-tone-${item.tone}` : ''}${s.highlighted ? ' active' : ''}`, children: [many ? (_jsx("span", { className: "dd-box", "aria-hidden": "true" })) : (_jsx("span", { className: "dd-check", "aria-hidden": "true", children: item.value === props.value ? '✓' : '' })), item.swatches && (_jsx("span", { className: "dd-swatches", "aria-hidden": "true", children: item.swatches.map((color, index) => (_jsx("span", { style: { background: color } }, index))) })), _jsxs("span", { className: "dd-text", children: [_jsx("span", { className: "dd-label", children: item.label }), item.detail && _jsx("span", { className: "dd-detail", children: item.detail })] }), item.badge && _jsx("span", { className: "dd-badge", children: item.badge })] }, item.value))), actions.length ? _jsx("div", { className: "dd-divider", role: "presentation" }) : null, actions.map((action, i) => (_jsxs(Select.Item, { value: `${ACTION}${i}`, label: action.label, className: (s) => `dd-option dd-action${s.highlighted ? ' active' : ''}`, children: [_jsx("span", { className: "dd-check", "aria-hidden": "true" }), _jsx("span", { className: "dd-text", children: _jsx("span", { className: "dd-label", children: action.label }) })] }, action.label)))] }) }) }) })] }) }));
85
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * One field: its label over the control, a hint under it, and the problem
3
+ * it holds under that — in the control's own column, so saying a problem
4
+ * never narrows or moves the control (docs/design.md, Access).
5
+ *
6
+ * The wiring is the point. The control is handed its id, what describes it
7
+ * (the hint, the problem), `aria-invalid` while it holds a problem and
8
+ * `aria-required` beside the star, so no form has to remember to join them.
9
+ * A control that is one element takes them as props (`children` an element);
10
+ * one that is several — a listbox's button, a date and its calendar, a
11
+ * picker — takes them through `children` as a function and puts them where
12
+ * its focusable is.
13
+ */
14
+ import { type ReactElement, type ReactNode } from 'react';
15
+ export interface FieldControlProps {
16
+ id: string;
17
+ 'aria-describedby'?: string;
18
+ 'aria-invalid'?: true;
19
+ 'aria-required'?: true;
20
+ /** A group's label is not a `<label>`: its control names itself by this. */
21
+ 'aria-labelledby'?: string;
22
+ }
23
+ interface FieldProps {
24
+ label: ReactNode;
25
+ /** A sentence that helps before anything goes wrong. */
26
+ hint?: ReactNode;
27
+ /** What is wrong with the value now, in the reader's words. Empty or absent: nothing. */
28
+ problem?: ReactNode;
29
+ required?: boolean;
30
+ /** The control's id; one is made when not given. */
31
+ id?: string;
32
+ /** A control that is not one labelable element (radios, a listbox's button): the label is a caption the control points at. */
33
+ group?: boolean;
34
+ /** A checkbox: the control first and its label beside it, on one line, the label checking it. */
35
+ check?: boolean;
36
+ /** The word that says a field may be left empty ("(optional)"), in the caller's language. Given on every field that is
37
+ not required, it is drawn only where the worn theme asks for it (`recipes.required: 'optional-word'`, the
38
+ stylesheet's to decide) and replaces the star. It stands beside the label, not in it, so the label's name is the
39
+ same either way; `aria-required` is untouched. */
40
+ optional?: ReactNode;
41
+ /** The label is read, not drawn: a group whose question is the heading above it. */
42
+ labelHidden?: boolean;
43
+ /** Something beside the label on its row: a help tip. */
44
+ labelAside?: ReactNode;
45
+ /** Keep the problem's line in the flow while it is empty, so a problem that arrives moves nothing below it. */
46
+ reserve?: boolean;
47
+ /** Compact: a filter's or a dense row's size. */
48
+ size?: 'compact' | 'default';
49
+ className?: string;
50
+ children: ReactElement<Record<string, unknown>> | ((control: FieldControlProps) => ReactNode);
51
+ }
52
+ export declare function Field({ label, hint, problem, required, id, group, check, size, optional, labelAside, labelHidden, reserve, className, children, }: FieldProps): import("react").JSX.Element;
53
+ export {};