calendar.tsx raw
1 import * as React from "react";
2 import {
3 ChevronDownIcon,
4 ChevronLeftIcon,
5 ChevronRightIcon,
6 } from "lucide-react";
7 import {
8 DayPicker,
9 getDefaultClassNames,
10 type DayButton,
11 } from "react-day-picker";
12
13 import { cn } from "src/lib/utils";
14 import { Button } from "src/components/ui/button";
15 import { buttonVariants } from "src/components/ui/buttonVariants";
16
17 function Calendar({
18 className,
19 classNames,
20 showOutsideDays = true,
21 captionLayout = "label",
22 buttonVariant = "ghost",
23 formatters,
24 components,
25 ...props
26 }: React.ComponentProps<typeof DayPicker> & {
27 buttonVariant?: React.ComponentProps<typeof Button>["variant"];
28 }) {
29 const defaultClassNames = getDefaultClassNames();
30
31 return (
32 <DayPicker
33 showOutsideDays={showOutsideDays}
34 className={cn(
35 "group/calendar bg-background p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
36 String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
37 String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
38 className
39 )}
40 captionLayout={captionLayout}
41 formatters={{
42 formatMonthDropdown: (date) =>
43 date.toLocaleString("default", { month: "short" }),
44 ...formatters,
45 }}
46 classNames={{
47 root: cn("w-fit", defaultClassNames.root),
48 months: cn(
49 "relative flex flex-col gap-4 md:flex-row",
50 defaultClassNames.months
51 ),
52 month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
53 nav: cn(
54 "absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1",
55 defaultClassNames.nav
56 ),
57 button_previous: cn(
58 buttonVariants({ variant: buttonVariant }),
59 "size-(--cell-size) p-0 select-none aria-disabled:opacity-50",
60 defaultClassNames.button_previous
61 ),
62 button_next: cn(
63 buttonVariants({ variant: buttonVariant }),
64 "size-(--cell-size) p-0 select-none aria-disabled:opacity-50",
65 defaultClassNames.button_next
66 ),
67 month_caption: cn(
68 "flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)",
69 defaultClassNames.month_caption
70 ),
71 dropdowns: cn(
72 "flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium",
73 defaultClassNames.dropdowns
74 ),
75 dropdown_root: cn(
76 "relative rounded-md border border-input shadow-xs has-focus:border-ring has-focus:ring-[3px] has-focus:ring-ring/50",
77 defaultClassNames.dropdown_root
78 ),
79 dropdown: cn(
80 "absolute inset-0 bg-popover opacity-0",
81 defaultClassNames.dropdown
82 ),
83 caption_label: cn(
84 "font-medium select-none",
85 captionLayout === "label"
86 ? "text-sm"
87 : "flex h-8 items-center gap-1 rounded-md pr-1 pl-2 text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground",
88 defaultClassNames.caption_label
89 ),
90 table: "w-full border-collapse",
91 weekdays: cn("flex", defaultClassNames.weekdays),
92 weekday: cn(
93 "flex-1 rounded-md text-[0.8rem] font-normal text-muted-foreground select-none",
94 defaultClassNames.weekday
95 ),
96 week: cn("mt-2 flex w-full", defaultClassNames.week),
97 week_number_header: cn(
98 "w-(--cell-size) select-none",
99 defaultClassNames.week_number_header
100 ),
101 week_number: cn(
102 "text-[0.8rem] text-muted-foreground select-none",
103 defaultClassNames.week_number
104 ),
105 day: cn(
106 "group/day relative aspect-square h-full w-full p-0 text-center select-none [&:last-child[data-selected=true]_button]:rounded-r-md",
107 props.showWeekNumber
108 ? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-md"
109 : "[&:first-child[data-selected=true]_button]:rounded-l-md",
110 defaultClassNames.day
111 ),
112 range_start: cn(
113 "rounded-l-md bg-accent",
114 defaultClassNames.range_start
115 ),
116 range_middle: cn("rounded-none", defaultClassNames.range_middle),
117 range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
118 today: cn(
119 "rounded-md bg-accent text-accent-foreground data-[selected=true]:rounded-none",
120 defaultClassNames.today
121 ),
122 outside: cn(
123 "text-muted-foreground aria-selected:text-muted-foreground",
124 defaultClassNames.outside
125 ),
126 disabled: cn(
127 "text-muted-foreground opacity-50",
128 defaultClassNames.disabled
129 ),
130 hidden: cn("invisible", defaultClassNames.hidden),
131 ...classNames,
132 }}
133 components={{
134 Root: ({ className, rootRef, ...props }) => {
135 return (
136 <div
137 data-slot="calendar"
138 ref={rootRef}
139 className={cn(className)}
140 {...props}
141 />
142 );
143 },
144 Chevron: ({ className, orientation, ...props }) => {
145 if (orientation === "left") {
146 return (
147 <ChevronLeftIcon className={cn("size-4", className)} {...props} />
148 );
149 }
150
151 if (orientation === "right") {
152 return (
153 <ChevronRightIcon
154 className={cn("size-4", className)}
155 {...props}
156 />
157 );
158 }
159
160 return (
161 <ChevronDownIcon className={cn("size-4", className)} {...props} />
162 );
163 },
164 DayButton: CalendarDayButton,
165 WeekNumber: ({ children, ...props }) => {
166 return (
167 <td {...props}>
168 <div className="flex size-(--cell-size) items-center justify-center text-center">
169 {children}
170 </div>
171 </td>
172 );
173 },
174 ...components,
175 }}
176 {...props}
177 />
178 );
179 }
180
181 function CalendarDayButton({
182 className,
183 day,
184 modifiers,
185 ...props
186 }: React.ComponentProps<typeof DayButton>) {
187 const defaultClassNames = getDefaultClassNames();
188
189 const ref = React.useRef<HTMLButtonElement>(null);
190 React.useEffect(() => {
191 if (modifiers.focused) {
192 ref.current?.focus();
193 }
194 }, [modifiers.focused]);
195
196 return (
197 <Button
198 ref={ref}
199 variant="ghost"
200 size="icon"
201 data-day={day.date.toLocaleDateString()}
202 data-selected-single={
203 modifiers.selected &&
204 !modifiers.range_start &&
205 !modifiers.range_end &&
206 !modifiers.range_middle
207 }
208 data-range-start={modifiers.range_start}
209 data-range-end={modifiers.range_end}
210 data-range-middle={modifiers.range_middle}
211 className={cn(
212 "flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-[3px] group-data-[focused=true]/day:ring-ring/50 data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:rounded-none data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground dark:hover:text-accent-foreground [&>span]:text-xs [&>span]:opacity-70",
213 defaultClassNames.day,
214 className
215 )}
216 {...props}
217 />
218 );
219 }
220
221 export { Calendar, CalendarDayButton };
222