@srcroot/ui 0.0.1 → 0.0.3
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/dist/index.js +91 -0
- package/package.json +9 -3
- package/registry/badge.tsx +9 -25
- package/registry/breadcrumb.tsx +1 -1
- package/registry/button-group.tsx +9 -29
- package/registry/button.tsx +20 -46
- package/registry/calendar.tsx +416 -142
- package/registry/card.tsx +21 -47
- package/registry/combobox.tsx +171 -0
- package/registry/command.tsx +300 -0
- package/registry/container.tsx +9 -25
- package/registry/context-menu.tsx +221 -0
- package/registry/date-picker.tsx +179 -0
- package/registry/drawer.tsx +241 -0
- package/registry/dropdown-menu.tsx +93 -74
- package/registry/file-upload.tsx +240 -0
- package/registry/hover-card.tsx +165 -0
- package/registry/image.tsx +2 -2
- package/registry/kbd.tsx +60 -0
- package/registry/menubar.tsx +246 -0
- package/registry/native-select.tsx +49 -0
- package/registry/pagination.tsx +3 -0
- package/registry/resizable.tsx +251 -0
- package/registry/scroll-area.tsx +119 -0
- package/registry/search.tsx +2 -1
- package/registry/sheet.tsx +63 -18
- package/registry/sidebar.tsx +512 -0
- package/registry/slider.tsx +133 -54
- package/registry/text.tsx +7 -16
- package/registry/toggle-group.tsx +129 -0
- package/registry/toggle.tsx +72 -0
- package/registry/tooltip.tsx +21 -3
package/registry/calendar.tsx
CHANGED
|
@@ -8,11 +8,13 @@ interface CalendarContextValue {
|
|
|
8
8
|
onSelect: (date: Date) => void
|
|
9
9
|
mode: "single" | "multiple" | "range"
|
|
10
10
|
rangeStart: Date | null
|
|
11
|
+
hoveredDate: Date | null
|
|
12
|
+
setHoveredDate: (date: Date | null) => void
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
const CalendarContext = React.createContext<CalendarContextValue | null>(null)
|
|
14
16
|
|
|
15
|
-
interface CalendarProps {
|
|
17
|
+
export interface CalendarProps {
|
|
16
18
|
/** Selection mode */
|
|
17
19
|
mode?: "single" | "multiple" | "range"
|
|
18
20
|
/** Selected date(s) */
|
|
@@ -27,14 +29,23 @@ interface CalendarProps {
|
|
|
27
29
|
maxDate?: Date
|
|
28
30
|
/** Whether calendar is disabled */
|
|
29
31
|
disabled?: boolean
|
|
32
|
+
/** Size variant */
|
|
33
|
+
size?: "default" | "sm" | "xs" | "md" | "lg"
|
|
34
|
+
/** Number of months to display */
|
|
35
|
+
numberOfMonths?: number
|
|
30
36
|
className?: string
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
const DAYS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"]
|
|
40
|
+
const DAYS_SHORT = ["S", "M", "T", "W", "T", "F", "S"]
|
|
34
41
|
const MONTHS = [
|
|
35
42
|
"January", "February", "March", "April", "May", "June",
|
|
36
43
|
"July", "August", "September", "October", "November", "December"
|
|
37
44
|
]
|
|
45
|
+
const MONTHS_SHORT = [
|
|
46
|
+
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
|
47
|
+
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
|
|
48
|
+
]
|
|
38
49
|
|
|
39
50
|
function getDaysInMonth(year: number, month: number): Date[] {
|
|
40
51
|
const days: Date[] = []
|
|
@@ -70,169 +81,432 @@ function isSameDay(d1: Date, d2: Date): boolean {
|
|
|
70
81
|
function isInRange(date: Date, start: Date | null, end: Date | null): boolean {
|
|
71
82
|
if (!start || !end) return false
|
|
72
83
|
const d = date.getTime()
|
|
73
|
-
|
|
84
|
+
const s = start.getTime()
|
|
85
|
+
const e = end.getTime()
|
|
86
|
+
return d >= Math.min(s, e) && d <= Math.max(s, e)
|
|
74
87
|
}
|
|
75
88
|
|
|
89
|
+
type ViewMode = 'days' | 'months' | 'years'
|
|
90
|
+
|
|
76
91
|
/**
|
|
77
|
-
* Calendar date picker component
|
|
92
|
+
* Calendar date picker component - Modern, Responsive, Advanced
|
|
78
93
|
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* @example
|
|
85
|
-
* // Date range
|
|
86
|
-
* const [range, setRange] = useState<Date[]>([])
|
|
87
|
-
* <Calendar mode="range" selected={range} onSelect={setRange} />
|
|
94
|
+
* Features:
|
|
95
|
+
* - Fluid sizing (xs, sm, default, md, lg)
|
|
96
|
+
* - Range hover preview
|
|
97
|
+
* - Year/Month selection views
|
|
98
|
+
* - Animated transitions (CSS)
|
|
88
99
|
*/
|
|
89
100
|
const Calendar = React.forwardRef<HTMLDivElement, CalendarProps>(
|
|
90
101
|
(
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (
|
|
121
|
-
|
|
102
|
+
(
|
|
103
|
+
{
|
|
104
|
+
mode = "single",
|
|
105
|
+
selected,
|
|
106
|
+
onSelect,
|
|
107
|
+
defaultMonth = new Date(),
|
|
108
|
+
minDate,
|
|
109
|
+
maxDate,
|
|
110
|
+
disabled,
|
|
111
|
+
size = "default",
|
|
112
|
+
className,
|
|
113
|
+
numberOfMonths = 1,
|
|
114
|
+
...props
|
|
115
|
+
},
|
|
116
|
+
ref
|
|
117
|
+
) => {
|
|
118
|
+
const [currentMonth, setCurrentMonth] = React.useState(defaultMonth)
|
|
119
|
+
const [view, setView] = React.useState<ViewMode>('days')
|
|
120
|
+
const [rangeStart, setRangeStart] = React.useState<Date | null>(null)
|
|
121
|
+
const [hoveredDate, setHoveredDate] = React.useState<Date | null>(null)
|
|
122
|
+
|
|
123
|
+
// Reset range start if mode changes or selection clears externally
|
|
124
|
+
React.useEffect(() => {
|
|
125
|
+
if (mode !== 'range' || !selected) {
|
|
126
|
+
setRangeStart(null)
|
|
127
|
+
}
|
|
128
|
+
}, [mode, selected])
|
|
129
|
+
|
|
130
|
+
const selectedDates = React.useMemo(() => {
|
|
131
|
+
if (!selected) return []
|
|
132
|
+
return Array.isArray(selected) ? selected : [selected]
|
|
133
|
+
}, [selected])
|
|
134
|
+
|
|
135
|
+
const handleSelect = (date: Date) => {
|
|
136
|
+
if (disabled) return
|
|
137
|
+
if (minDate && date < minDate) return
|
|
138
|
+
if (maxDate && date > maxDate) return
|
|
139
|
+
|
|
140
|
+
if (mode === "single") {
|
|
141
|
+
onSelect?.(date)
|
|
142
|
+
} else if (mode === "multiple") {
|
|
143
|
+
const exists = selectedDates.some(d => isSameDay(d, date))
|
|
144
|
+
if (exists) {
|
|
145
|
+
onSelect?.(selectedDates.filter(d => !isSameDay(d, date)))
|
|
146
|
+
} else {
|
|
147
|
+
onSelect?.([...selectedDates, date])
|
|
148
|
+
}
|
|
149
|
+
} else if (mode === "range") {
|
|
150
|
+
if (!rangeStart) {
|
|
151
|
+
setRangeStart(date)
|
|
152
|
+
onSelect?.([date]) // Partial selection
|
|
153
|
+
} else {
|
|
154
|
+
const start = rangeStart < date ? rangeStart : date
|
|
155
|
+
const end = rangeStart < date ? date : rangeStart
|
|
156
|
+
onSelect?.([start, end])
|
|
157
|
+
setRangeStart(null)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const navigatePrev = () => {
|
|
163
|
+
if (view === 'years') {
|
|
164
|
+
setCurrentMonth(new Date(currentMonth.getFullYear() - 12, currentMonth.getMonth(), 1))
|
|
165
|
+
} else if (view === 'months') {
|
|
166
|
+
setCurrentMonth(new Date(currentMonth.getFullYear() - 1, currentMonth.getMonth(), 1))
|
|
122
167
|
} else {
|
|
123
|
-
|
|
168
|
+
setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1, 1))
|
|
124
169
|
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const navigateNext = () => {
|
|
173
|
+
if (view === 'years') {
|
|
174
|
+
setCurrentMonth(new Date(currentMonth.getFullYear() + 12, currentMonth.getMonth(), 1))
|
|
175
|
+
} else if (view === 'months') {
|
|
176
|
+
setCurrentMonth(new Date(currentMonth.getFullYear() + 1, currentMonth.getMonth(), 1))
|
|
129
177
|
} else {
|
|
130
|
-
|
|
131
|
-
const end = rangeStart < date ? date : rangeStart
|
|
132
|
-
onSelect?.([start, end])
|
|
133
|
-
setRangeStart(null)
|
|
178
|
+
setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 1))
|
|
134
179
|
}
|
|
135
180
|
}
|
|
136
|
-
}
|
|
137
181
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
182
|
+
// --- Sizing Configuration ---
|
|
183
|
+
const sizeStyles = {
|
|
184
|
+
xs: {
|
|
185
|
+
container: "p-2", // width handled by layout now
|
|
186
|
+
width: "w-[220px]",
|
|
187
|
+
header: "mb-2",
|
|
188
|
+
navBtn: "h-5 w-5",
|
|
189
|
+
icon: "h-2.5 w-2.5",
|
|
190
|
+
headerTitle: "text-[10px] font-medium",
|
|
191
|
+
grid: "gap-0.5",
|
|
192
|
+
dayName: "text-[9px] h-5",
|
|
193
|
+
cell: "text-[10px]",
|
|
194
|
+
yearGrid: "grid-cols-3 gap-1",
|
|
195
|
+
yearCell: "text-xs py-1.5",
|
|
196
|
+
radius: "rounded-sm",
|
|
197
|
+
},
|
|
198
|
+
sm: {
|
|
199
|
+
container: "p-3",
|
|
200
|
+
width: "w-[280px]",
|
|
201
|
+
header: "mb-3",
|
|
202
|
+
navBtn: "h-6 w-6",
|
|
203
|
+
icon: "h-3 w-3",
|
|
204
|
+
headerTitle: "text-xs font-semibold",
|
|
205
|
+
grid: "gap-1",
|
|
206
|
+
dayName: "text-[10px] h-6",
|
|
207
|
+
cell: "text-xs",
|
|
208
|
+
yearGrid: "grid-cols-3 gap-2",
|
|
209
|
+
yearCell: "text-xs py-2",
|
|
210
|
+
radius: "rounded-md",
|
|
211
|
+
},
|
|
212
|
+
default: {
|
|
213
|
+
container: "p-4",
|
|
214
|
+
width: "w-[320px]",
|
|
215
|
+
header: "mb-4",
|
|
216
|
+
navBtn: "h-8 w-8",
|
|
217
|
+
icon: "h-4 w-4",
|
|
218
|
+
headerTitle: "text-sm font-semibold",
|
|
219
|
+
grid: "gap-1",
|
|
220
|
+
dayName: "text-xs h-8",
|
|
221
|
+
cell: "text-sm",
|
|
222
|
+
yearGrid: "grid-cols-3 gap-3",
|
|
223
|
+
yearCell: "text-sm py-2.5",
|
|
224
|
+
radius: "rounded-md",
|
|
225
|
+
},
|
|
226
|
+
md: {
|
|
227
|
+
container: "p-5",
|
|
228
|
+
width: "w-[380px]",
|
|
229
|
+
header: "mb-5",
|
|
230
|
+
navBtn: "h-9 w-9",
|
|
231
|
+
icon: "h-4 w-4",
|
|
232
|
+
headerTitle: "text-base font-semibold",
|
|
233
|
+
grid: "gap-2",
|
|
234
|
+
dayName: "text-xs h-9",
|
|
235
|
+
cell: "text-base",
|
|
236
|
+
yearGrid: "grid-cols-4 gap-3",
|
|
237
|
+
yearCell: "text-base py-3",
|
|
238
|
+
radius: "rounded-lg",
|
|
239
|
+
},
|
|
240
|
+
lg: {
|
|
241
|
+
container: "p-6",
|
|
242
|
+
width: "w-[440px]",
|
|
243
|
+
header: "mb-6",
|
|
244
|
+
navBtn: "h-10 w-10",
|
|
245
|
+
icon: "h-5 w-5",
|
|
246
|
+
headerTitle: "text-lg font-bold",
|
|
247
|
+
grid: "gap-3",
|
|
248
|
+
dayName: "text-sm h-10",
|
|
249
|
+
cell: "text-lg",
|
|
250
|
+
yearGrid: "grid-cols-4 gap-4",
|
|
251
|
+
yearCell: "text-lg py-4",
|
|
252
|
+
radius: "rounded-xl",
|
|
253
|
+
},
|
|
254
|
+
}
|
|
255
|
+
const s = sizeStyles[size]
|
|
141
256
|
|
|
142
|
-
|
|
143
|
-
setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 1))
|
|
144
|
-
}
|
|
257
|
+
// --- Render Helpers ---
|
|
145
258
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
return (
|
|
150
|
-
<div
|
|
151
|
-
ref={ref}
|
|
152
|
-
className={cn("p-3 bg-background border rounded-lg shadow-md w-fit", className)}
|
|
153
|
-
role="application"
|
|
154
|
-
aria-label="Calendar"
|
|
155
|
-
>
|
|
156
|
-
{/* Header */}
|
|
157
|
-
<div className="flex items-center justify-between mb-4">
|
|
158
|
-
<button
|
|
159
|
-
type="button"
|
|
160
|
-
onClick={navigatePrev}
|
|
161
|
-
className="p-1 rounded hover:bg-accent"
|
|
162
|
-
aria-label="Previous month"
|
|
163
|
-
>
|
|
164
|
-
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
165
|
-
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
|
|
166
|
-
</svg>
|
|
167
|
-
</button>
|
|
168
|
-
<span className="font-semibold">
|
|
169
|
-
{MONTHS[currentMonth.getMonth()]} {currentMonth.getFullYear()}
|
|
170
|
-
</span>
|
|
171
|
-
<button
|
|
172
|
-
type="button"
|
|
173
|
-
onClick={navigateNext}
|
|
174
|
-
className="p-1 rounded hover:bg-accent"
|
|
175
|
-
aria-label="Next month"
|
|
176
|
-
>
|
|
177
|
-
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
178
|
-
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
|
179
|
-
</svg>
|
|
180
|
-
</button>
|
|
181
|
-
</div>
|
|
259
|
+
const renderMonthGrid = (monthOffset: number, isFirst: boolean, isLast: boolean) => {
|
|
260
|
+
const displayDate = new Date(currentMonth.getFullYear(), currentMonth.getMonth() + monthOffset, 1)
|
|
261
|
+
const days = getDaysInMonth(displayDate.getFullYear(), displayDate.getMonth())
|
|
182
262
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
))}
|
|
190
|
-
|
|
263
|
+
// Calculate effective range end (either confirmed end or hover preview)
|
|
264
|
+
const rangeEnd = (mode === "range" && selectedDates.length === 2)
|
|
265
|
+
? selectedDates[1]
|
|
266
|
+
: (rangeStart && hoveredDate ? hoveredDate : null)
|
|
267
|
+
|
|
268
|
+
const label = size === "xs"
|
|
269
|
+
? `${MONTHS_SHORT[displayDate.getMonth()]} ${displayDate.getFullYear()}`
|
|
270
|
+
: `${MONTHS[displayDate.getMonth()]} ${displayDate.getFullYear()}`
|
|
271
|
+
|
|
272
|
+
const dayLabels = size === "xs" ? DAYS_SHORT : DAYS
|
|
191
273
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
274
|
+
return (
|
|
275
|
+
<div className={cn("relative", s.width)}>
|
|
276
|
+
<div className={cn("flex justify-between items-center", s.header)}>
|
|
277
|
+
{/* Prev Button - only show on first month */}
|
|
278
|
+
{isFirst && view === 'days' ? (
|
|
279
|
+
<button
|
|
280
|
+
type="button"
|
|
281
|
+
onClick={navigatePrev}
|
|
282
|
+
className={cn(
|
|
283
|
+
"inline-flex items-center justify-center rounded-md hover:bg-accent hover:text-accent-foreground active:scale-95 transition-all",
|
|
284
|
+
s.navBtn
|
|
285
|
+
)}
|
|
286
|
+
>
|
|
287
|
+
<svg className={s.icon} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
288
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
|
|
289
|
+
</svg>
|
|
290
|
+
</button>
|
|
291
|
+
) : <div className={s.navBtn} />}
|
|
292
|
+
|
|
293
|
+
{/* Month Title */}
|
|
206
294
|
<button
|
|
207
|
-
key={index}
|
|
208
295
|
type="button"
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
aria-disabled={isDisabled}
|
|
212
|
-
disabled={isDisabled}
|
|
213
|
-
onClick={() => handleSelect(date)}
|
|
214
|
-
className={cn(
|
|
215
|
-
"h-8 w-8 rounded text-sm font-medium transition-colors",
|
|
216
|
-
"focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-1",
|
|
217
|
-
!isCurrentMonth && "text-muted-foreground/50",
|
|
218
|
-
isCurrentMonth && "text-foreground",
|
|
219
|
-
isToday && "border border-primary",
|
|
220
|
-
isSelected && "bg-primary text-primary-foreground",
|
|
221
|
-
(isRangeStart || isRangeEnd) && "bg-primary text-primary-foreground",
|
|
222
|
-
inRange && !isSelected && "bg-accent",
|
|
223
|
-
!isSelected && !inRange && "hover:bg-accent",
|
|
224
|
-
isDisabled && "opacity-50 cursor-not-allowed"
|
|
225
|
-
)}
|
|
296
|
+
onClick={() => setView(view === 'days' ? 'months' : view === 'months' ? 'years' : 'days')}
|
|
297
|
+
className={cn("hover:bg-accent px-2 py-1 rounded transition-colors font-semibold", s.headerTitle)}
|
|
226
298
|
>
|
|
227
|
-
{
|
|
299
|
+
{label}
|
|
228
300
|
</button>
|
|
229
|
-
|
|
230
|
-
|
|
301
|
+
|
|
302
|
+
{/* Next Button - only show on last month */}
|
|
303
|
+
{isLast && view === 'days' ? (
|
|
304
|
+
<button
|
|
305
|
+
type="button"
|
|
306
|
+
onClick={navigateNext}
|
|
307
|
+
className={cn(
|
|
308
|
+
"inline-flex items-center justify-center rounded-md hover:bg-accent hover:text-accent-foreground active:scale-95 transition-all",
|
|
309
|
+
s.navBtn
|
|
310
|
+
)}
|
|
311
|
+
>
|
|
312
|
+
<svg className={s.icon} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
313
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
|
314
|
+
</svg>
|
|
315
|
+
</button>
|
|
316
|
+
) : <div className={s.navBtn} />}
|
|
317
|
+
</div>
|
|
318
|
+
|
|
319
|
+
{/* Day Names */}
|
|
320
|
+
<div className={cn("grid grid-cols-7 mb-2", s.grid)}>
|
|
321
|
+
{dayLabels.map((day, i) => (
|
|
322
|
+
<div key={i} className={cn("flex justify-center items-center text-muted-foreground font-medium", s.dayName)}>
|
|
323
|
+
{day}
|
|
324
|
+
</div>
|
|
325
|
+
))}
|
|
326
|
+
</div>
|
|
327
|
+
|
|
328
|
+
{/* Days Grid */}
|
|
329
|
+
<div className={cn("grid grid-cols-7", s.grid)} role="grid">
|
|
330
|
+
{days.map((date, index) => {
|
|
331
|
+
const isCurrentMonth = date.getMonth() === displayDate.getMonth()
|
|
332
|
+
const isSelected = selectedDates.some(d => isSameDay(d, date))
|
|
333
|
+
const isRangeStart = rangeStart && isSameDay(date, rangeStart)
|
|
334
|
+
// A date is effectively the "end" if it's the actual confirmed end OR the hovered end
|
|
335
|
+
// (but only if we are currently selecting a range)
|
|
336
|
+
const isRangeEnd = mode === "range" && rangeEnd && isSameDay(date, rangeEnd)
|
|
337
|
+
|
|
338
|
+
// Check if in range of (start -> end/hover)
|
|
339
|
+
const inRange = mode === "range" && isInRange(date, rangeStart || selectedDates[0], rangeEnd)
|
|
340
|
+
|
|
341
|
+
const isToday = isSameDay(date, new Date())
|
|
342
|
+
const isDisabled = disabled || (minDate && date < minDate) || (maxDate && date > maxDate)
|
|
343
|
+
|
|
344
|
+
return (
|
|
345
|
+
<button
|
|
346
|
+
key={index}
|
|
347
|
+
type="button"
|
|
348
|
+
onClick={() => handleSelect(date)}
|
|
349
|
+
onMouseEnter={() => setHoveredDate(date)}
|
|
350
|
+
onMouseLeave={() => setHoveredDate(null)}
|
|
351
|
+
disabled={isDisabled}
|
|
352
|
+
className={cn(
|
|
353
|
+
"relative p-0 text-center focus-within:relative focus-within:z-20",
|
|
354
|
+
"aspect-square w-full flex items-center justify-center transition-all",
|
|
355
|
+
s.cell,
|
|
356
|
+
s.radius, // Apply base radius
|
|
357
|
+
// Base states
|
|
358
|
+
!isCurrentMonth && "text-muted-foreground/30",
|
|
359
|
+
isCurrentMonth && "text-foreground font-normal",
|
|
360
|
+
isToday && !isSelected && "text-accent-foreground bg-accent/30 font-semibold ring-1 ring-inset ring-accent-foreground/20",
|
|
361
|
+
isDisabled && "opacity-30 cursor-not-allowed hover:bg-transparent",
|
|
362
|
+
|
|
363
|
+
// Hover states (only if enabled)
|
|
364
|
+
!isDisabled && !isSelected && !inRange && isCurrentMonth && "hover:bg-accent hover:text-accent-foreground",
|
|
365
|
+
|
|
366
|
+
// Selection states
|
|
367
|
+
isSelected && "bg-primary text-primary-foreground hover:bg-primary shadow-sm font-semibold z-10",
|
|
368
|
+
|
|
369
|
+
// Range styling
|
|
370
|
+
(isRangeStart || isRangeEnd) && "bg-primary text-primary-foreground hover:bg-primary shadow-sm font-semibold z-10",
|
|
371
|
+
inRange && !isSelected && "bg-accent/40 text-accent-foreground font-medium hover:bg-accent/60",
|
|
372
|
+
|
|
373
|
+
// Range rounding fix (connect bars)
|
|
374
|
+
inRange && "rounded-none",
|
|
375
|
+
isRangeStart && rangeEnd && "rounded-r-none",
|
|
376
|
+
isRangeEnd && rangeStart && "rounded-l-none",
|
|
377
|
+
|
|
378
|
+
// Specific case: Start == End (Single day range)
|
|
379
|
+
isRangeStart && isRangeEnd && s.radius
|
|
380
|
+
)}
|
|
381
|
+
>
|
|
382
|
+
<time dateTime={date.toISOString().split('T')[0]}>
|
|
383
|
+
{date.getDate()}
|
|
384
|
+
</time>
|
|
385
|
+
</button>
|
|
386
|
+
)
|
|
387
|
+
})}
|
|
388
|
+
</div>
|
|
389
|
+
</div>
|
|
390
|
+
)
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const renderMonths = () => {
|
|
394
|
+
const currentYear = currentMonth.getFullYear()
|
|
395
|
+
return (
|
|
396
|
+
<div className={cn("grid w-full", s.yearGrid)}>
|
|
397
|
+
{MONTHS_SHORT.map((month, index) => {
|
|
398
|
+
const isCurrent = new Date().getMonth() === index && new Date().getFullYear() === currentYear
|
|
399
|
+
const isSelected = currentMonth.getMonth() === index
|
|
400
|
+
return (
|
|
401
|
+
<button
|
|
402
|
+
key={month}
|
|
403
|
+
type="button"
|
|
404
|
+
onClick={() => {
|
|
405
|
+
setCurrentMonth(new Date(currentYear, index, 1))
|
|
406
|
+
setView('days')
|
|
407
|
+
}}
|
|
408
|
+
className={cn(
|
|
409
|
+
"flex items-center justify-center transition-colors hover:bg-accent rounded-md font-medium",
|
|
410
|
+
s.yearCell,
|
|
411
|
+
isCurrent && "border border-primary text-primary",
|
|
412
|
+
isSelected && "bg-primary text-primary-foreground hover:bg-primary"
|
|
413
|
+
)}
|
|
414
|
+
>
|
|
415
|
+
{size === 'xs' ? month.charAt(0) : month}
|
|
416
|
+
</button>
|
|
417
|
+
)
|
|
418
|
+
})}
|
|
419
|
+
</div>
|
|
420
|
+
)
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const renderYears = () => {
|
|
424
|
+
const bgYear = currentMonth.getFullYear()
|
|
425
|
+
const startYear = bgYear - (bgYear % 12)
|
|
426
|
+
const years = Array.from({ length: 12 }, (_, i) => startYear + i)
|
|
427
|
+
|
|
428
|
+
return (
|
|
429
|
+
<div className={cn("grid w-full", s.yearGrid)}>
|
|
430
|
+
{years.map((year) => {
|
|
431
|
+
const isCurrent = new Date().getFullYear() === year
|
|
432
|
+
const isSelected = currentMonth.getFullYear() === year
|
|
433
|
+
return (
|
|
434
|
+
<button
|
|
435
|
+
key={year}
|
|
436
|
+
type="button"
|
|
437
|
+
onClick={() => {
|
|
438
|
+
setCurrentMonth(new Date(year, currentMonth.getMonth(), 1))
|
|
439
|
+
setView('months')
|
|
440
|
+
}}
|
|
441
|
+
className={cn(
|
|
442
|
+
"flex items-center justify-center transition-colors hover:bg-accent rounded-md font-medium",
|
|
443
|
+
s.yearCell,
|
|
444
|
+
isCurrent && "border border-primary text-primary",
|
|
445
|
+
isSelected && "bg-primary text-primary-foreground hover:bg-primary"
|
|
446
|
+
)}
|
|
447
|
+
>
|
|
448
|
+
{year}
|
|
449
|
+
</button>
|
|
450
|
+
)
|
|
451
|
+
})}
|
|
452
|
+
</div>
|
|
453
|
+
)
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const monthsToRender = Array.from({ length: numberOfMonths }, (_, i) => i)
|
|
457
|
+
|
|
458
|
+
return (
|
|
459
|
+
<div
|
|
460
|
+
ref={ref}
|
|
461
|
+
className={cn(
|
|
462
|
+
"bg-background border rounded-lg shadow-sm transition-all duration-200 inline-block relative",
|
|
463
|
+
s.container,
|
|
464
|
+
className
|
|
465
|
+
)}
|
|
466
|
+
role="application"
|
|
467
|
+
aria-label="Calendar"
|
|
468
|
+
{...props}
|
|
469
|
+
>
|
|
470
|
+
{/* Navigation Overlays */}
|
|
471
|
+
|
|
472
|
+
<div className={cn("flex gap-8 relative")}> {/* Gap between months */}
|
|
473
|
+
{view === 'days' ? (
|
|
474
|
+
monthsToRender.map(offset => (
|
|
475
|
+
<React.Fragment key={offset}>
|
|
476
|
+
{renderMonthGrid(offset, offset === 0, offset === numberOfMonths - 1)}
|
|
477
|
+
</React.Fragment>
|
|
478
|
+
))
|
|
479
|
+
) : (
|
|
480
|
+
<div className={cn("w-full", s.width)}>
|
|
481
|
+
{/* Render View Controls Header */}
|
|
482
|
+
<div className={cn("flex justify-center items-center mb-4 relative")}>
|
|
483
|
+
<div className={cn("font-semibold", s.headerTitle)}>
|
|
484
|
+
{view === 'months' ? currentMonth.getFullYear() : `${currentMonth.getFullYear() - (currentMonth.getFullYear() % 12)} - ${currentMonth.getFullYear() - (currentMonth.getFullYear() % 12) + 11}`}
|
|
485
|
+
</div>
|
|
486
|
+
{/* Navigation for views */}
|
|
487
|
+
<div className="absolute inset-0 flex justify-between items-center">
|
|
488
|
+
<button onClick={navigatePrev} className={cn("p-1 rounded-md border hover:bg-accent", s.navBtn, "flex items-center justify-center")}>
|
|
489
|
+
<svg className={s.icon} fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" /></svg>
|
|
490
|
+
</button>
|
|
491
|
+
<button onClick={navigateNext} className={cn("p-1 rounded-md border hover:bg-accent", s.navBtn, "flex items-center justify-center")}>
|
|
492
|
+
<svg className={s.icon} fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /></svg>
|
|
493
|
+
</button>
|
|
494
|
+
</div>
|
|
495
|
+
</div>
|
|
496
|
+
|
|
497
|
+
<div className="h-[240px]">
|
|
498
|
+
{view === 'months' && renderMonths()}
|
|
499
|
+
{view === 'years' && renderYears()}
|
|
500
|
+
</div>
|
|
501
|
+
</div>
|
|
502
|
+
)}
|
|
503
|
+
</div>
|
|
231
504
|
</div>
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
505
|
+
)
|
|
506
|
+
}
|
|
507
|
+
))
|
|
508
|
+
|
|
236
509
|
Calendar.displayName = "Calendar"
|
|
237
510
|
|
|
238
511
|
export { Calendar }
|
|
512
|
+
|