@nomideusz/svelte-calendar 0.21.0 → 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/README.md +502 -269
- package/dist/adapters/composite.d.ts +12 -1
- package/dist/adapters/composite.js +109 -47
- package/dist/adapters/errors.d.ts +22 -0
- package/dist/adapters/errors.js +32 -0
- package/dist/adapters/index.d.ts +1 -0
- package/dist/adapters/index.js +1 -0
- package/dist/adapters/jmap.js +26 -17
- package/dist/adapters/mapped.d.ts +2 -2
- package/dist/adapters/mapped.js +18 -9
- package/dist/adapters/memory.d.ts +1 -1
- package/dist/adapters/memory.js +5 -3
- package/dist/adapters/recurring.d.ts +1 -1
- package/dist/adapters/recurring.js +37 -19
- package/dist/adapters/rest.d.ts +2 -2
- package/dist/adapters/rest.js +9 -4
- package/dist/calendar/Calendar.svelte +122 -78
- package/dist/calendar/Calendar.svelte.d.ts +69 -14
- package/dist/calendar/index.d.ts +2 -2
- package/dist/calendar/index.js +1 -1
- package/dist/core/clock.svelte.js +3 -2
- package/dist/core/locale.d.ts +11 -1
- package/dist/core/locale.js +12 -12
- package/dist/core/time.d.ts +9 -0
- package/dist/core/time.js +8 -0
- package/dist/core/timezone.d.ts +7 -3
- package/dist/core/timezone.js +10 -4
- package/dist/engine/event-store.svelte.js +93 -56
- package/dist/engine/view-state.svelte.d.ts +5 -1
- package/dist/engine/view-state.svelte.js +5 -3
- package/dist/headless/create-agenda.svelte.d.ts +8 -1
- package/dist/headless/create-agenda.svelte.js +32 -14
- package/dist/headless/create-calendar.svelte.js +53 -17
- package/dist/headless/create-range-agenda.svelte.d.ts +7 -0
- package/dist/headless/create-range-agenda.svelte.js +33 -12
- package/dist/headless/index.d.ts +1 -0
- package/dist/index.d.ts +10 -7
- package/dist/index.js +8 -5
- package/dist/primitives/DayHeader.svelte +3 -2
- package/dist/primitives/DayHeader.svelte.d.ts +1 -1
- package/dist/primitives/EmptySlot.svelte +5 -2
- package/dist/primitives/EventBlock.svelte +21 -14
- package/dist/primitives/FloatingPanel.svelte +110 -15
- package/dist/primitives/FloatingPanel.svelte.d.ts +7 -5
- package/dist/primitives/NowIndicator.svelte +11 -4
- package/dist/primitives/TimeGutter.svelte +10 -6
- package/dist/primitives/index.d.ts +1 -0
- package/dist/text-fit.d.ts +8 -2
- package/dist/views/agenda/AgendaDay.svelte +41 -33
- package/dist/views/agenda/AgendaWeek.svelte +71 -34
- package/dist/views/index.d.ts +2 -0
- package/dist/views/index.js +2 -0
- package/dist/views/mobile/MobileDay.svelte +98 -41
- package/dist/views/mobile/MobileWeek.svelte +29 -13
- package/dist/views/mobile/swipe.js +6 -1
- package/dist/views/month/MonthGrid.svelte +29 -17
- package/dist/views/month/MonthGrid.svelte.d.ts +1 -1
- package/dist/views/planner/PlannerScroll.svelte +183 -51
- package/dist/views/planner/PlannerWeek.svelte +148 -48
- package/dist/views/shared/context.svelte.d.ts +1 -1
- package/package.json +24 -9
- package/widget/widget.js +7983 -7688
- package/dist/assets/favicon.svg +0 -9
- package/dist/widget/CalendarWidget.svelte +0 -114
- package/dist/widget/CalendarWidget.svelte.d.ts +0 -37
- package/dist/widget/index.d.ts +0 -1
- package/dist/widget/index.js +0 -1
- package/dist/widget/widget.d.ts +0 -10
- package/dist/widget/widget.js +0 -96
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@nomideusz/svelte-calendar) [](https://github.com/nomideusz/svelte-calendar/blob/main/LICENSE)
|
|
4
4
|
|
|
5
|
-
A themeable **Svelte 5** calendar
|
|
5
|
+
A themeable **Svelte 5** calendar: day and week **Planner** time grids, a multi-week **Scroll** view, **Agenda** lists, a **Month** grid and touch-first **Mobile** layouts — with drag-and-drop, timezones, a headless API, an embeddable web component, and a smart auto-theme that adapts to any page.
|
|
6
6
|
|
|
7
7
|
**[Live demo → svelte-calendar.xyz](https://svelte-calendar.xyz/)**
|
|
8
8
|
|
|
@@ -12,7 +12,11 @@ A themeable **Svelte 5** calendar with **Day/Week Planner & Agenda** views, touc
|
|
|
12
12
|
pnpm add @nomideusz/svelte-calendar
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Requirements:
|
|
16
|
+
|
|
17
|
+
- **Svelte `^5.29.0`** (peer dependency — the package uses attachments, `{@attach}`).
|
|
18
|
+
- Runtime dependencies (installed for you): `date-fns`, `date-fns-tz`, `@chenglou/pretext`.
|
|
19
|
+
- Node ≥ 22 is only needed to develop the package itself, not to use it.
|
|
16
20
|
|
|
17
21
|
## Quick Start
|
|
18
22
|
|
|
@@ -29,67 +33,119 @@ pnpm add @nomideusz/svelte-calendar
|
|
|
29
33
|
<Calendar {adapter} />
|
|
30
34
|
```
|
|
31
35
|
|
|
32
|
-
That's it —
|
|
36
|
+
That's it — 8 built-in views, 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.
|
|
33
37
|
|
|
34
38
|
## Views
|
|
35
39
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
The default registry (`defaultViews`) holds eight views:
|
|
41
|
+
|
|
42
|
+
| View ID | Mode | What it is |
|
|
43
|
+
|---------|------|------------|
|
|
44
|
+
| `day-planner` | day | Vertical time grid, one column. **The default view** (first registered). |
|
|
45
|
+
| `week-planner` | week | The same time grid, one column per day (`days`, default 7). |
|
|
46
|
+
| `week-scroll` | week | Weeks stacked in one infinite vertical scroller (Hey-Calendar style): each chip sits at the height of its start time, month names run down the side. Drag to move, drop onto a day; it scrolls under a drag. |
|
|
47
|
+
| `day-agenda` | day | Live list for one day: now / up next / done. |
|
|
48
|
+
| `week-agenda` | week | List of the week's days (`columns` turns it into a timetable grid). |
|
|
49
|
+
| `day-mobile` | day | Touch-first time grid with swipe navigation. |
|
|
50
|
+
| `week-mobile` | week | Touch-first list of the week's days. |
|
|
51
|
+
| `month-grid` | month | Week-aligned month grid, chips per day with a "+N more" overflow. |
|
|
41
52
|
|
|
42
53
|
```svelte
|
|
43
|
-
<Calendar {adapter} view="week-planner" />
|
|
54
|
+
<Calendar {adapter} view="week-planner" />
|
|
44
55
|
<Calendar {adapter} view="week-scroll" />
|
|
45
|
-
<Calendar {adapter} view="day-planner" />
|
|
46
|
-
<Calendar {adapter} view="week-agenda" />
|
|
47
|
-
<Calendar {adapter} view="day-agenda" />
|
|
48
56
|
<Calendar {adapter} view="month-grid" />
|
|
49
57
|
```
|
|
50
58
|
|
|
51
|
-
Users
|
|
59
|
+
Users switch with the built-in pills: **Day / Week / Month** for the mode, plus view-type pills (**Planner / Scroll / Agenda**) when a mode has more than one view. Hide them when your app controls the view (`view` stays reactive after mount):
|
|
60
|
+
|
|
61
|
+
```svelte
|
|
62
|
+
<Calendar {adapter} view="day-planner" showModePills={false} />
|
|
63
|
+
```
|
|
52
64
|
|
|
53
65
|
Planner views are designed for direct manipulation:
|
|
54
66
|
|
|
55
67
|
- **Move** — drag an event to another day or time; a ghost previews the target before the move commits.
|
|
56
|
-
- **Resize** — drag an event's top/bottom
|
|
57
|
-
- **
|
|
68
|
+
- **Resize** — drag an event's top/bottom grip to change its duration; `minDuration`/`maxDuration` clamp the result.
|
|
69
|
+
- **Create** — press on empty canvas and sweep to draw a range, or click for a default-length slot. Both go through the same validation (blocked slots, disabled dates, `minDuration`/`maxDuration`) and then fire `oneventcreate`.
|
|
70
|
+
- **External drop** — an HTML5 drag from outside the calendar (a class chip, a template) dropped on the planner or `week-scroll` fires `onexternaldrop({ start, dataTransfer })`.
|
|
58
71
|
|
|
59
|
-
|
|
72
|
+
`week-scroll` supports move, click-to-create and external drops. In `week-agenda` with `columns` and an `oneventmove` handler, a card can be dragged to another day (it keeps its time of day); that move goes straight to `oneventmove` without writing the adapter.
|
|
60
73
|
|
|
61
|
-
|
|
74
|
+
The month grid shows up to three chips per day. Clicking a day or its "+N more" fires `ondayclick(date)`; without `ondayclick` it opens that date in a day view (the day view of the type you last used).
|
|
62
75
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
76
|
+
### Keyboard
|
|
77
|
+
|
|
78
|
+
When focus is inside the calendar:
|
|
79
|
+
|
|
80
|
+
- `t` — go to today.
|
|
81
|
+
- `←` / `→` — previous / next period (mirrored in RTL). Ignored while typing in a field or inside a pill group, where arrows move between pills.
|
|
82
|
+
- In `month-grid`: arrow keys move between days, `Enter` / `Space` opens the focused day (`ondayclick`).
|
|
66
83
|
|
|
67
84
|
### Custom Views
|
|
68
85
|
|
|
69
|
-
The `views` prop replaces the
|
|
86
|
+
The `views` prop replaces the registry. Spread `defaultViews` to add your own view and keep the built-ins:
|
|
70
87
|
|
|
71
88
|
```svelte
|
|
72
89
|
<script lang="ts">
|
|
73
|
-
import { Calendar, type CalendarView } from '@nomideusz/svelte-calendar';
|
|
90
|
+
import { Calendar, defaultViews, type CalendarView } from '@nomideusz/svelte-calendar';
|
|
74
91
|
import KanbanDay from './KanbanDay.svelte';
|
|
75
92
|
|
|
76
93
|
const views: CalendarView[] = [
|
|
77
|
-
|
|
78
|
-
{ id: '
|
|
94
|
+
...defaultViews,
|
|
95
|
+
{ id: 'day-kanban', label: 'Kanban', mode: 'day', component: KanbanDay, props: { columns: 3 } },
|
|
79
96
|
];
|
|
80
97
|
</script>
|
|
81
98
|
|
|
82
99
|
<Calendar {adapter} {views} view="day-kanban" />
|
|
83
100
|
```
|
|
84
101
|
|
|
85
|
-
|
|
102
|
+
A `CalendarView` is `{ id, label, mode: 'day' | 'week' | 'month', component, props? }`. `label` names the view-type pill; views sharing a label across modes are what the Day/Week/Month pills switch between.
|
|
103
|
+
|
|
104
|
+
The component receives `CalendarViewProps` (plus your `props`) — declare only the ones it uses:
|
|
105
|
+
|
|
106
|
+
| Prop | Type | Notes |
|
|
107
|
+
|------|------|-------|
|
|
108
|
+
| `events` | `TimelineEvent[]` | Everything loaded for the current range |
|
|
109
|
+
| `style` | `string` | The active `--dt-*` theme string |
|
|
110
|
+
| `height` | `number \| null` | Currently always `null` — size to the container |
|
|
111
|
+
| `mode` | `'day' \| 'week' \| 'month'` | |
|
|
112
|
+
| `mondayStart` | `boolean` | |
|
|
113
|
+
| `locale` | `string \| undefined` | |
|
|
114
|
+
| `focusDate` | `Date` | |
|
|
115
|
+
| `oneventclick` | `(event, anchor?: DOMRect) => void` | Selects the event and calls the host |
|
|
116
|
+
| `oneventcreate` | `((range) => void) \| undefined` | Already validated; `undefined` when `readOnly` or no host handler |
|
|
117
|
+
| `onexternaldrop` | `((info) => void) \| undefined` | |
|
|
118
|
+
| `readOnly` | `boolean` | |
|
|
119
|
+
| `visibleHours` | `[number, number] \| undefined` | |
|
|
120
|
+
| `selectedEventId` | `string \| null` | |
|
|
121
|
+
|
|
122
|
+
```svelte
|
|
123
|
+
<!-- KanbanDay.svelte -->
|
|
124
|
+
<script lang="ts">
|
|
125
|
+
import { useCalendarContext, type CalendarViewProps } from '@nomideusz/svelte-calendar';
|
|
126
|
+
|
|
127
|
+
let { events, oneventclick, columns = 3 }:
|
|
128
|
+
Pick<CalendarViewProps, 'events' | 'oneventclick'> & { columns?: number } = $props();
|
|
129
|
+
|
|
130
|
+
const ctx = useCalendarContext(); // viewState, drag, labels, config…
|
|
131
|
+
</script>
|
|
132
|
+
|
|
133
|
+
<h3>{ctx.labels.today}</h3>
|
|
134
|
+
{#each events as ev (ev.id)}
|
|
135
|
+
<button onclick={() => oneventclick(ev)}>{ev.title}</button>
|
|
136
|
+
{/each}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`useCalendarContext()` (type `CalendarContext`) gives a view the running Calendar's engines and config: `viewState`, `drag`, `commitDrag`, `labels`, `readOnly`, `snapInterval`, `minColumnWidth`, `blockedSlots`, `disabledDates` / `disabledSet`, `minDuration` / `maxDuration`, `hideDays`, `equalDays`, `showDates`, `compact`, `columns`, `isMobile`, `autoHeight`, `timezone`, `oneventmove`, `oneventhover`, `ondayclick`, `eventSnippet`, `emptySnippet`, `dayHeaderSnippet` and `loadRange` (a view can widen the range the store loads). Call it at component init, inside a `<Calendar>`.
|
|
140
|
+
|
|
141
|
+
The raw view components — `Planner`, `PlannerScroll`, `Agenda`, `Mobile`, `MonthGrid` — are exported so you can register them under your own ids. Building blocks for custom views are exported as primitives: `EventBlock` (an event card), `TimeGutter` (hour labels), `DayHeader`, `NowIndicator`, `EmptySlot` (click-to-create target) and `FloatingPanel` (see [Event details panel](#event-details-panel)).
|
|
86
142
|
|
|
87
143
|
## Mobile
|
|
88
144
|
|
|
89
|
-
|
|
145
|
+
When the calendar's **container** is narrower than 768px, it swaps to touch-first Mobile views with swipe navigation and a compact header:
|
|
90
146
|
|
|
91
147
|
```svelte
|
|
92
|
-
<!-- Auto-detect (default) — switches at 768px -->
|
|
148
|
+
<!-- Auto-detect (default) — switches at a 768px container width -->
|
|
93
149
|
<Calendar {adapter} />
|
|
94
150
|
|
|
95
151
|
<!-- Force mobile layout -->
|
|
@@ -99,40 +155,106 @@ On narrow screens (`< 768px`), the calendar automatically remaps Planner views t
|
|
|
99
155
|
<Calendar {adapter} mobile={false} />
|
|
100
156
|
```
|
|
101
157
|
|
|
102
|
-
|
|
103
|
-
- **
|
|
104
|
-
|
|
158
|
+
- **MobileDay** — vertical time grid with hour labels, swipe left/right to change days, all-day chips at the top, tap-to-create, stable columns for overlapping events.
|
|
159
|
+
- **MobileWeek** — vertical day list with relative labels (Today, Tomorrow, …) and accessible event buttons in each row.
|
|
160
|
+
|
|
161
|
+
In mobile mode every view except `*-agenda` and `*-mobile` is swapped for the `{mode}-mobile` entry of the same mode when one is registered — this includes custom views. Agenda views keep their list layout; `month-grid` has no mobile entry and stays. The mobile header shows only the Day/Week/Month pills.
|
|
105
162
|
|
|
106
|
-
|
|
163
|
+
With `'auto'`, the first render (and the server render) is the desktop layout; the container is measured on mount, so SSR hydrates cleanly.
|
|
107
164
|
|
|
108
165
|
## Callbacks
|
|
109
166
|
|
|
110
167
|
```svelte
|
|
111
168
|
<Calendar
|
|
112
169
|
{adapter}
|
|
113
|
-
oneventclick={(event) => console.log('Clicked', event.title)}
|
|
170
|
+
oneventclick={(event, anchor) => console.log('Clicked', event.title, anchor)}
|
|
114
171
|
oneventcreate={(range) => console.log('New slot', range.start, range.end)}
|
|
115
172
|
oneventmove={(event, start, end) => console.log('Moved', event.title, start, end)}
|
|
173
|
+
onexternaldrop={({ start, dataTransfer }) => console.log('Dropped', start, dataTransfer.getData('text/plain'))}
|
|
116
174
|
onviewchange={(viewId) => console.log('View', viewId)}
|
|
175
|
+
ondatechange={(date) => console.log('Date', date)}
|
|
117
176
|
ondayclick={(date) => console.log('Day', date)}
|
|
118
177
|
onerror={(error) => console.error('Calendar', error)}
|
|
119
178
|
/>
|
|
120
179
|
```
|
|
121
180
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
181
|
+
| Callback | When it fires |
|
|
182
|
+
|----------|---------------|
|
|
183
|
+
| `oneventclick(event, anchor?)` | An event is clicked. The event is also selected (highlighted across views). `anchor` is the clicked block's viewport rect where the view has one (planner, `week-scroll`, `month-grid`) — for positioning a `FloatingPanel`. |
|
|
184
|
+
| `oneventcreate({ start, end })` | A drag-create or click-to-create passed validation. The calendar does **not** store anything: add the event to your data and hand the Calendar the updated adapter. |
|
|
185
|
+
| `oneventmove(event, start, end)` | A drag or resize committed — **after** the adapter stored it, or when the adapter refused with a read-only error (the host owns persistence then; the block stays where it was dropped). Other adapter failures revert the block and go to `onerror`. (The `week-agenda` columns day-drag is the exception: it only calls `oneventmove`.) |
|
|
186
|
+
| `onexternaldrop({ start, dataTransfer })` | An outside HTML5 drag was dropped on the grid; `start` is the pointer's time, snapped. |
|
|
187
|
+
| `onviewchange(viewId)` | Once on mount, then on every view change. |
|
|
188
|
+
| `ondatechange(date)` | Once on mount, then whenever the focused date changes (navigation, scrolling, a tapped day). |
|
|
189
|
+
| `ondayclick(date)` | A month-grid or agenda day is clicked (an instant, like `ondatechange`). Default: open it in a day view. |
|
|
190
|
+
| `oneventhover(event)` | The pointer enters an event (tooltips, previews). |
|
|
191
|
+
| `onerror(error)` | Adapter load failures and failed drag commits that would otherwise only reach the console. |
|
|
192
|
+
|
|
193
|
+
`oneventcreate`, `oneventmove` and `onexternaldrop` receive real instants, also with [`timezone`](#timezone-support). None of the write callbacks fire when `readOnly` is set.
|
|
125
194
|
|
|
126
|
-
|
|
195
|
+
To show a newly created or moved event, update your data and give the Calendar a new adapter — the store reloads on a new adapter identity without blanking the grid:
|
|
196
|
+
|
|
197
|
+
```svelte
|
|
198
|
+
<script lang="ts">
|
|
199
|
+
import { Calendar, createMemoryAdapter, type TimelineEvent } from '@nomideusz/svelte-calendar';
|
|
200
|
+
|
|
201
|
+
let events = $state<TimelineEvent[]>([]);
|
|
202
|
+
const adapter = $derived(createMemoryAdapter(events));
|
|
203
|
+
</script>
|
|
204
|
+
|
|
205
|
+
<Calendar
|
|
206
|
+
{adapter}
|
|
207
|
+
oneventcreate={({ start, end }) => (events = [...events, { id: crypto.randomUUID(), title: 'New', start, end }])}
|
|
208
|
+
/>
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Read-only
|
|
212
|
+
|
|
213
|
+
Set `readOnly` to disable drag, resize, click-to-create and external drops:
|
|
127
214
|
|
|
128
215
|
```svelte
|
|
129
216
|
<Calendar {adapter} readOnly />
|
|
130
217
|
```
|
|
131
218
|
|
|
219
|
+
To lock single events, set `data.readOnly: true` on them: views refuse to move or resize those events, which can still be clicked. The recurring adapter uses this for its projected occurrences.
|
|
220
|
+
|
|
221
|
+
### Event details panel
|
|
222
|
+
|
|
223
|
+
`FloatingPanel` is a non-modal panel that opens beside the clicked block's rect (to its right, or left near the viewport edge), can be dragged by its header, and becomes a bottom sheet below a 640px viewport. Escape, its close button or a click outside closes it; the outside click is spent on closing, so it doesn't also open the event under it.
|
|
224
|
+
|
|
225
|
+
```svelte
|
|
226
|
+
<script lang="ts">
|
|
227
|
+
import { Calendar, FloatingPanel, type TimelineEvent } from '@nomideusz/svelte-calendar';
|
|
228
|
+
|
|
229
|
+
let open = $state<{ ev: TimelineEvent; anchor?: DOMRect } | null>(null);
|
|
230
|
+
</script>
|
|
231
|
+
|
|
232
|
+
<Calendar {adapter} oneventclick={(ev, anchor) => (open = { ev, anchor })} />
|
|
233
|
+
|
|
234
|
+
{#if open}
|
|
235
|
+
<FloatingPanel title={open.ev.title} anchor={open.anchor} onclose={() => (open = null)}>
|
|
236
|
+
<p>{open.ev.location}</p>
|
|
237
|
+
</FloatingPanel>
|
|
238
|
+
{/if}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
| Prop | Type | Default | Description |
|
|
242
|
+
|------|------|---------|-------------|
|
|
243
|
+
| `title` | `string` | `''` | Header text and dialog label |
|
|
244
|
+
| `anchor` | `FloatingPanelAnchor` | `{ x: 24, y: 24 }` | `{ x, y, width?, height? }` — a `DOMRect` works as is |
|
|
245
|
+
| `width` | `number` | `440` | Panel width in px |
|
|
246
|
+
| `theme` | `string` | `''` | A `--dt-*` theme string, as the Calendar takes |
|
|
247
|
+
| `closeLabel` | `string` | `'Close'` | Close button label |
|
|
248
|
+
| `closeOnOutside` | `boolean` | `true` | Close on a pointerdown outside |
|
|
249
|
+
| `onclose` | `() => void` | — | Called on Escape, the close button, or an outside click |
|
|
250
|
+
| `children` | `Snippet` | *required* | Panel body |
|
|
251
|
+
|
|
252
|
+
### More options
|
|
253
|
+
|
|
132
254
|
Hide nav controls (prev/next/today) and treat all days equally (no past-day dimming):
|
|
133
255
|
|
|
134
256
|
```svelte
|
|
135
|
-
<!--
|
|
257
|
+
<!-- Fixed weekly schedule, no browsing, all days equal -->
|
|
136
258
|
<Calendar
|
|
137
259
|
{adapter}
|
|
138
260
|
view="week-agenda"
|
|
@@ -146,7 +268,6 @@ Hide nav controls (prev/next/today) and treat all days equally (no past-day dimm
|
|
|
146
268
|
Hide weekends for a workweek view:
|
|
147
269
|
|
|
148
270
|
```svelte
|
|
149
|
-
<!-- Office planner: Mon–Fri only -->
|
|
150
271
|
<Calendar {adapter} view="week-planner" hideDays={[6, 7]} />
|
|
151
272
|
```
|
|
152
273
|
|
|
@@ -164,7 +285,7 @@ Control which date the calendar shows from your app:
|
|
|
164
285
|
/>
|
|
165
286
|
```
|
|
166
287
|
|
|
167
|
-
Show a rolling 3-day view:
|
|
288
|
+
Show a rolling 3-day view (week views other than 7 days start at the focused date):
|
|
168
289
|
|
|
169
290
|
```svelte
|
|
170
291
|
<Calendar {adapter} view="week-planner" days={3} />
|
|
@@ -177,8 +298,8 @@ Block lunch hours and enforce 30–120 min events:
|
|
|
177
298
|
import type { BlockedSlot } from '@nomideusz/svelte-calendar';
|
|
178
299
|
|
|
179
300
|
const blocked: BlockedSlot[] = [
|
|
180
|
-
{ start: 12, end: 13, label: 'Lunch' },
|
|
181
|
-
{ day: 6, start: 0, end: 24, label: 'Saturday' }, // block all Saturday
|
|
301
|
+
{ start: 12, end: 13, label: 'Lunch' }, // every day 12–1 PM
|
|
302
|
+
{ day: 6, start: 0, end: 24, label: 'Saturday' }, // block all Saturday (ISO weekday)
|
|
182
303
|
];
|
|
183
304
|
</script>
|
|
184
305
|
|
|
@@ -194,7 +315,7 @@ Disable specific dates:
|
|
|
194
315
|
/>
|
|
195
316
|
```
|
|
196
317
|
|
|
197
|
-
Disabled dates
|
|
318
|
+
Disabled dates refuse creating or moving events into those days. Existing events stay visible and clickable — useful for holidays or fully booked days.
|
|
198
319
|
|
|
199
320
|
Custom day headers and hover previews:
|
|
200
321
|
|
|
@@ -218,11 +339,12 @@ Replace the built-in navigation or the entire header chrome with your own contro
|
|
|
218
339
|
</Calendar>
|
|
219
340
|
|
|
220
341
|
<Calendar {adapter}>
|
|
221
|
-
{#snippet header({ dateLabel, mode, switchMode, prev, next, goToday })}
|
|
342
|
+
{#snippet header({ dateLabel, mode, modes, switchMode, prev, next, goToday })}
|
|
222
343
|
<nav class="my-toolbar">
|
|
223
344
|
<h2>{dateLabel}</h2>
|
|
224
|
-
|
|
225
|
-
|
|
345
|
+
{#each modes as m}
|
|
346
|
+
<button class:active={mode === m} onclick={() => switchMode(m)}>{m}</button>
|
|
347
|
+
{/each}
|
|
226
348
|
<button onclick={prev}>←</button>
|
|
227
349
|
<button onclick={goToday}>Today</button>
|
|
228
350
|
<button onclick={next}>→</button>
|
|
@@ -231,7 +353,7 @@ Replace the built-in navigation or the entire header chrome with your own contro
|
|
|
231
353
|
</Calendar>
|
|
232
354
|
```
|
|
233
355
|
|
|
234
|
-
Let agenda content determine height instead of forcing a fixed box — useful
|
|
356
|
+
Let agenda content determine height instead of forcing a fixed box — useful inside a scrolling page:
|
|
235
357
|
|
|
236
358
|
```svelte
|
|
237
359
|
<Calendar {adapter} view="week-agenda" height="auto" compact />
|
|
@@ -243,8 +365,8 @@ Three built-in presets:
|
|
|
243
365
|
|
|
244
366
|
| Preset | Description |
|
|
245
367
|
|--------|-------------|
|
|
246
|
-
| `auto` | **Default.** Probes the host page at mount — background, accent color, fonts, light/dark mode — and
|
|
247
|
-
| `neutral` | Explicit light theme. White bg, blue accent
|
|
368
|
+
| `auto` | **Default.** Probes the host page at mount — background, accent color, fonts, light/dark mode — and writes matching `--dt-*` tokens. Re-probes when the host theme changes. |
|
|
369
|
+
| `neutral` | Explicit light theme. White bg, blue accent. Use when embedding standalone. |
|
|
248
370
|
| `midnight` | Explicit dark theme. Charcoal bg, red accent. |
|
|
249
371
|
|
|
250
372
|
```svelte
|
|
@@ -257,21 +379,23 @@ Three built-in presets:
|
|
|
257
379
|
<Calendar {adapter} theme={midnight} /> <!-- explicit dark mode -->
|
|
258
380
|
```
|
|
259
381
|
|
|
382
|
+
`presets` maps the names to the strings (`presets.neutral`); the type `PresetName` is `'auto' | 'neutral' | 'midnight'`.
|
|
383
|
+
|
|
260
384
|
### Smart Auto Theme
|
|
261
385
|
|
|
262
|
-
The default `auto` preset probes the
|
|
386
|
+
The default `auto` preset probes the page around the calendar and generates a theme that blends in. It detects:
|
|
263
387
|
|
|
264
|
-
- **Background color** — from CSS variables, inline styles, or computed styles
|
|
388
|
+
- **Background color** — from common CSS variables on `:root` (`--bg`, `--background`, `--color-bg`, `--bs-body-bg`, …), inline styles, or computed styles
|
|
265
389
|
- **Light/dark mode** — from background luminance
|
|
266
|
-
- **Accent/brand color** — from CSS variables (`--accent`, `--primary`, `--bs-primary`,
|
|
390
|
+
- **Accent/brand color** — from common CSS variables (`--accent`, `--primary`, `--brand`, `--bs-primary`, …), link colors, or button colors
|
|
267
391
|
- **Text color** — validated for contrast against the background
|
|
268
|
-
- **Fonts** —
|
|
392
|
+
- **Fonts** — the host's computed font stack; the mono stack from `--font-mono` and similar variables
|
|
269
393
|
|
|
270
|
-
It
|
|
394
|
+
It watches for changes (a dark-mode toggle on `<html>`/`<body>`, the system color scheme) and updates automatically.
|
|
271
395
|
|
|
272
396
|
The probing engine is exported for standalone use: `probeHostTheme(element, options?)` returns a `--dt-*` CSS string for the page around `element`; `observeHostTheme(element, callback, options?)` re-probes on theme changes and returns a stop function.
|
|
273
397
|
|
|
274
|
-
Fine-tune auto-detection with the `autoTheme` prop:
|
|
398
|
+
Fine-tune auto-detection with the `autoTheme` prop (`AutoThemeOptions`):
|
|
275
399
|
|
|
276
400
|
```svelte
|
|
277
401
|
<!-- Force dark mode even if the page background is light -->
|
|
@@ -283,73 +407,63 @@ Fine-tune auto-detection with the `autoTheme` prop:
|
|
|
283
407
|
<!-- Override the font stack -->
|
|
284
408
|
<Calendar {adapter} autoTheme={{ font: '"Poppins", sans-serif' }} />
|
|
285
409
|
|
|
286
|
-
<!--
|
|
287
|
-
<Calendar {adapter} autoTheme={{ mode: 'dark', accent: '#10b981' }} />
|
|
288
|
-
|
|
289
|
-
<!-- Disable auto-probing entirely (passive CSS inheritance only) -->
|
|
410
|
+
<!-- Disable probing entirely: inherit --dt-* from ancestors -->
|
|
290
411
|
<Calendar {adapter} autoTheme={false} />
|
|
291
412
|
```
|
|
292
413
|
|
|
293
|
-
###
|
|
414
|
+
### Your own tokens
|
|
294
415
|
|
|
295
|
-
|
|
416
|
+
The calendar writes its theme inline on its own root element, so it **wins over** `--dt-*` values set on an ancestor or `:root`. With the default `auto` theme it writes a full probed set. To use your own values:
|
|
296
417
|
|
|
297
|
-
|
|
298
|
-
<!-- Wrap in a div with your overrides -->
|
|
299
|
-
<div style="--dt-accent: #e11d48; --dt-bg: #1a1a2e; --dt-text: rgba(255,255,255,0.87);">
|
|
300
|
-
<Calendar {adapter} />
|
|
301
|
-
</div>
|
|
302
|
-
```
|
|
418
|
+
- **Change a few tokens** — append them to a preset (later declarations win):
|
|
303
419
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
```css
|
|
307
|
-
:root {
|
|
308
|
-
--dt-bg: #fafafa;
|
|
309
|
-
--dt-accent: #2563eb;
|
|
310
|
-
--dt-text: rgba(0, 0, 0, 0.87);
|
|
311
|
-
--dt-border: rgba(0, 0, 0, 0.08);
|
|
312
|
-
}
|
|
313
|
-
```
|
|
420
|
+
```ts
|
|
421
|
+
import { neutral } from '@nomideusz/svelte-calendar';
|
|
314
422
|
|
|
315
|
-
|
|
423
|
+
const custom = `${neutral}; --dt-accent: #e11d48;`;
|
|
424
|
+
```
|
|
316
425
|
|
|
317
|
-
|
|
426
|
+
```svelte
|
|
427
|
+
<Calendar {adapter} theme={custom} />
|
|
428
|
+
```
|
|
318
429
|
|
|
319
|
-
|
|
430
|
+
- **Write your own theme string** — any `--dt-*` declarations; tokens you leave out are inherited from ancestors or fall back to neutral defaults.
|
|
431
|
+
- **Inherit from the page** — pass `autoTheme={false}` (keeping the default `auto` theme). The calendar then writes no tokens and reads whatever `--dt-*` its ancestors define:
|
|
320
432
|
|
|
321
|
-
```
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
```
|
|
433
|
+
```svelte
|
|
434
|
+
<div style="--dt-accent: #e11d48; --dt-bg: #1a1a2e; --dt-text: rgba(255,255,255,0.87);">
|
|
435
|
+
<Calendar {adapter} autoTheme={false} />
|
|
436
|
+
</div>
|
|
437
|
+
```
|
|
327
438
|
|
|
328
|
-
|
|
329
|
-
<Calendar {adapter} theme={custom} />
|
|
330
|
-
```
|
|
439
|
+
Component fallbacks use system fonts and a neutral blue accent, so the package stays clean in apps that set no tokens.
|
|
331
440
|
|
|
332
441
|
<details>
|
|
333
442
|
<summary>All design tokens</summary>
|
|
334
443
|
|
|
335
444
|
| Token | Purpose |
|
|
336
445
|
|-------|---------|
|
|
337
|
-
| `--dt-
|
|
338
|
-
| `--dt-
|
|
339
|
-
| `--dt-surface` | Elevated surface (alternating rows, headers) |
|
|
446
|
+
| `--dt-bg` | Calendar background |
|
|
447
|
+
| `--dt-surface` | Elevated surface (headers, alternating rows, chips) |
|
|
340
448
|
| `--dt-border` | Default border |
|
|
341
|
-
| `--dt-border-day` | Day-column dividers |
|
|
449
|
+
| `--dt-border-day` | Day-column / day-cell dividers |
|
|
342
450
|
| `--dt-text` | Primary text |
|
|
343
451
|
| `--dt-text-2` | Secondary text |
|
|
344
452
|
| `--dt-text-3` | Tertiary text |
|
|
345
453
|
| `--dt-accent` | Accent color |
|
|
346
|
-
| `--dt-accent-dim` | Accent at ~12% opacity |
|
|
454
|
+
| `--dt-accent-dim` | Accent at ~12% opacity (selection, highlights) |
|
|
455
|
+
| `--dt-accent-fg` | Text on the planner's today-number badge (default `#fff`; not set by the presets) |
|
|
456
|
+
| `--dt-btn-text` | Text on accent-filled buttons and badges |
|
|
347
457
|
| `--dt-glow` | Accent glow / focus ring |
|
|
348
458
|
| `--dt-today-bg` | Today column highlight |
|
|
349
|
-
| `--dt-
|
|
459
|
+
| `--dt-weekend-bg` | Weekend cell tint |
|
|
460
|
+
| `--dt-hover` | Hover background |
|
|
350
461
|
| `--dt-scrollbar` | Scrollbar thumb |
|
|
351
462
|
| `--dt-success` | Completed indicator |
|
|
352
|
-
| `--dt-
|
|
463
|
+
| `--dt-sans` / `--dt-mono` | Font stacks |
|
|
464
|
+
| `--dt-radius` | `FloatingPanel` corner radius (the calendar's own radius is the `borderRadius` prop) |
|
|
465
|
+
| `--dt-shadow` | `FloatingPanel` shadow |
|
|
466
|
+
| `--dt-stage-bg` | The page background around the calendar. Set by the presets and the auto theme for your page to use; the components don't read it. |
|
|
353
467
|
|
|
354
468
|
</details>
|
|
355
469
|
|
|
@@ -367,23 +481,23 @@ const custom = `${neutral}; --dt-accent: #e11d48;`;
|
|
|
367
481
|
| `subtitle` | `string?` | Secondary text below the title |
|
|
368
482
|
| `tags` | `string[]?` | Small accent-colored pills |
|
|
369
483
|
| `allDay` | `boolean?` | Render as an all-day event |
|
|
370
|
-
| `location` | `string?` | Room, venue, or address
|
|
484
|
+
| `location` | `string?` | Room, venue, or address |
|
|
371
485
|
| `status` | `EventStatus?` | `'confirmed'` (default), `'cancelled'`, `'tentative'`, `'full'`, `'limited'` |
|
|
372
486
|
| `externalId` | `string?` | ID from an upstream system (booking platform, CRM, LMS) |
|
|
373
|
-
| `resourceId` | `string?` | Resource this event belongs to (room, instructor, court)
|
|
374
|
-
| `data` | `Record
|
|
487
|
+
| `resourceId` | `string?` | Resource this event belongs to (room, instructor, court) |
|
|
488
|
+
| `data` | `Record<string, unknown>?` | Arbitrary payload for your app. `data.readOnly: true` locks the event against drags. |
|
|
375
489
|
|
|
376
|
-
Cancelled events render with a strikethrough
|
|
490
|
+
Cancelled events render with a strikethrough but stay visible, so the slot isn't mistaken for free time.
|
|
377
491
|
|
|
378
492
|
### Auto-Coloring
|
|
379
493
|
|
|
380
|
-
Omit `color` and
|
|
494
|
+
Omit `color` and the built-in adapters assign a vivid palette color, grouped by `category` (or `title`):
|
|
381
495
|
|
|
382
496
|
```ts
|
|
383
497
|
const events = [
|
|
384
|
-
{ id: '1', title: 'Yoga',
|
|
385
|
-
{ id: '2', title: 'Pilates', category: 'wellness', start
|
|
386
|
-
{ id: '3', title: 'Standup', start
|
|
498
|
+
{ id: '1', title: 'Yoga', category: 'wellness', start, end },
|
|
499
|
+
{ id: '2', title: 'Pilates', category: 'wellness', start, end }, // same color
|
|
500
|
+
{ id: '3', title: 'Standup', start, end }, // different color
|
|
387
501
|
];
|
|
388
502
|
```
|
|
389
503
|
|
|
@@ -392,29 +506,26 @@ Generate a theme-harmonious palette from any accent color:
|
|
|
392
506
|
```ts
|
|
393
507
|
import { createMemoryAdapter, generatePalette } from '@nomideusz/svelte-calendar';
|
|
394
508
|
|
|
395
|
-
|
|
396
|
-
const palette = generatePalette('#e11d48');
|
|
509
|
+
const palette = generatePalette('#e11d48'); // (accent?, count = 15)
|
|
397
510
|
const adapter = createMemoryAdapter(events, { palette });
|
|
398
511
|
```
|
|
399
512
|
|
|
400
|
-
Related exports: `VIVID_PALETTE` (the default palette) and `extractAccent(themeString)` (
|
|
513
|
+
Related exports: `VIVID_PALETTE` (the default palette) and `extractAccent(themeString)` (the accent color of a `--dt-*` theme string).
|
|
401
514
|
|
|
402
515
|
### Multi-day & All-day
|
|
403
516
|
|
|
404
|
-
Events spanning multiple days or flagged `allDay: true` render in a
|
|
517
|
+
Events spanning multiple days or flagged `allDay: true` render in a strip above timed events:
|
|
405
518
|
|
|
406
519
|
```ts
|
|
407
520
|
const events = [
|
|
408
521
|
{ id: '1', title: 'Conference', start: new Date('2026-03-15'), end: new Date('2026-03-18'), allDay: true },
|
|
409
|
-
{ id: '2', title: 'Sprint',
|
|
522
|
+
{ id: '2', title: 'Sprint', start: new Date('2026-03-15T00:00'), end: new Date('2026-03-17T00:00') },
|
|
410
523
|
];
|
|
411
524
|
```
|
|
412
525
|
|
|
413
526
|
### Custom Event Rendering
|
|
414
527
|
|
|
415
|
-
|
|
416
|
-
interactive shell — click, keyboard, drag, selection — stays intact). The
|
|
417
|
-
`empty` snippet replaces the day-agenda empty state:
|
|
528
|
+
The `event` snippet replaces the event *content* in every view (the interactive shell — click, keyboard, drag, selection — stays intact). The `empty` snippet replaces the empty state of the day agenda, the day on mobile, and an empty planner week:
|
|
418
529
|
|
|
419
530
|
```svelte
|
|
420
531
|
<Calendar {adapter}>
|
|
@@ -424,6 +535,9 @@ interactive shell — click, keyboard, drag, selection — stays intact). The
|
|
|
424
535
|
{#if ev.subtitle}<small>{ev.subtitle}</small>{/if}
|
|
425
536
|
</div>
|
|
426
537
|
{/snippet}
|
|
538
|
+
{#snippet empty()}
|
|
539
|
+
<p>Nothing booked — drag on the grid to add a class.</p>
|
|
540
|
+
{/snippet}
|
|
427
541
|
</Calendar>
|
|
428
542
|
```
|
|
429
543
|
|
|
@@ -441,10 +555,11 @@ For fixed repeating events (class timetables, office hours):
|
|
|
441
555
|
startDate: '2026-03-01', until: '2026-03-31' },
|
|
442
556
|
{ id: '3', title: 'Review', frequency: 'monthly', dayOfMonth: 15,
|
|
443
557
|
startTime: '10:00', endTime: '11:00' },
|
|
558
|
+
{ id: '4', title: 'Night shift', dayOfWeek: [5, 6], startTime: '22:00', endTime: '02:00' },
|
|
444
559
|
]);
|
|
445
560
|
</script>
|
|
446
561
|
|
|
447
|
-
<Calendar {adapter}
|
|
562
|
+
<Calendar {adapter} />
|
|
448
563
|
```
|
|
449
564
|
|
|
450
565
|
<details>
|
|
@@ -452,42 +567,52 @@ For fixed repeating events (class timetables, office hours):
|
|
|
452
567
|
|
|
453
568
|
| Field | Type | Default | Description |
|
|
454
569
|
|-------|------|---------|-------------|
|
|
455
|
-
| `id` | `string` | *required* |
|
|
570
|
+
| `id` | `string` | *required* | Rule identifier |
|
|
456
571
|
| `title` | `string` | *required* | Event title |
|
|
457
|
-
| `startTime` | `string` | *required* | Start time `"HH:MM"
|
|
458
|
-
| `endTime` | `string` | *required* | End time `"HH:MM"` |
|
|
572
|
+
| `startTime` | `string` | *required* | Start time `"HH:MM"`, 24-hour |
|
|
573
|
+
| `endTime` | `string` | *required* | End time `"HH:MM"`. Earlier than `startTime` → ends the next day (overnight). |
|
|
459
574
|
| `frequency` | `'daily' \| 'weekly' \| 'monthly'` | `'weekly'` | Recurrence frequency |
|
|
460
|
-
| `interval` | `number` | `1` |
|
|
575
|
+
| `interval` | `number` | `1` | Every N periods (`2` + weekly = biweekly). Needs `startDate` when > 1. |
|
|
461
576
|
| `dayOfWeek` | `number \| number[]` | — | ISO weekday 1=Mon…7=Sun. Required for weekly. |
|
|
462
|
-
| `dayOfMonth` | `number` | `1` | Day of month (1–31)
|
|
463
|
-
| `startDate` | `string` | — | First occurrence `"YYYY-MM-DD"` |
|
|
464
|
-
| `until` | `string` | — | Last occurrence `"YYYY-MM-DD"` |
|
|
465
|
-
| `count` | `number` | — | Max occurrences from `startDate` |
|
|
466
|
-
| `
|
|
577
|
+
| `dayOfMonth` | `number` | `1` | Day of month (1–31) for monthly; clamped to shorter months. |
|
|
578
|
+
| `startDate` | `string` | — | First possible occurrence `"YYYY-MM-DD"` |
|
|
579
|
+
| `until` | `string` | — | Last possible occurrence `"YYYY-MM-DD"` |
|
|
580
|
+
| `count` | `number` | — | Max occurrences from `startDate` (requires it). With `until`, the stricter wins. |
|
|
581
|
+
| `excludeDates` | `string[]` | — | `"YYYY-MM-DD"` dates this rule skips |
|
|
582
|
+
| `color` | `string` | auto | Accent color |
|
|
583
|
+
| `subtitle` / `tags` / `category` / `location` / `resourceId` | | — | Copied to every occurrence |
|
|
584
|
+
| `data` | `Record<string, unknown>` | — | Copied to every occurrence, plus `recurringId` |
|
|
467
585
|
|
|
468
586
|
</details>
|
|
469
587
|
|
|
588
|
+
Options (`RecurringAdapterOptions`): `mondayStart` (default `true`), `palette`, `movable` (default `false`).
|
|
589
|
+
|
|
590
|
+
Occurrences get the id `` `${rule.id}--YYYYMMDD` `` and `data.recurringId`. The adapter projects and stores nothing, so by default every occurrence carries `data.readOnly` and can't be dragged (it can still be clicked). With `movable: true` the views allow the drag and the adapter refuses the write with a read-only error, so `oneventmove` fires and the host stores the move: add the date to the rule's `excludeDates` and keep the moved occurrence with your one-off events (for example the memory side of a [composite adapter](#composite-adapter)).
|
|
591
|
+
|
|
470
592
|
## REST Adapter
|
|
471
593
|
|
|
472
594
|
Connect to any REST API:
|
|
473
595
|
|
|
474
596
|
```ts
|
|
475
|
-
import {
|
|
597
|
+
import { createRestAdapter } from '@nomideusz/svelte-calendar';
|
|
598
|
+
|
|
599
|
+
type ApiItem = { id: string; name: string; startAt: string; endAt: string };
|
|
476
600
|
|
|
477
601
|
const adapter = createRestAdapter({
|
|
478
602
|
baseUrl: 'https://api.example.com/v1',
|
|
479
603
|
headers: { Authorization: 'Bearer TOKEN' },
|
|
480
|
-
// Optional: map your API shape to TimelineEvent[]
|
|
481
|
-
mapEvents: (data) =>
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
604
|
+
// Optional: map your API's list shape to TimelineEvent[]
|
|
605
|
+
mapEvents: (data) =>
|
|
606
|
+
(data as { items: ApiItem[] }).items.map((item) => ({
|
|
607
|
+
id: item.id,
|
|
608
|
+
title: item.name,
|
|
609
|
+
start: new Date(item.startAt),
|
|
610
|
+
end: new Date(item.endAt),
|
|
611
|
+
})),
|
|
487
612
|
});
|
|
488
613
|
```
|
|
489
614
|
|
|
490
|
-
The adapter calls `GET /events?start
|
|
615
|
+
The adapter calls `GET {baseUrl}/events?start=…&end=…` (ISO strings), `POST /events`, `PATCH /events/:id` and `DELETE /events/:id`, sending JSON with your `headers`. By default a list response must be an array of events and a single response one event; `start`/`end` are parsed into `Date`s. Override with `mapEvents(data)` for lists and `mapEvent(data)` for single events. A non-2xx response or invalid JSON throws.
|
|
491
616
|
|
|
492
617
|
## JMAP Adapter
|
|
493
618
|
|
|
@@ -512,11 +637,11 @@ const adapter = createJmapAdapter(client, {
|
|
|
512
637
|
});
|
|
513
638
|
```
|
|
514
639
|
|
|
515
|
-
|
|
640
|
+
Read-only. Expands recurrences server-side and handles all-day events, ISO-duration ends and per-calendar colors (pass `calendars: [{ id, name, color }]`, or a function returning them).
|
|
516
641
|
|
|
517
642
|
## Mapped Adapter
|
|
518
643
|
|
|
519
|
-
Wrap any static array of external records (
|
|
644
|
+
Wrap any static array of external records (class 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, colors, tags and status:
|
|
520
645
|
|
|
521
646
|
```ts
|
|
522
647
|
import { createMappedAdapter } from '@nomideusz/svelte-calendar';
|
|
@@ -528,7 +653,6 @@ const adapter = createMappedAdapter(rawClasses, {
|
|
|
528
653
|
end: 'ends_at_iso',
|
|
529
654
|
subtitle: 'teacher',
|
|
530
655
|
location: 'room',
|
|
531
|
-
color: 'color',
|
|
532
656
|
externalId: 'reference_id',
|
|
533
657
|
status: 'is_cancelled', // boolean → 'cancelled' / 'confirmed'
|
|
534
658
|
tags: ['is_free', 'is_bookable_online'],
|
|
@@ -536,9 +660,12 @@ const adapter = createMappedAdapter(rawClasses, {
|
|
|
536
660
|
});
|
|
537
661
|
```
|
|
538
662
|
|
|
539
|
-
|
|
663
|
+
- Works without options too: it looks for common keys (`id`, `title`/`name`, `start`/`end`, `starts_at_iso`/`ends_at_iso`, `date` + `start_time`/`end_time`, `room`/`location`, `is_cancelled`). A missing end defaults to one hour after the start.
|
|
664
|
+
- `start`/`end` accept ISO strings, `Date`s or millisecond timestamps. When records split date and time (`date: "2026-03-03"`, `startTime: "07:00"`), map `date` + `startTime` / `endTime`; times may be `7:00`, `07:00` or `07:00:00`.
|
|
665
|
+
- `autoColor` defaults to **`true`**: source colors are ignored and events are colored from `palette` by category/title. Pass `autoColor: false` to keep the colors from your data.
|
|
666
|
+
- `includeData` (default `'*'`) copies unmapped source fields into `event.data`; pass a list of keys to limit it.
|
|
540
667
|
|
|
541
|
-
For full control, pass
|
|
668
|
+
For full control, pass `mapEvent(raw, index)` and skip `fields`:
|
|
542
669
|
|
|
543
670
|
```ts
|
|
544
671
|
const adapter = createMappedAdapter(rawData, {
|
|
@@ -553,53 +680,80 @@ const adapter = createMappedAdapter(rawData, {
|
|
|
553
680
|
});
|
|
554
681
|
```
|
|
555
682
|
|
|
556
|
-
Mapped adapters are read-only by default. Set `readOnly: false`
|
|
683
|
+
Mapped adapters are read-only by default (writes throw a read-only error, so drags reach `oneventmove`). Set `readOnly: false` to allow writes; supply `onMutate: { onCreate, onUpdate, onDelete }` to persist them (each returns the stored source record, which is mapped again), or omit it to keep changes in memory.
|
|
557
684
|
|
|
558
685
|
## Composite Adapter
|
|
559
686
|
|
|
560
|
-
Merge
|
|
687
|
+
Merge several adapters into one — e.g. a recurring weekly schedule plus one-off events:
|
|
561
688
|
|
|
562
689
|
```ts
|
|
563
|
-
import {
|
|
564
|
-
Calendar,
|
|
565
|
-
createMemoryAdapter,
|
|
566
|
-
createRecurringAdapter,
|
|
567
|
-
createCompositeAdapter,
|
|
568
|
-
} from '@nomideusz/svelte-calendar';
|
|
690
|
+
import { createMemoryAdapter, createRecurringAdapter, createCompositeAdapter } from '@nomideusz/svelte-calendar';
|
|
569
691
|
|
|
570
692
|
const memory = createMemoryAdapter(oneOffEvents);
|
|
571
693
|
const recurring = createRecurringAdapter(weeklySchedule);
|
|
572
694
|
|
|
573
695
|
const adapter = createCompositeAdapter([memory, recurring]);
|
|
574
|
-
// Reads from both; writes go to memory (the primary — first adapter by default)
|
|
575
696
|
```
|
|
576
697
|
|
|
577
|
-
|
|
698
|
+
- **Reads** query every child in parallel and merge the results (the first event with a given id wins).
|
|
699
|
+
- **Creates** go to the primary child — the first by default, or `primaryIndex`.
|
|
700
|
+
- **Updates and deletes** go to the child that returned the event. If that child can't write, the composite refuses with a read-only error (so `oneventmove` reaches the host). An id no fetch returned is offered to each writable child in turn.
|
|
701
|
+
- **`onFetchError(error, adapterIndex)`** — with it, a failing child is reported to you and the others still load; without it, one failing child fails the whole load (surfacing through `onerror`).
|
|
578
702
|
|
|
579
703
|
```ts
|
|
580
|
-
const adapter = createCompositeAdapter([recurring, memory], {
|
|
704
|
+
const adapter = createCompositeAdapter([recurring, memory], {
|
|
705
|
+
primaryIndex: 1,
|
|
706
|
+
onFetchError: (error, i) => console.warn(`source ${i} failed`, error),
|
|
707
|
+
});
|
|
581
708
|
```
|
|
582
709
|
|
|
583
|
-
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.
|
|
584
|
-
|
|
585
710
|
## Custom Adapter
|
|
586
711
|
|
|
587
|
-
Implement the `CalendarAdapter` interface to connect any data source:
|
|
712
|
+
Implement the `CalendarAdapter` interface to connect any data source. Only `fetchEvents` is required:
|
|
588
713
|
|
|
589
714
|
```ts
|
|
590
715
|
import type { CalendarAdapter, DateRange, TimelineEvent } from '@nomideusz/svelte-calendar';
|
|
591
716
|
|
|
592
717
|
const adapter: CalendarAdapter = {
|
|
718
|
+
// Required: events overlapping the range
|
|
593
719
|
fetchEvents: async (range: DateRange) => { /* return TimelineEvent[] */ },
|
|
594
|
-
|
|
595
|
-
|
|
720
|
+
// Optional: the same answer synchronously, when the data is already in memory
|
|
721
|
+
fetchEventsSync: (range) => { /* return TimelineEvent[], or undefined to go async */ },
|
|
722
|
+
// Optional writes — omit them for a read-only source
|
|
723
|
+
createEvent: async (event) => { /* return the created TimelineEvent with its id */ },
|
|
724
|
+
updateEvent: async (id, patch) => { /* return the updated TimelineEvent */ },
|
|
596
725
|
deleteEvent: async (id) => { /* void */ },
|
|
597
726
|
};
|
|
598
727
|
```
|
|
599
728
|
|
|
729
|
+
`WritableCalendarAdapter` is the same interface with all three write methods required.
|
|
730
|
+
|
|
731
|
+
**`fetchEventsSync`** lets the first render — including the server render — hold the events instead of a loading state. The memory, recurring and seeded adapters implement it. To seed an async adapter with events you already loaded on the server, wrap it:
|
|
732
|
+
|
|
733
|
+
```ts
|
|
734
|
+
import { createRestAdapter, withInitialEvents } from '@nomideusz/svelte-calendar';
|
|
735
|
+
|
|
736
|
+
// data.events: TimelineEvent[] for the range the page opens on
|
|
737
|
+
const adapter = withInitialEvents(createRestAdapter({ baseUrl: '/api' }), data.events);
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
The first load is answered synchronously from the seed; every later load (navigation, refresh) goes to the wrapped adapter.
|
|
741
|
+
|
|
742
|
+
### Refusing a write
|
|
743
|
+
|
|
744
|
+
The Calendar tells two refusals apart when `updateEvent` throws:
|
|
745
|
+
|
|
746
|
+
| Throw | Meaning | What happens |
|
|
747
|
+
|-------|---------|--------------|
|
|
748
|
+
| `new CalendarReadOnlyError(message?)` | "I hold this event but can't store the change." | The block stays where it was dropped and `oneventmove` fires — the host stores the move and refreshes. |
|
|
749
|
+
| `new EventNotFoundError(id)` | "Not my event." | A composite asks its next child; a drag ends quietly. |
|
|
750
|
+
| any other error | A real failure | The block reverts and the error goes to `onerror` (or `console.warn`). |
|
|
751
|
+
|
|
752
|
+
An adapter without `updateEvent` counts as read-only. Plain `Error`s whose message contains `read-only` (lower case) or `not found` (any case) are read the same way, so older adapters keep working. `isReadOnlyError(e)` and `isNotFoundError(e)` test both forms.
|
|
753
|
+
|
|
600
754
|
## Headless API
|
|
601
755
|
|
|
602
|
-
For full control over rendering, skip
|
|
756
|
+
For full control over rendering, skip `<Calendar>` and drive everything from reactive state. `createCalendar()` returns computed layouts, navigation actions, drag helpers and the raw engines — zero DOM, bring your own UI. Call it during component initialisation.
|
|
603
757
|
|
|
604
758
|
```svelte
|
|
605
759
|
<script lang="ts">
|
|
@@ -630,11 +784,15 @@ For full control over rendering, skip the `<Calendar>` component and drive every
|
|
|
630
784
|
{/each}
|
|
631
785
|
```
|
|
632
786
|
|
|
633
|
-
|
|
787
|
+
Options (`HeadlessCalendarOptions`) mirror the Calendar props: `adapter`, `view` (default `'week-planner'`), `mondayStart`, `initialDate`, `timezone`, `locale`, `visibleHours`, `snapInterval`, `equalDays`, `hideDays`, `blockedSlots`, `disabledDates`, `days`, `readOnly`, `minDuration`, `maxDuration`, and the callbacks `oneventclick`, `oneventcreate`, `oneventmove`.
|
|
788
|
+
|
|
789
|
+
`cal.days` gives flat `HeadlessDay[]` with events attached; `cal.weeks` groups them into periods (`HeadlessWeek`). `cal.todayQueue` returns `{ past, current, upcoming }` for today (`TodayQueue`), updated every second. `cal.hours` yields the visible hour numbers. Drag is fully wired: `beginDragMove`, `beginDragCreate`, `updateDrag`, `commitDrag` (validates like the Calendar), `cancelDrag`, plus `isDragging` / `dragPayload` / `dragMode`. `headerContext` / `navigationContext` match the Calendar's `header` / `navigation` snippet contexts. Raw engines (`store`, `viewState`, `selection`, `dragState`, `clock`) are exposed for advanced cases.
|
|
634
790
|
|
|
635
|
-
The engine factories
|
|
791
|
+
The engine factories are exported individually — `createEventStore(adapter)`, `createViewState(options)`, `createSelection()`, `createDragState()`, `createClock(timezone?)` — if you want to compose your own calendar loop (types: `EventStore`, `ViewState`, `ViewStateOptions`, `CalendarSelection`, `DragState`, `DragMode`, `DragPayload`, `CalendarViewId`, `BuiltInViewId`, `ViewMode`).
|
|
636
792
|
|
|
637
|
-
|
|
793
|
+
### createAgenda
|
|
794
|
+
|
|
795
|
+
A live single-day list: `createAgenda({ adapter, initialDate?, locale?, lookahead?, timezone? })`.
|
|
638
796
|
|
|
639
797
|
```svelte
|
|
640
798
|
<script lang="ts">
|
|
@@ -654,21 +812,47 @@ For a simpler day-only list view, use `createAgenda()`:
|
|
|
654
812
|
{/each}
|
|
655
813
|
```
|
|
656
814
|
|
|
815
|
+
It returns `dayEvents`, `allDay`, `past`, `current`, `upcoming`, `upcomingSlots` (`TimeSlot[]`: overlapping events grouped as `{ startMs, endMs, events }`), `count`, `eta(ev)`, `progress(ev)`, `fmtTime` / `fmtDuration` / `fmtRange`, and `prev` / `next` / `goToday` / `setDate`.
|
|
816
|
+
|
|
817
|
+
### createRangeAgenda
|
|
818
|
+
|
|
819
|
+
A window of days with events grouped per day — no clock, drag or selection. Built for read-only schedule surfaces (public timetables, embeds):
|
|
820
|
+
|
|
821
|
+
```svelte
|
|
822
|
+
<script lang="ts">
|
|
823
|
+
import { createRangeAgenda } from '@nomideusz/svelte-calendar';
|
|
824
|
+
|
|
825
|
+
const agenda = createRangeAgenda({ adapter, days: 7, locale: 'pl-PL', timezone: 'Europe/Warsaw' });
|
|
826
|
+
</script>
|
|
827
|
+
|
|
828
|
+
<button onclick={agenda.prev}>←</button>
|
|
829
|
+
<button onclick={agenda.next}>→</button>
|
|
830
|
+
|
|
831
|
+
{#each agenda.days.filter((d) => d.events.length > 0) as day (day.ms)}
|
|
832
|
+
<h3>{day.date.toLocaleDateString()}</h3>
|
|
833
|
+
{#each day.events as ev (ev.id)}
|
|
834
|
+
<p>{agenda.fmtRange(ev)} {ev.title}</p>
|
|
835
|
+
{/each}
|
|
836
|
+
{/each}
|
|
837
|
+
```
|
|
838
|
+
|
|
839
|
+
Options (`RangeAgendaOptions`): `adapter` (or a getter), `days` (default 7), `initialDate` (the first day, not aligned to a week start), `locale`, `timezone`. It returns `days` (`RangeAgendaDay[]`: `ms`, `date`, `weekday`, `isToday`, `isPast`, `events`), `range`, `count`, `loading`, `error`, `prev()`, `next()`, `goToday()`, `setDate(date)`, `refresh()`, `fmtTime(date)`, `fmtDuration(ev)`, `fmtRange(ev)`.
|
|
840
|
+
|
|
657
841
|
## Localization (i18n)
|
|
658
842
|
|
|
659
|
-
The `locale` prop controls date/time formatting (BCP 47)
|
|
843
|
+
The `locale` prop controls date/time formatting (BCP 47). Without it, the global default locale is used — `'en-US'` unless you call `setDefaultLocale()`:
|
|
660
844
|
|
|
661
845
|
```svelte
|
|
662
846
|
<Calendar {adapter} locale="de-DE" />
|
|
663
847
|
```
|
|
664
848
|
|
|
665
|
-
`setDefaultLocale('de-DE')` sets
|
|
849
|
+
`setDefaultLocale('de-DE')` sets it for every calendar and formatter; `getDefaultLocale()` reads it back; `is24HourLocale(tag?)` tells you how times will format. An RTL locale (`ar-SA`, `he-IL`, …) sets `dir="rtl"` where the browser reports the locale's text direction (`Intl.Locale` text info); pass `dir` to be explicit.
|
|
666
850
|
|
|
667
851
|
Override UI labels per calendar with the `labels` prop — it's reactive (swap it to switch language live) and instance-scoped (two calendars on one page can carry different languages):
|
|
668
852
|
|
|
669
853
|
```svelte
|
|
670
854
|
<Calendar {adapter} locale="de-DE" labels={{
|
|
671
|
-
today: 'Heute', day: 'Tag', week: 'Woche',
|
|
855
|
+
today: 'Heute', day: 'Tag', week: 'Woche', month: 'Monat',
|
|
672
856
|
noEvents: 'Keine Termine',
|
|
673
857
|
nMore: (n) => `+${n} weitere`,
|
|
674
858
|
}} />
|
|
@@ -682,92 +866,115 @@ import { setLabels } from '@nomideusz/svelte-calendar';
|
|
|
682
866
|
setLabels({ today: 'Heute', day: 'Tag', week: 'Woche' });
|
|
683
867
|
```
|
|
684
868
|
|
|
685
|
-
The `labels` prop merges over the global set.
|
|
869
|
+
Both take a `Partial<CalendarLabels>` — that is the supported input: pass the keys you translate, and keys added in later minor releases fall back to English. The `labels` prop merges over the global set. `resetLabels()` restores English (`defaultLabels` exports it; `getLabels()` reads the active set). Global `setLabels()` calls after mount don't re-render mounted calendars — use the `labels` prop to switch language live. Standalone primitives (`DayHeader`, `EventBlock`, …) and the format helpers read the global set.
|
|
686
870
|
|
|
687
871
|
<details>
|
|
688
872
|
<summary>All label keys</summary>
|
|
689
873
|
|
|
690
|
-
| Key | Default |
|
|
691
|
-
|
|
692
|
-
| `today` | `'Today'`
|
|
693
|
-
| `
|
|
694
|
-
| `
|
|
695
|
-
| `
|
|
696
|
-
| `
|
|
697
|
-
| `
|
|
698
|
-
| `
|
|
699
|
-
| `
|
|
700
|
-
| `
|
|
701
|
-
| `
|
|
702
|
-
| `
|
|
703
|
-
| `
|
|
704
|
-
| `
|
|
705
|
-
| `
|
|
706
|
-
| `
|
|
707
|
-
| `
|
|
708
|
-
| `
|
|
709
|
-
| `
|
|
710
|
-
| `
|
|
711
|
-
| `
|
|
712
|
-
| `
|
|
713
|
-
| `
|
|
714
|
-
| `
|
|
715
|
-
| `
|
|
716
|
-
| `
|
|
874
|
+
| Key | Default |
|
|
875
|
+
|-----|---------|
|
|
876
|
+
| `today` / `yesterday` / `tomorrow` | `'Today'` / `'Yesterday'` / `'Tomorrow'` |
|
|
877
|
+
| `day` / `week` / `month` | `'Day'` / `'Week'` / `'Month'` (mode pills) |
|
|
878
|
+
| `planner` / `agenda` / `scroll` | `'Planner'` / `'Agenda'` / `'Scroll'` (view-type pills) |
|
|
879
|
+
| `now` / `free` / `allDay` | `'now'` / `'free'` / `'All day'` |
|
|
880
|
+
| `done` / `upNext` / `until` | `'Done'` / `'Up next'` / `'until'` |
|
|
881
|
+
| `noEvents` | `'No events'` |
|
|
882
|
+
| `nothingScheduled` / `nothingScheduledYet` / `nothingWasScheduled` | `'Nothing scheduled'` / `'Nothing scheduled yet'` / `'Nothing was scheduled'` |
|
|
883
|
+
| `allDoneForToday` | `'All done for today'` |
|
|
884
|
+
| `goToToday` | `'Go to today'` |
|
|
885
|
+
| `previousDay` / `nextDay` | `'Previous day'` / `'Next day'` |
|
|
886
|
+
| `previousWeek` / `nextWeek` | `'Previous week'` / `'Next week'` |
|
|
887
|
+
| `previousMonth` / `nextMonth` | `'Previous month'` / `'Next month'` |
|
|
888
|
+
| `calendar` / `viewMode` | `'Calendar'` / `'View mode'` (aria) |
|
|
889
|
+
| `cancelled` / `tentative` / `full` / `limited` | `'cancelled'` / `'tentative'` / `'full'` / `'limited'` (event status) |
|
|
890
|
+
| `unavailable` | `'Unavailable'` (a blocked slot without its own label) |
|
|
891
|
+
| `noViews` | `'No views registered.'` |
|
|
892
|
+
| `dayNavigation` / `weekNavigation` | `'Day navigation'` / `'Week navigation'` (aria) |
|
|
893
|
+
| `dayPlanner` / `scrollableDayPlanner` | `'Day planner'` / `'Scrollable day planner'` (aria) |
|
|
894
|
+
| `todaysLineup` / `weekAhead` / `multiWeekGrid` | `"Today's lineup"` / `'Week ahead'` / `'Multi-week calendar grid'` (aria) |
|
|
895
|
+
| `currentTime` / `createEvent` | `'Current time'` / `'Create event'` (aria) |
|
|
896
|
+
| `happeningNow` / `past` / `completed` / `inProgress` | `'happening now'` / `'past'` / `'completed'` / `'in progress'` |
|
|
897
|
+
| `showLess` | `'Show less'` |
|
|
898
|
+
| `nMore(n)` | `+3 more` |
|
|
899
|
+
| `nEvents(n)` | `1 event`, `5 events` |
|
|
900
|
+
| `nCompleted(n)` | `3 completed` |
|
|
901
|
+
| `dayNOfTotal(current, total)` | `day 2 of 4` |
|
|
902
|
+
| `percentComplete(pct)` | `75% complete` |
|
|
903
|
+
| `inMinutes(mins)` / `inHours(hours, mins)` / `inDays(days)` | `in 45m` / `in 2h 15m` or `in 2h` / `in 3d` |
|
|
717
904
|
|
|
718
905
|
</details>
|
|
719
906
|
|
|
720
907
|
## Timezone Support
|
|
721
908
|
|
|
722
|
-
Render the whole calendar in any IANA timezone — events, the now-indicator
|
|
723
|
-
and day boundaries all shift; ranges passed to `oneventcreate`/`oneventmove`
|
|
724
|
-
convert back to real instants:
|
|
909
|
+
Render the whole calendar in any IANA timezone — events, the now-indicator and day boundaries all shift:
|
|
725
910
|
|
|
726
911
|
```svelte
|
|
727
912
|
<Calendar {adapter} timezone="Europe/Warsaw" />
|
|
728
913
|
```
|
|
729
914
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
915
|
+
- `initialDate` and `currentDate` are instants, read in that zone.
|
|
916
|
+
- `oneventcreate`, `oneventmove` and `onexternaldrop` receive real instants.
|
|
917
|
+
- `ondatechange` and `ondayclick` receive instants too — the start of that day in the zone — so feeding one back is stable: `currentDate={date} ondatechange={(d) => (date = d)}`. To read the date, format it in the zone:
|
|
918
|
+
|
|
919
|
+
```ts
|
|
920
|
+
import { formatInTimeZone } from '@nomideusz/svelte-calendar';
|
|
921
|
+
formatInTimeZone(d, 'Europe/Warsaw', { year: 'numeric', month: '2-digit', day: '2-digit' }, 'en-CA'); // "2026-10-24"
|
|
922
|
+
```
|
|
923
|
+
- Events handed to `oneventclick` / `oneventmove` / snippets are shifted the same way: their local fields show the zone's wall-clock time.
|
|
924
|
+
- `timezone` is read when the Calendar mounts. To change zones at runtime, remount it:
|
|
925
|
+
|
|
926
|
+
```svelte
|
|
927
|
+
{#key timezone}
|
|
928
|
+
<Calendar {adapter} {timezone} />
|
|
929
|
+
{/key}
|
|
930
|
+
```
|
|
734
931
|
|
|
735
|
-
|
|
932
|
+
Under the hood the adapter is wrapped with `wrapAdapterWithTimezone(adapter, zone)` (also exported), so views do plain local-time math on a zoned wall-clock plane. Known limit shared by every wall-clock calendar: the repeated hour of a DST fall-back is ambiguous, and writes inside it resolve to one of the two instants.
|
|
933
|
+
|
|
934
|
+
The headless helpers take the same option: `createCalendar({ timezone })`, `createAgenda({ timezone })`, `createRangeAgenda({ timezone })`, plus `createClock(timezone)` and `createViewState({ timezone })`.
|
|
935
|
+
|
|
936
|
+
Convert dates manually with the built-in helpers:
|
|
736
937
|
|
|
737
938
|
```ts
|
|
738
|
-
import { toZonedTime, fromZonedTime, nowInZone } from '@nomideusz/svelte-calendar';
|
|
939
|
+
import { toZonedTime, fromZonedTime, nowInZone, formatInTimeZone } from '@nomideusz/svelte-calendar';
|
|
739
940
|
|
|
740
|
-
//
|
|
941
|
+
// A Date whose local fields show the instant's wall-clock time in New York
|
|
741
942
|
const local = toZonedTime(utcDate, 'America/New_York');
|
|
742
943
|
|
|
743
|
-
//
|
|
744
|
-
const utc = fromZonedTime(
|
|
944
|
+
// Back to the real instant before saving
|
|
945
|
+
const utc = fromZonedTime(local, 'America/New_York');
|
|
745
946
|
|
|
746
|
-
// Current time in a timezone
|
|
947
|
+
// Current time in a timezone (wall-clock Date)
|
|
747
948
|
const now = nowInZone('Asia/Tokyo');
|
|
748
949
|
|
|
749
|
-
// Format
|
|
750
|
-
formatInTimeZone(
|
|
950
|
+
// Format an instant directly in a timezone: (date, zone, Intl options?, locale?)
|
|
951
|
+
formatInTimeZone(utcDate, 'America/New_York', { hour: '2-digit', minute: '2-digit' });
|
|
751
952
|
```
|
|
752
953
|
|
|
753
954
|
## Utilities
|
|
754
955
|
|
|
755
|
-
Small helpers used by the built-in views, exported for custom rendering:
|
|
956
|
+
Small helpers used by the built-in views, exported for custom rendering. Formatters take an optional `locale` (default: the global default locale); day helpers work on millisecond timestamps.
|
|
756
957
|
|
|
757
958
|
| Export | Purpose |
|
|
758
959
|
|--------|---------|
|
|
759
|
-
| `fmtTime(date, locale?)` / `fmtH(hour, locale?)` |
|
|
760
|
-
| `fmtDuration(
|
|
761
|
-
| `fmtDay`
|
|
762
|
-
| `
|
|
763
|
-
| `
|
|
960
|
+
| `fmtTime(date, locale?)` / `fmtH(hour, locale?)` | Compact time / hour labels: `9:00a` / `9a` in 12-hour locales, `9:00` / `9` in 24-hour ones |
|
|
961
|
+
| `fmtDuration(start, end)` | `"45m"`, `"1h"`, `"1h 30m"` |
|
|
962
|
+
| `fmtDay(ms, todayMs, opts?, locale?)` | `"Today · Feb 21"`, `"Mon, Feb 17"`; `{ short: true }` for the short form |
|
|
963
|
+
| `fmtWeekRange(startMs, locale?, endMs?)` | A date range in the locale's own order: `"Sep 21 – 27, 2026"`, `"21–27 wrz 2026"`. `endMs` is the last day shown (default: six days on). |
|
|
964
|
+
| `dateShort` / `dateWithWeekday` | `"Feb 21"` / `"Mon, Feb 17"` |
|
|
965
|
+
| `weekdayShort` / `weekdayLong` / `monthShort` / `monthLong` | Name parts of a timestamp |
|
|
966
|
+
| `sod(ms)` | Start of that day (local midnight) |
|
|
967
|
+
| `startOfWeek(ms, mondayStart = true)` | Start-of-week timestamp |
|
|
968
|
+
| `addDaysMs(ms, n)` / `diffDays(a, b)` | Add / count **calendar** days — DST-safe (a DST day is 23 or 25 hours, so don't add `n * 86_400_000`) |
|
|
764
969
|
| `isAllDay(ev)` / `isMultiDay(ev)` | Event classification |
|
|
765
|
-
| `segmentForDay(ev, dayMs)` | The slice of a multi-day event
|
|
766
|
-
| `createClock()` | Reactive clock
|
|
767
|
-
| `
|
|
768
|
-
| `
|
|
769
|
-
| `
|
|
770
|
-
| `
|
|
970
|
+
| `segmentForDay(ev, dayMs)` | The slice of a multi-day event on one day (`DaySegment`), or `null` |
|
|
971
|
+
| `createClock(timezone?)` | Reactive clock: `tick` (ms, every second), `today`, `hm`, `s`, `fractionalHour`, `destroy()` |
|
|
972
|
+
| `textWidth(text, font)` / `textHeight(text, font, width, lineHeight)` / `lineCount(text, font, width, options?)` / `fits(text, font, width, lines = 1)` | Text measurement via [pretext](https://github.com/chenglou/pretext) — before render, no layout thrash. `options` is `TextLayoutOptions` (`whiteSpace`, `wordBreak`, `letterSpacing`). Browser-only. |
|
|
973
|
+
| `pickFit(candidates, font, width, lines = 1)` | The longest candidate that fits |
|
|
974
|
+
| `fitLabel(candidates, lines = 1)` | Attachment: `<span {@attach fitLabel([ev.title, ev.short])}>` keeps the longest label that fits its box, re-fitting on resize |
|
|
975
|
+
| `fontOf(el)` | An element's computed font as a canvas font string |
|
|
976
|
+
| `fitParts(parts, budget, measure?)` | Which parts of a dense chip fit, along either axis → `Record<key, boolean>`. Each `ChipPart` is `{ key, text, font }` to measure or `{ key, size }` to state its cost, plus `priority` (`0` is an anchor and never drops; higher numbers drop first) and `extra` (a gap or icon). The first optional part that doesn't fit ends the list, so a chip never keeps a later detail after dropping an earlier one. A budget of `0` (server-rendered, not yet measured) leaves only the anchors. |
|
|
977
|
+
| `typeset(text)` / `breakLines(text, font, width)` | Knuth-Plass paragraph breaking over the text's own spaces and soft hyphens. `<p {@attach typeset(text)}>` sets justified lines, re-done on resize; if the browser disagrees with the measure, the plain text goes back. Progressive — SSR text stays. |
|
|
771
978
|
|
|
772
979
|
## Embeddable Widget
|
|
773
980
|
|
|
@@ -782,81 +989,107 @@ Drop into any HTML page — no build tools needed. Registers a `<day-calendar>`
|
|
|
782
989
|
view="week-planner"
|
|
783
990
|
height="600"
|
|
784
991
|
locale="en-US"
|
|
785
|
-
mondaystart="true"
|
|
786
992
|
></day-calendar>
|
|
787
993
|
```
|
|
788
994
|
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
|
794
|
-
|
|
795
|
-
| `
|
|
796
|
-
| `
|
|
797
|
-
| `
|
|
798
|
-
| `
|
|
799
|
-
| `
|
|
800
|
-
| `
|
|
801
|
-
| `
|
|
995
|
+
In a bundler, `import '@nomideusz/svelte-calendar/widget'` registers the same element.
|
|
996
|
+
|
|
997
|
+
The widget renders inside shadow DOM: host-page CSS cannot affect the calendar and calendar styles never leak out, while the `auto` theme still probes the host page's colors and fonts across the shadow boundary.
|
|
998
|
+
|
|
999
|
+
| Attribute | Default | Description |
|
|
1000
|
+
|-----------|---------|-------------|
|
|
1001
|
+
| `api` | — | Events endpoint, fetched as `GET {api}?start=…&end=…` (ISO instants; appended with `&` if `api` already has a query). It must answer JSON: an array of events, or `{ "events": [...] }`. |
|
|
1002
|
+
| `events` | — | Inline JSON array of events, used when there is no `api` |
|
|
1003
|
+
| `headers` | — | JSON object of extra request headers for `api`. Without it the GET is a simple CORS request (no preflight). |
|
|
1004
|
+
| `readonly` | read-only | The widget is read-only unless `readonly="false"`. There is no write endpoint: with `api`, drags are only visual; with `events`, they are kept in memory. |
|
|
1005
|
+
| `view` | `week-planner` | Initial view ID (`week-planner`, `day-agenda`, `month-grid`, …) |
|
|
1006
|
+
| `theme` | `auto` | `auto`, `neutral` or `midnight` (unknown names fall back to `neutral`) |
|
|
1007
|
+
| `height` | `600` | Height in px (`"600"` or `"600px"`), or `auto` |
|
|
1008
|
+
| `locale` | `en-US` | BCP 47 locale tag |
|
|
1009
|
+
| `dir` | from `locale` | `ltr`, `rtl` or `auto` |
|
|
1010
|
+
| `mondaystart` | `true` | `"false"` starts weeks on Sunday |
|
|
1011
|
+
| `pills` | `true` | `"false"` hides the view pills |
|
|
1012
|
+
| `nav` | `true` | `"false"` hides prev/next/today |
|
|
1013
|
+
| `mobile` | `auto` | `auto`, `true` or `false` |
|
|
1014
|
+
| `days` | `7` | Days in week views, `1`–`7` |
|
|
1015
|
+
| `compact` | `false` | `"true"` for compact agenda rows |
|
|
1016
|
+
| `timezone` | viewer's zone | IANA zone, e.g. `Europe/Warsaw` (read when the element mounts) |
|
|
1017
|
+
|
|
1018
|
+
Each event in `api` responses and `events` needs `start` and `end` as date strings (ISO 8601) and may carry `id`, `title`, `color`, `allDay`, `subtitle` and `location`; other fields are ignored, as are events with an invalid or empty range. Changing an attribute updates the calendar in place.
|
|
802
1019
|
|
|
803
1020
|
## All Props
|
|
804
1021
|
|
|
805
1022
|
<details>
|
|
806
|
-
<summary>Full Calendar props reference</summary>
|
|
1023
|
+
<summary>Full Calendar props reference (<code>CalendarProps</code>)</summary>
|
|
807
1024
|
|
|
808
1025
|
| Prop | Type | Default | Description |
|
|
809
1026
|
|------|------|---------|-------------|
|
|
810
|
-
| `adapter` | `CalendarAdapter` | *required* | Data
|
|
811
|
-
| `views` | `CalendarView[]` |
|
|
812
|
-
| `view` | `
|
|
813
|
-
| `theme` | `string` | `auto` | CSS theme string
|
|
814
|
-
| `autoTheme` | `AutoThemeOptions \| false` | `
|
|
815
|
-
| `mobile` | `'auto' \| boolean` | `'auto'` |
|
|
816
|
-
| `height` | `number \| 'auto'` | `600` | Height in
|
|
817
|
-
| `borderRadius` | `number` | `12` | Border radius in
|
|
818
|
-
| `locale` | `string` | `'en-US'` | BCP 47 locale tag |
|
|
819
|
-
| `
|
|
820
|
-
| `
|
|
821
|
-
| `
|
|
822
|
-
| `
|
|
823
|
-
| `
|
|
1027
|
+
| `adapter` | `CalendarAdapter` | *required* | Data source (memory, recurring, mapped, composite, REST, JMAP, or custom) |
|
|
1028
|
+
| `views` | `readonly CalendarView[]` | `defaultViews` | View registry (the 8 built-ins). Spread `defaultViews` to add your own. |
|
|
1029
|
+
| `view` | `CalendarViewId` | first registered (`'day-planner'`) | Active view ID. Reactive. |
|
|
1030
|
+
| `theme` | `string` | `auto` | CSS theme string of `--dt-*` properties. `auto` probes the host page. |
|
|
1031
|
+
| `autoTheme` | `AutoThemeOptions \| false` | `undefined` (probe) | Fine-tune auto-detection: `{ mode, accent, font }`. `false` disables probing. |
|
|
1032
|
+
| `mobile` | `'auto' \| boolean` | `'auto'` | `'auto'` switches to mobile views below a 768px container width |
|
|
1033
|
+
| `height` | `number \| 'auto'` | `600` | Height in px, or `'auto'` to grow with content (ideal for Agenda views) |
|
|
1034
|
+
| `borderRadius` | `number` | `12` | Border radius in px. `0` for none. |
|
|
1035
|
+
| `locale` | `string` | global default (`'en-US'`) | BCP 47 locale tag; see `setDefaultLocale` |
|
|
1036
|
+
| `labels` | `Partial<CalendarLabels>` | — | Per-instance UI labels, merged over the global set. Reactive. |
|
|
1037
|
+
| `dir` | `'ltr' \| 'rtl' \| 'auto'` | from `locale` | Text direction |
|
|
1038
|
+
| `mondayStart` | `boolean` | `true` | Start weeks on Monday |
|
|
1039
|
+
| `readOnly` | `boolean` | `false` | Disable drag, resize, click-to-create and external drops |
|
|
1040
|
+
| `visibleHours` | `[number, number]` | — | Crop the grid to `[startHour, endHour)` |
|
|
1041
|
+
| `initialDate` | `Date` | today | Date to focus at mount (an instant; read in `timezone`) |
|
|
1042
|
+
| `currentDate` | `Date` | — | Controlled focus date (an instant; read in `timezone`) |
|
|
1043
|
+
| `timezone` | `string` | viewer's zone | IANA zone to render in. Read at mount. |
|
|
824
1044
|
| `snapInterval` | `number` | `15` | Drag snap in minutes |
|
|
825
|
-
| `
|
|
826
|
-
| `
|
|
1045
|
+
| `minColumnWidth` | `number` | `110` | Minimum day-column width (px) in planner views; below the total the grid scrolls horizontally |
|
|
1046
|
+
| `days` | `number` | `7` | Days shown in week mode (e.g. `3` for a rolling 3-day view) |
|
|
1047
|
+
| `hideDays` | `number[]` | — | ISO weekdays to hide (1=Mon … 7=Sun), e.g. `[6, 7]` |
|
|
1048
|
+
| `showModePills` | `boolean` | `true` | Show the Day/Week/Month pills and the view-type pills |
|
|
1049
|
+
| `showNavigation` | `boolean` | `true` | Show prev/next/today |
|
|
1050
|
+
| `showDates` | `boolean` | `true` | Show date numbers and the date label. `false` = day names only (templates, recurring schedules) |
|
|
827
1051
|
| `equalDays` | `boolean` | `false` | Treat all days equally (no past-day dimming/collapsing) |
|
|
828
|
-
| `
|
|
829
|
-
| `
|
|
830
|
-
| `
|
|
831
|
-
| `
|
|
832
|
-
| `
|
|
833
|
-
| `
|
|
834
|
-
| `
|
|
835
|
-
| `
|
|
836
|
-
| `
|
|
837
|
-
| `
|
|
838
|
-
| `
|
|
839
|
-
| `
|
|
840
|
-
| `
|
|
841
|
-
| `
|
|
842
|
-
| `
|
|
843
|
-
| `
|
|
844
|
-
| `
|
|
845
|
-
| `
|
|
846
|
-
| `
|
|
847
|
-
| `
|
|
848
|
-
| `empty` | `Snippet` | — | Empty state content |
|
|
1052
|
+
| `blockedSlots` | `BlockedSlot[]` | — | `{ day?, start, end, label? }` hour ranges that can't be booked (hatched in planner views) |
|
|
1053
|
+
| `disabledDates` | `Date[]` | — | Dates that refuse creates/moves; existing events stay visible and clickable |
|
|
1054
|
+
| `minDuration` | `number` | — | Minimum duration in minutes; clamps creates and resizes |
|
|
1055
|
+
| `maxDuration` | `number` | — | Maximum duration in minutes; clamps creates and resizes |
|
|
1056
|
+
| `compact` | `boolean` | `false` | Minimal text rows in Agenda views (dot + time + title) |
|
|
1057
|
+
| `columns` | `boolean` | `false` | Timetable layout: `week-agenda` days side by side on desktop. Pairs with `equalDays`; overrides `compact` |
|
|
1058
|
+
| `event` | `Snippet<[TimelineEvent]>` | — | Custom event content |
|
|
1059
|
+
| `empty` | `Snippet` | — | Empty state for the day agenda, mobile day and an empty planner week |
|
|
1060
|
+
| `dayHeader` | `Snippet<[{ date, isToday, dayName }]>` | — | Custom day header |
|
|
1061
|
+
| `header` | `Snippet<[HeaderContext]>` | — | Replace the whole header (`dateLabel`, `mode`, `modes`, `switchMode`, `prev`, `next`, `goToday`, `isViewOnToday`, `focusDate`) |
|
|
1062
|
+
| `navigation` | `Snippet<[NavigationContext]>` | — | Replace prev/next/today (`prev`, `next`, `goToday`, `isViewOnToday`, `focusDate`, `mode`) |
|
|
1063
|
+
| `oneventclick` | `(event, anchor?: DOMRect) => void` | — | Event clicked (and selected) |
|
|
1064
|
+
| `oneventcreate` | `(range: { start, end }) => void` | — | A validated new range (real instants) |
|
|
1065
|
+
| `oneventmove` | `(event, start, end) => void` | — | Drag/resize stored, or refused read-only (real instants) |
|
|
1066
|
+
| `onexternaldrop` | `(info: { start, dataTransfer }) => void` | — | Outside HTML5 drag dropped on the grid |
|
|
1067
|
+
| `onviewchange` | `(viewId) => void` | — | Once on mount, then on every view change |
|
|
1068
|
+
| `ondatechange` | `(date) => void` | — | Once on mount, then on every focus-date change |
|
|
1069
|
+
| `ondayclick` | `(date) => void` | open a day view | A month-grid day clicked |
|
|
1070
|
+
| `oneventhover` | `(event) => void` | — | Pointer enters an event |
|
|
1071
|
+
| `onerror` | `(error: Error) => void` | — | Load and commit failures |
|
|
849
1072
|
|
|
850
1073
|
</details>
|
|
851
1074
|
|
|
1075
|
+
## Versioning
|
|
1076
|
+
|
|
1077
|
+
The package follows [semver](https://semver.org/) from 1.0:
|
|
1078
|
+
|
|
1079
|
+
- The public API is exactly what the `.` and `./widget` entry points export. Deep imports into `dist/` are not supported.
|
|
1080
|
+
- `TimelineEvent`, `CalendarAdapter` and `DateRange` are stable shapes: fields may be added in minor releases, never renamed, removed or given new meaning outside a major.
|
|
1081
|
+
- The `--dt-*` theme tokens are part of the contract, as are the `<day-calendar>` attributes.
|
|
1082
|
+
- `CalendarLabels` gains keys in minor releases — pass a `Partial` (see [Localization](#localization-i18n)).
|
|
1083
|
+
|
|
852
1084
|
## Development
|
|
853
1085
|
|
|
854
1086
|
```bash
|
|
855
1087
|
pnpm install
|
|
856
|
-
pnpm dev
|
|
857
|
-
pnpm check
|
|
858
|
-
pnpm
|
|
859
|
-
pnpm run build
|
|
1088
|
+
pnpm dev # demo site
|
|
1089
|
+
pnpm check # svelte-check
|
|
1090
|
+
pnpm test # vitest
|
|
1091
|
+
pnpm run package # build the library into dist/
|
|
1092
|
+
pnpm run build:widget # build widget/widget.js
|
|
860
1093
|
```
|
|
861
1094
|
|
|
862
1095
|
## License
|