atlas-react-kit 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/COMPONENTS.md +50 -8
  2. package/DESIGN_RULES.md +12 -0
  3. package/README.md +2 -2
  4. package/dist/accordion.d.ts +8 -1
  5. package/dist/accordion.js +8 -3
  6. package/dist/alert.js +1 -1
  7. package/dist/avatar.d.ts +5 -1
  8. package/dist/avatar.js +7 -2
  9. package/dist/badge.js +1 -1
  10. package/dist/breadcrumb.js +3 -2
  11. package/dist/button.js +6 -6
  12. package/dist/calendar.js +19 -12
  13. package/dist/card.d.ts +1 -1
  14. package/dist/checkbox.js +2 -2
  15. package/dist/color-legend.d.ts +6 -4
  16. package/dist/color-legend.js +7 -4
  17. package/dist/combobox.d.ts +7 -2
  18. package/dist/combobox.js +35 -25
  19. package/dist/date-picker.d.ts +7 -13
  20. package/dist/date-picker.js +14 -20
  21. package/dist/date-time-picker.d.ts +5 -13
  22. package/dist/date-time-picker.js +26 -30
  23. package/dist/dialog.d.ts +5 -1
  24. package/dist/dialog.js +4 -3
  25. package/dist/dropdown-menu.js +5 -4
  26. package/dist/empty-panel.d.ts +4 -3
  27. package/dist/empty-panel.js +3 -3
  28. package/dist/error-boundary.d.ts +5 -3
  29. package/dist/error-boundary.js +9 -4
  30. package/dist/field.d.ts +14 -1
  31. package/dist/field.js +16 -3
  32. package/dist/form/date-picker-field.d.ts +8 -2
  33. package/dist/form/date-picker-field.js +4 -4
  34. package/dist/form/date-time-picker-field.d.ts +8 -1
  35. package/dist/form/date-time-picker-field.js +3 -3
  36. package/dist/form/feedback-field.d.ts +2 -1
  37. package/dist/form/feedback-field.js +3 -2
  38. package/dist/form/field-error.d.ts +4 -1
  39. package/dist/form/field-error.js +3 -2
  40. package/dist/form/field-shell.d.ts +25 -1
  41. package/dist/form/field-shell.js +36 -3
  42. package/dist/form/markdown-field.d.ts +7 -1
  43. package/dist/form/markdown-field.js +3 -3
  44. package/dist/form/select-field.d.ts +7 -1
  45. package/dist/form/select-field.js +3 -3
  46. package/dist/form/text-field.d.ts +5 -1
  47. package/dist/form/text-field.js +6 -3
  48. package/dist/info-tip.d.ts +2 -2
  49. package/dist/info-tip.js +34 -16
  50. package/dist/internal/form-control.d.ts +5 -0
  51. package/dist/internal/form-control.js +7 -0
  52. package/dist/internal/listbox-navigation.d.ts +32 -0
  53. package/dist/internal/listbox-navigation.js +38 -0
  54. package/dist/internal/listbox-option.d.ts +25 -0
  55. package/dist/internal/listbox-option.js +23 -0
  56. package/dist/internal/math.d.ts +5 -0
  57. package/dist/internal/math.js +7 -0
  58. package/dist/internal/overlay-styles.d.ts +28 -0
  59. package/dist/internal/overlay-styles.js +28 -0
  60. package/dist/internal/styles.d.ts +27 -0
  61. package/dist/internal/styles.js +27 -0
  62. package/dist/internal/tabs-styles.d.ts +10 -1
  63. package/dist/internal/tabs-styles.js +27 -5
  64. package/dist/lazy-calendar.js +9 -1
  65. package/dist/markdown.js +10 -4
  66. package/dist/meter.d.ts +3 -2
  67. package/dist/meter.js +6 -3
  68. package/dist/pagination.d.ts +8 -1
  69. package/dist/pagination.js +15 -9
  70. package/dist/picker-shell.d.ts +59 -0
  71. package/dist/picker-shell.js +21 -0
  72. package/dist/popover.js +2 -1
  73. package/dist/progress.d.ts +7 -3
  74. package/dist/progress.js +10 -4
  75. package/dist/radio-group.js +2 -2
  76. package/dist/search-picker.d.ts +4 -6
  77. package/dist/search-picker.js +28 -33
  78. package/dist/select.js +12 -6
  79. package/dist/separator.d.ts +4 -0
  80. package/dist/separator.js +5 -1
  81. package/dist/slider.d.ts +8 -3
  82. package/dist/slider.js +7 -5
  83. package/dist/sortable-table-head.d.ts +3 -1
  84. package/dist/sortable-table-head.js +3 -2
  85. package/dist/stat-tile.d.ts +6 -4
  86. package/dist/stat-tile.js +6 -4
  87. package/dist/switch.js +2 -2
  88. package/dist/table-body-skeleton.d.ts +8 -2
  89. package/dist/table-body-skeleton.js +7 -3
  90. package/dist/table.d.ts +7 -1
  91. package/dist/table.js +6 -2
  92. package/dist/tabs.d.ts +13 -3
  93. package/dist/tabs.js +29 -10
  94. package/dist/theme-provider.d.ts +2 -1
  95. package/dist/theme-provider.js +1 -1
  96. package/dist/toggle.js +4 -4
  97. package/dist/tooltip.d.ts +7 -3
  98. package/dist/tooltip.js +17 -6
  99. package/package.json +1 -1
  100. package/theme.css +43 -50
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 { listboxOptionId, moveListboxHighlight, scrollOptionIntoView, } from "./internal/listbox-navigation.js";
8
+ import { foldSearchText, handleListboxKey, listboxOptionId, } from "./internal/listbox-navigation.js";
9
+ import { ListboxEmptyStatus, ListboxOption } from "./internal/listbox-option.js";
9
10
  import { Popover, PopoverContent, PopoverTrigger } from "./popover.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
  /**
@@ -26,7 +26,7 @@ function defaultCreateLabel(value) {
26
26
  /**
27
27
  * Value to offer as a new option, or `null` when the query is empty,
28
28
  * rejected by `normalize`, or already in the list (value or label,
29
- * case-insensitive). Domain catalogs stay with the caller.
29
+ * ignoring case and accents). Domain catalogs stay with the caller.
30
30
  */
