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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -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 React2 = require('react');
6
6
 
7
7
  function _interopNamespace(e) {
8
8
  if (e && e.__esModule) return e;
@@ -22,9 +22,38 @@ function _interopNamespace(e) {
22
22
  return Object.freeze(n);
23
23
  }
24
24
 
25
- var React__namespace = /*#__PURE__*/_interopNamespace(React);
25
+ var React2__namespace = /*#__PURE__*/_interopNamespace(React2);
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
+ }
28
57
 
29
58
  // src/components/constants.ts
30
59
  var STEP_MIN = 15;
@@ -34,26 +63,75 @@ var GUTTER_W = 72;
34
63
  var TOP_PAD = 12;
35
64
  var BOTTOM_PAD = 24;
36
65
  var RESOURCE_MIN_W = 420;
37
- var clamp = (n, min, max) => {
38
- return Math.max(min, Math.min(max, n));
66
+
67
+ // src/utils/scheduler-core.utils.ts
68
+ function isNameLike(resource) {
69
+ if (!resource || typeof resource !== "object") {
70
+ return false;
71
+ }
72
+ const maybe = resource;
73
+ return typeof maybe.firstName === "string" || typeof maybe.lastName === "string";
74
+ }
75
+ function toInputDateValue(date) {
76
+ const year = date.getFullYear();
77
+ const month = String(date.getMonth() + 1).padStart(2, "0");
78
+ const day = String(date.getDate()).padStart(2, "0");
79
+ return `${year}-${month}-${day}`;
80
+ }
81
+ function fromInputDateValue(value) {
82
+ const [yearRaw, monthRaw, dayRaw] = value.split("-");
83
+ const year = Number(yearRaw);
84
+ const month = Number(monthRaw);
85
+ const day = Number(dayRaw);
86
+ if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)) {
87
+ return null;
88
+ }
89
+ return new Date(year, month - 1, day);
90
+ }
91
+ var DEFAULT_APPOINTMENT_COLOR_TOKEN_CLASS_MAP = {
92
+ slate: "bg-slate-100 dark:bg-slate-900/40",
93
+ gray: "bg-gray-100 dark:bg-gray-900/40",
94
+ zinc: "bg-zinc-100 dark:bg-zinc-900/40",
95
+ neutral: "bg-neutral-100 dark:bg-neutral-900/40",
96
+ stone: "bg-stone-100 dark:bg-stone-900/40",
97
+ red: "bg-red-100 dark:bg-red-950/40",
98
+ orange: "bg-orange-100 dark:bg-orange-950/40",
99
+ amber: "bg-amber-100 dark:bg-amber-950/40",
100
+ yellow: "bg-yellow-100 dark:bg-yellow-950/40",
101
+ lime: "bg-lime-100 dark:bg-lime-950/40",
102
+ green: "bg-green-100 dark:bg-green-950/40",
103
+ emerald: "bg-emerald-100 dark:bg-emerald-950/40",
104
+ teal: "bg-teal-100 dark:bg-teal-950/40",
105
+ cyan: "bg-cyan-100 dark:bg-cyan-950/40",
106
+ sky: "bg-sky-100 dark:bg-sky-950/40",
107
+ blue: "bg-blue-100 dark:bg-blue-950/40",
108
+ indigo: "bg-indigo-100 dark:bg-indigo-950/40",
109
+ violet: "bg-violet-100 dark:bg-violet-950/40",
110
+ purple: "bg-purple-100 dark:bg-purple-950/40",
111
+ fuchsia: "bg-fuchsia-100 dark:bg-fuchsia-950/40",
112
+ pink: "bg-pink-100 dark:bg-pink-950/40",
113
+ rose: "bg-rose-100 dark:bg-rose-950/40"
39
114
  };
