atlas-react-kit 0.4.1 → 0.5.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 (51) hide show
  1. package/COMPONENTS.md +122 -39
  2. package/DESIGN_RULES.md +61 -17
  3. package/README.md +56 -10
  4. package/dist/accordion.d.ts +8 -0
  5. package/dist/accordion.js +20 -0
  6. package/dist/alert.d.ts +19 -0
  7. package/dist/alert.js +37 -0
  8. package/dist/avatar.d.ts +9 -0
  9. package/dist/avatar.js +16 -0
  10. package/dist/badge.d.ts +2 -2
  11. package/dist/breadcrumb.d.ts +16 -0
  12. package/dist/breadcrumb.js +33 -0
  13. package/dist/button.d.ts +6 -5
  14. package/dist/button.js +6 -5
  15. package/dist/calendar.d.ts +1 -1
  16. package/dist/calendar.js +19 -1
  17. package/dist/combobox.d.ts +4 -1
  18. package/dist/combobox.js +10 -5
  19. package/dist/date-picker.js +14 -3
  20. package/dist/date-time-picker.d.ts +36 -0
  21. package/dist/date-time-picker.js +105 -0
  22. package/dist/dropdown-menu.d.ts +26 -0
  23. package/dist/dropdown-menu.js +56 -0
  24. package/dist/error-boundary.d.ts +1 -1
  25. package/dist/field.js +1 -1
  26. package/dist/form/date-time-picker-field.d.ts +16 -0
  27. package/dist/form/date-time-picker-field.js +12 -0
  28. package/dist/form/text-field.d.ts +7 -1
  29. package/dist/form/text-field.js +3 -3
  30. package/dist/pagination.d.ts +18 -0
  31. package/dist/pagination.js +35 -0
  32. package/dist/progress.d.ts +8 -0
  33. package/dist/progress.js +13 -0
  34. package/dist/radio-group.d.ts +13 -0
  35. package/dist/radio-group.js +20 -0
  36. package/dist/spinner.d.ts +8 -0
  37. package/dist/spinner.js +13 -0
  38. package/dist/switch.d.ts +9 -0
  39. package/dist/switch.js +14 -0
  40. package/dist/table.d.ts +2 -0
  41. package/dist/table.js +25 -1
  42. package/dist/tabs.d.ts +6 -3
  43. package/dist/tabs.js +5 -2
  44. package/dist/theme-provider.d.ts +2 -1
  45. package/dist/theme-provider.js +28 -4
  46. package/dist/toggle.js +1 -1
  47. package/dist/tooltip.d.ts +15 -0
  48. package/dist/tooltip.js +26 -0
  49. package/package.json +58 -5
  50. package/skills/atlas-react-kit/SKILL.md +139 -0
  51. package/theme.css +5 -5
@@ -0,0 +1,33 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as React from "react";
3
+ import { Slot } from "radix-ui";
4
+ import { ChevronRightIcon, MoreHorizontalIcon } from "lucide-react";
5
+ import { cn } from "./utils.js";
6
+ /**
7
+ * Where the current page sits in a hierarchy. The last item is
8
+ * `BreadcrumbPage` (plain text with `aria-current="page"`), not a link.
9
+ */
10
+ function Breadcrumb({ ...props }) {
11
+ return _jsx("nav", { "aria-label": "Breadcrumb", "data-slot": "breadcrumb", ...props });
12
+ }
13
+ function BreadcrumbList({ className, ...props }) {
14
+ return (_jsx("ol", { "data-slot": "breadcrumb-list", className: cn("flex flex-wrap items-center gap-1.5 text-sm break-words text-muted-foreground sm:gap-2", className), ...props }));
15
+ }
16
+ function BreadcrumbItem({ className, ...props }) {
17
+ return (_jsx("li", { "data-slot": "breadcrumb-item", className: cn("inline-flex items-center gap-1.5", className), ...props }));
18
+ }
19
+ /** Pass `asChild` to render a router link (e.g. Next `Link`) with these styles. */
20
+ function BreadcrumbLink({ asChild, className, ...props }) {
21
+ const Comp = asChild ? Slot.Root : "a";
22
+ return (_jsx(Comp, { "data-slot": "breadcrumb-link", className: cn("transition-colors outline-none hover:text-selected-foreground focus-visible:text-selected-foreground focus-visible:underline focus-visible:underline-offset-4", className), ...props }));
23
+ }
24
+ function BreadcrumbPage({ className, ...props }) {
25
+ return (_jsx("span", { "data-slot": "breadcrumb-page", "aria-current": "page", className: cn("font-medium text-foreground", className), ...props }));
26
+ }
27
+ function BreadcrumbSeparator({ children, className, ...props }) {
28
+ return (_jsx("li", { "data-slot": "breadcrumb-separator", role: "presentation", "aria-hidden": "true", className: cn("[&>svg]:size-3.5", className), ...props, children: children ?? _jsx(ChevronRightIcon, {}) }));
29
+ }
30
+ function BreadcrumbEllipsis({ className, ...props }) {
31
+ return (_jsxs("span", { "data-slot": "breadcrumb-ellipsis", role: "presentation", "aria-hidden": "true", className: cn("flex size-6 items-center justify-center", className), ...props, children: [_jsx(MoreHorizontalIcon, { className: "size-4" }), _jsx("span", { className: "sr-only", children: "More" })] }));
32
+ }
33
+ export { Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, };
package/dist/button.d.ts CHANGED
@@ -1,14 +1,15 @@
1
1
  import * as React from "react";