31
31
  function resolveCreateValue(query, options, normalize) {
32
32
  const created = normalize(query);
@@ -35,9 +35,9 @@ function resolveCreateValue(query, options, normalize) {
35
35
  const value = created.trim();
36
36
  if (!value)
37
37
  return null;
38
- const needle = value.toLowerCase();
39
- const exists = options.some((option) => option.value.toLowerCase() === needle ||
40
- option.label.toLowerCase() === needle);
38
+ const needle = foldSearchText(value);
39
+ const exists = options.some((option) => foldSearchText(option.value) === needle ||
40
+ foldSearchText(option.label) === needle);
41
41
  return exists ? null : value;
42
42
  }
43
43
  /**
@@ -49,12 +49,14 @@ 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, listClassName, "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, contentClassName, "aria-invalid": ariaInvalid, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, creatable = false, normalizeCreateValue = defaultNormalizeCreateValue, createLabel = defaultCreateLabel, ...props }) {
53
53
  const [open, setOpen] = React.useState(false);
54
54
  const [query, setQuery] = React.useState("");
55
55
  const [highlight, setHighlight] = React.useState(0);
56
56
  const inputRef = React.useRef(null);
57
57
  const listId = React.useId();
58
+ const generatedId = React.useId();
59
+ const triggerId = id ?? generatedId;
58
60
  // A created value may not be in `options` yet (the app owns the catalog).
59
61
  // Keep it visible on the trigger and in the list so reopen/Enter still work.
60
62
  const resolvedOptions = React.useMemo(() => {
@@ -66,10 +68,10 @@ export function Combobox({ options, value, onValueChange, placeholder = "Select"
66
68
  }, [creatable, options, value]);
67
69
  const selected = resolvedOptions.find((option) => option.value === value);
68
70
  const filtered = React.useMemo(() => {
69
- const needle = query.trim().toLowerCase();
71
+ const needle = foldSearchText(query.trim());
70
72
  if (!needle)
71
73
  return resolvedOptions;
72
- return resolvedOptions.filter((option) => option.label.toLowerCase().includes(needle));
74
+ return resolvedOptions.filter((option) => foldSearchText(option.label).includes(needle));
73
75
  }, [resolvedOptions, query]);
74
76
  const createValue = creatable
75
77
  ? resolveCreateValue(query, resolvedOptions, normalizeCreateValue)
@@ -109,24 +111,32 @@ export function Combobox({ options, value, onValueChange, placeholder = "Select"
109
111
  setQuery("");
110
112
  };
111
113
  const activeOption = list[highlight];
112
- const onSearchKeyDown = (event) => {
113
- const nextHighlight = moveListboxHighlight(event.key, highlight, list.length);
114
- if (nextHighlight !== undefined) {
115
- event.preventDefault();
116
- setHighlight(nextHighlight);
114
+ const empty = list.length === 0;
115
+ // APG combobox: ArrowDown, ArrowUp, and Alt+ArrowDown open the list from
116
+ // the trigger. Radix only opens a popover on Enter, Space, and click.
117
+ const onTriggerKeyDown = (event) => {
118
+ if (open || (event.key !== "ArrowDown" && event.key !== "ArrowUp"))
117
119
  return;
118
- }
119
- if (event.key === "Enter") {
120
- event.preventDefault();
121
- const option = list[highlight];
122
- if (option)
123
- selectValue(option.value);
124
- }
120
+ event.preventDefault();
121
+ onOpenChange(true);
122
+ };
123
+ const onSearchKeyDown = (event) => {
124
+ handleListboxKey(event, {
125
+ highlight,
126
+ length: list.length,
127
+ setHighlight,
128
+ onEnter: (index) => {
129
+ const option = list[index];
130
+ if (option)
131
+ selectValue(option.value);
132
+ },
133
+ });
125
134
  };
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) => {
135
+ return (_jsxs(Popover, { open: open, onOpenChange: onOpenChange, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { ...props, id: triggerId, type: "button", variant: "outline", role: "combobox", "aria-expanded": open, "aria-controls": listId, "aria-invalid": ariaInvalid, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, disabled: disabled, onKeyDown: onTriggerKeyDown, 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: cn("w-[var(--radix-popover-trigger-width)] gap-0 rounded-none p-0", contentClassName), children: [_jsx("div", { "data-slot": "combobox-search", className: "border-b p-2", children: _jsx(Input, { ref: inputRef, value: query, onChange: onQueryChange, onKeyDown: onSearchKeyDown, placeholder: searchPlaceholder, "aria-label": searchPlaceholder, "aria-autocomplete": "list", "aria-controls": listId, "aria-activedescendant": activeOption ? listboxOptionId(listId, highlight) : undefined }) }), _jsx("ul", { id: listId, role: "listbox", "data-slot": "combobox-list", "aria-label": ariaLabelledBy ? undefined : ariaLabel, "aria-labelledby": ariaLabelledBy ?? (ariaLabel ? undefined : triggerId), className: empty
136
+ ? "sr-only"
137
+ : cn("max-h-60 overflow-y-auto p-1", listClassName), children: list.map((option, index) => {
127
138
  const isCreate = createOption != null && option === createOption;
128
139
  const isSelected = option.value === value && !isCreate;
129
- const isActive = index === highlight;
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));
131
- })) })] })] }));
140
+ return (_jsxs(ListboxOption, { "data-slot": "combobox-option", listId: listId, index: index, active: index === highlight, selected: isSelected, className: "pr-8", 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));
141
+ }) }), _jsx(ListboxEmptyStatus, { empty: empty, children: emptyText })] })] }));
132
142
  }
@@ -1,6 +1,6 @@
1
1
  import * as React from "react";
2
2
  import type { DateRange } from "react-day-picker";
3
- import { PopoverContent } from "./popover.js";
3
+ import { type PickerCalendarProps, type PickerTriggerProps } from "./picker-shell.js";
4
4
  export type { DateRange };
5
5
  export type DateLike = Date | string | number | null | undefined;
6
6
  /** From/to value for `DatePickerButton` in range mode; each side is `DateLike`. */
@@ -16,22 +16,14 @@ export type DatePickerRangeValue = {
16
16
  export declare function toDate(value: DateLike): Date | undefined;
17
17
  /** Format a Date as a timezone-safe calendar date (`yyyy-MM-dd`). */
18
18
  export declare function formatLocalDate(date: Date): string;
19
- type DatePickerSharedProps = {
20
- id?: string;
21
- placeholder?: string;
22
- disabled?: boolean;
23
- className?: string;
24
- "aria-invalid"?: boolean;
19
+ export type { PickerCalendarProps as DatePickerCalendarProps };
20
+ type DatePickerSharedProps = PickerTriggerProps & {
25
21
  /**
26
22
  * Visible months in the popover calendar. Defaults to 1 in single mode.
27
23
  * In range mode, defaults to 1 below the `md` breakpoint and 2 at `md`
28
24
  * and above so two stacked months do not open off-screen on mobile.
29
25
  */
30
26
  numberOfMonths?: number;
31
- /** Popover side. Defaults to `bottom`; Radix still flips on collision. */
32
- side?: React.ComponentProps<typeof PopoverContent>["side"];
33
- /** Month shown when the popover opens; falls back to the selected date. */
34
- defaultMonth?: Date;
35
27
  };
36
28
  export type DatePickerSingleProps = DatePickerSharedProps & {
37
29
  mode?: "single";
@@ -50,7 +42,9 @@ export type DatePickerButtonProps = DatePickerSingleProps | DatePickerRangeProps
50
42
  * `yyyy-MM-dd` and emits `Date | undefined`. `mode="range"` accepts
51
43
  * `{ from, to }` and emits `DateRange | undefined`. Range mode shows one
52
44
  * month below `md` and two at `md+` unless `numberOfMonths` is set. The
53
- * popover clamps to the viewport (`side` is overridable). Square corners
54
- * follow the shared shape rules.
45
+ * popover clamps to the viewport (`side` is overridable). `className` styles
46
+ * the trigger, `contentClassName` the popover, and `calendarProps` reaches
47
+ * Day Picker (disabled days, `startMonth`, `weekStartsOn`, `locale`). Square
48
+ * corners follow the shared shape rules.
55
49
  */
56
50
  export declare function DatePickerButton(props: DatePickerButtonProps): React.JSX.Element;
@@ -2,13 +2,9 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { format, isSameDay, parseISO } from "date-fns";
5
- import { CalendarIcon } from "lucide-react";
6
- import { Button } from "./button.js";
7
- import { PICKER_COLLISION_PADDING, pickerContentClassName, pickerTriggerClassName, } from "./internal/form-control.js";
8
5
  import { MD_MEDIA_QUERY, useMediaQuery } from "./internal/use-media-query.js";
9
- import { LazyCalendar, preloadCalendar } from "./lazy-calendar.js";
10
- import { Popover, PopoverContent, PopoverTrigger } from "./popover.js";
11
- import { cn } from "./utils.js";
6
+ import { LazyCalendar } from "./lazy-calendar.js";
7
+ import { PickerClearFooter, PickerShell, } from "./picker-shell.js";
12
8
  /**
13
9
  * Coerce a date-like value to a local `Date`.
14
10
  * Plain calendar dates (`yyyy-MM-dd`) parse as local midnight so they do not
@@ -56,19 +52,15 @@ function formatRangeTriggerLabel(from, to, placeholder) {
56
52
  return format(from, "PPP");
57
53
  return `${format(from, "PPP")} – ${format(to, "PPP")}`;
58
54
  }
59
- /** Shared trigger + auto-sized popover chrome for single and range modes. */
60
- function DatePickerShell({ id, disabled, ariaInvalid, className, empty, label, open, onOpenChange, side, children, }) {
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 })] }));
62
- }
63
- function DatePickerSingleButton({ id, value, onChange, placeholder = "Pick a date", disabled, className, "aria-invalid": ariaInvalid, numberOfMonths, side, defaultMonth, }) {
55
+ function DatePickerSingleButton({ value, onChange, placeholder = "Pick a date", numberOfMonths, defaultMonth, calendarProps, ...shell }) {
64
56
  const [open, setOpen] = React.useState(false);
65
57
  const date = toDate(value);
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) => {
58
+ return (_jsx(PickerShell, { ...shell, empty: !date, label: date ? format(date, "PPP") : placeholder, dialogLabel: "Choose date", open: open, onOpenChange: setOpen, children: _jsx(LazyCalendar, { ...calendarProps, mode: "single", autoFocus: true, selected: date, defaultMonth: defaultMonth ?? date, numberOfMonths: numberOfMonths, onSelect: (next) => {
67
59
  onChange(next);
68
60
  setOpen(false);
69
61
  } }) }));
70
62
  }
71
- function DatePickerRangeButton({ id, value, onChange, placeholder = "Pick a date range", disabled, className, "aria-invalid": ariaInvalid, numberOfMonths, side, defaultMonth, }) {
63
+ function DatePickerRangeButton({ value, onChange, placeholder = "Pick a date range", numberOfMonths, defaultMonth, calendarProps, ...shell }) {
72
64
  const [open, setOpen] = React.useState(false);
73
65
  const mdUp = useMediaQuery(MD_MEDIA_QUERY);
74
66
  const months = numberOfMonths ?? (mdUp ? 2 : 1);
@@ -84,7 +76,7 @@ function DatePickerRangeButton({ id, value, onChange, placeholder = "Pick a date
84
76
  restartRef.current = Boolean(from && to);
85
77
  setOpen(next);
86
78
  };
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) => {
79
+ return (_jsxs(PickerShell, { ...shell, empty: !from, label: formatRangeTriggerLabel(from, to, placeholder), dialogLabel: "Choose dates", open: open, onOpenChange: onOpenChange, children: [_jsx(LazyCalendar, { ...calendarProps, mode: "range", autoFocus: true, selected: selected, defaultMonth: defaultMonth ?? from ?? to, numberOfMonths: months, onSelect: (next, day) => {
88
80
  const range = restartRef.current ? { from: day, to: day } : next;
89
81
  restartRef.current = false;
90
82
  onChange(range);
@@ -93,10 +85,10 @@ function DatePickerRangeButton({ id, value, onChange, placeholder = "Pick a date
93
85
  if (range?.from && range.to && !isSameDay(range.from, range.to)) {
94
86
  setOpen(false);
95
87
  }
96
- } }), _jsx("div", { className: "flex justify-end border-t p-2", children: _jsx(Button, { type: "button", variant: "ghost", size: "sm", disabled: !from && !to, onClick: () => {
97
- onChange(undefined);
98
- setOpen(false);
99
- }, children: "Clear" }) })] }));
88
+ } }), _jsx(PickerClearFooter, { disabled: !from && !to, onClear: () => {
89
+ onChange(undefined);
90
+ setOpen(false);
91
+ } })] }));
100
92
  }
