@nomideusz/svelte-calendar 0.2.2 → 0.3.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.
Files changed (40) hide show
  1. package/README.md +207 -6
  2. package/dist/adapters/index.d.ts +3 -0
  3. package/dist/adapters/index.js +1 -0
  4. package/dist/adapters/memory.d.ts +7 -1
  5. package/dist/adapters/memory.js +35 -4
  6. package/dist/adapters/recurring.d.ts +39 -0
  7. package/dist/adapters/recurring.js +117 -0
  8. package/dist/calendar/Calendar.svelte +20 -5
  9. package/dist/calendar/Calendar.svelte.d.ts +4 -0
  10. package/dist/core/types.d.ts +4 -0
  11. package/dist/index.d.ts +4 -4
  12. package/dist/index.js +3 -3
  13. package/dist/primitives/EmptySlot.svelte +2 -2
  14. package/dist/primitives/EventBlock.svelte +75 -14
  15. package/dist/theme/index.d.ts +1 -1
  16. package/dist/theme/index.js +1 -1
  17. package/dist/theme/presets.d.ts +10 -15
  18. package/dist/theme/presets.js +5 -11
  19. package/dist/views/agenda/Agenda.svelte +3 -3
  20. package/dist/views/day/DayGrid.svelte +5 -0
  21. package/dist/views/day/DayGrid.svelte.d.ts +5 -0
  22. package/dist/views/index.d.ts +1 -0
  23. package/dist/views/index.js +1 -0
  24. package/dist/views/schedule/WeekSchedule.svelte +133 -0
  25. package/dist/views/schedule/WeekSchedule.svelte.d.ts +38 -0
  26. package/dist/views/schedule/index.d.ts +1 -0
  27. package/dist/views/schedule/index.js +2 -0
  28. package/dist/views/settings/Settings.svelte +6 -2
  29. package/dist/views/week/WeekGrid.svelte +7 -2
  30. package/dist/views/week/WeekGrid.svelte.d.ts +3 -0
  31. package/dist/views/week/WeekHeatmap.svelte +12 -5
  32. package/dist/views/week/WeekHeatmap.svelte.d.ts +5 -1
  33. package/dist/widget/CalendarWidget.svelte +138 -0
  34. package/dist/widget/CalendarWidget.svelte.d.ts +23 -0
  35. package/dist/widget/index.d.ts +1 -0
  36. package/dist/widget/index.js +1 -0
  37. package/dist/widget/widget.d.ts +7 -0
  38. package/dist/widget/widget.js +9 -0
  39. package/package.json +5 -2
  40. package/widget/widget.js +260 -0
package/README.md CHANGED
@@ -13,6 +13,7 @@ Switching between Day and Week preserves the active concept.
13
13
  | **Timeline** | DayTimeline | — | Horizontal day timeline. Day-only. |
14
14
  | **Agenda** | Agenda `mode="day"` | Agenda `mode="week"` | List / feed — Done, Now, Next (day) or grouped-by-day scroll (week). |
15
15
  | **Heatmap** | — | WeekHeatmap | Density view — 24 cells per day showing busy/free intensity. Week-only. |
16
+ | **Schedule** | — | WeekSchedule | Zero-config weekly schedule display. Single import convenience wrapper. |
16
17
 
17
18
  ## Installation
18
19
 
@@ -39,8 +40,16 @@ pnpm add @nomideusz/svelte-calendar
39
40
  import type { CalendarView, TimelineEvent } from '@nomideusz/svelte-calendar';
40
41
 
41
42
  const events: TimelineEvent[] = [
42
- { id: '1', title: 'Yoga Flow', start: new Date('2025-03-01T09:00'), end: new Date('2025-03-01T10:00'), color: '#818cf8' },
43
- { id: '2', title: 'Meditation', start: new Date('2025-03-01T12:00'), end: new Date('2025-03-01T12:45'), color: '#34d399' },
43
+ {
44
+ id: '1', title: 'Yoga Flow',
45
+ start: new Date('2025-03-01T09:00'), end: new Date('2025-03-01T10:00'),
46
+ color: '#818cf8', subtitle: 'With Anna', tags: ['Beginner'],
47
+ },
48
+ {
49
+ id: '2', title: 'Meditation',
50
+ start: new Date('2025-03-01T12:00'), end: new Date('2025-03-01T12:45'),
51
+ color: '#34d399',
52
+ },
44
53
  ];
45
54
 
46
55
  // Adapters provide the data layer (in-memory, REST, etc.)
@@ -67,6 +76,137 @@ pnpm add @nomideusz/svelte-calendar
67
76
  />
