@nomideusz/svelte-calendar 0.6.3 → 0.6.5

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 (47) hide show
  1. package/README.md +192 -10
  2. package/dist/adapters/composite.d.ts +15 -0
  3. package/dist/adapters/composite.js +65 -0
  4. package/dist/adapters/index.d.ts +5 -1
  5. package/dist/adapters/index.js +2 -0
  6. package/dist/adapters/mapped.d.ts +170 -0
  7. package/dist/adapters/mapped.js +343 -0
  8. package/dist/adapters/recurring.d.ts +4 -0
  9. package/dist/adapters/recurring.js +5 -10
  10. package/dist/adapters/types.d.ts +11 -2
  11. package/dist/assets/favicon.svg +9 -1
  12. package/dist/calendar/Calendar.svelte +161 -90
  13. package/dist/calendar/Calendar.svelte.d.ts +12 -0
  14. package/dist/core/index.d.ts +3 -1
  15. package/dist/core/index.js +2 -0
  16. package/dist/core/measure.d.ts +104 -0
  17. package/dist/core/measure.js +195 -0
  18. package/dist/core/time.js +2 -2
  19. package/dist/core/types.d.ts +32 -0
  20. package/dist/engine/event-store.svelte.js +22 -7
  21. package/dist/headless/create-agenda.svelte.d.ts +67 -0
  22. package/dist/headless/create-agenda.svelte.js +139 -0
  23. package/dist/headless/create-calendar.svelte.d.ts +2 -0
  24. package/dist/headless/create-calendar.svelte.js +380 -0
  25. package/dist/headless/index.d.ts +4 -0
  26. package/dist/headless/index.js +2 -0
  27. package/dist/headless/types.d.ts +219 -0
  28. package/dist/headless/types.js +1 -0
  29. package/dist/index.d.ts +6 -4
  30. package/dist/index.js +4 -2
  31. package/dist/primitives/DayHeader.svelte +6 -6
  32. package/dist/primitives/EmptySlot.svelte +3 -3
  33. package/dist/primitives/EventBlock.svelte +99 -14
  34. package/dist/primitives/NowIndicator.svelte +5 -5
  35. package/dist/views/agenda/AgendaDay.svelte +215 -116
  36. package/dist/views/agenda/AgendaWeek.svelte +166 -113
  37. package/dist/views/mobile/MobileDay.svelte +45 -21
  38. package/dist/views/mobile/MobileWeek.svelte +85 -35
  39. package/dist/views/planner/PlannerDay.svelte +116 -60
  40. package/dist/views/planner/PlannerWeek.svelte +228 -166
  41. package/dist/views/shared/context.svelte.d.ts +47 -0
  42. package/dist/views/shared/context.svelte.js +47 -0
  43. package/dist/views/shared/format.d.ts +19 -0
  44. package/dist/views/shared/format.js +46 -0
  45. package/dist/views/shared/index.d.ts +4 -0
  46. package/dist/views/shared/index.js +2 -0
  47. package/package.json +9 -2
