@moontra/moonui 2.0.8 → 2.0.9

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 (94) hide show
  1. package/package.json +9 -2
  2. package/src/__tests__/use-intersection-observer.test.tsx +216 -0
  3. package/src/__tests__/use-local-storage.test.tsx +174 -0
  4. package/src/__tests__/use-pro-access.test.tsx +183 -0
  5. package/src/components/ui/__tests__/accordion.test.tsx +571 -0
  6. package/src/components/ui/__tests__/alert.test.tsx +484 -0
  7. package/src/components/ui/__tests__/aspect-ratio.test.tsx +492 -0
  8. package/src/components/ui/__tests__/avatar.test.tsx +382 -0
  9. package/src/components/ui/__tests__/badge.test.tsx +364 -0
  10. package/src/components/ui/__tests__/breadcrumb.test.tsx +687 -0
  11. package/src/components/ui/__tests__/button.test.tsx +91 -0
  12. package/src/components/ui/__tests__/card.test.tsx +104 -0
  13. package/src/components/ui/__tests__/checkbox.test.tsx +591 -0
  14. package/src/components/ui/__tests__/collapsible.test.tsx +592 -0
  15. package/src/components/ui/__tests__/color-picker.test.tsx +365 -0
  16. package/src/components/ui/__tests__/command.test.tsx +677 -0
  17. package/src/components/ui/__tests__/data-table.test.tsx +256 -0
  18. package/src/components/ui/__tests__/date-picker.test.tsx +320 -0
  19. package/src/components/ui/__tests__/dialog.test.tsx +764 -0
  20. package/src/components/ui/__tests__/input.test.tsx +318 -0
  21. package/src/components/ui/__tests__/label.test.tsx +103 -0
  22. package/src/components/ui/__tests__/popover.test.tsx +637 -0
  23. package/src/components/ui/__tests__/progress.test.tsx +382 -0
  24. package/src/components/ui/__tests__/radio-group.test.tsx +555 -0
  25. package/src/components/ui/__tests__/select.test.tsx +705 -0
  26. package/src/components/ui/__tests__/skeleton.test.tsx +253 -0
  27. package/src/components/ui/__tests__/slider.test.tsx +493 -0
  28. package/src/components/ui/__tests__/switch.test.tsx +372 -0
  29. package/src/components/ui/__tests__/table.test.tsx +824 -0
  30. package/src/components/ui/__tests__/tabs.test.tsx +887 -0
  31. package/src/components/ui/__tests__/toast.test.tsx +458 -0
  32. package/src/components/ui/__tests__/tooltip.test.tsx +583 -0
  33. package/src/components/ui/accordion.tsx +63 -0
  34. package/src/components/ui/alert.tsx +130 -0
  35. package/src/components/ui/aspect-ratio.tsx +72 -0
  36. package/src/components/ui/avatar.tsx +135 -0
  37. package/src/components/ui/badge.tsx +225 -0
  38. package/src/components/ui/breadcrumb.tsx +207 -0
  39. package/src/components/ui/button.stories.tsx +162 -0
  40. package/src/components/ui/button.tsx +221 -0
  41. package/src/components/ui/calendar.tsx +262 -0
  42. package/src/components/ui/card.stories.tsx +208 -0
  43. package/src/components/ui/card.tsx +141 -0
  44. package/src/components/ui/checkbox.tsx +256 -0
  45. package/src/components/ui/collapsible.tsx +129 -0
  46. package/src/components/ui/color-picker.tsx +664 -0
  47. package/src/components/ui/command.tsx +218 -0
  48. package/src/components/ui/data-table.stories.tsx +509 -0
  49. package/src/components/ui/data-table.tsx +623 -0
  50. package/src/components/ui/date-picker.stories.tsx +321 -0
  51. package/src/components/ui/date-picker.tsx +603 -0
  52. package/src/components/ui/dialog.tsx +332 -0
  53. package/src/components/ui/dropdown-menu.tsx +200 -0
  54. package/src/components/ui/file-upload.tsx +400 -0
  55. package/src/components/ui/github-stars.tsx +201 -0
  56. package/src/components/ui/index.ts +12 -0
  57. package/src/components/ui/input.stories.tsx +255 -0
  58. package/src/components/ui/input.tsx +219 -0
  59. package/src/components/ui/label.tsx +26 -0
  60. package/src/components/ui/locked-component.tsx +215 -0
  61. package/src/components/ui/moon-logo.tsx +97 -0
  62. package/src/components/ui/pagination.tsx +116 -0
  63. package/src/components/ui/popover.tsx +183 -0
  64. package/src/components/ui/progress.tsx +182 -0
  65. package/src/components/ui/radio-group.tsx +243 -0
  66. package/src/components/ui/select.tsx +273 -0
  67. package/src/components/ui/separator.tsx +140 -0
  68. package/src/components/ui/simple-editor.tsx +652 -0
  69. package/src/components/ui/skeleton.tsx +351 -0
  70. package/src/components/ui/slider.tsx +351 -0
  71. package/src/components/ui/switch.tsx +83 -0
  72. package/src/components/ui/table.tsx +322 -0
  73. package/src/components/ui/tabs.tsx +195 -0
  74. package/src/components/ui/textarea.tsx +46 -0
  75. package/src/components/ui/toast.tsx +313 -0
  76. package/src/components/ui/toggle.tsx +47 -0
  77. package/src/components/ui/tooltip.tsx +152 -0
  78. package/src/docs/theme-system.md +1194 -0
  79. package/src/index.tsx +39 -0
  80. package/src/lib/micro-interactions.ts +255 -0
  81. package/src/lib/theme.tsx +398 -0
  82. package/src/lib/utils.ts +69 -0
  83. package/src/styles/design-system.css +365 -0
  84. package/src/styles/index.css +4 -0
  85. package/src/styles/tailwind.css +6 -0
  86. package/src/styles/tokens.css +453 -0
  87. package/src/use-intersection-observer.tsx +154 -0
  88. package/src/use-local-storage.tsx +71 -0
  89. package/src/use-paddle.ts +138 -0
  90. package/src/use-performance-optimizer.ts +379 -0
  91. package/src/use-pro-access.ts +141 -0
  92. package/src/use-scroll-animation.ts +221 -0
  93. package/src/use-subscription.ts +37 -0
  94. package/src/use-toast.ts +32 -0
