@marco.colia/react-fast-scheduler 1.0.2 → 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,225 +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 Scheduler({
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
+ );
217
+ }
218
+ function defaultRenderResourceHeader({ resource }) {
219
+ return /* @__PURE__ */ jsxRuntime.jsx("strong", { children: resource.label });
220
+ }
221
+
222
+ // src/utils/scheduler-data.utils.ts
223
+ function buildResourceMap({
224
+ fallback,
225
+ getValue,
131
226
  resources,
132
- appointments,
133
- selectedDate,
134
- 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({
135
236
  adapter,
136
- onPersistMoveResize,
137
- onAppointmentChange,
138
- renderResourceHeader,
139
- renderAppointment,
140
- getResourceAppointmentBackground,
141
- renderDatePicker,
142
- prevButtonLabel = "Prev",
143
- nextButtonLabel = "Next",
144
- dayStart = "09:00",
145
- dayEnd = "18:00"
237
+ appointments,
238
+ dayMinutes,
239
+ dayStartAbs,
240
+ selectedISO
146
241
  }) {
147
- const [drag, setDrag] = React__namespace.useState({ kind: "none" });
148
- const colRefs = React__namespace.useRef({});
149
- const suppressClickRef = React__namespace.useRef(false);
150
- const dayStartAbs = parseHHMM(dayStart);
151
- const dayEndAbs = parseHHMM(dayEnd);
152
- const dayMinutes = Math.max(0, dayEndAbs - dayStartAbs);
153
- const gridHeight = TOP_PAD + dayMinutes * PX_PER_MIN + BOTTOM_PAD;
154
- const gridMinWidth = GUTTER_W + Math.max(resources.length, 1) * RESOURCE_MIN_W;
155
- const selectedISO = React__namespace.useMemo(() => dayISO(selectedDate), [selectedDate]);
156
- const renderAppts = React__namespace.useMemo(() => {
157
- return appointments.map((appointment) => {
158
- const start = new Date(adapter.getStart(appointment));
159
- const end = new Date(adapter.getEnd(appointment));
160
- return {
161
- raw: appointment,
162
- id: adapter.getId(appointment),
163
- resourceId: adapter.getResourceId(appointment),
164
- start,
165
- end,
166
- startMin: clamp(minutesFromDayStart(dayStartAbs, start), 0, dayMinutes),
167
- endMin: clamp(minutesFromDayStart(dayStartAbs, end), 0, dayMinutes),
168
- title: adapter.getTitle(appointment)
169
- };
170
- }).filter((appointment) => dayISO(appointment.start) === selectedISO);
171
- }, [appointments, adapter, dayStartAbs, dayMinutes, selectedISO]);
172
- const appointmentMap = React__namespace.useMemo(() => {
173
- return new Map(renderAppts.map((appointment) => [appointment.id, appointment]));
174
- }, [renderAppts]);
175
- const effectiveAppts = React__namespace.useMemo(() => {
176
- if (drag.kind === "none") {
177
- return renderAppts;
178
- }
179
- return renderAppts.map((appointment) => {
180
- if (appointment.id !== drag.appointmentId) {
181
- return appointment;
182
- }
183
- return {
184
- ...appointment,
185
- resourceId: drag.resourceId,
186
- startMin: drag.startMin,
187
- endMin: drag.endMin,
188
- start: dateAtMinute(selectedDate, dayStartAbs, drag.startMin),
189
- end: dateAtMinute(selectedDate, dayStartAbs, drag.endMin)
190
- };
191
- });
192
- }, [drag, renderAppts, selectedDate, dayStartAbs]);
193
- const laidOutByResource = React__namespace.useMemo(() => {
194
- const byResource = /* @__PURE__ */ new Map();
195
- for (const resource of resources) {
196
- const events = effectiveAppts.filter((appointment) => appointment.resourceId === resource.id);
197
- byResource.set(resource.id, layoutOverlaps(events));
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;
198
267
  }
199
- return byResource;
200
- }, [effectiveAppts, resources]);
201
- const appointmentBgByResource = React__namespace.useMemo(() => {
202
- const map = /* @__PURE__ */ new Map();
203
- if (!getResourceAppointmentBackground) {
204
- return map;
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)
275
+ };
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;
205
298
  }
206
- for (const resource of resources) {
207
- map.set(resource.id, getResourceAppointmentBackground(resource));
299
+ const appearance = getResourceAppointmentAppearance?.(resource);
300
+ if (appearance) {
301
+ map.set(resource.id, appearance);
302
+ continue;
208
303
  }
209
- return map;
210
- }, [getResourceAppointmentBackground, resources]);
211
- const pointerToMin = (resourceId, clientY) => {
212
- const col = colRefs.current[String(resourceId)];
213
- if (!col) {
214
- return 0;
304
+ const tokenClass = appointmentColorTokenClassByResource.get(resource.id);
305
+ if (tokenClass) {
306
+ map.set(resource.id, { className: tokenClass });
307
+ continue;
215
308
  }
216
- const y = clientY - col.getBoundingClientRect().top - TOP_PAD;
217
- return clamp(snap(y / PX_PER_MIN), 0, dayMinutes);
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
218
348
  };
219
- const pointerToResourceId = (clientX, fallbackResourceId) => {
220
- for (const resource of resources) {
221
- const col = colRefs.current[String(resource.id)];
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)];
222
366
  if (!col) {
223
- continue;
367
+ return 0;
224
368
  }
225
- const rect = col.getBoundingClientRect();
226
- if (clientX >= rect.left && clientX <= rect.right) {
227
- return resource.id;
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
+ }
228
385
  }
229
- }
230
- return fallbackResourceId;
231
- };
232
- const hasOverlap = (appointmentId, resourceId, startMin, endMin) => {
233
- const events = renderAppts.filter((appointment) => appointment.resourceId === resourceId && appointment.id !== appointmentId);
234
- return events.some((appointment) => overlaps(appointment, { startMin, endMin }));
235
- };
236
- const persistDrag = async (state) => {
237
- const target = appointmentMap.get(state.appointmentId);
238
- if (!target) {
239
- return;
240
- }
241
- if (hasOverlap(state.appointmentId, state.resourceId, state.startMin, state.endMin)) {
242
- return;
243
- }
244
- const next = {
245
- resourceId: state.resourceId,
246
- start: dateAtMinute(selectedDate, dayStartAbs, state.startMin),
247
- end: dateAtMinute(selectedDate, dayStartAbs, state.endMin)
248
- };
249
- if (onAppointmentChange) {
250
- await onAppointmentChange({
251
- kind: state.kind,
252
- appointment: target.raw,
253
- previous: {
254
- resourceId: target.resourceId,
255
- start: target.start,
256
- end: target.end
257
- },
258
- next
259
- });
260
- return;
261
- }
262
- if (onPersistMoveResize) {
263
- await onPersistMoveResize({
264
- appointment: target.raw,
265
- resourceId: next.resourceId,
266
- start: next.start,
267
- end: next.end
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;
402
+ }
403
+ if (hasOverlap(state.appointmentId, state.resourceId, state.startMin, state.endMin)) {
404
+ return;
405
+ }
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;
423
+ }
424
+ if (onPersistMoveResize) {
425
+ await onPersistMoveResize({
426
+ appointment: target.raw,
427
+ resourceId: next.resourceId,
428
+ start: next.start,
429
+ end: next.end
430
+ });
431
+ }
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;
439
+ }
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
268
452
  });
