Introduces type-safe EventId with centralized normalization logic for clone and standard event IDs Refactors event ID management across multiple components to use consistent ID transformation methods Improves type safety and reduces potential ID-related bugs in drag-and-drop and event rendering
9.7 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
Calendar Plantempus is a professional TypeScript calendar component with offline-first architecture, drag-and-drop functionality, and real-time synchronization capabilities. Supports both date-based (day/week/month) and resource-based (people, rooms) calendar views.
Build & Development Commands
# Build the project (bundles to wwwroot/js/calendar.js)
npm run build
# Watch mode for development
npm run watch
# Clean build output
npm run clean
# Type check only
npx tsc --noEmit
# Run all tests (watch mode)
npm test
# Run tests once and exit
npm run test:run
# Run tests with UI
npm run test:ui
# Run single test file
npm test -- <test-file-name>
# CSS Development
npm run css:build # Build CSS
npm run css:watch # Watch and rebuild CSS
npm run css:build:prod # Build minified production CSS
npm run css:analyze # Analyze CSS metrics
Architecture
Core Design Pattern: Dependency Injection with NovaDI
The application uses NovaDI (@novadi/core) for dependency injection. All managers, services, and repositories are registered in src/index.ts and resolved through the DI container.
Key principle: Never instantiate managers or services directly with new. Always use constructor injection and register types in the container.
Event-Driven Architecture
The application uses a centralized EventBus (src/core/EventBus.ts) built on DOM CustomEvents for all inter-component communication. This is the ONLY way components should communicate.
- All event types are defined in
src/constants/CoreEvents.ts(reduced from 102+ to ~20 core events) - Components emit events via
eventBus.emit(CoreEvents.EVENT_NAME, payload) - Components subscribe via
eventBus.on(CoreEvents.EVENT_NAME, handler) - Never call methods directly between managers - always use events
Calendar Modes: Date vs Resource
The calendar supports two column modes, configured at initialization in src/index.ts:
Date Mode (DateColumnDataSource):
- Columns represent dates (day/week/month views)
- Uses
DateHeaderRendererandDateColumnRenderer
Resource Mode (ResourceColumnDataSource):
- Columns represent resources (people, rooms, equipment)
- Uses
ResourceHeaderRendererandResourceColumnRenderer - Events filtered per-resource via
IColumnInfo.events
Both modes implement IColumnDataSource interface, allowing polymorphic column handling.
Manager Hierarchy
CalendarManager (src/managers/CalendarManager.ts) - Top-level coordinator
- Manages calendar state (current view, current date)
- Orchestrates initialization sequence
- Coordinates other managers via EventBus
Key Managers:
- EventManager - Event CRUD operations, data loading from repository
- GridManager - Renders time grid structure
- NavigationManager - Date navigation and period calculations
- DragDropManager - Advanced drag-and-drop with smooth animations, type conversion (timed ↔ all-day), scroll compensation
- ResizeHandleManager - Event resizing with visual feedback
- AllDayManager - All-day event layout and rendering
- HeaderManager - Date headers and all-day event container
- ScrollManager - Scroll behavior and position management
- EdgeScrollManager - Automatic scrolling at viewport edges during drag
Repository Pattern
Data access is abstracted through IApiRepository interface (src/repositories/IApiRepository.ts):
- MockEventRepository, MockBookingRepository, MockCustomerRepository, MockResourceRepository - Development: Load from JSON files
- ApiEventRepository, ApiBookingRepository, ApiCustomerRepository, ApiResourceRepository - Production: Backend API calls
All repository methods accept an UpdateSource parameter ('local' | 'remote') to distinguish user actions from remote updates.
Entity Service Pattern
IEntityService (src/storage/IEntityService.ts) provides polymorphic entity handling:
EventService,BookingService,CustomerService,ResourceServiceimplement this interface- Encapsulates sync status manipulation (SyncManager delegates, never manipulates directly)
- Uses
entityTypediscriminator for runtime routing without switch statements - Enables polymorphic operations:
Array<IEntityService<any>>works across all entity types
Offline-First Sync Architecture
SyncManager (src/workers/SyncManager.ts) provides background synchronization:
- Local changes are written to IndexedDB immediately
- Operations are queued in OperationQueue
- SyncManager processes queue when online (5-second polling)
- Failed operations retry with exponential backoff (max 5 retries)
- Events have
syncStatus: 'synced' | 'pending' | 'error'
Rendering Strategy Pattern
EventRenderingService (src/renderers/EventRendererManager.ts) uses strategy pattern:
- IEventRenderer interface defines rendering contract
- DateEventRenderer - Renders timed events in day columns
- AllDayEventRenderer - Renders all-day events in header
- Strategies can be swapped without changing core logic
Layout Engines
EventStackManager (src/managers/EventStackManager.ts) - Uses CSS flexbox for overlapping events:
- Groups overlapping events into stacks
- Calculates flex positioning (basis, grow, shrink)
- Handles multi-column spanning events
AllDayLayoutEngine (src/utils/AllDayLayoutEngine.ts) - Row-based layout for all-day events:
- Detects overlaps and assigns row positions
- Supports collapsed view (max 4 rows) with "+N more" indicator
- Calculates container height dynamically
Configuration System
Configuration is loaded from wwwroot/data/calendar-config.json via ConfigManager:
- GridSettings - Hour height, work hours, snap interval
- DateViewSettings - Period type, first day of week
- TimeFormatConfig - Timezone, locale, 12/24-hour format
- WorkWeekSettings - Configurable work week presets
- Interaction - Enable/disable drag, resize, create
Access via injected Configuration instance, never load config directly.
Important Patterns & Conventions
Event Type Conversion (Drag & Drop)
When dragging events between timed grid and all-day area:
- Timed → All-day:
DragDropManageremitsdrag:mouseenter-header,AllDayManagercreates all-day clone - All-day → Timed:
DragDropManageremitsdrag:mouseenter-column,EventRenderingServicecreates timed clone - Original element is marked with
data-conversion-source="true" - Clone is marked with
data-converted-clone="true"
Scroll Compensation During Drag
DragDropManager tracks scroll delta during edge-scrolling:
- Listens to
edge-scroll:scrollingevents - Accumulates
scrollDeltaYfrom scroll events - Compensates dragged element position:
targetY = mouseY - scrollDeltaY - mouseOffset.y - Prevents visual "jumping" during scroll
Grid Snapping
When dropping events, snap to time grid:
- Get mouse Y position relative to column
- Convert to time using
PositionUtils.getTimeAtPosition() - Account for
mouseOffset.y(click position within event) - Snap to nearest
snapInterval(default 15 minutes)
Testing with Vitest
Tests use Vitest with jsdom environment:
- Config:
vitest.config.ts - Setup file:
test/setup.ts - Test helpers:
test/helpers/dom-helpers.ts,test/helpers/config-helpers.ts - Run single test:
npm test -- <test-file-name>
Key Files to Know
src/index.ts- DI container setup and initialization (also sets calendar mode: date vs resource)src/core/EventBus.ts- Central event dispatchersrc/constants/CoreEvents.ts- All event type constants (~34 core events)src/types/CalendarTypes.ts- Core type definitionssrc/types/ColumnDataSource.ts- Column abstraction for date/resource modessrc/storage/IEntityService.ts- Entity service interface for polymorphic syncsrc/managers/CalendarManager.ts- Main coordinatorsrc/managers/DragDropManager.ts- Detailed drag-drop architecture docssrc/configurations/CalendarConfig.ts- Configuration schemawwwroot/data/calendar-config.json- Runtime configuration
Common Tasks
Adding a New Event Type to CoreEvents
- Add constant to
src/constants/CoreEvents.ts - Define payload type in
src/types/EventTypes.ts - Emit with
eventBus.emit(CoreEvents.NEW_EVENT, payload) - Subscribe with
eventBus.on(CoreEvents.NEW_EVENT, handler)
Adding a New Manager
- Create in
src/managers/ - Inject dependencies via constructor (EventBus, Configuration, other managers)
- Register in DI container in
src/index.ts:builder.registerType(NewManager).as<NewManager>() - Communicate via EventBus only, never direct method calls
- Initialize in CalendarManager if needed
Modifying Event Data
Always go through EventManager:
- Create:
eventManager.createEvent(eventData) - Update:
eventManager.updateEvent(id, updates) - Delete:
eventManager.deleteEvent(id)
EventManager handles repository calls, event emission, and UI updates.
Debugging
Debug mode is enabled in development:
eventBus.setDebug(true); // In src/index.ts
Access debug interface in browser console:
window.calendarDebug.eventBus.getEventLog()
window.calendarDebug.calendarManager
window.calendarDebug.eventManager
window.calendarDebug.auditService
window.calendarDebug.syncManager
window.calendarDebug.app // Full DI container
Dependencies
- @novadi/core - Dependency injection framework
- dayjs - Date manipulation and formatting
- fuse.js - Fuzzy search for event filtering
- esbuild - Fast bundler for development
- vitest - Testing framework
- postcss - CSS processing and optimization