Skip to content

taskgenius/calendar

Repository files navigation

@taskgenius/calendar

npm version CI License: MIT npm downloads

Used in taskgenius/taskgenius-plugin

A lightweight, configurable TypeScript calendar component library with drag-and-drop support.

Features

  • Three view modes - month, week, and day views with dedicated all-day lane
  • Extensible view system - register custom views or extend built-ins
  • Cross-midnight & multi-day support - timed/all-day events render and drag correctly across days
  • Drag-and-drop/resize - move or resize events with optional date-only mode
  • Flexible layout controls - first day of week, hide weekends, custom day/time filters
  • Overflow handling - event count badges plus configurable "+N more" popover renderer
  • Custom rendering hooks - date cells, event styling, event content, and popovers
  • Lightweight - <12KB gzipped with zero runtime deps
  • Pluggable adapters - Day.js by default, custom adapters supported
  • TypeScript first - complete type definitions and SOLID architecture

Installation

npm install @taskgenius/calendar dayjs

Quick Start

import { Calendar } from '@taskgenius/calendar';
import '@taskgenius/calendar/styles.css'; // Required - component uses CSS classes

// Method 1: Pass CSS selector string
const calendar = new Calendar('#app', {
  view: { type: 'week' },
  events: [
    {
      id: '1',
      title: 'Team Meeting',
      start: '2025-11-20 10:00',
      end: '2025-11-20 11:30',
      color: '#3b82f6'
    }
  ],
  onEventClick: (event) => {
    console.log('Clicked:', event.title);
  },
  onEventDrop: (event, newStart, newEnd) => {
    console.log('Moved:', event.title, newStart.toISOString(), newEnd.toISOString());
  }
});

// Method 2: Pass HTMLElement directly
const container = document.getElementById('app');
const calendar2 = new Calendar(container, {
  view: { type: 'month' }
});

Tree Shaking & Optimization

For users who want to minimize bundle size, you can use CalendarCore instead of Calendar. The CalendarCore class does not import any built-in views by default, allowing your bundler to exclude unused view code.

Using Calendar (Recommended)

The default Calendar class provides a "batteries-included" experience with automatic view registration:

import { Calendar } from '@taskgenius/calendar';
import '@taskgenius/calendar/styles.css';

// All built-in views (Month, Week, Day) are automatically available
const calendar = new Calendar('#app', {
  view: { type: 'month' }
});

Using CalendarCore (Advanced - Smaller Bundle)

For optimal bundle size, use CalendarCore with explicit view registration:

import { CalendarCore, MonthView, ViewRegistry } from '@taskgenius/calendar';
import '@taskgenius/calendar/styles.css';

// Only imports MonthView code - Week and Day views are tree-shaken
const calendar = new CalendarCore('#app', {
  view: { type: 'month' },
  viewRegistry: new ViewRegistry().register(MonthView)
});

Multiple views example:

import { CalendarCore, MonthView, WeekView, ViewRegistry } from '@taskgenius/calendar';

const registry = new ViewRegistry()
  .register(MonthView)
  .register(WeekView);

const calendar = new CalendarCore('#app', {
  view: { type: 'week' },
  viewRegistry: registry
});

When to Use Each

  • Use Calendar: Quick start, prototyping, or when bundle size is not a concern
  • Use CalendarCore: Production builds where bundle optimization is important

Styles

Important: The component requires @taskgenius/calendar/styles.css to render correctly. Since v0.4.0, all layout and styling use external CSS classes instead of inline styles.

Import options:

  • ES modules: import '@taskgenius/calendar/styles.css'; (recommended)
  • HTML link: <link rel="stylesheet" href="/node_modules/@taskgenius/calendar/dist/styles.css">

Customization:

  • Theme settings are delivered via CSS variables: --tg-primary-color, --tg-primary-rgb, --tg-cell-height, --tg-font-header, --tg-font-event. The library sets these on the .tg-calendar root; you can override them in your own styles.
  • To fully customize the look, you can override the default CSS or provide your own styles using the exposed tg-* class names.

API Reference

Calendar Class

The main entry point for the calendar component. This class extends CalendarCore and automatically registers all built-in views (Month, Week, Day) for a "batteries-included" experience. Perfect for quick starts and when bundle size is not a primary concern.

For advanced users: Use CalendarCore directly for a leaner, tree-shakeable version that requires manual view registration. See the Tree Shaking & Optimization section for details.