269
- }
270
- };
271
- const onApptPointerDown = (event, appointment) => {
272
- if (event.button !== 0) {
273
- return;
274
- }
275
- event.stopPropagation();
276
- suppressClickRef.current = false;
277
- const pointerMin = pointerToMin(appointment.resourceId, event.clientY);
278
- setDrag({
279
- kind: "move",
280
- appointmentId: appointment.id,
281
- pointerId: event.pointerId,
282
- resourceId: appointment.resourceId,
283
- durationMin: Math.max(MIN_EVENT_MIN, appointment.endMin - appointment.startMin),
284
- offsetMin: pointerMin - appointment.startMin,
285
- startMin: appointment.startMin,
286
- endMin: appointment.endMin
287
- });
288
- };
289
- const onResizePointerDown = (event, appointment) => {
453
+ },
454
+ [pointerToMin]
455
+ );
456
+ const onResizePointerDown = React2__namespace.useCallback((event, appointment) => {
290
457
  if (event.button !== 0) {
291
458
  return;
292
459
  }
@@ -300,39 +467,42 @@ function Scheduler({
300
467
  startMin: appointment.startMin,
301
468
  endMin: appointment.endMin
302
469
  });
303
- };
304
- const onGlobalPointerMove = (event) => {
305
- if (drag.kind === "none") {
306
- return;
307
- }
308
- if (drag.kind === "move") {
309
- const nextResourceId = pointerToResourceId(event.clientX, drag.resourceId);
310
- const pointerMin2 = pointerToMin(nextResourceId, event.clientY);
311
- const rawStart = pointerMin2 - drag.offsetMin;
312
- const nextStartMin = clamp(snap(rawStart), 0, dayMinutes - drag.durationMin);
313
- const nextEndMin2 = nextStartMin + drag.durationMin;
314
- 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) {
315
496
  suppressClickRef.current = true;
316
497
  setDrag({
317
498
  ...drag,
318
- resourceId: nextResourceId,
319
- startMin: nextStartMin,
320
- endMin: nextEndMin2
499
+ endMin: nextEndMin
321
500
  });
322
501
  }
323
- return;
324
- }
325
- const pointerMin = pointerToMin(drag.resourceId, event.clientY);
326
- const nextEndMin = clamp(snap(pointerMin), drag.startMin + MIN_EVENT_MIN, dayMinutes);
327
- if (nextEndMin !== drag.endMin) {
328
- suppressClickRef.current = true;
329
- setDrag({
330
- ...drag,
331
- endMin: nextEndMin
332
- });
333
- }
334
- };
335
- const onGlobalPointerUp = () => {
502
+ },
503
+ [dayMinutes, drag, pointerToMin, pointerToResourceId]
504
+ );
505
+ const onGlobalPointerUp = React2__namespace.useCallback(() => {
336
506
  if (drag.kind === "none") {
337
507
  return;
338
508
  }
@@ -341,89 +511,281 @@ function Scheduler({
341
511
  if (suppressClickRef.current) {
342
512
  void persistDrag(current);
343
513
  }
514
+ }, [drag, persistDrag]);
515
+ return {
516
+ colRefs,
517
+ drag,
518
+ onApptPointerDown,
519
+ onGlobalPointerMove,
520
+ onGlobalPointerUp,
521
+ onResizePointerDown,
522
+ suppressClickRef
344
523
  };
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();
546
+ }
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 });
345
665
  return /* @__PURE__ */ jsxRuntime.jsxs(
346
666
  "section",
347
667
  {
348
- className: `rfs-root ${drag.kind === "move" ? "rfs-root--drag-move" : ""} ${drag.kind === "resize" ? "rfs-root--drag-resize" : ""}`,
668
+ className: `rfs-root rounded-lg bg-background text-foreground ${drag.kind === "move" ? "rfs-root--drag-move" : ""} ${drag.kind === "resize" ? "rfs-root--drag-resize" : ""}`,
349
669
  onPointerMove: onGlobalPointerMove,
350
670
  onPointerUp: onGlobalPointerUp,
351
671
  onPointerCancel: onGlobalPointerUp,
352
672
  children: [
353
673
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rfs-toolbar", children: [
354
- /* @__PURE__ */ jsxRuntime.jsx("button", { className: "rfs-btn", type: "button", onClick: () => onSelectedDateChange(shiftDays(selectedDate, -1)), children: prevButtonLabel }),
355
- renderDatePicker ? renderDatePicker({ selectedDate, onSelectedDateChange }) : /* @__PURE__ */ jsxRuntime.jsx("strong", { className: "rfs-date-label", children: selectedISO }),
356
- /* @__PURE__ */ jsxRuntime.jsx("button", { className: "rfs-btn", type: "button", onClick: () => onSelectedDateChange(shiftDays(selectedDate, 1)), children: nextButtonLabel })
674
+ /* @__PURE__ */ jsxRuntime.jsx(
675
+ "button",
676
+ {
677
+ className: "rfs-btn border-border bg-card text-foreground hover:bg-muted",
678
+ type: "button",
679
+ onClick: () => onSelectedDateChange(shiftDays(selectedDate, -1)),
680
+ children: prevButtonLabel
681
+ }
682
+ ),
683
+ (renderDatePicker ?? defaultRenderDatePicker)({ selectedDate, onSelectedDateChange }),
684
+ /* @__PURE__ */ jsxRuntime.jsx(
685
+ "button",
686
+ {
687
+ className: "rfs-btn border-border bg-card text-foreground hover:bg-muted",
688
+ type: "button",
689
+ onClick: () => onSelectedDateChange(shiftDays(selectedDate, 1)),
690
+ children: nextButtonLabel
691
+ }
692
+ )
357
693
  ] }),
