@omidrahmati/react-slot-scheduler 1.0.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/CHANGELOG.md +45 -0
- package/LICENSE +21 -0
- package/README.md +717 -0
- package/dist/index.cjs +1197 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.css +469 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.cts +217 -0
- package/dist/index.d.ts +217 -0
- package/dist/index.js +1154 -0
- package/dist/index.js.map +1 -0
- package/package.json +88 -0
package/README.md
ADDED
|
@@ -0,0 +1,717 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# react-slot-scheduler
|
|
4
|
+
|
|
5
|
+
**The lightweight slot calendar for React โ zero dependencies, RTL-first, 3 scheduler modes + complete demo patterns.**
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/@omidrahmati/react-slot-scheduler)
|
|
8
|
+
[](https://bundlephobia.com/package/@omidrahmati/react-slot-scheduler)
|
|
9
|
+
[](https://www.typescriptlang.org/)
|
|
10
|
+
[](https://react.dev/)
|
|
11
|
+
[](https://bundlephobia.com/package/@omidrahmati/react-slot-scheduler)
|
|
12
|
+
[](./LICENSE)
|
|
13
|
+
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
<table>
|
|
19
|
+
<tr>
|
|
20
|
+
<td width="50%">
|
|
21
|
+
<img src="https://raw.githubusercontent.com/omidrahmati2000/react-slot-scheduler/main/media/screenshot-fa-light.png" alt="React slot scheduler โ Persian RTL, Jalali calendar, appointment booking" />
|
|
22
|
+
<p align="center"><sub>๐ฎ๐ท Persian ยท RTL ยท Jalali ยท Appointment</sub></p>
|
|
23
|
+
</td>
|
|
24
|
+
<td width="50%">
|
|
25
|
+
<img src="https://raw.githubusercontent.com/omidrahmati2000/react-slot-scheduler/main/media/screenshot-meeting-dark.png" alt="React slot scheduler โ Meeting room drag and drop rescheduling, dark mode" />
|
|
26
|
+
<p align="center"><sub>๐ข Meeting Room ยท Drag & drop ยท Dark mode</sub></p>
|
|
27
|
+
</td>
|
|
28
|
+
</tr>
|
|
29
|
+
<tr>
|
|
30
|
+
<td width="50%">
|
|
31
|
+
<img src="https://raw.githubusercontent.com/omidrahmati2000/react-slot-scheduler/main/media/screenshot-task-timeline.png" alt="React slot scheduler โ Gantt task timeline, assignee, progress, dark mode" />
|
|
32
|
+
<p align="center"><sub>๐๏ธ Task Timeline ยท Gantt ยท Assignee ยท Progress %</sub></p>
|
|
33
|
+
</td>
|
|
34
|
+
<td width="50%">
|
|
35
|
+
<img src="https://raw.githubusercontent.com/omidrahmati2000/react-slot-scheduler/main/media/screenshot-resource-planner.png" alt="React slot scheduler โ Resource planner, rooms, doctors, overlap stacking" />
|
|
36
|
+
<p align="center"><sub>๐งฉ Resource Planner ยท Overlap stacking ยท Week/Month</sub></p>
|
|
37
|
+
</td>
|
|
38
|
+
</tr>
|
|
39
|
+
</table>
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Why react-slot-scheduler?
|
|
44
|
+
|
|
45
|
+
| | **react-slot-scheduler** | Alternatives |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| Bundle (gzip) | **27 kB** | 400โ800 kB (antd + react-dnd + dayjs) |
|
|
48
|
+
| Dependencies | **0** | 5โ10 packages |
|
|
49
|
+
| RTL / Persian / Arabic | โ
**built-in** | โ not supported |
|
|
50
|
+
| Dark mode | โ
**CSS tokens** | โ manual |
|
|
51
|
+
| TypeScript | โ
**100% typed** | partial |
|
|
52
|
+
| Scheduling modes | โ
**Multiple production modes** | time-grid only |
|
|
53
|
+
| API style | โ
**hooks props** | class-based (`SchedulerData`) |
|
|
54
|
+
| UI framework lock-in | โ
**none** | Ant Design required |
|
|
55
|
+
| Next.js / SSR | โ
**ready** | varies |
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Scheduling Modes
|
|
60
|
+
|
|
61
|
+
| Mode | Import / `mode` prop | Use case |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| **Time Grid** | default โ no `mode` needed | Appointment booking, clinic, salon |
|
|
64
|
+
| **Meeting Room** | default โ use `DaySchedule[]` | Office meeting room reservation |
|
|
65
|
+
| **Task Timeline** | `mode="task-timeline"` + `createTaskTimelineAdapter()` | Team task / sprint scheduling |
|
|
66
|
+
| **Resource Planner** | `mode="resource-planner"` + `createResourcePlannerAdapter()` | Multi-resource (rooms, doctors, staff) |
|
|
67
|
+
| **Theme Playground (Demo tab)** | โ | Preview 6 built-in color presets |
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Install
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npm i @omidrahmati/react-slot-scheduler
|
|
75
|
+
# yarn add @omidrahmati/react-slot-scheduler
|
|
76
|
+
# pnpm add @omidrahmati/react-slot-scheduler
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Import the stylesheet once at your app entry:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import '@omidrahmati/react-slot-scheduler/dist/index.css';
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Quick Start (from scratch)
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
import { useState } from 'react';
|
|
89
|
+
import { BookingCalendar, type DaySchedule } from '@omidrahmati/react-slot-scheduler';
|
|
90
|
+
import '@omidrahmati/react-slot-scheduler/dist/index.css';
|
|
91
|
+
|
|
92
|
+
const schedules: DaySchedule[] = [
|
|
93
|
+
{
|
|
94
|
+
date: '2026-06-01',
|
|
95
|
+
isWorkingDay: true,
|
|
96
|
+
slots: [
|
|
97
|
+
{ startTime: '09:00', endTime: '10:00', status: 'booked', itemId: 'a-1', title: 'Standup' },
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
export default function App() {
|
|
103
|
+
const [value, setValue] = useState(new Date('2026-06-01'));
|
|
104
|
+
return <BookingCalendar value={value} onChange={setValue} schedules={schedules} locale="en-US" />;
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Next.js App Router
|
|
109
|
+
|
|
110
|
+
Import CSS once in your app layout or root client entry:
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
// app/layout.tsx
|
|
114
|
+
import '@omidrahmati/react-slot-scheduler/dist/index.css';
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Mode 1 โ Appointment / Time Grid
|
|
120
|
+
|
|
121
|
+
The default mode. Pass a `DaySchedule[]` array โ each day has a list of time slots.
|
|
122
|
+
|
|
123
|
+
```tsx
|
|
124
|
+
import { useState } from 'react';
|
|
125
|
+
import { BookingCalendar } from '@omidrahmati/react-slot-scheduler';
|
|
126
|
+
import type { DaySchedule } from '@omidrahmati/react-slot-scheduler';
|
|
127
|
+
import '@omidrahmati/react-slot-scheduler/dist/index.css';
|
|
128
|
+
|
|
129
|
+
const schedules: DaySchedule[] = [
|
|
130
|
+
{
|
|
131
|
+
date: '2026-06-01', // YYYY-MM-DD
|
|
132
|
+
isWorkingDay: true,
|
|
133
|
+
workStartTime: '09:00', // sets the visible grid start
|
|
134
|
+
workEndTime: '18:00', // sets the visible grid end
|
|
135
|
+
slots: [
|
|
136
|
+
{
|
|
137
|
+
startTime: '09:00',
|
|
138
|
+
endTime: '10:00',
|
|
139
|
+
status: 'booked',
|
|
140
|
+
itemId: 'appt-1', // passed to onItemClick when clicked
|
|
141
|
+
title: 'Alice โ Haircut',
|
|
142
|
+
description: 'Confirmed',
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
startTime: '11:00',
|
|
146
|
+
endTime: '11:30',
|
|
147
|
+
status: 'blocked',
|
|
148
|
+
title: 'Lunch Break',
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
date: '2026-06-02',
|
|
154
|
+
isWorkingDay: false, // greys out the column, no slots rendered
|
|
155
|
+
slots: [],
|
|
156
|
+
},
|
|
157
|
+
];
|
|
158
|
+
|
|
159
|
+
export default function App() {
|
|
160
|
+
const [date, setDate] = useState(new Date());
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<BookingCalendar
|
|
164
|
+
value={date}
|
|
165
|
+
onChange={setDate}
|
|
166
|
+
schedules={schedules}
|
|
167
|
+
locale="en-US"
|
|
168
|
+
weekStartsOn={1} // 1 = Monday
|
|
169
|
+
onItemClick={(id) => console.log('booked slot clicked:', id)}
|
|
170
|
+
onSlotClick={(date, slot) => console.log('empty area clicked:', date, slot)}
|
|
171
|
+
/>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Drag & Drop Rescheduling
|
|
177
|
+
|
|
178
|
+
```tsx
|
|
179
|
+
const [schedules, setSchedules] = useState<DaySchedule[]>(initial);
|
|
180
|
+
|
|
181
|
+
function handleMove({ slot, from, to }: SlotMovePayload) {
|
|
182
|
+
setSchedules(prev =>
|
|
183
|
+
prev.map(day => {
|
|
184
|
+
if (day.date === from.date)
|
|
185
|
+
return {
|
|
186
|
+
...day,
|
|
187
|
+
slots: day.slots.filter(s => s.startTime !== from.startTime),
|
|
188
|
+
};
|
|
189
|
+
if (day.date === to.date)
|
|
190
|
+
return {
|
|
191
|
+
...day,
|
|
192
|
+
slots: [...day.slots, { ...slot, startTime: to.startTime, endTime: to.endTime }],
|
|
193
|
+
};
|
|
194
|
+
return day;
|
|
195
|
+
})
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
<BookingCalendar
|
|
200
|
+
draggableSlots
|
|
201
|
+
onSlotMove={handleMove}
|
|
202
|
+
schedules={schedules}
|
|
203
|
+
value={date}
|
|
204
|
+
onChange={setDate}
|
|
205
|
+
/>
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Multi-Select Empty Slots
|
|
209
|
+
|
|
210
|
+
Drag across empty cells to select a time range. Only fires on cells that have no existing slot.
|
|
211
|
+
|
|
212
|
+
```tsx
|
|
213
|
+
<BookingCalendar
|
|
214
|
+
selectionMode
|
|
215
|
+
onSlotDragSelectEnd={(slots) => {
|
|
216
|
+
// slots: Array<{ date: string; startTime: string; endTime: string }>
|
|
217
|
+
openBookingDialog(slots);
|
|
218
|
+
}}
|
|
219
|
+
onSelectionChange={(slots) => setSelectionCount(slots.length)}
|
|
220
|
+
schedules={schedules}
|
|
221
|
+
value={date}
|
|
222
|
+
onChange={setDate}
|
|
223
|
+
/>
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## Mode 2 โ Persian / Arabic RTL
|
|
229
|
+
|
|
230
|
+
Switch locale and the layout, dates, and labels all change automatically.
|
|
231
|
+
|
|
232
|
+
```tsx
|
|
233
|
+
<BookingCalendar
|
|
234
|
+
locale="fa-IR"
|
|
235
|
+
weekStartsOn={6} // 6 = Saturday (start of week in Iran)
|
|
236
|
+
direction="auto" // RTL detected from locale automatically
|
|
237
|
+
translations={{
|
|
238
|
+
previous: 'Previous',
|
|
239
|
+
today: 'Today',
|
|
240
|
+
next: 'Next',
|
|
241
|
+
day: 'Day',
|
|
242
|
+
week: 'Week',
|
|
243
|
+
}}
|
|
244
|
+
schedules={schedules}
|
|
245
|
+
value={date}
|
|
246
|
+
onChange={setDate}
|
|
247
|
+
/>
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Supported locales: `fa-IR`, `ar`, `ar-SA`, `ar-EG`, `en-US`, `en-GB`, and any BCP-47 string supported by `Intl`.
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## Mode 3 โ Task Timeline (Gantt)
|
|
255
|
+
|
|
256
|
+
Use `mode="task-timeline"` with `createTaskTimelineAdapter()`. Each item has an `assignee` and `progress` field.
|
|
257
|
+
|
|
258
|
+
```tsx
|
|
259
|
+
import {
|
|
260
|
+
BookingCalendar,
|
|
261
|
+
createTaskTimelineAdapter,
|
|
262
|
+
} from '@omidrahmati/react-slot-scheduler';
|
|
263
|
+
import type { TaskTimelineItem } from '@omidrahmati/react-slot-scheduler';
|
|
264
|
+
|
|
265
|
+
const tasks: TaskTimelineItem[] = [
|
|
266
|
+
{
|
|
267
|
+
id: 'task-1',
|
|
268
|
+
date: '2026-06-01',
|
|
269
|
+
startTime: '09:00',
|
|
270
|
+
endTime: '10:30',
|
|
271
|
+
title: 'Design Review',
|
|
272
|
+
status: 'booked', // 'booked' | 'blocked' | 'custom'
|
|
273
|
+
assignee: 'Sara',
|
|
274
|
+
progress: 70, // 0โ100
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
id: 'task-2',
|
|
278
|
+
date: '2026-06-01',
|
|
279
|
+
startTime: '11:00',
|
|
280
|
+
endTime: '12:00',
|
|
281
|
+
title: 'API Contract',
|
|
282
|
+
status: 'custom',
|
|
283
|
+
assignee: 'Arman',
|
|
284
|
+
progress: 35,
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
id: 'task-3',
|
|
288
|
+
date: '2026-06-02',
|
|
289
|
+
startTime: '10:00',
|
|
290
|
+
endTime: '11:30',
|
|
291
|
+
title: 'QA Sync',
|
|
292
|
+
status: 'blocked',
|
|
293
|
+
assignee: 'Neda',
|
|
294
|
+
progress: 50,
|
|
295
|
+
},
|
|
296
|
+
];
|
|
297
|
+
|
|
298
|
+
export default function TaskScheduler() {
|
|
299
|
+
const [date, setDate] = useState(new Date());
|
|
300
|
+
const adapter = useMemo(() => createTaskTimelineAdapter(), []);
|
|
301
|
+
|
|
302
|
+
return (
|
|
303
|
+
<BookingCalendar
|
|
304
|
+
mode="task-timeline"
|
|
305
|
+
value={date}
|
|
306
|
+
onChange={setDate}
|
|
307
|
+
schedules={[]} // not used in task-timeline mode
|
|
308
|
+
dataAdapter={adapter}
|
|
309
|
+
dataSource={tasks}
|
|
310
|
+
locale="en-US"
|
|
311
|
+
onItemClick={(id) => {
|
|
312
|
+
const task = tasks.find(t => t.id === id);
|
|
313
|
+
console.log('Task clicked:', task);
|
|
314
|
+
}}
|
|
315
|
+
/>
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### `TaskTimelineItem` interface
|
|
321
|
+
|
|
322
|
+
```ts
|
|
323
|
+
interface TaskTimelineItem {
|
|
324
|
+
id: string;
|
|
325
|
+
date: string; // start date: 'YYYY-MM-DD'
|
|
326
|
+
endDate?: string; // optional multi-day end date (inclusive)
|
|
327
|
+
startTime: string; // 'HH:mm'
|
|
328
|
+
endTime: string; // 'HH:mm'
|
|
329
|
+
title: string;
|
|
330
|
+
status?: 'available' | 'booked' | 'blocked' | 'outside' | 'custom';
|
|
331
|
+
description?: string;
|
|
332
|
+
assignee?: string;
|
|
333
|
+
progress?: number; // 0โ100
|
|
334
|
+
resourceId?: string;
|
|
335
|
+
meta?: Record<string, unknown>;
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## Mode 4 โ Resource Planner
|
|
342
|
+
|
|
343
|
+
Use `mode="resource-planner"` with `createResourcePlannerAdapter(resources)`. Each item is linked to a resource via `resourceId`. The resource name is shown as the sub-label in each slot.
|
|
344
|
+
|
|
345
|
+
```tsx
|
|
346
|
+
import {
|
|
347
|
+
BookingCalendar,
|
|
348
|
+
createResourcePlannerAdapter,
|
|
349
|
+
} from '@omidrahmati/react-slot-scheduler';
|
|
350
|
+
import type {
|
|
351
|
+
ResourceDefinition,
|
|
352
|
+
ResourcePlannerItem,
|
|
353
|
+
} from '@omidrahmati/react-slot-scheduler';
|
|
354
|
+
|
|
355
|
+
const resources: ResourceDefinition[] = [
|
|
356
|
+
{ id: 'room-a', title: 'Room A' },
|
|
357
|
+
{ id: 'room-b', title: 'Room B' },
|
|
358
|
+
{ id: 'dr-amini', title: 'Dr. Amini' },
|
|
359
|
+
];
|
|
360
|
+
|
|
361
|
+
const items: ResourcePlannerItem[] = [
|
|
362
|
+
{
|
|
363
|
+
id: 'rp-1',
|
|
364
|
+
date: '2026-06-01',
|
|
365
|
+
startTime: '09:00',
|
|
366
|
+
endTime: '10:00',
|
|
367
|
+
resourceId: 'room-a', // must match a resource id
|
|
368
|
+
title: 'Team Sync',
|
|
369
|
+
status: 'booked',
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
id: 'rp-2',
|
|
373
|
+
date: '2026-06-01',
|
|
374
|
+
startTime: '10:30',
|
|
375
|
+
endTime: '11:30',
|
|
376
|
+
resourceId: 'room-b',
|
|
377
|
+
title: 'Client Call',
|
|
378
|
+
status: 'custom',
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
id: 'rp-3',
|
|
382
|
+
date: '2026-06-02',
|
|
383
|
+
startTime: '14:00',
|
|
384
|
+
endTime: '15:00',
|
|
385
|
+
resourceId: 'dr-amini',
|
|
386
|
+
title: 'Consultation',
|
|
387
|
+
status: 'booked',
|
|
388
|
+
},
|
|
389
|
+
];
|
|
390
|
+
|
|
391
|
+
export default function ResourceCalendar() {
|
|
392
|
+
const [date, setDate] = useState(new Date());
|
|
393
|
+
const adapter = useMemo(() => createResourcePlannerAdapter(resources), []);
|
|
394
|
+
|
|
395
|
+
return (
|
|
396
|
+
<BookingCalendar
|
|
397
|
+
mode="resource-planner"
|
|
398
|
+
value={date}
|
|
399
|
+
onChange={setDate}
|
|
400
|
+
schedules={[]}
|
|
401
|
+
resources={resources}
|
|
402
|
+
dataAdapter={adapter}
|
|
403
|
+
dataSource={items}
|
|
404
|
+
locale="en-US"
|
|
405
|
+
onItemClick={(id) => {
|
|
406
|
+
const item = items.find(i => i.id === id);
|
|
407
|
+
const resource = resources.find(r => r.id === item?.resourceId);
|
|
408
|
+
console.log(`${item?.title} โ ${resource?.title}`);
|
|
409
|
+
}}
|
|
410
|
+
/>
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
### `ResourceDefinition` interface
|
|
416
|
+
|
|
417
|
+
```ts
|
|
418
|
+
interface ResourceDefinition {
|
|
419
|
+
id: string;
|
|
420
|
+
title: string; // displayed as slot sub-label
|
|
421
|
+
meta?: Record<string, unknown>;
|
|
422
|
+
}
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
### `ResourcePlannerItem` interface
|
|
426
|
+
|
|
427
|
+
```ts
|
|
428
|
+
interface ResourcePlannerItem {
|
|
429
|
+
id: string;
|
|
430
|
+
date: string; // 'YYYY-MM-DD'
|
|
431
|
+
startTime: string; // 'HH:mm'
|
|
432
|
+
endTime: string; // 'HH:mm'
|
|
433
|
+
resourceId: string; // must match a ResourceDefinition.id
|
|
434
|
+
title: string;
|
|
435
|
+
status?: 'available' | 'booked' | 'blocked' | 'outside' | 'custom';
|
|
436
|
+
description?: string;
|
|
437
|
+
meta?: Record<string, unknown>;
|
|
438
|
+
}
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
---
|
|
442
|
+
|
|
443
|
+
## Dark Mode / Custom Theming
|
|
444
|
+
|
|
445
|
+
Override any or all of the 9 CSS design tokens:
|
|
446
|
+
|
|
447
|
+
```tsx
|
|
448
|
+
<BookingCalendar
|
|
449
|
+
theme={{
|
|
450
|
+
primary: '#818cf8', // accent โ buttons, today cell, active tab
|
|
451
|
+
bg: '#0f172a', // grid background
|
|
452
|
+
panel: '#1e293b', // toolbar + time-label column
|
|
453
|
+
border: '#334155', // grid lines
|
|
454
|
+
text: '#f1f5f9', // primary text
|
|
455
|
+
mutedText: '#94a3b8', // time labels + slot sub-labels
|
|
456
|
+
bookedBg: '#3730a3', // booked slot fill
|
|
457
|
+
blockedBg: '#334155', // blocked / outside slot fill
|
|
458
|
+
customBg: '#5b21b6', // custom slot fill
|
|
459
|
+
}}
|
|
460
|
+
schedules={schedules}
|
|
461
|
+
value={date}
|
|
462
|
+
onChange={setDate}
|
|
463
|
+
/>
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
---
|
|
467
|
+
|
|
468
|
+
## Full API Reference
|
|
469
|
+
|
|
470
|
+
### `BookingCalendarProps`
|
|
471
|
+
|
|
472
|
+
| Prop | Type | Default | Description |
|
|
473
|
+
|---|---|---|---|
|
|
474
|
+
| `value` | `Date` | โ | **Required.** Currently focused date |
|
|
475
|
+
| `onChange` | `(date: Date) => void` | โ | **Required.** Navigation callback |
|
|
476
|
+
| `schedules` | `DaySchedule[]` | โ | **Required.** Pass `[]` when using `dataSource` |
|
|
477
|
+
| `mode` | `'time-grid' \| 'task-timeline' \| 'resource-planner'` | `'time-grid'` | Scheduler mode |
|
|
478
|
+
| `dataAdapter` | `SchedulerDataAdapter` | โ | Required when `mode` is not `'time-grid'` |
|
|
479
|
+
| `dataSource` | `TaskTimelineItem[] \| ResourcePlannerItem[]` | โ | Data for non-time-grid modes |
|
|
480
|
+
| `resources` | `ResourceDefinition[]` | โ | Required when `mode="resource-planner"` |
|
|
481
|
+
| `ganttTimeUnit` | `'hour' \| 'day'` | adapter-driven | Override Gantt time unit in non-time-grid modes |
|
|
482
|
+
| `ganttScale` | `'day' \| 'week' \| 'month'` | adapter/default | Override day-mode column scale |
|
|
483
|
+
| `viewMode` | `'day' \| 'week'` | auto | Controlled view; auto = Day on mobile |
|
|
484
|
+
| `onViewModeChange` | `(mode) => void` | โ | Fired on Day/Week toggle |
|
|
485
|
+
| `draggableSlots` | `boolean` | `false` | Enable HTML5 drag & drop |
|
|
486
|
+
| `onSlotMove` | `(payload: SlotMovePayload) => void` | โ | Fires after a successful drop |
|
|
487
|
+
| `onBeforeSlotMove` | `(payload) => boolean \| Promise<boolean>` | โ | Validate/deny drop before `onSlotMove` |
|
|
488
|
+
| `onSlotConflict` | `(payload: SlotConflictPayload) => void` | โ | Fires on overlap or policy denial |
|
|
489
|
+
| `onGanttItemMove` | `(payload: GanttMovePayload) => void` | โ | Fired when an item is moved in Gantt modes |
|
|
490
|
+
| `onGanttItemResize` | `(payload: GanttResizePayload) => void` | โ | Fired when an item is resized in hour-based Gantt |
|
|
491
|
+
| `onGanttItemCreate` | `(payload: GanttCreatePayload) => void` | โ | Fired when a new item is created by dragging on empty row space (hour mode) |
|
|
492
|
+
| `selectionMode` | `boolean` | `false` | Enable drag-to-select on empty cells |
|
|
493
|
+
| `selectedSlots` | `Array<{ date; startTime; endTime }>` | โ | Controlled external selection model |
|
|
494
|
+
| `onSlotDragSelectStart` | `(slot) => void` | โ | Fires when drag selection starts |
|
|
495
|
+
| `onSlotDragSelectMove` | `(slot) => void` | โ | Fires for each entered empty cell |
|
|
496
|
+
| `onSlotDragSelectEnd` | `(slots[]) => void` | โ | Fires on mouseup with selected cells |
|
|
497
|
+
| `onSelectionChange` | `(slots[]) => void` | โ | Fires on each added cell |
|
|
498
|
+
| `isSlotSelected` | `(slot) => boolean` | โ | Controlled external selection |
|
|
499
|
+
| `onSlotClick` | `(date, slot) => void` | โ | Click on any slot |
|
|
500
|
+
| `onItemClick` | `(itemId) => void` | โ | Click on slots with `itemId` / `id` |
|
|
501
|
+
| `onBookingClick` | `(bookingId) => void` | โ | Deprecated alias of `onItemClick` |
|
|
502
|
+
| `slotGranularity` | `number` | `30` | Minutes per grid row (15, 30, 60โฆ) |
|
|
503
|
+
| `locale` | `string` | `'fa-IR'` | BCP-47 locale โ drives labels & direction |
|
|
504
|
+
| `weekStartsOn` | `0 \| 1 \| 6` | `6` | Week start: 0=Sun, 1=Mon, 6=Sat |
|
|
505
|
+
| `direction` | `'rtl' \| 'ltr' \| 'auto'` | `'auto'` | Layout direction |
|
|
506
|
+
| `translations` | `Partial<Translations>` | โ | Override toolbar label strings |
|
|
507
|
+
| `theme` | `Partial<CalendarTheme>` | โ | Override design tokens |
|
|
508
|
+
| `hideTimeColumn` | `boolean` | `false` | Hide the time-label column |
|
|
509
|
+
| `className` | `string` | โ | Extra CSS class on root |
|
|
510
|
+
|
|
511
|
+
### Notes For Production Usage
|
|
512
|
+
|
|
513
|
+
- In `task-timeline` and `resource-planner` modes, pass `schedules={[]}` and provide `dataAdapter` + `dataSource`.
|
|
514
|
+
- `onGanttItemResize` applies to hour-based Gantt mode (`ganttTimeUnit="hour"`).
|
|
515
|
+
- `onGanttItemCreate` applies to hour-based Gantt mode (`ganttTimeUnit="hour"`).
|
|
516
|
+
- For policy validation in time-grid mode, use `onBeforeSlotMove` and handle denials via `onSlotConflict`.
|
|
517
|
+
|
|
518
|
+
---
|
|
519
|
+
|
|
520
|
+
### `DaySchedule`
|
|
521
|
+
|
|
522
|
+
Used in default `time-grid` mode. The `workStartTime` / `workEndTime` pair defines the visible hour range.
|
|
523
|
+
|
|
524
|
+
```ts
|
|
525
|
+
interface DaySchedule {
|
|
526
|
+
date: string; // 'YYYY-MM-DD'
|
|
527
|
+
isWorkingDay: boolean; // false โ column is greyed, slots ignored
|
|
528
|
+
workStartTime?: string; // 'HH:mm' โ grid visible start (default: min slot time)
|
|
529
|
+
workEndTime?: string; // 'HH:mm' โ grid visible end (default: max slot time)
|
|
530
|
+
slots: CalendarSlot[];
|
|
531
|
+
}
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
### `CalendarSlot`
|
|
535
|
+
|
|
536
|
+
```ts
|
|
537
|
+
interface CalendarSlot {
|
|
538
|
+
startTime: string; // 'HH:mm'
|
|
539
|
+
endTime: string; // 'HH:mm'
|
|
540
|
+
status: 'booked' | 'blocked' | 'outside' | 'custom';
|
|
541
|
+
itemId?: string; // โ onItemClick fired when clicked
|
|
542
|
+
bookingId?: string; // deprecated alias for itemId
|
|
543
|
+
title?: string; // main label in slot
|
|
544
|
+
description?: string; // sub-label in slot
|
|
545
|
+
}
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
> **Tip:** Do **not** create `status: 'available'` slots โ empty grid cells already represent available time.
|
|
549
|
+
> In `selectionMode`, users drag across empty cells to select them.
|
|
550
|
+
|
|
551
|
+
### Slot Status Guide
|
|
552
|
+
|
|
553
|
+
| Status | Typical use | Click | Drag |
|
|
554
|
+
|---|---|---|---|
|
|
555
|
+
| `booked` | Confirmed reservation | โ
`onItemClick` | โ
if `draggableSlots` |
|
|
556
|
+
| `blocked` | Break, holiday, lunch | โ | โ |
|
|
557
|
+
| `outside` | Outside working hours | โ | โ |
|
|
558
|
+
| `custom` | App-defined category | โ
`onItemClick` | โ
if `draggableSlots` |
|
|
559
|
+
|
|
560
|
+
### `SlotMovePayload`
|
|
561
|
+
|
|
562
|
+
```ts
|
|
563
|
+
interface SlotMovePayload {
|
|
564
|
+
slot: CalendarSlot;
|
|
565
|
+
from: { date: string; startTime: string; endTime: string };
|
|
566
|
+
to: { date: string; startTime: string; endTime: string };
|
|
567
|
+
}
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
### `SlotConflictPayload`
|
|
571
|
+
|
|
572
|
+
```ts
|
|
573
|
+
interface SlotConflictPayload extends SlotMovePayload {
|
|
574
|
+
reason: 'overlap' | 'blocked-by-policy';
|
|
575
|
+
conflictingSlot?: CalendarSlot;
|
|
576
|
+
}
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
### `GanttMovePayload`
|
|
580
|
+
|
|
581
|
+
```ts
|
|
582
|
+
interface GanttMovePayload {
|
|
583
|
+
item: GanttItem;
|
|
584
|
+
newRowId: string;
|
|
585
|
+
newDate: string; // 'YYYY-MM-DD'
|
|
586
|
+
newEndDate?: string; // set for multi-day item moves
|
|
587
|
+
newStartTime: string; // 'HH:mm'
|
|
588
|
+
newEndTime: string; // 'HH:mm'
|
|
589
|
+
}
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
### `GanttResizePayload`
|
|
593
|
+
|
|
594
|
+
```ts
|
|
595
|
+
interface GanttResizePayload {
|
|
596
|
+
item: GanttItem;
|
|
597
|
+
newStartTime: string;
|
|
598
|
+
newEndTime: string;
|
|
599
|
+
}
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
### `GanttCreatePayload`
|
|
603
|
+
|
|
604
|
+
```ts
|
|
605
|
+
interface GanttCreatePayload {
|
|
606
|
+
rowId: string;
|
|
607
|
+
date: string; // start date: 'YYYY-MM-DD'
|
|
608
|
+
endDate?: string; // end date in day-scale multi-column drag
|
|
609
|
+
startTime: string; // 'HH:mm'
|
|
610
|
+
endTime: string; // 'HH:mm'
|
|
611
|
+
}
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
### `CalendarTheme` Tokens
|
|
615
|
+
|
|
616
|
+
| Token | Default | Controls |
|
|
617
|
+
|---|---|---|
|
|
618
|
+
| `primary` | `#0f766e` | Buttons, today column, active view tab |
|
|
619
|
+
| `bg` | `#f4f7f7` | Grid background |
|
|
620
|
+
| `panel` | `#ffffff` | Toolbar + time column |
|
|
621
|
+
| `border` | `#d6e0df` | Grid lines |
|
|
622
|
+
| `text` | `#102725` | Primary text |
|
|
623
|
+
| `mutedText` | `#5c7270` | Time labels, slot sub-labels |
|
|
624
|
+
| `availableBg` | `#ffffff` | Empty-slot surface |
|
|
625
|
+
| `bookedBg` | `#fee2e2` | Booked slot background |
|
|
626
|
+
| `blockedBg` | `#e5e7eb` | Blocked/outside slot background |
|
|
627
|
+
| `customBg` | `#e0f2fe` | Custom slot background |
|
|
628
|
+
|
|
629
|
+
---
|
|
630
|
+
|
|
631
|
+
## Demo App
|
|
632
|
+
|
|
633
|
+
```bash
|
|
634
|
+
git clone https://github.com/omidrahmati2000/react-slot-scheduler
|
|
635
|
+
cd react-slot-scheduler/example
|
|
636
|
+
npm install
|
|
637
|
+
npm run dev # โ http://localhost:5173
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
### Deploy Demo on Vercel
|
|
641
|
+
|
|
642
|
+
The example app is deploy-ready for Vercel.
|
|
643
|
+
|
|
644
|
+
1. Import this repository into Vercel
|
|
645
|
+
2. Set Root Directory to `example`
|
|
646
|
+
3. Deploy
|
|
647
|
+
|
|
648
|
+
Detailed guide: [Vercel Deployment Guide](./docs/usage/vercel-deployment.md)
|
|
649
|
+
|
|
650
|
+
| Tab | Mode | What it shows |
|
|
651
|
+
|---|---|---|
|
|
652
|
+
| ๐
Appointment | time-grid | FA โ EN, drag & drop, multi-select, 4 color swatches |
|
|
653
|
+
| ๐ข Meeting Room | time-grid | Live drag-rescheduling with React state |
|
|
654
|
+
| ๐๏ธ Task Timeline | task-timeline | Bilingual tasks with assignee, dark/light |
|
|
655
|
+
| ๐งฉ Resource Planner | resource-planner | Multi-resource (rooms + doctors), FA/EN |
|
|
656
|
+
| ๐จ Themes | โ | 6 presets: Ocean, Forest, Sunset, Midnight, Sakura, Gold |
|
|
657
|
+
|
|
658
|
+
---
|
|
659
|
+
|
|
660
|
+
## Scripts
|
|
661
|
+
|
|
662
|
+
```bash
|
|
663
|
+
npm run build # compile โ dist/
|
|
664
|
+
npm run typecheck # TypeScript strict check
|
|
665
|
+
npm run test # vitest run
|
|
666
|
+
npm run test:coverage # coverage report
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
---
|
|
670
|
+
|
|
671
|
+
## Deep Usage Guides
|
|
672
|
+
|
|
673
|
+
- [Model Selection Guide](./docs/usage/model-selection-guide.md)
|
|
674
|
+
- [Conflict Policy Guide](./docs/usage/conflict-policy-guide.md)
|
|
675
|
+
- [Resource Planner Guide](./docs/usage/resource-planner-guide.md)
|
|
676
|
+
- [Gantt Interactions Guide](./docs/usage/gantt-interactions-guide.md)
|
|
677
|
+
- [Vercel Deployment Guide](./docs/usage/vercel-deployment.md)
|
|
678
|
+
|
|
679
|
+
## AI Usage & Attribution
|
|
680
|
+
|
|
681
|
+
- This project may use AI-assisted drafting for some docs, examples, or refactors.
|
|
682
|
+
- All published code and documentation are manually reviewed and validated before release.
|
|
683
|
+
- No generated output is accepted as-is without human verification.
|
|
684
|
+
- If a contribution includes AI-assisted content, keep prompts/private data out of commits and ensure licensing/compliance checks are completed.
|
|
685
|
+
|
|
686
|
+
## Documentation Standard
|
|
687
|
+
|
|
688
|
+
- All public documentation and code examples are maintained in English.
|
|
689
|
+
- Root `README.md` is the primary reference for installation, usage patterns, API, and production integration.
|
|
690
|
+
|
|
691
|
+
---
|
|
692
|
+
|
|
693
|
+
## Roadmap
|
|
694
|
+
|
|
695
|
+
- [ ] Month view
|
|
696
|
+
- [ ] Event resize โ drag slot start/end edges
|
|
697
|
+
- [x] Click-to-create (hour mode) โ drag on empty area to define a new slot
|
|
698
|
+
- [ ] Cross-day slots โ slots spanning midnight
|
|
699
|
+
|
|
700
|
+
PRs and issues welcome โ see [CONTRIBUTING.md](./CONTRIBUTING.md).
|
|
701
|
+
|
|
702
|
+
---
|
|
703
|
+
|
|
704
|
+
## License
|
|
705
|
+
|
|
706
|
+
MIT ยฉ [Omid Rahmati](https://github.com/omidrahmati2000)
|
|
707
|
+
|
|
708
|
+
## Author
|
|
709
|
+
|
|
710
|
+
Omid Rahmati
|
|
711
|
+
Email: `omidrahmati2000@gmail.com`
|
|
712
|
+
|
|
713
|
+
---
|
|
714
|
+
|
|
715
|
+
<div align="center">
|
|
716
|
+
<sub>React 18+ ยท Zero dependencies ยท 27 kB gzip ยท TypeScript-first ยท RTL/LTR ยท 3 scheduler modes</sub>
|
|
717
|
+
</div>
|