atlas-react-kit 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/COMPONENTS.md CHANGED
@@ -5,39 +5,39 @@ underlying element or Radix props, including `className`, alongside their option
5
5
  Form helpers and `TableBodySkeleton` accept only their documented props. There is
6
6
  no root component barrel.
7
7
 
8
- | Subpath | Main exports and options |
9
- | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
10
- | `button` | `Button`, `buttonVariants`; `variant`, `size`, `asChild` |
11
- | `badge` | `Badge`, `badgeVariants`; `variant`, `tone`, `asChild` |
12
- | `calendar` | `Calendar`, `CalendarDayButton`; React Day Picker props and `buttonVariant` |
13
- | `card` | `Card`, `CardHeader`, `CardTitle`, `CardDescription`, `CardAction`, `CardContent`, `CardFooter`; `Card` supports `flush` |
14
- | `checkbox` | `Checkbox` |
15
- | `combobox` | `Combobox`; `options` as `{ value, label }[]`, `value`, `onValueChange`; optional placeholder, search, empty text |
16
- | `date-picker` | `DatePickerButton`, `toDate`, `formatLocalDate`; `value` is `Date` or `yyyy-MM-dd` |
17
- | `dialog` | `Dialog`, trigger, portal, overlay, content, close, header, footer, title, description; `DialogContent` supports `showCloseButton` |
18
- | `empty-panel` | `EmptyPanel`; required `children`; optional `size`, `muted`, `className` |
19
- | `error-boundary` | `ErrorBoundary`, `ErrorFallback`; optional `message`; retry resets, reload refreshes the page |
20
- | `field` | Field, label, legend, description, group, set, title, content, separator, error; `Field` supports `orientation` |
21
- | `input`, `textarea`, `label` | `Input`, `Textarea`, `Label` |
22
- | `popover` | `Popover`, trigger, anchor, content, header, title, description |
23
- | `select` | Select, trigger, value, content, group, label, item, separator, scroll buttons; trigger `size` |
24
- | `separator`, `skeleton` | `Separator`, `Skeleton` |
25
- | `sortable-table-head` | `SortableTableHead`; `sorted`, `direction` (`asc` \| `desc`), `onClick`; sets `aria-sort` |
26
- | `sonner` | `Toaster`; Sonner props, with `next-themes` theme defaults |
27
- | `table` | Table, header, body, footer, row, head, cell, caption |
28
- | `table-body-skeleton` | `TableBodySkeleton`; `columns`, optional `rows` (default 5) |
29
- | `tabs` | `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent`, `TabsNav`, `tabsListVariants`; list `variant` is `default` or `line` |
30
- | `toggle` | `Toggle`, `toggleVariants`; `variant`, `size` |
31
- | `toggle-group` | `ToggleGroup`, `ToggleGroupItem`; `variant`, `size` |
32
- | `theme-provider` | `ThemeProvider`, `useTheme`; optional `storageKey` (default `atlas-theme`), `enableShortcut` for the `d` toggle |
33
- | `form/text-field` | `FormTextField`, type `TextFieldApi`; controlled field, label, multiline, description, input constraints |
34
- | `form/select-field` | `FormSelectField`; `value`, `onValueChange`, `label`, optional `meta` and placeholder; select item children |
35
- | `form/date-picker-field` | `FormDatePickerField`; `value`, `onValueChange`, `label`, optional `meta` and placeholder |
36
- | `form/field-error` | `FieldError`, `getFieldErrorMessage`; types `FieldErrorMessage`, `FieldMetaState` |
37
- | `form/feedback-field` | `FormFeedbackField`; nullable `message` |
38
- | `next/tabs-nav-link` | `TabsNavLink`; Next Link props and required `active` boolean; requires Next.js 16 |
39
- | `utils` | `cn`, `onInputChange` |
40
- | `theme.css` | Tailwind 4 theme; installation in [README](./README.md) |
8
+ | Subpath | Main exports and options |
9
+ | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
10
+ | `button` | `Button`, `buttonVariants`; `variant`, `size`, `asChild` |
11
+ | `badge` | `Badge`, `badgeVariants`; `variant`, `tone`, `asChild` |
12
+ | `calendar` | `Calendar`, `CalendarDayButton`; React Day Picker props and `buttonVariant` |
13
+ | `card` | `Card`, `CardHeader`, `CardTitle`, `CardDescription`, `CardAction`, `CardContent`, `CardFooter`; `Card` supports `flush` |
14
+ | `checkbox` | `Checkbox` |
15
+ | `combobox` | `Combobox`; `options` as `{ value, label }[]`, `value`, `onValueChange`; optional placeholder, search, empty text; keyboard highlight scrolls into view |
16
+ | `date-picker` | `DatePickerButton`, `toDate`, `formatLocalDate`, `DateRange`; `mode` `"single"` (default) or `"range"`; single `value` is `Date` or `yyyy-MM-dd`; range `value` is `{ from, to }`; optional `numberOfMonths` (range default 2) |
17
+ | `dialog` | `Dialog`, trigger, portal, overlay, content, close, header, footer, title, description; `DialogContent` supports `showCloseButton` |
18
+ | `empty-panel` | `EmptyPanel`; required `children`; optional `size`, `muted`, `className` |
19
+ | `error-boundary` | `ErrorBoundary`, `ErrorFallback`; optional `message`; retry resets, reload refreshes the page |
20
+ | `field` | Field, label, legend, description, group, set, title, content, separator, error; `Field` supports `orientation` |
21
+ | `input`, `textarea`, `label` | `Input`, `Textarea`, `Label` |
22
+ | `popover` | `Popover`, trigger, anchor, content, header, title, description |
23
+ | `select` | Select, trigger, value, content, group, label, item, separator, scroll buttons; trigger `size` |
24
+ | `separator`, `skeleton` | `Separator`, `Skeleton` |
25
+ | `sortable-table-head` | `SortableTableHead`; `sorted`, `direction` (`asc` \| `desc`), `onClick`; sets `aria-sort` |
26
+ | `sonner` | `Toaster`; Sonner props, with `next-themes` theme defaults |
27
+ | `table` | Table, header, body, footer, row, head, cell, caption |
28
+ | `table-body-skeleton` | `TableBodySkeleton`; `columns`, optional `rows` (default 5) |
29
+ | `tabs` | `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent`, `TabsNav`, `tabsListVariants`; list `variant` is `default` or `line` |
30
+ | `toggle` | `Toggle`, `toggleVariants`; `variant`, `size` |
31
+ | `toggle-group` | `ToggleGroup`, `ToggleGroupItem`; `variant`, `size` |
32
+ | `theme-provider` | `ThemeProvider`, `useTheme`; optional `storageKey` (default `atlas-theme`), `enableShortcut` for the `d` toggle |
33
+ | `form/text-field` | `FormTextField`, type `TextFieldApi`; controlled field, label, multiline, description, input constraints |
34
+ | `form/select-field` | `FormSelectField`; `value`, `onValueChange`, `label`, optional `meta` and placeholder; select item children |
35
+ | `form/date-picker-field` | `FormDatePickerField`; `value`, `onValueChange`, `label`, optional `meta` and placeholder |
36
+ | `form/field-error` | `FieldError`, `getFieldErrorMessage`; types `FieldErrorMessage`, `FieldMetaState` |
37
+ | `form/feedback-field` | `FormFeedbackField`; nullable `message` |
38
+ | `next/tabs-nav-link` | `TabsNavLink`; Next Link props and required `active` boolean; requires Next.js 16 |
39
+ | `utils` | `cn`, `onInputChange` |
40
+ | `theme.css` | Tailwind 4 theme; installation in [README](./README.md) |
41
41
 
