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
JavaScript
'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,
},