@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.
- package/package.json +9 -2
- package/src/__tests__/use-intersection-observer.test.tsx +216 -0
- package/src/__tests__/use-local-storage.test.tsx +174 -0
- package/src/__tests__/use-pro-access.test.tsx +183 -0
- package/src/components/ui/__tests__/accordion.test.tsx +571 -0
- package/src/components/ui/__tests__/alert.test.tsx +484 -0
- package/src/components/ui/__tests__/aspect-ratio.test.tsx +492 -0
- package/src/components/ui/__tests__/avatar.test.tsx +382 -0
- package/src/components/ui/__tests__/badge.test.tsx +364 -0
- package/src/components/ui/__tests__/breadcrumb.test.tsx +687 -0
- package/src/components/ui/__tests__/button.test.tsx +91 -0
- package/src/components/ui/__tests__/card.test.tsx +104 -0
- package/src/components/ui/__tests__/checkbox.test.tsx +591 -0
- package/src/components/ui/__tests__/collapsible.test.tsx +592 -0
- package/src/components/ui/__tests__/color-picker.test.tsx +365 -0
- package/src/components/ui/__tests__/command.test.tsx +677 -0
- package/src/components/ui/__tests__/data-table.test.tsx +256 -0
- package/src/components/ui/__tests__/date-picker.test.tsx +320 -0
- package/src/components/ui/__tests__/dialog.test.tsx +764 -0
- package/src/components/ui/__tests__/input.test.tsx +318 -0
- package/src/components/ui/__tests__/label.test.tsx +103 -0
- package/src/components/ui/__tests__/popover.test.tsx +637 -0
- package/src/components/ui/__tests__/progress.test.tsx +382 -0
- package/src/components/ui/__tests__/radio-group.test.tsx +555 -0
- package/src/components/ui/__tests__/select.test.tsx +705 -0
- package/src/components/ui/__tests__/skeleton.test.tsx +253 -0
- package/src/components/ui/__tests__/slider.test.tsx +493 -0
- package/src/components/ui/__tests__/switch.test.tsx +372 -0
- package/src/components/ui/__tests__/table.test.tsx +824 -0
- package/src/components/ui/__tests__/tabs.test.tsx +887 -0
- package/src/components/ui/__tests__/toast.test.tsx +458 -0
- package/src/components/ui/__tests__/tooltip.test.tsx +583 -0
- package/src/components/ui/accordion.tsx +63 -0
- package/src/components/ui/alert.tsx +130 -0
- package/src/components/ui/aspect-ratio.tsx +72 -0
- package/src/components/ui/avatar.tsx +135 -0
- package/src/components/ui/badge.tsx +225 -0
- package/src/components/ui/breadcrumb.tsx +207 -0
- package/src/components/ui/button.stories.tsx +162 -0
- package/src/components/ui/button.tsx +221 -0
- package/src/components/ui/calendar.tsx +262 -0
- package/src/components/ui/card.stories.tsx +208 -0
- package/src/components/ui/card.tsx +141 -0
- package/src/components/ui/checkbox.tsx +256 -0
- package/src/components/ui/collapsible.tsx +129 -0
- package/src/components/ui/color-picker.tsx +664 -0
- package/src/components/ui/command.tsx +218 -0
- package/src/components/ui/data-table.stories.tsx +509 -0
- package/src/components/ui/data-table.tsx +623 -0
- package/src/components/ui/date-picker.stories.tsx +321 -0
- package/src/components/ui/date-picker.tsx +603 -0
- package/src/components/ui/dialog.tsx +332 -0
- package/src/components/ui/dropdown-menu.tsx +200 -0
- package/src/components/ui/file-upload.tsx +400 -0
- package/src/components/ui/github-stars.tsx +201 -0
- package/src/components/ui/index.ts +12 -0
- package/src/components/ui/input.stories.tsx +255 -0
- package/src/components/ui/input.tsx +219 -0
- package/src/components/ui/label.tsx +26 -0
- package/src/components/ui/locked-component.tsx +215 -0
- package/src/components/ui/moon-logo.tsx +97 -0
- package/src/components/ui/pagination.tsx +116 -0
- package/src/components/ui/popover.tsx +183 -0
- package/src/components/ui/progress.tsx +182 -0
- package/src/components/ui/radio-group.tsx +243 -0
- package/src/components/ui/select.tsx +273 -0
- package/src/components/ui/separator.tsx +140 -0
- package/src/components/ui/simple-editor.tsx +652 -0
- package/src/components/ui/skeleton.tsx +351 -0
- package/src/components/ui/slider.tsx +351 -0
- package/src/components/ui/switch.tsx +83 -0
- package/src/components/ui/table.tsx +322 -0
- package/src/components/ui/tabs.tsx +195 -0
- package/src/components/ui/textarea.tsx +46 -0
- package/src/components/ui/toast.tsx +313 -0
- package/src/components/ui/toggle.tsx +47 -0
- package/src/components/ui/tooltip.tsx +152 -0
- package/src/docs/theme-system.md +1194 -0
- package/src/index.tsx +39 -0
- package/src/lib/micro-interactions.ts +255 -0
- package/src/lib/theme.tsx +398 -0
- package/src/lib/utils.ts +69 -0
- package/src/styles/design-system.css +365 -0
- package/src/styles/index.css +4 -0
- package/src/styles/tailwind.css +6 -0
- package/src/styles/tokens.css +453 -0
- package/src/use-intersection-observer.tsx +154 -0
- package/src/use-local-storage.tsx +71 -0
- package/src/use-paddle.ts +138 -0
- package/src/use-performance-optimizer.ts +379 -0
- package/src/use-pro-access.ts +141 -0
- package/src/use-scroll-animation.ts +221 -0
- package/src/use-subscription.ts +37 -0
- 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";
|