@nomideusz/svelte-calendar 0.3.1 → 0.5.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 (75) hide show
  1. package/README.md +193 -406
  2. package/dist/adapters/recurring.d.ts +45 -8
  3. package/dist/adapters/recurring.js +227 -49
  4. package/dist/adapters/rest.js +6 -1
  5. package/dist/calendar/Calendar.svelte +137 -29
  6. package/dist/calendar/Calendar.svelte.d.ts +10 -8
  7. package/dist/calendar/index.d.ts +0 -1
  8. package/dist/calendar/index.js +0 -1
  9. package/dist/core/index.d.ts +5 -4
  10. package/dist/core/index.js +2 -3
  11. package/dist/core/locale.d.ts +74 -0
  12. package/dist/core/locale.js +96 -4
  13. package/dist/core/time.d.ts +30 -0
  14. package/dist/core/time.js +42 -0
  15. package/dist/core/types.d.ts +1 -72
  16. package/dist/core/types.js +1 -8
  17. package/dist/engine/event-store.svelte.js +2 -2
  18. package/dist/engine/index.d.ts +1 -1
  19. package/dist/engine/view-state.svelte.d.ts +12 -6
  20. package/dist/engine/view-state.svelte.js +8 -4
  21. package/dist/index.d.ts +6 -7
  22. package/dist/index.js +4 -4
  23. package/dist/primitives/EmptySlot.svelte +6 -16
  24. package/dist/primitives/EventBlock.svelte +6 -19
  25. package/dist/primitives/NowIndicator.svelte +7 -4
  26. package/dist/primitives/TimeGutter.svelte +1 -1
  27. package/dist/theme/index.d.ts +1 -1
  28. package/dist/theme/index.js +1 -1
  29. package/dist/theme/presets.d.ts +11 -19
  30. package/dist/theme/presets.js +25 -111
  31. package/dist/views/agenda/Agenda.svelte +17 -1514
  32. package/dist/views/agenda/Agenda.svelte.d.ts +0 -13
  33. package/dist/views/agenda/AgendaDay.svelte +976 -0
  34. package/dist/views/agenda/AgendaDay.svelte.d.ts +14 -0
  35. package/dist/views/agenda/AgendaWeek.svelte +775 -0
  36. package/dist/views/agenda/AgendaWeek.svelte.d.ts +15 -0
  37. package/dist/views/agenda/index.d.ts +2 -0
  38. package/dist/views/agenda/index.js +2 -0
  39. package/dist/views/index.d.ts +2 -6
  40. package/dist/views/index.js +2 -6
  41. package/dist/views/planner/Planner.svelte +17 -0
  42. package/dist/views/planner/Planner.svelte.d.ts +7 -0
  43. package/dist/views/planner/PlannerDay.svelte +957 -0
  44. package/dist/views/{day/DayGrid.svelte.d.ts → planner/PlannerDay.svelte.d.ts} +8 -16
  45. package/dist/views/planner/PlannerWeek.svelte +826 -0
  46. package/dist/views/{week/WeekGrid.svelte.d.ts → planner/PlannerWeek.svelte.d.ts} +5 -9
  47. package/dist/views/planner/index.d.ts +1 -0
  48. package/dist/views/planner/index.js +1 -0
  49. package/dist/widget/CalendarWidget.svelte +54 -35
  50. package/dist/widget/CalendarWidget.svelte.d.ts +1 -1
  51. package/dist/widget/widget.d.ts +1 -7
  52. package/dist/widget/widget.js +44 -3
  53. package/package.json +5 -2
  54. package/widget/svelte-calendar.css +1 -0
  55. package/widget/widget.js +6 -144
  56. package/dist/calendar/Toolbar.svelte +0 -241
  57. package/dist/calendar/Toolbar.svelte.d.ts +0 -19
  58. package/dist/views/day/DayGrid.svelte +0 -1101
  59. package/dist/views/day/DayTimeline.svelte +0 -532
  60. package/dist/views/day/DayTimeline.svelte.d.ts +0 -4
  61. package/dist/views/day/index.d.ts +0 -2
  62. package/dist/views/day/index.js +0 -2
  63. package/dist/views/schedule/WeekSchedule.svelte +0 -133
  64. package/dist/views/schedule/WeekSchedule.svelte.d.ts +0 -38
  65. package/dist/views/schedule/index.d.ts +0 -1
  66. package/dist/views/schedule/index.js +0 -2
  67. package/dist/views/settings/Settings.svelte +0 -307
  68. package/dist/views/settings/Settings.svelte.d.ts +0 -23
  69. package/dist/views/settings/index.d.ts +0 -2
  70. package/dist/views/settings/index.js +0 -1
  71. package/dist/views/week/WeekGrid.svelte +0 -631
  72. package/dist/views/week/WeekHeatmap.svelte +0 -460
  73. package/dist/views/week/WeekHeatmap.svelte.d.ts +0 -8
  74. package/dist/views/week/index.d.ts +0 -2
  75. package/dist/views/week/index.js +0 -2
