@nomideusz/svelte-calendar 0.2.3 → 0.3.1

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 (44) hide show
  1. package/README.md +234 -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 +12 -1
  5. package/dist/adapters/memory.js +38 -4
  6. package/dist/adapters/recurring.d.ts +44 -0
  7. package/dist/adapters/recurring.js +120 -0
  8. package/dist/calendar/Calendar.svelte +20 -5
  9. package/dist/calendar/Calendar.svelte.d.ts +4 -0
  10. package/dist/core/index.d.ts +1 -0
  11. package/dist/core/index.js +2 -0
  12. package/dist/core/palette.d.ts +23 -0
  13. package/dist/core/palette.js +109 -0
  14. package/dist/core/types.d.ts +4 -0
  15. package/dist/index.d.ts +5 -5
  16. package/dist/index.js +4 -4
  17. package/dist/primitives/EventBlock.svelte +42 -0
  18. package/dist/theme/index.d.ts +1 -1
  19. package/dist/theme/index.js +1 -1
  20. package/dist/theme/presets.d.ts +10 -15
  21. package/dist/theme/presets.js +5 -11
  22. package/dist/views/agenda/Agenda.svelte +110 -0
  23. package/dist/views/day/DayGrid.svelte +38 -0
  24. package/dist/views/day/DayGrid.svelte.d.ts +5 -0
  25. package/dist/views/day/DayTimeline.svelte +27 -0
  26. package/dist/views/index.d.ts +1 -0
  27. package/dist/views/index.js +1 -0
  28. package/dist/views/schedule/WeekSchedule.svelte +133 -0
  29. package/dist/views/schedule/WeekSchedule.svelte.d.ts +38 -0
  30. package/dist/views/schedule/index.d.ts +1 -0
  31. package/dist/views/schedule/index.js +2 -0
  32. package/dist/views/settings/Settings.svelte +1 -1
  33. package/dist/views/week/WeekGrid.svelte +42 -5
  34. package/dist/views/week/WeekGrid.svelte.d.ts +3 -0
  35. package/dist/views/week/WeekHeatmap.svelte +12 -5
  36. package/dist/views/week/WeekHeatmap.svelte.d.ts +5 -1
  37. package/dist/widget/CalendarWidget.svelte +138 -0
  38. package/dist/widget/CalendarWidget.svelte.d.ts +23 -0
  39. package/dist/widget/index.d.ts +1 -0
  40. package/dist/widget/index.js +1 -0
  41. package/dist/widget/widget.d.ts +7 -0
  42. package/dist/widget/widget.js +9 -0
  43. package/package.json +5 -2
  44. 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,164 @@ 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` supports `subtitle` and `tags` fields — rendered automatically in **all views**:
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** — secondary text below the title (all views: WeekGrid, DayGrid, DayTimeline, Agenda, EventBlock)
189
+ - **tags** — accent-colored pills after the title (all views)
190
+ - In space-constrained views (DayGrid, DayTimeline), subtitle/tags appear only when the event block is tall/wide enough
191
+
192
+ ## Color Map & Auto-Coloring
193
+
194
+ Instead of setting `color` on every event, let the adapter assign colors by category or title:
195
+
196
+ ```ts
197
+ // Explicit mapping
198
+ const adapter = createMemoryAdapter(events, {
199
+ colorMap: {
200
+ yoga: '#818cf8',
201
+ wellness: '#34d399',
202
+ },
203
+ });
204
+
205
+ // Auto-assign from a built-in 15-color vivid palette
206
+ const adapter = createMemoryAdapter(events, { autoColor: true });
207
+ ```
208
+
209
+ ### Theme-Aware Auto-Coloring
210
+
211
+ 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:
212
+
213
+ ```ts
214
+ // Harmonious palette seeded from indigo accent
215
+ const adapter = createMemoryAdapter(events, { autoColor: '#6366f1' });
216
+
217
+ // Works with the recurring adapter too
218
+ const adapter = createRecurringAdapter(schedule, { autoColor: '#ef4444' });
219
+ ```
220
+
221
+ | `autoColor` value | Behaviour |
222
+ |---|---|
223
+ | `true` | Original 15-color vivid palette (fixed, theme-independent) |
224
+ | `'#ef4444'` | Golden-angle hue rotation from that accent; lightness adapted to dark/light |
225
+
226
+ You can also use the palette generator directly:
227
+
228
+ ```ts
229
+ import { generatePalette } from '@nomideusz/svelte-calendar';
230
+
231
+ generatePalette('#6366f1', 8); // 8 theme-harmonious hex colors
232
+ generatePalette(); // default vivid 15-color palette
233
+ ```
234
+
235
+ Both `createMemoryAdapter` and `createRecurringAdapter` accept `colorMap` and `autoColor` options. Events with an explicit `color` field always take priority.
236
+
70
237
  ## Settings Panel
71
238
 
72
239
  The `Settings` component provides a theme picker and dynamic fields for controlling view parameters:
@@ -254,10 +421,10 @@ const viewState = createViewState({
254
421
  src/lib/
255
422
  ├── core/ # Clock, time utils, locale, types
256
423
  ├── engine/ # Reactive state: event-store, view-state, selection, drag
257
- ├── adapters/ # Data layer: memory adapter, REST adapter
424
+ ├── adapters/ # Data layer: memory, recurring, REST adapters
258
425
  ├── primitives/ # Low-level UI atoms: NowIndicator, EventBlock, TimeGutter...
259
426
  ├── calendar/ # Calendar shell, Toolbar
260
- ├── views/ # View components (day/, week/, agenda/, settings/)
427
+ ├── views/ # View components (day/, week/, agenda/, schedule/, settings/)
261
428
  └── theme/ # Preset themes and token definitions
262
429
  ```
