@innosolutions/inno-calendar 1.0.2 → 1.0.4

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 (55) hide show
  1. package/AGENT.md +1253 -0
  2. package/README.md +99 -1185
  3. package/dist/{agenda-dropdown-DRS-BzmH.js → agenda-dropdown-Bt7X_Zfs.js} +130 -130
  4. package/dist/agenda-dropdown-Bt7X_Zfs.js.map +1 -0
  5. package/dist/agenda-dropdown-BxS1w8zQ.cjs +2 -0
  6. package/dist/agenda-dropdown-BxS1w8zQ.cjs.map +1 -0
  7. package/dist/{agenda-view-Bq2AFesz.js → agenda-view-BDVwfLye.js} +131 -129
  8. package/dist/{agenda-view-Bq2AFesz.js.map → agenda-view-BDVwfLye.js.map} +1 -1
  9. package/dist/agenda-view-CZPZMrdq.cjs +11 -0
  10. package/dist/{agenda-view-BWBRB_iZ.cjs.map → agenda-view-CZPZMrdq.cjs.map} +1 -1
  11. package/dist/components/index.cjs +1 -1
  12. package/dist/components/index.mjs +2 -2
  13. package/dist/components/views/day-view.d.ts.map +1 -1
  14. package/dist/components/views/week-view.d.ts.map +1 -1
  15. package/dist/core/context/inno-calendar-provider.d.ts +2 -8
  16. package/dist/core/context/inno-calendar-provider.d.ts.map +1 -1
  17. package/dist/core/index.cjs +1 -1
  18. package/dist/core/index.mjs +3 -3
  19. package/dist/core/types.d.ts +12 -6
  20. package/dist/core/types.d.ts.map +1 -1
  21. package/dist/core/utils/date-utils.d.ts +3 -16
  22. package/dist/core/utils/date-utils.d.ts.map +1 -1
  23. package/dist/index.cjs +1 -1
  24. package/dist/index.d.ts +5 -12
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.mjs +6 -6
  27. package/dist/{inno-calendar-provider-BwP6uUAq.cjs → inno-calendar-provider-BOdwC6tk.cjs} +2 -2
  28. package/dist/{inno-calendar-provider-BwP6uUAq.cjs.map → inno-calendar-provider-BOdwC6tk.cjs.map} +1 -1
  29. package/dist/{inno-calendar-provider-B0e1XLeo.js → inno-calendar-provider-Buls2aP9.js} +2 -2
  30. package/dist/{inno-calendar-provider-B0e1XLeo.js.map → inno-calendar-provider-Buls2aP9.js.map} +1 -1
  31. package/dist/presets/index.cjs +1 -1
  32. package/dist/presets/index.mjs +1 -1
  33. package/dist/slot-selection-context-3WVXrd0q.cjs +2 -0
  34. package/dist/slot-selection-context-3WVXrd0q.cjs.map +1 -0
  35. package/dist/{slot-selection-context-BCNnYGHC.js → slot-selection-context-msJxhYfm.js} +123 -123
  36. package/dist/slot-selection-context-msJxhYfm.js.map +1 -0
  37. package/dist/styles/index.css +1 -0
  38. package/dist/styles/index.d.ts +1 -0
  39. package/dist/{tailwind-calendar-BlRF-alx.js → tailwind-calendar-CyIGh6Xu.js} +93 -93
  40. package/dist/{tailwind-calendar-BlRF-alx.js.map → tailwind-calendar-CyIGh6Xu.js.map} +1 -1
  41. package/dist/tailwind-calendar-eBJzso44.cjs +2 -0
  42. package/dist/{tailwind-calendar-D6AeGjVG.cjs.map → tailwind-calendar-eBJzso44.cjs.map} +1 -1
  43. package/dist/{use-calendar-time-config-F-8WV_Sd.cjs → use-calendar-time-config-DQTW52ca.cjs} +2 -2
  44. package/dist/{use-calendar-time-config-F-8WV_Sd.cjs.map → use-calendar-time-config-DQTW52ca.cjs.map} +1 -1
  45. package/dist/{use-calendar-time-config-CtKkXw_L.js → use-calendar-time-config-DWTnhQx9.js} +3 -3
  46. package/dist/{use-calendar-time-config-CtKkXw_L.js.map → use-calendar-time-config-DWTnhQx9.js.map} +1 -1
  47. package/package.json +5 -2
  48. package/dist/agenda-dropdown-C3HEFmnk.cjs +0 -2
  49. package/dist/agenda-dropdown-C3HEFmnk.cjs.map +0 -1
  50. package/dist/agenda-dropdown-DRS-BzmH.js.map +0 -1
  51. package/dist/agenda-view-BWBRB_iZ.cjs +0 -11
  52. package/dist/slot-selection-context-BCNnYGHC.js.map +0 -1
  53. package/dist/slot-selection-context-C52lU9W4.cjs +0 -2
  54. package/dist/slot-selection-context-C52lU9W4.cjs.map +0 -1
  55. package/dist/tailwind-calendar-D6AeGjVG.cjs +0 -2
