// // Copyright 2026 DXOS.org // import { CalendarDate, parseDate } from '@internationalized/date'; import React, { type ComponentPropsWithoutRef, type ReactNode, forwardRef } from 'react'; import { type DateValue, Button as RACButton, Calendar as RACCalendar, CalendarCell as RACCalendarCell, CalendarGrid as RACCalendarGrid, CalendarGridBody as RACCalendarGridBody, CalendarGridHeader as RACCalendarGridHeader, CalendarHeaderCell as RACCalendarHeaderCell, type CalendarProps as RACCalendarProps, Heading as RACHeading, RangeCalendar as RACRangeCalendar, type RangeCalendarProps as RACRangeCalendarProps, } from 'react-aria-components'; import { type ClassNameValue } from '@dxos/ui-types'; import { useThemeContext } from '../../hooks'; import { Icon } from '../Icon'; // // Date <-> CalendarDate conversion. // External API stays `Date` (and `{from,to}`-style range objects) so existing call sites keep working. // const toCalendarDate = (date: Date | undefined): CalendarDate | null => { if (!date) { return null; } return new CalendarDate(date.getFullYear(), date.getMonth() + 1, date.getDate()); }; const fromCalendarDate = (value: DateValue | null | undefined): Date | undefined => { if (!value) { return undefined; } return new Date(value.year, value.month - 1, value.day); }; export type DateRange = { from: Date; to?: Date }; // // Public API. // type BaseCalendarProps = { classNames?: ClassNameValue; className?: string; isDisabled?: boolean; minValue?: Date; maxValue?: Date; /** Month to render when there's no selection — driven by the consuming picker. */ defaultMonth?: Date; }; type SingleProps = BaseCalendarProps & { mode?: 'single'; selected?: Date; onSelect?: (date: Date | undefined) => void; }; type RangeProps = BaseCalendarProps & { mode: 'range'; selected?: DateRange; onSelect?: (range: DateRange | undefined) => void; }; export type CalendarRootProps = SingleProps | RangeProps; const CalendarShell = ({ classNames, className, isDisabled, forwardedRef, children, }: { classNames?: ClassNameValue; className?: string; isDisabled?: boolean; forwardedRef?: React.Ref; children: ReactNode; }) => { const { tx } = useThemeContext(); return (
{children}
); }; const CalendarChrome = () => { const { tx } = useThemeContext(); return (
); }; const CalendarGridContent = () => { const { tx } = useThemeContext(); return ( {(day) => ( {day} )} {(date) => } ); }; const CalendarRoot = forwardRef((props, forwardedRef) => { const { classNames, className, isDisabled, minValue, maxValue, defaultMonth } = props; const defaultFocused = toCalendarDate(defaultMonth) ?? undefined; if (props.mode === 'range') { const racProps: RACRangeCalendarProps = { isDisabled, minValue: toCalendarDate(minValue) ?? undefined, maxValue: toCalendarDate(maxValue) ?? undefined, defaultFocusedValue: defaultFocused, value: props.selected?.from ? { start: toCalendarDate(props.selected.from)!, end: toCalendarDate(props.selected.to ?? props.selected.from)!, } : undefined, onChange: (next) => { if (!next) { props.onSelect?.(undefined); return; } const from = fromCalendarDate(next.start); const to = fromCalendarDate(next.end); if (from) { props.onSelect?.({ from, to }); } }, }; return ( ); } const racProps: RACCalendarProps = { isDisabled, minValue: toCalendarDate(minValue) ?? undefined, maxValue: toCalendarDate(maxValue) ?? undefined, defaultFocusedValue: defaultFocused, value: toCalendarDate(props.selected), onChange: (next) => props.onSelect?.(fromCalendarDate(next)), }; return ( ); }); CalendarRoot.displayName = 'Calendar.Root'; export const Calendar = { Root: CalendarRoot, }; // Re-export the iso parser for convenience. export { parseDate as parseCalendarDate }; export type { ComponentPropsWithoutRef };