2
2
  import { type VariantProps } from "class-variance-authority";
3
3
  /**
4
- * Outline and ghost hover/focus: brand purple border and text, optional
5
- * selected wash, no muted grey fill. Used by quiet actions such as
4
+ * Outline and ghost hover/focus: brand purple border, selected wash, and
5
+ * `selected-foreground` text (primary in light mode, a lighter purple that
6
+ * stays readable on dark), no muted grey fill. Used by quiet actions such as
6
7
  * "Clear selection" sitting on a selected row.
7
8
  */
8
- declare const quietButtonInteraction = "hover:border-primary hover:bg-selected hover:text-primary focus-visible:border-primary focus-visible:text-primary focus-visible:ring-primary/30 aria-expanded:border-primary aria-expanded:bg-selected aria-expanded:text-primary";
9
+ declare const quietButtonInteraction = "hover:border-primary hover:bg-selected hover:text-selected-foreground focus-visible:border-primary focus-visible:text-selected-foreground focus-visible:ring-primary/30 aria-expanded:border-primary aria-expanded:bg-selected aria-expanded:text-selected-foreground";
9
10
  declare const buttonVariants: (props?: ({
10
- variant?: "default" | "secondary" | "destructive" | "outline" | "ghost" | "link" | null | undefined;
11
- size?: "default" | "xs" | "sm" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg" | null | undefined;
11
+ variant?: "default" | "link" | "destructive" | "secondary" | "outline" | "ghost" | null | undefined;
12
+ size?: "default" | "sm" | "lg" | "xs" | "icon" | "icon-xs" | "icon-sm" | "icon-lg" | null | undefined;
12
13
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
13
14
  declare function Button({ className, variant, size, asChild, ...props }: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants> & {
14
15
  asChild?: boolean;
package/dist/button.js CHANGED
@@ -4,11 +4,12 @@ import { cva } from "class-variance-authority";
4
4
  import { Slot } from "radix-ui";
5
5
  import { cn } from "./utils.js";
6
6
  /**
7
- * Outline and ghost hover/focus: brand purple border and text, optional
8
- * selected wash, no muted grey fill. Used by quiet actions such as
7
+ * Outline and ghost hover/focus: brand purple border, selected wash, and
8
+ * `selected-foreground` text (primary in light mode, a lighter purple that
9
+ * stays readable on dark), no muted grey fill. Used by quiet actions such as
9
10
  * "Clear selection" sitting on a selected row.
10
11
  */
11
- const quietButtonInteraction = "hover:border-primary hover:bg-selected hover:text-primary focus-visible:border-primary focus-visible:text-primary focus-visible:ring-primary/30 aria-expanded:border-primary aria-expanded:bg-selected aria-expanded:text-primary";
12
+ const quietButtonInteraction = "hover:border-primary hover:bg-selected hover:text-selected-foreground focus-visible:border-primary focus-visible:text-selected-foreground focus-visible:ring-primary/30 aria-expanded:border-primary aria-expanded:bg-selected aria-expanded:text-selected-foreground";
12
13
  const buttonVariants = cva("group/button inline-flex shrink-0 items-center justify-center rounded-none border border-transparent bg-clip-padding text-xs font-semibold tracking-widest whitespace-nowrap uppercase transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/30 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-2 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5", {
13
14
  variants: {
14
15
  variant: {
@@ -16,8 +17,8 @@ const buttonVariants = cva("group/button inline-flex shrink-0 items-center justi
16
17
  outline: `border-border bg-transparent ${quietButtonInteraction}`,
17
18
  secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
18
19
  ghost: quietButtonInteraction,
19
- destructive: "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
20
- link: "text-primary underline underline-offset-4 hover:underline",
20
+ destructive: "bg-destructive/10 text-destructive hover:bg-destructive/15 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
21
+ link: "text-selected-foreground underline underline-offset-4 hover:underline",
21
22
  },
22
23
  size: {
23
24
  default: "h-10 gap-1.5 px-6 has-data-[icon=inline-end]:pr-4 has-data-[icon=inline-start]:pl-4",
@@ -1,7 +1,7 @@
1
1
  import * as React from "react";
2
2
  import { DayPicker, type DayButton, type Locale } from "react-day-picker";
3
3
  import { Button } from "./button.js";
4
- declare function Calendar({ className, classNames, showOutsideDays, captionLayout, buttonVariant, locale, formatters, components, ...props }: React.ComponentProps<typeof DayPicker> & {
4
+ declare function Calendar({ className, classNames, showOutsideDays, captionLayout, buttonVariant, locale, formatters, labels, components, ...props }: React.ComponentProps<typeof DayPicker> & {
5
5
  buttonVariant?: React.ComponentProps<typeof Button>["variant"];
6
6
  }): React.JSX.Element;
7
7
  declare function CalendarDayButton({ className, day, modifiers, locale, ...props }: React.ComponentProps<typeof DayButton> & {
package/dist/calendar.js CHANGED
@@ -5,11 +5,29 @@ import { DayPicker, getDefaultClassNames, } from "react-day-picker";
5
5
  import { cn } from "./utils.js";
6
6
  import { Button, buttonVariants } from "./button.js";
7
7
  import { ChevronLeftIcon, ChevronRightIcon, ChevronDownIcon, } from "lucide-react";
8
- function Calendar({ className, classNames, showOutsideDays = true, captionLayout = "label", buttonVariant = "ghost", locale, formatters, components, ...props }) {
8
+ function Calendar({ className, classNames, showOutsideDays = true, captionLayout = "label", buttonVariant = "ghost", locale, formatters, labels, components, ...props }) {
9
9
  const defaultClassNames = getDefaultClassNames();
10
10
  return (_jsx(DayPicker, { showOutsideDays: showOutsideDays, className: cn("group/calendar bg-background p-3 [--cell-radius:0] [--cell-size:--spacing(8)] in-data-[slot=card-content]:bg-transparent in-data-[slot=popover-content]:bg-transparent", String.raw `rtl:**:[.rdp-button\_next>svg]:rotate-180`, String.raw `rtl:**:[.rdp-button\_previous>svg]:rotate-180`, className), captionLayout: captionLayout, locale: locale, formatters: {
11
11
  formatMonthDropdown: (date) => date.toLocaleString(locale?.code, { month: "short" }),
12
12
  ...formatters,
13
+ }, labels: {
14
+ // Day numbers without ordinals ("September 27", not "27th") so the
15
+ // accessible name contains the visible "27" (WCAG 2.5.3 Label in
16
+ // Name); speech users can then say "click 27".
17
+ labelDayButton: (date, modifiers) => {
18
+ let label = date.toLocaleDateString(locale?.code, {
19
+ weekday: "long",
20
+ year: "numeric",
21
+ month: "long",
22
+ day: "numeric",
23
+ });
24
+ if (modifiers.today)
25
+ label = `Today, ${label}`;
26
+ if (modifiers.selected)
27
+ label = `${label}, selected`;
28
+ return label;
29
+ },
30
+ ...labels,
13
31
  }, classNames: {
14
32
  root: cn("w-fit", defaultClassNames.root),
15
33
  months: cn("relative flex flex-col gap-4 md:flex-row", defaultClassNames.months),
@@ -14,6 +14,9 @@ type ComboboxProps = {
14
14
  id?: string;
15
15
  className?: string;
16
16
  "aria-invalid"?: boolean;
17
+ /** Name the trigger when there is no `<Label htmlFor={id}>`. */
18
+ "aria-label"?: string;
19
+ "aria-labelledby"?: string;
17
20
  };
18
21
  /**
19
22
  * Searchable single-select built from Button, Popover, and Input.
@@ -22,5 +25,5 @@ type ComboboxProps = {
22
25
  * Keyboard highlight (arrows, Home/End, PageUp/PageDown) scrolls the active
23
26
  * option into view inside the overflow list.
24
27
  */
25
- export declare function Combobox({ options, value, onValueChange, placeholder, searchPlaceholder, emptyText, disabled, id, className, "aria-invalid": ariaInvalid, }: ComboboxProps): React.JSX.Element;
28
+ export declare function Combobox({ options, value, onValueChange, placeholder, searchPlaceholder, emptyText, disabled, id, className, "aria-invalid": ariaInvalid, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, }: ComboboxProps): React.JSX.Element;
26
29
  export {};
package/dist/combobox.js CHANGED
@@ -20,7 +20,7 @@ const LISTBOX_PAGE_SIZE = 10;
20
20
  * Keyboard highlight (arrows, Home/End, PageUp/PageDown) scrolls the active
21
21
  * option into view inside the overflow list.
22
22
  */
23
- export function Combobox({ options, value, onValueChange, placeholder = "Select", searchPlaceholder = "Search", emptyText = "No results", disabled, id, className, "aria-invalid": ariaInvalid, }) {
23
+ 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, }) {
24
24
  const [open, setOpen] = React.useState(false);
25
25
  const [query, setQuery] = React.useState("");
26
26
  const [highlight, setHighlight] = React.useState(0);
@@ -61,10 +61,17 @@ export function Combobox({ options, value, onValueChange, placeholder = "Select"
61
61
  setQuery(next);
62
62
  setHighlight(0);
63
63
  });
64
+ // Closing from here does not go through Radix's onOpenChange, so clear the
65
+ // query too. A stale query would filter the next open while the highlight
66
+ // is computed against the full list, and Enter would pick the wrong option.
64
67
  const selectValue = (next) => {
65
68
  onValueChange(next);
66
69
  setOpen(false);
70
+ setQuery("");
67
71
  };
72
+ // Index-based ids: option values may contain spaces, which are not valid
73
+ // in an id or an aria-activedescendant reference.
74
+ const optionId = (index) => `${listId}-option-${index}`;
68
75
  const activeOption = filtered[highlight];
69
76
  const onSearchKeyDown = (event) => {
70
77
  if (event.key === "ArrowDown") {
@@ -102,11 +109,9 @@ export function Combobox({ options, value, onValueChange, placeholder = "Select"
102
109
  setHighlight((index) => Math.max(index - LISTBOX_PAGE_SIZE, 0));
103
110
  }
104
111
  };
105
- 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, 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
106
- ? `${listId}-option-${activeOption.value}`
107
- : undefined }) }), _jsx("ul", { id: listId, role: "listbox", className: "max-h-60 overflow-y-auto p-1", children: filtered.length === 0 ? (_jsx("li", { className: "px-3 py-4 text-center text-sm text-muted-foreground", children: emptyText })) : (filtered.map((option, index) => {
112
+ 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 ? optionId(highlight) : undefined }) }), _jsx("ul", { id: listId, role: "listbox", className: "max-h-60 overflow-y-auto p-1", children: filtered.length === 0 ? (_jsx("li", { className: "px-3 py-4 text-center text-sm text-muted-foreground", children: emptyText })) : (filtered.map((option, index) => {
108
113
  const isSelected = option.value === value;
109
114
  const isActive = index === highlight;
110
- return (_jsx("li", { role: "none", children: _jsxs("button", { ref: isActive ? scrollHighlightedOption : undefined, id: `${listId}-option-${option.value}`, 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 && "bg-selected text-selected-foreground", isActive && "bg-accent text-accent-foreground"), 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] }) }, option.value));
115
+ return (_jsx("li", { role: "none", children: _jsxs("button", { ref: isActive ? scrollHighlightedOption : undefined, id: optionId(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 && "bg-selected text-selected-foreground", isActive && "bg-accent text-accent-foreground"), 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] }) }, option.value));
111
116
  })) })] })] }));
112
117
  }
@@ -77,11 +77,22 @@ function DatePickerRangeButton({ id, value, onChange, placeholder = "Pick a date
77
77
  const from = toDate(value?.from);
78
78
  const to = toDate(value?.to);
79
79
  const selected = from || to ? { from, to } : undefined;
80
- return (_jsxs(DatePickerShell, { id: id, disabled: disabled, ariaInvalid: ariaInvalid, className: className, empty: !from, label: formatRangeTriggerLabel(from, to, placeholder), open: open, onOpenChange: setOpen, side: side, children: [_jsx(Calendar, { mode: "range", selected: selected, defaultMonth: defaultMonth ?? from ?? to, numberOfMonths: months, onSelect: (next) => {
81
- onChange(next);
80
+ // Opening on a complete range means the next click starts a new one.
81
+ // Without this, Day Picker extends the old range and the popover closes
82
+ // on that first click, so a fresh range needs Clear first.
83
+ const restartRef = React.useRef(false);
84
+ const onOpenChange = (next) => {
85
+ if (next)
86
+ restartRef.current = Boolean(from && to);
87
+ setOpen(next);
88
+ };
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(Calendar, { mode: "range", selected: selected, defaultMonth: defaultMonth ?? from ?? to, numberOfMonths: months, onSelect: (next, day) => {
90
+ const range = restartRef.current ? { from: day, to: day } : next;
91
+ restartRef.current = false;
92
+ onChange(range);
82
93
  // First click is a same-day range; keep the popover open so the
83
94
  // user can set `to`. Close once the ends differ.
84
- if (next?.from && next.to && !isSameDay(next.from, next.to)) {
95
+ if (range?.from && range.to && !isSameDay(range.from, range.to)) {
85
96
  setOpen(false);
86
97
  }
87
98
  } }), _jsx("div", { className: "flex justify-end border-t p-2", children: _jsx(Button, { type: "button", variant: "ghost", size: "sm", disabled: !from && !to, onClick: () => {
@@ -0,0 +1,36 @@
1
+ import * as React from "react";
2
+ import { PopoverContent } from "./popover.js";
3
+ /** Default clock increment, in minutes. Must divide 60. */
4
+ export declare const DEFAULT_TIME_STEP = 15;
5
+ export type DateTimePickerButtonProps = {
6
+ /** Controlled instant. `null` or omitted is empty. */
7
+ value?: Date | null;
8
+ onChange: (date: Date | null) => void;
9
+ /**
10
+ * Minute increment for the time selects (1, 5, 15, 30, or 60).
11
+ * Values that do not divide 60 fall back to 15 so the list stays even.
12
+ */
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
+ };
24
+ /**
25
+ * Normalize a minute step so option lists stay even (1, 5, 15, 30, 60).
26
+ * Invalid values fall back to 15 so a bad prop cannot empty the minute list.
27
+ */
28
+ export declare function resolveTimeStep(step: number | undefined): number;
29
+ /**
30
+ * Calendar plus hour/minute popover trigger styled as a form control (same
31
+ * chrome as Input, SelectTrigger, and DatePickerButton). `value` is a `Date`
32
+ * or `null`. Picking a day keeps the popover open so the time can be set;
33
+ * Clear emits `null`. One month only, clamped to the viewport, so the panel
34
+ * stays on-screen at a 390px width.
35
+ */
36
+ export declare function DateTimePickerButton({ id, value, onChange, timeStep, placeholder, disabled, className, "aria-invalid": ariaInvalid, side, defaultMonth, }: DateTimePickerButtonProps): React.JSX.Element;
@@ -0,0 +1,105 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { format } from "date-fns";
5
+ import { CalendarIcon } from "lucide-react";
6
+ import { Button } from "./button.js";
7
+ import { Calendar } from "./calendar.js";
8
+ import { formControlTriggerClassName } from "./internal/form-control.js";
9
+ import { Popover, PopoverContent, PopoverTrigger } from "./popover.js";
10
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "./select.js";
11
+ import { cn } from "./utils.js";
12
+ /** Default clock increment, in minutes. Must divide 60. */
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
+ /**
17
+ * Normalize a minute step so option lists stay even (1, 5, 15, 30, 60).
18
+ * Invalid values fall back to 15 so a bad prop cannot empty the minute list.
19
+ */
20
+ export function resolveTimeStep(step) {
21
+ if (step == null)
22
+ return DEFAULT_TIME_STEP;
23
+ if (!Number.isInteger(step) || step <= 0 || step > 60 || 60 % step !== 0) {
24
+ return DEFAULT_TIME_STEP;
25
+ }
26
+ return step;
27
+ }
28
+ /** Two-digit clock fragment (`00`–`23` / `00`–`59`). */
29
+ function padTimePart(value) {
30
+ return String(value).padStart(2, "0");
31
+ }
32
+ /**
33
+ * Build a local Date from a calendar day and a clock time.
34
+ * Seconds and milliseconds stay zero so a stepped minute remains exact.
35
+ */
36
+ function combineDateAndTime(day, hours, minutes) {
37
+ const next = new Date(day);
38
+ next.setHours(hours, minutes, 0, 0);
39
+ return next;
40
+ }
41
+ /**
42
+ * Minute options for `timeStep`, plus the current minute when it is off-step
43
+ * so a loaded value such as 14:07 still appears in the select.
44
+ */
45
+ function minuteOptions(step, current) {
46
+ const minutes = [];
47
+ for (let minute = 0; minute < 60; minute += step) {
48
+ minutes.push(minute);
49
+ }
50
+ if (current != null && !minutes.includes(current)) {
51
+ minutes.push(current);
52
+ minutes.sort((a, b) => a - b);
53
+ }
54
+ return minutes;
55
+ }
56
+ const HOUR_OPTIONS = Array.from({ length: 24 }, (_, hour) => padTimePart(hour));
57
+ /**
58
+ * Keep the date-time popover open when a portaled Select option is clicked.
59
+ * Select content renders outside the popover, so Radix would otherwise treat
60
+ * that pointer down as an outside dismiss.
61
+ */
62
+ function preventDismissForPortaledSelect(event) {
63
+ const target = event.target;
64
+ if (target instanceof Element &&
65
+ target.closest("[data-slot=select-content]")) {
66
+ event.preventDefault();
67
+ }
68
+ }
69
+ /**
70
+ * Calendar plus hour/minute popover trigger styled as a form control (same
71
+ * chrome as Input, SelectTrigger, and DatePickerButton). `value` is a `Date`
72
+ * or `null`. Picking a day keeps the popover open so the time can be set;
73
+ * Clear emits `null`. One month only, clamped to the viewport, so the panel
74
+ * stays on-screen at a 390px width.
75
+ */
76
+ export function DateTimePickerButton({ id, value, onChange, timeStep, placeholder = "Pick a date and time", disabled, className, "aria-invalid": ariaInvalid, side, defaultMonth, }) {
77
+ const [open, setOpen] = React.useState(false);
78
+ const date = value ?? null;
79
+ const step = resolveTimeStep(timeStep);
80
+ const empty = date == null;
81
+ const label = date ? format(date, "PPP p") : placeholder;
82
+ const hourId = React.useId();
83
+ 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(formControlTriggerClassName, "justify-start data-[empty=true]:text-muted-foreground", className), children: [_jsx(CalendarIcon, { "data-icon": "inline-start" }), label] }) }), _jsxs(PopoverContent, { align: "start", side: side, collisionPadding: DATE_TIME_PICKER_COLLISION_PADDING, onInteractOutside: preventDismissForPortaledSelect, className: "w-auto min-w-0 max-w-[min(100vw-2rem,24rem)] max-h-(--radix-popover-content-available-height) overflow-y-auto rounded-none p-0", children: [_jsx(Calendar, { mode: "single", selected: date ?? undefined, defaultMonth: defaultMonth ?? date ?? undefined, onSelect: (next) => {
85
+ // Re-clicking a selected day would clear it; require Clear instead
86
+ // so a time already set is not dropped by an accidental second tap.
87
+ if (!next)
88
+ return;
89
+ onChange(combineDateAndTime(next, date?.getHours() ?? 0, date?.getMinutes() ?? 0));
90
+ } }), _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) => {
91
+ if (!date)
92
+ return;
93
+ onChange(combineDateAndTime(date, Number(nextHour), date.getMinutes()));
94
+ }, 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) => {
95
+ if (!date)
96
+ return;
97
+ onChange(combineDateAndTime(date, date.getHours(), Number(nextMinute)));
98
+ }, 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) => {
99
+ const value = padTimePart(minute);
100
+ return (_jsx(SelectItem, { value: value, children: value }, value));
101
+ }) })] })] })] }), _jsx("div", { className: "flex justify-end border-t p-2", children: _jsx(Button, { type: "button", variant: "ghost", size: "sm", disabled: empty, onClick: () => {
102
+ onChange(null);
103
+ setOpen(false);
104
+ }, children: "Clear" }) })] })] }));
105
+ }
@@ -0,0 +1,26 @@
1
+ import * as React from "react";
2
+ import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
3
+ /** Action menu for commands (row actions, overflow menus). Use `Select` to pick a value. */
4
+ declare function DropdownMenu({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>): React.JSX.Element;
5
+ declare function DropdownMenuPortal({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>): React.JSX.Element;
6
+ declare function DropdownMenuTrigger({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>): React.JSX.Element;
7
+ declare function DropdownMenuContent({ className, align, sideOffset, collisionPadding, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Content>): React.JSX.Element;
8
+ declare function DropdownMenuGroup({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>): React.JSX.Element;
9
+ declare function DropdownMenuItem({ className, inset, variant, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
10
+ inset?: boolean;
11
+ variant?: "default" | "destructive";
12
+ }): React.JSX.Element;
13
+ declare function DropdownMenuCheckboxItem({ className, children, checked, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>): React.JSX.Element;
14
+ declare function DropdownMenuRadioGroup({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>): React.JSX.Element;
15
+ declare function DropdownMenuRadioItem({ className, children, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>): React.JSX.Element;
16
+ declare function DropdownMenuLabel({ className, inset, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
17
+ inset?: boolean;
18
+ }): React.JSX.Element;
19
+ declare function DropdownMenuSeparator({ className, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>): React.JSX.Element;
20
+ declare function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<"span">): React.JSX.Element;
21
+ declare function DropdownMenuSub({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>): React.JSX.Element;
22
+ declare function DropdownMenuSubTrigger({ className, inset, children, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
23
+ inset?: boolean;
24
+ }): React.JSX.Element;
25
+ declare function DropdownMenuSubContent({ className, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>): React.JSX.Element;
26
+ export { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, };
@@ -0,0 +1,56 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
5
+ import { CheckIcon, ChevronRightIcon } from "lucide-react";
6
+ import { cn } from "./utils.js";
7
+ /** Item highlight and selected chrome shared with `SelectItem`. */
8
+ const menuItemClassName = "relative flex cursor-default items-center gap-2.5 rounded-none px-3 py-2 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-9 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5";
9
+ const menuContentClassName = "z-50 min-w-40 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";
10
+ /** Action menu for commands (row actions, overflow menus). Use `Select` to pick a value. */
11
+ function DropdownMenu({ ...props }) {
12
+ return _jsx(DropdownMenuPrimitive.Root, { "data-slot": "dropdown-menu", ...props });
13
+ }
14
+ function DropdownMenuPortal({ ...props }) {
15
+ return (_jsx(DropdownMenuPrimitive.Portal, { "data-slot": "dropdown-menu-portal", ...props }));
16
+ }
17
+ function DropdownMenuTrigger({ ...props }) {
18
+ return (_jsx(DropdownMenuPrimitive.Trigger, { "data-slot": "dropdown-menu-trigger", ...props }));
19
+ }
20
+ function DropdownMenuContent({ className, align = "start", sideOffset = 4, collisionPadding = 8, ...props }) {
21
+ return (_jsx(DropdownMenuPrimitive.Portal, { children: _jsx(DropdownMenuPrimitive.Content, { "data-slot": "dropdown-menu-content", align: align, sideOffset: sideOffset, collisionPadding: collisionPadding, className: cn(menuContentClassName, "max-h-(--radix-dropdown-menu-content-available-height) origin-(--radix-dropdown-menu-content-transform-origin)", className), ...props }) }));
22
+ }
23
+ function DropdownMenuGroup({ ...props }) {
24
+ return (_jsx(DropdownMenuPrimitive.Group, { "data-slot": "dropdown-menu-group", ...props }));
25
+ }
26
+ function DropdownMenuItem({ className, inset, variant = "default", ...props }) {
27
+ return (_jsx(DropdownMenuPrimitive.Item, { "data-slot": "dropdown-menu-item", "data-inset": inset || undefined, "data-variant": variant, className: cn(menuItemClassName, "data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive", className), ...props }));
28
+ }
29
+ function DropdownMenuCheckboxItem({ className, children, checked, ...props }) {
30
+ return (_jsxs(DropdownMenuPrimitive.CheckboxItem, { "data-slot": "dropdown-menu-checkbox-item", className: cn(menuItemClassName, "pl-9", className), checked: checked, ...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(CheckIcon, {}) }) }), children] }));
31
+ }
32
+ function DropdownMenuRadioGroup({ ...props }) {
33
+ return (_jsx(DropdownMenuPrimitive.RadioGroup, { "data-slot": "dropdown-menu-radio-group", ...props }));
34
+ }
35
+ function DropdownMenuRadioItem({ className, children, ...props }) {
36
+ 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] }));
37
+ }
38
+ function DropdownMenuLabel({ className, inset, ...props }) {
39
+ 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 }));
40
+ }
41
+ function DropdownMenuSeparator({ className, ...props }) {
42
+ return (_jsx(DropdownMenuPrimitive.Separator, { "data-slot": "dropdown-menu-separator", className: cn("-mx-1 my-1 h-px bg-border", className), ...props }));
43
+ }
44
+ function DropdownMenuShortcut({ className, ...props }) {
45
+ return (_jsx("span", { "data-slot": "dropdown-menu-shortcut", className: cn("ml-auto text-xs tracking-widest text-muted-foreground", className), ...props }));
46
+ }
47
+ function DropdownMenuSub({ ...props }) {
48
+ return _jsx(DropdownMenuPrimitive.Sub, { "data-slot": "dropdown-menu-sub", ...props });
49
+ }
50
+ function DropdownMenuSubTrigger({ className, inset, children, ...props }) {
51
+ return (_jsxs(DropdownMenuPrimitive.SubTrigger, { "data-slot": "dropdown-menu-sub-trigger", "data-inset": inset || undefined, className: cn(menuItemClassName, "data-open:bg-accent data-open:text-accent-foreground", className), ...props, children: [children, _jsx(ChevronRightIcon, { className: "ml-auto" })] }));
52
+ }
53
+ function DropdownMenuSubContent({ className, ...props }) {
54
+ return (_jsx(DropdownMenuPrimitive.SubContent, { "data-slot": "dropdown-menu-sub-content", className: cn(menuContentClassName, "origin-(--radix-dropdown-menu-content-transform-origin)", className), ...props }));
55
+ }
56
+ export { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, };
@@ -29,6 +29,6 @@ export declare class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBo
29
29
  componentDidCatch(error: Error, info: ErrorInfo): void;
