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