@nomideusz/svelte-calendar 0.4.0 → 0.5.2

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.
Files changed (73) hide show
  1. package/README.md +331 -568
  2. package/dist/adapters/recurring.d.ts +45 -8
  3. package/dist/adapters/recurring.js +227 -49
  4. package/dist/adapters/rest.js +6 -1
  5. package/dist/calendar/Calendar.svelte +118 -30
  6. package/dist/calendar/Calendar.svelte.d.ts +9 -8
  7. package/dist/calendar/index.d.ts +0 -1
  8. package/dist/calendar/index.js +0 -1
  9. package/dist/core/index.d.ts +5 -4
  10. package/dist/core/index.js +2 -3
  11. package/dist/core/locale.d.ts +74 -0
  12. package/dist/core/locale.js +96 -4
  13. package/dist/core/time.d.ts +30 -0
  14. package/dist/core/time.js +42 -0
  15. package/dist/core/types.d.ts +1 -72
  16. package/dist/core/types.js +1 -8
  17. package/dist/engine/event-store.svelte.js +2 -2
  18. package/dist/engine/index.d.ts +1 -1
  19. package/dist/engine/view-state.svelte.d.ts +11 -6
  20. package/dist/engine/view-state.svelte.js +5 -4
  21. package/dist/index.d.ts +6 -7
  22. package/dist/index.js +4 -4
  23. package/dist/primitives/EmptySlot.svelte +6 -16
  24. package/dist/primitives/EventBlock.svelte +6 -19
  25. package/dist/primitives/NowIndicator.svelte +7 -4
  26. package/dist/primitives/TimeGutter.svelte +1 -1
  27. package/dist/theme/index.d.ts +1 -1
  28. package/dist/theme/index.js +1 -1
  29. package/dist/theme/presets.d.ts +11 -19
  30. package/dist/theme/presets.js +25 -111
  31. package/dist/views/agenda/Agenda.svelte +17 -1516
  32. package/dist/views/agenda/Agenda.svelte.d.ts +0 -14
  33. package/dist/views/agenda/AgendaDay.svelte +976 -0
  34. package/dist/views/agenda/AgendaDay.svelte.d.ts +14 -0
  35. package/dist/views/agenda/AgendaWeek.svelte +775 -0
  36. package/dist/views/agenda/AgendaWeek.svelte.d.ts +15 -0
  37. package/dist/views/agenda/index.d.ts +2 -0
  38. package/dist/views/agenda/index.js +2 -0
  39. package/dist/views/index.d.ts +2 -6
  40. package/dist/views/index.js +2 -6
  41. package/dist/views/planner/Planner.svelte +17 -0
  42. package/dist/views/planner/Planner.svelte.d.ts +7 -0
  43. package/dist/views/planner/PlannerDay.svelte +957 -0
  44. package/dist/views/{day/DayGrid.svelte.d.ts → planner/PlannerDay.svelte.d.ts} +6 -16
  45. package/dist/views/planner/PlannerWeek.svelte +840 -0
  46. package/dist/views/{week/WeekGrid.svelte.d.ts → planner/PlannerWeek.svelte.d.ts} +4 -9
  47. package/dist/views/planner/index.d.ts +1 -0
  48. package/dist/views/planner/index.js +1 -0
  49. package/dist/widget/CalendarWidget.svelte +54 -33
  50. package/dist/widget/CalendarWidget.svelte.d.ts +1 -1
  51. package/package.json +70 -65
  52. package/widget/svelte-calendar.css +1 -0
  53. package/widget/widget.js +122 -260
  54. package/dist/calendar/Toolbar.svelte +0 -244
  55. package/dist/calendar/Toolbar.svelte.d.ts +0 -21
  56. package/dist/views/day/DayGrid.svelte +0 -1104
  57. package/dist/views/day/DayTimeline.svelte +0 -533
  58. package/dist/views/day/DayTimeline.svelte.d.ts +0 -8
  59. package/dist/views/day/index.d.ts +0 -2
  60. package/dist/views/day/index.js +0 -2
  61. package/dist/views/schedule/WeekSchedule.svelte +0 -137
  62. package/dist/views/schedule/WeekSchedule.svelte.d.ts +0 -40
  63. package/dist/views/schedule/index.d.ts +0 -1
  64. package/dist/views/schedule/index.js +0 -2
  65. package/dist/views/settings/Settings.svelte +0 -585
  66. package/dist/views/settings/Settings.svelte.d.ts +0 -39
  67. package/dist/views/settings/index.d.ts +0 -2
  68. package/dist/views/settings/index.js +0 -1
  69. package/dist/views/week/WeekGrid.svelte +0 -632
  70. package/dist/views/week/WeekHeatmap.svelte +0 -461
  71. package/dist/views/week/WeekHeatmap.svelte.d.ts +0 -9
  72. package/dist/views/week/index.d.ts +0 -2
  73. package/dist/views/week/index.js +0 -2