30
30
  retry: () => void;
31
31
  reload: () => void;
32
- render(): string | number | bigint | boolean | import("react").JSX.Element | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | null | undefined;
32
+ render(): string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | import("react").JSX.Element | null | undefined;
33
33
  }
34
34
  export {};
package/dist/field.js CHANGED
@@ -39,7 +39,7 @@ function FieldTitle({ className, ...props }) {
39
39
  return (_jsx("div", { "data-slot": "field-label", className: cn("flex w-fit items-center gap-2 text-xs font-semibold uppercase group-data-[disabled=true]/field:opacity-50 in-data-[slot=field-label]:font-semibold", className), ...props }));
40
40
  }
41
41
  function FieldDescription({ className, ...props }) {
42
- return (_jsx("p", { "data-slot": "field-description", className: cn("text-left text-sm leading-normal font-normal tracking-normal text-muted-foreground normal-case group-has-data-horizontal/field:text-balance [[data-variant=legend]+&]:-mt-1.5", "last:mt-0 nth-last-2:-mt-1", "[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary", className), ...props }));
42
+ return (_jsx("p", { "data-slot": "field-description", className: cn("text-left text-sm leading-normal font-normal tracking-normal text-muted-foreground normal-case group-has-data-horizontal/field:text-balance [[data-variant=legend]+&]:-mt-1.5", "last:mt-0 nth-last-2:-mt-1", "[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-selected-foreground", className), ...props }));
43
43
  }
