@nomideusz/svelte-calendar 0.6.0 → 0.6.4

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 (50) hide show
  1. package/README.md +183 -5
  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 +11 -0
  9. package/dist/adapters/recurring.js +9 -13
  10. package/dist/adapters/types.d.ts +11 -2
  11. package/dist/calendar/Calendar.svelte +847 -767
  12. package/dist/calendar/Calendar.svelte.d.ts +14 -2
  13. package/dist/core/index.d.ts +3 -1
  14. package/dist/core/index.js +2 -0
  15. package/dist/core/measure.d.ts +104 -0
  16. package/dist/core/measure.js +195 -0
  17. package/dist/core/time.js +2 -2
  18. package/dist/core/types.d.ts +32 -0
  19. package/dist/engine/event-store.svelte.js +22 -7
  20. package/dist/headless/create-agenda.svelte.d.ts +67 -0
  21. package/dist/headless/create-agenda.svelte.js +139 -0
  22. package/dist/headless/create-calendar.svelte.d.ts +2 -0
  23. package/dist/headless/create-calendar.svelte.js +380 -0
  24. package/dist/headless/index.d.ts +4 -0
  25. package/dist/headless/index.js +2 -0
  26. package/dist/headless/types.d.ts +219 -0
  27. package/dist/headless/types.js +1 -0
  28. package/dist/index.d.ts +6 -4
  29. package/dist/index.js +4 -2
  30. package/dist/primitives/DayHeader.svelte +103 -103
  31. package/dist/primitives/EmptySlot.svelte +105 -105
  32. package/dist/primitives/EventBlock.svelte +397 -312
  33. package/dist/primitives/NowIndicator.svelte +178 -178
  34. package/dist/primitives/TimeGutter.svelte +104 -104
  35. package/dist/theme/presets.js +36 -36
  36. package/dist/views/agenda/AgendaDay.svelte +1167 -1055
  37. package/dist/views/agenda/AgendaWeek.svelte +987 -924
  38. package/dist/views/mobile/Mobile.svelte +17 -17
  39. package/dist/views/mobile/MobileDay.svelte +729 -665
  40. package/dist/views/mobile/MobileWeek.svelte +476 -456
  41. package/dist/views/planner/PlannerDay.svelte +1184 -1091
  42. package/dist/views/planner/PlannerWeek.svelte +924 -945
  43. package/dist/views/shared/context.svelte.d.ts +47 -0
  44. package/dist/views/shared/context.svelte.js +47 -0
  45. package/dist/views/shared/format.d.ts +19 -0
  46. package/dist/views/shared/format.js +46 -0
  47. package/dist/views/shared/index.d.ts +4 -0
  48. package/dist/views/shared/index.js +2 -0
  49. package/dist/widget/CalendarWidget.svelte +142 -142
  50. package/package.json +76 -71
@@ -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,12 +57,23 @@ 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
  }
63
67
  export interface RecurringAdapterOptions {
64
68
  /** Start weeks on Monday (default: true) */
65
69
  mondayStart?: boolean;
70
+ /**
71
+ * Custom color palette for auto-coloring events.
72
+ * Pass `generatePalette('#yourAccent')` to get theme-harmonious colors,
73
+ * or provide your own array of hex strings.
74
+ * Defaults to the built-in vivid palette.
75
+ */
76
+ palette?: string[];
66
77
  }
67
78
  /**
68
79
  * Create a CalendarAdapter that projects recurring events onto concrete
@@ -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 ────────────────────────────
@@ -230,8 +232,9 @@ const PALETTE = VIVID_PALETTE;
230
232
  * Read-only by default — create/update/delete throw.
231
233
  */
232
234
  export function createRecurringAdapter(schedule, options = {}) {
233
- const { mondayStart = true } = options;
234
- // Auto-color: assign from vivid palette by category/title
235
+ const { mondayStart = true, palette } = options;
236
+ const colors = palette?.length ? palette : PALETTE;
237
+ // Auto-color: assign from palette by category/title
235
238
  const colorAssignments = new Map();
236
239
  let colorIndex = 0;
237
240
  for (const rec of schedule) {
@@ -239,7 +242,7 @@ export function createRecurringAdapter(schedule, options = {}) {
239
242
  continue;
240
243
  const key = rec.category ?? rec.title;
241
244
  if (!colorAssignments.has(key)) {
242
- colorAssignments.set(key, PALETTE[colorIndex % PALETTE.length]);
245
+ colorAssignments.set(key, colors[colorIndex % colors.length]);
243
246
  colorIndex++;
244
247
  }
245
248
  }
@@ -282,14 +285,7 @@ export function createRecurringAdapter(schedule, options = {}) {
282
285
  }
283
286
  return events;
284
287
  },
285
- async createEvent() {
286
- throw new Error('createRecurringAdapter is read-only. Use a memory or REST adapter for mutations.');
287
- },
288
- async updateEvent() {
289
- throw new Error('createRecurringAdapter is read-only. Use a memory or REST adapter for mutations.');
290
- },
291
- async deleteEvent() {
292
- throw new Error('createRecurringAdapter is read-only. Use a memory or REST adapter for mutations.');
293
- },
288
+ // Read-only adapter: CRUD methods intentionally omitted.
289
+ // Use createMemoryAdapter or createRestAdapter for mutations.
294
290
  };
295
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
  }