# Vue Calendar ๐Ÿ—“๏ธ [![npm version](https://img.shields.io/npm/v/@kodeglot/vue-calendar.svg)](https://www.npmjs.com/package/@kodeglot/vue-calendar) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![CI/CD](https://github.com/Kodeglot/vue-calendar/actions/workflows/ci.yml/badge.svg)](https://github.com/Kodeglot/vue-calendar/actions) [![Coverage](https://img.shields.io/codecov/c/github/Kodeglot/vue-calendar)](https://codecov.io/gh/Kodeglot/vue-calendar) [![Live Demo](https://img.shields.io/badge/demo-online-brightgreen)](https://kodeglot.github.io/vue-calendar/) A fully-featured, customizable calendar component for Vue 3 with built-in Tailwind CSS support. Perfect for building scheduling applications, event calendars, and date management systems. ## Live Demo ๐Ÿ‘‰ [Try the Vue Calendar Live Demo on GitHub Pages](https://kodeglot.github.io/vue-calendar/) ## Table of Contents - [Features](#features) - [Installation](#installation) - [Quick Start](#quick-start) - [Demo](#demo) - [API Reference](#api-reference) - [Usage Examples](#usage-examples) - [Customization](#customization) - [Plugin System](#plugin-system) - [How-to Guides](#how-to-guides) - [Development](#development) - [Contributing](#contributing) - [Roadmap](#roadmap) - [License](#license) ## Features - ๐ŸŽจ **Fully customizable** with Tailwind CSS - โšก๏ธ **Lightweight and performant** Vue 3 component - ๐Ÿงฉ **TypeScript support** with full type definitions - ๐Ÿ“ฑ **Responsive design** for all screen sizes - ๐Ÿ”Œ **Easy to integrate** with any Vue 3 project - ๐Ÿ›ก๏ธ **CSS Style Isolation** - All styles prefixed with `.vc-calendar` to prevent conflicts with host apps - ๐Ÿ–ฑ๏ธ **Drag and drop support** for event management with reliable time preservation - ๐Ÿ–ฑ๏ธ **Click to create events** - Click on empty space to create new events with automatic time rounding - ๐Ÿ–ฑ๏ธ **Improved event interaction**: Edit modal only opens on single click, not on drag or resize - ๐Ÿ”„ **Reactive event updates**: Calendar updates instantly when events are added/edited - ๐ŸŽจ **22 customizable event colors** using Tailwind color palette - ๐Ÿ—“๏ธ **Multiple view modes** (month, week, day) - ๐Ÿ”„ **Event resizing** with time snapping - ๐Ÿ“… **All-day event support** - ๐ŸŽฏ **Time-based positioning** with 5-minute snap intervals - ๐Ÿ› ๏ธ **Plugin architecture** for extensibility - [See Plugin System Documentation](docs/PLUGIN_SYSTEM.md) - ๐Ÿงช **Comprehensive testing** with Vitest (177+ tests across all components) - ๐ŸŽจ **Enhanced visual hierarchy** with improved month view styling - ๐Ÿ“ฑ **Mobile-optimized modals** with fixed headers/footers and scrollable content - ๐Ÿ”ง **Clean codebase** with regular maintenance and removal of unused code - ๐Ÿšซ **Demo events control** with `showDemoEvents` prop to prevent unwanted demo events in production ## Installation ### Using npm ```bash npm install @kodeglot/vue-calendar date-fns date-fns-tz ``` ### Using yarn ```bash yarn add @kodeglot/vue-calendar date-fns date-fns-tz ``` ### Using pnpm ```bash pnpm add @kodeglot/vue-calendar date-fns date-fns-tz ``` ### CDN Add the following script tag to your HTML: ```html ``` ## Demo ๐Ÿš€ **Live Demo**: [https://kodeglot.github.io/vue-calendar/](https://kodeglot.github.io/vue-calendar/) The demo showcases all the calendar features including: - Multiple view modes (month, week, day) - **Click to create events** with interactive guides - Custom event content and modals - Drag and drop functionality - Event resizing - Custom controls and navigation - Event creation with metadata - **Enhanced visual hierarchy** with improved styling - **Mobile-optimized modals** with responsive design - **Demo events** (enabled for testing purposes - disabled by default in production) ## Quick Start ### 1. Install Dependencies ```bash npm install @kodeglot/vue-calendar pinia date-fns date-fns-tz ``` ### 2. Basic Usage (No Demo Events) ```vue ``` **Important**: The `showDemoEvents` prop defaults to `false`, so your calendar will be clean without any demo events by default. ## CSS Style Isolation The Vue Calendar plugin includes comprehensive CSS style isolation to prevent conflicts with your host application: ### โœ… Safe Integration - All calendar styles are prefixed with `.vc-calendar` - No interference with your app's Tailwind CSS or other styling frameworks - Safe to use alongside any CSS framework or custom styles ### ๐Ÿ”ง How It Works The calendar automatically wraps all its content with the `vc-calendar` class, ensuring that: - Calendar styles only apply within the calendar component - Your app's global styles remain unaffected - No CSS conflicts or style leakage ### ๐Ÿ“ Usage Example ```vue ``` In this example, your app's grid layout will work normally, while the calendar's styles are completely isolated within the `.vc-calendar` wrapper. ### 2. Import Styles (REQUIRED) **Important**: You must import the CSS file for the calendar to display correctly: ```typescript // main.ts import { createApp } from 'vue' import { createPinia } from 'pinia' import App from './App.vue' // Import the calendar styles - THIS IS REQUIRED! import '@kodeglot/vue-calendar/style.css' const app = createApp(App) app.use(createPinia()) app.mount('#app') ``` **Alternative import path** (if the above doesn't work): ```typescript import '@kodeglot/vue-calendar/dist/style.css' ``` ### 3. Setup Pinia Store The calendar package provides two store options: #### Option 1: Standard Store (Recommended for most use cases) ```typescript import { useCalendarStore } from '@kodeglot/vue-calendar' const store = useCalendarStore() // Add events store.addEvent({ id: '1', title: 'Meeting', start: '2025-01-01T10:00:00Z', end: '2025-01-01T11:00:00Z', tailwindColor: 'blue' }) // Add multiple events at once store.addEvents([ { id: '1', title: 'Meeting', start: '2025-01-01T10:00:00Z', end: '2025-01-01T11:00:00Z', tailwindColor: 'blue' }, { id: '2', title: 'Lunch', start: '2025-01-01T12:00:00Z', end: '2025-01-01T13:00:00Z', tailwindColor: 'green' } ]) // Get events for a date const events = store.getEventsForDate(new Date('2025-01-01')) ``` #### Option 2: Custom Store with Overrides ```typescript import { useCustomCalendarStore } from '@kodeglot/vue-calendar' const store = useCustomCalendarStore({ addEvent: (event) => { // Custom validation if (!event.title.trim()) { throw new Error('Title is required') } // Call the reference implementation const baseStore = useCalendarStore() baseStore.addEvent(event) }, deleteEvent: (eventId) => { // Custom confirmation if (confirm('Are you sure?')) { const baseStore = useCalendarStore() baseStore.deleteEvent(eventId) } } }) ``` **Note**: Both stores share the same underlying state, so you can mix and match as needed. ## Custom Store Overrides The calendar package provides a flexible way to override specific store methods while keeping the reference implementation for others. This is useful when you need custom business logic for certain operations. ### Using `useCustomCalendarStore` Instead of implementing your own store from scratch, you can use `useCustomCalendarStore` to selectively override methods: ```typescript import { useCustomCalendarStore, type CalendarStoreOverrides } from '@kodeglot/vue-calendar' // Define your custom overrides const customOverrides: CalendarStoreOverrides = { // Override addEvent to add custom logging addEvent: (event) => { console.log('Custom addEvent called:', event) // Call the default implementation const baseStore = useCalendarStore() baseStore.addEvent(event) }, // Override deleteEvent to add confirmation deleteEvent: (eventId) => { if (confirm(`Are you sure you want to delete event "${eventId}"?`)) { const baseStore = useCalendarStore() baseStore.deleteEvent(eventId) } }, // Override getEventsForDate to filter private events getEventsForDate: (date) => { const baseStore = useCalendarStore() const allEvents = baseStore.getEventsForDate(date) // Filter out events with "private" in the title return allEvents.filter(event => !event.title.toLowerCase().includes('private') ) } } // Use the custom store const customStore = useCustomCalendarStore(customOverrides) ``` ### Available Override Methods You can override any of these methods: - `addEvent(event: CalendarEvent)` - Called when adding a new event - `addEvents(events: CalendarEvent[])` - Called when adding multiple events at once - `updateEvent(event: CalendarEvent)` - Called when updating an event - `deleteEvent(eventId: string)` - Called when deleting an event - `updateEventDate(eventId: string, newDate: Date)` - Called when changing event date - `updateEventDateOnly(eventId: string, newDate: Date)` - Called when changing only the date (preserves time) - `updateEventDuration(eventId: string, newStart: Date, newEnd: Date)` - Called when changing event duration - `updateEventTime(eventId: string, newTime: Date)` - Called when changing event time - `getEventsForDate(date: Date)` - Called when retrieving events for a specific date - `getEventsForWeek(startDate: Date)` - Called when retrieving events for a week - `getEventById(eventId: string)` - Called when retrieving a specific event ### Benefits of Custom Store Overrides 1. **Selective Customization**: Override only the methods you need 2. **Reference Implementation**: Keep the battle-tested default behavior for other methods 3. **Type Safety**: Full TypeScript support with proper interfaces 4. **Plugin Compatibility**: Works seamlessly with the plugin system 5. **Performance**: No performance overhead for non-overridden methods ### Example Use Cases #### 1. Custom Validation ```typescript const customOverrides: CalendarStoreOverrides = { addEvent: (event) => { // Validate event data if (!event.title.trim()) { throw new Error('Event title is required') } // Call default implementation const baseStore = useCalendarStore() baseStore.addEvent(event) } } ``` #### 2. API Integration ```typescript const customOverrides: CalendarStoreOverrides = { addEvent: async (event) => { // Save to API await api.saveEvent(event) // Call default implementation const baseStore = useCalendarStore() baseStore.addEvent(event) }, deleteEvent: async (eventId) => { // Delete from API await api.deleteEvent(eventId) // Call default implementation const baseStore = useCalendarStore() baseStore.deleteEvent(eventId) } } ``` #### 3. Event Filtering ```typescript const customOverrides: CalendarStoreOverrides = { getEventsForDate: (date) => { const baseStore = useCalendarStore() const allEvents = baseStore.getEventsForDate(date) // Filter based on user permissions return allEvents.filter(event => { if (event.metadata?.isPrivate && !userHasPermission('view-private')) { return false } return true }) } } ``` ## Plugin System The vue-calendar package includes a powerful plugin system that allows you to extend calendar functionality without modifying the core code. Plugins can react to events, modify behavior, and add custom functionality. ### Plugin Interface ```typescript export interface CalendarPlugin { // Called when the plugin is registered onRegister?: (store: { currentDate: Date events: CalendarEvent[] plugins: CalendarPlugin[] currentMonth: number monthEvents: CalendarEvent[] addEvent: (event: CalendarEvent) => void updateEvent: (event: CalendarEvent) => void deleteEvent: (eventId: string) => void updateEventDate: (eventId: string, newDate: Date) => void updateEventDateOnly: (eventId: string, newDate: Date) => void updateEventDuration: (eventId: string, newStart: Date, newEnd: Date) => void updateEventTime: (eventId: string, newTime: Date) => void getEventsForDate: (date: Date) => CalendarEvent[] getEventsForWeek: (startDate: Date) => CalendarEvent[] registerPlugin: (plugin: CalendarPlugin) => void }) => void // Called whenever a new event is added onEventAdd?: (event: CalendarEvent) => void } ``` ### Basic Plugin Usage ```typescript import { useCalendarStore, type CalendarPlugin } from '@kodeglot/vue-calendar' const store = useCalendarStore() // Create a simple logging plugin const eventLoggerPlugin: CalendarPlugin = { onRegister: (store) => { console.log('๐Ÿ”ต Event Logger Plugin registered') console.log('Current events:', store.events.length) }, onEventAdd: (event) => { console.log('๐Ÿ“… Event added:', { id: event.id, title: event.title, start: event.start, end: event.end }) } } // Register the plugin store.registerPlugin(eventLoggerPlugin) ``` ### Plugin Examples #### 1. Event Validation Plugin ```typescript const eventValidationPlugin: CalendarPlugin = { onRegister: (store) => { // Store the original addEvent method const originalAddEvent = store.addEvent // Override addEvent to add validation store.addEvent = (event) => { // Validate event if (!event.title || event.title.trim().length === 0) { throw new Error('Event title is required') } if (new Date(event.start) >= new Date(event.end)) { throw new Error('Event start time must be before end time') } // Call original method originalAddEvent(event) } } } ``` #### 2. Event Color Management Plugin ```typescript const eventColorPlugin: CalendarPlugin = { colorMap: { 'meeting': 'blue', 'appointment': 'green', 'reminder': 'yellow', 'deadline': 'red' }, onEventAdd: (event) => { // Auto-assign color based on title keywords const title = event.title.toLowerCase() for (const [keyword, color] of Object.entries(this.colorMap)) { if (title.includes(keyword)) { event.tailwindColor = color console.log(`๐ŸŽจ Assigned color ${color} to event: ${event.title}`) break } } } } ``` #### 3. Event Statistics Plugin ```typescript const eventStatsPlugin: CalendarPlugin = { stats: { totalEvents: 0, eventsByMonth: new Map(), averageEventDuration: 0 }, onRegister: (store) => { // Initialize stats from existing events store.events.forEach(event => { this.updateStats(event) }) }, onEventAdd: (event) => { this.updateStats(event) console.log('๐Ÿ“ˆ Updated stats:', this.stats) }, updateStats(event: CalendarEvent) { this.stats.totalEvents++ const month = new Date(event.start).getMonth() this.stats.eventsByMonth.set(month, (this.stats.eventsByMonth.get(month) || 0) + 1 ) const duration = new Date(event.end).getTime() - new Date(event.start).getTime() this.stats.averageEventDuration = (this.stats.averageEventDuration * (this.stats.totalEvents - 1) + duration) / this.stats.totalEvents } } ``` #### 4. Event Conflict Detection Plugin ```typescript const eventConflictPlugin: CalendarPlugin = { onEventAdd: (event) => { const conflicts = this.findConflicts(event, store.events) if (conflicts.length > 0) { console.warn('โš ๏ธ Event conflicts detected:', { event: event.title, conflicts: conflicts.map(c => c.title) }) // Highlight conflicting events conflicts.forEach(conflict => { conflict.tailwindColor = 'red' }) } }, findConflicts(newEvent: CalendarEvent, allEvents: CalendarEvent[]): CalendarEvent[] { const newStart = new Date(newEvent.start) const newEnd = new Date(newEvent.end) return Array.from(allEvents.values()).filter(event => { if (event.id === newEvent.id) return false const eventStart = new Date(event.start) const eventEnd = new Date(event.end) // Check for overlap return newStart < eventEnd && newEnd > eventStart }) } } ``` ### Plugin Best Practices 1. **Single Responsibility**: Each plugin should have one clear purpose 2. **Error Handling**: Always handle potential errors gracefully 3. **Performance**: Avoid expensive operations in hooks 4. **State Management**: Be careful when modifying store state 5. **Method Overrides**: Store original methods before overriding ### Plugin Integration with Custom Store Plugins work seamlessly with both store approaches: ```typescript import { useCalendarStore, useCustomCalendarStore } from '@kodeglot/vue-calendar' // Works with standard store const standardStore = useCalendarStore() standardStore.registerPlugin(eventLoggerPlugin) // Works with custom store const customStore = useCustomCalendarStore({ addEvent: (event) => { // Custom addEvent logic console.log('Custom addEvent called') } }) customStore.registerPlugin(eventLoggerPlugin) ``` ### Advanced Plugin Patterns #### Plugin Composition ```typescript // Combine multiple plugins into one const createCompositePlugin = (...plugins: CalendarPlugin[]): CalendarPlugin => { return { onRegister: (store) => { plugins.forEach(plugin => plugin.onRegister?.(store)) }, onEventAdd: (event) => { plugins.forEach(plugin => plugin.onEventAdd?.(event)) } } } // Usage const compositePlugin = createCompositePlugin( eventLoggerPlugin, eventValidationPlugin, eventColorPlugin ) ``` #### Configurable Plugins ```typescript // Make plugins configurable const createConfigurablePlugin = (config: { logLevel: 'info' | 'warn' | 'error' enableValidation: boolean }) => { return { config, onRegister: (store) => { if (config.enableValidation) { // Set up validation } }, onEventAdd: (event) => { if (config.logLevel === 'info') { console.log('Event added:', event) } } } as CalendarPlugin } // Usage const plugin = createConfigurablePlugin({ logLevel: 'info', enableValidation: true }) ``` For more detailed information about the plugin system, see the [Plugin System Documentation](docs/PLUGIN_SYSTEM.md). ## How-to Guides ### How to Initialize Calendar with Events ```typescript import { useCalendarStore } from '@kodeglot/vue-calendar' const store = useCalendarStore() // Initialize with multiple events store.addEvents([ { id: '1', title: 'Team Meeting', start: '2025-01-15T10:00:00Z', end: '2025-01-15T11:00:00Z', tailwindColor: 'blue' }, { title: 'Lunch Break', // ID will be auto-generated start: '2025-01-15T12:00:00Z', end: '2025-01-15T13:00:00Z', tailwindColor: 'green' } ]) ``` ### How to Add API Integration ```typescript import { useCustomCalendarStore } from '@kodeglot/vue-calendar' const store = useCustomCalendarStore({ addEvent: async (event) => { // Save to API await fetch('/api/events', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(event) }) // Call reference implementation const baseStore = useCalendarStore() baseStore.addEvent(event) }, deleteEvent: async (eventId) => { // Delete from API await fetch(`/api/events/${eventId}`, { method: 'DELETE' }) // Call reference implementation const baseStore = useCalendarStore() baseStore.deleteEvent(eventId) } }) ``` ### How to Add Event Validation ```typescript const store = useCustomCalendarStore({ addEvent: (event) => { // Validate event data if (!event.title?.trim()) { throw new Error('Event title is required') } if (new Date(event.start) >= new Date(event.end)) { throw new Error('Start time must be before end time') } // Call reference implementation const baseStore = useCalendarStore() baseStore.addEvent(event) } }) ``` ### How to Filter Events by Permission ```typescript const store = useCustomCalendarStore({ getEventsForDate: (date) => { const baseStore = useCalendarStore() const allEvents = baseStore.getEventsForDate(date) // Filter based on user permissions return allEvents.filter(event => { if (event.metadata?.isPrivate && !userHasPermission('view-private')) { return false } return true }) } }) ``` ### How to Add Event Logging ```typescript import { useCalendarStore, type CalendarPlugin } from '@kodeglot/vue-calendar' const store = useCalendarStore() const loggingPlugin: CalendarPlugin = { onEventAdd: (event) => { console.log('Event added:', event.title, event.start) // Send to analytics analytics.track('event_created', { title: event.title }) } } store.registerPlugin(loggingPlugin) ``` ### How to Handle Event Conflicts ```typescript const conflictPlugin: CalendarPlugin = { onEventAdd: (event) => { const conflicts = this.findConflicts(event, store.events) if (conflicts.length > 0) { console.warn('Event conflicts detected:', conflicts.map(c => c.title)) // Optionally highlight conflicting events conflicts.forEach(conflict => { conflict.tailwindColor = 'red' }) } }, findConflicts(newEvent, allEvents) { const newStart = new Date(newEvent.start) const newEnd = new Date(newEvent.end) return Array.from(allEvents.values()).filter(event => { if (event.id === newEvent.id) return false const eventStart = new Date(event.start) const eventEnd = new Date(event.end) return newStart < eventEnd && newEnd > eventStart }) } } store.registerPlugin(conflictPlugin) ``` ### 4. Use the Calendar Component ```vue ## Click to Create Events The calendar now supports creating events by clicking on empty space: ### Month View - Click on any date cell to create an event at 9 AM on that date - Previous/next month dates have a subtle gray background for better visual hierarchy ### Week/Day View - Click on any time slot to create an event at that specific time - Times are automatically rounded to the nearest 5-minute interval - All new events default to 1-hour duration ### Features - **Automatic time rounding**: Times are rounded to the nearest 5 minutes for consistency - **Default duration**: New events automatically span 1 hour - **Visual feedback**: Clear indication of clickable areas - **Cross-view consistency**: Works seamlessly across all view modes ## Event Interaction The calendar provides intuitive event interaction across all views: ### Creating Events - **Month View**: Click on empty date cells to create events at 9 AM on that date - **Week/Day View**: Click on empty time slots to create events at that specific time - **All Views**: Use the "Create Event" button for quick event creation ### Editing Events - **Click on any event** to open the edit modal - **Month View**: Events are draggable between days (preserves time) - **Week/Day View**: Events can be dragged and resized with time snapping - **All Views**: Edit modal provides full event management (title, time, color, delete) ### Drag and Drop - **Month View**: Drag events between days while preserving original time - **Week/Day View**: Drag events to new times and resize for duration changes - **Time Preservation**: Moving events between days maintains the original time components ## Debug Logging Vue Calendar includes a comprehensive debug logging system that helps with development and troubleshooting: ### Automatic Debug Mode - **Development**: Debug logs are automatically enabled when running `npm run dev` - **Production**: Debug logs are disabled by default to keep builds clean - **Conditional Output**: All debug logs are wrapped in conditional checks ### Browser Console Controls You can control debug mode directly from the browser console: ```javascript // Enable debug mode __enableCalendarDebug() // Disable debug mode __disableCalendarDebug() // Check current debug status __getCalendarDebugStatus() ``` ### Debug Coverage The debug system logs comprehensive information about: - **Calendar Lifecycle**: Component mounting, updates, and cleanup - **User Interactions**: Clicks, drags, resizes, and navigation - **Event Operations**: Creation, updates, deletions, and movements - **Data Flow**: Store updates, event filtering, and reactivity - **Performance**: Timing for expensive operations - **Errors**: Detailed error information with context ### Using Debug in Your Code ```typescript import { debug } from '@kodeglot/vue-calendar' // Basic logging debug.log('User clicked event:', eventId) // Warning and error logging debug.warn('Event not found:', eventId) debug.error('Failed to update event:', error) // Grouped logging for complex operations debug.group('Event Drag Operation') debug.log('Drag started:', { eventId, startTime }) debug.log('Drag position:', { x, y }) debug.groupEnd() // Performance timing debug.time('Event Filtering') // ... your code ... debug.timeEnd('Event Filtering') ``` ### Debug Output Example ``` [Vue Calendar Debug] Store: Adding event {eventId: "123", title: "Meeting", start: "2024-01-15T10:00:00Z"} [Vue Calendar Debug] CalendarView: Component mounted [Vue Calendar Debug] Event: Drag started {eventId: "123", position: {x: 100, y: 200}} [Vue Calendar Debug] Store: Updating event date only {eventId: "123", preservedTime: "10:0:0"} ``` ## Troubleshooting ### CSS Style Conflicts? The Vue Calendar plugin includes comprehensive CSS style isolation to prevent conflicts with your host application. All calendar styles are prefixed with `.vc-calendar` to ensure they only apply within the calendar component. If you're experiencing style conflicts, ensure that: - The calendar component is properly wrapped with the `vc-calendar` class (this happens automatically) - Your host app's styles are not interfering with the calendar's isolated styles - The CSS import is working correctly ### Calendar Not Styling Correctly? If the calendar appears unstyled or broken, make sure you've imported the CSS: 1. **Check your main.ts/main.js file** - ensure you have: ```typescript import '@kodeglot/vue-calendar/style.css' ``` 2. **Try the alternative import path**: ```typescript import '@kodeglot/vue-calendar/dist/style.css' ``` 3. **If using a bundler**, make sure it's configured to handle CSS imports 4. **For CDN usage**, include the CSS file: ```html ``` ### Missing Colors or Resize Handles? If you're experiencing issues with: - **Missing event colors** (orange, purple, etc.) - **Resize handles not visible** - **Hover effects not working** - **Cursor changes not appearing** This is likely a **CSS purging issue** in your implementing app. The calendar uses dynamic Tailwind classes that may be purged during your app's build process. #### Solution: Add to Your App's Tailwind Config Add this comprehensive safelist to your implementing app's `tailwind.config.js`: ```javascript /** @type {import('tailwindcss').Config} */ export default { content: [ // ... your content paths ], safelist: [ // Background colors - ALL variants 'bg-red-50', 'bg-red-100', 'bg-red-200', 'bg-red-300', 'bg-red-400', 'bg-red-500', 'bg-red-600', 'bg-red-700', 'bg-red-800', 'bg-red-900', 'bg-orange-50', 'bg-orange-100', 'bg-orange-200', 'bg-orange-300', 'bg-orange-400', 'bg-orange-500', 'bg-orange-600', 'bg-orange-700', 'bg-orange-800', 'bg-orange-900', 'bg-amber-50', 'bg-amber-100', 'bg-amber-200', 'bg-amber-300', 'bg-amber-400', 'bg-amber-500', 'bg-amber-600', 'bg-amber-700', 'bg-amber-800', 'bg-amber-900', 'bg-yellow-50', 'bg-yellow-100', 'bg-yellow-200', 'bg-yellow-300', 'bg-yellow-400', 'bg-yellow-500', 'bg-yellow-600', 'bg-yellow-700', 'bg-yellow-800', 'bg-yellow-900', 'bg-lime-50', 'bg-lime-100', 'bg-lime-200', 'bg-lime-300', 'bg-lime-400', 'bg-lime-500', 'bg-lime-600', 'bg-lime-700', 'bg-lime-800', 'bg-lime-900', 'bg-green-50', 'bg-green-100', 'bg-green-200', 'bg-green-300', 'bg-green-400', 'bg-green-500', 'bg-green-600', 'bg-green-700', 'bg-green-800', 'bg-green-900', 'bg-emerald-50', 'bg-emerald-100', 'bg-emerald-200', 'bg-emerald-300', 'bg-emerald-400', 'bg-emerald-500', 'bg-emerald-600', 'bg-emerald-700', 'bg-emerald-800', 'bg-emerald-900', 'bg-teal-50', 'bg-teal-100', 'bg-teal-200', 'bg-teal-300', 'bg-teal-400', 'bg-teal-500', 'bg-teal-600', 'bg-teal-700', 'bg-teal-800', 'bg-teal-900', 'bg-cyan-50', 'bg-cyan-100', 'bg-cyan-200', 'bg-cyan-300', 'bg-cyan-400', 'bg-cyan-500', 'bg-cyan-600', 'bg-cyan-700', 'bg-cyan-800', 'bg-cyan-900', 'bg-sky-50', 'bg-sky-100', 'bg-sky-200', 'bg-sky-300', 'bg-sky-400', 'bg-sky-500', 'bg-sky-600', 'bg-sky-700', 'bg-sky-800', 'bg-sky-900', 'bg-blue-50', 'bg-blue-100', 'bg-blue-200', 'bg-blue-300', 'bg-blue-400', 'bg-blue-500', 'bg-blue-600', 'bg-blue-700', 'bg-blue-800', 'bg-blue-900', 'bg-indigo-50', 'bg-indigo-100', 'bg-indigo-200', 'bg-indigo-300', 'bg-indigo-400', 'bg-indigo-500', 'bg-indigo-600', 'bg-indigo-700', 'bg-indigo-800', 'bg-indigo-900', 'bg-violet-50', 'bg-violet-100', 'bg-violet-200', 'bg-violet-300', 'bg-violet-400', 'bg-violet-500', 'bg-violet-600', 'bg-violet-700', 'bg-violet-800', 'bg-violet-900', 'bg-purple-50', 'bg-purple-100', 'bg-purple-200', 'bg-purple-300', 'bg-purple-400', 'bg-purple-500', 'bg-purple-600', 'bg-purple-700', 'bg-purple-800', 'bg-purple-900', 'bg-fuchsia-50', 'bg-fuchsia-100', 'bg-fuchsia-200', 'bg-fuchsia-300', 'bg-fuchsia-400', 'bg-fuchsia-500', 'bg-fuchsia-600', 'bg-fuchsia-700', 'bg-fuchsia-800', 'bg-fuchsia-900', 'bg-pink-50', 'bg-pink-100', 'bg-pink-200', 'bg-pink-300', 'bg-pink-400', 'bg-pink-500', 'bg-pink-600', 'bg-pink-700', 'bg-pink-800', 'bg-pink-900', 'bg-rose-50', 'bg-rose-100', 'bg-rose-200', 'bg-rose-300', 'bg-rose-400', 'bg-rose-500', 'bg-rose-600', 'bg-rose-700', 'bg-rose-800', 'bg-rose-900', 'bg-slate-50', 'bg-slate-100', 'bg-slate-200', 'bg-slate-300', 'bg-slate-400', 'bg-slate-500', 'bg-slate-600', 'bg-slate-700', 'bg-slate-800', 'bg-slate-900', 'bg-gray-50', 'bg-gray-100', 'bg-gray-200', 'bg-gray-300', 'bg-gray-400', 'bg-gray-500', 'bg-gray-600', 'bg-gray-700', 'bg-gray-800', 'bg-gray-900', 'bg-zinc-50', 'bg-zinc-100', 'bg-zinc-200', 'bg-zinc-300', 'bg-zinc-400', 'bg-zinc-500', 'bg-zinc-600', 'bg-zinc-700', 'bg-zinc-800', 'bg-zinc-900', 'bg-neutral-50', 'bg-neutral-100', 'bg-neutral-200', 'bg-neutral-300', 'bg-neutral-400', 'bg-neutral-500', 'bg-neutral-600', 'bg-neutral-700', 'bg-neutral-800', 'bg-neutral-900', 'bg-stone-50', 'bg-stone-100', 'bg-stone-200', 'bg-stone-300', 'bg-stone-400', 'bg-stone-500', 'bg-stone-600', 'bg-stone-700', 'bg-stone-800', 'bg-stone-900', // Left border colors specifically 'border-l-red-50', 'border-l-red-100', 'border-l-red-200', 'border-l-red-300', 'border-l-red-400', 'border-l-red-500', 'border-l-red-600', 'border-l-red-700', 'border-l-red-800', 'border-l-red-900', 'border-l-orange-50', 'border-l-orange-100', 'border-l-orange-200', 'border-l-orange-300', 'border-l-orange-400', 'border-l-orange-500', 'border-l-orange-600', 'border-l-orange-700', 'border-l-orange-800', 'border-l-orange-900', 'border-l-amber-50', 'border-l-amber-100', 'border-l-amber-200', 'border-l-amber-300', 'border-l-amber-400', 'border-l-amber-500', 'border-l-amber-600', 'border-l-amber-700', 'border-l-amber-800', 'border-l-amber-900', 'border-l-yellow-50', 'border-l-yellow-100', 'border-l-yellow-200', 'border-l-yellow-300', 'border-l-yellow-400', 'border-l-yellow-500', 'border-l-yellow-600', 'border-l-yellow-700', 'border-l-yellow-800', 'border-l-yellow-900', 'border-l-lime-50', 'border-l-lime-100', 'border-l-lime-200', 'border-l-lime-300', 'border-l-lime-400', 'border-l-lime-500', 'border-l-lime-600', 'border-l-lime-700', 'border-l-lime-800', 'border-l-lime-900', 'border-l-green-50', 'border-l-green-100', 'border-l-green-200', 'border-l-green-300', 'border-l-green-400', 'border-l-green-500', 'border-l-green-600', 'border-l-green-700', 'border-l-green-800', 'border-l-green-900', 'border-l-emerald-50', 'border-l-emerald-100', 'border-l-emerald-200', 'border-l-emerald-300', 'border-l-emerald-400', 'border-l-emerald-500', 'border-l-emerald-600', 'border-l-emerald-700', 'border-l-emerald-800', 'border-l-emerald-900', 'border-l-teal-50', 'border-l-teal-100', 'border-l-teal-200', 'border-l-teal-300', 'border-l-teal-400', 'border-l-teal-500', 'border-l-teal-600', 'border-l-teal-700', 'border-l-teal-800', 'border-l-teal-900', 'border-l-cyan-50', 'border-l-cyan-100', 'border-l-cyan-200', 'border-l-cyan-300', 'border-l-cyan-400', 'border-l-cyan-500', 'border-l-cyan-600', 'border-l-cyan-700', 'border-l-cyan-800', 'border-l-cyan-900', 'border-l-sky-50', 'border-l-sky-100', 'border-l-sky-200', 'border-l-sky-300', 'border-l-sky-400', 'border-l-sky-500', 'border-l-sky-600', 'border-l-sky-700', 'border-l-sky-800', 'border-l-sky-900', 'border-l-blue-50', 'border-l-blue-100', 'border-l-blue-200', 'border-l-blue-300', 'border-l-blue-400', 'border-l-blue-500', 'border-l-blue-600', 'border-l-blue-700', 'border-l-blue-800', 'border-l-blue-900', 'border-l-indigo-50', 'border-l-indigo-100', 'border-l-indigo-200', 'border-l-indigo-300', 'border-l-indigo-400', 'border-l-indigo-500', 'border-l-indigo-600', 'border-l-indigo-700', 'border-l-indigo-800', 'border-l-indigo-900', 'border-l-violet-50', 'border-l-violet-100', 'border-l-violet-200', 'border-l-violet-300', 'border-l-violet-400', 'border-l-violet-500', 'border-l-violet-600', 'border-l-violet-700', 'border-l-violet-800', 'border-l-violet-900', 'border-l-purple-50', 'border-l-purple-100', 'border-l-purple-200', 'border-l-purple-300', 'border-l-purple-400', 'border-l-purple-500', 'border-l-purple-600', 'border-l-purple-700', 'border-l-purple-800', 'border-l-purple-900', 'border-l-fuchsia-50', 'border-l-fuchsia-100', 'border-l-fuchsia-200', 'border-l-fuchsia-300', 'border-l-fuchsia-400', 'border-l-fuchsia-500', 'border-l-fuchsia-600', 'border-l-fuchsia-700', 'border-l-fuchsia-800', 'border-l-fuchsia-900', 'border-l-pink-50', 'border-l-pink-100', 'border-l-pink-200', 'border-l-pink-300', 'border-l-pink-400', 'border-l-pink-500', 'border-l-pink-600', 'border-l-pink-700', 'border-l-pink-800', 'border-l-pink-900', 'border-l-rose-50', 'border-l-rose-100', 'border-l-rose-200', 'border-l-rose-300', 'border-l-rose-400', 'border-l-rose-500', 'border-l-rose-600', 'border-l-rose-700', 'border-l-rose-800', 'border-l-rose-900', 'border-l-slate-50', 'border-l-slate-100', 'border-l-slate-200', 'border-l-slate-300', 'border-l-slate-400', 'border-l-slate-500', 'border-l-slate-600', 'border-l-slate-700', 'border-l-slate-800', 'border-l-slate-900', 'border-l-gray-50', 'border-l-gray-100', 'border-l-gray-200', 'border-l-gray-300', 'border-l-gray-400', 'border-l-gray-500', 'border-l-gray-600', 'border-l-gray-700', 'border-l-gray-800', 'border-l-gray-900', 'border-l-zinc-50', 'border-l-zinc-100', 'border-l-zinc-200', 'border-l-zinc-300', 'border-l-zinc-400', 'border-l-zinc-500', 'border-l-zinc-600', 'border-l-zinc-700', 'border-l-zinc-800', 'border-l-zinc-900', 'border-l-neutral-50', 'border-l-neutral-100', 'border-l-neutral-200', 'border-l-neutral-300', 'border-l-neutral-400', 'border-l-neutral-500', 'border-l-neutral-600', 'border-l-neutral-700', 'border-l-neutral-800', 'border-l-neutral-900', 'border-l-stone-50', 'border-l-stone-100', 'border-l-stone-200', 'border-l-stone-300', 'border-l-stone-400', 'border-l-stone-500', 'border-l-stone-600', 'border-l-stone-700', 'border-l-stone-800', 'border-l-stone-900', // Hover states 'hover:bg-red-50', 'hover:bg-red-100', 'hover:bg-red-200', 'hover:bg-red-300', 'hover:bg-red-400', 'hover:bg-red-500', 'hover:bg-red-600', 'hover:bg-red-700', 'hover:bg-red-800', 'hover:bg-red-900', 'hover:bg-orange-50', 'hover:bg-orange-100', 'hover:bg-orange-200', 'hover:bg-orange-300', 'hover:bg-orange-400', 'hover:bg-orange-500', 'hover:bg-orange-600', 'hover:bg-orange-700', 'hover:bg-orange-800', 'hover:bg-orange-900', 'hover:bg-amber-50', 'hover:bg-amber-100', 'hover:bg-amber-200', 'hover:bg-amber-300', 'hover:bg-amber-400', 'hover:bg-amber-500', 'hover:bg-amber-600', 'hover:bg-amber-700', 'hover:bg-amber-800', 'hover:bg-amber-900', 'hover:bg-yellow-50', 'hover:bg-yellow-100', 'hover:bg-yellow-200', 'hover:bg-yellow-300', 'hover:bg-yellow-400', 'hover:bg-yellow-500', 'hover:bg-yellow-600', 'hover:bg-yellow-700', 'hover:bg-yellow-800', 'hover:bg-yellow-900', 'hover:bg-lime-50', 'hover:bg-lime-100', 'hover:bg-lime-200', 'hover:bg-lime-300', 'hover:bg-lime-400', 'hover:bg-lime-500', 'hover:bg-lime-600', 'hover:bg-lime-700', 'hover:bg-lime-800', 'hover:bg-lime-900', 'hover:bg-green-50', 'hover:bg-green-100', 'hover:bg-green-200', 'hover:bg-green-300', 'hover:bg-green-400', 'hover:bg-green-500', 'hover:bg-green-600', 'hover:bg-green-700', 'hover:bg-green-800', 'hover:bg-green-900', 'hover:bg-emerald-50', 'hover:bg-emerald-100', 'hover:bg-emerald-200', 'hover:bg-emerald-300', 'hover:bg-emerald-400', 'hover:bg-emerald-500', 'hover:bg-emerald-600', 'hover:bg-emerald-700', 'hover:bg-emerald-800', 'hover:bg-emerald-900', 'hover:bg-teal-50', 'hover:bg-teal-100', 'hover:bg-teal-200', 'hover:bg-teal-300', 'hover:bg-teal-400', 'hover:bg-teal-500', 'hover:bg-teal-600', 'hover:bg-teal-700', 'hover:bg-teal-800', 'hover:bg-teal-900', 'hover:bg-cyan-50', 'hover:bg-cyan-100', 'hover:bg-cyan-200', 'hover:bg-cyan-300', 'hover:bg-cyan-400', 'hover:bg-cyan-500', 'hover:bg-cyan-600', 'hover:bg-cyan-700', 'hover:bg-cyan-800', 'hover:bg-cyan-900', 'hover:bg-sky-50', 'hover:bg-sky-100', 'hover:bg-sky-200', 'hover:bg-sky-300', 'hover:bg-sky-400', 'hover:bg-sky-500', 'hover:bg-sky-600', 'hover:bg-sky-700', 'hover:bg-sky-800', 'hover:bg-sky-900', 'hover:bg-blue-50', 'hover:bg-blue-100', 'hover:bg-blue-200', 'hover:bg-blue-300', 'hover:bg-blue-400', 'hover:bg-blue-500', 'hover:bg-blue-600', 'hover:bg-blue-700', 'hover:bg-blue-800', 'hover:bg-blue-900', 'hover:bg-indigo-50', 'hover:bg-indigo-100', 'hover:bg-indigo-200', 'hover:bg-indigo-300', 'hover:bg-indigo-400', 'hover:bg-indigo-500', 'hover:bg-indigo-600', 'hover:bg-indigo-700', 'hover:bg-indigo-800', 'hover:bg-indigo-900', 'hover:bg-violet-50', 'hover:bg-violet-100', 'hover:bg-violet-200', 'hover:bg-violet-300', 'hover:bg-violet-400', 'hover:bg-violet-500', 'hover:bg-violet-600', 'hover:bg-violet-700', 'hover:bg-violet-800', 'hover:bg-violet-900', 'hover:bg-purple-50', 'hover:bg-purple-100', 'hover:bg-purple-200', 'hover:bg-purple-300', 'hover:bg-purple-400', 'hover:bg-purple-500', 'hover:bg-purple-600', 'hover:bg-purple-700', 'hover:bg-purple-800', 'hover:bg-purple-900', 'hover:bg-fuchsia-50', 'hover:bg-fuchsia-100', 'hover:bg-fuchsia-200', 'hover:bg-fuchsia-300', 'hover:bg-fuchsia-400', 'hover:bg-fuchsia-500', 'hover:bg-fuchsia-600', 'hover:bg-fuchsia-700', 'hover:bg-fuchsia-800', 'hover:bg-fuchsia-900', 'hover:bg-pink-50', 'hover:bg-pink-100', 'hover:bg-pink-200', 'hover:bg-pink-300', 'hover:bg-pink-400', 'hover:bg-pink-500', 'hover:bg-pink-600', 'hover:bg-pink-700', 'hover:bg-pink-800', 'hover:bg-pink-900', 'hover:bg-rose-50', 'hover:bg-rose-100', 'hover:bg-rose-200', 'hover:bg-rose-300', 'hover:bg-rose-400', 'hover:bg-rose-500', 'hover:bg-rose-600', 'hover:bg-rose-700', 'hover:bg-rose-800', 'hover:bg-rose-900', 'hover:bg-slate-50', 'hover:bg-slate-100', 'hover:bg-slate-200', 'hover:bg-slate-300', 'hover:bg-slate-400', 'hover:bg-slate-500', 'hover:bg-slate-600', 'hover:bg-slate-700', 'hover:bg-slate-800', 'hover:bg-slate-900', 'hover:bg-gray-50', 'hover:bg-gray-100', 'hover:bg-gray-200', 'hover:bg-gray-300', 'hover:bg-gray-400', 'hover:bg-gray-500', 'hover:bg-gray-600', 'hover:bg-gray-700', 'hover:bg-gray-800', 'hover:bg-gray-900', 'hover:bg-zinc-50', 'hover:bg-zinc-100', 'hover:bg-zinc-200', 'hover:bg-zinc-300', 'hover:bg-zinc-400', 'hover:bg-zinc-500', 'hover:bg-zinc-600', 'hover:bg-zinc-700', 'hover:bg-zinc-800', 'hover:bg-zinc-900', 'hover:bg-neutral-50', 'hover:bg-neutral-100', 'hover:bg-neutral-200', 'hover:bg-neutral-300', 'hover:bg-neutral-400', 'hover:bg-neutral-500', 'hover:bg-neutral-600', 'hover:bg-neutral-700', 'hover:bg-neutral-800', 'hover:bg-neutral-900', 'hover:bg-stone-50', 'hover:bg-stone-100', 'hover:bg-stone-200', 'hover:bg-stone-300', 'hover:bg-stone-400', 'hover:bg-stone-500', 'hover:bg-stone-600', 'hover:bg-stone-700', 'hover:bg-stone-800', 'hover:bg-stone-900', // Cursor states 'cursor-move', 'cursor-ns-resize', 'cursor-ew-resize', 'cursor-nw-resize', 'cursor-ne-resize', 'cursor-sw-resize', 'cursor-se-resize', // Resize handles 'resize-x', 'resize-y', 'resize-none' ] } ``` ### Timezone Issues? The calendar handles timezone conversions automatically and stores all events in UTC format. If you're experiencing timezone-related issues: #### Common Timezone Problems 1. **Events showing wrong times**: The calendar stores events in UTC and displays them in the user's local timezone 2. **Drag-and-drop time changes**: Events should preserve their original time when dragged between dates 3. **Inconsistent time display**: Ensure your application is handling timezone conversions correctly #### Timezone Best Practices ```typescript // โœ… Correct: Store events in UTC const event = { id: '1', title: 'Meeting', start: '2025-01-15T14:00:00Z', // UTC time end: '2025-01-15T15:00:00Z', // UTC time tailwindColor: 'blue' } // โŒ Incorrect: Store events in local time const event = { id: '1', title: 'Meeting', start: '2025-01-15T14:00:00', // Local time (missing Z) end: '2025-01-15T15:00:00', // Local time (missing Z) tailwindColor: 'blue' } ``` #### Debugging Timezone Issues Enable debug logging to see timezone conversions: ```typescript // In your main.ts or main.js import { debug } from '@kodeglot/vue-calendar' // Enable debug logging debug.enable() ``` Look for these debug messages: - `[Vue Calendar Debug] Store: Updating event date only` - Shows time preservation during drag-and-drop - `[Vue Calendar Debug] Timezone: Converting to user timezone` - Shows timezone conversions - `[CalendarView] event-updated fired:` - Shows event update emissions #### Timezone Troubleshooting Checklist - [ ] All event times are stored in UTC format (with 'Z' suffix) - [ ] Your application handles timezone conversions correctly - [ ] Debug logging shows correct time preservation during drag-and-drop - [ ] Events maintain their original time when moved between dates - [ ] No hardcoded timezone offsets in your application code