import { computed, ref } from 'vue' export interface CalendarOptions { /** First day of week: 0 = Sunday, 1 = Monday, etc. */ firstDay?: number /** Minimum selectable date */ minDate?: Date | null /** Maximum selectable date */ maxDate?: Date | null /** Locale for formatting (default: 'en-US') */ locale?: string /** Short month names */ monthsShort?: string[] /** Full month names */ months?: string[] /** Short weekday names */ weekdaysShort?: string[] /** Full weekday names */ weekdays?: string[] /** Disable weekend days */ disableWeekends?: boolean /** Dates that have events (highlighted) */ events?: string[] /** Number of months to display side by side */ numberOfMonths?: number /** Right-to-left layout */ isRTL?: boolean /** Year range for the year selector [start, end] */ yearRange?: [number, number] /** Date format string */ format?: string /** Custom date-to-string formatter */ toString?: (date: Date, format: string) => string } export interface CalendarDay { date: Date day: number month: number year: number isToday: boolean isSelected: boolean isDisabled: boolean isOutsideMonth: boolean } const DEFAULT_MONTHS = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December', ] const DEFAULT_MONTHS_SHORT = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] const DEFAULT_WEEKDAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'] const DEFAULT_WEEKDAYS_SHORT = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] export function useCalendar(initialDate: Date | null = null, options: CalendarOptions = {}) { const { firstDay = 0, minDate = null, maxDate = null, months = DEFAULT_MONTHS, monthsShort = DEFAULT_MONTHS_SHORT, weekdays = DEFAULT_WEEKDAYS, weekdaysShort = DEFAULT_WEEKDAYS_SHORT, } = options // The currently selected date const selectedDate = ref(initialDate) // The month/year currently being viewed const viewDate = ref(initialDate ? new Date(initialDate) : new Date()) // Current view month/year const viewMonth = computed(() => viewDate.value.getMonth()) const viewYear = computed(() => viewDate.value.getFullYear()) // Month/year display strings const monthName = computed(() => months[viewMonth.value]) const monthNameShort = computed(() => monthsShort[viewMonth.value]) // Weekday headers adjusted for firstDay (short names for display) const weekdayHeaders = computed(() => { const headers: string[] = [] for (let i = 0; i < 7; i++) { headers.push(weekdaysShort[(i + firstDay) % 7] ?? '') } return headers }) // Full weekday names for accessibility (title attributes, screen readers) const weekdayHeadersFull = computed(() => { const headers: string[] = [] for (let i = 0; i < 7; i++) { headers.push(weekdays[(i + firstDay) % 7] ?? '') } return headers }) // Get days in a month function getDaysInMonth(year: number, month: number): number { return new Date(year, month + 1, 0).getDate() } // Check if two dates are the same day function isSameDay(a: Date | null, b: Date | null): boolean { if (!a || !b) return false return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate() } // Check if a date is today function isToday(date: Date): boolean { return isSameDay(date, new Date()) } // Check if a date is disabled function isDisabled(date: Date): boolean { if (minDate && date < minDate) return true if (maxDate && date > maxDate) return true return false } // Generate calendar days for the current view const calendarDays = computed(() => { const year = viewYear.value const month = viewMonth.value const days: CalendarDay[] = [] // First day of the month const firstOfMonth = new Date(year, month, 1) const startDayOfWeek = firstOfMonth.getDay() // Calculate offset based on firstDay option const offset = (startDayOfWeek - firstDay + 7) % 7 // Days from previous month const prevMonth = month === 0 ? 11 : month - 1 const prevYear = month === 0 ? year - 1 : year const daysInPrevMonth = getDaysInMonth(prevYear, prevMonth) for (let i = offset - 1; i >= 0; i--) { const day = daysInPrevMonth - i const date = new Date(prevYear, prevMonth, day) days.push({ date, day, month: prevMonth, year: prevYear, isToday: isToday(date), isSelected: isSameDay(date, selectedDate.value), isDisabled: isDisabled(date), isOutsideMonth: true, }) } // Days in current month const daysInMonth = getDaysInMonth(year, month) for (let day = 1; day <= daysInMonth; day++) { const date = new Date(year, month, day) days.push({ date, day, month, year, isToday: isToday(date), isSelected: isSameDay(date, selectedDate.value), isDisabled: isDisabled(date), isOutsideMonth: false, }) } // Days from next month to fill the grid (always 6 rows = 42 cells) const nextMonth = month === 11 ? 0 : month + 1 const nextYear = month === 11 ? year + 1 : year const remainingDays = 42 - days.length for (let day = 1; day <= remainingDays; day++) { const date = new Date(nextYear, nextMonth, day) days.push({ date, day, month: nextMonth, year: nextYear, isToday: isToday(date), isSelected: isSameDay(date, selectedDate.value), isDisabled: isDisabled(date), isOutsideMonth: true, }) } return days }) // Navigation function prevMonth() { const d = new Date(viewDate.value) d.setMonth(d.getMonth() - 1) viewDate.value = d } function nextMonth() { const d = new Date(viewDate.value) d.setMonth(d.getMonth() + 1) viewDate.value = d } function goToMonth(month: number) { const d = new Date(viewDate.value) d.setMonth(month) viewDate.value = d } function goToYear(year: number) { const d = new Date(viewDate.value) d.setFullYear(year) viewDate.value = d } function goToDate(date: Date) { viewDate.value = new Date(date) } function goToToday() { viewDate.value = new Date() } // Selection function selectDate(date: Date) { if (isDisabled(date)) return selectedDate.value = new Date(date) } function clearSelection() { selectedDate.value = null } // Format selected date function formatDate(format: string = 'D MMM YYYY'): string { if (!selectedDate.value) return '' const d = selectedDate.value const day = d.getDate() const month = d.getMonth() const year = d.getFullYear() return format .replace('YYYY', String(year)) .replace('YY', String(year).slice(-2)) .replace('MMMM', months[month] ?? '') .replace('MMM', monthsShort[month] ?? '') .replace('MM', String(month + 1).padStart(2, '0')) .replace('M', String(month + 1)) .replace('DD', String(day).padStart(2, '0')) .replace('D', String(day)) } return { // State selectedDate, viewDate, viewMonth, viewYear, // Display monthName, monthNameShort, weekdayHeaders, weekdayHeadersFull, calendarDays, // Navigation prevMonth, nextMonth, goToMonth, goToYear, goToDate, goToToday, // Selection selectDate, clearSelection, // Formatting formatDate, // Utilities isSameDay, isToday, isDisabled, } }