44
44
  function FieldSeparator({ children, className, ...props }) {
45
45
  return (_jsxs("div", { "data-slot": "field-separator", "data-content": !!children, className: cn("relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2", className), ...props, children: [_jsx(Separator, { className: "absolute inset-0 top-1/2" }), children && (_jsx("span", { className: "relative mx-auto block w-fit bg-background px-2 text-muted-foreground", "data-slot": "field-separator-content", children: children }))] }));
@@ -0,0 +1,16 @@
1
+ import { type FieldMetaState } from "./field-error.js";
2
+ type Props = {
3
+ value: Date | null;
4
+ onValueChange: (date: Date | null) => void;
5
+ label: string;
6
+ meta?: FieldMetaState;
7
+ placeholder?: string;
8
+ disabled?: boolean;
9
+ /** Minute increment forwarded to the picker. */
10
+ timeStep?: number;
11
+ /** Forwarded to the date-time picker trigger. */
12
+ className?: string;
13
+ };
14
+ /** Date-time picker with built-in field error text and invalid styling. */
15
+ export declare function FormDateTimePickerField({ value, onValueChange, label, meta, placeholder, disabled, timeStep, className, }: Props): import("react").JSX.Element;
16
+ export {};
@@ -0,0 +1,12 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useId } from "react";
4
+ import { DateTimePickerButton } from "../date-time-picker.js";
5
+ import { Label } from "../label.js";
6
+ import { FieldError, getFieldErrorMessage, } from "./field-error.js";
7
+ /** Date-time picker with built-in field error text and invalid styling. */
8
+ export function FormDateTimePickerField({ value, onValueChange, label, meta, placeholder, disabled, timeStep, className, }) {
9
+ const errorText = meta ? getFieldErrorMessage(meta.errors[0]) : undefined;
10
+ const id = useId();
11
+ return (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: id, children: label }), _jsx(DateTimePickerButton, { id: id, value: value, onChange: onValueChange, placeholder: placeholder, disabled: disabled, timeStep: timeStep, className: className, "aria-invalid": errorText ? true : undefined }), _jsx(FieldError, { text: errorText })] }));
12
+ }
@@ -8,8 +8,14 @@ export type TextFieldApi = {
8
8
  handleChange: (value: string) => void;
9
9
  handleBlur: () => void;
10
10
  };