358
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rfs-shell", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { minWidth: gridMinWidth }, children: [
359
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rfs-header", children: [
360
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rfs-gutter", style: { width: GUTTER_W } }),
361
- resources.map((resource) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rfs-resource-header", style: { minWidth: RESOURCE_MIN_W, flex: 1 }, children: [
362
- renderResourceHeader(resource),
363
- isNameLike(resource) ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rfs-resource-subtitle", children: fullName(resource) }) : null
364
- ] }, String(resource.id)))
694
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rfs-shell border-border bg-card", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { minWidth: gridMinWidth }, children: [
695
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rfs-header border-border bg-card", children: [
696
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rfs-gutter border-border", style: { width: GUTTER_W } }),
697
+ resources.map((resource) => /* @__PURE__ */ jsxRuntime.jsxs(
698
+ "div",
699
+ {
700
+ className: "rfs-resource-header border-border bg-card text-foreground",
701
+ style: { minWidth: RESOURCE_MIN_W, flex: 1 },
702
+ children: [
703
+ (renderResourceHeader ?? defaultResourceHeaderRenderer)(resource),
704
+ isNameLike(resource) ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rfs-resource-subtitle text-muted-foreground", children: fullName(resource) }) : null
705
+ ]
706
+ },
707
+ String(resource.id)
708
+ ))
365
709
  ] }),
