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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -90,7 +90,8 @@ export function SchedulerExample() {
90
90
  ```
91
91
 
92
92
  `renderAppointment` is optional. If you omit it, the scheduler uses the built-in appointment card with light/dark mode support.
93
- `renderDatePicker` is optional. If you omit it, the scheduler uses a built-in native date input.
93
+ `renderToolbar` is optional. If you omit it, the scheduler uses a built-in shadcn-style toolbar with prev/next buttons and a calendar popover date picker.
94
+ `renderDatePicker` is optional. If you omit it, the default toolbar uses the built-in calendar popover date picker.
94
95
  `onAppointmentChange` is a simple callback: use it to update your state and run your own side effects.
95
96
 
96
97
  Main props you need to pass:
@@ -104,8 +105,9 @@ Main props you need to pass:
104
105
  - `appointmentColorTokenClassMap`: optional map to override token -> class resolution.
105
106
  - `getResourceAppointmentAppearance`: optional low-level function to provide appointment `className` per resource.
106
107
  - `getResourceAppointmentBackground`: legacy optional function to provide appointment background as a string.
108
+ - `renderToolbar`: optional render hook to replace the entire scheduler toolbar/header controls.
107
109
  - `prevButtonLabel` and `nextButtonLabel`: custom labels for toolbar navigation buttons.
108
- - `renderDatePicker`: optional render hook for a custom date picker UI (for example shadcn calendar).
110
+ - `renderDatePicker`: optional render hook for the date picker slot used by the default toolbar.
109
111
  - `renderResourceHeader`: optional render hook for resource column headers.
110
112
  - `renderAppointment`: optional render hook to fully customize appointment cards.
111
113
 
@@ -164,7 +166,32 @@ Main props you need to pass:
164
166
  />
165
167
  ```
166
168
 
167
- ### shadcn Calendar Date Picker
169
+ ### Custom toolbar
170
+
171
+ ```tsx
172
+ <Scheduler
173
+ // ...other props
174
+ renderToolbar={({ selectedDate, goToPreviousDay, goToNextDay, defaultDatePicker }) => (
175
+ <div className="mb-4 flex items-center justify-between gap-3 rounded-xl border border-border bg-card px-4 py-3">
176
+ <div>
177
+ <div className="text-sm font-medium">{selectedDate.toDateString()}</div>
178
+ <div className="text-xs text-muted-foreground">Team schedule</div>
179
+ </div>
180
+ <div className="flex items-center gap-2">
181
+ <button className="rounded-md border px-3 py-2 text-sm" type="button" onClick={goToPreviousDay}>
182
+ Previous
183
+ </button>
184
+ {defaultDatePicker}
185
+ <button className="rounded-md border px-3 py-2 text-sm" type="button" onClick={goToNextDay}>
186
+ Next
187
+ </button>
188
+ </div>
189
+ </div>
190
+ )}
191
+ />
192
+ ```
193
+
194
+ ### Custom date picker slot
168
195
 
169
196
  ```tsx
170
197
  import { Button } from "@/components/ui/button";
@@ -248,4 +275,5 @@ This repo uses [Changesets](https://github.com/changesets/changesets) and GitHub
248
275
  1. Create a changeset: `npx changeset`
249
276
  2. Merge to `main`
250
277
  3. The `Release` workflow opens/updates a release PR
251
- 4. Merging that PR publishes to npm (via npm Trusted Publishing/OIDC, or `NPM_TOKEN` if configured)
278
+ 4. Merging that PR publishes to npm via npm Trusted Publishing and GitHub OIDC
279
+ 4. Merging that PR publishes to npm via npm Trusted Publishing and GitHub OIDC
package/dist/index.cjs CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  var jsxRuntime = require('react/jsx-runtime');
4
4
  var dateFns = require('date-fns');
5
- var React2 = require('react');
5
+ var React5 = require('react');
6
6
 
7
7
  function _interopNamespace(e) {
8
8
  if (e && e.__esModule) return e;
@@ -22,7 +22,7 @@ function _interopNamespace(e) {
22
22
  return Object.freeze(n);
23
23
  }
24
24
 
25
- var React2__namespace = /*#__PURE__*/_interopNamespace(React2);
25
+ var React5__namespace = /*#__PURE__*/_interopNamespace(React5);
26
26
 
27
27
  // src/components/defaultRenderAppointment/index.tsx
28
28
  function defaultRenderAppointment({
@@ -55,6 +55,206 @@ function defaultRenderAppointment({
55
55
  ] });
56
56
  }
57
57
 
58
+ // src/lib/cn.ts
59
+ function cn(...inputs) {
60
+ return inputs.filter(Boolean).join(" ");
61
+ }
62
+ var variantClasses = {
63
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
64
+ outline: "border border-border bg-card text-foreground hover:bg-muted",
65
+ ghost: "text-foreground hover:bg-muted"
66
+ };
67
+ var sizeClasses = {
68
+ default: "h-9 px-4 py-2",
69
+ sm: "h-8 px-3 text-xs",
70
+ icon: "h-8 w-8"
71
+ };
72
+ var Button = React5__namespace.forwardRef(function Button2({ className, variant = "default", size = "default", type = "button", ...props }, ref) {
73
+ return /* @__PURE__ */ jsxRuntime.jsx(
74
+ "button",
75
+ {
76
+ ref,
77
+ type,
78
+ className: cn(
79
+ "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
80
+ variantClasses[variant],
81
+ sizeClasses[size],
82
+ className
83
+ ),
84
+ ...props
85
+ }
86
+ );
87
+ });
88
+ function Calendar({
89
+ selected,
90
+ onSelect
91
+ }) {
92
+ const [month, setMonth] = React5__namespace.useState(selected ?? /* @__PURE__ */ new Date());
93
+ React5__namespace.useEffect(() => {
94
+ if (selected) {
95
+ setMonth(selected);
96
+ }
97
+ }, [selected]);
98
+ const monthStart = dateFns.startOfMonth(month);
99
+ const calendarStart = dateFns.startOfWeek(monthStart, { weekStartsOn: 1 });
100
+ const calendarEnd = dateFns.endOfWeek(dateFns.endOfMonth(month), { weekStartsOn: 1 });
101
+ const days = [];
102
+ for (let day = calendarStart; day <= calendarEnd; day = dateFns.addDays(day, 1)) {
103
+ days.push(day);
104
+ }
105
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-72 rounded-md bg-popover", children: [
106
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-2 flex items-center justify-between", children: [
107
+ /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "ghost", size: "icon", "aria-label": "Previous month", onClick: () => setMonth(dateFns.addMonths(month, -1)), children: "<" }),
108
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm font-medium", children: dateFns.format(month, "MMMM yyyy") }),
109
+ /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "ghost", size: "icon", "aria-label": "Next month", onClick: () => setMonth(dateFns.addMonths(month, 1)), children: ">" })
110
+ ] }),
111
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-7 gap-1 text-center text-xs text-muted-foreground", children: ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"].map((label) => /* @__PURE__ */ jsxRuntime.jsx("div", { className: "py-1", children: label }, label)) }),
112
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-7 gap-1", children: days.map((day) => {
113
+ const isSelected = selected ? dateFns.isSameDay(day, selected) : false;
114
+ return /* @__PURE__ */ jsxRuntime.jsx(
115
+ Button,
116
+ {
117
+ variant: isSelected ? "default" : "ghost",
118
+ size: "icon",
119
+ className: cn(
120
+ "text-xs",
121
+ !dateFns.isSameMonth(day, month) && "text-muted-foreground opacity-60",
122
+ dateFns.isToday(day) && !isSelected && "border border-border"
123
+ ),
124
+ onClick: () => onSelect?.(day),
125
+ children: dateFns.format(day, "d")
126
+ },
127
+ day.toISOString()
128
+ );
129
+ }) })
130
+ ] });
131
+ }
132
+ var PopoverContext = React5__namespace.createContext(null);
133
+ function usePopoverContext() {
134
+ const context = React5__namespace.useContext(PopoverContext);
135
+ if (!context) {
136
+ throw new Error("Popover components must be used within Popover.");
137
+ }
138
+ return context;
139
+ }
140
+ function Popover({
141
+ children,
142
+ open: controlledOpen,
143
+ onOpenChange
144
+ }) {
145
+ const [uncontrolledOpen, setUncontrolledOpen] = React5__namespace.useState(false);
146
+ const triggerRef = React5__namespace.useRef(null);
147
+ const contentRef = React5__namespace.useRef(null);
148
+ const open = controlledOpen ?? uncontrolledOpen;
149
+ const setOpen = React5__namespace.useCallback(
150
+ (value) => {
151
+ const nextValue = typeof value === "function" ? value(open) : value;
152
+ if (controlledOpen === void 0) {
153
+ setUncontrolledOpen(nextValue);
154
+ }
155
+ onOpenChange?.(nextValue);
156
+ },
157
+ [controlledOpen, onOpenChange, open]
158
+ );
159
+ React5__namespace.useEffect(() => {
160
+ if (!open) {
161
+ return void 0;
162
+ }
163
+ const handlePointerDown = (event) => {
164
+ const target = event.target;
165
+ if (triggerRef.current?.contains(target) || contentRef.current?.contains(target)) {
166
+ return;
167
+ }
168
+ setOpen(false);
169
+ };
170
+ const handleKeyDown = (event) => {
171
+ if (event.key === "Escape") {
172
+ setOpen(false);
173
+ }
174
+ };
175
+ document.addEventListener("mousedown", handlePointerDown);
176
+ document.addEventListener("keydown", handleKeyDown);
177
+ return () => {
178
+ document.removeEventListener("mousedown", handlePointerDown);
179
+ document.removeEventListener("keydown", handleKeyDown);
180
+ };
181
+ }, [open, setOpen]);
182
+ return /* @__PURE__ */ jsxRuntime.jsx(PopoverContext.Provider, { value: { open, setOpen, triggerRef, contentRef }, children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "relative inline-flex", children }) });
183
+ }
184
+ function PopoverTrigger({
185
+ children,
186
+ asChild = false
187
+ }) {
188
+ const { open, setOpen, triggerRef } = usePopoverContext();
189
+ if (asChild && React5__namespace.isValidElement(children)) {
190
+ const child = children;
191
+ return React5__namespace.cloneElement(child, {
192
+ ref: triggerRef,
193
+ "aria-expanded": open,
194
+ onClick: (event) => {
195
+ child.props.onClick?.(event);
196
+ setOpen((current) => !current);
197
+ }
198
+ });
199
+ }
200
+ return /* @__PURE__ */ jsxRuntime.jsx(
201
+ "button",
202
+ {
203
+ ref: triggerRef,
204
+ type: "button",
205
+ "aria-expanded": open,
206
+ onClick: () => setOpen((current) => !current),
207
+ children
208
+ }
209
+ );
210
+ }
211
+ function PopoverContent({
212
+ children,
213
+ className,
214
+ align = "start"
215
+ }) {
216
+ const { open, contentRef } = usePopoverContext();
217
+ if (!open) {
218
+ return null;
219
+ }
220
+ return /* @__PURE__ */ jsxRuntime.jsx(
221
+ "div",
222
+ {
223
+ ref: contentRef,
224
+ className: cn(
225
+ "absolute top-full z-50 mt-2 min-w-72 rounded-md border border-border bg-popover p-2 text-popover-foreground shadow-md outline-none",
226
+ align === "start" && "left-0",
227
+ align === "center" && "left-1/2 -translate-x-1/2",
228
+ align === "end" && "right-0",
229
+ className
230
+ ),
231
+ children
232
+ }
233
+ );
234
+ }
235
+ function defaultRenderDatePicker({ selectedDate, onSelectedDateChange }) {
236
+ return /* @__PURE__ */ jsxRuntime.jsxs(Popover, { children: [
237
+ /* @__PURE__ */ jsxRuntime.jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "outline", size: "sm", className: "min-w-40 justify-start text-left font-normal", children: dateFns.format(selectedDate, "PPP") }) }),
238
+ /* @__PURE__ */ jsxRuntime.jsx(PopoverContent, { className: "w-auto p-2", align: "center", children: /* @__PURE__ */ jsxRuntime.jsx(Calendar, { selected: selectedDate, onSelect: onSelectedDateChange }) })
239
+ ] });
240
+ }
241
+ function defaultRenderResourceHeader({ resource }) {
242
+ return /* @__PURE__ */ jsxRuntime.jsx("strong", { children: resource.label });
243
+ }
244
+ function defaultRenderToolbar({
245
+ goToPreviousDay,
246
+ goToNextDay,
247
+ defaultDatePicker,
248
+ prevButtonLabel = "Prev",
249
+ nextButtonLabel = "Next"
250
+ }) {
251
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rfs-toolbar", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 rounded-lg border border-border bg-card p-1 shadow-xs", children: [
252
+ /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "outline", size: "sm", onClick: goToPreviousDay, children: prevButtonLabel }),
253
+ defaultDatePicker,
254
+ /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "outline", size: "sm", onClick: goToNextDay, children: nextButtonLabel })
255
+ ] }) });
256
+ }
257
+
58
258
  // src/components/constants.ts
59
259
  var STEP_MIN = 15;
60
260
  var MIN_EVENT_MIN = 15;
@@ -63,8 +263,6 @@ var GUTTER_W = 72;
63
263
  var TOP_PAD = 12;
64
264
  var BOTTOM_PAD = 24;
65
265
  var RESOURCE_MIN_W = 420;
66
-
67
- // src/utils/scheduler-core.utils.ts
68
266
  function isNameLike(resource) {
69
267
  if (!resource || typeof resource !== "object") {
70
268
  return false;
@@ -72,22 +270,6 @@ function isNameLike(resource) {
72
270
  const maybe = resource;
73
271
  return typeof maybe.firstName === "string" || typeof maybe.lastName === "string";
74
272
  }
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
273
  var DEFAULT_APPOINTMENT_COLOR_TOKEN_CLASS_MAP = {
92
274
  slate: "bg-slate-100 dark:bg-slate-900/40",
93
275
  gray: "bg-gray-100 dark:bg-gray-900/40",
@@ -198,26 +380,6 @@ function layoutOverlaps(blocks) {
198
380
  }
199
381
  return output;
200
382
  }
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
383
 
222
384
  // src/utils/scheduler-data.utils.ts
223
385
  function buildResourceMap({
@@ -321,13 +483,13 @@ function useSchedulerBaseData({
321
483
  resources,
322
484
  selectedDate
323
485
  }) {
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(() => {
486
+ const dayStartAbs = React5__namespace.useMemo(() => parseHHMM(dayStart), [dayStart]);
487
+ const dayEndAbs = React5__namespace.useMemo(() => parseHHMM(dayEnd), [dayEnd]);
488
+ const dayMinutes = React5__namespace.useMemo(() => Math.max(0, dayEndAbs - dayStartAbs), [dayEndAbs, dayStartAbs]);
489
+ const gridHeight = React5__namespace.useMemo(() => TOP_PAD + dayMinutes * PX_PER_MIN + BOTTOM_PAD, [dayMinutes]);
490
+ const gridMinWidth = React5__namespace.useMemo(() => GUTTER_W + Math.max(resources.length, 1) * RESOURCE_MIN_W, [resources.length]);
491
+ const selectedISO = React5__namespace.useMemo(() => dayISO(selectedDate), [selectedDate]);
492
+ const renderAppts = React5__namespace.useMemo(() => {
331
493
  return normalizeAppointments({
332
494
  adapter,
333
495
  appointments,
@@ -336,7 +498,7 @@ function useSchedulerBaseData({
336
498
  selectedISO
337
499
  });
338
500
  }, [adapter, appointments, dayMinutes, dayStartAbs, selectedISO]);
339
- const appointmentMap = React2__namespace.useMemo(() => buildAppointmentMap(renderAppts), [renderAppts]);
501
+ const appointmentMap = React5__namespace.useMemo(() => buildAppointmentMap(renderAppts), [renderAppts]);
340
502
  return {
341
503
  appointmentMap,
342
504
  dayEndAbs,
@@ -357,10 +519,10 @@ function useSchedulerInteractions({
357
519
  resources,
358
520
  selectedDate
359
521
  }) {
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(
522
+ const [drag, setDrag] = React5__namespace.useState({ kind: "none" });
523
+ const colRefs = React5__namespace.useRef({});
524
+ const suppressClickRef = React5__namespace.useRef(false);
525
+ const pointerToMin = React5__namespace.useCallback(
364
526
  (resourceId, clientY) => {
365
527
  const col = colRefs.current[String(resourceId)];
366
528
  if (!col) {
@@ -371,7 +533,7 @@ function useSchedulerInteractions({
371
533
  },
372
534
  [dayMinutes]
373
535
  );
374
- const pointerToResourceId = React2__namespace.useCallback(
536
+ const pointerToResourceId = React5__namespace.useCallback(
375
537
  (clientX, fallbackResourceId) => {
376
538
  for (const resource of resources) {
377
539
  const col = colRefs.current[String(resource.id)];
@@ -387,14 +549,14 @@ function useSchedulerInteractions({
387
549
  },
388
550
  [resources]
389
551
  );
390
- const hasOverlap = React2__namespace.useCallback(
552
+ const hasOverlap = React5__namespace.useCallback(
391
553
  (appointmentId, resourceId, startMin, endMin) => {
392
554
  const events = renderAppts.filter((appointment) => appointment.resourceId === resourceId && appointment.id !== appointmentId);
393
555
  return events.some((appointment) => overlaps(appointment, { startMin, endMin }));
394
556
  },
395
557
  [renderAppts]
396
558
  );
397
- const persistDrag = React2__namespace.useCallback(
559
+ const persistDrag = React5__namespace.useCallback(
398
560
  async (state) => {
399
561
  const target = appointmentMap.get(state.appointmentId);
400
562
  if (!target) {
@@ -432,7 +594,7 @@ function useSchedulerInteractions({
432
594
  },
433
595
  [appointmentMap, dayStartAbs, hasOverlap, onAppointmentChange, onPersistMoveResize, selectedDate]
434
596
  );
435
- const onApptPointerDown = React2__namespace.useCallback(
597
+ const onApptPointerDown = React5__namespace.useCallback(
436
598
  (event, appointment) => {
437
599
  if (event.button !== 0) {
438
600
  return;
@@ -453,7 +615,7 @@ function useSchedulerInteractions({
453
615
  },
454
616
  [pointerToMin]
455
617
  );
456
- const onResizePointerDown = React2__namespace.useCallback((event, appointment) => {
618
+ const onResizePointerDown = React5__namespace.useCallback((event, appointment) => {
457
619
  if (event.button !== 0) {
458
620
  return;
459
621
  }
@@ -468,7 +630,7 @@ function useSchedulerInteractions({
468
630
  endMin: appointment.endMin
469
631
  });
470
632
  }, []);
471
- const onGlobalPointerMove = React2__namespace.useCallback(
633
+ const onGlobalPointerMove = React5__namespace.useCallback(
472
634
  (event) => {
473
635
  if (drag.kind === "none") {
474
636
  return;
@@ -502,7 +664,7 @@ function useSchedulerInteractions({
502
664
  },
503
665
  [dayMinutes, drag, pointerToMin, pointerToResourceId]
504
666
  );
505
- const onGlobalPointerUp = React2__namespace.useCallback(() => {
667
+ const onGlobalPointerUp = React5__namespace.useCallback(() => {
506
668
  if (drag.kind === "none") {
507
669
  return;
508
670
  }
@@ -534,13 +696,13 @@ function useSchedulerPresentationData({
534
696
  resources,
535
697
  selectedDate
536
698
  }) {
537
- const effectiveAppts = React2__namespace.useMemo(() => {
699
+ const effectiveAppts = React5__namespace.useMemo(() => {
538
700
  return applyDragToAppointments(renderAppts, drag, selectedDate, dayStartAbs);
539
701
  }, [dayStartAbs, drag, renderAppts, selectedDate]);
540
- const laidOutByResource = React2__namespace.useMemo(() => {
702
+ const laidOutByResource = React5__namespace.useMemo(() => {
541
703
  return buildLaidOutByResource(resources, effectiveAppts);
542
704
  }, [effectiveAppts, resources]);
543
- const appointmentBgByResource = React2__namespace.useMemo(() => {
705
+ const appointmentBgByResource = React5__namespace.useMemo(() => {
544
706
  if (!getResourceAppointmentBackground) {
545
707
  return /* @__PURE__ */ new Map();
546
708
  }
@@ -551,7 +713,7 @@ function useSchedulerPresentationData({
551
713
  toKey: (resource) => resource.id
552
714
  });
553
715
  }, [getResourceAppointmentBackground, resources]);
554
- const appointmentColorTokenClassByResource = React2__namespace.useMemo(() => {
716
+ const appointmentColorTokenClassByResource = React5__namespace.useMemo(() => {
555
717
  if (!getResourceAppointmentColorToken) {
556
718
  return /* @__PURE__ */ new Map();
557
719
  }
@@ -569,7 +731,7 @@ function useSchedulerPresentationData({
569
731
  toKey: (resource) => resource.id
570
732
  });
571
733
  }, [appointmentColorTokenClassMap, getResourceAppointmentColorToken, resources]);
572
- const appointmentAppearanceByResource = React2__namespace.useMemo(() => {
734
+ const appointmentAppearanceByResource = React5__namespace.useMemo(() => {
573
735
  return buildAppointmentAppearanceByResource(
574
736
  resources,
575
737
  resourceAppointmentClassMap,
@@ -606,6 +768,7 @@ function Scheduler({
606
768
  getResourceAppointmentColorToken,
607
769
  appointmentColorTokenClassMap,
608
770
  getResourceAppointmentBackground,
771
+ renderToolbar,
609
772
  renderDatePicker,
610
773
  prevButtonLabel = "Prev",
611
774
  nextButtonLabel = "Next",
@@ -662,6 +825,22 @@ function Scheduler({
662
825
  selectedDate
663
826
  });
664
827
  const defaultResourceHeaderRenderer = (resource) => defaultRenderResourceHeader({ resource });
828
+ const goToPreviousDay = () => onSelectedDateChange(shiftDays(selectedDate, -1));
829
+ const goToNextDay = () => onSelectedDateChange(shiftDays(selectedDate, 1));
830
+ const defaultDatePicker = (renderDatePicker ?? defaultRenderDatePicker)({ selectedDate, onSelectedDateChange });
831
+ const toolbar = renderToolbar ? renderToolbar({
832
+ selectedDate,
833
+ onSelectedDateChange,
834
+ goToPreviousDay,
835
+ goToNextDay,
836
+ defaultDatePicker
837
+ }) : defaultRenderToolbar({
838
+ goToPreviousDay,
839
+ goToNextDay,
840
+ defaultDatePicker,
841
+ prevButtonLabel,
842
+ nextButtonLabel
843
+ });
665
844
  return /* @__PURE__ */ jsxRuntime.jsxs(
666
845
  "section",
667
846
  {
@@ -670,27 +849,7 @@ function Scheduler({
670
849
  onPointerUp: onGlobalPointerUp,
671
850
  onPointerCancel: onGlobalPointerUp,
672
851
  children: [
673
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rfs-toolbar", children: [
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
- )
693
- ] }),
852
+ toolbar,
694
853
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rfs-shell border-border bg-card", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { minWidth: gridMinWidth }, children: [
695
854
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rfs-header border-border bg-card", children: [
696
855
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rfs-gutter border-border", style: { width: GUTTER_W } }),