263
430
 
@@ -281,8 +448,9 @@ import {
281
448
 
282
449
  | Adapter | Use |
283
450
  |---------|-----|
284
- | `createMemoryAdapter(events)` | In-memory — great for demos and prototyping |
285
- | `createRestAdapter(options)` | Fetch from a REST API with configurable endpoints |
451
+ | `createMemoryAdapter(events, options?)` | In-memory — great for demos and prototyping. Supports `colorMap` and `autoColor` (including theme-aware). |
452
+ | `createRecurringAdapter(schedule, options?)` | Weekly recurring schedules auto-projects onto viewed weeks. Read-only. Supports `colorMap` and `autoColor`. |
453
+ | `createRestAdapter(options)` | Fetch from a REST API with configurable endpoints. |
286
454
 
287
455
  ## Standalone Views
288
456
 
@@ -302,6 +470,65 @@ Each view works independently without the Calendar shell:
302
470
  <WeekHeatmap style={parchment} events={events} height={320} />
303
471
  ```
304
472
 
473
+ ## Embeddable Widget
474
+
475
+ Drop a single `<script>` tag into **any** HTML page — no Svelte, no build tools, no npm needed.
476
+
477
+ ### From CDN
478
+
479
+ ```html
480
+ <script src="https://cdn.jsdelivr.net/npm/@nomideusz/svelte-calendar/widget/widget.js"></script>
481
+
482
+ <day-calendar
483
+ api="https://myschool.com/api/events"
484
+ theme="neutral"
485
+ height="600"
486
+ ></day-calendar>
487
+ ```
488
+
489
+ That's it. Two lines.
490
+
491
+ ### With inline events (no API)
492
+
493
+ ```html
494
+ <script src="https://cdn.jsdelivr.net/npm/@nomideusz/svelte-calendar/widget/widget.js"></script>
495
+
496
+ <day-calendar
497
+ theme="midnight"
498
+ height="500"
499
+ events='[
500
+ { "id": "1", "title": "Yoga Flow", "start": "2025-03-01T09:00", "end": "2025-03-01T10:00", "color": "#818cf8" },
501
+ { "id": "2", "title": "Meditation", "start": "2025-03-01T12:00", "end": "2025-03-01T12:45", "color": "#34d399" }
502
+ ]'
503
+ ></day-calendar>
504
+ ```
505
+
506
+ ### Widget attributes
507
+
508
+ | Attribute | Default | Description |
509
+ |-----------|---------|-------------|
510
+ | `api` | — | REST API base URL — fetches from `{api}/events?start=...&end=...` |
511
+ | `events` | — | JSON string of events (alternative to `api`) |
512
+ | `theme` | `neutral` | Preset: `midnight`, `parchment`, `indigo`, `neutral`, `bare` |
513
+ | `view` | `week-grid` | Default view: `day-grid`, `week-grid`, `day-timeline`, `day-agenda`, `week-agenda`, `week-heatmap` |
514
+ | `height` | `600` | Height in pixels |
515
+ | `locale` | — | BCP 47 locale (`en-US`, `pl-PL`, `ar-SA`, etc.) |
516
+ | `dir` | — | Text direction: `ltr`, `rtl`, `auto` |
517
+ | `mondaystart` | `true` | Start week on Monday (`true`/`false`) |
518
+ | `headers` | — | JSON string of HTTP headers for the REST adapter |
519
+
520
+ ### REST API contract
521
+
522
+ When using the `api` attribute, the widget expects your endpoint to accept:
523
+
524
+ ```
525
+ GET {api}/events?start={ISO}&end={ISO}
526
+ ```
527
+
528
+ And return either:
529
+ - `[{ id, title, start, end, color? }, ...]`
530
+ - `{ events: [{ id, title, start, end, color? }, ...] }`
531
+
305
532
  ## Development
306
533
 
307
534
  ```bash