366
710
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rfs-body", children: [
367
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rfs-times", style: { width: GUTTER_W, height: gridHeight }, children: Array.from({ length: Math.floor(dayMinutes / 60) + 1 }).map((_, i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rfs-time-tick", style: { top: TOP_PAD + i * 60 * PX_PER_MIN }, children: [
368
- String(Math.floor((dayStartAbs + i * 60) / 60)).padStart(2, "0"),
369
- ":",
370
- String((dayStartAbs + i * 60) % 60).padStart(2, "0")
371
- ] }, `tick-${i}`)) }),
372
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rfs-grid", children: resources.map((resource) => /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rfs-col", style: { minWidth: RESOURCE_MIN_W, flex: 1 }, children: /* @__PURE__ */ jsxRuntime.jsxs(
711
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rfs-times border-border bg-card", style: { width: GUTTER_W, height: gridHeight }, children: Array.from({ length: Math.floor(dayMinutes / 60) + 1 }).map((_, i) => /* @__PURE__ */ jsxRuntime.jsxs(
373
712
  "div",
374
713
  {
375
- ref: (el) => {
376
- colRefs.current[String(resource.id)] = el;
377
- },
378
- className: "rfs-col-inner",
379
- style: { height: gridHeight },
714
+ className: "rfs-time-tick text-muted-foreground",
715
+ style: { top: TOP_PAD + i * 60 * PX_PER_MIN },
380
716
  children: [
381
- Array.from({ length: Math.floor(dayMinutes / 60) + 1 }).map((_, i) => /* @__PURE__ */ jsxRuntime.jsx(
382
- "div",
383
- {
384
- className: "rfs-hour-line",
385
- style: { top: TOP_PAD + i * 60 * PX_PER_MIN }
386
- },
387
- `major-${String(resource.id)}-${i}`
388
- )),
389
- Array.from({ length: Math.floor(dayMinutes / STEP_MIN) + 1 }).map((_, i) => /* @__PURE__ */ jsxRuntime.jsx(
390
- "div",
391
- {
392
- className: "rfs-slot-line",
393
- style: { top: TOP_PAD + i * STEP_MIN * PX_PER_MIN }
717
+ String(Math.floor((dayStartAbs + i * 60) / 60)).padStart(2, "0"),
718
+ ":",
719
+ String((dayStartAbs + i * 60) % 60).padStart(2, "0")
720
+ ]
721
+ },
722
+ `tick-${i}`
723
+ )) }),
724
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rfs-grid", children: resources.map((resource) => /* @__PURE__ */ jsxRuntime.jsx(
725
+ "div",
726
+ {
727
+ className: "rfs-col border-border bg-card",
728
+ style: { minWidth: RESOURCE_MIN_W, flex: 1 },
729
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
730
+ "div",
731
+ {
732
+ ref: (el) => {
733
+ colRefs.current[String(resource.id)] = el;
394
734
  },
395
- `minor-${String(resource.id)}-${i}`
396
- )),
397
- (laidOutByResource.get(resource.id) ?? []).map((appointment) => {
398
- const laneWidthPct = 100 / appointment.lanes;
399
- const top = TOP_PAD + appointment.startMin * PX_PER_MIN;
400
- const height = Math.max(MIN_EVENT_MIN * PX_PER_MIN, (appointment.endMin - appointment.startMin) * PX_PER_MIN);
401
- const appointmentBackgroundColor = appointmentBgByResource.get(resource.id);
402
- return /* @__PURE__ */ jsxRuntime.jsx(
403
- "div",
404
- {
405
- className: "rfs-card-slot",
406
- style: {
407
- top,
408
- left: `${laneWidthPct * appointment.lane}%`,
409
- width: `${laneWidthPct}%`,
410
- height
735
+ className: "rfs-col-inner bg-card",
736
+ style: { height: gridHeight },
737
+ children: [
738
+ Array.from({ length: Math.floor(dayMinutes / 60) + 1 }).map((_, i) => /* @__PURE__ */ jsxRuntime.jsx(
739
+ "div",
740
+ {
741
+ className: "rfs-hour-line border-border/70",
742
+ style: { top: TOP_PAD + i * 60 * PX_PER_MIN }
411
743
  },
412
- children: renderAppointment({
413
- appointment,
414
- onPointerDown: (event) => onApptPointerDown(event, appointment),
415
- onResizePointerDown: (event) => onResizePointerDown(event, appointment),
416
- appointmentBackgroundColor,
417
- drag,
418
- suppressClickRef
419
- })
420
- },
421
- String(appointment.id)
422
- );
423
- })
424
- ]
425
- }
426
- ) }, String(resource.id))) })
744
+ `major-${String(resource.id)}-${i}`
745
+ )),
746
+ Array.from({ length: Math.floor(dayMinutes / STEP_MIN) + 1 }).map((_, i) => /* @__PURE__ */ jsxRuntime.jsx(
747
+ "div",
748
+ {
749
+ className: "rfs-slot-line border-border/50",
750
+ style: { top: TOP_PAD + i * STEP_MIN * PX_PER_MIN }
751
+ },
752
+ `minor-${String(resource.id)}-${i}`
753
+ )),
754
+ (laidOutByResource.get(resource.id) ?? []).map((appointment) => {
755
+ const laneWidthPct = 100 / appointment.lanes;
756
+ const top = TOP_PAD + appointment.startMin * PX_PER_MIN;
757
+ const height = Math.max(MIN_EVENT_MIN * PX_PER_MIN, (appointment.endMin - appointment.startMin) * PX_PER_MIN);
758
+ const appointmentAppearance = appointmentAppearanceByResource.get(resource.id);
759
+ const appointmentBackgroundColor = appointmentBgByResource.get(resource.id);
760
+ return /* @__PURE__ */ jsxRuntime.jsx(
761
+ "div",
762
+ {
763
+ className: "rfs-card-slot",
764
+ style: {
765
+ top,
766
+ left: `${laneWidthPct * appointment.lane}%`,
767
+ width: `${laneWidthPct}%`,
768
+ height
769
+ },
770
+ children: (renderAppointment ?? defaultRenderAppointment)({
771
+ appointment,
772
+ onPointerDown: (event) => onApptPointerDown(event, appointment),
773
+ onResizePointerDown: (event) => onResizePointerDown(event, appointment),
774
+ appointmentAppearance,
775
+ appointmentBackgroundColor,
776
+ drag,
777
+ suppressClickRef
778
+ })
779
+ },
780
+ String(appointment.id)
781
+ );
782
+ })
783
+ ]
784
+ }
785
+ )
786
+ },
787
+ String(resource.id)
788
+ )) })
427
789
  ] })
428
790
  ] }) })
429
791
  ]
@@ -431,6 +793,19 @@ function Scheduler({
431
793
  );
432
794
  }
433
795
 
796
+ // src/utils/scheduler-appointment-change.utils.ts
797
+ function applySchedulerAppointmentChange(appointments, change, options) {
798
+ return appointments.map((item) => {
799
+ if (options.getId(item) !== options.getId(change.appointment)) {
800
+ return item;
801
+ }
802
+ const withResource = options.setResourceId(item, change.next.resourceId);
803
+ const withStart = options.setStart(withResource, change.next.start);
804
+ return options.setEnd(withStart, change.next.end);
805
+ });
806
+ }
807
+
434
808
  exports.Scheduler = Scheduler;
809
+ exports.applySchedulerAppointmentChange = applySchedulerAppointmentChange;
435
810
  //# sourceMappingURL=index.cjs.map
436
811
  //# sourceMappingURL=index.cjs.map