Calendar/packages/calendar
Janus C. H. Knudsen 7db22245e2 Adds comprehensive README for calendar package
Introduces detailed documentation for calendar library, covering:
- Core features and architecture
- Installation and setup guide
- Event system reference
- Types and configuration options
- Extensibility and utilities

Prepares for initial library release with thorough documentation
2026-02-02 23:34:17 +01:00
..
src Sets up calendar package with core infrastructure 2026-01-28 15:24:03 +01:00
build.js Sets up calendar package with core infrastructure 2026-01-28 15:24:03 +01:00
package-lock.json Sets up calendar package with core infrastructure 2026-01-28 15:24:03 +01:00
package.json Adds comprehensive README for calendar package 2026-02-02 23:34:17 +01:00
README.md Adds comprehensive README for calendar package 2026-02-02 23:34:17 +01:00
tsconfig.json Sets up calendar package with core infrastructure 2026-01-28 15:24:03 +01:00

Calendar

Professional TypeScript calendar component with offline-first architecture, drag-and-drop functionality, and real-time synchronization capabilities.

Features

  • Multiple View Modes: Date-based (day/week/month) and resource-based (people, rooms) views
  • Drag & Drop: Smooth event dragging with snap-to-grid, cross-column movement, and timed/all-day conversion
  • Event Resizing: Intuitive resize handles for adjusting event duration
  • Offline-First: IndexedDB storage with automatic background sync
  • Event-Driven Architecture: Decoupled components via centralized EventBus
  • Dependency Injection: Built on NovaDI for clean, testable architecture
  • Extensions: Modular extensions for teams, departments, bookings, customers, schedules, and audit logging

Installation

npm install calendar

Quick Start (AI-Friendly Setup Guide)

Step 1: Create HTML Structure

<!DOCTYPE html>
<html>
<head>
  <link rel="stylesheet" href="node_modules/calendar/dist/css/calendar.css">
</head>
<body>
  <div class="calendar-wrapper">
    <swp-calendar-container>
      <swp-time-axis>
        <swp-header-spacer></swp-header-spacer>
        <swp-time-axis-content id="time-axis"></swp-time-axis-content>
      </swp-time-axis>
      <swp-grid-container>
        <swp-header-viewport>
          <swp-header-track>
            <swp-calendar-header></swp-calendar-header>
          </swp-header-track>
          <swp-header-drawer></swp-header-drawer>
        </swp-header-viewport>
        <swp-content-viewport>
          <swp-content-track>
            <swp-scrollable-content>
              <swp-time-grid>
                <swp-grid-lines></swp-grid-lines>
                <swp-day-columns></swp-day-columns>
              </swp-time-grid>
            </swp-scrollable-content>
          </swp-content-track>
        </swp-content-viewport>
      </swp-grid-container>
    </swp-calendar-container>
  </div>

  <script type="module" src="dist/bundle.js"></script>
</body>
</html>

Step 2: Initialize Calendar

import { Container } from '@novadi/core';
import {
  registerCoreServices,
  CalendarApp,
  IndexedDBContext,
  SettingsService,
  ViewConfigService,
  EventService,
  EventBus,
  CalendarEvents
} from 'calendar';

async function init() {
  // 1. Create DI container and register services
  const container = new Container();
  const builder = container.builder();
  registerCoreServices(builder, {
    dbConfig: { dbName: 'MyCalendarDB', dbVersion: 1 }
  });
  const app = builder.build();

  // 2. Initialize IndexedDB
  const dbContext = app.resolveType<IndexedDBContext>();
  await dbContext.initialize();

  // 3. Seed required settings (first time only)
  const settingsService = app.resolveType<SettingsService>();
  const viewConfigService = app.resolveType<ViewConfigService>();

  await settingsService.save({
    id: 'grid',
    dayStartHour: 8,
    dayEndHour: 17,
    workStartHour: 9,
    workEndHour: 16,
    hourHeight: 64,
    snapInterval: 15,
    syncStatus: 'synced'
  });

  await settingsService.save({
    id: 'workweek',
    presets: {
      standard: { id: 'standard', label: 'Standard', workDays: [1, 2, 3, 4, 5], periodDays: 7 }
    },
    defaultPreset: 'standard',
    firstDayOfWeek: 1,
    syncStatus: 'synced'
  });

  await viewConfigService.save({
    id: 'simple',
    groupings: [{ type: 'date', values: [], idProperty: 'date', derivedFrom: 'start' }],
    syncStatus: 'synced'
  });

  // 4. Initialize CalendarApp
  const calendarApp = app.resolveType<CalendarApp>();
  const containerEl = document.querySelector('swp-calendar-container') as HTMLElement;
  await calendarApp.init(containerEl);

  // 5. Render a view
  const eventBus = app.resolveType<EventBus>();
  eventBus.emit(CalendarEvents.CMD_RENDER, { viewId: 'simple' });
}