@@ -309,6 +536,7 @@ pnpm install
309
536
  pnpm dev # SvelteKit dev server (demo app)
310
537
  pnpm check # Type check
311
538
  pnpm run package # Build the library into dist/
539
+ pnpm run build:widget # Build standalone widget.js
312
540
  ```
313
541
 
314
542
  ## 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,15 @@
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
+ /**
18
+ * Auto-assign colors to events by category or title.
19
+ * true → use the default vivid palette
20
+ * string → hex accent color (e.g. '#6366f1') to generate a
21
+ * theme-harmonious palette via golden-angle hue rotation
22
+ */
23
+ autoColor?: boolean | string;
24
+ }
25
+ export declare function createMemoryAdapter(initial?: TimelineEvent[], options?: MemoryAdapterOptions): CalendarAdapter;
@@ -1,27 +1,61 @@
1
+ import { generatePalette, VIVID_PALETTE } from '../core/palette.js';
1
2
  let counter = 0;
2
3
  function uid() {
3
4
  return `mem-${Date.now()}-${++counter}`;
4
5
  }
5
- export function createMemoryAdapter(initial = []) {
6
+ /** Default palette for auto-coloring */
7
+ const AUTO_COLORS = VIVID_PALETTE;
8
+ export function createMemoryAdapter(initial = [], options = {}) {
9
+ const { colorMap, autoColor } = options;
6
10
  const events = [...initial];
11
+ // Resolve palette: vivid default or theme-aware
12
+ const palette = autoColor
13
+ ? typeof autoColor === 'string'
14
+ ? generatePalette(autoColor)
15
+ : AUTO_COLORS
16
+ : AUTO_COLORS;
17
+ // Build auto-color assignments
18
+ const colorAssignments = new Map();
19
+ let colorIndex = 0;
20
+ function resolveColor(ev) {
21
+ if (ev.color)
22
+ return ev.color;
23
+ if (!colorMap && !autoColor)
24
+ return undefined;
25
+ const key = ev.category ?? ev.title;
26
+ if (colorMap?.[key])
27
+ return colorMap[key];
28
+ if (autoColor) {
29
+ if (!colorAssignments.has(key)) {
30
+ colorAssignments.set(key, palette[colorIndex % palette.length]);
31
+ colorIndex++;
32
+ }
33
+ return colorAssignments.get(key);
34
+ }
35
+ return undefined;
36
+ }
37
+ function withColor(ev) {
38
+ const color = resolveColor(ev);
39
+ return color ? { ...ev, color } : ev;
40
+ }
7
41
  function overlaps(ev, range) {
8
42
  return ev.start < range.end && ev.end > range.start;
9
43
  }
10
44
  return {
11
45
  async fetchEvents(range) {
12
- return events.filter((ev) => overlaps(ev, range));
46
+ return events.filter((ev) => overlaps(ev, range)).map(withColor);
13
47
  },
14
48
  async createEvent(data) {
15
49
  const ev = { ...data, id: uid() };
16
50
  events.push(ev);
17
- return ev;
51
+ return withColor(ev);
18
52
  },
19
53
  async updateEvent(id, patch) {
20
54
  const idx = events.findIndex((e) => e.id === id);
21
55
  if (idx < 0)
22
56
  throw new Error(`Event not found: ${id}`);
23
57
  events[idx] = { ...events[idx], ...patch, id };
24
- return events[idx];
58
+ return withColor(events[idx]);
25
59
  },
26
60
  async deleteEvent(id) {
27
61
  const idx = events.findIndex((e) => e.id === id);
@@ -0,0 +1,44 @@
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
+ /**
31
+ * Auto-assign colors to events by category or title.
32
+ * true → use the default vivid palette
33
+ * string → hex accent color (e.g. '#6366f1') to generate a
34
+ * theme-harmonious palette via golden-angle hue rotation
35
+ */
36
+ autoColor?: boolean | string;
37
+ }
38
+ /**
39
+ * Create a CalendarAdapter that projects recurring weekly events
40
+ * onto concrete dates for whatever range the calendar requests.
41
+ *
42
+ * Read-only by default — create/update/delete throw unless custom handlers are provided.
43
+ */
44
+ export declare function createRecurringAdapter(schedule: RecurringEvent[], options?: RecurringAdapterOptions): CalendarAdapter;
@@ -0,0 +1,120 @@
1
+ import { startOfWeek } from '../core/time.js';
2
+ import { DAY_MS } from '../core/time.js';
3
+ import { generatePalette, VIVID_PALETTE } from '../core/palette.js';
4
+ /** Parse "HH:MM" into [hours, minutes] */
5
+ function parseTime(time) {
6
+ const [h, m] = time.split(':').map(Number);
7
+ return [h, m ?? 0];
8
+ }
9
+ /**
10
+ * Convert ISO weekday (1=Mon…7=Sun) to JS Date weekday offset from Monday.
11
+ * Monday = 0, Tuesday = 1, … Sunday = 6
12
+ */
13
+ function isoWeekdayToOffset(dayOfWeek) {
14
+ return dayOfWeek - 1; // 1→0, 2→1, …, 7→6
15
+ }
16
+ /**
17
+ * Project a recurring event onto a specific week, returning a concrete TimelineEvent.
18
+ */
19
+ function projectToWeek(rec, weekStartMs, weekIndex) {
20
+ const dayOffset = isoWeekdayToOffset(rec.dayOfWeek);
21
+ const dayMs = weekStartMs + dayOffset * DAY_MS;
22
+ const dayDate = new Date(dayMs);
23
+ const [sh, sm] = parseTime(rec.startTime);
24
+ const [eh, em] = parseTime(rec.endTime);
25
+ const start = new Date(dayDate.getFullYear(), dayDate.getMonth(), dayDate.getDate(), sh, sm);
26
+ const end = new Date(dayDate.getFullYear(), dayDate.getMonth(), dayDate.getDate(), eh, em);
27
+ return {
28
+ id: `${rec.id}--w${weekIndex}--d${rec.dayOfWeek}`,
29
+ title: rec.title,
30
+ start,
31
+ end,
32
+ color: rec.color,
33
+ category: rec.category,
34
+ data: {
35
+ ...rec.data,
36
+ recurringId: rec.id,
37
+ ...(rec.subtitle ? { subtitle: rec.subtitle } : {}),
38
+ ...(rec.tags ? { tags: rec.tags } : {}),
39
+ },
40
+ };
41
+ }
42
+ /**
43
+ * Find all weeks that overlap a given date range.
44
+ * Returns an array of { weekStartMs, weekIndex } objects.
45
+ */
46
+ function getOverlappingWeeks(range, mondayStart) {
47
+ const weeks = [];
48
+ let cursor = startOfWeek(range.start.getTime(), mondayStart);
49
+ let index = 0;
50
+ while (cursor < range.end.getTime()) {
51
+ weeks.push({ weekStartMs: cursor, weekIndex: index });
52
+ cursor += 7 * DAY_MS;
53
+ index++;
54
+ }
55
+ return weeks;
56
+ }
57
+ /** Default palette for auto-coloring */
58
+ const AUTO_COLORS = VIVID_PALETTE;
59
+ /**
60
+ * Create a CalendarAdapter that projects recurring weekly events
61
+ * onto concrete dates for whatever range the calendar requests.
62
+ *
63
+ * Read-only by default — create/update/delete throw unless custom handlers are provided.
64
+ */
65
+ export function createRecurringAdapter(schedule, options = {}) {
66
+ const { mondayStart = true, colorMap, autoColor } = options;
67
+ // Resolve palette: vivid default or theme-aware
68
+ const palette = autoColor
69
+ ? typeof autoColor === 'string'
70
+ ? generatePalette(autoColor)
71
+ : AUTO_COLORS
72
+ : AUTO_COLORS;
73
+ // Build auto-color assignments
74
+ const colorAssignments = new Map();
75
+ if (autoColor || colorMap) {
76
+ let colorIndex = 0;
77
+ for (const rec of schedule) {
78
+ const key = rec.category ?? rec.title;
79
+ if (colorMap?.[key]) {
80
+ colorAssignments.set(key, colorMap[key]);
81
+ }
82
+ else if (autoColor && !colorAssignments.has(key)) {
83
+ colorAssignments.set(key, palette[colorIndex % palette.length]);
84
+ colorIndex++;
85
+ }
86
+ }
87
+ }
88
+ function resolveColor(rec) {
89
+ if (rec.color)
90
+ return rec.color;
91
+ const key = rec.category ?? rec.title;
92
+ return colorAssignments.get(key);
93
+ }
94
+ return {
95
+ async fetchEvents(range) {
96
+ const weeks = getOverlappingWeeks(range, mondayStart);
97
+ const events = [];
98
+ for (const { weekStartMs, weekIndex } of weeks) {
99
+ for (const rec of schedule) {
100
+ const coloredRec = { ...rec, color: resolveColor(rec) };
101
+ const ev = projectToWeek(coloredRec, weekStartMs, weekIndex);
102
+ // Only include if the event overlaps the requested range
103
+ if (ev.start < range.end && ev.end > range.start) {
104
+ events.push(ev);
105
+ }
106
+ }
107
+ }
108
+ return events;
109
+ },
110
+ async createEvent() {
111
+ throw new Error('createRecurringAdapter is read-only. Use a memory or REST adapter for mutations.');
112
+ },
113
+ async updateEvent() {
114
+ throw new Error('createRecurringAdapter is read-only. Use a memory or REST adapter for mutations.');
115
+ },
116
+ async deleteEvent() {
117
+ throw new Error('createRecurringAdapter is read-only. Use a memory or REST adapter for mutations.');
118
+ },
119
+ };
120
+ }
@@ -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;
@@ -5,3 +5,4 @@ export { setDefaultLocale, getDefaultLocale, is24HourLocale, fmtH, weekdayShort,
5
5
  export { toZonedTime, fromZonedTime, nowInZone, formatInTimeZone, } from './timezone.js';
6
6
  export type { TimelineEvent, WeekTimelineProps, DayTimelineProps, } from './types.js';
7
7
  export { timeToX } from './types.js';
8
+ export { generatePalette, VIVID_PALETTE } from './palette.js';
@@ -8,3 +8,5 @@ export { setDefaultLocale, getDefaultLocale, is24HourLocale, fmtH, weekdayShort,
8
8
  // Timezone utilities
9
9
  export { toZonedTime, fromZonedTime, nowInZone, formatInTimeZone, } from './timezone.js';
10
10
  export { timeToX } from './types.js';
11
+ // Palette
12
+ export { generatePalette, VIVID_PALETTE } from './palette.js';
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Smart auto-color palette generator.
3
+ *
4
+ * Given a base accent hex (e.g. from `--dt-accent`), generates a
5
+ * palette of perceptually distinct colors that harmonize with the theme.
6
+ *
7
+ * Usage:
8
+ * generatePalette('#ef4444', 8) // 8 theme-harmonious colors
9
+ * generatePalette(undefined, 8) // falls back to the vivid default
10
+ */
11
+ export declare const VIVID_PALETTE: string[];
12
+ /**
13
+ * Generate `count` visually distinct and theme-harmonious colors
14
+ * by rotating hue evenly from the base accent, keeping saturation
15
+ * and lightness within a pleasant range.
16
+ *
17
+ * Dark themes (l < 0.5): bump lightness to 0.55–0.65 so colors pop on dark bg.
18
+ * Light themes (l ≥ 0.5): pull lightness to 0.38–0.48 so colors read on light bg.
19
+ *
20
+ * @param accent Hex color string (e.g. '#ef4444'). If undefined, returns VIVID_PALETTE.
21
+ * @param count Number of colors to generate (default: 15).
22
+ */
23
+ export declare function generatePalette(accent?: string, count?: number): string[];