calendar.jsx 6.8 KB

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