@nomideusz/svelte-calendar 0.14.0 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/calendar/Calendar.svelte +451 -373
- 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 +185 -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 +181 -92
- 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/views/shared/context.svelte.d.ts +4 -0
- package/dist/views/shared/context.svelte.js +1 -0
- package/dist/widget/CalendarWidget.svelte +64 -83
- package/package.json +17 -10
- package/widget/widget.js +12723 -12135
|
@@ -1,4 +1,14 @@
|
|
|
1
|
-
<script lang="ts"
|
|
1
|
+
<script lang="ts">/**
|
|
2
|
+
* AgendaWeek — rolling N-day agenda view.
|
|
3
|
+
*
|
|
4
|
+
* "The Week Ahead":
|
|
5
|
+
* Today + tomorrow expanded with time slots/countdowns.
|
|
6
|
+
* Future days compact (dot + time + title).
|
|
7
|
+
* Past days dimmed.
|
|
8
|
+
*
|
|
9
|
+
* Answers: "What's coming up and when do I need to be ready?"
|
|
10
|
+
*/
|
|
11
|
+
import { createClock } from "../../core/clock.svelte.js";
|
|
2
12
|
import { sod, DAY_MS, startOfWeek, dayNum, isAllDay, isMultiDay, segmentForDay } from "../../core/time.js";
|
|
3
13
|
import { weekdayLong, monthLong } from "../../core/locale.js";
|
|
4
14
|
import { useCalendarContext } from "../shared/context.svelte.js";
|
|
@@ -6,16 +16,7 @@ import EventContent from "../shared/EventContent.svelte";
|
|
|
6
16
|
import { fmtTime, duration, timeUntilMs, progress, groupIntoSlots } from "../shared/format.js";
|
|
7
17
|
const ctx = useCalendarContext();
|
|
8
18
|
const L = $derived(ctx.labels);
|
|
9
|
-
let {
|
|
10
|
-
mondayStart = true,
|
|
11
|
-
locale,
|
|
12
|
-
height,
|
|
13
|
-
events = [],
|
|
14
|
-
style = "",
|
|
15
|
-
focusDate,
|
|
16
|
-
oneventclick,
|
|
17
|
-
selectedEventId = null
|
|
18
|
-
} = $props();
|
|
19
|
+
let { mondayStart = true, locale, height, events = [], style = "", focusDate, oneventclick, selectedEventId = null } = $props();
|
|
19
20
|
const clock = createClock(ctx.timezone);
|
|
20
21
|
const viewState = $derived(ctx.viewState);
|
|
21
22
|
const equalDays = $derived(ctx.equalDays);
|
|
@@ -24,110 +25,153 @@ const hideDays = $derived(ctx.hideDays);
|
|
|
24
25
|
const isMobile = $derived(ctx.isMobile);
|
|
25
26
|
const autoHeight = $derived(ctx.autoHeight);
|
|
26
27
|
const compact = $derived(ctx.compact);
|
|
28
|
+
// Timetable layout is a desktop affordance — stacked list stays on mobile.
|
|
27
29
|
const cols = $derived(ctx.columns && !isMobile);
|
|
28
30
|
const dayHeaderSnippet = $derived(ctx.dayHeaderSnippet);
|
|
29
31
|
const oneventhover = $derived(ctx.oneventhover);
|
|
32
|
+
const ondayclick = $derived(ctx.ondayclick);
|
|
30
33
|
const disabledSet = $derived(ctx.disabledSet);
|
|
34
|
+
function clickDay(ms) {
|
|
35
|
+
ondayclick?.(new Date(ms));
|
|
36
|
+
}
|
|
37
|
+
// ── Day-to-day card drag (timetable columns) ────────
|
|
38
|
+
// This view has no hour axis, so a drag can only choose a DAY: the card
|
|
39
|
+
// keeps its time of day. Wired through the same oneventmove the planner
|
|
40
|
+
// uses (readOnly-gated and timezone-unzoned at the Calendar layer);
|
|
41
|
+
// per-event data.readOnly opts out — same convention as PlannerWeek.
|
|
42
|
+
const oneventmove = $derived(ctx.oneventmove);
|
|
43
|
+
let dragId = $state(null);
|
|
44
|
+
let dropDay = $state(null);
|
|
45
|
+
const canDrag = (ev) => cols && !!oneventmove && !ev.data?.readOnly && !isAllDay(ev) && !isMultiDay(ev);
|
|
46
|
+
/** Valid drop day for the card in flight: not past, not closed. */
|
|
47
|
+
const isDropDay = (day) => dragId !== null && day.tier !== "past" && !disabledSet.has(day.ms);
|
|
48
|
+
function onCardDragStart(e, ev) {
|
|
49
|
+
dragId = String(ev.id);
|
|
50
|
+
if (e.dataTransfer) {
|
|
51
|
+
e.dataTransfer.effectAllowed = "move";
|
|
52
|
+
e.dataTransfer.setData("text/plain", String(ev.id));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function onDayDrop(e, day) {
|
|
56
|
+
if (!isDropDay(day)) return;
|
|
57
|
+
e.preventDefault();
|
|
58
|
+
e.stopPropagation();
|
|
59
|
+
const ev = events.find((x) => String(x.id) === dragId);
|
|
60
|
+
dragId = null;
|
|
61
|
+
dropDay = null;
|
|
62
|
+
if (!ev) return;
|
|
63
|
+
// Same-day drop is a no-op, not a zero-minute "move" for the host to reject.
|
|
64
|
+
if (sod(ev.start.getTime()) === day.ms) return;
|
|
65
|
+
const start = new Date(day.ms + (ev.start.getTime() - sod(ev.start.getTime())));
|
|
66
|
+
const end = new Date(start.getTime() + (ev.end.getTime() - ev.start.getTime()));
|
|
67
|
+
oneventmove?.(ev, start, end);
|
|
68
|
+
}
|
|
69
|
+
// ── Swipe navigation (mobile, touch only) ──────────
|
|
31
70
|
let swipeStartX = 0;
|
|
32
71
|
let swipeStartY = 0;
|
|
33
72
|
let swipeActive = false;
|
|
34
73
|
const SWIPE_THRESHOLD = 50;
|
|
35
74
|
function onPointerDown(e) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
75
|
+
if (!isMobile || e.pointerType !== "touch") return;
|
|
76
|
+
swipeActive = true;
|
|
77
|
+
swipeStartX = e.clientX;
|
|
78
|
+
swipeStartY = e.clientY;
|
|
40
79
|
}
|
|
41
80
|
function onPointerUp(e) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
81
|
+
if (!swipeActive || e.pointerType !== "touch") return;
|
|
82
|
+
swipeActive = false;
|
|
83
|
+
const dx = e.clientX - swipeStartX;
|
|
84
|
+
const dy = e.clientY - swipeStartY;
|
|
85
|
+
if (Math.abs(dx) > SWIPE_THRESHOLD && Math.abs(dx) > Math.abs(dy) * 1.4) {
|
|
86
|
+
if (dx > 0) viewState?.prev();
|
|
87
|
+
else viewState?.next();
|
|
88
|
+
}
|
|
50
89
|
}
|
|
51
90
|
function onPointerCancel() {
|
|
52
|
-
|
|
91
|
+
swipeActive = false;
|
|
53
92
|
}
|
|
93
|
+
// ── Compact-day disclosure ("+N more" → expand) ─────
|
|
54
94
|
let expandedDays = $state([]);
|
|
55
95
|
function toggleDayExpand(ms) {
|
|
56
|
-
|
|
96
|
+
expandedDays = expandedDays.includes(ms) ? expandedDays.filter((m) => m !== ms) : [...expandedDays, ms];
|
|
57
97
|
}
|
|
98
|
+
// ── Completed-events disclosure ("✓ N completed" → expand) ─────
|
|
58
99
|
let expandedPast = $state([]);
|
|
59
100
|
function togglePastExpand(ms) {
|
|
60
|
-
|
|
101
|
+
expandedPast = expandedPast.includes(ms) ? expandedPast.filter((m) => m !== ms) : [...expandedPast, ms];
|
|
61
102
|
}
|
|
103
|
+
// ── Format helpers (delegated to shared/format.ts) ──
|
|
104
|
+
// fmtTime, duration, groupIntoSlots imported at top
|
|
105
|
+
// Thin wrappers that bind locale / clock.tick:
|
|
62
106
|
const fmt = (d) => fmtTime(d, locale);
|
|
63
107
|
const eta = (ms) => timeUntilMs(ms, clock.tick, L);
|
|
64
108
|
const prog = (ev) => progress(ev, clock.tick);
|
|
109
|
+
// ── Event handlers ──────────────────────────────────
|
|
65
110
|
function handleClick(ev) {
|
|
66
|
-
|
|
111
|
+
oneventclick?.(ev);
|
|
67
112
|
}
|
|
68
|
-
const weekStartMs = $derived(
|
|
69
|
-
focusDate ? viewState?.dayCount === 7 ? startOfWeek(sod(focusDate.getTime()), mondayStart) : sod(focusDate.getTime()) : viewState?.dayCount === 7 ? startOfWeek(clock.today, mondayStart) : clock.today
|
|
70
|
-
);
|
|
113
|
+
const weekStartMs = $derived(focusDate ? viewState?.dayCount === 7 ? startOfWeek(sod(focusDate.getTime()), mondayStart) : sod(focusDate.getTime()) : viewState?.dayCount === 7 ? startOfWeek(clock.today, mondayStart) : clock.today);
|
|
71
114
|
const customDays = $derived(viewState?.dayCount ?? 7);
|
|
72
115
|
const weekDays = $derived.by(() => {
|
|
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
|
-
|
|
116
|
+
const now = clock.tick;
|
|
117
|
+
const todayMs = clock.today;
|
|
118
|
+
const tomorrowMs = todayMs + DAY_MS;
|
|
119
|
+
const out = [];
|
|
120
|
+
for (let i = 0; i < customDays; i++) {
|
|
121
|
+
const ms = weekStartMs + i * DAY_MS;
|
|
122
|
+
const dEnd = ms + DAY_MS;
|
|
123
|
+
const dayEvts = events.filter((ev) => ev.start.getTime() < dEnd && ev.end.getTime() > ms).sort((a, b) => a.start.getTime() - b.start.getTime());
|
|
124
|
+
const allDayEvts = dayEvts.filter((ev) => isAllDay(ev) || isMultiDay(ev));
|
|
125
|
+
const timedEvts = dayEvts.filter((ev) => !isAllDay(ev) && !isMultiDay(ev));
|
|
126
|
+
const totalMinutes = timedEvts.reduce((sum, ev) => {
|
|
127
|
+
const s = Math.max(ev.start.getTime(), ms);
|
|
128
|
+
const e = Math.min(ev.end.getTime(), dEnd);
|
|
129
|
+
return sum + (e - s) / 6e4;
|
|
130
|
+
}, 0);
|
|
131
|
+
const pastEvents = [];
|
|
132
|
+
const currentEvents = [];
|
|
133
|
+
const upcomingEvents = [];
|
|
134
|
+
for (const ev of timedEvts) {
|
|
135
|
+
if (ev.end.getTime() <= now) pastEvents.push(ev);
|
|
136
|
+
else if (ev.start.getTime() <= now && ev.end.getTime() > now) currentEvents.push(ev);
|
|
137
|
+
else upcomingEvents.push(ev);
|
|
138
|
+
}
|
|
139
|
+
let tier;
|
|
140
|
+
if (equalDays) {
|
|
141
|
+
tier = "upcoming";
|
|
142
|
+
} else if (ms === todayMs) {
|
|
143
|
+
tier = "today";
|
|
144
|
+
} else if (ms === tomorrowMs) {
|
|
145
|
+
tier = "tomorrow";
|
|
146
|
+
} else if (ms < todayMs) {
|
|
147
|
+
tier = "past";
|
|
148
|
+
} else {
|
|
149
|
+
tier = "upcoming";
|
|
150
|
+
}
|
|
151
|
+
out.push({
|
|
152
|
+
ms,
|
|
153
|
+
dayName: weekdayLong(ms, locale),
|
|
154
|
+
dateLabel: `${monthLong(ms, locale)} ${dayNum(ms)}`,
|
|
155
|
+
tier,
|
|
156
|
+
isToday: ms === todayMs,
|
|
157
|
+
events: dayEvts,
|
|
158
|
+
allDayEvents: allDayEvts,
|
|
159
|
+
timedEvents: timedEvts,
|
|
160
|
+
pastEvents,
|
|
161
|
+
currentEvents,
|
|
162
|
+
upcomingEvents,
|
|
163
|
+
totalHours: Math.round(totalMinutes / 60 * 10) / 10
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
// Filter hidden days if hideDays is set
|
|
167
|
+
if (hideDays?.length) {
|
|
168
|
+
return out.filter((d) => {
|
|
169
|
+
const jsDay = new Date(d.ms).getDay();
|
|
170
|
+
const iso = jsDay === 0 ? 7 : jsDay;
|
|
171
|
+
return !hideDays.includes(iso);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
return out;
|
|
131
175
|
});
|
|
132
176
|
</script>
|
|
133
177
|
|
|
@@ -143,6 +187,10 @@ const weekDays = $derived.by(() => {
|
|
|
143
187
|
class:ag-card--limited={ev.status === 'limited'}
|
|
144
188
|
style:--ev-color={ev.color || 'var(--dt-accent)'}
|
|
145
189
|
aria-label="{ev.title}{ev.status === 'cancelled' ? ' (cancelled)' : ''}{ev.status === 'tentative' ? ' (tentative)' : ''}{ev.status === 'full' ? ' (full)' : ''}{ev.status === 'limited' ? ' (limited)' : ''}, {fmt(ev.start)} to {fmt(ev.end)}, {duration(ev)}"
|
|
190
|
+
class:ag-card--drag={dragId === String(ev.id)}
|
|
191
|
+
draggable={canDrag(ev)}
|
|
192
|
+
ondragstart={(e) => onCardDragStart(e, ev)}
|
|
193
|
+
ondragend={() => ((dragId = null), (dropDay = null))}
|
|
146
194
|
onclick={() => handleClick(ev)}
|
|
147
195
|
onpointerenter={() => oneventhover?.(ev)}
|
|
148
196
|
>
|
|
@@ -252,8 +300,13 @@ const weekDays = $derived.by(() => {
|
|
|
252
300
|
{@const expanded = day.tier === 'today' || day.tier === 'tomorrow'}
|
|
253
301
|
{#if day.tier === 'past'}
|
|
254
302
|
<!-- Past day: single collapsed line -->
|
|
255
|
-
<div class="ag-wday ag-wday--past" class:ag-wday--disabled={disabledSet.has(day.ms)} role="listitem">
|
|
256
|
-
|
|
303
|
+
<div class="ag-wday ag-wday--past" class:ag-wday--disabled={disabledSet.has(day.ms)} data-day={day.ms} role="listitem">
|
|
304
|
+
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
|
|
305
|
+
<div
|
|
306
|
+
class="ag-wday-head"
|
|
307
|
+
class:ag-wday-head--click={!!ondayclick}
|
|
308
|
+
onclick={ondayclick ? () => clickDay(day.ms) : undefined}
|
|
309
|
+
>
|
|
257
310
|
<div class="ag-wday-head-left">
|
|
258
311
|
<span class="ag-wday-name">{day.dayName}</span>
|
|
259
312
|
{#if showDates}<span class="ag-wday-date">{day.dateLabel}</span>{/if}
|
|
@@ -278,16 +331,34 @@ const weekDays = $derived.by(() => {
|
|
|
278
331
|
{/if}
|
|
279
332
|
</div>
|
|
280
333
|
{:else}
|
|
334
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
281
335
|
<div
|
|
282
336
|
class="ag-wday"
|
|
283
337
|
class:ag-wday--today={day.tier === 'today'}
|
|
284
338
|
class:ag-wday--tomorrow={day.tier === 'tomorrow'}
|
|
285
339
|
class:ag-wday--equal={equalDays}
|
|
286
340
|
class:ag-wday--disabled={disabledSet.has(day.ms)}
|
|
341
|
+
class:ag-wday--drop={dropDay === day.ms && isDropDay(day)}
|
|
342
|
+
data-day={day.ms}
|
|
287
343
|
role="listitem"
|
|
344
|
+
ondragover={(e) => {
|
|
345
|
+
if (!isDropDay(day)) return;
|
|
346
|
+
e.preventDefault();
|
|
347
|
+
dropDay = day.ms;
|
|
348
|
+
if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';
|
|
349
|
+
}}
|
|
350
|
+
ondragleave={() => {
|
|
351
|
+
if (dropDay === day.ms) dropDay = null;
|
|
352
|
+
}}
|
|
353
|
+
ondrop={(e) => onDayDrop(e, day)}
|
|
288
354
|
>
|
|
289
355
|
<!-- Day header -->
|
|
290
|
-
|
|
356
|
+
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
|
|
357
|
+
<div
|
|
358
|
+
class="ag-wday-head"
|
|
359
|
+
class:ag-wday-head--click={!!ondayclick}
|
|
360
|
+
onclick={ondayclick ? () => clickDay(day.ms) : undefined}
|
|
361
|
+
>
|
|
291
362
|
<div class="ag-wday-head-left">
|
|
292
363
|
{#if day.isToday}
|
|
293
364
|
<span class="ag-wday-badge">{L.today}</span>
|
|
@@ -329,7 +400,12 @@ const weekDays = $derived.by(() => {
|
|
|
329
400
|
{/if}
|
|
330
401
|
|
|
331
402
|
{#if day.events.length === 0}
|
|
332
|
-
|
|
403
|
+
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
|
|
404
|
+
<div
|
|
405
|
+
class="ag-wday-empty"
|
|
406
|
+
class:ag-wday-empty--click={!!ondayclick}
|
|
407
|
+
onclick={ondayclick ? () => clickDay(day.ms) : undefined}
|
|
408
|
+
>{L.noEvents}</div>
|
|
333
409
|
{:else if compact && !cols}
|
|
334
410
|
<!-- Compact: minimal dot + time + title rows for all days.
|
|
335
411
|
Ignored in columns mode — single-line rows truncate to
|
|
@@ -692,6 +768,10 @@ const weekDays = $derived.by(() => {
|
|
|
692
768
|
padding: 2px 0 4px;
|
|
693
769
|
}
|
|
694
770
|
|
|
771
|
+
.ag-wday-head--click,
|
|
772
|
+
.ag-wday-empty--click {
|
|
773
|
+
cursor: pointer;
|
|
774
|
+
}
|
|
695
775
|
.ag-wday-head {
|
|
696
776
|
display: flex;
|
|
697
777
|
justify-content: space-between;
|
|
@@ -984,6 +1064,15 @@ const weekDays = $derived.by(() => {
|
|
|
984
1064
|
border-radius: 4px;
|
|
985
1065
|
}
|
|
986
1066
|
|
|
1067
|
+
/* ═══ Day-drop drag ═══ */
|
|
1068
|
+
.ag-card--drag {
|
|
1069
|
+
opacity: 0.4;
|
|
1070
|
+
}
|
|
1071
|
+
.ag-wday--drop {
|
|
1072
|
+
background: color-mix(in srgb, var(--dt-accent, #2563eb) 6%, transparent);
|
|
1073
|
+
box-shadow: inset 0 0 0 1px var(--dt-accent, #2563eb);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
987
1076
|
/* ═══ Timetable columns (desktop) ═══ */
|
|
988
1077
|
.ag--cols .ag-body {
|
|
989
1078
|
display: grid;
|