68
77
  ```
69
78
 
79
+ ## Recurring Weekly Schedules
80
+
81
+ Define a weekly schedule once — the adapter auto-projects it onto whatever week the calendar is viewing. No manual date math needed.
82
+
83
+ ```svelte
84
+ <script lang="ts">
85
+ import { Calendar, WeekGrid, createRecurringAdapter, neutral } from '@nomideusz/svelte-calendar';
86
+ import type { CalendarView, RecurringEvent } from '@nomideusz/svelte-calendar';
87
+
88
+ const schedule: RecurringEvent[] = [
89
+ { id: '1', title: 'Morning Yoga', dayOfWeek: 1, startTime: '07:00', endTime: '08:30', color: '#818cf8' },
90
+ { id: '2', title: 'Pilates', dayOfWeek: 3, startTime: '18:00', endTime: '19:00', color: '#f472b6' },
91
+ { id: '3', title: 'Sound Bath', dayOfWeek: 5, startTime: '19:00', endTime: '20:00', color: '#2dd4bf', subtitle: 'Crystal bowls', tags: ['Relaxing'] },
92
+ ];
93
+
94
+ const adapter = createRecurringAdapter(schedule);
95
+ const views: CalendarView[] = [
96
+ { id: 'week-grid', label: 'Grid', granularity: 'week', component: WeekGrid },
97
+ ];
98
+ </script>
99
+
100
+ <Calendar {views} {adapter} defaultView="week-grid" theme={neutral} readOnly />
101
+ ```
102
+
103
+ ### RecurringEvent
104
+
105
+ | Field | Type | Description |
106
+ |-------|------|-------------|
107
+ | `id` | `string` | Unique identifier |
108
+ | `title` | `string` | Event title |
109
+ | `dayOfWeek` | `1–7` | ISO weekday (1 = Monday … 7 = Sunday) |
110
+ | `startTime` | `string` | Start time in `"HH:MM"` format |
111
+ | `endTime` | `string` | End time in `"HH:MM"` format |
112
+ | `color` | `string?` | Accent color |
113
+ | `subtitle` | `string?` | Subtitle (rendered below title) |
114
+ | `tags` | `string[]?` | Tag pills |
115
+ | `category` | `string?` | Category for grouping / colorMap |
116
+ | `data` | `Record?` | Arbitrary payload |
117
+
118
+ ## WeekSchedule — Zero-Config Convenience
119
+
120
+ One import, one component. Pre-wires adapter, views, and toolbar internally:
121
+
122
+ ```svelte
123
+ <script>
124
+ import { WeekSchedule } from '@nomideusz/svelte-calendar';
125
+ import { neutral } from '@nomideusz/svelte-calendar';
126
+
127
+ const schedule = [
128
+ { id: '1', title: 'Yoga', dayOfWeek: 1, startTime: '07:00', endTime: '08:30', color: '#818cf8' },
129
+ { id: '2', title: 'Pilates', dayOfWeek: 3, startTime: '18:00', endTime: '19:00', color: '#f472b6' },
130
+ ];
131
+ </script>
132
+
133
+ <WeekSchedule {schedule} theme={neutral} locale="pl-PL" height={560} readOnly />
134
+ ```
135
+
136
+ Works with concrete events too:
137
+
138
+ ```svelte
139
+ <WeekSchedule events={myEvents} theme={neutral} height={560} />
140
+ ```
141
+
142
+ ## Read-Only Mode
143
+
144
+ Pass `readOnly` to disable drag, resize, and click-to-create interactions:
145
+
146
+ ```svelte
147
+ <Calendar {views} {adapter} readOnly />
148
+ ```
149
+
150
+ In read-only mode:
151
+ - Drag handles and resize affordances are disabled
152
+ - Empty-slot creation clicks are suppressed
153
+ - `oneventcreate` and `oneventmove` callbacks are not fired
154
+ - `oneventclick` still works for navigation/display purposes
155
+
156
+ ## Visible Hours
157
+
158
+ Crop the grid to relevant hours — no more scrolling past empty early morning / late night rows:
159
+
160
+ ```svelte
161
+ <!-- Only show 6 AM to 9 PM -->
162
+ <Calendar {views} {adapter} visibleHours={[6, 21]} />
163
+
164
+ <!-- Works on WeekSchedule too -->
165
+ <WeekSchedule {schedule} visibleHours={[7, 20]} />
166
+ ```
167
+
168
+ The `visibleHours` prop is a `[startHour, endHour)` tuple. It applies to the WeekHeatmap grid cells and is passed through to all views.
169
+
170
+ ## Subtitle & Tags on Events
171
+
172
+ `TimelineEvent` now supports `subtitle` and `tags` fields — rendered automatically by `EventBlock`:
173
+
174
+ ```ts
175
+ const events: TimelineEvent[] = [
176
+ {
177
+ id: '1',
178
+ title: 'Power Vinyasa',
179
+ start: new Date('2025-03-01T10:00'),
180
+ end: new Date('2025-03-01T11:15'),
181
+ color: '#f472b6',
182
+ subtitle: 'With Marco', // shown below the title
183
+ tags: ['Advanced', 'Hot'], // rendered as small color pills
184
+ },
185
+ ];
186
+ ```
187
+
188
+ - **subtitle** — displayed as secondary text below the title in card and row variants
189
+ - **tags** — rendered as small accent-colored pills
190
+
191
+ ## Color Map & Auto-Coloring
192
+
193
+ Instead of setting `color` on every event, let the adapter assign colors by category or title:
194
+
195
+ ```ts
196
+ // Explicit mapping
197
+ const adapter = createMemoryAdapter(events, {
198
+ colorMap: {
199
+ yoga: '#818cf8',
200
+ wellness: '#34d399',
201
+ },
202
+ });
203
+
204
+ // Or auto-assign from a built-in 15-color palette
205
+ const adapter = createMemoryAdapter(events, { autoColor: true });
206
+ ```
207
+
208
+ Both `createMemoryAdapter` and `createRecurringAdapter` accept `colorMap` and `autoColor` options. Events with an explicit `color` field always take priority.
209
+
70
210
  ## Settings Panel
71
211
 
72
212
  The `Settings` component provides a theme picker and dynamic fields for controlling view parameters:
@@ -254,10 +394,10 @@ const viewState = createViewState({
254
394
  src/lib/
255
395
  ├── core/ # Clock, time utils, locale, types
256
396
  ├── engine/ # Reactive state: event-store, view-state, selection, drag
257
- ├── adapters/ # Data layer: memory adapter, REST adapter
397
+ ├── adapters/ # Data layer: memory, recurring, REST adapters
258
398
  ├── primitives/ # Low-level UI atoms: NowIndicator, EventBlock, TimeGutter...
259
399
  ├── calendar/ # Calendar shell, Toolbar
260
- ├── views/ # View components (day/, week/, agenda/, settings/)
400
+ ├── views/ # View components (day/, week/, agenda/, schedule/, settings/)
261
401
  └── theme/ # Preset themes and token definitions
262
402
  ```
