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