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