@@ -1,16 +1,54 @@
1
1
  import type { CalendarAdapter } from './types.js';
2
2
  /**
3
- * A weekly recurring event definition.
3
+ * A recurring event definition.
4
+ *
5
+ * Backward-compatible: the only required fields are `id`, `title`,
6
+ * `startTime`, `endTime`, and `dayOfWeek` (for weekly, the default).
4
7
  */
5
8
  export interface RecurringEvent {
6
9
  id: string;
7
10
  title: string;
8
- /** ISO weekday: 1 = Monday … 7 = Sunday */
9
- dayOfWeek: number;
10
11
  /** Start time in "HH:MM" 24-hour format */
11
12
  startTime: string;
12
13
  /** End time in "HH:MM" 24-hour format */
13
14
  endTime: string;
15
+ /** Recurrence frequency (default: `'weekly'`) */
16
+ frequency?: 'daily' | 'weekly' | 'monthly';
17
+ /**
18
+ * Repeat every N periods — e.g. `2` with `weekly` = biweekly.
19
+ * Requires `startDate` when > 1 so the adapter knows the anchor.
20
+ * @default 1
21
+ */
22
+ interval?: number;
23
+ /**
24
+ * ISO weekday(s): 1 = Monday … 7 = Sunday.
25
+ * Required for `weekly` frequency. Pass an array for multiple days
26
+ * (e.g. `[1, 3, 5]` for Mon/Wed/Fri).
27
+ */
28
+ dayOfWeek?: number | number[];
29
+ /**
30
+ * Day of month (1–31). Required for `monthly` frequency.
31
+ * Clamped to the last day of shorter months (e.g. 31 → 28 in Feb).
32
+ * @default 1
33
+ */
34
+ dayOfMonth?: number;
35
+ /**
36
+ * First possible occurrence in `"YYYY-MM-DD"` format.
37
+ * Events before this date are excluded. Required when using
38
+ * `interval > 1` or `count`.
39
+ */
40
+ startDate?: string;
41
+ /**
42
+ * Last possible occurrence in `"YYYY-MM-DD"` format.
43
+ * No events are generated after this date.
44
+ */
45
+ until?: string;
46
+ /**
47
+ * Maximum number of occurrences, counted from `startDate`.
48
+ * Alternative to `until` — if both are set the stricter bound wins.
49
+ * Requires `startDate`.
50
+ */
51
+ count?: number;
14
52
  /** Accent color */
15
53
  color?: string;
16
54
  /** Optional subtitle displayed below the title */
@@ -30,15 +68,14 @@ export interface RecurringAdapterOptions {
30
68
  /**
31
69
  * Auto-assign colors to events by category or title.
32
70
  * 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
71
+ * string → hex accent color to generate a harmonious palette
35
72
  */
36
73
  autoColor?: boolean | string;
37
74
  }
38
75
  /**
39
- * Create a CalendarAdapter that projects recurring weekly events
40
- * onto concrete dates for whatever range the calendar requests.
76
+ * Create a CalendarAdapter that projects recurring events onto concrete
77
+ * dates for whatever range the calendar requests.
41
78
  *
42
- * Read-only by default — create/update/delete throw unless custom handlers are provided.
79
+ * Read-only by default — create/update/delete throw.
43
80
  */
44
81
  export declare function createRecurringAdapter(schedule: RecurringEvent[], options?: RecurringAdapterOptions): CalendarAdapter;
@@ -1,70 +1,231 @@
1
- import { startOfWeek } from '../core/time.js';
2
- import { DAY_MS } from '../core/time.js';
1
+ import { startOfWeek, DAY_MS } from '../core/time.js';
3
2
  import { generatePalette, VIVID_PALETTE } from '../core/palette.js';
3
+ // ── Helpers ─────────────────────────────────────────────
4
4
  /** Parse "HH:MM" into [hours, minutes] */
5
5
  function parseTime(time) {
6
- const [h, m] = time.split(':').map(Number);
7
- return [h, m ?? 0];
6
+ const parts = time.split(':');
7
+ if (parts.length < 2) {
8
+ throw new Error(`Invalid time format "${time}": expected "HH:MM"`);
9
+ }
10
+ const h = Number(parts[0]);
11
+ const m = Number(parts[1]);
12
+ if (!Number.isInteger(h) || h < 0 || h > 23) {
13
+ throw new Error(`Invalid hours in time "${time}": expected 0–23`);
14
+ }
15
+ if (!Number.isInteger(m) || m < 0 || m > 59) {
16
+ throw new Error(`Invalid minutes in time "${time}": expected 0–59`);
17
+ }
18
+ return [h, m];
8
19
  }
9
- /**
10
- * Convert ISO weekday (1=Mon…7=Sun) to JS Date weekday offset from Monday.
11
- * Monday = 0, Tuesday = 1, … Sunday = 6
12
- */
20
+ /** Parse "YYYY-MM-DD" to a midnight Date */
21
+ function parseDate(str) {
22
+ const parts = str.split('-');
23
+ if (parts.length !== 3)
24
+ throw new Error(`Invalid date "${str}": expected "YYYY-MM-DD"`);
25
+ const y = Number(parts[0]);
26
+ const m = Number(parts[1]) - 1;
27
+ const d = Number(parts[2]);
28
+ const date = new Date(y, m, d);
29
+ if (Number.isNaN(date.getTime()))
30
+ throw new Error(`Invalid date "${str}"`);
31
+ return date;
32
+ }
33
+ /** ISO weekday offset from Monday: 1→0, 2→1, … 7→6 */
13
34
  function isoWeekdayToOffset(dayOfWeek) {
14
- return dayOfWeek - 1; // 1→0, 2→1, …, 7→6
35
+ return dayOfWeek - 1;
15
36
  }
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);
37
+ /** Normalize `dayOfWeek` to a sorted array */
38
+ function normalizeDays(value) {
39
+ if (value == null)
40
+ return [];
41
+ const arr = Array.isArray(value) ? [...value] : [value];
42
+ return arr.sort((a, b) => a - b);
43
+ }
44
+ /** Clamp day to last day of month (e.g. 31 → 28 in Feb) */
45
+ function clampDayOfMonth(year, month, day) {
46
+ const lastDay = new Date(year, month + 1, 0).getDate();
47
+ return new Date(year, month, Math.min(day, lastDay));
48
+ }
49
+ /** Create a concrete TimelineEvent from a recurring def + occurrence date */
50
+ function createConcreteEvent(rec, date, instanceKey) {
23
51
  const [sh, sm] = parseTime(rec.startTime);
24
52
  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);