package/AGENT.md ADDED
@@ -0,0 +1,1253 @@
1
+ # @inno/calendar - Agent Implementation Guide
2
+
3
+ > This document contains comprehensive implementation details for AI coding agents. Copy this entire file into your context when working with @inno/calendar.
4
+
5
+ ---
6
+
7
+ ## Package Overview
8
+
9
+ **@innosolutions/inno-calendar** is a headless-first, fully customizable React calendar package for enterprise applications.
10
+
11
+ - **Version**: 1.0.4+
12
+ - **React**: >=18.0.0
13
+ - **TypeScript**: Full type coverage
14
+ - **Styling**: TailwindCSS compatible, CSS custom properties
15
+ - **Dependencies**: Minimal (clsx, tailwind-merge, class-variance-authority)
16
+
17
+ ---
18
+
19
+ ## Installation & Setup
20
+
21
+ ### Install Package
22
+
23
+ ```bash
24
+ npm install @innosolutions/inno-calendar
25
+ ```
26
+
27
+ ### Install Optional Peer Dependencies
28
+
29
+ ```bash
30
+ # For enhanced UI (popovers, tooltips, dropdowns)
31
+ npm install @radix-ui/react-popover @radix-ui/react-tooltip @radix-ui/react-dropdown-menu
32
+ ```
33
+
34
+ ### Import Styles
35
+
36
+ **Required** - Import in your app's entry point:
37
+
38
+ ```tsx
39
+ // In main.tsx or App.tsx
40
+ import '@innosolutions/inno-calendar/styles';
41
+ ```
42
+
43
+ Or in CSS:
44
+
45
+ ```css
46
+ @import '@innosolutions/inno-calendar/styles';
47
+ ```
48
+
49
+ ---
50
+
51
+ ## Core Types
52
+
53
+ ### CalendarEvent
54
+
55
+ ```tsx
56
+ interface CalendarEvent<TData = Record<string, unknown>> {
57
+ id: string;
58
+ title: string;
59
+ startDate: Date;
60
+ endDate: Date;
61
+ color?: TEventColor;
62
+ description?: string;
63
+ isCanceled?: boolean;
64
+ isAllDay?: boolean;
65
+ scheduleTypeId?: number;
66
+ scheduleTypeName?: string;
67
+ participants?: ICalendarUser[];
68
+ data?: TData;
69
+ }
70
+ ```
71
+
72
+ ### TEventColor
73
+
74
+ ```tsx
75
+ type TEventColor =
76
+ | 'blue' | 'green' | 'red' | 'yellow'
77
+ | 'purple' | 'orange' | 'pink' | 'teal'
78
+ | 'gray' | 'indigo';
79
+ ```
80
+
81
+ ### TCalendarView
82
+
83
+ ```tsx
84
+ type TCalendarView =
85
+ | 'day' | 'week' | 'month' | 'year' | 'agenda'
86
+ | 'timeline-day' | 'timeline-3day' | 'timeline-week';
87
+ ```
88
+
89
+ ### ICalendarUser
90
+
91
+ ```tsx
92
+ interface ICalendarUser {
93
+ id: string;
94
+ name: string;
95
+ avatar?: string;
96
+ email?: string;
97
+ }
98
+ ```
99
+
100
+ ### IScheduleType
101
+
102
+ ```tsx
103
+ interface IScheduleType {
104
+ id: number;
105
+ name: string;
106
+ color?: string;
107
+ }
108
+ ```
109
+
110
+ ### TWorkingHours
111
+
112
+ ```tsx
113
+ interface IWorkingHoursDay {
114
+ enabled: boolean;
115
+ from: number; // 0-23
116
+ to: number; // 0-23
117
+ }
118
+
119
+ type TWorkingHours = Record<number, IWorkingHoursDay>;
120
+ // Key: 0 = Sunday, 1 = Monday, ..., 6 = Saturday
121
+ ```
122
+
123
+ ### ISelectionResult
124
+
125
+ ```tsx
126
+ interface ISelectionResult {
127
+ startDate: Date;
128
+ endDate: Date;
129
+ mode?: 'time' | 'day';
130
+ }
131
+ ```
132
+
133
+ ### IDropResult
134
+
135
+ ```tsx
136
+ interface IDropResult<TData = Record<string, unknown>> {
137
+ event: CalendarEvent<TData>;
138
+ newStartDate: Date;
139
+ newEndDate: Date;
140
+ oldStartDate: Date;
141
+ oldEndDate: Date;
142
+ }
143
+ ```
144
+
145
+ ---
146
+
147
+ ## Main Components
148
+
149
+ ### InnoCalendar
150
+
151
+ The primary component providing a complete calendar experience.
152
+
153
+ ```tsx
154
+ import { InnoCalendar, type CalendarEvent } from '@innosolutions/inno-calendar';
155
+
156
+ <InnoCalendar
157
+ // Required
158
+ events={events}
159
+
160
+ // Data (optional)
161
+ users={users}
162
+ scheduleTypes={scheduleTypes}
163
+
164
+ // Initial state
165
+ initialView="week"
166
+ initialDate={new Date()}
167
+ initialSelectedUserId="all"
168
+ initialScheduleTypeIds={[]}
169
+ initialParticipantIds={[]}
170
+ initialWorkingHoursView="default"
171
+ initialSearchQuery=""
172
+
173
+ // Preferences
174
+ preferencesConfig={{
175
+ defaults: {
176
+ badgeVariant: 'colored',
177
+ slotDuration: 30,
178
+ workingHours: defaultWorkingHours,
179
+ },
180
+ locked: {
181
+ badgeVariant: false,
182
+ slotDuration: false,
183
+ },
184
+ }}
185
+
186
+ // Callbacks
187
+ onEventClick={(event) => {}}
188
+ onSlotClick={(date, hour) => {}}
189
+ onSlotSelect={(selection) => {}}
190
+ onAddEvent={() => {}}
191
+ onEventDrop={(result) => {}}
192
+ onDateChange={(date, view) => {}}
193
+ onViewChange={(view) => {}}
194
+
195
+ // UI options
196
+ showHeader={true}
197
+ minSelectionMinutes={30}
198
+ className="h-full"
199
+
200
+ // Custom rendering
201
+ renderPopover={({ event, onClose }) => <CustomPopover event={event} onClose={onClose} />}
202
+ settingsContent={<SettingsPanel />}
203
+ filterContent={<FiltersRow />}
204
+ />
205
+ ```
206
+
207
+ ### InnoCalendarProvider
208
+
209
+ Context provider for shared state across components.
210
+
211
+ ```tsx
212
+ import {
213
+ InnoCalendarProvider,
214
+ useInnoCalendar,
215
+ CalendarHeader,
216
+ WeekView
217
+ } from '@innosolutions/inno-calendar';
218
+
219
+ function App() {
220
+ return (
221
+ <InnoCalendarProvider
222
+ initialEvents={events}
223
+ initialUsers={users}
224
+ initialScheduleTypes={scheduleTypes}
225
+ initialView="week"
226
+ initialDate={new Date()}
227
+ preferencesConfig={preferencesConfig}
228
+ onDateChange={(date, view) => updateURL(date, view)}
229
+ onViewChange={(view) => updateURL(view)}
230
+ >
231
+ <CalendarContent />
232
+ </InnoCalendarProvider>
233
+ );
234
+ }
235
+
236
+ function CalendarContent() {
237
+ const {
238
+ // View & Navigation
239
+ view,
240
+ setView,
241
+ selectedDate,
242
+ setSelectedDate,
243
+
244
+ // Data
245
+ events,
246
+ setEvents,
247
+ filteredEvents,
248
+ users,
249
+ scheduleTypes,
250
+
251
+ // Filters
252
+ selectedUserId,
253
+ setSelectedUserId,
254
+ selectedScheduleTypeIds,
255
+ setSelectedScheduleTypeIds,
256
+ searchQuery,
257
+ setSearchQuery,
258
+
259
+ // Visual Settings
260
+ badgeVariant,
261
+ setBadgeVariant,
262
+
263
+ // Time Configuration
264
+ slotDuration,
265
+ setSlotDuration,
266
+ visibleHours,
267
+ setVisibleHours,
268
+ workingHours,
269
+ setWorkingHours,
270
+ showWorkingHoursOnly,
271
+ setShowWorkingHoursOnly,
272
+
273
+ // Preferences
274
+ isPreferenceLocked,
275
+ } = useInnoCalendar();
276
+
277
+ return (
278
+ <div>
279
+ <CalendarHeader />
280
+ <WeekView events={filteredEvents} date={selectedDate} />
281
+ </div>
282
+ );
283
+ }
284
+ ```
285
+
286
+ ### Individual View Components
287
+
288
+ ```tsx
289
+ import {
290
+ DayView,
291
+ WeekView,
292
+ MonthView,
293
+ YearView,
294
+ AgendaView,
295
+ TimelineView
296
+ } from '@innosolutions/inno-calendar';
297
+
298
+ // Day View
299
+ <DayView
300
+ events={events}
301
+ date={selectedDate}
302
+ visibleHours={{ startHour: 8, endHour: 18 }}
303
+ workingHours={workingHours}
304
+ slotDuration={30}
305
+ badgeVariant="colored"
306
+ onEventClick={handleEventClick}
307
+ onSlotSelect={handleSlotSelect}
308
+ renderPopover={renderCustomPopover}
309
+ />
310
+
311
+ // Week View
312
+ <WeekView
313
+ events={events}
314
+ date={selectedDate}
315
+ weekStartsOn={1} // 0 = Sunday, 1 = Monday
316
+ visibleHours={{ startHour: 0, endHour: 24 }}
317
+ workingHours={workingHours}
318
+ slotDuration={30}
319
+ badgeVariant="colored"
320
+ onEventClick={handleEventClick}
321
+ onSlotSelect={handleSlotSelect}
322
+ onDayClick={handleDayClick}
323
+ renderPopover={renderCustomPopover}
324
+ />
325
+
326
+ // Month View
327
+ <MonthView
328
+ events={events}
329
+ date={selectedDate}
330
+ badgeVariant="colored"
331
+ onEventClick={handleEventClick}
332
+ onDayClick={handleDayClick}
333
+ renderPopover={renderCustomPopover}
334
+ />
335
+
336
+ // Year View
337
+ <YearView
338
+ events={events}
339
+ year={2026}
340
+ onMonthClick={handleMonthClick}
341
+ onDayClick={handleDayClick}
342
+ />
343
+
344
+ // Agenda View
345
+ <AgendaView
346
+ events={events}
347
+ date={selectedDate}
348
+ onEventClick={handleEventClick}
349
+ renderPopover={renderCustomPopover}
350
+ />
351
+
352
+ // Timeline View (Resource-based)
353
+ <TimelineView
354
+ events={events}
355
+ users={users}
356
+ selectedDate={selectedDate}
357
+ daysToShow={3} // 1, 3, or 7
358
+ visibleHours={{ from: 8, to: 18 }}
359
+ onEventClick={handleEventClick}
360
+ renderPopover={renderCustomPopover}
361
+ />
362
+ ```
363
+
364
+ ### CalendarHeader
365
+
366
+ ```tsx
367
+ import { CalendarHeader } from '@innosolutions/inno-calendar';
368
+
369
+ <CalendarHeader
370
+ settingsContent={<SettingsPanel />}
371
+ filterContent={<FiltersRow />}
372
+ showAddButton={true}
373
+ onAddEvent={() => openCreateDialog()}
374
+ />
375
+ ```
376
+
377
+ ### Settings Components
378
+
379
+ ```tsx
380
+ import {
381
+ SlotDurationSetting,
382
+ VisibleHoursSetting,
383
+ BadgeVariantSetting,
384
+ WorkingHoursSetting,
385
+ } from '@innosolutions/inno-calendar';
386
+
387
+ function SettingsPanel() {
388
+ return (
389
+ <div className="space-y-4 p-4">
390
+ <SlotDurationSetting />
391
+ <BadgeVariantSetting />
392
+ <VisibleHoursSetting />
393
+ <WorkingHoursSetting />
394
+ </div>
395
+ );
396
+ }
397
+ ```
398
+
399
+ ### Filter Components
400
+
401
+ ```tsx
402
+ import { UserFilter, ScheduleTypeFilter } from '@innosolutions/inno-calendar';
403
+
404
+ function FiltersRow() {
405
+ const {
406
+ users,
407
+ selectedUserId,
408
+ setSelectedUserId,
409
+ scheduleTypes,
410
+ selectedScheduleTypeIds,
411
+ setSelectedScheduleTypeIds
412
+ } = useInnoCalendar();
413
+
414
+ return (
415
+ <div className="flex gap-2">
416
+ <UserFilter
417
+ users={users}
418
+ selectedUserId={selectedUserId}
419
+ onSelect={setSelectedUserId}
420
+ />
421
+ <ScheduleTypeFilter
422
+ scheduleTypes={scheduleTypes}
423
+ selectedIds={selectedScheduleTypeIds}
424
+ onChange={setSelectedScheduleTypeIds}
425
+ />
426
+ </div>
427
+ );
428
+ }
429
+ ```
430
+
431
+ ---
432
+
433
+ ## Data Transformation
434
+
435
+ Transform your API data to match the calendar's expected format:
436
+
437
+ ```tsx
438
+ // transformers.ts
439
+ import type { CalendarEvent, ICalendarUser, IScheduleType } from '@innosolutions/inno-calendar';
440
+
441
+ export function transformApiEvents(
442
+ apiEvents: ApiEvent[],
443
+ scheduleTypes: ApiScheduleType[]
444
+ ): CalendarEvent[] {
445
+ return apiEvents.map((event) => {
446
+ const scheduleType = scheduleTypes.find((st) => st.id === event.scheduleTypeId);
447
+
448
+ return {
449
+ id: String(event.id),
450
+ title: event.subject || event.title,
451
+ startDate: new Date(event.startDate),
452
+ endDate: new Date(event.endDate),
453
+ description: event.description ?? undefined,
454
+ color: mapHexToColor(scheduleType?.colorHex) ?? 'blue',
455
+ scheduleTypeId: event.scheduleTypeId,
456
+ scheduleTypeName: scheduleType?.code || scheduleType?.name,
457
+ isCanceled: event.isCanceled ?? false,
458
+ isAllDay: event.isAllDay ?? false,
459
+ participants: event.participants?.map(transformParticipant),
460
+ };
461
+ });
462
+ }
463
+
464
+ export function transformParticipants(apiParticipants: ApiParticipant[]): ICalendarUser[] {
465
+ return apiParticipants.map((p) => ({
466
+ id: String(p.id),
467
+ name: `${p.firstName} ${p.lastName}`.trim() || p.email,
468
+ avatar: p.pictureUrl || p.avatar,
469
+ email: p.email,
470
+ }));
471
+ }
472
+
473
+ export function transformScheduleTypes(apiTypes: ApiScheduleType[]): IScheduleType[] {
474
+ return apiTypes.map((st) => ({
475
+ id: st.id,
476
+ name: st.resourceKey ?? st.code ?? st.name ?? `Type ${st.id}`,
477
+ color: st.colorHex,
478
+ }));
479
+ }
480
+
481
+ // Helper to map hex colors to TEventColor
482
+ function mapHexToColor(hex?: string): TEventColor | undefined {
483
+ if (!hex) return undefined;
484
+
485
+ const colorMap: Record<string, TEventColor> = {
486
+ '#3b82f6': 'blue',
487
+ '#22c55e': 'green',
488
+ '#ef4444': 'red',
489
+ '#eab308': 'yellow',
490
+ '#a855f7': 'purple',
491
+ '#f97316': 'orange',
492
+ '#ec4899': 'pink',
493
+ '#14b8a6': 'teal',
494
+ '#6b7280': 'gray',
495
+ '#6366f1': 'indigo',
496
+ };
497
+
498
+ // Find closest match or default
499
+ const normalized = hex.toLowerCase();
500
+ return colorMap[normalized] ?? 'blue';
501
+ }
502
+ ```
503
+
504
+ ---
505
+
506
+ ## URL Synchronization
507
+
508
+ ### With TanStack Router
509
+
510
+ ```tsx
511
+ import { getRouteApi } from '@tanstack/react-router';
512
+ import { z } from 'zod';
513
+
514
+ // Schema for search params
515
+ export const calendarSearchSchema = z.object({
516
+ view: z.string().optional(),
517
+ selectedDate: z.string().optional(),
518
+ startDate: z.string().optional(),
519
+ endDate: z.string().optional(),
520
+ scheduleTypeIds: z.array(z.number()).optional(),
521
+ participants: z.array(z.string()).optional(),
522
+ search: z.string().optional(),
523
+ workingHoursView: z.enum(['default', 'working']).optional(),
524
+ });
525
+
526
+ // Route definition
527
+ export const Route = createFileRoute('/_layout/agenda/')({
528
+ validateSearch: calendarSearchSchema,
529
+ loaderDeps: ({ search }) => ({
530
+ startDate: search.startDate,
531
+ endDate: search.endDate,
532
+ scheduleTypeIds: search.scheduleTypeIds,
533
+ participants: search.participants,
534
+ }),
535
+ loader: async ({ context, deps }) => {
536
+ const [events, participants, scheduleTypes] = await Promise.all([
537
+ context.queryClient.ensureQueryData({
538
+ queryKey: ['agenda', deps.startDate, deps.endDate],
539
+ queryFn: () => fetchAgendaEvents(deps),
540
+ }),
541
+ context.queryClient.ensureQueryData({
542
+ queryKey: ['participants'],
543
+ queryFn: fetchParticipants,
544
+ }),
545
+ context.queryClient.ensureQueryData({
546
+ queryKey: ['schedule-types'],
547
+ queryFn: fetchScheduleTypes,
548
+ }),
549
+ ]);
550
+ return { events, participants, scheduleTypes };
551
+ },
552
+ });
553
+
554
+ // Component
555
+ const routeApi = getRouteApi('/_layout/agenda/');
556
+
557
+ function AgendaPage() {
558
+ const navigate = routeApi.useNavigate();
559
+ const searchParams = routeApi.useSearch();
560
+ const loaderData = routeApi.useLoaderData();
561
+
562
+ const handleDateChange = useCallback((date: Date, view: TCalendarView) => {
563
+ const { startDate, endDate } = getViewDateRange(date, view);
564
+ navigate({
565
+ search: (prev) => ({
566
+ ...prev,
567
+ selectedDate: formatDate(date),
568
+ startDate: formatDate(startDate),
569
+ endDate: formatDate(endDate),
570
+ }),
571
+ replace: true,
572
+ });
573
+ }, [navigate]);
574
+
575
+ const handleViewChange = useCallback((view: TCalendarView) => {
576
+ navigate({
577
+ search: (prev) => ({ ...prev, view }),
578
+ replace: true,
579
+ });
580
+ }, [navigate]);
581
+
582
+ return (
583
+ <InnoCalendar
584
+ events={loaderData.events}
585
+ initialView={searchParams.view ?? 'week'}
586
+ initialDate={parseDate(searchParams.selectedDate)}
587
+ onDateChange={handleDateChange}
588
+ onViewChange={handleViewChange}
589
+ />
590
+ );
591
+ }
592
+ ```
593
+
594
+ ### Date Range Helpers
595
+
596
+ ```tsx
597
+ export function getViewDateRange(date: Date, view: TCalendarView): { startDate: Date; endDate: Date } {
598
+ const d = new Date(date);
599
+
600
+ switch (view) {
601
+ case 'day':
602
+ return {
603
+ startDate: startOfDay(d),
604
+ endDate: endOfDay(d),
605
+ };
606
+ case 'week':
607
+ case 'timeline-week':
608
+ return {
609
+ startDate: startOfWeek(d),
610
+ endDate: endOfWeek(d),
611
+ };
612
+ case 'month':
613
+ // Include surrounding weeks for month grid
614
+ const monthStart = startOfMonth(d);
615
+ const monthEnd = endOfMonth(d);
616
+ return {
617
+ startDate: startOfWeek(monthStart),
618
+ endDate: endOfWeek(monthEnd),
619
+ };
620
+ case 'year':
621
+ return {
622
+ startDate: new Date(d.getFullYear(), 0, 1),
623
+ endDate: new Date(d.getFullYear(), 11, 31),
624
+ };
625
+ default:
626
+ return {
627
+ startDate: startOfMonth(d),
628
+ endDate: endOfMonth(d),
629
+ };
630
+ }
631
+ }
632
+
633
+ export function formatDateForApi(date: Date): string {
634
+ return date.toISOString().split('T')[0]; // YYYY-MM-DD
635
+ }
636
+ ```
637
+
638
+ ---
639
+
640
+ ## Dialog Integration
641
+
642
+ ### With Global Dialog Store
643
+
644
+ ```tsx
645
+ import { useDialog } from '@/hooks/use-dialog';
646
+ import dayjs from 'dayjs';
647
+
648
+ function CalendarWithDialogs() {
649
+ const { initialize } = useDialog();
650
+
651
+ const handleSlotSelect = useCallback((selection: ISelectionResult) => {
652
+ initialize({
653
+ title: 'Create Event',
654
+ children: (
655
+ <EventForm
656
+ startDate={dayjs(selection.startDate).format('YYYY-MM-DDTHH:mm:ss')}
657
+ endDate={dayjs(selection.endDate).format('YYYY-MM-DDTHH:mm:ss')}
658
+ />
659
+ ),
660
+ classes: { content: 'max-w-3xl' },
661
+ });
662
+ }, [initialize]);
663
+
664
+ const handleEventClick = useCallback((event: CalendarEvent) => {
665
+ initialize({
666
+ title: 'Event Details',
667
+ children: <EventDetailsDialog eventId={event.id} />,
668
+ });
669
+ }, [initialize]);
670
+
671
+ const handleEventDrop = useCallback((result: IDropResult) => {
672
+ initialize({
673
+ title: 'Reschedule Event',
674
+ children: (
675
+ <EventForm
676
+ id={result.event.id}
677
+ startDate={dayjs(result.newStartDate).format('YYYY-MM-DDTHH:mm:ss')}
678
+ endDate={dayjs(result.newEndDate).format('YYYY-MM-DDTHH:mm:ss')}
679
+ />
680
+ ),
681
+ });
682
+ }, [initialize]);
683
+
684
+ return (
685
+ <InnoCalendar
686
+ events={events}
687
+ onSlotSelect={handleSlotSelect}
688
+ onEventClick={handleEventClick}
689
+ onEventDrop={handleEventDrop}
690
+ />
691
+ );
692
+ }
693
+ ```
694
+
695
+ ---
696
+
697
+ ## Custom Event Popover
698
+
699
+ ```tsx
700
+ function CustomPopover({
701
+ event,
702
+ onClose
703
+ }: {
704
+ event: CalendarEvent;
705
+ onClose: () => void;
706
+ }) {
707
+ const { data: details, isLoading } = useQuery({
708
+ queryKey: ['event-details', event.id],
709
+ queryFn: () => fetchEventDetails(event.id),
710
+ });
711
+
712
+ if (isLoading) {
713
+ return <div className="p-4 animate-pulse">Loading...</div>;
714
+ }
715
+
716
+ return (
717
+ <div className="p-4 space-y-3 min-w-[300px]">
718
+ {/* Header */}
719
+ <div className="flex items-start justify-between">
720
+ <div>
721
+ <h3 className="font-semibold text-lg">{event.title}</h3>
722
+ {event.scheduleTypeName && (
723
+ <span className="text-xs px-2 py-0.5 rounded bg-muted">
724
+ {event.scheduleTypeName}
725
+ </span>
726
+ )}
727
+ </div>
728
+ <button onClick={onClose} className="text-muted-foreground">
729
+
730
+ </button>
731
+ </div>
732
+
733
+ {/* Time */}
734
+ <div className="text-sm text-muted-foreground">
735
+ {formatDateRange(event.startDate, event.endDate)}
736
+ </div>
737
+
738
+ {/* Description */}
739
+ {event.description && (
740
+ <p className="text-sm">{event.description}</p>
741
+ )}
742
+
743
+ {/* Participants */}
744
+ {details?.participants?.length > 0 && (
745
+ <div className="flex -space-x-2">
746
+ {details.participants.map((p) => (
747
+ <img
748
+ key={p.id}
749
+ src={p.avatar}
750
+ alt={p.name}
751
+ className="w-8 h-8 rounded-full border-2 border-white"
752
+ />
753
+ ))}
754
+ </div>
755
+ )}
756
+
757
+ {/* Actions */}
758
+ <div className="flex gap-2 pt-3 border-t">
759
+ <button
760
+ onClick={() => handleEdit(event)}
761
+ className="flex-1 px-3 py-1.5 text-sm border rounded hover:bg-muted"
762
+ >
763
+ Edit
764
+ </button>
765
+ <button
766
+ onClick={() => handleDelete(event)}
767
+ className="px-3 py-1.5 text-sm text-destructive border border-destructive rounded hover:bg-destructive/10"
768
+ >
769
+ Delete
770
+ </button>
771
+ </div>
772
+ </div>
773
+ );
774
+ }
775
+
776
+ // Usage
777
+ <InnoCalendar
778
+ events={events}
779
+ renderPopover={({ event, onClose }) => (
780
+ <CustomPopover event={event} onClose={onClose} />
781
+ )}
782
+ />
783
+ ```
784
+
785
+ ---
786
+
787
+ ## Working Hours Configuration
788
+
789
+ ```tsx
790
+ // Default working hours
791
+ const DEFAULT_WORKING_HOURS: TWorkingHours = {
792
+ 0: { enabled: false, from: 8, to: 17 }, // Sunday - disabled
793
+ 1: { enabled: true, from: 8, to: 17 }, // Monday
794
+ 2: { enabled: true, from: 8, to: 17 }, // Tuesday
795
+ 3: { enabled: true, from: 8, to: 17 }, // Wednesday
796
+ 4: { enabled: true, from: 8, to: 17 }, // Thursday
797
+ 5: { enabled: true, from: 8, to: 17 }, // Friday
798
+ 6: { enabled: true, from: 8, to: 12 }, // Saturday - half day
799
+ };
800
+
801
+ // Pass to calendar
802
+ <InnoCalendar
803
+ events={events}
804
+ preferencesConfig={{
805
+ defaults: {
806
+ workingHours: DEFAULT_WORKING_HOURS,
807
+ showWorkingHoursOnly: false,
808
+ },
809
+ }}
810
+ />
811
+ ```
812
+
813
+ ---
814
+
815
+ ## Preferences Configuration
816
+
817
+ ```tsx
818
+ interface IPreferencesConfig {
819
+ defaults?: {
820
+ badgeVariant?: 'dot' | 'colored' | 'mixed';
821
+ slotDuration?: 15 | 30 | 60;
822
+ visibleHours?: { start: number; end: number };
823
+ workingHours?: TWorkingHours;
824
+ showWorkingHoursOnly?: boolean;
825
+ };
826
+ locked?: {
827
+ badgeVariant?: boolean;
828
+ slotDuration?: boolean;
829
+ visibleHours?: boolean;
830
+ workingHours?: boolean;
831
+ showWorkingHoursOnly?: boolean;
832
+ };
833
+ }
834
+
835
+ // Example: Lock certain settings
836
+ <InnoCalendar
837
+ events={events}
838
+ preferencesConfig={{
839
+ defaults: {
840
+ badgeVariant: 'colored',
841
+ slotDuration: 30,
842
+ },
843
+ locked: {
844
+ badgeVariant: true, // Users cannot change
845
+ slotDuration: false, // Users can change
846
+ },
847
+ }}
848
+ />
849
+ ```
850
+
851
+ ---
852
+
853
+ ## CSS Custom Properties
854
+
855
+ The package uses CSS custom properties for theming. Override in your styles:
856
+
857
+ ```css
858
+ :root {
859
+ /* Core colors */
860
+ --inno-border-color: #e5e7eb;
861
+ --inno-background: #ffffff;
862
+ --inno-foreground: #111827;
863
+ --inno-muted: #f3f4f6;
864
+ --inno-muted-foreground: #6b7280;
865
+ --inno-primary: #3b82f6;
866
+ --inno-primary-foreground: #ffffff;
867
+
868
+ /* Event colors */
869
+ --inno-event-blue: #3b82f6;
870
+ --inno-event-green: #22c55e;
871
+ --inno-event-red: #ef4444;
872
+ --inno-event-yellow: #eab308;
873
+ --inno-event-purple: #a855f7;
874
+ --inno-event-orange: #f97316;
875
+ --inno-event-pink: #ec4899;
876
+ --inno-event-teal: #14b8a6;
877
+ --inno-event-gray: #6b7280;
878
+ --inno-event-indigo: #6366f1;
879
+
880
+ /* Layout */
881
+ --inno-hour-height: 96px;
882
+ --inno-header-height: 56px;
883
+ }
884
+
885
+ /* Dark mode */
886
+ .dark {
887
+ --inno-border-color: #374151;
888
+ --inno-background: #111827;
889
+ --inno-foreground: #f9fafb;
890
+ --inno-muted: #1f2937;
891
+ --inno-muted-foreground: #9ca3af;
892
+ }
893
+ ```
894
+
895
+ ---
896
+
897
+ ## Complete Implementation Example
898
+
899
+ ```tsx
900
+ // features/agenda/components/agenda-page.tsx
901
+ import { useDialog } from '@/hooks/use-dialog';
902
+ import { useCurrentUser } from '@/hooks/use-current-user';
903
+ import { getRouteApi } from '@tanstack/react-router';
904
+ import { useCallback, useEffect, useMemo, useState } from 'react';
905
+ import { useTranslation } from 'react-i18next';
906
+ import dayjs from 'dayjs';
907
+
908
+ import {
909
+ InnoCalendar,
910
+ type CalendarEvent,
911
+ type ISelectionResult,
912
+ type TCalendarView,
913
+ type IDropResult,
914
+ type IScheduleType,
915
+ } from '@innosolutions/inno-calendar';
916
+ import '@innosolutions/inno-calendar/styles';
917
+
918
+ import { transformApiEvents, transformParticipants } from '../transformers';
919
+ import { getViewDateRange, formatDateForApi } from '../helpers';
920
+ import { EventPopover } from './event-popover';
921
+ import { EventForm } from '../forms/event-form';
922
+ import { CalendarSettings } from './calendar-settings';
923
+ import { UserSelect, ScheduleTypeSelect } from './filters';
924
+
925
+ const routeApi = getRouteApi('/_protectedLayout/agenda/');
926
+
927
+ export function AgendaPage() {
928
+ const { t } = useTranslation();
929
+ const { initialize } = useDialog();
930
+ const navigate = routeApi.useNavigate();
931
+ const searchParams = routeApi.useSearch();
932
+
933
+ // Role-based access
934
+ const { user } = useCurrentUser();
935
+ const isAdmin = user?.roles?.some(
936
+ (r) => r.name === 'Admin' || r.name === 'Super Admin'
937
+ );
938
+
939
+ // Route data
940
+ const loaderData = routeApi.useLoaderData();
941
+ const rawEvents = loaderData?.data?.data ?? [];
942
+ const rawParticipants = loaderData?.participants?.data ?? [];
943
+ const rawScheduleTypes = loaderData?.scheduleTypes?.data ?? [];
944
+
945
+ // Transform data
946
+ const calendarEvents = useMemo(
947
+ () => transformApiEvents(rawEvents, rawScheduleTypes),
948
+ [rawEvents, rawScheduleTypes]
949
+ );
950
+
951
+ const calendarUsers = useMemo(
952
+ () => transformParticipants(rawParticipants),
953
+ [rawParticipants]
954
+ );
955
+
956
+ const scheduleTypes = useMemo(
957
+ () => rawScheduleTypes.map((st) => ({
958
+ id: st.id,
959
+ name: st.resourceKey ?? st.code ?? `Type ${st.id}`,
960
+ color: st.colorHex,
961
+ })) as IScheduleType[],
962
+ [rawScheduleTypes]
963
+ );
964
+
965
+ // Parse URL params
966
+ const initialDate = useMemo(() => {
967
+ if (searchParams.selectedDate) {
968
+ const parsed = new Date(searchParams.selectedDate);
969
+ if (!Number.isNaN(parsed.getTime())) return parsed;
970
+ }
971
+ return new Date();
972
+ }, [searchParams.selectedDate]);
973
+
974
+ const initialView = searchParams.view as TCalendarView | undefined;
975
+
976
+ // Handlers
977
+ const handleSlotSelect = useCallback(
978
+ (selection: ISelectionResult) => {
979
+ initialize({
980
+ title: t('actionText.addAgenda'),
981
+ children: (
982
+ <EventForm
983
+ startDate={dayjs(selection.startDate).format('YYYY-MM-DDTHH:mm:ss')}
984
+ endDate={dayjs(selection.endDate).format('YYYY-MM-DDTHH:mm:ss')}
985
+ />
986
+ ),
987
+ classes: { content: 'max-w-3xl !h-fit' },
988
+ });
989
+ },
990
+ [initialize, t]
991
+ );
992
+
993
+ const handleSlotClick = useCallback(
994
+ (date: Date, hour?: number) => {
995
+ const startDate = new Date(date);
996
+ if (typeof hour === 'number') {
997
+ startDate.setHours(hour, 0, 0, 0);
998
+ }
999
+ const endDate = new Date(startDate);
1000
+ endDate.setHours(endDate.getHours() + 1);
1001
+ handleSlotSelect({ startDate, endDate });
1002
+ },
1003
+ [handleSlotSelect]
1004
+ );
1005
+
1006
+ const handleAddEvent = useCallback(() => {
1007
+ handleSlotClick(new Date());
1008
+ }, [handleSlotClick]);
1009
+
1010
+ const handleEventDrop = useCallback(
1011
+ (result: IDropResult) => {
1012
+ initialize({
1013
+ title: t('forms.agenda.editMeeting'),
1014
+ children: (
1015
+ <EventForm
1016
+ id={result.event.id}
1017
+ startDate={dayjs(result.newStartDate).format('YYYY-MM-DDTHH:mm:ss')}
1018
+ endDate={dayjs(result.newEndDate).format('YYYY-MM-DDTHH:mm:ss')}
1019
+ />
1020
+ ),
1021
+ classes: { content: 'max-w-3xl !h-fit' },
1022
+ });
1023
+ },
1024
+ [initialize, t]
1025
+ );
1026
+
1027
+ const handleDateChange = useCallback(
1028
+ (newDate: Date, view: TCalendarView) => {
1029
+ const { startDate, endDate } = getViewDateRange(newDate, view);
1030
+ navigate({
1031
+ search: (prev) => ({
1032
+ ...prev,
1033
+ selectedDate: dayjs(newDate).format('YYYY-MM-DD'),
1034
+ startDate: formatDateForApi(startDate),
1035
+ endDate: formatDateForApi(endDate),
1036
+ }),
1037
+ replace: true,
1038
+ });
1039
+ },
1040
+ [navigate]
1041
+ );
1042
+
1043
+ const handleViewChange = useCallback(
1044
+ (newView: TCalendarView) => {
1045
+ const { startDate, endDate } = getViewDateRange(initialDate, newView);
1046
+ navigate({
1047
+ search: (prev) => ({
1048
+ ...prev,
1049
+ view: newView,
1050
+ startDate: formatDateForApi(startDate),
1051
+ endDate: formatDateForApi(endDate),
1052
+ }),
1053
+ replace: true,
1054
+ });
1055
+ },
1056
+ [navigate, initialDate]
1057
+ );
1058
+
1059
+ const renderPopover = useCallback(
1060
+ ({ event, onClose }: { event: CalendarEvent; onClose: () => void }) => (
1061
+ <EventPopover eventId={event.id} onClose={onClose} />
1062
+ ),
1063
+ []
1064
+ );
1065
+
1066
+ return (
1067
+ <div className="h-full">
1068
+ <InnoCalendar
1069
+ events={calendarEvents}
1070
+ users={calendarUsers}
1071
+ scheduleTypes={scheduleTypes}
1072
+ initialView={initialView}
1073
+ initialDate={initialDate}
1074
+ initialWorkingHoursView={searchParams.workingHoursView ?? 'default'}
1075
+ renderPopover={renderPopover}
1076
+ onSlotSelect={handleSlotSelect}
1077
+ onSlotClick={handleSlotClick}
1078
+ onAddEvent={handleAddEvent}
1079
+ onEventDrop={handleEventDrop}
1080
+ onDateChange={handleDateChange}
1081
+ onViewChange={handleViewChange}
1082
+ settingsContent={isAdmin ? <CalendarSettings /> : undefined}
1083
+ filterContent={
1084
+ <div className="flex items-center gap-2">
1085
+ {calendarUsers.length > 0 && <UserSelect />}
1086
+ {scheduleTypes.length > 0 && <ScheduleTypeSelect />}
1087
+ </div>
1088
+ }
1089
+ />
1090
+ </div>
1091
+ );
1092
+ }
1093
+ ```
1094
+
1095
+ ---
1096
+
1097
+ ## Migration from FullCalendar
1098
+
1099
+ ```tsx
1100
+ // Before (FullCalendar)
1101
+ import FullCalendar from '@fullcalendar/react';
1102
+ import dayGridPlugin from '@fullcalendar/daygrid';
1103
+ import timeGridPlugin from '@fullcalendar/timegrid';
1104
+
1105
+ <FullCalendar
1106
+ plugins={[dayGridPlugin, timeGridPlugin]}
1107
+ initialView="timeGridWeek"
1108
+ events={events}
1109
+ eventClick={(info) => handleClick(info.event)}
1110
+ slotMinTime="08:00:00"
1111
+ slotMaxTime="18:00:00"
1112
+ businessHours={{ daysOfWeek: [1,2,3,4,5], startTime: '09:00', endTime: '17:00' }}
1113
+ />
1114
+
1115
+ // After (@inno/calendar)
1116
+ import { InnoCalendar } from '@innosolutions/inno-calendar';
1117
+ import '@innosolutions/inno-calendar/styles';
1118
+
1119
+ <InnoCalendar
1120
+ events={transformedEvents}
1121
+ initialView="week"
1122
+ onEventClick={(event) => handleClick(event)}
1123
+ preferencesConfig={{
1124
+ defaults: {
1125
+ visibleHours: { start: 8, end: 18 },
1126
+ workingHours: {
1127
+ 0: { enabled: false, from: 9, to: 17 },
1128
+ 1: { enabled: true, from: 9, to: 17 },
1129
+ 2: { enabled: true, from: 9, to: 17 },
1130
+ 3: { enabled: true, from: 9, to: 17 },
1131
+ 4: { enabled: true, from: 9, to: 17 },
1132
+ 5: { enabled: true, from: 9, to: 17 },
1133
+ 6: { enabled: false, from: 9, to: 17 },
1134
+ },
1135
+ },
1136
+ }}
1137
+ />
1138
+ ```
1139
+
1140
+ ---
1141
+
1142
+ ## Exports Reference
1143
+
1144
+ ```tsx
1145
+ // Main component
1146
+ export { InnoCalendar } from '@innosolutions/inno-calendar';
1147
+
1148
+ // Provider & hooks
1149
+ export {
1150
+ InnoCalendarProvider,
1151
+ useInnoCalendar,
1152
+ useOptionalInnoCalendar,
1153
+ } from '@innosolutions/inno-calendar';
1154
+
1155
+ // View components
1156
+ export {
1157
+ DayView,
1158
+ WeekView,
1159
+ MonthView,
1160
+ YearView,
1161
+ AgendaView,
1162
+ TimelineView,
1163
+ } from '@innosolutions/inno-calendar';
1164
+
1165
+ // Header & settings
1166
+ export {
1167
+ CalendarHeader,
1168
+ SlotDurationSetting,
1169
+ VisibleHoursSetting,
1170
+ BadgeVariantSetting,
1171
+ WorkingHoursSetting,
1172
+ } from '@innosolutions/inno-calendar';
1173
+
1174
+ // Filters
1175
+ export {
1176
+ UserFilter,
1177
+ ScheduleTypeFilter,
1178
+ } from '@innosolutions/inno-calendar';
1179
+
1180
+ // Types
1181
+ export type {
1182
+ CalendarEvent,
1183
+ TCalendarView,
1184
+ TEventColor,
1185
+ TBadgeVariant,
1186
+ TWorkingHours,
1187
+ IWorkingHoursDay,
1188
+ TSlotDuration,
1189
+ ICalendarUser,
1190
+ IScheduleType,
1191
+ ISelectionResult,
1192
+ IDropResult,
1193
+ IVisibleHours,
1194
+ IPreferencesConfig,
1195
+ } from '@innosolutions/inno-calendar';
1196
+ ```
1197
+
1198
+ ---
1199
+
1200
+ ## Troubleshooting
1201
+
1202
+ ### Styles not applying
1203
+
1204
+ Ensure you import the styles:
1205
+
1206
+ ```tsx
1207
+ import '@innosolutions/inno-calendar/styles';
1208
+ ```
1209
+
1210
+ ### Working hours striped pattern not showing
1211
+
1212
+ The `.bg-calendar-disabled-hour` class requires CSS custom properties. Import styles or add:
1213
+
1214
+ ```css
1215
+ .bg-calendar-disabled-hour {
1216
+ background-image: repeating-linear-gradient(
1217
+ -60deg,
1218
+ var(--inno-border-color, #e5e7eb) 0 0.5px,
1219
+ transparent 0.5px 8px
1220
+ );
1221
+ }
1222
+ ```
1223
+
1224
+ ### TypeScript errors with custom event data
1225
+
1226
+ Use the generic parameter:
1227
+
1228
+ ```tsx
1229
+ import type { CalendarEvent } from '@innosolutions/inno-calendar';
1230
+
1231
+ interface MyData {
1232
+ customField: string;
1233
+ }
1234
+
1235
+ const events: CalendarEvent<MyData>[] = [...];
1236
+
1237
+ <InnoCalendar<MyData>
1238
+ events={events}
1239
+ onEventClick={(event) => {
1240
+ // event.data is typed as MyData | undefined
1241
+ console.log(event.data?.customField);
1242
+ }}
1243
+ />
1244
+ ```
1245
+
1246
+ ### Events not filtering
1247
+
1248
+ Ensure you're using `filteredEvents` from context, not the raw `events`:
1249
+
1250
+ ```tsx
1251
+ const { filteredEvents } = useInnoCalendar();
1252
+ // Use filteredEvents for rendering, not the original events prop
1253
+ ```