init().catch(console.error);

Step 3: Add Events

const eventService = app.resolveType<EventService>();

await eventService.save({
  id: crypto.randomUUID(),
  title: 'Meeting',
  start: new Date('2024-01-15T09:00:00'),
  end: new Date('2024-01-15T10:00:00'),
  type: 'meeting',
  allDay: false,
  syncStatus: 'synced'
});

Architecture

Core Components

Component Description
CalendarApp Main application entry point
CalendarOrchestrator Coordinates rendering pipeline
EventBus Central event dispatcher for all inter-component communication
DateService Date calculations and formatting
IndexedDBContext Offline storage infrastructure

Managers

Manager Description
DragDropManager Event drag-drop with smooth animations and snap-to-grid
EdgeScrollManager Automatic scrolling at viewport edges during drag
ResizeManager Event resizing with visual feedback
ScrollManager Scroll behavior and position management
HeaderDrawerManager All-day events drawer toggle
EventPersistenceManager Saves drag/resize changes to storage

Renderers

Renderer Description
DateRenderer Renders date-based column groupings
ResourceRenderer Renders resource-based column groupings
EventRenderer Renders timed events in columns
ScheduleRenderer Renders working hours backgrounds
HeaderDrawerRenderer Renders all-day events in header
TimeAxisRenderer Renders time labels on the left axis

Storage

Service Description
EventService / EventStore Calendar event CRUD
ResourceService / ResourceStore Resource management
SettingsService / SettingsStore Tenant settings
ViewConfigService / ViewConfigStore View configurations

Events Reference

Lifecycle Events

Event Constant Payload Description
core:initialized CoreEvents.INITIALIZED - Calendar core initialized
core:ready CoreEvents.READY - Calendar ready for interaction
core:destroyed CoreEvents.DESTROYED - Calendar destroyed

View Events

Event Constant Payload Description
view:changed CoreEvents.VIEW_CHANGED { viewId: string } View type changed
view:rendered CoreEvents.VIEW_RENDERED - View finished rendering

Navigation Events

Event Constant Payload Description
nav:date-changed CoreEvents.DATE_CHANGED { date: Date } Current date changed
nav:navigation-completed CoreEvents.NAVIGATION_COMPLETED - Navigation animation completed

Data Events

Event Constant Payload Description
data:loading CoreEvents.DATA_LOADING - Data loading started
data:loaded CoreEvents.DATA_LOADED - Data loading completed
data:error CoreEvents.DATA_ERROR { error: Error } Data loading error

Grid Events

Event Constant Payload Description
grid:rendered CoreEvents.GRID_RENDERED - Grid finished rendering
grid:clicked CoreEvents.GRID_CLICKED { time: Date, columnKey: string } Grid area clicked

Event Management

Event Constant Payload Description
event:created CoreEvents.EVENT_CREATED ICalendarEvent Event created
event:updated CoreEvents.EVENT_UPDATED IEventUpdatedPayload Event updated
event:deleted CoreEvents.EVENT_DELETED { eventId: string } Event deleted
event:selected CoreEvents.EVENT_SELECTED { eventId: string } Event selected

Drag-Drop Events

Event Constant Payload Description
event:drag-start CoreEvents.EVENT_DRAG_START IDragStartPayload Drag started
event:drag-move CoreEvents.EVENT_DRAG_MOVE IDragMovePayload Dragging (throttled)
event:drag-end CoreEvents.EVENT_DRAG_END IDragEndPayload Drag completed
event:drag-cancel CoreEvents.EVENT_DRAG_CANCEL IDragCancelPayload Drag cancelled
event:drag-column-change CoreEvents.EVENT_DRAG_COLUMN_CHANGE IDragColumnChangePayload Moved to different column

Header Drag Events (Timed to All-Day Conversion)

Event Constant Payload Description
event:drag-enter-header CoreEvents.EVENT_DRAG_ENTER_HEADER IDragEnterHeaderPayload Entered header area
event:drag-move-header CoreEvents.EVENT_DRAG_MOVE_HEADER IDragMoveHeaderPayload Moving in header area
event:drag-leave-header CoreEvents.EVENT_DRAG_LEAVE_HEADER IDragLeaveHeaderPayload Left header area