@@ -0,0 +1,603 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { format, isValid } from "date-fns";
5
+ import { Calendar as CalendarIcon, Clock } from "lucide-react";
6
+ import { cn } from "../../lib/utils";
7
+ import { Button } from "./button";
8
+ import { Calendar } from "./calendar";
9
+ import {
10
+ Popover,
11
+ PopoverContent,
12
+ PopoverTrigger,
13
+ } from "./popover";
14
+ import { cva, type VariantProps } from "class-variance-authority";
15
+ import { motion, AnimatePresence } from "framer-motion";
16
+ import { microInteractionVariants } from "@/lib/micro-interactions";
17
+
18
+ /**
19
+ * DatePicker Component
20
+ *
21
+ * A comprehensive date picker with calendar interface
22
+ */
23
+
24
+ const datePickerVariants = cva(
25
+ "w-full justify-start text-left font-normal",
26
+ {
27
+ variants: {
28
+ variant: {
29
+ default: "",
30
+ outline: "border-2",
31
+ ghost: "hover:bg-accent hover:text-accent-foreground",
32
+ minimal: "h-auto p-0 border-0 shadow-none",
33
+ },
34
+ size: {
35
+ default: "h-10 px-4 py-2",
36
+ sm: "h-8 px-3 text-sm",
37
+ lg: "h-12 px-6 text-base",
38
+ icon: "h-10 w-10",
39
+ },
40
+ state: {
41
+ default: "",
42
+ error: "border-destructive focus:ring-destructive",
43
+ success: "border-success focus:ring-success",
44
+ warning: "border-warning focus:ring-warning",
45
+ },
46
+ },
47
+ defaultVariants: {
48
+ variant: "default",
49
+ size: "default",
50
+ state: "default",
51
+ },
52
+ }
53
+ );
54
+
55
+ export interface DatePickerProps
56
+ extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onChange">,
57
+ VariantProps<typeof datePickerVariants> {
58
+ /**
59
+ * Selected date value
60
+ */
61
+ value?: Date;
62
+ /**
63
+ * Callback when date changes
64
+ */
65
+ onChange?: (date: Date | undefined) => void;
66
+ /**
67
+ * Placeholder text
68
+ */
69
+ placeholder?: string;
70
+ /**
71
+ * Date format string
72
+ */
73
+ formatString?: string;
74
+ /**
75
+ * Minimum selectable date
76
+ */
77
+ minDate?: Date;
78
+ /**
79
+ * Maximum selectable date
80
+ */
81
+ maxDate?: Date;
82
+ /**
83
+ * Disabled dates
84
+ */
85
+ disabledDates?: Date[];
86
+ /**
87
+ * Disabled days of week (0-6, Sunday is 0)
88
+ */
89
+ disabledDaysOfWeek?: number[];
90
+ /**
91
+ * Show week numbers
92
+ */
93
+ showWeekNumbers?: boolean;
94
+ /**
95
+ * Icon position
96
+ */
97
+ iconPosition?: "left" | "right";
98
+ /**
99
+ * Custom icon
100
+ */
101
+ icon?: React.ReactNode;
102
+ /**
103
+ * Allow clear
104
+ */
105
+ allowClear?: boolean;
106
+ /**
107
+ * Read only
108
+ */
109
+ readOnly?: boolean;
110
+ /**
111
+ * Show today button
112
+ */
113
+ showTodayButton?: boolean;
114
+ /**
115
+ * Calendar props
116
+ */
117
+ calendarProps?: React.ComponentProps<typeof Calendar>;
118
+ }
119
+
120
+ export function DatePicker({
121
+ className,
122
+ variant,
123
+ size,
124
+ state,
125
+ value,
126
+ onChange,
127
+ placeholder = "Pick a date",
128
+ formatString = "PPP",
129
+ minDate,
130
+ maxDate,
131
+ disabledDates,
132
+ disabledDaysOfWeek,
133
+ showWeekNumbers,
134
+ iconPosition = "left",
135
+ icon,
136
+ allowClear = true,
137
+ readOnly = false,
138
+ showTodayButton = true,
139
+ calendarProps,
140
+ disabled,
141
+ ...props
142
+ }: DatePickerProps) {
143
+ const [open, setOpen] = React.useState(false);
144
+ const [internalDate, setInternalDate] = React.useState<Date | undefined>(value);
145
+
146
+ React.useEffect(() => {
147
+ setInternalDate(value);
148
+ }, [value]);
149
+
150
+ const handleSelect = (date: Date | undefined) => {
151
+ setInternalDate(date);
152
+ onChange?.(date);
153
+ if (date) {
154
+ setOpen(false);
155
+ }
156
+ };
157
+
158
+ const handleClear = (e: React.MouseEvent) => {
159
+ e.stopPropagation();
160
+ handleSelect(undefined);
161
+ };
162
+
163
+ const handleToday = () => {
164
+ const today = new Date();
165
+ handleSelect(today);
166
+ };
167
+
168
+ const isDateDisabled = (date: Date) => {
169
+ if (minDate && date < minDate) return true;
170
+ if (maxDate && date > maxDate) return true;
171
+ if (disabledDates?.some(d => d.toDateString() === date.toDateString())) return true;
172
+ if (disabledDaysOfWeek?.includes(date.getDay())) return true;
173
+ return false;
174
+ };
175
+
176
+ const displayValue = internalDate && isValid(internalDate)
177
+ ? format(internalDate, formatString)
178
+ : null;
179
+
180
+ const iconElement = icon || <CalendarIcon className="h-4 w-4" />;
181
+
182
+ return (
183
+ <Popover open={open} onOpenChange={setOpen}>
184
+ <PopoverTrigger asChild>
185
+ <Button
186
+ variant={variant === "minimal" ? "ghost" : variant || "outline"}
187
+ className={cn(
188
+ datePickerVariants({ variant, size, state }),
189
+ !displayValue && "text-muted-foreground",
190
+ className
191
+ )}
192
+ disabled={disabled || readOnly}
193
+ {...props}
194
+ >
195
+ <AnimatePresence mode="wait">
196
+ <motion.div
197
+ key={displayValue || "placeholder"}
198
+ initial={{ opacity: 0, y: -2 }}
199
+ animate={{ opacity: 1, y: 0 }}
200
+ exit={{ opacity: 0, y: 2 }}
201
+ transition={{ duration: 0.15 }}
202
+ className="flex items-center gap-2 flex-1"
203
+ >
204
+ {iconPosition === "left" && iconElement}
205
+ <span className="flex-1 truncate">
206
+ {displayValue || placeholder}
207
+ </span>
208
+ {iconPosition === "right" && iconElement}
209
+ </motion.div>
210
+ </AnimatePresence>
211
+ {allowClear && displayValue && !readOnly && !disabled && (
212
+ <motion.div
213
+ initial={{ opacity: 0, scale: 0.8 }}
214
+ animate={{ opacity: 1, scale: 1 }}
215
+ exit={{ opacity: 0, scale: 0.8 }}
216
+ whileHover={{ scale: 1.1 }}
217
+ whileTap={{ scale: 0.9 }}
218
+ className="ml-2 h-4 w-4 shrink-0 opacity-50 hover:opacity-100 transition-opacity cursor-pointer"
219
+ onClick={handleClear}
220
+ role="button"
221
+ tabIndex={0}
222
+ onKeyDown={(e) => {
223
+ if (e.key === 'Enter' || e.key === ' ') {
224
+ e.preventDefault();
225
+ handleClear(e as any);
226
+ }
227
+ }}
228
+ >
229
+ <span className="sr-only">Clear date</span>
230
+ <svg
231
+ width="15"
232
+ height="15"
233
+ viewBox="0 0 15 15"
234
+ fill="none"
235
+ xmlns="http://www.w3.org/2000/svg"
236
+ >
237
+ <path
238
+ d="M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z"
239
+ fill="currentColor"
240
+ fillRule="evenodd"
241
+ clipRule="evenodd"
242
+ />
243
+ </svg>
244
+ </motion.div>
245
+ )}
246
+ </Button>
247
+ </PopoverTrigger>
248
+ <PopoverContent
249
+ className="w-[360px] p-0 shadow-xl rounded-2xl border-0 bg-background/95 backdrop-blur-sm"
250
+ align="start"
251
+ sideOffset={12}
252
+ >
253
+ <motion.div
254
+ {...microInteractionVariants.fadeIn}
255
+ className="rounded-2xl bg-background overflow-hidden"
256
+ >
257
+ <Calendar
258
+ mode="single"
259
+ selected={internalDate}
260
+ onSelect={handleSelect}
261
+ disabled={isDateDisabled}
262
+ showOutsideDays={false}
263
+ className={cn("rounded-2xl", calendarProps?.className)}
264
+ {...calendarProps}
265
+ />
266
+ {showTodayButton && (
267
+ <div className="border-t px-4 py-3">
268
+ <Button
269
+ variant="ghost"
270
+ size="sm"
271
+ onClick={handleToday}
272
+ className="w-full justify-center hover:bg-accent hover:text-accent-foreground transition-all duration-200"
273
+ >
274
+ Today
275
+ </Button>
276
+ </div>
277
+ )}
278
+ </motion.div>
279
+ </PopoverContent>
280
+ </Popover>
281
+ );
282
+ }
283
+
284
+ /**
285
+ * DateRangePicker Component
286
+ *
287
+ * Select a date range with start and end dates
288
+ */
289
+
290
+ export interface DateRangePickerProps
291
+ extends Omit<DatePickerProps, "value" | "onChange"> {
292
+ /**
293
+ * Selected date range
294
+ */
295
+ value?: { from: Date | undefined; to: Date | undefined };
296
+ /**
297
+ * Callback when date range changes
298
+ */
299
+ onChange?: (range: { from: Date | undefined; to: Date | undefined } | undefined) => void;
300
+ /**
301
+ * Separator between dates
302
+ */
303
+ separator?: string;
304
+ }
305
+
306
+ export function DateRangePicker({
307
+ className,
308
+ value,
309
+ onChange,
310
+ placeholder = "Pick a date range",
311
+ formatString = "LLL dd, y",
312
+ separator = " - ",
313
+ ...props
314
+ }: DateRangePickerProps) {
315
+ const [open, setOpen] = React.useState(false);
316
+ const [internalRange, setInternalRange] = React.useState(value);
317
+
318
+ React.useEffect(() => {
319
+ setInternalRange(value);
320
+ }, [value]);
321
+
322
+ const handleSelect = (range: { from: Date | undefined; to: Date | undefined } | undefined) => {
323
+ setInternalRange(range);
324
+ onChange?.(range);
325
+ if (range?.from && range?.to) {
326
+ setOpen(false);
327
+ }
328
+ };
329
+
330
+ const displayValue = React.useMemo(() => {
331
+ if (!internalRange?.from) return null;
332
+ if (!internalRange?.to) {
333
+ return format(internalRange.from, formatString);
334
+ }
335
+ return `${format(internalRange.from, formatString)}${separator}${format(
336
+ internalRange.to,
337
+ formatString
338
+ )}`;
339
+ }, [internalRange, formatString, separator]);
340
+
341
+ return (
342
+ <Popover open={open} onOpenChange={setOpen}>
343
+ <PopoverTrigger asChild>
344
+ <Button
345
+ variant={props.variant === "minimal" ? "ghost" : props.variant || "outline"}
346
+ className={cn(
347
+ datePickerVariants({
348
+ variant: props.variant,
349
+ size: props.size,
350
+ state: props.state
351
+ }),
352
+ !displayValue && "text-muted-foreground",
353
+ className
354
+ )}
355
+ disabled={props.disabled || props.readOnly}
356
+ >
357
+ <CalendarIcon className="mr-2 h-4 w-4" />
358
+ <span className="flex-1 truncate text-left">
359
+ {displayValue || placeholder}
360
+ </span>
361
+ </Button>
362
+ </PopoverTrigger>
363
+ <PopoverContent className="w-auto p-0 shadow-xl rounded-2xl border-0 bg-background/95 backdrop-blur-sm" align="start" sideOffset={12}>
364
+ <Calendar
365
+ mode="range"
366
+ defaultMonth={internalRange?.from}
367
+ selected={internalRange}
368
+ onSelect={handleSelect as any}
369
+ showOutsideDays={false}
370
+ {...props.calendarProps}
371
+ className="rounded-2xl"
372
+ />
373
+ </PopoverContent>
374
+ </Popover>
375
+ );
376
+ }
377
+
378
+ /**
379
+ * DateTimePicker Component
380
+ *
381
+ * Combined date and time picker
382
+ */
383
+
384
+ export interface DateTimePickerProps extends DatePickerProps {
385
+ /**
386
+ * Show time picker
387
+ */
388
+ showTimePicker?: boolean;
389
+ /**
390
+ * Time format (12 or 24 hour)
391
+ */
392
+ timeFormat?: "12" | "24";
393
+ /**
394
+ * Time interval in minutes
395
+ */
396
+ timeInterval?: number;
397
+ }
398
+
399
+ export function DateTimePicker({
400
+ value,
401
+ onChange,
402
+ formatString = "PPP p",
403
+ showTimePicker = true,
404
+ timeFormat = "24",
405
+ timeInterval = 15,
406
+ ...props
407
+ }: DateTimePickerProps) {
408
+ const [date, setDate] = React.useState<Date | undefined>(value);
409
+ const [time, setTime] = React.useState<string>(
410
+ value ? format(value, timeFormat === "24" ? "HH:mm" : "hh:mm a") : "00:00"
411
+ );
412
+
413
+ const handleDateChange = (newDate: Date | undefined) => {
414
+ if (!newDate) {
415
+ setDate(undefined);
416
+ onChange?.(undefined);
417
+ return;
418
+ }
419
+
420
+ const [hours, minutes] = time.split(":").map(Number);
421
+ newDate.setHours(hours, minutes);
422
+ setDate(newDate);
423
+ onChange?.(newDate);
424
+ };
425
+
426
+ const handleTimeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
427
+ const newTime = e.target.value;
428
+ setTime(newTime);
429
+
430
+ if (date) {
431
+ const [hours, minutes] = newTime.split(":").map(Number);
432
+ const newDate = new Date(date);
433
+ newDate.setHours(hours, minutes);
434
+ setDate(newDate);
435
+ onChange?.(newDate);
436
+ }
437
+ };
438
+
439
+ // Generate time options
440
+ const timeOptions = React.useMemo(() => {
441
+ const options = [];
442
+ for (let h = 0; h < 24; h++) {
443
+ for (let m = 0; m < 60; m += timeInterval) {
444
+ const hour24 = h.toString().padStart(2, "0");
445
+ const minute = m.toString().padStart(2, "0");
446
+ const time24 = `${hour24}:${minute}`;
447
+
448
+ if (timeFormat === "12") {
449
+ const hour12 = h === 0 ? 12 : h > 12 ? h - 12 : h;
450
+ const ampm = h < 12 ? "AM" : "PM";
451
+ const time12 = `${hour12}:${minute} ${ampm}`;
452
+ options.push({ value: time24, label: time12 });
453
+ } else {
454
+ options.push({ value: time24, label: time24 });
455
+ }
456
+ }
457
+ }
458
+ return options;
459
+ }, [timeFormat, timeInterval]);
460
+
461
+ return (
462
+ <div className="flex flex-col gap-2">
463
+ <DatePicker
464
+ {...props}
465
+ value={date}
466
+ onChange={handleDateChange}
467
+ formatString="PPP"
468
+ showTodayButton={false}
469
+ />
470
+ {showTimePicker && date && (
471
+ <motion.div
472
+ initial={{ opacity: 0, height: 0 }}
473
+ animate={{ opacity: 1, height: "auto" }}
474
+ exit={{ opacity: 0, height: 0 }}
475
+ className="flex items-center gap-2"
476
+ >
477
+ <Clock className="h-4 w-4 text-muted-foreground" />
478
+ <select
479
+ value={time}
480
+ onChange={handleTimeChange}
481
+ className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm transition-colors focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
482
+ >
483
+ {timeOptions.map((option) => (
484
+ <option key={option.value} value={option.value}>
485
+ {option.label}
486
+ </option>
487
+ ))}
488
+ </select>
489
+ </motion.div>
490
+ )}
491
+ </div>
492
+ );
493
+ }
494
+
495
+ /**
496
+ * MonthPicker Component
497
+ *
498
+ * Select month and year
499
+ */
500
+
501
+ export interface MonthPickerProps extends Omit<DatePickerProps, "formatString"> {
502
+ /**
503
+ * Format for displaying the month
504
+ */
505
+ formatString?: string;
506
+ }
507
+
508
+ export function MonthPicker({
509
+ value,
510
+ onChange,
511
+ placeholder = "Pick a month",
512
+ formatString = "MMMM yyyy",
513
+ ...props
514
+ }: MonthPickerProps) {
515
+ const [open, setOpen] = React.useState(false);
516
+ const [viewDate, setViewDate] = React.useState(value || new Date());
517
+
518
+ const months = [
519
+ "January", "February", "March", "April",
520
+ "May", "June", "July", "August",
521
+ "September", "October", "November", "December"
522
+ ];
523
+
524
+ const handleMonthSelect = (monthIndex: number) => {
525
+ const newDate = new Date(viewDate);
526
+ newDate.setMonth(monthIndex);
527
+ onChange?.(newDate);
528
+ setOpen(false);
529
+ };
530
+
531
+ const handleYearChange = (increment: number) => {
532
+ const newDate = new Date(viewDate);
533
+ newDate.setFullYear(newDate.getFullYear() + increment);
534
+ setViewDate(newDate);
535
+ };
536
+
537
+ const displayValue = value ? format(value, formatString) : null;
538
+
539
+ return (
540
+ <Popover open={open} onOpenChange={setOpen}>
541
+ <PopoverTrigger asChild>
542
+ <Button
543
+ variant={props.variant || "outline"}
544
+ className={cn(
545
+ datePickerVariants({
546
+ variant: props.variant,
547
+ size: props.size,
548
+ state: props.state
549
+ }),
550
+ !displayValue && "text-muted-foreground",
551
+ props.className
552
+ )}
553
+ disabled={props.disabled || props.readOnly}
554
+ >
555
+ <CalendarIcon className="mr-2 h-4 w-4" />
556
+ {displayValue || placeholder}
557
+ </Button>
558
+ </PopoverTrigger>
559
+ <PopoverContent className="w-64 p-0" align="start">
560
+ <div className="p-3">
561
+ <div className="flex items-center justify-between mb-3">
562
+ <Button
563
+ variant="outline"
564
+ size="icon"
565
+ onClick={() => handleYearChange(-1)}
566
+ >
567
+ <ChevronLeft className="h-4 w-4" />
568
+ </Button>
569
+ <span className="font-semibold">
570
+ {viewDate.getFullYear()}
571
+ </span>
572
+ <Button
573
+ variant="outline"
574
+ size="icon"
575
+ onClick={() => handleYearChange(1)}
576
+ >
577
+ <ChevronRight className="h-4 w-4" />
578
+ </Button>
579
+ </div>
580
+ <div className="grid grid-cols-3 gap-2">
581
+ {months.map((month, index) => (
582
+ <Button
583
+ key={month}
584
+ variant={value && value.getMonth() === index ? "default" : "outline"}
585
+ size="sm"
586
+ onClick={() => handleMonthSelect(index)}
587
+ className="h-8 text-xs"
588
+ >
589
+ {month.slice(0, 3)}
590
+ </Button>
591
+ ))}
592
+ </div>
593
+ </div>
594
+ </PopoverContent>
595
+ </Popover>
596
+ );
597
+ }
598
+
599
+ // Re-export utilities
600
+ export { format } from "date-fns";
601
+
602
+ // Import ChevronLeft and ChevronRight for MonthPicker
603
+ import { ChevronLeft, ChevronRight } from "lucide-react";