101
93
  /**
102
94
  * Calendar popover trigger styled as a form control (same chrome as Input
@@ -104,8 +96,10 @@ function DatePickerRangeButton({ id, value, onChange, placeholder = "Pick a date
104
96
  * `yyyy-MM-dd` and emits `Date | undefined`. `mode="range"` accepts
105
97
  * `{ from, to }` and emits `DateRange | undefined`. Range mode shows one
106
98
  * month below `md` and two at `md+` unless `numberOfMonths` is set. The
107
- * popover clamps to the viewport (`side` is overridable). Square corners
108
- * follow the shared shape rules.
99
+ * popover clamps to the viewport (`side` is overridable). `className` styles
100
+ * the trigger, `contentClassName` the popover, and `calendarProps` reaches
101
+ * Day Picker (disabled days, `startMonth`, `weekStartsOn`, `locale`). Square
102
+ * corners follow the shared shape rules.
109
103
  */
110
104
  export function DatePickerButton(props) {
111
105
  if (props.mode === "range") {
@@ -1,8 +1,8 @@
1
1
  import * as React from "react";
2
- import { PopoverContent } from "./popover.js";
2
+ import { type PickerTriggerProps } from "./picker-shell.js";
3
3
  /** Default clock increment, in minutes. Must divide 60. */
4
4
  export declare const DEFAULT_TIME_STEP = 15;
5
- export type DateTimePickerButtonProps = {
5
+ export type DateTimePickerButtonProps = PickerTriggerProps & {
6
6
  /** Controlled instant. `null` or omitted is empty. */
7
7
  value?: Date | null;
8
8
  onChange: (date: Date | null) => void;
@@ -11,15 +11,6 @@ export type DateTimePickerButtonProps = {
11
11
  * Values that do not divide 60 fall back to 15 so the list stays even.
12
12
  */
13
13
  timeStep?: number;
14
- id?: string;
15
- placeholder?: string;
16
- disabled?: boolean;
17
- className?: string;
18
- "aria-invalid"?: boolean;
19
- /** Popover side. Defaults to `bottom`; Radix still flips on collision. */
20
- side?: React.ComponentProps<typeof PopoverContent>["side"];
21
- /** Month shown when the popover opens; falls back to the selected date. */
22
- defaultMonth?: Date;
23
14
  };
24
15
  /**
25
16
  * Normalize a minute step so option lists stay even (1, 5, 15, 30, 60).
@@ -31,6 +22,7 @@ export declare function resolveTimeStep(step: number | undefined): number;
31
22
  * chrome as Input, SelectTrigger, and DatePickerButton). `value` is a `Date`
32
23
  * or `null`. Picking a day keeps the popover open so the time can be set;
33
24
  * Clear emits `null`. One month only, clamped to the viewport, so the panel
34
- * stays on-screen at a 390px width.
25
+ * stays on-screen at a 390px width. `className` styles the trigger,
26
+ * `contentClassName` the popover, and `calendarProps` reaches Day Picker.
35
27
  */
36
- export declare function DateTimePickerButton({ id, value, onChange, timeStep, placeholder, disabled, className, "aria-invalid": ariaInvalid, side, defaultMonth, }: DateTimePickerButtonProps): React.JSX.Element;
28
+ export declare function DateTimePickerButton({ value, onChange, timeStep, placeholder, defaultMonth, calendarProps, ...shell }: DateTimePickerButtonProps): React.JSX.Element;
@@ -2,13 +2,9 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { format } from "date-fns";
5
- import { CalendarIcon } from "lucide-react";
6
- import { Button } from "./button.js";
7
- import { PICKER_COLLISION_PADDING, pickerContentClassName, pickerTriggerClassName, } from "./internal/form-control.js";
8
- import { LazyCalendar, preloadCalendar } from "./lazy-calendar.js";
9
- import { Popover, PopoverContent, PopoverTrigger } from "./popover.js";
5
+ import { LazyCalendar } from "./lazy-calendar.js";
6
+ import { PickerClearFooter, PickerShell, } from "./picker-shell.js";
10
7
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "./select.js";
11
- import { cn } from "./utils.js";
12
8
  /** Default clock increment, in minutes. Must divide 60. */
13
9
  export const DEFAULT_TIME_STEP = 15;
14
10
  /**
@@ -69,35 +65,35 @@ function preventDismissForPortaledSelect(event) {
69
65
  * chrome as Input, SelectTrigger, and DatePickerButton). `value` is a `Date`
70
66
  * or `null`. Picking a day keeps the popover open so the time can be set;
71
67
  * Clear emits `null`. One month only, clamped to the viewport, so the panel
72
- * stays on-screen at a 390px width.
68
+ * stays on-screen at a 390px width. `className` styles the trigger,
69
+ * `contentClassName` the popover, and `calendarProps` reaches Day Picker.
73
70
  */
74
- export function DateTimePickerButton({ id, value, onChange, timeStep, placeholder = "Pick a date and time", disabled, className, "aria-invalid": ariaInvalid, side, defaultMonth, }) {
71
+ export function DateTimePickerButton({ value, onChange, timeStep, placeholder = "Pick a date and time", defaultMonth, calendarProps, ...shell }) {
75
72
  const [open, setOpen] = React.useState(false);
76
73
  const date = value ?? null;
77
74
  const step = resolveTimeStep(timeStep);
78
75
  const empty = date == null;
79
- const label = date ? format(date, "PPP p") : placeholder;
80
76
  const hourId = React.useId();
81
77
  const minuteId = React.useId();
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) => {
83
- // Re-clicking a selected day would clear it; require Clear instead
84
- // so a time already set is not dropped by an accidental second tap.
85
- if (!next)
86
- return;
87
- onChange(combineDateAndTime(next, date?.getHours() ?? 0, date?.getMinutes() ?? 0));
88
- } }), _jsxs("div", { role: "group", "aria-label": "Time", className: "flex min-w-0 flex-col gap-2 border-t p-3", children: [_jsx("p", { className: "text-xs font-semibold tracking-wide uppercase", children: "Time" }), _jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [_jsxs(Select, { value: date ? padTimePart(date.getHours()) : undefined, onValueChange: (nextHour) => {
89
- if (!date)
90
- return;
91
- onChange(combineDateAndTime(date, Number(nextHour), date.getMinutes()));
92
- }, disabled: empty, children: [_jsx(SelectTrigger, { id: hourId, "aria-label": "Hour", className: "min-w-0 flex-1", children: _jsx(SelectValue, { placeholder: "\u2013\u2013" }) }), _jsx(SelectContent, { children: HOUR_OPTIONS.map((hour) => (_jsx(SelectItem, { value: hour, children: hour }, hour))) })] }), _jsx("span", { className: "text-muted-foreground", "aria-hidden": "true", children: ":" }), _jsxs(Select, { value: date ? padTimePart(date.getMinutes()) : undefined, onValueChange: (nextMinute) => {
93
- if (!date)
94
- return;
95
- onChange(combineDateAndTime(date, date.getHours(), Number(nextMinute)));
96
- }, disabled: empty, children: [_jsx(SelectTrigger, { id: minuteId, "aria-label": "Minute", className: "min-w-0 flex-1", children: _jsx(SelectValue, { placeholder: "\u2013\u2013" }) }), _jsx(SelectContent, { children: minuteOptions(step, date?.getMinutes()).map((minute) => {
97
- const value = padTimePart(minute);
98
- return (_jsx(SelectItem, { value: value, children: value }, value));
99
- }) })] })] })] }), _jsx("div", { className: "flex justify-end border-t p-2", children: _jsx(Button, { type: "button", variant: "ghost", size: "sm", disabled: empty, onClick: () => {
100
- onChange(null);
101
- setOpen(false);
102
- }, children: "Clear" }) })] })] }));
78
+ return (_jsxs(PickerShell, { ...shell, empty: empty, label: date ? format(date, "PPP p") : placeholder, dialogLabel: "Choose date and time", open: open, onOpenChange: setOpen, onInteractOutside: preventDismissForPortaledSelect, children: [_jsx(LazyCalendar, { ...calendarProps, mode: "single", autoFocus: true, selected: date ?? undefined, defaultMonth: defaultMonth ?? date ?? undefined, onSelect: (next) => {
79
+ // Re-clicking a selected day would clear it; require Clear instead
80
+ // so a time already set is not dropped by an accidental second tap.
81
+ if (!next)
82
+ return;
83
+ onChange(combineDateAndTime(next, date?.getHours() ?? 0, date?.getMinutes() ?? 0));
84
+ } }), _jsxs("div", { role: "group", "aria-label": "Time", "data-slot": "date-time-picker-time", className: "flex min-w-0 flex-col gap-2 border-t p-3", children: [_jsx("p", { className: "text-xs font-semibold tracking-wide uppercase", children: "Time" }), _jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [_jsxs(Select, { value: date ? padTimePart(date.getHours()) : undefined, onValueChange: (nextHour) => {
85
+ if (!date)
86
+ return;
87
+ onChange(combineDateAndTime(date, Number(nextHour), date.getMinutes()));
88
+ }, disabled: empty, children: [_jsx(SelectTrigger, { id: hourId, "aria-label": "Hour", className: "min-w-0 flex-1", children: _jsx(SelectValue, { placeholder: "\u2013\u2013" }) }), _jsx(SelectContent, { children: HOUR_OPTIONS.map((hour) => (_jsx(SelectItem, { value: hour, children: hour }, hour))) })] }), _jsx("span", { className: "text-muted-foreground", "aria-hidden": "true", children: ":" }), _jsxs(Select, { value: date ? padTimePart(date.getMinutes()) : undefined, onValueChange: (nextMinute) => {
89
+ if (!date)
90
+ return;
91
+ onChange(combineDateAndTime(date, date.getHours(), Number(nextMinute)));
92
+ }, disabled: empty, children: [_jsx(SelectTrigger, { id: minuteId, "aria-label": "Minute", className: "min-w-0 flex-1", children: _jsx(SelectValue, { placeholder: "\u2013\u2013" }) }), _jsx(SelectContent, { children: minuteOptions(step, date?.getMinutes()).map((minute) => {
93
+ const value = padTimePart(minute);
94
+ return (_jsx(SelectItem, { value: value, children: value }, value));
95
+ }) })] })] })] }), _jsx(PickerClearFooter, { disabled: empty, onClear: () => {
96
+ onChange(null);
97
+ setOpen(false);
98
+ } })] }));
103
99
  }