Resize Events

Event Constant Payload Description
event:resize-start CoreEvents.EVENT_RESIZE_START IResizeStartPayload Resize started
event:resize-end CoreEvents.EVENT_RESIZE_END IResizeEndPayload Resize completed

Edge Scroll Events

Event Constant Payload Description
edge-scroll:tick CoreEvents.EDGE_SCROLL_TICK { deltaY: number } Scroll tick during edge scroll
edge-scroll:started CoreEvents.EDGE_SCROLL_STARTED - Edge scrolling started
edge-scroll:stopped CoreEvents.EDGE_SCROLL_STOPPED - Edge scrolling stopped

Sync Events

Event Constant Payload Description
sync:started CoreEvents.SYNC_STARTED - Background sync started
sync:completed CoreEvents.SYNC_COMPLETED - Background sync completed
sync:failed CoreEvents.SYNC_FAILED { error: Error } Background sync failed

Entity Events

Event Constant Payload Description
entity:saved CoreEvents.ENTITY_SAVED IEntitySavedPayload Entity saved to storage
entity:deleted CoreEvents.ENTITY_DELETED IEntityDeletedPayload Entity deleted from storage

Audit Events

Event Constant Payload Description
audit:logged CoreEvents.AUDIT_LOGGED IAuditLoggedPayload Audit entry logged

Rendering Events

Event Constant Payload Description
events:rendered CoreEvents.EVENTS_RENDERED - Events finished rendering

System Events

Event Constant Payload Description
system:error CoreEvents.ERROR { error: Error, context?: string } System error occurred

Command Events (Host to Calendar)

Use these to control the calendar from your application:

import { EventBus, CalendarEvents } from 'calendar';

const eventBus = app.resolveType<EventBus>();

// Navigate
eventBus.emit(CalendarEvents.CMD_NAVIGATE_PREV);
eventBus.emit(CalendarEvents.CMD_NAVIGATE_NEXT);

// Render a view
eventBus.emit(CalendarEvents.CMD_RENDER, { viewId: 'simple' });

// Toggle header drawer
eventBus.emit(CalendarEvents.CMD_DRAWER_TOGGLE);

// Change workweek preset
eventBus.emit(CalendarEvents.CMD_WORKWEEK_CHANGE, { presetId: 'standard' });

// Update view grouping
eventBus.emit(CalendarEvents.CMD_VIEW_UPDATE, { type: 'resource', values: ['r1', 'r2'] });
Command Constant Payload Description
Navigate Previous CalendarEvents.CMD_NAVIGATE_PREV - Go to previous period
Navigate Next CalendarEvents.CMD_NAVIGATE_NEXT - Go to next period
Render View CalendarEvents.CMD_RENDER { viewId: string } Render specified view
Toggle Drawer CalendarEvents.CMD_DRAWER_TOGGLE - Toggle all-day drawer
Change Workweek CalendarEvents.CMD_WORKWEEK_CHANGE { presetId: string } Change workweek preset
Update View CalendarEvents.CMD_VIEW_UPDATE { type: string, values: string[] } Update grouping values

Types

Core Types

// Event types
type CalendarEventType = 'customer' | 'vacation' | 'break' | 'meeting' | 'blocked';

interface ICalendarEvent {
  id: string;
  title: string;
  description?: string;
  start: Date;
  end: Date;
  type: CalendarEventType;
  allDay: boolean;
  bookingId?: string;
  resourceId?: string;
  customerId?: string;
  recurringId?: string;
  syncStatus: SyncStatus;
  metadata?: Record<string, unknown>;
}

// Resource types
type ResourceType = 'person' | 'room' | 'equipment' | 'vehicle' | 'custom';

interface IResource {
  id: string;
  name: string;
  displayName: string;
  type: ResourceType;
  avatarUrl?: string;
  color?: string;
  isActive?: boolean;
  defaultSchedule?: IWeekSchedule;
  syncStatus: SyncStatus;
}

// Sync status
type SyncStatus = 'synced' | 'pending' | 'error';

Settings Types

interface IGridSettings {
  id: 'grid';
  dayStartHour: number;
  dayEndHour: number;
  workStartHour: number;
  workEndHour: number;
  hourHeight: number;
  snapInterval: number;
}

interface IWorkweekPreset {
  id: string;
  workDays: number[];      // ISO weekdays: 1=Monday, 7=Sunday
  label: string;
  periodDays: number;      // Navigation step (1=day, 7=week)
}