42
42
  ## Forms
43
43
 
@@ -62,6 +62,34 @@ accept the same error meta shape. `FormDatePickerField` takes a `Date` or
62
62
  `yyyy-MM-dd` string and calls `onValueChange` with `Date | undefined`.
63
63
  `FieldError` accepts either `message` or pre-resolved `text`.
64
64
 
65
+ `DatePickerButton` is single-day by default. For a from/to range, set
66
+ `mode="range"`; `value` is `{ from, to }` (each side `Date` or `yyyy-MM-dd`)
67
+ and `onChange` receives `DateRange | undefined`. The popover shows two months
68
+ unless `numberOfMonths` is set, stays `w-auto` so it does not collapse to the
69
+ default popover width, and includes a Clear action.
70
+
71
+ ```tsx
72
+ import { DatePickerButton, type DateRange } from "atlas-react-kit/date-picker"
73
+
74
+ export function ExportRange({
75
+ value,
76
+ onChange,
77
+ }: {
78
+ value: DateRange | undefined
79
+ onChange: (range: DateRange | undefined) => void
80
+ }) {
81
+ return (
82
+ <DatePickerButton
83
+ mode="range"
84
+ value={value}
85
+ onChange={onChange}
86
+ numberOfMonths={2}
87
+ placeholder="Export range"
88
+ />
89
+ )
90
+ }
91
+ ```
92
+
65
93
  ## Theme
