banhaten 0.1.0 → 0.1.2

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 (81) hide show
  1. package/README.md +21 -9
  2. package/package.json +8 -2
  3. package/registry/components/accordion.tsx +37 -1
  4. package/registry/components/alert.tsx +14 -28
  5. package/registry/components/attribute.tsx +6 -10
  6. package/registry/components/autocomplete.tsx +637 -0
  7. package/registry/components/avatar.tsx +259 -24
  8. package/registry/components/badge.tsx +97 -35
  9. package/registry/components/button-group.tsx +1 -1
  10. package/registry/components/card.tsx +1 -1
  11. package/registry/components/checkbox.tsx +19 -16
  12. package/registry/components/date-picker-state.ts +253 -0
  13. package/registry/components/date-picker.tsx +115 -158
  14. package/registry/components/expanded/ActivityFeed.tsx +37 -23
  15. package/registry/components/expanded/Banner.tsx +54 -19
  16. package/registry/components/expanded/Breadcrumbs.tsx +10 -38
  17. package/registry/components/expanded/CatalogComponentsShowcase.tsx +11 -16
  18. package/registry/components/expanded/CatalogTag.tsx +4 -11
  19. package/registry/components/expanded/CommandBar.tsx +33 -53
  20. package/registry/components/expanded/EmptyState.tsx +155 -0
  21. package/registry/components/expanded/FileUpload.tsx +362 -59
  22. package/registry/components/expanded/OnboardingStepListItem.tsx +6 -10
  23. package/registry/components/expanded/PageHeader.tsx +2 -11
  24. package/registry/components/expanded/Slideout.tsx +12 -23
  25. package/registry/components/expanded/Steps.tsx +6 -8
  26. package/registry/components/expanded/Table.tsx +18 -40
  27. package/registry/components/expanded/Timeline.tsx +5 -24
  28. package/registry/components/expanded/activityFeed.css +10 -54
  29. package/registry/components/expanded/banner.css +8 -75
  30. package/registry/components/expanded/breadcrumbs.css +1 -1
  31. package/registry/components/expanded/commandBar.css +23 -26
  32. package/registry/components/expanded/divider.css +1 -1
  33. package/registry/components/expanded/emptyState.css +111 -0
  34. package/registry/components/expanded/fileUpload.css +304 -75
  35. package/registry/components/expanded/pageHeader.css +1 -1
  36. package/registry/components/expanded/slideout.css +1 -0
  37. package/registry/components/expanded/steps.css +15 -51
  38. package/registry/components/expanded/table.css +6 -1
  39. package/registry/components/expanded/timeline.css +18 -15
  40. package/registry/components/input-otp.tsx +574 -0
  41. package/registry/components/input.tsx +140 -59
  42. package/registry/components/menu.tsx +470 -80
  43. package/registry/components/pagination.tsx +6 -18
  44. package/registry/components/popover.tsx +840 -0
  45. package/registry/components/radio-card.tsx +25 -31
  46. package/registry/components/select-content.tsx +28 -123
  47. package/registry/components/select.tsx +13 -9
  48. package/registry/components/skeleton.css +57 -0
  49. package/registry/components/skeleton.tsx +482 -0
  50. package/registry/components/social-button.tsx +24 -90
  51. package/registry/components/spinner.tsx +91 -7
  52. package/registry/components/textarea.tsx +21 -36
  53. package/registry/components/toggle.tsx +7 -23
  54. package/registry/components/tooltip.tsx +8 -4
  55. package/registry/examples/attribute-demo.tsx +2 -2
  56. package/registry/examples/autocomplete-demo.tsx +109 -0
  57. package/registry/examples/avatar-demo.tsx +102 -47
  58. package/registry/examples/badge-demo.tsx +16 -0
  59. package/registry/examples/checkbox-demo.tsx +3 -8
  60. package/registry/examples/date-picker-demo.tsx +75 -22
  61. package/registry/examples/expanded/banner-demo.tsx +31 -6
  62. package/registry/examples/expanded/breadcrumbs-demo.tsx +59 -0
  63. package/registry/examples/expanded/command-bar-demo.tsx +236 -0
  64. package/registry/examples/expanded/empty-state-demo.tsx +39 -0
  65. package/registry/examples/expanded/file-upload-demo.tsx +60 -0
  66. package/registry/examples/expanded/steps-demo.tsx +11 -0
  67. package/registry/examples/expanded/table-demo.tsx +142 -0
  68. package/registry/examples/input-demo.tsx +1 -1
  69. package/registry/examples/input-otp-demo.tsx +72 -0
  70. package/registry/examples/menu-demo.tsx +101 -88
  71. package/registry/examples/popover-demo.tsx +546 -0
  72. package/registry/examples/progress-demo.tsx +2 -2
  73. package/registry/examples/select-demo.tsx +32 -18
  74. package/registry/examples/skeleton-demo.tsx +56 -0
  75. package/registry/examples/social-button-demo.tsx +33 -33
  76. package/registry/examples/spinner-demo.tsx +59 -0
  77. package/registry/examples/tag-demo.tsx +1 -1
  78. package/registry/examples/textarea-demo.tsx +1 -1
  79. package/registry/index.json +266 -20
  80. package/registry/styles/globals.css +93 -3
  81. package/src/cli/index.js +997 -62
