@innosolutions/inno-calendar 1.0.58 → 1.0.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/AGENT.md +1756 -1610
  2. package/README.md +279 -235
  3. package/dist/agenda-widget-BtjS59HE.cjs +2 -0
  4. package/dist/agenda-widget-BtjS59HE.cjs.map +1 -0
  5. package/dist/{agenda-widget-B77T2ZoX.js → agenda-widget-COpTIR70.js} +89 -36
  6. package/dist/agenda-widget-COpTIR70.js.map +1 -0
  7. package/dist/components/event/event-card.d.ts.map +1 -1
  8. package/dist/components/index.cjs +1 -1
  9. package/dist/components/index.mjs +2 -2
  10. package/dist/components/primitives/scroll-navigator.d.ts +1 -1
  11. package/dist/components/primitives/scroll-navigator.d.ts.map +1 -1
  12. package/dist/core/context/inno-calendar-provider.d.ts.map +1 -1
  13. package/dist/core/index.cjs +1 -1
  14. package/dist/core/index.mjs +115 -114
  15. package/dist/core/types.d.ts +6 -18
  16. package/dist/core/types.d.ts.map +1 -1
  17. package/dist/core/utils/event-utils.d.ts.map +1 -1
  18. package/dist/core/utils/index.d.ts +1 -0
  19. package/dist/core/utils/index.d.ts.map +1 -1
  20. package/dist/core/utils/react-node-utils.d.ts +14 -0
  21. package/dist/core/utils/react-node-utils.d.ts.map +1 -0
  22. package/dist/index.cjs +1 -1
  23. package/dist/index.mjs +207 -206
  24. package/dist/position-utils-BQpbtF6N.cjs +2 -0
  25. package/dist/position-utils-BQpbtF6N.cjs.map +1 -0
  26. package/dist/position-utils-DMVQFywD.js +936 -0
  27. package/dist/position-utils-DMVQFywD.js.map +1 -0
  28. package/dist/presets/index.cjs +1 -1
  29. package/dist/presets/index.mjs +1 -1
  30. package/dist/{slot-selection-context-BwghpxKA.js → slot-selection-context-CKGT_45R.js} +201 -200
  31. package/dist/{slot-selection-context-BwghpxKA.js.map → slot-selection-context-CKGT_45R.js.map} +1 -1
  32. package/dist/slot-selection-context-CS1te8uo.cjs +2 -0
  33. package/dist/{slot-selection-context-CHSaOPWP.cjs.map → slot-selection-context-CS1te8uo.cjs.map} +1 -1
  34. package/dist/{tailwind-calendar-DjtOowce.js → tailwind-calendar-C3DidCJ5.js} +80 -80
  35. package/dist/{tailwind-calendar-DjtOowce.js.map → tailwind-calendar-C3DidCJ5.js.map} +1 -1
  36. package/dist/tailwind-calendar-iFeIx0Ci.cjs +2 -0
  37. package/dist/{tailwind-calendar-DpEkgg5k.cjs.map → tailwind-calendar-iFeIx0Ci.cjs.map} +1 -1
  38. package/dist/{use-calendar-Clo9DFK4.js → use-calendar-D8XaVe44.js} +69 -69
  39. package/dist/{use-calendar-Clo9DFK4.js.map → use-calendar-D8XaVe44.js.map} +1 -1
  40. package/dist/use-calendar-DkT1_V3j.cjs +2 -0
  41. package/dist/{use-calendar-BEuelmSu.cjs.map → use-calendar-DkT1_V3j.cjs.map} +1 -1
  42. package/dist/{use-slot-selection-BCZKnZSM.js → use-slot-selection-CWRk_17s.js} +32 -32
  43. package/dist/{use-slot-selection-BCZKnZSM.js.map → use-slot-selection-CWRk_17s.js.map} +1 -1
  44. package/dist/use-slot-selection-Dj_tWg1O.cjs +2 -0
  45. package/dist/{use-slot-selection-Bwqwjiaq.cjs.map → use-slot-selection-Dj_tWg1O.cjs.map} +1 -1
  46. package/dist/utils.cjs +1 -1
  47. package/dist/utils.cjs.map +1 -1
  48. package/dist/utils.mjs +117 -920
  49. package/dist/utils.mjs.map +1 -1
  50. package/dist/week-view-B9_48WmF.cjs +11 -0
  51. package/dist/week-view-B9_48WmF.cjs.map +1 -0
  52. package/dist/{week-view-CKx6bJdJ.js → week-view-CNbtlkE_.js} +1102 -1078
  53. package/dist/week-view-CNbtlkE_.js.map +1 -0
  54. package/package.json +138 -138
  55. package/dist/agenda-widget-B77T2ZoX.js.map +0 -1
  56. package/dist/agenda-widget-CadA0D6F.cjs +0 -2
  57. package/dist/agenda-widget-CadA0D6F.cjs.map +0 -1
  58. package/dist/slot-selection-context-CHSaOPWP.cjs +0 -2
  59. package/dist/tailwind-calendar-DpEkgg5k.cjs +0 -2
  60. package/dist/use-calendar-BEuelmSu.cjs +0 -2
  61. package/dist/use-slot-selection-Bwqwjiaq.cjs +0 -2
  62. package/dist/week-view-CKx6bJdJ.js.map +0 -1
  63. package/dist/week-view-DXlwhFjn.cjs +0 -11
  64. package/dist/week-view-DXlwhFjn.cjs.map +0 -1