Constructor

new Calendar(container: string | HTMLElement, config?: CalendarConfig)

Parameters:

  • container - CSS selector string (e.g., '#app') or HTMLElement reference
  • config - Optional configuration object

Example:

// Using CSS selector
const cal1 = new Calendar('#calendar', { view: { type: 'week' } });

// Using HTMLElement
const element = document.getElementById('calendar');
const cal2 = new Calendar(element, { view: { type: 'week' } });

Methods

Method Description
registerView(ViewClass, options?) Register a custom view (class must expose static meta)
unregisterView(type) Remove a registered view type
getRegisteredViews() Get metadata for all registered views
getViewRegistry() Access the view registry instance
hasView(type) Check whether a view type is registered
setView(type: ViewType | string) Switch between registered views (built-in or custom)
getView() Get current view type
getActiveView() Get the active view instance
addEvent(event: CalendarEvent) Add a new event
removeEvent(id: string) Remove event by ID
updateEvent(id: string, updates: Partial<CalendarEvent>) Update event properties
setEvents(events: CalendarEvent[]) Replace all events
getEvents() Get all events
next() Navigate to next period
prev() Navigate to previous period
today() Navigate to today
goToDate(date: string | Date) Navigate to specific date
getCurrentDate() Get current displayed date (ISO string)
setDraggable(enabled: boolean) Enable or disable drag-and-drop at runtime
isDraggable() Check whether drag-and-drop is enabled
refresh() Force re-render
destroy() Cleanup and remove calendar

Configuration

interface CalendarConfig {
  view?: ViewConfig;
  events?: CalendarEvent[];
  draggable?: DraggableConfig;
  theme?: ThemeConfig;
  dateAdapter?: DateAdapter<unknown>;  // Custom date adapter (default: Day.js)
  dateFormats?: Partial<DateFormatConfig>;  // Custom date display formats (unicode tokens recommended)
  headerFormat?: {                    // Deprecated: mapped to dateFormats
    month?: string;
    day?: string;
  };
  showEventCounts?: boolean;          // Default: false - Show event count badges on date cells
  
  // Event interactions
  onEventClick?: (event: CalendarEvent) => void;
  onEventDoubleClick?: (event: CalendarEvent) => void;
  onEventContextMenu?: (event: CalendarEvent, x: number, y: number) => void;
  onEventDrop?: (event: CalendarEvent, newStart: Date, newEnd: Date) => void;    // v0.8.0+: Date objects
  onEventResize?: (event: CalendarEvent, newStart: Date, newEnd: Date) => void;  // v0.9.0+: Resize callback
  
  // View and navigation
  onViewChange?: (viewType: ViewType) => void;
  onDateChange?: (date: Date) => void;
  
  // Date cell interactions (month view)
  onDateClick?: (date: Date) => void;
  onDateDoubleClick?: (date: Date) => void;
  onDateContextMenu?: (date: Date, x: number, y: number) => void;
  
  // Time slot interactions (week/day view)
  onTimeSlotClick?: (dateTime: Date) => void;
  onTimeSlotDoubleClick?: (dateTime: Date) => void;
  onTimeSlotContextMenu?: (dateTime: Date, x: number, y: number) => void;
  
  // Range selection (drag to select multiple cells)
  onDateRangeSelect?: (startDate: Date, endDate: Date) => void;
  onTimeRangeSelect?: (startDateTime: Date, endDateTime: Date) => void;
  
  // Rendering hooks
  onRenderDateCell?: (ctx: DateCellContext) => void;      // Custom date cell rendering
  onStyleEvent?: (event: CalendarEvent) => EventStyle;    // Custom event styling
  onRenderEvent?: (ctx: EventRenderContext) => void;      // v0.12.0+: Custom event content
  onRenderMoreEventsPopover?: (                          // v0.10.0+: Custom "+N more" popover renderer
    events: CalendarEvent[],
    date: Date,
    anchorEl: HTMLElement,
    defaultRender: () => void
  ) => void;
}

For custom view registries, the constructor also accepts:

interface ExtendedCalendarConfig extends CalendarConfig {
  viewRegistry?: ViewRegistry;       // Provide a custom registry
  registerBuiltInViews?: boolean;    // Default: true - auto-register Month/Week/Day views
}

ViewConfig