66
94
 
67
95
  ```tsx
package/DESIGN_RULES.md CHANGED
@@ -37,7 +37,8 @@ Next adapter provides `TabsNavLink` with this behavior.
37
37
 
38
38
  Use `FormTextField`, `FormSelectField`, `FormDatePickerField`, and
39
39
  `FormFeedbackField` for portable form controls, and `FieldError` for field
40
- messages. `Combobox`, `DatePickerButton`, `ThemeProvider`, `ErrorBoundary`, and
41
- `SortableTableHead` are first-class kit pieces. Domain data and application
42
- shells stay with the application: currency or timezone option lists, query-library
43
- error views, markdown editors, kanban boards, and dashboard widgets.
40
+ messages. `Combobox`, `DatePickerButton` (including `mode="range"`),
41
+ `ThemeProvider`, `ErrorBoundary`, and `SortableTableHead` are first-class kit
42
+ pieces. Domain data and application shells stay with the application: currency
43
+ or timezone option lists, query-library error views, markdown editors, kanban
44
+ boards, and dashboard widgets.
package/README.md CHANGED
@@ -4,7 +4,7 @@ Shared React components and a Tailwind theme with square corners, semantic
4
4
  colors, and distinct selected states. React 19 and Tailwind 4 are required.
5
5
 
6
6
  ```sh
7
- npm install --save-exact atlas-react-kit@0.2.1
7
+ npm install --save-exact atlas-react-kit@0.3.0
8
8
  ```
9
9
 
10
10
  Import components by subpath:
@@ -59,7 +59,7 @@ without compilation and processed by the consuming application's Tailwind build.
59
59
  The optional Next.js 16 adapter requires Next only when imported. The package
60
60
  does not bundle React.
61
61
 
62
- 0.1.0 and 0.2.0 are already on npm. Bumping the version in `package.json` and
62
+ 0.2.1 is already on npm. Bumping the version in `package.json` and
63
63
  merging to `main` (path-filtered to package files) or running
64
64
  `workflow_dispatch` publishes through npm OIDC trusted publishing after
65
65
  check, test, and pack gates. Versions already on npm are skipped.
@@ -70,7 +70,7 @@ artifact with the package owner's npm login:
70
70
  ```sh
71
71
  mkdir -p release-artifacts
72
72
  npm pack --pack-destination release-artifacts
