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.
- package/README.md +21 -9
- package/package.json +8 -2
- package/registry/components/accordion.tsx +37 -1
- package/registry/components/alert.tsx +14 -28
- package/registry/components/attribute.tsx +6 -10
- package/registry/components/autocomplete.tsx +637 -0
- package/registry/components/avatar.tsx +259 -24
- package/registry/components/badge.tsx +97 -35
- package/registry/components/button-group.tsx +1 -1
- package/registry/components/card.tsx +1 -1
- package/registry/components/checkbox.tsx +19 -16
- package/registry/components/date-picker-state.ts +253 -0
- package/registry/components/date-picker.tsx +115 -158
- package/registry/components/expanded/ActivityFeed.tsx +37 -23
- package/registry/components/expanded/Banner.tsx +54 -19
- package/registry/components/expanded/Breadcrumbs.tsx +10 -38
- package/registry/components/expanded/CatalogComponentsShowcase.tsx +11 -16
- package/registry/components/expanded/CatalogTag.tsx +4 -11
- package/registry/components/expanded/CommandBar.tsx +33 -53
- package/registry/components/expanded/EmptyState.tsx +155 -0
- package/registry/components/expanded/FileUpload.tsx +362 -59
- package/registry/components/expanded/OnboardingStepListItem.tsx +6 -10
- package/registry/components/expanded/PageHeader.tsx +2 -11
- package/registry/components/expanded/Slideout.tsx +12 -23
- package/registry/components/expanded/Steps.tsx +6 -8
- package/registry/components/expanded/Table.tsx +18 -40
- package/registry/components/expanded/Timeline.tsx +5 -24
- package/registry/components/expanded/activityFeed.css +10 -54
- package/registry/components/expanded/banner.css +8 -75
- package/registry/components/expanded/breadcrumbs.css +1 -1
- package/registry/components/expanded/commandBar.css +23 -26
- package/registry/components/expanded/divider.css +1 -1
- package/registry/components/expanded/emptyState.css +111 -0
- package/registry/components/expanded/fileUpload.css +304 -75
- package/registry/components/expanded/pageHeader.css +1 -1
- package/registry/components/expanded/slideout.css +1 -0
- package/registry/components/expanded/steps.css +15 -51
- package/registry/components/expanded/table.css +6 -1
- package/registry/components/expanded/timeline.css +18 -15
- package/registry/components/input-otp.tsx +574 -0
- package/registry/components/input.tsx +140 -59
- package/registry/components/menu.tsx +470 -80
- package/registry/components/pagination.tsx +6 -18
- package/registry/components/popover.tsx +840 -0
- package/registry/components/radio-card.tsx +25 -31
- package/registry/components/select-content.tsx +28 -123
- package/registry/components/select.tsx +13 -9
- package/registry/components/skeleton.css +57 -0
- package/registry/components/skeleton.tsx +482 -0
- package/registry/components/social-button.tsx +24 -90
- package/registry/components/spinner.tsx +91 -7
- package/registry/components/textarea.tsx +21 -36
- package/registry/components/toggle.tsx +7 -23
- package/registry/components/tooltip.tsx +8 -4
- package/registry/examples/attribute-demo.tsx +2 -2
- package/registry/examples/autocomplete-demo.tsx +109 -0
- package/registry/examples/avatar-demo.tsx +102 -47
- package/registry/examples/badge-demo.tsx +16 -0
- package/registry/examples/checkbox-demo.tsx +3 -8
- package/registry/examples/date-picker-demo.tsx +75 -22
- package/registry/examples/expanded/banner-demo.tsx +31 -6
- package/registry/examples/expanded/breadcrumbs-demo.tsx +59 -0
- package/registry/examples/expanded/command-bar-demo.tsx +236 -0
- package/registry/examples/expanded/empty-state-demo.tsx +39 -0
- package/registry/examples/expanded/file-upload-demo.tsx +60 -0
- package/registry/examples/expanded/steps-demo.tsx +11 -0
- package/registry/examples/expanded/table-demo.tsx +142 -0
- package/registry/examples/input-demo.tsx +1 -1
- package/registry/examples/input-otp-demo.tsx +72 -0
- package/registry/examples/menu-demo.tsx +101 -88
- package/registry/examples/popover-demo.tsx +546 -0
- package/registry/examples/progress-demo.tsx +2 -2
- package/registry/examples/select-demo.tsx +32 -18
- package/registry/examples/skeleton-demo.tsx +56 -0
- package/registry/examples/social-button-demo.tsx +33 -33
- package/registry/examples/spinner-demo.tsx +59 -0
- package/registry/examples/tag-demo.tsx +1 -1
- package/registry/examples/textarea-demo.tsx +1 -1
- package/registry/index.json +266 -20
- package/registry/styles/globals.css +93 -3
- 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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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={
|
|
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
|
|
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
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
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
|
|
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 &&
|