interface ViewConfig {
  type: 'month' | 'week' | 'day';   // Default: 'week'
  showDateHeader?: boolean;          // Default: true (time views)
  showWeekNumbers?: boolean;         // Default: false (month view)
  firstDayOfWeek?: 0 | 1 | 2 | 3 | 4 | 5 | 6;  // Default: 0 (supports full 0-6 range)
  showWeekends?: boolean;            // Default: true (false is converted to a dayFilter)
  maxEventsPerRow?: number;          // Month view: cap events per row before showing "+N more"
  dayFilter?: (date: unknown, ctx: DayFilterContext) => DayFilterResult; // Hide/customize days
  timeFilter?: (hour: number) => TimeFilterResult;   // Hide/customize time slots
  timeFormatter?: TimeFormatter;     // Custom time axis labels
}

dayFilter/timeFilter can return boolean or config objects (DayRenderConfig / TimeSlotConfig). Setting DayRenderConfig.disabled keeps the cell visible but hides events for that date.

DraggableConfig

interface DraggableConfig {
  enabled: boolean;      // Default: true
  snapMinutes?: number;  // Default: 15
  ghostOpacity?: number; // Default: 0.5
  dateOnly?: boolean;    // Default: false - Only adjust dates, keep time unchanged
}

ThemeConfig

interface ThemeConfig {
  primaryColor?: string;  // Default: '#3b82f6'
  cellHeight?: number;    // Default: 60 (pixels per hour)
  fontSize?: {
    header?: string;      // Default: '14px'
    event?: string;       // Default: '12px'
  };
}

CalendarEvent

interface CalendarEvent {
  id: string;                        // Unique identifier
  title: string;                     // Display title
  start: string;                     // ISO format: 'yyyy-MM-dd HH:mm' (supports cross-midnight spans)
  end: string;                       // ISO format: 'yyyy-MM-dd HH:mm' (use 00:00/23:59 for all-day)
  color?: string;                    // CSS color value
  metadata?: Record<string, unknown>; // Custom data
}

Examples

Basic Usage

import { Calendar } from '@taskgenius/calendar';
import '@taskgenius/calendar/styles.css'; // Required

// Initialize with CSS selector
const calendar = new Calendar('#calendar-container');

// Or initialize with DOM element
const element = document.querySelector('.my-calendar');
const calendar2 = new Calendar(element);

// Add event
calendar.addEvent({
  id: '1',
  title: 'Meeting',
  start: '2025-11-20 10:00',
  end: '2025-11-20 11:30',
  color: '#3b82f6'
});

// Switch view
calendar.setView('month');

// Navigate
calendar.next();
calendar.prev();
calendar.today();

// Clean up when done (important to prevent memory leaks)
calendar.destroy();

With Callbacks

const calendar = new Calendar('#app', {
  events: myEvents,
  onEventClick: (event) => {
    showEventDetails(event);
  },
  onEventDrop: (event, newStart, newEnd) => {
    // Event was moved to a new position
    console.log('Event moved:', event.title);
    saveEventToServer(event.id, { 
      start: newStart.toISOString(), 
      end: newEnd.toISOString() 
    });
  },
  onEventResize: (event, newStart, newEnd) => {
    // Event duration was changed
    console.log('Event resized:', event.title);
    saveEventToServer(event.id, { 
      start: newStart.toISOString(), 
      end: newEnd.toISOString() 
    });
  },
  onViewChange: (view) => {
    analytics.track('view_changed', { view });
  }
});

Custom Theme

const calendar = new Calendar('#app', {
  theme: {
    primaryColor: '#8b5cf6',
    cellHeight: 80,
    fontSize: {
      header: '16px',
      event: '14px'
    }
  }
});

Disable Drag-and-Drop

const calendar = new Calendar('#app', {
  draggable: {
    enabled: false
  }
});

Date-Only Drag Mode

const calendar = new Calendar('#app', {
  draggable: {
    enabled: true,
    dateOnly: true  // Only adjust dates, preserve original time
  }
});

Week Configuration

const calendar = new Calendar('#app', {
  view: {
    type: 'week',
    firstDayOfWeek: 1,  // Start week on Monday
    showWeekends: false  // Hide Saturday and Sunday
  }
});

Day/Time Filtering

const calendar = new Calendar('#app', {
  view: {
    type: 'week',
    firstDayOfWeek: 1,
    dayFilter: (_date, ctx) => ctx.isWeekend ? { visible: false } : true, // Hide weekends with config
    timeFilter: (hour) => hour >= 8 && hour < 18,  // Working hours only
    timeFormatter: (hour) => `${hour}:00`          // Custom time axis labels
  }
});

