{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"date-selector","type":"registry:ui","title":"Date Selector","description":"","dependencies":["date-fns","react-day-picker"],"registryDependencies":["button","calendar","input","scroll-area","tabs","use-mobile"],"files":[{"path":"date-selector.tsx","type":"registry:ui","content":"\"use client\"\n\nimport type { ChangeEvent, ComponentProps } from \"react\"\nimport {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useState,\n} from \"react\"\nimport {\n addMonths,\n format,\n isBefore,\n isSameMonth,\n parse,\n subMonths,\n} from \"date-fns\"\nimport type { DateRange, DayButton } from \"react-day-picker\"\n\nimport { useIsMobile } from \"@/hooks/use-mobile\"\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport { Calendar, CalendarDayButton } from \"@/components/ui/calendar\"\nimport { Input } from \"@/components/ui/input\"\nimport { ScrollArea } from \"@/components/ui/scroll-area\"\nimport { Tabs, TabsList, TabsTrigger } from \"@/components/ui/tabs\"\nimport { IconPlaceholder } from \"@/app/(create)/components/icon-placeholder\"\n\nexport interface DateSelectorI18nConfig {\n // Labels\n selectDate: string\n apply: string\n cancel: string\n clear: string\n today: string\n // Filter types\n filterTypes: {\n is: string\n before: string\n after: string\n between: string\n }\n // Period types\n periodTypes: {\n day: string\n month: string\n quarter: string\n halfYear: string\n year: string\n }\n // Months\n months: string[]\n monthsShort: string[]\n // Quarters\n quarters: string[]\n // Half years\n halfYears: string[]\n // Weekdays\n weekdays: string[]\n weekdaysShort: string[]\n // Placeholders\n placeholder: string\n rangePlaceholder: string\n}\n\nexport const DEFAULT_DATE_SELECTOR_I18N: DateSelectorI18nConfig = {\n selectDate: \"Select date\",\n apply: \"Apply\",\n cancel: \"Cancel\",\n clear: \"Clear\",\n today: \"Today\",\n filterTypes: {\n is: \"is\",\n before: \"before\",\n after: \"after\",\n between: \"between\",\n },\n periodTypes: {\n day: \"Day\",\n month: \"Month\",\n quarter: \"Quarter\",\n halfYear: \"Half-year\",\n year: \"Year\",\n },\n months: [\n \"January\",\n \"February\",\n \"March\",\n \"April\",\n \"May\",\n \"June\",\n \"July\",\n \"August\",\n \"September\",\n \"October\",\n \"November\",\n \"December\",\n ],\n monthsShort: [\n \"Jan\",\n \"Feb\",\n \"Mar\",\n \"Apr\",\n \"May\",\n \"Jun\",\n \"Jul\",\n \"Aug\",\n \"Sep\",\n \"Oct\",\n \"Nov\",\n \"Dec\",\n ],\n quarters: [\"Q1\", \"Q2\", \"Q3\", \"Q4\"],\n halfYears: [\"H1\", \"H2\"],\n weekdays: [\n \"Sunday\",\n \"Monday\",\n \"Tuesday\",\n \"Wednesday\",\n \"Thursday\",\n \"Friday\",\n \"Saturday\",\n ],\n weekdaysShort: [\"Su\", \"Mo\", \"Tu\", \"We\", \"Th\", \"Fr\", \"Sa\"],\n placeholder: \"Select date...\",\n rangePlaceholder: \"Select date range...\",\n}\n\nexport type DateSelectorPeriodType =\n | \"day\"\n | \"month\"\n | \"quarter\"\n | \"half-year\"\n | \"year\"\nexport type DateSelectorFilterType = \"is\" | \"before\" | \"after\" | \"between\"\n\nexport interface DateSelectorValue {\n period: DateSelectorPeriodType\n operator: DateSelectorFilterType\n startDate?: Date\n endDate?: Date\n year?: number\n month?: number\n quarter?: number\n halfYear?: number\n rangeStart?: { year: number; value: number }\n rangeEnd?: { year: number; value: number }\n}\n\nexport interface DateSelectorContextValue {\n i18n: DateSelectorI18nConfig\n variant: \"outline\" | \"default\"\n size: \"sm\" | \"default\" | \"lg\"\n}\n\nconst DateSelectorContext = createContext({\n i18n: DEFAULT_DATE_SELECTOR_I18N,\n variant: \"outline\",\n size: \"default\",\n})\n\nexport const useDateSelectorContext = () => useContext(DateSelectorContext)\n\nexport function formatDateValue(\n value: DateSelectorValue,\n i18n: DateSelectorI18nConfig = DEFAULT_DATE_SELECTOR_I18N,\n dayDateFormat: string = \"MM/dd/yyyy\"\n): string {\n const {\n period,\n startDate,\n endDate,\n year,\n month,\n quarter,\n halfYear,\n rangeStart,\n rangeEnd,\n } = value\n\n if (period === \"day\") {\n if (startDate && endDate) {\n return `${format(startDate, dayDateFormat)} - ${format(endDate, dayDateFormat)}`\n }\n if (startDate) {\n return format(startDate, dayDateFormat)\n }\n return \"\"\n }\n\n if (period === \"month\") {\n if (rangeStart && rangeEnd) {\n return `${i18n.monthsShort[rangeStart.value]} ${rangeStart.year} - ${i18n.monthsShort[rangeEnd.value]} ${rangeEnd.year}`\n }\n if (year !== undefined && month !== undefined) {\n return `${i18n.monthsShort[month]} ${year}`\n }\n return \"\"\n }\n\n if (period === \"quarter\") {\n if (rangeStart && rangeEnd) {\n return `${i18n.quarters[rangeStart.value]} ${rangeStart.year} - ${i18n.quarters[rangeEnd.value]} ${rangeEnd.year}`\n }\n if (year !== undefined && quarter !== undefined) {\n return `${i18n.quarters[quarter]} ${year}`\n }\n return \"\"\n }\n\n if (period === \"half-year\") {\n if (rangeStart && rangeEnd) {\n return `${i18n.halfYears[rangeStart.value]} ${rangeStart.year} - ${i18n.halfYears[rangeEnd.value]} ${rangeEnd.year}`\n }\n if (year !== undefined && halfYear !== undefined) {\n return `${i18n.halfYears[halfYear]} ${year}`\n }\n return \"\"\n }\n\n if (period === \"year\") {\n if (rangeStart && rangeEnd) {\n return `${rangeStart.year} - ${rangeEnd.year}`\n }\n if (year !== undefined) {\n return `${year}`\n }\n return \"\"\n }\n\n return \"\"\n}\n\ninterface UseDateSelectorOptions {\n value?: DateSelectorValue\n onChange?: (value: DateSelectorValue) => void\n defaultPeriodType?: DateSelectorPeriodType\n defaultFilterType?: DateSelectorFilterType\n presetMode?: DateSelectorFilterType\n allowRange?: boolean\n yearRange?: number\n baseYear?: number\n minYear?: number\n maxYear?: number\n periodTypes?: DateSelectorPeriodType[]\n}\n\nexport function useDateSelector({\n value,\n onChange,\n defaultPeriodType = \"day\",\n defaultFilterType = \"is\",\n presetMode,\n allowRange = true,\n yearRange = 11,\n baseYear,\n minYear,\n maxYear,\n periodTypes,\n}: UseDateSelectorOptions) {\n const currentYear = baseYear ?? new Date().getFullYear()\n\n const validDefaultPeriodType = useMemo(() => {\n if (!periodTypes || periodTypes.length === 0) return defaultPeriodType\n if (periodTypes.includes(defaultPeriodType)) return defaultPeriodType\n return periodTypes[0]\n }, [periodTypes, defaultPeriodType])\n\n // Use presetMode if provided, otherwise use value or default\n const effectiveFilterType = presetMode ?? value?.operator ?? defaultFilterType\n\n const [periodType, setPeriodType] = useState(\n value?.period || validDefaultPeriodType\n )\n const [filterType, setFilterType] =\n useState(effectiveFilterType)\n const [selectedDate, setSelectedDate] = useState(\n value?.startDate\n )\n const [selectedEndDate, setSelectedEndDate] = useState(\n value?.endDate\n )\n const [calendarMonth, setCalendarMonth] = useState(\n value?.startDate || new Date()\n )\n const [selectedYear, setSelectedYear] = useState(\n value?.year\n )\n const [selectedMonth, setSelectedMonth] = useState(\n value?.month\n )\n const [selectedQuarter, setSelectedQuarter] = useState(\n value?.quarter\n )\n const [selectedHalfYear, setSelectedHalfYear] = useState(\n value?.halfYear\n )\n const [rangeStart, setRangeStart] = useState<\n { year: number; value: number } | undefined\n >(value?.rangeStart)\n const [rangeEnd, setRangeEnd] = useState<\n { year: number; value: number } | undefined\n >(value?.rangeEnd)\n const [hoverDate, setHoverDate] = useState()\n\n const years = useMemo(() => {\n if (minYear !== undefined && maxYear !== undefined) {\n return Array.from(\n { length: maxYear - minYear + 1 },\n (_, i) => minYear + i\n )\n }\n return Array.from(\n { length: yearRange },\n (_, i) => currentYear - Math.floor(yearRange / 2) + i\n )\n }, [currentYear, yearRange, minYear, maxYear])\n\n const currentValue = useMemo(\n () => ({\n period: periodType,\n operator: presetMode ?? filterType,\n startDate: selectedDate,\n endDate: selectedEndDate,\n year: selectedYear,\n month: selectedMonth,\n quarter: selectedQuarter,\n halfYear: selectedHalfYear,\n rangeStart,\n rangeEnd,\n }),\n [\n periodType,\n presetMode,\n filterType,\n selectedDate,\n selectedEndDate,\n selectedYear,\n selectedMonth,\n selectedQuarter,\n selectedHalfYear,\n rangeStart,\n rangeEnd,\n ]\n )\n\n const clearSelection = useCallback(() => {\n setSelectedDate(undefined)\n setSelectedEndDate(undefined)\n setSelectedYear(undefined)\n setSelectedMonth(undefined)\n setSelectedQuarter(undefined)\n setSelectedHalfYear(undefined)\n setRangeStart(undefined)\n setRangeEnd(undefined)\n }, [])\n\n const handleDayClick = useCallback(\n (day: Date) => {\n if (filterType === \"between\" && allowRange) {\n if (!selectedDate || (selectedDate && selectedEndDate)) {\n setSelectedDate(day)\n setSelectedEndDate(undefined)\n } else {\n if (isBefore(day, selectedDate)) {\n setSelectedEndDate(selectedDate)\n setSelectedDate(day)\n } else {\n setSelectedEndDate(day)\n }\n }\n } else {\n setSelectedDate(day)\n setSelectedEndDate(undefined)\n }\n },\n [filterType, allowRange, selectedDate, selectedEndDate]\n )\n\n const handlePeriodSelect = useCallback(\n (year: number, value: number) => {\n if (filterType === \"between\" && allowRange) {\n if (!rangeStart || (rangeStart && rangeEnd)) {\n setRangeStart({ year, value })\n setRangeEnd(undefined)\n setSelectedYear(year)\n if (periodType === \"month\") setSelectedMonth(value)\n if (periodType === \"quarter\") setSelectedQuarter(value)\n if (periodType === \"half-year\") setSelectedHalfYear(value)\n } else {\n const startKey = rangeStart.year * 100 + rangeStart.value\n const endKey = year * 100 + value\n if (endKey < startKey) {\n setRangeEnd(rangeStart)\n setRangeStart({ year, value })\n } else {\n setRangeEnd({ year, value })\n }\n }\n } else {\n setSelectedYear(year)\n if (periodType === \"month\") setSelectedMonth(value)\n if (periodType === \"quarter\") setSelectedQuarter(value)\n if (periodType === \"half-year\") setSelectedHalfYear(value)\n setRangeStart(undefined)\n setRangeEnd(undefined)\n }\n },\n [filterType, allowRange, rangeStart, rangeEnd, periodType]\n )\n\n const handleYearSelect = useCallback(\n (year: number) => {\n if (filterType === \"between\" && allowRange) {\n if (!rangeStart || (rangeStart && rangeEnd)) {\n setRangeStart({ year, value: 0 })\n setRangeEnd(undefined)\n setSelectedYear(year)\n } else {\n if (year < rangeStart.year) {\n setRangeEnd(rangeStart)\n setRangeStart({ year, value: 0 })\n } else {\n setRangeEnd({ year, value: 0 })\n }\n }\n } else {\n setSelectedYear(year)\n setRangeStart(undefined)\n setRangeEnd(undefined)\n }\n },\n [filterType, allowRange, rangeStart, rangeEnd]\n )\n\n const handlePeriodTypeChange = useCallback(\n (type: DateSelectorPeriodType) => {\n setPeriodType(type)\n clearSelection()\n },\n [clearSelection]\n )\n\n const handleFilterTypeChange = useCallback(\n (type: DateSelectorFilterType) => {\n // Don't allow changes if presetMode is set\n if (presetMode !== undefined) return\n setFilterType(type)\n clearSelection()\n },\n [clearSelection, presetMode]\n )\n\n const isInRange = useCallback(\n (year: number, value: number) => {\n if (!rangeStart || !rangeEnd) return false\n const key = year * 100 + value\n const startKey = rangeStart.year * 100 + rangeStart.value\n const endKey = rangeEnd.year * 100 + rangeEnd.value\n return key >= startKey && key <= endKey\n },\n [rangeStart, rangeEnd]\n )\n\n const isYearInRange = useCallback(\n (year: number) => {\n if (!rangeStart || !rangeEnd) return false\n return year >= rangeStart.year && year <= rangeEnd.year\n },\n [rangeStart, rangeEnd]\n )\n\n useEffect(() => {\n if (value) {\n setPeriodType(value.period || validDefaultPeriodType)\n // Use presetMode if provided, otherwise use value's operator or default\n const newFilterType = presetMode ?? value.operator ?? defaultFilterType\n setFilterType(newFilterType)\n setSelectedDate(value.startDate)\n setSelectedEndDate(value.endDate)\n setSelectedYear(value.year)\n setSelectedMonth(value.month)\n setSelectedQuarter(value.quarter)\n setSelectedHalfYear(value.halfYear)\n setRangeStart(value.rangeStart)\n setRangeEnd(value.rangeEnd)\n }\n }, [value, validDefaultPeriodType, defaultFilterType, presetMode])\n\n // Sync filterType when presetMode changes\n useEffect(() => {\n if (presetMode !== undefined) {\n setFilterType(presetMode)\n }\n }, [presetMode])\n\n useEffect(() => {\n onChange?.(currentValue)\n }, [currentValue, onChange])\n\n return {\n // State\n periodType,\n filterType,\n selectedDate,\n selectedEndDate,\n calendarMonth,\n selectedYear,\n selectedMonth,\n selectedQuarter,\n selectedHalfYear,\n rangeStart,\n rangeEnd,\n hoverDate,\n years,\n currentValue,\n allowRange,\n\n // Setters\n setPeriodType: handlePeriodTypeChange,\n setFilterType: handleFilterTypeChange,\n setSelectedDate,\n setSelectedEndDate,\n setCalendarMonth,\n setHoverDate,\n\n // Actions\n clearSelection,\n handleDayClick,\n handlePeriodSelect,\n handleYearSelect,\n isInRange,\n isYearInRange,\n }\n}\n\ninterface DateSelectorFilterToggleProps {\n value: DateSelectorFilterType\n onChange: (value: DateSelectorFilterType) => void\n showBetween?: boolean\n showIs?: boolean\n presetMode?: DateSelectorFilterType\n className?: string\n}\n\nfunction DateSelectorFilterToggle({\n value,\n onChange,\n showBetween = true,\n showIs = true,\n presetMode,\n className,\n}: DateSelectorFilterToggleProps) {\n const { i18n } = useDateSelectorContext()\n const isDisabled = presetMode !== undefined\n\n return (\n {\n if (!isDisabled && newValue) {\n onChange(newValue as DateSelectorFilterType)\n }\n }}\n className={className}\n >\n \n {showIs && (\n \n {i18n.filterTypes.is}\n \n )}\n \n {i18n.filterTypes.before}\n \n \n {i18n.filterTypes.after}\n \n {showBetween && (\n \n {i18n.filterTypes.between}\n \n )}\n \n \n )\n}\n\ninterface DateSelectorDateSelectorPeriodTabsProps {\n value: DateSelectorPeriodType\n onChange: (value: DateSelectorPeriodType) => void\n periodTypes?: DateSelectorPeriodType[]\n className?: string\n calendarMonth?: Date\n onMonthChange?: (date: Date) => void\n showNavigationButtons?: boolean\n}\n\nfunction DateSelectorPeriodTabs({\n value,\n onChange,\n periodTypes,\n className,\n calendarMonth,\n onMonthChange,\n showNavigationButtons = false,\n}: DateSelectorDateSelectorPeriodTabsProps) {\n const { i18n } = useDateSelectorContext()\n\n const tabs: { value: DateSelectorPeriodType; label: string }[] = [\n { value: \"day\", label: i18n.periodTypes.day },\n { value: \"month\", label: i18n.periodTypes.month },\n { value: \"quarter\", label: i18n.periodTypes.quarter },\n { value: \"half-year\", label: i18n.periodTypes.halfYear },\n { value: \"year\", label: i18n.periodTypes.year },\n ]\n\n const filteredTabs = periodTypes\n ? tabs.filter((tab) => periodTypes.includes(tab.value))\n : tabs\n\n return (\n \n {\n if (newValue) {\n onChange(newValue as DateSelectorPeriodType)\n }\n }}\n >\n \n {filteredTabs.map((tab) => (\n \n {tab.label}\n \n ))}\n \n \n {showNavigationButtons &&\n value === \"day\" &&\n calendarMonth &&\n onMonthChange && (\n
\n {(() => {\n const today = new Date()\n const isCurrentMonth = isSameMonth(calendarMonth, today)\n\n // Only show today button if not on current month\n if (isCurrentMonth) {\n return null\n }\n\n // Determine direction based on whether calendarMonth is in future or past\n const isFuture = calendarMonth > today\n\n return (\n onMonthChange(new Date())}\n title={i18n.today}\n >\n {isFuture ? (\n \n ) : (\n \n )}\n \n )\n })()}\n onMonthChange(subMonths(calendarMonth, 1))}\n >\n \n \n onMonthChange(addMonths(calendarMonth, 1))}\n >\n \n \n
\n )}\n \n )\n}\n\ninterface DateSelectorDayPickerProps {\n currentMonth: Date\n selectedDate?: Date\n selectedEndDate?: Date\n onDayClick: (day: Date) => void\n isRange: boolean\n onDayHover?: (day: Date | undefined) => void\n hoverDate?: Date\n showTwoMonths?: boolean\n weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6\n className?: string\n}\n\nfunction DateSelectorDayPicker({\n currentMonth,\n selectedDate,\n selectedEndDate,\n onDayClick,\n isRange,\n onDayHover,\n hoverDate,\n showTwoMonths = true,\n weekStartsOn,\n className,\n}: DateSelectorDayPickerProps) {\n const { i18n } = useDateSelectorContext()\n const isMobile = useIsMobile()\n\n // Convert to react-day-picker format\n const selected: Date | DateRange | undefined = isRange\n ? selectedDate && selectedEndDate\n ? { from: selectedDate, to: selectedEndDate }\n : selectedDate\n ? { from: selectedDate, to: hoverDate || selectedDate }\n : undefined\n : selectedDate\n\n const handleSelect = (date: Date | DateRange | undefined) => {\n if (!date) {\n return\n }\n\n if (isRange && \"from\" in date) {\n // For range mode\n if (date.from && !date.to) {\n // First click - set start date\n onDayClick(date.from)\n } else if (date.from && date.to) {\n // Range selected - set end date\n onDayClick(date.to)\n }\n } else if (!isRange && date instanceof Date) {\n onDayClick(date)\n }\n }\n\n // Create custom DayButton component with hover support\n const CustomDayButton = useCallback(\n (props: ComponentProps) => {\n return (\n {\n if (isRange && onDayHover && props.day) {\n onDayHover(props.day.date)\n }\n }}\n onMouseLeave={() => {\n if (isRange && onDayHover) {\n onDayHover(undefined)\n }\n }}\n />\n )\n },\n [isRange, onDayHover]\n )\n\n // Create custom formatters for i18n\n const formatters = {\n formatWeekdayName: (date: Date) => {\n const dayIndex = date.getDay()\n return i18n.weekdaysShort[dayIndex] || i18n.weekdays[dayIndex]\n },\n formatMonthCaption: (date: Date) => {\n const monthIndex = date.getMonth()\n const year = date.getFullYear()\n return `${i18n.months[monthIndex]} ${year}`\n },\n }\n\n return (\n
\n {isRange ? (\n void}\n numberOfMonths={isMobile ? 1 : showTwoMonths ? 2 : 1}\n showOutsideDays={true}\n weekStartsOn={weekStartsOn}\n formatters={formatters}\n className=\"w-full shrink-0 p-0\"\n classNames={{\n months: \"flex flex-wrap items-start justify-between gap-5 w-full\",\n month: \"flex flex-col items-center min-w-0 flex-1\",\n nav: \"hidden\",\n }}\n components={{\n DayButton: CustomDayButton,\n }}\n />\n ) : (\n void}\n numberOfMonths={isMobile ? 1 : showTwoMonths ? 2 : 1}\n showOutsideDays={true}\n weekStartsOn={weekStartsOn}\n formatters={formatters}\n className=\"w-full shrink-0 p-0\"\n classNames={{\n months: \"flex flex-wrap items-start justify-between gap-5 w-full\",\n month: \"flex flex-col items-center min-w-0 flex-1\",\n nav: \"hidden\",\n }}\n components={{\n DayButton: CustomDayButton,\n }}\n />\n )}\n
\n )\n}\n\ninterface DateSelectorDateSelectorPeriodGridProps {\n years: number[]\n items: string[]\n selectedYear?: number\n selectedValue?: number\n rangeStart?: { year: number; value: number }\n rangeEnd?: { year: number; value: number }\n isInRange: (year: number, value: number) => boolean\n onSelect: (year: number, value: number) => void\n columns: number\n className?: string\n}\n\nfunction DateSelectorPeriodGrid({\n years,\n items,\n selectedYear,\n selectedValue,\n rangeStart,\n rangeEnd,\n isInRange,\n onSelect,\n columns,\n className,\n}: DateSelectorDateSelectorPeriodGridProps) {\n return (\n
\n {years.map((year) => (\n
\n
\n {year}\n
\n \n {items.map((item, index) => {\n const isSelected =\n selectedYear === year && selectedValue === index\n const isRangeStart =\n rangeStart?.year === year && rangeStart?.value === index\n const isRangeEnd =\n rangeEnd?.year === year && rangeEnd?.value === index\n const inRange = isInRange(year, index)\n\n return (\n onSelect(year, index)}\n >\n {item}\n \n )\n })}\n
\n
\n ))}\n \n )\n}\n\ninterface DateSelectorYearListProps {\n years: number[]\n selectedYear?: number\n rangeStart?: { year: number; value: number }\n rangeEnd?: { year: number; value: number }\n isYearInRange: (year: number) => boolean\n onSelect: (year: number) => void\n className?: string\n}\n\nfunction DateSelectorYearList({\n years,\n selectedYear,\n rangeStart,\n rangeEnd,\n isYearInRange,\n onSelect,\n className,\n}: DateSelectorYearListProps) {\n return (\n
\n {years.map((year) => {\n const isSelected = selectedYear === year && !rangeStart && !rangeEnd\n const isRangeStart = rangeStart?.year === year\n const isRangeEnd = rangeEnd?.year === year\n const inRange = isYearInRange(year)\n\n return (\n onSelect(year)}\n >\n {year}\n \n )\n })}\n
\n )\n}\n\nexport interface DateSelectorProps {\n value?: DateSelectorValue\n onChange?: (value: DateSelectorValue) => void\n allowRange?: boolean\n periodTypes?: DateSelectorPeriodType[]\n defaultPeriodType?: DateSelectorPeriodType\n defaultFilterType?: DateSelectorFilterType\n presetMode?: DateSelectorFilterType\n showInput?: boolean\n showTwoMonths?: boolean\n label?: string\n className?: string\n yearRange?: number\n baseYear?: number\n minYear?: number\n maxYear?: number\n i18n?: Partial\n inputHint?: string\n dayDateFormat?: string\n dayDateFormats?: string[]\n weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6\n}\n\nexport function DateSelector({\n value,\n onChange,\n allowRange = true,\n periodTypes,\n defaultPeriodType = \"day\",\n defaultFilterType = \"is\",\n presetMode,\n showInput = true,\n showTwoMonths = true,\n label,\n className,\n yearRange = 10,\n baseYear,\n minYear = 2015,\n maxYear = 2026,\n i18n: i18nOverride,\n inputHint,\n dayDateFormat = \"MM/dd/yyyy\",\n dayDateFormats,\n weekStartsOn,\n}: DateSelectorProps) {\n const mergedI18n = useMemo(\n () => ({ ...DEFAULT_DATE_SELECTOR_I18N, ...i18nOverride }),\n [i18nOverride]\n )\n\n const selector = useDateSelector({\n value,\n onChange,\n defaultPeriodType,\n defaultFilterType,\n presetMode,\n allowRange,\n yearRange,\n baseYear,\n minYear,\n maxYear,\n periodTypes,\n })\n\n const {\n periodType,\n filterType,\n selectedDate,\n selectedEndDate,\n calendarMonth,\n selectedYear,\n selectedMonth,\n selectedQuarter,\n selectedHalfYear,\n rangeStart,\n rangeEnd,\n hoverDate,\n years,\n currentValue,\n setPeriodType,\n setFilterType,\n setCalendarMonth,\n setHoverDate,\n clearSelection,\n handleDayClick,\n handlePeriodSelect,\n handleYearSelect,\n isInRange,\n isYearInRange,\n } = selector\n\n const displayValue = formatDateValue(currentValue, mergedI18n, dayDateFormat)\n const [inputValue, setInputValue] = useState(displayValue)\n const [isInputFocused, setIsInputFocused] = useState(false)\n\n // Sync input value when displayValue changes (but not when user is typing)\n useEffect(() => {\n if (!isInputFocused) {\n setInputValue(displayValue)\n }\n }, [displayValue, isInputFocused])\n\n // Compute date formats for parsing\n const dateFormats = useMemo(() => {\n if (dayDateFormats && dayDateFormats.length > 0) {\n // Use provided formats, with dayDateFormat first if not already included\n const formats = [...dayDateFormats]\n if (!formats.includes(dayDateFormat)) {\n formats.unshift(dayDateFormat)\n }\n return formats\n }\n // Default formats: use dayDateFormat first, then common alternatives\n const defaultFormats = [\n dayDateFormat,\n \"dd/MM/yyyy\",\n \"yyyy-MM-dd\",\n \"MM-dd-yyyy\",\n \"dd-MM-yyyy\",\n ]\n // Remove duplicates while preserving order\n return Array.from(new Set(defaultFormats))\n }, [dayDateFormat, dayDateFormats])\n\n // Parse input text to DateSelectorValue\n const parseInputValue = useCallback(\n (text: string): DateSelectorValue | null => {\n if (!text.trim()) return null\n\n const trimmed = text.trim()\n\n // Try parsing as year (e.g., \"2025\")\n const yearMatch = trimmed.match(/^\\d{4}$/)\n if (yearMatch) {\n const year = parseInt(yearMatch[0])\n if (year >= 1900 && year <= 2100) {\n return {\n period: \"year\",\n operator: presetMode ?? filterType,\n year,\n }\n }\n }\n\n // Try parsing as quarter (e.g., \"Q4\", \"Q1 2025\")\n const quarterMatch = trimmed.match(/^Q([1-4])(?:\\s+(\\d{4}))?$/i)\n if (quarterMatch) {\n const quarter = parseInt(quarterMatch[1]) - 1\n const year = quarterMatch[2]\n ? parseInt(quarterMatch[2])\n : new Date().getFullYear()\n if (year >= 1900 && year <= 2100) {\n return {\n period: \"quarter\",\n operator: presetMode ?? filterType,\n year,\n quarter,\n }\n }\n }\n\n // Try parsing as date using computed formats\n for (const dateFormat of dateFormats) {\n try {\n const parsed = parse(trimmed, dateFormat, new Date())\n if (!isNaN(parsed.getTime())) {\n return {\n period: \"day\",\n operator: presetMode ?? filterType,\n startDate: parsed,\n }\n }\n } catch {\n // Continue to next format\n }\n }\n\n return null\n },\n [filterType, presetMode, dateFormats]\n )\n\n const handleInputChange = useCallback(\n (e: ChangeEvent) => {\n const newValue = e.target.value\n setInputValue(newValue)\n\n // Try to parse the input\n const parsed = parseInputValue(newValue)\n if (parsed) {\n onChange?.(parsed)\n }\n },\n [onChange, parseInputValue]\n )\n\n const handleInputBlur = useCallback(() => {\n setIsInputFocused(false)\n // Reset to display value if parsing failed\n if (!parseInputValue(inputValue)) {\n setInputValue(displayValue)\n }\n }, [inputValue, displayValue, parseInputValue])\n\n return (\n \n
\n
\n {label && (\n

\n {label}\n

\n )}\n \n
\n {showInput && (\n
\n setIsInputFocused(true)}\n onBlur={handleInputBlur}\n onChange={handleInputChange}\n />\n {(inputHint ? inputValue : displayValue) && (\n \n \n \n )}\n
\n )}\n \n\n {periodType === \"day\" ? (\n
\n \n
\n ) : (\n
\n \n {periodType === \"month\" && (\n \n )}\n\n {periodType === \"quarter\" && (\n \n )}\n\n {periodType === \"half-year\" && (\n \n )}\n\n {periodType === \"year\" && (\n \n )}\n \n
\n )}\n
\n \n )\n}","target":"components/neui/date-selector.tsx"}]}