@@ -0,0 +1,343 @@
1
+ import { VIVID_PALETTE } from '../core/palette.js';
2
+ // ── Internal helpers ────────────────────────────────────
3
+ let counter = 0;
4
+ function uid() {
5
+ return `mapped-${Date.now()}-${++counter}`;
6
+ }
7
+ /** Parse a date/time value into a Date object */
8
+ function toDate(value) {
9
+ if (value instanceof Date)
10
+ return value;
11
+ if (typeof value === 'number')
12
+ return new Date(value);
13
+ if (typeof value === 'string') {
14
+ const d = new Date(value);
15
+ if (isNaN(d.getTime())) {
16
+ throw new Error(`Cannot parse date: "${value}"`);
17
+ }
18
+ return d;
19
+ }
20
+ throw new Error(`Cannot convert to Date: ${typeof value}`);
21
+ }
22
+ /** Combine "YYYY-MM-DD" date and "HH:MM" time into a Date */
23
+ function combineDateAndTime(dateStr, timeStr) {
24
+ // Handle various date formats
25
+ const datePart = dateStr.includes('T') ? dateStr.split('T')[0] : dateStr;
26
+ const d = new Date(`${datePart}T${timeStr}:00`);
27
+ if (isNaN(d.getTime())) {
28
+ throw new Error(`Cannot combine date "${dateStr}" and time "${timeStr}"`);
29
+ }
30
+ return d;
31
+ }
32
+ /** Coerce a source value into an EventStatus */
33
+ function coerceStatus(value, fieldName) {
34
+ if (typeof value === 'string') {
35
+ const lower = value.toLowerCase();
36
+ if (lower === 'cancelled' || lower === 'canceled')
37
+ return 'cancelled';
38
+ if (lower === 'tentative')
39
+ return 'tentative';
40
+ if (lower === 'full')
41
+ return 'full';
42
+ if (lower === 'limited')
43
+ return 'limited';
44
+ return 'confirmed';
45
+ }
46
+ if (typeof value === 'boolean') {
47
+ // E.g. is_cancelled: true → 'cancelled'
48
+ const lowerField = fieldName.toLowerCase();
49
+ if (lowerField.includes('cancel'))
50
+ return value ? 'cancelled' : 'confirmed';
51
+ if (lowerField.includes('tentative'))
52
+ return value ? 'tentative' : 'confirmed';
53
+ return value ? 'confirmed' : 'tentative';
54
+ }
55
+ return 'confirmed';
56
+ }
57
+ /** Parse an RGB string like "rgb(62, 101, 255)" to a hex color */
58
+ function normalizeColor(value) {
59
+ if (!value)
60
+ return undefined;
61
+ if (typeof value !== 'string')
62
+ return undefined;
63
+ const str = value.trim();
64
+ // Already hex
65
+ if (str.startsWith('#'))
66
+ return str;
67
+ // rgb(r, g, b) format
68
+ const rgbMatch = str.match(/^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/i);
69
+ if (rgbMatch) {
70
+ const r = parseInt(rgbMatch[1], 10);
71
+ const g = parseInt(rgbMatch[2], 10);
72
+ const b = parseInt(rgbMatch[3], 10);
73
+ return `#${((1 << 24) | (r << 16) | (g << 8) | b).toString(16).slice(1)}`;
74
+ }
75
+ // CSS named colors and other formats — pass through
76
+ return str;
77
+ }
78
+ /**
79
+ * Collect unmapped fields into the `data` payload.
80
+ */
81
+ function collectData(raw, mappedKeys, include) {
82
+ const data = {};
83
+ const keys = include === '*' ? Object.keys(raw) : include;
84
+ for (const key of keys) {
85
+ if (!mappedKeys.has(key) && raw[key] !== undefined) {
86
+ data[key] = raw[key];
87
+ }
88
+ }
89
+ return data;
90
+ }
91
+ // ── Factory ─────────────────────────────────────────────
92
+ /**
93
+ * Create a CalendarAdapter from an array of external records + mapping config.
94
+ *
95
+ * @example
96
+ * ```ts
97
+ * // Yoga school schedule
98
+ * const adapter = createMappedAdapter(yogaSchedule.events, {
99
+ * fields: {
100
+ * title: 'class_name',
101
+ * start: 'starts_at_iso',
102
+ * end: 'ends_at_iso',
103
+ * subtitle: 'teacher',
104
+ * location: 'room',
105
+ * color: 'color',
106
+ * externalId: 'reference_id',
107
+ * status: 'is_cancelled',
108
+ * tags: ['is_free', 'is_bookable_online'],
109
+ * },
110
+ * });
111
+ *
112
+ * // Medical appointments
113
+ * const adapter = createMappedAdapter(appointments, {
114
+ * fields: {
115
+ * title: 'procedure_name',
116
+ * start: 'scheduled_at',
117
+ * end: 'scheduled_end',
118
+ * subtitle: 'doctor_name',
119
+ * location: 'office',
120
+ * resourceId: 'doctor_id',
121
+ * },
122
+ * });
123
+ *
124
+ * // Full custom mapper
125
+ * const adapter = createMappedAdapter(rawData, {
126
+ * mapEvent: (raw) => ({
127
+ * id: raw.uid,
128
+ * title: `${raw.first_name} ${raw.last_name}`,
129
+ * start: new Date(raw.timestamp),
130
+ * end: new Date(raw.timestamp + raw.duration_ms),
131
+ * location: raw.room,
132
+ * }),
133
+ * });
134
+ * ```
135
+ */
136
+ export function createMappedAdapter(sourceData, options = {}) {
137
+ const { fields, mapEvent: customMapper, includeData = '*', autoColor = true, palette = VIVID_PALETTE, readOnly = true, } = options;
138
+ // Build the internal event list
139
+ const events = [];
140
+ const colorAssignments = new Map();
141
+ let colorIndex = 0;
142
+ function resolveColor(ev) {
143
+ // When autoColor is on, always assign from palette (ignore source color)
144
+ if (!autoColor && ev.color)
145
+ return ev.color;
146
+ const key = ev.category ?? ev.title;
147
+ if (!colorAssignments.has(key)) {
148
+ colorAssignments.set(key, palette[colorIndex % palette.length]);
149
+ colorIndex++;
150
+ }
151
+ return colorAssignments.get(key);
152
+ }
153
+ function withColor(ev) {
154
+ const color = resolveColor(ev);
155
+ return color ? { ...ev, color } : ev;
156
+ }
157
+ /**
158
+ * Map a single raw record using the declarative field mapping.
159
+ */
160
+ function mapWithFields(raw, index) {
161
+ const f = fields;
162
+ const r = raw;
163
+ // ── ID ──
164
+ const id = f.id
165
+ ? String(r[f.id] ?? '')
166
+ : String(r['id'] ?? r['reference_id'] ?? r['externalId'] ?? r['_id'] ?? uid());
167
+ // ── Title ──
168
+ const titleKey = f.title ?? 'title';
169
+ const title = String(r[titleKey] ?? r['name'] ?? r['class_name'] ?? `Event ${index + 1}`);
170
+ // ── Start / End ──
171
+ let start;
172
+ let end;
173
+ if (f.start && r[f.start] !== undefined) {
174
+ start = toDate(r[f.start]);
175
+ }
176
+ else if (f.date && f.startTime && r[f.date] && r[f.startTime]) {
177
+ start = combineDateAndTime(String(r[f.date]), String(r[f.startTime]));
178
+ }
179
+ else if (r['start'] !== undefined) {
180
+ start = toDate(r['start']);
181
+ }
182
+ else if (r['starts_at_iso'] !== undefined) {
183
+ start = toDate(r['starts_at_iso']);
184
+ }
185
+ else if (r['start_time'] !== undefined && r['date'] !== undefined) {
186
+ start = combineDateAndTime(String(r['date']), String(r['start_time']));
187
+ }
188
+ else {
189
+ throw new Error(`Cannot determine start time for record ${index}: no matching field found`);
190
+ }
191
+ if (f.end && r[f.end] !== undefined) {
192
+ end = toDate(r[f.end]);
193
+ }
194
+ else if (f.date && f.endTime && r[f.date] && r[f.endTime]) {
195
+ end = combineDateAndTime(String(r[f.date]), String(r[f.endTime]));
196
+ }
197
+ else if (r['end'] !== undefined) {
198
+ end = toDate(r['end']);
199
+ }
200
+ else if (r['ends_at_iso'] !== undefined) {
201
+ end = toDate(r['ends_at_iso']);
202
+ }
203
+ else if (r['end_time'] !== undefined && r['date'] !== undefined) {
204
+ end = combineDateAndTime(String(r['date']), String(r['end_time']));
205
+ }
206
+ else {
207
+ // Default: 1 hour after start
208
+ end = new Date(start.getTime() + 60 * 60 * 1000);
209
+ }
210
+ // ── Optional fields ──
211
+ const color = normalizeColor(r[f.color ?? 'color']);
212
+ const subtitle = f.subtitle ? String(r[f.subtitle] ?? '') || undefined : undefined;
213
+ const location = f.location
214
+ ? String(r[f.location] ?? '') || undefined
215
+ : r['room'] ? String(r['room']) : r['location'] ? String(r['location']) : undefined;
216
+ const category = f.category ? String(r[f.category] ?? '') || undefined : undefined;
217
+ const resourceId = f.resourceId ? String(r[f.resourceId] ?? '') || undefined : undefined;
218
+ const externalId = f.externalId
219
+ ? String(r[f.externalId] ?? '') || undefined
220
+ : r['reference_id'] ? String(r['reference_id']) : undefined;
221
+ // ── Status ──
222
+ let status;
223
+ if (f.status && r[f.status] !== undefined) {
224
+ status = coerceStatus(r[f.status], f.status);
225
+ }
226
+ else if (r['is_cancelled'] !== undefined) {
227
+ status = coerceStatus(r['is_cancelled'], 'is_cancelled');
228
+ }
229
+ // ── Tags ──
230
+ let tags;
231
+ if (f.tags && f.tags.length > 0) {
232
+ tags = [];
233
+ for (const tagKey of f.tags) {
234
+ const val = r[tagKey];
235
+ if (val === true) {
236
+ // Convert key name to human-readable tag
237
+ tags.push(tagKey.replace(/^is_/, '').replace(/_/g, ' '));
238
+ }
239
+ else if (typeof val === 'string' && val) {
240
+ tags.push(val);
241
+ }
242
+ }
243
+ if (tags.length === 0)
244
+ tags = undefined;
245
+ }
246
+ // ── Data (unmapped fields) ──
247
+ const mappedKeys = new Set();
248
+ for (const v of Object.values(f)) {
249
+ if (typeof v === 'string')
250
+ mappedKeys.add(v);
251
+ if (Array.isArray(v))
252
+ v.forEach((k) => mappedKeys.add(k));
253
+ }
254
+ // Also mark auto-detected keys as mapped
255
+ for (const k of ['id', 'title', 'name', 'class_name', 'start', 'end',
256
+ 'starts_at_iso', 'ends_at_iso', 'start_time', 'end_time', 'date',
257
+ 'color', 'room', 'location', 'reference_id', 'is_cancelled']) {
258
+ mappedKeys.add(k);
259
+ }
260
+ const data = collectData(r, mappedKeys, includeData);
261
+ const ev = { id, title, start, end };
262
+ if (color)
263
+ ev.color = color;
264
+ if (subtitle)
265
+ ev.subtitle = subtitle;
266
+ if (location)
267
+ ev.location = location;
268
+ if (category)
269
+ ev.category = category;
270
+ if (resourceId)
271
+ ev.resourceId = resourceId;
272
+ if (externalId)
273
+ ev.externalId = externalId;
274
+ if (status && status !== 'confirmed')
275
+ ev.status = status;
276
+ if (tags)
277
+ ev.tags = tags;
278
+ if (Object.keys(data).length > 0)
279
+ ev.data = data;
280
+ return ev;
281
+ }
282
+ // ── Build events array ──
283
+ for (let i = 0; i < sourceData.length; i++) {
284
+ const mapper = customMapper ?? mapWithFields;
285
+ const ev = withColor(mapper(sourceData[i], i));
286
+ events.push(ev);
287
+ }
288
+ // ── Adapter interface ──
289
+ function overlaps(ev, range) {
290
+ return ev.start < range.end && ev.end > range.start;
291
+ }
292
+ return {
293
+ async fetchEvents(range) {
294
+ return events.filter((ev) => overlaps(ev, range));
295
+ },
296
+ async createEvent(data) {
297
+ if (readOnly) {
298
+ throw new Error('Mapped adapter is read-only. Set readOnly: false and provide onMutate to enable writes.');
299
+ }
300
+ const handler = options.onMutate?.onCreate;
301
+ if (handler) {
302
+ const raw = await handler(data);
303
+ const mapper = customMapper ?? mapWithFields;
304
+ const ev = withColor(mapper(raw, events.length));
305
+ events.push(ev);
306
+ return ev;
307
+ }
308
+ // Fallback: create locally
309
+ const ev = { ...data, id: uid() };
310
+ events.push(withColor(ev));
311
+ return ev;
312
+ },
313
+ async updateEvent(id, patch) {
314
+ if (readOnly) {
315
+ throw new Error('Mapped adapter is read-only. Set readOnly: false and provide onMutate to enable writes.');
316
+ }
317
+ const idx = events.findIndex((e) => e.id === id);
318
+ if (idx < 0)
319
+ throw new Error(`Event not found: ${id}`);
320
+ const handler = options.onMutate?.onUpdate;
321
+ if (handler) {
322
+ const raw = await handler(id, patch);
323
+ const mapper = customMapper ?? mapWithFields;
324
+ const ev = withColor(mapper(raw, idx));
325
+ events[idx] = ev;
326
+ return ev;
327
+ }
328
+ events[idx] = { ...events[idx], ...patch, id };
329
+ return events[idx];
330
+ },
331
+ async deleteEvent(id) {
332
+ if (readOnly) {
333
+ throw new Error('Mapped adapter is read-only. Set readOnly: false and provide onMutate to enable writes.');
334
+ }
335
+ const handler = options.onMutate?.onDelete;
336
+ if (handler)
337
+ await handler(id);
338
+ const idx = events.findIndex((e) => e.id === id);
339
+ if (idx >= 0)
340
+ events.splice(idx, 1);
341
+ },
342
+ };
343
+ }
@@ -57,6 +57,10 @@ export interface RecurringEvent {
57
57
  tags?: string[];
58
58
  /** Category for grouping */
59
59
  category?: string;
60
+ /** Location or room name */
61
+ location?: string;
62
+ /** Resource ID for multi-resource views */
63
+ resourceId?: string;
60
64
  /** Arbitrary payload */
61
65
  data?: Record<string, unknown>;
62
66
  }
