@nomideusz/svelte-calendar 0.5.2 → 0.6.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 +408 -178
- package/dist/adapters/memory.d.ts +7 -9
- package/dist/adapters/memory.js +8 -23
- package/dist/adapters/recurring.d.ts +3 -8
- package/dist/adapters/recurring.js +27 -30
- package/dist/calendar/Calendar.svelte +457 -46
- package/dist/calendar/Calendar.svelte.d.ts +55 -4
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +1 -1
- package/dist/core/locale.js +2 -2
- package/dist/core/palette.d.ts +5 -0
- package/dist/core/palette.js +8 -0
- package/dist/core/types.d.ts +14 -0
- package/dist/engine/event-store.svelte.d.ts +0 -17
- package/dist/engine/event-store.svelte.js +30 -17
- package/dist/engine/index.d.ts +1 -1
- package/dist/engine/view-state.svelte.d.ts +10 -5
- package/dist/engine/view-state.svelte.js +32 -16
- package/dist/index.d.ts +7 -7
- package/dist/index.js +3 -4
- package/dist/theme/auto.d.ts +53 -0
- package/dist/theme/auto.js +534 -0
- package/dist/theme/index.d.ts +3 -1
- package/dist/theme/index.js +3 -1
- package/dist/theme/presets.d.ts +20 -6
- package/dist/theme/presets.js +19 -6
- package/dist/views/agenda/AgendaDay.svelte +116 -37
- package/dist/views/agenda/AgendaWeek.svelte +219 -70
- package/dist/views/agenda/index.d.ts +0 -2
- package/dist/views/agenda/index.js +0 -2
- package/dist/views/index.d.ts +3 -2
- package/dist/views/index.js +3 -2
- package/dist/views/mobile/Mobile.svelte +17 -0
- package/dist/views/mobile/Mobile.svelte.d.ts +7 -0
- package/dist/views/mobile/MobileDay.svelte +665 -0
- package/dist/views/mobile/MobileDay.svelte.d.ts +20 -0
- package/dist/views/mobile/MobileWeek.svelte +456 -0
- package/dist/views/mobile/MobileWeek.svelte.d.ts +20 -0
- package/dist/views/mobile/index.d.ts +1 -0
- package/dist/views/mobile/index.js +1 -0
- package/dist/views/planner/PlannerDay.svelte +144 -10
- package/dist/views/planner/PlannerWeek.svelte +145 -40
- package/dist/widget/CalendarWidget.svelte +2 -17
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @nomideusz/svelte-calendar
|
|
2
2
|
|
|
3
|
-
A themeable **Svelte 5** calendar with **Day/Week Planner & Agenda** views.
|
|
3
|
+
A themeable **Svelte 5** calendar with **Day/Week Planner & Agenda** views, touch-first mobile views, and a smart auto-theme that adapts to any page.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -13,145 +13,353 @@ pnpm add @nomideusz/svelte-calendar
|
|
|
13
13
|
## Quick Start
|
|
14
14
|
|
|
15
15
|
```svelte
|
|
16
|
-
<script
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
16
|
+
<script>
|
|
17
|
+
import { Calendar, createMemoryAdapter } from '@nomideusz/svelte-calendar';
|
|
18
|
+
|
|
19
|
+
const adapter = createMemoryAdapter([
|
|
20
|
+
{ id: '1', title: 'Team Sync', start: new Date('2026-03-01T09:00'), end: new Date('2026-03-01T10:00') },
|
|
21
|
+
{ id: '2', title: 'Lunch', start: new Date('2026-03-01T12:00'), end: new Date('2026-03-01T13:00') },
|
|
22
|
+
]);
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<Calendar {adapter} />
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
That's it — 6 views (Day/Week × Planner, Agenda, Mobile), auto-coloring, drag-and-drop, all out of the box. The default `auto` theme probes your page's background, accent color, and fonts, so the calendar adapts to any design.
|
|
29
|
+
|
|
30
|
+
## Views
|
|
31
|
+
|
|
32
|
+
Switch between **Planner** (time grid) and **Agenda** (list) in Day or Week mode:
|
|
33
|
+
|
|
34
|
+
```svelte
|
|
35
|
+
<Calendar {adapter} view="week-planner" /> <!-- default -->
|
|
36
|
+
<Calendar {adapter} view="day-planner" />
|
|
37
|
+
<Calendar {adapter} view="week-agenda" />
|
|
38
|
+
<Calendar {adapter} view="day-agenda" />
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Users can also switch via the built-in Day/Week pills.
|
|
42
|
+
|
|
43
|
+
Hide the pills when your app controls the view externally:
|
|
44
|
+
|
|
45
|
+
```svelte
|
|
46
|
+
<Calendar {adapter} view="day-planner" showModePills={false} />
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Mobile
|
|
50
|
+
|
|
51
|
+
On narrow screens (`< 768px`), the calendar automatically remaps Planner views to touch-first Mobile views with swipe navigation, a centralized header with Day/Week pills, and a compact layout:
|
|
52
|
+
|
|
53
|
+
```svelte
|
|
54
|
+
<!-- Auto-detect (default) — switches at 768px -->
|
|
55
|
+
<Calendar {adapter} />
|
|
56
|
+
|
|
57
|
+
<!-- Force mobile layout -->
|
|
58
|
+
<Calendar {adapter} mobile={true} />
|
|
59
|
+
|
|
60
|
+
<!-- Force desktop layout -->
|
|
61
|
+
<Calendar {adapter} mobile={false} />
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Mobile views include:
|
|
65
|
+
- **MobileDay** — vertical time grid with hour labels, swipe left/right to change days, all-day event chips at the top, tap-to-create
|
|
66
|
+
- **MobileWeek** — vertical day list showing each day's events with relative labels (Today, Tomorrow, etc.)
|
|
67
|
+
|
|
68
|
+
Agenda views keep their list-based layout on mobile but hide desktop floating navigation in favor of the centralized header.
|
|
69
|
+
|
|
70
|
+
## Callbacks
|
|
71
|
+
|
|
72
|
+
```svelte
|
|
73
|
+
<Calendar
|
|
74
|
+
{adapter}
|
|
75
|
+
oneventclick={(event) => console.log('Clicked', event.title)}
|
|
76
|
+
oneventcreate={(range) => console.log('New slot', range.start, range.end)}
|
|
77
|
+
oneventmove={(event, start, end) => console.log('Moved', event.title, start, end)}
|
|
78
|
+
onviewchange={(viewId) => console.log('View', viewId)}
|
|
79
|
+
/>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Set `readOnly` to disable drag, resize, and click-to-create:
|
|
83
|
+
|
|
84
|
+
```svelte
|
|
85
|
+
<Calendar {adapter} readOnly />
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Hide nav controls (prev/next/today) and treat all days equally (no past-day dimming):
|
|
89
|
+
|
|
90
|
+
```svelte
|
|
91
|
+
<!-- Yoga studio: fixed weekly schedule, no browsing, all days equal -->
|
|
92
|
+
<Calendar
|
|
93
|
+
{adapter}
|
|
94
|
+
view="week-agenda"
|
|
95
|
+
readOnly
|
|
96
|
+
showNavigation={false}
|
|
97
|
+
showDates={false}
|
|
98
|
+
equalDays
|
|
99
|
+
/>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Hide weekends for a workweek view:
|
|
103
|
+
|
|
104
|
+
```svelte
|
|
105
|
+
<!-- Office planner: Mon–Fri only -->
|
|
106
|
+
<Calendar {adapter} view="week-planner" hideDays={[6, 7]} />
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Control which date the calendar shows from your app:
|
|
110
|
+
|
|
111
|
+
```svelte
|
|
112
|
+
<script>
|
|
113
|
+
let date = $state(new Date());
|
|
114
|
+
</script>
|
|
115
|
+
|
|
116
|
+
<Calendar
|
|
117
|
+
{adapter}
|
|
118
|
+
currentDate={date}
|
|
119
|
+
ondatechange={(d) => date = d}
|
|
120
|
+
/>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Show a rolling 3-day view:
|
|
124
|
+
|
|
125
|
+
```svelte
|
|
126
|
+
<Calendar {adapter} view="week-planner" days={3} />
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Block lunch hours and enforce 30–120 min events:
|
|
27
130
|
|
|
28
|
-
|
|
131
|
+
```svelte
|
|
132
|
+
<script>
|
|
133
|
+
import type { BlockedSlot } from '@nomideusz/svelte-calendar';
|
|
29
134
|
|
|
30
|
-
const
|
|
31
|
-
{
|
|
32
|
-
{
|
|
33
|
-
{ id: 'day-agenda', label: 'Agenda', granularity: 'day', component: Agenda, props: { mode: 'day' } },
|
|
34
|
-
{ id: 'week-agenda',label: 'Agenda', granularity: 'week', component: Agenda, props: { mode: 'week' } },
|
|
135
|
+
const blocked: BlockedSlot[] = [
|
|
136
|
+
{ start: 12, end: 13, label: 'Lunch' }, // every day 12–1 PM
|
|
137
|
+
{ day: 6, start: 0, end: 24, label: 'Saturday' }, // block all Saturday
|
|
35
138
|
];
|
|
36
139
|
</script>
|
|
37
140
|
|
|
141
|
+
<Calendar {adapter} blockedSlots={blocked} minDuration={30} maxDuration={120} />
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Disable specific dates:
|
|
145
|
+
|
|
146
|
+
```svelte
|
|
38
147
|
<Calendar
|
|
39
|
-
{views}
|
|
40
148
|
{adapter}
|
|
41
|
-
|
|
42
|
-
theme={neutral}
|
|
43
|
-
height={600}
|
|
44
|
-
oneventclick={(ev) => console.log('clicked', ev.id)}
|
|
45
|
-
oneventcreate={(range) => console.log('create', range)}
|
|
149
|
+
disabledDates={[new Date('2026-03-25'), new Date('2026-04-01')]}
|
|
46
150
|
/>
|
|
47
151
|
```
|
|
48
152
|
|
|
49
|
-
|
|
153
|
+
Custom day headers and hover previews:
|
|
50
154
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
| `locale` | `string` | `'en-US'` | BCP 47 locale tag |
|
|
59
|
-
| `dir` | `'ltr' \| 'rtl' \| 'auto'` | - | Text direction |
|
|
60
|
-
| `mondayStart` | `boolean` | `true` | Start week on Monday |
|
|
61
|
-
| `readOnly` | `boolean` | `false` | Disable drag, resize, and click-to-create |
|
|
62
|
-
| `visibleHours` | `[number, number]` | - | Crop grid to `[startHour, endHour)` |
|
|
63
|
-
| `initialDate` | `Date` | today | Date to focus on at mount |
|
|
64
|
-
| `snapInterval` | `number` | `15` | Drag snap in minutes |
|
|
65
|
-
| `oneventclick` | `(event) => void` | - | Event clicked |
|
|
66
|
-
| `oneventcreate` | `(range) => void` | - | New time range selected |
|
|
67
|
-
| `oneventmove` | `(event, start, end) => void` | - | Event dragged to new time |
|
|
68
|
-
| `onviewchange` | `(viewId) => void` | - | Active view changed |
|
|
69
|
-
| `event` | `Snippet<[TimelineEvent]>` | - | Custom event rendering |
|
|
70
|
-
| `empty` | `Snippet` | - | Empty state content |
|
|
155
|
+
```svelte
|
|
156
|
+
<Calendar {adapter} oneventhover={(ev) => showTooltip(ev)}>
|
|
157
|
+
{#snippet dayHeader({ date, isToday, dayName })}
|
|
158
|
+
<span style:font-weight={isToday ? 'bold' : 'normal'}>{dayName}</span>
|
|
159
|
+
{/snippet}
|
|
160
|
+
</Calendar>
|
|
161
|
+
```
|
|
71
162
|
|
|
72
|
-
##
|
|
163
|
+
## Themes
|
|
164
|
+
|
|
165
|
+
Three built-in presets:
|
|
166
|
+
|
|
167
|
+
| Preset | Description |
|
|
168
|
+
|--------|-------------|
|
|
169
|
+
| `auto` | **Default.** Probes the host page at mount — background, accent color, fonts, light/dark mode — and generates matching `--dt-*` tokens automatically. Reactively watches for host theme changes. |
|
|
170
|
+
| `neutral` | Explicit light theme. White bg, blue accent, inherits host fonts. Use when embedding standalone. |
|
|
171
|
+
| `midnight` | Explicit dark theme. Charcoal bg, red accent. |
|
|
172
|
+
|
|
173
|
+
```svelte
|
|
174
|
+
<script>
|
|
175
|
+
import { Calendar, neutral, midnight } from '@nomideusz/svelte-calendar';
|
|
176
|
+
</script>
|
|
177
|
+
|
|
178
|
+
<Calendar {adapter} /> <!-- auto: adapts to host page -->
|
|
179
|
+
<Calendar {adapter} theme={neutral} /> <!-- explicit light mode -->
|
|
180
|
+
<Calendar {adapter} theme={midnight} /> <!-- explicit dark mode -->
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Smart Auto Theme
|
|
184
|
+
|
|
185
|
+
The default `auto` preset probes the host page's design and generates a calendar that blends in — no configuration needed. It detects:
|
|
186
|
+
|
|
187
|
+
- **Background color** — from CSS variables, inline styles, or computed styles
|
|
188
|
+
- **Light/dark mode** — from background luminance
|
|
189
|
+
- **Accent/brand color** — from CSS variables (`--accent`, `--primary`, `--bs-primary`, etc.), link colors, or button colors
|
|
190
|
+
- **Text color** — validated for contrast against the background
|
|
191
|
+
- **Fonts** — inherited via CSS cascade
|
|
192
|
+
|
|
193
|
+
It also watches for changes (e.g. dark mode toggle) and updates automatically.
|
|
194
|
+
|
|
195
|
+
Fine-tune auto-detection with the `autoTheme` prop:
|
|
196
|
+
|
|
197
|
+
```svelte
|
|
198
|
+
<!-- Force dark mode even if the page background is light -->
|
|
199
|
+
<Calendar {adapter} autoTheme={{ mode: 'dark' }} />
|
|
200
|
+
|
|
201
|
+
<!-- Override the accent color (skip probing) -->
|
|
202
|
+
<Calendar {adapter} autoTheme={{ accent: '#e11d48' }} />
|
|
203
|
+
|
|
204
|
+
<!-- Override the font stack -->
|
|
205
|
+
<Calendar {adapter} autoTheme={{ font: '"Poppins", sans-serif' }} />
|
|
206
|
+
|
|
207
|
+
<!-- Combine overrides -->
|
|
208
|
+
<Calendar {adapter} autoTheme={{ mode: 'dark', accent: '#10b981' }} />
|
|
209
|
+
|
|
210
|
+
<!-- Disable auto-probing entirely (passive CSS inheritance only) -->
|
|
211
|
+
<Calendar {adapter} autoTheme={false} />
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Manual CSS Variables
|
|
215
|
+
|
|
216
|
+
Override any design token by setting `--dt-*` custom properties on an ancestor:
|
|
217
|
+
|
|
218
|
+
```html
|
|
219
|
+
<!-- Wrap in a div with your overrides -->
|
|
220
|
+
<div style="--dt-accent: #e11d48; --dt-bg: #1a1a2e; --dt-text: rgba(255,255,255,0.87);">
|
|
221
|
+
<Calendar {adapter} />
|
|
222
|
+
</div>
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Or set them at the page level:
|
|
226
|
+
|
|
227
|
+
```css
|
|
228
|
+
:root {
|
|
229
|
+
--dt-bg: #fafafa;
|
|
230
|
+
--dt-accent: #2563eb;
|
|
231
|
+
--dt-text: rgba(0, 0, 0, 0.87);
|
|
232
|
+
--dt-border: rgba(0, 0, 0, 0.08);
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
The auto-probe reads these first — if you set `--dt-bg` or `--accent` on `:root`, the calendar picks them up.
|
|
237
|
+
|
|
238
|
+
### Extending Presets
|
|
239
|
+
|
|
240
|
+
Build on a preset by appending overrides:
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
import { neutral } from '@nomideusz/svelte-calendar';
|
|
244
|
+
|
|
245
|
+
// neutral base + custom accent + rounded feel
|
|
246
|
+
const custom = `${neutral}; --dt-accent: #e11d48;`;
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
```svelte
|
|
250
|
+
<Calendar {adapter} theme={custom} />
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
<details>
|
|
254
|
+
<summary>All design tokens</summary>
|
|
255
|
+
|
|
256
|
+
| Token | Purpose |
|
|
257
|
+
|-------|---------|
|
|
258
|
+
| `--dt-stage-bg` | Background behind the calendar (page area) |
|
|
259
|
+
| `--dt-bg` | Calendar card background |
|
|
260
|
+
| `--dt-surface` | Elevated surface (alternating rows, headers) |
|
|
261
|
+
| `--dt-border` | Default border |
|
|
262
|
+
| `--dt-border-day` | Day-column dividers |
|
|
263
|
+
| `--dt-text` | Primary text |
|
|
264
|
+
| `--dt-text-2` | Secondary text |
|
|
265
|
+
| `--dt-text-3` | Tertiary text |
|
|
266
|
+
| `--dt-accent` | Accent color |
|
|
267
|
+
| `--dt-accent-dim` | Accent at ~12% opacity |
|
|
268
|
+
| `--dt-glow` | Accent glow / focus ring |
|
|
269
|
+
| `--dt-today-bg` | Today column highlight |
|
|
270
|
+
| `--dt-btn-text` | Button label color |
|
|
271
|
+
| `--dt-scrollbar` | Scrollbar thumb |
|
|
272
|
+
| `--dt-success` | Completed indicator |
|
|
273
|
+
| `--dt-serif` / `--dt-sans` / `--dt-mono` | Font stacks |
|
|
274
|
+
|
|
275
|
+
</details>
|
|
276
|
+
|
|
277
|
+
## Events
|
|
278
|
+
|
|
279
|
+
### TimelineEvent
|
|
73
280
|
|
|
74
281
|
| Field | Type | Description |
|
|
75
282
|
|-------|------|-------------|
|
|
76
283
|
| `id` | `string` | Unique identifier |
|
|
77
284
|
| `title` | `string` | Event title |
|
|
78
285
|
| `start` / `end` | `Date` | Time range |
|
|
79
|
-
| `color` | `string?` | Accent color |
|
|
80
|
-
| `category` | `string?` |
|
|
81
|
-
| `subtitle` | `string?` | Secondary text |
|
|
286
|
+
| `color` | `string?` | Accent color (auto-assigned if omitted) |
|
|
287
|
+
| `category` | `string?` | Grouping key — events with the same category share a color |
|
|
288
|
+
| `subtitle` | `string?` | Secondary text below the title |
|
|
82
289
|
| `tags` | `string[]?` | Small accent-colored pills |
|
|
83
290
|
| `allDay` | `boolean?` | Render as an all-day event |
|
|
84
|
-
| `data` | `Record?` | Arbitrary payload |
|
|
291
|
+
| `data` | `Record?` | Arbitrary payload for your app |
|
|
85
292
|
|
|
86
|
-
|
|
293
|
+
### Auto-Coloring
|
|
87
294
|
|
|
88
|
-
|
|
89
|
-
dedicated strip above timed events in every view:
|
|
295
|
+
Omit `color` and events are auto-assigned a vivid palette color, grouped by `category` (or `title`):
|
|
90
296
|
|
|
91
297
|
```ts
|
|
92
|
-
const events
|
|
93
|
-
|
|
94
|
-
{ id: '
|
|
95
|
-
|
|
96
|
-
{ id: '2', title: 'Sprint', start: new Date('2025-03-15T00:00'), end: new Date('2025-03-17T00:00') },
|
|
97
|
-
// Overnight timed event — also renders in the all-day strip
|
|
98
|
-
{ id: '3', title: 'Hackathon', start: new Date('2025-03-15T18:00'), end: new Date('2025-03-16T06:00') },
|
|
298
|
+
const events = [
|
|
299
|
+
{ id: '1', title: 'Yoga', category: 'wellness', start: ..., end: ... },
|
|
300
|
+
{ id: '2', title: 'Pilates', category: 'wellness', start: ..., end: ... }, // same color
|
|
301
|
+
{ id: '3', title: 'Standup', start: ..., end: ... }, // different color
|
|
99
302
|
];
|
|
100
303
|
```
|
|
101
304
|
|
|
102
|
-
|
|
305
|
+
Generate a theme-harmonious palette from any accent color:
|
|
103
306
|
|
|
104
307
|
```ts
|
|
105
|
-
import {
|
|
308
|
+
import { createMemoryAdapter, generatePalette } from '@nomideusz/svelte-calendar';
|
|
106
309
|
|
|
107
|
-
|
|
108
|
-
|
|
310
|
+
// Colors that harmonize with your theme's accent
|
|
311
|
+
const palette = generatePalette('#e11d48');
|
|
312
|
+
const adapter = createMemoryAdapter(events, { palette });
|
|
109
313
|
```
|
|
110
314
|
|
|
111
|
-
|
|
315
|
+
### Multi-day & All-day
|
|
316
|
+
|
|
317
|
+
Events spanning multiple days or flagged `allDay: true` render in a dedicated strip above timed events:
|
|
318
|
+
|
|
319
|
+
```ts
|
|
320
|
+
const events = [
|
|
321
|
+
{ id: '1', title: 'Conference', start: new Date('2026-03-15'), end: new Date('2026-03-18'), allDay: true },
|
|
322
|
+
{ id: '2', title: 'Sprint', start: new Date('2026-03-15T00:00'), end: new Date('2026-03-17T00:00') },
|
|
323
|
+
];
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### Custom Event Rendering
|
|
112
327
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
328
|
+
Use the `event` snippet to fully control how events look:
|
|
329
|
+
|
|
330
|
+
```svelte
|
|
331
|
+
<Calendar {adapter}>
|
|
332
|
+
{#snippet event(ev)}
|
|
333
|
+
<div style="padding: 4px 8px;">
|
|
334
|
+
<strong>{ev.title}</strong>
|
|
335
|
+
{#if ev.subtitle}<small>{ev.subtitle}</small>{/if}
|
|
336
|
+
</div>
|
|
337
|
+
{/snippet}
|
|
338
|
+
</Calendar>
|
|
339
|
+
```
|
|
117
340
|
|
|
118
341
|
## Recurring Schedules
|
|
119
342
|
|
|
343
|
+
For fixed repeating events (class timetables, office hours):
|
|
344
|
+
|
|
120
345
|
```svelte
|
|
121
|
-
<script
|
|
122
|
-
import { Calendar,
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
{ id: '2', title: 'Math', dayOfWeek: [1, 3, 5], startTime: '09:00', endTime: '10:00',
|
|
130
|
-
startDate: '2025-09-01', until: '2025-12-15' },
|
|
131
|
-
// Biweekly team sync
|
|
132
|
-
{ id: '3', title: 'Sync', frequency: 'weekly', interval: 2, dayOfWeek: 2,
|
|
133
|
-
startTime: '14:00', endTime: '15:00', startDate: '2025-03-01' },
|
|
134
|
-
// Daily standup, March only
|
|
135
|
-
{ id: '4', title: 'Standup', frequency: 'daily', startTime: '09:00', endTime: '09:15',
|
|
136
|
-
startDate: '2025-03-01', until: '2025-03-31' },
|
|
137
|
-
// Workshop — 8 Saturday sessions
|
|
138
|
-
{ id: '5', title: 'Workshop', dayOfWeek: 6, startTime: '10:00', endTime: '12:00',
|
|
139
|
-
startDate: '2025-03-01', count: 8 },
|
|
140
|
-
// Monthly review on the 15th
|
|
141
|
-
{ id: '6', title: 'Review', frequency: 'monthly', dayOfMonth: 15,
|
|
346
|
+
<script>
|
|
347
|
+
import { Calendar, createRecurringAdapter } from '@nomideusz/svelte-calendar';
|
|
348
|
+
|
|
349
|
+
const adapter = createRecurringAdapter([
|
|
350
|
+
{ id: '1', title: 'Yoga', dayOfWeek: 1, startTime: '07:00', endTime: '08:30' },
|
|
351
|
+
{ id: '2', title: 'Standup', frequency: 'daily', startTime: '09:00', endTime: '09:15',
|
|
352
|
+
startDate: '2026-03-01', until: '2026-03-31' },
|
|
353
|
+
{ id: '3', title: 'Review', frequency: 'monthly', dayOfMonth: 15,
|
|
142
354
|
startTime: '10:00', endTime: '11:00' },
|
|
143
|
-
];
|
|
144
|
-
|
|
145
|
-
const adapter = createRecurringAdapter(schedule);
|
|
146
|
-
const views: CalendarView[] = [
|
|
147
|
-
{ id: 'week-planner', label: 'Planner', granularity: 'week', component: Planner, props: { mode: 'week' } },
|
|
148
|
-
];
|
|
355
|
+
]);
|
|
149
356
|
</script>
|
|
150
357
|
|
|
151
|
-
<Calendar {
|
|
358
|
+
<Calendar {adapter} readOnly />
|
|
152
359
|
```
|
|
153
360
|
|
|
154
|
-
|
|
361
|
+
<details>
|
|
362
|
+
<summary>RecurringEvent fields</summary>
|
|
155
363
|
|
|
156
364
|
| Field | Type | Default | Description |
|
|
157
365
|
|-------|------|---------|-------------|
|
|
@@ -166,66 +374,79 @@ segmentForDay(event, dayTimestamp);
|
|
|
166
374
|
| `startDate` | `string` | — | First occurrence `"YYYY-MM-DD"` |
|
|
167
375
|
| `until` | `string` | — | Last occurrence `"YYYY-MM-DD"` |
|
|
168
376
|
| `count` | `number` | — | Max occurrences from `startDate` |
|
|
169
|
-
| `color` | `string?` | — | Accent color |
|
|
377
|
+
| `color` | `string?` | — | Accent color (auto-assigned if omitted) |
|
|
170
378
|
|
|
171
|
-
|
|
379
|
+
</details>
|
|
172
380
|
|
|
173
|
-
|
|
174
|
-
// Explicit mapping
|
|
175
|
-
const adapter = createMemoryAdapter(events, { colorMap: { yoga: '#818cf8', wellness: '#34d399' } });
|
|
381
|
+
## REST Adapter
|
|
176
382
|
|
|
177
|
-
|
|
178
|
-
const adapter = createMemoryAdapter(events, { autoColor: '#6366f1' });
|
|
383
|
+
Connect to any REST API:
|
|
179
384
|
|
|
180
|
-
|
|
181
|
-
|
|
385
|
+
```ts
|
|
386
|
+
import { Calendar, createRestAdapter } from '@nomideusz/svelte-calendar';
|
|
387
|
+
|
|
388
|
+
const adapter = createRestAdapter({
|
|
389
|
+
baseUrl: 'https://api.example.com/v1',
|
|
390
|
+
headers: { Authorization: 'Bearer TOKEN' },
|
|
391
|
+
// Optional: map your API shape to TimelineEvent[]
|
|
392
|
+
mapEvents: (data) => data.items.map(item => ({
|
|
393
|
+
id: item.id,
|
|
394
|
+
title: item.name,
|
|
395
|
+
start: new Date(item.startAt),
|
|
396
|
+
end: new Date(item.endAt),
|
|
397
|
+
})),
|
|
398
|
+
});
|
|
182
399
|
```
|
|
183
400
|
|
|
184
|
-
|
|
401
|
+
The adapter calls `GET /events?start=...&end=...`, `POST /events`, `PATCH /events/:id`, and `DELETE /events/:id`.
|
|
402
|
+
|
|
403
|
+
## Custom Adapter
|
|
404
|
+
|
|
405
|
+
Implement the `CalendarAdapter` interface to connect any data source:
|
|
406
|
+
|
|
407
|
+
```ts
|
|
408
|
+
import type { CalendarAdapter, DateRange, TimelineEvent } from '@nomideusz/svelte-calendar';
|
|
409
|
+
|
|
410
|
+
const adapter: CalendarAdapter = {
|
|
411
|
+
fetchEvents: async (range: DateRange) => { /* return TimelineEvent[] */ },
|
|
412
|
+
createEvent: async (event) => { /* return created TimelineEvent with id */ },
|
|
413
|
+
updateEvent: async (id, patch) => { /* return updated TimelineEvent */ },
|
|
414
|
+
deleteEvent: async (id) => { /* void */ },
|
|
415
|
+
};
|
|
416
|
+
```
|
|
185
417
|
|
|
186
418
|
## Localization (i18n)
|
|
187
419
|
|
|
188
|
-
|
|
420
|
+
The `locale` prop controls date/time formatting (BCP 47):
|
|
421
|
+
|
|
422
|
+
```svelte
|
|
423
|
+
<Calendar {adapter} locale="de-DE" />
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
Override UI labels for full translation:
|
|
189
427
|
|
|
190
428
|
```ts
|
|
191
|
-
import { setLabels
|
|
192
|
-
import type { CalendarLabels } from '@nomideusz/svelte-calendar';
|
|
429
|
+
import { setLabels } from '@nomideusz/svelte-calendar';
|
|
193
430
|
|
|
194
|
-
// Override any subset — unset keys stay English
|
|
195
431
|
setLabels({
|
|
196
|
-
today: 'Heute',
|
|
197
|
-
yesterday: 'Gestern',
|
|
198
|
-
tomorrow: 'Morgen',
|
|
199
|
-
day: 'Tag',
|
|
200
|
-
week: 'Woche',
|
|
201
|
-
now: 'jetzt',
|
|
202
|
-
free: 'frei',
|
|
203
|
-
allDay: 'Ganztägig',
|
|
204
|
-
done: 'Erledigt',
|
|
205
|
-
upNext: 'Als Nächstes',
|
|
432
|
+
today: 'Heute', day: 'Tag', week: 'Woche',
|
|
206
433
|
noEvents: 'Keine Termine',
|
|
207
|
-
goToToday: 'Heute anzeigen',
|
|
208
434
|
nMore: (n) => `+${n} weitere`,
|
|
209
|
-
nEvents: (n) => `${n} Termin${n === 1 ? '' : 'e'}`,
|
|
210
435
|
});
|
|
211
|
-
|
|
212
|
-
// Reset to English
|
|
213
|
-
resetLabels();
|
|
214
|
-
|
|
215
|
-
// Read current labels
|
|
216
|
-
const labels = getLabels();
|
|
217
436
|
```
|
|
218
437
|
|
|
438
|
+
Call `resetLabels()` to restore English defaults.
|
|
439
|
+
|
|
219
440
|
<details>
|
|
220
|
-
<summary
|
|
441
|
+
<summary>All label keys</summary>
|
|
221
442
|
|
|
222
443
|
| Key | Default | Description |
|
|
223
444
|
|-----|---------|-------------|
|
|
224
445
|
| `today` | `'Today'` | Relative day label / nav button |
|
|
225
446
|
| `yesterday` | `'Yesterday'` | Relative day label |
|
|
226
447
|
| `tomorrow` | `'Tomorrow'` | Relative day label |
|
|
227
|
-
| `day` | `'Day'` |
|
|
228
|
-
| `week` | `'Week'` |
|
|
448
|
+
| `day` | `'Day'` | Mode pill |
|
|
449
|
+
| `week` | `'Week'` | Mode pill |
|
|
229
450
|
| `planner` | `'Planner'` | View label |
|
|
230
451
|
| `agenda` | `'Agenda'` | View label |
|
|
231
452
|
| `now` | `'now'` | Live indicator badge |
|
|
@@ -249,55 +470,26 @@ const labels = getLabels();
|
|
|
249
470
|
|
|
250
471
|
</details>
|
|
251
472
|
|
|
252
|
-
##
|
|
253
|
-
|
|
254
|
-
| Preset | Description |
|
|
255
|
-
|--------|-------------|
|
|
256
|
-
| `midnight` | Dark navy/slate, red accent |
|
|
257
|
-
| `neutral` | White/gray, blue accent, inherits fonts — **recommended** |
|
|
473
|
+
## Timezone Support
|
|
258
474
|
|
|
259
|
-
|
|
260
|
-
<Calendar {views} {adapter} theme={neutral} />
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
Customize any token:
|
|
475
|
+
Convert events between timezones using the built-in helpers:
|
|
264
476
|
|
|
265
477
|
```ts
|
|
266
|
-
|
|
267
|
-
${neutral}
|
|
268
|
-
--dt-accent: #e11d48;
|
|
269
|
-
--dt-bg: var(--my-app-surface);
|
|
270
|
-
--dt-text: var(--my-app-text);
|
|
271
|
-
`;
|
|
272
|
-
```
|
|
273
|
-
```
|
|
478
|
+
import { toZonedTime, fromZonedTime, nowInZone } from '@nomideusz/svelte-calendar';
|
|
274
479
|
|
|
275
|
-
|
|
276
|
-
|
|
480
|
+
// Display a UTC date in a specific timezone
|
|
481
|
+
const local = toZonedTime(utcDate, 'America/New_York');
|
|
277
482
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
| `--dt-bg` | Main background |
|
|
281
|
-
| `--dt-surface` | Elevated surface |
|
|
282
|
-
| `--dt-border` | Default border |
|
|
283
|
-
| `--dt-border-day` | Day-column dividers |
|
|
284
|
-
| `--dt-text` | Primary text |
|
|
285
|
-
| `--dt-text-2` | Secondary text |
|
|
286
|
-
| `--dt-text-3` | Tertiary text |
|
|
287
|
-
| `--dt-accent` | Accent color |
|
|
288
|
-
| `--dt-accent-dim` | Accent at ~12% opacity |
|
|
289
|
-
| `--dt-glow` | Accent glow / focus ring |
|
|
290
|
-
| `--dt-today-bg` | Today column highlight |
|
|
291
|
-
| `--dt-btn-text` | Button label color |
|
|
292
|
-
| `--dt-scrollbar` | Scrollbar thumb |
|
|
293
|
-
| `--dt-success` | Completed indicator |
|
|
294
|
-
| `--dt-serif` / `--dt-sans` / `--dt-mono` | Font stacks |
|
|
483
|
+
// Convert back to UTC before saving
|
|
484
|
+
const utc = fromZonedTime(localDate, 'America/New_York');
|
|
295
485
|
|
|
296
|
-
|
|
486
|
+
// Current time in a timezone
|
|
487
|
+
const now = nowInZone('Asia/Tokyo');
|
|
488
|
+
```
|
|
297
489
|
|
|
298
490
|
## Embeddable Widget
|
|
299
491
|
|
|
300
|
-
Drop into any HTML page
|
|
492
|
+
Drop into any HTML page — no build tools needed:
|
|
301
493
|
|
|
302
494
|
```html
|
|
303
495
|
<script src="https://cdn.jsdelivr.net/npm/@nomideusz/svelte-calendar/widget/widget.js"></script>
|
|
@@ -309,12 +501,50 @@ Drop into any HTML page - no build tools needed:
|
|
|
309
501
|
></day-calendar>
|
|
310
502
|
```
|
|
311
503
|
|
|
312
|
-
##
|
|
504
|
+
## All Props
|
|
313
505
|
|
|
314
|
-
|
|
315
|
-
<
|
|
316
|
-
|
|
317
|
-
|
|
506
|
+
<details>
|
|
507
|
+
<summary>Full Calendar props reference</summary>
|
|
508
|
+
|
|
509
|
+
| Prop | Type | Default | Description |
|
|
510
|
+
|------|------|---------|-------------|
|
|
511
|
+
| `adapter` | `CalendarAdapter` | *required* | Data layer (memory, recurring, REST, or custom) |
|
|
512
|
+
| `views` | `CalendarView[]` | all 4 built-in | Registered view components |
|
|
513
|
+
| `view` | `string` | first view | Active view ID |
|
|
514
|
+
| `theme` | `string` | `auto` | CSS theme string (`--dt-*` custom properties). `auto` probes the host page and generates matching tokens. |
|
|
515
|
+
| `autoTheme` | `AutoThemeOptions \| false` | `{}` | Fine-tune auto-detection: `{ mode, accent, font }`. Set `false` to disable probing. |
|
|
516
|
+
| `mobile` | `'auto' \| boolean` | `'auto'` | Mobile mode. `'auto'` detects via `matchMedia(<768px)`. Remaps Planner→Mobile views. |
|
|
517
|
+
| `height` | `number` | `600` | Total height in pixels |
|
|
518
|
+
| `borderRadius` | `number` | `12` | Border radius in pixels. Set to `0` for no rounding. |
|
|
519
|
+
| `locale` | `string` | `'en-US'` | BCP 47 locale tag |
|
|
520
|
+
| `dir` | `'ltr' \| 'rtl' \| 'auto'` | — | Text direction |
|
|
521
|
+
| `mondayStart` | `boolean` | `true` | Start week on Monday |
|
|
522
|
+
| `readOnly` | `boolean` | `false` | Disable drag, resize, and click-to-create |
|
|
523
|
+
| `visibleHours` | `[number, number]` | — | Crop grid to `[startHour, endHour)` |
|
|
524
|
+
| `initialDate` | `Date` | today | Date to focus on at mount |
|
|
525
|
+
| `snapInterval` | `number` | `15` | Drag snap in minutes |
|
|
526
|
+
| `showModePills` | `boolean` | `true` | Show the Day/Week mode pills |
|
|
527
|
+
| `showNavigation` | `boolean` | `true` | Show prev/next/today navigation |
|
|
528
|
+
| `equalDays` | `boolean` | `false` | Treat all days equally (no past-day dimming/collapsing) |
|
|
529
|
+
| `showDates` | `boolean` | `true` | Show date numbers in headers. `false` = day names only (Mon, Tue, …) |
|
|
530
|
+
| `hideDays` | `number[]` | — | ISO weekdays to hide (1=Mon … 7=Sun). E.g. `[6, 7]` hides weekends |
|
|
531
|
+
| `currentDate` | `Date` | — | Controlled focus date (drives which date the calendar shows) |
|
|
532
|
+
| `days` | `number` | `7` | Number of days shown in week views (e.g. `3` for a rolling 3-day view) |
|
|
533
|
+
| `blockedSlots` | `BlockedSlot[]` | — | Time ranges that cannot be booked (hatched overlay in planner views) |
|
|
534
|
+
| `disabledDates` | `Date[]` | — | Dates that are fully disabled (dimmed, no interaction) |
|
|
535
|
+
| `minDuration` | `number` | — | Minimum event duration in minutes (enforced on create & resize) |
|
|
536
|
+
| `maxDuration` | `number` | — | Maximum event duration in minutes (enforced on create & resize) |
|
|
537
|
+
| `dayHeader` | `Snippet<[{ date, isToday, dayName }]>` | — | Custom day header snippet for planner/agenda views |
|
|
538
|
+
| `oneventclick` | `(event) => void` | — | Event clicked |
|
|
539
|
+
| `oneventcreate` | `(range) => void` | — | New time range selected |
|
|
540
|
+
| `oneventmove` | `(event, start, end) => void` | — | Event dragged to new time |
|
|
541
|
+
| `onviewchange` | `(viewId) => void` | — | Active view changed |
|
|
542
|
+
| `oneventhover` | `(event) => void` | — | Pointer enters an event (for tooltips, previews) |
|
|
543
|
+
| `ondatechange` | `(date) => void` | — | Focused date changed (navigation, scroll, etc.) |
|
|
544
|
+
| `event` | `Snippet<[TimelineEvent]>` | — | Custom event rendering |
|
|
545
|
+
| `empty` | `Snippet` | — | Empty state content |
|
|
546
|
+
|
|
547
|
+
</details>
|
|
318
548
|
|
|
319
549
|
## Development
|
|
320
550
|
|