@nomideusz/svelte-calendar 0.20.1 → 1.0.0
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 +502 -267
- package/dist/adapters/composite.d.ts +12 -1
- package/dist/adapters/composite.js +109 -47
- package/dist/adapters/errors.d.ts +22 -0
- package/dist/adapters/errors.js +32 -0
- package/dist/adapters/index.d.ts +1 -0
- package/dist/adapters/index.js +1 -0
- package/dist/adapters/jmap.js +26 -17
- package/dist/adapters/mapped.d.ts +2 -2
- package/dist/adapters/mapped.js +18 -9
- package/dist/adapters/memory.d.ts +1 -1
- package/dist/adapters/memory.js +5 -3
- package/dist/adapters/recurring.d.ts +1 -1
- package/dist/adapters/recurring.js +43 -25
- package/dist/adapters/rest.d.ts +2 -2
- package/dist/adapters/rest.js +9 -4
- package/dist/calendar/Calendar.svelte +122 -78
- package/dist/calendar/Calendar.svelte.d.ts +69 -14
- package/dist/calendar/index.d.ts +2 -2
- package/dist/calendar/index.js +1 -1
- package/dist/core/clock.svelte.js +3 -2
- package/dist/core/locale.d.ts +11 -1
- package/dist/core/locale.js +16 -16
- package/dist/core/time.d.ts +9 -0
- package/dist/core/time.js +12 -5
- package/dist/core/timezone.d.ts +7 -3
- package/dist/core/timezone.js +10 -4
- package/dist/engine/event-store.svelte.js +94 -57
- package/dist/engine/view-state.svelte.d.ts +5 -1
- package/dist/engine/view-state.svelte.js +8 -6
- package/dist/headless/create-agenda.svelte.d.ts +8 -1
- package/dist/headless/create-agenda.svelte.js +36 -18
- package/dist/headless/create-calendar.svelte.js +57 -21
- package/dist/headless/create-range-agenda.svelte.d.ts +7 -0
- package/dist/headless/create-range-agenda.svelte.js +38 -17
- package/dist/headless/index.d.ts +1 -0
- package/dist/index.d.ts +10 -7
- package/dist/index.js +8 -5
- package/dist/primitives/DayHeader.svelte +3 -2
- package/dist/primitives/DayHeader.svelte.d.ts +1 -1
- package/dist/primitives/EmptySlot.svelte +5 -2
- package/dist/primitives/EventBlock.svelte +21 -14
- package/dist/primitives/FloatingPanel.svelte +110 -15
- package/dist/primitives/FloatingPanel.svelte.d.ts +7 -5
- package/dist/primitives/NowIndicator.svelte +11 -4
- package/dist/primitives/TimeGutter.svelte +10 -6
- package/dist/primitives/index.d.ts +1 -0
- package/dist/text-fit.d.ts +8 -2
- package/dist/views/agenda/AgendaDay.svelte +43 -35
- package/dist/views/agenda/AgendaWeek.svelte +75 -38
- package/dist/views/index.d.ts +2 -0
- package/dist/views/index.js +2 -0
- package/dist/views/mobile/MobileDay.svelte +99 -42
- package/dist/views/mobile/MobileWeek.svelte +31 -15
- package/dist/views/mobile/swipe.js +6 -1
- package/dist/views/month/MonthGrid.svelte +33 -21
- package/dist/views/month/MonthGrid.svelte.d.ts +1 -1
- package/dist/views/planner/PlannerScroll.svelte +438 -164
- package/dist/views/planner/PlannerWeek.svelte +150 -50
- package/dist/views/shared/context.svelte.d.ts +1 -1
- package/package.json +24 -9
- package/widget/widget.js +10000 -9520
- package/dist/assets/favicon.svg +0 -9
- package/dist/widget/CalendarWidget.svelte +0 -114
- package/dist/widget/CalendarWidget.svelte.d.ts +0 -37
- package/dist/widget/index.d.ts +0 -1
- package/dist/widget/index.js +0 -1
- package/dist/widget/widget.d.ts +0 -10
- package/dist/widget/widget.js +0 -96
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
* vs.prev() — go back one period
|
|
12
12
|
* vs.goToday() — jump to today
|
|
13
13
|
*/
|
|
14
|
-
import { startOfWeek as calcStartOfWeek, addDaysMs
|
|
14
|
+
import { startOfWeek as calcStartOfWeek, addDaysMs } from '../core/time.js';
|
|
15
|
+
import { nowInZone } from '../core/timezone.js';
|
|
15
16
|
function inferMode(view) {
|
|
16
17
|
if (view.startsWith('day'))
|
|
17
18
|
return 'day';
|
|
@@ -23,7 +24,7 @@ function computeRange(focus, mode, mondayStart, dayCount = 7) {
|
|
|
23
24
|
if (mode === 'day') {
|
|
24
25
|
const start = new Date(focus);
|
|
25
26
|
start.setHours(0, 0, 0, 0);
|
|
26
|
-
const end = new Date(start.getTime()
|
|
27
|
+
const end = new Date(addDaysMs(start.getTime(), 1));
|
|
27
28
|
return { start, end };
|
|
28
29
|
}
|
|
29
30
|
if (mode === 'month') {
|
|
@@ -48,15 +49,16 @@ function computeRange(focus, mode, mondayStart, dayCount = 7) {
|
|
|
48
49
|
start.setHours(0, 0, 0, 0);
|
|
49
50
|
return {
|
|
50
51
|
start,
|
|
51
|
-
end: new Date(start.getTime()
|
|
52
|
+
end: new Date(addDaysMs(start.getTime(), dayCount)),
|
|
52
53
|
};
|
|
53
54
|
}
|
|
54
55
|
export function createViewState(options = {}) {
|
|
56
|
+
const timezone = options.timezone;
|
|
57
|
+
const today = () => (timezone ? nowInZone(timezone) : new Date());
|
|
55
58
|
let view = $state(options.view ?? 'week-planner');
|
|
56
|
-
let focusDate = $state(options.initialDate ??
|
|
59
|
+
let focusDate = $state(options.initialDate ?? today());
|
|
57
60
|
let mondayStart = $state(options.mondayStart ?? true);
|
|
58
61
|
let dayCount = $state(options.dayCount ?? 7);
|
|
59
|
-
const timezone = options.timezone;
|
|
60
62
|
const modeResolver = options.modeForView;
|
|
61
63
|
const mode = $derived(modeResolver?.(view) ?? inferMode(view));
|
|
62
64
|
const range = $derived(computeRange(focusDate, mode, mondayStart, dayCount));
|
|
@@ -112,7 +114,7 @@ export function createViewState(options = {}) {
|
|
|
112
114
|
focusDate = new Date(addDaysMs(focusDate.getTime(), days));
|
|
113
115
|
},
|
|
114
116
|
goToday() {
|
|
115
|
-
focusDate =
|
|
117
|
+
focusDate = today();
|
|
116
118
|
},
|
|
117
119
|
};
|
|
118
120
|
}
|
|
@@ -9,6 +9,13 @@ export interface AgendaOptions {
|
|
|
9
9
|
locale?: string;
|
|
10
10
|
/** Number of days to load ahead of focus date for the upcoming list (default: 7) */
|
|
11
11
|
lookahead?: number;
|
|
12
|
+
/**
|
|
13
|
+
* Show the agenda in an IANA timezone (e.g. 'Europe/Warsaw'), as the
|
|
14
|
+
* Calendar's `timezone` prop does: the day, "now" and every event Date are
|
|
15
|
+
* wall-clock values in that zone. `initialDate` and `setDate()` take real
|
|
16
|
+
* instants. Default: the viewer's zone.
|
|
17
|
+
*/
|
|
18
|
+
timezone?: string;
|
|
12
19
|
}
|
|
13
20
|
export interface HeadlessAgenda {
|
|
14
21
|
/** The focused date */
|
|
@@ -29,7 +36,7 @@ export interface HeadlessAgenda {
|
|
|
29
36
|
readonly dayEvents: TimelineEvent[];
|
|
30
37
|
/** All-day or multi-day events */
|
|
31
38
|
readonly allDay: TimelineEvent[];
|
|
32
|
-
/** Timed events that have ended
|
|
39
|
+
/** Timed events that have ended — measured against now, so every event of a past day and none of a future one */
|
|
33
40
|
readonly past: TimelineEvent[];
|
|
34
41
|
/** Timed events currently in progress */
|
|
35
42
|
readonly current: TimelineEvent[];
|
|
@@ -33,33 +33,51 @@
|
|
|
33
33
|
import { untrack } from 'svelte';
|
|
34
34
|
import { createEventStore } from '../engine/event-store.svelte.js';
|
|
35
35
|
import { createClock } from '../core/clock.svelte.js';
|
|
36
|
-
import { sod,
|
|
36
|
+
import { sod, addDaysMs, isAllDay, isMultiDay, overlapsRange } from '../core/time.js';
|
|
37
|
+
import { toZonedTime, wrapAdapterWithTimezone } from '../core/timezone.js';
|
|
37
38
|
import { fmtTime as _fmtTime, fmtDuration } from '../core/locale.js';
|
|
38
39
|
import { timeUntilMs, progress as _progress, groupIntoSlots } from '../views/shared/format.js';
|
|
39
40
|
// ─── Implementation ─────────────────────────────────────
|
|
40
41
|
export function createAgenda(options) {
|
|
41
|
-
const { initialDate, locale, lookahead = 7, } = options;
|
|
42
|
-
const
|
|
42
|
+
const { initialDate, locale, lookahead = 7, timezone, } = options;
|
|
43
|
+
const rawAdapter = typeof options.adapter === 'function'
|
|
43
44
|
? options.adapter
|
|
44
45
|
: () => options.adapter;
|
|
45
|
-
const
|
|
46
|
-
|
|
46
|
+
const resolveAdapter = timezone
|
|
47
|
+
? () => wrapAdapterWithTimezone(rawAdapter(), timezone)
|
|
48
|
+
: rawAdapter;
|
|
49
|
+
/** An instant on the plane the day is drawn on (zoned wall-clock with `timezone`). */
|
|
50
|
+
const plane = (ms) => (timezone ? toZonedTime(ms, timezone).getTime() : ms);
|
|
51
|
+
// One store for the agenda's life: a getter adapter that changes identity
|
|
52
|
+
// reloads it instead of rebuilding it empty (every row blinking out).
|
|
53
|
+
const store = createEventStore(resolveAdapter);
|
|
54
|
+
const clock = createClock(timezone);
|
|
47
55
|
// ── Focus date (reactive, writable) ──
|
|
48
|
-
let focusDayMs = $state(sod(initialDate
|
|
56
|
+
let focusDayMs = $state(sod(initialDate ? plane(initialDate.getTime()) : clock.tick));
|
|
49
57
|
// ── Load events for focus date range ──
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
58
|
+
// Eager initial load for synchronous adapters (the server render holds
|
|
59
|
+
// the rows); the effect's first run then has nothing new to fetch — a
|
|
60
|
+
// seeded adapter would otherwise refetch the seed at once.
|
|
61
|
+
let eagerKey = untrack(() => {
|
|
62
|
+
if (!resolveAdapter().fetchEventsSync)
|
|
63
|
+
return '';
|
|
64
|
+
store.load({ start: new Date(focusDayMs), end: new Date(addDaysMs(focusDayMs, lookahead)) });
|
|
65
|
+
return `${focusDayMs}`;
|
|
54
66
|
});
|
|
55
|
-
|
|
56
|
-
|
|
67
|
+
$effect(() => {
|
|
68
|
+
rawAdapter(); // a new adapter reloads
|
|
57
69
|
const start = new Date(focusDayMs);
|
|
58
|
-
const end = new Date(focusDayMs
|
|
70
|
+
const end = new Date(addDaysMs(focusDayMs, lookahead));
|
|
71
|
+
const key = `${focusDayMs}`;
|
|
72
|
+
if (key === eagerKey) {
|
|
73
|
+
eagerKey = '';
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
eagerKey = '';
|
|
59
77
|
store.load({ start, end });
|
|
60
78
|
});
|
|
61
79
|
// ── Day derivations ──
|
|
62
|
-
const dayEnd = $derived(focusDayMs
|
|
80
|
+
const dayEnd = $derived(addDaysMs(focusDayMs, 1));
|
|
63
81
|
const isToday = $derived(focusDayMs === clock.today);
|
|
64
82
|
const isPast = $derived(focusDayMs < clock.today);
|
|
65
83
|
const isFuture = $derived(focusDayMs > clock.today);
|
|
@@ -70,7 +88,7 @@ export function createAgenda(options) {
|
|
|
70
88
|
}));
|
|
71
89
|
const dayEvents = $derived.by(() => {
|
|
72
90
|
return store.events
|
|
73
|
-
.filter((ev) => ev
|
|
91
|
+
.filter((ev) => overlapsRange(ev, new Date(focusDayMs), new Date(dayEnd)))
|
|
74
92
|
.sort((a, b) => a.start.getTime() - b.start.getTime());
|
|
75
93
|
});
|
|
76
94
|
const allDay = $derived(dayEvents.filter((ev) => isAllDay(ev) || isMultiDay(ev)));
|
|
@@ -93,10 +111,10 @@ export function createAgenda(options) {
|
|
|
93
111
|
return { past, current, upcoming, upcomingSlots: groupIntoSlots(upcoming) };
|
|
94
112
|
});
|
|
95
113
|
// ── Navigation ──
|
|
96
|
-
function prev() { focusDayMs = focusDayMs -
|
|
97
|
-
function next() { focusDayMs = focusDayMs
|
|
114
|
+
function prev() { focusDayMs = addDaysMs(focusDayMs, -1); }
|
|
115
|
+
function next() { focusDayMs = addDaysMs(focusDayMs, 1); }
|
|
98
116
|
function goToday() { focusDayMs = clock.today; }
|
|
99
|
-
function setDate(date) { focusDayMs = sod(date.getTime()); }
|
|
117
|
+
function setDate(date) { focusDayMs = sod(plane(date.getTime())); }
|
|
100
118
|
// ── Format helpers ──
|
|
101
119
|
const fmtTime = (d) => _fmtTime(d, locale);
|
|
102
120
|
const fmtDur = (ev) => fmtDuration(ev.start, ev.end);
|
|
@@ -43,23 +43,25 @@ import { untrack } from 'svelte';
|
|
|
43
43
|
import { createEventStore } from '../engine/event-store.svelte.js';
|
|
44
44
|
import { createViewState } from '../engine/view-state.svelte.js';
|
|
45
45
|
import { toZonedTime, fromZonedTime, wrapAdapterWithTimezone } from '../core/timezone.js';
|
|
46
|
+
import { isReadOnlyError, isNotFoundError } from '../adapters/errors.js';
|
|
46
47
|
import { createSelection } from '../engine/selection.svelte.js';
|
|
47
48
|
import { createDragState } from '../engine/drag.svelte.js';
|
|
48
49
|
import { createClock } from '../core/clock.svelte.js';
|
|
49
|
-
import { sod,
|
|
50
|
-
import { monthLong, weekdayLong, weekdayShort } from '../core/locale.js';
|
|
50
|
+
import { sod, addDaysMs, HOUR_MS, startOfWeek as sowFn, isAllDay, isMultiDay, segmentForDay, } from '../core/time.js';
|
|
51
|
+
import { monthLong, weekdayLong, weekdayShort, fmtWeekRange, getDefaultLocale } from '../core/locale.js';
|
|
51
52
|
export function createCalendar(options) {
|
|
52
53
|
const { adapter, mondayStart: initialMondayStart = true, initialDate: rawInitialDate, locale, visibleHours, snapInterval = 15, equalDays = false, hideDays, blockedSlots, disabledDates, days: initialDayCount = 7, readOnly = false, minDuration, maxDuration, oneventclick, oneventcreate, oneventmove, } = options;
|
|
53
54
|
// ── Create engine ────────────────────────────────────
|
|
54
55
|
const timezone = options.timezone;
|
|
55
56
|
const initialDate = rawInitialDate && timezone ? toZonedTime(rawInitialDate, timezone) : rawInitialDate;
|
|
56
|
-
const
|
|
57
|
+
const effectiveAdapter = timezone ? wrapAdapterWithTimezone(adapter, timezone) : adapter;
|
|
58
|
+
const store = createEventStore(effectiveAdapter);
|
|
57
59
|
const viewState = createViewState({
|
|
58
60
|
view: options.view ?? 'week-planner',
|
|
59
61
|
mondayStart: initialMondayStart,
|
|
60
62
|
initialDate,
|
|
61
63
|
dayCount: initialDayCount,
|
|
62
|
-
|
|
64
|
+
timezone,
|
|
63
65
|
});
|
|
64
66
|
const selection = createSelection();
|
|
65
67
|
const drag = createDragState();
|
|
@@ -71,12 +73,25 @@ export function createCalendar(options) {
|
|
|
71
73
|
const endHour = visibleHours?.[1] ?? 24;
|
|
72
74
|
const hours = Array.from({ length: endHour - startHour }, (_, i) => startHour + i);
|
|
73
75
|
// ── Load events reactively ───────────────────────────
|
|
76
|
+
// Eager initial load for synchronous adapters (the server render holds the
|
|
77
|
+
// events); the effect's first run then has nothing new to fetch.
|
|
78
|
+
let eagerKey = untrack(() => {
|
|
79
|
+
if (!effectiveAdapter.fetchEventsSync)
|
|
80
|
+
return '';
|
|
81
|
+
const { start, end } = viewState.range;
|
|
82
|
+
store.load({ start, end });
|
|
83
|
+
return `${start.getTime()}-${end.getTime()}`;
|
|
84
|
+
});
|
|
74
85
|
$effect(() => {
|
|
75
86
|
const { start, end } = viewState.range;
|
|
87
|
+
const key = `${start.getTime()}-${end.getTime()}`;
|
|
88
|
+
if (key === eagerKey) {
|
|
89
|
+
eagerKey = '';
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
eagerKey = '';
|
|
76
93
|
store.load({ start, end });
|
|
77
94
|
});
|
|
78
|
-
// Eager initial load
|
|
79
|
-
untrack(() => store.load({ start: viewState.range.start, end: viewState.range.end }));
|
|
80
95
|
// ── Compute day cells ────────────────────────────────
|
|
81
96
|
const days = $derived.by(() => {
|
|
82
97
|
const { start, end } = viewState.range;
|
|
@@ -85,14 +100,14 @@ export function createCalendar(options) {
|
|
|
85
100
|
const events = store.events;
|
|
86
101
|
const todayMs = clock.today;
|
|
87
102
|
const result = [];
|
|
88
|
-
for (let ms = startMs; ms < endMs; ms
|
|
103
|
+
for (let ms = startMs; ms < endMs; ms = addDaysMs(ms, 1)) {
|
|
89
104
|
const date = new Date(ms);
|
|
90
105
|
const jsDay = date.getDay();
|
|
91
106
|
const isoDay = jsDay === 0 ? 7 : jsDay;
|
|
92
107
|
// Skip hidden days
|
|
93
108
|
if (hideDays?.includes(isoDay))
|
|
94
109
|
continue;
|
|
95
|
-
const dayEnd = ms
|
|
110
|
+
const dayEnd = addDaysMs(ms, 1);
|
|
96
111
|
const dayEventsAll = events
|
|
97
112
|
.filter((ev) => ev.start.getTime() < dayEnd && ev.end.getTime() > ms)
|
|
98
113
|
.sort((a, b) => a.start.getTime() - b.start.getTime());
|
|
@@ -134,7 +149,7 @@ export function createCalendar(options) {
|
|
|
134
149
|
if (chunk.length === 0)
|
|
135
150
|
continue;
|
|
136
151
|
const periodStart = chunk[0].ms;
|
|
137
|
-
const periodEnd = periodStart
|
|
152
|
+
const periodEnd = addDaysMs(periodStart, dayCount);
|
|
138
153
|
result.push({
|
|
139
154
|
periodStart,
|
|
140
155
|
isCurrent: todayMs >= periodStart && todayMs < periodEnd,
|
|
@@ -165,21 +180,36 @@ export function createCalendar(options) {
|
|
|
165
180
|
}
|
|
166
181
|
return { past, current, upcoming };
|
|
167
182
|
});
|
|
183
|
+
// "Now" on the same (zoned) plane as the range. A month view's range spills
|
|
184
|
+
// into the neighbouring months, so it compares the focused month instead.
|
|
185
|
+
function includesToday() {
|
|
186
|
+
const now = clock.tick;
|
|
187
|
+
if (viewState.mode === 'month') {
|
|
188
|
+
const f = viewState.focusDate;
|
|
189
|
+
const n = new Date(now);
|
|
190
|
+
return f.getMonth() === n.getMonth() && f.getFullYear() === n.getFullYear();
|
|
191
|
+
}
|
|
192
|
+
const { start, end } = viewState.range;
|
|
193
|
+
return now >= start.getTime() && now < end.getTime();
|
|
194
|
+
}
|
|
168
195
|
// ── Header context ───────────────────────────────────
|
|
169
196
|
const headerContext = $derived.by(() => {
|
|
170
197
|
const mode = viewState.mode;
|
|
171
198
|
const focus = viewState.focusDate;
|
|
172
|
-
const
|
|
173
|
-
const
|
|
174
|
-
const isViewOnToday = now >= start.getTime() && now < end.getTime();
|
|
199
|
+
const isViewOnToday = includesToday();
|
|
200
|
+
const loc = locale ?? getDefaultLocale();
|
|
175
201
|
let dateLabel;
|
|
176
202
|
if (mode === 'day') {
|
|
177
|
-
dateLabel = focus.toLocaleDateString(
|
|
203
|
+
dateLabel = focus.toLocaleDateString(loc, {
|
|
178
204
|
weekday: 'long', month: 'short', day: 'numeric',
|
|
179
205
|
});
|
|
180
206
|
}
|
|
207
|
+
else if (mode === 'week') {
|
|
208
|
+
const { start, end } = viewState.range;
|
|
209
|
+
dateLabel = fmtWeekRange(start.getTime(), loc, end.getTime() - 1);
|
|
210
|
+
}
|
|
181
211
|
else {
|
|
182
|
-
dateLabel = focus.toLocaleDateString(
|
|
212
|
+
dateLabel = focus.toLocaleDateString(loc, {
|
|
183
213
|
month: 'long', year: 'numeric',
|
|
184
214
|
});
|
|
185
215
|
}
|
|
@@ -189,7 +219,16 @@ export function createCalendar(options) {
|
|
|
189
219
|
modes: ['day', 'week', 'month'],
|
|
190
220
|
switchMode: (m) => {
|
|
191
221
|
const currentView = viewState.view;
|
|
192
|
-
const currentLabel = currentView.replace(/^(day|week)-/, '');
|
|
222
|
+
const currentLabel = currentView.replace(/^(day|week|month)-/, '');
|
|
223
|
+
// Month has one layout of its own; leaving it lands on the planner.
|
|
224
|
+
if (m === 'month') {
|
|
225
|
+
viewState.setView('month-grid');
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (currentView.startsWith('month-')) {
|
|
229
|
+
viewState.setView(`${m}-planner`);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
193
232
|
viewState.setView(`${m}-${currentLabel}`);
|
|
194
233
|
},
|
|
195
234
|
prev: () => viewState.prev(),
|
|
@@ -200,13 +239,11 @@ export function createCalendar(options) {
|
|
|
200
239
|
};
|
|
201
240
|
});
|
|
202
241
|
const navigationContext = $derived.by(() => {
|
|
203
|
-
const { start, end } = viewState.range;
|
|
204
|
-
const now = Date.now();
|
|
205
242
|
return {
|
|
206
243
|
prev: () => viewState.prev(),
|
|
207
244
|
next: () => viewState.next(),
|
|
208
245
|
goToday: () => viewState.goToday(),
|
|
209
|
-
isViewOnToday:
|
|
246
|
+
isViewOnToday: includesToday(),
|
|
210
247
|
focusDate: viewState.focusDate,
|
|
211
248
|
mode: viewState.mode,
|
|
212
249
|
};
|
|
@@ -288,15 +325,14 @@ export function createCalendar(options) {
|
|
|
288
325
|
oneventmove?.(ev, unzoneDate(start), unzoneDate(end));
|
|
289
326
|
}
|
|
290
327
|
catch (e) {
|
|
291
|
-
const msg = e instanceof Error ? e.message : '';
|
|
292
328
|
// Read-only adapter: the host still gets the callback — it owns
|
|
293
329
|
// persistence and refetches.
|
|
294
|
-
if (
|
|
330
|
+
if (isReadOnlyError(e)) {
|
|
295
331
|
const ev = store.byId(payload.eventId);
|
|
296
332
|
if (ev)
|
|
297
333
|
oneventmove?.(ev, unzoneDate(start), unzoneDate(end));
|
|
298
334
|
}
|
|
299
|
-
else if (!
|
|
335
|
+
else if (!isNotFoundError(e)) {
|
|
300
336
|
console.warn('[calendar] drag commit failed:', e);
|
|
301
337
|
}
|
|
302
338
|
return null;
|
|
@@ -12,6 +12,13 @@ export interface RangeAgendaOptions {
|
|
|
12
12
|
initialDate?: Date;
|
|
13
13
|
/** BCP 47 locale tag (e.g. 'en-US', 'pl-PL') for the format helpers */
|
|
14
14
|
locale?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Show the agenda in an IANA timezone (e.g. 'Europe/Warsaw'), as the
|
|
17
|
+
* Calendar's `timezone` prop does: days, "today" and every event Date are
|
|
18
|
+
* wall-clock values in that zone (read them with `getHours()`, `getDate()`…).
|
|
19
|
+
* `initialDate` and `setDate()` take real instants. Default: the viewer's zone.
|
|
20
|
+
*/
|
|
21
|
+
timezone?: string;
|
|
15
22
|
}
|
|
16
23
|
export interface RangeAgendaDay {
|
|
17
24
|
/** Start-of-day timestamp (ms) */
|
|
@@ -32,33 +32,54 @@
|
|
|
32
32
|
*/
|
|
33
33
|
import { untrack } from 'svelte';
|
|
34
34
|
import { createEventStore } from '../engine/event-store.svelte.js';
|
|
35
|
-
import { sod,
|
|
35
|
+
import { sod, addDaysMs, overlapsRange } from '../core/time.js';
|
|
36
|
+
import { toZonedTime, nowInZone, wrapAdapterWithTimezone } from '../core/timezone.js';
|
|
36
37
|
import { fmtTime as _fmtTime, fmtDuration } from '../core/locale.js';
|
|
37
38
|
// ─── Implementation ─────────────────────────────────────
|
|
38
39
|
export function createRangeAgenda(options) {
|
|
39
|
-
const { initialDate, locale, days: dayCount = 7 } = options;
|
|
40
|
-
const
|
|
40
|
+
const { initialDate, locale, days: dayCount = 7, timezone } = options;
|
|
41
|
+
const rawAdapter = typeof options.adapter === 'function'
|
|
41
42
|
? options.adapter
|
|
42
43
|
: () => options.adapter;
|
|
43
|
-
const
|
|
44
|
+
const resolveAdapter = timezone
|
|
45
|
+
? () => wrapAdapterWithTimezone(rawAdapter(), timezone)
|
|
46
|
+
: rawAdapter;
|
|
47
|
+
/** An instant on the plane the days are drawn on (zoned wall-clock with `timezone`). */
|
|
48
|
+
const plane = (ms) => (timezone ? toZonedTime(ms, timezone).getTime() : ms);
|
|
49
|
+
const nowMs = () => (timezone ? nowInZone(timezone).getTime() : Date.now());
|
|
50
|
+
// One store for the agenda's life: a getter adapter that changes identity
|
|
51
|
+
// reloads it instead of rebuilding it empty (every row blinking out).
|
|
52
|
+
const store = createEventStore(resolveAdapter);
|
|
44
53
|
// ── Window start (reactive, writable) ──
|
|
45
|
-
let startMs = $state(sod(initialDate
|
|
46
|
-
const endMs = $derived(startMs
|
|
54
|
+
let startMs = $state(sod(initialDate ? plane(initialDate.getTime()) : nowMs()));
|
|
55
|
+
const endMs = $derived(addDaysMs(startMs, dayCount));
|
|
47
56
|
// ── Load events for the window (re-runs on paging / adapter swap) ──
|
|
48
|
-
|
|
57
|
+
// Eager initial load for synchronous adapters (the server render holds the
|
|
58
|
+
// rows); the effect's first run then has nothing new to fetch — a seeded
|
|
59
|
+
// adapter would otherwise send its seed range straight to the network.
|
|
60
|
+
let eagerKey = untrack(() => {
|
|
61
|
+
if (!resolveAdapter().fetchEventsSync)
|
|
62
|
+
return '';
|
|
49
63
|
store.load({ start: new Date(startMs), end: new Date(endMs) });
|
|
64
|
+
return `${startMs}-${endMs}`;
|
|
50
65
|
});
|
|
51
|
-
|
|
52
|
-
|
|
66
|
+
$effect(() => {
|
|
67
|
+
rawAdapter(); // a new adapter reloads
|
|
68
|
+
const key = `${startMs}-${endMs}`;
|
|
69
|
+
if (key === eagerKey) {
|
|
70
|
+
eagerKey = '';
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
eagerKey = '';
|
|
53
74
|
store.load({ start: new Date(startMs), end: new Date(endMs) });
|
|
54
75
|
});
|
|
55
76
|
// ── Day derivations ──
|
|
56
77
|
const days = $derived.by(() => {
|
|
57
|
-
const todayMs = sod(
|
|
78
|
+
const todayMs = sod(nowMs());
|
|
58
79
|
const events = store.events;
|
|
59
80
|
return Array.from({ length: dayCount }, (_, i) => {
|
|
60
|
-
const ms = startMs
|
|
61
|
-
const dayEnd = ms
|
|
81
|
+
const ms = addDaysMs(startMs, i);
|
|
82
|
+
const dayEnd = addDaysMs(ms, 1);
|
|
62
83
|
const date = new Date(ms);
|
|
63
84
|
// JS getDay(): 0=Sun … 6=Sat → ISO 1=Mon … 7=Sun
|
|
64
85
|
const weekday = ((date.getDay() + 6) % 7) + 1;
|
|
@@ -69,7 +90,7 @@ export function createRangeAgenda(options) {
|
|
|
69
90
|
isToday: ms === todayMs,
|
|
70
91
|
isPast: dayEnd <= todayMs,
|
|
71
92
|
events: events
|
|
72
|
-
.filter((ev) => ev
|
|
93
|
+
.filter((ev) => overlapsRange(ev, date, new Date(dayEnd)))
|
|
73
94
|
.sort((a, b) => a.start.getTime() - b.start.getTime()),
|
|
74
95
|
};
|
|
75
96
|
});
|
|
@@ -92,16 +113,16 @@ export function createRangeAgenda(options) {
|
|
|
92
113
|
return store.error;
|
|
93
114
|
},
|
|
94
115
|
prev() {
|
|
95
|
-
startMs
|
|
116
|
+
startMs = addDaysMs(startMs, -dayCount);
|
|
96
117
|
},
|
|
97
118
|
next() {
|
|
98
|
-
startMs
|
|
119
|
+
startMs = addDaysMs(startMs, dayCount);
|
|
99
120
|
},
|
|
100
121
|
goToday() {
|
|
101
|
-
startMs = sod(
|
|
122
|
+
startMs = sod(nowMs());
|
|
102
123
|
},
|
|
103
124
|
setDate(date) {
|
|
104
|
-
startMs = sod(date.getTime());
|
|
125
|
+
startMs = sod(plane(date.getTime()));
|
|
105
126
|
},
|
|
106
127
|
refresh() {
|
|
107
128
|
void store.load({ start: new Date(startMs), end: new Date(endMs) });
|
package/dist/headless/index.d.ts
CHANGED
|
@@ -4,3 +4,4 @@ export type { AgendaOptions, HeadlessAgenda } from './create-agenda.svelte.js';
|
|
|
4
4
|
export { createRangeAgenda } from './create-range-agenda.svelte.js';
|
|
5
5
|
export type { RangeAgendaOptions, RangeAgendaDay, HeadlessRangeAgenda, } from './create-range-agenda.svelte.js';
|
|
6
6
|
export type { HeadlessCalendarOptions, HeadlessCalendar, HeadlessDay, HeadlessWeek, TodayQueue, HeaderContext, NavigationContext, } from './types.js';
|
|
7
|
+
export type { TimeSlot } from '../views/shared/format.js';
|
package/dist/index.d.ts
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
export { NowIndicator, EventBlock, TimeGutter, DayHeader, EmptySlot, FloatingPanel, } from './primitives/index.js';
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
4
|
-
export {
|
|
5
|
-
export
|
|
2
|
+
export type { FloatingPanelAnchor, FloatingPanelProps } from './primitives/index.js';
|
|
3
|
+
export { Calendar, defaultViews } from './calendar/index.js';
|
|
4
|
+
export type { CalendarView, CalendarViewProps, CalendarProps } from './calendar/index.js';
|
|
5
|
+
export { Planner, PlannerScroll, Agenda, Mobile, MonthGrid } from './views/index.js';
|
|
6
|
+
export { useCalendarContext } from './views/shared/context.svelte.js';
|
|
7
|
+
export type { CalendarContext } from './views/shared/context.svelte.js';
|
|
6
8
|
export { createEventStore, createViewState, createSelection, createDragState, } from './engine/index.js';
|
|
7
9
|
export type { EventStore, ViewState, ViewStateOptions, CalendarViewId, BuiltInViewId, ViewMode, Selection as CalendarSelection, DragState, DragMode, DragPayload, } from './engine/index.js';
|
|
8
10
|
export { createMemoryAdapter, createRestAdapter, createRecurringAdapter, createMappedAdapter, createCompositeAdapter, createJmapAdapter, withInitialEvents } from './adapters/index.js';
|
|
11
|
+
export { CalendarReadOnlyError, EventNotFoundError, isReadOnlyError, isNotFoundError } from './adapters/index.js';
|
|
9
12
|
export type { CalendarAdapter, WritableCalendarAdapter, DateRange, MemoryAdapterOptions, RestAdapterOptions, RecurringEvent, RecurringAdapterOptions, FieldMapping, MappedAdapterOptions, MutationHandler, CompositeAdapterOptions, JmapClient, JmapCalendarAdapterOptions, } from './adapters/index.js';
|
|
10
|
-
export { createClock, startOfWeek, fmtH, fmtTime, fmtDuration, weekdayShort, weekdayLong, monthShort, monthLong, dateShort, dateWithWeekday, fmtDay, fmtWeekRange, setDefaultLocale, getDefaultLocale, is24HourLocale, defaultLabels, setLabels, resetLabels, getLabels, toZonedTime, fromZonedTime, nowInZone, formatInTimeZone, generatePalette, extractAccent, VIVID_PALETTE, isMultiDay, isAllDay, segmentForDay, } from './core/index.js';
|
|
13
|
+
export { createClock, sod, startOfWeek, addDaysMs, diffDays, fmtH, fmtTime, fmtDuration, weekdayShort, weekdayLong, monthShort, monthLong, dateShort, dateWithWeekday, fmtDay, fmtWeekRange, setDefaultLocale, getDefaultLocale, is24HourLocale, defaultLabels, setLabels, resetLabels, getLabels, toZonedTime, fromZonedTime, nowInZone, formatInTimeZone, generatePalette, extractAccent, VIVID_PALETTE, isMultiDay, isAllDay, segmentForDay, } from './core/index.js';
|
|
11
14
|
export type { Clock, TimelineEvent, BlockedSlot, DaySegment, CalendarLabels, EventStatus, } from './core/index.js';
|
|
12
15
|
export { auto, neutral, midnight, presets } from './theme/index.js';
|
|
13
16
|
export { probeHostTheme, observeHostTheme } from './theme/index.js';
|
|
14
17
|
export { wrapAdapterWithTimezone } from './core/timezone.js';
|
|
15
18
|
export type { PresetName, AutoThemeOptions } from './theme/index.js';
|
|
16
19
|
export { createCalendar, createAgenda, createRangeAgenda } from './headless/index.js';
|
|
17
|
-
export type { HeadlessCalendarOptions, HeadlessCalendar, HeadlessDay, HeadlessWeek, TodayQueue, HeaderContext, NavigationContext, AgendaOptions, HeadlessAgenda, RangeAgendaOptions, RangeAgendaDay, HeadlessRangeAgenda, } from './headless/index.js';
|
|
20
|
+
export type { HeadlessCalendarOptions, HeadlessCalendar, HeadlessDay, HeadlessWeek, TodayQueue, HeaderContext, NavigationContext, AgendaOptions, HeadlessAgenda, RangeAgendaOptions, RangeAgendaDay, HeadlessRangeAgenda, TimeSlot, } from './headless/index.js';
|
|
18
21
|
export { fits, lineCount, textHeight, textWidth, pickFit, fontOf, fitLabel, fitParts, breakLines, typeset } from './text-fit.js';
|
|
19
|
-
export type { ChipPart } from './text-fit.js';
|
|
22
|
+
export type { ChipPart, TextLayoutOptions } from './text-fit.js';
|
package/dist/index.js
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
// ─── Primitives ─────────────────────────────────────────
|
|
2
2
|
export { NowIndicator, EventBlock, TimeGutter, DayHeader, EmptySlot, FloatingPanel, } from './primitives/index.js';
|
|
3
3
|
// ─── Calendar shell ─────────────────────────────────────
|
|
4
|
-
export { Calendar } from './calendar/index.js';
|
|
5
|
-
// Raw view components —
|
|
6
|
-
|
|
7
|
-
export {
|
|
4
|
+
export { Calendar, defaultViews } from './calendar/index.js';
|
|
5
|
+
// Raw view components — register them under your own ids, or compose your
|
|
6
|
+
// own shell around the engine. They read the Calendar's context.
|
|
7
|
+
export { Planner, PlannerScroll, Agenda, Mobile, MonthGrid } from './views/index.js';
|
|
8
|
+
// For custom views: the running Calendar's engines, config and labels.
|
|
9
|
+
export { useCalendarContext } from './views/shared/context.svelte.js';
|
|
8
10
|
// ─── Engine (reactive state) ────────────────────────────
|
|
9
11
|
export { createEventStore, createViewState, createSelection, createDragState, } from './engine/index.js';
|
|
10
12
|
// ─── Adapters ───────────────────────────────────────────
|
|
11
13
|
export { createMemoryAdapter, createRestAdapter, createRecurringAdapter, createMappedAdapter, createCompositeAdapter, createJmapAdapter, withInitialEvents } from './adapters/index.js';
|
|
14
|
+
export { CalendarReadOnlyError, EventNotFoundError, isReadOnlyError, isNotFoundError } from './adapters/index.js';
|
|
12
15
|
// ─── Core: clock, time, locale, types ───────────────────
|
|
13
|
-
export { createClock, startOfWeek, fmtH, fmtTime, fmtDuration, weekdayShort, weekdayLong, monthShort, monthLong, dateShort, dateWithWeekday, fmtDay, fmtWeekRange, setDefaultLocale, getDefaultLocale, is24HourLocale, defaultLabels, setLabels, resetLabels, getLabels, toZonedTime, fromZonedTime, nowInZone, formatInTimeZone, generatePalette, extractAccent, VIVID_PALETTE, isMultiDay, isAllDay, segmentForDay, } from './core/index.js';
|
|
16
|
+
export { createClock, sod, startOfWeek, addDaysMs, diffDays, fmtH, fmtTime, fmtDuration, weekdayShort, weekdayLong, monthShort, monthLong, dateShort, dateWithWeekday, fmtDay, fmtWeekRange, setDefaultLocale, getDefaultLocale, is24HourLocale, defaultLabels, setLabels, resetLabels, getLabels, toZonedTime, fromZonedTime, nowInZone, formatInTimeZone, generatePalette, extractAccent, VIVID_PALETTE, isMultiDay, isAllDay, segmentForDay, } from './core/index.js';
|
|
14
17
|
// ─── Themes ─────────────────────────────────────────────
|
|
15
18
|
export { auto, neutral, midnight, presets } from './theme/index.js';
|
|
16
19
|
export { probeHostTheme, observeHostTheme } from './theme/index.js';
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
- Short/long weekday + date number for week views
|
|
7
7
|
-->
|
|
8
8
|
<script lang="ts">import { weekdayShort, weekdayLong, monthShort, fmtDay } from "../core/locale.js";
|
|
9
|
-
|
|
9
|
+
import { sod } from "../core/time.js";
|
|
10
|
+
let { dayMs, todayMs = sod(Date.now()), format = "short", isToday = false, isPast = false } = $props();
|
|
10
11
|
const dayNum = $derived(new Date(dayMs).getDate());
|
|
11
12
|
</script>
|
|
12
13
|
|
|
@@ -16,7 +17,7 @@ const dayNum = $derived(new Date(dayMs).getDate());
|
|
|
16
17
|
class:dh-past={isPast}
|
|
17
18
|
>
|
|
18
19
|
{#if format === 'relative'}
|
|
19
|
-
<span class="dh-rel">{fmtDay(dayMs, todayMs)}</span>
|
|
20
|
+
<span class="dh-rel">{fmtDay(sod(dayMs), sod(todayMs))}</span>
|
|
20
21
|
{:else if format === 'long'}
|
|
21
22
|
<span class="dh-name dh-name-long">{weekdayLong(dayMs)}</span>
|
|
22
23
|
<span class="dh-date">{monthShort(dayMs)} {dayNum}</span>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
interface Props {
|
|
2
2
|
/** Timestamp (ms) of the day to label */
|
|
3
3
|
dayMs: number;
|
|
4
|
-
/**
|
|
4
|
+
/** Any timestamp (ms) on today, for relative labels — default: now */
|
|
5
5
|
todayMs?: number;
|
|
6
6
|
/** Display format */
|
|
7
7
|
format?: 'relative' | 'short' | 'long';
|
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
between events.
|
|
7
7
|
-->
|
|
8
8
|
<script lang="ts">import { fmtTime, fmtDuration, getLabels } from "../core/locale.js";
|
|
9
|
-
|
|
9
|
+
import { getContext } from "svelte";
|
|
10
|
+
// Inside a Calendar: its per-instance labels; standalone: the global ones.
|
|
11
|
+
const calendarCtx = getContext("calendar");
|
|
12
|
+
const L = $derived(calendarCtx?.labels ?? getLabels());
|
|
10
13
|
let { start, end, onclick, orientation = "vertical" } = $props();
|
|
11
14
|
const dur = $derived(`${fmtDuration(start, end)} ${L.free}`);
|
|
12
15
|
function handleKeydown(e) {
|
|
@@ -26,7 +29,7 @@ function handleKeydown(e) {
|
|
|
26
29
|
class:es-h={orientation === 'horizontal'}
|
|
27
30
|
role="button"
|
|
28
31
|
tabindex="0"
|
|
29
|
-
aria-label="{L.createEvent}, {fmtTime(start)}
|
|
32
|
+
aria-label="{L.createEvent}, {fmtTime(start)} – {fmtTime(end)}, {dur}"
|
|
30
33
|
onclick={() => onclick?.({ start, end })}
|
|
31
34
|
onkeydown={handleKeydown}
|
|
32
35
|
>
|