package/README.md CHANGED
@@ -1,568 +1,331 @@
1
- # @nomideusz/svelte-calendar
2
-
3
- [![GitHub stars](https://img.shields.io/github/stars/nomideusz/svelte-calendar?style=social)](https://github.com/nomideusz/svelte-calendar)
4
-
5
- A themeable, pluggable **Svelte 5** calendar component library with **Day** and **Week** views — ready for yoga studios, tour bookings, concerts, language schools, and more.
6
-
7
- > **Active development** — this library is under active development. APIs may evolve between minor versions. Feedback, issues, and PRs are welcome on [GitHub](https://github.com/nomideusz/svelte-calendar).
8
-
9
- ## Views — Concept-Paired
10
-
11
- Views are organised into **concepts** that span both Day and Week granularity.
12
- Switching between Day and Week preserves the active concept.
13
-
14
- | Concept | Day View | Week View | Description |
15
- |---------|----------|-----------|-------------|
16
- | **Grid** | DayGrid | WeekGrid | The primary planner — time blocks on a scrollable grid. |
17
- | **Timeline** | DayTimeline | — | Horizontal day timeline. Day-only. |
18
- | **Agenda** | Agenda `mode="day"` | Agenda `mode="week"` | List / feed — Done, Now, Next (day) or grouped-by-day scroll (week). |
19
- | **Heatmap** | — | WeekHeatmap | Density view — 24 cells per day showing busy/free intensity. Week-only. |
20
- | **Schedule** | — | WeekSchedule | Zero-config weekly schedule display. Single import convenience wrapper. |
21
-
22
- ## Installation
23
-
24
- ```bash
25
- pnpm add @nomideusz/svelte-calendar
26
- ```
27
-
28
- > **Peer dependency:** Svelte 5 (`^5.0.0`)
29
-
30
- ## Quick Start
31
-
32
- ```svelte
33
- <script lang="ts">
34
- import {
35
- Calendar,
36
- DayGrid,
37
- DayTimeline,
38
- Agenda,
39
- WeekGrid,
40
- WeekHeatmap,
41
- createMemoryAdapter,
42
- midnight,
43
- } from '@nomideusz/svelte-calendar';
44
- import type { CalendarView, TimelineEvent } from '@nomideusz/svelte-calendar';
45
-
46
- const events: TimelineEvent[] = [
47
- {
48
- id: '1', title: 'Yoga Flow',
49
- start: new Date('2025-03-01T09:00'), end: new Date('2025-03-01T10:00'),
50
- color: '#818cf8', subtitle: 'With Anna', tags: ['Beginner'],
51
- },
52
- {
53
- id: '2', title: 'Meditation',
54
- start: new Date('2025-03-01T12:00'), end: new Date('2025-03-01T12:45'),
55
- color: '#34d399',
56
- },
57
- ];
58
-
59
- // Adapters provide the data layer (in-memory, REST, etc.)
60
- const adapter = createMemoryAdapter(events);
61
-
62
- // Concepts are paired by label switching Day↔Week preserves the concept
63
- const views: CalendarView[] = [
64
- { id: 'day-grid', label: 'Grid', granularity: 'day', component: DayGrid },
65
- { id: 'week-grid', label: 'Grid', granularity: 'week', component: WeekGrid },
66
- { id: 'day-agenda', label: 'Agenda', granularity: 'day', component: Agenda, props: { mode: 'day' } },
67
- { id: 'week-agenda', label: 'Agenda', granularity: 'week', component: Agenda, props: { mode: 'week' } },
68
- { id: 'week-heatmap', label: 'Heatmap', granularity: 'week', component: WeekHeatmap },
69
- ];
70
- </script>
71
-
72
- <Calendar
73
- {views}
74
- {adapter}
75
- defaultView="week-grid"
76
- theme={midnight}
77
- height={600}
78
- oneventclick={(ev) => console.log('clicked', ev.id)}
79
- oneventcreate={(range) => console.log('create', range.start, range.end)}
80
- />
81
- ```
82
-
83
- ## Recurring Weekly Schedules
84
-
85
- Define a weekly schedule once — the adapter auto-projects it onto whatever week the calendar is viewing. No manual date math needed.
86
-
87
- ```svelte
88
- <script lang="ts">
89
- import { Calendar, WeekGrid, createRecurringAdapter, neutral } from '@nomideusz/svelte-calendar';
90
- import type { CalendarView, RecurringEvent } from '@nomideusz/svelte-calendar';
91
-
92
- const schedule: RecurringEvent[] = [
93
- { id: '1', title: 'Morning Yoga', dayOfWeek: 1, startTime: '07:00', endTime: '08:30', color: '#818cf8' },
94
- { id: '2', title: 'Pilates', dayOfWeek: 3, startTime: '18:00', endTime: '19:00', color: '#f472b6' },
95
- { id: '3', title: 'Sound Bath', dayOfWeek: 5, startTime: '19:00', endTime: '20:00', color: '#2dd4bf', subtitle: 'Crystal bowls', tags: ['Relaxing'] },
96
- ];
97
-
98
- const adapter = createRecurringAdapter(schedule);
99
- const views: CalendarView[] = [
100
- { id: 'week-grid', label: 'Grid', granularity: 'week', component: WeekGrid },
101
- ];
102
- </script>
103
-
104
- <Calendar {views} {adapter} defaultView="week-grid" theme={neutral} readOnly />
105
- ```
106
-
107
- ### RecurringEvent
108
-
109
- | Field | Type | Description |
110
- |-------|------|-------------|
111
- | `id` | `string` | Unique identifier |
112
- | `title` | `string` | Event title |
113
- | `dayOfWeek` | `1–7` | ISO weekday (1 = Monday … 7 = Sunday) |
114
- | `startTime` | `string` | Start time in `"HH:MM"` format |
115
- | `endTime` | `string` | End time in `"HH:MM"` format |
116
- | `color` | `string?` | Accent color |
117
- | `subtitle` | `string?` | Subtitle (rendered below title) |
118
- | `tags` | `string[]?` | Tag pills |
119
- | `category` | `string?` | Category for grouping / colorMap |
120
- | `data` | `Record?` | Arbitrary payload |
121
-
122
- ## WeekSchedule Zero-Config Convenience
123
-
124
- One import, one component. Pre-wires adapter, views, and toolbar internally:
125
-
126
- ```svelte
127
- <script>
128
- import { WeekSchedule } from '@nomideusz/svelte-calendar';
129
- import { neutral } from '@nomideusz/svelte-calendar';
130
-
131
- const schedule = [
132
- { id: '1', title: 'Yoga', dayOfWeek: 1, startTime: '07:00', endTime: '08:30', color: '#818cf8' },
133
- { id: '2', title: 'Pilates', dayOfWeek: 3, startTime: '18:00', endTime: '19:00', color: '#f472b6' },
134
- ];
135
- </script>
136
-
137
- <WeekSchedule {schedule} theme={neutral} locale="pl-PL" height={560} readOnly />
138
- ```
139
-
140
- Works with concrete events too:
141
-
142
- ```svelte
143
- <WeekSchedule events={myEvents} theme={neutral} height={560} />
144
- ```
145
-
146
- ## Read-Only Mode
147
-
148
- Pass `readOnly` to disable drag, resize, and click-to-create interactions:
149
-
150
- ```svelte
151
- <Calendar {views} {adapter} readOnly />
152
- ```
153
-
154
- In read-only mode:
155
- - Drag handles and resize affordances are disabled
156
- - Empty-slot creation clicks are suppressed
157
- - `oneventcreate` and `oneventmove` callbacks are not fired
158
- - `oneventclick` still works for navigation/display purposes
159
-
160
- ## Visible Hours
161
-
162
- Crop the grid to relevant hours no more scrolling past empty early morning / late night rows:
163
-
164
- ```svelte
165
- <!-- Only show 6 AM to 9 PM -->
166
- <Calendar {views} {adapter} visibleHours={[6, 21]} />
167
-
168
- <!-- Works on WeekSchedule too -->
169
- <WeekSchedule {schedule} visibleHours={[7, 20]} />
170
- ```
171
-
172
- The `visibleHours` prop is a `[startHour, endHour)` tuple. It applies to the WeekHeatmap grid cells and is passed through to all views.
173
-
174
- ## Subtitle & Tags on Events
175
-
176
- `TimelineEvent` supports `subtitle` and `tags` fields — rendered automatically in **all views**:
177
-
178
- ```ts
179
- const events: TimelineEvent[] = [
180
- {
181
- id: '1',
182
- title: 'Power Vinyasa',
183
- start: new Date('2025-03-01T10:00'),
184
- end: new Date('2025-03-01T11:15'),
185
- color: '#f472b6',
186
- subtitle: 'With Marco', // shown below the title
187
- tags: ['Advanced', 'Hot'], // rendered as small color pills
188
- },
189
- ];
190
- ```
191
-
192
- - **subtitle** secondary text below the title (all views: WeekGrid, DayGrid, DayTimeline, Agenda, EventBlock)
193
- - **tags** — accent-colored pills after the title (all views)
194
- - In space-constrained views (DayGrid, DayTimeline), subtitle/tags appear only when the event block is tall/wide enough
195
-
196
- ## Color Map & Auto-Coloring
197
-
198
- Instead of setting `color` on every event, let the adapter assign colors by category or title:
199
-
200
- ```ts
201
- // Explicit mapping
202
- const adapter = createMemoryAdapter(events, {
203
- colorMap: {
204
- yoga: '#818cf8',
205
- wellness: '#34d399',
206
- },
207
- });
208
-
209
- // Auto-assign from a built-in 15-color vivid palette
210
- const adapter = createMemoryAdapter(events, { autoColor: true });
211
- ```
212
-
213
- ### Theme-Aware Auto-Coloring
214
-
215
- Pass the theme's accent hex to `autoColor` and the palette is generated to harmonize with your theme — colors rotate via golden-angle hue spacing from the accent, with lightness adjusted for dark/light backgrounds:
216
-
217
- ```ts
218
- // Harmonious palette seeded from indigo accent
219
- const adapter = createMemoryAdapter(events, { autoColor: '#6366f1' });
220
-
221
- // Works with the recurring adapter too
222
- const adapter = createRecurringAdapter(schedule, { autoColor: '#ef4444' });
223
- ```
224
-
225
- | `autoColor` value | Behaviour |
226
- |---|---|
227
- | `true` | Original 15-color vivid palette (fixed, theme-independent) |
228
- | `'#ef4444'` | Golden-angle hue rotation from that accent; lightness adapted to dark/light |
229
-
230
- You can also use the palette generator directly:
231
-
232
- ```ts
233
- import { generatePalette } from '@nomideusz/svelte-calendar';
234
-
235
- generatePalette('#6366f1', 8); // 8 theme-harmonious hex colors
236
- generatePalette(); // default vivid 15-color palette
237
- ```
238
-
239
- Both `createMemoryAdapter` and `createRecurringAdapter` accept `colorMap` and `autoColor` options. Events with an explicit `color` field always take priority.
240
-
241
- ## Settings Panel
242
-
243
- The `Settings` component provides a theme picker and dynamic fields for controlling view parameters. It renders as a compact horizontal panel with auto-fill columns so the calendar stays visible while adjusting options.
244
-
245
- ```svelte
246
- <script lang="ts">
247
- import { Settings } from '@nomideusz/svelte-calendar';
248
- import type { SettingsField, PresetName } from '@nomideusz/svelte-calendar';
249
-
250
- let theme: PresetName = $state('midnight');
251
- let values = $state({ hourHeight: 60, elastic: true, startHour: 6, endHour: 21, visibleHours: false });
252
-
253
- const fields: SettingsField[] = [
254
- { key: 'hourHeight', label: 'Hour Height', group: 'Layout', type: 'range', min: 40, max: 120, step: 5 },
255
- { key: 'elastic', label: 'Elastic Compression', group: 'Behavior', type: 'toggle' },
256
- { key: 'visibleHours', label: 'Visible Hours', group: 'Time Range', type: 'toggle' },
257
- { key: 'startHour', label: 'Start Hour', group: 'Time Range', type: 'range', min: 0, max: 23, step: 1, enabledWhen: 'visibleHours' },
258
- { key: 'endHour', label: 'End Hour', group: 'Time Range', type: 'range', min: 1, max: 24, step: 1, enabledWhen: 'visibleHours' },
259
- ];
260
- </script>
261
-
262
- <Settings {fields} bind:values bind:theme />
263
- ```
264
-
265
- ### Field types
266
-
267
- | Type | Properties | Description |
268
- |------|-----------|-------------|
269
- | `range` | `min`, `max`, `step`, `enabledWhen?` | Slider with label and value display. `enabledWhen` references a toggle key — disables the slider when the toggle is off. |
270
- | `toggle` | — | Switch toggle (on/off) |
271
- | `select` | `options: {value, label}[]` | Dropdown select |
272
- | `segment` | `options: {value, label}[]` | Pill-button radio group |
273
-
274
- All fields support `group` for column grouping and `label` for display text.
275
-
276
- ## Themes
277
-
278
- Five built-in presets each view reads from the same `--dt-*` CSS custom property contract:
279
-
280
- | Preset | Tone |
281
- |--------|------|
282
- | `midnight` | Dark (deep navy/slate, red accent) |
283
- | `parchment` | Warm light (cream, burnt sienna accents) |
284
- | `indigo` | Cool light (white surface, indigo accents) |
285
- | `neutral` | **Site-friendly** white/gray, blue accent, `inherit` fonts so it matches your site |
286
- | `bare` | **Unstyled skeleton** — all `transparent`/`inherit`/`currentColor`, absorbs host styles entirely |
287
-
288
- ```svelte
289
- <script>
290
- import { midnight, parchment, indigo, neutral, bare, presets } from '@nomideusz/svelte-calendar';
291
- </script>
292
-
293
- <!-- Blends into your site with no extra work -->
294
- <Calendar {views} {adapter} theme={neutral} />
295
-
296
- <!-- Or use the presets map -->
297
- <WeekGrid style={presets['parchment']} events={events} />
298
- ```
299
-
300
- ### Matching Your Site
301
-
302
- The `neutral` preset inherits font families from your page (`--dt-sans: inherit; --dt-serif: inherit`) and uses a standard blue accent. For most sites this is all you need.
303
-
304
- If you need full control, start from `bare` — it sets everything to `transparent`/`inherit`/`currentColor` — then override only the tokens you care about:
305
-
306
- ```ts
307
- import { bare } from '@nomideusz/svelte-calendar';
308
-
309
- const myTheme = `
310
- ${bare}
311
- --dt-accent: #e11d48;
312
- --dt-bg: var(--my-app-surface);
313
- --dt-text: var(--my-app-text);
314
- --dt-border: var(--my-app-border);
315
- `;
316
- ```
317
-
318
- ### Custom Themes
319
-
320
- Pass any string of `--dt-*` custom property declarations:
321
-
322
- ```ts
323
- const custom = `
324
- --dt-bg: #0a0a0a;
325
- --dt-surface: #111;
326
- --dt-border: rgba(255,255,255,0.06);
327
- --dt-text: #e0e0e0;
328
- --dt-accent: #10b981;
329
- /* ...see presets.ts for the full token list */
330
- `;
331
- ```
332
-
333
- <details>
334
- <summary><strong>Full token reference</strong></summary>
335
-
336
- | Token | Purpose |
337
- |-------|----------|
338
- | `--dt-bg` | Main background |
339
- | `--dt-surface` | Elevated surface (cards, popovers) |
340
- | `--dt-border` | Default border |
341
- | `--dt-border-day` | Day-column dividers |
342
- | `--dt-text` | Primary text |
343
- | `--dt-text-2` | Secondary text |
344
- | `--dt-text-3` | Tertiary / muted text |
345
- | `--dt-accent` | Accent color (buttons, now-indicator, highlights) |
346
- | `--dt-accent-dim` | Accent at ~12% opacity |
347
- | `--dt-glow` | Accent glow / focus ring |
348
- | `--dt-today-bg` | Today column highlight |
349
- | `--dt-btn-text` | Button label color |
350
- | `--dt-scrollbar` | Scrollbar thumb |
351
- | `--dt-success` | Success / completed indicator |
352
- | `--dt-serif` | Serif font stack |
353
- | `--dt-sans` | Sans-serif font stack |
354
- | `--dt-mono` | Monospace font stack |
355
- | `--dt-hm-empty` | Heatmap: empty cell |
356
- | `--dt-hm-low` | Heatmap: low density |
357
- | `--dt-hm-mid` | Heatmap: medium density |
358
- | `--dt-hm-high` | Heatmap: high density |
359
- | `--dt-hm-max` | Heatmap: maximum density |
360
-
361
- </details>
362
-
363
- ## Accessibility
364
-
365
- All interactive elements include proper ARIA attributes and keyboard support:
366
-
367
- - **EventBlock** — `role="button"`, `aria-label` (title + time + duration + status), `tabindex="0"`, Enter/Space activates
368
- - **EmptySlot** — `role="button"`, `aria-label` (time range + duration), keyboard-accessible for event creation
369
- - **NowIndicator** — `role="status"`, `aria-live="polite"` announces the current time
370
- - **Calendar** — `role="region"`, `aria-label="Calendar"`
371
- - **Toolbar** — `aria-label="Calendar navigation"`
372
- - Focus-visible outlines on all interactive primitives
373
-
374
- ## Locale & i18n
375
-
376
- The calendar uses `Intl.DateTimeFormat` for all date/time formatting. Pass a BCP 47 locale tag to the `Calendar` shell:
377
-
378
- ```svelte
379
- <Calendar {views} {adapter} locale="pl-PL" dir="rtl" />
380
- ```
381
-
382
- | Prop | Type | Default | Description |
383
- |------|------|---------|-------------|
384
- | `locale` | `string` | `'en-US'` | BCP 47 tag — controls weekday names, month names, date formats |
385
- | `dir` | `'ltr' \| 'rtl' \| 'auto'` | — | Text direction (for Arabic, Hebrew, etc.) |
386
-
387
- The `locale` prop automatically switches between 12-hour and 24-hour time display based on the locale's hour cycle.
388
-
389
- ### Programmatic locale control
390
-
391
- ```ts
392
- import { setDefaultLocale, getDefaultLocale, is24HourLocale } from '@nomideusz/svelte-calendar';
393
-
394
- setDefaultLocale('de-DE'); // All formatting functions now use German
395
- is24HourLocale('en-US'); // false (12h)
396
- is24HourLocale('de-DE'); // true (24h)
397
- ```
398
-
399
- All formatting functions (`weekdayShort`, `monthLong`, `fmtDay`, `fmtWeekRange`, etc.) accept an optional `locale` parameter to override per-call.
400
-
401
- ## Timezones
402
-
403
- Timezone utilities are included via `date-fns-tz`:
404
-
405
- ```ts
406
- import {
407
- toZonedTime,
408
- fromZonedTime,
409
- nowInZone,
410
- formatInTimeZone,
411
- } from '@nomideusz/svelte-calendar';
412
-
413
- // Display a UTC date in a specific timezone
414
- const nyTime = toZonedTime(utcDate, 'America/New_York');
415
-
416
- // Convert back to UTC for storage
417
- const utc = fromZonedTime(displayDate, 'America/New_York');
418
-
419
- // Current time in Tokyo
420
- const tokyoNow = nowInZone('Asia/Tokyo');
421
-
422
- // Locale-aware formatting in a timezone
423
- formatInTimeZone(date, 'Europe/Warsaw', { hour: 'numeric', minute: '2-digit' }, 'pl-PL');
424
- // → "14:30"
425
- ```
426
-
427
- The engine's `createViewState` also accepts a `timezone` option:
428
-
429
- ```ts
430
- const viewState = createViewState({
431
- defaultView: 'week-grid',
432
- timezone: 'America/New_York', // stored on viewState.timezone
433
- });
434
- ```
435
-
436
- ## Architecture
437
-
438
- ```
439
- src/lib/
440
- ├── core/ # Clock, time utils, locale, types
441
- ├── engine/ # Reactive state: event-store, view-state, selection, drag
442
- ├── adapters/ # Data layer: memory, recurring, REST adapters
443
- ├── primitives/ # Low-level UI atoms: NowIndicator, EventBlock, TimeGutter...
444
- ├── calendar/ # Calendar shell, Toolbar
445
- ├── views/ # View components (day/, week/, agenda/, schedule/, settings/)
446
- └── theme/ # Preset themes and token definitions
447
- ```
448
-
449
- ## Engine
450
-
451
- ```ts
452
- import {
453
- createEventStore,
454
- createViewState,
455
- createSelection,
456
- createDragState,
457
- } from '@nomideusz/svelte-calendar';
458
- ```
459
-
460
- - **`createEventStore(adapter)`** — reactive event list with fetch/add/update/remove
461
- - **`createViewState(options)`** — current view, date range, navigation (prev/next/today)
462
- - **`createSelection()`** — selected event tracking
463
- - **`createDragState()`** — drag-to-create and drag-to-move state machine
464
-
465
- ## Adapters
466
-
467
- | Adapter | Use |
468
- |---------|-----|
469
- | `createMemoryAdapter(events, options?)` | In-memory — great for demos and prototyping. Supports `colorMap` and `autoColor` (including theme-aware). |
470
- | `createRecurringAdapter(schedule, options?)` | Weekly recurring schedules — auto-projects onto viewed weeks. Read-only. Supports `colorMap` and `autoColor`. |
471
- | `createRestAdapter(options)` | Fetch from a REST API using `/events` endpoints with configurable headers and response mappers (`mapEvents`, `mapEvent`). |
472
-
473
- ## Standalone Views
474
-
475
- Each view works independently without the Calendar shell:
476
-
477
- ```svelte
478
- <!-- Horizontal day timeline -->
479
- <DayTimeline style={midnight} events={events} />
480
-
481
- <!-- Vertical day grid with elastic night compression -->
482
- <DayGrid style={midnight} events={events} height={600} elastic />
483
-
484
- <!-- Agenda in day mode -->
485
- <Agenda mode="day" events={events} height={520} />
486
-
487
- <!-- Week density heatmap -->
488
- <WeekHeatmap style={parchment} events={events} height={320} />
489
- ```
490
-
491
- ## Embeddable Widget
492
-
493
- Drop a single `<script>` tag into **any** HTML page — no Svelte, no build tools, no npm needed.
494
-
495
- ### From CDN
496
-
497
- ```html
498
- <script src="https://cdn.jsdelivr.net/npm/@nomideusz/svelte-calendar/widget/widget.js"></script>
499
-
500
- <day-calendar
501
- api="https://myschool.com/api/events"
502
- theme="neutral"
503
- height="600"
504
- ></day-calendar>
505
- ```
506
-
507
- That's it. Two lines.
508
-
509
- ### With inline events (no API)
510
-
511
- ```html
512
- <script src="https://cdn.jsdelivr.net/npm/@nomideusz/svelte-calendar/widget/widget.js"></script>
513
-
514
- <day-calendar
515
- theme="midnight"
516
- height="500"
517
- events='[
518
- { "id": "1", "title": "Yoga Flow", "start": "2025-03-01T09:00", "end": "2025-03-01T10:00", "color": "#818cf8" },
519
- { "id": "2", "title": "Meditation", "start": "2025-03-01T12:00", "end": "2025-03-01T12:45", "color": "#34d399" }
520
- ]'
521
- ></day-calendar>
522
- ```
523
-
524
- ### Widget attributes
525
-
526
- | Attribute | Default | Description |
527
- |-----------|---------|-------------|
528
- | `api` | — | REST API base URL — fetches from `{api}/events?start=...&end=...` |
529
- | `events` | — | JSON string of events (alternative to `api`) |
530
- | `theme` | `neutral` | Preset: `midnight`, `parchment`, `indigo`, `neutral`, `bare` |
531
- | `view` | `week-grid` | Default view: `day-grid`, `week-grid`, `day-timeline`, `day-agenda`, `week-agenda`, `week-heatmap` |
532
- | `height` | `600` | Height in pixels |
533
- | `locale` | — | BCP 47 locale (`en-US`, `pl-PL`, `ar-SA`, etc.) |
534
- | `dir` | — | Text direction: `ltr`, `rtl`, `auto` |
535
- | `mondaystart` | `true` | Start week on Monday (`true`/`false`) |
536
- | `headers` | — | JSON string of HTTP headers for the REST adapter |
537
-
538
- > `events` and `headers` must be valid JSON strings. Invalid JSON is ignored (`events`) or throws during adapter setup (`headers`).
539
-
540
- ### REST API contract
541
-
542
- When using the `api` attribute, the widget expects your endpoint to accept:
543
-
544
- ```
545
- GET {api}/events?start={ISO}&end={ISO}
546
- ```
547
-
548
- And return either:
549
- - `[{ id, title, start, end, color? }, ...]`
550
- - `{ events: [{ id, title, start, end, color? }, ...] }`
551
-
552
- ## Development
553
-
554
- ```bash
555
- pnpm install
556
- pnpm dev # SvelteKit dev server (demo app)
557
- pnpm check # Type check
558
- pnpm run package # Build the library into dist/
559
- pnpm run build:widget # Build standalone widget.js
560
-
561
- # npm publish only runs `package` automatically (via prepublishOnly)
562
- # Run this too if you changed widget code:
563
- pnpm run build:widget
564
- ```
565
-
566
- ## License
567
-
568
- MIT
1
+ # @nomideusz/svelte-calendar
2
+
3
+ A themeable **Svelte 5** calendar with **Day/Week Planner & Agenda** views.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @nomideusz/svelte-calendar
9
+ ```
10
+
11
+ > Requires Svelte 5 (`^5.0.0`)
12
+
13
+ ## Quick Start
14
+
15
+ ```svelte
16
+ <script lang="ts">
17
+ import {
18
+ Calendar, Planner, Agenda,
19
+ createMemoryAdapter, neutral,
20
+ } from '@nomideusz/svelte-calendar';
21
+ import type { CalendarView, TimelineEvent } from '@nomideusz/svelte-calendar';
22
+
23
+ const events: TimelineEvent[] = [
24
+ { id: '1', title: 'Yoga Flow', start: new Date('2025-03-01T09:00'), end: new Date('2025-03-01T10:00'), color: '#818cf8' },
25
+ { id: '2', title: 'Meditation', start: new Date('2025-03-01T12:00'), end: new Date('2025-03-01T12:45'), color: '#34d399' },
26
+ ];
27
+
28
+ const adapter = createMemoryAdapter(events);
29
+
30
+ const views: CalendarView[] = [
31
+ { id: 'day-planner', label: 'Planner', granularity: 'day', component: Planner, props: { mode: 'day' } },
32
+ { id: 'week-planner', label: 'Planner', granularity: 'week', component: Planner, props: { mode: 'week' } },
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' } },
35
+ ];
36
+ </script>
37
+
38
+ <Calendar
39
+ {views}
40
+ {adapter}
41
+ defaultView="week-planner"
42
+ theme={neutral}
43
+ height={600}
44
+ oneventclick={(ev) => console.log('clicked', ev.id)}
45
+ oneventcreate={(range) => console.log('create', range)}
46
+ />
47
+ ```
48
+
49
+ ## Calendar Props
50
+
51
+ | Prop | Type | Default | Description |
52
+ |------|------|---------|-------------|
53
+ | `adapter` | `CalendarAdapter` | *required* | Data layer (memory, recurring, REST) |
54
+ | `views` | `CalendarView[]` | `[]` | Registered view components |
55
+ | `defaultView` | `string` | `'week-planner'` | Initial view ID |
56
+ | `theme` | `string` | `''` | CSS theme string (`--dt-*` custom properties) |
57
+ | `height` | `number` | `600` | Total height in pixels |
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 |
71
+
72
+ ## TimelineEvent
73
+
74
+ | Field | Type | Description |
75
+ |-------|------|-------------|
76
+ | `id` | `string` | Unique identifier |
77
+ | `title` | `string` | Event title |
78
+ | `start` / `end` | `Date` | Time range |
79
+ | `color` | `string?` | Accent color |
80
+ | `category` | `string?` | For grouping / colorMap |
81
+ | `subtitle` | `string?` | Secondary text |
82
+ | `tags` | `string[]?` | Small accent-colored pills |
83
+ | `allDay` | `boolean?` | Render as an all-day event |
84
+ | `data` | `Record?` | Arbitrary payload |
85
+
86
+ ## Multi-day & All-day Events
87
+
88
+ Events that span multiple days or are flagged `allDay: true` are rendered in a
89
+ dedicated strip above timed events in every view:
90
+
91
+ ```ts
92
+ const events: TimelineEvent[] = [
93
+ // Explicit all-day flag
94
+ { id: '1', title: 'Conference', start: new Date('2025-03-15'), end: new Date('2025-03-18'), allDay: true },
95
+ // Auto-detected: starts at midnight and spans 24 h
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') },
99
+ ];
100
+ ```
101
+
102
+ Utility helpers are exported for custom views:
103
+
104
+ ```ts
105
+ import { isAllDay, isMultiDay, segmentForDay } from '@nomideusz/svelte-calendar';
106
+
107
+ segmentForDay(event, dayTimestamp);
108
+ // → { ev, start, end, isStart, isEnd, dayIndex, totalDays, allDay } | null
109
+ ```
110
+
111
+ ## Views
112
+
113
+ | Concept | Day | Week | Description |
114
+ |---------|-----|------|-------------|
115
+ | **Planner** | `mode="day"` | `mode="week"` | Time blocks on a scrollable grid |
116
+ | **Agenda** | `mode="day"` | `mode="week"` | List view — Done, Now, Next (day) or grouped-by-day (week) |
117
+
118
+ ## Recurring Schedules
119
+
120
+ ```svelte
121
+ <script lang="ts">
122
+ import { Calendar, Planner, createRecurringAdapter, neutral } from '@nomideusz/svelte-calendar';
123
+ import type { CalendarView, RecurringEvent } from '@nomideusz/svelte-calendar';
124
+
125
+ const schedule: RecurringEvent[] = [
126
+ // Every Monday
127
+ { id: '1', title: 'Morning Yoga', dayOfWeek: 1, startTime: '07:00', endTime: '08:30', color: '#818cf8' },
128
+ // Mon/Wed/Fri during semester
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,
142
+ 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
+ ];
149
+ </script>
150
+
151
+ <Calendar {views} {adapter} defaultView="week-planner" theme={neutral} readOnly />
152
+ ```
153
+
154
+ ### RecurringEvent
155
+
156
+ | Field | Type | Default | Description |
157
+ |-------|------|---------|-------------|
158
+ | `id` | `string` | *required* | Unique identifier |
159
+ | `title` | `string` | *required* | Event title |
160
+ | `startTime` | `string` | *required* | Start time `"HH:MM"` |
161
+ | `endTime` | `string` | *required* | End time `"HH:MM"` |
162
+ | `frequency` | `'daily' \| 'weekly' \| 'monthly'` | `'weekly'` | Recurrence frequency |
163
+ | `interval` | `number` | `1` | Repeat every N periods (e.g. `2` = biweekly) |
164
+ | `dayOfWeek` | `number \| number[]` | — | ISO weekday 1=Mon…7=Sun. Required for weekly. |
165
+ | `dayOfMonth` | `number` | `1` | Day of month (1–31). For monthly. |
166
+ | `startDate` | `string` | — | First occurrence `"YYYY-MM-DD"` |
167
+ | `until` | `string` | — | Last occurrence `"YYYY-MM-DD"` |
168
+ | `count` | `number` | — | Max occurrences from `startDate` |
169
+ | `color` | `string?` | — | Accent color |
170
+
171
+ ## Color Map & Auto-Coloring
172
+
173
+ ```ts
174
+ // Explicit mapping
175
+ const adapter = createMemoryAdapter(events, { colorMap: { yoga: '#818cf8', wellness: '#34d399' } });
176
+
177
+ // Auto-assign from accent color
178
+ const adapter = createMemoryAdapter(events, { autoColor: '#6366f1' });
179
+
180
+ // Fixed vivid palette
181
+ const adapter = createMemoryAdapter(events, { autoColor: true });
182
+ ```
183
+
184
+ Events with explicit `color` always take priority.
185
+
186
+ ## Localization (i18n)
187
+
188
+ All UI strings are configurable via the labels system:
189
+
190
+ ```ts
191
+ import { setLabels, resetLabels, getLabels } from '@nomideusz/svelte-calendar';
192
+ import type { CalendarLabels } from '@nomideusz/svelte-calendar';
193
+
194
+ // Override any subset unset keys stay English
195
+ 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',
206
+ noEvents: 'Keine Termine',
207
+ goToToday: 'Heute anzeigen',
208
+ nMore: (n) => `+${n} weitere`,
209
+ nEvents: (n) => `${n} Termin${n === 1 ? '' : 'e'}`,
210
+ });
211
+
212
+ // Reset to English
213
+ resetLabels();
214
+
215
+ // Read current labels
216
+ const labels = getLabels();
217
+ ```
218
+
219
+ <details>
220
+ <summary><strong>Full label keys</strong></summary>
221
+
222
+ | Key | Default | Description |
223
+ |-----|---------|-------------|
224
+ | `today` | `'Today'` | Relative day label / nav button |
225
+ | `yesterday` | `'Yesterday'` | Relative day label |
226
+ | `tomorrow` | `'Tomorrow'` | Relative day label |
227
+ | `day` | `'Day'` | Granularity pill |
228
+ | `week` | `'Week'` | Granularity pill |
229
+ | `planner` | `'Planner'` | View label |
230
+ | `agenda` | `'Agenda'` | View label |
231
+ | `now` | `'now'` | Live indicator badge |
232
+ | `free` | `'free'` | Empty slot hint |
233
+ | `allDay` | `'All day'` | All-day event label |
234
+ | `done` | `'Done'` | Past section header |
235
+ | `upNext` | `'Up next'` | Upcoming section header |
236
+ | `until` | `'until'` | Time-until prefix |
237
+ | `noEvents` | `'No events'` | Empty day |
238
+ | `nothingScheduled` | `'Nothing scheduled'` | Empty state |
239
+ | `allDoneForToday` | `'All done for today'` | Completed state |
240
+ | `goToToday` | `'Go to today'` | Nav button aria |
241
+ | `previousDay` / `nextDay` | `'Previous day'` / `'Next day'` | Nav aria |
242
+ | `previousWeek` / `nextWeek` | `'Previous week'` / `'Next week'` | Nav aria |
243
+ | `calendar` | `'Calendar'` | Root region aria |
244
+ | `nMore(n)` | `` `+${n} more` `` | Overflow count |
245
+ | `nEvents(n)` | `` `${n} event(s)` `` | Event count aria |
246
+ | `nCompleted(n)` | `` `${n} completed` `` | Completed count |
247
+ | `dayNOfTotal(i, t)` | `` `day ${i} of ${t}` `` | Multi-day segment |
248
+ | `percentComplete(p)` | `` `${p}% complete` `` | Progress aria |
249
+
250
+ </details>
251
+
252
+ ## Themes
253
+
254
+ | Preset | Description |
255
+ |--------|-------------|
256
+ | `midnight` | Dark navy/slate, red accent |
257
+ | `neutral` | White/gray, blue accent, inherits fonts **recommended** |
258
+
259
+ ```svelte
260
+ <Calendar {views} {adapter} theme={neutral} />
261
+ ```
262
+
263
+ Customize any token:
264
+
265
+ ```ts
266
+ const myTheme = `
267
+ ${neutral}
268
+ --dt-accent: #e11d48;
269
+ --dt-bg: var(--my-app-surface);
270
+ --dt-text: var(--my-app-text);
271
+ `;
272
+ ```
273
+ ```
274
+
275
+ <details>
276
+ <summary><strong>Full token reference</strong></summary>
277
+
278
+ | Token | Purpose |
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 |
295
+
296
+ </details>
297
+
298
+ ## Embeddable Widget
299
+
300
+ Drop into any HTML page - no build tools needed:
301
+
302
+ ```html
303
+ <script src="https://cdn.jsdelivr.net/npm/@nomideusz/svelte-calendar/widget/widget.js"></script>
304
+
305
+ <day-calendar
306
+ api="https://myschool.com/api/events"
307
+ theme="neutral"
308
+ height="600"
309
+ ></day-calendar>
310
+ ```
311
+
312
+ ## Standalone Views
313
+
314
+ ```svelte
315
+ <Planner style={midnight} mode="day" events={events} height={600} />
316
+ <Agenda mode="day" events={events} height={520} />
317
+ ```
318
+
319
+ ## Development
320
+
321
+ ```bash
322
+ pnpm install
323
+ pnpm dev # SvelteKit dev server
324
+ pnpm check # Type check
325
+ pnpm run package # Build library
326
+ pnpm run build:widget # Build widget.js
327
+ ```
328
+
329
+ ## License
330
+
331
+ MIT