40
- var snap = (mins, stepMin = STEP_MIN) => {
115
+ function clamp(n, min, max) {
116
+ return Math.max(min, Math.min(max, n));
117
+ }
118
+ function snap(mins, stepMin = STEP_MIN) {
41
119
  return Math.round(mins / stepMin) * stepMin;
42
- };
43
- var parseHHMM = (hhmm) => {
120
+ }
121
+ function parseHHMM(hhmm) {
44
122
  const [h, m] = hhmm.split(":").map(Number);
45
123
  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) => {
124
+ }
125
+ function fullName(resource) {
126
+ return `${resource.firstName ?? ""} ${resource.lastName ?? ""}`.trim();
127
+ }
128
+ function dayISO(date) {
129
+ return dateFns.format(date, "yyyy-MM-dd");
130
+ }
131
+ function shiftDays(date, delta) {
132
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate() + delta);
133
+ }
134
+ function dateAtMinute(selectedDate, dayStartAbs, minuteFromStart) {
57
135
  const base = new Date(
58
136
  selectedDate.getFullYear(),
59
137
  selectedDate.getMonth(),
@@ -68,311 +146,314 @@ var dateAtMinute = (selectedDate, dayStartAbs, minuteFromStart) => {
68
146
  const mm = abs % 60;
69
147
  base.setHours(hh, mm, 0, 0);
70
148
  return base;
71
- };
72
- var minutesFromDayStart = (dayStartAbs, dt) => {
73
- return dt.getHours() * 60 + dt.getMinutes() - dayStartAbs;
74
- };
75
- var overlaps = (a, b) => {
149
+ }
150
+ function minutesFromDayStart(dayStartAbs, date) {
151
+ return date.getHours() * 60 + date.getMinutes() - dayStartAbs;
152
+ }
153
+ function overlaps(a, b) {
76
154
  return a.startMin < b.endMin && b.startMin < a.endMin;
77
- };
78
- var layoutOverlaps = (blocks) => {
155
+ }
156
+ function layoutOverlaps(blocks) {
79
157
  const sorted = [...blocks].sort((a, b) => a.startMin - b.startMin || a.endMin - b.endMin);
80
158
  const groups = [];
81
159
  let active = [];
82
160
  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));
161
+ for (const block of sorted) {
162
+ active = active.filter((item) => item.endMin > block.startMin);
163
+ const conflicts = active.some((item) => overlaps(item, block));
86
164
  if (!conflicts && current.length > 0) {
87
165
  groups.push(current);
88
166
  current = [];
89
167
  }
90
- current.push(e);
91
- active.push(e);
168
+ current.push(block);
169
+ active.push(block);
92
170
  }
93
171
  if (current.length) {
94
172
  groups.push(current);
95
173
  }
96
- const out = [];
174
+ const output = [];
97
175
  for (const group of groups) {
98
- const g = [...group].sort((a, b) => a.startMin - b.startMin || a.endMin - b.endMin);
176
+ const sortedGroup = [...group].sort((a, b) => a.startMin - b.startMin || a.endMin - b.endMin);
99
177
  const laneEnd = [];
100
178
  const laneOf = /* @__PURE__ */ new Map();
101
- for (const e of g) {
179
+ for (const block of sortedGroup) {
102
180
  let placed = false;
103
181
  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);
182
+ if (laneEnd[i] <= block.startMin) {
183
+ laneEnd[i] = block.endMin;
184
+ laneOf.set(block.id, i);
107
185
  placed = true;
108
186
  break;
109
187
  }
110
188
  }
111
189
  if (!placed) {
112
- laneEnd.push(e.endMin);
113
- laneOf.set(e.id, laneEnd.length - 1);
190
+ laneEnd.push(block.endMin);
191
+ laneOf.set(block.id, laneEnd.length - 1);
114
192
  }
115
193
  }
116
194
  const lanes = laneEnd.length;
117
- for (const e of g) {
118
- out.push({ ...e, lane: laneOf.get(e.id) ?? 0, lanes });
195
+ for (const block of sortedGroup) {
196
+ output.push({ ...block, lane: laneOf.get(block.id) ?? 0, lanes });
119
197
  }
120
198
  }
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";
199
+ return output;
129
200
  }
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}`;
201
+ function defaultRenderDatePicker({ selectedDate, onSelectedDateChange }) {
202
+ return /* @__PURE__ */ jsxRuntime.jsx(
203
+ "input",
204
+ {
205
+ type: "date",
206
+ value: toInputDateValue(selectedDate),
207
+ onChange: (event) => {
208
+ const nextDate = fromInputDateValue(event.target.value);
209
+ if (nextDate) {
210
+ onSelectedDateChange(nextDate);
211
+ }
212
+ },
213
+ className: "h-7 rounded-md border border-border bg-card px-2 text-xs text-foreground",
214
+ "aria-label": "Select date"
215
+ }
216
+ );
135
217
  }
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;
143
- }
144
- return new Date(year, month - 1, day);
218
+ function defaultRenderResourceHeader({ resource }) {
219
+ return /* @__PURE__ */ jsxRuntime.jsx("strong", { children: resource.label });
145
220
  }
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({
221
+
222
+ // src/utils/scheduler-data.utils.ts
223
+ function buildResourceMap({
224
+ fallback,
225
+ getValue,
171
226
  resources,
172
- appointments,
173
- selectedDate,
174
- onSelectedDateChange,
227
+ toKey
228
+ }) {
229
+ const map = /* @__PURE__ */ new Map();
230
+ for (const resource of resources) {
231
+ map.set(toKey(resource), getValue(resource) ?? fallback);
232
+ }
233
+ return map;
234
+ }
235
+ function normalizeAppointments({
175
236
  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"
237
+ appointments,
238
+ dayMinutes,
239
+ dayStartAbs,
240
+ selectedISO
190
241
  }) {
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));
242
+ return appointments.map((appointment) => {
243
+ const start = new Date(adapter.getStart(appointment));
244
+ const end = new Date(adapter.getEnd(appointment));
245
+ return {
246
+ raw: appointment,
247
+ id: adapter.getId(appointment),
248
+ resourceId: adapter.getResourceId(appointment),
249
+ start,
250
+ end,
251
+ startMin: clamp(minutesFromDayStart(dayStartAbs, start), 0, dayMinutes),
252
+ endMin: clamp(minutesFromDayStart(dayStartAbs, end), 0, dayMinutes),
253
+ title: adapter.getTitle(appointment)
254
+ };
255
+ }).filter((appointment) => dayISO(appointment.start) === selectedISO);
256
+ }
257
+ function buildAppointmentMap(appointments) {
258
+ return new Map(appointments.map((appointment) => [appointment.id, appointment]));
259
+ }
260
+ function applyDragToAppointments(appointments, drag, selectedDate, dayStartAbs) {
261
+ if (drag.kind === "none") {
262
+ return appointments;
263
+ }
264
+ return appointments.map((appointment) => {
265
+ if (appointment.id !== drag.appointmentId) {
266
+ return appointment;
252
267
  }
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 ?? {}
268
+ return {
269
+ ...appointment,
270
+ resourceId: drag.resourceId,
271
+ startMin: drag.startMin,
272
+ endMin: drag.endMin,
273
+ start: dateAtMinute(selectedDate, dayStartAbs, drag.startMin),
274
+ end: dateAtMinute(selectedDate, dayStartAbs, drag.endMin)
260
275
  };
261
- if (!getResourceAppointmentColorToken) {
262
- return map;
276
+ });
277
+ }
278
+ function buildLaidOutByResource(resources, appointments) {
279
+ const appointmentsByResource = /* @__PURE__ */ new Map();
280
+ for (const appointment of appointments) {
281
+ const current = appointmentsByResource.get(appointment.resourceId) ?? [];
282
+ current.push(appointment);
283
+ appointmentsByResource.set(appointment.resourceId, current);
284
+ }
285
+ const result = /* @__PURE__ */ new Map();
286
+ for (const resource of resources) {
287
+ result.set(resource.id, layoutOverlaps(appointmentsByResource.get(resource.id) ?? []));
288
+ }
289
+ return result;
290
+ }
291
+ function buildAppointmentAppearanceByResource(resources, resourceAppointmentClassMap, getResourceAppointmentAppearance, appointmentColorTokenClassByResource, appointmentBgByResource) {
292
+ const map = /* @__PURE__ */ new Map();
293
+ for (const resource of resources) {
294
+ const classFromIdMap = resourceAppointmentClassMap?.[String(resource.id)];
295
+ if (classFromIdMap) {
296
+ map.set(resource.id, { className: classFromIdMap });
297
+ continue;
263
298
  }
264
- for (const resource of resources) {
265
- const token = getResourceAppointmentColorToken(resource);
266
- map.set(resource.id, token ? mergedClassMap[token] : void 0);
299
+ const appearance = getResourceAppointmentAppearance?.(resource);
300
+ if (appearance) {
301
+ map.set(resource.id, appearance);
302
+ continue;
267
303
  }
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;
304
+ const tokenClass = appointmentColorTokenClassByResource.get(resource.id);
305
+ if (tokenClass) {
306
+ map.set(resource.id, { className: tokenClass });
307
+ continue;
308
+ }
309
+ const legacyBackground = appointmentBgByResource.get(resource.id);
310
+ map.set(resource.id, legacyBackground ? { className: legacyBackground } : void 0);
311
+ }
312
+ return map;
313
+ }
314
+
315
+ // src/hooks/useSchedulerBaseData.ts
316
+ function useSchedulerBaseData({
317
+ adapter,
318
+ appointments,
319
+ dayEnd = "18:00",
320
+ dayStart = "09:00",
321
+ resources,
322
+ selectedDate
323
+ }) {
324
+ const dayStartAbs = React2__namespace.useMemo(() => parseHHMM(dayStart), [dayStart]);
325
+ const dayEndAbs = React2__namespace.useMemo(() => parseHHMM(dayEnd), [dayEnd]);
326
+ const dayMinutes = React2__namespace.useMemo(() => Math.max(0, dayEndAbs - dayStartAbs), [dayEndAbs, dayStartAbs]);
327
+ const gridHeight = React2__namespace.useMemo(() => TOP_PAD + dayMinutes * PX_PER_MIN + BOTTOM_PAD, [dayMinutes]);
328
+ const gridMinWidth = React2__namespace.useMemo(() => GUTTER_W + Math.max(resources.length, 1) * RESOURCE_MIN_W, [resources.length]);
329
+ const selectedISO = React2__namespace.useMemo(() => dayISO(selectedDate), [selectedDate]);
330
+ const renderAppts = React2__namespace.useMemo(() => {
331
+ return normalizeAppointments({
332
+ adapter,
333
+ appointments,
334
+ dayMinutes,
335
+ dayStartAbs,
336
+ selectedISO
337
+ });
338
+ }, [adapter, appointments, dayMinutes, dayStartAbs, selectedISO]);
339
+ const appointmentMap = React2__namespace.useMemo(() => buildAppointmentMap(renderAppts), [renderAppts]);
340
+ return {
341
+ appointmentMap,
342
+ dayEndAbs,
343
+ dayMinutes,
344
+ dayStartAbs,
345
+ gridHeight,
346
+ gridMinWidth,
347
+ renderAppts
348
+ };
349
+ }
350
+ function useSchedulerInteractions({
351
+ appointmentMap,
352
+ dayMinutes,
353
+ dayStartAbs,
354
+ onAppointmentChange,
355
+ onPersistMoveResize,
356
+ renderAppts,
357
+ resources,
358
+ selectedDate
359
+ }) {
360
+ const [drag, setDrag] = React2__namespace.useState({ kind: "none" });
361
+ const colRefs = React2__namespace.useRef({});
362
+ const suppressClickRef = React2__namespace.useRef(false);
363
+ const pointerToMin = React2__namespace.useCallback(
364
+ (resourceId, clientY) => {
365
+ const col = colRefs.current[String(resourceId)];
366
+ if (!col) {
367
+ return 0;
368
+ }
369
+ const y = clientY - col.getBoundingClientRect().top - TOP_PAD;
370
+ return clamp(snap(y / PX_PER_MIN), 0, dayMinutes);
371
+ },
372
+ [dayMinutes]
373
+ );
374
+ const pointerToResourceId = React2__namespace.useCallback(
375
+ (clientX, fallbackResourceId) => {
376
+ for (const resource of resources) {
377
+ const col = colRefs.current[String(resource.id)];
378
+ if (!col) {
379
+ continue;
380
+ }
381
+ const rect = col.getBoundingClientRect();
382
+ if (clientX >= rect.left && clientX <= rect.right) {
383
+ return resource.id;
384
+ }
277
385
  }
278
- const appearance = getResourceAppointmentAppearance?.(resource);
279
- if (appearance) {
280
- map.set(resource.id, appearance);
281
- continue;
386
+ return fallbackResourceId;
387
+ },
388
+ [resources]
389
+ );
390
+ const hasOverlap = React2__namespace.useCallback(
391
+ (appointmentId, resourceId, startMin, endMin) => {
392
+ const events = renderAppts.filter((appointment) => appointment.resourceId === resourceId && appointment.id !== appointmentId);
393
+ return events.some((appointment) => overlaps(appointment, { startMin, endMin }));
394
+ },
395
+ [renderAppts]
396
+ );
397
+ const persistDrag = React2__namespace.useCallback(
398
+ async (state) => {
399
+ const target = appointmentMap.get(state.appointmentId);
400
+ if (!target) {
401
+ return;
282
402
  }
283
- const tokenClass = appointmentColorTokenClassByResource.get(resource.id);
284
- if (tokenClass) {
285
- map.set(resource.id, { className: tokenClass });
286
- continue;
403
+ if (hasOverlap(state.appointmentId, state.resourceId, state.startMin, state.endMin)) {
404
+ return;
287
405
  }
288
- const legacyBackground = appointmentBgByResource.get(resource.id);
289
- if (!legacyBackground) {
290
- map.set(resource.id, void 0);
291
- continue;
406
+ const next = {
407
+ resourceId: state.resourceId,
408
+ start: dateAtMinute(selectedDate, dayStartAbs, state.startMin),
409
+ end: dateAtMinute(selectedDate, dayStartAbs, state.endMin)
410
+ };
411
+ if (onAppointmentChange) {
412
+ await onAppointmentChange({
413
+ kind: state.kind,
414
+ appointment: target.raw,
415
+ previous: {
416
+ resourceId: target.resourceId,
417
+ start: target.start,
418
+ end: target.end
419
+ },
420
+ next
421
+ });
422
+ return;
292
423
  }
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;
424
+ if (onPersistMoveResize) {
425
+ await onPersistMoveResize({
426
+ appointment: target.raw,
427
+ resourceId: next.resourceId,
428
+ start: next.start,
429
+ end: next.end
430
+ });
310
431
  }
311
- const rect = col.getBoundingClientRect();
312
- if (clientX >= rect.left && clientX <= rect.right) {
313
- return resource.id;
432
+ },
433
+ [appointmentMap, dayStartAbs, hasOverlap, onAppointmentChange, onPersistMoveResize, selectedDate]
434
+ );
435
+ const onApptPointerDown = React2__namespace.useCallback(
436
+ (event, appointment) => {
437
+ if (event.button !== 0) {
438
+ return;
314
439
  }
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
440
+ event.stopPropagation();
441
+ suppressClickRef.current = false;
442
+ const pointerMin = pointerToMin(appointment.resourceId, event.clientY);
443
+ setDrag({
444
+ kind: "move",
445
+ appointmentId: appointment.id,
446
+ pointerId: event.pointerId,
447
+ resourceId: appointment.resourceId,
448
+ durationMin: Math.max(MIN_EVENT_MIN, appointment.endMin - appointment.startMin),
449
+ offsetMin: pointerMin - appointment.startMin,
450
+ startMin: appointment.startMin,
451
+ endMin: appointment.endMin
354
452
  });
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) => {
453
+ },
454
+ [pointerToMin]
455
+ );
456
+ const onResizePointerDown = React2__namespace.useCallback((event, appointment) => {
376
457
  if (event.button !== 0) {
377
458
  return;
378
459
  }
@@ -386,39 +467,42 @@ function Scheduler({
386
467
  startMin: appointment.startMin,
387
468
  endMin: appointment.endMin
388
469
  });
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) {
470
+ }, []);
471
+ const onGlobalPointerMove = React2__namespace.useCallback(
472
+ (event) => {
473
+ if (drag.kind === "none") {
474
+ return;
475
+ }
476
+ if (drag.kind === "move") {
477
+ const nextResourceId = pointerToResourceId(event.clientX, drag.resourceId);
478
+ const pointerMin2 = pointerToMin(nextResourceId, event.clientY);
479
+ const rawStart = pointerMin2 - drag.offsetMin;
480
+ const nextStartMin = clamp(snap(rawStart), 0, dayMinutes - drag.durationMin);
481
+ const nextEndMin2 = nextStartMin + drag.durationMin;
482
+ if (nextStartMin !== drag.startMin || nextEndMin2 !== drag.endMin || nextResourceId !== drag.resourceId) {
483
+ suppressClickRef.current = true;
484
+ setDrag({
485
+ ...drag,
486
+ resourceId: nextResourceId,
487
+ startMin: nextStartMin,
488
+ endMin: nextEndMin2
489
+ });
490
+ }
491
+ return;
492
+ }
493
+ const pointerMin = pointerToMin(drag.resourceId, event.clientY);
494
+ const nextEndMin = clamp(snap(pointerMin), drag.startMin + MIN_EVENT_MIN, dayMinutes);
495
+ if (nextEndMin !== drag.endMin) {
401
496
  suppressClickRef.current = true;
402
497
  setDrag({
403
498
  ...drag,
404
- resourceId: nextResourceId,
405
- startMin: nextStartMin,
406
- endMin: nextEndMin2
499
+ endMin: nextEndMin
407
500
  });
408
501
  }
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 = () => {
502
+ },
503
+ [dayMinutes, drag, pointerToMin, pointerToResourceId]
504
+ );
505
+ const onGlobalPointerUp = React2__namespace.useCallback(() => {
422
506
  if (drag.kind === "none") {
423
507
  return;
424
508
  }
@@ -427,55 +511,157 @@ function Scheduler({
427
511
  if (suppressClickRef.current) {
428
512
  void persistDrag(current);
429
513
  }
514
+ }, [drag, persistDrag]);
515
+ return {
516
+ colRefs,
517
+ drag,
518
+ onApptPointerDown,
519
+ onGlobalPointerMove,
520
+ onGlobalPointerUp,
521
+ onResizePointerDown,
522
+ suppressClickRef
430
523
  };
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"
524
+ }
525
+ function useSchedulerPresentationData({
526
+ appointmentColorTokenClassMap,
527
+ dayStartAbs,
528
+ drag,
529
+ getResourceAppointmentAppearance,
530
+ getResourceAppointmentBackground,
531
+ getResourceAppointmentColorToken,
532
+ renderAppts,
533
+ resourceAppointmentClassMap,
534
+ resources,
535
+ selectedDate
536
+ }) {
537
+ const effectiveAppts = React2__namespace.useMemo(() => {
538
+ return applyDragToAppointments(renderAppts, drag, selectedDate, dayStartAbs);
539
+ }, [dayStartAbs, drag, renderAppts, selectedDate]);
540
+ const laidOutByResource = React2__namespace.useMemo(() => {
541
+ return buildLaidOutByResource(resources, effectiveAppts);
542
+ }, [effectiveAppts, resources]);
543
+ const appointmentBgByResource = React2__namespace.useMemo(() => {
544
+ if (!getResourceAppointmentBackground) {
545
+ return /* @__PURE__ */ new Map();
476
546
  }
477
- );
478
- const defaultRenderResourceHeader = (resource) => /* @__PURE__ */ jsxRuntime.jsx("strong", { children: resource.label });
547
+ return buildResourceMap({
548
+ fallback: void 0,
549
+ getValue: getResourceAppointmentBackground,
550
+ resources,
551
+ toKey: (resource) => resource.id
552
+ });
553
+ }, [getResourceAppointmentBackground, resources]);
554
+ const appointmentColorTokenClassByResource = React2__namespace.useMemo(() => {
555
+ if (!getResourceAppointmentColorToken) {
556
+ return /* @__PURE__ */ new Map();
557
+ }
558
+ const mergedClassMap = {
559
+ ...DEFAULT_APPOINTMENT_COLOR_TOKEN_CLASS_MAP,
560
+ ...appointmentColorTokenClassMap ?? {}
561
+ };
562
+ return buildResourceMap({
563
+ fallback: void 0,
564
+ getValue: (resource) => {
565
+ const token = getResourceAppointmentColorToken(resource);
566
+ return token ? mergedClassMap[token] : void 0;
567
+ },
568
+ resources,
569
+ toKey: (resource) => resource.id
570
+ });
571
+ }, [appointmentColorTokenClassMap, getResourceAppointmentColorToken, resources]);
572
+ const appointmentAppearanceByResource = React2__namespace.useMemo(() => {
573
+ return buildAppointmentAppearanceByResource(
574
+ resources,
575
+ resourceAppointmentClassMap,
576
+ getResourceAppointmentAppearance,
577
+ appointmentColorTokenClassByResource,
578
+ appointmentBgByResource
579
+ );
580
+ }, [
581
+ appointmentBgByResource,
582
+ appointmentColorTokenClassByResource,
583
+ getResourceAppointmentAppearance,
584
+ resourceAppointmentClassMap,
585
+ resources
586
+ ]);
587
+ return {
588
+ appointmentAppearanceByResource,
589
+ appointmentBgByResource,
590
+ effectiveAppts,
591
+ laidOutByResource
592
+ };
593
+ }
594
+ function Scheduler({
595
+ resources,
596
+ appointments,
597
+ selectedDate,
598
+ onSelectedDateChange,
599
+ adapter,
600
+ onPersistMoveResize,
601
+ onAppointmentChange,
602
+ renderResourceHeader,
603
+ renderAppointment,
604
+ resourceAppointmentClassMap,
605
+ getResourceAppointmentAppearance,
606
+ getResourceAppointmentColorToken,
607
+ appointmentColorTokenClassMap,
608
+ getResourceAppointmentBackground,
609
+ renderDatePicker,
610
+ prevButtonLabel = "Prev",
611
+ nextButtonLabel = "Next",
612
+ dayStart = "09:00",
613
+ dayEnd = "18:00"
614
+ }) {
615
+ const {
616
+ appointmentMap,
617
+ dayMinutes,
618
+ dayStartAbs,
619
+ gridHeight,
620
+ gridMinWidth,
621
+ renderAppts
622
+ } = useSchedulerBaseData({
623
+ adapter,
624
+ appointments,
625
+ dayEnd,
626
+ dayStart,
627
+ resources,
628
+ selectedDate
629
+ });
630
+ const {
631
+ colRefs,
632
+ drag,
633
+ onApptPointerDown,
634
+ onGlobalPointerMove,
635
+ onGlobalPointerUp,
636
+ onResizePointerDown,
637
+ suppressClickRef
638
+ } = useSchedulerInteractions({
639
+ appointmentMap,
640
+ dayMinutes,
641
+ dayStartAbs,
642
+ onAppointmentChange,
643
+ onPersistMoveResize,
644
+ renderAppts,
645
+ resources,
646
+ selectedDate
647
+ });
648
+ const {
649
+ appointmentAppearanceByResource,
650
+ appointmentBgByResource,
651
+ laidOutByResource
652
+ } = useSchedulerPresentationData({
653
+ appointmentColorTokenClassMap,
654
+ dayStartAbs,
655
+ drag,
656
+ getResourceAppointmentAppearance,
657
+ getResourceAppointmentBackground,
658
+ getResourceAppointmentColorToken,
659
+ renderAppts,
660
+ resourceAppointmentClassMap,
661
+ resources,
662
+ selectedDate
663
+ });
664
+ const defaultResourceHeaderRenderer = (resource) => defaultRenderResourceHeader({ resource });
479
665
  return /* @__PURE__ */ jsxRuntime.jsxs(
480
666
  "section",
481
667
  {
@@ -514,7 +700,7 @@ function Scheduler({
514
700
  className: "rfs-resource-header border-border bg-card text-foreground",
515
701
  style: { minWidth: RESOURCE_MIN_W, flex: 1 },
516
702
  children: [
517
- (renderResourceHeader ?? defaultRenderResourceHeader)(resource),
703
+ (renderResourceHeader ?? defaultResourceHeaderRenderer)(resource),
518
704
  isNameLike(resource) ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rfs-resource-subtitle text-muted-foreground", children: fullName(resource) }) : null
519
705
  ]
520
706
  },
@@ -607,7 +793,7 @@ function Scheduler({
607
793
  );
608
794
  }
609
795
 
610
- // src/appointment-change.ts
796
+ // src/utils/scheduler-appointment-change.utils.ts
611
797
  function applySchedulerAppointmentChange(appointments, change, options) {
612
798
  return appointments.map((item) => {
613
799
  if (options.getId(item) !== options.getId(change.appointment)) {