@shadow-js/ui 0.0.0-stage → 1.0.1
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/CHANGELOG.md +30 -0
- package/LICENSE +21 -0
- package/README.md +598 -2
- package/dist/cli.mjs +2281 -0
- package/dist/dev/cli/schema.d.ts +71 -0
- package/dist/dev/index.d.ts +6 -0
- package/dist/dev/index.js +5 -0
- package/dist/dev/locales/ar.d.ts +139 -0
- package/dist/dev/locales/ar.js +127 -0
- package/dist/dev/locales/ar.js.map +1 -0
- package/dist/dev/locales/en.d.ts +139 -0
- package/dist/dev/locales/en.js +127 -0
- package/dist/dev/locales/en.js.map +1 -0
- package/dist/dev/registry/utils/utils.d.ts +27 -0
- package/dist/dev/registry/utils/utils.js +26 -0
- package/dist/dev/registry/utils/utils.js.map +1 -0
- package/dist/dev/theme/theme-logic.d.ts +185 -0
- package/dist/dev/theme/theme-logic.js +183 -0
- package/dist/dev/theme/theme-logic.js.map +1 -0
- package/dist/dev/theme/theme-provider.d.ts +88 -0
- package/dist/dev/theme/theme-provider.js +149 -0
- package/dist/dev/theme/theme-provider.js.map +1 -0
- package/dist/prod/index.js +1 -0
- package/dist/prod/locales/ar.js +2 -0
- package/dist/prod/locales/ar.js.map +1 -0
- package/dist/prod/locales/en.js +2 -0
- package/dist/prod/locales/en.js.map +1 -0
- package/dist/prod/registry/utils/utils.js +2 -0
- package/dist/prod/registry/utils/utils.js.map +1 -0
- package/dist/prod/theme/theme-logic.js +2 -0
- package/dist/prod/theme/theme-logic.js.map +1 -0
- package/dist/prod/theme/theme-provider.js +2 -0
- package/dist/prod/theme/theme-provider.js.map +1 -0
- package/package.json +102 -4
- package/registry/accordion/accordion.tsx +210 -0
- package/registry/accordion/item.json +4 -0
- package/registry/alert/alert.tsx +200 -0
- package/registry/alert/item.json +4 -0
- package/registry/alert-dialog/alert-dialog.tsx +530 -0
- package/registry/alert-dialog/item.json +4 -0
- package/registry/aspect-ratio/aspect-ratio.tsx +71 -0
- package/registry/aspect-ratio/item.json +4 -0
- package/registry/attachment/attachment.tsx +459 -0
- package/registry/attachment/item.json +4 -0
- package/registry/avatar/avatar.tsx +385 -0
- package/registry/avatar/item.json +4 -0
- package/registry/badge/badge.tsx +108 -0
- package/registry/badge/item.json +4 -0
- package/registry/breadcrumb/breadcrumb.tsx +354 -0
- package/registry/breadcrumb/item.json +4 -0
- package/registry/bubble/bubble.tsx +260 -0
- package/registry/bubble/item.json +4 -0
- package/registry/button/button.tsx +125 -0
- package/registry/button/item.json +4 -0
- package/registry/button-group/button-group.tsx +189 -0
- package/registry/button-group/item.json +4 -0
- package/registry/calendar/calendar.tsx +348 -0
- package/registry/calendar/item.json +4 -0
- package/registry/card/card.tsx +311 -0
- package/registry/card/item.json +4 -0
- package/registry/carousel/carousel.tsx +851 -0
- package/registry/carousel/item.json +4 -0
- package/registry/chart/chart.tsx +1956 -0
- package/registry/chart/item.json +4 -0
- package/registry/chart-scale/chart-scale.ts +670 -0
- package/registry/chart-scale/item.json +5 -0
- package/registry/checkbox/checkbox.tsx +79 -0
- package/registry/checkbox/item.json +4 -0
- package/registry/code-block/code-block.tsx +470 -0
- package/registry/code-block/item.json +4 -0
- package/registry/collapsible/collapsible.tsx +117 -0
- package/registry/collapsible/item.json +4 -0
- package/registry/combobox/combobox.tsx +977 -0
- package/registry/combobox/item.json +4 -0
- package/registry/command/command.tsx +757 -0
- package/registry/command/item.json +4 -0
- package/registry/context-menu/context-menu.tsx +697 -0
- package/registry/context-menu/item.json +4 -0
- package/registry/data-table/data-table.tsx +437 -0
- package/registry/data-table/item.json +4 -0
- package/registry/date-picker/date-picker.tsx +349 -0
- package/registry/date-picker/item.json +4 -0
- package/registry/dialog/dialog.tsx +412 -0
- package/registry/dialog/item.json +4 -0
- package/registry/direction/direction.tsx +88 -0
- package/registry/direction/item.json +4 -0
- package/registry/drawer/drawer.tsx +787 -0
- package/registry/drawer/item.json +4 -0
- package/registry/dropdown-menu/dropdown-menu.tsx +692 -0
- package/registry/dropdown-menu/item.json +4 -0
- package/registry/empty/empty.tsx +240 -0
- package/registry/empty/item.json +4 -0
- package/registry/field/field.tsx +624 -0
- package/registry/field/item.json +4 -0
- package/registry/form/form.tsx +1112 -0
- package/registry/form/item.json +4 -0
- package/registry/hover-card/hover-card.tsx +144 -0
- package/registry/hover-card/item.json +4 -0
- package/registry/icons/icons.tsx +1691 -0
- package/registry/icons/item.json +4 -0
- package/registry/input/input.tsx +43 -0
- package/registry/input/item.json +4 -0
- package/registry/input-group/input-group.tsx +315 -0
- package/registry/input-group/item.json +4 -0
- package/registry/input-otp/input-otp.tsx +329 -0
- package/registry/input-otp/item.json +4 -0
- package/registry/item/item.json +4 -0
- package/registry/item/item.tsx +439 -0
- package/registry/kbd/item.json +4 -0
- package/registry/kbd/kbd.tsx +65 -0
- package/registry/label/item.json +4 -0
- package/registry/label/label.tsx +60 -0
- package/registry/marker/item.json +4 -0
- package/registry/marker/marker.tsx +175 -0
- package/registry/menubar/item.json +4 -0
- package/registry/menubar/menubar.tsx +694 -0
- package/registry/message/item.json +4 -0
- package/registry/message/message.tsx +280 -0
- package/registry/message-scroller/item.json +4 -0
- package/registry/message-scroller/message-scroller.tsx +262 -0
- package/registry/native-select/item.json +4 -0
- package/registry/native-select/native-select.tsx +113 -0
- package/registry/navigation-menu/item.json +4 -0
- package/registry/navigation-menu/navigation-menu.tsx +362 -0
- package/registry/pagination/item.json +4 -0
- package/registry/pagination/pagination.tsx +460 -0
- package/registry/password-input/item.json +5 -0
- package/registry/password-input/password-input.tsx +494 -0
- package/registry/popover/item.json +4 -0
- package/registry/popover/popover.tsx +284 -0
- package/registry/progress/item.json +4 -0
- package/registry/progress/progress.tsx +190 -0
- package/registry/questionnaire/item.json +4 -0
- package/registry/questionnaire/questionnaire.tsx +569 -0
- package/registry/radio-group/item.json +4 -0
- package/registry/radio-group/radio-group.tsx +101 -0
- package/registry/resizable/item.json +4 -0
- package/registry/resizable/resizable.tsx +455 -0
- package/registry/scroll-area/item.json +4 -0
- package/registry/scroll-area/scroll-area.tsx +149 -0
- package/registry/select/item.json +4 -0
- package/registry/select/select.tsx +571 -0
- package/registry/separator/item.json +4 -0
- package/registry/separator/separator.tsx +52 -0
- package/registry/sheet/item.json +4 -0
- package/registry/sheet/sheet.tsx +476 -0
- package/registry/sidebar/item.json +4 -0
- package/registry/sidebar/sidebar.tsx +1870 -0
- package/registry/skeleton/item.json +4 -0
- package/registry/skeleton/skeleton.tsx +47 -0
- package/registry/slider/item.json +4 -0
- package/registry/slider/slider.tsx +149 -0
- package/registry/spinner/item.json +4 -0
- package/registry/spinner/spinner.tsx +51 -0
- package/registry/switch/item.json +4 -0
- package/registry/switch/switch.tsx +93 -0
- package/registry/table/item.json +4 -0
- package/registry/table/table.tsx +373 -0
- package/registry/tabs/item.json +4 -0
- package/registry/tabs/tabs.tsx +209 -0
- package/registry/textarea/item.json +4 -0
- package/registry/textarea/textarea.tsx +51 -0
- package/registry/toast/item.json +4 -0
- package/registry/toast/toast.tsx +949 -0
- package/registry/toggle/item.json +4 -0
- package/registry/toggle/toggle.tsx +110 -0
- package/registry/toggle-group/item.json +4 -0
- package/registry/toggle-group/toggle-group.tsx +189 -0
- package/registry/tooltip/item.json +4 -0
- package/registry/tooltip/tooltip.tsx +169 -0
- package/registry/trace/item.json +4 -0
- package/registry/trace/trace.tsx +356 -0
- package/registry/ui-strings/item.json +5 -0
- package/registry/ui-strings/ui-strings.tsx +454 -0
- package/registry/use-highlighted-code/item.json +5 -0
- package/registry/use-highlighted-code/use-highlighted-code.tsx +145 -0
- package/registry/use-mobile/item.json +5 -0
- package/registry/use-mobile/use-mobile.ts +46 -0
- package/registry/utils/item.json +5 -0
- package/registry/utils/utils.ts +38 -0
- package/registry.json +3274 -0
- package/styles/animations.css +321 -0
- package/styles/theme-core.css +55 -0
- package/styles/theme.css +12 -0
- package/styles/themes/gray.css +69 -0
- package/styles/themes/neutral.css +69 -0
- package/styles/themes/slate.css +69 -0
- package/styles/themes/stone.css +69 -0
- package/styles/themes/zinc.css +69 -0
- package/styles/typeset.css +251 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { splitProps, useEffect, useSignal } from "@shadow-js/core";
|
|
2
|
+
import {
|
|
3
|
+
type CalendarDate,
|
|
4
|
+
type CalendarValue,
|
|
5
|
+
compareDates,
|
|
6
|
+
formatDate,
|
|
7
|
+
parseDateInput,
|
|
8
|
+
useControllableState,
|
|
9
|
+
} from "@shadow-js/primitives";
|
|
10
|
+
import type { JSX } from "@shadow-js/web";
|
|
11
|
+
import { Button } from "@/components/ui/button";
|
|
12
|
+
import { Calendar, isCalendarDay } from "@/components/ui/calendar";
|
|
13
|
+
import { Input } from "@/components/ui/input";
|
|
14
|
+
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
15
|
+
import { useUiStrings } from "@/lib/ui-strings";
|
|
16
|
+
import { cn } from "@/lib/utils";
|
|
17
|
+
|
|
18
|
+
/** A calendar glyph for the trigger, inline so the kit needs no icon package. @internal */
|
|
19
|
+
function CalendarGlyph(): JSX.Element {
|
|
20
|
+
return (
|
|
21
|
+
<svg
|
|
22
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
23
|
+
viewBox="0 0 24 24"
|
|
24
|
+
fill="none"
|
|
25
|
+
stroke="currentColor"
|
|
26
|
+
stroke-width="2"
|
|
27
|
+
stroke-linecap="round"
|
|
28
|
+
stroke-linejoin="round"
|
|
29
|
+
class="size-4"
|
|
30
|
+
aria-hidden="true"
|
|
31
|
+
>
|
|
32
|
+
<rect width="18" height="18" x="3" y="4" rx="2" />
|
|
33
|
+
<path d="M16 2v4M8 2v4M3 10h18" />
|
|
34
|
+
</svg>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Props of {@link DatePicker}: the day (controlled or uncontrolled), the locale and the limits, and the
|
|
40
|
+
* field's own attributes such as `id` and `aria-describedby`.
|
|
41
|
+
* @see {@link DatePicker}
|
|
42
|
+
* @since 1.0.0
|
|
43
|
+
*/
|
|
44
|
+
export interface DatePickerProps {
|
|
45
|
+
/**
|
|
46
|
+
* The chosen day. Setting it controls the picker; `undefined` means the picker is uncontrolled.
|
|
47
|
+
* @default none
|
|
48
|
+
*/
|
|
49
|
+
readonly value?: CalendarDate | undefined;
|
|
50
|
+
/**
|
|
51
|
+
* The day when the picker is uncontrolled.
|
|
52
|
+
* @default none
|
|
53
|
+
*/
|
|
54
|
+
readonly defaultValue?: CalendarDate | undefined;
|
|
55
|
+
/**
|
|
56
|
+
* Called with the day when the user picks a day or types a valid one, and with `undefined` when the
|
|
57
|
+
* field is cleared.
|
|
58
|
+
* @default none
|
|
59
|
+
*/
|
|
60
|
+
readonly onValueChange?: ((value: CalendarDate | undefined) => void) | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* A BCP 47 tag, such as `"de-DE"`. It sets the format of the typed date and the language and week
|
|
63
|
+
* start of the calendar.
|
|
64
|
+
* @default "en-US"
|
|
65
|
+
*/
|
|
66
|
+
readonly locale?: string | undefined;
|
|
67
|
+
/**
|
|
68
|
+
* The earliest day that can be typed or picked.
|
|
69
|
+
* @default none
|
|
70
|
+
*/
|
|
71
|
+
readonly min?: CalendarDate | undefined;
|
|
72
|
+
/**
|
|
73
|
+
* The latest day that can be typed or picked.
|
|
74
|
+
* @default none
|
|
75
|
+
*/
|
|
76
|
+
readonly max?: CalendarDate | undefined;
|
|
77
|
+
/**
|
|
78
|
+
* The text shown while the field is empty.
|
|
79
|
+
* @default "MM/DD/YYYY"
|
|
80
|
+
*/
|
|
81
|
+
readonly placeholder?: string | undefined;
|
|
82
|
+
/**
|
|
83
|
+
* Reading direction of the calendar. The popover opens in a portal, so a `dir` attribute on an ancestor of the field
|
|
84
|
+
* does not reach it: by default the calendar takes the direction of the nearest ancestor `dir` attribute of the field.
|
|
85
|
+
* @default the nearest ancestor `dir` of the field, else inherited from `DirectionProvider` or the document
|
|
86
|
+
*/
|
|
87
|
+
readonly dir?: "ltr" | "rtl" | undefined;
|
|
88
|
+
/**
|
|
89
|
+
* The accessible name of the calendar button. Translate it for another language.
|
|
90
|
+
* @default "Open calendar"
|
|
91
|
+
*/
|
|
92
|
+
readonly triggerLabel?: string | undefined;
|
|
93
|
+
/**
|
|
94
|
+
* Marks the field invalid from outside, for example a required field left empty. A typed date that
|
|
95
|
+
* does not exist marks it invalid too.
|
|
96
|
+
* @default false
|
|
97
|
+
*/
|
|
98
|
+
readonly invalid?: boolean | undefined;
|
|
99
|
+
/**
|
|
100
|
+
* The id of the element that describes the field, such as an error message. It is set on the field,
|
|
101
|
+
* so a screen reader reads the message with it.
|
|
102
|
+
* @default none
|
|
103
|
+
*/
|
|
104
|
+
readonly "aria-describedby"?: string | undefined;
|
|
105
|
+
/**
|
|
106
|
+
* Disables the field and the calendar button.
|
|
107
|
+
* @default false
|
|
108
|
+
*/
|
|
109
|
+
readonly disabled?: boolean | undefined;
|
|
110
|
+
/**
|
|
111
|
+
* The id of the field. Point a `Label` at it with `htmlFor`.
|
|
112
|
+
* @default none
|
|
113
|
+
*/
|
|
114
|
+
readonly id?: string | undefined;
|
|
115
|
+
/**
|
|
116
|
+
* The name under which the typed date is submitted with a form.
|
|
117
|
+
* @default none
|
|
118
|
+
*/
|
|
119
|
+
readonly name?: string | undefined;
|
|
120
|
+
/**
|
|
121
|
+
* Classes for the wrapper around the field and the button.
|
|
122
|
+
* @default none
|
|
123
|
+
*/
|
|
124
|
+
readonly class?: string | undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The code point of zero in each script whose ten digits are consecutive: ASCII, Arabic-Indic (used by
|
|
129
|
+
* `ar-EG`), Persian, Devanagari, Bengali, Thai and the others a date can be written in. @internal
|
|
130
|
+
*/
|
|
131
|
+
const DIGIT_ZEROS: readonly number[] = [
|
|
132
|
+
0x30, 0x660, 0x6f0, 0x7c0, 0x966, 0x9e6, 0xa66, 0xae6, 0xb66, 0xbe6, 0xc66, 0xce6, 0xd66, 0xde6,
|
|
133
|
+
0xe50, 0xed0, 0xf20, 0x1040, 0x1090, 0x17e0, 0x1810, 0xff10,
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* The text with every digit written as ASCII. Locales such as `ar-EG` format with Arabic-Indic digits,
|
|
138
|
+
* and the parser reads ASCII digits only; any other character is kept as it is. @internal
|
|
139
|
+
*/
|
|
140
|
+
function asciiDigits(text: string): string {
|
|
141
|
+
return [...text]
|
|
142
|
+
.map((char) => {
|
|
143
|
+
const code = char.codePointAt(0) ?? 0;
|
|
144
|
+
const zero = DIGIT_ZEROS.find((start) => code >= start && code <= start + 9);
|
|
145
|
+
return zero === undefined ? char : String(code - zero);
|
|
146
|
+
})
|
|
147
|
+
.join("");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Whether a typed day is inside the allowed range. @internal */
|
|
151
|
+
function inBounds(
|
|
152
|
+
day: CalendarDate,
|
|
153
|
+
min: CalendarDate | undefined,
|
|
154
|
+
max: CalendarDate | undefined,
|
|
155
|
+
): boolean {
|
|
156
|
+
if (min !== undefined && compareDates(day, min) < 0) return false;
|
|
157
|
+
if (max !== undefined && compareDates(day, max) > 0) return false;
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* A text field for a day, with a calendar button that opens a calendar in a popover. Typing reads the
|
|
163
|
+
* day in the locale's format, or in ISO form (`2026-03-05`), when the field loses focus or Enter is
|
|
164
|
+
* pressed. A day that does not exist, or that is outside `min` or `max`, marks the field invalid and is
|
|
165
|
+
* not taken. Picking a day in the calendar fills the field and closes the popover. Use it for a date
|
|
166
|
+
* that people type; the calendar is there for picking.
|
|
167
|
+
*
|
|
168
|
+
* Keyboard and ARIA: the field is a text input with `inputmode="numeric"`, and Enter reads the typed
|
|
169
|
+
* date without submitting a surrounding form. Name it with a `Label` (`htmlFor` pointing at the `id`).
|
|
170
|
+
* An invalid field has `aria-invalid="true"`, and `aria-describedby` points at your error message. The
|
|
171
|
+
* calendar button is a native button named by `triggerLabel` (default "Open calendar"): Enter or Space
|
|
172
|
+
* opens the calendar and moves focus into it, Escape or a press outside closes it. Inside the calendar,
|
|
173
|
+
* the arrow keys move between days, PageUp and PageDown move a month, and Enter or Space picks the
|
|
174
|
+
* focused day. Shift with PageUp or PageDown moves a year.
|
|
175
|
+
*
|
|
176
|
+
* shadcn/ui differences: shadcn's date picker is usually a button that shows the chosen date. This one
|
|
177
|
+
* is a typed text field with a calendar button next to it, so a user can type a date or pick one. `class`
|
|
178
|
+
* replaces `className`, and the parts have `data-slot` values (`date-picker`, `date-picker-input` and
|
|
179
|
+
* `date-picker-trigger`).
|
|
180
|
+
*
|
|
181
|
+
* @param props The picker props: see {@link DatePickerProps}.
|
|
182
|
+
* @returns The field and its calendar button.
|
|
183
|
+
* @example
|
|
184
|
+
* import { useSignal } from "@shadow-js/core";
|
|
185
|
+
* import { DatePicker } from "@/components/ui/date-picker";
|
|
186
|
+
* import { Label } from "@/components/ui/label";
|
|
187
|
+
*
|
|
188
|
+
* export function DueDate() {
|
|
189
|
+
* const [due, setDue] = useSignal<{ year: number; month: number; day: number } | undefined>(
|
|
190
|
+
* undefined,
|
|
191
|
+
* );
|
|
192
|
+
* return (
|
|
193
|
+
* <>
|
|
194
|
+
* <Label htmlFor="due">Due date</Label>
|
|
195
|
+
* <DatePicker id="due" value={due()} onValueChange={setDue} />
|
|
196
|
+
* </>
|
|
197
|
+
* );
|
|
198
|
+
* }
|
|
199
|
+
* @see {@link DatePickerProps}
|
|
200
|
+
* @since 1.0.0
|
|
201
|
+
*/
|
|
202
|
+
function DatePicker(props: DatePickerProps): JSX.Element {
|
|
203
|
+
const [local, rest] = splitProps(props as DatePickerProps & Record<string, unknown>, [
|
|
204
|
+
"value",
|
|
205
|
+
"defaultValue",
|
|
206
|
+
"onValueChange",
|
|
207
|
+
"locale",
|
|
208
|
+
"min",
|
|
209
|
+
"max",
|
|
210
|
+
"placeholder",
|
|
211
|
+
"triggerLabel",
|
|
212
|
+
"invalid",
|
|
213
|
+
"disabled",
|
|
214
|
+
"id",
|
|
215
|
+
"name",
|
|
216
|
+
"class",
|
|
217
|
+
"dir",
|
|
218
|
+
]);
|
|
219
|
+
const t = useUiStrings();
|
|
220
|
+
let root: HTMLElement | undefined;
|
|
221
|
+
// The calendar is built when the popover opens, so this reads the field's surroundings at that moment.
|
|
222
|
+
const direction = (): "ltr" | "rtl" | undefined => {
|
|
223
|
+
if (local.dir === "ltr" || local.dir === "rtl") return local.dir;
|
|
224
|
+
const own = root?.closest("[dir]")?.getAttribute("dir");
|
|
225
|
+
return own === "ltr" || own === "rtl" ? own : undefined;
|
|
226
|
+
};
|
|
227
|
+
const locale = (local.locale as string | undefined) ?? "en-US";
|
|
228
|
+
const [day, setDay] = useControllableState<CalendarDate | undefined>({
|
|
229
|
+
value: () => local.value as CalendarDate | undefined,
|
|
230
|
+
defaultValue: () => local.defaultValue as CalendarDate | undefined,
|
|
231
|
+
onChange: () => {
|
|
232
|
+
const callback = local.onValueChange as
|
|
233
|
+
| ((value: CalendarDate | undefined) => void)
|
|
234
|
+
| undefined;
|
|
235
|
+
return callback === undefined ? undefined : (next) => callback(next);
|
|
236
|
+
},
|
|
237
|
+
name: "DatePicker value",
|
|
238
|
+
});
|
|
239
|
+
const [text, setText] = useSignal<string>(
|
|
240
|
+
day() === undefined ? "" : formatDate(locale, day() as CalendarDate),
|
|
241
|
+
);
|
|
242
|
+
const [invalid, setInvalid] = useSignal(false);
|
|
243
|
+
const [open, setOpen] = useSignal(false);
|
|
244
|
+
|
|
245
|
+
// The field shows the day the app holds, so a day set from outside (a controlled value that changes,
|
|
246
|
+
// or a locale switch) replaces the text, and an invalid typed text is cleared with it.
|
|
247
|
+
useEffect(() => {
|
|
248
|
+
const current = day();
|
|
249
|
+
const tag = (local.locale as string | undefined) ?? "en-US";
|
|
250
|
+
setText(current === undefined ? "" : formatDate(tag, current));
|
|
251
|
+
setInvalid(false);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const commitText = (): void => {
|
|
255
|
+
const trimmed = text().trim();
|
|
256
|
+
if (trimmed === "") {
|
|
257
|
+
setInvalid(false);
|
|
258
|
+
if (day() !== undefined) setDay(undefined);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const parsed = parseDateInput(asciiDigits(trimmed), locale);
|
|
262
|
+
if (
|
|
263
|
+
parsed === undefined ||
|
|
264
|
+
!inBounds(
|
|
265
|
+
parsed,
|
|
266
|
+
local.min as CalendarDate | undefined,
|
|
267
|
+
local.max as CalendarDate | undefined,
|
|
268
|
+
)
|
|
269
|
+
) {
|
|
270
|
+
setInvalid(true);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
setInvalid(false);
|
|
274
|
+
setDay(parsed);
|
|
275
|
+
setText(formatDate(locale, parsed));
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const pick = (next: CalendarValue): void => {
|
|
279
|
+
if (!isCalendarDay(next)) return;
|
|
280
|
+
setDay(next);
|
|
281
|
+
setText(formatDate(locale, next));
|
|
282
|
+
setInvalid(false);
|
|
283
|
+
setOpen(false);
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
return (
|
|
287
|
+
<div
|
|
288
|
+
ref={(element: HTMLElement) => {
|
|
289
|
+
root = element;
|
|
290
|
+
}}
|
|
291
|
+
data-slot="date-picker"
|
|
292
|
+
class={cn("relative flex w-full max-w-xs gap-2", local.class as string | undefined)}
|
|
293
|
+
>
|
|
294
|
+
<Input
|
|
295
|
+
id={local.id as string | undefined}
|
|
296
|
+
name={local.name as string | undefined}
|
|
297
|
+
data-slot="date-picker-input"
|
|
298
|
+
inputmode="numeric"
|
|
299
|
+
autocomplete="off"
|
|
300
|
+
placeholder={(local.placeholder as string | undefined) ?? "MM/DD/YYYY"}
|
|
301
|
+
value={text()}
|
|
302
|
+
disabled={local.disabled === true}
|
|
303
|
+
aria-invalid={invalid() || local.invalid === true ? "true" : undefined}
|
|
304
|
+
onInput={(event: Event) => {
|
|
305
|
+
setText((event.currentTarget as HTMLInputElement).value);
|
|
306
|
+
setInvalid(false);
|
|
307
|
+
}}
|
|
308
|
+
onBlur={commitText}
|
|
309
|
+
onKeyDown={(event: KeyboardEvent) => {
|
|
310
|
+
if (event.key === "Enter") {
|
|
311
|
+
event.preventDefault();
|
|
312
|
+
commitText();
|
|
313
|
+
}
|
|
314
|
+
}}
|
|
315
|
+
{...rest}
|
|
316
|
+
/>
|
|
317
|
+
<Popover open={open()} onOpenChange={setOpen}>
|
|
318
|
+
<PopoverTrigger
|
|
319
|
+
as={Button}
|
|
320
|
+
variant="outline"
|
|
321
|
+
size="icon"
|
|
322
|
+
aria-label={
|
|
323
|
+
(local.triggerLabel as string | undefined) ?? t("datePicker.open", "Open calendar")
|
|
324
|
+
}
|
|
325
|
+
data-slot="date-picker-trigger"
|
|
326
|
+
disabled={local.disabled === true}
|
|
327
|
+
>
|
|
328
|
+
<CalendarGlyph />
|
|
329
|
+
</PopoverTrigger>
|
|
330
|
+
<PopoverContent class="w-auto p-0" align="end">
|
|
331
|
+
{/* The calendar is built when the popover opens, so its selection starts from the field's day and
|
|
332
|
+
stays its own until it closes: a controlled value that is sometimes undefined would warn. */}
|
|
333
|
+
<Calendar
|
|
334
|
+
mode="single"
|
|
335
|
+
locale={locale}
|
|
336
|
+
dir={direction()}
|
|
337
|
+
defaultValue={day()}
|
|
338
|
+
min={local.min as CalendarDate | undefined}
|
|
339
|
+
max={local.max as CalendarDate | undefined}
|
|
340
|
+
defaultMonth={day() ?? (local.min as CalendarDate | undefined)}
|
|
341
|
+
onValueChange={pick}
|
|
342
|
+
/>
|
|
343
|
+
</PopoverContent>
|
|
344
|
+
</Popover>
|
|
345
|
+
</div>
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export { DatePicker };
|