@nomideusz/svelte-calendar 0.7.5 → 0.9.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 +33 -5
- package/dist/calendar/Calendar.svelte +379 -497
- package/dist/calendar/Calendar.svelte.d.ts +13 -2
- package/dist/core/clock.svelte.d.ts +1 -1
- package/dist/core/clock.svelte.js +20 -9
- package/dist/core/locale.d.ts +4 -0
- package/dist/core/locale.js +4 -0
- package/dist/core/timezone.d.ts +12 -0
- package/dist/core/timezone.js +31 -0
- package/dist/engine/view-state.svelte.d.ts +2 -2
- package/dist/engine/view-state.svelte.js +20 -0
- package/dist/headless/create-calendar.svelte.js +29 -7
- package/dist/headless/types.d.ts +8 -6
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -0
- package/dist/primitives/DayHeader.svelte +9 -25
- package/dist/primitives/EmptySlot.svelte +15 -31
- package/dist/primitives/EventBlock.svelte +33 -61
- package/dist/primitives/NowIndicator.svelte +16 -42
- package/dist/primitives/TimeGutter.svelte +9 -23
- package/dist/views/agenda/Agenda.svelte +3 -10
- package/dist/views/agenda/AgendaDay.svelte +195 -167
- package/dist/views/agenda/AgendaWeek.svelte +141 -195
- package/dist/views/mobile/Mobile.svelte +3 -10
- package/dist/views/mobile/MobileDay.svelte +422 -266
- package/dist/views/mobile/MobileWeek.svelte +131 -186
- package/dist/views/month/MonthGrid.svelte +334 -0
- package/dist/views/month/MonthGrid.svelte.d.ts +15 -0
- package/dist/views/planner/Planner.svelte +3 -10
- package/dist/views/planner/PlannerDay.svelte +636 -546
- package/dist/views/planner/PlannerWeek.svelte +299 -412
- package/dist/views/shared/EventContent.svelte +16 -0
- package/dist/views/shared/EventContent.svelte.d.ts +9 -0
- package/dist/views/shared/context.svelte.d.ts +2 -0
- package/dist/views/shared/context.svelte.js +2 -0
- package/dist/widget/CalendarWidget.svelte +79 -116
- package/package.json +3 -2
- package/widget/svelte-calendar.css +321 -10
- package/widget/widget.js +2508 -1056
|
@@ -17,495 +17,377 @@
|
|
|
17
17
|
oneventcreate={handleCreate}
|
|
18
18
|
/>
|
|
19
19
|
-->
|
|
20
|
-
<script lang="ts">
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
// Sync days prop → viewState.dayCount
|
|
393
|
-
$effect(() => {
|
|
394
|
-
if (days !== undefined && viewState.dayCount !== days) viewState.setDayCount(days);
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
// Notify host when focusDate changes
|
|
398
|
-
$effect(() => {
|
|
399
|
-
const d = viewState.focusDate;
|
|
400
|
-
ondatechange?.(d);
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
// Keep view state's week-start rule in sync with incoming prop changes.
|
|
404
|
-
$effect(() => {
|
|
405
|
-
if (viewState.mondayStart !== mondayStart) {
|
|
406
|
-
viewState.setMondayStart(mondayStart);
|
|
407
|
-
}
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
// Notify host when active view changes (e.g. via mode toggles).
|
|
411
|
-
$effect(() => {
|
|
412
|
-
onviewchange?.(viewState.view);
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
// ── Resolve active view ──
|
|
417
|
-
// When mobile is active, Planner views get remapped to Mobile variants.
|
|
418
|
-
// Agenda views stay as Agenda — they're already list-based and will adapt
|
|
419
|
-
// via the 'calendar:mobile' context flag.
|
|
420
|
-
const resolvedView = $derived.by(() => {
|
|
421
|
-
const requested = views.find((v) => v.id === viewState.view) ?? views[0];
|
|
422
|
-
if (!useMobile || !requested) return requested;
|
|
423
|
-
// Already a mobile view? Keep it.
|
|
424
|
-
if (requested.id.endsWith('-mobile')) return requested;
|
|
425
|
-
// Agenda views: keep as-is (they adapt internally via mobile context).
|
|
426
|
-
if (requested.label === 'Agenda') return requested;
|
|
427
|
-
// Planner / other views: remap to mobile variant with the same mode.
|
|
428
|
-
const mobileVariant = views.find(
|
|
429
|
-
(v) => v.id === `${requested.mode}-mobile`
|
|
430
|
-
);
|
|
431
|
-
return mobileVariant ?? requested;
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
// Backward-compat alias used in the template.
|
|
435
|
-
const activeView = $derived(resolvedView);
|
|
436
|
-
|
|
437
|
-
// Non-mobile views for mode pills (exclude mobile entries).
|
|
438
|
-
const desktopViews = $derived(views.filter((v) => !v.id.endsWith('-mobile')));
|
|
439
|
-
|
|
440
|
-
// ── Date label (always visible, centered over views) ──
|
|
441
|
-
const dateLabel = $derived.by(() => {
|
|
442
|
-
if (!showDates) {
|
|
443
|
-
// Template week mode: just show weekday name (day) or nothing (week)
|
|
444
|
-
if (viewState.mode === 'day') {
|
|
445
|
-
return viewState.focusDate.toLocaleDateString(locale, { weekday: 'long' });
|
|
446
|
-
}
|
|
447
|
-
return ''; // week views have their own day headers
|
|
448
|
-
}
|
|
449
|
-
if (viewState.mode === 'day') {
|
|
450
|
-
return viewState.focusDate.toLocaleDateString(locale, {
|
|
451
|
-
weekday: 'long',
|
|
452
|
-
month: 'short',
|
|
453
|
-
day: 'numeric',
|
|
454
|
-
});
|
|
455
|
-
}
|
|
456
|
-
return viewState.focusDate.toLocaleDateString(locale, {
|
|
457
|
-
month: 'long',
|
|
458
|
-
year: 'numeric',
|
|
459
|
-
});
|
|
460
|
-
});
|
|
461
|
-
|
|
462
|
-
// Which modes are available?
|
|
463
|
-
const modes = $derived.by(() => {
|
|
464
|
-
const g = new Set(desktopViews.map((v) => v.mode));
|
|
465
|
-
return (['day', 'week'] as const).filter((key) => g.has(key));
|
|
466
|
-
});
|
|
467
|
-
|
|
468
|
-
const L = $derived(getLabels());
|
|
469
|
-
|
|
470
|
-
/** Switch to a different mode (day/week), preserving the current view label. */
|
|
471
|
-
function switchMode(g: 'day' | 'week') {
|
|
472
|
-
const currentLabel = desktopViews.find((v) => v.id === viewState.view)?.label
|
|
473
|
-
?? activeView?.label;
|
|
474
|
-
const match = desktopViews.find((v) => v.mode === g && v.label === currentLabel);
|
|
475
|
-
const fallback = desktopViews.find((v) => v.mode === g);
|
|
476
|
-
const target = match ?? fallback;
|
|
477
|
-
if (target) viewState.setView(target.id);
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
/** True when the current view range already includes today. */
|
|
481
|
-
const viewIncludesToday = $derived.by(() => {
|
|
482
|
-
const now = Date.now();
|
|
483
|
-
const { start, end } = viewState.range;
|
|
484
|
-
return now >= start.getTime() && now < end.getTime();
|
|
485
|
-
});
|
|
486
|
-
|
|
487
|
-
/** Header context for custom header snippet */
|
|
488
|
-
const headerCtx = $derived({
|
|
489
|
-
dateLabel,
|
|
490
|
-
mode: viewState.mode,
|
|
491
|
-
modes,
|
|
492
|
-
switchMode,
|
|
493
|
-
prev: () => viewState.prev(),
|
|
494
|
-
next: () => viewState.next(),
|
|
495
|
-
goToday: () => viewState.goToday(),
|
|
496
|
-
isViewOnToday: viewIncludesToday,
|
|
497
|
-
focusDate: viewState.focusDate,
|
|
498
|
-
});
|
|
499
|
-
|
|
500
|
-
/** Navigation context for custom navigation snippet */
|
|
501
|
-
const navCtx = $derived({
|
|
502
|
-
prev: () => viewState.prev(),
|
|
503
|
-
next: () => viewState.next(),
|
|
504
|
-
goToday: () => viewState.goToday(),
|
|
505
|
-
isViewOnToday: viewIncludesToday,
|
|
506
|
-
focusDate: viewState.focusDate,
|
|
507
|
-
mode: viewState.mode,
|
|
508
|
-
});
|
|
20
|
+
<script lang="ts">import { setContext, untrack } from "svelte";
|
|
21
|
+
import { createEventStore } from "../engine/event-store.svelte.js";
|
|
22
|
+
import { createViewState } from "../engine/view-state.svelte.js";
|
|
23
|
+
import { createSelection } from "../engine/selection.svelte.js";
|
|
24
|
+
import { createDragState } from "../engine/drag.svelte.js";
|
|
25
|
+
import { onMount } from "svelte";
|
|
26
|
+
import { getLabels } from "../core/locale.js";
|
|
27
|
+
import { auto } from "../theme/presets.js";
|
|
28
|
+
import { probeHostTheme, observeHostTheme } from "../theme/auto.js";
|
|
29
|
+
import Planner from "../views/planner/Planner.svelte";
|
|
30
|
+
import Agenda from "../views/agenda/Agenda.svelte";
|
|
31
|
+
import Mobile from "../views/mobile/Mobile.svelte";
|
|
32
|
+
import MonthGrid from "../views/month/MonthGrid.svelte";
|
|
33
|
+
import { wrapAdapterWithTimezone, toZonedTime, fromZonedTime } from "../core/timezone.js";
|
|
34
|
+
const MOBILE_BREAKPOINT = 768;
|
|
35
|
+
const DEFAULT_VIEWS = [
|
|
36
|
+
{ id: "day-planner", label: "Planner", mode: "day", component: Planner },
|
|
37
|
+
{ id: "week-planner", label: "Planner", mode: "week", component: Planner },
|
|
38
|
+
{ id: "day-agenda", label: "Agenda", mode: "day", component: Agenda },
|
|
39
|
+
{ id: "week-agenda", label: "Agenda", mode: "week", component: Agenda },
|
|
40
|
+
{ id: "day-mobile", label: "Mobile", mode: "day", component: Mobile },
|
|
41
|
+
{ id: "week-mobile", label: "Mobile", mode: "week", component: Mobile },
|
|
42
|
+
{ id: "month-grid", label: "Month", mode: "month", component: MonthGrid }
|
|
43
|
+
];
|
|
44
|
+
let {
|
|
45
|
+
adapter,
|
|
46
|
+
views = DEFAULT_VIEWS,
|
|
47
|
+
view: activeViewId,
|
|
48
|
+
theme = auto,
|
|
49
|
+
autoTheme,
|
|
50
|
+
mondayStart = true,
|
|
51
|
+
height: heightProp = 600,
|
|
52
|
+
borderRadius = 12,
|
|
53
|
+
dir,
|
|
54
|
+
locale,
|
|
55
|
+
readOnly = false,
|
|
56
|
+
visibleHours,
|
|
57
|
+
initialDate,
|
|
58
|
+
snapInterval = 15,
|
|
59
|
+
showModePills = true,
|
|
60
|
+
showNavigation = true,
|
|
61
|
+
equalDays = false,
|
|
62
|
+
showDates = true,
|
|
63
|
+
hideDays,
|
|
64
|
+
currentDate,
|
|
65
|
+
blockedSlots,
|
|
66
|
+
days,
|
|
67
|
+
minDuration,
|
|
68
|
+
maxDuration,
|
|
69
|
+
disabledDates,
|
|
70
|
+
compact = false,
|
|
71
|
+
mobile: mobileProp = "auto",
|
|
72
|
+
event: eventSnippet,
|
|
73
|
+
empty: emptySnippet,
|
|
74
|
+
dayHeader: dayHeaderSnippet,
|
|
75
|
+
header: headerSnippet,
|
|
76
|
+
navigation: navigationSnippet,
|
|
77
|
+
oneventclick,
|
|
78
|
+
oneventcreate,
|
|
79
|
+
oneventmove,
|
|
80
|
+
onviewchange,
|
|
81
|
+
ondatechange,
|
|
82
|
+
oneventhover,
|
|
83
|
+
ondayclick,
|
|
84
|
+
onerror,
|
|
85
|
+
timezone
|
|
86
|
+
} = $props();
|
|
87
|
+
const unzone = (d) => timezone ? fromZonedTime(d, timezone) : d;
|
|
88
|
+
const effectiveCreate = $derived(
|
|
89
|
+
readOnly || !oneventcreate ? void 0 : (range) => oneventcreate({ start: unzone(range.start), end: unzone(range.end) })
|
|
90
|
+
);
|
|
91
|
+
const effectiveMove = $derived(
|
|
92
|
+
readOnly || !oneventmove ? void 0 : (ev, start, end) => oneventmove(ev, unzone(start), unzone(end))
|
|
93
|
+
);
|
|
94
|
+
function handleEventClick(ev) {
|
|
95
|
+
selection.select(ev.id);
|
|
96
|
+
oneventclick?.(ev);
|
|
97
|
+
}
|
|
98
|
+
let containerWidth = $state(0);
|
|
99
|
+
const isMobileContainer = $derived(containerWidth > 0 && containerWidth < MOBILE_BREAKPOINT);
|
|
100
|
+
const useMobile = $derived(
|
|
101
|
+
mobileProp === "auto" ? isMobileContainer : Boolean(mobileProp)
|
|
102
|
+
);
|
|
103
|
+
let calEl = $state();
|
|
104
|
+
let probedTheme = $state("");
|
|
105
|
+
const needsProbe = $derived(theme === auto && autoTheme !== false);
|
|
106
|
+
onMount(() => {
|
|
107
|
+
if (!calEl) return;
|
|
108
|
+
containerWidth = calEl.clientWidth;
|
|
109
|
+
const ro = new ResizeObserver((entries) => {
|
|
110
|
+
containerWidth = Math.round(entries[0].contentRect.width);
|
|
111
|
+
});
|
|
112
|
+
ro.observe(calEl);
|
|
113
|
+
if (!needsProbe) return () => ro.disconnect();
|
|
114
|
+
const opts = typeof autoTheme === "object" ? autoTheme : {};
|
|
115
|
+
const stopTheme = observeHostTheme(calEl, (vars) => {
|
|
116
|
+
probedTheme = vars;
|
|
117
|
+
}, opts);
|
|
118
|
+
return () => {
|
|
119
|
+
ro.disconnect();
|
|
120
|
+
stopTheme?.();
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
const effectiveTheme = $derived(theme === auto && autoTheme !== false ? probedTheme : theme);
|
|
124
|
+
const effectiveAdapter = $derived(
|
|
125
|
+
timezone ? wrapAdapterWithTimezone(adapter, timezone) : adapter
|
|
126
|
+
);
|
|
127
|
+
const store = $derived(createEventStore(effectiveAdapter));
|
|
128
|
+
const viewState = createViewState(untrack(() => ({
|
|
129
|
+
view: activeViewId ?? views[0]?.id,
|
|
130
|
+
mondayStart,
|
|
131
|
+
// Focus lives on the zoned plane too — day boundaries follow the zone.
|
|
132
|
+
initialDate: initialDate && timezone ? toZonedTime(initialDate, timezone) : initialDate,
|
|
133
|
+
dayCount: days,
|
|
134
|
+
timezone,
|
|
135
|
+
modeForView: (viewId) => views.find((v) => v.id === viewId)?.mode
|
|
136
|
+
})));
|
|
137
|
+
const selection = createSelection();
|
|
138
|
+
const drag = createDragState();
|
|
139
|
+
async function commitDrag() {
|
|
140
|
+
if (readOnly) {
|
|
141
|
+
drag.cancel();
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const mode = drag.mode;
|
|
145
|
+
const payload = drag.commit();
|
|
146
|
+
if (!payload) return;
|
|
147
|
+
let { start, end } = payload;
|
|
148
|
+
if (mode === "create" || mode === "resize-start" || mode === "resize-end") {
|
|
149
|
+
if (end.getTime() <= start.getTime()) {
|
|
150
|
+
const floorMs = Math.max(1, snapInterval) * 6e4;
|
|
151
|
+
if (mode === "resize-start") start = new Date(end.getTime() - floorMs);
|
|
152
|
+
else end = new Date(start.getTime() + floorMs);
|
|
153
|
+
}
|
|
154
|
+
const durationMs = end.getTime() - start.getTime();
|
|
155
|
+
const durationMin = durationMs / 6e4;
|
|
156
|
+
if (minDuration && durationMin < minDuration) {
|
|
157
|
+
if (mode === "resize-start") {
|
|
158
|
+
start = new Date(end.getTime() - minDuration * 6e4);
|
|
159
|
+
} else {
|
|
160
|
+
end = new Date(start.getTime() + minDuration * 6e4);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (maxDuration && durationMin > maxDuration) {
|
|
164
|
+
if (mode === "resize-start") {
|
|
165
|
+
start = new Date(end.getTime() - maxDuration * 6e4);
|
|
166
|
+
} else {
|
|
167
|
+
end = new Date(start.getTime() + maxDuration * 6e4);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (disabledDates?.length) {
|
|
172
|
+
const startDay = new Date(start);
|
|
173
|
+
startDay.setHours(0, 0, 0, 0);
|
|
174
|
+
const endDay = new Date(end.getTime() - 1);
|
|
175
|
+
endDay.setHours(0, 0, 0, 0);
|
|
176
|
+
for (const dd of disabledDates) {
|
|
177
|
+
const dt = new Date(dd);
|
|
178
|
+
dt.setHours(0, 0, 0, 0);
|
|
179
|
+
const ts = dt.getTime();
|
|
180
|
+
if (ts >= startDay.getTime() && ts <= endDay.getTime()) return;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (blockedSlots?.length) {
|
|
184
|
+
const startH = start.getHours() + start.getMinutes() / 60;
|
|
185
|
+
const endH = end.getHours() + end.getMinutes() / 60 + (end.getDate() !== start.getDate() ? 24 : 0);
|
|
186
|
+
const jsDay = start.getDay();
|
|
187
|
+
const isoDay = jsDay === 0 ? 7 : jsDay;
|
|
188
|
+
for (const slot of blockedSlots) {
|
|
189
|
+
if (slot.day && slot.day !== isoDay) continue;
|
|
190
|
+
if (startH < slot.end && endH > slot.start) return;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if ((mode === "move" || mode === "resize-start" || mode === "resize-end") && payload.eventId) {
|
|
194
|
+
try {
|
|
195
|
+
await store.move(payload.eventId, start, end);
|
|
196
|
+
const ev = store.byId(payload.eventId);
|
|
197
|
+
if (ev) effectiveMove?.(ev, start, end);
|
|
198
|
+
} catch (e) {
|
|
199
|
+
const msg = e instanceof Error ? e.message : "";
|
|
200
|
+
if (msg.includes("read-only")) {
|
|
201
|
+
const ev = store.byId(payload.eventId);
|
|
202
|
+
if (ev) effectiveMove?.(ev, start, end);
|
|
203
|
+
} else if (!msg.includes("not found")) {
|
|
204
|
+
if (onerror) onerror(e instanceof Error ? e : new Error(String(e)));
|
|
205
|
+
else console.warn("[calendar] drag commit failed:", e);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
} else if (mode === "create") {
|
|
209
|
+
effectiveCreate?.({ start, end });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
let viewLoadRange = $state(null);
|
|
213
|
+
setContext("calendar", {
|
|
214
|
+
// Engine objects (hold $state internally)
|
|
215
|
+
get store() {
|
|
216
|
+
return store;
|
|
217
|
+
},
|
|
218
|
+
viewState,
|
|
219
|
+
selection,
|
|
220
|
+
drag,
|
|
221
|
+
commitDrag,
|
|
222
|
+
// Callbacks
|
|
223
|
+
get oneventclick() {
|
|
224
|
+
return handleEventClick;
|
|
225
|
+
},
|
|
226
|
+
get oneventcreate() {
|
|
227
|
+
return effectiveCreate;
|
|
228
|
+
},
|
|
229
|
+
get oneventmove() {
|
|
230
|
+
return effectiveMove;
|
|
231
|
+
},
|
|
232
|
+
get oneventhover() {
|
|
233
|
+
return oneventhover;
|
|
234
|
+
},
|
|
235
|
+
get ondayclick() {
|
|
236
|
+
return ondayclick;
|
|
237
|
+
},
|
|
238
|
+
get timezone() {
|
|
239
|
+
return timezone;
|
|
240
|
+
},
|
|
241
|
+
// Config (reactive via getters)
|
|
242
|
+
get readOnly() {
|
|
243
|
+
return readOnly;
|
|
244
|
+
},
|
|
245
|
+
get visibleHours() {
|
|
246
|
+
return visibleHours;
|
|
247
|
+
},
|
|
248
|
+
get snapInterval() {
|
|
249
|
+
return snapInterval;
|
|
250
|
+
},
|
|
251
|
+
get eventSnippet() {
|
|
252
|
+
return eventSnippet;
|
|
253
|
+
},
|
|
254
|
+
get emptySnippet() {
|
|
255
|
+
return emptySnippet;
|
|
256
|
+
},
|
|
257
|
+
get equalDays() {
|
|
258
|
+
return equalDays;
|
|
259
|
+
},
|
|
260
|
+
get showDates() {
|
|
261
|
+
return showDates;
|
|
262
|
+
},
|
|
263
|
+
get hideDays() {
|
|
264
|
+
return hideDays;
|
|
265
|
+
},
|
|
266
|
+
get blockedSlots() {
|
|
267
|
+
return blockedSlots;
|
|
268
|
+
},
|
|
269
|
+
get dayHeaderSnippet() {
|
|
270
|
+
return dayHeaderSnippet;
|
|
271
|
+
},
|
|
272
|
+
get minDuration() {
|
|
273
|
+
return minDuration;
|
|
274
|
+
},
|
|
275
|
+
get maxDuration() {
|
|
276
|
+
return maxDuration;
|
|
277
|
+
},
|
|
278
|
+
get disabledDates() {
|
|
279
|
+
return disabledDates;
|
|
280
|
+
},
|
|
281
|
+
get mobile() {
|
|
282
|
+
return useMobile;
|
|
283
|
+
},
|
|
284
|
+
get autoHeight() {
|
|
285
|
+
return heightProp === "auto";
|
|
286
|
+
},
|
|
287
|
+
get compact() {
|
|
288
|
+
return compact;
|
|
289
|
+
},
|
|
290
|
+
// Load range (read/write)
|
|
291
|
+
get loadRange() {
|
|
292
|
+
return viewLoadRange;
|
|
293
|
+
},
|
|
294
|
+
setLoadRange(range) {
|
|
295
|
+
viewLoadRange = range;
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
$effect(() => {
|
|
299
|
+
const range = viewLoadRange ?? viewState.range;
|
|
300
|
+
store.load({ start: range.start, end: range.end });
|
|
301
|
+
});
|
|
302
|
+
untrack(() => store.load({ start: viewState.range.start, end: viewState.range.end }));
|
|
303
|
+
$effect(() => {
|
|
304
|
+
if (activeViewId) viewState.setView(activeViewId);
|
|
305
|
+
});
|
|
306
|
+
$effect(() => {
|
|
307
|
+
if (currentDate) viewState.setFocusDate(currentDate);
|
|
308
|
+
});
|
|
309
|
+
$effect(() => {
|
|
310
|
+
if (days !== void 0 && viewState.dayCount !== days) viewState.setDayCount(days);
|
|
311
|
+
});
|
|
312
|
+
$effect(() => {
|
|
313
|
+
const d = viewState.focusDate;
|
|
314
|
+
ondatechange?.(d);
|
|
315
|
+
});
|
|
316
|
+
$effect(() => {
|
|
317
|
+
if (viewState.mondayStart !== mondayStart) {
|
|
318
|
+
viewState.setMondayStart(mondayStart);
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
$effect(() => {
|
|
322
|
+
onviewchange?.(viewState.view);
|
|
323
|
+
});
|
|
324
|
+
$effect(() => {
|
|
325
|
+
if (store.error && onerror) onerror(new Error(store.error));
|
|
326
|
+
});
|
|
327
|
+
const resolvedView = $derived.by(() => {
|
|
328
|
+
const requested = views.find((v) => v.id === viewState.view) ?? views[0];
|
|
329
|
+
if (!useMobile || !requested) return requested;
|
|
330
|
+
if (requested.id.endsWith("-mobile")) return requested;
|
|
331
|
+
if (requested.label === "Agenda") return requested;
|
|
332
|
+
const mobileVariant = views.find(
|
|
333
|
+
(v) => v.id === `${requested.mode}-mobile`
|
|
334
|
+
);
|
|
335
|
+
return mobileVariant ?? requested;
|
|
336
|
+
});
|
|
337
|
+
const activeView = $derived(resolvedView);
|
|
338
|
+
const desktopViews = $derived(views.filter((v) => !v.id.endsWith("-mobile")));
|
|
339
|
+
const dateLabel = $derived.by(() => {
|
|
340
|
+
if (!showDates) {
|
|
341
|
+
return "";
|
|
342
|
+
}
|
|
343
|
+
if (viewState.mode === "day") {
|
|
344
|
+
return viewState.focusDate.toLocaleDateString(locale, {
|
|
345
|
+
weekday: "long",
|
|
346
|
+
month: "short",
|
|
347
|
+
day: "numeric"
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
return viewState.focusDate.toLocaleDateString(locale, {
|
|
351
|
+
month: "long",
|
|
352
|
+
year: "numeric"
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
const modes = $derived.by(() => {
|
|
356
|
+
const g = new Set(desktopViews.map((v) => v.mode));
|
|
357
|
+
return ["day", "week", "month"].filter((key) => g.has(key));
|
|
358
|
+
});
|
|
359
|
+
const L = $derived(getLabels());
|
|
360
|
+
function switchMode(g) {
|
|
361
|
+
const currentLabel = desktopViews.find((v) => v.id === viewState.view)?.label ?? activeView?.label;
|
|
362
|
+
const match = desktopViews.find((v) => v.mode === g && v.label === currentLabel);
|
|
363
|
+
const fallback = desktopViews.find((v) => v.mode === g);
|
|
364
|
+
const target = match ?? fallback;
|
|
365
|
+
if (target) viewState.setView(target.id);
|
|
366
|
+
}
|
|
367
|
+
const viewIncludesToday = $derived.by(() => {
|
|
368
|
+
const now = Date.now();
|
|
369
|
+
const { start, end } = viewState.range;
|
|
370
|
+
return now >= start.getTime() && now < end.getTime();
|
|
371
|
+
});
|
|
372
|
+
const headerCtx = $derived({
|
|
373
|
+
dateLabel,
|
|
374
|
+
mode: viewState.mode,
|
|
375
|
+
modes,
|
|
376
|
+
switchMode,
|
|
377
|
+
prev: () => viewState.prev(),
|
|
378
|
+
next: () => viewState.next(),
|
|
379
|
+
goToday: () => viewState.goToday(),
|
|
380
|
+
isViewOnToday: viewIncludesToday,
|
|
381
|
+
focusDate: viewState.focusDate
|
|
382
|
+
});
|
|
383
|
+
const navCtx = $derived({
|
|
384
|
+
prev: () => viewState.prev(),
|
|
385
|
+
next: () => viewState.next(),
|
|
386
|
+
goToday: () => viewState.goToday(),
|
|
387
|
+
isViewOnToday: viewIncludesToday,
|
|
388
|
+
focusDate: viewState.focusDate,
|
|
389
|
+
mode: viewState.mode
|
|
390
|
+
});
|
|
509
391
|
</script>
|
|
510
392
|
|
|
511
393
|
<div
|
|
@@ -523,13 +405,13 @@
|
|
|
523
405
|
{@render headerSnippet(headerCtx)}
|
|
524
406
|
|
|
525
407
|
<!-- ─── Mobile header (flow layout, no absolute) ─── -->
|
|
526
|
-
{:else if useMobile}
|
|
408
|
+
{:else if useMobile && (showNavigation || (showModePills && modes.length > 1) || dateLabel)}
|
|
527
409
|
<div class="cal-m-hd">
|
|
528
410
|
<div class="cal-m-left">
|
|
529
411
|
{#if navigationSnippet}
|
|
530
412
|
{@render navigationSnippet(navCtx)}
|
|
531
413
|
{:else if showNavigation}
|
|
532
|
-
<button class="cal-m-nav" onclick={() => viewState.prev()} aria-label={viewState.mode === 'day' ? L.previousDay : L.previousWeek}>
|
|
414
|
+
<button class="cal-m-nav" onclick={() => viewState.prev()} aria-label={viewState.mode === 'day' ? L.previousDay : viewState.mode === 'month' ? L.previousMonth : L.previousWeek}>
|
|
533
415
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" width="16" height="16" aria-hidden="true"><path d="M10 3 5 8l5 5"/></svg>
|
|
534
416
|
</button>
|
|
535
417
|
{/if}
|
|
@@ -543,7 +425,7 @@
|
|
|
543
425
|
aria-pressed={viewState.mode === g}
|
|
544
426
|
onclick={() => switchMode(g)}
|
|
545
427
|
>
|
|
546
|
-
{g === 'day' ? L.day : L.week}
|
|
428
|
+
{g === 'day' ? L.day : g === 'week' ? L.week : L.month}
|
|
547
429
|
</button>
|
|
548
430
|
{/each}
|
|
549
431
|
</div>
|
|
@@ -554,7 +436,7 @@
|
|
|
554
436
|
|
|
555
437
|
<div class="cal-m-right">
|
|
556
438
|
{#if !navigationSnippet && showNavigation}
|
|
557
|
-
<button class="cal-m-nav" onclick={() => viewState.next()} aria-label={viewState.mode === 'day' ? L.nextDay : L.nextWeek}>
|
|
439
|
+
<button class="cal-m-nav" onclick={() => viewState.next()} aria-label={viewState.mode === 'day' ? L.nextDay : viewState.mode === 'month' ? L.nextMonth : L.nextWeek}>
|
|
558
440
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" width="16" height="16" aria-hidden="true"><path d="M6 3l5 5-5 5"/></svg>
|
|
559
441
|
</button>
|
|
560
442
|
{/if}
|
|
@@ -582,10 +464,10 @@
|
|
|
582
464
|
{L.today}
|
|
583
465
|
</button>
|
|
584
466
|
{/if}
|
|
585
|
-
<button class="cal-hd-btn" onclick={() => viewState.prev()} aria-label={viewState.mode === 'day' ? L.previousDay : L.previousWeek}>
|
|
467
|
+
<button class="cal-hd-btn" onclick={() => viewState.prev()} aria-label={viewState.mode === 'day' ? L.previousDay : viewState.mode === 'month' ? L.previousMonth : L.previousWeek}>
|
|
586
468
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="16" height="16" aria-hidden="true"><path d="M10 3 5 8l5 5"/></svg>
|
|
587
469
|
</button>
|
|
588
|
-
<button class="cal-hd-btn" onclick={() => viewState.next()} aria-label={viewState.mode === 'day' ? L.nextDay : L.nextWeek}>
|
|
470
|
+
<button class="cal-hd-btn" onclick={() => viewState.next()} aria-label={viewState.mode === 'day' ? L.nextDay : viewState.mode === 'month' ? L.nextMonth : L.nextWeek}>
|
|
589
471
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="16" height="16" aria-hidden="true"><path d="M6 3l5 5-5 5"/></svg>
|
|
590
472
|
</button>
|
|
591
473
|
{/if}
|
|
@@ -601,7 +483,7 @@
|
|
|
601
483
|
aria-pressed={viewState.mode === g}
|
|
602
484
|
onclick={() => switchMode(g)}
|
|
603
485
|
>
|
|
604
|
-
{g === 'day' ? L.day : L.week}
|
|
486
|
+
{g === 'day' ? L.day : g === 'week' ? L.week : L.month}
|
|
605
487
|
</button>
|
|
606
488
|
{/each}
|
|
607
489
|
</div>
|
|
@@ -621,7 +503,7 @@
|
|
|
621
503
|
mondayStart={viewState.mondayStart}
|
|
622
504
|
{locale}
|
|
623
505
|
focusDate={viewState.focusDate}
|
|
624
|
-
oneventclick={
|
|
506
|
+
oneventclick={handleEventClick}
|
|
625
507
|
oneventcreate={effectiveCreate}
|
|
626
508
|
readOnly={readOnly}
|
|
627
509
|
visibleHours={visibleHours}
|