calkit 0.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/README.md ADDED
@@ -0,0 +1,734 @@
1
+ # CalKit
2
+
3
+ Vanilla JS web component library for date pickers, time pickers, booking calendars, and resource schedulers. Zero dependencies. Shadow DOM encapsulated. Themeable.
4
+
5
+ <!-- ![CalKit demo](demo-full.png) -->
6
+
7
+ ## Install
8
+
9
+ ### CDN (`<script>` tag)
10
+
11
+ ```html
12
+ <script src="https://cdn.jsdelivr.net/gh/SimonKefas/calkit/dist/calkit.umd.js"></script>
13
+ ```
14
+
15
+ All four components are registered automatically. No imports needed.
16
+
17
+ ### Bundler (Vite, Webpack, etc.)
18
+
19
+ ```js
20
+ import { CalDatepicker, CalBooking, CalTimepicker, CalScheduler } from 'calkit';
21
+ ```
22
+
23
+ Uses the ES module builds from `node_modules`.
24
+
25
+ ### Individual Bundles
26
+
27
+ Load only the components you need:
28
+
29
+ | Bundle | CDN (`<script>`) | ES Module (bundler) | Gzipped |
30
+ |--------|------------------|---------------------|---------|
31
+ | **Full** (all 4) | `calkit.umd.js` (148 KB) | `calkit.es.js` (176 KB) | ~34 KB |
32
+ | **Datepicker** only | `datepicker.umd.js` (48 KB) | `datepicker.es.js` (56 KB) | ~12 KB |
33
+ | **Timepicker** only | `timepicker.umd.js` (32 KB) | `timepicker.es.js` (36 KB) | ~8 KB |
34
+ | **Booking** only | `booking.umd.js` (56 KB) | `booking.es.js` (64 KB) | ~14 KB |
35
+ | **Scheduler** only | `scheduler.umd.js` (88 KB) | `scheduler.es.js` (104 KB) | ~21 KB |
36
+
37
+ ---
38
+
39
+ ## Quick Start
40
+
41
+ ### Datepicker
42
+
43
+ ```html
44
+ <cal-datepicker mode="single" theme="light"></cal-datepicker>
45
+
46
+ <script>
47
+ const picker = document.querySelector('cal-datepicker');
48
+ picker.addEventListener('cal:change', (e) => {
49
+ console.log('Selected:', e.detail.value); // "2026-03-15"
50
+ });
51
+ </script>
52
+ ```
53
+
54
+ ### Timepicker
55
+
56
+ ```html
57
+ <cal-timepicker start-time="09:00" end-time="17:00" interval="30" format="12h"></cal-timepicker>
58
+
59
+ <script>
60
+ const tp = document.querySelector('cal-timepicker');
61
+ tp.addEventListener('cal:time-change', (e) => {
62
+ console.log('Selected:', e.detail.value); // "14:30"
63
+ });
64
+ </script>
65
+ ```
66
+
67
+ ### Booking Calendar
68
+
69
+ ```html
70
+ <cal-booking theme="light"></cal-booking>
71
+
72
+ <script>
73
+ const booking = document.querySelector('cal-booking');
74
+ booking.bookings = [
75
+ { id: '1', start: '2026-03-10', end: '2026-03-14', label: 'Alice', color: 'blue' },
76
+ { id: '2', start: '2026-03-14', end: '2026-03-18', label: 'Bob', color: 'green' },
77
+ ];
78
+ booking.addEventListener('cal:change', (e) => {
79
+ console.log('Booked:', e.detail.value); // { start, end }
80
+ });
81
+ </script>
82
+ ```
83
+
84
+ ### Scheduler
85
+
86
+ ```html
87
+ <cal-scheduler view="week" start-time="08:00" end-time="18:00" theme="light"></cal-scheduler>
88
+
89
+ <script>
90
+ const sched = document.querySelector('cal-scheduler');
91
+ sched.resources = [
92
+ { id: 'room-a', name: 'Room A' },
93
+ { id: 'room-b', name: 'Room B' },
94
+ ];
95
+ sched.events = [
96
+ { id: '1', title: 'Meeting', start: '2026-03-02', startTime: '09:00', endTime: '10:30', resourceId: 'room-a', color: 'blue' },
97
+ ];
98
+ sched.addEventListener('cal:slot-select', (e) => {
99
+ console.log('Slot:', e.detail); // { date, startTime, endTime, resourceId, resource }
100
+ });
101
+ </script>
102
+ ```
103
+
104
+ ---
105
+
106
+ ## Components Overview
107
+
108
+ | Feature | Datepicker | Timepicker | Booking | Scheduler |
109
+ |---------|:----------:|:----------:|:-------:|:---------:|
110
+ | Selection modes | single, multi, range | single, multi, range | range | slot-based |
111
+ | Display modes | inline, popover | inline, popover | inline, popover | inline |
112
+ | Views | — | — | — | day, week, month |
113
+ | Theming | light, dark, auto | light, dark, auto | light, dark, auto | light, dark, auto |
114
+ | Loading skeleton | yes | yes | yes | yes |
115
+ | Keyboard navigation | yes | — | yes | Escape to dismiss |
116
+ | Drag interaction | — | — | — | move, resize, create |
117
+ | Resources | — | — | — | tabs, columns |
118
+
119
+ ---
120
+
121
+ ## cal-datepicker
122
+
123
+ A date picker supporting single, multi, and range selection with inline or popover display.
124
+
125
+ ### Attributes
126
+
127
+ | Attribute | Type | Default | Description |
128
+ |-----------|------|---------|-------------|
129
+ | `mode` | `"single"` \| `"multi"` \| `"range"` | `"single"` | Selection mode |
130
+ | `display` | `"inline"` \| `"popover"` | `"inline"` | Render mode |
131
+ | `theme` | `"light"` \| `"dark"` \| `"auto"` | `"light"` | Color theme |
132
+ | `value` | `string` | — | Initial value. Single: `"2026-03-15"`. Range: `"2026-03-10/2026-03-15"`. Multi: `"2026-03-10,2026-03-12"` |
133
+ | `min-date` | `string` | — | Earliest selectable date (`"YYYY-MM-DD"`) |
134
+ | `max-date` | `string` | — | Latest selectable date (`"YYYY-MM-DD"`) |
135
+ | `disabled-dates` | `string` | — | Comma-separated dates to disable (`"2026-03-25,2026-03-26"`) |
136
+ | `first-day` | `number` | `0` | First day of week (0 = Sunday, 1 = Monday) |
137
+ | `locale` | `string` | — | Locale for formatting |
138
+ | `presets` | `string` | — | Comma-separated preset keys for range mode: `"today,this-week,next-7,next-30"` |
139
+ | `placeholder` | `string` | `"Select date"` | Popover trigger placeholder text |
140
+ | `dual` | `boolean` | — | Show two months side-by-side (range mode) |
141
+ | `loading` | `boolean` | — | Show skeleton loading state |
142
+
143
+ ### Properties
144
+
145
+ | Property | Type | Description |
146
+ |----------|------|-------------|
147
+ | `value` | `string \| string[] \| {start: string, end: string} \| null` | Read/write. Shape depends on `mode` |
148
+ | `loading` | `boolean` | Read/write loading state |
149
+
150
+ ### Methods
151
+
152
+ | Method | Parameters | Description |
153
+ |--------|------------|-------------|
154
+ | `open()` | — | Open popover (popover mode only) |
155
+ | `close()` | — | Close popover |
156
+ | `goToMonth(month, year)` | `month: number (0-11), year: number` | Navigate view to specific month |
157
+ | `showStatus(type, message, opts?)` | `type: "error"\|"warning"\|"info"\|"success"` | Show status banner |
158
+ | `clearStatus()` | — | Clear status banner |
159
+
160
+ ### Events
161
+
162
+ | Event | `detail` shape | Fires when |
163
+ |-------|---------------|------------|
164
+ | `cal:change` | `{value: string}` (single) / `{value: {start, end}}` (range) / `{value: string[]}` (multi) | Date selection changes |
165
+ | `cal:month-change` | `{year: number, month: number}` | View navigates to new month |
166
+ | `cal:open` | `{}` | Popover opens |
167
+ | `cal:close` | `{}` | Popover closes |
168
+ | `cal:status` | `{type: string\|null, message: string\|null}` | Status banner changes |
169
+
170
+ ### Example: Inline Range with Presets
171
+
172
+ ```html
173
+ <cal-datepicker
174
+ mode="range"
175
+ dual
176
+ presets="today,this-week,next-7,next-30"
177
+ min-date="2026-01-01"
178
+ theme="light"
179
+ ></cal-datepicker>
180
+
181
+ <script>
182
+ document.querySelector('cal-datepicker').addEventListener('cal:change', (e) => {
183
+ const { start, end } = e.detail.value;
184
+ console.log(`Range: ${start} to ${end}`);
185
+ });
186
+ </script>
187
+ ```
188
+
189
+ ### Example: Popover with Initial Value
190
+
191
+ ```html
192
+ <cal-datepicker
193
+ mode="single"
194
+ display="popover"
195
+ value="2026-03-15"
196
+ placeholder="Pick a date"
197
+ ></cal-datepicker>
198
+ ```
199
+
200
+ > Full reference: [docs/cal-datepicker.md](docs/cal-datepicker.md)
201
+
202
+ ---
203
+
204
+ ## cal-timepicker
205
+
206
+ A time slot picker for selecting single times, multiple times, or time ranges.
207
+
208
+ ### Attributes
209
+
210
+ | Attribute | Type | Default | Description |
211
+ |-----------|------|---------|-------------|
212
+ | `mode` | `"single"` \| `"multi"` \| `"range"` | `"single"` | Selection mode |
213
+ | `display` | `"inline"` \| `"popover"` | `"inline"` | Render mode |
214
+ | `theme` | `"light"` \| `"dark"` \| `"auto"` | `"light"` | Color theme |
215
+ | `start-time` | `string` | `"09:00"` | First slot time (`"HH:MM"`) |
216
+ | `end-time` | `string` | `"17:00"` | Last slot boundary (`"HH:MM"`) |
217
+ | `interval` | `number` | `30` | Minutes between slots |
218
+ | `format` | `"12h"` \| `"24h"` | `"24h"` | Time display format |
219
+ | `value` | `string` | — | Initial value. Single: `"14:30"`. Range: `"09:00/12:00"`. Multi: `"09:00,10:30"` |
220
+ | `placeholder` | `string` | `"Select time"` | Popover trigger placeholder |
221
+ | `duration-labels` | `boolean` | — | Show duration labels on each slot |
222
+ | `loading` | `boolean` | — | Show skeleton loading state |
223
+
224
+ ### Properties
225
+
226
+ | Property | Type | Description |
227
+ |----------|------|-------------|
228
+ | `value` | `string \| string[] \| {start: string, end: string} \| null` | Read/write. Shape depends on `mode` |
229
+ | `slots` | `Array<{time: string, label?: string, available?: boolean}>` | Custom slot definitions (overrides auto-generation) |
230
+ | `unavailableTimes` | `string[]` | Array of `"HH:MM"` strings to mark unavailable |
231
+ | `loading` | `boolean` | Read/write loading state |
232
+
233
+ ### Methods
234
+
235
+ | Method | Parameters | Description |
236
+ |--------|------------|-------------|
237
+ | `open()` | — | Open popover |
238
+ | `close()` | — | Close popover |
239
+ | `showStatus(type, message, opts?)` | `type: "error"\|"warning"\|"info"\|"success"` | Show status banner |
240
+ | `clearStatus()` | — | Clear status banner |
241
+
242
+ ### Events
243
+
244
+ | Event | `detail` shape | Fires when |
245
+ |-------|---------------|------------|
246
+ | `cal:time-change` | `{value: string}` (single) / `{value: {start, end}}` (range) / `{value: string[]}` (multi) | Time selection changes |
247
+ | `cal:open` | `{}` | Popover opens |
248
+ | `cal:close` | `{}` | Popover closes |
249
+ | `cal:status` | `{type: string\|null, message: string\|null}` | Status banner changes |
250
+
251
+ ### Example: Duration Labels
252
+
253
+ ```html
254
+ <cal-timepicker
255
+ start-time="09:00"
256
+ end-time="17:00"
257
+ interval="60"
258
+ format="12h"
259
+ duration-labels
260
+ ></cal-timepicker>
261
+ ```
262
+
263
+ ### Example: Custom Slots with Unavailable Times
264
+
265
+ ```html
266
+ <cal-timepicker mode="single" format="12h"></cal-timepicker>
267
+
268
+ <script>
269
+ const tp = document.querySelector('cal-timepicker');
270
+ tp.slots = [
271
+ { time: '09:00', label: 'Morning', available: true },
272
+ { time: '12:00', label: 'Lunch', available: false },
273
+ { time: '14:00', label: 'Afternoon', available: true },
274
+ ];
275
+ </script>
276
+ ```
277
+
278
+ > Full reference: [docs/cal-timepicker.md](docs/cal-timepicker.md)
279
+
280
+ ---
281
+
282
+ ## cal-booking
283
+
284
+ A booking calendar that displays existing reservations as colored overlays and lets users select date ranges. Supports overlap validation and optional time slot selection.
285
+
286
+ ### Attributes
287
+
288
+ | Attribute | Type | Default | Description |
289
+ |-----------|------|---------|-------------|
290
+ | `theme` | `"light"` \| `"dark"` \| `"auto"` | `"light"` | Color theme |
291
+ | `display` | `"inline"` \| `"popover"` | `"inline"` | Render mode |
292
+ | `min-date` | `string` | — | Earliest selectable date |
293
+ | `max-date` | `string` | — | Latest selectable date |
294
+ | `first-day` | `number` | `0` | First day of week |
295
+ | `placeholder` | `string` | `"Select dates"` | Popover trigger placeholder |
296
+ | `dual` | `boolean` | — | Show two months side-by-side |
297
+ | `show-labels-on-hover` | `boolean` | — | Show booking labels on day hover |
298
+ | `time-slots` | `boolean` | — | Enable time slot selection after date range |
299
+ | `time-start` | `string` | `"09:00"` | Time grid start (when `time-slots` enabled) |
300
+ | `time-end` | `string` | `"17:00"` | Time grid end |
301
+ | `time-interval` | `number` | `60` | Time grid interval in minutes |
302
+ | `time-format` | `"12h"` \| `"24h"` | `"24h"` | Time display format |
303
+ | `duration-labels` | `boolean` | — | Show duration labels on time slots |
304
+ | `loading` | `boolean` | — | Show skeleton loading state |
305
+
306
+ ### Properties
307
+
308
+ | Property | Type | Description |
309
+ |----------|------|-------------|
310
+ | `value` | `{start: string, end: string, startTime?: string, endTime?: string} \| null` | Read/write selected range |
311
+ | `bookings` | `Booking[]` | Array of existing bookings to display |
312
+ | `dayData` | `Record<string, {label?: string, status?: string}>` | Per-date metadata |
313
+ | `labelFormula` | `(dateStr: string) => {label?: string, status?: string} \| null` | Dynamic label function (highest priority) |
314
+ | `timeSlots` | `Array<{time: string, label?: string, available?: boolean}>` | Custom time slot definitions |
315
+ | `loading` | `boolean` | Read/write loading state |
316
+
317
+ ### Methods
318
+
319
+ | Method | Parameters | Description |
320
+ |--------|------------|-------------|
321
+ | `open()` | — | Open popover |
322
+ | `close()` | — | Close popover |
323
+ | `goToMonth(month, year)` | `month: number (0-11), year: number` | Navigate to month |
324
+ | `showStatus(type, message, opts?)` | `type: "error"\|"warning"\|"info"\|"success"` | Show status banner |
325
+ | `clearStatus()` | — | Clear status banner |
326
+
327
+ ### Events
328
+
329
+ | Event | `detail` shape | Fires when |
330
+ |-------|---------------|------------|
331
+ | `cal:change` | `{value: {start, end, startTime?, endTime?}}` | Range selection completes |
332
+ | `cal:selection-invalid` | `{start: string, end: string}` | Selection overlaps existing booking |
333
+ | `cal:month-change` | `{year: number, month: number}` | View navigates to new month |
334
+ | `cal:open` | `{}` | Popover opens |
335
+ | `cal:close` | `{}` | Popover closes |
336
+ | `cal:status` | `{type: string\|null, message: string\|null}` | Status banner changes |
337
+
338
+ ### Example: Booking Calendar with Bookings
339
+
340
+ ```html
341
+ <cal-booking theme="light" dual></cal-booking>
342
+
343
+ <script>
344
+ const booking = document.querySelector('cal-booking');
345
+ booking.bookings = [
346
+ { id: '1', start: '2026-03-05', end: '2026-03-10', label: 'Alice', color: 'blue' },
347
+ { id: '2', start: '2026-03-10', end: '2026-03-15', label: 'Bob', color: 'green' },
348
+ { id: '3', start: '2026-03-20', end: '2026-03-25', label: 'Carol', color: 'orange' },
349
+ ];
350
+
351
+ // Price labels via formula
352
+ booking.labelFormula = (date) => {
353
+ const d = new Date(date);
354
+ const isWeekend = d.getDay() === 0 || d.getDay() === 6;
355
+ return { label: isWeekend ? '$150' : '$100' };
356
+ };
357
+
358
+ booking.addEventListener('cal:change', (e) => {
359
+ console.log('Selected range:', e.detail.value);
360
+ });
361
+
362
+ booking.addEventListener('cal:selection-invalid', () => {
363
+ console.log('Overlaps existing booking!');
364
+ });
365
+ </script>
366
+ ```
367
+
368
+ ### Example: Date + Time Flow
369
+
370
+ ```html
371
+ <cal-booking
372
+ time-slots
373
+ time-start="14:00"
374
+ time-end="22:00"
375
+ time-interval="30"
376
+ time-format="12h"
377
+ ></cal-booking>
378
+
379
+ <script>
380
+ document.querySelector('cal-booking').addEventListener('cal:change', (e) => {
381
+ // { start: "2026-03-10", end: "2026-03-12", startTime: "14:00", endTime: "11:00" }
382
+ console.log(e.detail.value);
383
+ });
384
+ </script>
385
+ ```
386
+
387
+ > Full reference: [docs/cal-booking.md](docs/cal-booking.md)
388
+
389
+ ---
390
+
391
+ ## cal-scheduler
392
+
393
+ A full-featured resource scheduling calendar with day, week, and month views. Supports drag-to-move, drag-to-resize, drag-to-create, resource tabs/columns, all-day events, and a floating action button.
394
+
395
+ ### Attributes
396
+
397
+ | Attribute | Type | Default | Description |
398
+ |-----------|------|---------|-------------|
399
+ | `theme` | `"light"` \| `"dark"` \| `"auto"` | `"light"` | Color theme |
400
+ | `view` | `"day"` \| `"week"` \| `"month"` | `"week"` | Current calendar view |
401
+ | `layout` | `"vertical"` | `"vertical"` | Grid layout |
402
+ | `date` | `string` | today | Anchor date (`"YYYY-MM-DD"`) |
403
+ | `start-time` | `string` | `"08:00"` | Day grid start time |
404
+ | `end-time` | `string` | `"18:00"` | Day grid end time |
405
+ | `interval` | `number` | `30` | Slot interval in minutes |
406
+ | `format` | `"12h"` \| `"24h"` | `"24h"` | Time display format |
407
+ | `first-day` | `number` | `0` | First day of week |
408
+ | `slot-height` | `number` | `48` | Pixel height per time slot |
409
+ | `resource-mode` | `"tabs"` \| `"columns"` | `"tabs"` | How resources are displayed |
410
+ | `show-event-time` | `"true"` \| `"false"` | `"true"` | Show time row on event blocks |
411
+ | `show-fab` | `boolean` | — | Show floating action button |
412
+ | `draggable-events` | `boolean` | — | Enable drag-to-move/resize/create |
413
+ | `snap-interval` | `number` | — | Drag snap interval in minutes (defaults to `interval`) |
414
+ | `min-duration` | `number` | — | Minimum event duration in minutes for drag operations |
415
+ | `max-duration` | `number` | — | Maximum event duration in minutes for drag operations |
416
+ | `loading` | `boolean` | — | Show skeleton loading state |
417
+
418
+ ### Properties
419
+
420
+ | Property | Type | Description |
421
+ |----------|------|-------------|
422
+ | `resources` | `Resource[]` | Array of resource objects |
423
+ | `events` | `Event[]` | Array of event objects |
424
+ | `eventActions` | `EventAction[]` | Action buttons shown in event detail popover |
425
+ | `eventContent` | `(event: Event, resource: Resource) => HTMLElement \| string` | Custom event block renderer |
426
+ | `value` | `{date, startTime, endTime, resourceId, resource} \| null` | Read-only last selected slot |
427
+ | `loading` | `boolean` | Read/write loading state |
428
+
429
+ ### Methods
430
+
431
+ | Method | Parameters | Description |
432
+ |--------|------------|-------------|
433
+ | `goToDate(dateStr)` | `dateStr: string` | Navigate to date |
434
+ | `setView(view)` | `view: "day"\|"week"\|"month"` | Switch view |
435
+ | `today()` | — | Navigate to today |
436
+ | `next()` | — | Navigate forward (day/week/month) |
437
+ | `prev()` | — | Navigate backward |
438
+ | `findAvailableSlot(opts)` | `{date?, duration, resourceId?, minCapacity?}` | Find first available slot across resources |
439
+ | `isSlotAvailable(date, startTime, endTime, resourceId)` | all `string` | Check if a specific slot is free |
440
+ | `showStatus(type, message, opts?)` | `type: "error"\|"warning"\|"info"\|"success"` | Show status banner |
441
+ | `clearStatus()` | — | Clear status banner |
442
+
443
+ ### Events
444
+
445
+ | Event | `detail` shape | Fires when |
446
+ |-------|---------------|------------|
447
+ | `cal:slot-select` | `{date, startTime, endTime, resourceId, resource}` | Empty slot is clicked |
448
+ | `cal:slot-create` | `{date, startTime, endTime, resourceId, resource}` | Drag-to-create completes |
449
+ | `cal:event-click` | `{event, resourceId, resource}` | Event block is clicked |
450
+ | `cal:event-move` | `{event, from: {date, startTime, endTime, resourceId}, to: {date, startTime, endTime, resourceId}}` | Drag-to-move completes |
451
+ | `cal:event-resize` | `{event, from: {endTime}, to: {endTime}}` | Drag-to-resize completes |
452
+ | `cal:event-action` | `{action: string, event, resourceId, resource}` | Event detail action button clicked |
453
+ | `cal:fab-create` | `{date: string, view: string}` | FAB button clicked |
454
+ | `cal:date-change` | `{date: string, view: string}` | Navigation changes date |
455
+ | `cal:view-change` | `{view: string, date: string}` | View type changes |
456
+ | `cal:status` | `{type: string\|null, message: string\|null}` | Status banner changes |
457
+
458
+ ### Example: Week View with Resources
459
+
460
+ ```html
461
+ <cal-scheduler
462
+ view="week"
463
+ resource-mode="tabs"
464
+ start-time="08:00"
465
+ end-time="18:00"
466
+ format="12h"
467
+ theme="light"
468
+ ></cal-scheduler>
469
+
470
+ <script>
471
+ const sched = document.querySelector('cal-scheduler');
472
+
473
+ sched.resources = [
474
+ { id: 'room-a', name: 'Room A', capacity: 10 },
475
+ { id: 'room-b', name: 'Room B', capacity: 20 },
476
+ ];
477
+
478
+ sched.events = [
479
+ {
480
+ id: '1', title: 'Team Standup',
481
+ start: '2026-03-02', startTime: '09:00', endTime: '09:30',
482
+ resourceId: 'room-a', color: 'blue',
483
+ },
484
+ {
485
+ id: '2', title: 'Workshop',
486
+ start: '2026-03-03', startTime: '13:00', endTime: '16:00',
487
+ resourceId: 'room-b', color: 'green',
488
+ },
489
+ ];
490
+
491
+ sched.eventActions = [
492
+ { label: 'Edit' },
493
+ { label: 'Delete', type: 'danger' },
494
+ ];
495
+
496
+ sched.addEventListener('cal:event-action', (e) => {
497
+ console.log(e.detail.action, e.detail.event);
498
+ });
499
+ </script>
500
+ ```
501
+
502
+ ### Example: Drag-to-Move and Resize
503
+
504
+ ```html
505
+ <cal-scheduler
506
+ view="week"
507
+ draggable-events
508
+ snap-interval="15"
509
+ min-duration="15"
510
+ max-duration="240"
511
+ ></cal-scheduler>
512
+
513
+ <script>
514
+ const sched = document.querySelector('cal-scheduler');
515
+ // ... set resources and events ...
516
+
517
+ sched.addEventListener('cal:event-move', (e) => {
518
+ const { event, from, to } = e.detail;
519
+ console.log(`Moved "${event.title}" from ${from.date} ${from.startTime} to ${to.date} ${to.startTime}`);
520
+ // Update your data and re-assign sched.events
521
+ });
522
+
523
+ sched.addEventListener('cal:event-resize', (e) => {
524
+ const { event, from, to } = e.detail;
525
+ console.log(`Resized "${event.title}" end: ${from.endTime} → ${to.endTime}`);
526
+ });
527
+
528
+ sched.addEventListener('cal:slot-create', (e) => {
529
+ console.log('New event via drag:', e.detail);
530
+ // { date, startTime, endTime, resourceId, resource }
531
+ });
532
+ </script>
533
+ ```
534
+
535
+ ### Example: Venue Booking with Availability Check
536
+
537
+ ```html
538
+ <cal-scheduler view="day" show-fab></cal-scheduler>
539
+
540
+ <script>
541
+ const sched = document.querySelector('cal-scheduler');
542
+
543
+ sched.resources = [
544
+ { id: 'court-1', name: 'Court 1', capacity: 4 },
545
+ { id: 'court-2', name: 'Court 2', capacity: 4 },
546
+ ];
547
+
548
+ // Find first available 60-minute slot
549
+ const slot = sched.findAvailableSlot({ duration: 60 });
550
+ if (slot) {
551
+ console.log(`Available: ${slot.date} ${slot.startTime}-${slot.endTime} on ${slot.resourceId}`);
552
+ }
553
+
554
+ // Check specific slot
555
+ const free = sched.isSlotAvailable('2026-03-02', '10:00', '11:00', 'court-1');
556
+
557
+ sched.addEventListener('cal:fab-create', (e) => {
558
+ console.log('Create new event for', e.detail.date);
559
+ });
560
+ </script>
561
+ ```
562
+
563
+ > Full reference: [docs/cal-scheduler.md](docs/cal-scheduler.md)
564
+
565
+ ---
566
+
567
+ ## Theming
568
+
569
+ All components support three theme modes via the `theme` attribute:
570
+
571
+ | Value | Behavior |
572
+ |-------|----------|
573
+ | `"light"` (default) | Light color scheme |
574
+ | `"dark"` | Dark color scheme |
575
+ | `"auto"` | Follows `prefers-color-scheme` media query |
576
+
577
+ ### Top CSS Custom Properties
578
+
579
+ Override from outside the Shadow DOM using the tag selector:
580
+
581
+ | Token | Light Default | Description |
582
+ |-------|--------------|-------------|
583
+ | `--cal-bg` | `0 0% 100%` | Background color (HSL channels) |
584
+ | `--cal-fg` | `240 6% 10%` | Foreground/text color |
585
+ | `--cal-accent` | `240 6% 10%` | Accent color for selected states |
586
+ | `--cal-border` | `240 6% 90%` | Border color |
587
+ | `--cal-radius` | `8px` | Border radius |
588
+
589
+ ### Override Example
590
+
591
+ ```css
592
+ cal-datepicker, cal-scheduler {
593
+ --cal-accent: 220 90% 56%;
594
+ --cal-accent-fg: 0 0% 100%;
595
+ --cal-radius: 12px;
596
+ }
597
+ ```
598
+
599
+ All color tokens use raw HSL channels (e.g., `240 6% 10%`) and are consumed internally as `hsl(var(--cal-...))`. This allows alpha modifications with the `/` syntax: `hsl(var(--cal-accent) / 0.5)`.
600
+
601
+ > Full token table: [docs/theming.md](docs/theming.md)
602
+
603
+ ---
604
+
605
+ ## Data Shapes
606
+
607
+ ```ts
608
+ interface Event {
609
+ id: string;
610
+ title: string;
611
+ start: string; // "YYYY-MM-DD"
612
+ end?: string; // "YYYY-MM-DD" (multi-day or all-day)
613
+ startTime?: string; // "HH:MM" (omit for all-day events)
614
+ endTime?: string; // "HH:MM"
615
+ resourceId?: string; // links to Resource.id
616
+ color?: "blue" | "green" | "red" | "orange" | "gray";
617
+ locked?: boolean; // prevents drag operations
618
+ metadata?: Record<string, string | number>;
619
+ }
620
+
621
+ interface Resource {
622
+ id: string;
623
+ name: string;
624
+ capacity?: number;
625
+ color?: "blue" | "green" | "red" | "orange" | "gray";
626
+ }
627
+
628
+ interface Booking {
629
+ id: string;
630
+ start: string; // "YYYY-MM-DD"
631
+ end: string; // "YYYY-MM-DD"
632
+ label?: string;
633
+ color?: "blue" | "green" | "red" | "orange" | "gray";
634
+ }
635
+
636
+ interface TimeSlot {
637
+ time: string; // "HH:MM"
638
+ label?: string; // display label
639
+ available?: boolean; // default: true
640
+ }
641
+
642
+ interface DayData {
643
+ [dateStr: string]: {
644
+ label?: string;
645
+ status?: string; // "available" | "booked" | custom
646
+ };
647
+ }
648
+
649
+ interface EventAction {
650
+ label: string;
651
+ type?: "danger"; // red styling
652
+ }
653
+ ```
654
+
655
+ ---
656
+
657
+ ## Framework Integration
658
+
659
+ CalKit components are standard web components. They work in any framework.
660
+
661
+ ### React
662
+
663
+ ```jsx
664
+ import 'calkit';
665
+
666
+ function App() {
667
+ const ref = useRef(null);
668
+
669
+ useEffect(() => {
670
+ const el = ref.current;
671
+ el.events = [/* ... */];
672
+
673
+ const handler = (e) => console.log(e.detail);
674
+ el.addEventListener('cal:change', handler);
675
+ return () => el.removeEventListener('cal:change', handler);
676
+ }, []);
677
+
678
+ return <cal-datepicker ref={ref} mode="single" theme="light" />;
679
+ }
680
+ ```
681
+
682
+ ### Vue
683
+
684
+ ```vue
685
+ <template>
686
+ <cal-datepicker
687
+ ref="picker"
688
+ mode="range"
689
+ theme="light"
690
+ @cal:change="onChange"
691
+ />
692
+ </template>
693
+
694
+ <script setup>
695
+ import 'calkit';
696
+
697
+ function onChange(e) {
698
+ console.log(e.detail.value);
699
+ }
700
+ </script>
701
+ ```
702
+
703
+ ### Svelte
704
+
705
+ ```svelte
706
+ <script>
707
+ import 'calkit';
708
+
709
+ function handleChange(e) {
710
+ console.log(e.detail.value);
711
+ }
712
+ </script>
713
+
714
+ <cal-datepicker mode="single" theme="light" on:cal:change={handleChange} />
715
+ ```
716
+
717
+ ---
718
+
719
+ ## Browser Support
720
+
721
+ CalKit requires [Constructable Stylesheets](https://web.dev/constructable-stylesheets/) (`adoptedStyleSheets`). Fallback `<style>` injection is included for older browsers.
722
+
723
+ | Browser | Version |
724
+ |---------|---------|
725
+ | Chrome | 73+ |
726
+ | Edge | 79+ |
727
+ | Firefox | 101+ |
728
+ | Safari | 16.4+ |
729
+
730
+ ---
731
+
732
+ ## License
733
+
734
+ MIT