aplance-ui 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.
Files changed (132) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +29 -0
  3. package/dist/api.d.ts +175 -0
  4. package/dist/api.js +1954 -0
  5. package/dist/api.js.map +1 -0
  6. package/dist/frameworks/astro/astro.config.snippet.md +23 -0
  7. package/dist/frameworks/astro/postcss.config.mjs.template +20 -0
  8. package/dist/frameworks/astro/vite-inline.snippet.md +22 -0
  9. package/dist/frameworks/electron-vite/vite.config.snippet.md +44 -0
  10. package/dist/frameworks/next/layout-import.snippet.md +19 -0
  11. package/dist/frameworks/next/next.config.snippet.md +15 -0
  12. package/dist/frameworks/next/postcss.config.js.template +16 -0
  13. package/dist/frameworks/tanstack-start/postcss.config.ts.template +17 -0
  14. package/dist/frameworks/tanstack-start/vite-inline.snippet.md +22 -0
  15. package/dist/frameworks/vite-react/postcss.config.ts.template +16 -0
  16. package/dist/frameworks/vite-react/vite-inline.snippet.md +20 -0
  17. package/dist/frameworks/vite-react/vite.config.ts.template +48 -0
  18. package/dist/index.js +2047 -0
  19. package/dist/index.js.map +1 -0
  20. package/package.json +63 -0
  21. package/registry/components/primitives/icon/icon.module.css +63 -0
  22. package/registry/components/primitives/icon/icon.tsx +53 -0
  23. package/registry/components/primitives/modal/modal-context.ts +19 -0
  24. package/registry/components/primitives/modal/modal.module.css +415 -0
  25. package/registry/components/primitives/modal/modal.tsx +550 -0
  26. package/registry/components/primitives/popover/popover-context.ts +28 -0
  27. package/registry/components/primitives/popover/popover.module.css +28 -0
  28. package/registry/components/primitives/popover/popover.tsx +465 -0
  29. package/registry/components/ui/accordion/accordion.module.css +208 -0
  30. package/registry/components/ui/accordion/accordion.tsx +327 -0
  31. package/registry/components/ui/avatar/avatar.module.css +32 -0
  32. package/registry/components/ui/avatar/avatar.tsx +37 -0
  33. package/registry/components/ui/badge/badge.module.css +103 -0
  34. package/registry/components/ui/badge/badge.tsx +29 -0
  35. package/registry/components/ui/button/button.module.css +216 -0
  36. package/registry/components/ui/button/button.tsx +133 -0
  37. package/registry/components/ui/chart/chart.module.css +168 -0
  38. package/registry/components/ui/chart/chart.tsx +1293 -0
  39. package/registry/components/ui/chart/types.ts +210 -0
  40. package/registry/components/ui/chart/utils.ts +59 -0
  41. package/registry/components/ui/copy-button/copy-button.module.css +21 -0
  42. package/registry/components/ui/copy-button/copy-button.tsx +123 -0
  43. package/registry/components/ui/countdown/countdown.module.css +5 -0
  44. package/registry/components/ui/countdown/countdown.tsx +152 -0
  45. package/registry/components/ui/data-table/checkbox-column.tsx +59 -0
  46. package/registry/components/ui/data-table/data-table.module.css +594 -0
  47. package/registry/components/ui/data-table/data-table.tsx +1109 -0
  48. package/registry/components/ui/data-table/filter-bar.tsx +33 -0
  49. package/registry/components/ui/data-table/pagination-bar.tsx +125 -0
  50. package/registry/components/ui/data-table/types.ts +594 -0
  51. package/registry/components/ui/data-table/useDataTable.tsx +573 -0
  52. package/registry/components/ui/data-table/utils.ts +445 -0
  53. package/registry/components/ui/empty-state/empty-state.module.css +26 -0
  54. package/registry/components/ui/empty-state/empty-state.tsx +32 -0
  55. package/registry/components/ui/input-fields/color-picker/color-palette.module.css +102 -0
  56. package/registry/components/ui/input-fields/color-picker/color-palette.tsx +92 -0
  57. package/registry/components/ui/input-fields/color-picker/color-picker.module.css +29 -0
  58. package/registry/components/ui/input-fields/color-picker/color-picker.tsx +168 -0
  59. package/registry/components/ui/input-fields/common/field-message/field-message.module.css +28 -0
  60. package/registry/components/ui/input-fields/common/field-message/field-message.tsx +39 -0
  61. package/registry/components/ui/input-fields/common/input-field-common.module.css +318 -0
  62. package/registry/components/ui/input-fields/common/input-field-native.module.css +267 -0
  63. package/registry/components/ui/input-fields/common/picker-popover-common.module.css +40 -0
  64. package/registry/components/ui/input-fields/common/select-button/select-button.module.css +9 -0
  65. package/registry/components/ui/input-fields/common/select-button/select-button.tsx +171 -0
  66. package/registry/components/ui/input-fields/common/types.ts +38 -0
  67. package/registry/components/ui/input-fields/custom-select/custom-select.module.css +56 -0
  68. package/registry/components/ui/input-fields/custom-select/custom-select.tsx +403 -0
  69. package/registry/components/ui/input-fields/date-picker/calendar.module.css +311 -0
  70. package/registry/components/ui/input-fields/date-picker/calendar.tsx +186 -0
  71. package/registry/components/ui/input-fields/date-picker/date-picker.module.css +57 -0
  72. package/registry/components/ui/input-fields/date-picker/date-picker.tsx +945 -0
  73. package/registry/components/ui/input-fields/date-picker/types.ts +102 -0
  74. package/registry/components/ui/input-fields/date-picker/utils.ts +236 -0
  75. package/registry/components/ui/input-fields/emoji-picker/emoji-palette.module.css +90 -0
  76. package/registry/components/ui/input-fields/emoji-picker/emoji-palette.tsx +168 -0
  77. package/registry/components/ui/input-fields/emoji-picker/emoji-picker.module.css +8 -0
  78. package/registry/components/ui/input-fields/emoji-picker/emoji-picker.tsx +102 -0
  79. package/registry/components/ui/input-fields/emoji-picker/utils.ts +505 -0
  80. package/registry/components/ui/input-fields/field-group/field-group.module.css +86 -0
  81. package/registry/components/ui/input-fields/field-group/field-group.tsx +86 -0
  82. package/registry/components/ui/input-fields/input/input.tsx +319 -0
  83. package/registry/components/ui/input-fields/media-input/kind-icons.tsx +62 -0
  84. package/registry/components/ui/input-fields/media-input/media-input.module.css +294 -0
  85. package/registry/components/ui/input-fields/media-input/media-input.tsx +327 -0
  86. package/registry/components/ui/input-fields/media-input/media-preview.tsx +101 -0
  87. package/registry/components/ui/input-fields/media-input/types.ts +129 -0
  88. package/registry/components/ui/input-fields/media-input/useMediaUpload.tsx +302 -0
  89. package/registry/components/ui/input-fields/media-input/utils.ts +325 -0
  90. package/registry/components/ui/input-fields/otp-input/otp-input.module.css +24 -0
  91. package/registry/components/ui/input-fields/otp-input/otp-input.tsx +291 -0
  92. package/registry/components/ui/input-fields/select/select.tsx +118 -0
  93. package/registry/components/ui/input-fields/switch/switch.module.css +70 -0
  94. package/registry/components/ui/input-fields/switch/switch.tsx +59 -0
  95. package/registry/components/ui/input-fields/textarea/textarea.tsx +133 -0
  96. package/registry/components/ui/input-fields/time-picker/time-picker.module.css +57 -0
  97. package/registry/components/ui/input-fields/time-picker/time-picker.tsx +437 -0
  98. package/registry/components/ui/loader/loader.module.css +156 -0
  99. package/registry/components/ui/loader/loader.tsx +102 -0
  100. package/registry/components/ui/stepper/stepper.module.css +294 -0
  101. package/registry/components/ui/stepper/stepper.tsx +197 -0
  102. package/registry/components/ui/tabs/tabs.module.css +150 -0
  103. package/registry/components/ui/tabs/tabs.tsx +134 -0
  104. package/registry/components/ui/toast/toast.module.css +159 -0
  105. package/registry/components/ui/toast/toast.tsx +104 -0
  106. package/registry/components/ui/tooltip/tooltip.module.css +82 -0
  107. package/registry/components/ui/tooltip/tooltip.tsx +72 -0
  108. package/registry/global.d.ts +4 -0
  109. package/registry/hooks/useFocusOutside.tsx +33 -0
  110. package/registry/hooks/useResizeObserver.tsx +32 -0
  111. package/registry/lib/dom.ts +25 -0
  112. package/registry/lib/string.ts +23 -0
  113. package/registry/registry.json +719 -0
  114. package/registry/styles/app.css +240 -0
  115. package/registry/styles/components.css +78 -0
  116. package/registry/styles/fonts/geist-regular.woff2 +0 -0
  117. package/registry/styles/globals.css +7 -0
  118. package/registry/styles/media.css +25 -0
  119. package/registry/styles/utilities.css +90 -0
  120. package/registry/themes/colors/blue.css +37 -0
  121. package/registry/themes/colors/green.css +37 -0
  122. package/registry/themes/colors/rose.css +37 -0
  123. package/registry/themes/colors/slate.css +37 -0
  124. package/registry/themes/colors/stone.css +37 -0
  125. package/registry/themes/colors/violet.css +37 -0
  126. package/registry/themes/colors/zinc.css +37 -0
  127. package/registry/themes/radius/pill.css +8 -0
  128. package/registry/themes/radius/rounded.css +8 -0
  129. package/registry/themes/radius/sharp.css +8 -0
  130. package/registry/themes/spacing/compact.css +17 -0
  131. package/registry/themes/spacing/spacious.css +17 -0
  132. package/registry/tsconfig.json +25 -0