11
- export declare function FormTextField({ field, label, placeholder, description, autoFocus, multiline, rows, inputType, showError, autoComplete, required, minLength, }: {
11
+ export declare function FormTextField({ field, id, label, placeholder, description, autoFocus, multiline, rows, inputType, showError, autoComplete, required, minLength, }: {
12
12
  field: TextFieldApi;
13
+ /**
14
+ * Control id the label points at. Defaults to `field.name`; pass a unique
15
+ * id when two forms on one page share a field name (login and sign-up
16
+ * `email`), or the second label focuses the first input.
17
+ */
18
+ id?: string;
13
19
  label: string;
14
20
  placeholder?: string;
15
21
  /** Optional hint under the label (employer vs referrer, etc.). */
@@ -5,13 +5,13 @@ import { Label } from "../label.js";
5
5
  import { Textarea } from "../textarea.js";
6
6
  import { onInputChange } from "../utils.js";
7
7
  import { FieldError, getFieldErrorMessage, } from "./field-error.js";
8
- export function FormTextField({ field, label, placeholder, description, autoFocus, multiline, rows = 3, inputType = "text", showError = true, autoComplete, required, minLength, }) {
8
+ export function FormTextField({ field, id = field.name, label, placeholder, description, autoFocus, multiline, rows = 3, inputType = "text", showError = true, autoComplete, required, minLength, }) {
9
9
  const onChange = onInputChange(field.handleChange);
10
10
  const errorText = showError
11
11
  ? getFieldErrorMessage(field.state.meta.errors[0])
12
12
  : undefined;
13
13
  const controlProps = {
14
- id: field.name,
14
+ id,
15
15
  value: field.state.value,
16
16
  onChange,
17
17
  onBlur: field.handleBlur,
@@ -21,5 +21,5 @@ export function FormTextField({ field, label, placeholder, description, autoFocu
21
21
  minLength,
22
22
  "aria-invalid": errorText ? true : undefined,
23
23
  };
24
- return (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: field.name, children: label }), 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 })), showError ? _jsx(FieldError, { text: errorText }) : null] }));
24
+ return (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: id, children: label }), 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 })), showError ? _jsx(FieldError, { text: errorText }) : null] }));
25
25
  }
