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 +61 -33
- package/DESIGN_RULES.md +5 -4
- package/README.md +3 -3
- package/dist/combobox.d.ts +2 -0
- package/dist/combobox.js +38 -7
- package/dist/date-picker.d.ts +31 -7
- package/dist/date-picker.js +47 -7
- package/package.json +1 -1
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
|
|
41
|
-
`SortableTableHead` are first-class kit
|
|
42
|
-
shells stay with the application: currency
|
|
43
|
-
error views, markdown editors, kanban
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
package/dist/combobox.d.ts
CHANGED
|
@@ -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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
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:
|
|
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
|
}
|
package/dist/date-picker.d.ts
CHANGED
|
@@ -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
|
|
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.
|
|
22
|
-
* `Date | undefined`.
|
|
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(
|
|
25
|
-
export {};
|
|
49
|
+
export declare function DatePickerButton(props: DatePickerButtonProps): React.JSX.Element;
|
package/dist/date-picker.js
CHANGED
|
@@ -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
|
-
*
|
|
48
|
-
*
|
|
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
|
-
|
|
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 (
|
|
54
|
-
|
|
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
|
}
|