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