atlas-react-kit 0.9.3 → 0.10.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/COMPONENTS.md +8 -2
- package/DESIGN_RULES.md +14 -0
- package/README.md +2 -2
- package/dist/combobox.d.ts +4 -1
- package/dist/combobox.js +5 -14
- package/dist/date-picker.js +5 -7
- package/dist/date-time-picker.js +3 -5
- package/dist/dialog.js +1 -1
- package/dist/dropdown-menu.js +1 -1
- package/dist/field.d.ts +1 -1
- package/dist/form/date-picker-field.d.ts +1 -1
- package/dist/form/date-picker-field.js +4 -5
- package/dist/form/date-time-picker-field.d.ts +1 -1
- package/dist/form/date-time-picker-field.js +4 -5
- package/dist/form/field-shell.d.ts +21 -0
- package/dist/form/field-shell.js +23 -0
- package/dist/form/markdown-field.js +10 -8
- package/dist/form/select-field.d.ts +1 -1
- package/dist/form/select-field.js +3 -4
- package/dist/form/text-field.d.ts +1 -1
- package/dist/form/text-field.js +4 -7
- package/dist/info-tip.d.ts +4 -1
- package/dist/info-tip.js +2 -2
- package/dist/internal/form-control.d.ts +9 -0
- package/dist/internal/form-control.js +9 -0
- package/dist/internal/listbox-navigation.d.ts +12 -0
- package/dist/internal/listbox-navigation.js +16 -0
- package/dist/internal/styles.d.ts +2 -0
- package/dist/internal/styles.js +2 -0
- package/dist/lazy-calendar.d.ts +6 -0
- package/dist/lazy-calendar.js +18 -0
- package/dist/popover.js +1 -1
- package/dist/search-picker.d.ts +3 -1
- package/dist/search-picker.js +16 -11
- package/dist/select.js +4 -3
- package/dist/table-body-skeleton.js +1 -1
- package/dist/tabs.js +1 -1
- package/package.json +1 -1
package/COMPONENTS.md
CHANGED
|
@@ -8,6 +8,12 @@ no root component barrel.
|
|
|
8
8
|
For installation, see [README.md](./README.md); for styling and accessibility,
|
|
9
9
|
see [DESIGN_RULES.md](./DESIGN_RULES.md).
|
|
10
10
|
|
|
11
|
+
For LLM-assisted implementation of patterns this kit does not export, use
|
|
12
|
+
[COSS UI](https://coss.com/ui) as a reference before building a component from
|
|
13
|
+
scratch. Its Base UI catalog includes input groups, number fields, OTP fields,
|
|
14
|
+
drawers, and segmented controls. COSS is a separate library; its components
|
|
15
|
+
are not Atlas exports.
|
|
16
|
+
|
|
11
17
|
## Optional peer dependencies
|
|
12
18
|
|
|
13
19
|
Core entries need only the kit. These feature entries need an extra install
|
|
@@ -131,8 +137,8 @@ action. Pass `side` to prefer a placement; Radix still flips on collision.
|
|
|
131
137
|
form-control chrome. The popover is one month, a labelled Hour and Minute
|
|
132
138
|
pair (24-hour, stepped by `timeStep`, default 15), and Clear. Picking a day
|
|
133
139
|
keeps the popover open so the time can be set; the time selects stay
|
|
134
|
-
disabled until a day is chosen. The panel is `w-auto`,
|
|
135
|
-
|
|
140
|
+
disabled until a day is chosen. The panel is `w-auto`, sized by the
|
|
141
|
+
calendar, and clamped to the viewport so it stays on-screen at 390px. Pass
|
|
136
142
|
`side` to prefer a placement. `FormDateTimePickerField` forwards `className`
|
|
137
143
|
and `timeStep` to that control.
|
|
138
144
|
|
package/DESIGN_RULES.md
CHANGED
|
@@ -107,6 +107,20 @@ column, and `min-w-0` so nested tables scroll instead of widening the card.
|
|
|
107
107
|
`Dialog` already scrolls inside the viewport on small screens; avoid
|
|
108
108
|
additional height limits.
|
|
109
109
|
|
|
110
|
+
Apps decide how wide and tall containers and overlays are. The kit sets
|
|
111
|
+
intrinsic control sizes (the button, input, tab and row heights, icons,
|
|
112
|
+
checkboxes, switches, avatars, slider thumbs, and track thickness) and
|
|
113
|
+
clamps overlays to the viewport, but no design widths or heights on
|
|
114
|
+
dialogs, popovers, menus, or lists. Set them with `className`: give each
|
|
115
|
+
`DialogContent` a width such as `sm:max-w-md`, and each `PopoverContent`
|
|
116
|
+
one such as `w-72`. Where the sized part is not the element `className`
|
|
117
|
+
reaches, a prop names it: `InfoTip` takes `contentClassName` (default
|
|
118
|
+
`w-72`), and `Combobox` and `SearchPicker` take `listClassName` (default
|
|
119
|
+
`max-h-60`). `TabsNav` fills a block container but sizes to its links in a
|
|
120
|
+
flex row; add `w-full` to stretch it there. `Tooltip` keeps a `max-w-xs` reading width, `Textarea` a
|
|
121
|
+
`min-h-16` starting height, and toasts their 356px width; override the first
|
|
122
|
+
two with `className`.
|
|
123
|
+
|
|
110
124
|
`Input`, `SelectTrigger`, `DatePickerButton`, `DateTimePickerButton`, and
|
|
111
125
|
`Combobox` share form-control chrome: 40px height, `px-3`,
|
|
112
126
|
`text-base md:text-sm`, `border-input`, and `shadow-xs`. Do not restyle those
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ React 19, Tailwind 4, and Node >=20.19 are required. Next 16 is needed only
|
|
|
8
8
|
when using `next/tabs-nav-link`.
|
|
9
9
|
|
|
10
10
|
```sh
|
|
11
|
-
npm install --save-exact atlas-react-kit@0.
|
|
11
|
+
npm install --save-exact atlas-react-kit@0.10.0
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
Import components by subpath:
|
|
@@ -128,7 +128,7 @@ artifact with the package owner's npm login:
|
|
|
128
128
|
```sh
|
|
129
129
|
mkdir -p release-artifacts
|
|
130
130
|
npm pack --pack-destination release-artifacts
|
|
131
|
-
npm publish ./release-artifacts/atlas-react-kit-0.
|
|
131
|
+
npm publish ./release-artifacts/atlas-react-kit-0.10.0.tgz --access public
|
|
132
132
|
```
|
|
133
133
|
|
|
134
134
|
Pin an exact package version in consuming applications. Roll back an upgrade by
|
package/dist/combobox.d.ts
CHANGED
|
@@ -12,7 +12,10 @@ type ComboboxProps = {
|
|
|
12
12
|
emptyText?: string;
|
|
13
13
|
disabled?: boolean;
|
|
14
14
|
id?: string;
|
|
15
|
+
/** Classes for the trigger, e.g. its width; the list matches it. */
|
|
15
16
|
className?: string;
|
|
17
|
+
/** Classes for the listbox, e.g. its height. Defaults to `max-h-60`. */
|
|
18
|
+
listClassName?: string;
|
|
16
19
|
"aria-invalid"?: boolean;
|
|
17
20
|
/** Name the trigger when there is no `<Label htmlFor={id}>`. */
|
|
18
21
|
"aria-label"?: string;
|
|
@@ -43,5 +46,5 @@ type ComboboxProps = {
|
|
|
43
46
|
* Opt-in `creatable` appends a create action when the query is not already
|
|
44
47
|
* an option; choosing it calls `onValueChange` like a normal selection.
|
|
45
48
|
*/
|
|
46
|
-
export declare function Combobox({ options, value, onValueChange, placeholder, searchPlaceholder, emptyText, disabled, id, className, "aria-invalid": ariaInvalid, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, creatable, normalizeCreateValue, createLabel, }: ComboboxProps): React.JSX.Element;
|
|
49
|
+
export declare function Combobox({ options, value, onValueChange, placeholder, searchPlaceholder, emptyText, disabled, id, className, listClassName, "aria-invalid": ariaInvalid, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, creatable, normalizeCreateValue, createLabel, }: ComboboxProps): React.JSX.Element;
|
|
47
50
|
export {};
|
package/dist/combobox.js
CHANGED
|
@@ -5,9 +5,9 @@ import { CheckIcon, ChevronDownIcon } from "lucide-react";
|
|
|
5
5
|
import { Button } from "./button.js";
|
|
6
6
|
import { Input } from "./input.js";
|
|
7
7
|
import { formControlTriggerClassName } from "./internal/form-control.js";
|
|
8
|
-
import { moveListboxHighlight } from "./internal/listbox-navigation.js";
|
|
8
|
+
import { listboxOptionId, moveListboxHighlight, scrollOptionIntoView, } from "./internal/listbox-navigation.js";
|
|
9
9
|
import { Popover, PopoverContent, PopoverTrigger } from "./popover.js";
|
|
10
|
-
import { optionState } from "./internal/styles.js";
|
|
10
|
+
import { listboxEmpty, optionState } from "./internal/styles.js";
|
|
11
11
|
import { cn, onInputChange } from "./utils.js";
|
|
12
12
|
const CREATE_OPTION_KEY = "__atlas-combobox-create";
|
|
13
13
|
/**
|
|
@@ -49,7 +49,7 @@ function resolveCreateValue(query, options, normalize) {
|
|
|
49
49
|
* Opt-in `creatable` appends a create action when the query is not already
|
|
50
50
|
* an option; choosing it calls `onValueChange` like a normal selection.
|
|
51
51
|
*/
|
|
52
|
-
export function Combobox({ options, value, onValueChange, placeholder = "Select", searchPlaceholder = "Search", emptyText = "No results", disabled, id, className, "aria-invalid": ariaInvalid, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, creatable = false, normalizeCreateValue = defaultNormalizeCreateValue, createLabel = defaultCreateLabel, }) {
|
|
52
|
+
export function Combobox({ options, value, onValueChange, placeholder = "Select", searchPlaceholder = "Search", emptyText = "No results", disabled, id, className, listClassName, "aria-invalid": ariaInvalid, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, creatable = false, normalizeCreateValue = defaultNormalizeCreateValue, createLabel = defaultCreateLabel, }) {
|
|
53
53
|
const [open, setOpen] = React.useState(false);
|
|
54
54
|
const [query, setQuery] = React.useState("");
|
|
55
55
|
const [highlight, setHighlight] = React.useState(0);
|
|
@@ -96,12 +96,6 @@ export function Combobox({ options, value, onValueChange, placeholder = "Select"
|
|
|
96
96
|
const frame = window.requestAnimationFrame(() => inputRef.current?.focus());
|
|
97
97
|
return () => window.cancelAnimationFrame(frame);
|
|
98
98
|
}, [open]);
|
|
99
|
-
// Callback ref (not an effect) so scroll runs when Radix mounts the list
|
|
100
|
-
// and when the highlight moves. A layout effect on `open` is too early:
|
|
101
|
-
// the option node does not exist until the popover content is in the DOM.
|
|
102
|
-
const scrollHighlightedOption = React.useCallback((node) => {
|
|
103
|
-
node?.scrollIntoView({ block: "nearest" });
|
|
104
|
-
}, []);
|
|
105
99
|
const onQueryChange = onInputChange((next) => {
|
|
106
100
|
setQuery(next);
|
|
107
101
|
setHighlight(0);
|
|
@@ -114,9 +108,6 @@ export function Combobox({ options, value, onValueChange, placeholder = "Select"
|
|
|
114
108
|
setOpen(false);
|
|
115
109
|
setQuery("");
|
|
116
110
|
};
|
|
117
|
-
// Index-based ids: option values may contain spaces, which are not valid
|
|
118
|
-
// in an id or an aria-activedescendant reference.
|
|
119
|
-
const optionId = (index) => `${listId}-option-${index}`;
|
|
120
111
|
const activeOption = list[highlight];
|
|
121
112
|
const onSearchKeyDown = (event) => {
|
|
122
113
|
const nextHighlight = moveListboxHighlight(event.key, highlight, list.length);
|
|
@@ -132,10 +123,10 @@ export function Combobox({ options, value, onValueChange, placeholder = "Select"
|
|
|
132
123
|
selectValue(option.value);
|
|
133
124
|
}
|
|
134
125
|
};
|
|
135
|
-
return (_jsxs(Popover, { open: open, onOpenChange: onOpenChange, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { id: id, type: "button", variant: "outline", role: "combobox", "aria-expanded": open, "aria-controls": listId, "aria-invalid": ariaInvalid, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, disabled: disabled, className: cn(formControlTriggerClassName, "justify-between", !selected && "text-muted-foreground", className), children: [_jsx("span", { className: "truncate", children: selected?.label ?? placeholder }), _jsx(ChevronDownIcon, { "data-icon": "inline-end", className: "opacity-50" })] }) }), _jsxs(PopoverContent, { align: "start", className: "w-[var(--radix-popover-trigger-width)] gap-0 rounded-none p-0", children: [_jsx("div", { className: "border-b p-2", children: _jsx(Input, { ref: inputRef, value: query, onChange: onQueryChange, onKeyDown: onSearchKeyDown, placeholder: searchPlaceholder, "aria-autocomplete": "list", "aria-controls": listId, "aria-activedescendant": activeOption ?
|
|
126
|
+
return (_jsxs(Popover, { open: open, onOpenChange: onOpenChange, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { id: id, type: "button", variant: "outline", role: "combobox", "aria-expanded": open, "aria-controls": listId, "aria-invalid": ariaInvalid, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, disabled: disabled, className: cn(formControlTriggerClassName, "justify-between", !selected && "text-muted-foreground", className), children: [_jsx("span", { className: "truncate", children: selected?.label ?? placeholder }), _jsx(ChevronDownIcon, { "data-icon": "inline-end", className: "opacity-50" })] }) }), _jsxs(PopoverContent, { align: "start", className: "w-[var(--radix-popover-trigger-width)] gap-0 rounded-none p-0", children: [_jsx("div", { className: "border-b p-2", children: _jsx(Input, { ref: inputRef, value: query, onChange: onQueryChange, onKeyDown: onSearchKeyDown, placeholder: searchPlaceholder, "aria-autocomplete": "list", "aria-controls": listId, "aria-activedescendant": activeOption ? listboxOptionId(listId, highlight) : undefined }) }), _jsx("ul", { id: listId, role: "listbox", className: cn("max-h-60 overflow-y-auto p-1", listClassName), children: list.length === 0 ? (_jsx("li", { className: listboxEmpty, children: emptyText })) : (list.map((option, index) => {
|
|
136
127
|
const isCreate = createOption != null && option === createOption;
|
|
137
128
|
const isSelected = option.value === value && !isCreate;
|
|
138
129
|
const isActive = index === highlight;
|
|
139
|
-
return (_jsx("li", { role: "none", children: _jsxs("button", { ref: isActive ?
|
|
130
|
+
return (_jsx("li", { role: "none", children: _jsxs("button", { ref: isActive ? scrollOptionIntoView : undefined, id: listboxOptionId(listId, index), type: "button", role: "option", "aria-selected": isSelected, className: cn("relative flex w-full cursor-default items-center gap-2 rounded-none py-2 pr-8 pl-3 text-left text-sm outline-hidden select-none", isSelected && optionState.selected, isActive && optionState.active), onMouseEnter: () => setHighlight(index), onClick: () => selectValue(option.value), children: [_jsx("span", { className: "truncate", children: option.label }), isSelected ? (_jsx(CheckIcon, { className: "pointer-events-none absolute right-2 size-3.5" })) : null] }) }, isCreate ? CREATE_OPTION_KEY : option.value));
|
|
140
131
|
})) })] })] }));
|
|
141
132
|
}
|
package/dist/date-picker.js
CHANGED
|
@@ -4,9 +4,9 @@ import * as React from "react";
|
|
|
4
4
|
import { format, isSameDay, parseISO } from "date-fns";
|
|
5
5
|
import { CalendarIcon } from "lucide-react";
|
|
6
6
|
import { Button } from "./button.js";
|
|
7
|
-
import {
|
|
8
|
-
import { formControlTriggerClassName } from "./internal/form-control.js";
|
|
7
|
+
import { PICKER_COLLISION_PADDING, pickerContentClassName, pickerTriggerClassName, } from "./internal/form-control.js";
|
|
9
8
|
import { MD_MEDIA_QUERY, useMediaQuery } from "./internal/use-media-query.js";
|
|
9
|
+
import { LazyCalendar, preloadCalendar } from "./lazy-calendar.js";
|
|
10
10
|
import { Popover, PopoverContent, PopoverTrigger } from "./popover.js";
|
|
11
11
|
import { cn } from "./utils.js";
|
|
12
12
|
/**
|
|
@@ -56,16 +56,14 @@ function formatRangeTriggerLabel(from, to, placeholder) {
|
|
|
56
56
|
return format(from, "PPP");
|
|
57
57
|
return `${format(from, "PPP")} – ${format(to, "PPP")}`;
|
|
58
58
|
}
|
|
59
|
-
/** Gutter used when the calendar popover collides with the viewport. */
|
|
60
|
-
const DATE_PICKER_COLLISION_PADDING = 16;
|
|
61
59
|
/** Shared trigger + auto-sized popover chrome for single and range modes. */
|
|
62
60
|
function DatePickerShell({ id, disabled, ariaInvalid, className, empty, label, open, onOpenChange, side, children, }) {
|
|
63
|
-
return (_jsxs(Popover, { open: open, onOpenChange: onOpenChange, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { id: id, type: "button", variant: "outline", disabled: disabled, "aria-invalid": ariaInvalid, "data-empty": empty, className: cn(
|
|
61
|
+
return (_jsxs(Popover, { open: open, onOpenChange: onOpenChange, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { id: id, type: "button", variant: "outline", disabled: disabled, "aria-invalid": ariaInvalid, "data-empty": empty, className: cn(pickerTriggerClassName, className), onPointerEnter: preloadCalendar, onFocus: preloadCalendar, children: [_jsx(CalendarIcon, { "data-icon": "inline-start" }), label] }) }), _jsx(PopoverContent, { align: "start", side: side, collisionPadding: PICKER_COLLISION_PADDING, className: cn("w-auto", pickerContentClassName), children: children })] }));
|
|
64
62
|
}
|
|
65
63
|
function DatePickerSingleButton({ id, value, onChange, placeholder = "Pick a date", disabled, className, "aria-invalid": ariaInvalid, numberOfMonths, side, defaultMonth, }) {
|
|
66
64
|
const [open, setOpen] = React.useState(false);
|
|
67
65
|
const date = toDate(value);
|
|
68
|
-
return (_jsx(DatePickerShell, { id: id, disabled: disabled, ariaInvalid: ariaInvalid, className: className, empty: !date, label: date ? format(date, "PPP") : placeholder, open: open, onOpenChange: setOpen, side: side, children: _jsx(
|
|
66
|
+
return (_jsx(DatePickerShell, { id: id, disabled: disabled, ariaInvalid: ariaInvalid, className: className, empty: !date, label: date ? format(date, "PPP") : placeholder, open: open, onOpenChange: setOpen, side: side, children: _jsx(LazyCalendar, { mode: "single", selected: date, defaultMonth: defaultMonth ?? date, numberOfMonths: numberOfMonths, onSelect: (next) => {
|
|
69
67
|
onChange(next);
|
|
70
68
|
setOpen(false);
|
|
71
69
|
} }) }));
|
|
@@ -86,7 +84,7 @@ function DatePickerRangeButton({ id, value, onChange, placeholder = "Pick a date
|
|
|
86
84
|
restartRef.current = Boolean(from && to);
|
|
87
85
|
setOpen(next);
|
|
88
86
|
};
|
|
89
|
-
return (_jsxs(DatePickerShell, { id: id, disabled: disabled, ariaInvalid: ariaInvalid, className: className, empty: !from, label: formatRangeTriggerLabel(from, to, placeholder), open: open, onOpenChange: onOpenChange, side: side, children: [_jsx(
|
|
87
|
+
return (_jsxs(DatePickerShell, { id: id, disabled: disabled, ariaInvalid: ariaInvalid, className: className, empty: !from, label: formatRangeTriggerLabel(from, to, placeholder), open: open, onOpenChange: onOpenChange, side: side, children: [_jsx(LazyCalendar, { mode: "range", selected: selected, defaultMonth: defaultMonth ?? from ?? to, numberOfMonths: months, onSelect: (next, day) => {
|
|
90
88
|
const range = restartRef.current ? { from: day, to: day } : next;
|
|
91
89
|
restartRef.current = false;
|
|
92
90
|
onChange(range);
|
package/dist/date-time-picker.js
CHANGED
|
@@ -4,15 +4,13 @@ import * as React from "react";
|
|
|
4
4
|
import { format } from "date-fns";
|
|
5
5
|
import { CalendarIcon } from "lucide-react";
|
|
6
6
|
import { Button } from "./button.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { PICKER_COLLISION_PADDING, pickerContentClassName, pickerTriggerClassName, } from "./internal/form-control.js";
|
|
8
|
+
import { LazyCalendar, preloadCalendar } from "./lazy-calendar.js";
|
|
9
9
|
import { Popover, PopoverContent, PopoverTrigger } from "./popover.js";
|
|
10
10
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "./select.js";
|
|
11
11
|
import { cn } from "./utils.js";
|
|
12
12
|
/** Default clock increment, in minutes. Must divide 60. */
|
|
13
13
|
export const DEFAULT_TIME_STEP = 15;
|
|
14
|
-
/** Gutter used when the calendar popover collides with the viewport. */
|
|
15
|
-
const DATE_TIME_PICKER_COLLISION_PADDING = 16;
|
|
16
14
|
/**
|
|
17
15
|
* Normalize a minute step so option lists stay even (1, 5, 15, 30, 60).
|
|
18
16
|
* Invalid values fall back to 15 so a bad prop cannot empty the minute list.
|
|
@@ -81,7 +79,7 @@ export function DateTimePickerButton({ id, value, onChange, timeStep, placeholde
|
|
|
81
79
|
const label = date ? format(date, "PPP p") : placeholder;
|
|
82
80
|
const hourId = React.useId();
|
|
83
81
|
const minuteId = React.useId();
|
|
84
|
-
return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { id: id, type: "button", variant: "outline", disabled: disabled, "aria-invalid": ariaInvalid, "data-empty": empty, className: cn(
|
|
82
|
+
return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { id: id, type: "button", variant: "outline", disabled: disabled, "aria-invalid": ariaInvalid, "data-empty": empty, className: cn(pickerTriggerClassName, className), onPointerEnter: preloadCalendar, onFocus: preloadCalendar, children: [_jsx(CalendarIcon, { "data-icon": "inline-start" }), label] }) }), _jsxs(PopoverContent, { align: "start", side: side, collisionPadding: PICKER_COLLISION_PADDING, onInteractOutside: preventDismissForPortaledSelect, className: cn("w-auto min-w-0", pickerContentClassName), children: [_jsx(LazyCalendar, { mode: "single", selected: date ?? undefined, defaultMonth: defaultMonth ?? date ?? undefined, onSelect: (next) => {
|
|
85
83
|
// Re-clicking a selected day would clear it; require Clear instead
|
|
86
84
|
// so a time already set is not dropped by an accidental second tap.
|
|
87
85
|
if (!next)
|
package/dist/dialog.js
CHANGED
|
@@ -21,7 +21,7 @@ function DialogOverlay({ className, ...props }) {
|
|
|
21
21
|
return (_jsx(DialogPrimitive.Overlay, { "data-slot": "dialog-overlay", className: cn("fixed inset-0 isolate z-50 bg-black/20 duration-100 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0 supports-backdrop-filter:backdrop-blur-sm", className), ...props }));
|
|
22
22
|
}
|
|
23
23
|
function DialogContent({ className, children, showCloseButton = true, ...props }) {
|
|
24
|
-
return (_jsxs(DialogPortal, { children: [_jsx(DialogOverlay, {}), _jsxs(DialogPrimitive.Content, { "data-slot": "dialog-content", className: cn("fixed top-1/2 left-1/2 z-50 grid max-h-[90dvh] w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-6 overflow-y-auto rounded-none bg-popover p-6 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95
|
|
24
|
+
return (_jsxs(DialogPortal, { children: [_jsx(DialogOverlay, {}), _jsxs(DialogPrimitive.Content, { "data-slot": "dialog-content", className: cn("fixed top-1/2 left-1/2 z-50 grid max-h-[90dvh] w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-6 overflow-y-auto rounded-none bg-popover p-6 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95", className), ...props, children: [children, showCloseButton && (_jsx(DialogPrimitive.Close, { "data-slot": "dialog-close", asChild: true, children: _jsxs(Button, { variant: "ghost", size: "icon-sm", className: "absolute top-4 right-4 text-muted-foreground hover:text-foreground", children: [_jsx(XIcon, {}), _jsx("span", { className: "sr-only", children: "Close" })] }) }))] })] }));
|
|
25
25
|
}
|
|
26
26
|
function DialogHeader({ className, ...props }) {
|
|
27
27
|
return (_jsx("div", { "data-slot": "dialog-header", className: cn("flex flex-col gap-2", className), ...props }));
|
package/dist/dropdown-menu.js
CHANGED
|
@@ -7,7 +7,7 @@ import { dataDisabledState, optionState } from "./internal/styles.js";
|
|
|
7
7
|
import { cn } from "./utils.js";
|
|
8
8
|
/** Item highlight and selected chrome shared with `SelectItem`. */
|
|
9
9
|
const menuItemClassName = cn(optionState.focus, dataDisabledState, "relative flex cursor-default items-center gap-2.5 rounded-none px-3 py-2 text-sm outline-hidden select-none data-inset:pl-9 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5");
|
|
10
|
-
const menuContentClassName = "z-50
|
|
10
|
+
const menuContentClassName = "z-50 overflow-x-hidden overflow-y-auto rounded-none bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95";
|
|
11
11
|
/** Action menu for commands (row actions, overflow menus). Use `Select` to pick a value. */
|
|
12
12
|
function DropdownMenu({ ...props }) {
|
|
13
13
|
return _jsx(DropdownMenuPrimitive.Root, { "data-slot": "dropdown-menu", ...props });
|
package/dist/field.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ declare function FieldLegend({ className, variant, ...props }: React.ComponentPr
|
|
|
6
6
|
}): import("react").JSX.Element;
|
|
7
7
|
declare function FieldGroup({ className, ...props }: React.ComponentProps<"div">): import("react").JSX.Element;
|
|
8
8
|
declare const fieldVariants: (props?: ({
|
|
9
|
-
orientation?: "
|
|
9
|
+
orientation?: "vertical" | "horizontal" | "responsive" | null | undefined;
|
|
10
10
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
11
11
|
declare function Field({ className, orientation, ...props }: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>): import("react").JSX.Element;
|
|
12
12
|
declare function FieldContent({ className, ...props }: React.ComponentProps<"div">): import("react").JSX.Element;
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { useId } from "react";
|
|
4
4
|
import { DatePickerButton } from "../date-picker.js";
|
|
5
|
-
import {
|
|
6
|
-
import { FieldError, getFieldErrorMessage, } from "./field-error.js";
|
|
5
|
+
import { FormFieldShell, invalidWhen, metaErrorText } from "./field-shell.js";
|
|
7
6
|
/** Date picker with built-in field error text and invalid styling. */
|
|
8
7
|
export function FormDatePickerField({ value, onValueChange, label, meta, placeholder, disabled, className, }) {
|
|
9
|
-
const errorText =
|
|
8
|
+
const errorText = metaErrorText(meta);
|
|
10
9
|
const id = useId();
|
|
11
|
-
return (
|
|
10
|
+
return (_jsx(FormFieldShell, { id: id, label: label, errorText: errorText, children: _jsx(DatePickerButton, { id: id, value: value, onChange: onValueChange, placeholder: placeholder, disabled: disabled, className: className, "aria-invalid": invalidWhen(errorText) }) }));
|
|
12
11
|
}
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { useId } from "react";
|
|
4
4
|
import { DateTimePickerButton } from "../date-time-picker.js";
|
|
5
|
-
import {
|
|
6
|
-
import { FieldError, getFieldErrorMessage, } from "./field-error.js";
|
|
5
|
+
import { FormFieldShell, invalidWhen, metaErrorText } from "./field-shell.js";
|
|
7
6
|
/** Date-time picker with built-in field error text and invalid styling. */
|
|
8
7
|
export function FormDateTimePickerField({ value, onValueChange, label, meta, placeholder, disabled, timeStep, className, }) {
|
|
9
|
-
const errorText =
|
|
8
|
+
const errorText = metaErrorText(meta);
|
|
10
9
|
const id = useId();
|
|
11
|
-
return (
|
|
10
|
+
return (_jsx(FormFieldShell, { id: id, label: label, errorText: errorText, children: _jsx(DateTimePickerButton, { id: id, value: value, onChange: onValueChange, placeholder: placeholder, disabled: disabled, timeStep: timeStep, className: className, "aria-invalid": invalidWhen(errorText) }) }));
|
|
12
11
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { type FieldMetaState } from "./field-error.js";
|
|
3
|
+
/**
|
|
4
|
+
* Not a public entry. The layout and error wiring every `form/*` field
|
|
5
|
+
* shares, so a field only says which control it renders.
|
|
6
|
+
*/
|
|
7
|
+
/** First error in `meta`, or `undefined` when there is no meta or no error. */
|
|
8
|
+
export declare function metaErrorText(meta: FieldMetaState | undefined): string | undefined;
|
|
9
|
+
/** `aria-invalid` for the control: `true` while there is error text. */
|
|
10
|
+
export declare function invalidWhen(errorText: string | undefined): true | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* Label, control, and error text stacked with the kit's field spacing.
|
|
13
|
+
* `labelAction` sits on the label's row (the Markdown field's mode toggle).
|
|
14
|
+
*/
|
|
15
|
+
export declare function FormFieldShell({ id, label, labelAction, errorText, children, }: {
|
|
16
|
+
id: string;
|
|
17
|
+
label: string;
|
|
18
|
+
labelAction?: ReactNode;
|
|
19
|
+
errorText: string | undefined;
|
|
20
|
+
children: ReactNode;
|
|
21
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Label } from "../label.js";
|
|
3
|
+
import { FieldError, getFieldErrorMessage, } from "./field-error.js";
|
|
4
|
+
/**
|
|
5
|
+
* Not a public entry. The layout and error wiring every `form/*` field
|
|
6
|
+
* shares, so a field only says which control it renders.
|
|
7
|
+
*/
|
|
8
|
+
/** First error in `meta`, or `undefined` when there is no meta or no error. */
|
|
9
|
+
export function metaErrorText(meta) {
|
|
10
|
+
return meta ? getFieldErrorMessage(meta.errors[0]) : undefined;
|
|
11
|
+
}
|
|
12
|
+
/** `aria-invalid` for the control: `true` while there is error text. */
|
|
13
|
+
export function invalidWhen(errorText) {
|
|
14
|
+
return errorText ? true : undefined;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Label, control, and error text stacked with the kit's field spacing.
|
|
18
|
+
* `labelAction` sits on the label's row (the Markdown field's mode toggle).
|
|
19
|
+
*/
|
|
20
|
+
export function FormFieldShell({ id, label, labelAction, errorText, children, }) {
|
|
21
|
+
const labelNode = _jsx(Label, { htmlFor: id, children: label });
|
|
22
|
+
return (_jsxs("div", { className: "flex flex-col gap-1.5", children: [labelAction ? (_jsxs("div", { className: "flex items-center justify-between gap-2", children: [labelNode, labelAction] })) : (labelNode), children, _jsx(FieldError, { text: errorText })] }));
|
|
23
|
+
}
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useState } from "react";
|
|
4
|
-
import {
|
|
5
|
-
import { Markdown } from "../markdown.js";
|
|
3
|
+
import { lazy, Suspense, useState } from "react";
|
|
4
|
+
import { Spinner } from "../spinner.js";
|
|
6
5
|
import { Textarea } from "../textarea.js";
|
|
7
6
|
import { ToggleGroup, ToggleGroupItem } from "../toggle-group.js";
|
|
8
7
|
import { fieldSurface } from "../internal/styles.js";
|
|
9
8
|
import { cn, onInputChange } from "../utils.js";
|
|
10
|
-
import {
|
|
9
|
+
import { FormFieldShell, invalidWhen, metaErrorText } from "./field-shell.js";
|
|
10
|
+
// The renderer (react-markdown + remark-gfm) is fetched the first time
|
|
11
|
+
// Preview is shown or hovered, so a form in Write mode stays light.
|
|
12
|
+
const loadMarkdown = () => import("../markdown.js");
|
|
13
|
+
const preloadMarkdown = () => void loadMarkdown();
|
|
14
|
+
const Markdown = lazy(() => loadMarkdown().then((module) => ({ default: module.Markdown })));
|
|
11
15
|
/**
|
|
12
16
|
* GFM body field: Write is a textarea, Preview is the sanitized Markdown
|
|
13
17
|
* renderer. String-in/string-out — not a WYSIWYG document model.
|
|
@@ -15,13 +19,11 @@ import { FieldError, getFieldErrorMessage } from "./field-error.js";
|
|
|
15
19
|
export function FormMarkdownField({ field, id = field.name, label, placeholder = "Markdown supported", rows = 5, showError = true, }) {
|
|
16
20
|
const [mode, setMode] = useState("write");
|
|
17
21
|
const onChange = onInputChange(field.handleChange);
|
|
18
|
-
const errorText = showError
|
|
19
|
-
? getFieldErrorMessage(field.state.meta.errors[0])
|
|
20
|
-
: undefined;
|
|
22
|
+
const errorText = showError ? metaErrorText(field.state.meta) : undefined;
|
|
21
23
|
const trimmed = field.state.value.trim();
|
|
22
24
|
function handleModeChange(value) {
|
|
23
25
|
if (value === "write" || value === "preview")
|
|
24
26
|
setMode(value);
|
|
25
27
|
}
|
|
26
|
-
return (
|
|
28
|
+
return (_jsx(FormFieldShell, { id: id, label: label, errorText: errorText, labelAction: _jsxs(ToggleGroup, { type: "single", value: mode, onValueChange: handleModeChange, variant: "outline", size: "sm", spacing: 0, "aria-label": `${label} mode`, children: [_jsx(ToggleGroupItem, { value: "write", children: "Write" }), _jsx(ToggleGroupItem, { value: "preview", onPointerEnter: preloadMarkdown, onFocus: preloadMarkdown, children: "Preview" })] }), children: mode === "write" ? (_jsx(Textarea, { id: id, value: field.state.value, onChange: onChange, onBlur: field.handleBlur, placeholder: placeholder, rows: rows, "aria-invalid": invalidWhen(errorText) })) : (_jsx("div", { className: cn(fieldSurface, "min-h-16"), "aria-live": "polite", children: trimmed ? (_jsx(Suspense, { fallback: _jsx(Spinner, { "aria-label": "Loading preview" }), children: _jsx(Markdown, { content: trimmed }) })) : (_jsx("p", { className: "text-sm text-muted-foreground", children: "Nothing to preview" })) })) }));
|
|
27
29
|
}
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useId } from "react";
|
|
4
|
-
import { Label } from "../label.js";
|
|
5
4
|
import { Select, SelectContent, SelectTrigger, SelectValue } from "../select.js";
|
|
6
|
-
import {
|
|
5
|
+
import { FormFieldShell, invalidWhen, metaErrorText } from "./field-shell.js";
|
|
7
6
|
/** Select control with built-in field error text and invalid styling. */
|
|
8
7
|
export function FormSelectField({ value, meta, label, onValueChange, placeholder, children, }) {
|
|
9
|
-
const errorText =
|
|
8
|
+
const errorText = metaErrorText(meta);
|
|
10
9
|
const id = useId();
|
|
11
|
-
return (
|
|
10
|
+
return (_jsx(FormFieldShell, { id: id, label: label, errorText: errorText, children: _jsxs(Select, { value: value, onValueChange: onValueChange, children: [_jsx(SelectTrigger, { id: id, "aria-invalid": invalidWhen(errorText), children: _jsx(SelectValue, { placeholder: placeholder }) }), _jsx(SelectContent, { children: children })] }) }));
|
|
12
11
|
}
|
package/dist/form/text-field.js
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Input } from "../input.js";
|
|
4
|
-
import { Label } from "../label.js";
|
|
5
4
|
import { Textarea } from "../textarea.js";
|
|
6
5
|
import { onInputChange } from "../utils.js";
|
|
7
|
-
import {
|
|
6
|
+
import { FormFieldShell, invalidWhen, metaErrorText } from "./field-shell.js";
|
|
8
7
|
export function FormTextField({ field, id = field.name, label, placeholder, description, autoFocus, multiline, rows = 3, inputType = "text", showError = true, autoComplete, required, minLength, }) {
|
|
9
8
|
const onChange = onInputChange(field.handleChange);
|
|
10
|
-
const errorText = showError
|
|
11
|
-
? getFieldErrorMessage(field.state.meta.errors[0])
|
|
12
|
-
: undefined;
|
|
9
|
+
const errorText = showError ? metaErrorText(field.state.meta) : undefined;
|
|
13
10
|
const controlProps = {
|
|
14
11
|
id,
|
|
15
12
|
value: field.state.value,
|
|
@@ -19,7 +16,7 @@ export function FormTextField({ field, id = field.name, label, placeholder, desc
|
|
|
19
16
|
autoComplete,
|
|
20
17
|
required,
|
|
21
18
|
minLength,
|
|
22
|
-
"aria-invalid": errorText
|
|
19
|
+
"aria-invalid": invalidWhen(errorText),
|
|
23
20
|
};
|
|
24
|
-
return (_jsxs(
|
|
21
|
+
return (_jsxs(FormFieldShell, { id: id, label: label, errorText: errorText, children: [description ? (_jsx("p", { className: "text-xs text-muted-foreground", children: description })) : null, multiline ? (_jsx(Textarea, { ...controlProps, rows: rows })) : (_jsx(Input, { ...controlProps, type: inputType, autoFocus: autoFocus }))] }));
|
|
25
22
|
}
|
package/dist/info-tip.d.ts
CHANGED
|
@@ -14,14 +14,17 @@ export type InfoTipProps = {
|
|
|
14
14
|
align?: React.ComponentProps<typeof PopoverContent>["align"];
|
|
15
15
|
badge?: React.ReactNode;
|
|
16
16
|
icon?: React.ReactNode;
|
|
17
|
+
/** Classes for the trigger button. */
|
|
17
18
|
className?: string;
|
|
19
|
+
/** Classes for the panel, e.g. its width. Defaults to `w-72`. */
|
|
20
|
+
contentClassName?: string;
|
|
18
21
|
};
|
|
19
22
|
/**
|
|
20
23
|
* Longer help than a Tooltip: a Popover with a 20px glyph and a 44px hit
|
|
21
24
|
* area. Hover opens only on a fine pointer; click, tap, and keyboard always
|
|
22
25
|
* toggle it. Not a glossary or "coming soon" chip — pass that copy in.
|
|
23
26
|
*/
|
|
24
|
-
declare function InfoTip({ label, title, description, children, layout, side, align, badge, icon, className, }: InfoTipProps): React.JSX.Element;
|
|
27
|
+
declare function InfoTip({ label, title, description, children, layout, side, align, badge, icon, className, contentClassName, }: InfoTipProps): React.JSX.Element;
|
|
25
28
|
/** InfoTip that sits in a line of text without stretching the line box. */
|
|
26
29
|
declare function InlineTip(props: Omit<InfoTipProps, "layout">): React.JSX.Element;
|
|
27
30
|
export { InfoTip, InlineTip };
|
package/dist/info-tip.js
CHANGED
|
@@ -17,7 +17,7 @@ function canHoverOpen() {
|
|
|
17
17
|
* area. Hover opens only on a fine pointer; click, tap, and keyboard always
|
|
18
18
|
* toggle it. Not a glossary or "coming soon" chip — pass that copy in.
|
|
19
19
|
*/
|
|
20
|
-
function InfoTip({ label, title, description, children, layout = "box", side = "top", align = "center", badge, icon, className, }) {
|
|
20
|
+
function InfoTip({ label, title, description, children, layout = "box", side = "top", align = "center", badge, icon, className, contentClassName, }) {
|
|
21
21
|
const [open, setOpen] = React.useState(false);
|
|
22
22
|
const closeTimer = React.useRef(null);
|
|
23
23
|
const hoverOpened = React.useRef(false);
|
|
@@ -55,7 +55,7 @@ function InfoTip({ label, title, description, children, layout = "box", side = "
|
|
|
55
55
|
setOpen(next);
|
|
56
56
|
}, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx("button", { type: "button", "aria-label": label, "data-slot": "info-tip-trigger", "data-layout": layout, className: cn(focusRing, "relative inline-flex shrink-0 items-center justify-center rounded-none text-muted-foreground hover:text-selected-foreground focus-visible:text-selected-foreground", inline
|
|
57
57
|
? "size-5 align-middle after:absolute after:top-1/2 after:left-1/2 after:size-11 after:-translate-x-1/2 after:-translate-y-1/2"
|
|
58
|
-
: "size-11", className), onMouseEnter: openFromHover, onMouseLeave: closeFromHover, children: icon ?? (_jsx(CircleHelpIcon, { className: "size-5", "data-icon": "info-tip", "aria-hidden": true })) }) }), _jsxs(PopoverContent, { side: side, align: align, className: "w-72", onMouseEnter: openFromHover, onMouseLeave: closeFromHover, children: [_jsxs(PopoverHeader, { children: [_jsxs("div", { className: "flex items-start justify-between gap-2", children: [title ? _jsx(PopoverTitle, { children: title }) : null, badge] }), description ? (_jsx(PopoverDescription, { children: description })) : null] }), children] })] }));
|
|
58
|
+
: "size-11", className), onMouseEnter: openFromHover, onMouseLeave: closeFromHover, children: icon ?? (_jsx(CircleHelpIcon, { className: "size-5", "data-icon": "info-tip", "aria-hidden": true })) }) }), _jsxs(PopoverContent, { side: side, align: align, className: cn("w-72", contentClassName), onMouseEnter: openFromHover, onMouseLeave: closeFromHover, children: [_jsxs(PopoverHeader, { children: [_jsxs("div", { className: "flex items-start justify-between gap-2", children: [title ? _jsx(PopoverTitle, { children: title }) : null, badge] }), description ? (_jsx(PopoverDescription, { children: description })) : null] }), children] })] }));
|
|
59
59
|
}
|
|
60
60
|
/** InfoTip that sits in a line of text without stretching the line box. */
|
|
61
61
|
function InlineTip(props) {
|
|
@@ -7,3 +7,12 @@
|
|
|
7
7
|
* outline Button's purple selected wash.
|
|
8
8
|
*/
|
|
9
9
|
export declare const formControlTriggerClassName = "h-10 w-full border-input bg-background px-3 font-normal text-base tracking-normal shadow-xs normal-case md:text-sm hover:border-input hover:bg-background hover:text-foreground focus-visible:text-foreground aria-expanded:border-input aria-expanded:bg-background aria-expanded:text-foreground has-data-[icon=inline-start]:pl-3 has-data-[icon=inline-end]:pr-3 dark:hover:bg-background dark:aria-expanded:bg-background";
|
|
10
|
+
/**
|
|
11
|
+
* Date and date-time picker trigger: the form-control chrome, start-aligned,
|
|
12
|
+
* with muted text while empty (`data-empty`).
|
|
13
|
+
*/
|
|
14
|
+
export declare const pickerTriggerClassName = "h-10 w-full border-input bg-background px-3 font-normal text-base tracking-normal shadow-xs normal-case md:text-sm hover:border-input hover:bg-background hover:text-foreground focus-visible:text-foreground aria-expanded:border-input aria-expanded:bg-background aria-expanded:text-foreground has-data-[icon=inline-start]:pl-3 has-data-[icon=inline-end]:pr-3 dark:hover:bg-background dark:aria-expanded:bg-background justify-start data-[empty=true]:text-muted-foreground";
|
|
15
|
+
/** Calendar popover body: fits the viewport height and scrolls inside it. */
|
|
16
|
+
export declare const pickerContentClassName = "max-h-(--radix-popover-content-available-height) overflow-y-auto rounded-none p-0";
|
|
17
|
+
/** Gutter used when a calendar popover collides with the viewport. */
|
|
18
|
+
export declare const PICKER_COLLISION_PADDING = 16;
|
|
@@ -7,3 +7,12 @@
|
|
|
7
7
|
* outline Button's purple selected wash.
|
|
8
8
|
*/
|
|
9
9
|
export const formControlTriggerClassName = "h-10 w-full border-input bg-background px-3 font-normal text-base tracking-normal shadow-xs normal-case md:text-sm hover:border-input hover:bg-background hover:text-foreground focus-visible:text-foreground aria-expanded:border-input aria-expanded:bg-background aria-expanded:text-foreground has-data-[icon=inline-start]:pl-3 has-data-[icon=inline-end]:pr-3 dark:hover:bg-background dark:aria-expanded:bg-background";
|
|
10
|
+
/**
|
|
11
|
+
* Date and date-time picker trigger: the form-control chrome, start-aligned,
|
|
12
|
+
* with muted text while empty (`data-empty`).
|
|
13
|
+
*/
|
|
14
|
+
export const pickerTriggerClassName = `${formControlTriggerClassName} justify-start data-[empty=true]:text-muted-foreground`;
|
|
15
|
+
/** Calendar popover body: fits the viewport height and scrolls inside it. */
|
|
16
|
+
export const pickerContentClassName = "max-h-(--radix-popover-content-available-height) overflow-y-auto rounded-none p-0";
|
|
17
|
+
/** Gutter used when a calendar popover collides with the viewport. */
|
|
18
|
+
export const PICKER_COLLISION_PADDING = 16;
|
|
@@ -1,2 +1,14 @@
|
|
|
1
1
|
/** Return the next highlighted index for a listbox navigation key. */
|
|
2
2
|
export declare function moveListboxHighlight(key: string, index: number, length: number): number | undefined;
|
|
3
|
+
/**
|
|
4
|
+
* Option id for `aria-activedescendant`. Index-based because option values
|
|
5
|
+
* may contain spaces, which are not valid in an id reference.
|
|
6
|
+
*/
|
|
7
|
+
export declare function listboxOptionId(listId: string, index: number): string;
|
|
8
|
+
/**
|
|
9
|
+
* Callback ref for the highlighted option. It runs when the option mounts
|
|
10
|
+
* (including when a popover first renders the list) and when the highlight
|
|
11
|
+
* moves, keeping the option in view inside the overflow list. Module-level,
|
|
12
|
+
* so the ref is stable without `useCallback`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function scrollOptionIntoView(node: HTMLElement | null): void;
|
|
@@ -19,3 +19,19 @@ export function moveListboxHighlight(key, index, length) {
|
|
|
19
19
|
return undefined;
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Option id for `aria-activedescendant`. Index-based because option values
|
|
24
|
+
* may contain spaces, which are not valid in an id reference.
|
|
25
|
+
*/
|
|
26
|
+
export function listboxOptionId(listId, index) {
|
|
27
|
+
return `${listId}-option-${index}`;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Callback ref for the highlighted option. It runs when the option mounts
|
|
31
|
+
* (including when a popover first renders the list) and when the highlight
|
|
32
|
+
* moves, keeping the option in view inside the overflow list. Module-level,
|
|
33
|
+
* so the ref is stable without `useCallback`.
|
|
34
|
+
*/
|
|
35
|
+
export function scrollOptionIntoView(node) {
|
|
36
|
+
node?.scrollIntoView({ block: "nearest" });
|
|
37
|
+
}
|
|
@@ -37,6 +37,8 @@ export declare const selectedState: {
|
|
|
37
37
|
/** Quiet buttons and toggles on hover. */
|
|
38
38
|
hover: string;
|
|
39
39
|
};
|
|
40
|
+
/** The "No results" row of a listbox. */
|
|
41
|
+
export declare const listboxEmpty = "px-3 py-4 text-center text-sm text-muted-foreground";
|
|
40
42
|
/** Options in menus and listboxes. */
|
|
41
43
|
export declare const optionState: {
|
|
42
44
|
/** Highlight as focus moves: grey fill plus a 2px ring bar (the fill alone is 1.1:1). */
|
package/dist/internal/styles.js
CHANGED
|
@@ -37,6 +37,8 @@ export const selectedState = {
|
|
|
37
37
|
/** Quiet buttons and toggles on hover. */
|
|
38
38
|
hover: "hover:border-selected-foreground hover:bg-selected hover:text-selected-foreground",
|
|
39
39
|
};
|
|
40
|
+
/** The "No results" row of a listbox. */
|
|
41
|
+
export const listboxEmpty = "px-3 py-4 text-center text-sm text-muted-foreground";
|
|
40
42
|
/** Options in menus and listboxes. */
|
|
41
43
|
export const optionState = {
|
|
42
44
|
/** Highlight as focus moves: grey fill plus a 2px ring bar (the fill alone is 1.1:1). */
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
/** Start fetching the calendar early, e.g. when the trigger is hovered. */
|
|
3
|
+
export declare function preloadCalendar(): void;
|
|
4
|
+
declare const LazyCalendarInner: React.LazyExoticComponent<typeof import("./calendar.js").Calendar>;
|
|
5
|
+
export declare function LazyCalendar(props: React.ComponentProps<typeof LazyCalendarInner>): React.JSX.Element;
|
|
6
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Spinner } from "./spinner.js";
|
|
5
|
+
/**
|
|
6
|
+
* Calendar for the picker popovers, fetched on first use so a page that only
|
|
7
|
+
* shows the trigger does not download Day Picker. Not a public entry: apps
|
|
8
|
+
* that want an inline calendar import `atlas-react-kit/calendar`.
|
|
9
|
+
*/
|
|
10
|
+
const loadCalendar = () => import("./calendar.js");
|
|
11
|
+
/** Start fetching the calendar early, e.g. when the trigger is hovered. */
|
|
12
|
+
export function preloadCalendar() {
|
|
13
|
+
void loadCalendar();
|
|
14
|
+
}
|
|
15
|
+
const LazyCalendarInner = React.lazy(() => loadCalendar().then((module) => ({ default: module.Calendar })));
|
|
16
|
+
export function LazyCalendar(props) {
|
|
17
|
+
return (_jsx(React.Suspense, { fallback: _jsx("div", { className: "flex h-72 w-64 items-center justify-center", children: _jsx(Spinner, { "aria-label": "Loading calendar" }) }), children: _jsx(LazyCalendarInner, { ...props }) }));
|
|
18
|
+
}
|
package/dist/popover.js
CHANGED
|
@@ -10,7 +10,7 @@ function PopoverTrigger({ ...props }) {
|
|
|
10
10
|
return _jsx(PopoverPrimitive.Trigger, { "data-slot": "popover-trigger", ...props });
|
|
11
11
|
}
|
|
12
12
|
function PopoverContent({ className, align = "center", sideOffset = 4, collisionPadding = 8, ...props }) {
|
|
13
|
-
return (_jsx(PopoverPrimitive.Portal, { children: _jsx(PopoverPrimitive.Content, { "data-slot": "popover-content", align: align, sideOffset: sideOffset, collisionPadding: collisionPadding, className: cn("z-50 flex w-
|
|
13
|
+
return (_jsx(PopoverPrimitive.Portal, { children: _jsx(PopoverPrimitive.Content, { "data-slot": "popover-content", align: align, sideOffset: sideOffset, collisionPadding: collisionPadding, className: cn("z-50 flex max-w-(--radix-popover-content-available-width) origin-(--radix-popover-content-transform-origin) flex-col gap-4 rounded-none bg-popover p-4 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95", className), ...props }) }));
|
|
14
14
|
}
|
|
15
15
|
function PopoverAnchor({ ...props }) {
|
|
16
16
|
return _jsx(PopoverPrimitive.Anchor, { "data-slot": "popover-anchor", ...props });
|
package/dist/search-picker.d.ts
CHANGED
|
@@ -20,11 +20,13 @@ export type SearchPickerProps<T> = {
|
|
|
20
20
|
listLabel?: string;
|
|
21
21
|
disabled?: boolean;
|
|
22
22
|
className?: string;
|
|
23
|
+
/** Classes for the listbox, e.g. its height. Defaults to `max-h-60`. */
|
|
24
|
+
listClassName?: string;
|
|
23
25
|
};
|
|
24
26
|
/**
|
|
25
27
|
* Always-visible combobox: a labelled input and a listbox. Unlike Combobox,
|
|
26
28
|
* the list is not in a popover. Keyboard: arrows, Home/End, PageUp/PageDown,
|
|
27
29
|
* Enter, Escape.
|
|
28
30
|
*/
|
|
29
|
-
declare function SearchPicker<T>({ items, getValue, getLabel, filter, value, onSelect, label, placeholder, emptyText, listLabel, disabled, className, }: SearchPickerProps<T>): React.JSX.Element;
|
|
31
|
+
declare function SearchPicker<T>({ items, getValue, getLabel, filter, value, onSelect, label, placeholder, emptyText, listLabel, disabled, className, listClassName, }: SearchPickerProps<T>): React.JSX.Element;
|
|
30
32
|
export { SearchPicker };
|
package/dist/search-picker.js
CHANGED
|
@@ -3,8 +3,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { Input } from "./input.js";
|
|
5
5
|
import { Label } from "./label.js";
|
|
6
|
-
import { moveListboxHighlight } from "./internal/listbox-navigation.js";
|
|
7
|
-
import { optionState } from "./internal/styles.js";
|
|
6
|
+
import { listboxOptionId, moveListboxHighlight, scrollOptionIntoView, } from "./internal/listbox-navigation.js";
|
|
7
|
+
import { listboxEmpty, optionState } from "./internal/styles.js";
|
|
8
8
|
import { cn } from "./utils.js";
|
|
9
9
|
/**
|
|
10
10
|
* Case-fold and strip combining marks so "nino" matches "Niño".
|
|
@@ -26,7 +26,7 @@ export function defaultSearchFilter(item, query, getLabel) {
|
|
|
26
26
|
* the list is not in a popover. Keyboard: arrows, Home/End, PageUp/PageDown,
|
|
27
27
|
* Enter, Escape.
|
|
28
28
|
*/
|
|
29
|
-
function SearchPicker({ items, getValue, getLabel, filter = defaultSearchFilter, value, onSelect, label, placeholder = "Search", emptyText = "No results", listLabel, disabled, className, }) {
|
|
29
|
+
function SearchPicker({ items, getValue, getLabel, filter = defaultSearchFilter, value, onSelect, label, placeholder = "Search", emptyText = "No results", listLabel, disabled, className, listClassName, }) {
|
|
30
30
|
const [query, setQuery] = React.useState("");
|
|
31
31
|
const listId = React.useId();
|
|
32
32
|
const inputId = React.useId();
|
|
@@ -49,14 +49,19 @@ function SearchPicker({ items, getValue, getLabel, filter = defaultSearchFilter,
|
|
|
49
49
|
return index >= 0 ? index : 0;
|
|
50
50
|
}, [filtered, getValue, value]);
|
|
51
51
|
const [highlight, setHighlight] = React.useState(selectedIndex);
|
|
52
|
-
|
|
52
|
+
// Reset the highlight while rendering, not in an effect, when the query or
|
|
53
|
+
// the selected row changes: an effect commits every keystroke twice, once
|
|
54
|
+
// with the stale highlight and again with the reset.
|
|
55
|
+
const [highlightFor, setHighlightFor] = React.useState({
|
|
56
|
+
query,
|
|
57
|
+
selectedIndex,
|
|
58
|
+
});
|
|
59
|
+
if (highlightFor.query !== query ||
|
|
60
|
+
highlightFor.selectedIndex !== selectedIndex) {
|
|
61
|
+
setHighlightFor({ query, selectedIndex });
|
|
53
62
|
setHighlight(query ? 0 : selectedIndex);
|
|
54
|
-
}
|
|
55
|
-
const optionId = (index) => `${listId}-option-${index}`;
|
|
63
|
+
}
|
|
56
64
|
const active = filtered[highlight];
|
|
57
|
-
const scrollHighlightedOption = React.useCallback((node) => {
|
|
58
|
-
node?.scrollIntoView({ block: "nearest" });
|
|
59
|
-
}, []);
|
|
60
65
|
const selectItem = (item) => {
|
|
61
66
|
onSelect(item);
|
|
62
67
|
setQuery("");
|
|
@@ -82,11 +87,11 @@ function SearchPicker({ items, getValue, getLabel, filter = defaultSearchFilter,
|
|
|
82
87
|
event.currentTarget.blur();
|
|
83
88
|
}
|
|
84
89
|
};
|
|
85
|
-
return (_jsxs("div", { "data-slot": "search-picker", className: cn("flex flex-col gap-1.5", className), children: [_jsx(Label, { htmlFor: inputId, children: label }), _jsx(Input, { id: inputId, role: "combobox", "aria-expanded": true, "aria-controls": listId, "aria-autocomplete": "list", "aria-activedescendant": active ?
|
|
90
|
+
return (_jsxs("div", { "data-slot": "search-picker", className: cn("flex flex-col gap-1.5", className), children: [_jsx(Label, { htmlFor: inputId, children: label }), _jsx(Input, { id: inputId, role: "combobox", "aria-expanded": true, "aria-controls": listId, "aria-autocomplete": "list", "aria-activedescendant": active ? listboxOptionId(listId, highlight) : undefined, value: query, disabled: disabled, placeholder: placeholder, onChange: (event) => setQuery(event.target.value), onKeyDown: onKeyDown }), _jsx("ul", { id: listId, role: "listbox", "aria-label": listLabel ?? label, className: cn("max-h-60 overflow-y-auto border border-input bg-background", listClassName), children: filtered.length === 0 ? (_jsx("li", { className: listboxEmpty, children: emptyText })) : (filtered.map((item, index) => {
|
|
86
91
|
const itemValue = getValue(item);
|
|
87
92
|
const isSelected = itemValue === value;
|
|
88
93
|
const isActive = index === highlight;
|
|
89
|
-
return (_jsx("li", { role: "none", children: _jsx("button", { ref: isActive ?
|
|
94
|
+
return (_jsx("li", { role: "none", children: _jsx("button", { ref: isActive ? scrollOptionIntoView : undefined, id: listboxOptionId(listId, index), type: "button", role: "option", "aria-selected": isSelected, disabled: disabled, className: cn("relative flex w-full cursor-default items-center rounded-none py-2 pr-3 pl-3 text-left text-sm outline-hidden select-none", isSelected && optionState.selected, isActive && optionState.active, isActive &&
|
|
90
95
|
!isSelected &&
|
|
91
96
|
"bg-accent text-accent-foreground"), onMouseEnter: () => setHighlight(index), onClick: () => selectItem(item), children: _jsx("span", { className: "truncate", children: getLabel(item) }) }) }, itemValue));
|
|
92
97
|
})) })] }));
|
package/dist/select.js
CHANGED
|
@@ -5,6 +5,7 @@ import { Select as SelectPrimitive } from "radix-ui";
|
|
|
5
5
|
import { dataDisabledState, disabledState, fieldSurface, focusRing, invalidState, optionState, } from "./internal/styles.js";
|
|
6
6
|
import { cn } from "./utils.js";
|
|
7
7
|
import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react";
|
|
8
|
+
const scrollButtonClassName = "z-10 flex cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-3.5";
|
|
8
9
|
function Select({ ...props }) {
|
|
9
10
|
return _jsx(SelectPrimitive.Root, { "data-slot": "select", ...props });
|
|
10
11
|
}
|
|
@@ -19,7 +20,7 @@ function SelectTrigger({ className, size = "default", children, ...props }) {
|
|
|
19
20
|
}
|
|
20
21
|
function SelectContent({ className, children, position = "popper", align = "center", sideOffset = 4, collisionPadding = 8, ...props }) {
|
|
21
22
|
return (_jsx(SelectPrimitive.Portal, { children: _jsxs(SelectPrimitive.Content, { "data-slot": "select-content", "data-align-trigger": position === "item-aligned", className: cn("relative z-50 max-h-(--radix-select-content-available-height) min-w-(--radix-select-trigger-width) origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-none bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", position === "popper" &&
|
|
22
|
-
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", className), position: position, align: align, sideOffset: sideOffset, collisionPadding: collisionPadding, ...props, children: [_jsx(SelectScrollUpButton, {}), _jsx(SelectPrimitive.Viewport, { "data-position": position, className:
|
|
23
|
+
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", className), position: position, align: align, sideOffset: sideOffset, collisionPadding: collisionPadding, ...props, children: [_jsx(SelectScrollUpButton, {}), _jsx(SelectPrimitive.Viewport, { "data-position": position, className: "data-[position=popper]:h-(--radix-select-trigger-height) data-[position=popper]:w-full data-[position=popper]:min-w-(--radix-select-trigger-width)", children: children }), _jsx(SelectScrollDownButton, {})] }) }));
|
|
23
24
|
}
|
|
24
25
|
function SelectLabel({ className, ...props }) {
|
|
25
26
|
return (_jsx(SelectPrimitive.Label, { "data-slot": "select-label", className: cn("px-3 py-2 text-xs font-semibold tracking-wider text-muted-foreground uppercase", className), ...props }));
|
|
@@ -31,9 +32,9 @@ function SelectSeparator({ className, ...props }) {
|
|
|
31
32
|
return (_jsx(SelectPrimitive.Separator, { "data-slot": "select-separator", className: cn("pointer-events-none -mx-1.5 my-1.5 h-px bg-border/50", className), ...props }));
|
|
32
33
|
}
|
|
33
34
|
function SelectScrollUpButton({ className, ...props }) {
|
|
34
|
-
return (_jsx(SelectPrimitive.ScrollUpButton, { "data-slot": "select-scroll-up-button", className: cn(
|
|
35
|
+
return (_jsx(SelectPrimitive.ScrollUpButton, { "data-slot": "select-scroll-up-button", className: cn(scrollButtonClassName, className), ...props, children: _jsx(ChevronUpIcon, {}) }));
|
|
35
36
|
}
|
|
36
37
|
function SelectScrollDownButton({ className, ...props }) {
|
|
37
|
-
return (_jsx(SelectPrimitive.ScrollDownButton, { "data-slot": "select-scroll-down-button", className: cn(
|
|
38
|
+
return (_jsx(SelectPrimitive.ScrollDownButton, { "data-slot": "select-scroll-down-button", className: cn(scrollButtonClassName, className), ...props, children: _jsx(ChevronDownIcon, {}) }));
|
|
38
39
|
}
|
|
39
40
|
export { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, };
|
|
@@ -3,5 +3,5 @@ import { Skeleton } from "./skeleton.js";
|
|
|
3
3
|
import { TableBody, TableCell, TableRow } from "./table.js";
|
|
4
4
|
/** Skeleton table body rows for client query loading states. */
|
|
5
5
|
export function TableBodySkeleton({ columns, rows = 5 }) {
|
|
6
|
-
return (_jsx(TableBody, { children: Array.from({ length: rows }, (_, row) => (_jsx(TableRow, { children: Array.from({ length: columns }, (_, col) => (_jsx(TableCell, { children: _jsx(Skeleton, { className: "h-4 w-full
|
|
6
|
+
return (_jsx(TableBody, { children: Array.from({ length: rows }, (_, row) => (_jsx(TableRow, { children: Array.from({ length: columns }, (_, col) => (_jsx(TableCell, { children: _jsx(Skeleton, { className: "h-4 w-full" }) }, col))) }, row))) }));
|
|
7
7
|
}
|
package/dist/tabs.js
CHANGED
|
@@ -60,6 +60,6 @@ function TabsContent({ className, ...props }) {
|
|
|
60
60
|
* `overflow-x-auto`.
|
|
61
61
|
*/
|
|
62
62
|
function TabsNav({ className, children, ...props }) {
|
|
63
|
-
return (_jsx("nav", { "data-slot": "tabs-nav", "data-orientation": "horizontal", className: cn("group/tabs min-w-0
|
|
63
|
+
return (_jsx("nav", { "data-slot": "tabs-nav", "data-orientation": "horizontal", className: cn("group/tabs min-w-0 max-w-full", className), ...props, children: _jsx(TabsScroller, { children: _jsx("div", { "data-slot": "tabs-list", "data-variant": "default", className: cn(tabsListVariants(), "bg-transparent"), children: children }) }) }));
|
|
64
64
|
}
|
|
65
65
|
export { Tabs, TabsList, TabsTrigger, TabsContent, TabsNav, tabsListVariants, tabsTriggerVariants, };
|