package/dist/dialog.d.ts CHANGED
@@ -5,8 +5,12 @@ declare function DialogTrigger({ ...props }: React.ComponentProps<typeof DialogP
5
5
  declare function DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>): React.JSX.Element;
6
6
  declare function DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>): React.JSX.Element;
7
7
  declare function DialogOverlay({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Overlay>): React.JSX.Element;
8
- declare function DialogContent({ className, children, showCloseButton, ...props }: React.ComponentProps<typeof DialogPrimitive.Content> & {
8
+ declare function DialogContent({ className, children, showCloseButton, overlayClassName, closeButtonClassName, ...props }: React.ComponentProps<typeof DialogPrimitive.Content> & {
9
9
  showCloseButton?: boolean;
10
+ /** Classes for the backdrop, e.g. a darker scrim. */
11
+ overlayClassName?: string;
12
+ /** Classes for the built-in close button, e.g. its position. */
13
+ closeButtonClassName?: string;
10
14
  }): React.JSX.Element;
11
15
  declare function DialogHeader({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
12
16
  declare function DialogFooter({ className, children, ...props }: React.ComponentProps<"div">): React.JSX.Element;
package/dist/dialog.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { Dialog as DialogPrimitive } from "radix-ui";
5
+ import { overlayFade, overlayFadeZoom, overlaySurface, } from "./internal/overlay-styles.js";
5
6
  import { cn } from "./utils.js";
6
7
  import { Button } from "./button.js";
7
8
  import { XIcon } from "lucide-react";
@@ -18,10 +19,10 @@ function DialogClose({ ...props }) {
18
19
  return _jsx(DialogPrimitive.Close, { "data-slot": "dialog-close", ...props });
19
20
  }
20
21
  function DialogOverlay({ className, ...props }) {
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
+ return (_jsx(DialogPrimitive.Overlay, { "data-slot": "dialog-overlay", className: cn(overlayFade, "fixed inset-0 isolate z-50 bg-black/20 supports-backdrop-filter:backdrop-blur-sm", className), ...props }));
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", 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" })] }) }))] })] }));
24
+ function DialogContent({ className, children, showCloseButton = true, overlayClassName, closeButtonClassName, ...props }) {
25
+ return (_jsxs(DialogPortal, { children: [_jsx(DialogOverlay, { className: overlayClassName }), _jsxs(DialogPrimitive.Content, { "data-slot": "dialog-content", className: cn(overlaySurface, overlayFadeZoom, "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 p-6 text-sm outline-none", className), ...props, children: [children, showCloseButton && (_jsx(DialogPrimitive.Close, { "data-slot": "dialog-close-button", asChild: true, children: _jsxs(Button, { variant: "ghost", size: "icon-sm", className: cn("absolute top-4 right-4 text-muted-foreground hover:text-foreground", closeButtonClassName), children: [_jsx(XIcon, {}), _jsx("span", { className: "sr-only", children: "Close" })] }) }))] })] }));
25
26
  }
