@marco.colia/react-fast-scheduler 2.1.0 → 2.1.1

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,6 +1,6 @@
1
- import { jsxs, jsx } from 'react/jsx-runtime';
2
- import { format } from 'date-fns';
3
- import * as React2 from 'react';
1
+ import { jsx, jsxs } from 'react/jsx-runtime';
2
+ import { format, startOfMonth, startOfWeek, endOfWeek, endOfMonth, addDays, addMonths, isSameDay, isToday, isSameMonth } from 'date-fns';
3
+ import * as React5 from 'react';
4
4
 
5
5
  // src/components/defaultRenderAppointment/index.tsx
6
6
  function defaultRenderAppointment({
@@ -33,6 +33,206 @@ function defaultRenderAppointment({
33
33
  ] });
34
34
  }
35
35
 
36
+ // src/lib/cn.ts
37
+ function cn(...inputs) {
38
+ return inputs.filter(Boolean).join(" ");
39
+ }
40
+ var variantClasses = {
41
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
42
+ outline: "border border-border bg-card text-foreground hover:bg-muted",
43
+ ghost: "text-foreground hover:bg-muted"
44
+ };
45
+ var sizeClasses = {
46
+ default: "h-9 px-4 py-2",
47
+ sm: "h-8 px-3 text-xs",
48
+ icon: "h-8 w-8"
49
+ };
50
+ var Button = React5.forwardRef(function Button2({ className, variant = "default", size = "default", type = "button", ...props }, ref) {
51
+ return /* @__PURE__ */ jsx(
52
+ "button",
53
+ {
54
+ ref,
55
+ type,
56
+ className: cn(
57
+ "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",
58
+ variantClasses[variant],
59
+ sizeClasses[size],
60
+ className
61
+ ),
62
+ ...props
63
+ }
64
+ );
65
+ });
66
+ function Calendar({
67
+ selected,
68
+ onSelect
69
+ }) {
70
+ const [month, setMonth] = React5.useState(selected ?? /* @__PURE__ */ new Date());
71
+ React5.useEffect(() => {
72
+ if (selected) {
73
+ setMonth(selected);
74
+ }
75
+ }, [selected]);
76
+ const monthStart = startOfMonth(month);
77
+ const calendarStart = startOfWeek(monthStart, { weekStartsOn: 1 });
78
+ const calendarEnd = endOfWeek(endOfMonth(month), { weekStartsOn: 1 });
79
+ const days = [];
80
+ for (let day = calendarStart; day <= calendarEnd; day = addDays(day, 1)) {
81
+ days.push(day);
82
+ }
83
+ return /* @__PURE__ */ jsxs("div", { className: "w-72 rounded-md bg-popover", children: [
84
+ /* @__PURE__ */ jsxs("div", { className: "mb-2 flex items-center justify-between", children: [
85
+ /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon", "aria-label": "Previous month", onClick: () => setMonth(addMonths(month, -1)), children: "<" }),
86
+ /* @__PURE__ */ jsx("div", { className: "text-sm font-medium", children: format(month, "MMMM yyyy") }),
87
+ /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon", "aria-label": "Next month", onClick: () => setMonth(addMonths(month, 1)), children: ">" })
88
+ ] }),
89
+ /* @__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("div", { className: "py-1", children: label }, label)) }),
90
+ /* @__PURE__ */ jsx("div", { className: "grid grid-cols-7 gap-1", children: days.map((day) => {
91
+ const isSelected = selected ? isSameDay(day, selected) : false;
92
+ return /* @__PURE__ */ jsx(
93
+ Button,
94
+ {
95
+ variant: isSelected ? "default" : "ghost",
96
+ size: "icon",
97
+ className: cn(
98
+ "text-xs",
99
+ !isSameMonth(day, month) && "text-muted-foreground opacity-60",
100
+ isToday(day) && !isSelected && "border border-border"
101
+ ),
102
+ onClick: () => onSelect?.(day),
103
+ children: format(day, "d")
104
+ },
105
+ day.toISOString()
106
+ );
107
+ }) })
108
+ ] });
109
+ }
110
+ var PopoverContext = React5.createContext(null);
111
+ function usePopoverContext() {
112
+ const context = React5.useContext(PopoverContext);
113
+ if (!context) {
114
+ throw new Error("Popover components must be used within Popover.");
115
+ }
116
+ return context;
117
+ }
118
+ function Popover({
119
+ children,
120
+ open: controlledOpen,
121
+ onOpenChange
122
+ }) {
123
+ const [uncontrolledOpen, setUncontrolledOpen] = React5.useState(false);
124
+ const triggerRef = React5.useRef(null);
125
+ const contentRef = React5.useRef(null);
126
+ const open = controlledOpen ?? uncontrolledOpen;
127
+ const setOpen = React5.useCallback(
128
+ (value) => {
129
+ const nextValue = typeof value === "function" ? value(open) : value;
130
+ if (controlledOpen === void 0) {
131
+ setUncontrolledOpen(nextValue);
132
+ }
133
+ onOpenChange?.(nextValue);
134
+ },
135
+ [controlledOpen, onOpenChange, open]
136
+ );
137
+ React5.useEffect(() => {
138
+ if (!open) {
139
+ return void 0;
140
+ }
141
+ const handlePointerDown = (event) => {
142
+ const target = event.target;
143
+ if (triggerRef.current?.contains(target) || contentRef.current?.contains(target)) {
144
+ return;
145
+ }
146
+ setOpen(false);
147
+ };
148
+ const handleKeyDown = (event) => {
149
+ if (event.key === "Escape") {
150
+ setOpen(false);
151
+ }
152
+ };
153
+ document.addEventListener("mousedown", handlePointerDown);
154
+ document.addEventListener("keydown", handleKeyDown);
155
+ return () => {
156
+ document.removeEventListener("mousedown", handlePointerDown);
157
+ document.removeEventListener("keydown", handleKeyDown);
158
+ };
159
+ }, [open, setOpen]);
160
+ return /* @__PURE__ */ jsx(PopoverContext.Provider, { value: { open, setOpen, triggerRef, contentRef }, children: /* @__PURE__ */ jsx("div", { className: "relative inline-flex", children }) });
161
+ }
162
+ function PopoverTrigger({
163
+ children,
164
+ asChild = false
165
+ }) {
166
+ const { open, setOpen, triggerRef } = usePopoverContext();
167
+ if (asChild && React5.isValidElement(children)) {
168
+ const child = children;
169
+ return React5.cloneElement(child, {
170
+ ref: triggerRef,
171
+ "aria-expanded": open,
172
+ onClick: (event) => {
173
+ child.props.onClick?.(event);
174
+ setOpen((current) => !current);
175
+ }
176
+ });
177
+ }
178
+ return /* @__PURE__ */ jsx(
179
+ "button",
180
+ {
181
+ ref: triggerRef,
182
+ type: "button",
183
+ "aria-expanded": open,
184
+ onClick: () => setOpen((current) => !current),
185
+ children
186
+ }
187
+ );
188
+ }
189
+ function PopoverContent({
190
+ children,
191
+ className,
192
+ align = "start"
193
+ }) {
194
+ const { open, contentRef } = usePopoverContext();
195
+ if (!open) {
196
+ return null;
197
+ }
198
+ return /* @__PURE__ */ jsx(
199
+ "div",
200
+ {
201
+ ref: contentRef,
202
+ className: cn(
203
+ "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",
204
+ align === "start" && "left-0",
205
+ align === "center" && "left-1/2 -translate-x-1/2",
206
+ align === "end" && "right-0",
207
+ className
208
+ ),
209
+ children
210
+ }
211
+ );
212
+ }
213
+ function defaultRenderDatePicker({ selectedDate, onSelectedDateChange }) {
214
+ return /* @__PURE__ */ jsxs(Popover, { children: [
215
+ /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", className: "min-w-40 justify-start text-left font-normal", children: format(selectedDate, "PPP") }) }),
216
+ /* @__PURE__ */ jsx(PopoverContent, { className: "w-auto p-2", align: "center", children: /* @__PURE__ */ jsx(Calendar, { selected: selectedDate, onSelect: onSelectedDateChange }) })
217
+ ] });
218
+ }
219
+ function defaultRenderResourceHeader({ resource }) {
220
+ return /* @__PURE__ */ jsx("strong", { children: resource.label });
221
+ }
222
+ function defaultRenderToolbar({
223
+ goToPreviousDay,
224
+ goToNextDay,
225
+ defaultDatePicker,
226
+ prevButtonLabel = "Prev",
227
+ nextButtonLabel = "Next"
228
+ }) {
229
+ 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: [
230
+ /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: goToPreviousDay, children: prevButtonLabel }),
231
+ defaultDatePicker,
232
+ /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: goToNextDay, children: nextButtonLabel })
233
+ ] }) });
234
+ }
235
+
36
236
  // src/components/constants.ts