Event Count Badges

const calendar = new Calendar('#app', {
  view: { type: 'month' },
  showEventCounts: true  // Display event count on each date cell
});

Month Overflow & "+N more" Popover (v0.10.0+)

const calendar = new Calendar('#app', {
  view: {
    type: 'month',
    maxEventsPerRow: 3
  },
  onRenderMoreEventsPopover: (events, date, anchorEl, defaultRender) => {
    console.log('Hidden events on', date.toISOString(), events.length);
    defaultRender(); // Or render a custom popover
  }
});

User Interactions (v0.7.0+)

const calendar = new Calendar('#app', {
  view: { type: 'month' },
  
  // Date cell interactions (month view)
  onDateClick: (date) => {
    console.log('Clicked date:', date); // Date object
    showCreateEventDialog(date);
  },
  
  onDateDoubleClick: (date) => {
    console.log('Double-clicked date:', date);
    createQuickEvent(date);
  },
  
  onDateContextMenu: (date, x, y) => {
    console.log('Right-clicked date:', date, 'at position:', x, y);
    showContextMenu(date, x, y);
  },
  
  // Range selection (drag to select)
  onDateRangeSelect: (startDate, endDate) => {
    console.log('Selected range:', startDate, 'to', endDate);
    createMultiDayEvent(startDate, endDate);
  }
});

// For week/day views with time slots
const weekCalendar = new Calendar('#app', {
  view: { type: 'week' },
  
  // Time slot interactions
  onTimeSlotClick: (dateTime) => {
    console.log('Clicked time slot:', dateTime); // Date object with time
    createEventAt(dateTime);
  },
  
  onTimeSlotDoubleClick: (dateTime) => {
    console.log('Double-clicked time slot:', dateTime);
    quickCreateEvent(dateTime);
  },
  
  onTimeSlotContextMenu: (dateTime, x, y) => {
    console.log('Right-clicked time slot:', dateTime);
    showTimeSlotMenu(dateTime, x, y);
  },
  
  // Time range selection (drag to select multiple slots)
  onTimeRangeSelect: (startDateTime, endDateTime) => {
    console.log('Selected time range:', startDateTime, 'to', endDateTime);
    createTimedEvent(startDateTime, endDateTime);
  }
});

Custom Date Formats (v0.8.0+)

const calendar = new Calendar('#app', {
  // Customize date display formats (uses Unicode tokens)
  dateFormats: {
    date: 'yyyy/MM/dd',           // Default: 'yyyy-MM-dd'
    dateTime: 'yyyy/MM/dd HH:mm', // Default: 'yyyy-MM-dd HH:mm'
    time: 'HH:mm',                // Default: 'HH:mm'
    monthHeader: 'MMMM yyyy',     // Default: 'yyyy\u5e74M\u6708'
    dayHeader: 'MMMM d, yyyy'     // Default: 'yyyy\u5e74M\u6708d\u65e5'
  }
});

Note: Date format tokens use Unicode standard (compatible with date-fns, Day.js, and native adapter):

  • Year: yyyy (2025), yy (25)
  • Month: MM (01-12), M (1-12), MMMM (January), MMM (Jan)
  • Day: dd (01-31), d (1-31)
  • Hour: HH (00-23), H (0-23)
  • Minute: mm (00-59), m (0-59)
  • Legacy headerFormat is deprecated; it maps to dateFormats.monthHeader/dayHeader for backward compatibility.

Custom Date Cell Rendering

const calendar = new Calendar('#app', {
  onRenderDateCell: (ctx) => {
    // Add custom badge for past due dates with events
    if (ctx.isPastDue && ctx.events.length > 0) {
      const badge = document.createElement('div');
      badge.className = 'overdue-badge';
      badge.textContent = '!';
      ctx.cellEl.appendChild(badge);
    }
    
    // Add custom class for weekends
    if (ctx.date.getDay() === 0 || ctx.date.getDay() === 6) {
      ctx.cellEl.classList.add('weekend');
    }
  }
});

Add corresponding CSS for custom elements:

/* Style custom overdue badge */
.overdue-badge {
  position: absolute;
  top: 2px;
  right: 2px;
  background: #ef4444;
  color: white;
  border-radius: 50%;
  width: 18px;
  height: 18px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
  font-weight: bold;
}

