import { computed, ref, type ComputedRef, type Ref } from 'vue' export interface CalendarGridDay { date: Date isCurrentMonth: boolean isDisabled: boolean } export interface UseCalendarGridOptions { min?: () => string | undefined max?: () => string | undefined locale?: () => string | undefined } export interface UseCalendarGridReturn { currentMonth: Ref weekDays: readonly string[] monthYear: ComputedRef calendarDays: ComputedRef isDateDisabled: (date: Date) => boolean isSameDay: (date: Date, other: Date | null | undefined) => boolean isToday: (date: Date) => boolean formatDateValue: (date: Date) => string setCurrentMonthFromDate: (date: Date) => void prevMonth: () => void nextMonth: () => void } const WEEK_DAYS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'] as const /** Shared calendar grid state for date and datetime pickers. */ export function useCalendarGrid(options: UseCalendarGridOptions = {}): UseCalendarGridReturn { const currentMonth = ref(new Date()) const monthYear = computed(() => { return currentMonth.value.toLocaleDateString(options.locale?.() ?? 'en-US', { year: 'numeric', month: 'long', }) }) const calendarDays = computed(() => { const year = currentMonth.value.getFullYear() const month = currentMonth.value.getMonth() const firstDay = new Date(year, month, 1) const lastDay = new Date(year, month + 1, 0) const days: CalendarGridDay[] = [] const startPadding = firstDay.getDay() for (let i = startPadding - 1; i >= 0; i--) { const date = new Date(year, month, -i) days.push({ date, isCurrentMonth: false, isDisabled: isDateDisabled(date) }) } for (let i = 1; i <= lastDay.getDate(); i++) { const date = new Date(year, month, i) days.push({ date, isCurrentMonth: true, isDisabled: isDateDisabled(date) }) } const endPadding = 42 - days.length for (let i = 1; i <= endPadding; i++) { const date = new Date(year, month + 1, i) days.push({ date, isCurrentMonth: false, isDisabled: isDateDisabled(date) }) } return days }) function isDateDisabled(date: Date): boolean { const minDate = parseBoundaryDate(options.min?.()) if (minDate && startOfDay(date) < startOfDay(minDate)) return true const maxDate = parseBoundaryDate(options.max?.()) if (maxDate && startOfDay(date) > startOfDay(maxDate)) return true return false } function isSameDay(date: Date, other: Date | null | undefined): boolean { if (!other) return false return date.toDateString() === other.toDateString() } function isToday(date: Date): boolean { return isSameDay(date, new Date()) } function formatDateValue(date: Date): string { const year = date.getFullYear() const month = String(date.getMonth() + 1).padStart(2, '0') const day = String(date.getDate()).padStart(2, '0') return `${year}-${month}-${day}` } function setCurrentMonthFromDate(date: Date): void { currentMonth.value = new Date(date.getFullYear(), date.getMonth(), 1) } function prevMonth(): void { currentMonth.value = new Date( currentMonth.value.getFullYear(), currentMonth.value.getMonth() - 1, 1, ) } function nextMonth(): void { currentMonth.value = new Date( currentMonth.value.getFullYear(), currentMonth.value.getMonth() + 1, 1, ) } return { currentMonth, weekDays: WEEK_DAYS, monthYear, calendarDays, isDateDisabled, isSameDay, isToday, formatDateValue, setCurrentMonthFromDate, prevMonth, nextMonth, } } function parseBoundaryDate(value: string | undefined): Date | null { if (!value) return null const date = value.includes('T') ? new Date(value) : new Date(`${value}T00:00:00`) return Number.isNaN(date.getTime()) ? null : date } function startOfDay(date: Date): Date { const copy = new Date(date) copy.setHours(0, 0, 0, 0) return copy }