@nomideusz/svelte-calendar 0.14.1 → 0.15.2
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/dist/calendar/Calendar.svelte +450 -372
- package/dist/calendar/Calendar.svelte.d.ts +2 -1
- package/dist/headless/types.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/primitives/DayHeader.svelte +1 -7
- package/dist/primitives/EmptySlot.svelte +8 -10
- package/dist/primitives/EventBlock.svelte +12 -22
- package/dist/primitives/FloatingPanel.svelte +191 -0
- package/dist/primitives/FloatingPanel.svelte.d.ts +23 -0
- package/dist/primitives/NowIndicator.svelte +2 -13
- package/dist/primitives/TimeGutter.svelte +1 -7
- package/dist/primitives/index.d.ts +1 -0
- package/dist/primitives/index.js +1 -0
- package/dist/views/agenda/AgendaDay.svelte +68 -45
- package/dist/views/agenda/AgendaWeek.svelte +120 -105
- package/dist/views/mobile/MobileDay.svelte +313 -276
- package/dist/views/mobile/MobileWeek.svelte +91 -84
- package/dist/views/month/MonthGrid.svelte +101 -104
- package/dist/views/planner/PlannerWeek.svelte +475 -407
- package/dist/views/planner/PlannerWeek.svelte.d.ts +1 -1
- package/dist/widget/CalendarWidget.svelte +64 -83
- package/package.json +17 -10
- package/widget/widget.js +12723 -12203
|
@@ -31,445 +31,523 @@ import Agenda from "../views/agenda/Agenda.svelte";
|
|
|
31
31
|
import Mobile from "../views/mobile/Mobile.svelte";
|
|
32
32
|
import MonthGrid from "../views/month/MonthGrid.svelte";
|
|
33
33
|
import { wrapAdapterWithTimezone, toZonedTime, fromZonedTime } from "../core/timezone.js";
|
|
34
|
+
/** Breakpoint (px) at which auto-mobile activates */
|
|
34
35
|
const MOBILE_BREAKPOINT = 768;
|
|
36
|
+
// ── Built-in views (used when no custom views are provided) ──
|
|
35
37
|
const DEFAULT_VIEWS = [
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
{
|
|
39
|
+
id: "day-planner",
|
|
40
|
+
label: "Planner",
|
|
41
|
+
mode: "day",
|
|
42
|
+
component: Planner
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: "week-planner",
|
|
46
|
+
label: "Planner",
|
|
47
|
+
mode: "week",
|
|
48
|
+
component: Planner
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: "day-agenda",
|
|
52
|
+
label: "Agenda",
|
|
53
|
+
mode: "day",
|
|
54
|
+
component: Agenda
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: "week-agenda",
|
|
58
|
+
label: "Agenda",
|
|
59
|
+
mode: "week",
|
|
60
|
+
component: Agenda
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: "day-mobile",
|
|
64
|
+
label: "Mobile",
|
|
65
|
+
mode: "day",
|
|
66
|
+
component: Mobile
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: "week-mobile",
|
|
70
|
+
label: "Mobile",
|
|
71
|
+
mode: "week",
|
|
72
|
+
component: Mobile
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: "month-grid",
|
|
76
|
+
label: "Month",
|
|
77
|
+
mode: "month",
|
|
78
|
+
component: MonthGrid
|
|
79
|
+
}
|
|
43
80
|
];
|
|
44
|
-
let {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
view: activeViewId,
|
|
48
|
-
theme = auto,
|
|
49
|
-
autoTheme,
|
|
50
|
-
mondayStart = true,
|
|
51
|
-
height: heightProp = 600,
|
|
52
|
-
borderRadius = 12,
|
|
53
|
-
dir,
|
|
54
|
-
locale,
|
|
55
|
-
labels: labelsProp,
|
|
56
|
-
readOnly = false,
|
|
57
|
-
visibleHours,
|
|
58
|
-
initialDate,
|
|
59
|
-
snapInterval = 15,
|
|
60
|
-
minColumnWidth = 110,
|
|
61
|
-
showModePills = true,
|
|
62
|
-
showNavigation = true,
|
|
63
|
-
equalDays = false,
|
|
64
|
-
showDates = true,
|
|
65
|
-
hideDays,
|
|
66
|
-
currentDate,
|
|
67
|
-
blockedSlots,
|
|
68
|
-
days,
|
|
69
|
-
minDuration,
|
|
70
|
-
maxDuration,
|
|
71
|
-
disabledDates,
|
|
72
|
-
compact = false,
|
|
73
|
-
columns = false,
|
|
74
|
-
mobile: mobileProp = "auto",
|
|
75
|
-
event: eventSnippet,
|
|
76
|
-
empty: emptySnippet,
|
|
77
|
-
dayHeader: dayHeaderSnippet,
|
|
78
|
-
header: headerSnippet,
|
|
79
|
-
navigation: navigationSnippet,
|
|
80
|
-
oneventclick,
|
|
81
|
-
oneventcreate,
|
|
82
|
-
oneventmove,
|
|
83
|
-
onviewchange,
|
|
84
|
-
ondatechange,
|
|
85
|
-
oneventhover,
|
|
86
|
-
ondayclick,
|
|
87
|
-
onerror,
|
|
88
|
-
timezone
|
|
89
|
-
} = $props();
|
|
81
|
+
let { adapter, views = DEFAULT_VIEWS, view: activeViewId, theme = auto, autoTheme, mondayStart = true, height: heightProp = 600, borderRadius = 12, dir, locale, labels: labelsProp, readOnly = false, visibleHours, initialDate, snapInterval = 15, minColumnWidth = 110, showModePills = true, showNavigation = true, equalDays = false, showDates = true, hideDays, currentDate, blockedSlots, days, minDuration, maxDuration, disabledDates, compact = false, columns = false, mobile: mobileProp = "auto", event: eventSnippet, empty: emptySnippet, dayHeader: dayHeaderSnippet, header: headerSnippet, navigation: navigationSnippet, oneventclick, oneventcreate, oneventmove, onviewchange, ondatechange, oneventhover, ondayclick, onerror, timezone } = $props();
|
|
82
|
+
// In readOnly mode, suppress mutation callbacks. With a timezone, the
|
|
83
|
+
// drag plane is zoned wall-clock — hosts always receive real instants.
|
|
90
84
|
const unzone = (d) => timezone ? fromZonedTime(d, timezone) : d;
|
|
91
|
-
const effectiveCreate = $derived(
|
|
92
|
-
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
85
|
+
const effectiveCreate = $derived(readOnly || !oneventcreate ? undefined : (range) => oneventcreate({
|
|
86
|
+
start: unzone(range.start),
|
|
87
|
+
end: unzone(range.end)
|
|
88
|
+
}));
|
|
89
|
+
const effectiveMove = $derived(readOnly || !oneventmove ? undefined : (ev, start, end) => oneventmove(ev, unzone(start), unzone(end)));
|
|
90
|
+
// Clicking an event selects it (highlight via selectedEventId) and then
|
|
91
|
+
// notifies the host — selection used to be created but never driven.
|
|
92
|
+
function handleEventClick(ev, anchor) {
|
|
93
|
+
selection.select(ev.id);
|
|
94
|
+
oneventclick?.(ev, anchor);
|
|
100
95
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
96
|
+
// ── Mobile detection (container-based, not viewport) ──
|
|
97
|
+
// Seeded from viewport width so the first client paint doesn't flash the
|
|
98
|
+
// desktop chrome on phones; the ResizeObserver measurement takes over on mount.
|
|
99
|
+
let containerWidth = $state(typeof window !== "undefined" && window.matchMedia?.(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`).matches ? window.innerWidth : 0);
|
|
104
100
|
const isMobileContainer = $derived(containerWidth > 0 && containerWidth < MOBILE_BREAKPOINT);
|
|
105
|
-
const useMobile = $derived(
|
|
106
|
-
|
|
107
|
-
|
|
101
|
+
const useMobile = $derived(mobileProp === "auto" ? isMobileContainer : Boolean(mobileProp));
|
|
102
|
+
// Below this container width the mobile header can't fit pills + nav + a
|
|
103
|
+
// readable date label on one row, so the label moves to its own row.
|
|
108
104
|
const HEADER_STACK_BREAKPOINT = 520;
|
|
109
|
-
const stackHeader = $derived(
|
|
110
|
-
|
|
111
|
-
)
|
|
105
|
+
const stackHeader = $derived(useMobile && containerWidth > 0 && containerWidth < HEADER_STACK_BREAKPOINT);
|
|
106
|
+
// ── Smart auto-theme ──
|
|
107
|
+
// When theme is empty (auto preset) and autoTheme is not false,
|
|
108
|
+
// probe the host page on mount and reactively watch for host theme changes.
|
|
112
109
|
let calEl = $state();
|
|
113
110
|
let probedTheme = $state("");
|
|
111
|
+
// Only delay rendering when auto-probe is actually needed
|
|
114
112
|
const needsProbe = $derived(theme === auto && autoTheme !== false);
|
|
115
113
|
onMount(() => {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
114
|
+
if (!calEl) return;
|
|
115
|
+
// Measure container width for mobile detection
|
|
116
|
+
containerWidth = calEl.clientWidth;
|
|
117
|
+
const ro = new ResizeObserver((entries) => {
|
|
118
|
+
containerWidth = Math.round(entries[0].contentRect.width);
|
|
119
|
+
});
|
|
120
|
+
ro.observe(calEl);
|
|
121
|
+
// Only probe theme when using the auto preset
|
|
122
|
+
if (!needsProbe) return () => ro.disconnect();
|
|
123
|
+
const opts = typeof autoTheme === "object" ? autoTheme : {};
|
|
124
|
+
const stopTheme = observeHostTheme(calEl, (vars) => {
|
|
125
|
+
probedTheme = vars;
|
|
126
|
+
}, opts);
|
|
127
|
+
return () => {
|
|
128
|
+
ro.disconnect();
|
|
129
|
+
stopTheme?.();
|
|
130
|
+
};
|
|
131
131
|
});
|
|
132
|
+
/** Effective theme: user-provided takes priority, otherwise probed auto. */
|
|
132
133
|
const effectiveTheme = $derived(theme === auto && autoTheme !== false ? probedTheme : theme);
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
134
|
+
// ── Create reactive state ──
|
|
135
|
+
const effectiveAdapter = $derived(timezone ? wrapAdapterWithTimezone(adapter, timezone) : adapter);
|
|
136
|
+
// The store is created ONCE and reads the adapter through a getter. A
|
|
137
|
+
// `$derived` store would be rebuilt — empty — whenever the host handed over a
|
|
138
|
+
// new adapter identity (a common "force a refetch" idiom), blanking the grid
|
|
139
|
+
// until the refetch landed. The load effect below re-runs on adapter change
|
|
140
|
+
// because store.load() reads effectiveAdapter inside it.
|
|
136
141
|
const store = createEventStore(() => effectiveAdapter);
|
|
137
142
|
const viewState = createViewState(untrack(() => ({
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
143
|
+
view: activeViewId ?? views[0]?.id,
|
|
144
|
+
mondayStart,
|
|
145
|
+
// Focus lives on the zoned plane too — day boundaries follow the zone.
|
|
146
|
+
initialDate: initialDate && timezone ? toZonedTime(initialDate, timezone) : initialDate,
|
|
147
|
+
dayCount: days,
|
|
148
|
+
timezone,
|
|
149
|
+
modeForView: (viewId) => views.find((v) => v.id === viewId)?.mode
|
|
145
150
|
})));
|
|
146
151
|
const selection = createSelection();
|
|
147
152
|
const drag = createDragState();
|
|
153
|
+
// ── Drag commit handler ──
|
|
154
|
+
// Views call this on pointer-up to process drag results.
|
|
148
155
|
async function commitDrag() {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
156
|
+
if (readOnly) {
|
|
157
|
+
drag.cancel();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const mode = drag.mode;
|
|
161
|
+
const payload = drag.commit();
|
|
162
|
+
if (!payload) return;
|
|
163
|
+
let { start, end } = payload;
|
|
164
|
+
// Enforce min/max duration for create and resize
|
|
165
|
+
if (mode === "create" || mode === "resize-start" || mode === "resize-end") {
|
|
166
|
+
// Defensive floor: never accept a zero/negative duration, even when
|
|
167
|
+
// minDuration is unset (views clamp, but the engine must hold alone).
|
|
168
|
+
if (end.getTime() <= start.getTime()) {
|
|
169
|
+
const floorMs = Math.max(1, snapInterval) * 6e4;
|
|
170
|
+
if (mode === "resize-start") start = new Date(end.getTime() - floorMs);
|
|
171
|
+
else end = new Date(start.getTime() + floorMs);
|
|
172
|
+
}
|
|
173
|
+
const durationMs = end.getTime() - start.getTime();
|
|
174
|
+
const durationMin = durationMs / 6e4;
|
|
175
|
+
if (minDuration && durationMin < minDuration) {
|
|
176
|
+
if (mode === "resize-start") {
|
|
177
|
+
start = new Date(end.getTime() - minDuration * 6e4);
|
|
178
|
+
} else {
|
|
179
|
+
end = new Date(start.getTime() + minDuration * 6e4);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (maxDuration && durationMin > maxDuration) {
|
|
183
|
+
if (mode === "resize-start") {
|
|
184
|
+
start = new Date(end.getTime() - maxDuration * 6e4);
|
|
185
|
+
} else {
|
|
186
|
+
end = new Date(start.getTime() + maxDuration * 6e4);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// Reject if target lands on a disabled date
|
|
191
|
+
if (disabledDates?.length) {
|
|
192
|
+
const startDay = new Date(start);
|
|
193
|
+
startDay.setHours(0, 0, 0, 0);
|
|
194
|
+
const endDay = new Date(end.getTime() - 1);
|
|
195
|
+
endDay.setHours(0, 0, 0, 0);
|
|
196
|
+
for (const dd of disabledDates) {
|
|
197
|
+
const dt = new Date(dd);
|
|
198
|
+
dt.setHours(0, 0, 0, 0);
|
|
199
|
+
const ts = dt.getTime();
|
|
200
|
+
if (ts >= startDay.getTime() && ts <= endDay.getTime()) return;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// Reject if target overlaps a blocked slot
|
|
204
|
+
if (blockedSlots?.length) {
|
|
205
|
+
const startH = start.getHours() + start.getMinutes() / 60;
|
|
206
|
+
const endH = end.getHours() + end.getMinutes() / 60 + (end.getDate() !== start.getDate() ? 24 : 0);
|
|
207
|
+
const jsDay = start.getDay();
|
|
208
|
+
const isoDay = jsDay === 0 ? 7 : jsDay;
|
|
209
|
+
for (const slot of blockedSlots) {
|
|
210
|
+
if (slot.day && slot.day !== isoDay) continue;
|
|
211
|
+
if (startH < slot.end && endH > slot.start) return;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if ((mode === "move" || mode === "resize-start" || mode === "resize-end") && payload.eventId) {
|
|
215
|
+
try {
|
|
216
|
+
await store.move(payload.eventId, start, end);
|
|
217
|
+
const ev = store.byId(payload.eventId);
|
|
218
|
+
if (ev) effectiveMove?.(ev, start, end);
|
|
219
|
+
} catch (e) {
|
|
220
|
+
const msg = e instanceof Error ? e.message : "";
|
|
221
|
+
// Read-only adapter: the store can't persist, but the HOST still
|
|
222
|
+
// gets the callback — it owns persistence (e.g. scheduler RPC) and
|
|
223
|
+
// refetches. Missing event stays silent; real failures surface.
|
|
224
|
+
if (msg.includes("read-only")) {
|
|
225
|
+
const ev = store.byId(payload.eventId);
|
|
226
|
+
if (ev) effectiveMove?.(ev, start, end);
|
|
227
|
+
} else if (!msg.includes("not found")) {
|
|
228
|
+
if (onerror) onerror(e instanceof Error ? e : new Error(String(e)));
|
|
229
|
+
else console.warn("[calendar] drag commit failed:", e);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
} else if (mode === "create") {
|
|
233
|
+
effectiveCreate?.({
|
|
234
|
+
start,
|
|
235
|
+
end
|
|
236
|
+
});
|
|
237
|
+
}
|
|
220
238
|
}
|
|
239
|
+
// ── Load range signal ──
|
|
240
|
+
// Views can write a wider range here to override the default viewState.range.
|
|
241
|
+
// This lets infinite-scroll views (PlannerWeek) declare their
|
|
242
|
+
// buffer needs without directly calling store.load().
|
|
221
243
|
let viewLoadRange = $state(null);
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
244
|
+
// Per-instance labels: prop overrides merged over the global set, memoized so
|
|
245
|
+
// views reading ctx.labels per-render don't allocate a fresh object each access.
|
|
246
|
+
//
|
|
247
|
+
// MUST stay above setContext: the context exposes this through a getter, and
|
|
248
|
+
// a view reading ctx.labels while the binding is still uninitialised got the
|
|
249
|
+
// English globals instead — silently, because the fallback in
|
|
250
|
+
// useCalendarContext is `?? getLabels()`. Every localized calendar rendered
|
|
251
|
+
// its empty states in English.
|
|
252
|
+
const mergedLabels = $derived(labelsProp ? {
|
|
253
|
+
...getLabels(),
|
|
254
|
+
...labelsProp
|
|
255
|
+
} : getLabels());
|
|
225
256
|
const L = $derived(mergedLabels);
|
|
257
|
+
// ── Single context object ──
|
|
258
|
+
// All view state is exposed through one context key with reactive getters.
|
|
259
|
+
// Views read this via useCalendarContext() from views/shared/context.svelte.ts.
|
|
226
260
|
setContext("calendar", {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
261
|
+
// Engine objects (hold $state internally)
|
|
262
|
+
get store() {
|
|
263
|
+
return store;
|
|
264
|
+
},
|
|
265
|
+
viewState,
|
|
266
|
+
selection,
|
|
267
|
+
drag,
|
|
268
|
+
commitDrag,
|
|
269
|
+
// Callbacks
|
|
270
|
+
get oneventclick() {
|
|
271
|
+
return handleEventClick;
|
|
272
|
+
},
|
|
273
|
+
get oneventcreate() {
|
|
274
|
+
return effectiveCreate;
|
|
275
|
+
},
|
|
276
|
+
get oneventmove() {
|
|
277
|
+
return effectiveMove;
|
|
278
|
+
},
|
|
279
|
+
get oneventhover() {
|
|
280
|
+
return oneventhover;
|
|
281
|
+
},
|
|
282
|
+
get ondayclick() {
|
|
283
|
+
return ondayclick ?? defaultDayClick;
|
|
284
|
+
},
|
|
285
|
+
get timezone() {
|
|
286
|
+
return timezone;
|
|
287
|
+
},
|
|
288
|
+
// Config (reactive via getters)
|
|
289
|
+
get readOnly() {
|
|
290
|
+
return readOnly;
|
|
291
|
+
},
|
|
292
|
+
get visibleHours() {
|
|
293
|
+
return visibleHours;
|
|
294
|
+
},
|
|
295
|
+
get snapInterval() {
|
|
296
|
+
return snapInterval;
|
|
297
|
+
},
|
|
298
|
+
get minColumnWidth() {
|
|
299
|
+
return minColumnWidth;
|
|
300
|
+
},
|
|
301
|
+
get eventSnippet() {
|
|
302
|
+
return eventSnippet;
|
|
303
|
+
},
|
|
304
|
+
get emptySnippet() {
|
|
305
|
+
return emptySnippet;
|
|
306
|
+
},
|
|
307
|
+
get equalDays() {
|
|
308
|
+
return equalDays;
|
|
309
|
+
},
|
|
310
|
+
get showDates() {
|
|
311
|
+
return showDates;
|
|
312
|
+
},
|
|
313
|
+
get hideDays() {
|
|
314
|
+
return hideDays;
|
|
315
|
+
},
|
|
316
|
+
get blockedSlots() {
|
|
317
|
+
return blockedSlots;
|
|
318
|
+
},
|
|
319
|
+
get dayHeaderSnippet() {
|
|
320
|
+
return dayHeaderSnippet;
|
|
321
|
+
},
|
|
322
|
+
get minDuration() {
|
|
323
|
+
return minDuration;
|
|
324
|
+
},
|
|
325
|
+
get maxDuration() {
|
|
326
|
+
return maxDuration;
|
|
327
|
+
},
|
|
328
|
+
get disabledDates() {
|
|
329
|
+
return disabledDates;
|
|
330
|
+
},
|
|
331
|
+
get mobile() {
|
|
332
|
+
return useMobile;
|
|
333
|
+
},
|
|
334
|
+
get autoHeight() {
|
|
335
|
+
return heightProp === "auto";
|
|
336
|
+
},
|
|
337
|
+
get compact() {
|
|
338
|
+
return compact;
|
|
339
|
+
},
|
|
340
|
+
get columns() {
|
|
341
|
+
return columns;
|
|
342
|
+
},
|
|
343
|
+
get labels() {
|
|
344
|
+
return mergedLabels;
|
|
345
|
+
},
|
|
346
|
+
// Load range (read/write)
|
|
347
|
+
get loadRange() {
|
|
348
|
+
return viewLoadRange;
|
|
349
|
+
},
|
|
350
|
+
setLoadRange(range) {
|
|
351
|
+
viewLoadRange = range;
|
|
352
|
+
}
|
|
319
353
|
});
|
|
354
|
+
// ── Load events when effective range changes ──
|
|
320
355
|
$effect(() => {
|
|
321
|
-
|
|
322
|
-
|
|
356
|
+
const range = viewLoadRange ?? viewState.range;
|
|
357
|
+
store.load({
|
|
358
|
+
start: range.start,
|
|
359
|
+
end: range.end
|
|
360
|
+
});
|
|
323
361
|
});
|
|
324
|
-
|
|
362
|
+
// Eager initial load — $effect runs after paint, but for sync adapters
|
|
363
|
+
// (memory, recurring) this resolves in the same microtask.
|
|
364
|
+
untrack(() => store.load({
|
|
365
|
+
start: viewState.range.start,
|
|
366
|
+
end: viewState.range.end
|
|
367
|
+
}));
|
|
368
|
+
// Keep active view in sync when external view prop changes after mount.
|
|
325
369
|
$effect(() => {
|
|
326
|
-
|
|
370
|
+
if (activeViewId) viewState.setView(activeViewId);
|
|
327
371
|
});
|
|
372
|
+
// Sync controlled currentDate prop → viewState
|
|
328
373
|
$effect(() => {
|
|
329
|
-
|
|
374
|
+
if (currentDate) viewState.setFocusDate(currentDate);
|
|
330
375
|
});
|
|
376
|
+
// Sync days prop → viewState.dayCount
|
|
331
377
|
$effect(() => {
|
|
332
|
-
|
|
378
|
+
if (days !== undefined && viewState.dayCount !== days) viewState.setDayCount(days);
|
|
333
379
|
});
|
|
380
|
+
// Notify host when focusDate changes
|
|
334
381
|
$effect(() => {
|
|
335
|
-
|
|
336
|
-
|
|
382
|
+
const d = viewState.focusDate;
|
|
383
|
+
ondatechange?.(d);
|
|
337
384
|
});
|
|
385
|
+
// Keep view state's week-start rule in sync with incoming prop changes.
|
|
338
386
|
$effect(() => {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
387
|
+
if (viewState.mondayStart !== mondayStart) {
|
|
388
|
+
viewState.setMondayStart(mondayStart);
|
|
389
|
+
}
|
|
342
390
|
});
|
|
391
|
+
// Notify host when active view changes (e.g. via mode toggles).
|
|
343
392
|
$effect(() => {
|
|
344
|
-
|
|
393
|
+
onviewchange?.(viewState.view);
|
|
345
394
|
});
|
|
395
|
+
// Surface adapter failures — store.error was previously write-only.
|
|
346
396
|
$effect(() => {
|
|
347
|
-
|
|
397
|
+
if (store.error && onerror) onerror(new Error(store.error));
|
|
348
398
|
});
|
|
399
|
+
// ── Resolve active view ──
|
|
400
|
+
// When mobile is active, Planner views get remapped to Mobile variants.
|
|
401
|
+
// Agenda views stay as Agenda — they're already list-based and will adapt
|
|
402
|
+
// via the 'calendar:mobile' context flag.
|
|
349
403
|
const resolvedView = $derived.by(() => {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
404
|
+
const requested = views.find((v) => v.id === viewState.view) ?? views[0];
|
|
405
|
+
if (!useMobile || !requested) return requested;
|
|
406
|
+
// Already a mobile view? Keep it.
|
|
407
|
+
if (requested.id.endsWith("-mobile")) return requested;
|
|
408
|
+
// Agenda views: keep as-is (they adapt internally via mobile context).
|
|
409
|
+
if (requested.label === "Agenda") return requested;
|
|
410
|
+
// Planner / other views: remap to mobile variant with the same mode.
|
|
411
|
+
const mobileVariant = views.find((v) => v.id === `${requested.mode}-mobile`);
|
|
412
|
+
return mobileVariant ?? requested;
|
|
358
413
|
});
|
|
414
|
+
// Backward-compat alias used in the template.
|
|
359
415
|
const activeView = $derived(resolvedView);
|
|
416
|
+
// Non-mobile views for mode pills (exclude mobile entries).
|
|
360
417
|
const desktopViews = $derived(views.filter((v) => !v.id.endsWith("-mobile")));
|
|
418
|
+
// ── Date label (always visible, centered over views) ──
|
|
361
419
|
const dateLabel = $derived.by(() => {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
month: "long",
|
|
381
|
-
year: "numeric"
|
|
382
|
-
});
|
|
420
|
+
if (!showDates) {
|
|
421
|
+
return "";
|
|
422
|
+
}
|
|
423
|
+
if (viewState.mode === "day") {
|
|
424
|
+
return viewState.focusDate.toLocaleDateString(locale, {
|
|
425
|
+
weekday: "long",
|
|
426
|
+
month: "short",
|
|
427
|
+
day: "numeric"
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
if (viewState.mode === "week") {
|
|
431
|
+
// The actual visible span — respects custom day counts (3/5-day views).
|
|
432
|
+
return fmtWeekRange(viewState.range.start.getTime(), locale, viewState.range.end.getTime() - 1);
|
|
433
|
+
}
|
|
434
|
+
return viewState.focusDate.toLocaleDateString(locale, {
|
|
435
|
+
month: "long",
|
|
436
|
+
year: "numeric"
|
|
437
|
+
});
|
|
383
438
|
});
|
|
439
|
+
// Which modes are available?
|
|
384
440
|
const modes = $derived.by(() => {
|
|
385
|
-
|
|
386
|
-
|
|
441
|
+
const g = new Set(desktopViews.map((v) => v.mode));
|
|
442
|
+
return [
|
|
443
|
+
"day",
|
|
444
|
+
"week",
|
|
445
|
+
"month"
|
|
446
|
+
].filter((key) => g.has(key));
|
|
387
447
|
});
|
|
388
|
-
|
|
448
|
+
// Remember the last non-month view label so Month → Week round-trips
|
|
449
|
+
// restore the user's chosen view type (Agenda used to downgrade to Planner).
|
|
450
|
+
let lastViewLabel = $state(undefined);
|
|
389
451
|
$effect(() => {
|
|
390
|
-
|
|
391
|
-
|
|
452
|
+
const current = views.find((v) => v.id === viewState.view);
|
|
453
|
+
if (current && current.mode !== "month") lastViewLabel = current.label;
|
|
392
454
|
});
|
|
455
|
+
/** Switch to a different mode (day/week/month), preserving the current view label. */
|
|
393
456
|
function switchMode(g) {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
457
|
+
const currentView = desktopViews.find((v) => v.id === viewState.view) ?? activeView;
|
|
458
|
+
const preferredLabel = currentView?.mode === "month" ? lastViewLabel ?? currentView?.label : currentView?.label;
|
|
459
|
+
const match = desktopViews.find((v) => v.mode === g && v.label === preferredLabel);
|
|
460
|
+
const fallback = desktopViews.find((v) => v.mode === g);
|
|
461
|
+
const target = match ?? fallback;
|
|
462
|
+
if (target) viewState.setView(target.id);
|
|
400
463
|
}
|
|
464
|
+
// ── View-type switcher (Planner ↔ Agenda) ──
|
|
465
|
+
// Distinct labels registered for the current mode; when there's more than
|
|
466
|
+
// one, the header shows a second pill group to switch between them.
|
|
401
467
|
const labelsForMode = $derived.by(() => {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
468
|
+
const seen = [];
|
|
469
|
+
for (const v of desktopViews) {
|
|
470
|
+
if (v.mode === viewState.mode && !seen.includes(v.label)) seen.push(v.label);
|
|
471
|
+
}
|
|
472
|
+
return seen;
|
|
407
473
|
});
|
|
408
474
|
function switchLabel(label) {
|
|
409
|
-
|
|
410
|
-
|
|
475
|
+
const target = desktopViews.find((v) => v.mode === viewState.mode && v.label === label);
|
|
476
|
+
if (target) viewState.setView(target.id);
|
|
411
477
|
}
|
|
478
|
+
// Default month → day drill-down: when the host doesn't wire ondayclick,
|
|
479
|
+
// clicking a month cell (or its "+N more") focuses that date in a day view,
|
|
480
|
+
// so the month grid is usable out of the box.
|
|
412
481
|
const defaultDayClick = $derived.by(() => {
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
482
|
+
const target = desktopViews.find((v) => v.mode === "day" && v.label === lastViewLabel) ?? desktopViews.find((v) => v.mode === "day");
|
|
483
|
+
if (!target) return undefined;
|
|
484
|
+
return (date) => {
|
|
485
|
+
viewState.setFocusDate(date);
|
|
486
|
+
viewState.setView(target.id);
|
|
487
|
+
};
|
|
419
488
|
});
|
|
489
|
+
/** True when the current view range already includes today. */
|
|
420
490
|
const viewIncludesToday = $derived.by(() => {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
491
|
+
const now = new Date();
|
|
492
|
+
if (viewState.mode === "month") {
|
|
493
|
+
// The month grid's range is week-aligned and includes adjacent-month
|
|
494
|
+
// spill days — compare against the focused month instead.
|
|
495
|
+
const f = viewState.focusDate;
|
|
496
|
+
return f.getMonth() === now.getMonth() && f.getFullYear() === now.getFullYear();
|
|
497
|
+
}
|
|
498
|
+
const { start, end } = viewState.range;
|
|
499
|
+
return now.getTime() >= start.getTime() && now.getTime() < end.getTime();
|
|
428
500
|
});
|
|
501
|
+
/** Text direction: explicit prop wins, otherwise derived from the locale. */
|
|
429
502
|
const resolvedDir = $derived.by(() => {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
503
|
+
if (dir) return dir;
|
|
504
|
+
if (!locale) return undefined;
|
|
505
|
+
try {
|
|
506
|
+
const info = new Intl.Locale(locale);
|
|
507
|
+
const direction = info.textInfo?.direction ?? info.getTextInfo?.().direction;
|
|
508
|
+
return direction === "rtl" ? "rtl" : undefined;
|
|
509
|
+
} catch {
|
|
510
|
+
return undefined;
|
|
511
|
+
}
|
|
439
512
|
});
|
|
513
|
+
// ── Keyboard shortcuts (scoped to focus-within-calendar, not global) ──
|
|
514
|
+
// t → today, ←/→ → prev/next. Skips events already handled by views
|
|
515
|
+
// (e.g. month-grid arrow navigation) and anything typed into a field.
|
|
440
516
|
function handleShortcuts(e) {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
517
|
+
if (e.defaultPrevented || e.metaKey || e.ctrlKey || e.altKey) return;
|
|
518
|
+
const t = e.target;
|
|
519
|
+
if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable)) return;
|
|
520
|
+
if (e.key === "t" || e.key === "T") {
|
|
521
|
+
e.preventDefault();
|
|
522
|
+
viewState.goToday();
|
|
523
|
+
} else if (e.key === "ArrowLeft") {
|
|
524
|
+
e.preventDefault();
|
|
525
|
+
viewState.prev();
|
|
526
|
+
} else if (e.key === "ArrowRight") {
|
|
527
|
+
e.preventDefault();
|
|
528
|
+
viewState.next();
|
|
529
|
+
}
|
|
454
530
|
}
|
|
531
|
+
/** Header context for custom header snippet */
|
|
455
532
|
const headerCtx = $derived({
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
533
|
+
dateLabel,
|
|
534
|
+
mode: viewState.mode,
|
|
535
|
+
modes,
|
|
536
|
+
switchMode,
|
|
537
|
+
prev: () => viewState.prev(),
|
|
538
|
+
next: () => viewState.next(),
|
|
539
|
+
goToday: () => viewState.goToday(),
|
|
540
|
+
isViewOnToday: viewIncludesToday,
|
|
541
|
+
focusDate: viewState.focusDate
|
|
465
542
|
});
|
|
543
|
+
/** Navigation context for custom navigation snippet */
|
|
466
544
|
const navCtx = $derived({
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
545
|
+
prev: () => viewState.prev(),
|
|
546
|
+
next: () => viewState.next(),
|
|
547
|
+
goToday: () => viewState.goToday(),
|
|
548
|
+
isViewOnToday: viewIncludesToday,
|
|
549
|
+
focusDate: viewState.focusDate,
|
|
550
|
+
mode: viewState.mode
|
|
473
551
|
});
|
|
474
552
|
</script>
|
|
475
553
|
|