'use client' import * as React from 'react' import { DayPicker, type ChevronProps, type DateRange, type DayButtonProps, type DropdownProps, type Matcher, } from 'react-day-picker' import { buttonVariants } from '../button/button-variants' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../select/select' import { cn } from '../../internal/utils' type CalendarSize = 'sm' | 'md' | 'lg' type DayPickerProps = React.ComponentProps type DistributiveOmit = T extends unknown ? Omit : never interface CalendarExtensions { /** * Cell size and text scale (Appica extension). * @default 'md' */ size?: CalendarSize /** Extra classes on the root, merged via `tailwind-merge`. */ className?: string } type CalendarProps = DistributiveOmit & CalendarExtensions const ROOT_CONFIG: Record = { sm: { text: 'text-xs', cellVar: '[--cell-size:--spacing(6)]', cellOuter: 'min-w-6.5' }, md: { text: 'text-sm', cellVar: '[--cell-size:--spacing(8)]', cellOuter: 'min-w-8.5' }, lg: { text: 'text-base', cellVar: '[--cell-size:--spacing(10)]', cellOuter: 'min-w-10.5' }, } const MONTH_GAP: Record = { sm: 'gap-4', md: 'gap-4.5', lg: 'gap-4.5', } const HEADER_GAP: Record = { sm: 'gap-1', md: 'gap-2', lg: 'gap-2.5', } const WEEKDAY_PADDING: Record = { sm: 'pb-1.5', md: 'pb-2', lg: 'pb-2', } const ROW_GAP: Record = { sm: 'mt-1', md: 'mt-1', lg: 'mt-1', } const ROUNDED: Record = { sm: { full: 'rounded-xs', start: 'rounded-s-xs', end: 'rounded-e-xs' }, md: { full: 'rounded-sm', start: 'rounded-s-sm', end: 'rounded-e-sm' }, lg: { full: 'rounded-md', start: 'rounded-s-md', end: 'rounded-e-md' }, } const NAV_BUTTON_CLASS: Record = { sm: cn( buttonVariants({ variant: 'outline', size: 'icon-sm' }), "size-(--cell-size) rounded-xs [&_svg:not([class*='size-'])]:size-3.5", ), md: cn(buttonVariants({ variant: 'outline', size: 'icon-sm' }), 'size-(--cell-size)'), lg: cn(buttonVariants({ variant: 'outline', size: 'icon-md' }), 'size-(--cell-size)'), } const PREV_CHEVRON_FLIP = '[&_svg]:rtl:rotate-180' const NEXT_CHEVRON_FLIP = '[&_svg]:rotate-180 [&_svg]:rtl:rotate-0' const SELECT_CONFIG: Record = { sm: { size: 'sm', extra: 'w-auto h-6 px-2 rounded-xs gap-1 [&>svg]:size-3.5' }, md: { size: 'md', extra: 'w-auto h-8 px-2.5 rounded-sm text-sm [&>svg]:size-4' }, lg: { size: 'md', extra: 'w-auto text-base' }, } const DEFAULT_START_MONTH = new Date(1925, 0) const DEFAULT_END_MONTH = new Date(2050, 11) interface CalendarContextValue { size: CalendarSize rounded: string } const CalendarContext = React.createContext({ size: 'md', rounded: ROUNDED.md.full }) function CalendarDropdownSlot(props: DropdownProps) { const { size } = React.useContext(CalendarContext) return } function CalendarDayButtonSlot(props: DayButtonProps) { const { size, rounded } = React.useContext(CalendarContext) return } const CALENDAR_COMPONENTS = { Chevron: CalendarChevron, Dropdown: CalendarDropdownSlot, DayButton: CalendarDayButtonSlot, } function Calendar({ size = 'md', showOutsideDays = true, captionLayout = 'dropdown', startMonth = DEFAULT_START_MONTH, endMonth = DEFAULT_END_MONTH, weekStartsOn = 1, formatters, className, ...props }: CalendarProps) { const cfg = ROOT_CONFIG[size] const r = ROUNDED[size] const isLabel = captionLayout === 'label' const calendarContext = React.useMemo(() => ({ size, rounded: r.full }), [size, r.full]) return (
date.toLocaleString('default', { month: 'short' }), ...formatters, }} classNames={{ months: cn('relative flex flex-col', MONTH_GAP[size]), month: cn('flex w-full flex-col', MONTH_GAP[size]), month_caption: 'flex h-(--cell-size) items-center justify-center px-[calc(var(--cell-size)+0.5rem)]', caption_label: isLabel ? 'text-foreground-intense font-medium' : 'sr-only', nav: 'pointer-events-none absolute inset-x-0 top-0 flex items-center justify-between', button_previous: cn(NAV_BUTTON_CLASS[size], 'pointer-events-auto', PREV_CHEVRON_FLIP), button_next: cn(NAV_BUTTON_CLASS[size], 'pointer-events-auto', NEXT_CHEVRON_FLIP), dropdowns: cn('flex items-center', HEADER_GAP[size]), dropdown_root: 'relative', dropdown: 'sr-only', month_grid: 'border-collapse', weekdays: 'flex', weekday: cn( 'text-foreground-muted flex flex-1 items-end justify-center font-normal', cfg.cellOuter, WEEKDAY_PADDING[size], ), weeks: '', week: cn('flex w-full', ROW_GAP[size]), day: cn('relative flex aspect-square flex-1 items-center justify-center', cfg.cellOuter), range_start: cn('bg-background-muted', r.start), range_middle: 'bg-background-muted', range_end: cn('bg-background-muted', r.end), }} components={CALENDAR_COMPONENTS} {...(props as DayPickerProps)} />
) } interface CalendarDayButtonProps extends DayButtonProps { calendarSize: CalendarSize rounded: string } const DAY_BUTTON_BASE = cn( 'relative isolate flex size-(--cell-size) cursor-pointer items-center justify-center', 'font-normal whitespace-nowrap outline-offset-1 outline-ring', 'transform-gpu transition duration-250 ease-[cubic-bezier(0.175,0.885,0.32,1.5)]', 'active:scale-[0.97] active:translate-y-px active:duration-100 active:ease-in-out', 'motion-reduce:transition-none', 'before:pointer-events-none before:absolute before:inset-0 before:-z-1 before:rounded-[inherit]', 'before:bg-background-muted before:opacity-0', 'before:transition-all before:duration-300 motion-reduce:before:transition-none', ) function CalendarDayButton({ calendarSize: _calendarSize, rounded, modifiers, day: _day, className, ...props }: CalendarDayButtonProps) { const isSelected = Boolean(modifiers.selected) const isToday = Boolean(modifiers.today) const isOutside = Boolean(modifiers.outside) const isDisabled = Boolean(modifiers.disabled) const isRangeStart = Boolean(modifiers.range_start) const isRangeMiddle = Boolean(modifiers.range_middle) const isRangeEnd = Boolean(modifiers.range_end) const isRangeEndpoint = isRangeStart || isRangeEnd const isFilled = (isSelected && !isRangeMiddle) || isRangeEndpoint return (