interface IWeekSchedule {
  [day: number]: ITimeSlot | null;  // null = off that day
}

interface ITimeSlot {
  start: string;  // "HH:mm"
  end: string;    // "HH:mm"
}

Drag-Drop Payloads

interface IDragStartPayload {
  eventId: string;
  element: HTMLElement;
  ghostElement: HTMLElement;
  startY: number;
  mouseOffset: { x: number; y: number };
  columnElement: HTMLElement;
}

interface IDragEndPayload {
  swpEvent: SwpEvent;
  sourceColumnKey: string;
  target: 'grid' | 'header';
}

Extensions

Import extensions separately to keep bundle size minimal:

// Teams extension
import { registerTeams, TeamService, TeamStore, TeamRenderer } from 'calendar/teams';

// Departments extension
import { registerDepartments, DepartmentService, DepartmentStore } from 'calendar/departments';

// Bookings extension
import { registerBookings, BookingService, BookingStore } from 'calendar/bookings';

// Customers extension
import { registerCustomers, CustomerService, CustomerStore } from 'calendar/customers';

// Schedules extension (working hours)
import { registerSchedules, ResourceScheduleService, ScheduleOverrideService } from 'calendar/schedules';

// Audit extension
import { registerAudit, AuditService, AuditStore } from 'calendar/audit';

// Register with container builder
const builder = container.builder();
registerCoreServices(builder);
registerTeams(builder);
registerSchedules(builder);
// ... etc

Configuration

Calendar Options

interface ICalendarOptions {
  timeConfig?: ITimeFormatConfig;
  gridConfig?: IGridConfig;
  dbConfig?: IDBConfig;
}

// Time format configuration
interface ITimeFormatConfig {
  timezone: string;         // e.g., 'Europe/Copenhagen'
  use24HourFormat: boolean;
  locale: string;           // e.g., 'da-DK'
  dateFormat: string;
  showSeconds: boolean;
}

// Grid configuration
interface IGridConfig {
  hourHeight: number;               // Pixels per hour (default: 64)
  dayStartHour: number;             // Grid start hour (default: 6)
  dayEndHour: number;               // Grid end hour (default: 18)
  snapInterval: number;             // Minutes to snap to (default: 15)
  gridStartThresholdMinutes: number;
}

// Database configuration
interface IDBConfig {
  dbName: string;    // IndexedDB database name
  dbVersion: number; // Schema version
}

Default Configuration

import {
  defaultTimeFormatConfig,
  defaultGridConfig,
  defaultDBConfig
} from 'calendar';

// Defaults:
// timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
// use24HourFormat: true
// locale: 'da-DK'
// hourHeight: 64
// dayStartHour: 6
// dayEndHour: 18
// snapInterval: 15

Utilities

Position Utilities

import {
  calculateEventPosition,
  minutesToPixels,
  pixelsToMinutes,
  snapToGrid
} from 'calendar';

// Convert time to pixels
const pixels = minutesToPixels(120, 64); // 120 mins at 64px/hour = 128px

// Snap to 15-minute grid
const snapped = snapToGrid(new Date(), 15);

Event Layout Engine

import { eventsOverlap, calculateColumnLayout } from 'calendar';

// Check if two events overlap
const overlap = eventsOverlap(event1, event2);

// Calculate layout for overlapping events
const layout = calculateColumnLayout(events);

Listening to Events

import { EventBus, CoreEvents } from 'calendar';

const eventBus = app.resolveType<EventBus>();

// Subscribe to event updates
eventBus.on(CoreEvents.EVENT_UPDATED, (e: Event) => {
  const { eventId, sourceColumnKey, targetColumnKey } = (e as CustomEvent).detail;
  console.log(`Event ${eventId} moved from ${sourceColumnKey} to ${targetColumnKey}`);
});

// Subscribe to drag events
eventBus.on(CoreEvents.EVENT_DRAG_END, (e: Event) => {
  const { swpEvent, target } = (e as CustomEvent).detail;
  console.log(`Dropped on ${target}:`, swpEvent);
});

// One-time listener
eventBus.once(CoreEvents.READY, () => {
  console.log('Calendar is ready!');
});

CSS Customization

The calendar uses CSS custom properties for theming. Override these in your CSS:

:root {
  --calendar-hour-height: 64px;
  --calendar-header-height: 48px;
  --calendar-column-min-width: 120px;
  --calendar-event-border-radius: 4px;
}

Dependencies

  • @novadi/core - Dependency injection framework (peer dependency)
  • dayjs - Date manipulation and formatting

License

Proprietary - SWP