@@ -0,0 +1,18 @@
1
+ import * as React from "react";
2
+ /**
3
+ * Page links for long lists and tables. Links are plain anchors so the page
4
+ * lives in the URL; pass `onClick` with `href` for client-side routing.
5
+ */
6
+ declare function Pagination({ className, ...props }: React.ComponentProps<"nav">): React.JSX.Element;
7
+ declare function PaginationContent({ className, ...props }: React.ComponentProps<"ul">): React.JSX.Element;
8
+ declare function PaginationItem({ ...props }: React.ComponentProps<"li">): React.JSX.Element;
9
+ type PaginationLinkProps = {
10
+ isActive?: boolean;
11
+ size?: "default" | "icon";
12
+ } & React.ComponentProps<"a">;
13
+ /** The current page gets `aria-current="page"` and the selected wash. */
14
+ declare function PaginationLink({ className, isActive, size, ...props }: PaginationLinkProps): React.JSX.Element;
15
+ declare function PaginationPrevious({ className, children, ...props }: React.ComponentProps<typeof PaginationLink>): React.JSX.Element;
16
+ declare function PaginationNext({ className, children, ...props }: React.ComponentProps<typeof PaginationLink>): React.JSX.Element;
17
+ declare function PaginationEllipsis({ className, ...props }: React.ComponentProps<"span">): React.JSX.Element;
18
+ export { Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, };
@@ -0,0 +1,35 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as React from "react";
3
+ import { ChevronLeftIcon, ChevronRightIcon, MoreHorizontalIcon, } from "lucide-react";
4
+ import { buttonVariants } from "./button.js";
5
+ import { cn } from "./utils.js";
6
+ /**
7
+ * Page links for long lists and tables. Links are plain anchors so the page
8
+ * lives in the URL; pass `onClick` with `href` for client-side routing.
9
+ */
10
+ function Pagination({ className, ...props }) {
11
+ return (_jsx("nav", { "aria-label": "Pagination", "data-slot": "pagination", className: cn("mx-auto flex w-full justify-center", className), ...props }));
12
+ }
13
+ function PaginationContent({ className, ...props }) {
14
+ return (_jsx("ul", { "data-slot": "pagination-content", className: cn("flex flex-row items-center gap-1", className), ...props }));
15
+ }
16
+ function PaginationItem({ ...props }) {
17
+ return _jsx("li", { "data-slot": "pagination-item", ...props });
18
+ }
19
+ /** The current page gets `aria-current="page"` and the selected wash. */
20
+ function PaginationLink({ className, isActive, size = "icon", ...props }) {
21
+ return (_jsx("a", { "aria-current": isActive ? "page" : undefined, "data-slot": "pagination-link", "data-active": isActive, className: cn(buttonVariants({
22
+ variant: isActive ? "outline" : "ghost",
23
+ size: size === "icon" ? "icon-sm" : "sm",
24
+ }), "font-medium tracking-normal normal-case tabular-nums data-[active=true]:border-primary data-[active=true]:bg-selected data-[active=true]:text-selected-foreground", className), ...props }));
25
+ }
26
+ function PaginationPrevious({ className, children = "Previous", ...props }) {
27
+ return (_jsxs(PaginationLink, { "aria-label": "Go to previous page", size: "default", className: cn("gap-1 px-2.5 sm:pl-2", className), ...props, children: [_jsx(ChevronLeftIcon, { "data-icon": "inline-start" }), _jsx("span", { className: "hidden sm:block", children: children })] }));
28
+ }
29
+ function PaginationNext({ className, children = "Next", ...props }) {
30
+ return (_jsxs(PaginationLink, { "aria-label": "Go to next page", size: "default", className: cn("gap-1 px-2.5 sm:pr-2", className), ...props, children: [_jsx("span", { className: "hidden sm:block", children: children }), _jsx(ChevronRightIcon, { "data-icon": "inline-end" })] }));
31
+ }
32
+ function PaginationEllipsis({ className, ...props }) {
33
+ return (_jsxs("span", { "aria-hidden": true, "data-slot": "pagination-ellipsis", className: cn("flex size-9 items-center justify-center text-muted-foreground", className), ...props, children: [_jsx(MoreHorizontalIcon, { className: "size-4" }), _jsx("span", { className: "sr-only", children: "More pages" })] }));
34
+ }
35
+ export { Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, };