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