@srcroot/ui 0.0.1 → 0.0.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.
@@ -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
- return d >= start.getTime() && d <= end.getTime()
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
- * @example
80
- * // Single date
81
- * const [date, setDate] = useState<Date>()
82
- * <Calendar mode="single" selected={date} onSelect={setDate} />
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
- mode = "single",
93
- selected,
94
- onSelect,
95
- defaultMonth = new Date(),
96
- minDate,
97
- maxDate,
98
- disabled,
99
- className,
100
- },
101
- ref
102
- ) => {
103
- const [currentMonth, setCurrentMonth] = React.useState(defaultMonth)
104
- const [rangeStart, setRangeStart] = React.useState<Date | null>(null)
105
-
106
- const selectedDates = React.useMemo(() => {
107
- if (!selected) return []
108
- return Array.isArray(selected) ? selected : [selected]
109
- }, [selected])
110
-
111
- const handleSelect = (date: Date) => {
112
- if (disabled) return
113
- if (minDate && date < minDate) return
114
- if (maxDate && date > maxDate) return
115
-
116
- if (mode === "single") {
117
- onSelect?.(date)
118
- } else if (mode === "multiple") {
119
- const exists = selectedDates.some(d => isSameDay(d, date))
120
- if (exists) {
121
- onSelect?.(selectedDates.filter(d => !isSameDay(d, date)))
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
- onSelect?.([...selectedDates, date])
168
+ setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1, 1))
124
169
  }
125
- } else if (mode === "range") {
126
- if (!rangeStart) {
127
- setRangeStart(date)
128
- onSelect?.([date])
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
- const start = rangeStart < date ? rangeStart : date
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
- const navigatePrev = () => {
139
- setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1, 1))
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
- const navigateNext = () => {
143
- setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 1))
144
- }
257
+ // --- Render Helpers ---
145
258
 
146
- const days = getDaysInMonth(currentMonth.getFullYear(), currentMonth.getMonth())
147
- const rangeEnd = mode === "range" && selectedDates.length === 2 ? selectedDates[1] : null
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
- {/* Day names */}
184
- <div className="grid grid-cols-7 gap-1 mb-1">
185
- {DAYS.map(day => (
186
- <div key={day} className="text-center text-xs text-muted-foreground font-medium py-1">
187
- {day}
188
- </div>
189
- ))}
190
- </div>
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
- {/* Days grid */}
193
- <div className="grid grid-cols-7 gap-1" role="grid">
194
- {days.map((date, index) => {
195
- const isCurrentMonth = date.getMonth() === currentMonth.getMonth()
196
- const isSelected = selectedDates.some(d => isSameDay(d, date))
197
- const isRangeStart = rangeStart && isSameDay(date, rangeStart)
198
- const isRangeEnd = rangeEnd && isSameDay(date, rangeEnd)
199
- const inRange = isInRange(date, rangeStart || selectedDates[0], rangeEnd)
200
- const isToday = isSameDay(date, new Date())
201
- const isDisabled = disabled ||
202
- (minDate && date < minDate) ||
203
- (maxDate && date > maxDate)
204
-
205
- return (
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
- role="gridcell"
210
- aria-selected={isSelected}
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
- {date.getDate()}
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
- </div>
233
- )
234
- }
235
- )
505
+ )
506
+ }
507
+ ))
508
+
236
509
  Calendar.displayName = "Calendar"
237
510
 
238
511
  export { Calendar }
512
+