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