263
403
 
@@ -281,8 +421,9 @@ import {
281
421
 
282
422
  | Adapter | Use |
283
423
  |---------|-----|
284
- | `createMemoryAdapter(events)` | In-memory — great for demos and prototyping |
285
- | `createRestAdapter(options)` | Fetch from a REST API with configurable endpoints |
424
+ | `createMemoryAdapter(events, options?)` | In-memory — great for demos and prototyping. Supports `colorMap` and `autoColor`. |
425
+ | `createRecurringAdapter(schedule, options?)` | Weekly recurring schedules auto-projects onto viewed weeks. Read-only. |
426
+ | `createRestAdapter(options)` | Fetch from a REST API with configurable endpoints. |
286
427
 
287
428
  ## Standalone Views
288
429
 
@@ -302,6 +443,65 @@ Each view works independently without the Calendar shell:
302
443
  <WeekHeatmap style={parchment} events={events} height={320} />
303
444
  ```
304
445
 
446
+ ## Embeddable Widget
447
+
448
+ Drop a single `<script>` tag into **any** HTML page — no Svelte, no build tools, no npm needed.
449
+
450
+ ### From CDN
451
+
452
+ ```html
453
+ <script src="https://cdn.jsdelivr.net/npm/@nomideusz/svelte-calendar/widget/widget.js"></script>
454
+
455
+ <day-calendar
456
+ api="https://myschool.com/api/events"
457
+ theme="neutral"
458
+ height="600"
459
+ ></day-calendar>
460
+ ```
461
+
462
+ That's it. Two lines.
463
+
464
+ ### With inline events (no API)
465
+
466
+ ```html
467
+ <script src="https://cdn.jsdelivr.net/npm/@nomideusz/svelte-calendar/widget/widget.js"></script>
468
+
469
+ <day-calendar
470
+ theme="midnight"
471
+ height="500"
472
+ events='[
473
+ { "id": "1", "title": "Yoga Flow", "start": "2025-03-01T09:00", "end": "2025-03-01T10:00", "color": "#818cf8" },
474
+ { "id": "2", "title": "Meditation", "start": "2025-03-01T12:00", "end": "2025-03-01T12:45", "color": "#34d399" }
475
+ ]'
476
+ ></day-calendar>
477
+ ```
478
+
479
+ ### Widget attributes
480
+
481
+ | Attribute | Default | Description |
482
+ |-----------|---------|-------------|
483
+ | `api` | — | REST API base URL — fetches from `{api}/events?start=...&end=...` |
484
+ | `events` | — | JSON string of events (alternative to `api`) |
485
+ | `theme` | `neutral` | Preset: `midnight`, `parchment`, `indigo`, `neutral`, `bare` |
486
+ | `view` | `week-grid` | Default view: `day-grid`, `week-grid`, `day-timeline`, `day-agenda`, `week-agenda`, `week-heatmap` |
487
+ | `height` | `600` | Height in pixels |
488
+ | `locale` | — | BCP 47 locale (`en-US`, `pl-PL`, `ar-SA`, etc.) |
489
+ | `dir` | — | Text direction: `ltr`, `rtl`, `auto` |
490
+ | `mondaystart` | `true` | Start week on Monday (`true`/`false`) |
491
+ | `headers` | — | JSON string of HTTP headers for the REST adapter |
492
+
493
+ ### REST API contract
494
+
495
+ When using the `api` attribute, the widget expects your endpoint to accept:
496
+
497
+ ```
498
+ GET {api}/events?start={ISO}&end={ISO}
499
+ ```
500
+
501
+ And return either:
502
+ - `[{ id, title, start, end, color? }, ...]`
503
+ - `{ events: [{ id, title, start, end, color? }, ...] }`
504
+
305
505
  ## Development
306
506
 
307
507
  ```bash
