picocrank 1.14.1 → 1.15.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "picocrank",
3
- "version": "1.14.1",
3
+ "version": "1.15.0",
4
4
  "description": "A set of zero dependency Vue components built on the Femtocrank global CSS theme.",
5
5
  "main": "vite.config.js",
6
6
  "scripts": {
@@ -18,14 +18,14 @@
18
18
  "url": "https://github.com/jamesread/picocrank/issues"
19
19
  },
20
20
  "dependencies": {
21
- "@hugeicons/core-free-icons": "^4.0.0",
21
+ "@hugeicons/core-free-icons": "^4.1.1",
22
22
  "@hugeicons/vue": "^1.0.5",
23
- "@vitejs/plugin-vue": "^6.0.4",
23
+ "@vitejs/plugin-vue": "^6.0.6",
24
24
  "femtocrank": "^2.5.0",
25
- "unplugin-vue-components": "^31.0.0",
26
- "vite": "^7.3.1",
27
- "vue": "^3.5.30",
28
- "vue-router": "^5.0.3"
25
+ "unplugin-vue-components": "^32.0.0",
26
+ "vite": "^8.0.10",
27
+ "vue": "^3.5.33",
28
+ "vue-router": "^5.0.6"
29
29
  },
30
30
  "devDependencies": {
31
31
  "semantic-release": "^25.0.3"
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { ref, computed } from 'vue'
2
+ import { ref, computed, watch, onUnmounted } from 'vue'
3
3
 
4
4
  export interface CalendarEvent {
5
5
  id: string | number
@@ -10,12 +10,31 @@ export interface CalendarEvent {
10
10
  [key: string]: any
11
11
  }
12
12
 
13
+ /** Payload for `event-move-request`: call `respond(true)` after a successful commit, or `respond(false)` to decline (e.g. failed server update). */
14
+ export interface CalendarEventMoveRequest {
15
+ event: CalendarEvent
16
+ /** Calendar cell where the drag started (for multi-day events, the visible segment day). */
17
+ sourceDate: Date
18
+ /** Calendar cell where the event was dropped. */
19
+ targetDate: Date
20
+ /** Invoke exactly once with the outcome. If omitted until timeout, the move is treated as rejected. */
21
+ respond: (accepted: boolean) => void
22
+ }
23
+
13
24
  export interface CalendarProps {
14
25
  events: CalendarEvent[]
15
26
  monthNames?: string[]
16
27
  dayNames?: string[]
17
28
  loading?: boolean
18
29
  error?: string | null
30
+ /** When true, events can be dragged to another day. */
31
+ eventDragEnabled?: boolean
32
+ /**
33
+ * Max time (ms) to wait for `respond()` on `event-move-request`.
34
+ * If `respond` is not called in time, the move is rejected and a warning is logged.
35
+ * Set to `0` to disable the timeout (not recommended if listeners may omit `respond`).
36
+ */
37
+ eventMoveResponseTimeoutMs?: number
19
38
  // Formatter functions
20
39
  getEventDate?: (event: CalendarEvent) => Date | null
21
40
  getEventDateRange?: (event: CalendarEvent) => { start: Date | null; end: Date | null }
@@ -35,14 +54,20 @@ const props = withDefaults(defineProps<CalendarProps>(), {
35
54
  dayNames: () => ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
36
55
  loading: false,
37
56
  error: null,
57
+ eventDragEnabled: true,
58
+ eventMoveResponseTimeoutMs: 15000,
38
59
  showNavigation: true,
39
60
  })
40
61
 
41
62
  const emit = defineEmits<{
42
63
  'event-click': [event: CalendarEvent]
43
64
  'date-click': [date: Date]
65
+ 'date-range-select': [start: Date, end: Date]
44
66
  'month-change': [month: number, year: number]
45
67
  'event-context-menu': [event: CalendarEvent, mouseEvent: MouseEvent]
68
+ 'event-move-request': [payload: CalendarEventMoveRequest]
69
+ 'event-moved': [payload: { event: CalendarEvent; sourceDate: Date; targetDate: Date }]
70
+ 'event-move-rejected': [payload: { event: CalendarEvent; sourceDate: Date; targetDate: Date }]
46
71
  }>()
47
72
 
48
73
  // Internal calendar state
@@ -316,16 +341,239 @@ function handleEventClick(event: CalendarEvent) {
316
341
  emit('event-click', event)
317
342
  }
318
343
 
319
- function handleDateClick(date: Date) {
320
- emit('date-click', date)
321
- }
322
-
323
344
  function handleContextMenu(event: CalendarEvent, mouseEvent: MouseEvent) {
324
345
  mouseEvent.preventDefault()
325
346
  mouseEvent.stopPropagation()
326
347
  emit('event-context-menu', event, mouseEvent)
327
348
  }
328
- import { watch } from 'vue'
349
+
350
+ // --- Date range selection (click / drag) ---
351
+
352
+ function startOfDay(d: Date): Date {
353
+ const x = new Date(d)
354
+ x.setHours(0, 0, 0, 0)
355
+ return x
356
+ }
357
+
358
+ function toDateKey(d: Date): string {
359
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
360
+ }
361
+
362
+ function dateFromKey(key: string): Date {
363
+ const [y, m, day] = key.split('-').map(Number)
364
+ return startOfDay(new Date(y, m - 1, day))
365
+ }
366
+
367
+ const isDraggingRange = ref(false)
368
+ const rangeDragAnchor = ref<Date | null>(null)
369
+ const rangeDragEnd = ref<Date | null>(null)
370
+ const selectedRangeStart = ref<Date | null>(null)
371
+ const selectedRangeEnd = ref<Date | null>(null)
372
+
373
+ function isEventTarget(ev: Event): boolean {
374
+ const t = ev.target
375
+ if (!(t instanceof Node)) return false
376
+ return Boolean((t as HTMLElement).closest?.('.calendar-event'))
377
+ }
378
+
379
+ function getRangeBounds(): { lo: Date; hi: Date } | null {
380
+ const a = rangeDragAnchor.value
381
+ const b = rangeDragEnd.value
382
+ if (!a || !b) return null
383
+ const sa = startOfDay(a).getTime()
384
+ const sb = startOfDay(b).getTime()
385
+ if (sa <= sb) return { lo: startOfDay(a), hi: startOfDay(b) }
386
+ return { lo: startOfDay(b), hi: startOfDay(a) }
387
+ }
388
+
389
+ function rangeHighlightForDate(date: Date): 'none' | 'single' | 'start' | 'end' | 'middle' {
390
+ const bounds = isDraggingRange.value
391
+ ? getRangeBounds()
392
+ : selectedRangeStart.value && selectedRangeEnd.value
393
+ ? (() => {
394
+ const sa = startOfDay(selectedRangeStart.value!).getTime()
395
+ const sb = startOfDay(selectedRangeEnd.value!).getTime()
396
+ const lo = sa <= sb ? startOfDay(selectedRangeStart.value!) : startOfDay(selectedRangeEnd.value!)
397
+ const hi = sa <= sb ? startOfDay(selectedRangeEnd.value!) : startOfDay(selectedRangeStart.value!)
398
+ return { lo, hi }
399
+ })()
400
+ : null
401
+ if (!bounds) return 'none'
402
+ const t = startOfDay(date).getTime()
403
+ if (t < bounds.lo.getTime() || t > bounds.hi.getTime()) return 'none'
404
+ if (bounds.lo.getTime() === bounds.hi.getTime()) return 'single'
405
+ if (t === bounds.lo.getTime()) return 'start'
406
+ if (t === bounds.hi.getTime()) return 'end'
407
+ return 'middle'
408
+ }
409
+
410
+ function onWindowTouchMove(ev: TouchEvent): void {
411
+ if (!isDraggingRange.value || ev.touches.length === 0) return
412
+ const { clientX, clientY } = ev.touches[0]
413
+ const el = document.elementFromPoint(clientX, clientY)
414
+ if (!el) return
415
+ const cell = (el as HTMLElement).closest?.('[data-calendar-date]')
416
+ const key = cell?.dataset?.calendarDate
417
+ if (!key) return
418
+ rangeDragEnd.value = dateFromKey(key)
419
+ }
420
+
421
+ function detachRangePointerListeners(): void {
422
+ window.removeEventListener('mouseup', endRangeSelectionFromPointer)
423
+ window.removeEventListener('blur', endRangeSelectionFromPointer)
424
+ window.removeEventListener('touchmove', onWindowTouchMove)
425
+ window.removeEventListener('touchend', endRangeSelectionFromPointer)
426
+ window.removeEventListener('touchcancel', endRangeSelectionFromPointer)
427
+ }
428
+
429
+ function endRangeSelectionFromPointer(): void {
430
+ if (!isDraggingRange.value) return
431
+ const bounds = getRangeBounds()
432
+ isDraggingRange.value = false
433
+ rangeDragAnchor.value = null
434
+ rangeDragEnd.value = null
435
+ detachRangePointerListeners()
436
+
437
+ if (!bounds) return
438
+
439
+ selectedRangeStart.value = new Date(bounds.lo)
440
+ selectedRangeEnd.value = new Date(bounds.hi)
441
+ emit('date-range-select', new Date(bounds.lo), new Date(bounds.hi))
442
+
443
+ if (bounds.lo.getTime() === bounds.hi.getTime()) {
444
+ emit('date-click', new Date(bounds.lo))
445
+ }
446
+ }
447
+
448
+ function onDayPointerDown(date: Date, ev: MouseEvent | TouchEvent): void {
449
+ if (isEventTarget(ev)) return
450
+ if (ev instanceof MouseEvent && ev.button !== 0) return
451
+
452
+ const d = startOfDay(date)
453
+ isDraggingRange.value = true
454
+ rangeDragAnchor.value = new Date(d)
455
+ rangeDragEnd.value = new Date(d)
456
+ window.addEventListener('mouseup', endRangeSelectionFromPointer)
457
+ window.addEventListener('blur', endRangeSelectionFromPointer)
458
+ window.addEventListener('touchmove', onWindowTouchMove, { passive: true })
459
+ window.addEventListener('touchend', endRangeSelectionFromPointer)
460
+ window.addEventListener('touchcancel', endRangeSelectionFromPointer)
461
+ if (ev instanceof MouseEvent) {
462
+ ev.preventDefault()
463
+ }
464
+ }
465
+
466
+ function onDayPointerEnter(date: Date): void {
467
+ if (!isDraggingRange.value) return
468
+ rangeDragEnd.value = startOfDay(date)
469
+ }
470
+
471
+ onUnmounted(() => {
472
+ detachRangePointerListeners()
473
+ })
474
+
475
+ // --- Calendar event drag and drop (HTML5) ---
476
+
477
+ const CALENDAR_EVENT_DRAG_MIME = 'application/x-picocrank-calendar-event-id'
478
+
479
+ const draggingEventId = ref<string | number | null>(null)
480
+ const draggingSourceDate = ref<Date | null>(null)
481
+ const dragOverTargetDate = ref<Date | null>(null)
482
+ const eventMoveLocked = ref(false)
483
+
484
+ function sameCalendarDay(a: Date | null, b: Date | null): boolean {
485
+ if (!a || !b) return false
486
+ return startOfDay(a).getTime() === startOfDay(b).getTime()
487
+ }
488
+
489
+ function onEventDragStart(event: CalendarEvent, sourceCellDate: Date, dragEvent: DragEvent): void {
490
+ if (!props.eventDragEnabled) {
491
+ dragEvent.preventDefault()
492
+ return
493
+ }
494
+ draggingEventId.value = event.id
495
+ draggingSourceDate.value = startOfDay(sourceCellDate)
496
+ dragEvent.dataTransfer?.setData(CALENDAR_EVENT_DRAG_MIME, String(event.id))
497
+ dragEvent.dataTransfer?.setData('text/plain', event.title)
498
+ if (dragEvent.dataTransfer) {
499
+ dragEvent.dataTransfer.effectAllowed = 'move'
500
+ }
501
+ }
502
+
503
+ function onEventDragEnd(): void {
504
+ draggingEventId.value = null
505
+ draggingSourceDate.value = null
506
+ dragOverTargetDate.value = null
507
+ }
508
+
509
+ function onDayDragOver(dayDate: Date, ev: DragEvent): void {
510
+ if (!props.eventDragEnabled || draggingEventId.value === null) return
511
+ ev.preventDefault()
512
+ if (ev.dataTransfer) {
513
+ ev.dataTransfer.dropEffect = 'move'
514
+ }
515
+ dragOverTargetDate.value = startOfDay(dayDate)
516
+ }
517
+
518
+ function onDayDrop(dayDate: Date, ev: DragEvent): void {
519
+ ev.preventDefault()
520
+ dragOverTargetDate.value = null
521
+ if (!props.eventDragEnabled) return
522
+
523
+ const idStr = ev.dataTransfer?.getData(CALENDAR_EVENT_DRAG_MIME)
524
+ if (!idStr) return
525
+
526
+ const moved = props.events.find(e => String(e.id) === idStr)
527
+ if (!moved) return
528
+
529
+ const sourceRaw = draggingSourceDate.value
530
+ if (!sourceRaw) return
531
+
532
+ const targetDate = startOfDay(dayDate)
533
+ const sourceDate = startOfDay(sourceRaw)
534
+ if (targetDate.getTime() === sourceDate.getTime()) return
535
+ if (eventMoveLocked.value) return
536
+ eventMoveLocked.value = true
537
+
538
+ const sourceClone = new Date(sourceDate)
539
+ const targetClone = new Date(targetDate)
540
+
541
+ let settled = false
542
+ let watchdog: ReturnType<typeof setTimeout> | undefined
543
+
544
+ function respond(accepted: boolean): void {
545
+ if (settled) return
546
+ settled = true
547
+ if (watchdog !== undefined) {
548
+ clearTimeout(watchdog)
549
+ }
550
+ eventMoveLocked.value = false
551
+ if (accepted) {
552
+ emit('event-moved', { event: moved, sourceDate: sourceClone, targetDate: targetClone })
553
+ } else {
554
+ emit('event-move-rejected', { event: moved, sourceDate: sourceClone, targetDate: targetClone })
555
+ }
556
+ }
557
+
558
+ emit('event-move-request', {
559
+ event: moved,
560
+ sourceDate: sourceClone,
561
+ targetDate: targetClone,
562
+ respond,
563
+ })
564
+
565
+ const timeoutMs = props.eventMoveResponseTimeoutMs
566
+ if (timeoutMs > 0) {
567
+ watchdog = setTimeout(() => {
568
+ if (!settled) {
569
+ console.warn(
570
+ '[Calendar] event-move-request: respond() was not called before timeout; treating as rejected.'
571
+ )
572
+ respond(false)
573
+ }
574
+ }, timeoutMs)
575
+ }
576
+ }
329
577
 
330
578
  // Sync grid start when controlled month/year props change (but not during scrolling)
331
579
  watch([currentMonth, currentYear], () => {
@@ -352,7 +600,14 @@ watch([currentMonth, currentYear], () => {
352
600
  <div class="calendar-container">
353
601
  <div v-if="error" class="calendar-error">{{ error }}</div>
354
602
  <div v-if="loading" class="calendar-loading-overlay">Loading…</div>
355
- <div class="calendar-grid" @wheel.prevent="handleWheel">
603
+ <div
604
+ class="calendar-grid"
605
+ :class="{
606
+ 'is-dragging-range': isDraggingRange,
607
+ 'is-dragging-event': draggingEventId !== null
608
+ }"
609
+ @wheel.prevent="handleWheel"
610
+ >
356
611
  <!-- Day headers -->
357
612
  <div v-for="day in dayNames" :key="day" class="day-header">{{ day }}</div>
358
613
 
@@ -361,15 +616,26 @@ watch([currentMonth, currentYear], () => {
361
616
  v-for="(day, index) in calendarDays"
362
617
  :key="index"
363
618
  class="calendar-day"
619
+ :data-calendar-date="toDateKey(day.date)"
620
+ @mousedown="onDayPointerDown(day.date, $event)"
621
+ @touchstart.passive="onDayPointerDown(day.date, $event)"
622
+ @mouseenter="onDayPointerEnter(day.date)"
623
+ @dragover="onDayDragOver(day.date, $event)"
624
+ @drop="onDayDrop(day.date, $event)"
364
625
  :class="{
365
- 'today': day && day.date.toDateString() === new Date().toDateString(),
366
- 'weekend': day && (day.date.getDay() === 0 || day.date.getDay() === 6),
367
- 'prev-month': day && day.date.getMonth() !== currentMonth && day.date.getMonth() !== (currentMonth + 1) % 12,
368
- 'next-month': day && day.date.getMonth() !== currentMonth && day.date.getMonth() === (currentMonth + 1) % 12,
369
- 'past': day && isPastDay(day.date)
626
+ 'today': day.date.toDateString() === new Date().toDateString(),
627
+ 'weekend': day.date.getDay() === 0 || day.date.getDay() === 6,
628
+ 'prev-month': day.date.getMonth() !== currentMonth && day.date.getMonth() !== (currentMonth + 1) % 12,
629
+ 'next-month': day.date.getMonth() !== currentMonth && day.date.getMonth() === (currentMonth + 1) % 12,
630
+ 'past': isPastDay(day.date),
631
+ 'range-single': rangeHighlightForDate(day.date) === 'single',
632
+ 'range-start': rangeHighlightForDate(day.date) === 'start',
633
+ 'range-end': rangeHighlightForDate(day.date) === 'end',
634
+ 'range-middle': rangeHighlightForDate(day.date) === 'middle',
635
+ 'drag-over-target': eventDragEnabled && sameCalendarDay(dragOverTargetDate, day.date)
370
636
  }"
371
637
  >
372
- <div v-if="day" class="day-content" @click="handleDateClick(day.date)">
638
+ <div class="day-content">
373
639
  <div class="day-number clickable">
374
640
  {{ day.date.getDate() }}<span class="day-month">{{ getOrdinalSuffix(day.date.getDate()) }} {{ monthNames[day.date.getMonth()].substring(0, 3) }}</span>
375
641
  </div>
@@ -388,8 +654,12 @@ watch([currentMonth, currentYear], () => {
388
654
  'multi-day': isMultiDayEvent(event),
389
655
  'multi-day-start': isMultiDayEvent(event) && getMultiDayPosition(event, day.date) === 'start',
390
656
  'multi-day-middle': isMultiDayEvent(event) && getMultiDayPosition(event, day.date) === 'middle',
391
- 'multi-day-end': isMultiDayEvent(event) && getMultiDayPosition(event, day.date) === 'end'
657
+ 'multi-day-end': isMultiDayEvent(event) && getMultiDayPosition(event, day.date) === 'end',
658
+ 'drag-source': draggingEventId === event.id
392
659
  }"
660
+ :draggable="eventDragEnabled"
661
+ @dragstart.stop="onEventDragStart(event, day.date, $event)"
662
+ @dragend.stop="onEventDragEnd"
393
663
  @click.stop="handleEventClick(event)"
394
664
  @contextmenu.stop="handleContextMenu(event, $event)"
395
665
  >
@@ -482,6 +752,45 @@ watch([currentMonth, currentYear], () => {
482
752
  min-height: 400px;
483
753
  }
484
754
 
755
+ .calendar-grid.is-dragging-range,
756
+ .calendar-grid.is-dragging-event {
757
+ user-select: none;
758
+ }
759
+
760
+ .calendar-day.range-single {
761
+ background-color: rgba(0, 123, 255, 0.22) !important;
762
+ box-shadow: inset 0 0 0 2px #007bff;
763
+ }
764
+
765
+ .calendar-day.range-start {
766
+ background-color: rgba(0, 123, 255, 0.14) !important;
767
+ box-shadow:
768
+ inset 2px 0 0 0 #007bff,
769
+ inset 0 2px 0 0 #007bff,
770
+ inset 0 -2px 0 0 #007bff;
771
+ }
772
+
773
+ .calendar-day.range-end {
774
+ background-color: rgba(0, 123, 255, 0.14) !important;
775
+ box-shadow:
776
+ inset -2px 0 0 0 #007bff,
777
+ inset 0 2px 0 0 #007bff,
778
+ inset 0 -2px 0 0 #007bff;
779
+ }
780
+
781
+ .calendar-day.range-middle {
782
+ background-color: rgba(0, 123, 255, 0.14) !important;
783
+ box-shadow:
784
+ inset 0 2px 0 0 #007bff,
785
+ inset 0 -2px 0 0 #007bff;
786
+ }
787
+
788
+ .calendar-day.drag-over-target {
789
+ background-color: rgba(0, 123, 255, 0.1) !important;
790
+ outline: 2px dashed #007bff;
791
+ outline-offset: -3px;
792
+ }
793
+
485
794
  .day-header {
486
795
  padding: 1rem;
487
796
  text-align: center;
@@ -604,6 +913,18 @@ watch([currentMonth, currentYear], () => {
604
913
  padding: 0.1rem 0.1rem;
605
914
  }
606
915
 
916
+ .calendar-event[draggable='true'] {
917
+ cursor: grab;
918
+ }
919
+
920
+ .calendar-event.drag-source {
921
+ opacity: 0.55;
922
+ }
923
+
924
+ .calendar-event[draggable='true']:active {
925
+ cursor: grabbing;
926
+ }
927
+
607
928
  .calendar-event:hover {
608
929
  background: #b3de6e;
609
930
  border-color: #c4db96;
@@ -777,6 +1098,40 @@ watch([currentMonth, currentYear], () => {
777
1098
  background: #374151;
778
1099
  }
779
1100
 
1101
+ .calendar-day.range-single {
1102
+ background-color: rgba(96, 165, 250, 0.28) !important;
1103
+ box-shadow: inset 0 0 0 2px #60a5fa;
1104
+ }
1105
+
1106
+ .calendar-day.range-start {
1107
+ background-color: rgba(96, 165, 250, 0.18) !important;
1108
+ box-shadow:
1109
+ inset 2px 0 0 0 #60a5fa,
1110
+ inset 0 2px 0 0 #60a5fa,
1111
+ inset 0 -2px 0 0 #60a5fa;
1112
+ }
1113
+
1114
+ .calendar-day.range-end {
1115
+ background-color: rgba(96, 165, 250, 0.18) !important;
1116
+ box-shadow:
1117
+ inset -2px 0 0 0 #60a5fa,
1118
+ inset 0 2px 0 0 #60a5fa,
1119
+ inset 0 -2px 0 0 #60a5fa;
1120
+ }
1121
+
1122
+ .calendar-day.range-middle {
1123
+ background-color: rgba(96, 165, 250, 0.18) !important;
1124
+ box-shadow:
1125
+ inset 0 2px 0 0 #60a5fa,
1126
+ inset 0 -2px 0 0 #60a5fa;
1127
+ }
1128
+
1129
+ .calendar-day.drag-over-target {
1130
+ background-color: rgba(96, 165, 250, 0.12) !important;
1131
+ outline: 2px dashed #60a5fa;
1132
+ outline-offset: -3px;
1133
+ }
1134
+
780
1135
  .calendar-day.past {
781
1136
  opacity: 0.6;
782
1137
  }
@@ -27,6 +27,10 @@
27
27
  :current-year="currentYear"
28
28
  @event-click="handleEventClick"
29
29
  @date-click="handleDateClick"
30
+ @date-range-select="handleDateRangeSelect"
31
+ @event-move-request="handleEventMoveRequest"
32
+ @event-moved="handleEventMoved"
33
+ @event-move-rejected="handleEventMoveRejected"
30
34
  @month-change="handleMonthChange"
31
35
  />
32
36
  </Section>
@@ -187,8 +191,51 @@ function handleEventClick(event) {
187
191
  }
188
192
 
189
193
  function handleDateClick(date) {
194
+ // Single-day picks also emit `date-range-select`; keep this for integrations that only listen for `date-click`.
190
195
  console.log('Date clicked:', date)
191
- alert(`Date selected: ${date.toLocaleDateString()}`)
196
+ }
197
+
198
+ function handleDateRangeSelect(start, end) {
199
+ const same = start.getTime() === end.getTime()
200
+ const label = same
201
+ ? start.toLocaleDateString()
202
+ : `${start.toLocaleDateString()} – ${end.toLocaleDateString()}`
203
+ console.log('Date range:', start, end)
204
+ alert(`Range selected: ${label}`)
205
+ }
206
+
207
+ function handleEventMoveRequest({ respond, event, sourceDate, targetDate }) {
208
+ window.setTimeout(() => {
209
+ const ok = window.confirm(
210
+ `Move "${event.title}" to ${targetDate.toLocaleDateString()}? (Cancel = decline)`
211
+ )
212
+ respond(ok)
213
+ }, 50)
214
+ }
215
+
216
+ function applyEventDateShift(event, deltaMs) {
217
+ const i = events.value.findIndex(e => String(e.id) === String(event.id))
218
+ if (i === -1) return
219
+ const ev = events.value[i]
220
+ const next = { ...ev }
221
+ if (next.startDate != null && next.endDate != null) {
222
+ next.startDate = new Date(new Date(next.startDate).getTime() + deltaMs)
223
+ next.endDate = new Date(new Date(next.endDate).getTime() + deltaMs)
224
+ } else if (next.date != null) {
225
+ next.date = new Date(new Date(next.date).getTime() + deltaMs)
226
+ }
227
+ const copy = [...events.value]
228
+ copy[i] = next
229
+ events.value = copy
230
+ }
231
+
232
+ function handleEventMoved({ event, sourceDate, targetDate }) {
233
+ const deltaMs = targetDate.getTime() - sourceDate.getTime()
234
+ applyEventDateShift(event, deltaMs)
235
+ }
236
+
237
+ function handleEventMoveRejected(payload) {
238
+ console.log('event-move-rejected', payload)
192
239
  }
193
240
 
194
241
  // Helper function to update the date picker value