calendar.tsx raw

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