@ttt-productions/ui-core 0.11.18 → 0.11.20

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 (60) hide show
  1. package/dist/react/components/date-time-picker.d.ts +43 -0
  2. package/dist/react/components/date-time-picker.d.ts.map +1 -0
  3. package/dist/react/components/date-time-picker.js +188 -0
  4. package/dist/react/components/date-time-picker.js.map +1 -0
  5. package/dist/react/index.d.ts +2 -0
  6. package/dist/react/index.d.ts.map +1 -1
  7. package/dist/react/index.js +1 -0
  8. package/dist/react/index.js.map +1 -1
  9. package/package.json +4 -6
  10. package/src/index.ts +0 -3
  11. package/src/lib/format-numbers.ts +0 -17
  12. package/src/lib/utils.ts +0 -9
  13. package/src/react/components/accordion.tsx +0 -53
  14. package/src/react/components/alert-dialog.tsx +0 -131
  15. package/src/react/components/alert.tsx +0 -59
  16. package/src/react/components/avatar.tsx +0 -51
  17. package/src/react/components/badge.tsx +0 -31
  18. package/src/react/components/button.tsx +0 -102
  19. package/src/react/components/card.tsx +0 -54
  20. package/src/react/components/checkbox.tsx +0 -28
  21. package/src/react/components/chunk-error-recovery.tsx +0 -57
  22. package/src/react/components/collapsible.tsx +0 -11
  23. package/src/react/components/consequence-dialog.tsx +0 -282
  24. package/src/react/components/date-picker.tsx +0 -567
  25. package/src/react/components/dialog.tsx +0 -114
  26. package/src/react/components/dropdown-menu.tsx +0 -200
  27. package/src/react/components/end-of-list-indicator.tsx +0 -27
  28. package/src/react/components/form.tsx +0 -174
  29. package/src/react/components/input.tsx +0 -23
  30. package/src/react/components/label.tsx +0 -21
  31. package/src/react/components/layout/screen-adaptive-view.tsx +0 -106
  32. package/src/react/components/list-pagination.tsx +0 -101
  33. package/src/react/components/menubar.tsx +0 -220
  34. package/src/react/components/popover.tsx +0 -31
  35. package/src/react/components/progress.tsx +0 -27
  36. package/src/react/components/radio-group.tsx +0 -36
  37. package/src/react/components/relative-time.tsx +0 -56
  38. package/src/react/components/scroll-area.tsx +0 -56
  39. package/src/react/components/scroll-to-top-button.tsx +0 -85
  40. package/src/react/components/search-dropdown.tsx +0 -233
  41. package/src/react/components/select.tsx +0 -165
  42. package/src/react/components/separator.tsx +0 -26
  43. package/src/react/components/sheet.tsx +0 -140
  44. package/src/react/components/skeleton.tsx +0 -16
  45. package/src/react/components/slider.tsx +0 -25
  46. package/src/react/components/spinner.tsx +0 -49
  47. package/src/react/components/switch.tsx +0 -41
  48. package/src/react/components/table.tsx +0 -121
  49. package/src/react/components/tabs.tsx +0 -59
  50. package/src/react/components/textarea.tsx +0 -22
  51. package/src/react/components/toast.tsx +0 -193
  52. package/src/react/components/toaster.tsx +0 -43
  53. package/src/react/components/tooltip.tsx +0 -28
  54. package/src/react/hooks/use-async-action.ts +0 -63
  55. package/src/react/hooks/use-cursor-page.ts +0 -139
  56. package/src/react/hooks/use-media-query.tsx +0 -26
  57. package/src/react/hooks/use-paged-list.ts +0 -106
  58. package/src/react/hooks/use-return-scroll.ts +0 -149
  59. package/src/react/hooks/use-toast.tsx +0 -176
  60. package/src/react/index.ts +0 -85
