@marco.colia/react-fast-scheduler 2.0.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,8 +1,237 @@
1
- import * as React from 'react';
2
- import { format } from 'date-fns';
3
- import { jsxs, jsx } from 'react/jsx-runtime';
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
- // src/components/Scheduler.tsx
5
+ // src/components/defaultRenderAppointment/index.tsx
6
+ function defaultRenderAppointment({
7
+ appointment,
8
+ onPointerDown,
9
+ onResizePointerDown,
10
+ appointmentAppearance,
11
+ appointmentBackgroundColor
12
+ }) {
13
+ const rawMaybe = appointment.raw;
14
+ const description = typeof rawMaybe.description === "string" ? rawMaybe.description : void 0;
15
+ const legacyClassName = typeof appointmentBackgroundColor === "string" ? appointmentBackgroundColor : void 0;
16
+ 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
+ ] });
34
+ }
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
+ }
6
235
 
7
236
  // src/components/constants.ts
8
237
  var STEP_MIN = 15;
@@ -12,26 +241,57 @@ var GUTTER_W = 72;
12
241
  var TOP_PAD = 12;
13
242
  var BOTTOM_PAD = 24;
14
243
  var RESOURCE_MIN_W = 420;
15
- var clamp = (n, min, max) => {
16
- return Math.max(min, Math.min(max, n));
244
+ function isNameLike(resource) {
245
+ if (!resource || typeof resource !== "object") {
246
+ return false;
247
+ }
248
+ const maybe = resource;
249
+ return typeof maybe.firstName === "string" || typeof maybe.lastName === "string";
250
+ }
251
+ var DEFAULT_APPOINTMENT_COLOR_TOKEN_CLASS_MAP = {
252
+ slate: "bg-slate-100 dark:bg-slate-900/40",
253
+ gray: "bg-gray-100 dark:bg-gray-900/40",
254
+ zinc: "bg-zinc-100 dark:bg-zinc-900/40",
255
+ neutral: "bg-neutral-100 dark:bg-neutral-900/40",
256
+ stone: "bg-stone-100 dark:bg-stone-900/40",
257
+ red: "bg-red-100 dark:bg-red-950/40",
258
+ orange: "bg-orange-100 dark:bg-orange-950/40",
259
+ amber: "bg-amber-100 dark:bg-amber-950/40",
260
+ yellow: "bg-yellow-100 dark:bg-yellow-950/40",
261
+ lime: "bg-lime-100 dark:bg-lime-950/40",
262
+ green: "bg-green-100 dark:bg-green-950/40",
263
+ emerald: "bg-emerald-100 dark:bg-emerald-950/40",
264
+ teal: "bg-teal-100 dark:bg-teal-950/40",
265
+ cyan: "bg-cyan-100 dark:bg-cyan-950/40",
266
+ sky: "bg-sky-100 dark:bg-sky-950/40",
267
+ blue: "bg-blue-100 dark:bg-blue-950/40",
268
+ indigo: "bg-indigo-100 dark:bg-indigo-950/40",
269
+ violet: "bg-violet-100 dark:bg-violet-950/40",
270
+ purple: "bg-purple-100 dark:bg-purple-950/40",
271
+ fuchsia: "bg-fuchsia-100 dark:bg-fuchsia-950/40",
272
+ pink: "bg-pink-100 dark:bg-pink-950/40",
273
+ rose: "bg-rose-100 dark:bg-rose-950/40"
17
274
  };
18
- var snap = (mins, stepMin = STEP_MIN) => {
275
+ function clamp(n, min, max) {
276
+ return Math.max(min, Math.min(max, n));
277
+ }
278
+ function snap(mins, stepMin = STEP_MIN) {
19
279
  return Math.round(mins / stepMin) * stepMin;
20
- };
21
- var parseHHMM = (hhmm) => {
280
+ }
281
+ function parseHHMM(hhmm) {
22
282
  const [h, m] = hhmm.split(":").map(Number);
23
283
  return h * 60 + m;
24
- };
25
- var fullName = (u) => {
26
- return `${u.firstName ?? ""} ${u.lastName ?? ""}`.trim();
27
- };
28
- var dayISO = (d) => {
29
- return format(d, "yyyy-MM-dd");
30
- };
31
- var shiftDays = (d, delta) => {
32
- return new Date(d.getFullYear(), d.getMonth(), d.getDate() + delta);
33
- };
34
- var dateAtMinute = (selectedDate, dayStartAbs, minuteFromStart) => {
284
+ }
285
+ function fullName(resource) {
286
+ return `${resource.firstName ?? ""} ${resource.lastName ?? ""}`.trim();
287
+ }
288
+ function dayISO(date) {
289
+ return format(date, "yyyy-MM-dd");
290
+ }
291
+ function shiftDays(date, delta) {
292
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate() + delta);
293
+ }
294
+ function dateAtMinute(selectedDate, dayStartAbs, minuteFromStart) {
35
295
  const base = new Date(
36
296
  selectedDate.getFullYear(),
37
297
  selectedDate.getMonth(),
@@ -46,311 +306,294 @@ var dateAtMinute = (selectedDate, dayStartAbs, minuteFromStart) => {
46
306
  const mm = abs % 60;
47
307
  base.setHours(hh, mm, 0, 0);
48
308
  return base;
49
- };
50
- var minutesFromDayStart = (dayStartAbs, dt) => {
51
- return dt.getHours() * 60 + dt.getMinutes() - dayStartAbs;
52
- };
53
- var overlaps = (a, b) => {
309
+ }
310
+ function minutesFromDayStart(dayStartAbs, date) {
311
+ return date.getHours() * 60 + date.getMinutes() - dayStartAbs;
312
+ }
313
+ function overlaps(a, b) {
54
314
  return a.startMin < b.endMin && b.startMin < a.endMin;
55
- };
56
- var layoutOverlaps = (blocks) => {
315
+ }
316
+ function layoutOverlaps(blocks) {
57
317
  const sorted = [...blocks].sort((a, b) => a.startMin - b.startMin || a.endMin - b.endMin);
58
318
  const groups = [];
59
319
  let active = [];
60
320
  let current = [];
61
- for (const e of sorted) {
62
- active = active.filter((x) => x.endMin > e.startMin);
63
- const conflicts = active.some((x) => overlaps(x, e));
321
+ for (const block of sorted) {
322
+ active = active.filter((item) => item.endMin > block.startMin);
323
+ const conflicts = active.some((item) => overlaps(item, block));
64
324
  if (!conflicts && current.length > 0) {
65
325
  groups.push(current);
66
326
  current = [];
67
327
  }
68
- current.push(e);
69
- active.push(e);
328
+ current.push(block);
329
+ active.push(block);
70
330
  }
71
331
  if (current.length) {
72
332
  groups.push(current);
73
333
  }
74
- const out = [];
334
+ const output = [];
75
335
  for (const group of groups) {
76
- const g = [...group].sort((a, b) => a.startMin - b.startMin || a.endMin - b.endMin);
336
+ const sortedGroup = [...group].sort((a, b) => a.startMin - b.startMin || a.endMin - b.endMin);
77
337
  const laneEnd = [];
78
338
  const laneOf = /* @__PURE__ */ new Map();
79
- for (const e of g) {
339
+ for (const block of sortedGroup) {
80
340
  let placed = false;
81
341
  for (let i = 0; i < laneEnd.length; i++) {
82
- if (laneEnd[i] <= e.startMin) {
83
- laneEnd[i] = e.endMin;
84
- laneOf.set(e.id, i);
342
+ if (laneEnd[i] <= block.startMin) {
343
+ laneEnd[i] = block.endMin;
344
+ laneOf.set(block.id, i);
85
345
  placed = true;
86
346
  break;
87
347
  }
88
348
  }
89
349
  if (!placed) {
90
- laneEnd.push(e.endMin);
91
- laneOf.set(e.id, laneEnd.length - 1);
350
+ laneEnd.push(block.endMin);
351
+ laneOf.set(block.id, laneEnd.length - 1);
92
352
  }
93
353
  }
94
354
  const lanes = laneEnd.length;
95
- for (const e of g) {
96
- out.push({ ...e, lane: laneOf.get(e.id) ?? 0, lanes });
355
+ for (const block of sortedGroup) {
356
+ output.push({ ...block, lane: laneOf.get(block.id) ?? 0, lanes });
97
357
  }
98
358
  }
99
- return out;
100
- };
101
- function isNameLike(resource) {
102
- if (!resource || typeof resource !== "object") {
103
- return false;
104
- }
105
- const maybe = resource;
106
- return typeof maybe.firstName === "string" || typeof maybe.lastName === "string";
359
+ return output;
107
360
  }
108
- function toInputDateValue(date) {
109
- const year = date.getFullYear();
110
- const month = String(date.getMonth() + 1).padStart(2, "0");
111
- const day = String(date.getDate()).padStart(2, "0");
112
- return `${year}-${month}-${day}`;
113
- }
114
- function fromInputDateValue(value) {
115
- const [yearRaw, monthRaw, dayRaw] = value.split("-");
116
- const year = Number(yearRaw);
117
- const month = Number(monthRaw);
118
- const day = Number(dayRaw);
119
- if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)) {
120
- return null;
361
+
362
+ // src/utils/scheduler-data.utils.ts
363
+ function buildResourceMap({
364
+ fallback,
365
+ getValue,
366
+ resources,
367
+ toKey
368
+ }) {
369
+ const map = /* @__PURE__ */ new Map();
370
+ for (const resource of resources) {
371
+ map.set(toKey(resource), getValue(resource) ?? fallback);
121
372
  }
122
- return new Date(year, month - 1, day);
373
+ return map;
123
374
  }
124
- var DEFAULT_APPOINTMENT_COLOR_TOKEN_CLASS_MAP = {
125
- slate: "bg-slate-100 dark:bg-slate-900/40",
126
- gray: "bg-gray-100 dark:bg-gray-900/40",
127
- zinc: "bg-zinc-100 dark:bg-zinc-900/40",
128
- neutral: "bg-neutral-100 dark:bg-neutral-900/40",
129
- stone: "bg-stone-100 dark:bg-stone-900/40",
130
- red: "bg-red-100 dark:bg-red-950/40",
131
- orange: "bg-orange-100 dark:bg-orange-950/40",
132
- amber: "bg-amber-100 dark:bg-amber-950/40",
133
- yellow: "bg-yellow-100 dark:bg-yellow-950/40",
134
- lime: "bg-lime-100 dark:bg-lime-950/40",
135
- green: "bg-green-100 dark:bg-green-950/40",
136
- emerald: "bg-emerald-100 dark:bg-emerald-950/40",
137
- teal: "bg-teal-100 dark:bg-teal-950/40",
138
- cyan: "bg-cyan-100 dark:bg-cyan-950/40",
139
- sky: "bg-sky-100 dark:bg-sky-950/40",
140
- blue: "bg-blue-100 dark:bg-blue-950/40",
141
- indigo: "bg-indigo-100 dark:bg-indigo-950/40",
142
- violet: "bg-violet-100 dark:bg-violet-950/40",
143
- purple: "bg-purple-100 dark:bg-purple-950/40",
144
- fuchsia: "bg-fuchsia-100 dark:bg-fuchsia-950/40",
145
- pink: "bg-pink-100 dark:bg-pink-950/40",
146
- rose: "bg-rose-100 dark:bg-rose-950/40"
147
- };
148
- function Scheduler({
149
- resources,
150
- appointments,
151
- selectedDate,
152
- onSelectedDateChange,
375
+ function normalizeAppointments({
153
376
  adapter,
154
- onPersistMoveResize,
155
- onAppointmentChange,
156
- renderResourceHeader,
157
- renderAppointment,
158
- resourceAppointmentClassMap,
159
- getResourceAppointmentAppearance,
160
- getResourceAppointmentColorToken,
161
- appointmentColorTokenClassMap,
162
- getResourceAppointmentBackground,
163
- renderDatePicker,
164
- prevButtonLabel = "Prev",
165
- nextButtonLabel = "Next",
166
- dayStart = "09:00",
167
- dayEnd = "18:00"
377
+ appointments,
378
+ dayMinutes,
379
+ dayStartAbs,
380
+ selectedISO
168
381
  }) {
169
- const [drag, setDrag] = React.useState({ kind: "none" });
170
- const colRefs = React.useRef({});
171
- const suppressClickRef = React.useRef(false);
172
- const dayStartAbs = parseHHMM(dayStart);
173
- const dayEndAbs = parseHHMM(dayEnd);
174
- const dayMinutes = Math.max(0, dayEndAbs - dayStartAbs);
175
- const gridHeight = TOP_PAD + dayMinutes * PX_PER_MIN + BOTTOM_PAD;
176
- const gridMinWidth = GUTTER_W + Math.max(resources.length, 1) * RESOURCE_MIN_W;
177
- const selectedISO = React.useMemo(() => dayISO(selectedDate), [selectedDate]);
178
- const renderAppts = React.useMemo(() => {
179
- return appointments.map((appointment) => {
180
- const start = new Date(adapter.getStart(appointment));
181
- const end = new Date(adapter.getEnd(appointment));
182
- return {
183
- raw: appointment,
184
- id: adapter.getId(appointment),
185
- resourceId: adapter.getResourceId(appointment),
186
- start,
187
- end,
188
- startMin: clamp(minutesFromDayStart(dayStartAbs, start), 0, dayMinutes),
189
- endMin: clamp(minutesFromDayStart(dayStartAbs, end), 0, dayMinutes),
190
- title: adapter.getTitle(appointment)
191
- };
192
- }).filter((appointment) => dayISO(appointment.start) === selectedISO);
193
- }, [appointments, adapter, dayStartAbs, dayMinutes, selectedISO]);
194
- const appointmentMap = React.useMemo(() => {
195
- return new Map(renderAppts.map((appointment) => [appointment.id, appointment]));
196
- }, [renderAppts]);
197
- const effectiveAppts = React.useMemo(() => {
198
- if (drag.kind === "none") {
199
- return renderAppts;
200
- }
201
- return renderAppts.map((appointment) => {
202
- if (appointment.id !== drag.appointmentId) {
203
- return appointment;
204
- }
205
- return {
206
- ...appointment,
207
- resourceId: drag.resourceId,
208
- startMin: drag.startMin,
209
- endMin: drag.endMin,
210
- start: dateAtMinute(selectedDate, dayStartAbs, drag.startMin),
211
- end: dateAtMinute(selectedDate, dayStartAbs, drag.endMin)
212
- };
213
- });
214
- }, [drag, renderAppts, selectedDate, dayStartAbs]);
215
- const laidOutByResource = React.useMemo(() => {
216
- const byResource = /* @__PURE__ */ new Map();
217
- for (const resource of resources) {
218
- const events = effectiveAppts.filter((appointment) => appointment.resourceId === resource.id);
219
- byResource.set(resource.id, layoutOverlaps(events));
220
- }
221
- return byResource;
222
- }, [effectiveAppts, resources]);
223
- const appointmentBgByResource = React.useMemo(() => {
224
- const map = /* @__PURE__ */ new Map();
225
- if (!getResourceAppointmentBackground) {
226
- return map;
227
- }
228
- for (const resource of resources) {
229
- map.set(resource.id, getResourceAppointmentBackground(resource));
382
+ return appointments.map((appointment) => {
383
+ const start = new Date(adapter.getStart(appointment));
384
+ const end = new Date(adapter.getEnd(appointment));
385
+ return {
386
+ raw: appointment,
387
+ id: adapter.getId(appointment),
388
+ resourceId: adapter.getResourceId(appointment),
389
+ start,
390
+ end,
391
+ startMin: clamp(minutesFromDayStart(dayStartAbs, start), 0, dayMinutes),
392
+ endMin: clamp(minutesFromDayStart(dayStartAbs, end), 0, dayMinutes),
393
+ title: adapter.getTitle(appointment)
394
+ };
395
+ }).filter((appointment) => dayISO(appointment.start) === selectedISO);
396
+ }
397
+ function buildAppointmentMap(appointments) {
398
+ return new Map(appointments.map((appointment) => [appointment.id, appointment]));
399
+ }
400
+ function applyDragToAppointments(appointments, drag, selectedDate, dayStartAbs) {
401
+ if (drag.kind === "none") {
402
+ return appointments;
403
+ }
404
+ return appointments.map((appointment) => {
405
+ if (appointment.id !== drag.appointmentId) {
406
+ return appointment;
230
407
  }
231
- return map;
232
- }, [getResourceAppointmentBackground, resources]);
233
- const appointmentColorTokenClassByResource = React.useMemo(() => {
234
- const map = /* @__PURE__ */ new Map();
235
- const mergedClassMap = {
236
- ...DEFAULT_APPOINTMENT_COLOR_TOKEN_CLASS_MAP,
237
- ...appointmentColorTokenClassMap ?? {}
408
+ return {
409
+ ...appointment,
410
+ resourceId: drag.resourceId,
411
+ startMin: drag.startMin,
412
+ endMin: drag.endMin,
413
+ start: dateAtMinute(selectedDate, dayStartAbs, drag.startMin),
414
+ end: dateAtMinute(selectedDate, dayStartAbs, drag.endMin)
238
415
  };
239
- if (!getResourceAppointmentColorToken) {
240
- return map;
416
+ });
417
+ }
418
+ function buildLaidOutByResource(resources, appointments) {
419
+ const appointmentsByResource = /* @__PURE__ */ new Map();
420
+ for (const appointment of appointments) {
421
+ const current = appointmentsByResource.get(appointment.resourceId) ?? [];
422
+ current.push(appointment);
423
+ appointmentsByResource.set(appointment.resourceId, current);
424
+ }
425
+ const result = /* @__PURE__ */ new Map();
426
+ for (const resource of resources) {
427
+ result.set(resource.id, layoutOverlaps(appointmentsByResource.get(resource.id) ?? []));
428
+ }
429
+ return result;
430
+ }
431
+ function buildAppointmentAppearanceByResource(resources, resourceAppointmentClassMap, getResourceAppointmentAppearance, appointmentColorTokenClassByResource, appointmentBgByResource) {
432
+ const map = /* @__PURE__ */ new Map();
433
+ for (const resource of resources) {
434
+ const classFromIdMap = resourceAppointmentClassMap?.[String(resource.id)];
435
+ if (classFromIdMap) {
436
+ map.set(resource.id, { className: classFromIdMap });
437
+ continue;
241
438
  }
242
- for (const resource of resources) {
243
- const token = getResourceAppointmentColorToken(resource);
244
- map.set(resource.id, token ? mergedClassMap[token] : void 0);
439
+ const appearance = getResourceAppointmentAppearance?.(resource);
440
+ if (appearance) {
441
+ map.set(resource.id, appearance);
442
+ continue;
245
443
  }
246
- return map;
247
- }, [appointmentColorTokenClassMap, getResourceAppointmentColorToken, resources]);
248
- const appointmentAppearanceByResource = React.useMemo(() => {
249
- const map = /* @__PURE__ */ new Map();
250
- for (const resource of resources) {
251
- const classFromIdMap = resourceAppointmentClassMap?.[String(resource.id)];
252
- if (classFromIdMap) {
253
- map.set(resource.id, { className: classFromIdMap });
254
- continue;
444
+ const tokenClass = appointmentColorTokenClassByResource.get(resource.id);
445
+ if (tokenClass) {
446
+ map.set(resource.id, { className: tokenClass });
447
+ continue;
448
+ }
449
+ const legacyBackground = appointmentBgByResource.get(resource.id);
450
+ map.set(resource.id, legacyBackground ? { className: legacyBackground } : void 0);
451
+ }
452
+ return map;
453
+ }
454
+
455
+ // src/hooks/useSchedulerBaseData.ts
456
+ function useSchedulerBaseData({
457
+ adapter,
458
+ appointments,
459
+ dayEnd = "18:00",
460
+ dayStart = "09:00",
461
+ resources,
462
+ selectedDate
463
+ }) {
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(() => {
471
+ return normalizeAppointments({
472
+ adapter,
473
+ appointments,
474
+ dayMinutes,
475
+ dayStartAbs,
476
+ selectedISO
477
+ });
478
+ }, [adapter, appointments, dayMinutes, dayStartAbs, selectedISO]);
479
+ const appointmentMap = React5.useMemo(() => buildAppointmentMap(renderAppts), [renderAppts]);
480
+ return {
481
+ appointmentMap,
482
+ dayEndAbs,
483
+ dayMinutes,
484
+ dayStartAbs,
485
+ gridHeight,
486
+ gridMinWidth,
487
+ renderAppts
488
+ };
489
+ }
490
+ function useSchedulerInteractions({
491
+ appointmentMap,
492
+ dayMinutes,
493
+ dayStartAbs,
494
+ onAppointmentChange,
495
+ onPersistMoveResize,
496
+ renderAppts,
497
+ resources,
498
+ selectedDate
499
+ }) {
500
+ const [drag, setDrag] = React5.useState({ kind: "none" });
501
+ const colRefs = React5.useRef({});
502
+ const suppressClickRef = React5.useRef(false);
503
+ const pointerToMin = React5.useCallback(
504
+ (resourceId, clientY) => {
505
+ const col = colRefs.current[String(resourceId)];
506
+ if (!col) {
507
+ return 0;
255
508
  }
256
- const appearance = getResourceAppointmentAppearance?.(resource);
257
- if (appearance) {
258
- map.set(resource.id, appearance);
259
- continue;
509
+ const y = clientY - col.getBoundingClientRect().top - TOP_PAD;
510
+ return clamp(snap(y / PX_PER_MIN), 0, dayMinutes);
511
+ },
512
+ [dayMinutes]
513
+ );
514
+ const pointerToResourceId = React5.useCallback(
515
+ (clientX, fallbackResourceId) => {
516
+ for (const resource of resources) {
517
+ const col = colRefs.current[String(resource.id)];
518
+ if (!col) {
519
+ continue;
520
+ }
521
+ const rect = col.getBoundingClientRect();
522
+ if (clientX >= rect.left && clientX <= rect.right) {
523
+ return resource.id;
524
+ }
260
525
  }
261
- const tokenClass = appointmentColorTokenClassByResource.get(resource.id);
262
- if (tokenClass) {
263
- map.set(resource.id, { className: tokenClass });
264
- continue;
526
+ return fallbackResourceId;
527
+ },
528
+ [resources]
529
+ );
530
+ const hasOverlap = React5.useCallback(
531
+ (appointmentId, resourceId, startMin, endMin) => {
532
+ const events = renderAppts.filter((appointment) => appointment.resourceId === resourceId && appointment.id !== appointmentId);
533
+ return events.some((appointment) => overlaps(appointment, { startMin, endMin }));
534
+ },
535
+ [renderAppts]
536
+ );
537
+ const persistDrag = React5.useCallback(
538
+ async (state) => {
539
+ const target = appointmentMap.get(state.appointmentId);
540
+ if (!target) {
541
+ return;
265
542
  }
266
- const legacyBackground = appointmentBgByResource.get(resource.id);
267
- if (!legacyBackground) {
268
- map.set(resource.id, void 0);
269
- continue;
543
+ if (hasOverlap(state.appointmentId, state.resourceId, state.startMin, state.endMin)) {
544
+ return;
270
545
  }
271
- map.set(resource.id, { className: legacyBackground });
272
- }
273
- return map;
274
- }, [appointmentBgByResource, appointmentColorTokenClassByResource, getResourceAppointmentAppearance, resourceAppointmentClassMap, resources]);
275
- const pointerToMin = (resourceId, clientY) => {
276
- const col = colRefs.current[String(resourceId)];
277
- if (!col) {
278
- return 0;
279
- }
280
- const y = clientY - col.getBoundingClientRect().top - TOP_PAD;
281
- return clamp(snap(y / PX_PER_MIN), 0, dayMinutes);
282
- };
283
- const pointerToResourceId = (clientX, fallbackResourceId) => {
284
- for (const resource of resources) {
285
- const col = colRefs.current[String(resource.id)];
286
- if (!col) {
287
- continue;
546
+ const next = {
547
+ resourceId: state.resourceId,
548
+ start: dateAtMinute(selectedDate, dayStartAbs, state.startMin),
549
+ end: dateAtMinute(selectedDate, dayStartAbs, state.endMin)
550
+ };
551
+ if (onAppointmentChange) {
552
+ await onAppointmentChange({
553
+ kind: state.kind,
554
+ appointment: target.raw,
555
+ previous: {
556
+ resourceId: target.resourceId,
557
+ start: target.start,
558
+ end: target.end
559
+ },
560
+ next
561
+ });
562
+ return;
288
563
  }
289
- const rect = col.getBoundingClientRect();
290
- if (clientX >= rect.left && clientX <= rect.right) {
291
- return resource.id;
564
+ if (onPersistMoveResize) {
565
+ await onPersistMoveResize({
566
+ appointment: target.raw,
567
+ resourceId: next.resourceId,
568
+ start: next.start,
569
+ end: next.end
570
+ });
292
571
  }
293
- }
294
- return fallbackResourceId;
295
- };
296
- const hasOverlap = (appointmentId, resourceId, startMin, endMin) => {
297
- const events = renderAppts.filter((appointment) => appointment.resourceId === resourceId && appointment.id !== appointmentId);
298
- return events.some((appointment) => overlaps(appointment, { startMin, endMin }));
299
- };
300
- const persistDrag = async (state) => {
301
- const target = appointmentMap.get(state.appointmentId);
302
- if (!target) {
303
- return;
304
- }
305
- if (hasOverlap(state.appointmentId, state.resourceId, state.startMin, state.endMin)) {
306
- return;
307
- }
308
- const next = {
309
- resourceId: state.resourceId,
310
- start: dateAtMinute(selectedDate, dayStartAbs, state.startMin),
311
- end: dateAtMinute(selectedDate, dayStartAbs, state.endMin)
312
- };
313
- if (onAppointmentChange) {
314
- await onAppointmentChange({
315
- kind: state.kind,
316
- appointment: target.raw,
317
- previous: {
318
- resourceId: target.resourceId,
319
- start: target.start,
320
- end: target.end
321
- },
322
- next
323
- });
324
- return;
325
- }
326
- if (onPersistMoveResize) {
327
- await onPersistMoveResize({
328
- appointment: target.raw,
329
- resourceId: next.resourceId,
330
- start: next.start,
331
- end: next.end
572
+ },
573
+ [appointmentMap, dayStartAbs, hasOverlap, onAppointmentChange, onPersistMoveResize, selectedDate]
574
+ );
575
+ const onApptPointerDown = React5.useCallback(
576
+ (event, appointment) => {
577
+ if (event.button !== 0) {
578
+ return;
579
+ }
580
+ event.stopPropagation();
581
+ suppressClickRef.current = false;
582
+ const pointerMin = pointerToMin(appointment.resourceId, event.clientY);
583
+ setDrag({
584
+ kind: "move",
585
+ appointmentId: appointment.id,
586
+ pointerId: event.pointerId,
587
+ resourceId: appointment.resourceId,
588
+ durationMin: Math.max(MIN_EVENT_MIN, appointment.endMin - appointment.startMin),
589
+ offsetMin: pointerMin - appointment.startMin,
590
+ startMin: appointment.startMin,
591
+ endMin: appointment.endMin
332
592
  });
333
- }
334
- };
335
- const onApptPointerDown = (event, appointment) => {
336
- if (event.button !== 0) {
337
- return;
338
- }
339
- event.stopPropagation();
340
- suppressClickRef.current = false;
341
- const pointerMin = pointerToMin(appointment.resourceId, event.clientY);
342
- setDrag({
343
- kind: "move",
344
- appointmentId: appointment.id,
345
- pointerId: event.pointerId,
346
- resourceId: appointment.resourceId,
347
- durationMin: Math.max(MIN_EVENT_MIN, appointment.endMin - appointment.startMin),
348
- offsetMin: pointerMin - appointment.startMin,
349
- startMin: appointment.startMin,
350
- endMin: appointment.endMin
351
- });
352
- };
353
- const onResizePointerDown = (event, appointment) => {
593
+ },
594
+ [pointerToMin]
595
+ );
596
+ const onResizePointerDown = React5.useCallback((event, appointment) => {
354
597
  if (event.button !== 0) {
355
598
  return;
356
599
  }
@@ -364,39 +607,42 @@ function Scheduler({
364
607
  startMin: appointment.startMin,
365
608
  endMin: appointment.endMin
366
609
  });
367
- };
368
- const onGlobalPointerMove = (event) => {
369
- if (drag.kind === "none") {
370
- return;
371
- }
372
- if (drag.kind === "move") {
373
- const nextResourceId = pointerToResourceId(event.clientX, drag.resourceId);
374
- const pointerMin2 = pointerToMin(nextResourceId, event.clientY);
375
- const rawStart = pointerMin2 - drag.offsetMin;
376
- const nextStartMin = clamp(snap(rawStart), 0, dayMinutes - drag.durationMin);
377
- const nextEndMin2 = nextStartMin + drag.durationMin;
378
- if (nextStartMin !== drag.startMin || nextEndMin2 !== drag.endMin || nextResourceId !== drag.resourceId) {
610
+ }, []);
611
+ const onGlobalPointerMove = React5.useCallback(
612
+ (event) => {
613
+ if (drag.kind === "none") {
614
+ return;
615
+ }
616
+ if (drag.kind === "move") {
617
+ const nextResourceId = pointerToResourceId(event.clientX, drag.resourceId);
618
+ const pointerMin2 = pointerToMin(nextResourceId, event.clientY);
619
+ const rawStart = pointerMin2 - drag.offsetMin;
620
+ const nextStartMin = clamp(snap(rawStart), 0, dayMinutes - drag.durationMin);
621
+ const nextEndMin2 = nextStartMin + drag.durationMin;
622
+ if (nextStartMin !== drag.startMin || nextEndMin2 !== drag.endMin || nextResourceId !== drag.resourceId) {
623
+ suppressClickRef.current = true;
624
+ setDrag({
625
+ ...drag,
626
+ resourceId: nextResourceId,
627
+ startMin: nextStartMin,
628
+ endMin: nextEndMin2
629
+ });
630
+ }
631
+ return;
632
+ }
633
+ const pointerMin = pointerToMin(drag.resourceId, event.clientY);
634
+ const nextEndMin = clamp(snap(pointerMin), drag.startMin + MIN_EVENT_MIN, dayMinutes);
635
+ if (nextEndMin !== drag.endMin) {
379
636
  suppressClickRef.current = true;
380
637
  setDrag({
381
638
  ...drag,
382
- resourceId: nextResourceId,
383
- startMin: nextStartMin,
384
- endMin: nextEndMin2
639
+ endMin: nextEndMin
385
640
  });
386
641
  }
387
- return;
388
- }
389
- const pointerMin = pointerToMin(drag.resourceId, event.clientY);
390
- const nextEndMin = clamp(snap(pointerMin), drag.startMin + MIN_EVENT_MIN, dayMinutes);
391
- if (nextEndMin !== drag.endMin) {
392
- suppressClickRef.current = true;
393
- setDrag({
394
- ...drag,
395
- endMin: nextEndMin
396
- });
397
- }
398
- };
399
- const onGlobalPointerUp = () => {
642
+ },
643
+ [dayMinutes, drag, pointerToMin, pointerToResourceId]
644
+ );
645
+ const onGlobalPointerUp = React5.useCallback(() => {
400
646
  if (drag.kind === "none") {
401
647
  return;
402
648
  }
@@ -405,55 +651,174 @@ function Scheduler({
405
651
  if (suppressClickRef.current) {
406
652
  void persistDrag(current);
407
653
  }
654
+ }, [drag, persistDrag]);
655
+ return {
656
+ colRefs,
657
+ drag,
658
+ onApptPointerDown,
659
+ onGlobalPointerMove,
660
+ onGlobalPointerUp,
661
+ onResizePointerDown,
662
+ suppressClickRef
408
663
  };
409
- const defaultRenderAppointment = ({
410
- appointment,
411
- onPointerDown,
412
- onResizePointerDown: onResizePointerDown2,
413
- appointmentAppearance,
414
- appointmentBackgroundColor
415
- }) => {
416
- const rawMaybe = appointment.raw;
417
- const description = typeof rawMaybe.description === "string" ? rawMaybe.description : void 0;
418
- const legacyClassName = typeof appointmentBackgroundColor === "string" ? appointmentBackgroundColor : void 0;
419
- const backgroundClassName = appointmentAppearance?.className ?? legacyClassName ?? "bg-muted/40 dark:bg-muted/30";
420
- const className = `relative h-full cursor-grab overflow-hidden rounded-md border border-border p-2 pb-5 text-foreground ${backgroundClassName}`;
421
- return /* @__PURE__ */ jsxs("div", { onPointerDown, className, children: [
422
- /* @__PURE__ */ jsx("div", { className: "text-xs font-semibold", children: appointment.title }),
423
- description ? /* @__PURE__ */ jsx("div", { className: "mt-1 text-[10px] font-medium text-muted-foreground", children: description }) : null,
424
- /* @__PURE__ */ jsx(
425
- "div",
426
- {
427
- role: "button",
428
- "aria-label": "Resize appointment",
429
- onPointerDown: (event) => {
430
- event.stopPropagation();
431
- onResizePointerDown2(event);
432
- },
433
- className: "absolute inset-x-1 bottom-1 h-2 cursor-ns-resize rounded-full bg-border/90 transition-colors hover:bg-border"
434
- }
435
- )
436
- ] });
437
- };
438
- const defaultRenderDatePicker = ({
439
- selectedDate: value,
440
- onSelectedDateChange: onDateChange
441
- }) => /* @__PURE__ */ jsx(
442
- "input",
443
- {
444
- type: "date",
445
- value: toInputDateValue(value),
446
- onChange: (event) => {
447
- const nextDate = fromInputDateValue(event.target.value);
448
- if (nextDate) {
449
- onDateChange(nextDate);
450
- }
451
- },
452
- className: "h-7 rounded-md border border-border bg-card px-2 text-xs text-foreground",
453
- "aria-label": "Select date"
664
+ }
665
+ function useSchedulerPresentationData({
666
+ appointmentColorTokenClassMap,
667
+ dayStartAbs,
668
+ drag,
669
+ getResourceAppointmentAppearance,
670
+ getResourceAppointmentBackground,
671
+ getResourceAppointmentColorToken,
672
+ renderAppts,
673
+ resourceAppointmentClassMap,
674
+ resources,
675
+ selectedDate
676
+ }) {
677
+ const effectiveAppts = React5.useMemo(() => {
678
+ return applyDragToAppointments(renderAppts, drag, selectedDate, dayStartAbs);
679
+ }, [dayStartAbs, drag, renderAppts, selectedDate]);
680
+ const laidOutByResource = React5.useMemo(() => {
681
+ return buildLaidOutByResource(resources, effectiveAppts);
682
+ }, [effectiveAppts, resources]);
683
+ const appointmentBgByResource = React5.useMemo(() => {
684
+ if (!getResourceAppointmentBackground) {
685
+ return /* @__PURE__ */ new Map();
454
686
  }
455
- );
456
- const defaultRenderResourceHeader = (resource) => /* @__PURE__ */ jsx("strong", { children: resource.label });
687
+ return buildResourceMap({
688
+ fallback: void 0,
689
+ getValue: getResourceAppointmentBackground,
690
+ resources,
691
+ toKey: (resource) => resource.id
692
+ });
693
+ }, [getResourceAppointmentBackground, resources]);
694
+ const appointmentColorTokenClassByResource = React5.useMemo(() => {
695
+ if (!getResourceAppointmentColorToken) {
696
+ return /* @__PURE__ */ new Map();
697
+ }
698
+ const mergedClassMap = {
699
+ ...DEFAULT_APPOINTMENT_COLOR_TOKEN_CLASS_MAP,
700
+ ...appointmentColorTokenClassMap ?? {}
701
+ };
702
+ return buildResourceMap({
703
+ fallback: void 0,
704
+ getValue: (resource) => {
705
+ const token = getResourceAppointmentColorToken(resource);
706
+ return token ? mergedClassMap[token] : void 0;
707
+ },
708
+ resources,
709
+ toKey: (resource) => resource.id
710
+ });
711
+ }, [appointmentColorTokenClassMap, getResourceAppointmentColorToken, resources]);
712
+ const appointmentAppearanceByResource = React5.useMemo(() => {
713
+ return buildAppointmentAppearanceByResource(
714
+ resources,
715
+ resourceAppointmentClassMap,
716
+ getResourceAppointmentAppearance,
717
+ appointmentColorTokenClassByResource,
718
+ appointmentBgByResource
719
+ );
720
+ }, [
721
+ appointmentBgByResource,
722
+ appointmentColorTokenClassByResource,
723
+ getResourceAppointmentAppearance,
724
+ resourceAppointmentClassMap,
725
+ resources
726
+ ]);
727
+ return {
728
+ appointmentAppearanceByResource,
729
+ appointmentBgByResource,
730
+ effectiveAppts,
731
+ laidOutByResource
732
+ };
733
+ }
734
+ function Scheduler({
735
+ resources,
736
+ appointments,
737
+ selectedDate,
738
+ onSelectedDateChange,
739
+ adapter,
740
+ onPersistMoveResize,
741
+ onAppointmentChange,
742
+ renderResourceHeader,
743
+ renderAppointment,
744
+ resourceAppointmentClassMap,
745
+ getResourceAppointmentAppearance,
746
+ getResourceAppointmentColorToken,
747
+ appointmentColorTokenClassMap,
748
+ getResourceAppointmentBackground,
749
+ renderToolbar,
750
+ renderDatePicker,
751
+ prevButtonLabel = "Prev",
752
+ nextButtonLabel = "Next",
753
+ dayStart = "09:00",
754
+ dayEnd = "18:00"
755
+ }) {
756
+ const {
757
+ appointmentMap,
758
+ dayMinutes,
759
+ dayStartAbs,
760
+ gridHeight,
761
+ gridMinWidth,
762
+ renderAppts
763
+ } = useSchedulerBaseData({
764
+ adapter,
765
+ appointments,
766
+ dayEnd,
767
+ dayStart,
768
+ resources,
769
+ selectedDate
770
+ });
771
+ const {
772
+ colRefs,
773
+ drag,
774
+ onApptPointerDown,
775
+ onGlobalPointerMove,
776
+ onGlobalPointerUp,
777
+ onResizePointerDown,
778
+ suppressClickRef
779
+ } = useSchedulerInteractions({
780
+ appointmentMap,
781
+ dayMinutes,
782
+ dayStartAbs,
783
+ onAppointmentChange,
784
+ onPersistMoveResize,
785
+ renderAppts,
786
+ resources,
787
+ selectedDate
788
+ });
789
+ const {
790
+ appointmentAppearanceByResource,
791
+ appointmentBgByResource,
792
+ laidOutByResource
793
+ } = useSchedulerPresentationData({
794
+ appointmentColorTokenClassMap,
795
+ dayStartAbs,
796
+ drag,
797
+ getResourceAppointmentAppearance,
798
+ getResourceAppointmentBackground,
799
+ getResourceAppointmentColorToken,
800
+ renderAppts,
801
+ resourceAppointmentClassMap,
802
+ resources,
803
+ selectedDate
804
+ });
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
+ });
457
822
  return /* @__PURE__ */ jsxs(
458
823
  "section",
459
824
  {
@@ -462,27 +827,7 @@ function Scheduler({
462
827
  onPointerUp: onGlobalPointerUp,
463
828
  onPointerCancel: onGlobalPointerUp,
464
829
  children: [
465
- /* @__PURE__ */ jsxs("div", { className: "rfs-toolbar", children: [
466
- /* @__PURE__ */ jsx(
467
- "button",
468
- {
469
- className: "rfs-btn border-border bg-card text-foreground hover:bg-muted",
470
- type: "button",
471
- onClick: () => onSelectedDateChange(shiftDays(selectedDate, -1)),
472
- children: prevButtonLabel
473
- }
474
- ),
475
- (renderDatePicker ?? defaultRenderDatePicker)({ selectedDate, onSelectedDateChange }),
476
- /* @__PURE__ */ jsx(
477
- "button",
478
- {
479
- className: "rfs-btn border-border bg-card text-foreground hover:bg-muted",
480
- type: "button",
481
- onClick: () => onSelectedDateChange(shiftDays(selectedDate, 1)),
482
- children: nextButtonLabel
483
- }
484
- )
485
- ] }),
830
+ toolbar,
486
831
  /* @__PURE__ */ jsx("div", { className: "rfs-shell border-border bg-card", children: /* @__PURE__ */ jsxs("div", { style: { minWidth: gridMinWidth }, children: [
487
832
  /* @__PURE__ */ jsxs("div", { className: "rfs-header border-border bg-card", children: [
488
833
  /* @__PURE__ */ jsx("div", { className: "rfs-gutter border-border", style: { width: GUTTER_W } }),
@@ -492,7 +837,7 @@ function Scheduler({
492
837
  className: "rfs-resource-header border-border bg-card text-foreground",
493
838
  style: { minWidth: RESOURCE_MIN_W, flex: 1 },
494
839
  children: [
495
- (renderResourceHeader ?? defaultRenderResourceHeader)(resource),
840
+ (renderResourceHeader ?? defaultResourceHeaderRenderer)(resource),
496
841
  isNameLike(resource) ? /* @__PURE__ */ jsx("div", { className: "rfs-resource-subtitle text-muted-foreground", children: fullName(resource) }) : null
497
842
  ]
498
843
  },
@@ -585,7 +930,7 @@ function Scheduler({
585
930
  );
586
931
  }
587
932
 
588
- // src/appointment-change.ts
933
+ // src/utils/scheduler-appointment-change.utils.ts
589
934
  function applySchedulerAppointmentChange(appointments, change, options) {
590
935
  return appointments.map((item) => {
591
936
  if (options.getId(item) !== options.getId(change.appointment)) {