calendar.tsx 7.5 KB

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