UNPKG

whensday-calendar-lib

Version:

A flexible calendar library with Whensday/Gregorian calendar systems, event management, and customizable UI components

875 lines (863 loc) 56.3 kB
'use strict'; var jsxRuntime = require('react/jsx-runtime'); var React = require('react'); // Whensday Calendar Constants const WHENSDAY_MONTHS = [ 'Snowlyn', 'Aquilaria', 'Aquelaelum', 'Emberfell', 'Mayfall', 'Peachcup', 'Sol', 'Butterdoodle', 'Sweetpop', 'Shadowfell', 'Knocturn', 'Finnabell', 'Zephania' ]; const SOL_MONTH_INDEX = 6; // Index for Sol month (leap day month) // Gregorian Calendar Constants const GREGORIAN_MONTH_NAMES = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ]; // Default Colors const DEFAULT_COLORS = [ '#ef4444', // red-500 '#f97316', // orange-500 '#eab308', // yellow-500 '#22c55e', // green-500 '#06b6d4', // cyan-500 '#3b82f6', // blue-500 '#8b5cf6', // violet-500 '#ec4899', // pink-500 '#6b7280', // gray-500 '#84cc16', // lime-500 '#14b8a6', // teal-500 '#f59e0b', // amber-500 ]; // Default Theme const DEFAULT_THEME = { colors: { primary: '#3b82f6', secondary: '#6b7280', accent: '#f59e0b', background: '#111827', surface: '#1f2937', text: '#f9fafb', textSecondary: '#9ca3af', border: '#374151', today: '#f59e0b', whensdayLeap: '#ec4899', }, fonts: { primary: 'Inter, system-ui, sans-serif', secondary: 'Bangers, cursive', }, spacing: { xs: '0.25rem', sm: '0.5rem', md: '1rem', lg: '1.5rem', xl: '2rem', }, borderRadius: { sm: '0.25rem', md: '0.5rem', lg: '0.75rem', }, }; // Week day names const WEEK_DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; const WEEK_DAYS_FULL = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; // Time constants const HOURS_IN_DAY = 24; const MINUTES_IN_HOUR = 60; const MINUTES_IN_DAY = HOURS_IN_DAY * MINUTES_IN_HOUR; // Default hour height for week/day views const DEFAULT_HOUR_HEIGHT = 8; // rem const isLeapYear = (year) => { return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; }; const getDaysInWhensdayMonth = (monthIndex, year) => { if (monthIndex === SOL_MONTH_INDEX && isLeapYear(year)) { return 29; // Sol has 29 days in leap years } return 28; // All months have 28 days normally }; const gregorianToWhensday = (gregorianDate) => { const year = gregorianDate.getUTCFullYear(); const startOfYear = Date.UTC(year, 0, 1); const dateInUtc = Date.UTC(year, gregorianDate.getUTCMonth(), gregorianDate.getUTCDate()); const dayOfYear = Math.floor((dateInUtc - startOfYear) / (1000 * 60 * 60 * 24)) + 1; // January 1st is Weensday if (dayOfYear === 1) { return { year, month: -1, day: 1, monthName: 'Weensday', isWeensday: true }; } // Calculate days into the 13-month calendar (starting from January 2nd) let whensdayDayCount = dayOfYear - 1; const leap = isLeapYear(year); let monthIndex = 0; for (monthIndex = 0; monthIndex < WHENSDAY_MONTHS.length; monthIndex++) { const daysInThisMonth = getDaysInWhensdayMonth(monthIndex, year); if (whensdayDayCount <= daysInThisMonth) { break; } whensdayDayCount -= daysInThisMonth; } // Handle case where date is beyond the last day of the year if (monthIndex >= WHENSDAY_MONTHS.length) { const lastMonth = WHENSDAY_MONTHS.length - 1; return { year, month: lastMonth, day: getDaysInWhensdayMonth(lastMonth, year), monthName: WHENSDAY_MONTHS[lastMonth], }; } const dayOfMonth = whensdayDayCount; return { year, month: monthIndex, day: dayOfMonth, monthName: WHENSDAY_MONTHS[monthIndex], isWhensday: leap && monthIndex === SOL_MONTH_INDEX && dayOfMonth === 15, }; }; const whensdayToGregorian = (whensdayDate) => { // Handle Weensday (January 1st) if (whensdayDate.isWeensday || whensdayDate.month === -1) { return new Date(Date.UTC(whensdayDate.year, 0, 1)); } let dayCount = 0; // Count days from all previous months for (let i = 0; i < whensdayDate.month; i++) { dayCount += getDaysInWhensdayMonth(i, whensdayDate.year); } // Add days in current month dayCount += whensdayDate.day; // Add 1 because January 1st is Weensday const gregorianDayOfYear = dayCount + 1; return new Date(Date.UTC(whensdayDate.year, 0, gregorianDayOfYear)); }; const getFirstDayOfWeekForMonth = (monthIndex, year) => { const firstDayOfWhensdayMonth = { year, month: monthIndex, day: 1, monthName: WHENSDAY_MONTHS[monthIndex], }; const gregorianDate = whensdayToGregorian(firstDayOfWhensdayMonth); return gregorianDate.getUTCDay(); // 0 for Sunday }; // Helper to convert any DayInGrid to a standard JS Date for calculations const dayInGridToDate = (day, system) => { if (system === 'gregorian') { return new Date(Date.UTC(day.year, day.month, day.day)); } // It's a whensday date, convert it return whensdayToGregorian({ ...day, monthName: WHENSDAY_MONTHS[day.month] || 'Weensday', isWeensday: day.month === -1, isWhensday: !!day.isWhensdayLeapDay }); }; const areDayInGridEqual = (day1, day2) => { return day1.year === day2.year && day1.month === day2.month && day1.day === day2.day; }; const daysBetween = (startDate, endDate) => { const startUTC = Date.UTC(startDate.getUTCFullYear(), startDate.getUTCMonth(), startDate.getUTCDate()); const endUTC = Date.UTC(endDate.getUTCFullYear(), endDate.getUTCMonth(), endDate.getUTCDate()); return Math.round((endUTC - startUTC) / (1000 * 60 * 60 * 24)); }; // --- Universal Timestamp Helpers --- const areUniversalDaysEqual = (ts1, ts2) => { const d1 = new Date(ts1); const d2 = new Date(ts2); return d1.getUTCFullYear() === d2.getUTCFullYear() && d1.getUTCMonth() === d2.getUTCMonth() && d1.getUTCDate() === d2.getUTCDate(); }; const universalDateToDayInGrid = (ts, system) => { const date = new Date(ts); if (system === 'whensday') { const wd = gregorianToWhensday(date); return { year: wd.year, month: wd.month, day: wd.day, monthName: wd.monthName, isWhensdayLeapDay: wd.isWhensday }; } else { // gregorian const month = date.getUTCMonth(); return { year: date.getUTCFullYear(), month, day: date.getUTCDate(), monthName: GREGORIAN_MONTH_NAMES[month] }; } }; const formatUniversalDate = (ts, system) => { if (!ts) return 'Not Set'; const dayInGrid = universalDateToDayInGrid(ts, system); return `${dayInGrid.monthName} ${dayInGrid.day}, ${dayInGrid.year}`; }; const getDayKey$2 = (date) => `${date.year}-${date.month}-${date.day}`; // Helper function to get today's date in a specific calendar system const getTodayInSystem = (system) => { const today = new Date(); if (system === 'whensday') { const whensdayToday = gregorianToWhensday(today); return { year: whensdayToday.year, month: whensdayToday.month, day: whensdayToday.day, monthName: whensdayToday.monthName, isWhensdayLeapDay: whensdayToday.isWhensday, }; } else { return { year: today.getFullYear(), month: today.getMonth(), day: today.getDate(), monthName: GREGORIAN_MONTH_NAMES[today.getMonth()] || '', }; } }; const useCalendar = (initialView = 'month', initialSystem = 'gregorian', initialDate) => { // State const [view, setView] = React.useState(initialView); const [system, setSystemState] = React.useState(initialSystem); const [displayDate, setDisplayDate] = React.useState(() => { return initialDate || getTodayInSystem(initialSystem); }); const [hourHeight, setHourHeight] = React.useState(8); // Get today's date in current system const todayInCurrentSystem = React.useMemo(() => getTodayInSystem(system), [system]); // Custom setSystem that converts the display date const setSystem = React.useCallback((newSystem) => { if (newSystem === system) return; // Convert current display date to new system const currentDate = dayInGridToDate(displayDate, system); const newDisplayDate = universalDateToDayInGrid(currentDate.getTime(), newSystem); setDisplayDate(newDisplayDate); setSystemState(newSystem); }, [displayDate, system]); // Calendar grid calculation const calendarGrid = React.useMemo(() => { const year = displayDate.year; const month = displayDate.month; const grid = []; if (system === 'whensday') { const daysInMonth = getDaysInWhensdayMonth(month, year); const firstDayOfWeek = getFirstDayOfWeekForMonth(month, year); const monthName = WHENSDAY_MONTHS[month] || WHENSDAY_MONTHS[0]; // Add empty cells for days before the first day of the month for (let i = 0; i < firstDayOfWeek; i++) { grid.push({ date: { year, month, day: 0, monthName: '' }, isToday: false, isCurrentMonth: false }); } // Add days of the month for (let day = 1; day <= daysInMonth; day++) { const isToday = day === todayInCurrentSystem.day && month === todayInCurrentSystem.month && year === todayInCurrentSystem.year; const isLeap = isLeapYear(year); grid.push({ date: { year, month, day, monthName, isWhensdayLeapDay: isLeap && month === SOL_MONTH_INDEX && day === 15 }, isToday, isCurrentMonth: true, }); } } else { // Gregorian calendar const monthName = GREGORIAN_MONTH_NAMES[month]; const firstDayOfMonth = new Date(year, month, 1); const daysInMonth = new Date(year, month + 1, 0).getDate(); const firstDayOfWeek = firstDayOfMonth.getDay(); // Add empty cells for days before the first day of the month for (let i = 0; i < firstDayOfWeek; i++) { grid.push({ date: { year, month, day: 0, monthName: '' }, isToday: false, isCurrentMonth: false }); } // Add days of the month for (let day = 1; day <= daysInMonth; day++) { const isToday = day === todayInCurrentSystem.day && month === todayInCurrentSystem.month && year === todayInCurrentSystem.year; grid.push({ date: { year, month, day, monthName }, isToday, isCurrentMonth: true, }); } } return grid; }, [displayDate, system, todayInCurrentSystem]); // Current month/year info const currentMonthName = React.useMemo(() => { if (system === 'whensday') { return WHENSDAY_MONTHS[displayDate.month] || WHENSDAY_MONTHS[0]; } else { return GREGORIAN_MONTH_NAMES[displayDate.month] || ''; } }, [displayDate.month, system]); const currentYear = displayDate.year; // Navigation functions const goToNextPeriod = React.useCallback(() => { setDisplayDate(prev => { if (system === 'whensday') { const monthLimit = 12; // 13 months (0-12) if (prev.month === monthLimit) { getTodayInSystem(system); return { ...prev, year: prev.year + 1, month: 0, monthName: WHENSDAY_MONTHS[0] }; } else { return { ...prev, month: prev.month + 1, monthName: WHENSDAY_MONTHS[prev.month + 1] }; } } else { const monthLimit = 11; // 12 months (0-11) if (prev.month === monthLimit) { return { ...prev, year: prev.year + 1, month: 0, monthName: GREGORIAN_MONTH_NAMES[0] }; } else { return { ...prev, month: prev.month + 1, monthName: GREGORIAN_MONTH_NAMES[prev.month + 1] }; } } }); }, [system]); const goToPrevPeriod = React.useCallback(() => { setDisplayDate(prev => { if (system === 'whensday') { const monthLimit = 12; if (prev.month === 0) { return { ...prev, year: prev.year - 1, month: monthLimit, monthName: WHENSDAY_MONTHS[monthLimit] }; } else { return { ...prev, month: prev.month - 1, monthName: WHENSDAY_MONTHS[prev.month - 1] }; } } else { const monthLimit = 11; if (prev.month === 0) { return { ...prev, year: prev.year - 1, month: monthLimit, monthName: GREGORIAN_MONTH_NAMES[monthLimit] }; } else { return { ...prev, month: prev.month - 1, monthName: GREGORIAN_MONTH_NAMES[prev.month - 1] }; } } }); }, [system]); const goToToday = React.useCallback(() => { const todayDate = getTodayInSystem(system); setDisplayDate(todayDate); }, [system]); const goToDate = React.useCallback((date) => { setDisplayDate(date); }, []); const toggleSystem = React.useCallback(() => { const newSystem = system === 'gregorian' ? 'whensday' : 'gregorian'; setSystem(newSystem); }, [system, setSystem]); // Schedule calculations const useScheduleCalculations = (days, schedules, layersById, visibleLayerIds, areSchedulesGloballyVisible = true) => { return React.useMemo(() => { if (!areSchedulesGloballyVisible) { return new Map(); } const schedulesByDay = new Map(); for (const day of days) { if (!day.isCurrentMonth) continue; const dayKey = getDayKey$2(day.date); const dayTimestamp = dayInGridToDate(day.date, system).getTime(); const daySchedules = []; for (const schedule of schedules) { if (!schedule.isVisible) continue; if (schedule.layerId && !visibleLayerIds.has(schedule.layerId)) continue; // Calculate schedule state for this day const state = calculateScheduleState(schedule, dayTimestamp); if (state === 'off') continue; const layer = schedule.layerId ? layersById.get(schedule.layerId) : null; const color = schedule.color || layer?.color || '#6B7280'; const isCompleted = schedule.completedDays?.includes(dayTimestamp) || false; daySchedules.push({ schedule, color, state, isCompleted, }); } if (daySchedules.length > 0) { schedulesByDay.set(dayKey, daySchedules); } } return schedulesByDay; }, [days, schedules, layersById, visibleLayerIds, system, areSchedulesGloballyVisible]); }; // Helper function to calculate schedule state const calculateScheduleState = (schedule, dayTimestamp) => { // Check if it's a holiday if (schedule.bookedHolidays?.includes(dayTimestamp)) { return 'holiday'; } // Check if it's a bonus day if (schedule.bonusDays?.includes(dayTimestamp)) { return 'bonus'; } // Check if it's in an exemption period if (schedule.exemptions) { for (const exemption of schedule.exemptions) { if (dayTimestamp >= exemption.startDate && dayTimestamp <= exemption.endDate) { return 'exemption'; } } } // Calculate pattern state const daysSinceStart = Math.floor((dayTimestamp - schedule.startDate) / (1000 * 60 * 60 * 24)); if (daysSinceStart < 0) return 'off'; const patternLength = schedule.pattern.on + schedule.pattern.off; if (patternLength === 0) return 'off'; const dayInPattern = daysSinceStart % patternLength; return dayInPattern < schedule.pattern.on ? 'on' : 'off'; }; return { // State view, system, displayDate, calendarGrid, currentMonthName, currentYear, hourHeight, // Setters setView, setSystem, setDisplayDate, setHourHeight, // Navigation goToNextPeriod, goToPrevPeriod, goToToday, goToDate, toggleSystem, // Utilities useScheduleCalculations, getDayKey: getDayKey$2, }; }; const CalendarHeader = ({ currentMonthName, currentYear, view, system, onViewChange, onSystemChange, onNextPeriod, onPrevPeriod, onToday, }) => { const views = ['month', 'week', 'day']; const getViewButtonClasses = (buttonView) => { const base = "calendar-view-button"; if (view === buttonView) { return `${base} calendar-view-button-active`; } return `${base} calendar-view-button-inactive`; }; return (jsxRuntime.jsxs("div", { className: "calendar-header", children: [jsxRuntime.jsxs("div", { className: "calendar-header-top", children: [jsxRuntime.jsxs("h2", { className: "calendar-title", children: [currentMonthName, " ", jsxRuntime.jsx("span", { className: "calendar-year", children: currentYear })] }), jsxRuntime.jsx("button", { onClick: () => onSystemChange(system === 'gregorian' ? 'whensday' : 'gregorian'), className: "calendar-system-toggle", children: system === 'whensday' ? 'Whensday' : 'Gregorian' })] }), jsxRuntime.jsxs("div", { className: "calendar-header-bottom", children: [jsxRuntime.jsxs("div", { className: "calendar-navigation", children: [jsxRuntime.jsx("button", { onClick: onPrevPeriod, className: "calendar-nav-button", "aria-label": "Previous period", children: jsxRuntime.jsx("svg", { viewBox: "0 0 20 20", fill: "currentColor", children: jsxRuntime.jsx("path", { fillRule: "evenodd", d: "M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z", clipRule: "evenodd" }) }) }), jsxRuntime.jsx("button", { onClick: onToday, className: "calendar-today-button", children: "Today" }), jsxRuntime.jsx("button", { onClick: onNextPeriod, className: "calendar-nav-button", "aria-label": "Next period", children: jsxRuntime.jsx("svg", { viewBox: "0 0 20 20", fill: "currentColor", children: jsxRuntime.jsx("path", { fillRule: "evenodd", d: "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", clipRule: "evenodd" }) }) })] }), jsxRuntime.jsx("div", { className: "calendar-view-toggles", children: views.map(v => (jsxRuntime.jsx("button", { onClick: () => onViewChange(v), className: getViewButtonClasses(v), children: v }, v))) })] })] })); }; const EventPill = ({ event, color, onDragStart, onClick }) => (jsxRuntime.jsx("div", { "data-event-pill": "true", draggable: true, onDragStart: onDragStart, onClick: onClick, style: { backgroundColor: color }, className: "calendar-event-pill", children: event.name })); const TaskPill = ({ event, color, onEventClick, onDragStart, onToggleComplete }) => { const handleToggle = (e) => { e.stopPropagation(); if (onToggleComplete) { onToggleComplete({ ...event, isCompleted: !event.isCompleted }); } }; return (jsxRuntime.jsxs("div", { "data-event-pill": "true", draggable: true, onDragStart: onDragStart, style: { backgroundColor: color, color: '#FFF' }, className: "calendar-task-pill", onClick: (e) => { e.stopPropagation(); onEventClick(event); }, children: [jsxRuntime.jsx("div", { onClick: handleToggle, className: "calendar-task-checkbox", children: jsxRuntime.jsx("input", { type: "checkbox", readOnly: true, checked: !!event.isCompleted, className: "calendar-checkbox" }) }), jsxRuntime.jsx("span", { className: `calendar-task-text ${event.isCompleted ? 'calendar-task-completed' : ''}`, children: event.name })] })); }; const getDayKey$1 = (date) => `${date.year}-${date.month}-${date.day}`; const DayCell = ({ day, daySchedules, dayEvents, onEventAdd, onEventClick, onEventToggle, onDragStart, onDrop, setDraggingOverDay, isDraggingOver, system, layersById, showTodayIndicator, showHolidayIndicators, showScheduleIndicators }) => { const pressTimer = React.useRef(null); const longPressTriggered = React.useRef(false); // Refs and state for scrollable events const eventsContainerRef = React.useRef(null); const [isOverflowing, setIsOverflowing] = React.useState(false); const [hiddenEventsCount, setHiddenEventsCount] = React.useState(0); const [isScrolled, setIsScrolled] = React.useState(false); React.useLayoutEffect(() => { const container = eventsContainerRef.current; if (!container) return; const checkOverflow = () => { const hasOverflow = container.scrollHeight > container.clientHeight; setIsOverflowing(hasOverflow); if (hasOverflow) { const pillHeight = 26; // Approx height of a pill + margin const visibleCount = Math.floor(container.clientHeight / pillHeight); const hiddenCount = dayEvents.length - visibleCount; setHiddenEventsCount(Math.max(0, hiddenCount)); } else { setHiddenEventsCount(0); } const scrolled = container.scrollTop > 0; setIsScrolled(scrolled); }; checkOverflow(); const resizeObserver = new ResizeObserver(checkOverflow); resizeObserver.observe(container); container.addEventListener('scroll', checkOverflow); return () => { resizeObserver.disconnect(); container.removeEventListener('scroll', checkOverflow); }; }, [dayEvents.length]); const handleMouseDown = (e) => { if (e.target !== e.currentTarget) return; longPressTriggered.current = false; pressTimer.current = window.setTimeout(() => { longPressTriggered.current = true; onEventAdd(day.date); }, 500); }; const handleMouseUp = () => { if (pressTimer.current) { clearTimeout(pressTimer.current); pressTimer.current = null; } }; const handleClick = (e) => { if (e.target !== e.currentTarget) return; if (longPressTriggered.current) return; // Single click - could navigate to day view or just select }; const handleDragEnter = () => setDraggingOverDay(getDayKey$1(day.date)); const handleDragLeave = (e) => { const rect = e.currentTarget.getBoundingClientRect(); const x = e.clientX; const y = e.clientY; if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) { setDraggingOverDay(null); } }; const handleDragOver = (e) => { e.preventDefault(); e.dataTransfer.dropEffect = "move"; }; const handleEventDragStart = (e, eventId) => { onDragStart(e, eventId); }; // Schedule indicators const hasHoliday = daySchedules.some(s => s.state === 'holiday'); const isBonusDay = daySchedules.some(s => s.state === 'bonus'); const onSchedulesForPills = daySchedules.filter(s => s.state === 'on' || s.state === 'bonus'); return (jsxRuntime.jsx("div", { className: `calendar-month-day ${day.isCurrentMonth ? 'calendar-month-day-current' : 'calendar-month-day-other'} ${day.isToday && showTodayIndicator ? 'calendar-month-day-today' : ''} ${isDraggingOver ? 'calendar-month-day-dragging' : ''}`, onMouseDown: handleMouseDown, onMouseUp: handleMouseUp, onMouseLeave: handleMouseUp, onClick: handleClick, onDragEnter: handleDragEnter, onDragLeave: handleDragLeave, onDragOver: handleDragOver, onDrop: (e) => onDrop(e, day.date), children: day.isCurrentMonth && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs("div", { className: "calendar-month-day-header", children: [showScheduleIndicators && onSchedulesForPills.length > 0 && (jsxRuntime.jsx("div", { className: "calendar-month-schedule-pills", children: onSchedulesForPills.map(sfd => (jsxRuntime.jsx("div", { className: "calendar-month-schedule-pill", style: { backgroundColor: sfd.color, opacity: sfd.isCompleted ? 1 : 0.7 }, title: `${sfd.schedule.name} - ${sfd.isCompleted ? 'completed' : 'pending'}` }, sfd.schedule.id))) })), jsxRuntime.jsxs("div", { className: "calendar-month-day-info", children: [showHolidayIndicators && hasHoliday && (jsxRuntime.jsx("div", { className: "calendar-month-holiday-indicator", title: "Holiday Booked", children: jsxRuntime.jsx("svg", { viewBox: "0 0 20 20", fill: "currentColor", children: jsxRuntime.jsx("path", { fillRule: "evenodd", d: "M5 5a3 3 0 015.252-2.121l.738.646.738-.646A3 3 0 0115 5v2.25a.75.75 0 01-1.5 0V5a1.5 1.5 0 00-2.625-1.06l-.738.646-.738-.646A1.5 1.5 0 006.5 5v2.25a.75.75 0 01-1.5 0V5zm1.5 4.5a.75.75 0 01.75.75v3a.75.75 0 01-1.5 0v-3a.75.75 0 01.75-.75zM12.5 9.5a.75.75 0 01.75.75v3a.75.75 0 01-1.5 0v-3a.75.75 0 01.75-.75zM2 10a8 8 0 1116 0 8 8 0 01-16 0z", clipRule: "evenodd" }) }) })), isBonusDay && (jsxRuntime.jsx("div", { className: "calendar-month-bonus-indicator", title: "Bonus Day", children: jsxRuntime.jsx("svg", { viewBox: "0 0 20 20", fill: "currentColor", children: jsxRuntime.jsx("path", { d: "M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" }) }) })), jsxRuntime.jsx("span", { className: "calendar-month-day-number", children: day.date.day })] })] }), day.date.isWhensdayLeapDay && (jsxRuntime.jsx("div", { className: "calendar-whensday-leap", children: "WHENSDAY" })), jsxRuntime.jsx("div", { ref: eventsContainerRef, className: `calendar-month-events ${isScrolled ? 'calendar-month-events-scrolled' : ''}`, children: dayEvents.map(event => { const layer = event.layerId ? layersById.get(event.layerId) : null; if (!layer && event.layerId) return null; const eventColor = event.color || layer?.color || '#6B7280'; if (event.eventType === 'task') { return (jsxRuntime.jsx(TaskPill, { event: event, color: eventColor, onEventClick: onEventClick, onDragStart: (e) => handleEventDragStart(e, event.id), onToggleComplete: onEventToggle }, event.id)); } else { return (jsxRuntime.jsx(EventPill, { event: event, color: eventColor, onDragStart: (e) => handleEventDragStart(e, event.id), onClick: (e) => { e.stopPropagation(); onEventClick(event); } }, event.id)); } }) }), isOverflowing && (jsxRuntime.jsx("div", { className: "calendar-month-overflow", children: hiddenEventsCount > 0 && (jsxRuntime.jsxs("span", { className: "calendar-month-overflow-text", children: ["+", hiddenEventsCount, " more"] })) }))] })) })); }; const MonthView = ({ calendarGrid, events, schedulesByDay, layersById, system, onEventClick, onEventAdd, onEventMove, onDateClick, showTodayIndicator, showHolidayIndicators, showScheduleIndicators, }) => { const [draggingOverDay, setDraggingOverDay] = React.useState(null); const WEEK_DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; // Group events by day const eventsByDay = React.useMemo(() => { const map = new Map(); for (const day of calendarGrid) { if (!day.isCurrentMonth) continue; const dayKey = getDayKey$1(day.date); const gridDayTimestamp = dayInGridToDate(day.date, system).getTime(); const dayEvents = events.filter(event => areUniversalDaysEqual(event.startDate, gridDayTimestamp)); map.set(dayKey, dayEvents); } return map; }, [events, calendarGrid, system]); const handleDragStart = (e, eventId) => { e.dataTransfer.setData("text/plain", eventId); e.dataTransfer.effectAllowed = "move"; }; const handleDrop = (e, day) => { e.preventDefault(); const eventId = e.dataTransfer.getData("text/plain"); if (!eventId || !onEventMove) return; const newDayTimestamp = dayInGridToDate(day, system).getTime(); onEventMove(eventId, newDayTimestamp); setDraggingOverDay(null); }; const handleEventToggle = (event) => { // This would typically call an onEventUpdate callback // For now, we'll just call onEventClick onEventClick(event); }; return (jsxRuntime.jsxs("div", { className: "calendar-month-view", children: [jsxRuntime.jsx("div", { className: "calendar-month-weekdays", children: WEEK_DAYS.map(day => (jsxRuntime.jsx("div", { className: "calendar-month-weekday", children: day }, day))) }), jsxRuntime.jsx("div", { className: "calendar-month-grid", children: calendarGrid.map((day, index) => (jsxRuntime.jsx(DayCell, { day: day, daySchedules: schedulesByDay.get(getDayKey$1(day.date)) || [], dayEvents: eventsByDay.get(getDayKey$1(day.date)) || [], onEventAdd: onEventAdd, onEventClick: onEventClick, onEventToggle: handleEventToggle, onDragStart: handleDragStart, onDrop: handleDrop, setDraggingOverDay: setDraggingOverDay, isDraggingOver: draggingOverDay === getDayKey$1(day.date), system: system, layersById: layersById, showTodayIndicator: showTodayIndicator, showHolidayIndicators: showHolidayIndicators, showScheduleIndicators: showScheduleIndicators }, index))) })] })); }; const WeekView = (props) => { return (jsxRuntime.jsx("div", { className: "calendar-week-view", children: jsxRuntime.jsx("div", { className: "calendar-week-placeholder", children: "Week View - Coming Soon" }) })); }; const HOURS$1 = Array.from({ length: 24 }, (_, i) => i); const parseTime = (timeStr) => { const [h, m] = timeStr.split(':').map(Number); return h * 60 + m; }; const DayColumn = ({ day, timedEvents, schedules, onTimeSlotClick, onEventClick, hourHeight, system, layersById, dragState, ghostTopPercent, isDraggingOver, onDragStart, onDragOver, onDrop, onDragEnd }) => { const columnRef = React.useRef(null); const handleTimeSlotClick = (e) => { if (e.target.closest('[data-event-id]')) { return; } const rect = e.currentTarget.getBoundingClientRect(); const clickY = e.clientY - rect.top; const totalHeight = e.currentTarget.offsetHeight; const minutesInDay = 24 * 60; const clickedMinute = Math.floor((clickY / totalHeight) * minutesInDay); const clickedHour = Math.floor(clickedMinute / 60); const time = `${String(clickedHour).padStart(2, '0')}:00`; onTimeSlotClick(day.date, time); }; const getScheduleBgStyle = () => { const onSchedules = schedules.filter(s => s.state === 'on' || s.state === 'bonus'); if (onSchedules.length === 0) return {}; const completedCount = onSchedules.filter(s => s.isCompleted).length; const totalScheds = onSchedules.length; const completionRatio = totalScheds > 0 ? completedCount / totalScheds : 0; const style = { opacity: 0.2 + (completionRatio * 0.2), filter: `brightness(${1 + (completionRatio * 0.3)})`, transition: 'opacity 0.2s ease-in-out, filter 0.2s ease-in-out', }; if (onSchedules.length === 1) { style.backgroundColor = onSchedules[0].color; } else { const colors = onSchedules.map(s => s.color).join(', '); style.backgroundImage = `linear-gradient(45deg, ${colors})`; } return style; }; const TravelTimeIndicator = ({ top, height, color }) => { if (height <= 0) return null; return (jsxRuntime.jsx("div", { className: "calendar-travel-time", style: { top: `${top}%`, height: `${height}%` }, children: jsxRuntime.jsx("div", { className: "calendar-travel-time-bar", style: { backgroundColor: color, opacity: 0.25, backgroundImage: 'repeating-linear-gradient(-45deg, transparent, transparent 4px, rgba(255,255,255,0.1) 4px, rgba(255,255,255,0.1) 8px)' } }) })); }; return (jsxRuntime.jsxs("div", { ref: columnRef, className: `calendar-day-column ${isDraggingOver ? 'calendar-day-column-dragging' : ''}`, style: { minHeight: `${24 * (hourHeight / 4)}rem` }, onDragOver: (e) => onDragOver(e, columnRef, day.date), onDrop: (e) => onDrop(e, day.date), onDragEnd: onDragEnd, onClick: handleTimeSlotClick, children: [jsxRuntime.jsx("div", { className: "calendar-schedule-background", style: getScheduleBgStyle() }), HOURS$1.map(hour => (jsxRuntime.jsx("div", { className: "calendar-hour-line", style: { height: `${hourHeight / 4}rem` } }, hour))), jsxRuntime.jsxs("div", { className: "calendar-events-container", children: [timedEvents.map(event => { const layer = event.layerId ? layersById.get(event.layerId) : null; if ((!layer && event.layerId) || !event.startTime || !event.endTime) return null; const eventColor = event.color || layer?.color || '#6B7280'; const gridDayTimestamp = dayInGridToDate(day.date, system).getTime(); const startsBeforeToday = !areUniversalDaysEqual(event.startDate, gridDayTimestamp); const endsAfterToday = event.endDate ? !areUniversalDaysEqual(event.endDate, gridDayTimestamp) : false; const startMinutes = startsBeforeToday ? 0 : parseTime(event.startTime); const endMinutes = endsAfterToday ? 24 * 60 : parseTime(event.endTime); const duration = Math.max(15, endMinutes - startMinutes); const top = (startMinutes / (24 * 60)) * 100; const height = (duration / (24 * 60)) * 100; const travelBeforeTop = ((startMinutes - (event.travelTimeBefore || 0)) / (24 * 60)) * 100; const travelBeforeHeight = ((event.travelTimeBefore || 0) / (24 * 60)) * 100; const travelAfterTop = (endMinutes / (24 * 60)) * 100; const travelAfterHeight = ((event.travelTimeAfter || 0) / (24 * 60)) * 100; const isBeingDragged = dragState?.eventId === event.id; return (jsxRuntime.jsxs(React.Fragment, { children: [jsxRuntime.jsx(TravelTimeIndicator, { top: travelBeforeTop, height: travelBeforeHeight, color: eventColor }), jsxRuntime.jsx("div", { draggable: true, onDragStart: (e) => onDragStart(e, event), "data-event-id": event.id, className: `calendar-event ${isBeingDragged ? 'calendar-event-dragging' : ''}`, style: { top: `${top}%`, height: `${height}%`, backgroundColor: eventColor }, onClick: (e) => { e.stopPropagation(); onEventClick(event); }, children: jsxRuntime.jsxs("div", { className: "calendar-event-content", children: [jsxRuntime.jsx("p", { className: "calendar-event-title", children: event.name }), jsxRuntime.jsxs("p", { className: "calendar-event-time", children: [!startsBeforeToday && event.startTime, (startsBeforeToday || endsAfterToday) && ' - ', !endsAfterToday && event.endTime] })] }) }), jsxRuntime.jsx(TravelTimeIndicator, { top: travelAfterTop, height: travelAfterHeight, color: eventColor })] }, event.id)); }), dragState && ghostTopPercent !== null && isDraggingOver && (jsxRuntime.jsx("div", { className: "calendar-event-ghost", style: { top: `${ghostTopPercent}%`, height: `${dragState.ghostHeightPercent}%`, backgroundColor: dragState.layer.color }, children: jsxRuntime.jsx("div", { className: "calendar-event-content", children: jsxRuntime.jsx("p", { className: "calendar-event-title", children: dragState.event.name }) }) }))] })] })); }; const HOURS = Array.from({ length: 24 }, (_, i) => i); const TimeGutter = ({ hourHeight, hourLabelInterval }) => { return (jsxRuntime.jsx("div", { className: "calendar-time-gutter", children: HOURS.map(hour => (jsxRuntime.jsx("div", { className: "calendar-time-slot", style: { height: `${hourHeight / 4}rem` }, children: hour > 0 && hour % hourLabelInterval === 0 && (jsxRuntime.jsx("span", { className: "calendar-time-label", children: hour === 12 ? '12 PM' : hour > 12 ? `${hour - 12} PM` : `${hour} AM` })) }, hour))) })); }; const getDayKey = (date) => `${date.year}-${date.month}-${date.day}`; const WEEK_DAY_NAMES = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; const DayHeader = ({ day, dayName, allDayEvents, schedules, layersById, onEventClick, onAllDayDrop, onDragOver, isDraggingOver, showTodayIndicator, showHolidayIndicators, showScheduleIndicators }) => { const hasHoliday = schedules.some(s => s.state === 'holiday'); const isBonusDay = schedules.some(s => s.state === 'bonus'); const onSchedulesForPills = schedules.filter(s => s.state === 'on' || s.state === 'bonus'); const handleAllDayDragStart = (e, eventId) => { e.dataTransfer.setData("text/plain", eventId); e.dataTransfer.effectAllowed = "move"; }; return (jsxRuntime.jsxs("div", { className: `calendar-day-header ${isDraggingOver ? 'calendar-day-header-dragging' : ''}`, children: [jsxRuntime.jsxs("div", { className: "calendar-day-header-content", children: [showScheduleIndicators && (jsxRuntime.jsx("div", { className: "calendar-schedule-pills", children: onSchedulesForPills.map(sfd => (jsxRuntime.jsx("div", { className: "calendar-schedule-pill", style: { backgroundColor: sfd.color, opacity: sfd.isCompleted ? 1 : 0.7 }, title: `${sfd.schedule.name} - ${sfd.isCompleted ? 'completed' : 'pending'}` }, sfd.schedule.id))) })), jsxRuntime.jsxs("div", { className: "calendar-day-info", children: [showHolidayIndicators && hasHoliday && (jsxRuntime.jsx("div", { className: "calendar-holiday-indicator", title: "Holiday Booked", children: jsxRuntime.jsx("svg", { viewBox: "0 0 20 20", fill: "currentColor", children: jsxRuntime.jsx("path", { fillRule: "evenodd", d: "M5 5a3 3 0 015.252-2.121l.738.646.738-.646A3 3 0 0115 5v2.25a.75.75 0 01-1.5 0V5a1.5 1.5 0 00-2.625-1.06l-.738.646-.738-.646A1.5 1.5 0 006.5 5v2.25a.75.75 0 01-1.5 0V5zm1.5 4.5a.75.75 0 01.75.75v3a.75.75 0 01-1.5 0v-3a.75.75 0 01.75-.75zM12.5 9.5a.75.75 0 01.75.75v3a.75.75 0 01-1.5 0v-3a.75.75 0 01.75-.75zM2 10a8 8 0 1116 0 8 8 0 01-16 0z", clipRule: "evenodd" }) }) })), isBonusDay && (jsxRuntime.jsx("div", { className: "calendar-bonus-indicator", title: "Bonus Day", children: jsxRuntime.jsx("svg", { viewBox: "0 0 20 20", fill: "currentColor", children: jsxRuntime.jsx("path", { d: "M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" }) }) })), jsxRuntime.jsx("span", { className: "calendar-day-name", children: dayName }), jsxRuntime.jsx("p", { className: `calendar-day-number ${day.isToday && showTodayIndicator ? 'calendar-day-today' : ''}`, children: day.date.day })] })] }), jsxRuntime.jsx("div", { className: "calendar-all-day-events", onDrop: (e) => onAllDayDrop(e, day.date), onDragOver: onDragOver, children: allDayEvents.map(event => { const layer = event.layerId ? layersById.get(event.layerId) : null; if (!layer && event.layerId) return null; const eventColor = event.color || layer?.color || '#6B7280'; return (jsxRuntime.jsx("div", { draggable: true, onDragStart: (e) => handleAllDayDragStart(e, event.id), "data-event-id": event.id, onClick: (e) => { e.stopPropagation(); onEventClick(event); }, className: "calendar-all-day-event", style: { backgroundColor: eventColor }, children: event.name }, event.id)); }) })] })); }; const DayView = ({ displayDate, events, schedulesByDay, layersById, system, hourHeight, onEventClick, onEventAdd, onEventMove, onDateClick, showTodayIndicator, showHolidayIndicators, showScheduleIndicators }) => { const timelineContainerRef = React.useRef(null); const [dragState, setDragState] = React.useState(null); const [ghostTopPercent, setGhostTopPercent] = React.useState(null); const [isDraggingOver, setIsDraggingOver] = React.useState(false); const todayGregorian = React.useMemo(() => new Date(), []); const todayComparableTimestamp = React.useMemo(() => new Date(Date.UTC(todayGregorian.getFullYear(), todayGregorian.getMonth(), todayGregorian.getDate())).getTime(), [todayGregorian]); const hourLabelInterval = React.useMemo(() => (hourHeight > 12 ? 1 : hourHeight > 8 ? 2 : hourHeight > 5 ? 3 : 4), [hourHeight]); const day = React.useMemo(() => { const dateTimestamp = dayInGridToDate(displayDate, system).getTime(); return { date: displayDate, isToday: areUniversalDaysEqual(dateTimestamp, todayComparableTimestamp), isCurrentMonth: true }; }, [displayDate, system, todayComparableTimestamp]); const dayName = React.useMemo(() => WEEK_DAY_NAMES[dayInGridToDate(displayDate, system).getUTCDay()], [displayDate, system]); const daySchedules = schedulesByDay.get(getDayKey(day.date)) || []; const { allDayEvents, timedEvents } = React.useMemo(() => { const gridDayTimestamp = dayInGridToDate(day.date, system).getTime(); const dayEvents = events.filter(event => areUniversalDaysEqual(event.startDate, gridDayTimestamp)); return { allDayEvents: dayEvents.filter(e => e.isAllDay), timedEvents: dayEvents.filter(e => !e.isAllDay) }; }, [events, day.date, system]); const parseTime = (timeStr) => { const [h, m] = timeStr.split(':').map(Number); return h * 60 + m; }; const handleDragStart = (e, event) => { const layer = event.layerId ? layersById.get(event.layerId) : null; if ((!layer && event.layerId) || !event.startTime || !event.endTime) return; const ghostLayer = layer || { id: 'ghost', name: 'Ghost', color: '#6B7280', isVisible: true }; const eventRect = e.currentTarget.getBoundingClientRect(); const offsetY = e.clientY - eventRect.top; const duration = Math.max(15, parseTime(event.endTime) - parseTime(event.startTime)); const ghostHeightPercent = (duration / (24 * 60)) * 100; setDragState({ eventId: event.id, offsetY, ghostHeightPercent, layer: ghostLayer, event }); e.dataTransfer.setDragImage(new Image(), 0, 0); e.dataTransfer.setData("text/plain", event.id); e.dataTransfer.effectAllowed = "move"; }; const handleTimedDragOver = (e, columnRef) => { e.preventDefault(); e.dataTransfer.dropEffect = "move"; if (!dragState || !columnRef.current) return; setIsDraggingOver(true); const columnRect = columnRef.current.getBoundingClientRect(); setGhostTopPercent((e.clientY - columnRect.top - dragState.offsetY) / columnRect.height * 100); }; const handleAllDayDragOver = (e) => { e.preventDefault(); e.dataTransfer.dropEffect = "move"; setIsDraggingOver(true); setGhostTopPercent(null); }; const handleDrop = (e, day, isAllDayDrop) => { e.preventDefault(); const eventId = e.dataTransfer.getData("text/plain"); if (!eventId || !onEventMove) return; const newDayTimestamp = dayInGridToDate(day, system).getTime(); if (isAllDayDrop) { onEventMove(eventId, newDayTimestamp); } else if (ghostTopPercent !== null && dragState) { const minutesInDay = 24 * 60; const snappedMinute = Math.round(((ghostTopPercent / 100) * minutesInDay) / 15) * 15; const finalMinute = Math.max(0, Math.min(minutesInDay - 1, snappedMinute)); const newStartTime = `${String(Math.floor(finalMinute / 60)).padStart(2, '0')}:${String(finalMinute % 60).padStart(2, '0')}`; onEventMove(dragState.eventId, newDayTimestamp, newStartTime); } handleDragEnd(); }; const handleDragEnd = () => { setDragState(null); setGhostTopPercent(null); setIsDraggingOver(false); }; // Auto-scroll to 6 AM React.useEffect(() => { if (timelineContainerRef.current) { timelineContainerRef.current.scrollTop = 6 * (hourHeight / 4) * 16; } }, [displayDate, hourHeight]); return (jsxRuntime.jsxs("div", { className: "calendar-day-view", children: [jsxRuntime.jsxs("div", { className: "calendar-day-view-header", children: [jsxRuntime.jsx("div", { className: "calendar-day-view-header-spacer" }), jsxRuntime.jsx("div", { className: "calendar-day-view-header-content", children: jsxRuntime.jsx(DayHeader, { day: day, dayName: dayName, allDayEvents: allDayEvents, schedules: daySchedules, layersById: layersById, onEventClick: onEventClick, onAllDayDrop: (e, d) => handleDrop(e, d, true), onDragOver: handleAllDayDragOver, isDraggingOver: isDraggingOver && ghostTopPercent === null, showTodayIndicator: showTodayIndicator, showHolidayIndicators: showHolidayIndicators, showScheduleIndicators: showScheduleIndicators }) })] }), jsxRuntime.jsx("div", { className: "calendar-day-view-timeline", ref: timelineContainerRef, children: jsxRuntime.jsxs("div", { className: "calendar-day-view-timeline-content", children: [jsxRuntime.jsx(TimeGutter, { hourHeight: hourHeight, hourLabelInterval: hourLabelInterval }), jsxRuntime.jsx("div", { className: "calendar-day-view-column", children: jsxRuntime.jsx(DayColumn, { day: day, timedEvents: timedEvents, schedules: daySchedules, onTimeSlotClick: (date, time) => onEventAdd(date, time), onEventClick: onEventClick, hourHeight: hourHeight, system: system, layersById: layersById, dragState: dragState, ghostTopPercent: ghostTopPercent, isDraggingOver: isDraggingOver, onDragStart: handleDragStart, onDragOver: handleTimedDragOver, onDrop: (e, d) => handleDrop(e, d, false), onDragEnd: handleDragEnd, onEventMove: onEventMove }) })] }) })] })); }; const Calendar = ({ // Data events = [], layers = [], schedules = [], // State view: initialView = 'month', system: initialSystem = 'gregorian', displayDate: initialDisplayDate, // Event handlers onEventClick, onEventAdd, onEventUpdate, onEventDelete, onEventMove, onDateClick, onViewChange, onSystemChange, // Customization theme: customTheme, className = '', style = {}, // Features enableDragAndDrop = true, enableScheduleManagement = true, enableHolidayBooking = true, enableTravelTime = true, enableRecurringEvents = true, // Display options showWeekNumbers = false, showTodayIndicator = true, showHolidayIndicators = true, showScheduleIndicators = true, // Localization locale = 'en', weekStart = 0, }) => { // Merge theme const theme = React.useMemo(() => { return { ...DEFAULT_THEME, ...customTheme, colors: { ...DEFAULT_THEME.colors, ...customTheme?.colors, }, fonts: { ...DEFAULT_THEME.fonts, ...customTheme?.fonts, }, spacing: { ...DEFAULT_THEME.spacing, ...customTheme?.spacing, }, borderRadius: { ...DEFAULT_THEME.borderRadius, ...customTheme?.borderRadius, },