UNPKG

@crescender/calendar

Version:

A comprehensive TypeScript calendar library with musician-specific capabilities, architected for client/server separation.

1 lines 81.7 kB
{"version":3,"sources":["../../src/client/utils/date-helpers.ts","../../src/client/utils/formatting.ts","../../src/shared/utils.ts","../../src/shared/enums.ts","../../src/client/utils/validation.ts","../../src/client/utils/event-processing.ts","../../src/client/components/EventCard.tsx","../../src/client/components/CalendarView.tsx","../../src/shared/constants.ts"],"sourcesContent":["/**\n * Pure date utility functions - browser-safe\n */\n\n// Note: formatDateAustralian is now exported from shared utils\n\n// Australian time formatting\nexport const formatTimeAustralian = (date: Date | string): string => {\n const d = typeof date === 'string' ? new Date(date) : date;\n return d.toLocaleTimeString('en-AU', {\n hour: '2-digit',\n minute: '2-digit',\n hour12: true\n });\n};\n\n// Australian datetime formatting\nexport const formatDateTimeAustralian = (date: Date | string): string => {\n const d = typeof date === 'string' ? new Date(date) : date;\n return d.toLocaleString('en-AU', {\n day: '2-digit',\n month: '2-digit',\n year: 'numeric',\n hour: '2-digit',\n minute: '2-digit',\n hour12: true\n });\n};\n\n// Calculate days until a date\nexport const daysUntil = (date: Date | string): number => {\n const targetDate = typeof date === 'string' ? new Date(date) : date;\n const now = new Date();\n const diffTime = targetDate.getTime() - now.getTime();\n return Math.ceil(diffTime / (1000 * 60 * 60 * 24));\n};\n\n// Check if date is today\nexport const isToday = (date: Date | string): boolean => {\n const d = typeof date === 'string' ? new Date(date) : date;\n const today = new Date();\n return d.toDateString() === today.toDateString();\n};\n\n// Check if date is in the past\nexport const isPast = (date: Date | string): boolean => {\n const d = typeof date === 'string' ? new Date(date) : date;\n return d < new Date();\n};\n\n// Check if date is in the future\nexport const isFuture = (date: Date | string): boolean => {\n const d = typeof date === 'string' ? new Date(date) : date;\n return d > new Date();\n};\n\n// Check if date is this week\nexport const isThisWeek = (date: Date | string): boolean => {\n const d = typeof date === 'string' ? new Date(date) : date;\n const now = new Date();\n const startOfWeek = new Date(now.setDate(now.getDate() - now.getDay()));\n const endOfWeek = new Date(now.setDate(now.getDate() - now.getDay() + 6));\n return d >= startOfWeek && d <= endOfWeek;\n};\n\n// Check if date is this month\nexport const isThisMonth = (date: Date | string): boolean => {\n const d = typeof date === 'string' ? new Date(date) : date;\n const now = new Date();\n return d.getMonth() === now.getMonth() && d.getFullYear() === now.getFullYear();\n};\n\n// Get start of day\nexport const startOfDay = (date: Date | string): Date => {\n const d = typeof date === 'string' ? new Date(date) : new Date(date);\n d.setHours(0, 0, 0, 0);\n return d;\n};\n\n// Get end of day\nexport const endOfDay = (date: Date | string): Date => {\n const d = typeof date === 'string' ? new Date(date) : new Date(date);\n d.setHours(23, 59, 59, 999);\n return d;\n};\n\n// Add days to date\nexport const addDays = (date: Date | string, days: number): Date => {\n const d = typeof date === 'string' ? new Date(date) : new Date(date);\n d.setDate(d.getDate() + days);\n return d;\n};\n\n// Add months to date\nexport const addMonths = (date: Date | string, months: number): Date => {\n const d = typeof date === 'string' ? new Date(date) : new Date(date);\n d.setMonth(d.getMonth() + months);\n return d;\n};\n\n// Get month name\nexport const getMonthName = (date: Date | string): string => {\n const d = typeof date === 'string' ? new Date(date) : date;\n return d.toLocaleDateString('en-AU', { month: 'long' });\n};\n\n// Get day name\nexport const getDayName = (date: Date | string): string => {\n const d = typeof date === 'string' ? new Date(date) : date;\n return d.toLocaleDateString('en-AU', { weekday: 'long' });\n}; ","/**\n * Australian formatting utilities - browser-safe\n */\n\n// Currency formatting (Australian Dollar)\nexport const formatCurrencyAUD = (amount: number): string => {\n return new Intl.NumberFormat('en-AU', {\n style: 'currency',\n currency: 'AUD',\n minimumFractionDigits: 2\n }).format(amount);\n};\n\n// Phone number formatting (Australian format)\nexport const formatPhoneAustralian = (phone: string): string => {\n // Remove all non-digit characters\n const cleaned = phone.replace(/\\D/g, '');\n \n // Handle different Australian phone number formats\n if (cleaned.length === 10) {\n // Mobile: 0412 345 678\n if (cleaned.startsWith('04')) {\n return cleaned.replace(/(\\d{4})(\\d{3})(\\d{3})/, '$1 $2 $3');\n }\n // Landline: (02) 1234 5678\n else if (cleaned.startsWith('02') || cleaned.startsWith('03') || \n cleaned.startsWith('07') || cleaned.startsWith('08')) {\n return cleaned.replace(/(\\d{2})(\\d{4})(\\d{4})/, '($1) $2 $3');\n }\n }\n \n // Return original if doesn't match expected patterns\n return phone;\n};\n\n// Address formatting (Australian style)\nexport const formatAddressAustralian = (address: {\n street?: string;\n suburb?: string;\n state?: string;\n postcode?: string;\n country?: string;\n}): string => {\n const parts = [];\n \n if (address.street) parts.push(address.street);\n if (address.suburb) parts.push(address.suburb);\n if (address.state && address.postcode) {\n parts.push(`${address.state} ${address.postcode}`);\n } else if (address.state) {\n parts.push(address.state);\n } else if (address.postcode) {\n parts.push(address.postcode);\n }\n if (address.country && address.country !== 'Australia') {\n parts.push(address.country);\n }\n \n return parts.join(', ');\n};\n\n// Percentage formatting\nexport const formatPercentage = (value: number, decimals: number = 1): string => {\n return `${value.toFixed(decimals)}%`;\n};\n\n// Number formatting with Australian locale\nexport const formatNumber = (value: number, decimals: number = 0): string => {\n return new Intl.NumberFormat('en-AU', {\n minimumFractionDigits: decimals,\n maximumFractionDigits: decimals\n }).format(value);\n};\n\n// Duration formatting (minutes to hours:minutes)\nexport const formatDuration = (minutes: number): string => {\n const hours = Math.floor(minutes / 60);\n const mins = minutes % 60;\n \n if (hours === 0) {\n return `${mins} min`;\n } else if (mins === 0) {\n return hours === 1 ? `${hours} hr` : `${hours} hrs`;\n } else {\n const hourText = hours === 1 ? 'hr' : 'hrs';\n return `${hours} ${hourText} ${mins} min`;\n }\n};\n\n// File size formatting\nexport const formatFileSize = (bytes: number): string => {\n const sizes = ['Bytes', 'KB', 'MB', 'GB'];\n if (bytes === 0) return '0 Bytes';\n \n const i = Math.floor(Math.log(bytes) / Math.log(1024));\n return `${Math.round(bytes / Math.pow(1024, i) * 100) / 100} ${sizes[i]}`;\n};\n\n// Capitalize first letter of each word\nexport const capitalizeWords = (text: string): string => {\n return text.replace(/\\w\\S*/g, (txt) => \n txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase()\n );\n};\n\n// Truncate text with ellipsis\nexport const truncateText = (text: string, maxLength: number): string => {\n if (text.length <= maxLength) return text;\n return text.substr(0, maxLength - 3) + '...';\n};\n\n// Format initials from name\nexport const getInitials = (name: string): string => {\n return name\n .split(' ')\n .map(word => word.charAt(0).toUpperCase())\n .join('')\n .substr(0, 2);\n}; ","/**\n * @file Shared utility functions for the calendar library.\n * These utilities are safe to use in both client and server environments.\n */\n\n/**\n * Formats a date in Australian format (dd/mmm/yyyy).\n * Uses UTC date to avoid timezone issues in tests.\n */\nexport function formatDateAustralian(date: Date): string {\n const months = [\n 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',\n 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'\n ];\n \n const day = date.getUTCDate().toString().padStart(2, '0');\n const month = months[date.getUTCMonth()];\n const year = date.getUTCFullYear();\n \n return `${day}/${month}/${year}`;\n}\n\n/**\n * Calculates the duration between two dates in minutes.\n */\nexport function getDurationMinutes(start: Date, end: Date): number {\n return Math.round((end.getTime() - start.getTime()) / (1000 * 60));\n}\n\n/**\n * Checks if two dates are on the same day.\n */\nexport function isSameDay(date1: Date, date2: Date): boolean {\n return date1.toDateString() === date2.toDateString();\n}\n\n/**\n * Gets the start of week (Monday) for a given date.\n */\nexport function getStartOfWeek(date: Date): Date {\n const result = new Date(date);\n const day = result.getDay();\n const diff = result.getDate() - day + (day === 0 ? -6 : 1); // Adjust when day is Sunday\n result.setDate(diff);\n result.setHours(0, 0, 0, 0);\n return result;\n}\n\n/**\n * Gets the end of week (Sunday) for a given date.\n */\nexport function getEndOfWeek(date: Date): Date {\n const result = new Date(date);\n const day = result.getDay();\n const diff = result.getDate() - day + (day === 0 ? 0 : 7); // Adjust when day is Sunday\n result.setDate(diff);\n result.setHours(23, 59, 59, 999);\n return result;\n}\n\n/**\n * Validates an email address.\n */\nexport function isValidEmail(email: string): boolean {\n const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n return emailRegex.test(email);\n}\n\n/**\n * Validates a phone number (basic validation).\n */\nexport function isValidPhone(phone: string): boolean {\n // Basic validation for Australian phone numbers\n const phoneRegex = /^(\\+61|0)[2-9]\\d{8}$/;\n return phoneRegex.test(phone.replace(/\\s/g, ''));\n}\n\n/**\n * Generates a unique ID (simple implementation for client-side).\n * Note: For production, use a proper UUID library on the server.\n */\nexport function generateTempId(): string {\n return `temp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;\n} ","/**\n * @file Shared enums for the calendar library.\n * These enums are safe to use in both client and server environments.\n */\n\n/**\n * Event types for musician-specific functionality.\n */\nexport const EVENT_TYPES = {\n GIG: 'gig',\n LESSON: 'lesson',\n AUDITION: 'audition',\n PRACTICE: 'practice',\n REHEARSAL: 'rehearsal',\n RECORDING: 'recording',\n MEETING: 'meeting'\n} as const;\n\nexport type EventType = typeof EVENT_TYPES[keyof typeof EVENT_TYPES];\n\n/**\n * Payment status options.\n */\nexport const PAYMENT_STATUS = {\n PENDING: 'Pending',\n PAID: 'Paid',\n OVERDUE: 'Overdue',\n CANCELLED: 'Cancelled'\n} as const;\n\nexport type PaymentStatus = typeof PAYMENT_STATUS[keyof typeof PAYMENT_STATUS];\n\n/**\n * Event status options.\n */\nexport const EVENT_STATUS = {\n CONFIRMED: 'Confirmed',\n TENTATIVE: 'Tentative',\n CANCELLED: 'Cancelled',\n COMPLETED: 'Completed'\n} as const;\n\nexport type EventStatus = typeof EVENT_STATUS[keyof typeof EVENT_STATUS];\n\n/**\n * Calendar types.\n */\nexport const CALENDAR_TYPES = {\n INDIVIDUAL: 'individual',\n GROUP: 'group',\n SHARED: 'shared'\n} as const;\n\nexport type CalendarType = typeof CALENDAR_TYPES[keyof typeof CALENDAR_TYPES];\n\n/**\n * Student levels for lessons.\n */\nexport const STUDENT_LEVELS = {\n BEGINNER: 'Beginner',\n INTERMEDIATE: 'Intermediate',\n ADVANCED: 'Advanced',\n PROFESSIONAL: 'Professional'\n} as const;\n\nexport type StudentLevel = typeof STUDENT_LEVELS[keyof typeof STUDENT_LEVELS];\n\n/**\n * Difficulty levels for pieces/repertoire.\n */\nexport const DIFFICULTY_LEVELS = {\n EASY: 'Easy',\n MEDIUM: 'Medium',\n HARD: 'Hard',\n EXPERT: 'Expert'\n} as const;\n\nexport type DifficultyLevel = typeof DIFFICULTY_LEVELS[keyof typeof DIFFICULTY_LEVELS];\n\n/**\n * Common musical genres.\n */\nexport const GENRES = {\n CLASSICAL: 'Classical',\n JAZZ: 'Jazz',\n ROCK: 'Rock',\n POP: 'Pop',\n BLUES: 'Blues',\n COUNTRY: 'Country',\n FOLK: 'Folk',\n ELECTRONIC: 'Electronic',\n WORLD: 'World Music',\n OTHER: 'Other'\n} as const;\n\nexport type Genre = typeof GENRES[keyof typeof GENRES];\n\n/**\n * Common instruments.\n */\nexport const INSTRUMENTS = {\n PIANO: 'Piano',\n GUITAR: 'Guitar',\n VIOLIN: 'Violin',\n DRUMS: 'Drums',\n BASS: 'Bass',\n SAXOPHONE: 'Saxophone',\n TRUMPET: 'Trumpet',\n FLUTE: 'Flute',\n CELLO: 'Cello',\n VOICE: 'Voice',\n OTHER: 'Other'\n} as const;\n\nexport type Instrument = typeof INSTRUMENTS[keyof typeof INSTRUMENTS]; ","/**\n * @file Client-side validation for the calendar library.\n * These validation functions are safe for browser environments.\n */\n\nimport { isValidEmail, isValidPhone } from '../../shared/utils';\nimport type { EventFormData, CreateEventData } from '../types/events';\nimport type { CalendarFormData } from '../types/calendar';\nimport { EVENT_TYPES, PAYMENT_STATUS, EVENT_STATUS } from '../../shared/enums';\n\nexport interface ValidationResult {\n isValid: boolean;\n errors: Record<string, string[]>;\n}\n\n// Income/Expense form data types for validation\nexport interface IncomeFormData {\n description: string;\n amount: number;\n currency: string;\n notes?: string;\n}\n\nexport interface ExpenseFormData {\n description: string;\n amount: number;\n currency: string;\n notes?: string;\n receipt?: string;\n}\n\n/**\n * Validates event form data.\n */\nexport function validateEvent(data: Partial<EventFormData>): ValidationResult {\n const errors: Record<string, string[]> = {};\n\n // Required fields\n if (!data.title || data.title.trim().length === 0) {\n errors.title = ['Event title is required'];\n } else if (data.title.length > 200) {\n errors.title = ['Event title must not exceed 200 characters'];\n }\n\n if (!data.startDate) {\n errors.startDate = ['Start date is required'];\n }\n\n if (!data.startTime) {\n errors.startTime = ['Start time is required'];\n }\n\n if (data.startDate && data.endDate && data.startTime && data.endTime) {\n const start = new Date(`${data.startDate}T${data.startTime}`);\n const end = new Date(`${data.endDate || data.startDate}T${data.endTime || data.startTime}`);\n \n if (start >= end) {\n errors.endTime = ['End time must be after start time'];\n }\n \n // Check for reasonable duration (not more than 24 hours for most events)\n const durationHours = (end.getTime() - start.getTime()) / (1000 * 60 * 60);\n if (durationHours > 24) {\n errors.endTime = ['Event duration cannot exceed 24 hours'];\n }\n }\n\n if (!data.eventType) {\n errors.eventType = ['Event type is required'];\n } else if (!Object.values(EVENT_TYPES).includes(data.eventType as any)) {\n errors.eventType = ['Invalid event type'];\n }\n\n if (!data.calendarId) {\n errors.calendarId = ['Calendar selection is required'];\n }\n\n // Optional field validations\n if (data.description && data.description.length > 1000) {\n errors.description = ['Description must not exceed 1000 characters'];\n }\n\n if (data.status && !Object.values(EVENT_STATUS).includes(data.status as any)) {\n errors.status = ['Invalid event status'];\n }\n\n if (data.notes && data.notes.length > 1000) {\n errors.notes = ['Notes must not exceed 1000 characters'];\n }\n\n // Recurrence validations\n if (data.isRecurring && !data.recurrenceRule) {\n errors.recurrenceRule = ['Recurrence rule is required for recurring events'];\n }\n\n if (data.recurrenceRule && !data.isRecurring) {\n errors.isRecurring = ['Recurring flag must be set when recurrence rule is provided'];\n }\n\n if (data.recurrenceEndDate && data.startDate) {\n const recurrenceEnd = new Date(data.recurrenceEndDate);\n const eventStart = new Date(data.startDate);\n if (recurrenceEnd <= eventStart) {\n errors.recurrenceEndDate = ['Recurrence end date must be after event start date'];\n }\n }\n\n if (data.maxOccurrences !== undefined && data.maxOccurrences < 1) {\n errors.maxOccurrences = ['Maximum occurrences must be at least 1'];\n }\n\n return {\n isValid: Object.keys(errors).length === 0,\n errors\n };\n}\n\n/**\n * Validates income form data.\n */\nexport function validateIncome(data: Partial<IncomeFormData>): ValidationResult {\n const errors: Record<string, string[]> = {};\n\n if (!data.description || data.description.trim().length === 0) {\n errors.description = ['Income description is required'];\n } else if (data.description.length > 200) {\n errors.description = ['Description must not exceed 200 characters'];\n }\n\n if (data.amount === undefined || data.amount === null) {\n errors.amount = ['Amount is required'];\n } else if (data.amount < 0) {\n errors.amount = ['Amount must be positive'];\n } else if (data.amount > 999999.99) {\n errors.amount = ['Amount must not exceed $999,999.99'];\n }\n\n if (!data.currency || data.currency.trim().length === 0) {\n errors.currency = ['Currency is required'];\n } else if (!/^[A-Z]{3}$/.test(data.currency)) {\n errors.currency = ['Currency must be a valid 3-letter code (e.g., AUD, USD)'];\n }\n\n if (data.notes && data.notes.length > 500) {\n errors.notes = ['Notes must not exceed 500 characters'];\n }\n\n return {\n isValid: Object.keys(errors).length === 0,\n errors\n };\n}\n\n/**\n * Validates expense form data.\n */\nexport function validateExpense(data: Partial<ExpenseFormData>): ValidationResult {\n const errors: Record<string, string[]> = {};\n\n if (!data.description || data.description.trim().length === 0) {\n errors.description = ['Expense description is required'];\n } else if (data.description.length > 200) {\n errors.description = ['Description must not exceed 200 characters'];\n }\n\n if (data.amount === undefined || data.amount === null) {\n errors.amount = ['Amount is required'];\n } else if (data.amount < 0) {\n errors.amount = ['Amount must be positive'];\n } else if (data.amount > 999999.99) {\n errors.amount = ['Amount must not exceed $999,999.99'];\n }\n\n if (!data.currency || data.currency.trim().length === 0) {\n errors.currency = ['Currency is required'];\n } else if (!/^[A-Z]{3}$/.test(data.currency)) {\n errors.currency = ['Currency must be a valid 3-letter code (e.g., AUD, USD)'];\n }\n\n if (data.notes && data.notes.length > 500) {\n errors.notes = ['Notes must not exceed 500 characters'];\n }\n\n // Receipt validation (if provided)\n if (data.receipt && data.receipt.length > 500) {\n errors.receipt = ['Receipt path must not exceed 500 characters'];\n }\n\n return {\n isValid: Object.keys(errors).length === 0,\n errors\n };\n}\n\n/**\n * Validates calendar form data.\n */\nexport function validateCalendar(data: Partial<CalendarFormData>): ValidationResult {\n const errors: Record<string, string[]> = {};\n\n if (!data.name || data.name.trim().length === 0) {\n errors.name = ['Calendar name is required'];\n } else if (data.name.length > 100) {\n errors.name = ['Calendar name must not exceed 100 characters'];\n }\n\n if (data.description && data.description.length > 500) {\n errors.description = ['Description must not exceed 500 characters'];\n }\n\n if (data.color && !/^#[0-9A-F]{6}$/i.test(data.color)) {\n errors.color = ['Color must be a valid hex color code'];\n }\n\n if (data.timeZone && data.timeZone.length > 50) {\n errors.timeZone = ['Time zone must not exceed 50 characters'];\n }\n\n return {\n isValid: Object.keys(errors).length === 0,\n errors\n };\n}\n\n/**\n * Validates venue form data.\n */\nexport function validateVenue(data: {\n name?: string;\n address?: string;\n city?: string;\n state?: string;\n country?: string;\n website?: string;\n contactEmail?: string;\n contactPhone?: string;\n notes?: string;\n}): ValidationResult {\n const errors: Record<string, string[]> = {};\n\n if (!data.name || data.name.trim().length === 0) {\n errors.name = ['Venue name is required'];\n } else if (data.name.length > 200) {\n errors.name = ['Venue name must not exceed 200 characters'];\n }\n\n if (data.address && data.address.length > 300) {\n errors.address = ['Address must not exceed 300 characters'];\n }\n\n if (data.city && data.city.length > 100) {\n errors.city = ['City must not exceed 100 characters'];\n }\n\n if (data.state && data.state.length > 100) {\n errors.state = ['State must not exceed 100 characters'];\n }\n\n if (data.country && data.country.length > 100) {\n errors.country = ['Country must not exceed 100 characters'];\n }\n\n if (data.website && !isValidWebsite(data.website)) {\n errors.website = ['Please enter a valid website URL'];\n }\n\n if (data.contactEmail && !isValidEmail(data.contactEmail)) {\n errors.contactEmail = ['Please enter a valid email address'];\n }\n\n if (data.contactPhone && !isValidPhone(data.contactPhone)) {\n errors.contactPhone = ['Please enter a valid phone number'];\n }\n\n if (data.notes && data.notes.length > 1000) {\n errors.notes = ['Notes must not exceed 1000 characters'];\n }\n\n return {\n isValid: Object.keys(errors).length === 0,\n errors\n };\n}\n\n/**\n * Validates contact form data.\n */\nexport function validateContact(data: {\n name?: string;\n email?: string;\n phone?: string;\n role?: string;\n notes?: string;\n}): ValidationResult {\n const errors: Record<string, string[]> = {};\n\n if (!data.name || data.name.trim().length === 0) {\n errors.name = ['Contact name is required'];\n } else if (data.name.length > 200) {\n errors.name = ['Contact name must not exceed 200 characters'];\n }\n\n if (data.email && !isValidEmail(data.email)) {\n errors.email = ['Please enter a valid email address'];\n }\n\n if (data.phone && !isValidPhone(data.phone)) {\n errors.phone = ['Please enter a valid phone number'];\n }\n\n if (data.role && data.role.length > 100) {\n errors.role = ['Role must not exceed 100 characters'];\n }\n\n if (data.notes && data.notes.length > 1000) {\n errors.notes = ['Notes must not exceed 1000 characters'];\n }\n\n return {\n isValid: Object.keys(errors).length === 0,\n errors\n };\n}\n\n/**\n * Validates recurrence rule format.\n */\nexport function validateRecurrenceRule(rule: string): ValidationResult {\n const errors: Record<string, string[]> = {};\n\n if (!rule || rule.trim().length === 0) {\n errors.rule = ['Recurrence rule is required'];\n return { isValid: false, errors };\n }\n\n // Basic RRULE format validation\n if (!rule.startsWith('RRULE:')) {\n errors.rule = ['Recurrence rule must start with \"RRULE:\"'];\n }\n\n // Check for required FREQ parameter\n if (!rule.includes('FREQ=')) {\n errors.rule = ['Recurrence rule must include FREQ parameter'];\n }\n\n return {\n isValid: Object.keys(errors).length === 0,\n errors\n };\n}\n\n// Helper function for website validation\nfunction isValidWebsite(url: string): boolean {\n try {\n const urlObj = new URL(url);\n return urlObj.protocol === 'http:' || urlObj.protocol === 'https:';\n } catch {\n return false;\n }\n} ","/**\n * @file Client-side event processing utilities.\n * These utilities are safe for browser environments and work with rrule for recurrence.\n */\n\nimport { RRule } from 'rrule';\nimport type { IEvent } from '../../shared/types';\nimport type { ClientEvent, EventFilters, EventsByDate, EventFinancialSummary } from '../types/events';\nimport { formatDateAustralian } from '../../shared/utils';\nimport { formatTimeAustralian, daysUntil, isPast, isFuture } from './date-helpers';\nimport { formatCurrencyAUD } from './formatting';\n\n/**\n * Expands a recurring event into multiple occurrences within a date range.\n */\nexport function expandRecurrence(\n event: IEvent,\n startRange: Date,\n endRange: Date,\n maxOccurrences: number = 100\n): IEvent[] {\n if (!event.recurrenceRule) {\n // Non-recurring event - return single occurrence if it falls within range\n if (event.start >= startRange && event.start <= endRange) {\n return [event];\n }\n return [];\n }\n\n try {\n // Create RRule with the event's start date as DTSTART\n const rule = new RRule({\n ...RRule.parseString(event.recurrenceRule),\n dtstart: event.start\n });\n \n // Get occurrences within the date range\n const occurrences = rule.between(startRange, endRange, true);\n \n // Limit the number of occurrences\n const limitedOccurrences = occurrences.slice(0, maxOccurrences);\n \n // Create event instances for each occurrence\n return limitedOccurrences.map((occurrenceStart, index) => {\n const duration = event.end.getTime() - event.start.getTime();\n const occurrenceEnd = new Date(occurrenceStart.getTime() + duration);\n \n return {\n ...event,\n id: `${event.id}_${index}`,\n start: occurrenceStart,\n end: occurrenceEnd\n };\n });\n } catch (error) {\n console.warn('Failed to parse recurrence rule:', event.recurrenceRule, error);\n return [event]; // Return original event if parsing fails\n }\n}\n\n/**\n * Enhances a client event with computed properties.\n */\nexport function enhanceClientEvent(event: ClientEvent): ClientEvent {\n const enhanced = { ...event };\n\n // Calculate duration\n enhanced.duration = Math.round((event.end.getTime() - event.start.getTime()) / (1000 * 60));\n\n // Calculate financial totals\n if (event.income) {\n enhanced.totalIncome = event.income.reduce((sum: number, income: any) => sum + income.amount, 0);\n }\n \n if (event.expenses) {\n enhanced.totalExpenses = event.expenses.reduce((sum: number, expense: any) => sum + expense.amount, 0);\n }\n\n // Calculate profit\n const totalIncome = enhanced.totalIncome || 0;\n const totalExpenses = enhanced.totalExpenses || 0;\n enhanced.profit = totalIncome - totalExpenses;\n enhanced.netProfit = enhanced.profit;\n\n // Add formatted dates and times\n enhanced.displayDate = formatDateAustralian(event.start);\n enhanced.formattedDate = formatDateAustralian(event.start);\n enhanced.formattedTime = formatTimeAustralian(event.start);\n\n // Add formatted duration\n const hours = Math.floor(enhanced.duration / 60);\n const minutes = enhanced.duration % 60;\n if (hours === 0) {\n enhanced.formattedDuration = `${minutes} min${minutes !== 1 ? 's' : ''}`;\n } else if (minutes === 0) {\n enhanced.formattedDuration = `${hours} hr${hours !== 1 ? 's' : ''}`;\n } else {\n enhanced.formattedDuration = `${hours} hr${hours !== 1 ? 's' : ''} ${minutes} min`;\n }\n\n // Add temporal properties\n enhanced.isPast = isPast(event.start);\n enhanced.isUpcoming = isFuture(event.start);\n enhanced.daysUntil = daysUntil(event.start);\n\n return enhanced;\n}\n\n/**\n * Filters events based on provided criteria.\n */\nexport function filterEvents(events: ClientEvent[], filters: EventFilters): ClientEvent[] {\n return events.filter(event => {\n // Filter by event type\n if (filters.eventType && filters.eventType.length > 0) {\n if (!filters.eventType.includes(event.type)) {\n return false;\n }\n }\n\n // Filter by status\n if (filters.status && filters.status.length > 0) {\n if (!event.status || !filters.status.includes(event.status)) {\n return false;\n }\n }\n\n // Filter by calendar ID\n if (filters.calendarId && filters.calendarId.length > 0) {\n if (!filters.calendarId.includes(event.calendarId)) {\n return false;\n }\n }\n\n // Filter by venue ID\n if (filters.venueId && filters.venueId.length > 0) {\n if (!event.venueId || !filters.venueId.includes(event.venueId)) {\n return false;\n }\n }\n\n // Filter by date range\n if (filters.dateRange) {\n if (event.start < filters.dateRange.start || event.start > filters.dateRange.end) {\n return false;\n }\n }\n\n // Filter by search query\n if (filters.searchQuery) {\n const query = filters.searchQuery.toLowerCase();\n const searchableText = [\n event.summary,\n event.description,\n event.genre,\n event.instrument\n ].filter(Boolean).join(' ').toLowerCase();\n \n if (!searchableText.includes(query)) {\n return false;\n }\n }\n\n // Filter by upcoming/past\n if (filters.isUpcoming !== undefined) {\n const isUpcoming = isFuture(event.start);\n if (filters.isUpcoming !== isUpcoming) {\n return false;\n }\n }\n\n if (filters.isPast !== undefined) {\n const isPastEvent = isPast(event.start);\n if (filters.isPast !== isPastEvent) {\n return false;\n }\n }\n\n return true;\n });\n}\n\n/**\n * Sorts events based on criteria.\n */\nexport type EventSortOptions = {\n field: 'start' | 'end' | 'summary' | 'type' | 'createdAt';\n direction: 'asc' | 'desc';\n};\n\nexport function sortEvents(events: ClientEvent[], sortOptions: EventSortOptions): ClientEvent[] {\n return [...events].sort((a, b) => {\n let aValue: any;\n let bValue: any;\n\n switch (sortOptions.field) {\n case 'start':\n aValue = a.start.getTime();\n bValue = b.start.getTime();\n break;\n case 'end':\n aValue = a.end.getTime();\n bValue = b.end.getTime();\n break;\n case 'summary':\n aValue = a.summary.toLowerCase();\n bValue = b.summary.toLowerCase();\n break;\n case 'type':\n aValue = a.type.toLowerCase();\n bValue = b.type.toLowerCase();\n break;\n case 'createdAt':\n aValue = a.createdAt.getTime();\n bValue = b.createdAt.getTime();\n break;\n default:\n return 0;\n }\n\n if (aValue < bValue) {\n return sortOptions.direction === 'asc' ? -1 : 1;\n }\n if (aValue > bValue) {\n return sortOptions.direction === 'asc' ? 1 : -1;\n }\n return 0;\n });\n}\n\n/**\n * Groups events by date.\n */\nexport function groupEventsByDate(events: ClientEvent[]): EventsByDate {\n return events.reduce((groups, event) => {\n const dateKey = formatDateAustralian(event.start);\n if (!groups[dateKey]) {\n groups[dateKey] = [];\n }\n groups[dateKey].push(event);\n return groups;\n }, {} as EventsByDate);\n}\n\n/**\n * Gets upcoming events (future events).\n */\nexport function getUpcomingEvents(events: ClientEvent[]): ClientEvent[] {\n const now = new Date();\n return events\n .filter(event => event.start > now)\n .sort((a, b) => a.start.getTime() - b.start.getTime());\n}\n\n/**\n * Gets today's events.\n */\nexport function getTodaysEvents(events: ClientEvent[]): ClientEvent[] {\n const today = new Date();\n const startOfDay = new Date(today.getFullYear(), today.getMonth(), today.getDate());\n const endOfDay = new Date(today.getFullYear(), today.getMonth(), today.getDate(), 23, 59, 59, 999);\n\n return events.filter(event => event.start >= startOfDay && event.start <= endOfDay);\n}\n\n/**\n * Calculates financial summary for a set of events.\n */\nexport function calculateFinancialSummary(events: ClientEvent[]): EventFinancialSummary {\n let totalIncome = 0;\n let totalExpenses = 0;\n let eventCount = 0;\n\n events.forEach(event => {\n if (event.income) {\n totalIncome += event.income.reduce((sum: number, income: any) => sum + income.amount, 0);\n }\n if (event.expenses) {\n totalExpenses += event.expenses.reduce((sum: number, expense: any) => sum + expense.amount, 0);\n }\n eventCount++;\n });\n\n const netProfit = totalIncome - totalExpenses;\n const averageProfit = eventCount > 0 ? netProfit / eventCount : 0;\n\n return {\n totalIncome,\n totalExpenses,\n netProfit,\n eventCount,\n averageProfit\n };\n} ","import React from 'react';\nimport type { ClientEvent } from '../types/events';\nimport { formatDateAustralian } from '../../shared/utils';\nimport { formatTimeAustralian } from '../utils/date-helpers';\nimport { formatCurrencyAUD } from '../utils/formatting';\n\nexport interface EventCardProps {\n event: ClientEvent;\n onClick?: (event: ClientEvent) => void;\n onEdit?: (event: ClientEvent) => void;\n onDelete?: (event: ClientEvent) => void;\n showFinancials?: boolean;\n className?: string;\n}\n\nexport const EventCard: React.FC<EventCardProps> = ({\n event,\n onClick,\n onEdit,\n onDelete,\n showFinancials = false,\n className = ''\n}) => {\n const handleClick = () => {\n onClick?.(event);\n };\n\n const handleEdit = (e: React.MouseEvent) => {\n e.stopPropagation();\n onEdit?.(event);\n };\n\n const handleDelete = (e: React.MouseEvent) => {\n e.stopPropagation();\n onDelete?.(event);\n };\n\n return (\n <div\n className={`event-card ${className}`}\n onClick={handleClick}\n style={{\n border: '1px solid #e0e0e0',\n borderRadius: '8px',\n padding: '16px',\n margin: '8px 0',\n backgroundColor: '#fff',\n cursor: onClick ? 'pointer' : 'default',\n boxShadow: '0 2px 4px rgba(0,0,0,0.1)',\n transition: 'box-shadow 0.2s ease'\n }}\n onMouseEnter={(e) => {\n if (onClick) {\n e.currentTarget.style.boxShadow = '0 4px 8px rgba(0,0,0,0.15)';\n }\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.boxShadow = '0 2px 4px rgba(0,0,0,0.1)';\n }}\n >\n <div className=\"event-card-header\" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>\n <div className=\"event-info\" style={{ flex: 1 }}>\n <h3 style={{ margin: '0 0 8px 0', fontSize: '18px', fontWeight: '600' }}>\n {event.summary}\n </h3>\n <div className=\"event-meta\" style={{ fontSize: '14px', color: '#666', marginBottom: '8px' }}>\n <div>\n {event.formattedDate || formatDateAustralian(event.start)} \n {event.formattedTime && ` at ${event.formattedTime}`}\n </div>\n <div style={{ marginTop: '4px' }}>\n <span className=\"event-type\" style={{ \n backgroundColor: '#f0f0f0', \n padding: '2px 6px', \n borderRadius: '4px', \n fontSize: '12px' \n }}>\n {event.type}\n </span>\n {event.status && (\n <span className=\"event-status\" style={{ \n backgroundColor: getStatusColor(event.status), \n color: '#fff',\n padding: '2px 6px', \n borderRadius: '4px', \n fontSize: '12px',\n marginLeft: '8px'\n }}>\n {event.status}\n </span>\n )}\n </div>\n </div>\n {event.description && (\n <p style={{ margin: '8px 0', fontSize: '14px', color: '#555' }}>\n {event.description.length > 100 \n ? `${event.description.substring(0, 100)}...` \n : event.description\n }\n </p>\n )}\n {event.venueDetails && (\n <div style={{ fontSize: '14px', color: '#666', marginTop: '8px' }}>\n 📍 {event.venueDetails.name}\n </div>\n )}\n </div>\n \n {(onEdit || onDelete) && (\n <div className=\"event-actions\" style={{ display: 'flex', gap: '8px' }}>\n {onEdit && (\n <button\n onClick={handleEdit}\n style={{\n background: 'none',\n border: '1px solid #ddd',\n borderRadius: '4px',\n padding: '4px 8px',\n cursor: 'pointer',\n fontSize: '12px'\n }}\n >\n Edit\n </button>\n )}\n {onDelete && (\n <button\n onClick={handleDelete}\n style={{\n background: 'none',\n border: '1px solid #ff4444',\n borderRadius: '4px',\n padding: '4px 8px',\n cursor: 'pointer',\n fontSize: '12px',\n color: '#ff4444'\n }}\n >\n Delete\n </button>\n )}\n </div>\n )}\n </div>\n\n {showFinancials && (event.totalIncome || event.totalExpenses) && (\n <div className=\"event-financials\" style={{ \n marginTop: '12px', \n padding: '12px', \n backgroundColor: '#f8f9fa', \n borderRadius: '4px',\n fontSize: '14px'\n }}>\n <div style={{ display: 'flex', justifyContent: 'space-between' }}>\n {event.totalIncome && (\n <span style={{ color: '#28a745' }}>\n Income: {formatCurrencyAUD(event.totalIncome)}\n </span>\n )}\n {event.totalExpenses && (\n <span style={{ color: '#dc3545' }}>\n Expenses: {formatCurrencyAUD(event.totalExpenses)}\n </span>\n )}\n </div>\n {event.netProfit !== undefined && (\n <div style={{ \n marginTop: '4px', \n fontWeight: '600',\n color: event.netProfit >= 0 ? '#28a745' : '#dc3545'\n }}>\n Net: {formatCurrencyAUD(event.netProfit)}\n </div>\n )}\n </div>\n )}\n\n {event.isUpcoming && event.daysUntil !== undefined && (\n <div style={{ \n marginTop: '8px', \n fontSize: '12px', \n color: '#007bff',\n fontWeight: '500'\n }}>\n {event.daysUntil === 0 ? 'Today' : \n event.daysUntil === 1 ? 'Tomorrow' : \n `In ${event.daysUntil} days`}\n </div>\n )}\n </div>\n );\n};\n\n// Helper function to get status color\nfunction getStatusColor(status: string | undefined): string {\n if (!status) return '#6c757d';\n \n switch (status.toLowerCase()) {\n case 'confirmed':\n return '#28a745';\n case 'pending':\n return '#ffc107';\n case 'cancelled':\n return '#dc3545';\n case 'completed':\n return '#17a2b8';\n default:\n return '#6c757d';\n }\n} ","import React, { useState } from 'react';\nimport type { ClientEvent } from '../types/events';\nimport type { ClientCalendar } from '../types/calendar';\nimport { EventCard } from './EventCard';\nimport { formatDateAustralian } from '../../shared/utils';\nimport { isToday, isThisWeek, isThisMonth } from '../utils/date-helpers';\n\nexport interface CalendarViewProps {\n calendar: ClientCalendar;\n events: ClientEvent[];\n view?: 'month' | 'week' | 'day' | 'list';\n onEventClick?: (event: ClientEvent) => void;\n onEventEdit?: (event: ClientEvent) => void;\n onEventDelete?: (event: ClientEvent) => void;\n onViewChange?: (view: 'month' | 'week' | 'day' | 'list') => void;\n showFinancials?: boolean;\n className?: string;\n}\n\nexport const CalendarView: React.FC<CalendarViewProps> = ({\n calendar,\n events,\n view = 'list',\n onEventClick,\n onEventEdit,\n onEventDelete,\n onViewChange,\n showFinancials = false,\n className = ''\n}) => {\n const [currentDate, setCurrentDate] = useState(new Date());\n\n // Filter events based on current view\n const getFilteredEvents = () => {\n const now = new Date();\n \n switch (view) {\n case 'day':\n return events.filter(event => isToday(event.start));\n case 'week':\n return events.filter(event => isThisWeek(event.start));\n case 'month':\n return events.filter(event => isThisMonth(event.start));\n case 'list':\n default:\n return events.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime());\n }\n };\n\n const filteredEvents = getFilteredEvents();\n\n const renderViewSelector = () => (\n <div style={{ display: 'flex', gap: '8px', marginBottom: '16px' }}>\n {(['day', 'week', 'month', 'list'] as const).map((viewOption) => (\n <button\n key={viewOption}\n onClick={() => onViewChange?.(viewOption)}\n style={{\n padding: '8px 16px',\n border: '1px solid #ddd',\n borderRadius: '4px',\n backgroundColor: view === viewOption ? '#007bff' : '#fff',\n color: view === viewOption ? '#fff' : '#333',\n cursor: 'pointer',\n textTransform: 'capitalize'\n }}\n >\n {viewOption}\n </button>\n ))}\n </div>\n );\n\n const renderCalendarHeader = () => (\n <div style={{ \n marginBottom: '20px', \n padding: '16px', \n backgroundColor: '#f8f9fa', \n borderRadius: '8px' \n }}>\n <h2 style={{ margin: '0 0 8px 0', fontSize: '24px', fontWeight: '600' }}>\n {calendar.name}\n </h2>\n {calendar.description && (\n <p style={{ margin: '0 0 12px 0', color: '#666' }}>\n {calendar.description}\n </p>\n )}\n <div style={{ display: 'flex', gap: '20px', fontSize: '14px', color: '#555' }}>\n <span>📅 {filteredEvents.length} events</span>\n {calendar.upcomingEventCount !== undefined && (\n <span>⏰ {calendar.upcomingEventCount} upcoming</span>\n )}\n {showFinancials && calendar.netProfit !== undefined && (\n <span style={{ \n color: calendar.netProfit >= 0 ? '#28a745' : '#dc3545',\n fontWeight: '600'\n }}>\n 💰 Net: ${calendar.netProfit.toFixed(2)}\n </span>\n )}\n </div>\n </div>\n );\n\n const renderListView = () => (\n <div className=\"calendar-list-view\">\n {filteredEvents.length === 0 ? (\n <div style={{ \n textAlign: 'center', \n padding: '40px', \n color: '#666',\n backgroundColor: '#f8f9fa',\n borderRadius: '8px'\n }}>\n <p>No events found for this view.</p>\n </div>\n ) : (\n <div>\n {filteredEvents.map((event) => (\n <EventCard\n key={event.id}\n event={event}\n onClick={onEventClick}\n onEdit={onEventEdit}\n onDelete={onEventDelete}\n showFinancials={showFinancials}\n />\n ))}\n </div>\n )}\n </div>\n );\n\n const renderGridView = () => {\n // Simple grid layout for month/week/day views\n // This is a basic implementation - you might want to use a proper calendar library\n const groupedEvents = filteredEvents.reduce((groups, event) => {\n const dateKey = formatDateAustralian(event.start);\n if (!groups[dateKey]) {\n groups[dateKey] = [];\n }\n groups[dateKey].push(event);\n return groups;\n }, {} as Record<string, ClientEvent[]>);\n\n return (\n <div className=\"calendar-grid-view\">\n {Object.keys(groupedEvents).length === 0 ? (\n <div style={{ \n textAlign: 'center', \n padding: '40px', \n color: '#666',\n backgroundColor: '#f8f9fa',\n borderRadius: '8px'\n }}>\n <p>No events found for this {view}.</p>\n </div>\n ) : (\n <div style={{ display: 'grid', gap: '16px' }}>\n {Object.entries(groupedEvents).map(([date, dayEvents]) => (\n <div key={date} style={{ \n border: '1px solid #e0e0e0', \n borderRadius: '8px', \n padding: '16px',\n backgroundColor: '#fff'\n }}>\n <h3 style={{ \n margin: '0 0 12px 0', \n fontSize: '16px', \n fontWeight: '600',\n color: '#333',\n borderBottom: '1px solid #eee',\n paddingBottom: '8px'\n }}>\n {date}\n </h3>\n <div style={{ display: 'grid', gap: '8px' }}>\n {dayEvents.map((event) => (\n <div\n key={event.id}\n onClick={() => onEventClick?.(event)}\n style={{\n padding: '8px 12px',\n backgroundColor: '#f8f9fa',\n borderRadius: '4px',\n cursor: onEventClick ? 'pointer' : 'default',\n fontSize: '14px',\n border: '1px solid transparent'\n }}\n onMouseEnter={(e) => {\n if (onEventClick) {\n e.currentTarget.style.backgroundColor = '#e9ecef';\n e.currentTarget.style.borderColor = '#dee2e6';\n }\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.backgroundColor = '#f8f9fa';\n e.currentTarget.style.borderColor = 'transparent';\n }}\n >\n <div style={{ fontWeight: '500' }}>{event.summary}</div>\n {event.formattedTime && (\n <div style={{ fontSize: '12px', color: '#666' }}>\n {event.formattedTime}\n </div>\n )}\n </div>\n ))}\n </div>\n </div>\n ))}\n </div>\n )}\n </div>\n );\n };\n\n return (\n <div className={`calendar-view ${className}`}>\n {renderCalendarHeader()}\n {onViewChange && renderViewSelector()}\n \n <div className=\"calendar-content\">\n {view === 'list' ? renderListView() : renderGridView()}\n </div>\n </div>\n );\n}; ","/**\n * @file Shared constants for the calendar library.\n * These constants are safe to use in both client and server environments.\n */\n\n/**\n * Default currency for financial calculations.\n */\nexport const DEFAULT_CURRENCY = 'AUD';\n\n/**\n * Maximum event duration in hours.\n */\nexport const MAX_EVENT_DURATION_HOURS = 24;\n\n/**\n * Maximum number of recurrence occurrences.\n */\nexport const MAX_RECURRENCE_OCCURRENCES = 365;\n\n/**\n * Default event duration in minutes.\n */\nexport const DEFAULT_EVENT_DURATION_MINUTES = 60;\n\n/**\n * Maximum field lengths for validation.\n */\nexport const MAX_LENGTHS = {\n EVENT_TITLE: 200,\n EVENT_DESCRIPTION: 1000,\n CALENDAR_NAME: 100,\n CALENDAR_DESCRIPTION: 500,\n VENUE_NAME: 200,\n VENUE_ADDRESS: 300,\n VENUE_CITY: 100,\n CONTACT_NAME: 200,\n CONTACT_ROLE: 100,\n NOTES: 1000,\n INCOME_DESCRIPTION: 200,\n EXPENSE_DESCRIPTION: 200,\n RECEIPT_PATH: 500\n} as const;\n\n/**\n * Date format patterns for different locales.\n */\nexport const DATE_FORMATS = {\n AUSTRALIAN: 'DD/MM/YYYY',\n AMERICAN: 'MM/DD/YYYY',\n ISO: 'YYYY-MM-DD'\n} as const;\n\n/**\n * Time format patterns.\n */\nexport const TIME_FORMATS = {\n TWELVE_HOUR: '12h',\n TWENTY_FOUR_HOUR: '24h'\n} as const;\n\n/**\n * Default calendar view options.\n */\nexport const CALENDAR_VIEWS = {\n DAY: 'day',\n WEEK: 'week',\n MONTH: 'month',\n LIST: 'list'\n} as const;\n\n/**\n * Default start of week (0 = Sunday, 1 = Monday, etc.)\n */\nexport const DEFAULT_START_OF_WEEK = 1; // Monday\n\n/**\n * Australian states and territories.\n */\nexport const AUSTRALIAN_STATES = {\n NSW: 'New South Wales',\n VIC: 'Victoria',\n QLD: 'Queensland',\n WA: 'Western Australia',\n SA: 'South Australia',\n TAS: 'Tasmania',\n ACT: 'Australian Capital Territory',\n NT: 'Northern Territory'\n} as const;\n\n/**\n * Common currency codes.\n */\nexport const CURRENCIES = {\n AUD: 'Australian Dollar',\n USD: 'US Dollar',\n EUR: 'Euro',\n GBP: 'British Pound',\n CAD: 'Canadian Dollar',\n NZD: 'New Zealand Dollar'\n} as const;\n\n/**\n * Validation patterns.\n */\nexport const VALIDATION_PATTERNS = {\n EMAIL: /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/,\n PHONE_AU: /^(\\+61|0)[2-478](?:[ -]?[0-9]){8}$/,\n CURRENCY_CODE: /^[A-Z]{3}$/,\n HEX_COLOR: /^#[0-9A-F]{6}$/i,\n URL: /^https?:\\/\\/.+/\n} as const;\n\n/**\n * API configuration constants.\n */\nexport const API_CONFIG = {\n DEFAULT_PAGE_SIZE: 20,\n MAX_PAGE_SIZE: 100,\n REQUEST_TIMEOUT: 30000, // 30 seconds\n RETRY_ATTEMPTS: 3\n} as const;\n\n/**\n * File upload constraints.\n */\nexport const FILE_UPLOAD = {\n MAX_SIZE_MB: 10,\n ALLOWED_TYPES: ['image/jpeg', 'image/png', 'image/gif', 'application/pdf'],\n ALLOWED_EXTENSIONS: ['.jpg', '.jpeg', '.png', '.gif', '.pdf']\n} as const; "],"mappings":";;;;AAOO,IAAMA,uBAAuB,wBAACC,SAAAA;AACnC,QAAMC,IAAI,OAAOD,SAAS,WAAW,IAAIE,KAAKF,IAAAA,IAAQA;AACtD,SAAOC,EAAEE,mBAAmB,SAAS;IACnCC,MAAM;IACNC,QAAQ;IACRC,QAAQ;EACV,CAAA;AACF,GAPoC;AAU7B,IAAMC,2BAA2B,wBAACP,SAAAA;AACvC,QAAMC,IAAI,OAAOD,SAAS,WAAW,IAAIE,KAAKF,IAAAA,IAAQA;AACtD,SAAOC,EAAEO,eAAe,SAAS;IAC/BC,KAAK;IACLC,OAAO;IACPC,MAAM;IACNP,MAAM;IACNC,QAAQ;IACRC,QAAQ;EACV,CAAA;AACF,GAVwC;AAajC,IAAMM,YAAY,wBAACZ,SAAAA;AACxB,QAAMa,aAAa,OAAOb,SAAS,WAAW,IAAIE,KAAKF,IAAAA,IAAQA;AAC/D,QAAMc,MAAM,oBAAIZ,KAAAA;AAChB,QAAMa,WAAWF,WAAWG,QAAO,IAAKF,IAAIE,QAAO;AACnD,SAAOC,KAAKC,KAAKH,YAAY,MAAO,KAAK,KAAK,GAAC;AACjD,GALyB;AAQlB,IAAMI,UAAU,wBAACnB,SAAAA;AACtB,QAAMC,IAAI,OAAOD,SAAS,WAAW,IAAIE,KAAKF,IAAAA,IAAQA;AACtD,QAAMoB,QAAQ,oBAAIlB,KAAAA;AAClB,SAAOD,EAAEoB,aAAY,MAAOD,MAAMC,aAAY;AAChD,GAJuB;AAOhB,IAAMC,SAAS,wBAACtB,