export type CalendarDay = { date: Date value: string inMonth: boolean isToday: boolean isSelected: boolean } export type AnchorRect = Pick const DAY_MS = 86_400_000 export function parseLocalDate(value: string): Date | null { const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value) if (!match) return null const year = Number(match[1]) const month = Number(match[2]) - 1 const day = Number(match[3]) const date = new Date(year, month, day) return date.getFullYear() === year && date.getMonth() === month && date.getDate() === day ? date : null } export function formatLocalDate(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}` } export function addCalendarDays(date: Date, amount: number): Date { const next = new Date(date.getFullYear(), date.getMonth(), date.getDate()) next.setDate(next.getDate() + amount) return next } function addCalendarMonths(date: Date, amount: number): Date { const wantedDay = date.getDate() const next = new Date(date.getFullYear(), date.getMonth() + amount, 1) const finalDay = new Date(next.getFullYear(), next.getMonth() + 1, 0).getDate() next.setDate(Math.min(wantedDay, finalDay)) return next } export function moveCalendarFocus(date: Date, key: string): Date { if (key === 'ArrowLeft') return addCalendarDays(date, -1) if (key === 'ArrowRight') return addCalendarDays(date, 1) if (key === 'ArrowUp') return addCalendarDays(date, -7) if (key === 'ArrowDown') return addCalendarDays(date, 7) if (key === 'PageUp') return addCalendarMonths(date, -1) if (key === 'PageDown') return addCalendarMonths(date, 1) const mondayIndex = (date.getDay() + 6) % 7 if (key === 'Home') return addCalendarDays(date, -mondayIndex) if (key === 'End') return addCalendarDays(date, 6 - mondayIndex) return new Date(date) } export function buildCalendarMonth(month: Date, today: Date, selected: Date | null): CalendarDay[] { const first = new Date(month.getFullYear(), month.getMonth(), 1) const start = addCalendarDays(first, -((first.getDay() + 6) % 7)) const todayValue = formatLocalDate(today) const selectedValue = selected ? formatLocalDate(selected) : '' return Array.from({ length: 42 }, (_, index) => { const date = addCalendarDays(start, index) const value = formatLocalDate(date) return { date, value, inMonth: date.getMonth() === month.getMonth(), isToday: value === todayValue, isSelected: value === selectedValue } }) } export function positionCalendarPopover(anchor: AnchorRect, viewportWidth: number, viewportHeight: number, height: number) { const width = 328 const gap = 8 const edge = 12 const left = Math.min(Math.max(anchor.left, edge), viewportWidth - width - edge) const below = anchor.bottom + gap const above = anchor.top - height - gap const maxTop = Math.max(edge, viewportHeight - height - edge) const top = below + height <= viewportHeight - edge ? below : above >= edge ? above : Math.min(Math.max(anchor.bottom - anchor.height + gap, edge), maxTop) return { left, top } } export const calendarDayDistance = (from: Date, to: Date) => Math.round((Date.UTC(to.getFullYear(), to.getMonth(), to.getDate()) - Date.UTC(from.getFullYear(), from.getMonth(), from.getDate())) / DAY_MS)