@@ -68,7 +68,9 @@ function createConcreteEvent(rec, date) {
68
68
  category: rec.category,
69
69
  subtitle: rec.subtitle,
70
70
  tags: rec.tags,
71
- data: { ...rec.data, recurringId: rec.id },
71
+ location: rec.location,
72
+ resourceId: rec.resourceId,
73
+ data: { ...rec.data, recurringId: rec.id, readOnly: true },
72
74
  };
73
75
  }
74
76
  // ── Count → until resolution ────────────────────────────
@@ -283,14 +285,7 @@ export function createRecurringAdapter(schedule, options = {}) {
283
285
  }
284
286
  return events;
285
287
  },
286
- async createEvent() {
287
- throw new Error('createRecurringAdapter is read-only. Use a memory or REST adapter for mutations.');
288
- },
289
- async updateEvent() {
290
- throw new Error('createRecurringAdapter is read-only. Use a memory or REST adapter for mutations.');
291
- },
292
- async deleteEvent() {
293
- throw new Error('createRecurringAdapter is read-only. Use a memory or REST adapter for mutations.');
294
- },
288
+ // Read-only adapter: CRUD methods intentionally omitted.
289
+ // Use createMemoryAdapter or createRestAdapter for mutations.
295
290
  };
296
291
  }