73
- npm publish ./release-artifacts/atlas-react-kit-0.2.1.tgz --access public
73
+ npm publish ./release-artifacts/atlas-react-kit-0.3.0.tgz --access public
74
74
  ```
75
75
 
76
76
  Pin an exact package version in consuming applications. Roll back an upgrade by
@@ -18,6 +18,8 @@ type ComboboxProps = {
18
18
  /**
19
19
  * Searchable single-select built from Button, Popover, and Input.
20
20
  * Options are a plain `{ value, label }` list; domain data stays in the app.
21
+ * Keyboard highlight (arrows, Home/End, PageUp/PageDown) scrolls the active
22
+ * option into view inside the overflow list.
21
23
  */
22
24
  export declare function Combobox({ options, value, onValueChange, placeholder, searchPlaceholder, emptyText, disabled, id, className, "aria-invalid": ariaInvalid, }: ComboboxProps): React.JSX.Element;
23
25
  export {};
package/dist/combobox.js CHANGED
@@ -6,9 +6,17 @@ import { Button } from "./button.js";
6
6
  import { Input } from "./input.js";
7
7
  import { Popover, PopoverContent, PopoverTrigger } from "./popover.js";
8
8
  import { cn, onInputChange } from "./utils.js";
9
+ /**
10
+ * PageUp/PageDown jump size. The option list is `max-h-60`; ten rows is a
11
+ * bit more than one viewport so long lists (timezones, currencies) move
12
+ * without requiring an End key.
13
+ */
14
+ const LISTBOX_PAGE_SIZE = 10;
9
15
  /**
10
16
  * Searchable single-select built from Button, Popover, and Input.
11
17
  * Options are a plain `{ value, label }` list; domain data stays in the app.
18
+ * Keyboard highlight (arrows, Home/End, PageUp/PageDown) scrolls the active
19
+ * option into view inside the overflow list.
12
20
  */
13
21
  export function Combobox({ options, value, onValueChange, placeholder = "Select", searchPlaceholder = "Search", emptyText = "No results", disabled, id, className, "aria-invalid": ariaInvalid, }) {
14
22
  const [open, setOpen] = React.useState(false);
@@ -23,18 +31,30 @@ export function Combobox({ options, value, onValueChange, placeholder = "Select"
23
31
  return options;
24
32
  return options.filter((option) => option.label.toLowerCase().includes(needle));
25
33
  }, [options, query]);
26
- // Highlight and search reset when the popover opens or closes; options/value
27
- // are read for the initial highlight rather than subscribed as deps.
28
- React.useEffect(() => {
29
- if (!open) {
34
+ const onOpenChange = (next) => {
35
+ setOpen(next);
36
+ if (!next) {
30
37
  setQuery("");
31
38
  return;
32
39
  }
40
+ // Set the highlight in the same turn as open so the first painted list
41
+ // already points at the selected option (a timezone at the end of 400
42
+ // rows) instead of flashing index 0 and scrolling twice.
33
43
  const selectedIndex = options.findIndex((option) => option.value === value);
34
44
  setHighlight(selectedIndex >= 0 ? selectedIndex : 0);
45
+ };
46
+ React.useEffect(() => {
47
+ if (!open)
48
+ return;
35
49
  const frame = window.requestAnimationFrame(() => inputRef.current?.focus());
36
50
  return () => window.cancelAnimationFrame(frame);
37
51
  }, [open]);
52
+ // Callback ref (not an effect) so scroll runs when Radix mounts the list
53
+ // and when the highlight moves. A layout effect on `open` is too early:
54
+ // the option node does not exist until the popover content is in the DOM.
55
+ const scrollHighlightedOption = React.useCallback((node) => {
56
+ node?.scrollIntoView({ block: "nearest" });
57
+ }, []);
38
58
  const onQueryChange = onInputChange((next) => {
39
59
  setQuery(next);
40
60
  setHighlight(0);
@@ -59,7 +79,8 @@ export function Combobox({ options, value, onValueChange, placeholder = "Select"
59
79
  if (option)
60
80
  selectValue(option.value);
61
81
  }
62
- // Home/End jump the highlight; preventDefault so the search caret stays put.
82
+ // Home/End/Page jump the highlight; preventDefault so the search caret
83
+ // stays put and the page behind the list does not scroll.
63
84
  if (event.key === "Home") {
64
85
  event.preventDefault();
65
86
  setHighlight(0);
@@ -68,12 +89,22 @@ export function Combobox({ options, value, onValueChange, placeholder = "Select"
68
89
  event.preventDefault();
69
90
  setHighlight(filtered.length === 0 ? 0 : filtered.length - 1);
70
91
  }
92
+ if (event.key === "PageDown") {
93
+ event.preventDefault();
94
+ setHighlight((index) => filtered.length === 0
95
+ ? 0
96
+ : Math.min(index + LISTBOX_PAGE_SIZE, filtered.length - 1));
97
+ }
98
+ if (event.key === "PageUp") {
99
+ event.preventDefault();
100
+ setHighlight((index) => Math.max(index - LISTBOX_PAGE_SIZE, 0));
101
+ }
71
102
  };
72
- return (_jsxs(Popover, { open: open, onOpenChange: setOpen, 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("h-10 w-full justify-between font-normal tracking-normal normal-case", !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
103
+ 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("h-10 w-full justify-between font-normal tracking-normal normal-case", !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
73
104
  ? `${listId}-option-${activeOption.value}`
74
105
  : 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) => {
75
106
  const isSelected = option.value === value;
76
107
  const isActive = index === highlight;
77
- return (_jsx("li", { role: "none", children: _jsxs("button", { 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));
108
+ 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));
78
109
  })) })] })] }));
79
110
  }
@@ -1,5 +1,12 @@
1
1
  import * as React from "react";
2
+ import type { DateRange } from "react-day-picker";
3
+ export type { DateRange };
2
4
  export type DateLike = Date | string | number | null | undefined;
5
+ /** From/to value for `DatePickerButton` in range mode; each side is `DateLike`. */
6
+ export type DatePickerRangeValue = {
7
+ from?: DateLike;
8
+ to?: DateLike;
9
+ };
3
10
  /**
4
11
  * Coerce a date-like value to a local `Date`.
5
12
  * Plain calendar dates (`yyyy-MM-dd`) parse as local midnight so they do not
@@ -8,18 +15,35 @@ export type DateLike = Date | string | number | null | undefined;
8
15
  export declare function toDate(value: DateLike): Date | undefined;
9
16
  /** Format a Date as a timezone-safe calendar date (`yyyy-MM-dd`). */
10
17
  export declare function formatLocalDate(date: Date): string;
11
- type DatePickerButtonProps = {
18
+ type DatePickerSharedProps = {
12
19
  id?: string;
13
- value?: DateLike;
14
- onChange: (date: Date | undefined) => void;
15
20
  placeholder?: string;
16
21
  disabled?: boolean;
17
22
  className?: string;
18
23
  "aria-invalid"?: boolean;
24
+ /**
25
+ * Visible months in the popover calendar. Defaults to 1 in single mode
26
+ * and 2 in range mode.
27
+ */
28
+ numberOfMonths?: number;
29
+ /** Month shown when the popover opens; falls back to the selected date. */
30
+ defaultMonth?: Date;
31
+ };
32
+ export type DatePickerSingleProps = DatePickerSharedProps & {
33
+ mode?: "single";
34
+ value?: DateLike;
35
+ onChange: (date: Date | undefined) => void;
36
+ };
37
+ export type DatePickerRangeProps = DatePickerSharedProps & {
38
+ mode: "range";
39
+ value?: DatePickerRangeValue;
40
+ onChange: (range: DateRange | undefined) => void;
19
41
  };
42
+ export type DatePickerButtonProps = DatePickerSingleProps | DatePickerRangeProps;
20
43
  /**
21
- * Calendar popover trigger. Accepts `Date` or `yyyy-MM-dd` strings; emits
22
- * `Date | undefined`. Square corners follow the shared shape rules.
44
+ * Calendar popover trigger. `mode="single"` (default) accepts `Date` or
45
+ * `yyyy-MM-dd` and emits `Date | undefined`. `mode="range"` accepts
46
+ * `{ from, to }` and emits `DateRange | undefined`. Square corners follow
47
+ * the shared shape rules.
23
48
  */
24
- export declare function DatePickerButton({ id, value, onChange, placeholder, disabled, className, "aria-invalid": ariaInvalid, }: DatePickerButtonProps): React.JSX.Element;
25
- export {};
49
+ export declare function DatePickerButton(props: DatePickerButtonProps): React.JSX.Element;
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
- import { format, parseISO } from "date-fns";
4
+ import { format, isSameDay, parseISO } from "date-fns";
5
5
  import { CalendarIcon } from "lucide-react";
6
6
  import { Button } from "./button.js";
7
7
  import { Calendar } from "./calendar.js";
@@ -44,14 +44,54 @@ export function formatLocalDate(date) {
44
44
  return format(date, "yyyy-MM-dd");
45
45
  }
46
46
  /**
47
- * Calendar popover trigger. Accepts `Date` or `yyyy-MM-dd` strings; emits
48
- * `Date | undefined`. Square corners follow the shared shape rules.
47
+ * Trigger label for a range: one PPP date while the end is missing or the
48
+ * same day as the start, otherwise `from – to`.
49
49
  */
50
- export function DatePickerButton({ id, value, onChange, placeholder = "Pick a date", disabled, className, "aria-invalid": ariaInvalid, }) {
50
+ function formatRangeTriggerLabel(from, to, placeholder) {
51
+ if (!from)
52
+ return placeholder;
53
+ if (!to || isSameDay(from, to))
54
+ return format(from, "PPP");
55
+ return `${format(from, "PPP")} – ${format(to, "PPP")}`;
56
+ }
57
+ /** Shared trigger + auto-sized popover chrome for single and range modes. */
58
+ function DatePickerShell({ id, disabled, ariaInvalid, className, empty, label, open, onOpenChange, children, }) {
59
+ 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("h-10 w-full justify-start font-normal tracking-normal normal-case data-[empty=true]:text-muted-foreground", className), children: [_jsx(CalendarIcon, { "data-icon": "inline-start" }), label] }) }), _jsx(PopoverContent, { align: "start", className: "w-auto rounded-none p-0", children: children })] }));
60
+ }
61
+ function DatePickerSingleButton({ id, value, onChange, placeholder = "Pick a date", disabled, className, "aria-invalid": ariaInvalid, numberOfMonths, defaultMonth, }) {
51
62
  const [open, setOpen] = React.useState(false);
52
63
  const date = toDate(value);
53
- 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": !date, className: cn("h-10 w-full justify-start font-normal tracking-normal normal-case data-[empty=true]:text-muted-foreground", className), children: [_jsx(CalendarIcon, { "data-icon": "inline-start" }), date ? format(date, "PPP") : placeholder] }) }), _jsx(PopoverContent, { align: "start", className: "w-auto rounded-none p-0", children: _jsx(Calendar, { mode: "single", selected: date, defaultMonth: date, onSelect: (next) => {
54
- onChange(next);
64
+ return (_jsx(DatePickerShell, { id: id, disabled: disabled, ariaInvalid: ariaInvalid, className: className, empty: !date, label: date ? format(date, "PPP") : placeholder, open: open, onOpenChange: setOpen, children: _jsx(Calendar, { mode: "single", selected: date, defaultMonth: defaultMonth ?? date, numberOfMonths: numberOfMonths, onSelect: (next) => {
65
+ onChange(next);
66
+ setOpen(false);
67
+ } }) }));
68
+ }
69
+ function DatePickerRangeButton({ id, value, onChange, placeholder = "Pick a date range", disabled, className, "aria-invalid": ariaInvalid, numberOfMonths = 2, defaultMonth, }) {
70
+ const [open, setOpen] = React.useState(false);
71
+ const from = toDate(value?.from);
72
+ const to = toDate(value?.to);
73
+ const selected = from || to ? { from, to } : undefined;
74
+ return (_jsxs(DatePickerShell, { id: id, disabled: disabled, ariaInvalid: ariaInvalid, className: className, empty: !from, label: formatRangeTriggerLabel(from, to, placeholder), open: open, onOpenChange: setOpen, children: [_jsx(Calendar, { mode: "range", selected: selected, defaultMonth: defaultMonth ?? from ?? to, numberOfMonths: numberOfMonths, onSelect: (next) => {
75
+ onChange(next);
76
+ // First click is a same-day range; keep the popover open so the
77
+ // user can set `to`. Close once the ends differ.
78
+ if (next?.from && next.to && !isSameDay(next.from, next.to)) {
79
+ setOpen(false);
80
+ }
81
+ } }), _jsx("div", { className: "flex justify-end border-t p-2", children: _jsx(Button, { type: "button", variant: "ghost", size: "sm", disabled: !from && !to, onClick: () => {
82
+ onChange(undefined);
55
83
  setOpen(false);
56
- } }) })] }));
84
+ }, children: "Clear" }) })] }));
85
+ }
86
+ /**
87
+ * Calendar popover trigger. `mode="single"` (default) accepts `Date` or
88
+ * `yyyy-MM-dd` and emits `Date | undefined`. `mode="range"` accepts
89
+ * `{ from, to }` and emits `DateRange | undefined`. Square corners follow
90
+ * the shared shape rules.
91
+ */
92
+ export function DatePickerButton(props) {
93
+ if (props.mode === "range") {
94
+ return _jsx(DatePickerRangeButton, { ...props });
95
+ }
96
+ return _jsx(DatePickerSingleButton, { ...props });
57
97
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atlas-react-kit",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Shared React components and Tailwind theme for Atlas",
5
5
  "type": "module",
6
6
  "license": "MIT",