@@ -0,0 +1,253 @@
1
+ export type DatePickerDirection = "ltr" | "rtl" | "auto"
2
+ export type CalendarView = "days" | "month-year"
3
+ export type CalendarMode = "single" | "range"
4
+ export type RangeCalendarType = "single" | "double" | "double-with-presets"
5
+
6
+ export type CalendarRange = {
7
+ from?: Date
8
+ to?: Date
9
+ }
10
+
11
+ export type CalendarCell = {
12
+ date: Date
13
+ weekday: number
14
+ }
15
+
16
+ export type CalendarDayStateName =
17
+ | "default"
18
+ | "event"
19
+ | "outside"
20
+ | "range"
21
+ | "selected"
22
+
23
+ export type CalendarDayState = {
24
+ hasEvent: boolean
25
+ inRange: boolean
26
+ outside: boolean
27
+ rangeEndpoint: boolean
28
+ selected: boolean
29
+ state: CalendarDayStateName
30
+ }
31
+
32
+ export function getCalendarCells(month: Date): CalendarCell[] {
33
+ const first = startOfMonth(month)
34
+ const start = addDays(first, -first.getDay())
35
+ const last = new Date(first.getFullYear(), first.getMonth() + 1, 0)
36
+ const end = addDays(last, 6 - last.getDay())
37
+ const dayCount = differenceInCalendarDays(end, start) + 1
38
+
39
+ return Array.from({ length: dayCount }, (_, index) => {
40
+ const date = addDays(start, index)
41
+ return {
42
+ date,
43
+ weekday: date.getDay(),
44
+ }
45
+ })
46
+ }
47
+
48
+ export function getCalendarDayState({
49
+ date,
50
+ displayMonth,
51
+ eventDates,
52
+ mode,
53
+ range,
54
+ selectedDate,
55
+ }: {
56
+ date: Date
57
+ displayMonth: Date
58
+ eventDates?: Date[]
59
+ mode: CalendarMode
60
+ range?: CalendarRange
61
+ selectedDate?: Date
62
+ }): CalendarDayState {
63
+ const outside = !isSameMonth(date, displayMonth)
64
+ const hasEvent = eventDates?.some((eventDate) => isSameDay(eventDate, date)) ?? false
65
+ const selected =
66
+ mode === "single"
67
+ ? Boolean(selectedDate && isSameDay(selectedDate, date))
68
+ : Boolean(
69
+ range?.from &&
70
+ (isSameDay(range.from, date) ||
71
+ (range.to && isSameDay(range.to, date)))
72
+ )
73
+ const inRange =
74
+ mode === "range" &&
75
+ Boolean(
76
+ range?.from &&
77
+ range?.to &&
78
+ isAfterOrSameDay(date, range.from) &&
79
+ isBeforeOrSameDay(date, range.to)
80
+ )
81
+ const rangeEndpoint =
82
+ mode === "range" &&
83
+ Boolean(
84
+ range?.from &&
85
+ (isSameDay(range.from, date) ||
86
+ (range.to && isSameDay(range.to, date)))
87
+ )
88
+ const state = selected
89
+ ? "selected"
90
+ : inRange
91
+ ? "range"
92
+ : outside
93
+ ? "outside"
94
+ : hasEvent
95
+ ? "event"
96
+ : "default"
97
+
98
+ return {
99
+ hasEvent,
100
+ inRange,
101
+ outside,
102
+ rangeEndpoint,
103
+ selected,
104
+ state,
105
+ }
106
+ }
107
+
108
+ export function getCalendarKeyboardTarget(date: Date, key: string) {
109
+ switch (key) {
110
+ case "ArrowLeft":
111
+ return addDays(date, -1)
112
+ case "ArrowRight":
113
+ return addDays(date, 1)
114
+ case "ArrowUp":
115
+ return addDays(date, -7)
116
+ case "ArrowDown":
117
+ return addDays(date, 7)
118
+ case "Home":
119
+ return addDays(date, -date.getDay())
120
+ case "End":
121
+ return addDays(date, 6 - date.getDay())
122
+ case "PageUp":
123
+ return addCalendarMonthsPreservingDay(date, -1)
124
+ case "PageDown":
125
+ return addCalendarMonthsPreservingDay(date, 1)
126
+ default:
127
+ return undefined
128
+ }
129
+ }
130
+
131
+ export function getNextRange(currentRange: CalendarRange, date: Date): CalendarRange {
132
+ const nextDate = startOfDay(date)
133
+ const from = currentRange.from ? startOfDay(currentRange.from) : undefined
134
+ const to = currentRange.to ? startOfDay(currentRange.to) : undefined
135
+
136
+ if (!from || to) {
137
+ return { from: nextDate, to: undefined }
138
+ }
139
+
140
+ if (isBeforeDay(nextDate, from)) {
141
+ return { from: nextDate, to: from }
142
+ }
143
+
144
+ return { from, to: nextDate }
145
+ }
146
+
147
+ export function normalizeRange(range: CalendarRange): CalendarRange {
148
+ if (!range.from || !range.to) return range
149
+
150
+ return isBeforeDay(range.to, range.from)
151
+ ? { from: range.to, to: range.from }
152
+ : range
153
+ }
154
+
155
+ export function formatCalendarRange(range: CalendarRange) {
156
+ if (!range.from && !range.to) return undefined
157
+ if (range.from && !range.to) {
158
+ return `${formatCalendarDate(range.from, { month: "short" })} -`
159
+ }
160
+ if (!range.from && range.to) {
161
+ return `- ${formatCalendarDate(range.to, { month: "short" })}`
162
+ }
163
+
164
+ return `${formatCalendarDate(range.from, {
165
+ month: "short",
166
+ })} - ${formatCalendarDate(range.to, { month: "short" })}`
167
+ }
168
+
169
+ export function formatCalendarDate(
170
+ date: Date | undefined,
171
+ options?: { month?: "short" | "long" }
172
+ ) {
173
+ if (!date) return ""
174
+ const month = date.toLocaleString("en", { month: options?.month ?? "short" })
175
+ return `${month} ${date.getDate()}`
176
+ }
177
+
178
+ export function formatMonthTitle(date: Date) {
179
+ return `${date.toLocaleString("en", { month: "long" })} ${date.getFullYear()}`
180
+ }
181
+
182
+ export function toCalendarDateId(date: Date) {
183
+ const year = date.getFullYear()
184
+ const month = String(date.getMonth() + 1).padStart(2, "0")
185
+ const day = String(date.getDate()).padStart(2, "0")
186
+
187
+ return `${year}-${month}-${day}`
188
+ }
189
+
190
+ export function startOfDay(date: Date) {
191
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate())
192
+ }
193
+
194
+ export function startOfMonth(date: Date) {
195
+ return new Date(date.getFullYear(), date.getMonth(), 1)
196
+ }
197
+
198
+ export function addDays(date: Date, amount: number) {
199
+ const next = new Date(date)
200
+ next.setDate(next.getDate() + amount)
201
+ return startOfDay(next)
202
+ }
203
+
204
+ export function addMonths(date: Date, amount: number) {
205
+ return new Date(date.getFullYear(), date.getMonth() + amount, 1)
206
+ }
207
+
208
+ export function isSameMonth(left: Date, right: Date) {
209
+ return (
210
+ left.getFullYear() === right.getFullYear() &&
211
+ left.getMonth() === right.getMonth()
212
+ )
213
+ }
214
+
215
+ export function isSameDay(left: Date, right: Date) {
216
+ return startOfDay(left).getTime() === startOfDay(right).getTime()
217
+ }
218
+
219
+ export function isBeforeDay(left: Date, right: Date) {
220
+ return startOfDay(left).getTime() < startOfDay(right).getTime()
221
+ }
222
+
223
+ export function isAfterOrSameDay(left: Date, right: Date) {
224
+ return startOfDay(left).getTime() >= startOfDay(right).getTime()
225
+ }
226
+
227
+ export function isBeforeOrSameDay(left: Date, right: Date) {
228
+ return startOfDay(left).getTime() <= startOfDay(right).getTime()
229
+ }
230
+
231
+ function addCalendarMonthsPreservingDay(date: Date, amount: number) {
232
+ const targetMonth = new Date(date.getFullYear(), date.getMonth() + amount, 1)
233
+ const lastDay = new Date(
234
+ targetMonth.getFullYear(),
235
+ targetMonth.getMonth() + 1,
236
+ 0
237
+ ).getDate()
238
+
239
+ return startOfDay(
240
+ new Date(
241
+ targetMonth.getFullYear(),
242
+ targetMonth.getMonth(),
243
+ Math.min(date.getDate(), lastDay)
244
+ )
245
+ )
246
+ }
247
+
248
+ function differenceInCalendarDays(left: Date, right: Date) {
249
+ const dayLength = 24 * 60 * 60 * 1000
250
+ return Math.round(
251
+ (startOfDay(left).getTime() - startOfDay(right).getTime()) / dayLength
252
+ )
253
+ }
@@ -11,11 +11,28 @@ import {
11
11
  import { cva } from "class-variance-authority"
12
12
 
13
13
  import { cn } from "@/lib/utils"
14
-
15
- type DatePickerDirection = "ltr" | "rtl" | "auto"
16
- type CalendarView = "days" | "month-year"
17
- type CalendarMode = "single" | "range"
18
- type RangeCalendarType = "single" | "double" | "double-with-presets"
14
+ import {
15
+ addMonths,
16
+ formatCalendarDate,
17
+ formatCalendarRange,
18
+ formatMonthTitle,
19
+ getCalendarCells,
20
+ getCalendarDayState,
21
+ getCalendarKeyboardTarget,
22
+ getNextRange,
23
+ isSameMonth,
24
+ normalizeRange,
25
+ startOfMonth,
26
+ toCalendarDateId,
27
+ } from "./date-picker-state"
28
+ import type {
29
+ CalendarCell,
30
+ CalendarMode,
31
+ CalendarRange,
32
+ CalendarView,
33
+ DatePickerDirection,
34
+ RangeCalendarType,
35
+ } from "./date-picker-state"
19
36
 
20
37
  type CalendarTimeValue = {
21
38
  hours?: string
@@ -32,11 +49,6 @@ type CalendarMonthYearItem = {
32
49
  disabled?: boolean
33
50
  }
34
51
 
35
- type CalendarRange = {
36
- from?: Date
37
- to?: Date
38
- }
39
-
40
52
  type CalendarPreset = {
41
53
  id?: string
42
54
  label: React.ReactNode
@@ -502,8 +514,18 @@ function DatePicker({
502
514
  }: DatePickerProps) {
503
515
  const [internalOpen, setInternalOpen] = React.useState(defaultOpen)
504
516
  const [internalValue, setInternalValue] = React.useState(defaultValue)
517
+ const [internalDisplayMonth, setInternalDisplayMonth] = React.useState(() =>
518
+ startOfMonth(value ?? defaultValue ?? defaultDisplayMonth)
519
+ )
505
520
  const selected = value ?? internalValue
506
521
  const isOpen = open ?? internalOpen
522
+ const selectedTime = selected?.getTime()
523
+
524
+ React.useEffect(() => {
525
+ if (!isOpen || calendarProps?.displayMonth || !selected) return
526
+
527
+ setInternalDisplayMonth(startOfMonth(selected))
528
+ }, [calendarProps?.displayMonth, isOpen, selectedTime])
507
529
 
508
530
  function updateOpen(nextOpen: boolean) {
509
531
  if (open === undefined) {
@@ -519,6 +541,14 @@ function DatePicker({
519
541
  onSelect?.(nextDate)
520
542
  }
521
543
 
544
+ function updateDisplayMonth(nextMonth: Date) {
545
+ const normalized = startOfMonth(nextMonth)
546
+ if (calendarProps?.displayMonth === undefined) {
547
+ setInternalDisplayMonth(normalized)
548
+ }
549
+ calendarProps?.onDisplayMonthChange?.(normalized)
550
+ }
551
+
522
552
  return (
523
553
  <div
524
554
  data-open={isOpen ? "true" : "false"}
@@ -539,16 +569,17 @@ function DatePicker({
539
569
 
540
570
  {isOpen ? (
541
571
  <Calendar
572
+ {...calendarProps}
542
573
  defaultValue={defaultValue ?? defaultSelectedDate}
543
- displayMonth={selected ?? defaultValue ?? defaultDisplayMonth}
574
+ displayMonth={calendarProps?.displayMonth ?? internalDisplayMonth}
544
575
  selectedDate={selected}
545
576
  surfaceSize="picker"
546
- {...calendarProps}
547
577
  className={cn(
548
578
  "absolute left-1/2 top-[calc(100%+var(--bh-space-md-8))] z-[var(--bh-z-popover)] -translate-x-1/2",
549
579
  calendarProps?.className
550
580
  )}
551
581
  dir={calendarProps?.dir ?? dir}
582
+ onDisplayMonthChange={updateDisplayMonth}
552
583
  onSelect={(nextDate) => {
553
584
  selectDate(nextDate)
554
585
  calendarProps?.onSelect?.(nextDate)
@@ -580,7 +611,16 @@ function DateRangePicker({
580
611
  const [internalValue, setInternalValue] = React.useState(defaultValue)
581
612
  const selectedRange = normalizeRange(value ?? internalValue ?? {})
582
613
  const isOpen = open ?? internalOpen
583
- const displayMonth = selectedRange.from ?? defaultDisplayMonth
614
+ const [internalDisplayMonth, setInternalDisplayMonth] = React.useState(() =>
615
+ startOfMonth(selectedRange.from ?? defaultValue?.from ?? defaultDisplayMonth)
616
+ )
617
+ const selectedRangeFromTime = selectedRange.from?.getTime()
618
+
619
+ React.useEffect(() => {
620
+ if (!isOpen || calendarProps?.displayMonth || !selectedRange.from) return
621
+
622
+ setInternalDisplayMonth(startOfMonth(selectedRange.from))
623
+ }, [calendarProps?.displayMonth, isOpen, selectedRangeFromTime])
584
624
 
585
625
  function updateOpen(nextOpen: boolean) {
586
626
  if (open === undefined) {
@@ -596,6 +636,14 @@ function DateRangePicker({
596
636
  onSelect?.(nextRange)
597
637
  }
598
638
 
639
+ function updateDisplayMonth(nextMonth: Date) {
640
+ const normalized = startOfMonth(nextMonth)
641
+ if (calendarProps?.displayMonth === undefined) {
642
+ setInternalDisplayMonth(normalized)
643
+ }
644
+ calendarProps?.onDisplayMonthChange?.(normalized)
645
+ }
646
+
599
647
  return (
600
648
  <div
601
649
  data-open={isOpen ? "true" : "false"}
@@ -617,16 +665,17 @@ function DateRangePicker({
617
665
 
618
666
  {isOpen ? (
619
667
  <RangeCalendar
668
+ {...calendarProps}
620
669
  defaultValue={defaultValue ?? defaultRange}
621
- displayMonth={displayMonth}
670
+ displayMonth={calendarProps?.displayMonth ?? internalDisplayMonth}
622
671
  type={type}
623
672
  value={selectedRange}
624
- {...calendarProps}
625
673
  className={cn(
626
674
  "absolute left-1/2 top-[calc(100%+var(--bh-space-md-8))] z-[var(--bh-z-popover)] -translate-x-1/2",
627
675
  calendarProps?.className
628
676
  )}
629
677
  dir={calendarProps?.dir ?? dir}
678
+ onDisplayMonthChange={updateDisplayMonth}
630
679
  onSelect={(nextRange) => {
631
680
  selectRange(nextRange)
632
681
  calendarProps?.onSelect?.(nextRange)
@@ -727,9 +776,36 @@ function CalendarMonth({
727
776
  view?: CalendarView
728
777
  }) {
729
778
  const cells = React.useMemo(() => getCalendarCells(displayMonth), [displayMonth])
779
+ const gridRef = React.useRef<HTMLDivElement>(null)
780
+ const pendingFocusDateRef = React.useRef<Date | undefined>(undefined)
730
781
  const monthTitle = title ?? formatMonthTitle(displayMonth)
731
782
  const hasMonthToggle = mode === "single"
732
783
 
784
+ React.useLayoutEffect(() => {
785
+ if (!pendingFocusDateRef.current) return
786
+
787
+ focusCalendarDate(pendingFocusDateRef.current)
788
+ pendingFocusDateRef.current = undefined
789
+ }, [cells])
790
+
791
+ function focusCalendarDate(date: Date) {
792
+ const target = gridRef.current?.querySelector<HTMLButtonElement>(
793
+ `[data-calendar-date="${toCalendarDateId(date)}"]`
794
+ )
795
+
796
+ target?.focus()
797
+ }
798
+
799
+ function handleKeyboardNavigateDate(nextDate: Date) {
800
+ if (isSameMonth(nextDate, displayMonth)) {
801
+ focusCalendarDate(nextDate)
802
+ return
803
+ }
804
+
805
+ pendingFocusDateRef.current = nextDate
806
+ onDisplayMonthChange(startOfMonth(nextDate))
807
+ }
808
+
733
809
  return (
734
810
  <div
735
811
  data-slot="calendar-month"
@@ -810,6 +886,7 @@ function CalendarMonth({
810
886
  <div
811
887
  data-slot="calendar-grid"
812
888
  role="grid"
889
+ ref={gridRef}
813
890
  className="grid w-full grid-cols-7"
814
891
  >
815
892
  {cells.map((cell) => (
@@ -819,6 +896,7 @@ function CalendarMonth({
819
896
  eventDates={eventDates}
820
897
  key={cell.date.toISOString()}
821
898
  mode={mode}
899
+ onKeyboardNavigateDate={handleKeyboardNavigateDate}
822
900
  onSelectDate={onSelectDate}
823
901
  range={range}
824
902
  selectedDate={selectedDate}
@@ -843,6 +921,7 @@ function CalendarDayCell({
843
921
  displayMonth,
844
922
  eventDates,
845
923
  mode,
924
+ onKeyboardNavigateDate,
846
925
  onSelectDate,
847
926
  range,
848
927
  selectedDate,
@@ -851,50 +930,35 @@ function CalendarDayCell({
851
930
  displayMonth: Date
852
931
  eventDates?: Date[]
853
932
  mode: CalendarMode
933
+ onKeyboardNavigateDate: (date: Date) => void
854
934
  onSelectDate: (date: Date) => void
855
935
  range?: CalendarRange
856
936
  selectedDate?: Date
857
937
  }) {
858
- const outside = cell.date.getMonth() !== displayMonth.getMonth()
859
- const hasEvent = eventDates?.some((date) => isSameDay(date, cell.date)) ?? false
860
- const selected =
861
- mode === "single"
862
- ? Boolean(selectedDate && isSameDay(selectedDate, cell.date))
863
- : Boolean(
864
- range?.from &&
865
- (isSameDay(range.from, cell.date) ||
866
- (range.to && isSameDay(range.to, cell.date)))
867
- )
868
- const inRange =
869
- mode === "range" &&
870
- Boolean(
871
- range?.from &&
872
- range?.to &&
873
- isAfterOrSameDay(cell.date, range.from) &&
874
- isBeforeOrSameDay(cell.date, range.to)
875
- )
876
- const rangeEndpoint =
877
- mode === "range" &&
878
- Boolean(
879
- range?.from &&
880
- (isSameDay(range.from, cell.date) ||
881
- (range.to && isSameDay(range.to, cell.date)))
882
- )
883
- const rangeState = selected
884
- ? "selected"
885
- : inRange
886
- ? "range"
887
- : outside
888
- ? "outside"
889
- : hasEvent
890
- ? "event"
891
- : "default"
938
+ const { inRange, outside, rangeEndpoint, selected, state } = getCalendarDayState({
939
+ date: cell.date,
940
+ displayMonth,
941
+ eventDates,
942
+ mode,
943
+ range,
944
+ selectedDate,
945
+ })
946
+
947
+ function handleKeyDown(event: React.KeyboardEvent<HTMLButtonElement>) {
948
+ const nextDate = getCalendarKeyboardTarget(cell.date, event.key)
949
+
950
+ if (!nextDate) return
951
+
952
+ event.preventDefault()
953
+ onKeyboardNavigateDate(nextDate)
954
+ }
892
955
 
893
956
  return (
894
957
  <button
895
958
  aria-disabled={outside || undefined}
896
959
  aria-label={formatCalendarDate(cell.date, { month: "long" })}
897
960
  aria-selected={selected || undefined}
961
+ data-calendar-date={toCalendarDateId(cell.date)}
898
962
  data-outside={outside ? "true" : undefined}
899
963
  data-range-endpoint={rangeEndpoint ? "true" : undefined}
900
964
  data-slot="calendar-cell"
@@ -902,11 +966,12 @@ function CalendarDayCell({
902
966
  role="gridcell"
903
967
  type="button"
904
968
  className={cn(
905
- dayCellButton({ state: rangeState }),
969
+ dayCellButton({ state }),
906
970
  inRange && cell.weekday === 0 && "rounded-s-[var(--bh-control-default)]",
907
971
  inRange && cell.weekday === 6 && "rounded-e-[var(--bh-control-default)]"
908
972
  )}
909
973
  onClick={() => onSelectDate(cell.date)}
974
+ onKeyDown={handleKeyDown}
910
975
  >
911
976
  {cell.date.getDate()}
912
977
  </button>
@@ -1002,114 +1067,6 @@ function MonthYearList({ items }: { items: CalendarMonthYearItem[] }) {
1002
1067
  )
1003
1068
  }
1004
1069
 
1005
- type CalendarCell = {
1006
- date: Date
1007
- weekday: number
1008
- }
1009
-
1010
- function getCalendarCells(month: Date): CalendarCell[] {
1011
- const first = startOfMonth(month)
1012
- const start = addDays(first, -first.getDay())
1013
- const last = new Date(first.getFullYear(), first.getMonth() + 1, 0)
1014
- const end = addDays(last, 6 - last.getDay())
1015
- const dayCount = differenceInCalendarDays(end, start) + 1
1016
-
1017
- return Array.from({ length: dayCount }, (_, index) => {
1018
- const date = addDays(start, index)
1019
- return {
1020
- date,
1021
- weekday: date.getDay(),
1022
- }
1023
- })
1024
- }
1025
-
1026
- function getNextRange(currentRange: CalendarRange, date: Date): CalendarRange {
1027
- const nextDate = startOfDay(date)
1028
- const from = currentRange.from ? startOfDay(currentRange.from) : undefined
1029
- const to = currentRange.to ? startOfDay(currentRange.to) : undefined
1030
-
1031
- if (!from || to) {
1032
- return { from: nextDate, to: undefined }
1033
- }
1034
-
1035
- if (isBeforeDay(nextDate, from)) {
1036
- return { from: nextDate, to: from }
1037
- }
1038
-
1039
- return { from, to: nextDate }
1040
- }
1041
-
1042
- function normalizeRange(range: CalendarRange): CalendarRange {
1043
- if (!range.from || !range.to) return range
1044
-
1045
- return isBeforeDay(range.to, range.from)
1046
- ? { from: range.to, to: range.from }
1047
- : range
1048
- }
1049
-
1050
- function formatCalendarRange(range: CalendarRange) {
1051
- if (!range.from && !range.to) return undefined
1052
- if (range.from && !range.to) {
1053
- return `${formatCalendarDate(range.from, { month: "short" })} -`
1054
- }
1055
- if (!range.from && range.to) {
1056
- return `- ${formatCalendarDate(range.to, { month: "short" })}`
1057
- }
1058
-
1059
- return `${formatCalendarDate(range.from, {
1060
- month: "short",
1061
- })} - ${formatCalendarDate(range.to, { month: "short" })}`
1062
- }
1063
-
1064
- function formatCalendarDate(date: Date | undefined, options?: { month?: "short" | "long" }) {
1065
- if (!date) return ""
1066
- const month = date.toLocaleString("en", { month: options?.month ?? "short" })
1067
- return `${month} ${date.getDate()}`
1068
- }
1069
-
1070
- function formatMonthTitle(date: Date) {
1071
- return `${date.toLocaleString("en", { month: "long" })} ${date.getFullYear()}`
1072
- }
1073
-
1074
- function startOfDay(date: Date) {
1075
- return new Date(date.getFullYear(), date.getMonth(), date.getDate())
1076
- }
1077
-
1078
- function startOfMonth(date: Date) {
1079
- return new Date(date.getFullYear(), date.getMonth(), 1)
1080
- }
1081
-
1082
- function addDays(date: Date, amount: number) {
1083
- const next = new Date(date)
1084
- next.setDate(next.getDate() + amount)
1085
- return startOfDay(next)
1086
- }
1087
-
1088
- function addMonths(date: Date, amount: number) {
1089
- return new Date(date.getFullYear(), date.getMonth() + amount, 1)
1090
- }
1091
-
1092
- function differenceInCalendarDays(left: Date, right: Date) {
1093
- const dayLength = 24 * 60 * 60 * 1000
1094
- return Math.round((startOfDay(left).getTime() - startOfDay(right).getTime()) / dayLength)
1095
- }
1096
-
1097
- function isSameDay(left: Date, right: Date) {
1098
- return startOfDay(left).getTime() === startOfDay(right).getTime()
1099
- }
1100
-
1101
- function isBeforeDay(left: Date, right: Date) {
1102
- return startOfDay(left).getTime() < startOfDay(right).getTime()
1103
- }
1104
-
1105
- function isAfterOrSameDay(left: Date, right: Date) {
1106
- return startOfDay(left).getTime() >= startOfDay(right).getTime()
1107
- }
1108
-
1109
- function isBeforeOrSameDay(left: Date, right: Date) {
1110
- return startOfDay(left).getTime() <= startOfDay(right).getTime()
1111
- }
1112
-
1113
1070
  function hasRenderableContent(content: React.ReactNode) {
1114
1071
  return (
1115
1072
  content !== undefined &&