@@ -0,0 +1,945 @@
1
+ import {
2
+ useCallback,
3
+ useEffect,
4
+ useId,
5
+ useMemo,
6
+ useState,
7
+ type ChangeEvent,
8
+ type KeyboardEvent,
9
+ } from "react";
10
+ import clsx from "clsx";
11
+ import type { DateRange as RDPDateRange, Matcher } from "react-day-picker";
12
+ import { IconCalendar } from "@tabler/icons-react";
13
+ import {
14
+ Popover,
15
+ PopoverMenu,
16
+ PopoverTrigger,
17
+ } from "../../../primitives/popover/popover";
18
+ import { Icon } from "../../../primitives/icon/icon";
19
+ import { Input } from "../input/input";
20
+ import { Button } from "../../button/button";
21
+ import { FieldMessage } from "../common/field-message/field-message";
22
+ import sCommon from "../common/input-field-common.module.css";
23
+ import sPickerCommon from "../common/picker-popover-common.module.css";
24
+ import { SelectButton } from "../common/select-button/select-button";
25
+ import { TimePicker } from "../time-picker/time-picker";
26
+ import { Calendar } from "./calendar";
27
+ import type {
28
+ DatePickerProps,
29
+ DateRange,
30
+ DatePickerMode,
31
+ RangeDatePickerProps,
32
+ SingleDatePickerProps,
33
+ } from "./types";
34
+ import {
35
+ addMonthOffset,
36
+ autoFormatDateText,
37
+ coerceToDate,
38
+ coerceToRange,
39
+ defaultFormatDate,
40
+ formatTriggerDisplay,
41
+ hasRangeValue,
42
+ isRangeValue,
43
+ isSameMonth,
44
+ mergeDateWithTimeOf,
45
+ parseDateTextString,
46
+ startOfDay,
47
+ toDateInputString,
48
+ toMonthStart,
49
+ toTimeString,
50
+ withTimeString,
51
+ type InternalValue,
52
+ } from "./utils";
53
+ import s from "./date-picker.module.css";
54
+
55
+ export { Calendar } from "./calendar";
56
+ export type { CalendarProps } from "./calendar";
57
+ export * from "./types";
58
+
59
+ const DEFAULT_START_PLACEHOLDER = "Start date";
60
+ const DEFAULT_END_PLACEHOLDER = "End date";
61
+
62
+ export const DatePicker = (props: DatePickerProps) => {
63
+ const {
64
+ label,
65
+ className,
66
+ style,
67
+ id,
68
+ info,
69
+ errors,
70
+ layout = "default",
71
+ layoutDirection = "default",
72
+
73
+ placeholder,
74
+ startPlaceholder = DEFAULT_START_PLACEHOLDER,
75
+ endPlaceholder = DEFAULT_END_PLACEHOLDER,
76
+
77
+ minDate,
78
+ maxDate,
79
+ disabledDates,
80
+
81
+ disabled = false,
82
+ readOnly = false,
83
+ required = false,
84
+ closeOnSelection,
85
+ showManualInput = true,
86
+
87
+ showTime = false,
88
+ timeStep,
89
+ timeFormat = "12h",
90
+ timePickerPopover,
91
+ showActionFooter = true,
92
+
93
+ showClear = true,
94
+ onClear,
95
+ onBlur,
96
+
97
+ numberOfMonths,
98
+ weekStartsOn = 1,
99
+ captionLayout = "dropdown",
100
+ showWeekNumber = false,
101
+
102
+ formatDisplay,
103
+ formatDate = defaultFormatDate,
104
+
105
+ popover,
106
+ popoverTrigger,
107
+ popoverMenu,
108
+ selectButton,
109
+ calendar,
110
+ } = props;
111
+
112
+ const mode: DatePickerMode = props.mode ?? "single";
113
+ const isRange = mode === "range";
114
+
115
+ const generatedId = useId();
116
+ const inputId = id ?? generatedId;
117
+
118
+ const isControlled = "value" in props;
119
+
120
+ const anchorDateOnly = !showTime;
121
+
122
+ const [internalValue, setInternalValue] = useState<InternalValue>(() => {
123
+ if (isRange) {
124
+ return coerceToRange(
125
+ (props as RangeDatePickerProps).defaultValue,
126
+ anchorDateOnly,
127
+ );
128
+ }
129
+ return coerceToDate(
130
+ (props as SingleDatePickerProps).defaultValue,
131
+ anchorDateOnly,
132
+ );
133
+ });
134
+
135
+ const currentValue: InternalValue = isControlled
136
+ ? isRange
137
+ ? coerceToRange(
138
+ (props as RangeDatePickerProps).value,
139
+ anchorDateOnly,
140
+ )
141
+ : coerceToDate(
142
+ (props as SingleDatePickerProps).value,
143
+ anchorDateOnly,
144
+ )
145
+ : internalValue;
146
+
147
+ const selectedSingle =
148
+ !isRange && currentValue instanceof Date ? currentValue : undefined;
149
+ const selectedRange =
150
+ isRange && isRangeValue(currentValue)
151
+ ? (currentValue as DateRange)
152
+ : undefined;
153
+
154
+ const [isOpen, setIsOpen] = useState(false);
155
+ const [singleManualText, setSingleManualText] = useState("");
156
+ const [rangeManualText, setRangeManualText] = useState<{
157
+ from: string;
158
+ to: string;
159
+ }>({ from: "", to: "" });
160
+
161
+ const resolvedRangeMonthCount = isRange
162
+ ? Math.max(1, numberOfMonths ?? calendar?.numberOfMonths ?? 2)
163
+ : 1;
164
+
165
+ // Controlled month state for live calendar navigation from manual input
166
+ const [calendarMonth, setCalendarMonth] = useState<Date | undefined>(() => {
167
+ if (isRange) {
168
+ const seed = selectedRange?.from ?? selectedRange?.to;
169
+ return seed ? toMonthStart(seed) : undefined;
170
+ }
171
+ return selectedSingle ? toMonthStart(selectedSingle) : undefined;
172
+ });
173
+
174
+ const [rangeCalendarMonths, setRangeCalendarMonths] = useState<Date[]>(() => {
175
+ const fromMonth = selectedRange?.from
176
+ ? toMonthStart(selectedRange.from)
177
+ : undefined;
178
+ const toMonth = selectedRange?.to
179
+ ? toMonthStart(selectedRange.to)
180
+ : undefined;
181
+
182
+ if (fromMonth && toMonth && resolvedRangeMonthCount > 1) {
183
+ return [
184
+ fromMonth,
185
+ ...Array.from({ length: resolvedRangeMonthCount - 2 }, (_, index) =>
186
+ addMonthOffset(fromMonth, index + 1),
187
+ ),
188
+ toMonth,
189
+ ];
190
+ }
191
+
192
+ const seed = fromMonth ?? toMonth ?? toMonthStart(new Date());
193
+ return Array.from({ length: resolvedRangeMonthCount }, (_, index) =>
194
+ addMonthOffset(seed, index),
195
+ );
196
+ });
197
+
198
+ const hasValue = isRange
199
+ ? hasRangeValue(currentValue as DateRange | undefined)
200
+ : Boolean(currentValue);
201
+
202
+ const shouldCloseOnSelection = closeOnSelection ?? false;
203
+
204
+ const minDay = useMemo(
205
+ () => (minDate ? startOfDay(minDate) : undefined),
206
+ [minDate],
207
+ );
208
+ const maxDay = useMemo(
209
+ () => (maxDate ? startOfDay(maxDate) : undefined),
210
+ [maxDate],
211
+ );
212
+
213
+ const isWithinBounds = useCallback(
214
+ (date: Date) => {
215
+ const day = startOfDay(date);
216
+ if (minDay && day < minDay) return false;
217
+ if (maxDay && day > maxDay) return false;
218
+ return true;
219
+ },
220
+ [maxDay, minDay],
221
+ );
222
+
223
+ const resolvedPlaceholder =
224
+ placeholder ?? (isRange ? "Select date range..." : "Select date...");
225
+
226
+ const selectedSingleText = selectedSingle
227
+ ? toDateInputString(selectedSingle)
228
+ : "";
229
+ const selectedRangeFromText = selectedRange?.from
230
+ ? toDateInputString(selectedRange.from)
231
+ : "";
232
+ const selectedRangeToText = selectedRange?.to
233
+ ? toDateInputString(selectedRange.to)
234
+ : "";
235
+
236
+ useEffect(() => {
237
+ if (!isRange || resolvedRangeMonthCount <= 1) return;
238
+
239
+ setRangeCalendarMonths((prev) => {
240
+ if (prev.length === resolvedRangeMonthCount) return prev;
241
+
242
+ const seed = prev[0] ??
243
+ toMonthStart(selectedRange?.from ?? selectedRange?.to ?? new Date());
244
+
245
+ return Array.from({ length: resolvedRangeMonthCount }, (_, index) =>
246
+ prev[index] ?? addMonthOffset(seed, index),
247
+ );
248
+ });
249
+ }, [
250
+ isRange,
251
+ resolvedRangeMonthCount,
252
+ selectedRange?.from,
253
+ selectedRange?.to,
254
+ ]);
255
+
256
+ useEffect(() => {
257
+ if (isRange) {
258
+ setRangeManualText({
259
+ from: selectedRangeFromText,
260
+ to: selectedRangeToText,
261
+ });
262
+ return;
263
+ }
264
+
265
+ setSingleManualText(selectedSingleText);
266
+ }, [isRange, selectedRangeFromText, selectedRangeToText, selectedSingleText]);
267
+
268
+ // Sync visible calendar months when popover opens or value changes externally
269
+ useEffect(() => {
270
+ if (!isOpen) return;
271
+
272
+ if (isRange) {
273
+ const fromMonth = selectedRange?.from
274
+ ? toMonthStart(selectedRange.from)
275
+ : undefined;
276
+ const toMonth = selectedRange?.to
277
+ ? toMonthStart(selectedRange.to)
278
+ : undefined;
279
+
280
+ if (resolvedRangeMonthCount > 1) {
281
+ setRangeCalendarMonths((prev) => {
282
+ const seed = fromMonth ?? toMonth ?? prev[0] ?? toMonthStart(new Date());
283
+ const next = Array.from(
284
+ { length: resolvedRangeMonthCount },
285
+ (_, index) => prev[index] ?? addMonthOffset(seed, index),
286
+ );
287
+
288
+ if (fromMonth) next[0] = fromMonth;
289
+ if (toMonth) next[resolvedRangeMonthCount - 1] = toMonth;
290
+
291
+ const unchanged =
292
+ prev.length === next.length &&
293
+ prev.every((month, index) => isSameMonth(month, next[index]));
294
+
295
+ return unchanged ? prev : next;
296
+ });
297
+ }
298
+
299
+ const target = selectedRange?.from ?? selectedRange?.to;
300
+ if (target) setCalendarMonth(toMonthStart(target));
301
+ return;
302
+ }
303
+
304
+ if (selectedSingle) {
305
+ setCalendarMonth(toMonthStart(selectedSingle));
306
+ }
307
+ // eslint-disable-next-line react-hooks/exhaustive-deps
308
+ }, [isOpen]);
309
+
310
+ const displayText = useMemo(() => {
311
+ if (formatDisplay) {
312
+ return (
313
+ formatDisplay(currentValue, { mode, withTime: showTime }) ||
314
+ resolvedPlaceholder
315
+ );
316
+ }
317
+ return formatTriggerDisplay(
318
+ currentValue,
319
+ mode,
320
+ showTime,
321
+ resolvedPlaceholder,
322
+ startPlaceholder,
323
+ endPlaceholder,
324
+ formatDate,
325
+ );
326
+ }, [
327
+ currentValue,
328
+ endPlaceholder,
329
+ formatDate,
330
+ formatDisplay,
331
+ mode,
332
+ resolvedPlaceholder,
333
+ showTime,
334
+ startPlaceholder,
335
+ ]);
336
+
337
+ const disabledMatcher = useMemo<Matcher | Matcher[] | undefined>(() => {
338
+ const matchers: Matcher[] = [];
339
+ if (minDate) matchers.push({ before: minDate });
340
+ if (maxDate) matchers.push({ after: maxDate });
341
+ if (disabledDates) {
342
+ if (Array.isArray(disabledDates)) matchers.push(...disabledDates);
343
+ else matchers.push(disabledDates);
344
+ }
345
+ const external = calendar?.disabled;
346
+ if (external) {
347
+ if (Array.isArray(external)) matchers.push(...external);
348
+ else matchers.push(external);
349
+ }
350
+ return matchers.length ? matchers : undefined;
351
+ }, [calendar?.disabled, disabledDates, maxDate, minDate]);
352
+
353
+ const onChangeSingle = !isRange
354
+ ? (props as SingleDatePickerProps).onChange
355
+ : undefined;
356
+ const onChangeRange = isRange
357
+ ? (props as RangeDatePickerProps).onChange
358
+ : undefined;
359
+
360
+ const commit = useCallback(
361
+ (next: InternalValue, closeMenu = false) => {
362
+ if (!isControlled) setInternalValue(next);
363
+ if (isRange) {
364
+ onChangeRange?.(next as DateRange | undefined);
365
+ } else {
366
+ onChangeSingle?.(next as Date | undefined);
367
+ }
368
+ if (closeMenu) setIsOpen(false);
369
+ },
370
+ [isControlled, isRange, onChangeRange, onChangeSingle],
371
+ );
372
+
373
+ const resolveDayValue = useCallback(
374
+ (date: Date, existingTime: Date | undefined): Date =>
375
+ showTime ? mergeDateWithTimeOf(date, existingTime) : startOfDay(date),
376
+ [showTime],
377
+ );
378
+
379
+ const handleSingleSelect = useCallback(
380
+ (nextDate: Date | undefined) => {
381
+ if (!nextDate) {
382
+ commit(undefined);
383
+ return;
384
+ }
385
+
386
+ const existing = currentValue instanceof Date ? currentValue : undefined;
387
+ commit(resolveDayValue(nextDate, existing), shouldCloseOnSelection);
388
+ },
389
+ [commit, currentValue, resolveDayValue, shouldCloseOnSelection],
390
+ );
391
+
392
+ const handleSingleDateInputChange = useCallback(
393
+ (event: ChangeEvent<HTMLInputElement>) => {
394
+ const raw = event.target.value;
395
+ const formatted = autoFormatDateText(raw, singleManualText);
396
+ setSingleManualText(formatted);
397
+
398
+ const trimmed = formatted.trim();
399
+ if (!trimmed) {
400
+ commit(undefined);
401
+ return;
402
+ }
403
+
404
+ const nextDate = parseDateTextString(trimmed);
405
+
406
+ if (!nextDate || !isWithinBounds(nextDate)) {
407
+ return;
408
+ }
409
+
410
+ const existing = currentValue instanceof Date ? currentValue : undefined;
411
+
412
+ // Live-sync calendar month
413
+ setCalendarMonth(toMonthStart(nextDate));
414
+ commit(resolveDayValue(nextDate, existing));
415
+ },
416
+ [commit, currentValue, isWithinBounds, resolveDayValue, singleManualText],
417
+ );
418
+
419
+ const handleSingleDateInputBlur = useCallback(() => {
420
+ setSingleManualText(selectedSingleText);
421
+ }, [selectedSingleText]);
422
+
423
+ const handleRangeDateInputChange = useCallback(
424
+ (key: "from" | "to", event: ChangeEvent<HTMLInputElement>) => {
425
+ const raw = event.target.value;
426
+ const prev = rangeManualText[key];
427
+ const formatted = autoFormatDateText(raw, prev);
428
+
429
+ setRangeManualText((prev) => ({
430
+ ...prev,
431
+ [key]: formatted,
432
+ }));
433
+
434
+ const trimmed = formatted.trim();
435
+ const existing: DateRange = isRangeValue(currentValue)
436
+ ? (currentValue as DateRange)
437
+ : {};
438
+
439
+ if (!trimmed) {
440
+ const nextRange =
441
+ key === "from"
442
+ ? { ...existing, from: undefined }
443
+ : { ...existing, to: undefined };
444
+
445
+ commit(nextRange.from || nextRange.to ? nextRange : undefined);
446
+ return;
447
+ }
448
+
449
+ const nextDateRaw = parseDateTextString(trimmed);
450
+ if (!nextDateRaw || !isWithinBounds(nextDateRaw)) {
451
+ return;
452
+ }
453
+
454
+ const nextDate = resolveDayValue(nextDateRaw, existing[key]);
455
+
456
+ const nextRange =
457
+ key === "from"
458
+ ? { ...existing, from: nextDate }
459
+ : { ...existing, to: nextDate };
460
+
461
+ if (nextRange.from && nextRange.to && nextRange.from > nextRange.to) {
462
+ if (key === "from") {
463
+ nextRange.to = nextRange.from;
464
+ } else {
465
+ nextRange.from = nextRange.to;
466
+ }
467
+ }
468
+
469
+ // Live-sync calendar month
470
+ const nextMonth = toMonthStart(nextDateRaw);
471
+
472
+ if (resolvedRangeMonthCount > 1) {
473
+ const panelIndex = key === "from" ? 0 : resolvedRangeMonthCount - 1;
474
+
475
+ setRangeCalendarMonths((prev) => {
476
+ const seed = prev[0] ?? nextMonth;
477
+ const next = Array.from(
478
+ { length: resolvedRangeMonthCount },
479
+ (_, index) => prev[index] ?? addMonthOffset(seed, index),
480
+ );
481
+ next[panelIndex] = nextMonth;
482
+ return next;
483
+ });
484
+ } else {
485
+ setCalendarMonth(nextMonth);
486
+ }
487
+
488
+ commit(nextRange);
489
+ },
490
+ [
491
+ commit,
492
+ currentValue,
493
+ isWithinBounds,
494
+ rangeManualText,
495
+ resolveDayValue,
496
+ resolvedRangeMonthCount,
497
+ ],
498
+ );
499
+
500
+ const handleRangeDateInputBlur = useCallback(() => {
501
+ setRangeManualText({
502
+ from: selectedRangeFromText,
503
+ to: selectedRangeToText,
504
+ });
505
+ }, [selectedRangeFromText, selectedRangeToText]);
506
+
507
+ const handleManualInputKeyDown = useCallback(
508
+ (event: KeyboardEvent<HTMLInputElement>) => {
509
+ if (event.key !== "Enter") return;
510
+ event.preventDefault();
511
+ event.currentTarget.blur();
512
+ },
513
+ [],
514
+ );
515
+
516
+ const handleRangeSelect = useCallback(
517
+ (next: DateRange | undefined) => {
518
+ if (!next) {
519
+ commit(undefined);
520
+ return;
521
+ }
522
+
523
+ const existing = isRangeValue(currentValue)
524
+ ? (currentValue as DateRange)
525
+ : undefined;
526
+
527
+ const from = next.from
528
+ ? resolveDayValue(next.from, existing?.from)
529
+ : undefined;
530
+ const to = next.to
531
+ ? resolveDayValue(next.to, existing?.to)
532
+ : undefined;
533
+
534
+ const nextRange: DateRange | undefined =
535
+ from || to ? { from, to } : undefined;
536
+
537
+ const canClose =
538
+ shouldCloseOnSelection && Boolean(nextRange?.from && nextRange?.to);
539
+
540
+ commit(nextRange, canClose);
541
+ },
542
+ [commit, currentValue, resolveDayValue, shouldCloseOnSelection],
543
+ );
544
+
545
+ const handleRangePanelMonthChange = useCallback(
546
+ (index: number, nextMonthRaw: Date) => {
547
+ const nextMonth = toMonthStart(nextMonthRaw);
548
+
549
+ setRangeCalendarMonths((prev) => {
550
+ const seed = prev[0] ?? nextMonth;
551
+ const next = Array.from(
552
+ { length: resolvedRangeMonthCount },
553
+ (_, monthIndex) => prev[monthIndex] ?? addMonthOffset(seed, monthIndex),
554
+ );
555
+ next[index] = nextMonth;
556
+ return next;
557
+ });
558
+ },
559
+ [resolvedRangeMonthCount],
560
+ );
561
+
562
+ const handleSingleTimeChange = useCallback(
563
+ (time: string | undefined) => {
564
+ if (!time) return;
565
+ if (!(currentValue instanceof Date)) return;
566
+ commit(withTimeString(currentValue, time));
567
+ },
568
+ [commit, currentValue],
569
+ );
570
+
571
+ const handleRangeTimeChange = useCallback(
572
+ (key: "from" | "to", time: string | undefined) => {
573
+ if (!time) return;
574
+ const range = isRangeValue(currentValue)
575
+ ? (currentValue as DateRange)
576
+ : undefined;
577
+ if (!range?.[key]) return;
578
+ commit({ ...range, [key]: withTimeString(range[key]!, time) });
579
+ },
580
+ [commit, currentValue],
581
+ );
582
+
583
+ const popoverOnClose = popover?.onClose;
584
+ const handlePopoverClose = useCallback(() => {
585
+ popoverOnClose?.();
586
+ onBlur?.();
587
+ }, [onBlur, popoverOnClose]);
588
+
589
+ const {
590
+ className: popoverClassName,
591
+ offset: popoverOffset = 4,
592
+ ...popoverRest
593
+ } = popover ?? {};
594
+
595
+ const {
596
+ className: popoverTriggerClassName,
597
+ style: popoverTriggerStyle,
598
+ ...popoverTriggerRest
599
+ } = popoverTrigger ?? {};
600
+
601
+ const {
602
+ className: popoverMenuClassName,
603
+ style: popoverMenuStyle,
604
+ ...popoverMenuRest
605
+ } = popoverMenu ?? {};
606
+
607
+ const timeValueSingle =
608
+ selectedSingle && showTime ? toTimeString(selectedSingle) : "";
609
+
610
+ const timeValueRangeFrom =
611
+ selectedRange?.from && showTime ? toTimeString(selectedRange.from) : "";
612
+ const timeValueRangeTo =
613
+ selectedRange?.to && showTime ? toTimeString(selectedRange.to) : "";
614
+
615
+ const timeMinuteStep = useMemo(() => {
616
+ if (!timeStep || !Number.isFinite(timeStep) || timeStep <= 0) return 1;
617
+ return Math.max(1, Math.round(timeStep / 60));
618
+ }, [timeStep]);
619
+
620
+ const resolvedTimePickerPopover = useMemo(
621
+ () => ({
622
+ renderOnPortal: false,
623
+ closeOnPathChange: false,
624
+ closeOnClickOutside: true,
625
+ position: timePickerPopover?.position ?? "bottom",
626
+ align: timePickerPopover?.align ?? "start",
627
+ offset: 2,
628
+ }),
629
+ [timePickerPopover?.align, timePickerPopover?.position],
630
+ );
631
+
632
+ const isIndependentRangeMonths = isRange && resolvedRangeMonthCount > 1;
633
+
634
+ return (
635
+ <div className={clsx(sCommon.container, className)} style={style}>
636
+ {label && (
637
+ <div
638
+ className={clsx(
639
+ sCommon.field_container,
640
+ layout !== "default" && sCommon[layout],
641
+ layout !== "default" &&
642
+ layoutDirection !== "default" &&
643
+ sCommon[layoutDirection],
644
+ )}
645
+ >
646
+ <label className={sCommon.label} htmlFor={inputId}>
647
+ {label}
648
+ </label>
649
+ </div>
650
+ )}
651
+
652
+ <Popover
653
+ closeOnPathChange={popover?.closeOnPathChange ?? true}
654
+ closeOnClickOutside={popover?.closeOnClickOutside ?? true}
655
+ animationDuration={popover?.animationDuration ?? 200}
656
+ position={popover?.position ?? "bottom"}
657
+ align={popover?.align ?? "start"}
658
+ isOpen={isOpen}
659
+ setIsOpen={setIsOpen}
660
+ className={clsx(sPickerCommon.picker_popover, popoverClassName)}
661
+ offset={popoverOffset}
662
+ {...popoverRest}
663
+ onClose={handlePopoverClose}
664
+ >
665
+ <PopoverTrigger
666
+ asChild
667
+ className={clsx(
668
+ sPickerCommon.picker_popover_trigger,
669
+ popoverTriggerClassName,
670
+ )}
671
+ style={popoverTriggerStyle}
672
+ disabled={disabled || readOnly}
673
+ {...popoverTriggerRest}
674
+ >
675
+ <SelectButton
676
+ id={inputId}
677
+ disabled={disabled || readOnly}
678
+ required={required}
679
+ className={clsx(
680
+ !hasValue && sPickerCommon.picker_value_placeholder,
681
+ selectButton?.className,
682
+ )}
683
+ style={selectButton?.style}
684
+ buttonVariant={selectButton?.buttonVariant}
685
+ buttonSize={selectButton?.buttonSize}
686
+ tooltip={selectButton?.tooltip}
687
+ tooltipPosition={selectButton?.tooltipPosition}
688
+ showClear={hasValue && showClear && !disabled && !readOnly}
689
+ onClear={(e: React.MouseEvent) => {
690
+ e.stopPropagation();
691
+ e.preventDefault();
692
+ commit(undefined);
693
+ onClear?.();
694
+ }}
695
+ showChevron={false}
696
+ iconEnd={
697
+ <Icon aria-hidden="true" clickable>
698
+ <IconCalendar/>
699
+ </Icon>
700
+ }
701
+ >
702
+ {displayText}
703
+ </SelectButton>
704
+ </PopoverTrigger>
705
+
706
+ {!disabled && !readOnly && (
707
+ <PopoverMenu
708
+ className={clsx(
709
+ sPickerCommon.picker_popover_menu_base,
710
+ s.popover_menu,
711
+ popoverMenuClassName,
712
+ )}
713
+ style={popoverMenuStyle}
714
+ {...popoverMenuRest}
715
+ >
716
+ <div className={s.menu_content}>
717
+ {showManualInput && (
718
+ <div className={s.manual_panel}>
719
+ {isRange ? (
720
+ <div className={s.manual_grid}>
721
+ <Input
722
+ type="text"
723
+ value={rangeManualText.from}
724
+ placeholder={`${startPlaceholder} (YYYY-MM-DD)`}
725
+ required={required}
726
+ autoComplete="off"
727
+ onChange={(event) =>
728
+ handleRangeDateInputChange("from", event)
729
+ }
730
+ onBlur={handleRangeDateInputBlur}
731
+ onKeyDown={handleManualInputKeyDown}
732
+ />
733
+ <Input
734
+ type="text"
735
+ value={rangeManualText.to}
736
+ placeholder={`${endPlaceholder} (YYYY-MM-DD)`}
737
+ required={required}
738
+ autoComplete="off"
739
+ onChange={(event) =>
740
+ handleRangeDateInputChange("to", event)
741
+ }
742
+ onBlur={handleRangeDateInputBlur}
743
+ onKeyDown={handleManualInputKeyDown}
744
+ />
745
+ </div>
746
+ ) : (
747
+ <Input
748
+ type="text"
749
+ value={singleManualText}
750
+ placeholder="YYYY-MM-DD"
751
+ required={required}
752
+ autoComplete="off"
753
+ onChange={handleSingleDateInputChange}
754
+ onBlur={handleSingleDateInputBlur}
755
+ onKeyDown={handleManualInputKeyDown}
756
+ />
757
+ )}
758
+ </div>
759
+ )}
760
+
761
+ <div className={s.calendar_wrap}>
762
+ {isRange ? (
763
+ isIndependentRangeMonths ? (
764
+ <div className={s.range_calendars}>
765
+ {Array.from({ length: resolvedRangeMonthCount }, (_, index) => {
766
+ const month =
767
+ rangeCalendarMonths[index] ??
768
+ addMonthOffset(
769
+ rangeCalendarMonths[0] ?? toMonthStart(new Date()),
770
+ index,
771
+ );
772
+
773
+ return (
774
+ <Calendar
775
+ key={`range-month-panel-${index}`}
776
+ {...calendar}
777
+ className={clsx(s.calendar, calendar?.className)}
778
+ mode="range"
779
+ selected={
780
+ selectedRange
781
+ ? { from: selectedRange.from, to: selectedRange.to }
782
+ : undefined
783
+ }
784
+ onSelect={(next: RDPDateRange | undefined) =>
785
+ handleRangeSelect(
786
+ next ? { from: next.from, to: next.to } : undefined,
787
+ )
788
+ }
789
+ month={month}
790
+ onMonthChange={(nextMonth) =>
791
+ handleRangePanelMonthChange(index, nextMonth)
792
+ }
793
+ numberOfMonths={1}
794
+ weekStartsOn={weekStartsOn}
795
+ showWeekNumber={
796
+ showWeekNumber || calendar?.showWeekNumber
797
+ }
798
+ captionLayout={calendar?.captionLayout ?? captionLayout}
799
+ disabled={disabledMatcher}
800
+ required={required as false}
801
+ />
802
+ );
803
+ })}
804
+ </div>
805
+ ) : (
806
+ <Calendar
807
+ {...calendar}
808
+ className={clsx(s.calendar, calendar?.className)}
809
+ mode="range"
810
+ selected={
811
+ selectedRange
812
+ ? { from: selectedRange.from, to: selectedRange.to }
813
+ : undefined
814
+ }
815
+ onSelect={(next: RDPDateRange | undefined) =>
816
+ handleRangeSelect(
817
+ next ? { from: next.from, to: next.to } : undefined,
818
+ )
819
+ }
820
+ month={calendarMonth}
821
+ onMonthChange={(nextMonth) =>
822
+ setCalendarMonth(toMonthStart(nextMonth))
823
+ }
824
+ defaultMonth={
825
+ calendar?.defaultMonth ??
826
+ (selectedRange?.from ?? selectedRange?.to)
827
+ }
828
+ numberOfMonths={1}
829
+ weekStartsOn={weekStartsOn}
830
+ showWeekNumber={showWeekNumber || calendar?.showWeekNumber}
831
+ captionLayout={calendar?.captionLayout ?? captionLayout}
832
+ disabled={disabledMatcher}
833
+ required={required as false}
834
+ />
835
+ )
836
+ ) : (
837
+ <Calendar
838
+ {...calendar}
839
+ className={clsx(s.calendar, calendar?.className)}
840
+ mode="single"
841
+ selected={selectedSingle}
842
+ onSelect={handleSingleSelect}
843
+ month={calendarMonth}
844
+ onMonthChange={(nextMonth) =>
845
+ setCalendarMonth(toMonthStart(nextMonth))
846
+ }
847
+ defaultMonth={calendar?.defaultMonth ?? selectedSingle}
848
+ numberOfMonths={
849
+ numberOfMonths ?? calendar?.numberOfMonths ?? 1
850
+ }
851
+ weekStartsOn={weekStartsOn}
852
+ showWeekNumber={showWeekNumber || calendar?.showWeekNumber}
853
+ captionLayout={calendar?.captionLayout ?? captionLayout}
854
+ disabled={disabledMatcher}
855
+ required={required as false}
856
+ />
857
+ )}
858
+ </div>
859
+
860
+ {showTime && (
861
+ <div className={s.time_panel}>
862
+ {isRange ? (
863
+ <div className={s.range_time_grid}>
864
+ <TimePicker
865
+ placeholder={startPlaceholder}
866
+ value={timeValueRangeFrom || undefined}
867
+ minuteStep={timeMinuteStep}
868
+ timeFormat={timeFormat}
869
+ required={required}
870
+ disabled={!selectedRange?.from}
871
+ showClear={false}
872
+ showActionFooter={false}
873
+ onChange={(value) => handleRangeTimeChange("from", value)}
874
+ popover={resolvedTimePickerPopover}
875
+ selectButton={{ buttonVariant: "none" }}
876
+ />
877
+ <TimePicker
878
+ placeholder={endPlaceholder}
879
+ value={timeValueRangeTo || undefined}
880
+ minuteStep={timeMinuteStep}
881
+ timeFormat={timeFormat}
882
+ required={required}
883
+ disabled={!selectedRange?.to}
884
+ showClear={false}
885
+ showActionFooter={false}
886
+ onChange={(value) => handleRangeTimeChange("to", value)}
887
+ popover={resolvedTimePickerPopover}
888
+ selectButton={{ buttonVariant: "none" }}
889
+ />
890
+ </div>
891
+ ) : (
892
+ <TimePicker
893
+ placeholder="Time"
894
+ value={timeValueSingle || undefined}
895
+ minuteStep={timeMinuteStep}
896
+ timeFormat={timeFormat}
897
+ required={required}
898
+ disabled={!selectedSingle}
899
+ showClear={false}
900
+ showActionFooter={false}
901
+ onChange={handleSingleTimeChange}
902
+ popover={resolvedTimePickerPopover}
903
+ selectButton={{ buttonVariant: "none" }}
904
+ />
905
+ )}
906
+ </div>
907
+ )}
908
+
909
+ {showActionFooter && (
910
+ <div className={sPickerCommon.picker_actions_footer}>
911
+ <Button
912
+ type="button"
913
+ variant="ghost"
914
+ size="sm"
915
+ disabled={!hasValue}
916
+ onClick={() => {
917
+ if (disabled || readOnly) return;
918
+ commit(undefined);
919
+ onClear?.();
920
+ }}
921
+ >
922
+ Clear
923
+ </Button>
924
+
925
+ <Button
926
+ type="button"
927
+ variant="primary"
928
+ size="sm"
929
+ onClick={() => {
930
+ setIsOpen(false);
931
+ }}
932
+ >
933
+ Done
934
+ </Button>
935
+ </div>
936
+ )}
937
+ </div>
938
+ </PopoverMenu>
939
+ )}
940
+ </Popover>
941
+
942
+ <FieldMessage errors={errors} info={info} />
943
+ </div>
944
+ );
945
+ };