@@ -1,567 +0,0 @@
1
- "use client"
2
-
3
- import * as React from "react"
4
- import { ChevronLeft, ChevronRight } from "lucide-react"
5
- import { Button } from "./button.js"
6
- import { cn } from "../../lib/utils.js"
7
-
8
- export interface DatePickerProps {
9
- selected?: Date
10
- onSelect?: (date: Date | undefined) => void
11
- disabled?: (date: Date) => boolean
12
- disablePast?: boolean
13
- disableFuture?: boolean
14
- className?: string
15
- }
16
-
17
- const MONTHS = [
18
- "January", "February", "March", "April", "May", "June",
19
- "July", "August", "September", "October", "November", "December",
20
- ]
21
-
22
- const MONTHS_SHORT = [
23
- "Jan", "Feb", "Mar", "Apr", "May", "Jun",
24
- "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
25
- ]
26
-
27
- const DAYS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"]
28
- const DAY_NAMES_LONG = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
29
-
30
- const YEARS_PER_PAGE = 12
31
- /** Six rows × seven columns — a calendar month never spans more, and always
32
- * filling six rows keeps the day grid (and therefore the whole popover
33
- * footprint) the same height for every month. */
34
- const CALENDAR_CELLS = 42
35
-
36
- type View = "days" | "months" | "years"
37
- type PendingFocus = "day" | "monthTrigger" | "yearTrigger" | "monthGrid" | "yearGrid"
38
-
39
- const startOfDay = (d: Date) => {
40
- const x = new Date(d.getFullYear(), d.getMonth(), d.getDate())
41
- return x
42
- }
43
- const monthStart = (year: number, month: number) => new Date(year, month, 1)
44
- const addMonths = (d: Date, n: number) => new Date(d.getFullYear(), d.getMonth() + n, 1)
45
- const daysInMonth = (year: number, month: number) => new Date(year, month + 1, 0).getDate()
46
- const isSameDay = (a: Date, b: Date) =>
47
- a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate()
48
- const isSameMonth = (a: Date, b: Date) =>
49
- a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth()
50
- const fullDateLabel = (date: Date) => `${MONTHS[date.getMonth()]} ${date.getDate()}, ${date.getFullYear()}`
51
-
52
- /**
53
- * Generic date-of-birth-friendly calendar.
54
- *
55
- * Stable footprint: the six-row day grid is always rendered, so the outer width
56
- * and content height never change; the month and year choosers render as an
57
- * overlay ON TOP of that grid rather than replacing it, so a Radix popover never
58
- * flips sides just because the internal view changed.
59
- *
60
- * Month and year are INDEPENDENT pickers: choosing a month keeps the current
61
- * year and returns to the day grid; choosing a year keeps the current month and
62
- * returns DIRECTLY to the day grid (it never opens the month grid). Only
63
- * choosing a day calls `onSelect` (which commits and, in a popover, closes).
64
- *
65
- * Keyboard: roving day focus with Arrow (±1 day / ±7 days), Home/End (within the
66
- * week), PageUp/PageDown (±1 month), and Enter/Space to select — no extra
67
- * dependency. Exactly one day sits in the tab order at a time.
68
- */
69
- function DatePicker({
70
- selected,
71
- onSelect,
72
- disabled,
73
- disablePast = false,
74
- disableFuture = false,
75
- className,
76
- }: DatePickerProps) {
77
- const today = React.useMemo(() => startOfDay(new Date()), [])
78
-
79
- const [viewDate, setViewDate] = React.useState(() =>
80
- monthStart((selected ?? today).getFullYear(), (selected ?? today).getMonth()),
81
- )
82
- const [view, setView] = React.useState<View>("days")
83
- const [yearPageStart, setYearPageStart] = React.useState(
84
- () => Math.floor((selected ?? today).getFullYear() / YEARS_PER_PAGE) * YEARS_PER_PAGE,
85
- )
86
- const [focusedDate, setFocusedDate] = React.useState(() => startOfDay(selected ?? today))
87
-
88
- const monthTriggerRef = React.useRef<HTMLButtonElement | null>(null)
89
- const yearTriggerRef = React.useRef<HTMLButtonElement | null>(null)
90
- const daysGridRef = React.useRef<HTMLDivElement | null>(null)
91
- const overlayRef = React.useRef<HTMLDivElement | null>(null)
92
- const pendingFocusRef = React.useRef<PendingFocus | null>(null)
93
-
94
- const isDateDisabled = (date: Date) => {
95
- const checkDate = startOfDay(date)
96
- if (disablePast && checkDate < today) return true
97
- if (disableFuture && checkDate > today) return true
98
- if (disabled && disabled(checkDate)) return true
99
- return false
100
- }
101
-
102
- /** A whole month is unpickable only when every day in it is out of range. */
103
- const isMonthDisabled = (year: number, month: number) => {
104
- if (disableFuture && new Date(year, month, 1) > today) return true
105
- if (disablePast && new Date(year, month + 1, 0) < today) return true
106
- return false
107
- }
108
-
109
- const isYearDisabled = (year: number) => {
110
- if (disableFuture && year > today.getFullYear()) return true
111
- if (disablePast && year < today.getFullYear()) return true
112
- return false
113
- }
114
-
115
- const rangeAllYearsDisabled = (from: number, to: number) => {
116
- for (let y = from; y <= to; y++) if (!isYearDisabled(y)) return false
117
- return true
118
- }
119
-
120
- const isSelectedDay = (date: Date) => (selected ? isSameDay(startOfDay(selected), startOfDay(date)) : false)
121
- const isTodayDate = (date: Date) => isSameDay(startOfDay(date), today)
122
-
123
- /** The best in-month day to seed roving focus: the selected day, then today,
124
- * then the first available day, then the 1st. */
125
- const pickInMonthFocus = (vd: Date): Date => {
126
- const year = vd.getFullYear()
127
- const month = vd.getMonth()
128
- if (selected) {
129
- const s = startOfDay(selected)
130
- if (isSameMonth(s, vd) && !isDateDisabled(s)) return s
131
- }
132
- if (isSameMonth(today, vd) && !isDateDisabled(today)) return new Date(today)
133
- const dim = daysInMonth(year, month)
134
- for (let d = 1; d <= dim; d++) {
135
- const cand = new Date(year, month, d)
136
- if (!isDateDisabled(cand)) return cand
137
- }
138
- return new Date(year, month, 1)
139
- }
140
-
141
- // Resynchronize the visible month/year (and roving focus) when the controlled
142
- // `selected` prop changes — e.g. the consumer's typed MM/DD/YYYY fields.
143
- const selectedTime = selected ? startOfDay(selected).getTime() : undefined
144
- React.useEffect(() => {
145
- if (selectedTime === undefined) return
146
- const d = new Date(selectedTime)
147
- setViewDate(monthStart(d.getFullYear(), d.getMonth()))
148
- setFocusedDate(startOfDay(d))
149
- }, [selectedTime])
150
-
151
- // Deliberate focus after every view transition — never left on document.body.
152
- React.useLayoutEffect(() => {
153
- const target = pendingFocusRef.current
154
- if (!target) return
155
- pendingFocusRef.current = null
156
- if (target === "monthTrigger") {
157
- monthTriggerRef.current?.focus()
158
- } else if (target === "yearTrigger") {
159
- yearTriggerRef.current?.focus()
160
- } else if (target === "day") {
161
- daysGridRef.current?.querySelector<HTMLButtonElement>('[data-roving="true"]')?.focus()
162
- } else if (target === "monthGrid" || target === "yearGrid") {
163
- const active = overlayRef.current?.querySelector<HTMLButtonElement>('[data-active="true"]:not([disabled])')
164
- const fallback = overlayRef.current?.querySelector<HTMLButtonElement>("button:not([disabled])")
165
- ;(active ?? fallback)?.focus()
166
- }
167
- })
168
-
169
- const generateCalendarDays = (): (Date | null)[] => {
170
- const year = viewDate.getFullYear()
171
- const month = viewDate.getMonth()
172
- const dim = daysInMonth(year, month)
173
- const firstDay = new Date(year, month, 1).getDay()
174
- const days: (Date | null)[] = []
175
- for (let i = 0; i < firstDay; i++) days.push(null)
176
- for (let day = 1; day <= dim; day++) days.push(new Date(year, month, day))
177
- while (days.length < CALENDAR_CELLS) days.push(null)
178
- return days
179
- }
180
-
181
- const inViewMonth = isSameMonth(focusedDate, viewDate)
182
- const rovingDate = inViewMonth ? focusedDate : pickInMonthFocus(viewDate)
183
-
184
- const commitDay = (date: Date) => {
185
- if (isDateDisabled(date)) return
186
- onSelect?.(date)
187
- }
188
-
189
- const moveFocusTo = (date: Date) => {
190
- const d = startOfDay(date)
191
- setFocusedDate(d)
192
- if (!isSameMonth(d, viewDate)) setViewDate(monthStart(d.getFullYear(), d.getMonth()))
193
- pendingFocusRef.current = "day"
194
- }
195
-
196
- /** From `origin`, walk `step` days at a time until an available day is found
197
- * (inclusive of `origin`). Returns null if none within `maxSteps`. */
198
- const firstAvailableFrom = (origin: Date, step: number, maxSteps: number): Date | null => {
199
- let d = startOfDay(origin)
200
- for (let i = 0; i < maxSteps; i++) {
201
- if (!isDateDisabled(d)) return d
202
- d = new Date(d.getFullYear(), d.getMonth(), d.getDate() + step)
203
- }
204
- return null
205
- }
206
-
207
- const handleDaysKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
208
- const base = rovingDate
209
- let candidate: Date
210
- let probeStep: number
211
-
212
- switch (e.key) {
213
- case "ArrowLeft":
214
- candidate = new Date(base.getFullYear(), base.getMonth(), base.getDate() - 1)
215
- probeStep = -1
216
- break
217
- case "ArrowRight":
218
- candidate = new Date(base.getFullYear(), base.getMonth(), base.getDate() + 1)
219
- probeStep = 1
220
- break
221
- case "ArrowUp":
222
- candidate = new Date(base.getFullYear(), base.getMonth(), base.getDate() - 7)
223
- probeStep = -1
224
- break
225
- case "ArrowDown":
226
- candidate = new Date(base.getFullYear(), base.getMonth(), base.getDate() + 7)
227
- probeStep = 1
228
- break
229
- case "Home":
230
- candidate = new Date(base.getFullYear(), base.getMonth(), base.getDate() - base.getDay())
231
- probeStep = 1
232
- break
233
- case "End":
234
- candidate = new Date(base.getFullYear(), base.getMonth(), base.getDate() + (6 - base.getDay()))
235
- probeStep = -1
236
- break
237
- case "PageUp": {
238
- const prev = addMonths(base, -1)
239
- candidate = new Date(
240
- prev.getFullYear(),
241
- prev.getMonth(),
242
- Math.min(base.getDate(), daysInMonth(prev.getFullYear(), prev.getMonth())),
243
- )
244
- probeStep = 1
245
- break
246
- }
247
- case "PageDown": {
248
- const next = addMonths(base, 1)
249
- candidate = new Date(
250
- next.getFullYear(),
251
- next.getMonth(),
252
- Math.min(base.getDate(), daysInMonth(next.getFullYear(), next.getMonth())),
253
- )
254
- probeStep = 1
255
- break
256
- }
257
- case "Enter":
258
- case " ":
259
- e.preventDefault()
260
- commitDay(base)
261
- return
262
- default:
263
- return
264
- }
265
-
266
- e.preventDefault()
267
- let target = firstAvailableFrom(candidate, probeStep, 62)
268
- if (!target && probeStep !== -1) target = firstAvailableFrom(candidate, -1, 62)
269
- if (target) moveFocusTo(target)
270
- }
271
-
272
- // Header prev/next adapt to the active view; disabled when the whole target
273
- // range is unavailable so navigation never lands on a dead page.
274
- const prevMonthDate = addMonths(viewDate, -1)
275
- const nextMonthDate = addMonths(viewDate, 1)
276
-
277
- let prevDisabled: boolean
278
- let nextDisabled: boolean
279
- let prevLabel: string
280
- let nextLabel: string
281
-
282
- if (view === "days") {
283
- prevDisabled = isMonthDisabled(prevMonthDate.getFullYear(), prevMonthDate.getMonth())
284
- nextDisabled = isMonthDisabled(nextMonthDate.getFullYear(), nextMonthDate.getMonth())
285
- prevLabel = "Previous month"
286
- nextLabel = "Next month"
287
- } else if (view === "months") {
288
- prevDisabled = isYearDisabled(viewDate.getFullYear() - 1)
289
- nextDisabled = isYearDisabled(viewDate.getFullYear() + 1)
290
- prevLabel = "Previous year"
291
- nextLabel = "Next year"
292
- } else {
293
- prevDisabled = rangeAllYearsDisabled(yearPageStart - YEARS_PER_PAGE, yearPageStart - 1)
294
- nextDisabled = rangeAllYearsDisabled(yearPageStart + YEARS_PER_PAGE, yearPageStart + YEARS_PER_PAGE * 2 - 1)
295
- prevLabel = `Previous ${YEARS_PER_PAGE} years`
296
- nextLabel = `Next ${YEARS_PER_PAGE} years`
297
- }
298
-
299
- const handlePrev = () => {
300
- if (view === "days") setViewDate(prevMonthDate)
301
- else if (view === "months") setViewDate(monthStart(viewDate.getFullYear() - 1, viewDate.getMonth()))
302
- else setYearPageStart((s) => s - YEARS_PER_PAGE)
303
- }
304
- const handleNext = () => {
305
- if (view === "days") setViewDate(nextMonthDate)
306
- else if (view === "months") setViewDate(monthStart(viewDate.getFullYear() + 1, viewDate.getMonth()))
307
- else setYearPageStart((s) => s + YEARS_PER_PAGE)
308
- }
309
-
310
- const openMonths = () => {
311
- if (view === "months") {
312
- setView("days")
313
- pendingFocusRef.current = "monthTrigger"
314
- } else {
315
- setView("months")
316
- pendingFocusRef.current = "monthGrid"
317
- }
318
- }
319
- const openYears = () => {
320
- if (view === "years") {
321
- setView("days")
322
- pendingFocusRef.current = "yearTrigger"
323
- } else {
324
- setYearPageStart(Math.floor(viewDate.getFullYear() / YEARS_PER_PAGE) * YEARS_PER_PAGE)
325
- setView("years")
326
- pendingFocusRef.current = "yearGrid"
327
- }
328
- }
329
-
330
- // Month selection: keep the year, return to days, focus the Month trigger.
331
- const handleMonthPick = (month: number) => {
332
- setViewDate(monthStart(viewDate.getFullYear(), month))
333
- setView("days")
334
- pendingFocusRef.current = "monthTrigger"
335
- }
336
- // Year selection: keep the month, return to days (never months), focus Year.
337
- const handleYearPick = (year: number) => {
338
- setViewDate(monthStart(year, viewDate.getMonth()))
339
- setView("days")
340
- pendingFocusRef.current = "yearTrigger"
341
- }
342
-
343
- const calendarDays = generateCalendarDays()
344
- const yearPage = Array.from({ length: YEARS_PER_PAGE }, (_, i) => yearPageStart + i)
345
- const overlayActive = view !== "days"
346
-
347
- const triggerClass =
348
- "min-h-[1.5rem] px-1 text-base font-bold text-foreground underline decoration-1 underline-offset-4 decoration-border rounded-sm " +
349
- "hover:opacity-80 transition-opacity focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
350
-
351
- const gridCellClass =
352
- "h-10 rounded-lg text-sm font-semibold transition-transform flex items-center justify-center " +
353
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
354
-
355
- return (
356
- <div className={cn("p-4 bg-card rounded-lg", className)}>
357
- {/* Header — same footprint in every view. */}
358
- <div className="flex items-center justify-between mb-4">
359
- <Button
360
- variant="ghost"
361
- size="icon"
362
- className="h-9 w-9 p-0"
363
- aria-label={prevLabel}
364
- disabled={prevDisabled}
365
- onClick={handlePrev}
366
- >
367
- <ChevronLeft className="h-4 w-4" aria-hidden="true" />
368
- </Button>
369
-
370
- <div className="flex items-center gap-1.5">
371
- <button
372
- ref={monthTriggerRef}
373
- type="button"
374
- onClick={openMonths}
375
- aria-expanded={view === "months"}
376
- aria-label={`Choose month, currently ${MONTHS[viewDate.getMonth()]}`}
377
- className={triggerClass}
378
- >
379
- {MONTHS[viewDate.getMonth()]}
380
- </button>
381
- <button
382
- ref={yearTriggerRef}
383
- type="button"
384
- onClick={openYears}
385
- aria-expanded={view === "years"}
386
- aria-label={`Choose year, currently ${viewDate.getFullYear()}`}
387
- className={triggerClass}
388
- >
389
- {viewDate.getFullYear()}
390
- </button>
391
- </div>
392
-
393
- <Button
394
- variant="ghost"
395
- size="icon"
396
- className="h-9 w-9 p-0"
397
- aria-label={nextLabel}
398
- disabled={nextDisabled}
399
- onClick={handleNext}
400
- >
401
- <ChevronRight className="h-4 w-4" aria-hidden="true" />
402
- </Button>
403
- </div>
404
-
405
- {/* One polite live region for the displayed month/year. */}
406
- <div aria-live="polite" aria-atomic="true" className="sr-only">
407
- {MONTHS[viewDate.getMonth()]} {viewDate.getFullYear()}
408
- </div>
409
-
410
- <div className="relative">
411
- {/* Day grid — ALWAYS rendered so width and height stay stable; inert
412
- while a chooser overlay covers it. */}
413
- <div aria-hidden={overlayActive ? true : undefined}>
414
- <div className="grid grid-cols-7 gap-1 mb-2">
415
- {DAYS.map((day, i) => (
416
- <div key={day} className="text-center text-sm font-bold py-2 text-foreground" aria-hidden="true">
417
- <span className="sr-only">{DAY_NAMES_LONG[i]}</span>
418
- <span aria-hidden="true">{day}</span>
419
- </div>
420
- ))}
421
- </div>
422
-
423
- <div
424
- ref={daysGridRef}
425
- className="grid grid-cols-7 gap-1"
426
- role="group"
427
- aria-label="Calendar"
428
- onKeyDown={overlayActive ? undefined : handleDaysKeyDown}
429
- >
430
- {calendarDays.map((date, index) => {
431
- if (!date) return <div key={`empty-${index}`} className="h-9" aria-hidden="true" />
432
-
433
- const dateDisabled = isDateDisabled(date)
434
- const dateSelected = isSelectedDay(date)
435
- const todayDate = isTodayDate(date)
436
- const roving = isSameDay(date, rovingDate)
437
- const tabIndex = !overlayActive && roving ? 0 : -1
438
-
439
- return (
440
- <button
441
- key={date.toISOString()}
442
- type="button"
443
- onClick={() => commitDay(date)}
444
- disabled={dateDisabled}
445
- tabIndex={tabIndex}
446
- data-roving={roving ? "true" : undefined}
447
- aria-label={fullDateLabel(date)}
448
- aria-pressed={dateSelected}
449
- aria-current={todayDate ? "date" : undefined}
450
- className={cn(
451
- "h-9 w-9 rounded-lg text-sm font-semibold transition-transform",
452
- "motion-safe:hover:scale-105 motion-safe:active:scale-95",
453
- "flex items-center justify-center",
454
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
455
- !dateDisabled && !dateSelected && !todayDate && "text-foreground hover:bg-primary/10",
456
- todayDate && !dateSelected && "bg-accent text-accent-foreground font-bold ring-2 ring-border",
457
- dateSelected && "bg-primary text-primary-foreground font-bold",
458
- dateDisabled && "opacity-30 cursor-not-allowed motion-safe:hover:scale-100 hover:bg-transparent relative",
459
- )}
460
- >
461
- {date.getDate()}
462
- {dateDisabled && (
463
- <span className="absolute inset-0 flex items-center justify-center" aria-hidden="true">
464
- <span className="w-px h-full bg-current transform rotate-45" />
465
- </span>
466
- )}
467
- </button>
468
- )
469
- })}
470
- </div>
471
-
472
- <div className="mt-4 pt-3 border-t border-border text-xs space-y-1">
473
- <div className="flex items-center gap-2">
474
- <div className="h-3 w-3 rounded-sm bg-primary" aria-hidden="true" />
475
- <span className="text-muted-foreground">Selected</span>
476
- </div>
477
- <div className="flex items-center gap-2">
478
- <div className="h-3 w-3 rounded-sm bg-accent ring-2 ring-border" aria-hidden="true" />
479
- <span className="text-muted-foreground">Today</span>
480
- </div>
481
- {(disablePast || disableFuture || disabled) && (
482
- <div className="flex items-center gap-2">
483
- <div className="h-3 w-3 rounded-sm bg-muted opacity-30 relative" aria-hidden="true">
484
- <div className="absolute inset-0 flex items-center justify-center">
485
- <div className="w-px h-full bg-foreground transform rotate-45" />
486
- </div>
487
- </div>
488
- <span className="text-muted-foreground">Unavailable</span>
489
- </div>
490
- )}
491
- </div>
492
- </div>
493
-
494
- {/* Month / year choosers overlay the day grid — same footprint. */}
495
- {overlayActive && (
496
- <div ref={overlayRef} className="absolute inset-0 bg-card rounded-lg flex flex-col justify-center">
497
- {view === "years" && (
498
- <div className="mb-2 text-center text-sm font-semibold text-muted-foreground" aria-hidden="true">
499
- {yearPageStart} – {yearPageStart + YEARS_PER_PAGE - 1}
500
- </div>
501
- )}
502
-
503
- {view === "months" && (
504
- <div className="grid grid-cols-3 gap-1">
505
- {MONTHS_SHORT.map((label, month) => {
506
- const monthDisabled = isMonthDisabled(viewDate.getFullYear(), month)
507
- const isViewMonth = month === viewDate.getMonth()
508
- return (
509
- <button
510
- key={label}
511
- type="button"
512
- disabled={monthDisabled}
513
- data-active={isViewMonth ? "true" : undefined}
514
- aria-pressed={isViewMonth}
515
- aria-label={`${MONTHS[month]} ${viewDate.getFullYear()}`}
516
- onClick={() => handleMonthPick(month)}
517
- className={cn(
518
- gridCellClass,
519
- !monthDisabled && !isViewMonth && "text-foreground hover:bg-primary/10",
520
- isViewMonth && "bg-primary text-primary-foreground font-bold",
521
- monthDisabled && "opacity-30 cursor-not-allowed",
522
- )}
523
- >
524
- {label}
525
- </button>
526
- )
527
- })}
528
- </div>
529
- )}
530
-
531
- {view === "years" && (
532
- <div className="grid grid-cols-3 gap-1">
533
- {yearPage.map((year) => {
534
- const yearDisabled = isYearDisabled(year)
535
- const isViewYear = year === viewDate.getFullYear()
536
- return (
537
- <button
538
- key={year}
539
- type="button"
540
- disabled={yearDisabled}
541
- data-active={isViewYear ? "true" : undefined}
542
- aria-pressed={isViewYear}
543
- aria-label={`Year ${year}`}
544
- onClick={() => handleYearPick(year)}
545
- className={cn(
546
- gridCellClass,
547
- !yearDisabled && !isViewYear && "text-foreground hover:bg-primary/10",
548
- isViewYear && "bg-primary text-primary-foreground font-bold",
549
- yearDisabled && "opacity-30 cursor-not-allowed",
550
- )}
551
- >
552
- {year}
553
- </button>
554
- )
555
- })}
556
- </div>
557
- )}
558
- </div>
559
- )}
560
- </div>
561
- </div>
562
- )
563
- }
564
-
565
- DatePicker.displayName = "DatePicker"
566
-
567
- export { DatePicker }
@@ -1,114 +0,0 @@
1
- "use client"
2
-
3
- import * as React from "react"
4
- import * as DialogPrimitive from "@radix-ui/react-dialog"
5
- import { X } from "lucide-react"
6
-
7
- import { cn } from "../../lib/utils.js"
8
-
9
- const Dialog = DialogPrimitive.Root
10
- const DialogTrigger = DialogPrimitive.Trigger
11
- const DialogPortal = DialogPrimitive.Portal
12
- const DialogClose = DialogPrimitive.Close
13
-
14
- const DialogOverlay = React.forwardRef<
15
- React.ElementRef<typeof DialogPrimitive.Overlay>,
16
- React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
17
- >(({ className, ...props }, ref) => (
18
- <DialogPrimitive.Overlay
19
- ref={ref}
20
- className={cn(
21
- "fixed inset-0 z-50 bg-[color:var(--scrim)] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
22
- className
23
- )}
24
- {...props}
25
- />
26
- ))
27
- DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
28
-
29
- const DialogContent = React.forwardRef<
30
- React.ElementRef<typeof DialogPrimitive.Content>,
31
- React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
32
- >(({ className, children, ...props }, ref) => (
33
- <DialogPortal>
34
- <DialogOverlay />
35
- <DialogPrimitive.Content
36
- ref={ref}
37
- className={cn(
38
- "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-card p-6 elevation-raised duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
39
- "max-h-[90vh] overflow-y-auto [&>*]:min-w-0",
40
- className
41
- )}
42
- {...props}
43
- >
44
- {children}
45
- <DialogPrimitive.Close className="absolute right-4 top-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
46
- <X className="h-4 w-4" />
47
- <span className="sr-only">Close</span>
48
- </DialogPrimitive.Close>
49
- </DialogPrimitive.Content>
50
- </DialogPortal>
51
- ))
52
- DialogContent.displayName = DialogPrimitive.Content.displayName
53
-
54
- const DialogHeader = ({
55
- className,
56
- ...props
57
- }: React.HTMLAttributes<HTMLDivElement>) => (
58
- <div
59
- className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)}
60
- {...props}
61
- />
62
- )
63
- DialogHeader.displayName = "DialogHeader"
64
-
65
- const DialogFooter = ({
66
- className,
67
- ...props
68
- }: React.HTMLAttributes<HTMLDivElement>) => (
69
- <div
70
- className={cn(
71
- "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
72
- className
73
- )}
74
- {...props}
75
- />
76
- )
77
- DialogFooter.displayName = "DialogFooter"
78
-
79
- const DialogTitle = React.forwardRef<
80
- React.ElementRef<typeof DialogPrimitive.Title>,
81
- React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
82
- >(({ className, ...props }, ref) => (
83
- <DialogPrimitive.Title
84
- ref={ref}
85
- className={cn("text-lg font-semibold leading-none tracking-tight break-words", className)}
86
- {...props}
87
- />
88
- ))
89
- DialogTitle.displayName = DialogPrimitive.Title.displayName
90
-
91
- const DialogDescription = React.forwardRef<
92
- React.ElementRef<typeof DialogPrimitive.Description>,
93
- React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
94
- >(({ className, ...props }, ref) => (
95
- <DialogPrimitive.Description
96
- ref={ref}
97
- className={cn("text-sm text-muted-foreground break-words", className)}
98
- {...props}
99
- />
100
- ))
101
- DialogDescription.displayName = DialogPrimitive.Description.displayName
102
-
103
- export {
104
- Dialog,
105
- DialogPortal,
106
- DialogOverlay,
107
- DialogClose,
108
- DialogTrigger,
109
- DialogContent,
110
- DialogHeader,
111
- DialogFooter,
112
- DialogTitle,
113
- DialogDescription,
114
- }