@@ -309,6 +509,7 @@ pnpm install
309
509
  pnpm dev # SvelteKit dev server (demo app)
310
510
  pnpm check # Type check
311
511
  pnpm run package # Build the library into dist/
512
+ pnpm run build:widget # Build standalone widget.js
312
513
  ```
313
514
 
314
515
  ## License
@@ -1,4 +1,7 @@
1
1
  export type { CalendarAdapter, DateRange } from './types.js';
2
2
  export { createMemoryAdapter } from './memory.js';
3
+ export type { MemoryAdapterOptions } from './memory.js';
3
4
  export { createRestAdapter } from './rest.js';
4
5
  export type { RestAdapterOptions } from './rest.js';
6
+ export { createRecurringAdapter } from './recurring.js';
7
+ export type { RecurringEvent, RecurringAdapterOptions } from './recurring.js';
@@ -1,2 +1,3 @@
1
1
  export { createMemoryAdapter } from './memory.js';
2
2
  export { createRestAdapter } from './rest.js';
3
+ export { createRecurringAdapter } from './recurring.js';
@@ -11,4 +11,10 @@
11
11
  */
12
12
  import type { TimelineEvent } from '../core/types.js';
13
13
  import type { CalendarAdapter } from './types.js';
14
- export declare function createMemoryAdapter(initial?: TimelineEvent[]): CalendarAdapter;
14
+ export interface MemoryAdapterOptions {
15
+ /** Map of category/title to color */
16
+ colorMap?: Record<string, string>;
17
+ /** Auto-assign colors to events by category or title */
18
+ autoColor?: boolean;
19
+ }
20
+ export declare function createMemoryAdapter(initial?: TimelineEvent[], options?: MemoryAdapterOptions): CalendarAdapter;
@@ -2,26 +2,57 @@ let counter = 0;
2
2
  function uid() {
3
3
  return `mem-${Date.now()}-${++counter}`;
4
4
  }
5
- export function createMemoryAdapter(initial = []) {
5
+ /** Default palette for auto-coloring */
6
+ const AUTO_COLORS = [
7
+ '#ef4444', '#f97316', '#eab308', '#22c55e', '#14b8a6',
8
+ '#3b82f6', '#6366f1', '#a855f7', '#ec4899', '#f43f5e',
9
+ '#06b6d4', '#84cc16', '#d946ef', '#0ea5e9', '#10b981',
10
+ ];
11
+ export function createMemoryAdapter(initial = [], options = {}) {
12
+ const { colorMap, autoColor } = options;
6
13
  const events = [...initial];
14
+ // Build auto-color assignments
15
+ const colorAssignments = new Map();
16
+ let colorIndex = 0;
17
+ function resolveColor(ev) {
18
+ if (ev.color)
19
+ return ev.color;
20
+ if (!colorMap && !autoColor)
21
+ return undefined;
22
+ const key = ev.category ?? ev.title;
23
+ if (colorMap?.[key])
24
+ return colorMap[key];
25
+ if (autoColor) {
26
+ if (!colorAssignments.has(key)) {
27
+ colorAssignments.set(key, AUTO_COLORS[colorIndex % AUTO_COLORS.length]);
28
+ colorIndex++;
29
+ }
30
+ return colorAssignments.get(key);
31
+ }
32
+ return undefined;
33
+ }
34
+ function withColor(ev) {
35
+ const color = resolveColor(ev);
36
+ return color ? { ...ev, color } : ev;
37
+ }
7
38
  function overlaps(ev, range) {
8
39
  return ev.start < range.end && ev.end > range.start;
9
40
  }
10
41
  return {
11
42
  async fetchEvents(range) {
12
- return events.filter((ev) => overlaps(ev, range));
43
+ return events.filter((ev) => overlaps(ev, range)).map(withColor);
13
44
  },
14
45
  async createEvent(data) {
15
46
  const ev = { ...data, id: uid() };
16
47
  events.push(ev);
17
- return ev;
48
+ return withColor(ev);
18
49
  },
19
50
  async updateEvent(id, patch) {
20
51
  const idx = events.findIndex((e) => e.id === id);
21
52
  if (idx < 0)
22
53
  throw new Error(`Event not found: ${id}`);
23
54
  events[idx] = { ...events[idx], ...patch, id };
24
- return events[idx];
55
+ return withColor(events[idx]);
25
56
  },
26
57
  async deleteEvent(id) {
27
58
  const idx = events.findIndex((e) => e.id === id);
@@ -0,0 +1,39 @@
1
+ import type { CalendarAdapter } from './types.js';
2
+ /**
3
+ * A weekly recurring event definition.
4
+ */
5
+ export interface RecurringEvent {
6
+ id: string;
7
+ title: string;
8
+ /** ISO weekday: 1 = Monday … 7 = Sunday */
9
+ dayOfWeek: number;
10
+ /** Start time in "HH:MM" 24-hour format */
11
+ startTime: string;
12
+ /** End time in "HH:MM" 24-hour format */
13
+ endTime: string;
14
+ /** Accent color */
15
+ color?: string;
16
+ /** Optional subtitle displayed below the title */
17
+ subtitle?: string;
18
+ /** Optional tags displayed as small pills */
19
+ tags?: string[];
20
+ /** Category for grouping */
21
+ category?: string;
22
+ /** Arbitrary payload */
23
+ data?: Record<string, unknown>;
24
+ }
25
+ export interface RecurringAdapterOptions {
26
+ /** Start weeks on Monday (default: true) */
27
+ mondayStart?: boolean;
28
+ /** Map of category/title to color */
29
+ colorMap?: Record<string, string>;
30
+ /** Auto-assign colors to events by category or title */
31
+ autoColor?: boolean;
32
+ }
33
+ /**
34
+ * Create a CalendarAdapter that projects recurring weekly events
35
+ * onto concrete dates for whatever range the calendar requests.
36
+ *
37
+ * Read-only by default — create/update/delete throw unless custom handlers are provided.
38
+ */
39
+ export declare function createRecurringAdapter(schedule: RecurringEvent[], options?: RecurringAdapterOptions): CalendarAdapter;
@@ -0,0 +1,117 @@
1
+ import { startOfWeek } from '../core/time.js';
2
+ import { DAY_MS } from '../core/time.js';
3
+ /** Parse "HH:MM" into [hours, minutes] */
4
+ function parseTime(time) {
5
+ const [h, m] = time.split(':').map(Number);
6
+ return [h, m ?? 0];
7
+ }
8
+ /**
9
+ * Convert ISO weekday (1=Mon…7=Sun) to JS Date weekday offset from Monday.
10
+ * Monday = 0, Tuesday = 1, … Sunday = 6
11
+ */
12
+ function isoWeekdayToOffset(dayOfWeek) {
13
+ return dayOfWeek - 1; // 1→0, 2→1, …, 7→6
14
+ }
15
+ /**
16
+ * Project a recurring event onto a specific week, returning a concrete TimelineEvent.
17
+ */
18
+ function projectToWeek(rec, weekStartMs, weekIndex) {
19
+ const dayOffset = isoWeekdayToOffset(rec.dayOfWeek);
20
+ const dayMs = weekStartMs + dayOffset * DAY_MS;
21
+ const dayDate = new Date(dayMs);
22
+ const [sh, sm] = parseTime(rec.startTime);
23
+ const [eh, em] = parseTime(rec.endTime);
24
+ const start = new Date(dayDate.getFullYear(), dayDate.getMonth(), dayDate.getDate(), sh, sm);
25
+ const end = new Date(dayDate.getFullYear(), dayDate.getMonth(), dayDate.getDate(), eh, em);
26
+ return {
27
+ id: `${rec.id}--w${weekIndex}--d${rec.dayOfWeek}`,
28
+ title: rec.title,
29
+ start,
30
+ end,
31
+ color: rec.color,
32
+ category: rec.category,
33
+ data: {
34
+ ...rec.data,
35
+ recurringId: rec.id,
36
+ ...(rec.subtitle ? { subtitle: rec.subtitle } : {}),
37
+ ...(rec.tags ? { tags: rec.tags } : {}),
38
+ },
39
+ };
40
+ }
41
+ /**
42
+ * Find all weeks that overlap a given date range.
43
+ * Returns an array of { weekStartMs, weekIndex } objects.
44
+ */
45
+ function getOverlappingWeeks(range, mondayStart) {
46
+ const weeks = [];
47
+ let cursor = startOfWeek(range.start.getTime(), mondayStart);
48
+ let index = 0;
49
+ while (cursor < range.end.getTime()) {
50
+ weeks.push({ weekStartMs: cursor, weekIndex: index });
51
+ cursor += 7 * DAY_MS;
52
+ index++;
53
+ }
54
+ return weeks;
55
+ }
56
+ /** Default palette for auto-coloring */
57
+ const AUTO_COLORS = [
58
+ '#ef4444', '#f97316', '#eab308', '#22c55e', '#14b8a6',
59
+ '#3b82f6', '#6366f1', '#a855f7', '#ec4899', '#f43f5e',
60
+ '#06b6d4', '#84cc16', '#d946ef', '#0ea5e9', '#10b981',
61
+ ];
62
+ /**
63
+ * Create a CalendarAdapter that projects recurring weekly events
64
+ * onto concrete dates for whatever range the calendar requests.
65
+ *
66
+ * Read-only by default — create/update/delete throw unless custom handlers are provided.
67
+ */
68
+ export function createRecurringAdapter(schedule, options = {}) {
69
+ const { mondayStart = true, colorMap, autoColor } = options;
70
+ // Build auto-color assignments
71
+ const colorAssignments = new Map();
72
+ if (autoColor || colorMap) {
73
+ let colorIndex = 0;
74
+ for (const rec of schedule) {
75
+ const key = rec.category ?? rec.title;
76
+ if (colorMap?.[key]) {
77
+ colorAssignments.set(key, colorMap[key]);
78
+ }
79
+ else if (autoColor && !colorAssignments.has(key)) {
80
+ colorAssignments.set(key, AUTO_COLORS[colorIndex % AUTO_COLORS.length]);
81
+ colorIndex++;
82
+ }
83
+ }
84
+ }
85
+ function resolveColor(rec) {
86
+ if (rec.color)
87
+ return rec.color;
88
+ const key = rec.category ?? rec.title;
89
+ return colorAssignments.get(key);
90
+ }
91
+ return {
92
+ async fetchEvents(range) {
93
+ const weeks = getOverlappingWeeks(range, mondayStart);
94
+ const events = [];
95
+ for (const { weekStartMs, weekIndex } of weeks) {
96
+ for (const rec of schedule) {
97
+ const coloredRec = { ...rec, color: resolveColor(rec) };
98
+ const ev = projectToWeek(coloredRec, weekStartMs, weekIndex);
99
+ // Only include if the event overlaps the requested range
100
+ if (ev.start < range.end && ev.end > range.start) {
101
+ events.push(ev);
102
+ }
103
+ }
104
+ }
105
+ return events;
106
+ },
107
+ async createEvent() {
108
+ throw new Error('createRecurringAdapter is read-only. Use a memory or REST adapter for mutations.');
109
+ },
110
+ async updateEvent() {
111
+ throw new Error('createRecurringAdapter is read-only. Use a memory or REST adapter for mutations.');
112
+ },
113
+ async deleteEvent() {
114
+ throw new Error('createRecurringAdapter is read-only. Use a memory or REST adapter for mutations.');
115
+ },
116
+ };
117
+ }
@@ -63,6 +63,10 @@
63
63
  dir?: 'ltr' | 'rtl' | 'auto';
64
64
  /** BCP 47 locale tag (e.g. 'en-US', 'ar-SA') — sets lang and locale for formatting */
65
65
  locale?: string;
66
+ /** Read-only mode: disables drag, resize, empty-slot creation */
67
+ readOnly?: boolean;
68
+ /** Visible hour range: [startHour, endHour). Crops the grid to these hours. */
69
+ visibleHours?: [number, number];
66
70
 
67
71
  // ── Callbacks ──
68
72
  oneventclick?: (event: TimelineEvent) => void;
@@ -81,11 +85,17 @@
81
85
  links = [],
82
86
  dir,
83
87
  locale,
88
+ readOnly = false,
89
+ visibleHours,
84
90
  oneventclick,
85
91
  oneventcreate,
86
92
  oneventmove,
87
93
  }: Props = $props();
88
94
 
95
+ // In readOnly mode, suppress mutation callbacks
96
+ const effectiveCreate = $derived(readOnly ? undefined : oneventcreate);
97
+ const effectiveMove = $derived(readOnly ? undefined : oneventmove);
98
+
89
99
  import { setDefaultLocale } from '../core/locale.js';
90
100
 
91
101
  // ── Set locale when provided ──
@@ -105,6 +115,7 @@
105
115
  // ── Drag commit handler ──
106
116
  // Views call this on pointer-up to process drag results.
107
117
  function commitDrag(): void {
118
+ if (readOnly) { drag.cancel(); return; }
108
119
  const mode = drag.mode;
109
120
  const payload = drag.commit();
110
121
  if (!payload) return;
@@ -112,9 +123,9 @@
112
123
  if ((mode === 'move' || mode === 'resize-start' || mode === 'resize-end') && payload.eventId) {
113
124
  store.move(payload.eventId, payload.start, payload.end);
114
125
  const ev = store.byId(payload.eventId);
115
- if (ev) oneventmove?.(ev, payload.start, payload.end);
126
+ if (ev) effectiveMove?.(ev, payload.start, payload.end);
116
127
  } else if (mode === 'create') {
117
- oneventcreate?.({ start: payload.start, end: payload.end });
128
+ effectiveCreate?.({ start: payload.start, end: payload.end });
118
129
  }
119
130
  }
120
131
 
@@ -130,9 +141,11 @@
130
141
  // always see the latest callback references.
131
142
  setContext('calendar:callbacks', {
132
143
  get oneventclick() { return oneventclick; },
133
- get oneventcreate() { return oneventcreate; },
134
- get oneventmove() { return oneventmove; },
144
+ get oneventcreate() { return effectiveCreate; },
145
+ get oneventmove() { return effectiveMove; },
135
146
  });
147
+ setContext('calendar:readOnly', { get current() { return readOnly; } });
148
+ setContext('calendar:visibleHours', { get current() { return visibleHours; } });
136
149
 
137
150
  // ── Load events when range changes ──
138
151
  $effect(() => {
@@ -170,7 +183,9 @@
170
183
  mondayStart={viewState.mondayStart}
171
184
  focusDate={viewState.focusDate}
172
185
  oneventclick={oneventclick}
173
- oneventcreate={oneventcreate}
186
+ oneventcreate={effectiveCreate}
187
+ readOnly={readOnly}
188
+ visibleHours={visibleHours}
174
189
  selectedEventId={selection.selectedId}
175
190
  {...activeView.props ?? {}}
176
191
  />
@@ -37,6 +37,10 @@ interface Props {
37
37
  dir?: 'ltr' | 'rtl' | 'auto';
38
38
  /** BCP 47 locale tag (e.g. 'en-US', 'ar-SA') — sets lang and locale for formatting */
39
39
  locale?: string;
40
+ /** Read-only mode: disables drag, resize, empty-slot creation */
41
+ readOnly?: boolean;
42
+ /** Visible hour range: [startHour, endHour). Crops the grid to these hours. */
43
+ visibleHours?: [number, number];
40
44
  oneventclick?: (event: TimelineEvent) => void;
41
45
  oneventcreate?: (range: {
42
46
  start: Date;
@@ -19,6 +19,10 @@ export interface TimelineEvent {
19
19
  recurrence?: string;
20
20
  /** Whether this event can be moved / resized by the user */
21
21
  editable?: boolean;
22
+ /** Subtitle displayed below the title (e.g. instructor name, level) */
23
+ subtitle?: string;
24
+ /** Tags displayed as small pills (e.g. ["Beginner", "Yoga"]) */
25
+ tags?: string[];
22
26
  /** Arbitrary payload from the source app (bookings, attendees, etc.) */
23
27
  data?: Record<string, unknown>;
24
28
  }
package/dist/index.d.ts CHANGED
@@ -1,13 +1,13 @@
1
- export { DayGrid, DayTimeline, WeekGrid, WeekHeatmap, Agenda, Settings, } from './views/index.js';
1
+ export { DayGrid, DayTimeline, WeekGrid, WeekHeatmap, Agenda, Settings, WeekSchedule, } from './views/index.js';
2
2
  export type { SettingsField } from './views/index.js';
3
3
  export { NowIndicator, EventBlock, TimeGutter, DayHeader, EmptySlot, } from './primitives/index.js';
4
4
  export { Calendar, Toolbar } from './calendar/index.js';
5
5
  export type { CalendarView } from './calendar/index.js';
6
6
  export { createEventStore, createViewState, createSelection, createDragState, } from './engine/index.js';
7
7
  export type { EventStore, ViewState, ViewStateOptions, CalendarViewId, BuiltInViewId, ViewGranularity, ViewDateRange, Selection, DragState, DragMode, DragPayload, } from './engine/index.js';
8
- export { createMemoryAdapter, createRestAdapter } from './adapters/index.js';
9
- export type { CalendarAdapter, DateRange, RestAdapterOptions } from './adapters/index.js';
8
+ export { createMemoryAdapter, createRestAdapter, createRecurringAdapter } from './adapters/index.js';
9
+ export type { CalendarAdapter, DateRange, RestAdapterOptions, RecurringEvent, RecurringAdapterOptions, MemoryAdapterOptions, } from './adapters/index.js';
10
10
  export { createClock, DAY_MS, HOUR_MS, HOURS, sod, startOfWeek, addDaysMs, diffDays, pad, fractionalHour, fmtHM, fmtS, dayNum, dayOfWeek, fmtH, weekdayShort, weekdayLong, monthShort, monthLong, dateShort, dateWithWeekday, fmtDay, fmtWeekRange, setDefaultLocale, getDefaultLocale, is24HourLocale, timeToX, toZonedTime, fromZonedTime, nowInZone, formatInTimeZone, } from './core/index.js';
11
11
  export type { Clock, TimelineEvent, WeekTimelineProps, DayTimelineProps, } from './core/index.js';
12
- export { midnight, parchment, indigo, neutral, bare, presets, stageBg } from './theme/index.js';
12
+ export { midnight, parchment, indigo, neutral, bare, presets } from './theme/index.js';
13
13
  export type { PresetName } from './theme/index.js';
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // ─── Views ──────────────────────────────────────────────
2
- export { DayGrid, DayTimeline, WeekGrid, WeekHeatmap, Agenda, Settings, } from './views/index.js';
2
+ export { DayGrid, DayTimeline, WeekGrid, WeekHeatmap, Agenda, Settings, WeekSchedule, } from './views/index.js';
3
3
  // ─── Primitives ─────────────────────────────────────────
4
4
  export { NowIndicator, EventBlock, TimeGutter, DayHeader, EmptySlot, } from './primitives/index.js';
5
5
  // ─── Calendar shell ─────────────────────────────────────
@@ -7,8 +7,8 @@ export { Calendar, Toolbar } from './calendar/index.js';
7
7
  // ─── Engine (reactive state) ────────────────────────────
8
8
  export { createEventStore, createViewState, createSelection, createDragState, } from './engine/index.js';
9
9
  // ─── Adapters ───────────────────────────────────────────
10
- export { createMemoryAdapter, createRestAdapter } from './adapters/index.js';
10
+ export { createMemoryAdapter, createRestAdapter, createRecurringAdapter } from './adapters/index.js';
11
11
  // ─── Core: clock, time, locale, types ───────────────────
12
12
  export { createClock, DAY_MS, HOUR_MS, HOURS, sod, startOfWeek, addDaysMs, diffDays, pad, fractionalHour, fmtHM, fmtS, dayNum, dayOfWeek, fmtH, weekdayShort, weekdayLong, monthShort, monthLong, dateShort, dateWithWeekday, fmtDay, fmtWeekRange, setDefaultLocale, getDefaultLocale, is24HourLocale, timeToX, toZonedTime, fromZonedTime, nowInZone, formatInTimeZone, } from './core/index.js';
13
13
  // ─── Themes ─────────────────────────────────────────────
14
- export { midnight, parchment, indigo, neutral, bare, presets, stageBg } from './theme/index.js';
14
+ export { midnight, parchment, indigo, neutral, bare, presets } from './theme/index.js';
@@ -32,7 +32,7 @@
32
32
  return m === 0 ? `${h12}${suffix}` : `${h12}:${String(m).padStart(2, '0')}${suffix}`;
33
33
  }
34
34
 
35
- const dur = $derived(() => {
35
+ const dur = $derived.by(() => {
36
36
  const mins = Math.round((end.getTime() - start.getTime()) / 60_000);
37
37
  if (mins < 60) return `${mins}m free`;
38
38
  const h = Math.floor(mins / 60);
@@ -54,7 +54,7 @@
54
54
  class:es-h={orientation === 'horizontal'}
55
55
  role="button"
56
56
  tabindex="0"
57
- aria-label="Create event, {fmtTime(start)} to {fmtTime(end)}, {dur()}"
57
+ aria-label="Create event, {fmtTime(start)} to {fmtTime(end)}, {dur}"
58
58
  onclick={() => onclick?.({ start, end })}
59
59
  onkeydown={handleKeydown}
60
60
  >