# Vue Calendar ๐๏ธ
[](https://www.npmjs.com/package/@kodeglot/vue-calendar)
[](https://opensource.org/licenses/MIT)
[](https://github.com/Kodeglot/vue-calendar/actions)
[](https://codecov.io/gh/Kodeglot/vue-calendar)
[](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
Your content
```
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