diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..11820b8 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,219 @@ +# 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. + +## Build & Development Commands + +```bash +# 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 +npm test + +# Run tests in watch mode +npm run test + +# Run tests once and exit +npm run test:run + +# Run tests with UI +npm run test:ui + +# 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 + +### 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 +- **ViewManager** - Handles view switching (day/week/month) +- **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 + +Event data access is abstracted through the **IEventRepository** interface (`src/repositories/IEventRepository.ts`): +- **IndexedDBEventRepository** - Primary: Local storage with offline support +- **ApiEventRepository** - Sends changes to backend API +- **MockEventRepository** - Legacy: Loads from JSON file + +All repository methods accept an `UpdateSource` parameter ('local' | 'remote') to distinguish user actions from remote updates. + +### Offline-First Sync Architecture + +**SyncManager** (`src/workers/SyncManager.ts`) provides background synchronization: +1. Local changes are written to **IndexedDB** immediately +2. Operations are queued in **OperationQueue** +3. SyncManager processes queue when online (5-second polling) +4. Failed operations retry with exponential backoff (max 5 retries) +5. 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**: `DragDropManager` emits `drag:mouseenter-header`, `AllDayManager` creates all-day clone +- **All-day → Timed**: `DragDropManager` emits `drag:mouseenter-column`, `EventRenderingService` creates 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: +1. Listens to `edge-scroll:scrolling` events +2. Accumulates `scrollDeltaY` from scroll events +3. Compensates dragged element position: `targetY = mouseY - scrollDeltaY - mouseOffset.y` +4. Prevents visual "jumping" during scroll + +### Grid Snapping + +When dropping events, snap to time grid: +1. Get mouse Y position relative to column +2. Convert to time using `PositionUtils.getTimeAtPosition()` +3. Account for `mouseOffset.y` (click position within event) +4. Snap to nearest `snapInterval` (default 15 minutes) + +### Testing with Vitest + +Tests use **Vitest** with **jsdom** environment: +- Setup file: `test/setup.ts` +- Test helpers: `test/helpers/dom-helpers.ts` +- Run single test: `npm test -- ` + +## Key Files to Know + +- `src/index.ts` - DI container setup and initialization +- `src/core/EventBus.ts` - Central event dispatcher +- `src/constants/CoreEvents.ts` - All event type constants +- `src/types/CalendarTypes.ts` - Core type definitions +- `src/managers/CalendarManager.ts` - Main coordinator +- `src/managers/DragDropManager.ts` - Detailed drag-drop architecture docs +- `src/configurations/CalendarConfig.ts` - Configuration schema +- `wwwroot/data/calendar-config.json` - Runtime configuration + +## Common Tasks + +### Adding a New Event Type to CoreEvents + +1. Add constant to `src/constants/CoreEvents.ts` +2. Define payload type in `src/types/EventTypes.ts` +3. Emit with `eventBus.emit(CoreEvents.NEW_EVENT, payload)` +4. Subscribe with `eventBus.on(CoreEvents.NEW_EVENT, handler)` + +### Adding a New Manager + +1. Create in `src/managers/` +2. Inject dependencies via constructor (EventBus, Configuration, other managers) +3. Register in DI container in `src/index.ts`: `builder.registerType(NewManager).as()` +4. Communicate via EventBus only, never direct method calls +5. 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: +```javascript +eventBus.setDebug(true); // In src/index.ts +``` + +Access debug interface in browser console: +```javascript +window.calendarDebug.eventBus.getEventLog() +window.calendarDebug.calendarManager +window.calendarDebug.eventManager +``` + +## Dependencies + +- **@novadi/core** - Dependency injection framework +- **date-fns** / **date-fns-tz** - Date manipulation and timezone support +- **fuse.js** - Fuzzy search for event filtering +- **esbuild** - Fast bundler for development +- **vitest** - Testing framework +- **postcss** - CSS processing and optimization diff --git a/src/managers/CalendarManager.ts b/src/managers/CalendarManager.ts index 6bfcb80..5cc0b28 100644 --- a/src/managers/CalendarManager.ts +++ b/src/managers/CalendarManager.ts @@ -111,7 +111,7 @@ export class CalendarManager { /** * Setup event listeners for at håndtere events fra andre managers - */ + */ private setupEventListeners(): void { // Listen for workweek changes only this.eventBus.on(CoreEvents.WORKWEEK_CHANGED, (event: Event) => { @@ -185,22 +185,6 @@ export class CalendarManager { */ private handleWorkweekChange(): void { - // Force a complete grid rebuild by clearing existing structure - const container = document.querySelector('swp-calendar-container'); - if (container) { - container.innerHTML = ''; // Clear everything to force full rebuild - } - - // Re-render the grid with new workweek settings (will now rebuild everything) - this.gridManager.render(); - - // Re-initialize scroll manager after grid rebuild - this.scrollManager.initialize(); - - // Re-render events in the new grid structure - this.rerenderEvents(); - - // Notify HeaderManager with correct current date after grid rebuild this.eventBus.emit('workweek:header-update', { currentDate: this.currentDate, currentView: this.currentView, @@ -208,26 +192,4 @@ export class CalendarManager { }); } - /** - * Re-render events after grid structure changes - */ - private async rerenderEvents(): Promise { - - // Get current period data to determine date range - const periodData = this.calculateCurrentPeriod(); - - // Find the grid container to render events in - const container = document.querySelector('swp-calendar-container'); - if (!container) { - return; - } - - // Trigger event rendering for the current date range using correct method - await this.eventRenderer.renderEvents({ - container: container as HTMLElement, - startDate: new Date(periodData.start), - endDate: new Date(periodData.end) - }); - } - } diff --git a/src/managers/HeaderManager.ts b/src/managers/HeaderManager.ts index bf6073a..9462a5c 100644 --- a/src/managers/HeaderManager.ts +++ b/src/managers/HeaderManager.ts @@ -83,6 +83,9 @@ export class HeaderManager { }); // Listen for workweek header updates after grid rebuild + //currentDate: this.currentDate, + //currentView: this.currentView, + //workweek: this.config.currentWorkWeek eventBus.on('workweek:header-update', (event) => { const { currentDate } = (event as CustomEvent).detail; this.updateHeader(currentDate); diff --git a/src/managers/ViewManager.ts b/src/managers/ViewManager.ts index b6fc6a0..659b46a 100644 --- a/src/managers/ViewManager.ts +++ b/src/managers/ViewManager.ts @@ -102,6 +102,11 @@ export class ViewManager { this.updateAllButtons(); const settings = this.config.getWorkWeekSettings(); + + //currentDate: this.currentDate, + //currentView: this.currentView, + //workweek: this.config.currentWorkWeek + this.eventBus.emit(CoreEvents.WORKWEEK_CHANGED, { workWeekId: workweekId, settings: settings diff --git a/workweek-preset-sequence.md b/workweek-preset-sequence.md new file mode 100644 index 0000000..931ba6c --- /dev/null +++ b/workweek-preset-sequence.md @@ -0,0 +1,72 @@ +# Workweek Preset Click Sequence Diagram + +Dette diagram viser hvad der sker når brugeren klikker på en workweek preset knap (f.eks. "Mon-Fri", "Mon-Thu", etc.) + +```mermaid +sequenceDiagram + actor User + participant HTML as swp-preset-button + participant VM as ViewManager + participant Config as Configuration + participant CM as ConfigManager + participant EventBus + participant GM as GridManager + participant GR as GridRenderer + participant HM as HeaderManager + participant HR as HeaderRenderer + participant DOM + + User->>HTML: Click på preset button
(data-workweek="compressed") + HTML->>VM: click event + + Note over VM: setupButtonGroup handler + VM->>VM: getAttribute('data-workweek')
→ "compressed" + VM->>VM: changeWorkweek("compressed") + + VM->>Config: setWorkWeek("compressed") + Note over Config: Opdaterer currentWorkWeek
og workweek settings + + VM->>CM: updateCSSProperties(config) + Note over CM: Opdaterer CSS custom properties + CM->>DOM: setProperty('--grid-columns', '4') + CM->>DOM: setProperty('--hour-height', '80px') + CM->>DOM: setProperty('--day-start-hour', '6') + CM->>DOM: setProperty('--work-start-hour', '8') + Note over DOM: CSS grid layout opdateres + + VM->>VM: updateAllButtons() + VM->>DOM: Update data-active attributter
på alle preset buttons + Note over DOM: Compressed knap får
data-active="true"
Andre knapper mister active + + VM->>Config: getWorkWeekSettings() + Config-->>VM: { id: 'compressed',
workDays: [1,2,3,4],
totalDays: 4 } + + VM->>EventBus: emit(WORKWEEK_CHANGED, payload) + Note over EventBus: Event: 'workweek:changed'
Payload: { workWeekId, settings } + + EventBus->>GM: WORKWEEK_CHANGED event + Note over GM: Listener setup i subscribeToEvents() + GM->>GM: render() + GM->>GR: renderGrid(container, currentDate) + + alt First render (empty grid) + GR->>GR: createCompleteGridStructure() + GR->>DOM: Create time axis + GR->>DOM: Create grid container + GR->>DOM: Create 4 columns (Mon-Thu) + else Update existing grid + GR->>GR: updateGridContent() + GR->>DOM: Update existing columns + end + + GM->>EventBus: emit(GRID_RENDERED) + + EventBus->>HM: WORKWEEK_CHANGED event + Note over HM: Via 'workweek:header-update'
from CalendarManager + HM->>HM: updateHeader(currentDate) + HM->>HR: render(context) + HR->>DOM: Update header med 4 dage
(Mon, Tue, Wed, Thu) + + Note over DOM: Grid viser nu kun
Man-Tor (4 dage)
med opdaterede headers + + DOM-->>User: Visuelt feedback:
4-dages arbejdsuge