@nomideusz/svelte-calendar 0.4.0 → 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.
- package/README.md +331 -568
- package/dist/adapters/recurring.d.ts +45 -8
- package/dist/adapters/recurring.js +227 -49
- package/dist/adapters/rest.js +6 -1
- package/dist/calendar/Calendar.svelte +118 -30
- package/dist/calendar/Calendar.svelte.d.ts +9 -8
- package/dist/calendar/index.d.ts +0 -1
- package/dist/calendar/index.js +0 -1
- package/dist/core/index.d.ts +5 -4
- package/dist/core/index.js +2 -3
- package/dist/core/locale.d.ts +74 -0
- package/dist/core/locale.js +96 -4
- package/dist/core/time.d.ts +30 -0
- package/dist/core/time.js +42 -0
- package/dist/core/types.d.ts +1 -72
- package/dist/core/types.js +1 -8
- package/dist/engine/event-store.svelte.js +2 -2
- package/dist/engine/index.d.ts +1 -1
- package/dist/engine/view-state.svelte.d.ts +11 -6
- package/dist/engine/view-state.svelte.js +5 -4
- package/dist/index.d.ts +6 -7
- package/dist/index.js +4 -4
- package/dist/primitives/EmptySlot.svelte +6 -16
- package/dist/primitives/EventBlock.svelte +6 -19
- package/dist/primitives/NowIndicator.svelte +7 -4
- package/dist/primitives/TimeGutter.svelte +1 -1
- package/dist/theme/index.d.ts +1 -1
- package/dist/theme/index.js +1 -1
- package/dist/theme/presets.d.ts +11 -19
- package/dist/theme/presets.js +25 -111
- package/dist/views/agenda/Agenda.svelte +17 -1516
- package/dist/views/agenda/Agenda.svelte.d.ts +0 -14
- package/dist/views/agenda/AgendaDay.svelte +976 -0
- package/dist/views/agenda/AgendaDay.svelte.d.ts +14 -0
- package/dist/views/agenda/AgendaWeek.svelte +775 -0
- package/dist/views/agenda/AgendaWeek.svelte.d.ts +15 -0
- package/dist/views/agenda/index.d.ts +2 -0
- package/dist/views/agenda/index.js +2 -0
- package/dist/views/index.d.ts +2 -6
- package/dist/views/index.js +2 -6
- package/dist/views/planner/Planner.svelte +17 -0
- package/dist/views/planner/Planner.svelte.d.ts +7 -0
- package/dist/views/planner/PlannerDay.svelte +957 -0
- package/dist/views/{day/DayGrid.svelte.d.ts → planner/PlannerDay.svelte.d.ts} +6 -16
- package/dist/views/planner/PlannerWeek.svelte +826 -0
- package/dist/views/{week/WeekGrid.svelte.d.ts → planner/PlannerWeek.svelte.d.ts} +4 -9
- package/dist/views/planner/index.d.ts +1 -0
- package/dist/views/planner/index.js +1 -0
- package/dist/widget/CalendarWidget.svelte +54 -33
- package/dist/widget/CalendarWidget.svelte.d.ts +1 -1
- package/package.json +70 -65
- package/widget/svelte-calendar.css +1 -0
- package/widget/widget.js +122 -260
- package/dist/calendar/Toolbar.svelte +0 -244
- package/dist/calendar/Toolbar.svelte.d.ts +0 -21
- package/dist/views/day/DayGrid.svelte +0 -1104
- package/dist/views/day/DayTimeline.svelte +0 -533
- package/dist/views/day/DayTimeline.svelte.d.ts +0 -8
- package/dist/views/day/index.d.ts +0 -2
- package/dist/views/day/index.js +0 -2
- package/dist/views/schedule/WeekSchedule.svelte +0 -137
- package/dist/views/schedule/WeekSchedule.svelte.d.ts +0 -40
- package/dist/views/schedule/index.d.ts +0 -1
- package/dist/views/schedule/index.js +0 -2
- package/dist/views/settings/Settings.svelte +0 -585
- package/dist/views/settings/Settings.svelte.d.ts +0 -39
- package/dist/views/settings/index.d.ts +0 -2
- package/dist/views/settings/index.js +0 -1
- package/dist/views/week/WeekGrid.svelte +0 -632
- package/dist/views/week/WeekHeatmap.svelte +0 -461
- package/dist/views/week/WeekHeatmap.svelte.d.ts +0 -9
- package/dist/views/week/index.d.ts +0 -2
- package/dist/views/week/index.js +0 -2
|
@@ -1,16 +1,54 @@
|
|
|
1
1
|
import type { CalendarAdapter } from './types.js';
|
|
2
2
|
/**
|
|
3
|
-
* A
|
|
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
|
|
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
|
|
40
|
-
*
|
|
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
|
|
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
|
|
7
|
-
|
|
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
|
-
|
|
11
|
-
|
|
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;
|
|
35
|
+
return dayOfWeek - 1;
|
|
15
36
|
}
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
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(
|
|
26
|
-
const end = new Date(
|
|
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}
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
*
|
|
44
|
-
*
|
|
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
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
61
|
-
*
|
|
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
|
|
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
|
|
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
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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;
|
package/dist/adapters/rest.js
CHANGED
|
@@ -16,7 +16,12 @@ export function createRestAdapter(options) {
|
|
|
16
16
|
}
|
|
17
17
|
if (res.status === 204)
|
|
18
18
|
return undefined;
|
|
19
|
-
|
|
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) {
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
Calendar — the unified orchestrator.
|
|
3
3
|
|
|
4
|
-
Brings together: adapter → event store → view state + selection →
|
|
4
|
+
Brings together: adapter → event store → view state + selection → active view.
|
|
5
5
|
Provides context so any descendant view can read the store/state via getContext().
|
|
6
6
|
|
|
7
7
|
Usage:
|
|
8
8
|
<Calendar
|
|
9
9
|
adapter={myAdapter}
|
|
10
10
|
views={[
|
|
11
|
-
{ id: 'week-
|
|
12
|
-
{ id: 'week-agenda',
|
|
13
|
-
{ id: 'day',
|
|
11
|
+
{ id: 'week-planner', label: 'Planner', component: Planner, props: { mode: 'week' } },
|
|
12
|
+
{ id: 'week-agenda', label: 'Agenda', component: Agenda },
|
|
13
|
+
{ id: 'day-planner', label: 'Planner', component: Planner, props: { mode: 'day' } },
|
|
14
14
|
]}
|
|
15
|
-
defaultView="week-
|
|
15
|
+
defaultView="week-planner"
|
|
16
16
|
theme={presets.midnight}
|
|
17
17
|
height={600}
|
|
18
18
|
oneventclick={handleClick}
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
/>
|
|
21
21
|
-->
|
|
22
22
|
<script lang="ts">
|
|
23
|
-
import { setContext, untrack, type Component } from 'svelte';
|
|
23
|
+
import { setContext, untrack, type Component, type Snippet } from 'svelte';
|
|
24
24
|
import type { CalendarAdapter } from '../adapters/types.js';
|
|
25
25
|
import type { CalendarViewId } from '../engine/view-state.svelte.js';
|
|
26
26
|
import { createEventStore, type EventStore } from '../engine/event-store.svelte.js';
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
import { createSelection, type Selection } from '../engine/selection.svelte.js';
|
|
29
29
|
import { createDragState, type DragState } from '../engine/drag.svelte.js';
|
|
30
30
|
import type { TimelineEvent } from '../core/types.js';
|
|
31
|
-
import
|
|
31
|
+
import { getLabels } from '../core/locale.js';
|
|
32
32
|
|
|
33
33
|
/** One view registration */
|
|
34
34
|
export interface CalendarView {
|
|
@@ -55,10 +55,6 @@
|
|
|
55
55
|
mondayStart?: boolean;
|
|
56
56
|
/** Total height */
|
|
57
57
|
height?: number;
|
|
58
|
-
/** Show toolbar */
|
|
59
|
-
showToolbar?: boolean;
|
|
60
|
-
/** Links to display in toolbar */
|
|
61
|
-
links?: { href: string; label: string }[];
|
|
62
58
|
/** Text direction: 'ltr' (default), 'rtl', or 'auto' */
|
|
63
59
|
dir?: 'ltr' | 'rtl' | 'auto';
|
|
64
60
|
/** BCP 47 locale tag (e.g. 'en-US', 'ar-SA') — sets lang and locale for formatting */
|
|
@@ -67,6 +63,16 @@
|
|
|
67
63
|
readOnly?: boolean;
|
|
68
64
|
/** Visible hour range: [startHour, endHour). Crops the grid to these hours. */
|
|
69
65
|
visibleHours?: [number, number];
|
|
66
|
+
/** Initial date to focus on (defaults to today) */
|
|
67
|
+
initialDate?: Date;
|
|
68
|
+
/** Drag snap interval in minutes (default: 15) */
|
|
69
|
+
snapInterval?: number;
|
|
70
|
+
|
|
71
|
+
// ── Snippets ──
|
|
72
|
+
/** Custom event rendering snippet */
|
|
73
|
+
event?: Snippet<[TimelineEvent]>;
|
|
74
|
+
/** Content to show when no events are loaded */
|
|
75
|
+
empty?: Snippet;
|
|
70
76
|
|
|
71
77
|
// ── Callbacks ──
|
|
72
78
|
oneventclick?: (event: TimelineEvent) => void;
|
|
@@ -78,16 +84,18 @@
|
|
|
78
84
|
let {
|
|
79
85
|
adapter,
|
|
80
86
|
views = [],
|
|
81
|
-
defaultView = 'week-
|
|
87
|
+
defaultView = 'week-planner',
|
|
82
88
|
theme = '',
|
|
83
89
|
mondayStart = true,
|
|
84
90
|
height = 600,
|
|
85
|
-
showToolbar = true,
|
|
86
|
-
links = [],
|
|
87
91
|
dir,
|
|
88
92
|
locale,
|
|
89
93
|
readOnly = false,
|
|
90
94
|
visibleHours,
|
|
95
|
+
initialDate,
|
|
96
|
+
snapInterval = 15,
|
|
97
|
+
event: eventSnippet,
|
|
98
|
+
empty: emptySnippet,
|
|
91
99
|
oneventclick,
|
|
92
100
|
oneventcreate,
|
|
93
101
|
oneventmove,
|
|
@@ -98,18 +106,13 @@
|
|
|
98
106
|
const effectiveCreate = $derived(readOnly ? undefined : oneventcreate);
|
|
99
107
|
const effectiveMove = $derived(readOnly ? undefined : oneventmove);
|
|
100
108
|
|
|
101
|
-
import { setDefaultLocale } from '../core/locale.js';
|
|
102
|
-
|
|
103
|
-
// ── Set locale when provided ──
|
|
104
|
-
$effect(() => {
|
|
105
|
-
if (locale) setDefaultLocale(locale);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
109
|
// ── Create reactive state ──
|
|
109
110
|
const store: EventStore = $derived(createEventStore(adapter));
|
|
110
111
|
const viewState: ViewState = createViewState(untrack(() => ({
|
|
111
112
|
defaultView,
|
|
112
113
|
mondayStart,
|
|
114
|
+
initialDate,
|
|
115
|
+
granularityForView: (viewId) => views.find((v) => v.id === viewId)?.granularity,
|
|
113
116
|
})));
|
|
114
117
|
const selection: Selection = createSelection();
|
|
115
118
|
const drag: DragState = createDragState();
|
|
@@ -148,6 +151,9 @@
|
|
|
148
151
|
});
|
|
149
152
|
setContext('calendar:readOnly', { get current() { return readOnly; } });
|
|
150
153
|
setContext('calendar:visibleHours', { get current() { return visibleHours; } });
|
|
154
|
+
setContext('calendar:snapInterval', { get current() { return snapInterval; } });
|
|
155
|
+
setContext('calendar:eventSnippet', { get current() { return eventSnippet; } });
|
|
156
|
+
setContext('calendar:emptySnippet', { get current() { return emptySnippet; } });
|
|
151
157
|
|
|
152
158
|
// ── Load events when range changes ──
|
|
153
159
|
$effect(() => {
|
|
@@ -167,7 +173,7 @@
|
|
|
167
173
|
}
|
|
168
174
|
});
|
|
169
175
|
|
|
170
|
-
// Notify host when active view changes (e.g. via
|
|
176
|
+
// Notify host when active view changes (e.g. via granularity toggles).
|
|
171
177
|
$effect(() => {
|
|
172
178
|
onviewchange?.(viewState.view);
|
|
173
179
|
});
|
|
@@ -175,21 +181,58 @@
|
|
|
175
181
|
// ── Resolve active view ──
|
|
176
182
|
const activeView = $derived(views.find((v) => v.id === viewState.view) ?? views[0]);
|
|
177
183
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
184
|
+
// ── Date label (always visible, centered over views) ──
|
|
185
|
+
const dateLabel = $derived.by(() => {
|
|
186
|
+
if (viewState.granularity === 'day') {
|
|
187
|
+
return viewState.focusDate.toLocaleDateString(locale, {
|
|
188
|
+
weekday: 'long',
|
|
189
|
+
month: 'short',
|
|
190
|
+
day: 'numeric',
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return viewState.focusDate.toLocaleDateString(locale, {
|
|
194
|
+
month: 'long',
|
|
195
|
+
year: 'numeric',
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Which granularities are available?
|
|
200
|
+
const granularities = $derived.by(() => {
|
|
201
|
+
const g = new Set(views.map((v) => v.granularity));
|
|
202
|
+
return (['day', 'week'] as const).filter((key) => g.has(key));
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const L = $derived(getLabels());
|
|
181
206
|
</script>
|
|
182
207
|
|
|
183
208
|
<div
|
|
184
209
|
class="cal"
|
|
185
210
|
style="{theme}; --cal-h: {height}px"
|
|
186
211
|
role="region"
|
|
187
|
-
aria-label=
|
|
212
|
+
aria-label={L.calendar}
|
|
188
213
|
dir={dir}
|
|
189
214
|
lang={locale}
|
|
190
215
|
>
|
|
191
|
-
|
|
192
|
-
|
|
216
|
+
<!-- Floating granularity pills (hidden for Agenda views) -->
|
|
217
|
+
{#if granularities.length > 1 && activeView?.label !== 'Agenda'}
|
|
218
|
+
<div class="cal-pills" role="group" aria-label={L.viewMode}>
|
|
219
|
+
{#each granularities as g}
|
|
220
|
+
<button
|
|
221
|
+
class="cal-pill"
|
|
222
|
+
class:cal-pill--active={viewState.granularity === g}
|
|
223
|
+
aria-pressed={viewState.granularity === g}
|
|
224
|
+
onclick={() => {
|
|
225
|
+
const currentLabel = views.find((v) => v.id === viewState.view)?.label;
|
|
226
|
+
const match = views.find((v) => v.granularity === g && v.label === currentLabel);
|
|
227
|
+
const fallback = views.find((v) => v.granularity === g);
|
|
228
|
+
const target = match ?? fallback;
|
|
229
|
+
if (target) viewState.setView(target.id);
|
|
230
|
+
}}
|
|
231
|
+
>
|
|
232
|
+
{g === 'day' ? L.day : L.week}
|
|
233
|
+
</button>
|
|
234
|
+
{/each}
|
|
235
|
+
</div>
|
|
193
236
|
{/if}
|
|
194
237
|
|
|
195
238
|
<div class="cal-body">
|
|
@@ -198,7 +241,7 @@
|
|
|
198
241
|
<Comp
|
|
199
242
|
events={store.events}
|
|
200
243
|
style={theme}
|
|
201
|
-
height={
|
|
244
|
+
height={null}
|
|
202
245
|
mondayStart={viewState.mondayStart}
|
|
203
246
|
{locale}
|
|
204
247
|
focusDate={viewState.focusDate}
|
|
@@ -225,15 +268,60 @@
|
|
|
225
268
|
height: var(--cal-h, 600px);
|
|
226
269
|
background: var(--dt-bg, #0b0e14);
|
|
227
270
|
border-radius: 12px;
|
|
228
|
-
overflow:
|
|
271
|
+
overflow: clip;
|
|
229
272
|
display: flex;
|
|
230
273
|
flex-direction: column;
|
|
231
274
|
border: 1px solid var(--dt-border, rgba(148, 163, 184, 0.07));
|
|
232
275
|
}
|
|
233
276
|
|
|
277
|
+
/* ── Floating pills ── */
|
|
278
|
+
.cal-pills {
|
|
279
|
+
position: absolute;
|
|
280
|
+
top: 22px;
|
|
281
|
+
bottom: auto;
|
|
282
|
+
left: 10px;
|
|
283
|
+
z-index: 20;
|
|
284
|
+
display: flex;
|
|
285
|
+
gap: 2px;
|
|
286
|
+
background: color-mix(in srgb, var(--dt-surface, #10141c) 85%, transparent);
|
|
287
|
+
backdrop-filter: blur(6px);
|
|
288
|
+
-webkit-backdrop-filter: blur(6px);
|
|
289
|
+
border-radius: 8px;
|
|
290
|
+
padding: 2px;
|
|
291
|
+
border: 1px solid var(--dt-border, rgba(148, 163, 184, 0.07));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.cal-pill {
|
|
295
|
+
border: none;
|
|
296
|
+
background: transparent;
|
|
297
|
+
color: var(--dt-text-2, rgba(148, 163, 184, 0.55));
|
|
298
|
+
cursor: pointer;
|
|
299
|
+
font: 600 11px / 1 var(--dt-sans, 'Outfit', system-ui, sans-serif);
|
|
300
|
+
padding: 6px 12px;
|
|
301
|
+
border-radius: 6px;
|
|
302
|
+
letter-spacing: 0.04em;
|
|
303
|
+
text-transform: uppercase;
|
|
304
|
+
transition: background 100ms, color 100ms;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.cal-pill:hover {
|
|
308
|
+
color: var(--dt-text, rgba(226, 232, 240, 0.85));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.cal-pill--active {
|
|
312
|
+
background: var(--dt-accent, #ef4444);
|
|
313
|
+
color: var(--dt-btn-text, #fff);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.cal-pill:focus-visible {
|
|
317
|
+
outline: 2px solid color-mix(in srgb, var(--dt-accent, #ef4444) 55%, transparent);
|
|
318
|
+
outline-offset: 2px;
|
|
319
|
+
}
|
|
320
|
+
|
|
234
321
|
.cal-body {
|
|
235
322
|
flex: 1;
|
|
236
323
|
min-height: 0;
|
|
324
|
+
position: relative;
|
|
237
325
|
overflow: hidden;
|
|
238
326
|
}
|
|
239
327
|
|