26
27
  function DialogHeader({ className, ...props }) {
27
28
  return (_jsx("div", { "data-slot": "dialog-header", className: cn("flex flex-col gap-2", className), ...props }));
@@ -3,11 +3,12 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
5
5
  import { CheckIcon, ChevronRightIcon } from "lucide-react";
6
+ import { menuLabel, optionIcon, optionRow, overlayFadeZoom, overlaySideSlide, overlaySurface, } from "./internal/overlay-styles.js";
6
7
  import { dataDisabledState, optionState } from "./internal/styles.js";
7
8
  import { cn } from "./utils.js";
8
- /** Item highlight and selected chrome shared with `SelectItem`. */
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 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";
9
+ /** Menu row: the shared option row with menu padding and the inset slot. */
10
+ const menuItemClassName = cn(optionRow, optionState.focus, dataDisabledState, optionIcon, "pr-3 data-inset:pl-9");
11
+ const menuContentClassName = cn(overlaySurface, overlaySideSlide, overlayFadeZoom, "z-50 overflow-x-hidden overflow-y-auto p-1");
11
12
  /** Action menu for commands (row actions, overflow menus). Use `Select` to pick a value. */
12
13
  function DropdownMenu({ ...props }) {
13
14
  return _jsx(DropdownMenuPrimitive.Root, { "data-slot": "dropdown-menu", ...props });
@@ -37,7 +38,7 @@ function DropdownMenuRadioItem({ className, children, ...props }) {
37
38
  return (_jsxs(DropdownMenuPrimitive.RadioItem, { "data-slot": "dropdown-menu-radio-item", className: cn(menuItemClassName, "pl-9", className), ...props, children: [_jsx("span", { className: "pointer-events-none absolute left-3 flex size-3.5 items-center justify-center", children: _jsx(DropdownMenuPrimitive.ItemIndicator, { children: _jsx("span", { className: "block size-1.5 rounded-full bg-current" }) }) }), children] }));
38
39
  }
39
40
  function DropdownMenuLabel({ className, inset, ...props }) {
40
- return (_jsx(DropdownMenuPrimitive.Label, { "data-slot": "dropdown-menu-label", "data-inset": inset || undefined, className: cn("px-3 py-2 text-xs font-semibold tracking-wider text-muted-foreground uppercase data-inset:pl-9", className), ...props }));
41
+ return (_jsx(DropdownMenuPrimitive.Label, { "data-slot": "dropdown-menu-label", "data-inset": inset || undefined, className: cn(menuLabel, "data-inset:pl-9", className), ...props }));
41
42
  }
42
43
  function DropdownMenuSeparator({ className, ...props }) {
43
44
  return (_jsx(DropdownMenuPrimitive.Separator, { "data-slot": "dropdown-menu-separator", className: cn("-mx-1 my-1 h-px bg-border", className), ...props }));
@@ -1,10 +1,11 @@
1
- type Props = {
1
+ import type * as React from "react";
2
+ type Props = React.ComponentProps<"div"> & {
2
3
  children: React.ReactNode;
3
4
  /** Tighter padding for compact widgets; default is standard form empty states. */
4
5
  size?: "sm" | "md" | "lg";
5
6
  muted?: boolean;
6
7
  className?: string;
7
8
  };
8
- /** Dashed empty-state panel for lists, widgets, and form sections. */
9
- export declare function EmptyPanel({ children, size, muted, className, }: Props): import("react").JSX.Element;
9
+ /** Dashed empty-state panel for lists, widgets, and form sections. Other props go to the root `div`. */
10
+ export declare function EmptyPanel({ children, size, muted, className, ...props }: Props): React.JSX.Element;
10
11
  export {};
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { cn } from "./utils.js";
3
- /** Dashed empty-state panel for lists, widgets, and form sections. */
4
- export function EmptyPanel({ children, size = "md", muted = true, className, }) {
5
- return (_jsx("div", { className: cn("rounded-none border border-dashed text-center text-sm text-muted-foreground", muted && "bg-muted/30", size === "sm" && "px-3 py-4", size === "md" && "px-4 py-6", size === "lg" && "px-4 py-8", className), children: children }));
3
+ /** Dashed empty-state panel for lists, widgets, and form sections. Other props go to the root `div`. */
4
+ export function EmptyPanel({ children, size = "md", muted = true, className, ...props }) {
5
+ return (_jsx("div", { "data-slot": "empty-panel", ...props, className: cn("rounded-none border border-dashed text-center text-sm text-muted-foreground", muted && "bg-muted/30", size === "sm" && "px-3 py-4", size === "md" && "px-4 py-6", size === "lg" && "px-4 py-8", className), children: children }));
6
6
  }
@@ -4,12 +4,14 @@ type ErrorFallbackProps = {
4
4
  message?: string;
5
5
  onRetry?: () => void;
6
6
  onReload?: () => void;
7
+ className?: string;
7
8
  };
8
9
  /**
9
- * Presentational recovery UI used by `ErrorBoundary`. Exported so the markup
10
- * can be tested without throwing during render.
10
+ * Presentational recovery UI used by `ErrorBoundary`: a large `EmptyPanel`
11
+ * with the message and recovery buttons. Exported so the markup can be
12
+ * tested without throwing during render.
11
13
  */
12
- export declare function ErrorFallback({ message, onRetry, onReload, }: ErrorFallbackProps): import("react").JSX.Element;
14
+ export declare function ErrorFallback({ message, onRetry, onReload, className, }: ErrorFallbackProps): import("react").JSX.Element;
13
15
  type ErrorBoundaryProps = {
14
16
  children: ReactNode;
15
17
  /** Overrides the default recovery message. */
@@ -2,12 +2,17 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { Component } from "react";
4
4
  import { Button } from "./button.js";
5
+ import { EmptyPanel } from "./empty-panel.js";
6
+ import { cn } from "./utils.js";
5
7
  /**
6
- * Presentational recovery UI used by `ErrorBoundary`. Exported so the markup
7
- * can be tested without throwing during render.
8
+ * Presentational recovery UI used by `ErrorBoundary`: a large `EmptyPanel`
9
+ * with the message and recovery buttons. Exported so the markup can be
10
+ * tested without throwing during render.
8
11
  */
9
- export function ErrorFallback({ message = "Something went wrong.", onRetry, onReload, }) {
10
- return (_jsxs("div", { role: "alert", "data-slot": "error-boundary", className: "flex flex-col items-center gap-4 rounded-none border border-dashed bg-muted/30 px-4 py-8 text-center", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: message }), _jsxs("div", { className: "flex flex-wrap items-center justify-center gap-2", children: [onRetry ? (_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: onRetry, children: "Try again" })) : null, onReload ? (_jsx(Button, { type: "button", size: "sm", onClick: onReload, children: "Reload" })) : null] })] }));
12
+ export function ErrorFallback({ message = "Something went wrong.", onRetry, onReload, className, }) {
13
+ return (_jsxs(EmptyPanel, { role: "alert", "data-slot": "error-boundary", size: "lg",
14
+ // The panel's muted text is for its message; the buttons keep their own.
15
+ className: cn("flex flex-col items-center gap-4 text-foreground", className), children: [_jsx("p", { className: "text-sm text-muted-foreground", children: message }), _jsxs("div", { className: "flex flex-wrap items-center justify-center gap-2", children: [onRetry ? (_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: onRetry, children: "Try again" })) : null, onReload ? (_jsx(Button, { type: "button", size: "sm", onClick: onReload, children: "Reload" })) : null] })] }));
11
16
  }
