@nomideusz/svelte-calendar 0.20.1 → 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 -267
- 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 +43 -25
- 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 +16 -16
- package/dist/core/time.d.ts +9 -0
- package/dist/core/time.js +12 -5
- package/dist/core/timezone.d.ts +7 -3
- package/dist/core/timezone.js +10 -4
- package/dist/engine/event-store.svelte.js +94 -57
- package/dist/engine/view-state.svelte.d.ts +5 -1
- package/dist/engine/view-state.svelte.js +8 -6
- package/dist/headless/create-agenda.svelte.d.ts +8 -1
- package/dist/headless/create-agenda.svelte.js +36 -18
- package/dist/headless/create-calendar.svelte.js +57 -21
- package/dist/headless/create-range-agenda.svelte.d.ts +7 -0
- package/dist/headless/create-range-agenda.svelte.js +38 -17
- 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 +43 -35
- package/dist/views/agenda/AgendaWeek.svelte +75 -38
- package/dist/views/index.d.ts +2 -0
- package/dist/views/index.js +2 -0
- package/dist/views/mobile/MobileDay.svelte +99 -42
- package/dist/views/mobile/MobileWeek.svelte +31 -15
- package/dist/views/mobile/swipe.js +6 -1
- package/dist/views/month/MonthGrid.svelte +33 -21
- package/dist/views/month/MonthGrid.svelte.d.ts +1 -1
- package/dist/views/planner/PlannerScroll.svelte +438 -164
- package/dist/views/planner/PlannerWeek.svelte +150 -50
- package/dist/views/shared/context.svelte.d.ts +1 -1
- package/package.json +24 -9
- package/widget/widget.js +10000 -9520
- 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,65 +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
|
-
|
|
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. |
|
|
39
52
|
|
|
40
53
|
```svelte
|
|
41
|
-
<Calendar {adapter} view="week-planner" />
|
|
54
|
+
<Calendar {adapter} view="week-planner" />
|
|
42
55
|
<Calendar {adapter} view="week-scroll" />
|
|
43
|
-
<Calendar {adapter} view="day-planner" />
|
|
44
|
-
<Calendar {adapter} view="week-agenda" />
|
|
45
|
-
<Calendar {adapter} view="day-agenda" />
|
|
46
56
|
<Calendar {adapter} view="month-grid" />
|
|
47
57
|
```
|
|
48
58
|
|
|
49
|
-
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
|
+
```
|
|
50
64
|
|
|
51
65
|
Planner views are designed for direct manipulation:
|
|
52
66
|
|
|
53
67
|
- **Move** — drag an event to another day or time; a ghost previews the target before the move commits.
|
|
54
|
-
- **Resize** — drag an event's top/bottom
|
|
55
|
-
- **
|
|
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 })`.
|
|
56
71
|
|
|
57
|
-
|
|
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.
|
|
58
73
|
|
|
59
|
-
|
|
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).
|
|
60
75
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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`).
|
|
64
83
|
|
|
65
84
|
### Custom Views
|
|
66
85
|
|
|
67
|
-
The `views` prop replaces the
|
|
86
|
+
The `views` prop replaces the registry. Spread `defaultViews` to add your own view and keep the built-ins:
|
|
68
87
|
|
|
69
88
|
```svelte
|
|
70
89
|
<script lang="ts">
|
|
71
|
-
import { Calendar, type CalendarView } from '@nomideusz/svelte-calendar';
|
|
90
|
+
import { Calendar, defaultViews, type CalendarView } from '@nomideusz/svelte-calendar';
|
|
72
91
|
import KanbanDay from './KanbanDay.svelte';
|
|
73
92
|
|
|
74
93
|
const views: CalendarView[] = [
|
|
75
|
-
|
|
76
|
-
{ id: '
|
|
94
|
+
...defaultViews,
|
|
95
|
+
{ id: 'day-kanban', label: 'Kanban', mode: 'day', component: KanbanDay, props: { columns: 3 } },
|
|
77
96
|
];
|
|
78
97
|
</script>
|
|
79
98
|
|
|
80
99
|
<Calendar {adapter} {views} view="day-kanban" />
|
|
81
100
|
```
|
|
82
101
|
|
|
83
|
-
|
|
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)).
|
|
84
142
|
|
|
85
143
|
## Mobile
|
|
86
144
|
|
|
87
|
-
|
|
145
|
+
When the calendar's **container** is narrower than 768px, it swaps to touch-first Mobile views with swipe navigation and a compact header:
|
|
88
146
|
|
|
89
147
|
```svelte
|
|
90
|
-
<!-- Auto-detect (default) — switches at 768px -->
|
|
148
|
+
<!-- Auto-detect (default) — switches at a 768px container width -->
|
|
91
149
|
<Calendar {adapter} />
|
|
92
150
|
|
|
93
151
|
<!-- Force mobile layout -->
|
|
@@ -97,40 +155,106 @@ On narrow screens (`< 768px`), the calendar automatically remaps Planner views t
|
|
|
97
155
|
<Calendar {adapter} mobile={false} />
|
|
98
156
|
```
|
|
99
157
|
|
|
100
|
-
|
|
101
|
-
- **
|
|
102
|
-
|
|
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.
|
|
103
162
|
|
|
104
|
-
|
|
163
|
+
With `'auto'`, the first render (and the server render) is the desktop layout; the container is measured on mount, so SSR hydrates cleanly.
|
|
105
164
|
|
|
106
165
|
## Callbacks
|
|
107
166
|
|
|
108
167
|
```svelte
|
|
109
168
|
<Calendar
|
|
110
169
|
{adapter}
|
|
111
|
-
oneventclick={(event) => console.log('Clicked', event.title)}
|
|
170
|
+
oneventclick={(event, anchor) => console.log('Clicked', event.title, anchor)}
|
|
112
171
|
oneventcreate={(range) => console.log('New slot', range.start, range.end)}
|
|
113
172
|
oneventmove={(event, start, end) => console.log('Moved', event.title, start, end)}
|
|
173
|
+
onexternaldrop={({ start, dataTransfer }) => console.log('Dropped', start, dataTransfer.getData('text/plain'))}
|
|
114
174
|
onviewchange={(viewId) => console.log('View', viewId)}
|
|
175
|
+
ondatechange={(date) => console.log('Date', date)}
|
|
115
176
|
ondayclick={(date) => console.log('Day', date)}
|
|
116
177
|
onerror={(error) => console.error('Calendar', error)}
|
|
117
178
|
/>
|
|
118
179
|
```
|
|
119
180
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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.
|
|
123
194
|
|
|
124
|
-
|
|
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:
|
|
125
214
|
|
|
126
215
|
```svelte
|
|
127
216
|
<Calendar {adapter} readOnly />
|
|
128
217
|
```
|
|
129
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
|
+
|
|
130
254
|
Hide nav controls (prev/next/today) and treat all days equally (no past-day dimming):
|
|
131
255
|
|
|
132
256
|
```svelte
|
|
133
|
-
<!--
|
|
257
|
+
<!-- Fixed weekly schedule, no browsing, all days equal -->
|
|
134
258
|
<Calendar
|
|
135
259
|
{adapter}
|
|
136
260
|
view="week-agenda"
|
|
@@ -144,7 +268,6 @@ Hide nav controls (prev/next/today) and treat all days equally (no past-day dimm
|
|
|
144
268
|
Hide weekends for a workweek view:
|
|
145
269
|
|
|
146
270
|
```svelte
|
|
147
|
-
<!-- Office planner: Mon–Fri only -->
|
|
148
271
|
<Calendar {adapter} view="week-planner" hideDays={[6, 7]} />
|
|
149
272
|
```
|
|
150
273
|
|
|
@@ -162,7 +285,7 @@ Control which date the calendar shows from your app:
|
|
|
162
285
|
/>
|
|
163
286
|
```
|
|
164
287
|
|
|
165
|
-
Show a rolling 3-day view:
|
|
288
|
+
Show a rolling 3-day view (week views other than 7 days start at the focused date):
|
|
166
289
|
|
|
167
290
|
```svelte
|
|
168
291
|
<Calendar {adapter} view="week-planner" days={3} />
|
|
@@ -175,8 +298,8 @@ Block lunch hours and enforce 30–120 min events:
|
|
|
175
298
|
import type { BlockedSlot } from '@nomideusz/svelte-calendar';
|
|
176
299
|
|
|
177
300
|
const blocked: BlockedSlot[] = [
|
|
178
|
-
{ start: 12, end: 13, label: 'Lunch' },
|
|
179
|
-
{ 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)
|
|
180
303
|
];
|
|
181
304
|
</script>
|
|
182
305
|
|
|
@@ -192,7 +315,7 @@ Disable specific dates:
|
|
|
192
315
|
/>
|
|
193
316
|
```
|
|
194
317
|
|
|
195
|
-
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.
|
|
196
319
|
|
|
197
320
|
Custom day headers and hover previews:
|
|
198
321
|
|
|
@@ -216,11 +339,12 @@ Replace the built-in navigation or the entire header chrome with your own contro
|
|
|
216
339
|
</Calendar>
|
|
217
340
|
|
|
218
341
|
<Calendar {adapter}>
|
|
219
|
-
{#snippet header({ dateLabel, mode, switchMode, prev, next, goToday })}
|
|
342
|
+
{#snippet header({ dateLabel, mode, modes, switchMode, prev, next, goToday })}
|
|
220
343
|
<nav class="my-toolbar">
|
|
221
344
|
<h2>{dateLabel}</h2>
|
|
222
|
-
|
|
223
|
-
|
|
345
|
+
{#each modes as m}
|
|
346
|
+
<button class:active={mode === m} onclick={() => switchMode(m)}>{m}</button>
|
|
347
|
+
{/each}
|
|
224
348
|
<button onclick={prev}>←</button>
|
|
225
349
|
<button onclick={goToday}>Today</button>
|
|
226
350
|
<button onclick={next}>→</button>
|
|
@@ -229,7 +353,7 @@ Replace the built-in navigation or the entire header chrome with your own contro
|
|
|
229
353
|
</Calendar>
|
|
230
354
|
```
|
|
231
355
|
|
|
232
|
-
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:
|
|
233
357
|
|
|
234
358
|
```svelte
|
|
235
359
|
<Calendar {adapter} view="week-agenda" height="auto" compact />
|
|
@@ -241,8 +365,8 @@ Three built-in presets:
|
|
|
241
365
|
|
|
242
366
|
| Preset | Description |
|
|
243
367
|
|--------|-------------|
|
|
244
|
-
| `auto` | **Default.** Probes the host page at mount — background, accent color, fonts, light/dark mode — and
|
|
245
|
-
| `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. |
|
|
246
370
|
| `midnight` | Explicit dark theme. Charcoal bg, red accent. |
|
|
247
371
|
|
|
248
372
|
```svelte
|
|
@@ -255,21 +379,23 @@ Three built-in presets:
|
|
|
255
379
|
<Calendar {adapter} theme={midnight} /> <!-- explicit dark mode -->
|
|
256
380
|
```
|
|
257
381
|
|
|
382
|
+
`presets` maps the names to the strings (`presets.neutral`); the type `PresetName` is `'auto' | 'neutral' | 'midnight'`.
|
|
383
|
+
|
|
258
384
|
### Smart Auto Theme
|
|
259
385
|
|
|
260
|
-
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:
|
|
261
387
|
|
|
262
|
-
- **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
|
|
263
389
|
- **Light/dark mode** — from background luminance
|
|
264
|
-
- **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
|
|
265
391
|
- **Text color** — validated for contrast against the background
|
|
266
|
-
- **Fonts** —
|
|
392
|
+
- **Fonts** — the host's computed font stack; the mono stack from `--font-mono` and similar variables
|
|
267
393
|
|
|
268
|
-
It
|
|
394
|
+
It watches for changes (a dark-mode toggle on `<html>`/`<body>`, the system color scheme) and updates automatically.
|
|
269
395
|
|
|
270
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.
|
|
271
397
|
|
|
272
|
-
Fine-tune auto-detection with the `autoTheme` prop:
|
|
398
|
+
Fine-tune auto-detection with the `autoTheme` prop (`AutoThemeOptions`):
|
|
273
399
|
|
|
274
400
|
```svelte
|
|
275
401
|
<!-- Force dark mode even if the page background is light -->
|
|
@@ -281,73 +407,63 @@ Fine-tune auto-detection with the `autoTheme` prop:
|
|
|
281
407
|
<!-- Override the font stack -->
|
|
282
408
|
<Calendar {adapter} autoTheme={{ font: '"Poppins", sans-serif' }} />
|
|
283
409
|
|
|
284
|
-
<!--
|
|
285
|
-
<Calendar {adapter} autoTheme={{ mode: 'dark', accent: '#10b981' }} />
|
|
286
|
-
|
|
287
|
-
<!-- Disable auto-probing entirely (passive CSS inheritance only) -->
|
|
410
|
+
<!-- Disable probing entirely: inherit --dt-* from ancestors -->
|
|
288
411
|
<Calendar {adapter} autoTheme={false} />
|
|
289
412
|
```
|
|
290
413
|
|
|
291
|
-
###
|
|
414
|
+
### Your own tokens
|
|
292
415
|
|
|
293
|
-
|
|
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:
|
|
294
417
|
|
|
295
|
-
|
|
296
|
-
<!-- Wrap in a div with your overrides -->
|
|
297
|
-
<div style="--dt-accent: #e11d48; --dt-bg: #1a1a2e; --dt-text: rgba(255,255,255,0.87);">
|
|
298
|
-
<Calendar {adapter} />
|
|
299
|
-
</div>
|
|
300
|
-
```
|
|
418
|
+
- **Change a few tokens** — append them to a preset (later declarations win):
|
|
301
419
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
```css
|
|
305
|
-
:root {
|
|
306
|
-
--dt-bg: #fafafa;
|
|
307
|
-
--dt-accent: #2563eb;
|
|
308
|
-
--dt-text: rgba(0, 0, 0, 0.87);
|
|
309
|
-
--dt-border: rgba(0, 0, 0, 0.08);
|
|
310
|
-
}
|
|
311
|
-
```
|
|
420
|
+
```ts
|
|
421
|
+
import { neutral } from '@nomideusz/svelte-calendar';
|
|
312
422
|
|
|
313
|
-
|
|
423
|
+
const custom = `${neutral}; --dt-accent: #e11d48;`;
|
|
424
|
+
```
|
|
314
425
|
|
|
315
|
-
|
|
426
|
+
```svelte
|
|
427
|
+
<Calendar {adapter} theme={custom} />
|
|
428
|
+
```
|
|
316
429
|
|
|
317
|
-
|
|
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:
|
|
318
432
|
|
|
319
|
-
```
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
```
|
|
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
|
+
```
|
|
325
438
|
|
|
326
|
-
|
|
327
|
-
<Calendar {adapter} theme={custom} />
|
|
328
|
-
```
|
|
439
|
+
Component fallbacks use system fonts and a neutral blue accent, so the package stays clean in apps that set no tokens.
|
|
329
440
|
|
|
330
441
|
<details>
|
|
331
442
|
<summary>All design tokens</summary>
|
|
332
443
|
|
|
333
444
|
| Token | Purpose |
|
|
334
445
|
|-------|---------|
|
|
335
|
-
| `--dt-
|
|
336
|
-
| `--dt-
|
|
337
|
-
| `--dt-surface` | Elevated surface (alternating rows, headers) |
|
|
446
|
+
| `--dt-bg` | Calendar background |
|
|
447
|
+
| `--dt-surface` | Elevated surface (headers, alternating rows, chips) |
|
|
338
448
|
| `--dt-border` | Default border |
|
|
339
|
-
| `--dt-border-day` | Day-column dividers |
|
|
449
|
+
| `--dt-border-day` | Day-column / day-cell dividers |
|
|
340
450
|
| `--dt-text` | Primary text |
|
|
341
451
|
| `--dt-text-2` | Secondary text |
|
|
342
452
|
| `--dt-text-3` | Tertiary text |
|
|
343
453
|
| `--dt-accent` | Accent color |
|
|
344
|
-
| `--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 |
|
|
345
457
|
| `--dt-glow` | Accent glow / focus ring |
|
|
346
458
|
| `--dt-today-bg` | Today column highlight |
|
|
347
|
-
| `--dt-
|
|
459
|
+
| `--dt-weekend-bg` | Weekend cell tint |
|
|
460
|
+
| `--dt-hover` | Hover background |
|
|
348
461
|
| `--dt-scrollbar` | Scrollbar thumb |
|
|
349
462
|
| `--dt-success` | Completed indicator |
|
|
350
|
-
| `--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. |
|
|
351
467
|
|
|
352
468
|
</details>
|
|
353
469
|
|
|
@@ -365,23 +481,23 @@ const custom = `${neutral}; --dt-accent: #e11d48;`;
|
|
|
365
481
|
| `subtitle` | `string?` | Secondary text below the title |
|
|
366
482
|
| `tags` | `string[]?` | Small accent-colored pills |
|
|
367
483
|
| `allDay` | `boolean?` | Render as an all-day event |
|
|
368
|
-
| `location` | `string?` | Room, venue, or address
|
|
484
|
+
| `location` | `string?` | Room, venue, or address |
|
|
369
485
|
| `status` | `EventStatus?` | `'confirmed'` (default), `'cancelled'`, `'tentative'`, `'full'`, `'limited'` |
|
|
370
486
|
| `externalId` | `string?` | ID from an upstream system (booking platform, CRM, LMS) |
|
|
371
|
-
| `resourceId` | `string?` | Resource this event belongs to (room, instructor, court)
|
|
372
|
-
| `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. |
|
|
373
489
|
|
|
374
|
-
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.
|
|
375
491
|
|
|
376
492
|
### Auto-Coloring
|
|
377
493
|
|
|
378
|
-
Omit `color` and
|
|
494
|
+
Omit `color` and the built-in adapters assign a vivid palette color, grouped by `category` (or `title`):
|
|
379
495
|
|
|
380
496
|
```ts
|
|
381
497
|
const events = [
|
|
382
|
-
{ id: '1', title: 'Yoga',
|
|
383
|
-
{ id: '2', title: 'Pilates', category: 'wellness', start
|
|
384
|
-
{ 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
|
|
385
501
|
];
|
|
386
502
|
```
|
|
387
503
|
|
|
@@ -390,29 +506,26 @@ Generate a theme-harmonious palette from any accent color:
|
|
|
390
506
|
```ts
|
|
391
507
|
import { createMemoryAdapter, generatePalette } from '@nomideusz/svelte-calendar';
|
|
392
508
|
|
|
393
|
-
|
|
394
|
-
const palette = generatePalette('#e11d48');
|
|
509
|
+
const palette = generatePalette('#e11d48'); // (accent?, count = 15)
|
|
395
510
|
const adapter = createMemoryAdapter(events, { palette });
|
|
396
511
|
```
|
|
397
512
|
|
|
398
|
-
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).
|
|
399
514
|
|
|
400
515
|
### Multi-day & All-day
|
|
401
516
|
|
|
402
|
-
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:
|
|
403
518
|
|
|
404
519
|
```ts
|
|
405
520
|
const events = [
|
|
406
521
|
{ id: '1', title: 'Conference', start: new Date('2026-03-15'), end: new Date('2026-03-18'), allDay: true },
|
|
407
|
-
{ id: '2', title: 'Sprint',
|
|
522
|
+
{ id: '2', title: 'Sprint', start: new Date('2026-03-15T00:00'), end: new Date('2026-03-17T00:00') },
|
|
408
523
|
];
|
|
409
524
|
```
|
|
410
525
|
|
|
411
526
|
### Custom Event Rendering
|
|
412
527
|
|
|
413
|
-
|
|
414
|
-
interactive shell — click, keyboard, drag, selection — stays intact). The
|
|
415
|
-
`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:
|
|
416
529
|
|
|
417
530
|
```svelte
|
|
418
531
|
<Calendar {adapter}>
|
|
@@ -422,6 +535,9 @@ interactive shell — click, keyboard, drag, selection — stays intact). The
|
|
|
422
535
|
{#if ev.subtitle}<small>{ev.subtitle}</small>{/if}
|
|
423
536
|
</div>
|
|
424
537
|
{/snippet}
|
|
538
|
+
{#snippet empty()}
|
|
539
|
+
<p>Nothing booked — drag on the grid to add a class.</p>
|
|
540
|
+
{/snippet}
|
|
425
541
|
</Calendar>
|
|
426
542
|
```
|
|
427
543
|
|
|
@@ -439,10 +555,11 @@ For fixed repeating events (class timetables, office hours):
|
|
|
439
555
|
startDate: '2026-03-01', until: '2026-03-31' },
|
|
440
556
|
{ id: '3', title: 'Review', frequency: 'monthly', dayOfMonth: 15,
|
|
441
557
|
startTime: '10:00', endTime: '11:00' },
|
|
558
|
+
{ id: '4', title: 'Night shift', dayOfWeek: [5, 6], startTime: '22:00', endTime: '02:00' },
|
|
442
559
|
]);
|
|
443
560
|
</script>
|
|
444
561
|
|
|
445
|
-
<Calendar {adapter}
|
|
562
|
+
<Calendar {adapter} />
|
|
446
563
|
```
|
|
447
564
|
|
|
448
565
|
<details>
|
|
@@ -450,42 +567,52 @@ For fixed repeating events (class timetables, office hours):
|
|
|
450
567
|
|
|
451
568
|
| Field | Type | Default | Description |
|
|
452
569
|
|-------|------|---------|-------------|
|
|
453
|
-
| `id` | `string` | *required* |
|
|
570
|
+
| `id` | `string` | *required* | Rule identifier |
|
|
454
571
|
| `title` | `string` | *required* | Event title |
|
|
455
|
-
| `startTime` | `string` | *required* | Start time `"HH:MM"
|
|
456
|
-
| `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). |
|
|
457
574
|
| `frequency` | `'daily' \| 'weekly' \| 'monthly'` | `'weekly'` | Recurrence frequency |
|
|
458
|
-
| `interval` | `number` | `1` |
|
|
575
|
+
| `interval` | `number` | `1` | Every N periods (`2` + weekly = biweekly). Needs `startDate` when > 1. |
|
|
459
576
|
| `dayOfWeek` | `number \| number[]` | — | ISO weekday 1=Mon…7=Sun. Required for weekly. |
|
|
460
|
-
| `dayOfMonth` | `number` | `1` | Day of month (1–31)
|
|
461
|
-
| `startDate` | `string` | — | First occurrence `"YYYY-MM-DD"` |
|
|
462
|
-
| `until` | `string` | — | Last occurrence `"YYYY-MM-DD"` |
|
|
463
|
-
| `count` | `number` | — | Max occurrences from `startDate` |
|
|
464
|
-
| `
|
|
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` |
|
|
465
585
|
|
|
466
586
|
</details>
|
|
467
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
|
+
|
|
468
592
|
## REST Adapter
|
|
469
593
|
|
|
470
594
|
Connect to any REST API:
|
|
471
595
|
|
|
472
596
|
```ts
|
|
473
|
-
import {
|
|
597
|
+
import { createRestAdapter } from '@nomideusz/svelte-calendar';
|
|
598
|
+
|
|
599
|
+
type ApiItem = { id: string; name: string; startAt: string; endAt: string };
|
|
474
600
|
|
|
475
601
|
const adapter = createRestAdapter({
|
|
476
602
|
baseUrl: 'https://api.example.com/v1',
|
|
477
603
|
headers: { Authorization: 'Bearer TOKEN' },
|
|
478
|
-
// Optional: map your API shape to TimelineEvent[]
|
|
479
|
-
mapEvents: (data) =>
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
+
})),
|
|
485
612
|
});
|
|
486
613
|
```
|
|
487
614
|
|
|
488
|
-
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.
|
|
489
616
|
|
|
490
617
|
## JMAP Adapter
|
|
491
618
|
|
|
@@ -510,11 +637,11 @@ const adapter = createJmapAdapter(client, {
|
|
|
510
637
|
});
|
|
511
638
|
```
|
|
512
639
|
|
|
513
|
-
|
|
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).
|
|
514
641
|
|
|
515
642
|
## Mapped Adapter
|
|
516
643
|
|
|
517
|
-
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:
|
|
518
645
|
|
|
519
646
|
```ts
|
|
520
647
|
import { createMappedAdapter } from '@nomideusz/svelte-calendar';
|
|
@@ -526,7 +653,6 @@ const adapter = createMappedAdapter(rawClasses, {
|
|
|
526
653
|
end: 'ends_at_iso',
|
|
527
654
|
subtitle: 'teacher',
|
|
528
655
|
location: 'room',
|
|
529
|
-
color: 'color',
|
|
530
656
|
externalId: 'reference_id',
|
|
531
657
|
status: 'is_cancelled', // boolean → 'cancelled' / 'confirmed'
|
|
532
658
|
tags: ['is_free', 'is_bookable_online'],
|
|
@@ -534,9 +660,12 @@ const adapter = createMappedAdapter(rawClasses, {
|
|
|
534
660
|
});
|
|
535
661
|
```
|
|
536
662
|
|
|
537
|
-
|
|
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.
|
|
538
667
|
|
|
539
|
-
For full control, pass
|
|
668
|
+
For full control, pass `mapEvent(raw, index)` and skip `fields`:
|
|
540
669
|
|
|
541
670
|
```ts
|
|
542
671
|
const adapter = createMappedAdapter(rawData, {
|
|
@@ -551,53 +680,80 @@ const adapter = createMappedAdapter(rawData, {
|
|
|
551
680
|
});
|
|
552
681
|
```
|
|
553
682
|
|
|
554
|
-
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.
|
|
555
684
|
|
|
556
685
|
## Composite Adapter
|
|
557
686
|
|
|
558
|
-
Merge
|
|
687
|
+
Merge several adapters into one — e.g. a recurring weekly schedule plus one-off events:
|
|
559
688
|
|
|
560
689
|
```ts
|
|
561
|
-
import {
|
|
562
|
-
Calendar,
|
|
563
|
-
createMemoryAdapter,
|
|
564
|
-
createRecurringAdapter,
|
|
565
|
-
createCompositeAdapter,
|
|
566
|
-
} from '@nomideusz/svelte-calendar';
|
|
690
|
+
import { createMemoryAdapter, createRecurringAdapter, createCompositeAdapter } from '@nomideusz/svelte-calendar';
|
|
567
691
|
|
|
568
692
|
const memory = createMemoryAdapter(oneOffEvents);
|
|
569
693
|
const recurring = createRecurringAdapter(weeklySchedule);
|
|
570
694
|
|
|
571
695
|
const adapter = createCompositeAdapter([memory, recurring]);
|
|
572
|
-
// Reads from both; writes go to memory (the primary — first adapter by default)
|
|
573
696
|
```
|
|
574
697
|
|
|
575
|
-
|
|
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`).
|
|
576
702
|
|
|
577
703
|
```ts
|
|
578
|
-
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
|
+
});
|
|
579
708
|
```
|
|
580
709
|
|
|
581
|
-
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.
|
|
582
|
-
|
|
583
710
|
## Custom Adapter
|
|
584
711
|
|
|
585
|
-
Implement the `CalendarAdapter` interface to connect any data source:
|
|
712
|
+
Implement the `CalendarAdapter` interface to connect any data source. Only `fetchEvents` is required:
|
|
586
713
|
|
|
587
714
|
```ts
|
|
588
715
|
import type { CalendarAdapter, DateRange, TimelineEvent } from '@nomideusz/svelte-calendar';
|
|
589
716
|
|
|
590
717
|
const adapter: CalendarAdapter = {
|
|
718
|
+
// Required: events overlapping the range
|
|
591
719
|
fetchEvents: async (range: DateRange) => { /* return TimelineEvent[] */ },
|
|
592
|
-
|
|
593
|
-
|
|
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 */ },
|
|
594
725
|
deleteEvent: async (id) => { /* void */ },
|
|
595
726
|
};
|
|
596
727
|
```
|
|
597
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
|
+
|
|
598
754
|
## Headless API
|
|
599
755
|
|
|
600
|
-
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.
|
|
601
757
|
|
|
602
758
|
```svelte
|
|
603
759
|
<script lang="ts">
|
|
@@ -628,11 +784,15 @@ For full control over rendering, skip the `<Calendar>` component and drive every
|
|
|
628
784
|
{/each}
|
|
629
785
|
```
|
|
630
786
|
|
|
631
|
-
|
|
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.
|
|
632
790
|
|
|
633
|
-
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`).
|
|
634
792
|
|
|
635
|
-
|
|
793
|
+
### createAgenda
|
|
794
|
+
|
|
795
|
+
A live single-day list: `createAgenda({ adapter, initialDate?, locale?, lookahead?, timezone? })`.
|
|
636
796
|
|
|
637
797
|
```svelte
|
|
638
798
|
<script lang="ts">
|
|
@@ -652,21 +812,47 @@ For a simpler day-only list view, use `createAgenda()`:
|
|
|
652
812
|
{/each}
|
|
653
813
|
```
|
|
654
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
|
+
|
|
655
841
|
## Localization (i18n)
|
|
656
842
|
|
|
657
|
-
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()`:
|
|
658
844
|
|
|
659
845
|
```svelte
|
|
660
846
|
<Calendar {adapter} locale="de-DE" />
|
|
661
847
|
```
|
|
662
848
|
|
|
663
|
-
`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.
|
|
664
850
|
|
|
665
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):
|
|
666
852
|
|
|
667
853
|
```svelte
|
|
668
854
|
<Calendar {adapter} locale="de-DE" labels={{
|
|
669
|
-
today: 'Heute', day: 'Tag', week: 'Woche',
|
|
855
|
+
today: 'Heute', day: 'Tag', week: 'Woche', month: 'Monat',
|
|
670
856
|
noEvents: 'Keine Termine',
|
|
671
857
|
nMore: (n) => `+${n} weitere`,
|
|
672
858
|
}} />
|
|
@@ -680,92 +866,115 @@ import { setLabels } from '@nomideusz/svelte-calendar';
|
|
|
680
866
|
setLabels({ today: 'Heute', day: 'Tag', week: 'Woche' });
|
|
681
867
|
```
|
|
682
868
|
|
|
683
|
-
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.
|
|
684
870
|
|
|
685
871
|
<details>
|
|
686
872
|
<summary>All label keys</summary>
|
|
687
873
|
|
|
688
|
-
| Key | Default |
|
|
689
|
-
|
|
690
|
-
| `today` | `'Today'`
|
|
691
|
-
| `
|
|
692
|
-
| `
|
|
693
|
-
| `
|
|
694
|
-
| `
|
|
695
|
-
| `
|
|
696
|
-
| `
|
|
697
|
-
| `
|
|
698
|
-
| `
|
|
699
|
-
| `
|
|
700
|
-
| `
|
|
701
|
-
| `
|
|
702
|
-
| `
|
|
703
|
-
| `
|
|
704
|
-
| `
|
|
705
|
-
| `
|
|
706
|
-
| `
|
|
707
|
-
| `
|
|
708
|
-
| `
|
|
709
|
-
| `
|
|
710
|
-
| `
|
|
711
|
-
| `
|
|
712
|
-
| `
|
|
713
|
-
| `
|
|
714
|
-
| `
|
|
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` |
|
|
715
904
|
|
|
716
905
|
</details>
|
|
717
906
|
|
|
718
907
|
## Timezone Support
|
|
719
908
|
|
|
720
|
-
Render the whole calendar in any IANA timezone — events, the now-indicator
|
|
721
|
-
and day boundaries all shift; ranges passed to `oneventcreate`/`oneventmove`
|
|
722
|
-
convert back to real instants:
|
|
909
|
+
Render the whole calendar in any IANA timezone — events, the now-indicator and day boundaries all shift:
|
|
723
910
|
|
|
724
911
|
```svelte
|
|
725
912
|
<Calendar {adapter} timezone="Europe/Warsaw" />
|
|
726
913
|
```
|
|
727
914
|
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
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
|
+
```
|
|
732
931
|
|
|
733
|
-
|
|
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:
|
|
734
937
|
|
|
735
938
|
```ts
|
|
736
|
-
import { toZonedTime, fromZonedTime, nowInZone } from '@nomideusz/svelte-calendar';
|
|
939
|
+
import { toZonedTime, fromZonedTime, nowInZone, formatInTimeZone } from '@nomideusz/svelte-calendar';
|
|
737
940
|
|
|
738
|
-
//
|
|
941
|
+
// A Date whose local fields show the instant's wall-clock time in New York
|
|
739
942
|
const local = toZonedTime(utcDate, 'America/New_York');
|
|
740
943
|
|
|
741
|
-
//
|
|
742
|
-
const utc = fromZonedTime(
|
|
944
|
+
// Back to the real instant before saving
|
|
945
|
+
const utc = fromZonedTime(local, 'America/New_York');
|
|
743
946
|
|
|
744
|
-
// Current time in a timezone
|
|
947
|
+
// Current time in a timezone (wall-clock Date)
|
|
745
948
|
const now = nowInZone('Asia/Tokyo');
|
|
746
949
|
|
|
747
|
-
// Format
|
|
748
|
-
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' });
|
|
749
952
|
```
|
|
750
953
|
|
|
751
954
|
## Utilities
|
|
752
955
|
|
|
753
|
-
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.
|
|
754
957
|
|
|
755
958
|
| Export | Purpose |
|
|
756
959
|
|--------|---------|
|
|
757
|
-
| `fmtTime(date, locale?)` / `fmtH(hour, locale?)` |
|
|
758
|
-
| `fmtDuration(
|
|
759
|
-
| `fmtDay`
|
|
760
|
-
| `
|
|
761
|
-
| `
|
|
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`) |
|
|
762
969
|
| `isAllDay(ev)` / `isMultiDay(ev)` | Event classification |
|
|
763
|
-
| `segmentForDay(ev, dayMs)` | The slice of a multi-day event
|
|
764
|
-
| `createClock()` | Reactive clock
|
|
765
|
-
| `
|
|
766
|
-
| `
|
|
767
|
-
| `
|
|
768
|
-
| `
|
|
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. |
|
|
769
978
|
|
|
770
979
|
## Embeddable Widget
|
|
771
980
|
|
|
@@ -780,81 +989,107 @@ Drop into any HTML page — no build tools needed. Registers a `<day-calendar>`
|
|
|
780
989
|
view="week-planner"
|
|
781
990
|
height="600"
|
|
782
991
|
locale="en-US"
|
|
783
|
-
mondaystart="true"
|
|
784
992
|
></day-calendar>
|
|
785
993
|
```
|
|
786
994
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
|
792
|
-
|
|
793
|
-
| `
|
|
794
|
-
| `
|
|
795
|
-
| `
|
|
796
|
-
| `
|
|
797
|
-
| `
|
|
798
|
-
| `
|
|
799
|
-
| `
|
|
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.
|
|
800
1019
|
|
|
801
1020
|
## All Props
|
|
802
1021
|
|
|
803
1022
|
<details>
|
|
804
|
-
<summary>Full Calendar props reference</summary>
|
|
1023
|
+
<summary>Full Calendar props reference (<code>CalendarProps</code>)</summary>
|
|
805
1024
|
|
|
806
1025
|
| Prop | Type | Default | Description |
|
|
807
1026
|
|------|------|---------|-------------|
|
|
808
|
-
| `adapter` | `CalendarAdapter` | *required* | Data
|
|
809
|
-
| `views` | `CalendarView[]` |
|
|
810
|
-
| `view` | `
|
|
811
|
-
| `theme` | `string` | `auto` | CSS theme string
|
|
812
|
-
| `autoTheme` | `AutoThemeOptions \| false` | `
|
|
813
|
-
| `mobile` | `'auto' \| boolean` | `'auto'` |
|
|
814
|
-
| `height` | `number \| 'auto'` | `600` | Height in
|
|
815
|
-
| `borderRadius` | `number` | `12` | Border radius in
|
|
816
|
-
| `locale` | `string` | `'en-US'` | BCP 47 locale tag |
|
|
817
|
-
| `
|
|
818
|
-
| `
|
|
819
|
-
| `
|
|
820
|
-
| `
|
|
821
|
-
| `
|
|
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. |
|
|
822
1044
|
| `snapInterval` | `number` | `15` | Drag snap in minutes |
|
|
823
|
-
| `
|
|
824
|
-
| `
|
|
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) |
|
|
825
1051
|
| `equalDays` | `boolean` | `false` | Treat all days equally (no past-day dimming/collapsing) |
|
|
826
|
-
| `
|
|
827
|
-
| `
|
|
828
|
-
| `
|
|
829
|
-
| `
|
|
830
|
-
| `
|
|
831
|
-
| `
|
|
832
|
-
| `
|
|
833
|
-
| `
|
|
834
|
-
| `
|
|
835
|
-
| `
|
|
836
|
-
| `
|
|
837
|
-
| `
|
|
838
|
-
| `
|
|
839
|
-
| `
|
|
840
|
-
| `
|
|
841
|
-
| `
|
|
842
|
-
| `
|
|
843
|
-
| `
|
|
844
|
-
| `
|
|
845
|
-
| `
|
|
846
|
-
| `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 |
|
|
847
1072
|
|
|
848
1073
|
</details>
|
|
849
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
|
+
|
|
850
1084
|
## Development
|
|
851
1085
|
|
|
852
1086
|
```bash
|
|
853
1087
|
pnpm install
|
|
854
|
-
pnpm dev
|
|
855
|
-
pnpm check
|
|
856
|
-
pnpm
|
|
857
|
-
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
|
|
858
1093
|
```
|
|
859
1094
|
|
|
860
1095
|
## License
|