@@ -15,9 +15,18 @@ export interface CalendarAdapter {
15
15
  /** Fetch events that overlap the given date range */
16
16
  fetchEvents(range: DateRange): Promise<TimelineEvent[]>;
17
17
  /** Create a new event, return it with a server-assigned ID */
18
- createEvent(event: Omit<TimelineEvent, 'id'>): Promise<TimelineEvent>;
18
+ createEvent?(event: Omit<TimelineEvent, 'id'>): Promise<TimelineEvent>;
19
19
  /** Update an event, return the full updated event */
20
- updateEvent(id: string, patch: Partial<TimelineEvent>): Promise<TimelineEvent>;
20
+ updateEvent?(id: string, patch: Partial<TimelineEvent>): Promise<TimelineEvent>;
21
21
  /** Delete an event by ID */
22
+ deleteEvent?(id: string): Promise<void>;
23
+ }
24
+ /**
25
+ * A CalendarAdapter that supports full CRUD operations.
26
+ * Use this type when you need to guarantee write support.
27
+ */
28
+ export interface WritableCalendarAdapter extends CalendarAdapter {
29
+ createEvent(event: Omit<TimelineEvent, 'id'>): Promise<TimelineEvent>;
30
+ updateEvent(id: string, patch: Partial<TimelineEvent>): Promise<TimelineEvent>;
22
31
  deleteEvent(id: string): Promise<void>;
23
32
  }
@@ -1 +1,9 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
2
+ <title>svelte-calendar</title>
3
+ <rect width="64" height="64" rx="16" fill="#2563eb"/>
4
+ <rect x="14" y="18" width="36" height="32" rx="6" fill="#fff"/>
5
+ <path d="M14 27h36" stroke="#2563eb" stroke-width="4"/>
6
+ <path d="M23 13v10M41 13v10" stroke="#fff" stroke-width="5" stroke-linecap="round"/>
7
+ <rect x="21" y="33" width="7" height="7" rx="2" fill="#2563eb" opacity=".9"/>
8
+ <rect x="36" y="33" width="7" height="7" rx="2" fill="#2563eb" opacity=".35"/>
9
+ </svg>