@nomideusz/svelte-calendar 0.6.3 → 0.6.5
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 +192 -10
- package/dist/adapters/composite.d.ts +15 -0
- package/dist/adapters/composite.js +65 -0
- package/dist/adapters/index.d.ts +5 -1
- package/dist/adapters/index.js +2 -0
- package/dist/adapters/mapped.d.ts +170 -0
- package/dist/adapters/mapped.js +343 -0
- package/dist/adapters/recurring.d.ts +4 -0
- package/dist/adapters/recurring.js +5 -10
- package/dist/adapters/types.d.ts +11 -2
- package/dist/assets/favicon.svg +9 -1
- package/dist/calendar/Calendar.svelte +161 -90
- package/dist/calendar/Calendar.svelte.d.ts +12 -0
- package/dist/core/index.d.ts +3 -1
- package/dist/core/index.js +2 -0
- package/dist/core/measure.d.ts +104 -0
- package/dist/core/measure.js +195 -0
- package/dist/core/time.js +2 -2
- package/dist/core/types.d.ts +32 -0
- package/dist/engine/event-store.svelte.js +22 -7
- package/dist/headless/create-agenda.svelte.d.ts +67 -0
- package/dist/headless/create-agenda.svelte.js +139 -0
- package/dist/headless/create-calendar.svelte.d.ts +2 -0
- package/dist/headless/create-calendar.svelte.js +380 -0
- package/dist/headless/index.d.ts +4 -0
- package/dist/headless/index.js +2 -0
- package/dist/headless/types.d.ts +219 -0
- package/dist/headless/types.js +1 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.js +4 -2
- package/dist/primitives/DayHeader.svelte +6 -6
- package/dist/primitives/EmptySlot.svelte +3 -3
- package/dist/primitives/EventBlock.svelte +99 -14
- package/dist/primitives/NowIndicator.svelte +5 -5
- package/dist/views/agenda/AgendaDay.svelte +215 -116
- package/dist/views/agenda/AgendaWeek.svelte +166 -113
- package/dist/views/mobile/MobileDay.svelte +45 -21
- package/dist/views/mobile/MobileWeek.svelte +85 -35
- package/dist/views/planner/PlannerDay.svelte +116 -60
- package/dist/views/planner/PlannerWeek.svelte +228 -166
- package/dist/views/shared/context.svelte.d.ts +47 -0
- package/dist/views/shared/context.svelte.js +47 -0
- package/dist/views/shared/format.d.ts +19 -0
- package/dist/views/shared/format.js +46 -0
- package/dist/views/shared/index.d.ts +4 -0
- package/dist/views/shared/index.js +2 -0
- package/package.json +9 -2
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ pnpm add @nomideusz/svelte-calendar
|
|
|
25
25
|
<Calendar {adapter} />
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
That's it — 6 views (Day/Week × Planner, Agenda, Mobile), auto-coloring, drag-and-drop, all out of the box. The default `auto` theme probes your page's background, accent color, and fonts, so the calendar adapts to any design.
|
|
28
|
+
That's it — 6 views (Day/Week × Planner, Agenda, Mobile), auto-coloring, drag-and-drop with live previews, all out of the box. The default `auto` theme probes your page's background, accent color, and fonts, so the calendar adapts to any design.
|
|
29
29
|
|
|
30
30
|
## Views
|
|
31
31
|
|
|
@@ -40,6 +40,8 @@ Switch between **Planner** (time grid) and **Agenda** (list) in Day or Week mode
|
|
|
40
40
|
|
|
41
41
|
Users can also switch via the built-in Day/Week pills.
|
|
42
42
|
|
|
43
|
+
Planner views are designed for direct manipulation: drag an event to another day or time and the calendar renders a ghost preview at the target position before committing the move.
|
|
44
|
+
|
|
43
45
|
Hide the pills when your app controls the view externally:
|
|
44
46
|
|
|
45
47
|
```svelte
|
|
@@ -62,8 +64,8 @@ On narrow screens (`< 768px`), the calendar automatically remaps Planner views t
|
|
|
62
64
|
```
|
|
63
65
|
|
|
64
66
|
Mobile views include:
|
|
65
|
-
- **MobileDay** — vertical time grid with hour labels, swipe left/right to change days, all-day event chips at the top, tap-to-create
|
|
66
|
-
- **MobileWeek** — vertical day list showing each day's events with relative labels (Today, Tomorrow, etc.)
|
|
67
|
+
- **MobileDay** — vertical time grid with hour labels, swipe left/right to change days, all-day event chips at the top, tap-to-create, and stable columns for overlapping events
|
|
68
|
+
- **MobileWeek** — vertical day list showing each day's events with relative labels (Today, Tomorrow, etc.) and accessible event buttons inside each row
|
|
67
69
|
|
|
68
70
|
Agenda views keep their list-based layout on mobile but hide desktop floating navigation in favor of the centralized header.
|
|
69
71
|
|
|
@@ -150,6 +152,8 @@ Disable specific dates:
|
|
|
150
152
|
/>
|
|
151
153
|
```
|
|
152
154
|
|
|
155
|
+
Disabled dates prevent creating or moving events into those days. Existing events remain visible and clickable, which is useful for holidays, fully booked days, or read-only imported schedules.
|
|
156
|
+
|
|
153
157
|
Custom day headers and hover previews:
|
|
154
158
|
|
|
155
159
|
```svelte
|
|
@@ -160,6 +164,37 @@ Custom day headers and hover previews:
|
|
|
160
164
|
</Calendar>
|
|
161
165
|
```
|
|
162
166
|
|
|
167
|
+
Replace the built-in navigation or the entire header chrome with your own controls:
|
|
168
|
+
|
|
169
|
+
```svelte
|
|
170
|
+
<Calendar {adapter}>
|
|
171
|
+
{#snippet navigation({ prev, next, goToday, isViewOnToday, mode })}
|
|
172
|
+
<button onclick={prev}>‹</button>
|
|
173
|
+
{#if !isViewOnToday}<button onclick={goToday}>Today</button>{/if}
|
|
174
|
+
<button onclick={next}>›</button>
|
|
175
|
+
{/snippet}
|
|
176
|
+
</Calendar>
|
|
177
|
+
|
|
178
|
+
<Calendar {adapter}>
|
|
179
|
+
{#snippet header({ dateLabel, mode, switchMode, prev, next, goToday })}
|
|
180
|
+
<nav class="my-toolbar">
|
|
181
|
+
<h2>{dateLabel}</h2>
|
|
182
|
+
<button class:active={mode === 'day'} onclick={() => switchMode('day')}>Day</button>
|
|
183
|
+
<button class:active={mode === 'week'} onclick={() => switchMode('week')}>Week</button>
|
|
184
|
+
<button onclick={prev}>←</button>
|
|
185
|
+
<button onclick={goToday}>Today</button>
|
|
186
|
+
<button onclick={next}>→</button>
|
|
187
|
+
</nav>
|
|
188
|
+
{/snippet}
|
|
189
|
+
</Calendar>
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Let agenda content determine height instead of forcing a fixed box — useful when embedding inside a scrolling page:
|
|
193
|
+
|
|
194
|
+
```svelte
|
|
195
|
+
<Calendar {adapter} view="week-agenda" height="auto" compact />
|
|
196
|
+
```
|
|
197
|
+
|
|
163
198
|
## Themes
|
|
164
199
|
|
|
165
200
|
Three built-in presets:
|
|
@@ -233,7 +268,7 @@ Or set them at the page level:
|
|
|
233
268
|
}
|
|
234
269
|
```
|
|
235
270
|
|
|
236
|
-
The auto-probe reads these first — if you set `--dt-bg` or `--accent` on `:root`, the calendar picks them up.
|
|
271
|
+
The auto-probe reads these first — if you set `--dt-bg` or `--accent` on `:root`, the calendar picks them up. Component fallbacks intentionally use system fonts and neutral blue accents so the package stays clean in apps that do not provide custom tokens.
|
|
237
272
|
|
|
238
273
|
### Extending Presets
|
|
239
274
|
|
|
@@ -288,8 +323,14 @@ const custom = `${neutral}; --dt-accent: #e11d48;`;
|
|
|
288
323
|
| `subtitle` | `string?` | Secondary text below the title |
|
|
289
324
|
| `tags` | `string[]?` | Small accent-colored pills |
|
|
290
325
|
| `allDay` | `boolean?` | Render as an all-day event |
|
|
326
|
+
| `location` | `string?` | Room, venue, or address (universal across domains) |
|
|
327
|
+
| `status` | `EventStatus?` | `'confirmed'` (default), `'cancelled'`, `'tentative'`, `'full'`, `'limited'` |
|
|
328
|
+
| `externalId` | `string?` | ID from an upstream system (booking platform, CRM, LMS) |
|
|
329
|
+
| `resourceId` | `string?` | Resource this event belongs to (room, instructor, court) for multi-resource views |
|
|
291
330
|
| `data` | `Record?` | Arbitrary payload for your app |
|
|
292
331
|
|
|
332
|
+
Cancelled events render with a strikethrough on the grid but remain visible so the slot isn't confused for free time.
|
|
333
|
+
|
|
293
334
|
### Auto-Coloring
|
|
294
335
|
|
|
295
336
|
Omit `color` and events are auto-assigned a vivid palette color, grouped by `category` (or `title`):
|
|
@@ -400,6 +441,74 @@ const adapter = createRestAdapter({
|
|
|
400
441
|
|
|
401
442
|
The adapter calls `GET /events?start=...&end=...`, `POST /events`, `PATCH /events/:id`, and `DELETE /events/:id`.
|
|
402
443
|
|
|
444
|
+
## Mapped Adapter
|
|
445
|
+
|
|
446
|
+
Wrap any static array of external records (yoga classes, gym schedules, appointments, timetables) without writing a custom adapter. Supply a declarative field mapping — or a `mapEvent` function for full control — and the adapter handles parsing, color assignment, tag extraction, and status coercion:
|
|
447
|
+
|
|
448
|
+
```ts
|
|
449
|
+
import { createMappedAdapter } from '@nomideusz/svelte-calendar';
|
|
450
|
+
|
|
451
|
+
const adapter = createMappedAdapter(rawClasses, {
|
|
452
|
+
fields: {
|
|
453
|
+
title: 'class_name',
|
|
454
|
+
start: 'starts_at_iso',
|
|
455
|
+
end: 'ends_at_iso',
|
|
456
|
+
subtitle: 'teacher',
|
|
457
|
+
location: 'room',
|
|
458
|
+
color: 'color',
|
|
459
|
+
externalId: 'reference_id',
|
|
460
|
+
status: 'is_cancelled', // boolean → 'cancelled' / 'confirmed'
|
|
461
|
+
tags: ['is_free', 'is_bookable_online'],
|
|
462
|
+
},
|
|
463
|
+
});
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
`start`/`end` accept ISO strings, `Date` objects, or Unix timestamps. When source records split date and time (`date: "2026-03-03"`, `startTime: "07:00"`), use `date` + `startTime` / `endTime` instead.
|
|
467
|
+
|
|
468
|
+
For full control, pass a `mapEvent` transform and skip `fields` entirely:
|
|
469
|
+
|
|
470
|
+
```ts
|
|
471
|
+
const adapter = createMappedAdapter(rawData, {
|
|
472
|
+
mapEvent: (raw) => ({
|
|
473
|
+
id: raw.uid,
|
|
474
|
+
title: raw.procedure_name,
|
|
475
|
+
start: new Date(raw.scheduled_at),
|
|
476
|
+
end: new Date(raw.scheduled_end),
|
|
477
|
+
location: raw.office,
|
|
478
|
+
resourceId: raw.doctor_id,
|
|
479
|
+
}),
|
|
480
|
+
});
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
Mapped adapters are read-only by default. Set `readOnly: false` and supply `onMutate: { onCreate, onUpdate, onDelete }` handlers to enable writes.
|
|
484
|
+
|
|
485
|
+
## Composite Adapter
|
|
486
|
+
|
|
487
|
+
Merge multiple adapters into one — e.g. a recurring weekly schedule combined with one-off events:
|
|
488
|
+
|
|
489
|
+
```ts
|
|
490
|
+
import {
|
|
491
|
+
Calendar,
|
|
492
|
+
createMemoryAdapter,
|
|
493
|
+
createRecurringAdapter,
|
|
494
|
+
createCompositeAdapter,
|
|
495
|
+
} from '@nomideusz/svelte-calendar';
|
|
496
|
+
|
|
497
|
+
const memory = createMemoryAdapter(oneOffEvents);
|
|
498
|
+
const recurring = createRecurringAdapter(weeklySchedule);
|
|
499
|
+
|
|
500
|
+
const adapter = createCompositeAdapter([memory, recurring]);
|
|
501
|
+
// Reads from both; writes go to memory (the primary — first adapter by default)
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
Change which adapter handles mutations with `primaryIndex`:
|
|
505
|
+
|
|
506
|
+
```ts
|
|
507
|
+
const adapter = createCompositeAdapter([recurring, memory], { primaryIndex: 1 });
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
Updates and deletes fall through each adapter in order — handy when the recurring adapter generates occurrences that the primary memory adapter doesn't know about.
|
|
511
|
+
|
|
403
512
|
## Custom Adapter
|
|
404
513
|
|
|
405
514
|
Implement the `CalendarAdapter` interface to connect any data source:
|
|
@@ -415,6 +524,61 @@ const adapter: CalendarAdapter = {
|
|
|
415
524
|
};
|
|
416
525
|
```
|
|
417
526
|
|
|
527
|
+
## Headless API
|
|
528
|
+
|
|
529
|
+
For full control over rendering, skip the `<Calendar>` component and drive everything from reactive state. `createCalendar()` returns computed layouts, navigation actions, drag helpers, and raw engines — zero DOM, bring your own UI:
|
|
530
|
+
|
|
531
|
+
```svelte
|
|
532
|
+
<script lang="ts">
|
|
533
|
+
import { createCalendar, createMemoryAdapter } from '@nomideusz/svelte-calendar';
|
|
534
|
+
|
|
535
|
+
const adapter = createMemoryAdapter([/* ... */]);
|
|
536
|
+
const cal = createCalendar({ adapter, view: 'week-planner' });
|
|
537
|
+
</script>
|
|
538
|
+
|
|
539
|
+
<header>
|
|
540
|
+
<button onclick={cal.prev}>←</button>
|
|
541
|
+
<span>{cal.headerContext.dateLabel}</span>
|
|
542
|
+
<button onclick={cal.next}>→</button>
|
|
543
|
+
<button onclick={cal.goToday}>Today</button>
|
|
544
|
+
</header>
|
|
545
|
+
|
|
546
|
+
{#each cal.weeks as week}
|
|
547
|
+
<div class="week-row">
|
|
548
|
+
{#each week.days as day}
|
|
549
|
+
<div class:today={day.isToday} class:past={day.isPast}>
|
|
550
|
+
<h3>{day.dayNum}</h3>
|
|
551
|
+
{#each day.events as ev}
|
|
552
|
+
<div style:background={ev.color}>{ev.title}</div>
|
|
553
|
+
{/each}
|
|
554
|
+
</div>
|
|
555
|
+
{/each}
|
|
556
|
+
</div>
|
|
557
|
+
{/each}
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
`cal.days` gives flat `HeadlessDay[]` with events attached; `cal.weeks` groups them into periods. `cal.todayQueue` returns `{ past, current, upcoming }` for today — updates every second via the built-in clock. `cal.hours` yields the visible hour numbers (cropped by `visibleHours`). Drag support is fully wired: `beginDragMove`, `beginDragCreate`, `updateDrag`, `commitDrag`, `cancelDrag`, plus `isDragging` / `dragPayload` / `dragMode` signals. Raw engines (`store`, `viewState`, `selection`, `dragState`, `clock`) are exposed for advanced cases.
|
|
561
|
+
|
|
562
|
+
For a simpler day-only list view, use `createAgenda()`:
|
|
563
|
+
|
|
564
|
+
```svelte
|
|
565
|
+
<script lang="ts">
|
|
566
|
+
import { createAgenda, createMemoryAdapter } from '@nomideusz/svelte-calendar';
|
|
567
|
+
|
|
568
|
+
const adapter = createMemoryAdapter([/* ... */]);
|
|
569
|
+
const agenda = createAgenda({ adapter });
|
|
570
|
+
</script>
|
|
571
|
+
|
|
572
|
+
<h2>{agenda.dateLabel}</h2>
|
|
573
|
+
{#each agenda.upcoming as ev}
|
|
574
|
+
<div>
|
|
575
|
+
<time>{agenda.fmtTime(ev.start)}</time>
|
|
576
|
+
<span>{ev.title}</span>
|
|
577
|
+
<small>{agenda.eta(ev)}</small>
|
|
578
|
+
</div>
|
|
579
|
+
{/each}
|
|
580
|
+
```
|
|
581
|
+
|
|
418
582
|
## Localization (i18n)
|
|
419
583
|
|
|
420
584
|
The `locale` prop controls date/time formatting (BCP 47):
|
|
@@ -489,7 +653,7 @@ const now = nowInZone('Asia/Tokyo');
|
|
|
489
653
|
|
|
490
654
|
## Embeddable Widget
|
|
491
655
|
|
|
492
|
-
Drop into any HTML page — no build tools needed:
|
|
656
|
+
Drop into any HTML page — no build tools needed. Registers a `<day-calendar>` custom element:
|
|
493
657
|
|
|
494
658
|
```html
|
|
495
659
|
<script src="https://cdn.jsdelivr.net/npm/@nomideusz/svelte-calendar/widget/widget.js"></script>
|
|
@@ -497,10 +661,25 @@ Drop into any HTML page — no build tools needed:
|
|
|
497
661
|
<day-calendar
|
|
498
662
|
api="https://myschool.com/api/events"
|
|
499
663
|
theme="neutral"
|
|
664
|
+
view="week-planner"
|
|
500
665
|
height="600"
|
|
666
|
+
locale="en-US"
|
|
667
|
+
mondaystart="true"
|
|
501
668
|
></day-calendar>
|
|
502
669
|
```
|
|
503
670
|
|
|
671
|
+
| Attribute | Description |
|
|
672
|
+
|-----------|-------------|
|
|
673
|
+
| `api` | REST endpoint — fetched as `GET {api}?start=...&end=...` |
|
|
674
|
+
| `events` | Inline JSON array of events (alternative to `api`) |
|
|
675
|
+
| `view` | Initial view ID (`week-planner`, `day-agenda`, …) |
|
|
676
|
+
| `theme` | `auto`, `neutral`, or `midnight` |
|
|
677
|
+
| `height` | Calendar height in px |
|
|
678
|
+
| `locale` | BCP 47 locale tag |
|
|
679
|
+
| `dir` | `ltr` or `rtl` |
|
|
680
|
+
| `mondaystart` | `"true"` / `"false"` — week-start day |
|
|
681
|
+
| `headers` | JSON string of extra headers to send with `api` requests |
|
|
682
|
+
|
|
504
683
|
## All Props
|
|
505
684
|
|
|
506
685
|
<details>
|
|
@@ -508,13 +687,13 @@ Drop into any HTML page — no build tools needed:
|
|
|
508
687
|
|
|
509
688
|
| Prop | Type | Default | Description |
|
|
510
689
|
|------|------|---------|-------------|
|
|
511
|
-
| `adapter` | `CalendarAdapter` | *required* | Data layer (memory, recurring, REST, or custom) |
|
|
512
|
-
| `views` | `CalendarView[]` |
|
|
690
|
+
| `adapter` | `CalendarAdapter` | *required* | Data layer (memory, recurring, mapped, composite, REST, or custom) |
|
|
691
|
+
| `views` | `CalendarView[]` | 6 built-in | Registered view components (4 desktop + 2 mobile variants) |
|
|
513
692
|
| `view` | `string` | first view | Active view ID |
|
|
514
693
|
| `theme` | `string` | `auto` | CSS theme string (`--dt-*` custom properties). `auto` probes the host page and generates matching tokens. |
|
|
515
694
|
| `autoTheme` | `AutoThemeOptions \| false` | `{}` | Fine-tune auto-detection: `{ mode, accent, font }`. Set `false` to disable probing. |
|
|
516
|
-
| `mobile` | `'auto' \| boolean` | `'auto'` | Mobile mode. `'auto'` detects via
|
|
517
|
-
| `height` | `number` | `600` |
|
|
695
|
+
| `mobile` | `'auto' \| boolean` | `'auto'` | Mobile mode. `'auto'` detects via container width (<768px). Remaps Planner→Mobile views. |
|
|
696
|
+
| `height` | `number \| 'auto'` | `600` | Height in pixels, or `'auto'` to let content grow naturally (ideal for Agenda views) |
|
|
518
697
|
| `borderRadius` | `number` | `12` | Border radius in pixels. Set to `0` for no rounding. |
|
|
519
698
|
| `locale` | `string` | `'en-US'` | BCP 47 locale tag |
|
|
520
699
|
| `dir` | `'ltr' \| 'rtl' \| 'auto'` | — | Text direction |
|
|
@@ -531,10 +710,13 @@ Drop into any HTML page — no build tools needed:
|
|
|
531
710
|
| `currentDate` | `Date` | — | Controlled focus date (drives which date the calendar shows) |
|
|
532
711
|
| `days` | `number` | `7` | Number of days shown in week views (e.g. `3` for a rolling 3-day view) |
|
|
533
712
|
| `blockedSlots` | `BlockedSlot[]` | — | Time ranges that cannot be booked (hatched overlay in planner views) |
|
|
534
|
-
| `disabledDates` | `Date[]` | — | Dates that
|
|
713
|
+
| `disabledDates` | `Date[]` | — | Dates that reject creation/moves while keeping existing events visible and clickable |
|
|
535
714
|
| `minDuration` | `number` | — | Minimum event duration in minutes (enforced on create & resize) |
|
|
536
715
|
| `maxDuration` | `number` | — | Maximum event duration in minutes (enforced on create & resize) |
|
|
716
|
+
| `compact` | `boolean` | `false` | Minimal text-row rendering in Agenda views (dot + time + title) |
|
|
537
717
|
| `dayHeader` | `Snippet<[{ date, isToday, dayName }]>` | — | Custom day header snippet for planner/agenda views |
|
|
718
|
+
| `header` | `Snippet<[HeaderContext]>` | — | Replace entire header chrome (date label + mode pills + nav) |
|
|
719
|
+
| `navigation` | `Snippet<[NavigationContext]>` | — | Replace just the prev/next/today controls |
|
|
538
720
|
| `oneventclick` | `(event) => void` | — | Event clicked |
|
|
539
721
|
| `oneventcreate` | `(range) => void` | — | New time range selected |
|
|
540
722
|
| `oneventmove` | `(event, start, end) => void` | — | Event dragged to new time |
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CalendarAdapter } from './types.js';
|
|
2
|
+
export interface CompositeAdapterOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Index of the adapter that handles mutations (create/update/delete).
|
|
5
|
+
* Defaults to `0` (the first adapter).
|
|
6
|
+
*/
|
|
7
|
+
primaryIndex?: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Create a CalendarAdapter that merges events from multiple child adapters.
|
|
11
|
+
*
|
|
12
|
+
* @param adapters Array of child adapters to merge.
|
|
13
|
+
* @param options Optional configuration.
|
|
14
|
+
*/
|
|
15
|
+
export declare function createCompositeAdapter(adapters: CalendarAdapter[], options?: CompositeAdapterOptions): CalendarAdapter;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create a CalendarAdapter that merges events from multiple child adapters.
|
|
3
|
+
*
|
|
4
|
+
* @param adapters Array of child adapters to merge.
|
|
5
|
+
* @param options Optional configuration.
|
|
6
|
+
*/
|
|
7
|
+
export function createCompositeAdapter(adapters, options = {}) {
|
|
8
|
+
if (adapters.length === 0) {
|
|
9
|
+
throw new Error('createCompositeAdapter requires at least one adapter');
|
|
10
|
+
}
|
|
11
|
+
const { primaryIndex = 0 } = options;
|
|
12
|
+
if (primaryIndex < 0 || primaryIndex >= adapters.length) {
|
|
13
|
+
throw new Error(`primaryIndex ${primaryIndex} is out of range [0, ${adapters.length - 1}]`);
|
|
14
|
+
}
|
|
15
|
+
const primary = adapters[primaryIndex];
|
|
16
|
+
return {
|
|
17
|
+
async fetchEvents(range) {
|
|
18
|
+
const results = await Promise.all(adapters.map((a) => a.fetchEvents(range)));
|
|
19
|
+
// Flatten and deduplicate by id (first occurrence wins)
|
|
20
|
+
const seen = new Set();
|
|
21
|
+
const merged = [];
|
|
22
|
+
for (const batch of results) {
|
|
23
|
+
for (const ev of batch) {
|
|
24
|
+
if (!seen.has(ev.id)) {
|
|
25
|
+
seen.add(ev.id);
|
|
26
|
+
merged.push(ev);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return merged;
|
|
31
|
+
},
|
|
32
|
+
// Create always goes to primary
|
|
33
|
+
...(primary.createEvent ? { createEvent: (event) => primary.createEvent(event) } : {}),
|
|
34
|
+
// Update/delete: try each adapter that supports the operation.
|
|
35
|
+
// This handles the case where a recurring adapter generates events
|
|
36
|
+
// that the primary (memory) adapter doesn't know about.
|
|
37
|
+
async updateEvent(id, patch) {
|
|
38
|
+
for (const adapter of adapters) {
|
|
39
|
+
if (!adapter.updateEvent)
|
|
40
|
+
continue;
|
|
41
|
+
try {
|
|
42
|
+
return await adapter.updateEvent(id, patch);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Event not in this adapter, try next
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
throw new Error(`Event not found in any adapter: ${id}`);
|
|
49
|
+
},
|
|
50
|
+
async deleteEvent(id) {
|
|
51
|
+
for (const adapter of adapters) {
|
|
52
|
+
if (!adapter.deleteEvent)
|
|
53
|
+
continue;
|
|
54
|
+
try {
|
|
55
|
+
await adapter.deleteEvent(id);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// Event not in this adapter, try next
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
throw new Error(`Event not found in any adapter: ${id}`);
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
package/dist/adapters/index.d.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
export type { CalendarAdapter, DateRange } from './types.js';
|
|
1
|
+
export type { CalendarAdapter, WritableCalendarAdapter, DateRange } from './types.js';
|
|
2
2
|
export { createMemoryAdapter } from './memory.js';
|
|
3
3
|
export type { MemoryAdapterOptions } from './memory.js';
|
|
4
4
|
export { createRestAdapter } from './rest.js';
|
|
5
5
|
export type { RestAdapterOptions } from './rest.js';
|
|
6
6
|
export { createRecurringAdapter } from './recurring.js';
|
|
7
7
|
export type { RecurringEvent, RecurringAdapterOptions } from './recurring.js';
|
|
8
|
+
export { createMappedAdapter } from './mapped.js';
|
|
9
|
+
export type { FieldMapping, MappedAdapterOptions, MutationHandler } from './mapped.js';
|
|
10
|
+
export { createCompositeAdapter } from './composite.js';
|
|
11
|
+
export type { CompositeAdapterOptions } from './composite.js';
|
package/dist/adapters/index.js
CHANGED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mapped adapter — universal adapter factory for external schedule data.
|
|
3
|
+
*
|
|
4
|
+
* Takes any array of external records (yoga classes, gym schedules,
|
|
5
|
+
* medical appointments, school timetables, etc.) and a mapping
|
|
6
|
+
* configuration that describes how to extract `TimelineEvent` fields
|
|
7
|
+
* from each record.
|
|
8
|
+
*
|
|
9
|
+
* Two usage modes:
|
|
10
|
+
* 1. **Field mapping** — declarative config mapping field names:
|
|
11
|
+
* createMappedAdapter(rawEvents, {
|
|
12
|
+
* fields: { title: 'class_name', start: 'starts_at_iso', ... }
|
|
13
|
+
* });
|
|
14
|
+
*
|
|
15
|
+
* 2. **Custom mapper** — full control via a transform function:
|
|
16
|
+
* createMappedAdapter(rawEvents, {
|
|
17
|
+
* mapEvent: (raw, i) => ({ id: raw.ref, title: raw.name, ... })
|
|
18
|
+
* });
|
|
19
|
+
*
|
|
20
|
+
* The adapter is read-only by default (no create/update/delete).
|
|
21
|
+
* Pass `readOnly: false` and supply mutation callbacks to enable writes.
|
|
22
|
+
*/
|
|
23
|
+
import type { TimelineEvent } from '../core/types.js';
|
|
24
|
+
import type { CalendarAdapter } from './types.js';
|
|
25
|
+
/**
|
|
26
|
+
* Declarative field mapping from external record keys to TimelineEvent fields.
|
|
27
|
+
*
|
|
28
|
+
* Each value is a key name (string) of the source object whose value
|
|
29
|
+
* will be used for the corresponding TimelineEvent field.
|
|
30
|
+
*
|
|
31
|
+
* For `start` and `end`, the source value can be:
|
|
32
|
+
* - an ISO 8601 string ("2026-03-03T07:00:00+01:00")
|
|
33
|
+
* - a Date object
|
|
34
|
+
* - a Unix timestamp (number, ms)
|
|
35
|
+
* - a pair of date + time strings when using `startDate`+`startTime`
|
|
36
|
+
*/
|
|
37
|
+
export interface FieldMapping {
|
|
38
|
+
/** Source key for `id`. Falls back to `reference_id`, `externalId`, or auto-generated. */
|
|
39
|
+
id?: string;
|
|
40
|
+
/** Source key for `title` (required unless `mapEvent` is used) */
|
|
41
|
+
title?: string;
|
|
42
|
+
/** Source key for a full start timestamp (ISO string, Date, or ms) */
|
|
43
|
+
start?: string;
|
|
44
|
+
/** Source key for a full end timestamp */
|
|
45
|
+
end?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Source key for a date-only string (e.g. "2026-03-03").
|
|
48
|
+
* Combined with `startTime` / `endTime` when `start`/`end` are not available.
|
|
49
|
+
*/
|
|
50
|
+
date?: string;
|
|
51
|
+
/** Source key for start time string (e.g. "07:00") — combined with `date` */
|
|
52
|
+
startTime?: string;
|
|
53
|
+
/** Source key for end time string (e.g. "08:15") — combined with `date` */
|
|
54
|
+
endTime?: string;
|
|
55
|
+
/** Source key for `color` */
|
|
56
|
+
color?: string;
|
|
57
|
+
/** Source key for `subtitle` */
|
|
58
|
+
subtitle?: string;
|
|
59
|
+
/** Source key for `location` */
|
|
60
|
+
location?: string;
|
|
61
|
+
/** Source key for `category` */
|
|
62
|
+
category?: string;
|
|
63
|
+
/** Source key for `resourceId` */
|
|
64
|
+
resourceId?: string;
|
|
65
|
+
/** Source key for `externalId` */
|
|
66
|
+
externalId?: string;
|
|
67
|
+
/**
|
|
68
|
+
* Source key for status. The source value is coerced:
|
|
69
|
+
* - boolean `true`/`false` on an `is_cancelled`-type field → 'cancelled'/'confirmed'
|
|
70
|
+
* - string 'cancelled'/'tentative'/'confirmed' used directly
|
|
71
|
+
*/
|
|
72
|
+
status?: string;
|
|
73
|
+
/**
|
|
74
|
+
* Source keys that should be collected as tags.
|
|
75
|
+
* Each key's value is included as a tag if truthy/non-empty.
|
|
76
|
+
* String values are used directly; booleans use the key name.
|
|
77
|
+
*/
|
|
78
|
+
tags?: string[];
|
|
79
|
+
}
|
|
80
|
+
export interface MappedAdapterOptions<T = Record<string, unknown>> {
|
|
81
|
+
/**
|
|
82
|
+
* Declarative field mapping — quick way to wire external fields
|
|
83
|
+
* to TimelineEvent properties.
|
|
84
|
+
*/
|
|
85
|
+
fields?: FieldMapping;
|
|
86
|
+
/**
|
|
87
|
+
* Custom mapper function — full control over the transformation.
|
|
88
|
+
* When provided, `fields` is ignored.
|
|
89
|
+
*/
|
|
90
|
+
mapEvent?: (raw: T, index: number) => TimelineEvent;
|
|
91
|
+
/**
|
|
92
|
+
* Fields to always include in `data` (the catch-all payload).
|
|
93
|
+
* Pass `'*'` to include all unmapped source fields.
|
|
94
|
+
* @default '*'
|
|
95
|
+
*/
|
|
96
|
+
includeData?: string[] | '*';
|
|
97
|
+
/**
|
|
98
|
+
* When true (default), ignore source colors and auto-assign from
|
|
99
|
+
* the palette grouped by category / title. This keeps the calendar
|
|
100
|
+
* visually consistent regardless of the upstream source.
|
|
101
|
+
* Set to `false` to preserve the original colors from the data.
|
|
102
|
+
* @default true
|
|
103
|
+
*/
|
|
104
|
+
autoColor?: boolean;
|
|
105
|
+
/**
|
|
106
|
+
* Color palette used for auto-coloring.
|
|
107
|
+
* Defaults to VIVID_PALETTE.
|
|
108
|
+
*/
|
|
109
|
+
palette?: string[];
|
|
110
|
+
/**
|
|
111
|
+
* When true (default), create/update/delete throw an error.
|
|
112
|
+
* Set to false and provide `onMutate` to enable writes.
|
|
113
|
+
*/
|
|
114
|
+
readOnly?: boolean;
|
|
115
|
+
/**
|
|
116
|
+
* Optional mutation handler for write operations.
|
|
117
|
+
* Only used when `readOnly` is false.
|
|
118
|
+
*/
|
|
119
|
+
onMutate?: MutationHandler<T>;
|
|
120
|
+
}
|
|
121
|
+
export interface MutationHandler<T = Record<string, unknown>> {
|
|
122
|
+
onCreate?: (event: Omit<TimelineEvent, 'id'>) => Promise<T>;
|
|
123
|
+
onUpdate?: (id: string, patch: Partial<TimelineEvent>) => Promise<T>;
|
|
124
|
+
onDelete?: (id: string) => Promise<void>;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Create a CalendarAdapter from an array of external records + mapping config.
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```ts
|
|
131
|
+
* // Yoga school schedule
|
|
132
|
+
* const adapter = createMappedAdapter(yogaSchedule.events, {
|
|
133
|
+
* fields: {
|
|
134
|
+
* title: 'class_name',
|
|
135
|
+
* start: 'starts_at_iso',
|
|
136
|
+
* end: 'ends_at_iso',
|
|
137
|
+
* subtitle: 'teacher',
|
|
138
|
+
* location: 'room',
|
|
139
|
+
* color: 'color',
|
|
140
|
+
* externalId: 'reference_id',
|
|
141
|
+
* status: 'is_cancelled',
|
|
142
|
+
* tags: ['is_free', 'is_bookable_online'],
|
|
143
|
+
* },
|
|
144
|
+
* });
|
|
145
|
+
*
|
|
146
|
+
* // Medical appointments
|
|
147
|
+
* const adapter = createMappedAdapter(appointments, {
|
|
148
|
+
* fields: {
|
|
149
|
+
* title: 'procedure_name',
|
|
150
|
+
* start: 'scheduled_at',
|
|
151
|
+
* end: 'scheduled_end',
|
|
152
|
+
* subtitle: 'doctor_name',
|
|
153
|
+
* location: 'office',
|
|
154
|
+
* resourceId: 'doctor_id',
|
|
155
|
+
* },
|
|
156
|
+
* });
|
|
157
|
+
*
|
|
158
|
+
* // Full custom mapper
|
|
159
|
+
* const adapter = createMappedAdapter(rawData, {
|
|
160
|
+
* mapEvent: (raw) => ({
|
|
161
|
+
* id: raw.uid,
|
|
162
|
+
* title: `${raw.first_name} ${raw.last_name}`,
|
|
163
|
+
* start: new Date(raw.timestamp),
|
|
164
|
+
* end: new Date(raw.timestamp + raw.duration_ms),
|
|
165
|
+
* location: raw.room,
|
|
166
|
+
* }),
|
|
167
|
+
* });
|
|
168
|
+
* ```
|
|
169
|
+
*/
|
|
170
|
+
export declare function createMappedAdapter<T extends Record<string, unknown> = Record<string, unknown>>(sourceData: T[], options?: MappedAdapterOptions<T>): CalendarAdapter;
|