package/AGENT.md CHANGED
@@ -1,1610 +1,1756 @@
1
- # @innosolutions/inno-calendar — AI Agent Reference
2
-
3
- > **Version 1.0.49** | React 18+/19 | TypeScript 5.9 | Vite 7 library-mode
4
- > Headless-first, fully customizable React calendar for enterprise applications.
5
-
6
- ---
7
-
8
- ## Table of Contents
9
-
10
- 1. [What This Package Is](#1-what-this-package-is)
11
- 2. [npm Scripts](#2-npm-scripts)
12
- 3. [Package Exports](#3-package-exports)
13
- 4. [Core Types](#4-core-types)
14
- 5. [InnoCalendar Props](#5-innocalendar-props)
15
- 6. [Slots System](#6-slots-system)
16
- 7. [ClassNames API](#7-classnames-api)
17
- 8. [EventCard & EventBlock](#8-eventcard--eventblock)
18
- 9. [EventPopover Component](#9-eventpopover-component)
19
- 10. [Preferences System](#10-preferences-system)
20
- 11. [Widget Components](#11-widget-components)
21
- 12. [Filter Components](#12-filter-components)
22
- 13. [Settings Components](#13-settings-components)
23
- 14. [Context & Hooks](#14-context--hooks)
24
- 15. [Drag & Drop](#15-drag--drop)
25
- 16. [Core Utilities](#16-core-utilities)
26
- 17. [Presets](#17-presets)
27
- 18. [CSS & Theming](#18-css--theming)
28
- 19. [Constants](#19-constants)
29
- 20. [Integration Guide](#20-integration-guide)
30
- 21. [Full Export Catalog](#21-full-export-catalog)
31
- 22. [File Naming Conventions](#22-file-naming-conventions)
32
- 23. [Styling Patterns](#23-styling-patterns)
33
- 24. [Biome Configuration](#24-biome-configuration)
34
- 25. [State Synchronization Guide](#25-state-synchronization-guide)
35
- 26. [Critical Rules](#26-critical-rules)
36
-
37
- ---
38
-
39
- ## 1. What This Package Is
40
-
41
- A **headless-first** React calendar component library — all view logic lives in hooks and contexts; every UI element is an optional, replaceable component.
42
-
43
- ### Feature Set
44
-
45
- | Category | Features |
46
- |---|---|
47
- | **Views** | Day, Week, Month, Year, Agenda, Resource-Day, Resource-Week, Timeline-Day, Timeline-3Day, Timeline-Week |
48
- | **Interaction** | Drag-to-select (time & day mode), event click, slot click, drag-and-drop reposition, event resize |
49
- | **Customization** | 25+ component slots, 30+ classNames keys, render props, custom popover |
50
- | **Data** | Generic `CalendarEvent<TData>`, adapter pattern, BYO data fetching |
51
- | **Preferences** | localStorage persistence, locking, session mode, per-instance storage keys |
52
- | **Filtering** | Built-in schedule type filter, user/participant filter, text search |
53
- | **UI Extras** | Loading overlay, focus-event deep-linking, imperative ref API, settings panel, filter sidebar |
54
- | **Embeddable** | AgendaWidget (standalone card), AgendaDropdown (bell-icon notification panel) |
55
- | **Presets** | DefaultCalendar (styled), TailwindCalendar (Tailwind-optimized) |
56
-
57
- ### Tech Stack
58
-
59
- | Layer | Technology |
60
- |---|---|
61
- | Runtime | React 18+/19, TypeScript 5.9 |
62
- | Build | Vite 7 (library mode), vite-plugin-dts |
63
- | Styling | TailwindCSS 4+, class-variance-authority (CVA), clsx, tailwind-merge |
64
- | UI Primitives | Radix UI (optional peer deps: popover, tooltip, dropdown-menu, dialog, scroll-area, select, avatar) |
65
- | Icons | lucide-react |
66
- | Date handling | Native Date API (no external date library) |
67
- | Linting | Biome 2.2 |
68
-
69
- ---
70
-
71
- ## 2. npm Scripts
72
-
73
- ```bash
74
- npm run dev # Vite dev mode
75
- npm run build # Production build (Vite + dts declarations)
76
- npm run typecheck # tsc --noEmit
77
- npm run lint # Biome lint check
78
- npm run format # Biome format
79
- ```
80
-
81
- ---
82
-
83
- ## 3. Package Exports
84
-
85
- Defined in `package.json`:
86
-
87
- ```jsonc
88
- {
89
- "main": "./dist/index.cjs",
90
- "module": "./dist/index.js",
91
- "types": "./dist/index.d.ts",
92
- "exports": {
93
- ".": {
94
- "import": "./dist/index.js",
95
- "require": "./dist/index.cjs",
96
- "types": "./dist/index.d.ts"
97
- },
98
- "./styles": "./dist/styles/index.css"
99
- }
100
- }
101
- ```
102
-
103
- Consumer import patterns:
104
-
105
- ```tsx
106
- // Components and types
107
- import { InnoCalendar, type CalendarEvent } from '@innosolutions/inno-calendar';
108
- // Styles (required for built-in components)
109
- import '@innosolutions/inno-calendar/styles';
110
- ```
111
-
112
- ---
113
-
114
- ## 4. Core Types
115
-
116
- All types are defined in `src/core/types.ts` (1473 lines).
117
-
118
- ### CalendarEvent\<TData\>
119
-
120
- ```typescript
121
- interface CalendarEvent<TData = Record<string, unknown>> extends IBaseEvent {
122
- // Required (from IBaseEvent)
123
- id: string;
124
- title: string;
125
- startDate: Date;
126
- endDate: Date;
127
-
128
- // Optional visual/behavioral
129
- description?: string;
130
- color?: TEventColor;
131
- hexColor?: string;
132
- isAllDay?: boolean;
133
- isMultiDay?: boolean;
134
- isCanceled?: boolean;
135
- isRecurring?: boolean;
136
- resourceId?: string;
137
-
138
- // Built-in filtering (optional)
139
- scheduleTypeId?: number;
140
- scheduleTypeName?: string;
141
- participants?: ICalendarUser[];
142
-
143
- // Consumer data bag (RECOMMENDED for all domain fields)
144
- data?: TData;
145
-
146
- // @deprecated still functional, will be removed in next major
147
- meetingTookPlace?: boolean;
148
- isAccepted?: boolean;
149
- companyId?: number;
150
- cancelReason?: string | null;
151
- scheduleTypeCode?: string;
152
- opportunityId?: number;
153
- propertyId?: number;
154
- projectId?: number;
155
- contactId?: number;
156
- user?: ICalendarUser;
157
- }
158
- ```
159
-
160
- ### IResource\<TData\>
161
-
162
- ```typescript
163
- interface IResource<TData = Record<string, unknown>> {
164
- id: string;
165
- name: string;
166
- title?: string;
167
- avatar?: string;
168
- color?: TEventColor | string;
169
- data?: TData;
170
- }
171
- ```
172
-
173
- ### IScheduleType\<TData\>
174
-
175
- ```typescript
176
- interface IScheduleType<TData = Record<string, unknown>> {
177
- id: number;
178
- name: string;
179
- color?: TEventColor | string;
180
- icon?: string;
181
- isDefault?: boolean;
182
- isActive?: boolean;
183
- code?: string;
184
- resourceKey?: string;
185
- colorHex?: string;
186
- defaultMinutes?: number;
187
- data?: TData;
188
- }
189
- ```
190
-
191
- ### ICalendarUser\<TData\>
192
-
193
- ```typescript
194
- interface ICalendarUser<TData = Record<string, unknown>> {
195
- id: string;
196
- name: string;
197
- email?: string;
198
- avatar?: string;
199
- data?: TData;
200
- }
201
- ```
202
-
203
- ### ICalendarRefHandle
204
-
205
- Imperative handle exposed via `React.forwardRef` on `<InnoCalendar>`:
206
-
207
- ```typescript
208
- interface ICalendarRefHandle {
209
- scrollToToday: () => void;
210
- scrollToWorkingHours: () => void;
211
- getViewRect: () => DOMRect | null;
212
- focusEvent: (eventId: string) => void;
213
- }
214
- ```
215
-
216
- Usage:
217
-
218
- ```tsx
219
- const ref = useRef<ICalendarRefHandle>(null);
220
- <InnoCalendar ref={ref} events={events} />
221
- // Later:
222
- ref.current?.scrollToToday();
223
- ref.current?.focusEvent('appointment-42');
224
- ```
225
-
226
- ### ICalendarPreferences
227
-
228
- ```typescript
229
- interface ICalendarPreferences {
230
- startHour: number; // 0–23
231
- endHour: number; // 0–24
232
- firstDayOfWeek: 0|1|2|3|4|5|6;
233
- slotDuration: number;
234
- showWeekends: boolean;
235
- timeFormat: '12h' | '24h';
236
- badgeVariant: TBadgeVariant;
237
- showCanceledEvents: boolean;
238
- }
239
- ```
240
-
241
- ### View Types
242
-
243
- ```typescript
244
- type TCalendarView =
245
- | 'day' | 'week' | 'month' | 'year' | 'agenda'
246
- | 'resource-day' | 'resource-week'
247
- | 'timeline-day' | 'timeline-3day' | 'timeline-week';
248
- ```
249
-
250
- ### Other Key Types
251
-
252
- | Type | Purpose |
253
- |---|---|
254
- | `TEventColor` | `'blue'│'green'│'red'│'yellow'│'purple'│'orange'│'pink'│'teal'│'gray'│'indigo'` |
255
- | `TBadgeVariant` | `'dot'│'colored'│'mixed'` |
256
- | `TSlotDuration` | `15│30│60` |
257
- | `TWorkingHours` | `Record<number, IWorkingHoursDay>` (0=Sun..6=Sat) |
258
- | `ISelectionResult` | `{ startDate: Date; endDate: Date; resourceId?: string }` |
259
- | `IDropResult<TData>` | Drag-drop result with event, newStartDate, newEndDate, newResourceId |
260
- | `IDateRange` | `{ startDate: Date; endDate: Date }` |
261
- | `IEventPosition` | `{ top, height, left, width, zIndex }` |
262
- | `IPositionedEvent<TData>` | Event + IEventPosition + column info |
263
- | `ICalendarFilters` | `{ scheduleTypeIds?, resourceIds?, search?, showCanceled?, dateRange?, [key]: unknown }` |
264
- | `TSelectionMode` | `'time'│'day'` |
265
- | `TEventDetailMode` | `'popover'│'dialog'│'sheet'│'custom'` — controls how event details are shown |
266
- | `ICalendarHeaderConfig` | Object with 7 optional boolean keys controlling header section visibility |
267
-
268
- ### Callback Types
269
-
270
- | Type | Signature |
271
- |---|---|
272
- | `TOnEventClick<TData>` | `(event, e?) => void` |
273
- | `TOnSlotSelect` | `(selection: ISelectionResult) => void` |
274
- | `TOnEventDrop<TData>` | `(event, newStart, newEnd, newResourceId?) => void` |
275
- | `TOnEventResize<TData>` | `(event, newStart, newEnd) => void` |
276
- | `TOnViewChange` | `(view: TCalendarView) => void` |
277
- | `TOnDateChange` | `(date: Date) => void` |
278
- | `TOnFiltersChange` | `(filters: ICalendarFilters) => void` |
279
-
280
- ---
281
-
282
- ## 5. InnoCalendar Props
283
-
284
- `<InnoCalendar>` is the primary entry point. It wraps `InnoCalendarProvider` + `SlotSelectionProvider` + `DragDropProvider` internally.
285
-
286
- ```typescript
287
- interface InnoCalendarProps<TEventData> {
288
- // === DATA ===
289
- events: CalendarEvent<TEventData>[];
290
- users?: ICalendarUser[];
291
- scheduleTypes?: IScheduleType[];
292
-
293
- // === INITIAL STATE ===
294
- initialView?: TCalendarView; // default: 'week'
295
- initialDate?: Date; // default: new Date()
296
- initialSelectedUserId?: string | 'all'; // default: 'all'
297
- initialScheduleTypeIds?: number[];
298
- initialParticipantIds?: string[];
299
- initialWorkingHoursView?: 'default' | 'enabled' | 'disabled';
300
- initialSearchQuery?: string;
301
-
302
- // === PREFERENCES ===
303
- preferencesConfig?: IPreferencesConfig;
304
-
305
- // === CALLBACKS ===
306
- onEventClick?: (event: CalendarEvent<TEventData>) => void;
307
- onSlotClick?: (date: Date, hour?: number) => void;
308
- onSlotSelect?: (selection: ISelectionResult) => void;
309
- onAddEvent?: () => void;
310
- onEventDrop?: (result: IDropResult<TEventData>) => void;
311
- onDateChange?: (date: Date, view: TCalendarView) => void;
312
- onViewChange?: (view: TCalendarView) => void;
313
-
314
- // === UI OPTIONS ===
315
- className?: string;
316
- showHeader?: boolean; // default: true
317
- minSelectionMinutes?: number; // default: 30
318
- showMoreMode?: 'dayView' | 'popover' | 'expand'; // default: 'expand'
319
- isLoading?: boolean; // default: false
320
-
321
- // === CUSTOM RENDERING ===
322
- renderPopover?: (props: { event; onClose }) => ReactNode;
323
- slots?: ICalendarSlots<TEventData>;
324
- classNames?: ICalendarClassNames;
325
-
326
- // === HEADER CONFIGURATION ===
327
- headerConfig?: ICalendarHeaderConfig; // fine-grained header visibility
328
- // see ICalendarHeaderConfig below
329
-
330
- // === EVENT DETAIL MODE ===
331
- eventDetailMode?: TEventDetailMode; // default: 'popover'
332
- renderEventDetail?: (props: { event; onClose }) => ReactNode; // for 'custom' + override in dialog/sheet
333
-
334
- // === CONTENT SLOTS ===
335
- settingsContent?: ReactNode;
336
- filterContent?: ReactNode;
337
- headerActions?: ReactNode;
338
-
339
- // === DEEP LINKING ===
340
- focusEventId?: string | null;
341
-
342
- // @deprecated use showMoreMode instead
343
- showMoreEventsInPopover?: boolean;
344
- }
345
- ```
346
-
347
- #### ICalendarHeaderConfig
348
-
349
- Fine-grained visibility control for calendar header sections. Each key defaults to `true` when omitted.
350
-
351
- ```typescript
352
- interface ICalendarHeaderConfig {
353
- showToday?: boolean; // Today button
354
- showDateNavigator?: boolean; // Prev/Next arrows + date label
355
- showCalendarViews?: boolean; // Day/Week/Month/Agenda dropdown
356
- showResourceViews?: boolean; // Timeline/Resource dropdown
357
- showSettings?: boolean; // Settings gear icon
358
- showAddEvent?: boolean; // "+" add event button
359
- showFilters?: boolean; // Filter row below header
360
- }
361
- ```
362
-
363
- #### TEventDetailMode
364
-
365
- Controls how event details are displayed when clicking an event:
366
-
367
- - `'popover'` Radix popover anchored to the event card (default)
368
- - `'dialog'` Centered modal dialog overlay
369
- - `'sheet'` — Slide-in side panel from the right
370
- - `'custom'` No built-in container; delegates to `renderEventDetail`
371
-
372
- ```typescript
373
- type TEventDetailMode = 'popover' | 'dialog' | 'sheet' | 'custom';
374
- ```
375
-
376
- ### Ref Support
377
-
378
- `InnoCalendar` is wrapped with `forwardRef` and exposes `ICalendarRefHandle`:
379
-
380
- ```tsx
381
- const calendarRef = useRef<ICalendarRefHandle>(null);
382
- <InnoCalendar ref={calendarRef} events={events} />
383
- ```
384
-
385
- ---
386
-
387
- ## 6. Slots System
388
-
389
- Every visual region can be replaced via `slots`:
390
-
391
- ```typescript
392
- interface ICalendarSlots<TEventData, TScheduleTypeData, TResourceData> {
393
- // Structural
394
- header?: ComponentType<IHeaderSlotProps>;
395
- footer?: ComponentType<IFooterSlotProps<TEventData>>;
396
- viewWrapper?: ComponentType<IViewWrapperSlotProps>;
397
-
398
- // Day Cell (month/year views)
399
- dayCell?: ComponentType<IDayCellSlotPropsExtended<TEventData>>;
400
- dayCellHeader?: ComponentType<IDayCellAdornmentSlotProps<TEventData>>;
401
- dayCellFooter?: ComponentType<IDayCellAdornmentSlotProps<TEventData>>;
402
- dayCellStartAdornment?: ComponentType<IDayCellAdornmentSlotProps<TEventData>>;
403
- dayCellEndAdornment?: ComponentType<IDayCellAdornmentSlotProps<TEventData>>;
404
-
405
- // Time Grid (day/week views)
406
- timeGutter?: ComponentType<ITimeGutterSlotProps>;
407
- timeSlotBackground?: ComponentType<ITimeSlotBackgroundSlotProps>;
408
- columnHeader?: ComponentType<IColumnHeaderSlotProps>;
409
- currentTimeIndicator?: ComponentType<ICurrentTimeIndicatorSlotProps>;
410
- multiDayBanner?: ComponentType<IMultiDayBannerSlotProps<TEventData>>;
411
-
412
- // Events
413
- eventCard?: ComponentType<IEventCardSlotProps<TEventData>>;
414
- eventPopover?: ComponentType<IEventPopoverSlotProps<TEventData>>;
415
-
416
- // Resource/Timeline
417
- resourceHeader?: ComponentType<IResourceHeaderSlotProps<TResourceData>>;
418
- rowHeader?: ComponentType<IRowHeaderSlotProps<TResourceData>>;
419
-
420
- // Filters
421
- filter?: ComponentType<IFilterSlotProps<TScheduleTypeData>>;
422
-
423
- // Empty States
424
- emptyCell?: ComponentType<IEmptyCellSlotProps>;
425
- emptyState?: ComponentType<IEmptyStateSlotProps>;
426
-
427
- // @deprecated
428
- timeSlot?: ComponentType<ITimeSlotSlotProps>;
429
- }
430
- ```
431
-
432
- ### Slot Props Interfaces
433
-
434
- | Slot | Props Interface | Key Fields |
435
- |---|---|---|
436
- | `header` | `IHeaderSlotProps` | currentDate, view, onNavigate, onViewChange |
437
- | `footer` | `IFooterSlotProps` | view, currentDate, events |
438
- | `viewWrapper` | `IViewWrapperSlotProps` | view, children |
439
- | `dayCell` | `IDayCellSlotPropsExtended` | date, events, isToday, isCurrentMonth, isWeekend, view, children, onDayClick |
440
- | `dayCellHeader/Footer` | `IDayCellAdornmentSlotProps` | date, events, isToday, isCurrentMonth, isWeekend, view |
441
- | `timeGutter` | `ITimeGutterSlotProps` | hour, formattedLabel, isFirst |
442
- | `timeSlotBackground` | `ITimeSlotBackgroundSlotProps` | date, hour, minute, isWorkingHour, isToday |
443
- | `columnHeader` | `IColumnHeaderSlotProps` | date, isToday, onDayClick, columnIndex |
444
- | `currentTimeIndicator` | `ICurrentTimeIndicatorSlotProps` | currentTime, topOffset |
445
- | `multiDayBanner` | `IMultiDayBannerSlotProps` | events, rangeStart, onEventClick |
446
- | `eventCard` | `IEventCardSlotProps` | event, view, isCompact, onClick |
447
- | `eventPopover` | `IEventPopoverSlotProps` | event, onClose, onEdit, onDelete |
448
- | `resourceHeader` | `IResourceHeaderSlotProps` | resource |
449
- | `rowHeader` | `IRowHeaderSlotProps` | resource, rowIndex |
450
- | `filter` | `IFilterSlotProps` | scheduleTypes, filters, onFiltersChange |
451
- | `emptyCell` | `IEmptyCellSlotProps` | date, isToday |
452
- | `emptyState` | `IEmptyStateSlotProps` | view, dateRange |
453
-
454
- ---
455
-
456
- ## 7. ClassNames API
457
-
458
- `ICalendarClassNames` provides 30+ CSS class override keys. Classes are merged with defaults via `cn()`.
459
-
460
- ```typescript
461
- interface ICalendarClassNames {
462
- // Structural
463
- root?: string;
464
- header?: string;
465
- footer?: string;
466
- viewContainer?: string;
467
-
468
- // Month View
469
- monthGrid?: string;
470
- weekdayHeader?: string;
471
- weekdayLabel?: string;
472
- dayCell?: string;
473
- dayCellToday?: string;
474
- dayCellOutside?: string;
475
- dayCellWeekend?: string;
476
- dayCellNumber?: string;
477
- dayCellContent?: string;
478
- dayCellFooter?: string;
479
-
480
- // Week/Day View
481
- timeGutter?: string;
482
- timeGutterLabel?: string;
483
- timeSlot?: string;
484
- timeSlotWorking?: string;
485
- timeSlotNonWorking?: string;
486
- columnHeader?: string;
487
- columnHeaderToday?: string;
488
- currentTimeIndicator?: string;
489
- scrollContainer?: string;
490
-
491
- // Events
492
- eventCard?: string;
493
- eventCardCompact?: string;
494
- eventPopover?: string;
495
- multiDayBanner?: string;
496
- moreEventsIndicator?: string;
497
-
498
- // Timeline
499
- resourceHeader?: string;
500
- resourceRow?: string;
501
- timelineCell?: string;
502
-
503
- // Agenda
504
- agendaList?: string;
505
- agendaDayGroup?: string;
506
- agendaDayHeader?: string;
507
- }
508
- ```
509
-
510
- ---
511
-
512
- ## 8. EventCard & EventBlock
513
-
514
- Located in `src/components/event/event-card.tsx`.
515
-
516
- ### EventCard
517
-
518
- Full-size event card used in day/week/timeline views. Renders differently based on `badgeVariant`:
519
-
520
- | Badge Variant | Rendering |
521
- |---|---|
522
- | `'colored'` | Filled background with accent color, white/dark text |
523
- | `'dot'` | White background with colored left-border accent |
524
- | `'mixed'` | Light tinted background with colored left-border |
525
-
526
- Key props:
527
-
528
- | Prop | Type | Default | Description |
529
- |---|---|---|---|
530
- | `event` | `CalendarEvent<TData>` | required | Event data |
531
- | `variant` | `'full'\|'compact'\|'dot'` | `'full'` | Visual layout |
532
- | `badgeVariant` | `TBadgeVariant` | `'colored'` | Color treatment |
533
- | `disablePopover` | `boolean` | `false` | Skip built-in popover, fire `onClick` |
534
- | `renderPopover` | `(props) => ReactNode` | — | Custom content inside the default popover shell |
535
- | `enableDrag` | `boolean` | `false` | Enable HTML5 drag & drop |
536
- | `eventDetailMode` | `TEventDetailMode` | `'popover'` | Container for event details: popover, dialog, sheet, custom |
537
- | `renderEventDetail` | `(props) => ReactNode` | — | Custom renderer for 'custom' mode; also inner content for dialog/sheet |
538
-
539
- Built-in cards display enrichment data read from `event.data` first, with fallback to deprecated top-level fields:
540
-
541
- ```typescript
542
- // Runtime enrichment reading pattern:
543
- const eventData = event.data as Record<string, unknown> | undefined;
544
- const meetingTookPlace = (eventData?.meetingTookPlace as boolean) ?? event.meetingTookPlace ?? false;
545
- const typeName = eventData?.typeName as string | undefined;
546
- const siteName = eventData?.siteName as string | undefined;
547
- const nrParticipant = eventData?.nrParticipant as number | undefined;
548
- ```
549
-
550
- ### EventBlock
551
-
552
- Compact event chip for month/year view cells. Displays title with optional color dot.
553
-
554
- ### MultiDayEventBar
555
-
556
- Horizontal bar for multi-day/all-day events in the banner strip above day/week time grids.
557
-
558
- ### Exports
559
-
560
- ```typescript
561
- export { EventCard, type EventCardProps }
562
- export { EventBlock, type EventBlockProps }
563
- export { MultiDayEventBar, type MultiDayEventBarProps }
564
- export { getEventColorClasses } // Utility: TEventColor → { bg, text, border }
565
- ```
566
-
567
- ---
568
-
569
- ## 9. EventPopover Component
570
-
571
- Located in `src/components/event/event-popover.tsx` (1519 lines).
572
-
573
- Full-featured event detail popover with action buttons, participant list, and domain data display.
574
-
575
- ### Key Props
576
-
577
- ```typescript
578
- interface EventPopoverProps<TData = Record<string, unknown>> {
579
- event: CalendarEvent<TData>;
580
- onClose: () => void;
581
-
582
- // Action callbacks
583
- onEdit?: () => void;
584
- onDelete?: () => void;
585
- onCancel?: () => void;
586
- onAccept?: () => void;
587
- onDecline?: () => void;
588
- onConfirmMeeting?: () => void;
589
-
590
- // Permission flags
591
- canEdit?: boolean;
592
- canDelete?: boolean;
593
- canCancel?: boolean;
594
-
595
- // Loading states
596
- isAccepting?: boolean;
597
- isDeclining?: boolean;
598
- isConfirming?: boolean;
599
-
600
- // Render customization
601
- renderHeader?: (event) => ReactNode;
602
- renderFooter?: (event) => ReactNode;
603
- renderActions?: (event) => ReactNode;
604
-
605
- // i18n labels
606
- labels?: IEventPopoverLabels;
607
- }
608
- ```
609
-
610
- ### IEventPopoverLabels
611
-
612
- All label strings are customizable for i18n:
613
-
614
- ```typescript
615
- interface IEventPopoverLabels {
616
- edit?: string;
617
- delete?: string;
618
- cancel?: string;
619
- accept?: string;
620
- decline?: string;
621
- confirmMeeting?: string;
622
- participants?: string;
623
- description?: string;
624
- // ... more keys
625
- }
626
- ```
627
-
628
- ---
629
-
630
- ## 10. Preferences System
631
-
632
- Located in `src/core/preferences/`.
633
-
634
- ### IPreferencesConfig
635
-
636
- Passed via `<InnoCalendar preferencesConfig={...}>`:
637
-
638
- ```typescript
639
- interface IPreferencesConfig {
640
- modes?: TPreferenceModes; // Per-key control mode
641
- locked?: TLockedPreferences; // Values for locked preferences
642
- defaults?: TPartialPreferences; // Custom defaults
643
- storageKey?: string; // Custom localStorage key
644
- disableStorage?: boolean; // Disable localStorage entirely
645
- }
646
- ```
647
-
648
- ### Preference Modes
649
-
650
- | Mode | Behavior |
651
- |---|---|
652
- | `'user'` | User can modify, persisted to localStorage (default) |
653
- | `'locked'` | Developer-controlled, user cannot modify |
654
- | `'session'` | User can modify during session, not persisted |
655
-
656
- ### IPreferences
657
-
658
- ```typescript
659
- interface IPreferences {
660
- view: TCalendarView;
661
- badgeVariant: TBadgeVariant;
662
- slotDuration: TSlotDuration;
663
- visibleHours: IVisibleHoursConfig; // { from: number; to: number }
664
- workingHours: TWorkingHoursConfig; // { [dayIndex]: { from, to } }
665
- showWorkingHoursOnly: boolean;
666
- showWeekends: boolean;
667
- firstDayOfWeek: 0|1|2|3|4|5|6;
668
- }
669
- ```
670
-
671
- ### useCalendarPreferences Hook
672
-
673
- Returns `IUsePreferencesReturn`:
674
-
675
- ```typescript
676
- interface IUsePreferencesReturn {
677
- preferences: IPreferences;
678
- setPreference: <K extends TPreferenceKey>(key: K, value: IPreferences[K]) => void;
679
- setPreferences: (updates: TPartialPreferences) => void;
680
- resetPreferences: () => void;
681
- resetPreference: (key: TPreferenceKey) => void;
682
- isLocked: (key: TPreferenceKey) => boolean;
683
- isPersisted: (key: TPreferenceKey) => boolean;
684
- getMode: (key: TPreferenceKey) => TPreferenceMode;
685
- }
686
- ```
687
-
688
- ### Example: Locking slot duration
689
-
690
- ```tsx
691
- <InnoCalendar
692
- events={events}
693
- preferencesConfig={{
694
- modes: { slotDuration: 'locked' },
695
- locked: { slotDuration: 30 },
696
- }}
697
- />
698
- ```
699
-
700
- ---
701
-
702
- ## 11. Widget Components
703
-
704
- Located in `src/components/widget/`.
705
-
706
- ### AgendaWidget
707
-
708
- Embeddable agenda card for dashboards. Can work standalone or tap into an existing `InnoCalendarProvider`.
709
-
710
- ```typescript
711
- interface AgendaWidgetProps {
712
- events: CalendarEvent[];
713
- maxItems?: number;
714
- onEventClick?: (event: CalendarEvent) => void;
715
- className?: string;
716
- // ... additional display options
717
- }
718
- ```
719
-
720
- ### AgendaDropdown
721
-
722
- Bell-style notification dropdown for upcoming events:
723
-
724
- ```typescript
725
- interface AgendaDropdownProps {
726
- events: CalendarEvent[];
727
- onEventClick?: (event: CalendarEvent) => void;
728
- className?: string;
729
- // ... trigger customization
730
- }
731
- ```
732
-
733
- ---
734
-
735
- ## 12. Filter Components
736
-
737
- Located in `src/components/filters/`.
738
-
739
- ### CalendarFilterSidebar
740
-
741
- Full filter panel with schedule type and user filters:
742
-
743
- ```typescript
744
- interface CalendarFilterSidebarProps {
745
- scheduleTypes: IScheduleTypeOption[];
746
- users: IUserOption[];
747
- labels?: CalendarFilterSidebarLabels;
748
- // Controlled state or context-driven
749
- }
750
- ```
751
-
752
- ### ScheduleTypeFilter
753
-
754
- Standalone schedule type filter component with checkbox list.
755
-
756
- ### UserFilter
757
-
758
- Standalone user/participant filter with avatar support.
759
-
760
- ---
761
-
762
- ## 13. Settings Components
763
-
764
- Located in `src/components/settings/`.
765
-
766
- | Component | Props | What It Controls |
767
- |---|---|---|
768
- | `BadgeVariantSetting` | `BadgeVariantSettingProps` | Dot / Colored / Mixed badge display |
769
- | `SlotDurationSetting` | `SlotDurationSettingProps` | 15 / 30 / 60 minute time slot granularity |
770
- | `VisibleHoursSetting` | `VisibleHoursSettingProps` | Start/end hour range for day/week views |
771
- | `WorkingHoursSetting` | `WorkingHoursSettingProps` | Per-day working hours with enable/disable toggle |
772
-
773
- All settings components read/write preferences through the preferences context.
774
-
775
- ---
776
-
777
- ## 14. Context & Hooks
778
-
779
- ### Context Providers
780
-
781
- | Provider | Purpose |
782
- |---|---|
783
- | `InnoCalendarProvider` | Main state: events, date, view, filters, search, preferences, time config |
784
- | `CalendarProvider` | Lower-level provider (used by `Calendar` component) |
785
- | `SlotSelectionProvider` | Drag-to-select state management |
786
- | `DragDropProvider` | Event drag-and-drop state management |
787
-
788
- ### Primary Hooks
789
-
790
- | Hook | Return Type | Purpose |
791
- |---|---|---|
792
- | `useInnoCalendar()` | `IInnoCalendarContext` | Full context access (events, view, date, filters, search, preferences) |
793
- | `useCalendarContext()` | `IInnoCalendarContext` | Alias for `useInnoCalendar` |
794
- | `useCalendar()` | `UseCalendarReturn` | Core calendar state (date, view, navigation, event helpers) |
795
- | `useSlotSelection()` | `UseSlotSelectionReturn` | Drag-to-select hook |
796
- | `useCalendarTimeConfig()` | `UseCalendarTimeConfigReturn` | Slot duration, visible hours, working hours |
797
- | `usePreferences()` | `UsePreferencesReturn` | Basic preferences hook |
798
- | `useAdvancedPreferences()` | `IUsePreferencesReturn` | Full preferences with locking support |
799
- | `useDragDrop()` | `IDragDropContext` | Drag-drop state and handlers |
800
-
801
- ### Selective Context Hooks
802
-
803
- These read only a slice of the context for performance:
804
-
805
- | Hook | Returns |
806
- |---|---|
807
- | `useCalendarDate()` | Current date + navigation |
808
- | `useCalendarView()` | Current view + view change |
809
- | `useCalendarEvents()` | Events array + setEvents |
810
- | `useCalendarFilters()` | Filter state + setters |
811
- | `useCalendarPreferences()` | Preferences from context |
812
- | `useInnoCalendarView()` | View from InnoCalendar context |
813
- | `useInnoCalendarEvents()` | Events from InnoCalendar context |
814
- | `useInnoCalendarFilters()` | Filters from InnoCalendar context |
815
- | `useInnoCalendarTimeConfig()` | Time config from InnoCalendar context |
816
-
817
- ### Optional Hooks (no throw if outside provider)
818
-
819
- | Hook | Returns |
820
- |---|---|
821
- | `useOptionalCalendar()` | `context │ undefined` |
822
- | `useOptionalCalendarContext()` | `context │ undefined` |
823
- | `useOptionalInnoCalendar()` | `context │ undefined` |
824
- | `useOptionalDragDrop()` | `context │ undefined` |
825
- | `useOptionalSlotSelection()` | `context │ undefined` |
826
-
827
- ### Legacy Aliases (backward compat)
828
-
829
- | Alias | Maps To |
830
- |---|---|
831
- | `IntegratedCalendarProvider` | `InnoCalendarProvider` |
832
- | `useIntegratedCalendar()` | `useInnoCalendar()` |
833
- | `useIntegratedCalendarEvents()` | `useInnoCalendarEvents()` |
834
- | `useIntegratedCalendarFilters()` | `useInnoCalendarFilters()` |
835
- | `useIntegratedCalendarView()` | `useInnoCalendarView()` |
836
- | `useIntegratedCalendarTimeConfig()` | `useInnoCalendarTimeConfig()` |
837
- | `useOptionalIntegratedCalendar()` | `useOptionalInnoCalendar()` |
838
- | `IntegratedCalendar` | `InnoCalendar` |
839
-
840
- ---
841
-
842
- ## 15. Drag & Drop
843
-
844
- Located in `src/core/context/drag-drop-context.tsx`.
845
-
846
- ### DragDropProvider
847
-
848
- Manages drag state for event repositioning. Wraps the calendar tree automatically when using `<InnoCalendar>`.
849
-
850
- ```typescript
851
- interface IDragState<TData = Record<string, unknown>> {
852
- event: CalendarEvent<TData>;
853
- originalStartDate: Date;
854
- originalEndDate: Date;
855
- previewDate?: Date; // current preview position
856
- previewHour?: number; // for time-based views
857
- previewMinute?: number;
858
- originalResourceId?: string; // for resource/timeline views
859
- targetResourceId?: string; // current drag target resource
860
- }
861
-
862
- interface IDropResult<TData = Record<string, unknown>> {
863
- event: CalendarEvent<TData>;
864
- newStartDate: Date;
865
- newEndDate: Date; // maintains original duration
866
- newResourceId?: string; // set when dropped on different resource
867
- }
868
-
869
- interface IDragDropContext<TData = Record<string, unknown>> {
870
- dragState: IDragState<TData> | null;
871
- isDragging: boolean;
872
- startDrag: (event: CalendarEvent<any>) => void;
873
- updateDragPreview: (date: Date, hour?: number, minute?: number, resourceId?: string) => void;
874
- endDrag: () => IDropResult<TData> | null;
875
- cancelDrag: () => void;
876
- }
877
- ```
878
-
879
- ### Timeline / Resource View Drag & Drop
880
-
881
- `TimelineView` supports HTML5 drag & drop out of the box:
882
-
883
- - Events render with `enableDrag={true}` by default
884
- - Each resource row acts as a drop zone
885
- - Dragging across rows updates `targetResourceId` in the drag state
886
- - Dropping calls `endDrag()` which fires `onEventDrop` on `DragDropProvider`
887
-
888
- ```tsx
889
- <InnoCalendar
890
- events={events}
891
- initialView="timeline-week"
892
- onEventDrop={(result) => {
893
- console.log(result.event.id, result.newStartDate, result.newResourceId);
894
- // Update your backend
895
- }}
896
- />
897
- ```
898
-
899
- ---
900
-
901
- ## 16. Core Utilities
902
-
903
- Located in `src/core/utils/`. All exported from `src/core/utils/index.ts`.
904
-
905
- ### date-utils.ts
906
-
907
- Date manipulation using native Date API:
908
-
909
- | Function | Signature |
910
- |---|---|
911
- | `startOfDay` | `(date: Date) => Date` |
912
- | `endOfDay` | `(date: Date) => Date` |
913
- | `startOfWeek` | `(date: Date, firstDay?: number) => Date` |
914
- | `endOfWeek` | `(date: Date, firstDay?: number) => Date` |
915
- | `startOfMonth` | `(date: Date) => Date` |
916
- | `endOfMonth` | `(date: Date) => Date` |
917
- | `startOfYear` | `(date: Date) => Date` |
918
- | `endOfYear` | `(date: Date) => Date` |
919
- | `addDays` | `(date: Date, days: number) => Date` |
920
- | `addMonths` | `(date: Date, months: number) => Date` |
921
- | `addYears` | `(date: Date, years: number) => Date` |
922
- | `addHours` | `(date: Date, hours: number) => Date` |
923
- | `addMinutes` | `(date: Date, minutes: number) => Date` |
924
- | `isSameDay` | `(a: Date, b: Date) => boolean` |
925
- | `isSameMonth` | `(a: Date, b: Date) => boolean` |
926
- | `isSameYear` | `(a: Date, b: Date) => boolean` |
927
- | `isToday` | `(date: Date) => boolean` |
928
- | `isWeekend` | `(date: Date) => boolean` |
929
- | `isBefore` | `(a: Date, b: Date) => boolean` |
930
- | `isAfter` | `(a: Date, b: Date) => boolean` |
931
- | `isBetween` | `(date: Date, start: Date, end: Date) => boolean` |
932
- | `getWeekDays` | `(start: Date, count?: number) => Date[]` |
933
- | `getMonthDays` | `(date: Date, firstDay?: number) => Date[]` |
934
- | `getYearMonths` | `(date: Date) => Date[]` |
935
- | `getDaysBetween` | `(start: Date, end: Date) => number` |
936
- | `getMinutesBetween` | `(start: Date, end: Date) => number` |
937
- | `formatTime` | `(date: Date, format: '12h'│'24h') => string` |
938
- | `formatDate` | `(date: Date, options?: Intl.DateTimeFormatOptions) => string` |
939
- | `formatDateRange` | `(start: Date, end: Date) => string` |
940
- | `getWeekNumber` | `(date: Date) => number` |
941
- | `getDaysInMonth` | `(date: Date) => number` |
942
- | `clampDate` | `(date: Date, min: Date, max: Date) => Date` |
943
-
944
- ### event-utils.ts
945
-
946
- Event filtering, sorting, and classification:
947
-
948
- | Function | Purpose |
949
- |---|---|
950
- | `isMultiDayEvent` | Checks if event spans midnight |
951
- | `isAllDayEvent` | Checks if event is all-day |
952
- | `getEventsForDay` | Filters events that overlap a given day |
953
- | `getEventsForRange` | Filters events that overlap a date range |
954
- | `sortEvents` | Sort by start date, then duration |
955
- | `groupEventsByDay` | Groups events by calendar date |
956
- | `groupEventsByResource` | Groups events by resourceId |
957
- | `filterEventsByScheduleType` | Client-side schedule type filter |
958
- | `filterEventsByParticipant` | Client-side participant filter |
959
- | `searchEvents` | Text search across event fields |
960
- | `getEventDurationMinutes` | Returns event duration in minutes |
961
- | `splitMultiDayEvent` | Splits multi-day event into per-day segments |
962
-
963
- ### grid-utils.ts
964
-
965
- Grid layout calculations:
966
-
967
- | Function | Purpose |
968
- |---|---|
969
- | `generateTimeSlots` | Creates time slot array for given hour range |
970
- | `getHourHeight` | Pixel height for one hour from slot config |
971
- | `getSlotHeight` | Pixel height for one slot |
972
- | `timeToPixels` | Converts time to vertical pixel offset |
973
- | `pixelsToTime` | Converts vertical pixel offset to time |
974
- | `getColumnsForDay` | Column count for a day in week view |
975
- | `snapToSlot` | Snaps a time to nearest slot boundary |
976
-
977
- ### position-utils.ts
978
-
979
- Event overlap and positioning:
980
-
981
- | Function | Purpose |
982
- |---|---|
983
- | `calculateEventPositions` | Computes non-overlapping layout for events in a column |
984
- | `findOverlappingGroups` | Groups events that share time ranges |
985
- | `calculateColumnSpans` | Assigns columns and spans within overlap groups |
986
-
987
- ---
988
-
989
- ## 17. Presets
990
-
991
- Located in `src/presets/`.
992
-
993
- ### DefaultCalendar
994
-
995
- Pre-configured calendar with sensible defaults:
996
-
997
- ```tsx
998
- import { DefaultCalendar } from '@innosolutions/inno-calendar';
999
-
1000
- <DefaultCalendar events={events} />
1001
- ```
1002
-
1003
- ### TailwindCalendar
1004
-
1005
- Tailwind-optimized calendar preset with carefully chosen utility classes:
1006
-
1007
- ```tsx
1008
- import { TailwindCalendar } from '@innosolutions/inno-calendar';
1009
-
1010
- <TailwindCalendar events={events} />
1011
- ```
1012
-
1013
- ---
1014
-
1015
- ## 18. CSS & Theming
1016
-
1017
- Styles are in `src/styles/calendar.css` (imported via `@innosolutions/inno-calendar/styles`).
1018
-
1019
- ### CSS Custom Properties
1020
-
1021
- ```css
1022
- :root {
1023
- /* Surfaces */
1024
- --calendar-bg: #ffffff;
1025
- --calendar-surface: #f8fafc;
1026
-
1027
- /* Borders */
1028
- --calendar-border: #e2e8f0;
1029
- --calendar-border-strong: #cbd5e1;
1030
-
1031
- /* Text */
1032
- --calendar-text: #0f172a;
1033
- --calendar-text-muted: #64748b;
1034
- --calendar-text-inverted: #ffffff;
1035
-
1036
- /* Interactive */
1037
- --calendar-primary: #3b82f6;
1038
- --calendar-primary-hover: #2563eb;
1039
- --calendar-hover: #f1f5f9;
1040
-
1041
- /* Grid */
1042
- --calendar-hour-height: 64px;
1043
- --calendar-slot-height: 32px;
1044
- --calendar-header-height: 56px;
1045
- --calendar-gutter-width: 72px;
1046
-
1047
- /* Selection */
1048
- --calendar-selection-bg: rgba(59, 130, 246, 0.1);
1049
- --calendar-selection-border: rgba(59, 130, 246, 0.3);
1050
- --calendar-selection-handle: #3b82f6;
1051
-
1052
- /* Current time */
1053
- --calendar-now-line: #ef4444;
1054
- --calendar-now-dot: #ef4444;
1055
-
1056
- /* Working hours */
1057
- --calendar-working-bg: transparent;
1058
- --calendar-non-working-bg: #f8fafc;
1059
-
1060
- /* Sidebar / filter */
1061
- --calendar-sidebar-width: 280px;
1062
- }
1063
- ```
1064
-
1065
- ### Dark Mode
1066
-
1067
- Override with `.dark` class:
1068
-
1069
- ```css
1070
- .dark {
1071
- --calendar-bg: #0f172a;
1072
- --calendar-surface: #1e293b;
1073
- --calendar-border: #334155;
1074
- --calendar-text: #f1f5f9;
1075
- --calendar-text-muted: #94a3b8;
1076
- /* ... etc */
1077
- }
1078
- ```
1079
-
1080
- ### Key CSS Classes
1081
-
1082
- | Class | Purpose |
1083
- |---|---|
1084
- | `.calendar-loading-overlay` | Translucent loading indicator overlay |
1085
- | `.calendar-scroll-container` | Scrollable grid container |
1086
- | `.calendar-time-gutter` | Hour labels column |
1087
- | `.calendar-current-time-indicator` | Red line + dot for current time |
1088
- | `.calendar-selection-overlay` | Blue drag selection highlight |
1089
- | `.day-events-expansion-backdrop` | Backdrop for expanded month cell |
1090
- | `.day-events-expansion-panel` | Expanded events panel in month view |
1091
-
1092
- ---
1093
-
1094
- ## 19. Constants
1095
-
1096
- Located in `src/core/constants.ts`.
1097
-
1098
- ### View List
1099
-
1100
- ```typescript
1101
- const CALENDAR_VIEWS: TCalendarView[] = [
1102
- 'day', 'week', 'month', 'year', 'agenda',
1103
- 'resource-day', 'resource-week',
1104
- 'timeline-day', 'timeline-week'
1105
- ];
1106
- ```
1107
-
1108
- ### Default Preferences
1109
-
1110
- ```typescript
1111
- const DEFAULT_PREFERENCES: ICalendarPreferences = {
1112
- startHour: 8,
1113
- endHour: 18,
1114
- firstDayOfWeek: 1, // Monday
1115
- slotDuration: 30,
1116
- showWeekends: true,
1117
- timeFormat: '24h',
1118
- badgeVariant: 'colored',
1119
- showCanceledEvents: false,
1120
- };
1121
- ```
1122
-
1123
- ### Color Palette
1124
-
1125
- ```typescript
1126
- const EVENT_COLORS: Record<TEventColor, string> = {
1127
- blue: '#3b82f6', green: '#22c55e', red: '#ef4444',
1128
- yellow: '#eab308', purple: '#a855f7', orange: '#f97316',
1129
- pink: '#ec4899', teal: '#14b8a6', gray: '#6b7280', indigo: '#6366f1',
1130
- };
1131
-
1132
- const EVENT_COLOR_CLASSES: Record<TEventColor, { bg, text, border }>;
1133
- const HEX_TO_EVENT_COLOR: Record<string, TEventColor>;
1134
- ```
1135
-
1136
- ### Grid Constants
1137
-
1138
- | Constant | Value |
1139
- |---|---|
1140
- | `DEFAULT_HOUR_HEIGHT` | 64px |
1141
- | `DEFAULT_SLOT_HEIGHT` | 32px |
1142
- | `DEFAULT_HEADER_HEIGHT` | 56px |
1143
- | `DEFAULT_RESOURCE_WIDTH` | 200px |
1144
- | `MIN_EVENT_HEIGHT` | 20px |
1145
- | `HOURS_IN_DAY` | 24 |
1146
- | `MINUTES_IN_HOUR` | 60 |
1147
- | `MS_PER_MINUTE` | 60000 |
1148
- | `MS_PER_HOUR` | 3600000 |
1149
- | `MS_PER_DAY` | 86400000 |
1150
-
1151
- ### Time Defaults
1152
-
1153
- ```typescript
1154
- const DEFAULT_VISIBLE_HOURS: IVisibleHours = { startHour: 0, endHour: 24 };
1155
- const DEFAULT_BUSINESS_HOURS: IVisibleHours = { startHour: 8, endHour: 18 };
1156
- ```
1157
-
1158
- ---
1159
-
1160
- ## 20. Integration Guide
1161
-
1162
- ### Minimal Setup
1163
-
1164
- ```tsx
1165
- import { InnoCalendar, type CalendarEvent } from '@innosolutions/inno-calendar';
1166
- import '@innosolutions/inno-calendar/styles';
1167
-
1168
- const events: CalendarEvent[] = [
1169
- {
1170
- id: '1',
1171
- title: 'Team Meeting',
1172
- startDate: new Date('2026-02-12T09:00:00'),
1173
- endDate: new Date('2026-02-12T10:00:00'),
1174
- color: 'blue',
1175
- },
1176
- ];
1177
-
1178
- function App() {
1179
- return (
1180
- <InnoCalendar
1181
- events={events}
1182
- onEventClick={(event) => console.log('Clicked:', event.title)}
1183
- onSlotSelect={(sel) => console.log('Selected:', sel.startDate, sel.endDate)}
1184
- />
1185
- );
1186
- }
1187
- ```
1188
-
1189
- ### With Custom Popover
1190
-
1191
- ```tsx
1192
- <InnoCalendar
1193
- events={events}
1194
- renderPopover={({ event, onClose }) => (
1195
- <div className="p-4 space-y-2">
1196
- <h3 className="font-bold">{event.title}</h3>
1197
- <p>{event.description}</p>
1198
- <button onClick={onClose}>Close</button>
1199
- </div>
1200
- )}
1201
- />
1202
- ```
1203
-
1204
- ### With Loading State
1205
-
1206
- ```tsx
1207
- const [isLoading, setIsLoading] = useState(false);
1208
-
1209
- <InnoCalendar
1210
- events={events}
1211
- isLoading={isLoading}
1212
- onDateChange={async (date, view) => {
1213
- setIsLoading(true);
1214
- const newEvents = await fetchEvents(date, view);
1215
- setEvents(newEvents);
1216
- setIsLoading(false);
1217
- }}
1218
- />
1219
- ```
1220
-
1221
- ### With Deep-Linking
1222
-
1223
- ```tsx
1224
- // URL: /calendar?eventId=appointment-42
1225
- const eventId = useSearchParams().get('eventId');
1226
-
1227
- <InnoCalendar events={events} focusEventId={eventId} />
1228
- ```
1229
-
1230
- ### Imperative Control
1231
-
1232
- ```tsx
1233
- const ref = useRef<ICalendarRefHandle>(null);
1234
-
1235
- <Button onClick={() => ref.current?.scrollToWorkingHours()}>
1236
- Go to Work Hours
1237
- </Button>
1238
-
1239
- <InnoCalendar ref={ref} events={events} />
1240
- ```
1241
-
1242
- ### Resource / Timeline Views
1243
-
1244
- ```tsx
1245
- const resources: IResource[] = [
1246
- { id: 'room-a', name: 'Meeting Room A', color: 'blue' },
1247
- { id: 'room-b', name: 'Board Room', color: 'green' },
1248
- ];
1249
-
1250
- <InnoCalendar
1251
- events={events}
1252
- initialView="timeline-week"
1253
- onEventDrop={(result) => {
1254
- // result.newResourceId is set when event is dragged to a different row
1255
- api.updateEvent(result.event.id, {
1256
- startDate: result.newStartDate,
1257
- endDate: result.newEndDate,
1258
- resourceId: result.newResourceId,
1259
- });
1260
- }}
1261
- />
1262
- ```
1263
-
1264
- ### Header Configuration
1265
-
1266
- ```tsx
1267
- // Hide resource views and settings — only show calendar views + Today + navigation
1268
- <InnoCalendar
1269
- events={events}
1270
- headerConfig={{
1271
- showResourceViews: false,
1272
- showSettings: false,
1273
- showFilters: false,
1274
- }}
1275
- />
1276
- ```
1277
-
1278
- ### Event Detail Mode
1279
-
1280
- ```tsx
1281
- // Show event details in a slide-in sheet instead of default popover
1282
- <InnoCalendar
1283
- events={events}
1284
- eventDetailMode="sheet"
1285
- />
1286
-
1287
- // Fully custom event detail container
1288
- <InnoCalendar
1289
- events={events}
1290
- eventDetailMode="custom"
1291
- renderEventDetail={({ event, onClose }) => (
1292
- <MyCustomDrawer event={event} onClose={onClose} />
1293
- )}
1294
- />
1295
- ```
1296
-
1297
- ### With Preferences Locking
1298
-
1299
- ```tsx
1300
- <InnoCalendar
1301
- events={events}
1302
- preferencesConfig={{
1303
- defaults: { view: 'month', badgeVariant: 'dot' },
1304
- modes: {
1305
- slotDuration: 'locked',
1306
- showWeekends: 'locked',
1307
- },
1308
- locked: {
1309
- slotDuration: 30,
1310
- showWeekends: false,
1311
- },
1312
- }}
1313
- />
1314
- ```
1315
-
1316
- ### Widget Embedding
1317
-
1318
- ```tsx
1319
- import { AgendaWidget, AgendaDropdown } from '@innosolutions/inno-calendar';
1320
-
1321
- // Dashboard card
1322
- <AgendaWidget events={todayEvents} onEventClick={openEvent} />
1323
-
1324
- // Navbar notification bell
1325
- <AgendaDropdown events={upcomingEvents} onEventClick={openEvent} />
1326
- ```
1327
-
1328
- ### Custom Slot Example
1329
-
1330
- ```tsx
1331
- <InnoCalendar
1332
- events={events}
1333
- slots={{
1334
- dayCellFooter: ({ date, events }) => (
1335
- <div className="text-xs text-muted-foreground border-t mt-1 pt-1">
1336
- {events.length} events
1337
- </div>
1338
- ),
1339
- columnHeader: ({ date, isToday, onDayClick }) => (
1340
- <button
1341
- className={cn('text-center', isToday && 'font-bold text-primary')}
1342
- onClick={() => onDayClick?.(date)}
1343
- >
1344
- {date.toLocaleDateString(undefined, { weekday: 'short', day: 'numeric' })}
1345
- </button>
1346
- ),
1347
- }}
1348
- />
1349
- ```
1350
-
1351
- ---
1352
-
1353
- ## 21. Full Export Catalog
1354
-
1355
- ### From `src/components/`
1356
-
1357
- **Main Components:**
1358
- - `InnoCalendar`, `InnoCalendarProps`
1359
- - `Calendar`, `CalendarProps`
1360
- - `IntegratedCalendar` (alias), `IntegratedCalendarProps` (alias)
1361
-
1362
- **Event Components:**
1363
- - `EventCard`, `EventCardProps`
1364
- - `EventBlock`, `EventBlockProps`
1365
- - `EventPopover`, `EventPopoverProps`, `IEventPopoverLabels`, `IPopoverEvent`, `IEventParticipant`
1366
- - `MultiDayEventBar`, `MultiDayEventBarProps`
1367
- - `getEventColorClasses`
1368
-
1369
- **Header Components:**
1370
- - `CalendarHeader`, `CalendarHeaderProps`
1371
- - `DateNavigator`, `DateNavigatorProps`
1372
- - `TodayButton`, `TodayButtonProps`
1373
-
1374
- **View Components:**
1375
- - `DayView`, `DayViewProps`
1376
- - `WeekView`, `WeekViewProps`
1377
- - `MonthView`, `MonthViewProps`
1378
- - `YearView`, `YearViewProps`
1379
- - `AgendaView`, `AgendaViewProps`
1380
- - `TimelineView`, `TimelineViewProps`
1381
- - `DayEventsExpansion`, `DayEventsExpansionProps`
1382
-
1383
- **Primitive Components:**
1384
- - `DaySlot`, `DaySlotProps`
1385
- - `TimeSlot`
1386
- - `MultiDayBanner`, `MultiDayBannerProps`
1387
-
1388
- **Settings Components:**
1389
- - `BadgeVariantSetting`, `BadgeVariantSettingProps`
1390
- - `SlotDurationSetting`, `SlotDurationSettingProps`
1391
- - `VisibleHoursSetting`, `VisibleHoursSettingProps`
1392
- - `WorkingHoursSetting`, `WorkingHoursSettingProps`
1393
- - `DEFAULT_WEEK_WORKING_HOURS`, `IDayWorkingHours`, `IWeekWorkingHours`, `TDayOfWeek`
1394
-
1395
- **Filter Components:**
1396
- - `CalendarFilterSidebar`, `CalendarFilterSidebarProps`, `CalendarFilterSidebarLabels`
1397
- - `ScheduleTypeFilter`, `ScheduleTypeFilterProps`, `IScheduleTypeOption`
1398
- - `UserFilter`, `UserFilterProps`, `IUserOption`
1399
-
1400
- **Widget Components:**
1401
- - `AgendaWidget`, `AgendaWidgetProps`
1402
- - `AgendaDropdown`, `AgendaDropdownProps`
1403
-
1404
- **UI Primitives:**
1405
- - `Button`, `ButtonProps`, `buttonVariants`
1406
- - `Dialog`, `DialogContent`, `DialogTrigger`, `DialogClose`, `DialogHeader`, `DialogTitle`, `DialogDescription`
1407
- - `Popover`, `PopoverContent`, `PopoverTrigger`, `PopoverAnchor`
1408
- - `Sheet`, `SheetContent`, `SheetTrigger`, `SheetClose`, `SheetHeader`, `SheetTitle`, `SheetDescription`
1409
- - `Tooltip`, `TooltipContent`, `TooltipProvider`, `TooltipTrigger`
1410
-
1411
- ### From `src/core/`
1412
-
1413
- **Types** (60+ type exports) — see Section 4.
1414
-
1415
- **Context Providers:**
1416
- - `InnoCalendarProvider`, `InnoCalendarProviderProps`
1417
- - `CalendarProvider`, `CalendarProviderProps`
1418
- - `SlotSelectionProvider`, `SlotSelectionProviderProps`
1419
- - `DragDropProvider`, `DragDropProviderProps`
1420
- - `IntegratedCalendarProvider` (alias), `IntegratedCalendarProviderProps` (alias)
1421
-
1422
- **Hooks** see Section 14.
1423
-
1424
- **Constants** — see Section 19.
1425
-
1426
- **Utilities** — see Section 16. All re-exported via `export * from './utils'`.
1427
-
1428
- **Preferences:**
1429
- - `IPreferences`, `IPreferencesConfig`, `IUsePreferencesReturn`
1430
- - `TPreferenceMode`, `TPreferenceModes`, `TLockedPreferences`, `TPartialPreferences`, `TPreferenceKey`
1431
- - `IVisibleHoursConfig`, `TWorkingHoursConfig`
1432
- - `PREFERENCES_STORAGE_KEY`, default constants
1433
- - `useAdvancedPreferences` (re-exported as alias)
1434
-
1435
- ### From `src/presets/`
1436
-
1437
- - `DefaultCalendar`
1438
- - `TailwindCalendar`
1439
-
1440
- ### From `src/lib/`
1441
-
1442
- - `cn` — `clsx(...inputs) | twMerge` className composer
1443
-
1444
- ---
1445
-
1446
- ## 22. File Naming Conventions
1447
-
1448
- **All files and folders use kebab-case.**
1449
-
1450
- | Type | Pattern | Example |
1451
- |---|---|---|
1452
- | Components | `kebab-case.tsx` | `event-card.tsx` |
1453
- | Hooks | `use-kebab-case.ts` | `use-calendar.ts` |
1454
- | Context | `kebab-case.tsx` | `calendar-context.tsx` |
1455
- | Types | `types.ts` | `types.ts` |
1456
- | Utilities | `kebab-case.ts` | `date-utils.ts` |
1457
- | Constants | `constants.ts` | `constants.ts` |
1458
- | Barrel exports | `index.ts` | `index.ts` |
1459
- | Styles | `kebab-case.css` | `calendar.css` |
1460
-
1461
- ---
1462
-
1463
- ## 23. Styling Patterns
1464
-
1465
- ### className Composition
1466
-
1467
- ```tsx
1468
- import { cn } from '../../lib/utils';
1469
-
1470
- <div className={cn('base-styles', isActive && 'active-styles', className)} />
1471
- ```
1472
-
1473
- ### CVA for Variants
1474
-
1475
- ```tsx
1476
- import { cva, type VariantProps } from 'class-variance-authority';
1477
-
1478
- const buttonVariants = cva('base-classes', {
1479
- variants: {
1480
- variant: { default: '...', outline: '...' },
1481
- size: { sm: '...', md: '...' },
1482
- },
1483
- defaultVariants: { variant: 'default', size: 'md' },
1484
- });
1485
- ```
1486
-
1487
- ---
1488
-
1489
- ## 24. Biome Configuration
1490
-
1491
- From `biome.json`:
1492
-
1493
- - **Formatter**: tabs, single quotes, 100 line width
1494
- - **Linter**: recommended + `noUnusedVariables`, `noExplicitAny`
1495
- - **Organize imports**: enabled
1496
-
1497
- ```bash
1498
- npm run lint # Check
1499
- npm run format # Auto-fix
1500
- ```
1501
-
1502
- ---
1503
-
1504
- ## 25. State Synchronization Guide
1505
-
1506
- How state flows through the calendar and guidance for integrating with consumer projects.
1507
-
1508
- ### Data Flow
1509
-
1510
- ```
1511
- Consumer App
1512
- └─ <InnoCalendar events={events} onEventDrop={...} ...>
1513
- └─ DragDropProvider (drag state, onEventDrop callback)
1514
- └─ InnoCalendarProvider (view, date, preferences, filtered events)
1515
- └─ SlotSelectionProvider (drag-to-select state)
1516
- └─ InnerCalendar (renders header + active view)
1517
- ├─ CalendarHeader (nav, view switcher, settings)
1518
- └─ Active View (DayView, WeekView, TimelineView, etc.)
1519
- └─ EventCard (popover/dialog/sheet/custom)
1520
- ```
1521
-
1522
- ### Events: BYO Approach
1523
-
1524
- The package **never fetches data**. The consumer owns the events array and updates it:
1525
-
1526
- ```tsx
1527
- // Using React Query
1528
- const { data: events } = useQuery(['events', dateRange], fetchEvents);
1529
- const updateEvent = useMutation(api.updateEvent, {
1530
- onSuccess: () => queryClient.invalidateQueries(['events']),
1531
- });
1532
-
1533
- <InnoCalendar
1534
- events={events ?? []}
1535
- onEventDrop={(result) => updateEvent.mutate({
1536
- id: result.event.id,
1537
- startDate: result.newStartDate,
1538
- endDate: result.newEndDate,
1539
- resourceId: result.newResourceId,
1540
- })}
1541
- onSlotSelect={(sel) => openCreateDialog(sel.startDate, sel.endDate)}
1542
- />
1543
- ```
1544
-
1545
- ### Preferences Persistence
1546
-
1547
- Preferences (view, slot duration, badge variant, etc.) persist to `localStorage` by default. The key is configurable:
1548
-
1549
- ```tsx
1550
- <InnoCalendar
1551
- preferencesConfig={{
1552
- storageKey: 'my-app-calendar-prefs', // default: 'inno-calendar-preferences'
1553
- defaults: { view: 'week', badgeVariant: 'colored' },
1554
- modes: { slotDuration: 'locked' }, // prevent user from changing
1555
- locked: { slotDuration: 30 },
1556
- }}
1557
- />
1558
- ```
1559
-
1560
- ### Cross-Project State Patterns
1561
-
1562
- For apps that embed InnoCalendar (e.g., promosport-erp, generationimmo-erp):
1563
-
1564
- 1. **URL Sync** — Sync view/date with URL params so back-button works:
1565
- ```tsx
1566
- <InnoCalendar
1567
- initialView={searchParams.get('view') as TCalendarView ?? 'week'}
1568
- initialDate={searchParams.get('date') ? new Date(searchParams.get('date')!) : undefined}
1569
- onViewChange={(v) => setSearchParams({ view: v })}
1570
- onDateChange={(d) => setSearchParams({ date: d.toISOString() })}
1571
- />
1572
- ```
1573
-
1574
- 2. **Deep-linking events** Navigate to a specific event from another page:
1575
- ```tsx
1576
- <InnoCalendar events={events} focusEventId={eventIdFromUrl} />
1577
- ```
1578
-
1579
- 3. **External filter state** — The calendar's built-in filtering is opt-in. You can filter server-side and just pass filtered `events`:
1580
- ```tsx
1581
- const filteredEvents = useFilteredEvents(allEvents, externalFilters);
1582
- <InnoCalendar events={filteredEvents} />
1583
- ```
1584
-
1585
- 4. **Imperative control** — Use ref for programmatic scroll/navigation:
1586
- ```tsx
1587
- const ref = useRef<ICalendarRefHandle>(null);
1588
- ref.current?.scrollToToday();
1589
- ref.current?.scrollToWorkingHours();
1590
- ref.current?.focusEvent(eventId);
1591
- ```
1592
-
1593
- ---
1594
-
1595
- ## 26. Critical Rules
1596
-
1597
- 1. **CalendarEvent must stay domain-agnostic** — no new top-level fields. Use `data?: TData` for domain data.
1598
- 2. **Deprecated fields stay** until next major version — never remove them prematurely.
1599
- 3. **Built-in components read `event.data` first**, then fall back to deprecated top-level fields.
1600
- 4. **Core module has zero external dependencies** — only React + native APIs.
1601
- 5. **Follow existing patterns** before inventing new ones (composition, slot props, CVA).
1602
- 6. **kebab-case everywhere** files, folders, no exceptions.
1603
- 7. **JSDoc on every public export** — summary + `@param` + `@example` + `@default`.
1604
- 8. **Run `npm run typecheck`** before committing.
1605
- 9. **Update AGENT.md, README.md, and copilot-instructions.md** when public API changes.
1606
- 10. **Section separators** in type files: `// ============================================================================`
1607
- 11. **Performance**: `useMemo`, `useCallback`, memoize context values, stable keys.
1608
- 12. **Accessibility**: semantic HTML, ARIA attributes, keyboard navigation (Arrow keys, Enter/Space, Escape, Tab).
1609
- 13. **No inline objects/arrays in JSX props** — extract or memoize them.
1610
- 14. **Generic type propagation** — maintain `<TData>` through hooks, context, and component boundaries.
1
+ # @innosolutions/inno-calendar — AI Agent Reference
2
+
3
+ > **Version 1.0.58** | React 18+/19 | TypeScript 5.9 | Vite 7 library-mode
4
+ > Headless-first, fully customizable React calendar for enterprise applications.
5
+
6
+ ---
7
+
8
+ ## Table of Contents
9
+
10
+ 1. [What This Package Is](#1-what-this-package-is)
11
+ 2. [npm Scripts](#2-npm-scripts)
12
+ 3. [Package Exports](#3-package-exports)
13
+ 4. [Core Types](#4-core-types)
14
+ 5. [InnoCalendar Props](#5-innocalendar-props)
15
+ 6. [Slots System](#6-slots-system)
16
+ 7. [ClassNames API](#7-classnames-api)
17
+ 8. [EventCard & EventBlock](#8-eventcard--eventblock)
18
+ 9. [EventPopover Component](#9-eventpopover-component)
19
+ 10. [Preferences System](#10-preferences-system)
20
+ 11. [Widget Components](#11-widget-components)
21
+ 12. [Filter Components](#12-filter-components)
22
+ 13. [Settings Components](#13-settings-components)
23
+ 14. [Context & Hooks](#14-context--hooks)
24
+ 15. [Drag & Drop](#15-drag--drop)
25
+ 16. [Core Utilities](#16-core-utilities)
26
+ 17. [Presets](#17-presets)
27
+ 18. [CSS & Theming](#18-css--theming)
28
+ 19. [Constants](#19-constants)
29
+ 20. [Integration Guide](#20-integration-guide)
30
+ 21. [Full Export Catalog](#21-full-export-catalog)
31
+ 22. [File Naming Conventions](#22-file-naming-conventions)
32
+ 23. [Styling Patterns](#23-styling-patterns)
33
+ 24. [Biome Configuration](#24-biome-configuration)
34
+ 25. [State Synchronization Guide](#25-state-synchronization-guide)
35
+ 26. [Critical Rules](#26-critical-rules)
36
+
37
+ ---
38
+
39
+ ## 1. What This Package Is
40
+
41
+ A **headless-first** React calendar component library — all view logic lives in hooks and contexts; every UI element is an optional, replaceable component.
42
+
43
+ ### Feature Set
44
+
45
+ | Category | Features |
46
+ |---|---|
47
+ | **Views** | Day, Week, Month, Year, Agenda, Resource-Day, Resource-Week, Timeline-Day, Timeline-3Day, Timeline-Week |
48
+ | **Interaction** | Drag-to-select (time & day mode), event click, slot click, drag-and-drop reposition, event resize |
49
+ | **Customization** | 25+ component slots, 30+ classNames keys, render props, custom popover |
50
+ | **Data** | Generic `CalendarEvent<TData>`, adapter pattern, BYO data fetching |
51
+ | **Preferences** | localStorage persistence, locking, session mode, per-instance storage keys |
52
+ | **Filtering** | Built-in schedule type filter, user/participant filter, text search |
53
+ | **UI Extras** | Loading overlay, focus-event deep-linking, imperative ref API, settings panel, filter sidebar |
54
+ | **Embeddable** | AgendaWidget (standalone card), AgendaDropdown (bell-icon notification panel) |
55
+ | **Presets** | DefaultCalendar (styled), TailwindCalendar (Tailwind-optimized) |
56
+
57
+ ### Tech Stack
58
+
59
+ | Layer | Technology |
60
+ |---|---|
61
+ | Runtime | React 18+/19, TypeScript 5.9 |
62
+ | Build | Vite 7 (library mode), vite-plugin-dts |
63
+ | Styling | TailwindCSS 4+, class-variance-authority (CVA), clsx, tailwind-merge |
64
+ | UI Primitives | Radix UI (`@radix-ui/react-dialog` as direct dep; optional peer deps: popover, tooltip, dropdown-menu, scroll-area, select, avatar) |
65
+ | Icons | lucide-react |
66
+ | Date handling | Native Date API (no external date library) |
67
+ | Linting | Biome 2.3 |
68
+
69
+ ---
70
+
71
+ ## 2. npm Scripts
72
+
73
+ ```bash
74
+ npm run dev # Vite dev mode
75
+ npm run build # Production build (Vite + dts declarations)
76
+ npm run typecheck # tsc --noEmit
77
+ npm run lint # Biome lint check
78
+ npm run format # Biome format
79
+ ```
80
+
81
+ ---
82
+
83
+ ## 3. Package Exports
84
+
85
+ Defined in `package.json`:
86
+
87
+ ```jsonc
88
+ {
89
+ "main": "./dist/index.cjs",
90
+ "module": "./dist/index.mjs",
91
+ "types": "./dist/index.d.ts",
92
+ "exports": {
93
+ ".": {
94
+ "types": "./dist/index.d.ts",
95
+ "import": "./dist/index.mjs",
96
+ "require": "./dist/index.cjs"
97
+ },
98
+ "./utils": {
99
+ "types": "./dist/utils.d.ts",
100
+ "import": "./dist/utils.mjs",
101
+ "require": "./dist/utils.cjs"
102
+ },
103
+ "./core": {
104
+ "types": "./dist/core/index.d.ts",
105
+ "import": "./dist/core/index.mjs",
106
+ "require": "./dist/core/index.cjs"
107
+ },
108
+ "./components": {
109
+ "types": "./dist/components/index.d.ts",
110
+ "import": "./dist/components/index.mjs",
111
+ "require": "./dist/components/index.cjs"
112
+ },
113
+ "./presets": {
114
+ "types": "./dist/presets/index.d.ts",
115
+ "import": "./dist/presets/index.mjs",
116
+ "require": "./dist/presets/index.cjs"
117
+ },
118
+ "./lib": {
119
+ "types": "./dist/lib/index.d.ts",
120
+ "import": "./dist/lib/index.mjs",
121
+ "require": "./dist/lib/index.cjs"
122
+ },
123
+ "./styles": "./dist/styles/index.css",
124
+ "./styles.css": "./dist/styles/index.css"
125
+ }
126
+ }
127
+ ```
128
+
129
+ Consumer import patterns:
130
+
131
+ ```tsx
132
+ // Components and types
133
+ import { InnoCalendar, type CalendarEvent } from '@innosolutions/inno-calendar';
134
+ // Core-only (headless hooks, types, utils)
135
+ import { useCalendar, type TCalendarView } from '@innosolutions/inno-calendar/core';
136
+ // Components only
137
+ import { EventCard } from '@innosolutions/inno-calendar/components';
138
+ // Presets only
139
+ import { DefaultCalendar } from '@innosolutions/inno-calendar/presets';
140
+ // Styles (required for built-in components)
141
+ import '@innosolutions/inno-calendar/styles';
142
+ ```
143
+
144
+ ---
145
+
146
+ ## 4. Core Types
147
+
148
+ All types are defined in `src/core/types.ts` (1449 lines).
149
+
150
+ ### CalendarEvent\<TData\>
151
+
152
+ ```typescript
153
+ interface CalendarEvent<TData = Record<string, unknown>> extends IBaseEvent {
154
+ // Required (from IBaseEvent)
155
+ id: string;
156
+ title: ReactNode; // String or JSX — plain strings are fully backwards compatible
157
+ startDate: Date;
158
+ endDate: Date;
159
+
160
+ // Optional visual/behavioral
161
+ description?: ReactNode; // String or JSX
162
+ color?: TEventColor;
163
+ hexColor?: string;
164
+ isAllDay?: boolean;
165
+ isMultiDay?: boolean;
166
+ isCanceled?: boolean;
167
+ isRecurring?: boolean;
168
+ resourceId?: string;
169
+
170
+ // Built-in filtering (optional)
171
+ scheduleTypeId?: number;
172
+ scheduleTypeName?: ReactNode; // String or JSX
173
+ participants?: ICalendarUser[];
174
+
175
+ // Consumer data bag (RECOMMENDED for all domain fields)
176
+ data?: TData;
177
+
178
+ // @deprecated — still functional, will be removed in next major
179
+ meetingTookPlace?: boolean;
180
+ isAccepted?: boolean;
181
+ companyId?: number;
182
+ cancelReason?: string | null;
183
+ scheduleTypeCode?: string;
184
+ opportunityId?: number;
185
+ propertyId?: number;
186
+ projectId?: number;
187
+ contactId?: number;
188
+ user?: ICalendarUser;
189
+ }
190
+ ```
191
+
192
+ #### ReactNode Props
193
+
194
+ `title`, `description`, and `scheduleTypeName` accept `ReactNode`. Pass plain strings (backwards compatible) or custom JSX. Built-in search and `aria-label` use `reactNodeToText()` to extract plain text from JSX nodes automatically.
195
+
196
+ #### data bag: ReactNode-compatible fields
197
+
198
+ The following `event.data` fields are rendered as `ReactNode` in EventCard/EventBlock:
199
+
200
+ | Field | Type | Description |
201
+ |---|---|---|
202
+ | `productName` | `ReactNode` | Product/activity name line |
203
+ | `siteName` | `ReactNode` | Location site name |
204
+ | `siteCity` | `ReactNode` | Location city |
205
+ | `extendedProps` | `ReactNode[]` | Arbitrary custom lines rendered below productName |
206
+
207
+ `extendedProps` renders each array entry as its own row. Visible on EventCard (full variant always, EventBlock when >= 45min). Also shown in tooltips.
208
+
209
+ ### IResource\<TData\>
210
+
211
+ ```typescript
212
+ interface IResource<TData = Record<string, unknown>> {
213
+ id: string;
214
+ name: string;
215
+ title?: string;
216
+ avatar?: string;
217
+ color?: TEventColor | string;
218
+ data?: TData;
219
+ }
220
+ ```
221
+
222
+ ### IScheduleType\<TData\>
223
+
224
+ ```typescript
225
+ interface IScheduleType<TData = Record<string, unknown>> {
226
+ id: number;
227
+ name: string;
228
+ color?: TEventColor | string;
229
+ icon?: string;
230
+ isDefault?: boolean;
231
+ isActive?: boolean;
232
+ code?: string;
233
+ resourceKey?: string;
234
+ colorHex?: string;
235
+ defaultMinutes?: number;
236
+ data?: TData;
237
+ }
238
+ ```
239
+
240
+ ### ICalendarUser\<TData\>
241
+
242
+ ```typescript
243
+ interface ICalendarUser<TData = Record<string, unknown>> {
244
+ id: string;
245
+ name: string;
246
+ email?: string;
247
+ avatar?: string;
248
+ data?: TData;
249
+ }
250
+ ```
251
+
252
+ ### ICalendarRefHandle
253
+
254
+ Imperative handle exposed via `React.forwardRef` on `<InnoCalendar>`:
255
+
256
+ ```typescript
257
+ interface ICalendarRefHandle {
258
+ scrollToToday: () => void;
259
+ scrollToWorkingHours: () => void;
260
+ getViewRect: () => DOMRect | null;
261
+ focusEvent: (eventId: string) => void;
262
+ }
263
+ ```
264
+
265
+ Usage:
266
+
267
+ ```tsx
268
+ const ref = useRef<ICalendarRefHandle>(null);
269
+ <InnoCalendar ref={ref} events={events} />
270
+ // Later:
271
+ ref.current?.scrollToToday();
272
+ ref.current?.focusEvent('appointment-42');
273
+ ```
274
+
275
+ ### ICalendarPreferences
276
+
277
+ ```typescript
278
+ interface ICalendarPreferences {
279
+ startHour: number; // 0–23
280
+ endHour: number; // 0–24
281
+ firstDayOfWeek: 0|1|2|3|4|5|6;
282
+ slotDuration: number;
283
+ showWeekends: boolean;
284
+ timeFormat: '12h' | '24h';
285
+ badgeVariant: TBadgeVariant;
286
+ showCanceledEvents: boolean;
287
+ }
288
+ ```
289
+
290
+ ### View Types
291
+
292
+ ```typescript
293
+ type TCalendarView =
294
+ | 'day' | 'week' | 'month' | 'year' | 'agenda'
295
+ | 'resource-day' | 'resource-week'
296
+ | 'timeline-day' | 'timeline-3day' | 'timeline-week';
297
+ ```
298
+
299
+ ### Other Key Types
300
+
301
+ | Type | Purpose |
302
+ |---|---|
303
+ | `TEventColor` | `'blue'│'green'│'red'│'yellow'│'purple'│'orange'│'pink'│'teal'│'gray'│'indigo'` |
304
+ | `TBadgeVariant` | `'dot'│'colored'│'mixed'` |
305
+ | `TSlotDuration` | `15│30│60` |
306
+ | `TWorkingHours` | `Record<number, IWorkingHoursDay>` (0=Sun..6=Sat) |
307
+ | `ISelectionResult` | `{ startDate: Date; endDate: Date; resourceId?: string }` |
308
+ | `IDropResult<TData>` | Drag-drop result with event, newStartDate, newEndDate, newResourceId |
309
+ | `IDateRange` | `{ startDate: Date; endDate: Date }` |
310
+ | `IEventPosition` | `{ top, height, left, width, zIndex }` |
311
+ | `IPositionedEvent<TData>` | Event + IEventPosition + column info |
312
+ | `ICalendarFilters` | `{ scheduleTypeIds?, resourceIds?, search?, showCanceled?, dateRange?, [key]: unknown }` |
313
+ | `TSelectionMode` | `'time'│'day'` |
314
+ | `TEventDetailMode` | `'popover'│'dialog'│'sheet'│'custom'` — controls how event details are shown |
315
+ | `ICalendarHeaderConfig` | Object with 7 optional boolean keys controlling header section visibility |
316
+
317
+ ### Callback Types
318
+
319
+ | Type | Signature |
320
+ |---|---|
321
+ | `TOnEventClick<TData>` | `(event, e?) => void` |
322
+ | `TOnSlotSelect` | `(selection: ISelectionResult) => void` |
323
+ | `TOnEventDrop<TData>` | `(event, newStart, newEnd, newResourceId?) => void` |
324
+ | `TOnEventResize<TData>` | `(event, newStart, newEnd) => void` |
325
+ | `TOnViewChange` | `(view: TCalendarView) => void` |
326
+ | `TOnDateChange` | `(date: Date) => void` |
327
+ | `TOnFiltersChange` | `(filters: ICalendarFilters) => void` |
328
+
329
+ ---
330
+
331
+ ## 5. InnoCalendar Props
332
+
333
+ `<InnoCalendar>` is the primary entry point. It wraps `InnoCalendarProvider` + `SlotSelectionProvider` + `DragDropProvider` internally.
334
+
335
+ ```typescript
336
+ interface InnoCalendarProps<TEventData> {
337
+ // === DATA ===
338
+ events: CalendarEvent<TEventData>[];
339
+ users?: ICalendarUser[];
340
+ scheduleTypes?: IScheduleType[];
341
+
342
+ // === INITIAL STATE ===
343
+ initialView?: TCalendarView; // default: 'week'
344
+ initialDate?: Date; // default: new Date()
345
+ initialSelectedUserId?: string | 'all'; // default: 'all'
346
+ initialScheduleTypeIds?: number[];
347
+ initialParticipantIds?: string[];
348
+ initialWorkingHoursView?: 'default' | 'enabled' | 'disabled';
349
+ initialSearchQuery?: string;
350
+
351
+ // === PREFERENCES ===
352
+ preferencesConfig?: IPreferencesConfig;
353
+
354
+ // === CALLBACKS ===
355
+ onEventClick?: (event: CalendarEvent<TEventData>) => void;
356
+ onSlotClick?: (date: Date, hour?: number) => void;
357
+ onSlotSelect?: (selection: ISelectionResult) => void;
358
+ onAddEvent?: () => void;
359
+ onEventDrop?: (result: IDropResult<TEventData>) => void;
360
+ onDateChange?: (date: Date, view: TCalendarView) => void;
361
+ onViewChange?: (view: TCalendarView) => void;
362
+
363
+ // === UI OPTIONS ===
364
+ className?: string;
365
+ showHeader?: boolean; // default: true
366
+ minSelectionMinutes?: number; // default: 30
367
+ showMoreMode?: 'dayView' | 'popover' | 'expand'; // default: 'expand'
368
+ isLoading?: boolean; // default: false
369
+
370
+ // === CUSTOM RENDERING ===
371
+ renderPopover?: (props: { event; onClose }) => ReactNode;
372
+ slots?: ICalendarSlots<TEventData>;
373
+ classNames?: ICalendarClassNames;
374
+
375
+ // === HEADER CONFIGURATION ===
376
+ headerConfig?: ICalendarHeaderConfig; // fine-grained header visibility
377
+ // see ICalendarHeaderConfig below
378
+
379
+ // === EVENT DETAIL MODE ===
380
+ eventDetailMode?: TEventDetailMode; // default: 'popover'
381
+ renderEventDetail?: (props: { event; onClose }) => ReactNode; // for 'custom' + override in dialog/sheet
382
+
383
+ // === CONTENT SLOTS ===
384
+ settingsContent?: ReactNode;
385
+ filterContent?: ReactNode;
386
+ headerActions?: ReactNode;
387
+
388
+ // === DEEP LINKING ===
389
+ focusEventId?: string | null;
390
+
391
+ // @deprecated — use showMoreMode instead
392
+ showMoreEventsInPopover?: boolean;
393
+ }
394
+ ```
395
+
396
+ #### ICalendarHeaderConfig
397
+
398
+ Fine-grained visibility control for calendar header sections. Each key defaults to `true` when omitted.
399
+
400
+ ```typescript
401
+ interface ICalendarHeaderConfig {
402
+ showToday?: boolean; // Today button
403
+ showDateNavigator?: boolean; // Prev/Next arrows + date label
404
+ showCalendarViews?: boolean; // Day/Week/Month/Agenda dropdown
405
+ showResourceViews?: boolean; // Timeline/Resource dropdown
406
+ showSettings?: boolean; // Settings gear icon
407
+ showAddEvent?: boolean; // "+" add event button
408
+ showFilters?: boolean; // Filter row below header
409
+ }
410
+ ```
411
+
412
+ #### TEventDetailMode
413
+
414
+ Controls how event details are displayed when clicking an event:
415
+
416
+ - `'popover'` — Radix popover anchored to the event card (default)
417
+ - `'dialog'` — Centered modal dialog overlay
418
+ - `'sheet'` — Slide-in side panel from the right
419
+ - `'custom'` — No built-in container; delegates to `renderEventDetail`
420
+
421
+ ```typescript
422
+ type TEventDetailMode = 'popover' | 'dialog' | 'sheet' | 'custom';
423
+ ```
424
+
425
+ ### Ref Support
426
+
427
+ `InnoCalendar` is wrapped with `forwardRef` and exposes `ICalendarRefHandle`:
428
+
429
+ ```tsx
430
+ const calendarRef = useRef<ICalendarRefHandle>(null);
431
+ <InnoCalendar ref={calendarRef} events={events} />
432
+ ```
433
+
434
+ ---
435
+
436
+ ## 6. Slots System
437
+
438
+ Every visual region can be replaced via `slots`:
439
+
440
+ ```typescript
441
+ interface ICalendarSlots<TEventData, TScheduleTypeData, TResourceData> {
442
+ // Structural
443
+ header?: ComponentType<IHeaderSlotProps>;
444
+ footer?: ComponentType<IFooterSlotProps<TEventData>>;
445
+ viewWrapper?: ComponentType<IViewWrapperSlotProps>;
446
+
447
+ // Day Cell (month/year views)
448
+ dayCell?: ComponentType<IDayCellSlotPropsExtended<TEventData>>;
449
+ dayCellHeader?: ComponentType<IDayCellAdornmentSlotProps<TEventData>>;
450
+ dayCellFooter?: ComponentType<IDayCellAdornmentSlotProps<TEventData>>;
451
+ dayCellStartAdornment?: ComponentType<IDayCellAdornmentSlotProps<TEventData>>;
452
+ dayCellEndAdornment?: ComponentType<IDayCellAdornmentSlotProps<TEventData>>;
453
+
454
+ // Time Grid (day/week views)
455
+ timeGutter?: ComponentType<ITimeGutterSlotProps>;
456
+ timeSlotBackground?: ComponentType<ITimeSlotBackgroundSlotProps>;
457
+ columnHeader?: ComponentType<IColumnHeaderSlotProps>;
458
+ currentTimeIndicator?: ComponentType<ICurrentTimeIndicatorSlotProps>;
459
+ multiDayBanner?: ComponentType<IMultiDayBannerSlotProps<TEventData>>;
460
+
461
+ // Events
462
+ eventCard?: ComponentType<IEventCardSlotProps<TEventData>>;
463
+ eventPopover?: ComponentType<IEventPopoverSlotProps<TEventData>>;
464
+
465
+ // Resource/Timeline
466
+ resourceHeader?: ComponentType<IResourceHeaderSlotProps<TResourceData>>;
467
+ rowHeader?: ComponentType<IRowHeaderSlotProps<TResourceData>>;
468
+
469
+ // Filters
470
+ filter?: ComponentType<IFilterSlotProps<TScheduleTypeData>>;
471
+
472
+ // Empty States
473
+ emptyCell?: ComponentType<IEmptyCellSlotProps>;
474
+ emptyState?: ComponentType<IEmptyStateSlotProps>;
475
+
476
+ // @deprecated
477
+ timeSlot?: ComponentType<ITimeSlotSlotProps>;
478
+ }
479
+ ```
480
+
481
+ ### Slot Props Interfaces
482
+
483
+ | Slot | Props Interface | Key Fields |
484
+ |---|---|---|
485
+ | `header` | `IHeaderSlotProps` | currentDate, view, onNavigate, onViewChange |
486
+ | `footer` | `IFooterSlotProps` | view, currentDate, events |
487
+ | `viewWrapper` | `IViewWrapperSlotProps` | view, children |
488
+ | `dayCell` | `IDayCellSlotPropsExtended` | date, events, isToday, isCurrentMonth, isWeekend, view, children, onDayClick |
489
+ | `dayCellHeader/Footer` | `IDayCellAdornmentSlotProps` | date, events, isToday, isCurrentMonth, isWeekend, view |
490
+ | `timeGutter` | `ITimeGutterSlotProps` | hour, formattedLabel, isFirst |
491
+ | `timeSlotBackground` | `ITimeSlotBackgroundSlotProps` | date, hour, minute, isWorkingHour, isToday |
492
+ | `columnHeader` | `IColumnHeaderSlotProps` | date, isToday, onDayClick, columnIndex |
493
+ | `currentTimeIndicator` | `ICurrentTimeIndicatorSlotProps` | currentTime, topOffset |
494
+ | `multiDayBanner` | `IMultiDayBannerSlotProps` | events, rangeStart, onEventClick |
495
+ | `eventCard` | `IEventCardSlotProps` | event, view, isCompact, onClick |
496
+ | `eventPopover` | `IEventPopoverSlotProps` | event, onClose, onEdit, onDelete |
497
+ | `resourceHeader` | `IResourceHeaderSlotProps` | resource |
498
+ | `rowHeader` | `IRowHeaderSlotProps` | resource, rowIndex |
499
+ | `filter` | `IFilterSlotProps` | scheduleTypes, filters, onFiltersChange |
500
+ | `emptyCell` | `IEmptyCellSlotProps` | date, isToday |
501
+ | `emptyState` | `IEmptyStateSlotProps` | view, dateRange |
502
+
503
+ ---
504
+
505
+ ## 7. ClassNames API
506
+
507
+ `ICalendarClassNames` provides 34 CSS class override keys. Classes are merged with defaults via `cn()`.
508
+
509
+ ```typescript
510
+ interface ICalendarClassNames {
511
+ // Structural
512
+ root?: string;
513
+ header?: string;
514
+ footer?: string;
515
+ viewContainer?: string;
516
+
517
+ // Month View
518
+ monthGrid?: string;
519
+ weekdayHeader?: string;
520
+ weekdayLabel?: string;
521
+ dayCell?: string;
522
+ dayCellToday?: string;
523
+ dayCellOutside?: string;
524
+ dayCellWeekend?: string;
525
+ dayCellNumber?: string;
526
+ dayCellContent?: string;
527
+ dayCellFooter?: string;
528
+
529
+ // Week/Day View
530
+ timeGutter?: string;
531
+ timeGutterLabel?: string;
532
+ timeSlot?: string;
533
+ timeSlotWorking?: string;
534
+ timeSlotNonWorking?: string;
535
+ columnHeader?: string;
536
+ columnHeaderToday?: string;
537
+ currentTimeIndicator?: string;
538
+ scrollContainer?: string;
539
+
540
+ // Events
541
+ eventCard?: string;
542
+ eventCardCompact?: string;
543
+ eventPopover?: string;
544
+ multiDayBanner?: string;
545
+ moreEventsIndicator?: string;
546
+
547
+ // Timeline
548
+ resourceHeader?: string;
549
+ resourceRow?: string;
550
+ timelineCell?: string;
551
+
552
+ // Agenda
553
+ agendaList?: string;
554
+ agendaDayGroup?: string;
555
+ agendaDayHeader?: string;
556
+ }
557
+ ```
558
+
559
+ ---
560
+
561
+ ## 8. EventCard & EventBlock
562
+
563
+ Located in `src/components/event/event-card.tsx`.
564
+
565
+ ### EventCard
566
+
567
+ Full-size event card used in day/week/timeline views. Renders differently based on `badgeVariant`:
568
+
569
+ | Badge Variant | Rendering |
570
+ |---|---|
571
+ | `'colored'` | Filled background with accent color, white/dark text |
572
+ | `'dot'` | White background with colored left-border accent |
573
+ | `'mixed'` | Light tinted background with colored left-border |
574
+
575
+ Key props:
576
+
577
+ | Prop | Type | Default | Description |
578
+ |---|---|---|---|
579
+ | `event` | `CalendarEvent<TData>` | required | Event data |
580
+ | `variant` | `'full'\|'compact'\|'dot'` | `'full'` | Visual layout |
581
+ | `badgeVariant` | `TBadgeVariant` | `'colored'` | Color treatment |
582
+ | `onClick` | `(event) => void` | — | Click callback |
583
+ | `showTime` | `boolean` | `true` | Show time display |
584
+ | `showDescription` | `boolean` | `false` | Show event description |
585
+ | `showParticipants` | `boolean` | `false` | Show participant avatars |
586
+ | `disablePopover` | `boolean` | `false` | Skip built-in popover, fire `onClick` |
587
+ | `renderPopover` | `(props) => ReactNode` | — | Custom content inside the default popover shell |
588
+ | `enableDrag` | `boolean` | `false` | Enable HTML5 drag & drop |
589
+ | `enablePageTransition` | `boolean` | `false` | Enable page transition animation |
590
+ | `pageTransitionDuration` | `number` | `400` | Page transition duration in ms |
591
+ | `eventDetailMode` | `TEventDetailMode` | `'popover'` | Container for event details: popover, dialog, sheet, custom |
592
+ | `renderEventDetail` | `(props) => ReactNode` | — | Custom renderer for 'custom' mode; also inner content for dialog/sheet |
593
+ | `style` | `CSSProperties` | — | Inline styles |
594
+ | `className` | `string` | — | Additional CSS classes |
595
+
596
+ Built-in cards display enrichment data read from `event.data` first, with fallback to deprecated top-level fields:
597
+
598
+ ```typescript
599
+ // Runtime enrichment reading pattern:
600
+ const eventData = event.data as Record<string, unknown> | undefined;
601
+ const meetingTookPlace = (eventData?.meetingTookPlace as boolean) ?? event.meetingTookPlace ?? false;
602
+ const productName = eventData?.productName as ReactNode | undefined;
603
+ const siteName = eventData?.siteName as ReactNode | undefined;
604
+ const siteCity = eventData?.siteCity as ReactNode | undefined;
605
+ const extendedProps = eventData?.extendedProps as ReactNode[] | undefined;
606
+ const nrParticipant = eventData?.nrParticipant as number | undefined;
607
+ ```
608
+
609
+ ### EventBlock
610
+
611
+ Positioned event block for day/week time grid. Renders with absolute positioning inside the time column.
612
+
613
+ ### MultiDayEventBar
614
+
615
+ Horizontal bar for multi-day/all-day events in the banner strip above day/week time grids.
616
+
617
+ ### Exports
618
+
619
+ ```typescript
620
+ export { EventCard, type EventCardProps }
621
+ export { EventBlock, type EventBlockProps }
622
+ export { MultiDayEventBar, type MultiDayEventBarProps }
623
+ export { getEventColorClasses } // Utility: TEventColor → { bg, text, border }
624
+ ```
625
+
626
+ ---
627
+
628
+ ## 9. EventPopover Component
629
+
630
+ Located in `src/components/event/event-popover.tsx` (~1400 lines).
631
+
632
+ Full-featured event detail popover with action buttons, participant list, and domain data display.
633
+
634
+ ### Key Props
635
+
636
+ ```typescript
637
+ interface EventPopoverProps<TData = Record<string, unknown>> {
638
+ event: IPopoverEvent<TData>;
639
+ children: ReactNode;
640
+ open?: boolean;
641
+ onOpenChange?: (open: boolean) => void;
642
+ isLoading?: boolean;
643
+
644
+ // Action callbacks
645
+ onEdit?: (event: IPopoverEvent<TData>) => void;
646
+ onDelete?: (event: IPopoverEvent<TData>) => void;
647
+ onCancel?: (event: IPopoverEvent<TData>) => void;
648
+ onAccept?: (event: IPopoverEvent<TData>) => void;
649
+ onDecline?: (event: IPopoverEvent<TData>) => void;
650
+ onConfirmMeeting?: (event: IPopoverEvent<TData>) => void;
651
+
652
+ // Permission flags
653
+ canEdit?: boolean;
654
+ canDelete?: boolean;
655
+ canCancel?: boolean;
656
+
657
+ // Current user state
658
+ isCurrentUserParticipant?: boolean;
659
+ isCurrentUserClient?: boolean;
660
+ currentUserAcceptStatus?: boolean | null;
661
+
662
+ // Loading states
663
+ isAcceptLoading?: boolean;
664
+ isDeclineLoading?: boolean;
665
+ isConfirmLoading?: boolean;
666
+ isDeleteLoading?: boolean;
667
+
668
+ // Render customization
669
+ renderParticipant?: (participant: IEventParticipant, index: number) => ReactNode;
670
+ renderHeaderActions?: (props: { onClose: () => void }) => ReactNode;
671
+ renderCancelReason?: (event: IPopoverEvent<TData>) => ReactNode;
672
+ renderDeleteConfirmation?: (props: {
673
+ onConfirm: () => void;
674
+ onCancel: () => void;
675
+ isLoading?: boolean;
676
+ }) => ReactNode;
677
+
678
+ // Formatting
679
+ formatDate?: (date: Date, format?: string) => string;
680
+ formatTimeRange?: (start: Date, end: Date) => string;
681
+
682
+ // Styling
683
+ className?: string;
684
+ width?: number;
685
+
686
+ // i18n labels
687
+ labels?: Partial<IEventPopoverLabels>;
688
+ }
689
+ ```
690
+
691
+ ### IEventPopoverLabels
692
+
693
+ All label strings are customizable for i18n:
694
+
695
+ ```typescript
696
+ interface IEventPopoverLabels {
697
+ edit?: string;
698
+ delete?: string;
699
+ cancel?: string;
700
+ close?: string;
701
+ going?: string;
702
+ notGoing?: string;
703
+ confirmMeeting?: string;
704
+ completed?: string;
705
+ canceled?: string;
706
+ participants?: string;
707
+ guest?: string;
708
+ guests?: string;
709
+ confirmed?: string;
710
+ organizer?: string;
711
+ client?: string;
712
+ more?: string;
713
+ noDateProvided?: string;
714
+ eventNotFound?: string;
715
+ cancellationNote?: string;
716
+ canceledOn?: string;
717
+ acceptThisEvent?: string;
718
+ acceptAllEvents?: string;
719
+ deleteConfirmTitle?: string;
720
+ deleteConfirmDescription?: string;
721
+ }
722
+ ```
723
+
724
+ ---
725
+
726
+ ## 10. Preferences System
727
+
728
+ Located in `src/core/preferences/`.
729
+
730
+ ### IPreferencesConfig
731
+
732
+ Passed via `<InnoCalendar preferencesConfig={...}>`:
733
+
734
+ ```typescript
735
+ interface IPreferencesConfig {
736
+ modes?: TPreferenceModes; // Per-key control mode
737
+ locked?: TLockedPreferences; // Values for locked preferences
738
+ defaults?: TPartialPreferences; // Custom defaults
739
+ storageKey?: string; // Custom localStorage key
740
+ disableStorage?: boolean; // Disable localStorage entirely
741
+ }
742
+ ```
743
+
744
+ ### Preference Modes
745
+
746
+ | Mode | Behavior |
747
+ |---|---|
748
+ | `'user'` | User can modify, persisted to localStorage (default) |
749
+ | `'locked'` | Developer-controlled, user cannot modify |
750
+ | `'session'` | User can modify during session, not persisted |
751
+
752
+ ### IPreferences
753
+
754
+ ```typescript
755
+ interface IPreferences {
756
+ view: TCalendarView;
757
+ badgeVariant: TBadgeVariant;
758
+ slotDuration: TSlotDuration;
759
+ visibleHours: IVisibleHoursConfig; // { from: number; to: number }
760
+ workingHours: TWorkingHoursConfig; // { [dayIndex]: { from, to } }
761
+ showWorkingHoursOnly: boolean;
762
+ showWeekends: boolean;
763
+ firstDayOfWeek: 0|1|2|3|4|5|6;
764
+ }
765
+ ```
766
+
767
+ ### useCalendarPreferences Hook
768
+
769
+ Returns `IUsePreferencesReturn`:
770
+
771
+ ```typescript
772
+ interface IUsePreferencesReturn {
773
+ preferences: IPreferences;
774
+ setPreference: <K extends TPreferenceKey>(key: K, value: IPreferences[K]) => void;
775
+ setPreferences: (updates: TPartialPreferences) => void;
776
+ resetPreferences: () => void;
777
+ resetPreference: (key: TPreferenceKey) => void;
778
+ isLocked: (key: TPreferenceKey) => boolean;
779
+ isPersisted: (key: TPreferenceKey) => boolean;
780
+ getMode: (key: TPreferenceKey) => TPreferenceMode;
781
+ }
782
+ ```
783
+
784
+ ### Example: Locking slot duration
785
+
786
+ ```tsx
787
+ <InnoCalendar
788
+ events={events}
789
+ preferencesConfig={{
790
+ modes: { slotDuration: 'locked' },
791
+ locked: { slotDuration: 30 },
792
+ }}
793
+ />
794
+ ```
795
+
796
+ ---
797
+
798
+ ## 11. Widget Components
799
+
800
+ Located in `src/components/widget/`.
801
+
802
+ ### AgendaWidget
803
+
804
+ Embeddable agenda card for dashboards. Can work standalone or tap into an existing `InnoCalendarProvider`.
805
+
806
+ ```typescript
807
+ interface AgendaWidgetProps {
808
+ events: CalendarEvent[];
809
+ maxItems?: number;
810
+ onEventClick?: (event: CalendarEvent) => void;
811
+ className?: string;
812
+ // ... additional display options
813
+ }
814
+ ```
815
+
816
+ ### AgendaDropdown
817
+
818
+ Bell-style notification dropdown for upcoming events:
819
+
820
+ ```typescript
821
+ interface AgendaDropdownProps {
822
+ events: CalendarEvent[];
823
+ onEventClick?: (event: CalendarEvent) => void;
824
+ className?: string;
825
+ // ... trigger customization
826
+ }
827
+ ```
828
+
829
+ ---
830
+
831
+ ## 12. Filter Components
832
+
833
+ Located in `src/components/filters/`.
834
+
835
+ ### CalendarFilterSidebar
836
+
837
+ Full filter panel with schedule type and user filters:
838
+
839
+ ```typescript
840
+ interface CalendarFilterSidebarProps {
841
+ scheduleTypes: IScheduleTypeOption[];
842
+ users: IUserOption[];
843
+ labels?: CalendarFilterSidebarLabels;
844
+ // Controlled state or context-driven
845
+ }
846
+ ```
847
+
848
+ ### ScheduleTypeFilter
849
+
850
+ Standalone schedule type filter component with checkbox list.
851
+
852
+ ### UserFilter
853
+
854
+ Standalone user/participant filter with avatar support.
855
+
856
+ ---
857
+
858
+ ## 13. Settings Components
859
+
860
+ Located in `src/components/settings/`.
861
+
862
+ | Component | Props | What It Controls |
863
+ |---|---|---|
864
+ | `BadgeVariantSetting` | `BadgeVariantSettingProps` | Dot / Colored / Mixed badge display |
865
+ | `SlotDurationSetting` | `SlotDurationSettingProps` | 15 / 30 / 60 minute time slot granularity |
866
+ | `VisibleHoursSetting` | `VisibleHoursSettingProps` | Start/end hour range for day/week views |
867
+ | `WorkingHoursSetting` | `WorkingHoursSettingProps` | Per-day working hours with enable/disable toggle |
868
+
869
+ All settings components read/write preferences through the preferences context.
870
+
871
+ ---
872
+
873
+ ## 14. Context & Hooks
874
+
875
+ ### Context Providers
876
+
877
+ | Provider | Purpose |
878
+ |---|---|
879
+ | `InnoCalendarProvider` | Main state: events, date, view, filters, search, preferences, time config |
880
+ | `CalendarProvider` | Lower-level provider (used by `Calendar` component) |
881
+ | `SlotSelectionProvider` | Drag-to-select state management |
882
+ | `DragDropProvider` | Event drag-and-drop state management |
883
+
884
+ ### Primary Hooks
885
+
886
+ | Hook | Return Type | Purpose |
887
+ |---|---|---|
888
+ | `useInnoCalendar()` | `IInnoCalendarContext` | Full context access (events, view, date, filters, search, preferences) |
889
+ | `useCalendarContext()` | `IInnoCalendarContext` | Alias for `useInnoCalendar` |
890
+ | `useCalendar()` | `UseCalendarReturn` | Core calendar state (date, view, navigation, event helpers) |
891
+ | `useSlotSelection()` | `UseSlotSelectionReturn` | Drag-to-select hook |
892
+ | `useCalendarTimeConfig()` | `UseCalendarTimeConfigReturn` | Slot duration, visible hours, working hours |
893
+ | `usePreferences()` | `UsePreferencesReturn` | Basic preferences hook |
894
+ | `useAdvancedPreferences()` | `IUsePreferencesReturn` | Full preferences with locking support |
895
+ | `useDragDrop()` | `IDragDropContext` | Drag-drop state and handlers |
896
+
897
+ ### Selective Context Hooks
898
+
899
+ These read only a slice of the context for performance:
900
+
901
+ | Hook | Returns |
902
+ |---|---|
903
+ | `useCalendarDate()` | Current date + navigation |
904
+ | `useCalendarView()` | Current view + view change |
905
+ | `useCalendarEvents()` | Events array + setEvents |
906
+ | `useCalendarFilters()` | Filter state + setters |
907
+ | `useCalendarPreferences()` | Preferences from context |
908
+ | `useInnoCalendarView()` | View from InnoCalendar context |
909
+ | `useInnoCalendarEvents()` | Events from InnoCalendar context |
910
+ | `useInnoCalendarFilters()` | Filters from InnoCalendar context |
911
+ | `useInnoCalendarTimeConfig()` | Time config from InnoCalendar context |
912
+
913
+ ### Optional Hooks (no throw if outside provider)
914
+
915
+ | Hook | Returns |
916
+ |---|---|
917
+ | `useOptionalCalendar()` | `context undefined` |
918
+ | `useOptionalCalendarContext()` | `context undefined` |
919
+ | `useOptionalInnoCalendar()` | `context undefined` |
920
+ | `useOptionalDragDrop()` | `context undefined` |
921
+ | `useOptionalSlotSelection()` | `context undefined` |
922
+
923
+ ### Legacy Aliases (backward compat)
924
+
925
+ | Alias | Maps To |
926
+ |---|---|
927
+ | `IntegratedCalendarProvider` | `InnoCalendarProvider` |
928
+ | `useIntegratedCalendar()` | `useInnoCalendar()` |
929
+ | `useIntegratedCalendarEvents()` | `useInnoCalendarEvents()` |
930
+ | `useIntegratedCalendarFilters()` | `useInnoCalendarFilters()` |
931
+ | `useIntegratedCalendarView()` | `useInnoCalendarView()` |
932
+ | `useIntegratedCalendarTimeConfig()` | `useInnoCalendarTimeConfig()` |
933
+ | `useOptionalIntegratedCalendar()` | `useOptionalInnoCalendar()` |
934
+ | `IntegratedCalendar` | `InnoCalendar` |
935
+
936
+ ---
937
+
938
+ ## 15. Drag & Drop
939
+
940
+ Located in `src/core/context/drag-drop-context.tsx`.
941
+
942
+ ### DragDropProvider
943
+
944
+ Manages drag state for event repositioning. Wraps the calendar tree automatically when using `<InnoCalendar>`.
945
+
946
+ ```typescript
947
+ interface IDragState<TData = Record<string, unknown>> {
948
+ event: CalendarEvent<TData>;
949
+ originalStartDate: Date;
950
+ originalEndDate: Date;
951
+ previewDate?: Date; // current preview position
952
+ previewHour?: number; // for time-based views
953
+ previewMinute?: number;
954
+ originalResourceId?: string; // for resource/timeline views
955
+ targetResourceId?: string; // current drag target resource
956
+ }
957
+
958
+ interface IDropResult<TData = Record<string, unknown>> {
959
+ event: CalendarEvent<TData>;
960
+ newStartDate: Date;
961
+ newEndDate: Date; // maintains original duration
962
+ newResourceId?: string; // set when dropped on different resource
963
+ }
964
+
965
+ interface IDragDropContext<TData = Record<string, unknown>> {
966
+ dragState: IDragState<TData> | null;
967
+ isDragging: boolean;
968
+ startDrag: (event: CalendarEvent<any>) => void;
969
+ updateDragPreview: (date: Date, hour?: number, minute?: number, resourceId?: string) => void;
970
+ endDrag: () => IDropResult<TData> | null;
971
+ cancelDrag: () => void;
972
+ }
973
+ ```
974
+
975
+ ### Timeline / Resource View Drag & Drop
976
+
977
+ `TimelineView` supports HTML5 drag & drop out of the box:
978
+
979
+ - Events render with `enableDrag={true}` by default
980
+ - Each resource row acts as a drop zone
981
+ - Dragging across rows updates `targetResourceId` in the drag state
982
+ - Dropping calls `endDrag()` which fires `onEventDrop` on `DragDropProvider`
983
+
984
+ ```tsx
985
+ <InnoCalendar
986
+ events={events}
987
+ initialView="timeline-week"
988
+ onEventDrop={(result) => {
989
+ console.log(result.event.id, result.newStartDate, result.newResourceId);
990
+ // Update your backend
991
+ }}
992
+ />
993
+ ```
994
+
995
+ ---
996
+
997
+ ## 16. Core Utilities
998
+
999
+ Located in `src/core/utils/`. All exported from `src/core/utils/index.ts`.
1000
+
1001
+ ### date-utils.ts
1002
+
1003
+ Date manipulation using native Date API:
1004
+
1005
+ | Function | Signature |
1006
+ |---|---|
1007
+ | `startOfDay` | `(date: Date) => Date` |
1008
+ | `endOfDay` | `(date: Date) => Date` |
1009
+ | `startOfWeek` | `(date: Date, weekStartsOn?: 0\|1\|2\|3\|4\|5\|6) => Date` |
1010
+ | `endOfWeek` | `(date: Date, weekStartsOn?: 0\|1\|2\|3\|4\|5\|6) => Date` |
1011
+ | `startOfMonth` | `(date: Date) => Date` |
1012
+ | `endOfMonth` | `(date: Date) => Date` |
1013
+ | `startOfYear` | `(date: Date) => Date` |
1014
+ | `endOfYear` | `(date: Date) => Date` |
1015
+ | `addDays` | `(date: Date, days: number) => Date` |
1016
+ | `subDays` | `(date: Date, days: number) => Date` |
1017
+ | `addWeeks` | `(date: Date, weeks: number) => Date` |
1018
+ | `subWeeks` | `(date: Date, weeks: number) => Date` |
1019
+ | `addMonths` | `(date: Date, months: number) => Date` |
1020
+ | `subMonths` | `(date: Date, months: number) => Date` |
1021
+ | `addYears` | `(date: Date, years: number) => Date` |
1022
+ | `subYears` | `(date: Date, years: number) => Date` |
1023
+ | `addHours` | `(date: Date, hours: number) => Date` |
1024
+ | `addMinutes` | `(date: Date, minutes: number) => Date` |
1025
+ | `setTime` | `(date: Date, hours: number, minutes?: number, seconds?: number) => Date` |
1026
+ | `getDecimalHours` | `(date: Date) => number` |
1027
+ | `isSameDay` | `(a: Date, b: Date) => boolean` |
1028
+ | `isSameWeek` | `(a: Date, b: Date, weekStartsOn?) => boolean` |
1029
+ | `isSameMonth` | `(a: Date, b: Date) => boolean` |
1030
+ | `isSameYear` | `(a: Date, b: Date) => boolean` |
1031
+ | `isToday` | `(date: Date) => boolean` |
1032
+ | `isWeekend` | `(date: Date) => boolean` |
1033
+ | `getDayOfWeek` | `(date: Date) => number` |
1034
+ | `isBefore` | `(a: Date, b: Date) => boolean` |
1035
+ | `isAfter` | `(a: Date, b: Date) => boolean` |
1036
+ | `isBetween` | `(date: Date, start: Date, end: Date) => boolean` |
1037
+ | `minDate` | `(dates: Date[]) => Date \| undefined` |
1038
+ | `maxDate` | `(dates: Date[]) => Date \| undefined` |
1039
+ | `differenceInMilliseconds` | `(a: Date, b: Date) => number` |
1040
+ | `differenceInMinutes` | `(a: Date, b: Date) => number` |
1041
+ | `differenceInHours` | `(a: Date, b: Date) => number` |
1042
+ | `differenceInDays` | `(a: Date, b: Date) => number` |
1043
+ | `getWeekDays` | `(date: Date, weekStartsOn?) => Date[]` |
1044
+ | `getWeekNumber` | `(date: Date) => number` |
1045
+ | `getDaysInMonth` | `(date: Date) => number` |
1046
+ | `getYearMonths` | `(year: number) => Date[]` |
1047
+ | `formatTime` | `(date: Date, format?: '12h'\|'24h') => string` |
1048
+ | `formatDateISO` | `(date: Date) => string` |
1049
+ | `formatHourLabel` | `(hour: number, format?: '12h'\|'24h') => string` |
1050
+ | `parseISO` | `(dateString: string) => Date` |
1051
+ | `eachDayOfInterval` | `(start: Date, end: Date) => Date[]` |
1052
+ | `eachHourOfInterval` | `(start: Date, end: Date) => Date[]` |
1053
+ | `getWeekdayNames` | `(locale?, format?) => string[]` |
1054
+ | `getMonthNames` | `(locale?, format?) => string[]` |
1055
+ | `getVisibleHoursArray` | `(visibleHours) => number[]` |
1056
+ | `generateMonthGrid` | `(date: Date, weekStartsOn?) => Array<{ date, isCurrentMonth, isWeekend }>` |
1057
+ | `detectAllDayEvent` | `(event) => boolean` |
1058
+ | `separateEventsByDuration` | `<TEvent>(events) => { singleDay, multiDay }` |
1059
+ | `getEventsInRange` | `<TEvent>(events, rangeStart, rangeEnd) => TEvent[]` |
1060
+ | `isWorkingHour` | `(date, hour, workingHours?) => boolean` |
1061
+ | `getWorkingHoursForDay` | `(date, workingHours) => { from, to } \| null` |
1062
+
1063
+ ### event-utils.ts
1064
+
1065
+ Event filtering, sorting, classification, and color utilities:
1066
+
1067
+ | Function | Purpose |
1068
+ |---|---|
1069
+ | `isMultiDayEvent` | Checks if event spans midnight |
1070
+ | `getEventsForDay` | Filters events that overlap a given day |
1071
+ | `getAllDayEvents` | Filters all-day events from array |
1072
+ | `getTimedEvents` | Filters timed (non-all-day) events |
1073
+ | `getMultiDayEvents` | Filters multi-day events |
1074
+ | `filterEventsByDateRange` | Filters events overlapping a date range |
1075
+ | `filterEventsByScheduleType` | Client-side schedule type filter |
1076
+ | `filterEventsByResource` | Client-side resource filter |
1077
+ | `filterEventsBySearch` | Text search across event fields |
1078
+ | `filterOutCanceled` | Remove canceled events |
1079
+ | `applyEventFilters` | Composite filter applying multiple criteria |
1080
+ | `sortEventsByStart` | Sort by start date |
1081
+ | `sortEventsByEnd` | Sort by end date |
1082
+ | `sortEventsByDuration` | Sort by event duration |
1083
+ | `groupEventsByDate` | Groups events by calendar date |
1084
+ | `groupEventsByScheduleType` | Groups events by schedule type |
1085
+ | `groupEventsByResource` | Groups events by resourceId |
1086
+ | `getEventDurationMinutes` | Returns event duration in minutes |
1087
+ | `getEventColor` | Resolves event color with fallback |
1088
+ | `formatEventTimeDisplay` | Formats event time for display |
1089
+
1090
+ ### grid-utils.ts
1091
+
1092
+ Grid layout calculations, view navigation, and cell generation:
1093
+
1094
+ | Function | Purpose |
1095
+ |---|---|
1096
+ | `getViewDateRange` | Returns date range for any view |
1097
+ | `navigateNext` | Advance date by one view step |
1098
+ | `navigatePrev` | Go back one view step |
1099
+ | `navigateToday` | Returns today's Date |
1100
+ | `generateMonthCells` | Creates cell array for month grid |
1101
+ | `generateWeekCells` | Creates cell array for week view |
1102
+ | `generateYearCells` | Creates cell array for year view |
1103
+ | `generateTimeSlots` | Creates time slot array for given hour range and slot duration |
1104
+ | `generateHourLabels` | Creates hour label array for gutter |
1105
+ | `getViewTitle` | Formatted title string for current view + date |
1106
+
1107
+ ### react-node-utils.ts
1108
+
1109
+ Utility for extracting plain text from ReactNode trees:
1110
+
1111
+ | Function | Signature | Purpose |
1112
+ |---|---|---|
1113
+ | `reactNodeToText` | `(node: ReactNode) => string` | Recursively extracts plain text from any ReactNode. Used internally for search, aria-labels, and string operations on ReactNode-typed fields. |
1114
+
1115
+ ```typescript
1116
+ reactNodeToText("hello") // "hello"
1117
+ reactNodeToText(<span>hello <b>world</b></span>) // "hello world"
1118
+ reactNodeToText(<><MapPinIcon /> BELGIUM</>) // " BELGIUM"
1119
+ reactNodeToText(null) // ""
1120
+ reactNodeToText(42) // "42"
1121
+ ```
1122
+
1123
+ ### position-utils.ts
1124
+
1125
+ Event overlap and positioning:
1126
+
1127
+ | Function | Purpose |
1128
+ |---|---|
1129
+ | `calculateEventPosition` | Computes position for a single event in a column |
1130
+ | `calculateOverlappingPositions` | Computes non-overlapping layout for multiple events |
1131
+ | `yToTime` | Converts vertical pixel offset to Date |
1132
+ | `timeToY` | Converts Date to vertical pixel offset |
1133
+ | `eventsOverlap` | Checks if two events overlap in time |
1134
+ | `getOverlappingGroups` | Groups events that share time ranges |
1135
+ | `calculateSelectionOverlay` | Computes drag selection overlay dimensions |
1136
+
1137
+ ---
1138
+
1139
+ ## 17. Presets
1140
+
1141
+ Located in `src/presets/`.
1142
+
1143
+ ### DefaultCalendar
1144
+
1145
+ Pre-configured calendar with sensible defaults:
1146
+
1147
+ ```tsx
1148
+ import { DefaultCalendar } from '@innosolutions/inno-calendar';
1149
+
1150
+ <DefaultCalendar events={events} />
1151
+ ```
1152
+
1153
+ ### TailwindCalendar
1154
+
1155
+ Tailwind-optimized calendar preset with carefully chosen utility classes:
1156
+
1157
+ ```tsx
1158
+ import { TailwindCalendar } from '@innosolutions/inno-calendar';
1159
+
1160
+ <TailwindCalendar events={events} />
1161
+ ```
1162
+
1163
+ ---
1164
+
1165
+ ## 18. CSS & Theming
1166
+
1167
+ Styles are in `src/styles/calendar.css` (imported via `@innosolutions/inno-calendar/styles`).
1168
+
1169
+ ### CSS Custom Properties
1170
+
1171
+ ```css
1172
+ :root {
1173
+ /* Surfaces */
1174
+ --inno-border-color: #e5e7eb;
1175
+ --inno-background: #ffffff;
1176
+ --inno-foreground: #111827;
1177
+ --inno-muted: #f3f4f6;
1178
+ --inno-muted-foreground: #6b7280;
1179
+ --inno-primary: #3b82f6;
1180
+ --inno-primary-foreground: #ffffff;
1181
+
1182
+ /* Event Colors */
1183
+ --inno-event-blue: #3b82f6;
1184
+ --inno-event-green: #22c55e;
1185
+ --inno-event-red: #ef4444;
1186
+ --inno-event-yellow: #eab308;
1187
+ --inno-event-purple: #a855f7;
1188
+ --inno-event-orange: #f97316;
1189
+ --inno-event-pink: #ec4899;
1190
+ --inno-event-teal: #14b8a6;
1191
+ --inno-event-gray: #6b7280;
1192
+ --inno-event-indigo: #6366f1;
1193
+
1194
+ /* Layout */
1195
+ --inno-hour-height: 96px;
1196
+ --inno-header-height: 56px;
1197
+ --inno-sidebar-width: 200px;
1198
+ }
1199
+ ```
1200
+
1201
+ ### Dark Mode
1202
+
1203
+ Override with `.dark` class:
1204
+
1205
+ ```css
1206
+ .dark {
1207
+ --inno-border-color: #374151;
1208
+ --inno-background: #111827;
1209
+ --inno-foreground: #f9fafb;
1210
+ --inno-muted: #1f2937;
1211
+ --inno-muted-foreground: #9ca3af;
1212
+ }
1213
+ ```
1214
+
1215
+ ### Key CSS Classes
1216
+
1217
+ | Class | Purpose |
1218
+ |---|---|
1219
+ | `.inno-calendar-root` | Root container with scrollbar styles |
1220
+ | `.inno-calendar-loading-overlay` | Translucent loading indicator overlay |
1221
+ | `.inno-calendar-spinner` | Spinner animation |
1222
+ | `.inno-calendar-selection` | Drag-to-select overlay |
1223
+ | `.inno-calendar-current-time` | Red current time indicator |
1224
+ | `.inno-calendar-event` | Event card styling |
1225
+ | `.inno-calendar-event-canceled` | Canceled event styling |
1226
+ | `.inno-calendar-disabled-hour` | Disabled/non-working hours pattern |
1227
+ | `.inno-calendar-no-scrollbar` | Hide scrollbar utility |
1228
+ | `.inno-scroll-nav` | D-pad controller container |
1229
+ | `.inno-scroll-nav-btn` | Directional buttons (up, down, left, right) |
1230
+ | `.ic-expansion-backdrop` | Backdrop for expanded month cell |
1231
+ | `.ic-expansion-panel` | Expanded events panel in month view |
1232
+
1233
+ ---
1234
+
1235
+ ## 19. Constants
1236
+
1237
+ Located in `src/core/constants.ts`.
1238
+
1239
+ ### View List
1240
+
1241
+ ```typescript
1242
+ const CALENDAR_VIEWS: TCalendarView[] = [
1243
+ 'day', 'week', 'month', 'year', 'agenda',
1244
+ 'resource-day', 'resource-week',
1245
+ 'timeline-day', 'timeline-week'
1246
+ ];
1247
+ ```
1248
+
1249
+ ### Default Preferences
1250
+
1251
+ ```typescript
1252
+ const DEFAULT_PREFERENCES: ICalendarPreferences = {
1253
+ startHour: 8,
1254
+ endHour: 18,
1255
+ firstDayOfWeek: 1, // Monday
1256
+ slotDuration: 30,
1257
+ showWeekends: true,
1258
+ timeFormat: '24h',
1259
+ badgeVariant: 'colored',
1260
+ showCanceledEvents: false,
1261
+ };
1262
+ ```
1263
+
1264
+ ### Color Palette
1265
+
1266
+ ```typescript
1267
+ const EVENT_COLORS: Record<TEventColor, string> = {
1268
+ blue: '#3b82f6', green: '#22c55e', red: '#ef4444',
1269
+ yellow: '#eab308', purple: '#a855f7', orange: '#f97316',
1270
+ pink: '#ec4899', teal: '#14b8a6', gray: '#6b7280', indigo: '#6366f1',
1271
+ };
1272
+
1273
+ const EVENT_COLOR_CLASSES: Record<TEventColor, { bg, text, border }>;
1274
+ const HEX_TO_EVENT_COLOR: Record<string, TEventColor>;
1275
+ ```
1276
+
1277
+ ### Grid Constants
1278
+
1279
+ | Constant | Value |
1280
+ |---|---|
1281
+ | `DEFAULT_HOUR_HEIGHT` | 64px |
1282
+ | `DEFAULT_SLOT_HEIGHT` | 32px |
1283
+ | `DEFAULT_HEADER_HEIGHT` | 56px |
1284
+ | `DEFAULT_RESOURCE_WIDTH` | 200px |
1285
+ | `MIN_EVENT_HEIGHT` | 20px |
1286
+ | `HOURS_IN_DAY` | 24 |
1287
+ | `MINUTES_IN_HOUR` | 60 |
1288
+ | `MS_PER_MINUTE` | 60000 |
1289
+ | `MS_PER_HOUR` | 3600000 |
1290
+ | `MS_PER_DAY` | 86400000 |
1291
+
1292
+ ### Time Defaults
1293
+
1294
+ ```typescript
1295
+ const DEFAULT_VISIBLE_HOURS: IVisibleHours = { startHour: 0, endHour: 24 };
1296
+ const DEFAULT_BUSINESS_HOURS: IVisibleHours = { startHour: 8, endHour: 18 };
1297
+ ```
1298
+
1299
+ ---
1300
+
1301
+ ## 20. Integration Guide
1302
+
1303
+ ### Minimal Setup
1304
+
1305
+ ```tsx
1306
+ import { InnoCalendar, type CalendarEvent } from '@innosolutions/inno-calendar';
1307
+ import '@innosolutions/inno-calendar/styles';
1308
+
1309
+ const events: CalendarEvent[] = [
1310
+ {
1311
+ id: '1',
1312
+ title: 'Team Meeting',
1313
+ startDate: new Date('2026-02-12T09:00:00'),
1314
+ endDate: new Date('2026-02-12T10:00:00'),
1315
+ color: 'blue',
1316
+ },
1317
+ ];
1318
+
1319
+ function App() {
1320
+ return (
1321
+ <InnoCalendar
1322
+ events={events}
1323
+ onEventClick={(event) => console.log('Clicked:', event.title)}
1324
+ onSlotSelect={(sel) => console.log('Selected:', sel.startDate, sel.endDate)}
1325
+ />
1326
+ );
1327
+ }
1328
+ ```
1329
+
1330
+ ### With Custom Popover
1331
+
1332
+ ```tsx
1333
+ <InnoCalendar
1334
+ events={events}
1335
+ renderPopover={({ event, onClose }) => (
1336
+ <div className="p-4 space-y-2">
1337
+ <h3 className="font-bold">{event.title}</h3>
1338
+ <p>{event.description}</p>
1339
+ <button onClick={onClose}>Close</button>
1340
+ </div>
1341
+ )}
1342
+ />
1343
+ ```
1344
+
1345
+ ### With Loading State
1346
+
1347
+ ```tsx
1348
+ const [isLoading, setIsLoading] = useState(false);
1349
+
1350
+ <InnoCalendar
1351
+ events={events}
1352
+ isLoading={isLoading}
1353
+ onDateChange={async (date, view) => {
1354
+ setIsLoading(true);
1355
+ const newEvents = await fetchEvents(date, view);
1356
+ setEvents(newEvents);
1357
+ setIsLoading(false);
1358
+ }}
1359
+ />
1360
+ ```
1361
+
1362
+ ### With Deep-Linking
1363
+
1364
+ ```tsx
1365
+ // URL: /calendar?eventId=appointment-42
1366
+ const eventId = useSearchParams().get('eventId');
1367
+
1368
+ <InnoCalendar events={events} focusEventId={eventId} />
1369
+ ```
1370
+
1371
+ ### Imperative Control
1372
+
1373
+ ```tsx
1374
+ const ref = useRef<ICalendarRefHandle>(null);
1375
+
1376
+ <Button onClick={() => ref.current?.scrollToWorkingHours()}>
1377
+ Go to Work Hours
1378
+ </Button>
1379
+
1380
+ <InnoCalendar ref={ref} events={events} />
1381
+ ```
1382
+
1383
+ ### Resource / Timeline Views
1384
+
1385
+ ```tsx
1386
+ const resources: IResource[] = [
1387
+ { id: 'room-a', name: 'Meeting Room A', color: 'blue' },
1388
+ { id: 'room-b', name: 'Board Room', color: 'green' },
1389
+ ];
1390
+
1391
+ <InnoCalendar
1392
+ events={events}
1393
+ initialView="timeline-week"
1394
+ onEventDrop={(result) => {
1395
+ // result.newResourceId is set when event is dragged to a different row
1396
+ api.updateEvent(result.event.id, {
1397
+ startDate: result.newStartDate,
1398
+ endDate: result.newEndDate,
1399
+ resourceId: result.newResourceId,
1400
+ });
1401
+ }}
1402
+ />
1403
+ ```
1404
+
1405
+ ### Header Configuration
1406
+
1407
+ ```tsx
1408
+ // Hide resource views and settings only show calendar views + Today + navigation
1409
+ <InnoCalendar
1410
+ events={events}
1411
+ headerConfig={{
1412
+ showResourceViews: false,
1413
+ showSettings: false,
1414
+ showFilters: false,
1415
+ }}
1416
+ />
1417
+ ```
1418
+
1419
+ ### Event Detail Mode
1420
+
1421
+ ```tsx
1422
+ // Show event details in a slide-in sheet instead of default popover
1423
+ <InnoCalendar
1424
+ events={events}
1425
+ eventDetailMode="sheet"
1426
+ />
1427
+
1428
+ // Fully custom event detail container
1429
+ <InnoCalendar
1430
+ events={events}
1431
+ eventDetailMode="custom"
1432
+ renderEventDetail={({ event, onClose }) => (
1433
+ <MyCustomDrawer event={event} onClose={onClose} />
1434
+ )}
1435
+ />
1436
+ ```
1437
+
1438
+ ### With Preferences Locking
1439
+
1440
+ ```tsx
1441
+ <InnoCalendar
1442
+ events={events}
1443
+ preferencesConfig={{
1444
+ defaults: { view: 'month', badgeVariant: 'dot' },
1445
+ modes: {
1446
+ slotDuration: 'locked',
1447
+ showWeekends: 'locked',
1448
+ },
1449
+ locked: {
1450
+ slotDuration: 30,
1451
+ showWeekends: false,
1452
+ },
1453
+ }}
1454
+ />
1455
+ ```
1456
+
1457
+ ### Widget Embedding
1458
+
1459
+ ```tsx
1460
+ import { AgendaWidget, AgendaDropdown } from '@innosolutions/inno-calendar';
1461
+
1462
+ // Dashboard card
1463
+ <AgendaWidget events={todayEvents} onEventClick={openEvent} />
1464
+
1465
+ // Navbar notification bell
1466
+ <AgendaDropdown events={upcomingEvents} onEventClick={openEvent} />
1467
+ ```
1468
+
1469
+ ### Custom Slot Example
1470
+
1471
+ ```tsx
1472
+ <InnoCalendar
1473
+ events={events}
1474
+ slots={{
1475
+ dayCellFooter: ({ date, events }) => (
1476
+ <div className="text-xs text-muted-foreground border-t mt-1 pt-1">
1477
+ {events.length} events
1478
+ </div>
1479
+ ),
1480
+ columnHeader: ({ date, isToday, onDayClick }) => (
1481
+ <button
1482
+ className={cn('text-center', isToday && 'font-bold text-primary')}
1483
+ onClick={() => onDayClick?.(date)}
1484
+ >
1485
+ {date.toLocaleDateString(undefined, { weekday: 'short', day: 'numeric' })}
1486
+ </button>
1487
+ ),
1488
+ }}
1489
+ />
1490
+ ```
1491
+
1492
+ ---
1493
+
1494
+ ## 21. Full Export Catalog
1495
+
1496
+ ### From `src/components/`
1497
+
1498
+ **Main Components:**
1499
+ - `InnoCalendar`, `InnoCalendarProps`
1500
+ - `Calendar`, `CalendarProps`
1501
+ - `IntegratedCalendar` (alias), `IntegratedCalendarProps` (alias)
1502
+
1503
+ **Event Components:**
1504
+ - `EventCard`, `EventCardProps`
1505
+ - `EventBlock`, `EventBlockProps`
1506
+ - `EventPopover`, `EventPopoverProps`, `IEventPopoverLabels`, `IPopoverEvent`, `IEventParticipant`
1507
+ - `MultiDayEventBar`, `MultiDayEventBarProps`
1508
+ - `getEventColorClasses`
1509
+
1510
+ **Header Components:**
1511
+ - `CalendarHeader`, `CalendarHeaderProps`
1512
+ - `DateNavigator`, `DateNavigatorProps`
1513
+ - `TodayButton`, `TodayButtonProps`
1514
+
1515
+ **View Components:**
1516
+ - `DayView`, `DayViewProps`
1517
+ - `WeekView`, `WeekViewProps`
1518
+ - `MonthView`, `MonthViewProps`
1519
+ - `YearView`, `YearViewProps`
1520
+ - `AgendaView`, `AgendaViewProps`
1521
+ - `TimelineView`, `TimelineViewProps`
1522
+ - `DayEventsExpansion`, `DayEventsExpansionProps`
1523
+
1524
+ **Primitive Components:**
1525
+ - `DaySlot`, `DaySlotProps`
1526
+ - `MultiDayBanner`, `MultiDayBannerProps`
1527
+ - `TimeSlot`
1528
+
1529
+ > **Internal-only Primitives** (not re-exported via public barrel): `CalendarTimeline`, `ScrollNavigator`, `ScrollNavigatorProps`, `SelectableSlot`, `SelectableSlotProps`, `TimeSlotProps`, `WeekAllDayRow`, `WeekAllDayRowProps`
1530
+
1531
+ **Settings Components:**
1532
+ - `BadgeVariantSetting`, `BadgeVariantSettingProps`
1533
+ - `SlotDurationSetting`, `SlotDurationSettingProps`
1534
+ - `VisibleHoursSetting`, `VisibleHoursSettingProps`
1535
+ - `WorkingHoursSetting`, `WorkingHoursSettingProps`
1536
+ - `DEFAULT_WEEK_WORKING_HOURS`, `IDayWorkingHours`, `IWeekWorkingHours`, `TDayOfWeek`
1537
+
1538
+ **Filter Components:**
1539
+ - `CalendarFilterSidebar`, `CalendarFilterSidebarProps`, `CalendarFilterSidebarLabels`
1540
+ - `ScheduleTypeFilter`, `ScheduleTypeFilterProps`, `IScheduleTypeOption`
1541
+ - `UserFilter`, `UserFilterProps`, `IUserOption`
1542
+
1543
+ **Widget Components:**
1544
+ - `AgendaWidget`, `AgendaWidgetProps`
1545
+ - `AgendaDropdown`, `AgendaDropdownProps`
1546
+
1547
+ **UI Primitives:**
1548
+ - `Button`, `ButtonProps`, `buttonVariants`
1549
+ - `Dialog`, `DialogClose`, `DialogContent`, `DialogDescription`, `DialogHeader`, `DialogTitle`, `DialogTrigger`
1550
+ - `Popover`, `PopoverAnchor`, `PopoverContent`, `PopoverTrigger`
1551
+ - `Sheet`, `SheetClose`, `SheetContent`, `SheetDescription`, `SheetHeader`, `SheetTitle`, `SheetTrigger`
1552
+ - `Tooltip`, `TooltipContent`, `TooltipProvider`, `TooltipTrigger`
1553
+
1554
+ > **Internal-only UI** (not re-exported via public barrel, available via direct `./ui` import within the package): `Badge`, `BadgeProps`, `badgeVariants`, `DropdownMenu` family, `DialogOverlay`, `DialogPortal`, `Label`, `LabelProps`, `Select`, `SelectProps`, `SheetOverlay`, `SheetPortal`
1555
+
1556
+ ### From `src/core/`
1557
+
1558
+ **Types** (60+ type exports) — see Section 4.
1559
+
1560
+ **Context Providers:**
1561
+ - `InnoCalendarProvider`, `InnoCalendarProviderProps`
1562
+ - `CalendarProvider`, `CalendarProviderProps`
1563
+ - `SlotSelectionProvider`, `SlotSelectionProviderProps`
1564
+ - `DragDropProvider`, `DragDropProviderProps`
1565
+ - `IntegratedCalendarProvider` (alias), `IntegratedCalendarProviderProps` (alias)
1566
+
1567
+ **Hooks** see Section 14.
1568
+
1569
+ **Constants** see Section 19.
1570
+
1571
+ **Utilities** — see Section 16. All re-exported via `export * from './utils'`.
1572
+
1573
+ **Preferences:**
1574
+ - `IPreferences`, `IPreferencesConfig`, `IUsePreferencesReturn`
1575
+ - `TPreferenceMode`, `TPreferenceModes`, `TLockedPreferences`, `TPartialPreferences`, `TPreferenceKey`
1576
+ - `IVisibleHoursConfig`, `TWorkingHoursConfig`
1577
+ - `PREFERENCES_STORAGE_KEY`, default constants
1578
+ - `useAdvancedPreferences` (re-exported as alias)
1579
+
1580
+ ### From `src/presets/`
1581
+
1582
+ - `DefaultCalendar`, `DefaultCalendarProps`
1583
+ - `TailwindCalendar`, `TailwindCalendarProps`
1584
+ - `RenderEventProps`, `RenderEventPopoverProps`, `RenderFilterSidebarProps`, `RenderHeaderProps`
1585
+
1586
+ ### From `src/lib/`
1587
+
1588
+ - `cn` — `clsx(...inputs) | twMerge` className composer
1589
+
1590
+ ---
1591
+
1592
+ ## 22. File Naming Conventions
1593
+
1594
+ **All files and folders use kebab-case.**
1595
+
1596
+ | Type | Pattern | Example |
1597
+ |---|---|---|
1598
+ | Components | `kebab-case.tsx` | `event-card.tsx` |
1599
+ | Hooks | `use-kebab-case.ts` | `use-calendar.ts` |
1600
+ | Context | `kebab-case.tsx` | `calendar-context.tsx` |
1601
+ | Types | `types.ts` | `types.ts` |
1602
+ | Utilities | `kebab-case.ts` | `date-utils.ts` |
1603
+ | Constants | `constants.ts` | `constants.ts` |
1604
+ | Barrel exports | `index.ts` | `index.ts` |
1605
+ | Styles | `kebab-case.css` | `calendar.css` |
1606
+
1607
+ ---
1608
+
1609
+ ## 23. Styling Patterns
1610
+
1611
+ ### className Composition
1612
+
1613
+ ```tsx
1614
+ import { cn } from '../../lib/utils';
1615
+
1616
+ <div className={cn('base-styles', isActive && 'active-styles', className)} />
1617
+ ```
1618
+
1619
+ ### CVA for Variants
1620
+
1621
+ ```tsx
1622
+ import { cva, type VariantProps } from 'class-variance-authority';
1623
+
1624
+ const buttonVariants = cva('base-classes', {
1625
+ variants: {
1626
+ variant: { default: '...', outline: '...' },
1627
+ size: { sm: '...', md: '...' },
1628
+ },
1629
+ defaultVariants: { variant: 'default', size: 'md' },
1630
+ });
1631
+ ```
1632
+
1633
+ ---
1634
+
1635
+ ## 24. Biome Configuration
1636
+
1637
+ From `biome.json` (schema v2.3.13):
1638
+
1639
+ - **Formatter**: tabs, single quotes, JSX double quotes, 100 line width, trailing commas (ES5), semicolons always
1640
+ - **Linter**: recommended + `noUnusedVariables`, `noExplicitAny` (warn), `noNonNullAssertion` (warn), `useConst` (error), `noExcessiveCognitiveComplexity` (max 15, warn)
1641
+ - **Organize imports**: enabled via assist actions
1642
+
1643
+ ```bash
1644
+ npm run lint # Check
1645
+ npm run format # Auto-fix
1646
+ ```
1647
+
1648
+ ---
1649
+
1650
+ ## 25. State Synchronization Guide
1651
+
1652
+ How state flows through the calendar and guidance for integrating with consumer projects.
1653
+
1654
+ ### Data Flow
1655
+
1656
+ ```
1657
+ Consumer App
1658
+ └─ <InnoCalendar events={events} onEventDrop={...} ...>
1659
+ └─ DragDropProvider (drag state, onEventDrop callback)
1660
+ └─ InnoCalendarProvider (view, date, preferences, filtered events)
1661
+ └─ SlotSelectionProvider (drag-to-select state)
1662
+ └─ InnerCalendar (renders header + active view)
1663
+ ├─ CalendarHeader (nav, view switcher, settings)
1664
+ └─ Active View (DayView, WeekView, TimelineView, etc.)
1665
+ └─ EventCard (popover/dialog/sheet/custom)
1666
+ ```
1667
+
1668
+ ### Events: BYO Approach
1669
+
1670
+ The package **never fetches data**. The consumer owns the events array and updates it:
1671
+
1672
+ ```tsx
1673
+ // Using React Query
1674
+ const { data: events } = useQuery(['events', dateRange], fetchEvents);
1675
+ const updateEvent = useMutation(api.updateEvent, {
1676
+ onSuccess: () => queryClient.invalidateQueries(['events']),
1677
+ });
1678
+
1679
+ <InnoCalendar
1680
+ events={events ?? []}
1681
+ onEventDrop={(result) => updateEvent.mutate({
1682
+ id: result.event.id,
1683
+ startDate: result.newStartDate,
1684
+ endDate: result.newEndDate,
1685
+ resourceId: result.newResourceId,
1686
+ })}
1687
+ onSlotSelect={(sel) => openCreateDialog(sel.startDate, sel.endDate)}
1688
+ />
1689
+ ```
1690
+
1691
+ ### Preferences Persistence
1692
+
1693
+ Preferences (view, slot duration, badge variant, etc.) persist to `localStorage` by default. The key is configurable:
1694
+
1695
+ ```tsx
1696
+ <InnoCalendar
1697
+ preferencesConfig={{
1698
+ storageKey: 'my-app-calendar-prefs', // default: 'inno-calendar-preferences'
1699
+ defaults: { view: 'week', badgeVariant: 'colored' },
1700
+ modes: { slotDuration: 'locked' }, // prevent user from changing
1701
+ locked: { slotDuration: 30 },
1702
+ }}
1703
+ />
1704
+ ```
1705
+
1706
+ ### Cross-Project State Patterns
1707
+
1708
+ For apps that embed InnoCalendar (e.g., promosport-erp, generationimmo-erp):
1709
+
1710
+ 1. **URL Sync** — Sync view/date with URL params so back-button works:
1711
+ ```tsx
1712
+ <InnoCalendar
1713
+ initialView={searchParams.get('view') as TCalendarView ?? 'week'}
1714
+ initialDate={searchParams.get('date') ? new Date(searchParams.get('date')!) : undefined}
1715
+ onViewChange={(v) => setSearchParams({ view: v })}
1716
+ onDateChange={(d) => setSearchParams({ date: d.toISOString() })}
1717
+ />
1718
+ ```
1719
+
1720
+ 2. **Deep-linking events** — Navigate to a specific event from another page:
1721
+ ```tsx
1722
+ <InnoCalendar events={events} focusEventId={eventIdFromUrl} />
1723
+ ```
1724
+
1725
+ 3. **External filter state** — The calendar's built-in filtering is opt-in. You can filter server-side and just pass filtered `events`:
1726
+ ```tsx
1727
+ const filteredEvents = useFilteredEvents(allEvents, externalFilters);
1728
+ <InnoCalendar events={filteredEvents} />
1729
+ ```
1730
+
1731
+ 4. **Imperative control** — Use ref for programmatic scroll/navigation:
1732
+ ```tsx
1733
+ const ref = useRef<ICalendarRefHandle>(null);
1734
+ ref.current?.scrollToToday();
1735
+ ref.current?.scrollToWorkingHours();
1736
+ ref.current?.focusEvent(eventId);
1737
+ ```
1738
+
1739
+ ---
1740
+
1741
+ ## 26. Critical Rules
1742
+
1743
+ 1. **CalendarEvent must stay domain-agnostic** — no new top-level fields. Use `data?: TData` for domain data.
1744
+ 2. **Deprecated fields stay** until next major version — never remove them prematurely.
1745
+ 3. **Built-in components read `event.data` first**, then fall back to deprecated top-level fields.
1746
+ 4. **Core module has zero external dependencies** — only React + native APIs.
1747
+ 5. **Follow existing patterns** before inventing new ones (composition, slot props, CVA).
1748
+ 6. **kebab-case everywhere** — files, folders, no exceptions.
1749
+ 7. **JSDoc on every public export** — summary + `@param` + `@example` + `@default`.
1750
+ 8. **Run `npm run typecheck`** before committing.
1751
+ 9. **Update AGENT.md, README.md, and copilot-instructions.md** when public API changes.
1752
+ 10. **Section separators** in type files: `// ============================================================================`
1753
+ 11. **Performance**: `useMemo`, `useCallback`, memoize context values, stable keys.
1754
+ 12. **Accessibility**: semantic HTML, ARIA attributes, keyboard navigation (Arrow keys, Enter/Space, Escape, Tab).
1755
+ 13. **No inline objects/arrays in JSX props** — extract or memoize them.
1756
+ 14. **Generic type propagation** — maintain `<TData>` through hooks, context, and component boundaries.