@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.
@@ -1,4 +1,14 @@
1
- <script lang="ts">import { createClock } from "../../core/clock.svelte.js";
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,139 +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);
30
32
  const ondayclick = $derived(ctx.ondayclick);
31
33
  const disabledSet = $derived(ctx.disabledSet);
32
34
  function clickDay(ms) {
33
- ondayclick?.(new Date(ms));
35
+ ondayclick?.(new Date(ms));
34
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.
35
42
  const oneventmove = $derived(ctx.oneventmove);
36
43
  let dragId = $state(null);
37
44
  let dropDay = $state(null);
38
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. */
39
47
  const isDropDay = (day) => dragId !== null && day.tier !== "past" && !disabledSet.has(day.ms);
40
48
  function onCardDragStart(e, ev) {
41
- dragId = String(ev.id);
42
- if (e.dataTransfer) {
43
- e.dataTransfer.effectAllowed = "move";
44
- e.dataTransfer.setData("text/plain", String(ev.id));
45
- }
49
+ dragId = String(ev.id);
50
+ if (e.dataTransfer) {
51
+ e.dataTransfer.effectAllowed = "move";
52
+ e.dataTransfer.setData("text/plain", String(ev.id));
53
+ }
46
54
  }
47
55
  function onDayDrop(e, day) {
48
- if (!isDropDay(day)) return;
49
- e.preventDefault();
50
- e.stopPropagation();
51
- const ev = events.find((x) => String(x.id) === dragId);
52
- dragId = null;
53
- dropDay = null;
54
- if (!ev) return;
55
- if (sod(ev.start.getTime()) === day.ms) return;
56
- const start = new Date(day.ms + (ev.start.getTime() - sod(ev.start.getTime())));
57
- const end = new Date(start.getTime() + (ev.end.getTime() - ev.start.getTime()));
58
- oneventmove?.(ev, start, end);
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);
59
68
  }
69
+ // ── Swipe navigation (mobile, touch only) ──────────
60
70
  let swipeStartX = 0;
61
71
  let swipeStartY = 0;
62
72
  let swipeActive = false;
63
73
  const SWIPE_THRESHOLD = 50;
64
74
  function onPointerDown(e) {
65
- if (!isMobile || e.pointerType !== "touch") return;
66
- swipeActive = true;
67
- swipeStartX = e.clientX;
68
- swipeStartY = e.clientY;
75
+ if (!isMobile || e.pointerType !== "touch") return;
76
+ swipeActive = true;
77
+ swipeStartX = e.clientX;
78
+ swipeStartY = e.clientY;
69
79
  }
70
80
  function onPointerUp(e) {
71
- if (!swipeActive || e.pointerType !== "touch") return;
72
- swipeActive = false;
73
- const dx = e.clientX - swipeStartX;
74
- const dy = e.clientY - swipeStartY;
75
- if (Math.abs(dx) > SWIPE_THRESHOLD && Math.abs(dx) > Math.abs(dy) * 1.4) {
76
- if (dx > 0) viewState?.prev();
77
- else viewState?.next();
78
- }
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
+ }
79
89
  }
80
90
  function onPointerCancel() {
81
- swipeActive = false;
91
+ swipeActive = false;
82
92
  }
93
+ // ── Compact-day disclosure ("+N more" → expand) ─────
83
94
  let expandedDays = $state([]);
84
95
  function toggleDayExpand(ms) {
85
- expandedDays = expandedDays.includes(ms) ? expandedDays.filter((m) => m !== ms) : [...expandedDays, ms];
96
+ expandedDays = expandedDays.includes(ms) ? expandedDays.filter((m) => m !== ms) : [...expandedDays, ms];
86
97
  }
98
+ // ── Completed-events disclosure ("✓ N completed" → expand) ─────
87
99
  let expandedPast = $state([]);
