calendar.tsx 7.5 KB

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