53
+ const start = new Date(date.getFullYear(), date.getMonth(), date.getDate(), sh, sm);
54
+ const end = new Date(date.getFullYear(), date.getMonth(), date.getDate(), eh, em);
27
55
  return {
28
- id: `${rec.id}--w${weekIndex}--d${rec.dayOfWeek}`,
56
+ id: `${rec.id}--${instanceKey}`,
29
57
  title: rec.title,
30
58
  start,
31
59
  end,
32
60
  color: rec.color,
33
61
  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
- },
62
+ subtitle: rec.subtitle,
63
+ tags: rec.tags,
64
+ data: { ...rec.data, recurringId: rec.id },
40
65
  };
41
66
  }
67
+ // ── Count → until resolution ────────────────────────────
42
68
  /**
43
- * Find all weeks that overlap a given date range.
44
- * Returns an array of { weekStartMs, weekIndex } objects.
69
+ * When `count` is set, compute the date of the last occurrence so the
70
+ * projection loop only needs to check date bounds.
45
71
  */
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++;
72
+ function computeUntilFromCount(rec, startDateObj, mondayStart) {
73
+ const count = rec.count;
74
+ if (count == null || count <= 0)
75
+ return undefined;
76
+ const freq = rec.frequency ?? 'weekly';
77
+ const interval = rec.interval ?? 1;
78
+ switch (freq) {
79
+ case 'daily': {
80
+ const d = new Date(startDateObj);
81
+ d.setDate(d.getDate() + (count - 1) * interval);
82
+ return d;
83
+ }
84
+ case 'weekly': {
85
+ const days = normalizeDays(rec.dayOfWeek);
86
+ if (days.length === 0)
87
+ return undefined;
88
+ const startMs = startDateObj.getTime();
89
+ let weekMs = startOfWeek(startMs, mondayStart);
90
+ let remaining = count;
91
+ const maxCycles = Math.ceil(count / days.length) + 2;
92
+ for (let c = 0; c < maxCycles; c++) {
93
+ for (const day of days) {
94
+ const dayMs = weekMs + isoWeekdayToOffset(day) * DAY_MS;
95
+ if (dayMs >= startMs) {
96
+ remaining--;
97
+ if (remaining === 0)
98
+ return new Date(dayMs);
99
+ }
100
+ }
101
+ weekMs += interval * 7 * DAY_MS;
102
+ }
103
+ return undefined;
104
+ }
105
+ case 'monthly': {
106
+ const totalMonths = startDateObj.getFullYear() * 12 +
107
+ startDateObj.getMonth() +
108
+ (count - 1) * interval;
109
+ const y = Math.floor(totalMonths / 12);
110
+ const m = totalMonths % 12;
111
+ return clampDayOfMonth(y, m, rec.dayOfMonth ?? 1);
112
+ }
54
113
  }
55
- return weeks;
56
114
  }