88
100
  function togglePastExpand(ms) {
89
- expandedPast = expandedPast.includes(ms) ? expandedPast.filter((m) => m !== ms) : [...expandedPast, ms];
101
+ expandedPast = expandedPast.includes(ms) ? expandedPast.filter((m) => m !== ms) : [...expandedPast, ms];
90
102
  }
103
+ // ── Format helpers (delegated to shared/format.ts) ──
104
+ // fmtTime, duration, groupIntoSlots imported at top
105
+ // Thin wrappers that bind locale / clock.tick:
91
106
  const fmt = (d) => fmtTime(d, locale);
92
107
  const eta = (ms) => timeUntilMs(ms, clock.tick, L);
93
108
  const prog = (ev) => progress(ev, clock.tick);
109
+ // ── Event handlers ──────────────────────────────────
94
110
  function handleClick(ev) {
95
- oneventclick?.(ev);
111
+ oneventclick?.(ev);
96
112
  }
97
- const weekStartMs = $derived(
98
- focusDate ? viewState?.dayCount === 7 ? startOfWeek(sod(focusDate.getTime()), mondayStart) : sod(focusDate.getTime()) : viewState?.dayCount === 7 ? startOfWeek(clock.today, mondayStart) : clock.today
99
- );
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);
100
114
  const customDays = $derived(viewState?.dayCount ?? 7);
101
115
  const weekDays = $derived.by(() => {
102
- const now = clock.tick;
103
- const todayMs = clock.today;
104
- const tomorrowMs = todayMs + DAY_MS;
105
- const out = [];
106
- for (let i = 0; i < customDays; i++) {
107
- const ms = weekStartMs + i * DAY_MS;
108
- const dEnd = ms + DAY_MS;
109
- const dayEvts = events.filter((ev) => ev.start.getTime() < dEnd && ev.end.getTime() > ms).sort((a, b) => a.start.getTime() - b.start.getTime());
110
- const allDayEvts = dayEvts.filter((ev) => isAllDay(ev) || isMultiDay(ev));
111
- const timedEvts = dayEvts.filter((ev) => !isAllDay(ev) && !isMultiDay(ev));
112
- const totalMinutes = timedEvts.reduce((sum, ev) => {
113
- const s = Math.max(ev.start.getTime(), ms);
114
- const e = Math.min(ev.end.getTime(), dEnd);
115
- return sum + (e - s) / 6e4;
116
- }, 0);
117
- const pastEvents = [];
118
- const currentEvents = [];
119
- const upcomingEvents = [];
120
- for (const ev of timedEvts) {
121
- if (ev.end.getTime() <= now) pastEvents.push(ev);
122
- else if (ev.start.getTime() <= now && ev.end.getTime() > now) currentEvents.push(ev);
123
- else upcomingEvents.push(ev);
124
- }
125
- let tier;
126
- if (equalDays) {
127
- tier = "upcoming";
128
- } else if (ms === todayMs) {
129
- tier = "today";
130
- } else if (ms === tomorrowMs) {
131
- tier = "tomorrow";
132
- } else if (ms < todayMs) {
133
- tier = "past";
134
- } else {
135
- tier = "upcoming";
136
- }
137
- out.push({
138
- ms,
139
- dayName: weekdayLong(ms, locale),
140
- dateLabel: `${monthLong(ms, locale)} ${dayNum(ms)}`,
141
- tier,
142
- isToday: ms === todayMs,
143
- events: dayEvts,
144
- allDayEvents: allDayEvts,
145
- timedEvents: timedEvts,
146
- pastEvents,
147
- currentEvents,
148
- upcomingEvents,
149
- totalHours: Math.round(totalMinutes / 60 * 10) / 10
150
- });
151
- }
152
- if (hideDays?.length) {
153
- return out.filter((d) => {
154
- const jsDay = new Date(d.ms).getDay();
155
- const iso = jsDay === 0 ? 7 : jsDay;
156
- return !hideDays.includes(iso);
157
- });
158
- }
159
- return out;
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;
160
175
  });
161
176
  </script>
162
177