@object-ui/plugin-calendar 2.0.0 → 3.0.0

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.
@@ -46,6 +46,7 @@ export interface CalendarViewProps {
46
46
  onViewChange?: (view: "month" | "week" | "day") => void
47
47
  onNavigate?: (date: Date) => void
48
48
  onAddClick?: () => void
49
+ onEventDrop?: (event: CalendarEvent, newStart: Date, newEnd?: Date) => void
49
50
  className?: string
50
51
  }
51
52
 
@@ -59,6 +60,7 @@ function CalendarView({
59
60
  onViewChange,
60
61
  onNavigate,
61
62
  onAddClick,
63
+ onEventDrop,
62
64
  className,
63
65
  }: CalendarViewProps) {
64
66
  const [selectedView, setSelectedView] = React.useState(view)
@@ -73,6 +75,23 @@ function CalendarView({
73
75
  setSelectedView(view)
74
76
  }, [view])
75
77
 
78
+ // Auto-switch to day view on mobile
79
+ const onViewChangeRef = React.useRef(onViewChange)
80
+ onViewChangeRef.current = onViewChange
81
+
82
+ React.useEffect(() => {
83
+ const mq = window.matchMedia("(max-width: 639px)")
84
+ const handleChange = (e: MediaQueryListEvent | MediaQueryList) => {
85
+ if (e.matches) {
86
+ setSelectedView("day")
87
+ onViewChangeRef.current?.("day")
88
+ }
89
+ }
90
+ handleChange(mq)
91
+ mq.addEventListener("change", handleChange)
92
+ return () => mq.removeEventListener("change", handleChange)
93
+ }, [])
94
+
76
95
  const handlePrevious = () => {
77
96
  const newDate = new Date(selectedDate)
78
97
  if (selectedView === "month") {
@@ -138,6 +157,23 @@ function CalendarView({
138
157
  }
139
158
  }
140
159
 
160
+ // Swipe navigation for mobile
161
+ const touchStart = React.useRef<number>(0)
162
+ const handleTouchStart = (e: React.TouchEvent) => {
163
+ touchStart.current = e.touches[0].clientX
164
+ }
165
+ const handleTouchEnd = (e: React.TouchEvent) => {
166
+ const diff = touchStart.current - e.changedTouches[0].clientX
167
+ if (Math.abs(diff) > 50) {
168
+ const newDate = new Date(selectedDate)
169
+ if (selectedView === "day") newDate.setDate(newDate.getDate() + (diff > 0 ? 1 : -1))
170
+ else if (selectedView === "week") newDate.setDate(newDate.getDate() + (diff > 0 ? 7 : -7))
171
+ else newDate.setMonth(newDate.getMonth() + (diff > 0 ? 1 : -1))
172
+ setSelectedDate(newDate)
173
+ onNavigate?.(newDate)
174
+ }
175
+ }
176
+
141
177
  const handleDateSelect = (date: Date | undefined) => {
142
178
  if (date) {
143
179
  setSelectedDate(date)
@@ -146,18 +182,19 @@ function CalendarView({
146
182
  }
147
183
 
148
184
  return (
149
- <div className={cn("flex flex-col h-full bg-background", className)}>
185
+ <div role="region" aria-label="Calendar" className={cn("flex flex-col h-full bg-background", className)}>
150
186
  {/* Header */}
151
187
  <div className="flex items-center justify-between p-4 border-b">
152
188
  <div className="flex items-center gap-4">
153
189
  <div className="flex items-center bg-muted/50 rounded-lg p-1 gap-1">
154
- <Button variant="ghost" size="sm" onClick={handleToday} className="h-8">
190
+ <Button variant="ghost" size="sm" onClick={handleToday} className="h-8" aria-label="Go to today">
155
191
  Today
156
192
  </Button>
157
193
  <div className="h-4 w-px bg-border mx-1" />
158
194
  <Button
159
195
  variant="ghost"
160
196
  size="icon"
197
+ aria-label="Previous period"
161
198
  onClick={handlePrevious}
162
199
  className="h-8 w-8"
163
200
  >
@@ -166,6 +203,7 @@ function CalendarView({
166
203
  <Button
167
204
  variant="ghost"
168
205
  size="icon"
206
+ aria-label="Next period"
169
207
  onClick={handleNext}
170
208
  className="h-8 w-8"
171
209
  >
@@ -177,6 +215,7 @@ function CalendarView({
177
215
  <PopoverTrigger asChild>
178
216
  <Button
179
217
  variant="ghost"
218
+ aria-label={`Current date: ${getDateLabel()}`}
180
219
  className={cn(
181
220
  "text-xl font-semibold h-auto px-3 py-1 hover:bg-muted/50 transition-colors",
182
221
  "flex items-center gap-2"
@@ -221,19 +260,21 @@ function CalendarView({
221
260
  </div>
222
261
 
223
262
  {/* Calendar Grid */}
224
- <div className="flex-1 overflow-auto">
263
+ <div className="flex-1 overflow-auto" onTouchStart={handleTouchStart} onTouchEnd={handleTouchEnd}>
225
264
  {selectedView === "month" && (
226
265
  <MonthView
227
266
  date={selectedDate}
228
267
  events={events}
229
268
  onEventClick={onEventClick}
230
269
  onDateClick={onDateClick}
270
+ onEventDrop={onEventDrop}
231
271
  />
232
272
  )}
233
273
  {selectedView === "week" && (
234
274
  <WeekView
235
275
  date={selectedDate}
236
276
  events={events}
277
+ locale={locale}
237
278
  onEventClick={onEventClick}
238
279
  onDateClick={onDateClick}
239
280
  />
@@ -243,6 +284,7 @@ function CalendarView({
243
284
  date={selectedDate}
244
285
  events={events}
245
286
  onEventClick={onEventClick}
287
+ onDateClick={onDateClick}
246
288
  />
247
289
  )}
248
290
  </div>
@@ -322,20 +364,79 @@ interface MonthViewProps {
322
364
  events: CalendarEvent[]
323
365
  onEventClick?: (event: CalendarEvent) => void
324
366
  onDateClick?: (date: Date) => void
367
+ onEventDrop?: (event: CalendarEvent, newStart: Date, newEnd?: Date) => void
325
368
  }
326
369
 
327
- function MonthView({ date, events, onEventClick, onDateClick }: MonthViewProps) {
370
+ function MonthView({ date, events, onEventClick, onDateClick, onEventDrop }: MonthViewProps) {
328
371
  const days = getMonthDays(date)
329
372
  const today = new Date()
330
373
  const weekDays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
374
+ const [draggedEventId, setDraggedEventId] = React.useState<string | number | null>(null)
375
+ const [dropTargetIndex, setDropTargetIndex] = React.useState<number | null>(null)
376
+
377
+ const handleDragStart = (e: React.DragEvent, event: CalendarEvent) => {
378
+ setDraggedEventId(event.id)
379
+ e.dataTransfer.effectAllowed = "move"
380
+ e.dataTransfer.setData("text/plain", String(event.id))
381
+ }
382
+
383
+ const handleDragEnd = () => {
384
+ setDraggedEventId(null)
385
+ setDropTargetIndex(null)
386
+ }
387
+
388
+ const handleDragOver = (e: React.DragEvent, index: number) => {
389
+ e.preventDefault()
390
+ e.dataTransfer.dropEffect = "move"
391
+ setDropTargetIndex(index)
392
+ }
393
+
394
+ const handleDragLeave = (e: React.DragEvent) => {
395
+ // Only clear when actually leaving the cell, not when moving over child elements
396
+ if (!e.currentTarget.contains(e.relatedTarget as Node)) {
397
+ setDropTargetIndex(null)
398
+ }
399
+ }
400
+
401
+ const handleDrop = (e: React.DragEvent, targetDay: Date) => {
402
+ e.preventDefault()
403
+ setDropTargetIndex(null)
404
+ setDraggedEventId(null)
405
+
406
+ if (!onEventDrop) return
407
+
408
+ const eventId = e.dataTransfer.getData("text/plain")
409
+ const draggedEvent = events.find((ev) => String(ev.id) === eventId)
410
+ if (!draggedEvent) return
411
+
412
+ const oldStart = new Date(draggedEvent.start)
413
+ const oldStartDay = new Date(oldStart)
414
+ oldStartDay.setHours(0, 0, 0, 0)
415
+
416
+ const newTargetDay = new Date(targetDay)
417
+ newTargetDay.setHours(0, 0, 0, 0)
418
+
419
+ const deltaMs = newTargetDay.getTime() - oldStartDay.getTime()
420
+ if (deltaMs === 0) return
421
+
422
+ const newStart = new Date(oldStart.getTime() + deltaMs)
423
+
424
+ let newEnd: Date | undefined
425
+ if (draggedEvent.end) {
426
+ newEnd = new Date(new Date(draggedEvent.end).getTime() + deltaMs)
427
+ }
428
+
429
+ onEventDrop(draggedEvent, newStart, newEnd)
430
+ }
331
431
 
332
432
  return (
333
433
  <div className="flex flex-col h-full">
334
434
  {/* Week day headers */}
335
- <div className="grid grid-cols-7 border-b">
435
+ <div role="row" className="grid grid-cols-7 border-b">
336
436
  {weekDays.map((day) => (
337
437
  <div
338
438
  key={day}
439
+ role="columnheader"
339
440
  className="p-2 text-center text-sm font-medium text-muted-foreground border-r last:border-r-0"
340
441
  >
341
442
  {day}
@@ -344,7 +445,7 @@ function MonthView({ date, events, onEventClick, onDateClick }: MonthViewProps)
344
445
  </div>
345
446
 
346
447
  {/* Calendar days */}
347
- <div className="grid grid-cols-7 flex-1 auto-rows-fr">
448
+ <div role="grid" aria-label="Calendar grid" className="grid grid-cols-7 flex-1 auto-rows-fr">
348
449
  {days.map((day, index) => {
349
450
  const dayEvents = getEventsForDate(day, events)
350
451
  const isCurrentMonth = day.getMonth() === date.getMonth()
@@ -353,11 +454,17 @@ function MonthView({ date, events, onEventClick, onDateClick }: MonthViewProps)
353
454
  return (
354
455
  <div
355
456
  key={index}
457
+ role="gridcell"
458
+ aria-label={`${day.toLocaleDateString("default", { weekday: "long", month: "long", day: "numeric", year: "numeric" })}${dayEvents.length > 0 ? `, ${dayEvents.length} event${dayEvents.length > 1 ? "s" : ""}` : ""}`}
356
459
  className={cn(
357
460
  "border-b border-r last:border-r-0 p-2 min-h-[100px] cursor-pointer hover:bg-accent/50",
358
- !isCurrentMonth && "bg-muted/30 text-muted-foreground"
461
+ !isCurrentMonth && "bg-muted/30 text-muted-foreground",
462
+ dropTargetIndex === index && "ring-2 ring-primary"
359
463
  )}
360
464
  onClick={() => onDateClick?.(day)}
465
+ onDragOver={(e) => handleDragOver(e, index)}
466
+ onDragLeave={handleDragLeave}
467
+ onDrop={(e) => handleDrop(e, day)}
361
468
  >
362
469
  <div
363
470
  className={cn(
@@ -365,6 +472,7 @@ function MonthView({ date, events, onEventClick, onDateClick }: MonthViewProps)
365
472
  isToday &&
366
473
  "inline-flex items-center justify-center rounded-full bg-primary text-primary-foreground h-6 w-6"
367
474
  )}
475
+ {...(isToday ? { "aria-current": "date" as const } : {})}
368
476
  >
369
477
  {day.getDate()}
370
478
  </div>
@@ -372,9 +480,15 @@ function MonthView({ date, events, onEventClick, onDateClick }: MonthViewProps)
372
480
  {dayEvents.slice(0, 3).map((event) => (
373
481
  <div
374
482
  key={event.id}
483
+ role="button"
484
+ aria-label={event.title}
485
+ draggable={!!onEventDrop}
486
+ onDragStart={(e) => handleDragStart(e, event)}
487
+ onDragEnd={handleDragEnd}
375
488
  className={cn(
376
489
  "text-xs px-2 py-1 rounded truncate cursor-pointer hover:opacity-80",
377
- event.color || DEFAULT_EVENT_COLOR
490
+ event.color || DEFAULT_EVENT_COLOR,
491
+ draggedEventId === event.id && "opacity-50"
378
492
  )}
379
493
  style={
380
494
  event.color && event.color.startsWith("#")
@@ -406,11 +520,28 @@ function MonthView({ date, events, onEventClick, onDateClick }: MonthViewProps)
406
520
  interface WeekViewProps {
407
521
  date: Date
408
522
  events: CalendarEvent[]
523
+ locale?: string
409
524
  onEventClick?: (event: CalendarEvent) => void
410
525
  onDateClick?: (date: Date) => void
411
526
  }
412
527
 
413
- function WeekView({ date, events, onEventClick, onDateClick }: WeekViewProps) {
528
+ function WeekView({ date, events, locale = "default", onEventClick, onDateClick }: WeekViewProps) {
529
+ const longPressTimer = React.useRef<ReturnType<typeof setTimeout> | null>(null)
530
+
531
+ const handleSlotTouchStart = (day: Date) => {
532
+ if (!onDateClick) return
533
+ longPressTimer.current = setTimeout(() => {
534
+ onDateClick(day)
535
+ }, 500)
536
+ }
537
+
538
+ const handleSlotTouchEnd = () => {
539
+ if (longPressTimer.current) {
540
+ clearTimeout(longPressTimer.current)
541
+ longPressTimer.current = null
542
+ }
543
+ }
544
+
414
545
  const weekStart = getWeekStart(date)
415
546
  const weekDays = Array.from({ length: 7 }, (_, i) => {
416
547
  const day = new Date(weekStart)
@@ -448,21 +579,27 @@ function WeekView({ date, events, onEventClick, onDateClick }: WeekViewProps) {
448
579
  </div>
449
580
 
450
581
  {/* Week events */}
451
- <div className="grid grid-cols-7 flex-1">
582
+ <div role="grid" className="grid grid-cols-7 flex-1">
452
583
  {weekDays.map((day) => {
453
584
  const dayEvents = getEventsForDate(day, events)
454
585
  return (
455
586
  <div
456
587
  key={day.toISOString()}
588
+ role="gridcell"
589
+ aria-label={`${day.toLocaleDateString("default", { weekday: "long", month: "long", day: "numeric", year: "numeric" })}${dayEvents.length > 0 ? `, ${dayEvents.length} event${dayEvents.length > 1 ? "s" : ""}` : ""}`}
457
590
  className="border-r last:border-r-0 p-2 min-h-[400px] cursor-pointer hover:bg-accent/50"
458
591
  onClick={() => onDateClick?.(day)}
592
+ onTouchStart={() => handleSlotTouchStart(day)}
593
+ onTouchEnd={handleSlotTouchEnd}
459
594
  >
460
595
  <div className="space-y-2">
461
596
  {dayEvents.map((event) => (
462
597
  <div
463
598
  key={event.id}
599
+ role="button"
600
+ aria-label={event.title}
464
601
  className={cn(
465
- "text-sm px-3 py-2 rounded cursor-pointer hover:opacity-80",
602
+ "text-xs sm:text-sm px-2 sm:px-3 py-1.5 sm:py-2 rounded cursor-pointer hover:opacity-80",
466
603
  event.color || DEFAULT_EVENT_COLOR
467
604
  )}
468
605
  style={
@@ -475,7 +612,7 @@ function WeekView({ date, events, onEventClick, onDateClick }: WeekViewProps) {
475
612
  onEventClick?.(event)
476
613
  }}
477
614
  >
478
- <div className="font-medium">{event.title}</div>
615
+ <div className="font-medium truncate">{event.title}</div>
479
616
  {!event.allDay && (
480
617
  <div className="text-xs opacity-90 mt-1">
481
618
  {event.start.toLocaleTimeString("default", {
@@ -499,15 +636,33 @@ interface DayViewProps {
499
636
  date: Date
500
637
  events: CalendarEvent[]
501
638
  onEventClick?: (event: CalendarEvent) => void
639
+ onDateClick?: (date: Date) => void
502
640
  }
503
641
 
504
- function DayView({ date, events, onEventClick }: DayViewProps) {
642
+ function DayView({ date, events, onEventClick, onDateClick }: DayViewProps) {
505
643
  const dayEvents = getEventsForDate(date, events)
506
644
  const hours = Array.from({ length: 24 }, (_, i) => i)
645
+ const longPressTimer = React.useRef<ReturnType<typeof setTimeout> | null>(null)
646
+
647
+ const handleSlotTouchStart = (hour: number) => {
648
+ if (!onDateClick) return
649
+ longPressTimer.current = setTimeout(() => {
650
+ const clickDate = new Date(date)
651
+ clickDate.setHours(hour, 0, 0, 0)
652
+ onDateClick(clickDate)
653
+ }, 500)
654
+ }
655
+
656
+ const handleSlotTouchEnd = () => {
657
+ if (longPressTimer.current) {
658
+ clearTimeout(longPressTimer.current)
659
+ longPressTimer.current = null
660
+ }
661
+ }
507
662
 
508
663
  return (
509
664
  <div className="flex flex-col h-full">
510
- <div className="flex-1 overflow-auto">
665
+ <div role="list" className="flex-1 overflow-auto">
511
666
  {hours.map((hour) => {
512
667
  const hourEvents = dayEvents.filter((event) => {
513
668
  if (event.allDay) return hour === 0
@@ -516,7 +671,7 @@ function DayView({ date, events, onEventClick }: DayViewProps) {
516
671
  })
517
672
 
518
673
  return (
519
- <div key={hour} className="flex border-b min-h-[60px]">
674
+ <div key={hour} role="listitem" className="flex border-b min-h-[60px]">
520
675
  <div className="w-20 p-2 text-sm text-muted-foreground border-r">
521
676
  {hour === 0
522
677
  ? "12 AM"
@@ -526,12 +681,17 @@ function DayView({ date, events, onEventClick }: DayViewProps) {
526
681
  ? "12 PM"
527
682
  : `${hour - 12} PM`}
528
683
  </div>
529
- <div className="flex-1 p-2 space-y-2">
684
+ <div
685
+ className="flex-1 p-2 space-y-2"
686
+ onTouchStart={() => handleSlotTouchStart(hour)}
687
+ onTouchEnd={handleSlotTouchEnd}
688
+ >
530
689
  {hourEvents.map((event) => (
531
690
  <div
532
691
  key={event.id}
692
+ aria-label={event.title}
533
693
  className={cn(
534
- "px-3 py-2 rounded cursor-pointer hover:opacity-80",
694
+ "px-2 sm:px-3 py-1.5 sm:py-2 rounded cursor-pointer hover:opacity-80",
535
695
  event.color || DEFAULT_EVENT_COLOR
536
696
  )}
537
697
  style={
@@ -541,7 +701,7 @@ function DayView({ date, events, onEventClick }: DayViewProps) {
541
701
  }
542
702
  onClick={() => onEventClick?.(event)}
543
703
  >
544
- <div className="font-medium">{event.title}</div>
704
+ <div className="font-medium truncate">{event.title}</div>
545
705
  {!event.allDay && (
546
706
  <div className="text-xs opacity-90 mt-1">
547
707
  {event.start.toLocaleTimeString("default", {
@@ -0,0 +1,82 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { SchemaRenderer, SchemaRendererProvider } from '@object-ui/react';
3
+ import type { BaseSchema } from '@object-ui/types';
4
+ import { createStorybookDataSource } from '@storybook-config/datasource';
5
+
6
+ const meta = {
7
+ title: 'Plugins/ObjectCalendar',
8
+ component: SchemaRenderer,
9
+ parameters: {
10
+ layout: 'padded',
11
+ },
12
+ tags: ['autodocs'],
13
+ argTypes: {
14
+ schema: { table: { disable: true } },
15
+ },
16
+ } satisfies Meta<any>;
17
+
18
+ export default meta;
19
+ type Story = StoryObj<typeof meta>;
20
+
21
+ const dataSource = createStorybookDataSource();
22
+
23
+ const renderStory = (args: any) => (
24
+ <SchemaRendererProvider dataSource={dataSource}>
25
+ <SchemaRenderer schema={args as unknown as BaseSchema} />
26
+ </SchemaRendererProvider>
27
+ );
28
+
29
+ export const Default: Story = {
30
+ render: renderStory,
31
+ args: {
32
+ type: 'object-grid',
33
+ objectName: 'Event',
34
+ viewType: 'calendar',
35
+ calendar: {
36
+ startDateField: 'startDate',
37
+ endDateField: 'endDate',
38
+ titleField: 'title',
39
+ },
40
+ columns: [
41
+ { field: 'title', header: 'Title' },
42
+ { field: 'startDate', header: 'Start Date' },
43
+ { field: 'endDate', header: 'End Date' },
44
+ { field: 'category', header: 'Category' },
45
+ ],
46
+ data: [
47
+ { id: 1, title: 'Team Standup', startDate: '2024-03-04T09:00:00', endDate: '2024-03-04T09:30:00', category: 'Meeting' },
48
+ { id: 2, title: 'Sprint Planning', startDate: '2024-03-05T10:00:00', endDate: '2024-03-05T12:00:00', category: 'Meeting' },
49
+ { id: 3, title: 'Design Review', startDate: '2024-03-06T14:00:00', endDate: '2024-03-06T15:00:00', category: 'Review' },
50
+ { id: 4, title: 'Product Demo', startDate: '2024-03-07T16:00:00', endDate: '2024-03-07T17:00:00', category: 'Demo' },
51
+ { id: 5, title: 'Retrospective', startDate: '2024-03-08T11:00:00', endDate: '2024-03-08T12:00:00', category: 'Meeting' },
52
+ ],
53
+ className: 'w-full',
54
+ } as any,
55
+ };
56
+
57
+ export const MonthlyEvents: Story = {
58
+ render: renderStory,
59
+ args: {
60
+ type: 'object-grid',
61
+ objectName: 'Appointment',
62
+ viewType: 'calendar',
63
+ calendar: {
64
+ startDateField: 'date',
65
+ titleField: 'name',
66
+ },
67
+ columns: [
68
+ { field: 'name', header: 'Name' },
69
+ { field: 'date', header: 'Date' },
70
+ { field: 'type', header: 'Type' },
71
+ ],
72
+ data: [
73
+ { id: 1, name: 'Board Meeting', date: '2024-03-01T10:00:00', type: 'Corporate' },
74
+ { id: 2, name: 'Client Call', date: '2024-03-05T14:00:00', type: 'Sales' },
75
+ { id: 3, name: 'Team Lunch', date: '2024-03-12T12:00:00', type: 'Social' },
76
+ { id: 4, name: 'Quarterly Review', date: '2024-03-15T09:00:00', type: 'Corporate' },
77
+ { id: 5, name: 'Workshop', date: '2024-03-20T13:00:00', type: 'Training' },
78
+ { id: 6, name: 'Release Day', date: '2024-03-25T08:00:00', type: 'Engineering' },
79
+ ],
80
+ className: 'w-full',
81
+ } as any,
82
+ };
@@ -25,6 +25,9 @@
25
25
  import React, { useEffect, useState, useCallback, useMemo } from 'react';
26
26
  import type { ObjectGridSchema, DataSource, ViewData, CalendarConfig } from '@object-ui/types';
27
27
  import { CalendarView, type CalendarEvent } from './CalendarView';
28
+ import { usePullToRefresh } from '@object-ui/mobile';
29
+ import { useNavigationOverlay } from '@object-ui/react';
30
+ import { NavigationOverlay } from '@object-ui/components';
28
31
 
29
32
  export interface CalendarSchema {
30
33
  type: 'calendar';
@@ -44,6 +47,7 @@ export interface ObjectCalendarProps {
44
47
  dataSource?: DataSource;
45
48
  className?: string;
46
49
  onEventClick?: (record: any) => void;
50
+ onRowClick?: (record: any) => void;
47
51
  onDateClick?: (date: Date) => void;
48
52
  onEdit?: (record: any) => void;
49
53
  onDelete?: (record: any) => void;
@@ -136,6 +140,7 @@ export const ObjectCalendar: React.FC<ObjectCalendarProps> = ({
136
140
  dataSource,
137
141
  className,
138
142
  onEventClick,
143
+ onRowClick,
139
144
  onDateClick,
140
145
  onNavigate,
141
146
  onViewChange,
@@ -147,6 +152,16 @@ export const ObjectCalendar: React.FC<ObjectCalendarProps> = ({
147
152
  const [objectSchema, setObjectSchema] = useState<any>(null);
148
153
  const [currentDate, setCurrentDate] = useState(new Date());
149
154
  const [view, setView] = useState<'month' | 'week' | 'day'>('month');
155
+ const [refreshKey, setRefreshKey] = useState(0);
156
+
157
+ const handlePullRefresh = useCallback(async () => {
158
+ setRefreshKey(k => k + 1);
159
+ }, []);
160
+
161
+ const { ref: pullRef, isRefreshing, pullDistance } = usePullToRefresh<HTMLDivElement>({
162
+ onRefresh: handlePullRefresh,
163
+ enabled: !!dataSource && !!schema.objectName,
164
+ });
150
165
 
151
166
  const dataConfig = useMemo(() => getDataConfig(schema), [
152
167
  (schema as any).data,
@@ -232,7 +247,7 @@ export const ObjectCalendar: React.FC<ObjectCalendarProps> = ({
232
247
 
233
248
  fetchData();
234
249
  return () => { isMounted = false; };
235
- }, [dataConfig, dataSource, hasInlineData, schema.filter, schema.sort]);
250
+ }, [dataConfig, dataSource, hasInlineData, schema.filter, schema.sort, refreshKey]);
236
251
 
237
252
  // Fetch object schema for field metadata
238
253
  useEffect(() => {
@@ -292,6 +307,14 @@ export const ObjectCalendar: React.FC<ObjectCalendarProps> = ({
292
307
  onDateClick?.(today);
293
308
  }, [onDateClick]);
294
309
 
310
+ // --- NavigationConfig support ---
311
+ // Must be called before any early returns to satisfy React hooks rules
312
+ const navigation = useNavigationOverlay({
313
+ navigation: (schema as any).navigation,
314
+ objectName: schema.objectName,
315
+ onRowClick,
316
+ });
317
+
295
318
  if (loading) {
296
319
  return (
297
320
  <div className={className}>
@@ -325,13 +348,24 @@ export const ObjectCalendar: React.FC<ObjectCalendarProps> = ({
325
348
  }
326
349
 
327
350
  return (
328
- <div className={className}>
329
- <div className="border rounded-lg bg-background h-[calc(100vh-200px)] min-h-[600px]">
351
+ <div ref={pullRef} className={className}>
352
+ {pullDistance > 0 && (
353
+ <div
354
+ className="flex items-center justify-center text-xs text-muted-foreground"
355
+ style={{ height: pullDistance }}
356
+ >
357
+ {isRefreshing ? 'Refreshing…' : 'Pull to refresh'}
358
+ </div>
359
+ )}
360
+ <div className="border rounded-lg bg-background h-[calc(100vh-120px)] sm:h-[calc(100vh-160px)] md:h-[calc(100vh-200px)] min-h-[400px] sm:min-h-[600px]">
330
361
  <CalendarView
331
362
  events={events}
332
363
  currentDate={currentDate}
333
364
  view={(schema as any).defaultView || 'month'}
334
- onEventClick={(event) => onEventClick?.(event.data)}
365
+ onEventClick={(event) => {
366
+ navigation.handleClick(event.data);
367
+ onEventClick?.(event.data);
368
+ }}
335
369
  onDateClick={onDateClick}
336
370
  onNavigate={(date) => {
337
371
  setCurrentDate(date);
@@ -344,6 +378,22 @@ export const ObjectCalendar: React.FC<ObjectCalendarProps> = ({
344
378
  onAddClick={handleCreate}
345
379
  />
346
380
  </div>
381
+ {navigation.isOverlay && (
382
+ <NavigationOverlay {...navigation} title="Event Details">
383
+ {(record) => (
384
+ <div className="space-y-3">
385
+ {Object.entries(record).map(([key, value]) => (
386
+ <div key={key} className="flex flex-col">
387
+ <span className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
388
+ {key.replace(/_/g, ' ')}
389
+ </span>
390
+ <span className="text-sm">{String(value ?? '—')}</span>
391
+ </div>
392
+ ))}
393
+ </div>
394
+ )}
395
+ </NavigationOverlay>
396
+ )}
347
397
  </div>
348
398
  );
349
399
  };