12
17
  /**
13
18
  * Generic React error boundary with retry (reset) and full-page reload.
package/dist/field.d.ts CHANGED
@@ -4,9 +4,22 @@ declare function FieldSet({ className, ...props }: React.ComponentProps<"fieldse
4
4
  declare function FieldLegend({ className, variant, ...props }: React.ComponentProps<"legend"> & {
5
5
  variant?: "legend" | "label";
6
6
  }): import("react").JSX.Element;
7
+ /**
8
+ * Stack of fields and the `@container/field-group` that responsive fields
9
+ * query. It keeps `w-full`: an inline-size container has no intrinsic
10
+ * width, so without it the group collapses to zero in a flex row or any
11
+ * shrink-to-fit parent. Override with `className` (`w-auto`, `max-w-md`).
12
+ */
7
13
  declare function FieldGroup({ className, ...props }: React.ComponentProps<"div">): import("react").JSX.Element;
14
+ /**
15
+ * Vertical (and responsive, below `@md`) fields are a flex column, so
16
+ * children stretch to the field's width by default (`align-items: stretch`)
17
+ * while a child's own width class (`w-32`, `w-fit`) still wins. The field
18
+ * itself sets no width: it fills a block or column parent and sizes to its
19
+ * content in a row.
20
+ */
8
21
  declare const fieldVariants: (props?: ({
9
- orientation?: "vertical" | "horizontal" | "responsive" | null | undefined;
22
+ orientation?: "horizontal" | "vertical" | "responsive" | null | undefined;
10
23
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
11
24
  declare function Field({ className, orientation, ...props }: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>): import("react").JSX.Element;
12
25
  declare function FieldContent({ className, ...props }: React.ComponentProps<"div">): import("react").JSX.Element;
package/dist/field.js CHANGED
@@ -11,15 +11,28 @@ function FieldSet({ className, ...props }) {
11
11
  function FieldLegend({ className, variant = "legend", ...props }) {
12
12
  return (_jsx("legend", { "data-slot": "field-legend", "data-variant": variant, className: cn("mb-3 font-semibold uppercase data-[variant=label]:text-xs data-[variant=legend]:text-xs", className), ...props }));
13
13
  }
14
+ /**
15
+ * Stack of fields and the `@container/field-group` that responsive fields
16
+ * query. It keeps `w-full`: an inline-size container has no intrinsic
17
+ * width, so without it the group collapses to zero in a flex row or any
18
+ * shrink-to-fit parent. Override with `className` (`w-auto`, `max-w-md`).
19
+ */
14
20
  function FieldGroup({ className, ...props }) {
15
21
  return (_jsx("div", { "data-slot": "field-group", className: cn("group/field-group @container/field-group flex w-full flex-col gap-10 data-[slot=checkbox-group]:gap-3 *:data-[slot=field-group]:gap-4", className), ...props }));
16
22
  }
17
- const fieldVariants = cva("group/field flex w-full gap-3 data-[invalid=true]:text-destructive", {
23
+ /**
24
+ * Vertical (and responsive, below `@md`) fields are a flex column, so
25
+ * children stretch to the field's width by default (`align-items: stretch`)
26
+ * while a child's own width class (`w-32`, `w-fit`) still wins. The field
27
+ * itself sets no width: it fills a block or column parent and sizes to its
28
+ * content in a row.
29
+ */
30
+ const fieldVariants = cva("group/field flex gap-3 data-[invalid=true]:text-destructive", {
18
31
  variants: {
19
32
  orientation: {
20
- vertical: "flex-col *:w-full [&>.sr-only]:w-auto",
33
+ vertical: "flex-col",
21
34
  horizontal: "flex-row items-center has-[>[data-slot=field-content]]:items-start *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
22
- responsive: "flex-col *:w-full @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:*:data-[slot=field-label]:flex-auto [&>.sr-only]:w-auto @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
35
+ responsive: "flex-col @md/field-group:flex-row @md/field-group:items-center @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:*:data-[slot=field-label]:flex-auto @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
23
36
  },
24
37
  },
25
38
  defaultVariants: {
@@ -1,4 +1,4 @@
1
- import { type DateLike } from "../date-picker.js";
1
+ import { type DateLike, type DatePickerCalendarProps } from "../date-picker.js";
2
2
  import type { FieldMetaState } from "./field-error.js";
3
3
  type Props = {
4
4
  value: DateLike;
@@ -9,7 +9,13 @@ type Props = {
9
9
  disabled?: boolean;
10
10
  /** Forwarded to the date picker trigger. */
11
11
  className?: string;
12
+ /** Classes for the field wrapper (label, trigger, and error). */
13
+ fieldClassName?: string;
14
+ /** Forwarded to the date picker popover. */
15
+ contentClassName?: string;
16
+ /** Day Picker options forwarded to the calendar (disabled days, etc.). */
17
+ calendarProps?: DatePickerCalendarProps;
12
18
  };
13
19
  /** Date picker with built-in field error text and invalid styling. */
14
- export declare function FormDatePickerField({ value, onValueChange, label, meta, placeholder, disabled, className, }: Props): import("react").JSX.Element;
20
+ export declare function FormDatePickerField({ onValueChange, label, meta, fieldClassName, ...picker }: Props): import("react").JSX.Element;
15
21
  export {};
@@ -1,11 +1,11 @@
1
1
  "use client";
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { useId } from "react";
4
- import { DatePickerButton } from "../date-picker.js";
5
- import { FormFieldShell, invalidWhen, metaErrorText } from "./field-shell.js";
4
+ import { DatePickerButton, } from "../date-picker.js";
5
+ import { FormFieldShell, metaErrorText, pickerFieldAria, } from "./field-shell.js";
6
6
  /** Date picker with built-in field error text and invalid styling. */
7
- export function FormDatePickerField({ value, onValueChange, label, meta, placeholder, disabled, className, }) {
7
+ export function FormDatePickerField({ onValueChange, label, meta, fieldClassName, ...picker }) {
8
8
  const errorText = metaErrorText(meta);
9
9
  const id = useId();
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) }) }));
10
+ return (_jsx(FormFieldShell, { id: id, label: label, errorText: errorText, className: fieldClassName, children: _jsx(DatePickerButton, { ...picker, id: id, onChange: onValueChange, ...pickerFieldAria(id, errorText) }) }));
11
11
  }