/* Style weekend cells */
.weekend {
  background-color: #fef3c7;
}

Custom Event Styling

const calendar = new Calendar('#app', {
  onStyleEvent: (event) => {
    // Style based on metadata
    const priority = event.metadata?.priority as number;
    const isCompleted = event.metadata?.completed as boolean;
    
    return {
      color: priority >= 2 ? '#ef4444' : '#3b82f6',
      opacity: isCompleted ? 0.5 : 1,
      className: isCompleted ? 'completed-event' : ''
    };
  }
});

Add CSS for custom event classes (optional):

/* Additional styling for completed events */
.completed-event {
  text-decoration: line-through;
}

Custom Event Rendering (v0.12.0+)

const calendar = new Calendar('#app', {
  onRenderEvent: (ctx) => {
    ctx.defaultRender(); // Keep default title/time rendering

    if (ctx.event.metadata?.priority === 'high') {
      const badge = document.createElement('span');
      badge.className = 'priority-badge';
      badge.textContent = '!';
      ctx.el.appendChild(badge);
    }
  }
});

Architecture

The library follows SOLID principles with a modular architecture:

src/
├── core/           # Main Calendar class, EventManager, DragController
├── adapters/       # Date library adapters (DayJs, etc.)
├── engines/        # Layout calculation (MonthEngine, TimeEngine)
├── renderers/      # DOM rendering (MonthRenderer, TimeRenderer)
├── styles/         # Static CSS + theme variable helpers (no auto-injection)
├── types/          # TypeScript type definitions
└── utils/          # DOM utilities

Key Components

  • Calendar - Main API and orchestration
  • EventManager - CRUD operations for events
  • DragController - Drag-and-drop interactions
  • MonthEngine/TimeEngine - Layout calculations
  • MonthRenderer/TimeRenderer - DOM generation
  • DateAdapter - Pluggable date library interface

Testing

# Run tests
npm test

# Run tests with UI
npm run test:ui

# Run tests with coverage
npm run test:coverage

Best Practices

Memory Management

Always call destroy() when you no longer need the calendar instance to prevent memory leaks:

// In React
useEffect(() => {
  const calendar = new Calendar(containerRef.current, config);
  
  return () => {
    calendar.destroy(); // Cleanup on unmount
  };
}, []);

// In Vue
onMounted(() => {
  calendar = new Calendar(el.value, config);
});

onUnmounted(() => {
  calendar.destroy(); // Cleanup on unmount
});

// In vanilla JS
function createCalendar() {
  const calendar = new Calendar('#app', config);
  
  // When removing the calendar
  function cleanup() {
    calendar.destroy();
    document.getElementById('app').innerHTML = '';
  }
  
  return { calendar, cleanup };
}

Initialization Options

You can initialize the calendar using either a CSS selector or a direct DOM element reference:

// Option 1: CSS Selector (simple and convenient)
const calendar = new Calendar('#calendar', config);

// Option 2: DOM Element (useful in frameworks)
const container = document.getElementById('calendar');
const calendar = new Calendar(container, config);

// Option 3: Dynamic element (e.g., in React with refs)
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
  if (containerRef.current) {
    const calendar = new Calendar(containerRef.current, config);
    return () => calendar.destroy();
  }
}, []);

Development

# Install dependencies
npm install

# Run demo
npm run demo

# Build library
npm run build

# Type check
npx tsc --noEmit

Project Structure

@taskgenius/calendar/
├── src/                 # Source code
├── tests/               # Test files
│   ├── unit/           # Unit tests
│   ├── integration/    # Integration tests
│   └── fixtures/       # Test data
├── examples/
│   └── demo/           # Vite demo project
├── dist/               # Built output
└── docs/               # Documentation

Date Adapters

The library uses a pluggable date adapter system. By default, it uses Day.js.

Using Day.js (Default)

import { Calendar } from '@taskgenius/calendar';
// Automatically uses DayJsAdapter

Custom Adapter

Implement the DateAdapter interface to use a different date library:

interface DateAdapter<T> {
  create(date?: string | Date | T): T;
  parse(dateStr: string, format?: string): T;
  format(date: T, format: string): string;
  // ... other methods
}

License

MIT ©TaskGenius

Contributing

Contributions are welcome! Please read the contributing guidelines before submitting a PR.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

About

A lightweight, configurable TypeScript calendar component library with drag-and-drop support.

Resources

License

Stars

Watchers

Forks

Packages

No packages published