37
237
  var STEP_MIN = 15;
38
238
  var MIN_EVENT_MIN = 15;
@@ -41,8 +241,6 @@ var GUTTER_W = 72;
41
241
  var TOP_PAD = 12;
42
242
  var BOTTOM_PAD = 24;
43
243
  var RESOURCE_MIN_W = 420;
44
-
45
- // src/utils/scheduler-core.utils.ts
46
244
  function isNameLike(resource) {
47
245
  if (!resource || typeof resource !== "object") {
48
246
  return false;
@@ -50,22 +248,6 @@ function isNameLike(resource) {
50
248
  const maybe = resource;
51
249
  return typeof maybe.firstName === "string" || typeof maybe.lastName === "string";
52
250
  }
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
251
  var DEFAULT_APPOINTMENT_COLOR_TOKEN_CLASS_MAP = {
70
252
  slate: "bg-slate-100 dark:bg-slate-900/40",
71
253
  gray: "bg-gray-100 dark:bg-gray-900/40",
@@ -176,26 +358,6 @@ function layoutOverlaps(blocks) {
176
358
  }
177
359
  return output;
178
360
  }
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
361
 
200
362
  // src/utils/scheduler-data.utils.ts
201
363
  function buildResourceMap({
@@ -299,13 +461,13 @@ function useSchedulerBaseData({
299
461
  resources,
300
462
  selectedDate
301
463
  }) {
302
- const dayStartAbs = React2.useMemo(() => parseHHMM(dayStart), [dayStart]);
303
- 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]);
307
- const selectedISO = React2.useMemo(() => dayISO(selectedDate), [selectedDate]);
308
- const renderAppts = React2.useMemo(() => {
464
+ const dayStartAbs = React5.useMemo(() => parseHHMM(dayStart), [dayStart]);
465
+ const dayEndAbs = React5.useMemo(() => parseHHMM(dayEnd), [dayEnd]);
466
+ const dayMinutes = React5.useMemo(() => Math.max(0, dayEndAbs - dayStartAbs), [dayEndAbs, dayStartAbs]);
467
+ const gridHeight = React5.useMemo(() => TOP_PAD + dayMinutes * PX_PER_MIN + BOTTOM_PAD, [dayMinutes]);
468
+ const gridMinWidth = React5.useMemo(() => GUTTER_W + Math.max(resources.length, 1) * RESOURCE_MIN_W, [resources.length]);
469
+ const selectedISO = React5.useMemo(() => dayISO(selectedDate), [selectedDate]);
470
+ const renderAppts = React5.useMemo(() => {
309
471
  return normalizeAppointments({
310
472
  adapter,
311
473
  appointments,
@@ -314,7 +476,7 @@ function useSchedulerBaseData({
314
476
  selectedISO
315
477
  });
316
478
  }, [adapter, appointments, dayMinutes, dayStartAbs, selectedISO]);
317
- const appointmentMap = React2.useMemo(() => buildAppointmentMap(renderAppts), [renderAppts]);
479
+ const appointmentMap = React5.useMemo(() => buildAppointmentMap(renderAppts), [renderAppts]);
318
480
  return {
319
481
  appointmentMap,
320
482
  dayEndAbs,
@@ -335,10 +497,10 @@ function useSchedulerInteractions({
335
497
  resources,
336
498
  selectedDate
337
499
  }) {
338
- const [drag, setDrag] = React2.useState({ kind: "none" });
339
- const colRefs = React2.useRef({});
340
- const suppressClickRef = React2.useRef(false);
341
- const pointerToMin = React2.useCallback(
500
+ const [drag, setDrag] = React5.useState({ kind: "none" });
501
+ const colRefs = React5.useRef({});
502
+ const suppressClickRef = React5.useRef(false);
503
+ const pointerToMin = React5.useCallback(
342
504
  (resourceId, clientY) => {
343
505
  const col = colRefs.current[String(resourceId)];
344
506
  if (!col) {
@@ -349,7 +511,7 @@ function useSchedulerInteractions({
349
511
  },
350
512
  [dayMinutes]
351
513
  );
352
- const pointerToResourceId = React2.useCallback(
514
+ const pointerToResourceId = React5.useCallback(
353
515
  (clientX, fallbackResourceId) => {
354
516
  for (const resource of resources) {
355
517
  const col = colRefs.current[String(resource.id)];
@@ -365,14 +527,14 @@ function useSchedulerInteractions({
365
527
  },
366
528
  [resources]
367
529
  );
368
- const hasOverlap = React2.useCallback(
530
+ const hasOverlap = React5.useCallback(
369
531
  (appointmentId, resourceId, startMin, endMin) => {
370
532
  const events = renderAppts.filter((appointment) => appointment.resourceId === resourceId && appointment.id !== appointmentId);
371
533
  return events.some((appointment) => overlaps(appointment, { startMin, endMin }));
372
534
  },
373
535
  [renderAppts]
374
536
  );
375
- const persistDrag = React2.useCallback(
537
+ const persistDrag = React5.useCallback(
376
538
  async (state) => {
377
539
  const target = appointmentMap.get(state.appointmentId);
378
540
  if (!target) {
@@ -410,7 +572,7 @@ function useSchedulerInteractions({
410
572
  },
411
573
  [appointmentMap, dayStartAbs, hasOverlap, onAppointmentChange, onPersistMoveResize, selectedDate]
412
574
  );
413
- const onApptPointerDown = React2.useCallback(
575
+ const onApptPointerDown = React5.useCallback(
414
576
  (event, appointment) => {
415
577
  if (event.button !== 0) {
416
578
  return;
@@ -431,7 +593,7 @@ function useSchedulerInteractions({
431
593
  },
432
594
  [pointerToMin]
433
595
  );
434
- const onResizePointerDown = React2.useCallback((event, appointment) => {
596
+ const onResizePointerDown = React5.useCallback((event, appointment) => {
435
597
  if (event.button !== 0) {
436
598
  return;
437
599
  }
@@ -446,7 +608,7 @@ function useSchedulerInteractions({
446
608
  endMin: appointment.endMin
447
609
  });
448
610
  }, []);
449
- const onGlobalPointerMove = React2.useCallback(
611
+ const onGlobalPointerMove = React5.useCallback(
450
612
  (event) => {
451
613
  if (drag.kind === "none") {
452
614
  return;
@@ -480,7 +642,7 @@ function useSchedulerInteractions({
480
642
  },
481
643
  [dayMinutes, drag, pointerToMin, pointerToResourceId]
482
644
  );
483
- const onGlobalPointerUp = React2.useCallback(() => {
645
+ const onGlobalPointerUp = React5.useCallback(() => {
484
646
  if (drag.kind === "none") {
485
647
  return;
486
648
  }
@@ -512,13 +674,13 @@ function useSchedulerPresentationData({
512
674
  resources,
513
675
  selectedDate
514
676
  }) {
515
- const effectiveAppts = React2.useMemo(() => {
677
+ const effectiveAppts = React5.useMemo(() => {
516
678
  return applyDragToAppointments(renderAppts, drag, selectedDate, dayStartAbs);
517
679
  }, [dayStartAbs, drag, renderAppts, selectedDate]);
518
- const laidOutByResource = React2.useMemo(() => {
680
+ const laidOutByResource = React5.useMemo(() => {
519
681
  return buildLaidOutByResource(resources, effectiveAppts);
520
682
  }, [effectiveAppts, resources]);
521
- const appointmentBgByResource = React2.useMemo(() => {
683
+ const appointmentBgByResource = React5.useMemo(() => {
522
684
  if (!getResourceAppointmentBackground) {
523
685
  return /* @__PURE__ */ new Map();
524
686
  }
@@ -529,7 +691,7 @@ function useSchedulerPresentationData({
529
691
  toKey: (resource) => resource.id
530
692
  });
531
693
  }, [getResourceAppointmentBackground, resources]);
532
- const appointmentColorTokenClassByResource = React2.useMemo(() => {
694
+ const appointmentColorTokenClassByResource = React5.useMemo(() => {
533
695
  if (!getResourceAppointmentColorToken) {
534
696
  return /* @__PURE__ */ new Map();
535
697
  }
@@ -547,7 +709,7 @@ function useSchedulerPresentationData({
547
709
  toKey: (resource) => resource.id
548
710
  });
549
711
  }, [appointmentColorTokenClassMap, getResourceAppointmentColorToken, resources]);
550
- const appointmentAppearanceByResource = React2.useMemo(() => {
712
+ const appointmentAppearanceByResource = React5.useMemo(() => {
551
713
  return buildAppointmentAppearanceByResource(
552
714
  resources,
553
715
  resourceAppointmentClassMap,
@@ -584,6 +746,7 @@ function Scheduler({
584
746
  getResourceAppointmentColorToken,
585
747
  appointmentColorTokenClassMap,
586
748
  getResourceAppointmentBackground,
749
+ renderToolbar,
587
750
  renderDatePicker,
588
751
  prevButtonLabel = "Prev",
589
752
  nextButtonLabel = "Next",
@@ -640,6 +803,22 @@ function Scheduler({
640
803
  selectedDate
641
804
  });
642
805
  const defaultResourceHeaderRenderer = (resource) => defaultRenderResourceHeader({ resource });
806
+ const goToPreviousDay = () => onSelectedDateChange(shiftDays(selectedDate, -1));
807
+ const goToNextDay = () => onSelectedDateChange(shiftDays(selectedDate, 1));
808
+ const defaultDatePicker = (renderDatePicker ?? defaultRenderDatePicker)({ selectedDate, onSelectedDateChange });
809
+ const toolbar = renderToolbar ? renderToolbar({
810
+ selectedDate,
811
+ onSelectedDateChange,
812
+ goToPreviousDay,
813
+ goToNextDay,
814
+ defaultDatePicker
815
+ }) : defaultRenderToolbar({
816
+ goToPreviousDay,
817
+ goToNextDay,
818
+ defaultDatePicker,
819
+ prevButtonLabel,
820
+ nextButtonLabel
821
+ });
643
822
  return /* @__PURE__ */ jsxs(
644
823
  "section",
645
824
  {
@@ -648,27 +827,7 @@ function Scheduler({
648
827
  onPointerUp: onGlobalPointerUp,
649
828
  onPointerCancel: onGlobalPointerUp,
650
829
  children: [
651
- /* @__PURE__ */ jsxs("div", { className: "rfs-toolbar", children: [
652
- /* @__PURE__ */ jsx(
653
- "button",
654
- {
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
659
- }
660
- ),
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
- ] }),
830
+ toolbar,
672
831
  /* @__PURE__ */ jsx("div", { className: "rfs-shell border-border bg-card", children: /* @__PURE__ */ jsxs("div", { style: { minWidth: gridMinWidth }, children: [
673
832
  /* @__PURE__ */ jsxs("div", { className: "rfs-header border-border bg-card", children: [
674
833
  /* @__PURE__ */ jsx("div", { className: "rfs-gutter border-border", style: { width: GUTTER_W } }),