115
+ // ── Projection functions ────────────────────────────────
116
+ function projectDaily(rec, range, startDate, effectiveUntil, out) {
117
+ const interval = rec.interval ?? 1;
118
+ // Anchor at startDate or range.start (midnight)
119
+ const anchor = new Date(startDate ?? range.start);
120
+ anchor.setHours(0, 0, 0, 0);
121
+ const rangeStart = new Date(range.start);
122
+ rangeStart.setHours(0, 0, 0, 0);
123
+ // Skip ahead to first occurrence at or after range.start
124
+ const cursor = new Date(anchor);
125
+ if (cursor < rangeStart) {
126
+ const daysBetween = Math.round((rangeStart.getTime() - cursor.getTime()) / DAY_MS);
127
+ const skip = Math.floor(daysBetween / interval);
128
+ cursor.setDate(cursor.getDate() + skip * interval);
129
+ if (cursor < rangeStart)
130
+ cursor.setDate(cursor.getDate() + interval);
131
+ }
132
+ let idx = 0;
133
+ while (cursor < range.end) {
134
+ if (effectiveUntil && cursor > effectiveUntil)
135
+ break;
136
+ const ev = createConcreteEvent(rec, cursor, `d${idx}`);
137
+ if (ev.start < range.end && ev.end > range.start) {
138
+ out.push(ev);
139
+ }
140
+ cursor.setDate(cursor.getDate() + interval);
141
+ idx++;
142
+ }
143
+ }
144
+ function projectWeekly(rec, range, startDate, effectiveUntil, mondayStart, out) {
145
+ const interval = rec.interval ?? 1;
146
+ const days = normalizeDays(rec.dayOfWeek);
147
+ if (days.length === 0)
148
+ return;
149
+ const startDateMs = startDate?.getTime();
150
+ const rangeStartMs = range.start.getTime();
151
+ const rangeEndMs = range.end.getTime();
152
+ // Anchor week: week containing startDate, or range.start if none
153
+ const anchorMs = startDateMs ?? rangeStartMs;
154
+ const anchorWeekMs = startOfWeek(anchorMs, mondayStart);
155
+ // Jump to first interval-aligned week that could overlap the range
156
+ let weekMs;
157
+ if (anchorWeekMs >= rangeStartMs) {
158
+ weekMs = anchorWeekMs;
159
+ }
160
+ else {
161
+ const weeksBetween = Math.floor((rangeStartMs - anchorWeekMs) / (7 * DAY_MS));
162
+ const skip = Math.floor(weeksBetween / interval);
163
+ weekMs = anchorWeekMs + skip * interval * 7 * DAY_MS;
164
+ if (weekMs + 7 * DAY_MS <= rangeStartMs) {
165
+ weekMs += interval * 7 * DAY_MS;
166
+ }
167
+ }
168
+ let weekIndex = Math.round((weekMs - anchorWeekMs) / (7 * DAY_MS));
169
+ while (weekMs < rangeEndMs) {
170
+ for (const day of days) {
171
+ const dayMs = weekMs + isoWeekdayToOffset(day) * DAY_MS;
172
+ if (startDateMs != null && dayMs < startDateMs)
173
+ continue;
174
+ if (dayMs >= rangeEndMs)
175
+ break;
176
+ if (effectiveUntil && dayMs > effectiveUntil.getTime())
177
+ return;
178
+ const dayDate = new Date(dayMs);
179
+ const ev = createConcreteEvent(rec, dayDate, `w${weekIndex}--d${day}`);
180
+ if (ev.start < range.end && ev.end > range.start) {
181
+ out.push(ev);
182
+ }
183
+ }
184
+ weekMs += interval * 7 * DAY_MS;
185
+ weekIndex += interval;
186
+ }
187
+ }
188
+ function projectMonthly(rec, range, startDate, effectiveUntil, out) {
189
+ const interval = rec.interval ?? 1;
190
+ const dayOfMonth = rec.dayOfMonth ?? 1;
191
+ const anchor = startDate ?? range.start;
192
+ let totalMonths = anchor.getFullYear() * 12 + anchor.getMonth();
193
+ // Skip ahead to first interval-aligned month at or near range.start
194
+ if (startDate && startDate < range.start) {
195
+ const rangeMonths = range.start.getFullYear() * 12 + range.start.getMonth();
196
+ const gap = rangeMonths - totalMonths;
197
+ const skip = Math.floor(gap / interval);
198
+ totalMonths += skip * interval;
199
+ }
200
+ for (let i = 0; i < 120; i++) {
201
+ const y = Math.floor(totalMonths / 12);
202
+ const m = totalMonths % 12;
203
+ const date = clampDayOfMonth(y, m, dayOfMonth);
204
+ if (date >= range.end)
205
+ break;
206
+ if (effectiveUntil && date > effectiveUntil)
207
+ break;
208
+ if (date >= range.start && (!startDate || date >= startDate)) {
209
+ const ev = createConcreteEvent(rec, date, `m${y}-${String(m + 1).padStart(2, '0')}`);
210
+ if (ev.start < range.end && ev.end > range.start) {
211
+ out.push(ev);
212
+ }
213
+ }
214
+ totalMonths += interval;
215
+ }
216
+ }
217
+ // ── Adapter factory ─────────────────────────────────────
57
218
  /** Default palette for auto-coloring */
