UNPKG

react-calendar-plus

Version:

A flexible, themeable, and internationalized React calendar component with date and time selection. Includes month/week/day views, range selection, and custom event handling.

1,196 lines (1,186 loc) 39.4 kB
// src/components/Calendar.tsx import { useState as useState3, useEffect as useEffect3, useMemo } from "react"; import { format as format4, startOfWeek as startOfWeek2, isSameDay as isSameDay2 } from "date-fns"; import { enUS as enUS4 } from "date-fns/locale"; import clsx5 from "clsx"; // src/components/CalendarHeader.tsx import clsx2 from "clsx"; // src/utils/dateUtils.ts import { format, startOfMonth, endOfMonth, startOfWeek, endOfWeek, addDays, addWeeks, addMonths, subDays, subWeeks, subMonths, setYear, setMonth, isSameDay, isSameMonth, isWithinInterval, parseISO, isValid, getDay, eachDayOfInterval, getWeek } from "date-fns"; import { enUS } from "date-fns/locale"; var DEFAULT_LOCALE = { locale: "en-US", weekStartsOn: 0, // Sunday formatOptions: { weekday: "short", month: "long", day: "numeric" } }; var getCalendarDays = (date, locale = DEFAULT_LOCALE) => { const start = startOfWeek(startOfMonth(date), { weekStartsOn: locale.weekStartsOn }); const end = endOfWeek(endOfMonth(date), { weekStartsOn: locale.weekStartsOn }); return eachDayOfInterval({ start, end }); }; var getWeekDays = (date, locale = DEFAULT_LOCALE) => { const start = startOfWeek(date, { weekStartsOn: locale.weekStartsOn }); const end = endOfWeek(date, { weekStartsOn: locale.weekStartsOn }); return eachDayOfInterval({ start, end }); }; var formatDate = (date, formatString) => { return format(date, formatString, { locale: enUS }); }; var formatDateForDisplay = (date, locale = DEFAULT_LOCALE) => { return new Intl.DateTimeFormat(locale.locale, locale.formatOptions).format(date); }; var isDateInRange = (date, range) => { if (!range.start || !range.end) return false; return isWithinInterval(date, { start: range.start, end: range.end }); }; var isDateSelected = (date, selectedValue) => { if (!selectedValue) return false; if (selectedValue instanceof Date) { return isSameDay(date, selectedValue); } if (Array.isArray(selectedValue)) { return selectedValue.some((selectedDate) => isSameDay(date, selectedDate)); } if (selectedValue.start && selectedValue.end) { return isDateInRange(date, selectedValue); } else if (selectedValue.start) { return isSameDay(date, selectedValue.start); } return false; }; var isDateDisabled = (date, minDate, maxDate, disabledDates, disabledDaysOfWeek, disableWeekends) => { if (minDate && date < minDate) return true; if (maxDate && date > maxDate) return true; if (disabledDates && disabledDates.some((disabledDate) => isSameDay(date, disabledDate))) { return true; } const dayOfWeek = getDay(date); if (disabledDaysOfWeek && disabledDaysOfWeek.includes(dayOfWeek)) { return true; } if (disableWeekends && (dayOfWeek === 0 || dayOfWeek === 6)) { return true; } return false; }; var getViewTitle = (date, view) => { switch (view) { case "month": return format(date, "MMMM yyyy", { locale: enUS }); case "week": const weekStart = startOfWeek(date); const weekEnd = endOfWeek(date); if (isSameMonth(weekStart, weekEnd)) { return `${format(weekStart, "MMM d", { locale: enUS })} - ${format(weekEnd, "d, yyyy", { locale: enUS })}`; } else { return `${format(weekStart, "MMM d", { locale: enUS })} - ${format(weekEnd, "MMM d, yyyy", { locale: enUS })}`; } case "day": return format(date, "EEEE, MMMM d, yyyy", { locale: enUS }); default: return format(date, "MMMM yyyy", { locale: enUS }); } }; var navigateDate = (date, direction, view) => { switch (view) { case "month": return direction === "previous" ? subMonths(date, 1) : addMonths(date, 1); case "week": return direction === "previous" ? subWeeks(date, 1) : addWeeks(date, 1); case "day": return direction === "previous" ? subDays(date, 1) : addDays(date, 1); default: return date; } }; var parseDateTime = (dateTimeString) => { try { const parsed = parseISO(dateTimeString); return isValid(parsed) ? parsed : null; } catch { return null; } }; var combineDateTime = (date, time) => { const newDate = new Date(date); newDate.setHours(time.getHours(), time.getMinutes(), time.getSeconds()); return newDate; }; var getWeekNumber = (date) => { return getWeek(date); }; var navigateToYear = (date, year) => { return setYear(date, year); }; var navigateToMonth = (date, month) => { return setMonth(date, month); }; var navigateToYearMonth = (date, year, month) => { return setMonth(setYear(date, year), month); }; var getYearRange = (centerYear, range = 50) => { return Array.from({ length: range * 2 + 1 }, (_, i) => centerYear - range + i); }; var getMonthNames = () => { return Array.from( { length: 12 }, (_, i) => format(new Date(2024, i, 1), "MMMM", { locale: enUS }) ); }; // src/components/YearMonthSelector.tsx import { useState, useRef, useEffect } from "react"; import clsx from "clsx"; import { format as format2, setYear as setYear2, setMonth as setMonth2, getYear, getMonth } from "date-fns"; import { enUS as enUS2 } from "date-fns/locale"; import { jsx, jsxs } from "react/jsx-runtime"; var YearMonthSelector = ({ currentDate, onDateChange, minDate, maxDate, className }) => { const [isYearOpen, setIsYearOpen] = useState(false); const [isMonthOpen, setIsMonthOpen] = useState(false); const yearRef = useRef(null); const monthRef = useRef(null); const currentYear = getYear(currentDate); const currentMonth = getMonth(currentDate); const yearOptions = Array.from({ length: 101 }, (_, i) => currentYear - 50 + i); const monthOptions = Array.from({ length: 12 }, (_, i) => ({ value: i, label: format2(new Date(2024, i, 1), "MMMM", { locale: enUS2 }) })); const filteredYearOptions = yearOptions.filter((year) => { if (minDate && year < getYear(minDate)) return false; if (maxDate && year > getYear(maxDate)) return false; return true; }); const filteredMonthOptions = monthOptions.filter(({ value }) => { const testDate = new Date(currentYear, value, 1); if (minDate && testDate < minDate) return false; if (maxDate && testDate > maxDate) return false; return true; }); useEffect(() => { const handleClickOutside = (event) => { if (yearRef.current && !yearRef.current.contains(event.target)) { setIsYearOpen(false); } if (monthRef.current && !monthRef.current.contains(event.target)) { setIsMonthOpen(false); } }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, []); const handleYearChange = (year) => { const newDate = setYear2(currentDate, year); onDateChange(newDate); setIsYearOpen(false); }; const handleMonthChange = (month) => { const newDate = setMonth2(currentDate, month); onDateChange(newDate); setIsMonthOpen(false); }; return /* @__PURE__ */ jsxs("div", { className: clsx("rcp-year-month-selector", className), children: [ /* @__PURE__ */ jsxs("div", { className: "rcp-year-month-selector__group", ref: monthRef, children: [ /* @__PURE__ */ jsxs( "button", { type: "button", className: "rcp-year-month-selector__button", onClick: () => setIsMonthOpen(!isMonthOpen), "aria-label": "Select month", children: [ format2(currentDate, "MMMM", { locale: enUS2 }), /* @__PURE__ */ jsx( "svg", { className: clsx( "rcp-year-month-selector__arrow", isMonthOpen && "rcp-year-month-selector__arrow--open" ), width: "12", height: "12", viewBox: "0 0 12 12", fill: "currentColor", children: /* @__PURE__ */ jsx("path", { d: "M3 4.5L6 7.5L9 4.5", stroke: "currentColor", strokeWidth: "1.5", fill: "none", strokeLinecap: "round", strokeLinejoin: "round" }) } ) ] } ), isMonthOpen && /* @__PURE__ */ jsx("div", { className: "rcp-year-month-selector__dropdown", children: /* @__PURE__ */ jsx("div", { className: "rcp-year-month-selector__dropdown-content", children: filteredMonthOptions.map(({ value, label }) => /* @__PURE__ */ jsx( "button", { type: "button", className: clsx( "rcp-year-month-selector__option", currentMonth === value && "rcp-year-month-selector__option--selected" ), onClick: () => handleMonthChange(value), children: label }, value )) }) }) ] }), /* @__PURE__ */ jsxs("div", { className: "rcp-year-month-selector__group", ref: yearRef, children: [ /* @__PURE__ */ jsxs( "button", { type: "button", className: "rcp-year-month-selector__button", onClick: () => setIsYearOpen(!isYearOpen), "aria-label": "Select year", children: [ currentYear, /* @__PURE__ */ jsx( "svg", { className: clsx( "rcp-year-month-selector__arrow", isYearOpen && "rcp-year-month-selector__arrow--open" ), width: "12", height: "12", viewBox: "0 0 12 12", fill: "currentColor", children: /* @__PURE__ */ jsx("path", { d: "M3 4.5L6 7.5L9 4.5", stroke: "currentColor", strokeWidth: "1.5", fill: "none", strokeLinecap: "round", strokeLinejoin: "round" }) } ) ] } ), isYearOpen && /* @__PURE__ */ jsx("div", { className: "rcp-year-month-selector__dropdown", children: /* @__PURE__ */ jsx("div", { className: "rcp-year-month-selector__dropdown-content rcp-year-month-selector__dropdown-content--years", children: filteredYearOptions.map((year) => /* @__PURE__ */ jsx( "button", { type: "button", className: clsx( "rcp-year-month-selector__option", currentYear === year && "rcp-year-month-selector__option--selected" ), onClick: () => handleYearChange(year), children: year }, year )) }) }) ] }) ] }); }; // src/components/CalendarHeader.tsx import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime"; var CalendarHeader = ({ currentDate, view, onPrevious, onNext, onViewChange, onDateChange, minDate, maxDate, className }) => { const viewOptions = [ { value: "month", label: "Month" }, { value: "week", label: "Week" }, { value: "day", label: "Day" } ]; return /* @__PURE__ */ jsxs2("div", { className: clsx2("rcp-calendar-header", className), children: [ /* @__PURE__ */ jsxs2("div", { className: "rcp-calendar-header__navigation", children: [ /* @__PURE__ */ jsx2( "button", { type: "button", onClick: onPrevious, className: "rcp-calendar-header__nav-button", "aria-label": "Previous", children: /* @__PURE__ */ jsx2("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "currentColor", children: /* @__PURE__ */ jsx2("path", { d: "M10.5 13L5.5 8L10.5 3", stroke: "currentColor", strokeWidth: "2", fill: "none", strokeLinecap: "round", strokeLinejoin: "round" }) }) } ), /* @__PURE__ */ jsx2("div", { className: "rcp-calendar-header__title-section", children: view === "month" && onDateChange ? /* @__PURE__ */ jsx2( YearMonthSelector, { currentDate, onDateChange, minDate, maxDate, className: "rcp-calendar-header__year-month-selector" } ) : /* @__PURE__ */ jsx2("h2", { className: "rcp-calendar-header__title", children: getViewTitle(currentDate, view) }) }), /* @__PURE__ */ jsx2( "button", { type: "button", onClick: onNext, className: "rcp-calendar-header__nav-button", "aria-label": "Next", children: /* @__PURE__ */ jsx2("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "currentColor", children: /* @__PURE__ */ jsx2("path", { d: "M5.5 3L10.5 8L5.5 13", stroke: "currentColor", strokeWidth: "2", fill: "none", strokeLinecap: "round", strokeLinejoin: "round" }) }) } ) ] }), /* @__PURE__ */ jsx2("div", { className: "rcp-calendar-header__view-selector", children: viewOptions.map(({ value, label }) => /* @__PURE__ */ jsx2( "button", { type: "button", onClick: () => onViewChange(value), className: clsx2( "rcp-calendar-header__view-button", view === value && "rcp-calendar-header__view-button--active" ), children: label }, value )) }) ] }); }; // src/components/CalendarDay.tsx import { isToday, isSameMonth as isSameMonth2 } from "date-fns"; import clsx3 from "clsx"; import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime"; var CalendarDay = ({ date, currentMonth, selectedValue, events, onDateClick, dayRenderer, minDate, maxDate, disabledDates, disabledDaysOfWeek, disableWeekends, showOtherMonths = true, className }) => { const isCurrentDay = isToday(date); const isSelected = isDateSelected(date, selectedValue); const isDisabled = isDateDisabled( date, minDate, maxDate, disabledDates, disabledDaysOfWeek, disableWeekends ); const isOutOfMonth = !isSameMonth2(date, currentMonth); const dayEvents = events.filter( (event) => event.start <= date && (!event.end || event.end >= date) ); const handleClick = (event) => { if (isDisabled) return; onDateClick(date, event); }; const dayProps = { date, isToday: isCurrentDay, isSelected, isInRange: false, // This will be handled by the parent for range selection isDisabled, isOutOfMonth, events: dayEvents, onClick: (date2) => onDateClick(date2, {}) }; if (dayRenderer) { return /* @__PURE__ */ jsx3(Fragment, { children: dayRenderer(dayProps) }); } if (isOutOfMonth && !showOtherMonths) { return /* @__PURE__ */ jsx3("div", { className: "rcp-calendar-day rcp-calendar-day--empty" }); } return /* @__PURE__ */ jsxs3( "div", { className: clsx3( "rcp-calendar-day", { "rcp-calendar-day--today": isCurrentDay, "rcp-calendar-day--selected": isSelected, "rcp-calendar-day--disabled": isDisabled, "rcp-calendar-day--out-of-month": isOutOfMonth, "rcp-calendar-day--has-events": dayEvents.length > 0 }, className ), onClick: handleClick, role: "button", tabIndex: isDisabled ? -1 : 0, "aria-label": `${date.getDate()} ${date.toLocaleDateString()}`, "aria-selected": isSelected, "aria-disabled": isDisabled, onKeyDown: (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); handleClick(e); } }, children: [ /* @__PURE__ */ jsx3("span", { className: "rcp-calendar-day__number", children: date.getDate() }), dayEvents.length > 0 && /* @__PURE__ */ jsxs3("div", { className: "rcp-calendar-day__events", children: [ dayEvents.slice(0, 3).map((event, index) => /* @__PURE__ */ jsx3( "div", { className: "rcp-calendar-day__event", style: { backgroundColor: event.color, color: event.textColor }, title: event.title, children: event.title }, event.id || index )), dayEvents.length > 3 && /* @__PURE__ */ jsxs3("div", { className: "rcp-calendar-day__event-more", children: [ "+", dayEvents.length - 3, " more" ] }) ] }) ] } ); }; // src/components/TimePicker.tsx import { useState as useState2, useEffect as useEffect2 } from "react"; import { format as format3 } from "date-fns"; import { enUS as enUS3 } from "date-fns/locale"; import clsx4 from "clsx"; import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime"; var TimePicker = ({ value, onChange, format: timeFormat = "24h", minuteStep = 1, disabled = false, className }) => { const [hours, setHours] = useState2(value ? value.getHours() : 0); const [minutes, setMinutes] = useState2(value ? value.getMinutes() : 0); const [period, setPeriod] = useState2("AM"); useEffect2(() => { if (value) { const date = new Date(value); setHours(date.getHours()); setMinutes(date.getMinutes()); setPeriod(date.getHours() >= 12 ? "PM" : "AM"); } }, [value]); const handleTimeChange = (newHours, newMinutes) => { if (disabled) return; const date = new Date(value || /* @__PURE__ */ new Date()); date.setHours(newHours, newMinutes, 0, 0); onChange?.(date); }; const handleHourChange = (newHours) => { let adjustedHours = newHours; if (timeFormat === "12h") { if (period === "PM" && newHours !== 12) { adjustedHours = newHours + 12; } else if (period === "AM" && newHours === 12) { adjustedHours = 0; } } setHours(adjustedHours); handleTimeChange(adjustedHours, minutes); }; const handleMinuteChange = (newMinutes) => { setMinutes(newMinutes); handleTimeChange(hours, newMinutes); }; const handlePeriodChange = (newPeriod) => { setPeriod(newPeriod); let adjustedHours = hours; if (newPeriod === "PM" && hours < 12) { adjustedHours = hours + 12; } else if (newPeriod === "AM" && hours >= 12) { adjustedHours = hours - 12; } setHours(adjustedHours); handleTimeChange(adjustedHours, minutes); }; const displayHours = timeFormat === "12h" ? hours === 0 ? 12 : hours > 12 ? hours - 12 : hours : hours; const hourOptions = Array.from( { length: timeFormat === "12h" ? 12 : 24 }, (_, i) => timeFormat === "12h" ? i + 1 : i ); const minuteOptions = Array.from( { length: Math.floor(60 / minuteStep) }, (_, i) => i * minuteStep ); return /* @__PURE__ */ jsxs4("div", { className: clsx4("rcp-time-picker", className), children: [ /* @__PURE__ */ jsxs4("div", { className: "rcp-time-picker__container", children: [ /* @__PURE__ */ jsxs4("div", { className: "rcp-time-picker__field", children: [ /* @__PURE__ */ jsx4("label", { className: "rcp-time-picker__label", children: "Hour" }), /* @__PURE__ */ jsx4( "select", { value: displayHours, onChange: (e) => handleHourChange(parseInt(e.target.value)), disabled, className: "rcp-time-picker__select", children: hourOptions.map((hour) => /* @__PURE__ */ jsx4("option", { value: hour, children: hour.toString().padStart(2, "0") }, hour)) } ) ] }), /* @__PURE__ */ jsx4("div", { className: "rcp-time-picker__separator", children: ":" }), /* @__PURE__ */ jsxs4("div", { className: "rcp-time-picker__field", children: [ /* @__PURE__ */ jsx4("label", { className: "rcp-time-picker__label", children: "Minute" }), /* @__PURE__ */ jsx4( "select", { value: minutes, onChange: (e) => handleMinuteChange(parseInt(e.target.value)), disabled, className: "rcp-time-picker__select", children: minuteOptions.map((minute) => /* @__PURE__ */ jsx4("option", { value: minute, children: minute.toString().padStart(2, "0") }, minute)) } ) ] }), timeFormat === "12h" && /* @__PURE__ */ jsxs4("div", { className: "rcp-time-picker__field", children: [ /* @__PURE__ */ jsx4("label", { className: "rcp-time-picker__label", children: "Period" }), /* @__PURE__ */ jsxs4( "select", { value: period, onChange: (e) => handlePeriodChange(e.target.value), disabled, className: "rcp-time-picker__select", children: [ /* @__PURE__ */ jsx4("option", { value: "AM", children: "AM" }), /* @__PURE__ */ jsx4("option", { value: "PM", children: "PM" }) ] } ) ] }) ] }), /* @__PURE__ */ jsx4("div", { className: "rcp-time-picker__display", children: value && format3(value, timeFormat === "12h" ? "h:mm a" : "HH:mm", { locale: enUS3 }) }) ] }); }; // src/components/Calendar.tsx import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime"; var Calendar = ({ value, defaultValue, onChange, view: controlledView, defaultView = "month", onViewChange, selectionMode = "single", showTimePicker = false, timeFormat = "24h", minuteStep = 1, locale = DEFAULT_LOCALE, className, theme, onDateClick, onRangeSelect, onMonthChange, onWeekChange, onDayChange, dayRenderer, headerRenderer, minDate, maxDate, disabledDates, disabledDaysOfWeek, disableWeekends, events = [], responsive = true, showWeekNumbers = false, showOtherMonths = true }) => { const [currentView, setCurrentView] = useState3(controlledView || defaultView); const [currentDate, setCurrentDate] = useState3(/* @__PURE__ */ new Date()); const [selectedValue, setSelectedValue] = useState3(value || defaultValue || null); const [rangeStart, setRangeStart] = useState3(null); const [hoveredDate, setHoveredDate] = useState3(null); useEffect3(() => { if (controlledView !== void 0) { setCurrentView(controlledView); } }, [controlledView]); useEffect3(() => { if (value !== void 0) { setSelectedValue(value); } }, [value]); const displayDays = useMemo(() => { switch (currentView) { case "month": return getCalendarDays(currentDate, locale); case "week": return getWeekDays(currentDate, locale); case "day": return [currentDate]; default: return getCalendarDays(currentDate, locale); } }, [currentDate, currentView, locale]); const weekHeaders = useMemo(() => { const startOfCurrentWeek = startOfWeek2(/* @__PURE__ */ new Date(), { weekStartsOn: locale.weekStartsOn }); return getWeekDays(startOfCurrentWeek, locale); }, [locale]); const handleViewChange = (newView) => { setCurrentView(newView); onViewChange?.(newView); switch (newView) { case "month": onMonthChange?.(currentDate); break; case "week": onWeekChange?.(startOfWeek2(currentDate, { weekStartsOn: locale.weekStartsOn })); break; case "day": onDayChange?.(currentDate); break; } }; const handlePrevious = () => { const newDate = navigateDate(currentDate, "previous", currentView); setCurrentDate(newDate); switch (currentView) { case "month": onMonthChange?.(newDate); break; case "week": onWeekChange?.(startOfWeek2(newDate, { weekStartsOn: locale.weekStartsOn })); break; case "day": onDayChange?.(newDate); break; } }; const handleNext = () => { const newDate = navigateDate(currentDate, "next", currentView); setCurrentDate(newDate); switch (currentView) { case "month": onMonthChange?.(newDate); break; case "week": onWeekChange?.(startOfWeek2(newDate, { weekStartsOn: locale.weekStartsOn })); break; case "day": onDayChange?.(newDate); break; } }; const handleDateChange = (newDate) => { setCurrentDate(newDate); switch (currentView) { case "month": onMonthChange?.(newDate); break; case "week": onWeekChange?.(startOfWeek2(newDate, { weekStartsOn: locale.weekStartsOn })); break; case "day": onDayChange?.(newDate); break; } }; const handleDateClick = (date, event) => { onDateClick?.(date, event); let newValue = null; switch (selectionMode) { case "single": newValue = date; if (showTimePicker && selectedValue instanceof Date) { newValue = combineDateTime(date, selectedValue); } break; case "multiple": if (Array.isArray(selectedValue)) { const exists = selectedValue.find((d) => isSameDay2(d, date)); if (exists) { newValue = selectedValue.filter((d) => !isSameDay2(d, date)); } else { newValue = [...selectedValue, date]; } } else { newValue = [date]; } break; case "range": if (!rangeStart || rangeStart && hoveredDate) { setRangeStart(date); setHoveredDate(null); newValue = { start: date, end: null }; } else { const start = rangeStart < date ? rangeStart : date; const end = rangeStart < date ? date : rangeStart; newValue = { start, end }; setRangeStart(null); onRangeSelect?.({ start, end }); } break; } setSelectedValue(newValue); onChange?.(newValue); }; const handleTimeChange = (time) => { if (selectedValue instanceof Date) { const newValue = combineDateTime(selectedValue, time); setSelectedValue(newValue); onChange?.(newValue); } }; const isInRange = (date) => { if (selectionMode !== "range") return false; if (selectedValue && typeof selectedValue === "object" && "start" in selectedValue) { return isDateInRange(date, selectedValue); } if (rangeStart && hoveredDate) { const start = rangeStart < hoveredDate ? rangeStart : hoveredDate; const end = rangeStart < hoveredDate ? hoveredDate : rangeStart; return isDateInRange(date, { start, end }); } return false; }; const renderWeekNumber = (date) => { if (!showWeekNumbers) return null; return /* @__PURE__ */ jsx5("div", { className: "rcp-calendar__week-number", children: getWeekNumber(date) }); }; const renderCalendarGrid = () => { const weeks = []; let currentWeek = []; displayDays.forEach((day, index) => { currentWeek.push(day); if (currentWeek.length === 7 || index === displayDays.length - 1) { weeks.push([...currentWeek]); currentWeek = []; } }); return weeks.map((week, weekIndex) => /* @__PURE__ */ jsxs5("div", { className: "rcp-calendar__week", children: [ showWeekNumbers && renderWeekNumber(week[0]), week.map((day) => /* @__PURE__ */ jsx5( CalendarDay, { date: day, currentMonth: currentDate, selectedValue: Array.isArray(selectedValue) ? selectedValue : selectedValue instanceof Date ? selectedValue : null, events, onDateClick: handleDateClick, dayRenderer, minDate, maxDate, disabledDates, disabledDaysOfWeek, disableWeekends, showOtherMonths, className: clsx5( isInRange(day) && "rcp-calendar-day--in-range" ) }, day.toISOString() )) ] }, weekIndex)); }; const themeStyles = theme ? { "--rcp-primary": theme.primary, "--rcp-secondary": theme.secondary, "--rcp-background": theme.background, "--rcp-text": theme.text, "--rcp-text-secondary": theme.textSecondary, "--rcp-border": theme.border, "--rcp-hover": theme.hover, "--rcp-selected": theme.selected, "--rcp-disabled": theme.disabled, "--rcp-today": theme.today } : {}; return /* @__PURE__ */ jsxs5( "div", { className: clsx5( "rcp-calendar", `rcp-calendar--${currentView}`, responsive && "rcp-calendar--responsive", showTimePicker && "rcp-calendar--with-time", className ), style: themeStyles, children: [ headerRenderer ? headerRenderer({ date: currentDate, view: currentView, onPrevious: handlePrevious, onNext: handleNext, onViewChange: handleViewChange }) : /* @__PURE__ */ jsx5( CalendarHeader, { currentDate, view: currentView, onPrevious: handlePrevious, onNext: handleNext, onViewChange: handleViewChange, onDateChange: handleDateChange, minDate, maxDate } ), /* @__PURE__ */ jsxs5("div", { className: "rcp-calendar__body", children: [ currentView !== "day" && /* @__PURE__ */ jsxs5("div", { className: "rcp-calendar__weekdays", children: [ showWeekNumbers && /* @__PURE__ */ jsx5("div", { className: "rcp-calendar__week-number-header", children: "#" }), weekHeaders.map((day) => /* @__PURE__ */ jsx5("div", { className: "rcp-calendar__weekday", children: format4(day, "EEE", { locale: enUS4 }) }, day.toISOString())) ] }), /* @__PURE__ */ jsx5( "div", { className: "rcp-calendar__grid", onMouseLeave: () => setHoveredDate(null), children: renderCalendarGrid() } ) ] }), showTimePicker && selectedValue instanceof Date && /* @__PURE__ */ jsx5("div", { className: "rcp-calendar__time-picker", children: /* @__PURE__ */ jsx5( TimePicker, { value: selectedValue, onChange: handleTimeChange, format: timeFormat, minuteStep } ) }) ] } ); }; // src/components/CalendarInput.tsx import { useState as useState4, useRef as useRef2, useEffect as useEffect4 } from "react"; import { format as format5 } from "date-fns"; import clsx6 from "clsx"; import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime"; var CalendarInput = ({ value, defaultValue, onChange, placeholder = "Select date...", inputClassName, calendarClassName, showInput = true, inputFormat = "MM/dd/yyyy", disabled = false, readOnly = false, clearable = false, onClear, dropdownPosition = "auto", dropdownOffset = 4, closeOnSelect = true, closeOnOutsideClick = true, onOpen, onClose, name, id, autoComplete = "off", required = false, selectionMode = "single", showTimePicker = false, ...calendarProps }) => { const [isOpen, setIsOpen] = useState4(false); const [inputValue, setInputValue] = useState4(""); const [dropdownStyle, setDropdownStyle] = useState4({}); const inputRef = useRef2(null); const calendarRef = useRef2(null); const containerRef = useRef2(null); const formatValue = (val) => { if (!val) return ""; if (val instanceof Date) { return format5(val, inputFormat); } if (Array.isArray(val)) { return val.map((d) => format5(d, inputFormat)).join(", "); } const start = val.start ? format5(val.start, inputFormat) : ""; const end = val.end ? format5(val.end, inputFormat) : ""; return start && end ? `${start} - ${end}` : start || end; }; useEffect4(() => { setInputValue(formatValue(value ?? defaultValue ?? null)); }, [value, defaultValue, inputFormat]); const handleInputChange = (e) => { if (readOnly) return; setInputValue(e.target.value); }; const handleInputClick = () => { if (disabled || readOnly) return; if (!isOpen) { setIsOpen(true); onOpen?.(); } }; const handleCalendarChange = (newValue) => { onChange?.(newValue); setInputValue(formatValue(newValue)); if (closeOnSelect && selectionMode === "single" && !showTimePicker) { setIsOpen(false); onClose?.(); } }; const handleClear = (e) => { e.stopPropagation(); onChange?.(null); setInputValue(""); onClear?.(); }; useEffect4(() => { if (isOpen && inputRef.current) { const inputRect = inputRef.current.getBoundingClientRect(); const viewportHeight = window.innerHeight; let top = inputRect.bottom + dropdownOffset; let position = "bottom"; if (dropdownPosition === "auto") { const spaceBelow = viewportHeight - inputRect.bottom; const estimatedCalendarHeight = 400; if (spaceBelow < estimatedCalendarHeight && inputRect.top > estimatedCalendarHeight) { position = "top"; } } else if (dropdownPosition === "top") { position = "top"; } setDropdownStyle({ position: "fixed", top: position === "top" ? "auto" : top, bottom: position === "top" ? viewportHeight - inputRect.top + dropdownOffset : "auto", left: inputRect.left, zIndex: 1e3, minWidth: inputRect.width }); } }, [isOpen, dropdownPosition, dropdownOffset]); useEffect4(() => { const handleClickOutside = (event) => { if (closeOnOutsideClick && isOpen && containerRef.current && !containerRef.current.contains(event.target)) { setIsOpen(false); onClose?.(); } }; if (isOpen) { document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); } }, [isOpen, closeOnOutsideClick, onClose]); useEffect4(() => { const handleEscape = (event) => { if (event.key === "Escape" && isOpen) { setIsOpen(false); onClose?.(); } }; if (isOpen) { document.addEventListener("keydown", handleEscape); return () => document.removeEventListener("keydown", handleEscape); } }, [isOpen, onClose]); useEffect4(() => { let timeoutId = null; const handleScroll = () => { if (isOpen && inputRef.current) { if (timeoutId) { clearTimeout(timeoutId); } timeoutId = setTimeout(() => { const inputRect = inputRef.current.getBoundingClientRect(); const viewportHeight = window.innerHeight; const viewportWidth = window.innerWidth; if (inputRect.bottom < 0 || inputRect.top > viewportHeight || inputRect.right < 0 || inputRect.left > viewportWidth) { setIsOpen(false); onClose?.(); return; } let top = inputRect.bottom + dropdownOffset; let position = "bottom"; if (dropdownPosition === "auto") { const spaceBelow = viewportHeight - inputRect.bottom; const estimatedCalendarHeight = 400; if (spaceBelow < estimatedCalendarHeight && inputRect.top > estimatedCalendarHeight) { position = "top"; } } else if (dropdownPosition === "top") { position = "top"; } setDropdownStyle({ position: "fixed", top: position === "top" ? "auto" : top, bottom: position === "top" ? viewportHeight - inputRect.top + dropdownOffset : "auto", left: inputRect.left, zIndex: 1e3, minWidth: inputRect.width }); }, 16); } }; if (isOpen) { window.addEventListener("scroll", handleScroll, true); window.addEventListener("resize", handleScroll); return () => { if (timeoutId) { clearTimeout(timeoutId); } window.removeEventListener("scroll", handleScroll, true); window.removeEventListener("resize", handleScroll); }; } }, [isOpen, dropdownPosition, dropdownOffset]); return /* @__PURE__ */ jsxs6("div", { ref: containerRef, className: "rcp-calendar-input", children: [ showInput && /* @__PURE__ */ jsxs6("div", { className: "rcp-calendar-input__wrapper", children: [ /* @__PURE__ */ jsx6( "input", { ref: inputRef, type: "text", value: inputValue, onChange: handleInputChange, onClick: handleInputClick, placeholder, disabled, readOnly, name, id, autoComplete, required, className: clsx6( "rcp-calendar-input__field", disabled && "rcp-calendar-input__field--disabled", readOnly && "rcp-calendar-input__field--readonly", isOpen && "rcp-calendar-input__field--open", inputClassName ) } ), clearable && inputValue && !disabled && !readOnly && /* @__PURE__ */ jsx6( "button", { type: "button", onClick: handleClear, className: "rcp-calendar-input__clear", "aria-label": "Clear date", children: "\xD7" } ), /* @__PURE__ */ jsx6( "button", { type: "button", onClick: handleInputClick, disabled, className: clsx6( "rcp-calendar-input__toggle", disabled && "rcp-calendar-input__toggle--disabled", isOpen && "rcp-calendar-input__toggle--open" ), "aria-label": "Open calendar", children: "\u{1F4C5}" } ) ] }), isOpen && /* @__PURE__ */ jsxs6( "div", { ref: calendarRef, className: clsx6( "rcp-calendar-input__dropdown", calendarClassName ), style: dropdownStyle, children: [ /* @__PURE__ */ jsx6( Calendar, { value: value || defaultValue, onChange: handleCalendarChange, selectionMode, showTimePicker, ...calendarProps } ), showTimePicker && /* @__PURE__ */ jsx6("div", { style: { padding: "12px 16px", borderTop: "1px solid var(--rcp-border)", backgroundColor: "var(--rcp-hover)", display: "flex", justifyContent: "flex-end", gap: "8px" }, children: /* @__PURE__ */ jsx6( "button", { type: "button", onClick: () => { setIsOpen(false); onClose?.(); }, style: { padding: "8px 16px", backgroundColor: "var(--rcp-primary)", color: "white", border: "none", borderRadius: "4px", cursor: "pointer", fontSize: "14px", fontWeight: "500" }, children: "Done" } ) }) ] } ) ] }); }; // src/index.ts import "./calendar-5YM4I3IC.css"; export { Calendar, CalendarDay, CalendarHeader, CalendarInput, DEFAULT_LOCALE, TimePicker, YearMonthSelector, combineDateTime, formatDate, formatDateForDisplay, getCalendarDays, getMonthNames, getViewTitle, getWeekDays, getWeekNumber, getYearRange, isDateDisabled, isDateInRange, isDateSelected, navigateDate, navigateToMonth, navigateToYear, navigateToYearMonth, parseDateTime };