'use client'; import * as React from 'react'; import { ChevronLeft, ChevronRight, ChevronDown } from 'lucide-react'; import { DayPicker, getDefaultClassNames, type DayButton } from 'react-day-picker'; import { cn } from '../../shared/utils'; import { Button, buttonVariants } from '../button'; export type CalendarProps = React.ComponentProps & { buttonVariant?: React.ComponentProps['variant']; }; /** * Monthly date picker built on react-day-picker. * * @description * Renders a customized month/year calendar styled for the Xertica UI design system. * Supports single date selection, date ranges, and disabled date rules. * * @ai-rules * 1. Commonly embedded inside a `` to create a full DatePicker in forms. * 2. Control the selected date via the REQUIRED `selected={Date}` prop. * 3. For range selection, pass `mode="range"` and `selected={{ from, to }}`. */ function Calendar({ className, classNames, showOutsideDays = true, captionLayout = 'label', buttonVariant = 'ghost', formatters, components, ...props }: CalendarProps) { const defaultClassNames = getDefaultClassNames(); const isDropdownCaption = captionLayout.startsWith('dropdown'); return ( svg]:rotate-180`, String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`, className )} captionLayout={captionLayout} formatters={formatters} classNames={{ root: cn('w-fit', defaultClassNames.root), months: cn('flex gap-6 flex-col md:flex-row relative', defaultClassNames.months), month: cn('flex flex-col w-full gap-5', defaultClassNames.month), nav: cn( 'flex items-center gap-2 w-full absolute top-0 inset-x-0 justify-between', defaultClassNames.nav ), button_previous: cn( buttonVariants({ variant: buttonVariant }), 'size-(--cell-size) aria-disabled:opacity-40 p-0 select-none hover:bg-accent/50 transition-colors duration-200', defaultClassNames.button_previous ), button_next: cn( buttonVariants({ variant: buttonVariant }), 'size-(--cell-size) aria-disabled:opacity-40 p-0 select-none hover:bg-accent/50 transition-colors duration-200', defaultClassNames.button_next ), month_caption: cn( 'flex items-center justify-center w-full', isDropdownCaption ? 'h-auto px-0' : 'h-(--cell-size) px-(--cell-size)', defaultClassNames.month_caption ), dropdowns: cn( 'w-full flex items-center justify-center gap-2', isDropdownCaption ? 'h-auto' : 'h-(--cell-size)', defaultClassNames.dropdowns ), dropdown_root: cn( isDropdownCaption ? 'relative flex min-w-[7rem] items-center rounded-[var(--radius)] border border-border bg-background text-foreground transition-colors outline-none focus-within:ring-2 focus-within:ring-primary focus-within:border-transparent data-[disabled=true]:cursor-not-allowed data-[disabled=true]:opacity-50 px-2.5 py-1' : 'relative rounded-[var(--radius)] hover:bg-accent transition-colors', defaultClassNames.dropdown_root ), dropdown: cn( isDropdownCaption ? 'absolute inset-0 cursor-pointer opacity-0' : 'absolute bg-popover inset-0 opacity-0 cursor-pointer', defaultClassNames.dropdown ), caption_label: cn( 'select-none font-[family-name:var(--font-family,Roboto)] tracking-tight transition-all duration-200', captionLayout === 'label' ? 'rounded-[var(--radius)] px-2 py-1 text-[length:var(--calendar-caption-size)] font-[number:var(--font-weight-semibold)] hover:bg-accent hover:text-accent-foreground' : 'flex h-full w-full items-center justify-between gap-2 rounded-[var(--radius)] text-[length:var(--calendar-caption-size)] font-[number:var(--font-weight-semibold)] [&>svg]:text-muted-foreground [&>svg]:size-4', defaultClassNames.caption_label ), table: 'w-full border-collapse mt-1', weekdays: cn('flex gap-1', defaultClassNames.weekdays), weekday: cn( 'text-muted-foreground rounded-(--cell-radius) flex-1 select-none text-[length:var(--calendar-weekday-size)] font-[number:var(--font-weight-medium)] uppercase tracking-wider', defaultClassNames.weekday ), week: cn('flex w-full gap-1', defaultClassNames.week), week_number_header: cn('select-none w-(--cell-size)', defaultClassNames.week_number_header), week_number: cn( 'select-none text-muted-foreground text-[length:var(--calendar-weekday-size)]', defaultClassNames.week_number ), day: cn( 'relative w-full rounded-(--cell-radius) h-full p-0 text-center [&:last-child[data-selected=true]_button]:rounded-r-(--cell-radius) group/day aspect-square select-none', props.showWeekNumber ? '[&:nth-child(2)[data-selected=true]_button]:rounded-l-(--cell-radius)' : '[&:first-child[data-selected=true]_button]:rounded-l-(--cell-radius)', defaultClassNames.day ), range_start: cn( 'rounded-l-(--cell-radius) bg-accent/50 relative after:bg-accent/50 after:absolute after:inset-y-0 after:w-2 after:right-0 -z-0 isolate', defaultClassNames.range_start ), range_middle: cn('rounded-none bg-accent/30', defaultClassNames.range_middle), range_end: cn( 'rounded-r-(--cell-radius) bg-accent/50 relative after:bg-accent/50 after:absolute after:inset-y-0 after:w-2 after:left-0 -z-0 isolate', defaultClassNames.range_end ), today: cn( 'bg-accent/60 text-accent-foreground rounded-(--cell-radius) data-[selected=true]:rounded-none font-[number:var(--font-weight-semibold)]', defaultClassNames.today ), outside: cn( 'text-muted-foreground aria-selected:text-muted-foreground', defaultClassNames.outside ), disabled: cn('text-muted-foreground/30 opacity-40', defaultClassNames.disabled), hidden: cn('invisible', defaultClassNames.hidden), ...classNames, }} components={{ Root: ({ className, rootRef, ...props }) => { return
; }, Chevron: ({ className, orientation, ...props }) => { if (orientation === 'left') { return ( ); } if (orientation === 'right') { return ( ); } return ; }, DayButton: CalendarDayButton, WeekNumber: ({ children, ...props }) => { return (
{children}
); }, ...components, }} {...props} /> ); } function CalendarDayButton({ className, day, modifiers, ...props }: React.ComponentProps) { const defaultClassNames = getDefaultClassNames(); const ref = React.useRef(null); React.useEffect(() => { if (modifiers.focused) ref.current?.focus(); }, [modifiers.focused]); return ( ); } export { Calendar, CalendarDayButton };