@marco.colia/react-fast-scheduler 2.1.0 → 2.2.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/dist/index.cjs CHANGED
@@ -27,6 +27,7 @@ var React2__namespace = /*#__PURE__*/_interopNamespace(React2);
27
27
  // src/components/defaultRenderAppointment/index.tsx
28
28
  function defaultRenderAppointment({
29
29
  appointment,
30
+ isDropInvalid,
30
31
  onPointerDown,
31
32
  onResizePointerDown,
32
33
  appointmentAppearance,
@@ -36,23 +37,36 @@ function defaultRenderAppointment({
36
37
  const description = typeof rawMaybe.description === "string" ? rawMaybe.description : void 0;
37
38
  const legacyClassName = typeof appointmentBackgroundColor === "string" ? appointmentBackgroundColor : void 0;
38
39
  const backgroundClassName = appointmentAppearance?.className ?? legacyClassName ?? "bg-muted/40 dark:bg-muted/30";
39
- const className = `relative h-full cursor-grab overflow-hidden rounded-md border border-border p-2 pb-5 text-foreground ${backgroundClassName}`;
40
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { onPointerDown, className, children: [
41
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs font-semibold", children: appointment.title }),
42
- description ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-1 text-[10px] font-medium text-muted-foreground", children: description }) : null,
43
- /* @__PURE__ */ jsxRuntime.jsx(
44
- "div",
45
- {
46
- role: "button",
47
- "aria-label": "Resize appointment",
48
- onPointerDown: (event) => {
49
- event.stopPropagation();
50
- onResizePointerDown(event);
51
- },
52
- className: "absolute inset-x-1 bottom-1 h-2 cursor-ns-resize rounded-full bg-border/90 transition-colors hover:bg-border"
53
- }
54
- )
55
- ] });
40
+ const invalidStateClassName = isDropInvalid ? "border-red-500 bg-red-100/80 text-red-950 ring-1 ring-red-500/60 dark:bg-red-950/40 dark:text-red-100" : "";
41
+ const visualStateClassName = appointment.visualState === "ghost" ? "cursor-grab border-dashed opacity-35" : appointment.visualState === "dragging" ? "cursor-grabbing opacity-55 shadow-xl ring-1 ring-border/70" : "cursor-grab";
42
+ const className = `relative h-full overflow-hidden rounded-md border border-border p-2 pb-5 text-foreground ${backgroundClassName} ${invalidStateClassName} ${visualStateClassName}`;
43
+ return /* @__PURE__ */ jsxRuntime.jsxs(
44
+ "div",
45
+ {
46
+ onPointerDown,
47
+ className,
48
+ children: [
49
+ isDropInvalid ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute right-1 top-1 flex h-4 w-4 items-center justify-center rounded-full bg-red-600 text-[10px] font-bold text-white", children: "X" }) : null,
50
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs font-semibold", children: appointment.title }),
51
+ description ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-1 text-[10px] font-medium text-muted-foreground", children: description }) : null,
52
+ /* @__PURE__ */ jsxRuntime.jsx(
53
+ "div",
54
+ {
55
+ role: "button",
56
+ "aria-label": "Resize appointment",
57
+ onPointerDown: (event) => {
58
+ if (appointment.visualState === "ghost") {
59
+ return;
60
+ }
61
+ event.stopPropagation();
62
+ onResizePointerDown(event);
63
+ },
64
+ className: "absolute inset-x-1 bottom-1 h-2 cursor-ns-resize rounded-full bg-border/90 transition-colors hover:bg-border"
65
+ }
66
+ )
67
+ ]
68
+ }
69
+ );
56
70
  }
57
71
 
58
72
  // src/components/constants.ts
@@ -63,8 +77,6 @@ var GUTTER_W = 72;
63
77
  var TOP_PAD = 12;
64
78
  var BOTTOM_PAD = 24;
65
79
  var RESOURCE_MIN_W = 420;
66
-
67
- // src/utils/scheduler-core.utils.ts
68
80
  function isNameLike(resource) {
69
81
  if (!resource || typeof resource !== "object") {
70
82
  return false;
@@ -72,22 +84,6 @@ function isNameLike(resource) {
72
84
  const maybe = resource;
73
85
  return typeof maybe.firstName === "string" || typeof maybe.lastName === "string";
74
86
  }
75
- function toInputDateValue(date) {
76
- const year = date.getFullYear();
77
- const month = String(date.getMonth() + 1).padStart(2, "0");
78
- const day = String(date.getDate()).padStart(2, "0");
79
- return `${year}-${month}-${day}`;
80
- }
81
- function fromInputDateValue(value) {
82
- const [yearRaw, monthRaw, dayRaw] = value.split("-");
83
- const year = Number(yearRaw);
84
- const month = Number(monthRaw);
85
- const day = Number(dayRaw);
86
- if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)) {
87
- return null;
88
- }
89
- return new Date(year, month - 1, day);
90
- }
91
87
  var DEFAULT_APPOINTMENT_COLOR_TOKEN_CLASS_MAP = {
92
88
  slate: "bg-slate-100 dark:bg-slate-900/40",
93
89
  gray: "bg-gray-100 dark:bg-gray-900/40",
@@ -198,26 +194,6 @@ function layoutOverlaps(blocks) {
198
194
  }
199
195
  return output;
200
196
  }
201
- function defaultRenderDatePicker({ selectedDate, onSelectedDateChange }) {
202
- return /* @__PURE__ */ jsxRuntime.jsx(
203
- "input",
204
- {
205
- type: "date",
206
- value: toInputDateValue(selectedDate),
207
- onChange: (event) => {
208
- const nextDate = fromInputDateValue(event.target.value);
209
- if (nextDate) {
210
- onSelectedDateChange(nextDate);
211
- }
212
- },
213
- className: "h-7 rounded-md border border-border bg-card px-2 text-xs text-foreground",
214
- "aria-label": "Select date"
215
- }
216
- );
217
- }
218
- function defaultRenderResourceHeader({ resource }) {
219
- return /* @__PURE__ */ jsxRuntime.jsx("strong", { children: resource.label });
220
- }
221
197
 
222
198
  // src/utils/scheduler-data.utils.ts
223
199
  function buildResourceMap({
@@ -250,17 +226,22 @@ function normalizeAppointments({
250
226
  end,
251
227
  startMin: clamp(minutesFromDayStart(dayStartAbs, start), 0, dayMinutes),
252
228
  endMin: clamp(minutesFromDayStart(dayStartAbs, end), 0, dayMinutes),
253
- title: adapter.getTitle(appointment)
229
+ title: adapter.getTitle(appointment),
230
+ renderKey: String(adapter.getId(appointment)),
231
+ visualState: "normal"
254
232
  };
255
233
  }).filter((appointment) => dayISO(appointment.start) === selectedISO);
256
234
  }
257
235
  function buildAppointmentMap(appointments) {
258
236
  return new Map(appointments.map((appointment) => [appointment.id, appointment]));
259
237
  }
260
- function applyDragToAppointments(appointments, drag, selectedDate, dayStartAbs) {
238
+ function buildPresentationAppointments(appointments, drag, selectedDate, dayStartAbs) {
261
239
  if (drag.kind === "none") {
262
240
  return appointments;
263
241
  }
242
+ if (drag.kind === "move") {
243
+ return appointments;
244
+ }
264
245
  return appointments.map((appointment) => {
265
246
  if (appointment.id !== drag.appointmentId) {
266
247
  return appointment;
@@ -271,7 +252,8 @@ function applyDragToAppointments(appointments, drag, selectedDate, dayStartAbs)
271
252
  startMin: drag.startMin,
272
253
  endMin: drag.endMin,
273
254
  start: dateAtMinute(selectedDate, dayStartAbs, drag.startMin),
274
- end: dateAtMinute(selectedDate, dayStartAbs, drag.endMin)
255
+ end: dateAtMinute(selectedDate, dayStartAbs, drag.endMin),
256
+ visualState: "dragging"
275
257
  };
276
258
  });
277
259
  }
@@ -284,10 +266,22 @@ function buildLaidOutByResource(resources, appointments) {
284
266
  }
285
267
  const result = /* @__PURE__ */ new Map();
286
268
  for (const resource of resources) {
287
- result.set(resource.id, layoutOverlaps(appointmentsByResource.get(resource.id) ?? []));
269
+ result.set(resource.id, buildLaidOutAppointments(appointmentsByResource.get(resource.id) ?? []));
288
270
  }
289
271
  return result;
290
272
  }
273
+ function buildLaidOutAppointments(appointments) {
274
+ return layoutOverlaps(
275
+ appointments.map((appointment) => ({
276
+ ...appointment,
277
+ appointmentId: appointment.id,
278
+ id: appointment.renderKey
279
+ }))
280
+ ).map(({ appointmentId, ...appointment }) => ({
281
+ ...appointment,
282
+ id: appointmentId
283
+ }));
284
+ }
291
285
  function buildAppointmentAppearanceByResource(resources, resourceAppointmentClassMap, getResourceAppointmentAppearance, appointmentColorTokenClassByResource, appointmentBgByResource) {
292
286
  const map = /* @__PURE__ */ new Map();
293
287
  for (const resource of resources) {
@@ -311,8 +305,6 @@ function buildAppointmentAppearanceByResource(resources, resourceAppointmentClas
311
305
  }
312
306
  return map;
313
307
  }
314
-
315
- // src/hooks/useSchedulerBaseData.ts
316
308
  function useSchedulerBaseData({
317
309
  adapter,
318
310
  appointments,
@@ -323,9 +315,18 @@ function useSchedulerBaseData({
323
315
  }) {
324
316
  const dayStartAbs = React2__namespace.useMemo(() => parseHHMM(dayStart), [dayStart]);
325
317
  const dayEndAbs = React2__namespace.useMemo(() => parseHHMM(dayEnd), [dayEnd]);
326
- const dayMinutes = React2__namespace.useMemo(() => Math.max(0, dayEndAbs - dayStartAbs), [dayEndAbs, dayStartAbs]);
327
- const gridHeight = React2__namespace.useMemo(() => TOP_PAD + dayMinutes * PX_PER_MIN + BOTTOM_PAD, [dayMinutes]);
328
- const gridMinWidth = React2__namespace.useMemo(() => GUTTER_W + Math.max(resources.length, 1) * RESOURCE_MIN_W, [resources.length]);
318
+ const dayMinutes = React2__namespace.useMemo(
319
+ () => Math.max(0, dayEndAbs - dayStartAbs),
320
+ [dayEndAbs, dayStartAbs]
321
+ );
322
+ const gridHeight = React2__namespace.useMemo(
323
+ () => TOP_PAD + dayMinutes * PX_PER_MIN + BOTTOM_PAD,
324
+ [dayMinutes]
325
+ );
326
+ const gridMinWidth = React2__namespace.useMemo(
327
+ () => GUTTER_W + Math.max(resources.length, 1) * RESOURCE_MIN_W,
328
+ [resources.length]
329
+ );
329
330
  const selectedISO = React2__namespace.useMemo(() => dayISO(selectedDate), [selectedDate]);
330
331
  const renderAppts = React2__namespace.useMemo(() => {
331
332
  return normalizeAppointments({
@@ -347,7 +348,157 @@ function useSchedulerBaseData({
347
348
  renderAppts
348
349
  };
349
350
  }
350
- function useSchedulerInteractions({
351
+
352
+ // src/hooks/useSchedulerInteractions.helpers.ts
353
+ var DRAG_START_THRESHOLD_PX = 4;
354
+ var updateMovePreviewPosition = (setMovePreview, pointer) => {
355
+ setMovePreview(
356
+ (current) => current ? {
357
+ ...current,
358
+ clientX: pointer.clientX,
359
+ clientY: pointer.clientY
360
+ } : current
361
+ );
362
+ };
363
+ var startPendingDragIfThresholdPassed = (args) => {
364
+ const { dragRef, pendingDragRef, pointer, setDrag, setMovePreview, suppressClickRef } = args;
365
+ const pending = pendingDragRef.current;
366
+ if (!pending || pending.drag.pointerId !== pointer.pointerId) {
367
+ return;
368
+ }
369
+ const deltaX = pointer.clientX - pending.startClientX;
370
+ const deltaY = pointer.clientY - pending.startClientY;
371
+ if (Math.hypot(deltaX, deltaY) < DRAG_START_THRESHOLD_PX) {
372
+ return;
373
+ }
374
+ suppressClickRef.current = true;
375
+ dragRef.current = pending.drag;
376
+ setDrag(pending.drag);
377
+ if (pending.drag.kind === "move") {
378
+ setMovePreview({
379
+ appointment: pending.appointment,
380
+ clientX: pointer.clientX,
381
+ clientY: pointer.clientY,
382
+ width: pending.width,
383
+ height: pending.height,
384
+ offsetX: pending.offsetX,
385
+ offsetY: pending.offsetY
386
+ });
387
+ }
388
+ pendingDragRef.current = null;
389
+ };
390
+ var buildNextMoveDrag = (drag, pointer, dayMinutes, pointerToResourceId, pointerToMin) => {
391
+ const nextResourceId = pointerToResourceId(pointer.clientX, drag.resourceId);
392
+ const pointerMin = pointerToMin(nextResourceId, pointer.clientY);
393
+ const rawStart = pointerMin - drag.offsetMin;
394
+ const nextStartMin = clamp(snap(rawStart), 0, dayMinutes - drag.durationMin);
395
+ const nextEndMin = nextStartMin + drag.durationMin;
396
+ if (nextStartMin === drag.startMin && nextEndMin === drag.endMin && nextResourceId === drag.resourceId) {
397
+ return null;
398
+ }
399
+ return {
400
+ ...drag,
401
+ resourceId: nextResourceId,
402
+ startMin: nextStartMin,
403
+ endMin: nextEndMin
404
+ };
405
+ };
406
+ var buildNextResizeDrag = (drag, pointer, dayMinutes, pointerToMin) => {
407
+ const pointerMin = pointerToMin(drag.resourceId, pointer.clientY);
408
+ const nextEndMin = clamp(snap(pointerMin), drag.startMin + MIN_EVENT_MIN, dayMinutes);
409
+ if (nextEndMin === drag.endMin) {
410
+ return null;
411
+ }
412
+ return {
413
+ ...drag,
414
+ endMin: nextEndMin
415
+ };
416
+ };
417
+ var processPointerFrame = (args) => {
418
+ const {
419
+ dayMinutes,
420
+ dragRef,
421
+ latestPointerRef,
422
+ pendingDragRef,
423
+ pointerToMin,
424
+ pointerToResourceId,
425
+ setDrag,
426
+ setMovePreview,
427
+ suppressClickRef
428
+ } = args;
429
+ const pointer = latestPointerRef.current;
430
+ if (!pointer) {
431
+ return;
432
+ }
433
+ const currentDrag = dragRef.current;
434
+ if (currentDrag.kind === "none" && pendingDragRef.current) {
435
+ startPendingDragIfThresholdPassed({
436
+ pointer,
437
+ pendingDragRef,
438
+ setDrag,
439
+ setMovePreview,
440
+ dragRef,
441
+ suppressClickRef
442
+ });
443
+ return;
444
+ }
445
+ if (currentDrag.kind === "none") {
446
+ return;
447
+ }
448
+ if (currentDrag.kind === "move") {
449
+ updateMovePreviewPosition(setMovePreview, pointer);
450
+ const nextDrag2 = buildNextMoveDrag(
451
+ currentDrag,
452
+ pointer,
453
+ dayMinutes,
454
+ pointerToResourceId,
455
+ pointerToMin
456
+ );
457
+ if (nextDrag2) {
458
+ suppressClickRef.current = true;
459
+ dragRef.current = nextDrag2;
460
+ setDrag(nextDrag2);
461
+ }
462
+ return;
463
+ }
464
+ const nextDrag = buildNextResizeDrag(currentDrag, pointer, dayMinutes, pointerToMin);
465
+ if (nextDrag) {
466
+ suppressClickRef.current = true;
467
+ dragRef.current = nextDrag;
468
+ setDrag(nextDrag);
469
+ }
470
+ };
471
+ var finishDrag = (args) => {
472
+ const {
473
+ dragRef,
474
+ frameRef,
475
+ latestPointerRef,
476
+ pendingDragRef,
477
+ persistDrag,
478
+ setDrag,
479
+ setMovePreview,
480
+ suppressClickRef
481
+ } = args;
482
+ if (frameRef.current !== null) {
483
+ window.cancelAnimationFrame(frameRef.current);
484
+ frameRef.current = null;
485
+ }
486
+ latestPointerRef.current = null;
487
+ pendingDragRef.current = null;
488
+ setMovePreview(null);
489
+ const current = dragRef.current;
490
+ if (current.kind === "none") {
491
+ return;
492
+ }
493
+ setDrag({ kind: "none" });
494
+ dragRef.current = { kind: "none" };
495
+ if (suppressClickRef.current) {
496
+ void persistDrag(current);
497
+ }
498
+ };
499
+
500
+ // src/hooks/useSchedulerInteractions.ts
501
+ var useSchedulerInteractions = ({
351
502
  appointmentMap,
352
503
  dayMinutes,
353
504
  dayStartAbs,
@@ -356,10 +507,27 @@ function useSchedulerInteractions({
356
507
  renderAppts,
357
508
  resources,
358
509
  selectedDate
359
- }) {
510
+ }) => {
360
511
  const [drag, setDrag] = React2__namespace.useState({ kind: "none" });
512
+ const [movePreview, setMovePreview] = React2__namespace.useState(
513
+ null
514
+ );
361
515
  const colRefs = React2__namespace.useRef({});
362
516
  const suppressClickRef = React2__namespace.useRef(false);
517
+ const dragRef = React2__namespace.useRef({ kind: "none" });
518
+ const pendingDragRef = React2__namespace.useRef(null);
519
+ const latestPointerRef = React2__namespace.useRef(null);
520
+ const frameRef = React2__namespace.useRef(null);
521
+ React2__namespace.useEffect(() => {
522
+ dragRef.current = drag;
523
+ }, [drag]);
524
+ React2__namespace.useEffect(() => {
525
+ return () => {
526
+ if (frameRef.current !== null) {
527
+ window.cancelAnimationFrame(frameRef.current);
528
+ }
529
+ };
530
+ }, []);
363
531
  const pointerToMin = React2__namespace.useCallback(
364
532
  (resourceId, clientY) => {
365
533
  const col = colRefs.current[String(resourceId)];
@@ -389,11 +557,21 @@ function useSchedulerInteractions({
389
557
  );
390
558
  const hasOverlap = React2__namespace.useCallback(
391
559
  (appointmentId, resourceId, startMin, endMin) => {
392
- const events = renderAppts.filter((appointment) => appointment.resourceId === resourceId && appointment.id !== appointmentId);
393
- return events.some((appointment) => overlaps(appointment, { startMin, endMin }));
560
+ const events = renderAppts.filter(
561
+ (appointment) => appointment.resourceId === resourceId && appointment.id !== appointmentId
562
+ );
563
+ return events.some(
564
+ (appointment) => overlaps(appointment, { startMin, endMin })
565
+ );
394
566
  },
395
567
  [renderAppts]
396
568
  );
569
+ const isDropInvalid = React2__namespace.useMemo(() => {
570
+ if (drag.kind === "none") {
571
+ return false;
572
+ }
573
+ return hasOverlap(drag.appointmentId, drag.resourceId, drag.startMin, drag.endMin);
574
+ }, [drag, hasOverlap]);
397
575
  const persistDrag = React2__namespace.useCallback(
398
576
  async (state) => {
399
577
  const target = appointmentMap.get(state.appointmentId);
@@ -430,7 +608,14 @@ function useSchedulerInteractions({
430
608
  });
431
609
  }
432
610
  },
433
- [appointmentMap, dayStartAbs, hasOverlap, onAppointmentChange, onPersistMoveResize, selectedDate]
611
+ [
612
+ appointmentMap,
613
+ dayStartAbs,
614
+ hasOverlap,
615
+ onAppointmentChange,
616
+ onPersistMoveResize,
617
+ selectedDate
618
+ ]
434
619
  );
435
620
  const onApptPointerDown = React2__namespace.useCallback(
436
621
  (event, appointment) => {
@@ -439,89 +624,108 @@ function useSchedulerInteractions({
439
624
  }
440
625
  event.stopPropagation();
441
626
  suppressClickRef.current = false;
627
+ const rect = event.currentTarget.getBoundingClientRect();
442
628
  const pointerMin = pointerToMin(appointment.resourceId, event.clientY);
443
- setDrag({
444
- kind: "move",
445
- appointmentId: appointment.id,
446
- pointerId: event.pointerId,
447
- resourceId: appointment.resourceId,
448
- durationMin: Math.max(MIN_EVENT_MIN, appointment.endMin - appointment.startMin),
449
- offsetMin: pointerMin - appointment.startMin,
450
- startMin: appointment.startMin,
451
- endMin: appointment.endMin
452
- });
629
+ pendingDragRef.current = {
630
+ drag: {
631
+ kind: "move",
632
+ appointmentId: appointment.id,
633
+ pointerId: event.pointerId,
634
+ resourceId: appointment.resourceId,
635
+ durationMin: Math.max(MIN_EVENT_MIN, appointment.endMin - appointment.startMin),
636
+ offsetMin: pointerMin - appointment.startMin,
637
+ startMin: appointment.startMin,
638
+ endMin: appointment.endMin
639
+ },
640
+ appointment,
641
+ startClientX: event.clientX,
642
+ startClientY: event.clientY,
643
+ width: rect.width,
644
+ height: rect.height,
645
+ offsetX: event.clientX - rect.left,
646
+ offsetY: event.clientY - rect.top
647
+ };
453
648
  },
454
649
  [pointerToMin]
455
650
  );
456
- const onResizePointerDown = React2__namespace.useCallback((event, appointment) => {
457
- if (event.button !== 0) {
458
- return;
459
- }
460
- event.stopPropagation();
461
- suppressClickRef.current = false;
462
- setDrag({
463
- kind: "resize",
464
- appointmentId: appointment.id,
465
- pointerId: event.pointerId,
466
- resourceId: appointment.resourceId,
467
- startMin: appointment.startMin,
468
- endMin: appointment.endMin
469
- });
470
- }, []);
471
- const onGlobalPointerMove = React2__namespace.useCallback(
472
- (event) => {
473
- if (drag.kind === "none") {
651
+ const onResizePointerDown = React2__namespace.useCallback(
652
+ (event, appointment) => {
653
+ if (event.button !== 0) {
474
654
  return;
475
655
  }
476
- if (drag.kind === "move") {
477
- const nextResourceId = pointerToResourceId(event.clientX, drag.resourceId);
478
- const pointerMin2 = pointerToMin(nextResourceId, event.clientY);
479
- const rawStart = pointerMin2 - drag.offsetMin;
480
- const nextStartMin = clamp(snap(rawStart), 0, dayMinutes - drag.durationMin);
481
- const nextEndMin2 = nextStartMin + drag.durationMin;
482
- if (nextStartMin !== drag.startMin || nextEndMin2 !== drag.endMin || nextResourceId !== drag.resourceId) {
483
- suppressClickRef.current = true;
484
- setDrag({
485
- ...drag,
486
- resourceId: nextResourceId,
487
- startMin: nextStartMin,
488
- endMin: nextEndMin2
489
- });
490
- }
656
+ event.stopPropagation();
657
+ suppressClickRef.current = false;
658
+ pendingDragRef.current = {
659
+ drag: {
660
+ kind: "resize",
661
+ appointmentId: appointment.id,
662
+ pointerId: event.pointerId,
663
+ resourceId: appointment.resourceId,
664
+ startMin: appointment.startMin,
665
+ endMin: appointment.endMin
666
+ },
667
+ appointment,
668
+ startClientX: event.clientX,
669
+ startClientY: event.clientY,
670
+ width: 0,
671
+ height: 0,
672
+ offsetX: 0,
673
+ offsetY: 0
674
+ };
675
+ },
676
+ []
677
+ );
678
+ const onGlobalPointerMove = React2__namespace.useCallback(
679
+ (event) => {
680
+ latestPointerRef.current = {
681
+ clientX: event.clientX,
682
+ clientY: event.clientY,
683
+ pointerId: event.pointerId
684
+ };
685
+ if (frameRef.current !== null) {
491
686
  return;
492
687
  }
493
- const pointerMin = pointerToMin(drag.resourceId, event.clientY);
494
- const nextEndMin = clamp(snap(pointerMin), drag.startMin + MIN_EVENT_MIN, dayMinutes);
495
- if (nextEndMin !== drag.endMin) {
496
- suppressClickRef.current = true;
497
- setDrag({
498
- ...drag,
499
- endMin: nextEndMin
688
+ frameRef.current = window.requestAnimationFrame(() => {
689
+ frameRef.current = null;
690
+ processPointerFrame({
691
+ dayMinutes,
692
+ dragRef,
693
+ latestPointerRef,
694
+ pendingDragRef,
695
+ pointerToMin,
696
+ pointerToResourceId,
697
+ setDrag,
698
+ setMovePreview,
699
+ suppressClickRef
500
700
  });
501
- }
701
+ });
502
702
  },
503
- [dayMinutes, drag, pointerToMin, pointerToResourceId]
703
+ [dayMinutes, pointerToMin, pointerToResourceId]
504
704
  );
505
705
  const onGlobalPointerUp = React2__namespace.useCallback(() => {
506
- if (drag.kind === "none") {
507
- return;
508
- }
509
- const current = drag;
510
- setDrag({ kind: "none" });
511
- if (suppressClickRef.current) {
512
- void persistDrag(current);
513
- }
514
- }, [drag, persistDrag]);
706
+ finishDrag({
707
+ dragRef,
708
+ frameRef,
709
+ latestPointerRef,
710
+ pendingDragRef,
711
+ persistDrag,
712
+ setDrag,
713
+ setMovePreview,
714
+ suppressClickRef
715
+ });
716
+ }, [persistDrag]);
515
717
  return {
516
718
  colRefs,
517
719
  drag,
720
+ isDropInvalid,
721
+ movePreview,
518
722
  onApptPointerDown,
519
723
  onGlobalPointerMove,
520
724
  onGlobalPointerUp,
521
725
  onResizePointerDown,
522
726
  suppressClickRef
523
727
  };
524
- }
728
+ };
525
729
  function useSchedulerPresentationData({
526
730
  appointmentColorTokenClassMap,
527
731
  dayStartAbs,
@@ -535,11 +739,22 @@ function useSchedulerPresentationData({
535
739
  selectedDate
536
740
  }) {
537
741
  const effectiveAppts = React2__namespace.useMemo(() => {
538
- return applyDragToAppointments(renderAppts, drag, selectedDate, dayStartAbs);
742
+ return buildPresentationAppointments(renderAppts, drag, selectedDate, dayStartAbs);
539
743
  }, [dayStartAbs, drag, renderAppts, selectedDate]);
744
+ const baseLaidOutByResource = React2__namespace.useMemo(() => {
745
+ return buildLaidOutByResource(resources, renderAppts);
746
+ }, [renderAppts, resources]);
540
747
  const laidOutByResource = React2__namespace.useMemo(() => {
541
- return buildLaidOutByResource(resources, effectiveAppts);
542
- }, [effectiveAppts, resources]);
748
+ if (drag.kind !== "resize") {
749
+ return baseLaidOutByResource;
750
+ }
751
+ const next = new Map(baseLaidOutByResource);
752
+ const nextAppointmentsForResource = effectiveAppts.filter(
753
+ (appointment) => appointment.resourceId === drag.resourceId
754
+ );
755
+ next.set(drag.resourceId, buildLaidOutAppointments(nextAppointmentsForResource));
756
+ return next;
757
+ }, [baseLaidOutByResource, drag, effectiveAppts]);
543
758
  const appointmentBgByResource = React2__namespace.useMemo(() => {
544
759
  if (!getResourceAppointmentBackground) {
545
760
  return /* @__PURE__ */ new Map();
@@ -591,6 +806,453 @@ function useSchedulerPresentationData({
591
806
  laidOutByResource
592
807
  };
593
808
  }
809
+ function Gutter({ dayMinutes, dayStartAbs, gridHeight }) {
810
+ return /* @__PURE__ */ jsxRuntime.jsx(
811
+ "div",
812
+ {
813
+ className: "rfs-times border-border bg-card",
814
+ style: { width: GUTTER_W, height: gridHeight },
815
+ children: Array.from({ length: Math.floor(dayMinutes / 60) + 1 }).map((_, i) => /* @__PURE__ */ jsxRuntime.jsxs(
816
+ "div",
817
+ {
818
+ className: "rfs-time-tick text-muted-foreground",
819
+ style: { top: TOP_PAD + i * 60 * PX_PER_MIN },
820
+ children: [
821
+ String(Math.floor((dayStartAbs + i * 60) / 60)).padStart(2, "0"),
822
+ ":",
823
+ String((dayStartAbs + i * 60) % 60).padStart(2, "0")
824
+ ]
825
+ },
826
+ `tick-${i}`
827
+ ))
828
+ }
829
+ );
830
+ }
831
+ function defaultRenderResourceHeader({ resource }) {
832
+ return /* @__PURE__ */ jsxRuntime.jsx("strong", { children: resource.label });
833
+ }
834
+ function Header({ resources, renderResourceHeader }) {
835
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rfs-header border-border bg-card", children: [
836
+ /* @__PURE__ */ jsxRuntime.jsx(
837
+ "div",
838
+ {
839
+ className: "rfs-gutter border-border",
840
+ style: { width: GUTTER_W }
841
+ }
842
+ ),
843
+ resources.map((resource) => /* @__PURE__ */ jsxRuntime.jsxs(
844
+ "div",
845
+ {
846
+ className: "rfs-resource-header border-border bg-card text-foreground",
847
+ style: { minWidth: RESOURCE_MIN_W, flex: 1 },
848
+ children: [
849
+ (renderResourceHeader ?? ((r) => defaultRenderResourceHeader({ resource: r })))(
850
+ resource
851
+ ),
852
+ isNameLike(resource) ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rfs-resource-subtitle text-muted-foreground", children: fullName(resource) }) : null
853
+ ]
854
+ },
855
+ String(resource.id)
856
+ ))
857
+ ] });
858
+ }
859
+ var ResourceColInner = ({
860
+ resource,
861
+ colRefs,
862
+ gridHeight,
863
+ dayMinutes,
864
+ appointments,
865
+ appointmentAppearance,
866
+ appointmentBg,
867
+ isDropInvalid,
868
+ renderAppointment,
869
+ onApptPointerDown,
870
+ onResizePointerDown,
871
+ drag,
872
+ suppressClickRef
873
+ }) => {
874
+ return /* @__PURE__ */ jsxRuntime.jsx(
875
+ "div",
876
+ {
877
+ className: "rfs-col border-border bg-card",
878
+ style: { minWidth: RESOURCE_MIN_W, flex: 1 },
879
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
880
+ "div",
881
+ {
882
+ ref: (el) => {
883
+ colRefs.current[String(resource.id)] = el;
884
+ },
885
+ className: "rfs-col-inner bg-card",
886
+ style: { height: gridHeight },
887
+ children: [
888
+ Array.from({ length: Math.floor(dayMinutes / 60) + 1 }).map((_, i) => /* @__PURE__ */ jsxRuntime.jsx(
889
+ "div",
890
+ {
891
+ className: "rfs-hour-line border-border/70",
892
+ style: { top: TOP_PAD + i * 60 * PX_PER_MIN }
893
+ },
894
+ `major-${i}`
895
+ )),
896
+ Array.from({ length: Math.floor(dayMinutes / STEP_MIN) + 1 }).map((_, i) => /* @__PURE__ */ jsxRuntime.jsx(
897
+ "div",
898
+ {
899
+ className: "rfs-slot-line border-border/50",
900
+ style: { top: TOP_PAD + i * STEP_MIN * PX_PER_MIN }
901
+ },
902
+ `minor-${i}`
903
+ )),
904
+ appointments.map((appointment) => {
905
+ const laneWidthPct = 100 / appointment.lanes;
906
+ const top = TOP_PAD + appointment.startMin * PX_PER_MIN;
907
+ const height = Math.max(
908
+ MIN_EVENT_MIN * PX_PER_MIN,
909
+ (appointment.endMin - appointment.startMin) * PX_PER_MIN
910
+ );
911
+ return /* @__PURE__ */ jsxRuntime.jsx(
912
+ "div",
913
+ {
914
+ className: `rfs-card-slot ${appointment.visualState === "ghost" ? "rfs-card-slot--ghost" : ""} ${appointment.visualState === "dragging" ? "rfs-card-slot--dragging" : ""}`,
915
+ style: {
916
+ top,
917
+ left: `${laneWidthPct * appointment.lane}%`,
918
+ width: `${laneWidthPct}%`,
919
+ height
920
+ },
921
+ children: (renderAppointment ?? defaultRenderAppointment)({
922
+ appointment,
923
+ isDropInvalid: isDropInvalid && drag.kind === "resize" && appointment.id === drag.appointmentId,
924
+ onPointerDown: (event) => onApptPointerDown(event, appointment),
925
+ onResizePointerDown: (event) => onResizePointerDown(event, appointment),
926
+ appointmentAppearance,
927
+ appointmentBackgroundColor: appointmentBg,
928
+ drag,
929
+ suppressClickRef
930
+ })
931
+ },
932
+ appointment.renderKey
933
+ );
934
+ })
935
+ ]
936
+ }
937
+ )
938
+ }
939
+ );
940
+ };
941
+
942
+ // src/components/scheduler/parts/ResourceCol.tsx
943
+ var areResourceColsEqual = (prev, next) => [
944
+ prev.resource === next.resource,
945
+ prev.colRefs === next.colRefs,
946
+ prev.gridHeight === next.gridHeight,
947
+ prev.dayMinutes === next.dayMinutes,
948
+ prev.appointments === next.appointments,
949
+ prev.appointmentAppearance === next.appointmentAppearance,
950
+ prev.appointmentBg === next.appointmentBg,
951
+ prev.isDropInvalid === next.isDropInvalid,
952
+ prev.renderAppointment === next.renderAppointment,
953
+ prev.onApptPointerDown === next.onApptPointerDown,
954
+ prev.onResizePointerDown === next.onResizePointerDown,
955
+ prev.drag === next.drag,
956
+ prev.suppressClickRef === next.suppressClickRef
957
+ ].every(Boolean);
958
+ var ResourceCol = React2__namespace.memo(
959
+ ResourceColInner,
960
+ areResourceColsEqual
961
+ );
962
+
963
+ // src/lib/cn.ts
964
+ function cn(...inputs) {
965
+ return inputs.filter(Boolean).join(" ");
966
+ }
967
+ var variantClasses = {
968
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
969
+ outline: "border border-border bg-card text-foreground hover:bg-muted",
970
+ ghost: "text-foreground hover:bg-muted"
971
+ };
972
+ var sizeClasses = {
973
+ default: "h-9 px-4 py-2",
974
+ sm: "h-8 px-3 text-xs",
975
+ icon: "h-8 w-8"
976
+ };
977
+ var Button = React2__namespace.forwardRef(function Button2({ className, variant = "default", size = "default", type = "button", ...props }, ref) {
978
+ return /* @__PURE__ */ jsxRuntime.jsx(
979
+ "button",
980
+ {
981
+ ref,
982
+ type,
983
+ className: cn(
984
+ "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
985
+ variantClasses[variant],
986
+ sizeClasses[size],
987
+ className
988
+ ),
989
+ ...props
990
+ }
991
+ );
992
+ });
993
+ function Calendar({
994
+ selected,
995
+ onSelect
996
+ }) {
997
+ const [month, setMonth] = React2__namespace.useState(selected ?? /* @__PURE__ */ new Date());
998
+ React2__namespace.useEffect(() => {
999
+ if (selected) {
1000
+ setMonth(selected);
1001
+ }
1002
+ }, [selected]);
1003
+ const monthStart = dateFns.startOfMonth(month);
1004
+ const calendarStart = dateFns.startOfWeek(monthStart, { weekStartsOn: 1 });
1005
+ const calendarEnd = dateFns.endOfWeek(dateFns.endOfMonth(month), { weekStartsOn: 1 });
1006
+ const days = [];
1007
+ for (let day = calendarStart; day <= calendarEnd; day = dateFns.addDays(day, 1)) {
1008
+ days.push(day);
1009
+ }
1010
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-72 rounded-md bg-popover", children: [
1011
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-2 flex items-center justify-between", children: [
1012
+ /* @__PURE__ */ jsxRuntime.jsx(
1013
+ Button,
1014
+ {
1015
+ variant: "ghost",
1016
+ size: "icon",
1017
+ "aria-label": "Previous month",
1018
+ onClick: () => setMonth(dateFns.addMonths(month, -1)),
1019
+ children: "<"
1020
+ }
1021
+ ),
1022
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm font-medium", children: dateFns.format(month, "MMMM yyyy") }),
1023
+ /* @__PURE__ */ jsxRuntime.jsx(
1024
+ Button,
1025
+ {
1026
+ variant: "ghost",
1027
+ size: "icon",
1028
+ "aria-label": "Next month",
1029
+ onClick: () => setMonth(dateFns.addMonths(month, 1)),
1030
+ children: ">"
1031
+ }
1032
+ )
1033
+ ] }),
1034
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-7 gap-1 text-center text-xs text-muted-foreground", children: ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"].map((label) => /* @__PURE__ */ jsxRuntime.jsx(
1035
+ "div",
1036
+ {
1037
+ className: "py-1",
1038
+ children: label
1039
+ },
1040
+ label
1041
+ )) }),
1042
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-7 gap-1", children: days.map((day) => {
1043
+ const isSelected = selected ? dateFns.isSameDay(day, selected) : false;
1044
+ return /* @__PURE__ */ jsxRuntime.jsx(
1045
+ Button,
1046
+ {
1047
+ variant: isSelected ? "default" : "ghost",
1048
+ size: "icon",
1049
+ className: cn(
1050
+ "text-xs",
1051
+ !dateFns.isSameMonth(day, month) && "text-muted-foreground opacity-60",
1052
+ dateFns.isToday(day) && !isSelected && "border border-border"
1053
+ ),
1054
+ onClick: () => onSelect?.(day),
1055
+ children: dateFns.format(day, "d")
1056
+ },
1057
+ day.toISOString()
1058
+ );
1059
+ }) })
1060
+ ] });
1061
+ }
1062
+ var PopoverContext = React2__namespace.createContext(null);
1063
+ function usePopoverContext() {
1064
+ const context = React2__namespace.useContext(PopoverContext);
1065
+ if (!context) {
1066
+ throw new Error("Popover components must be used within Popover.");
1067
+ }
1068
+ return context;
1069
+ }
1070
+ function Popover({
1071
+ children,
1072
+ open: controlledOpen,
1073
+ onOpenChange
1074
+ }) {
1075
+ const [uncontrolledOpen, setUncontrolledOpen] = React2__namespace.useState(false);
1076
+ const triggerRef = React2__namespace.useRef(null);
1077
+ const contentRef = React2__namespace.useRef(null);
1078
+ const open = controlledOpen ?? uncontrolledOpen;
1079
+ const setOpen = React2__namespace.useCallback(
1080
+ (value) => {
1081
+ const nextValue = typeof value === "function" ? value(open) : value;
1082
+ if (controlledOpen === void 0) {
1083
+ setUncontrolledOpen(nextValue);
1084
+ }
1085
+ onOpenChange?.(nextValue);
1086
+ },
1087
+ [controlledOpen, onOpenChange, open]
1088
+ );
1089
+ React2__namespace.useEffect(() => {
1090
+ if (!open) {
1091
+ return void 0;
1092
+ }
1093
+ const handlePointerDown = (event) => {
1094
+ const target = event.target;
1095
+ if (triggerRef.current?.contains(target) || contentRef.current?.contains(target)) {
1096
+ return;
1097
+ }
1098
+ setOpen(false);
1099
+ };
1100
+ const handleKeyDown = (event) => {
1101
+ if (event.key === "Escape") {
1102
+ setOpen(false);
1103
+ }
1104
+ };
1105
+ document.addEventListener("mousedown", handlePointerDown);
1106
+ document.addEventListener("keydown", handleKeyDown);
1107
+ return () => {
1108
+ document.removeEventListener("mousedown", handlePointerDown);
1109
+ document.removeEventListener("keydown", handleKeyDown);
1110
+ };
1111
+ }, [open, setOpen]);
1112
+ return /* @__PURE__ */ jsxRuntime.jsx(PopoverContext.Provider, { value: { open, setOpen, triggerRef, contentRef }, children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "relative inline-flex", children }) });
1113
+ }
1114
+ function PopoverTrigger({
1115
+ children,
1116
+ asChild = false
1117
+ }) {
1118
+ const { open, setOpen, triggerRef } = usePopoverContext();
1119
+ if (asChild && React2__namespace.isValidElement(children)) {
1120
+ const child = children;
1121
+ return React2__namespace.cloneElement(child, {
1122
+ ref: triggerRef,
1123
+ "aria-expanded": open,
1124
+ onClick: (event) => {
1125
+ child.props.onClick?.(event);
1126
+ setOpen((current) => !current);
1127
+ }
1128
+ });
1129
+ }
1130
+ return /* @__PURE__ */ jsxRuntime.jsx(
1131
+ "button",
1132
+ {
1133
+ ref: triggerRef,
1134
+ type: "button",
1135
+ "aria-expanded": open,
1136
+ onClick: () => setOpen((current) => !current),
1137
+ children
1138
+ }
1139
+ );
1140
+ }
1141
+ function PopoverContent({
1142
+ children,
1143
+ className,
1144
+ align = "start"
1145
+ }) {
1146
+ const { open, contentRef } = usePopoverContext();
1147
+ if (!open) {
1148
+ return null;
1149
+ }
1150
+ return /* @__PURE__ */ jsxRuntime.jsx(
1151
+ "div",
1152
+ {
1153
+ ref: contentRef,
1154
+ className: cn(
1155
+ "absolute top-full z-50 mt-2 min-w-72 rounded-md border border-border bg-popover p-2 text-popover-foreground shadow-md outline-none",
1156
+ align === "start" && "left-0",
1157
+ align === "center" && "left-1/2 -translate-x-1/2",
1158
+ align === "end" && "right-0",
1159
+ className
1160
+ ),
1161
+ children
1162
+ }
1163
+ );
1164
+ }
1165
+ function defaultRenderDatePicker({
1166
+ selectedDate,
1167
+ onSelectedDateChange
1168
+ }) {
1169
+ return /* @__PURE__ */ jsxRuntime.jsxs(Popover, { children: [
1170
+ /* @__PURE__ */ jsxRuntime.jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(
1171
+ Button,
1172
+ {
1173
+ variant: "outline",
1174
+ size: "sm",
1175
+ className: "min-w-40 justify-start text-left font-normal",
1176
+ children: dateFns.format(selectedDate, "PPP")
1177
+ }
1178
+ ) }),
1179
+ /* @__PURE__ */ jsxRuntime.jsx(
1180
+ PopoverContent,
1181
+ {
1182
+ className: "w-auto p-2",
1183
+ align: "center",
1184
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1185
+ Calendar,
1186
+ {
1187
+ selected: selectedDate,
1188
+ onSelect: onSelectedDateChange
1189
+ }
1190
+ )
1191
+ }
1192
+ )
1193
+ ] });
1194
+ }
1195
+ function defaultRenderToolbar({
1196
+ goToPreviousDay,
1197
+ goToNextDay,
1198
+ defaultDatePicker,
1199
+ prevButtonLabel = "Prev",
1200
+ nextButtonLabel = "Next"
1201
+ }) {
1202
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rfs-toolbar", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 rounded-lg border border-border bg-card p-1 shadow-xs", children: [
1203
+ /* @__PURE__ */ jsxRuntime.jsx(
1204
+ Button,
1205
+ {
1206
+ variant: "outline",
1207
+ size: "sm",
1208
+ onClick: goToPreviousDay,
1209
+ children: prevButtonLabel
1210
+ }
1211
+ ),
1212
+ defaultDatePicker,
1213
+ /* @__PURE__ */ jsxRuntime.jsx(
1214
+ Button,
1215
+ {
1216
+ variant: "outline",
1217
+ size: "sm",
1218
+ onClick: goToNextDay,
1219
+ children: nextButtonLabel
1220
+ }
1221
+ )
1222
+ ] }) });
1223
+ }
1224
+ function Toolbar({
1225
+ selectedDate,
1226
+ onSelectedDateChange,
1227
+ prevButtonLabel,
1228
+ nextButtonLabel,
1229
+ renderToolbar,
1230
+ renderDatePicker
1231
+ }) {
1232
+ const goToPreviousDay = () => onSelectedDateChange(shiftDays(selectedDate, -1));
1233
+ const goToNextDay = () => onSelectedDateChange(shiftDays(selectedDate, 1));
1234
+ const defaultDatePicker = (renderDatePicker ?? defaultRenderDatePicker)({
1235
+ selectedDate,
1236
+ onSelectedDateChange
1237
+ });
1238
+ if (renderToolbar) {
1239
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: renderToolbar({
1240
+ selectedDate,
1241
+ onSelectedDateChange,
1242
+ goToPreviousDay,
1243
+ goToNextDay,
1244
+ defaultDatePicker
1245
+ }) });
1246
+ }
1247
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: defaultRenderToolbar({
1248
+ goToPreviousDay,
1249
+ goToNextDay,
1250
+ defaultDatePicker,
1251
+ prevButtonLabel,
1252
+ nextButtonLabel
1253
+ }) });
1254
+ }
1255
+ var IDLE_DRAG = { kind: "none" };
594
1256
  function Scheduler({
595
1257
  resources,
596
1258
  appointments,
@@ -606,30 +1268,19 @@ function Scheduler({
606
1268
  getResourceAppointmentColorToken,
607
1269
  appointmentColorTokenClassMap,
608
1270
  getResourceAppointmentBackground,
1271
+ renderToolbar,
609
1272
  renderDatePicker,
610
1273
  prevButtonLabel = "Prev",
611
1274
  nextButtonLabel = "Next",
612
1275
  dayStart = "09:00",
613
1276
  dayEnd = "18:00"
614
1277
  }) {
615
- const {
616
- appointmentMap,
617
- dayMinutes,
618
- dayStartAbs,
619
- gridHeight,
620
- gridMinWidth,
621
- renderAppts
622
- } = useSchedulerBaseData({
623
- adapter,
624
- appointments,
625
- dayEnd,
626
- dayStart,
627
- resources,
628
- selectedDate
629
- });
1278
+ const { appointmentMap, dayMinutes, dayStartAbs, gridHeight, gridMinWidth, renderAppts } = useSchedulerBaseData({ adapter, appointments, dayEnd, dayStart, resources, selectedDate });
630
1279
  const {
631
1280
  colRefs,
632
1281
  drag,
1282
+ isDropInvalid,
1283
+ movePreview,
633
1284
  onApptPointerDown,
634
1285
  onGlobalPointerMove,
635
1286
  onGlobalPointerUp,
@@ -645,11 +1296,7 @@ function Scheduler({
645
1296
  resources,
646
1297
  selectedDate
647
1298
  });
648
- const {
649
- appointmentAppearanceByResource,
650
- appointmentBgByResource,
651
- laidOutByResource
652
- } = useSchedulerPresentationData({
1299
+ const { appointmentAppearanceByResource, appointmentBgByResource, laidOutByResource } = useSchedulerPresentationData({
653
1300
  appointmentColorTokenClassMap,
654
1301
  dayStartAbs,
655
1302
  drag,
@@ -661,7 +1308,29 @@ function Scheduler({
661
1308
  resources,
662
1309
  selectedDate
663
1310
  });
664
- const defaultResourceHeaderRenderer = (resource) => defaultRenderResourceHeader({ resource });
1311
+ const moveOverlay = React2__namespace.useMemo(() => {
1312
+ if (drag.kind !== "move" || !movePreview) {
1313
+ return null;
1314
+ }
1315
+ return {
1316
+ appointment: {
1317
+ ...movePreview.appointment,
1318
+ resourceId: drag.resourceId,
1319
+ startMin: drag.startMin,
1320
+ endMin: drag.endMin,
1321
+ visualState: "ghost",
1322
+ renderKey: `${movePreview.appointment.renderKey}-overlay`,
1323
+ lane: 0,
1324
+ lanes: 1
1325
+ },
1326
+ appearance: appointmentAppearanceByResource.get(drag.resourceId),
1327
+ background: appointmentBgByResource.get(drag.resourceId),
1328
+ left: movePreview.clientX - movePreview.offsetX,
1329
+ top: movePreview.clientY - movePreview.offsetY,
1330
+ width: movePreview.width,
1331
+ height: movePreview.height
1332
+ };
1333
+ }, [appointmentAppearanceByResource, appointmentBgByResource, drag, movePreview]);
665
1334
  return /* @__PURE__ */ jsxRuntime.jsxs(
666
1335
  "section",
667
1336
  {
@@ -670,124 +1339,77 @@ function Scheduler({
670
1339
  onPointerUp: onGlobalPointerUp,
671
1340
  onPointerCancel: onGlobalPointerUp,
672
1341
  children: [
673
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rfs-toolbar", children: [
1342
+ /* @__PURE__ */ jsxRuntime.jsx(
1343
+ Toolbar,
1344
+ {
1345
+ selectedDate,
1346
+ onSelectedDateChange,
1347
+ prevButtonLabel,
1348
+ nextButtonLabel,
1349
+ renderToolbar,
1350
+ renderDatePicker
1351
+ }
1352
+ ),
1353
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rfs-shell border-border bg-card", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { minWidth: gridMinWidth }, children: [
674
1354
  /* @__PURE__ */ jsxRuntime.jsx(
675
- "button",
1355
+ Header,
676
1356
  {
677
- className: "rfs-btn border-border bg-card text-foreground hover:bg-muted",
678
- type: "button",
679
- onClick: () => onSelectedDateChange(shiftDays(selectedDate, -1)),
680
- children: prevButtonLabel
1357
+ resources,
1358
+ renderResourceHeader
681
1359
  }
682
1360
  ),
683
- (renderDatePicker ?? defaultRenderDatePicker)({ selectedDate, onSelectedDateChange }),
684
- /* @__PURE__ */ jsxRuntime.jsx(
685
- "button",
686
- {
687
- className: "rfs-btn border-border bg-card text-foreground hover:bg-muted",
688
- type: "button",
689
- onClick: () => onSelectedDateChange(shiftDays(selectedDate, 1)),
690
- children: nextButtonLabel
691
- }
692
- )
693
- ] }),
694
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rfs-shell border-border bg-card", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { minWidth: gridMinWidth }, children: [
695
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rfs-header border-border bg-card", children: [
696
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rfs-gutter border-border", style: { width: GUTTER_W } }),
697
- resources.map((resource) => /* @__PURE__ */ jsxRuntime.jsxs(
698
- "div",
699
- {
700
- className: "rfs-resource-header border-border bg-card text-foreground",
701
- style: { minWidth: RESOURCE_MIN_W, flex: 1 },
702
- children: [
703
- (renderResourceHeader ?? defaultResourceHeaderRenderer)(resource),
704
- isNameLike(resource) ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rfs-resource-subtitle text-muted-foreground", children: fullName(resource) }) : null
705
- ]
706
- },
707
- String(resource.id)
708
- ))
709
- ] }),
710
1361
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rfs-body", children: [
711
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rfs-times border-border bg-card", style: { width: GUTTER_W, height: gridHeight }, children: Array.from({ length: Math.floor(dayMinutes / 60) + 1 }).map((_, i) => /* @__PURE__ */ jsxRuntime.jsxs(
712
- "div",
1362
+ /* @__PURE__ */ jsxRuntime.jsx(
1363
+ Gutter,
713
1364
  {
714
- className: "rfs-time-tick text-muted-foreground",
715
- style: { top: TOP_PAD + i * 60 * PX_PER_MIN },
716
- children: [
717
- String(Math.floor((dayStartAbs + i * 60) / 60)).padStart(2, "0"),
718
- ":",
719
- String((dayStartAbs + i * 60) % 60).padStart(2, "0")
720
- ]
721
- },
722
- `tick-${i}`
723
- )) }),
1365
+ dayMinutes,
1366
+ dayStartAbs,
1367
+ gridHeight
1368
+ }
1369
+ ),
724
1370
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rfs-grid", children: resources.map((resource) => /* @__PURE__ */ jsxRuntime.jsx(
725
- "div",
1371
+ ResourceCol,
726
1372
  {
727
- className: "rfs-col border-border bg-card",
728
- style: { minWidth: RESOURCE_MIN_W, flex: 1 },
729
- children: /* @__PURE__ */ jsxRuntime.jsxs(
730
- "div",
731
- {
732
- ref: (el) => {
733
- colRefs.current[String(resource.id)] = el;
734
- },
735
- className: "rfs-col-inner bg-card",
736
- style: { height: gridHeight },
737
- children: [
738
- Array.from({ length: Math.floor(dayMinutes / 60) + 1 }).map((_, i) => /* @__PURE__ */ jsxRuntime.jsx(
739
- "div",
740
- {
741
- className: "rfs-hour-line border-border/70",
742
- style: { top: TOP_PAD + i * 60 * PX_PER_MIN }
743
- },
744
- `major-${String(resource.id)}-${i}`
745
- )),
746
- Array.from({ length: Math.floor(dayMinutes / STEP_MIN) + 1 }).map((_, i) => /* @__PURE__ */ jsxRuntime.jsx(
747
- "div",
748
- {
749
- className: "rfs-slot-line border-border/50",
750
- style: { top: TOP_PAD + i * STEP_MIN * PX_PER_MIN }
751
- },
752
- `minor-${String(resource.id)}-${i}`
753
- )),
754
- (laidOutByResource.get(resource.id) ?? []).map((appointment) => {
755
- const laneWidthPct = 100 / appointment.lanes;
756
- const top = TOP_PAD + appointment.startMin * PX_PER_MIN;
757
- const height = Math.max(MIN_EVENT_MIN * PX_PER_MIN, (appointment.endMin - appointment.startMin) * PX_PER_MIN);
758
- const appointmentAppearance = appointmentAppearanceByResource.get(resource.id);
759
- const appointmentBackgroundColor = appointmentBgByResource.get(resource.id);
760
- return /* @__PURE__ */ jsxRuntime.jsx(
761
- "div",
762
- {
763
- className: "rfs-card-slot",
764
- style: {
765
- top,
766
- left: `${laneWidthPct * appointment.lane}%`,
767
- width: `${laneWidthPct}%`,
768
- height
769
- },
770
- children: (renderAppointment ?? defaultRenderAppointment)({
771
- appointment,
772
- onPointerDown: (event) => onApptPointerDown(event, appointment),
773
- onResizePointerDown: (event) => onResizePointerDown(event, appointment),
774
- appointmentAppearance,
775
- appointmentBackgroundColor,
776
- drag,
777
- suppressClickRef
778
- })
779
- },
780
- String(appointment.id)
781
- );
782
- })
783
- ]
784
- }
785
- )
1373
+ resource,
1374
+ colRefs,
1375
+ gridHeight,
1376
+ dayMinutes,
1377
+ appointments: laidOutByResource.get(resource.id) ?? [],
1378
+ appointmentAppearance: appointmentAppearanceByResource.get(resource.id),
1379
+ appointmentBg: appointmentBgByResource.get(resource.id),
1380
+ isDropInvalid,
1381
+ renderAppointment,
1382
+ onApptPointerDown,
1383
+ onResizePointerDown,
1384
+ drag: drag.kind === "resize" && drag.resourceId === resource.id ? drag : IDLE_DRAG,
1385
+ suppressClickRef
786
1386
  },
787
1387
  String(resource.id)
788
1388
  )) })
789
1389
  ] })
790
- ] }) })
1390
+ ] }) }),
1391
+ moveOverlay ? /* @__PURE__ */ jsxRuntime.jsx(
1392
+ "div",
1393
+ {
1394
+ className: "rfs-drag-overlay",
1395
+ style: {
1396
+ left: moveOverlay.left,
1397
+ top: moveOverlay.top,
1398
+ width: moveOverlay.width,
1399
+ height: moveOverlay.height
1400
+ },
1401
+ children: (renderAppointment ?? defaultRenderAppointment)({
1402
+ appointment: moveOverlay.appointment,
1403
+ isDropInvalid,
1404
+ onPointerDown: () => void 0,
1405
+ onResizePointerDown: () => void 0,
1406
+ appointmentAppearance: moveOverlay.appearance,
1407
+ appointmentBackgroundColor: moveOverlay.background,
1408
+ drag,
1409
+ suppressClickRef
1410
+ })
1411
+ }
1412
+ ) : null
791
1413
  ]
792
1414
  }
793
1415
  );