58
219
  const AUTO_COLORS = VIVID_PALETTE;
59
220
  /**
60
- * Create a CalendarAdapter that projects recurring weekly events
61
- * onto concrete dates for whatever range the calendar requests.
221
+ * Create a CalendarAdapter that projects recurring events onto concrete
222
+ * dates for whatever range the calendar requests.
62
223
  *
63
- * Read-only by default — create/update/delete throw unless custom handlers are provided.
224
+ * Read-only by default — create/update/delete throw.
64
225
  */
65
226
  export function createRecurringAdapter(schedule, options = {}) {
66
227
  const { mondayStart = true, colorMap, autoColor } = options;
67
- // Resolve palette: vivid default or theme-aware
228
+ // Resolve palette
68
229
  const palette = autoColor
69
230
  ? typeof autoColor === 'string'
70
231
  ? generatePalette(autoColor)
@@ -93,16 +254,33 @@ export function createRecurringAdapter(schedule, options = {}) {
93
254
  }
94
255
  return {
95
256
  async fetchEvents(range) {
96
- const weeks = getOverlappingWeeks(range, mondayStart);
97
257
  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
- }
258
+ for (const rec of schedule) {
259
+ const colored = { ...rec, color: resolveColor(rec) };
260
+ const freq = rec.frequency ?? 'weekly';
261
+ // Parse bounds
262
+ const sd = rec.startDate ? parseDate(rec.startDate) : undefined;
263
+ const untilDate = rec.until ? parseDate(rec.until) : undefined;
264
+ const countUntil = sd
265
+ ? computeUntilFromCount(rec, sd, mondayStart)
266
+ : undefined;
267
+ // Effective until = tighter of the two bounds
268
+ let effectiveUntil = untilDate;
269
+ if (countUntil) {
270
+ effectiveUntil = effectiveUntil
271
+ ? countUntil < effectiveUntil ? countUntil : effectiveUntil
272
+ : countUntil;
273
+ }
274
+ switch (freq) {
275
+ case 'daily':
276
+ projectDaily(colored, range, sd, effectiveUntil, events);
277
+ break;
278
+ case 'weekly':
279
+ projectWeekly(colored, range, sd, effectiveUntil, mondayStart, events);
280
+ break;
281
+ case 'monthly':
282
+ projectMonthly(colored, range, sd, effectiveUntil, events);
283
+ break;
106
284
  }
107
285
  }
108
286
  return events;
@@ -16,7 +16,12 @@ export function createRestAdapter(options) {
16
16
  }
17
17
  if (res.status === 204)
18
18
  return undefined;
19
- return res.json();
19
+ try {
20
+ return await res.json();
21
+ }
22
+ catch {
23
+ throw new Error(`Calendar API error: invalid JSON response from ${path}`);
24
+ }
20
25
  }
21
26
  return {
22
27
  async fetchEvents(range) {