@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.
Files changed (39) hide show
  1. package/README.md +33 -5
  2. package/dist/calendar/Calendar.svelte +379 -497
  3. package/dist/calendar/Calendar.svelte.d.ts +13 -2
  4. package/dist/core/clock.svelte.d.ts +1 -1
  5. package/dist/core/clock.svelte.js +20 -9
  6. package/dist/core/locale.d.ts +4 -0
  7. package/dist/core/locale.js +4 -0
  8. package/dist/core/timezone.d.ts +12 -0
  9. package/dist/core/timezone.js +31 -0
  10. package/dist/engine/view-state.svelte.d.ts +2 -2
  11. package/dist/engine/view-state.svelte.js +20 -0
  12. package/dist/headless/create-calendar.svelte.js +29 -7
  13. package/dist/headless/types.d.ts +8 -6
  14. package/dist/index.d.ts +3 -0
  15. package/dist/index.js +4 -0
  16. package/dist/primitives/DayHeader.svelte +9 -25
  17. package/dist/primitives/EmptySlot.svelte +15 -31
  18. package/dist/primitives/EventBlock.svelte +33 -61
  19. package/dist/primitives/NowIndicator.svelte +16 -42
  20. package/dist/primitives/TimeGutter.svelte +9 -23
  21. package/dist/views/agenda/Agenda.svelte +3 -10
  22. package/dist/views/agenda/AgendaDay.svelte +195 -167
  23. package/dist/views/agenda/AgendaWeek.svelte +141 -195
  24. package/dist/views/mobile/Mobile.svelte +3 -10
  25. package/dist/views/mobile/MobileDay.svelte +422 -266
  26. package/dist/views/mobile/MobileWeek.svelte +131 -186
  27. package/dist/views/month/MonthGrid.svelte +334 -0
  28. package/dist/views/month/MonthGrid.svelte.d.ts +15 -0
  29. package/dist/views/planner/Planner.svelte +3 -10
  30. package/dist/views/planner/PlannerDay.svelte +636 -546
  31. package/dist/views/planner/PlannerWeek.svelte +299 -412
  32. package/dist/views/shared/EventContent.svelte +16 -0
  33. package/dist/views/shared/EventContent.svelte.d.ts +9 -0
  34. package/dist/views/shared/context.svelte.d.ts +2 -0
  35. package/dist/views/shared/context.svelte.js +2 -0
  36. package/dist/widget/CalendarWidget.svelte +79 -116
  37. package/package.json +3 -2
  38. package/widget/svelte-calendar.css +321 -10
  39. package/widget/widget.js +2508 -1056
@@ -8,413 +8,298 @@
8
8
  • Events are clean horizontal bars with colour fill, "9AM- 10AM Title" inline.
9
9
  • Generous whitespace, thin dividers, minimal chrome.
10
10
  -->
11
- <script lang="ts">
12
- import { onMount, tick, untrack } from 'svelte';
13
- import { flip } from 'svelte/animate';
14
- import { crossfade } from 'svelte/transition';
15
- import { prefersReducedMotion } from 'svelte/motion';
16
- import { useCalendarContext } from '../shared/context.svelte.js';
17
- import { createClock } from '../../core/clock.svelte.js';
18
- import type { TimelineEvent, BlockedSlot } from '../../core/types.js';
19
- import type { DragState } from '../../engine/drag.svelte.js';
20
- import type { ViewState } from '../../engine/view-state.svelte.js';
21
- import { DAY_MS, HOUR_MS, sod } from '../../core/time.js';
22
- import { startOfWeek as sowFn, fractionalHour, isAllDay, isMultiDay, segmentForDay } from '../../core/time.js';
23
- import type { DaySegment } from '../../core/time.js';
24
- import { weekdayShort, monthLong, fmtTime as _fmtTime, getLabels } from '../../core/locale.js';
25
-
26
- const L = $derived(getLabels());
27
-
28
- interface Props {
29
- mondayStart?: boolean;
30
- locale?: string;
31
- height?: number | null;
32
- events?: TimelineEvent[];
33
- style?: string;
34
- focusDate?: Date;
35
- oneventclick?: (event: TimelineEvent) => void;
36
- oneventcreate?: (range: { start: Date; end: Date }) => void;
37
- selectedEventId?: string | null;
38
- readOnly?: boolean;
39
- [key: string]: unknown;
40
- }
41
-
42
- let {
43
- mondayStart = true,
44
- locale,
45
- height = 520,
46
- events = [],
47
- style = '',
48
- focusDate,
49
- oneventclick,
50
- oneventcreate,
51
- selectedEventId = null,
52
- readOnly = false,
53
- }: Props = $props();
54
-
55
- const ctx = useCalendarContext();
56
- const clock = createClock();
57
-
58
- // Drag ghost flies between day cells instead of teleporting.
59
- // No fallback: without a counterpart (drag start/end) it appears/disappears instantly.
60
- const ANIM = $derived(prefersReducedMotion.current ? 0 : 180);
61
- const [previewSend, previewReceive] = crossfade({ duration: () => (prefersReducedMotion.current ? 0 : 160) });
62
- const drag = $derived(ctx.drag);
63
- const commitDragCtx = $derived(ctx.commitDrag);
64
- const viewState = $derived(ctx.viewState);
65
- const loadRangeCtx = $derived(ctx.loadRange);
66
- const equalDays = $derived(ctx.equalDays);
67
- const showDates = $derived(ctx.showDates);
68
- const hideDays = $derived(ctx.hideDays);
69
- const blockedSlots = $derived(ctx.blockedSlots);
70
- const dayHeaderSnippet = $derived(ctx.dayHeaderSnippet);
71
- const minDuration = $derived(ctx.minDuration);
72
- const autoHeight = $derived(ctx.autoHeight);
73
- const oneventhover = $derived(ctx.oneventhover);
74
- const disabledSet = $derived(ctx.disabledSet);
75
-
76
- // ─── Buffer config ─────────────────────────────
77
- const INITIAL_BUFFER = 52; // ±1 year on load
78
- const EXTEND_BY = 26; // +6 months when user hits edge
79
- const EDGE_PX = 200; // extend when this close to scroll edge
80
- let bufferBefore = $state(INITIAL_BUFFER);
81
- let bufferAfter = $state(INITIAL_BUFFER);
82
- const MAX_EVENTS_SHOWN = 5;
83
-
84
- const _initMs = untrack(() => sod(focusDate?.getTime() ?? Date.now()));
85
- let internalFocusMs = $state(_initMs);
86
- let lastExternalMs = _initMs;
87
-
88
- let el: HTMLDivElement;
89
-
90
- function scrollWeekIntoContainer(targetMs?: number, behavior: ScrollBehavior = 'auto') {
91
- if (!el) return;
92
- let target: HTMLElement | null = null;
93
- if (targetMs !== undefined) {
94
- // Find the week row containing this date
95
- const rows = el.querySelectorAll<HTMLElement>('[data-week]');
96
- for (const row of rows) {
97
- const weekMs = Number(row.dataset.week);
98
- if (weekMs <= targetMs && targetMs < weekMs + customDays * DAY_MS) {
99
- target = row;
100
- break;
101
- }
102
- }
103
- }
104
- // Fall back to current week
105
- if (!target) target = el.querySelector<HTMLElement>('.wg-week--current');
106
- if (!target) return;
107
- const targetTop = target.offsetTop - (el.clientHeight - target.offsetHeight) / 2;
108
- el.scrollTo({ top: Math.max(0, targetTop), behavior });
109
- }
110
-
111
- // ─── Derived ────────────────────────────────────────
112
- const todayMs = $derived(clock.today);
113
- const customDays = $derived(viewState?.dayCount ?? 7);
114
- const anchorPeriodStart = $derived(
115
- customDays === 7
116
- ? sowFn(internalFocusMs, mondayStart)
117
- : sod(internalFocusMs)
118
- );
119
-
120
- // ─── Declare load range for entire visible buffer ────────
121
- // Instead of calling store.load() directly, we tell Calendar
122
- // what range we need. Calendar's single $effect handles loading.
123
- $effect(() => {
124
- if (!loadRangeCtx) return;
125
- const rangeStart = new Date(anchorPeriodStart - bufferBefore * customDays * DAY_MS);
126
- const rangeEnd = new Date(anchorPeriodStart + (bufferAfter + 1) * customDays * DAY_MS);
127
- loadRangeCtx.set({ start: rangeStart, end: rangeEnd });
128
- return () => loadRangeCtx.set(null);
129
- });
130
-
131
- // ─── Week data ──────────────────────────────────────
132
- interface WeekRow {
133
- weekStart: number;
134
- isCurrent: boolean;
135
- monthLabel: string | null;
136
- days: DayCell[];
137
- }
138
-
139
- interface DayCell {
140
- ms: number;
141
- dayNum: number;
142
- isToday: boolean;
143
- isPast: boolean;
144
- isWeekend: boolean;
145
- isFirstOfMonth: boolean;
146
- monthLabel: string | null;
147
- events: TimelineEvent[];
148
- /** All-day or multi-day event segments for this day */
149
- allDaySegments: DaySegment[];
150
- }
151
-
152
- const weeks = $derived.by(() => {
153
- const result: WeekRow[] = [];
154
-
155
- for (let w = -bufferBefore; w <= bufferAfter; w++) {
156
- const periodStart = anchorPeriodStart + w * customDays * DAY_MS;
157
- const isCurrent = todayMs >= periodStart && todayMs < periodStart + customDays * DAY_MS;
158
- const days: DayCell[] = [];
159
-
160
- for (let d = 0; d < customDays; d++) {
161
- const ms = periodStart + d * DAY_MS;
162
- const date = new Date(ms);
163
- const dayNum = date.getDate();
164
- const dow = date.getDay();
165
- const isWeekend = dow === 0 || dow === 6;
166
- const isToday = ms === todayMs;
167
- const isPast = equalDays ? false : ms < todayMs;
168
- const isFirstOfMonth = dayNum === 1;
169
- const monthLabel = (d === 0 || isFirstOfMonth)
170
- ? monthLong(ms, locale).toUpperCase()
171
- : null;
172
-
173
- const dayEnd = ms + DAY_MS;
174
- const dayEventsAll = events
175
- .filter((ev) => ev.start.getTime() < dayEnd && ev.end.getTime() > ms)
176
- .sort((a, b) => a.start.getTime() - b.start.getTime());
177
-
178
- // Separate all-day / multi-day from timed events
179
- const timedEvents: TimelineEvent[] = [];
180
- const allDaySegments: DaySegment[] = [];
181
- for (const ev of dayEventsAll) {
182
- if (isAllDay(ev) || isMultiDay(ev)) {
183
- const seg = segmentForDay(ev, ms);
184
- if (seg) allDaySegments.push(seg);
185
- } else {
186
- timedEvents.push(ev);
187
- }
188
- }
189
-
190
- days.push({ ms, dayNum, isToday, isPast, isWeekend, isFirstOfMonth, monthLabel, events: timedEvents, allDaySegments });
191
- }
192
-
193
- // Month label: show when first day of period is day 1-7 (for 7-day), or first day of period (for custom)
194
- const startDate = new Date(periodStart);
195
- const showMonth = customDays === 7 ? startDate.getDate() <= 7 : startDate.getDate() <= customDays;
196
- const monthLabel = showMonth ? monthLong(periodStart, locale).toUpperCase() : null;
197
-
198
- result.push({ weekStart: periodStart, isCurrent, monthLabel, days });
199
- }
200
-
201
- // Filter hidden days if hideDays is set
202
- if (hideDays?.length) {
203
- for (const row of result) {
204
- row.days = row.days.filter((d) => {
205
- const isoDay = new Date(d.ms).getDay();
206
- // Convert JS day (0=Sun) to ISO (7=Sun)
207
- const iso = isoDay === 0 ? 7 : isoDay;
208
- return !hideDays.includes(iso);
209
- });
210
- }
211
- }
212
-
213
- return result;
214
- });
215
-
216
- // ─── Format helpers ─────────────────────────────────
217
- function fmtAmPm(d: Date): string {
218
- return _fmtTime(d, locale);
219
- }
220
-
221
- function fmtNowTime(tick: number): string {
222
- return _fmtTime(new Date(tick), locale);
223
- }
224
-
225
- // ─── Now indicator fraction ─────────────────────────
226
- const nowFrac = $derived(fractionalHour(clock.tick) / 24);
227
-
228
- // ─── Scroll to current week on mount ────────────────
229
- onMount(async () => {
230
- await tick();
231
- scrollWeekIntoContainer();
232
- });
233
-
234
- // ─── External navigation (arrows, goToday) ──────────
235
- $effect(() => {
236
- const ext = focusDate ? sod(focusDate.getTime()) : clock.today;
237
- if (ext !== lastExternalMs) {
238
- lastExternalMs = ext;
239
- internalFocusMs = ext;
240
- tick().then(() => scrollWeekIntoContainer(ext));
241
- }
242
- });
243
-
244
- /** Push the week at the viewport centre to viewState (drives the header label + Today button). */
245
- function syncFocusFromScroll() {
246
- if (!el || !viewState) return;
247
- const centerY = el.scrollTop + el.clientHeight / 2;
248
- const rows = el.querySelectorAll<HTMLElement>('[data-week]');
249
- for (const row of rows) {
250
- if (row.offsetTop + row.offsetHeight >= centerY) {
251
- const ms = Number(row.dataset.week);
252
- if (Number.isFinite(ms) && ms !== lastExternalMs) {
253
- lastExternalMs = ms;
254
- viewState.setFocusDate(new Date(ms));
255
- }
256
- return;
257
- }
258
- }
259
- }
260
-
261
- let extending = false;
262
-
263
- function handleUserScroll() {
264
- syncFocusFromScroll();
265
- if (!el || extending) return;
266
- // Extend buffer when user scrolls near the edge
267
- if (el.scrollTop < EDGE_PX) {
268
- extending = true;
269
- const oldHeight = el.scrollHeight;
270
- bufferBefore += EXTEND_BY;
271
- // After DOM updates, compensate scroll for prepended content
272
- tick().then(() => {
273
- el.scrollTop += el.scrollHeight - oldHeight;
274
- extending = false;
275
- });
276
- } else {
277
- const bottomRemaining = el.scrollHeight - el.clientHeight - el.scrollTop;
278
- if (bottomRemaining < EDGE_PX) {
279
- bufferAfter += EXTEND_BY;
280
- }
281
- }
282
- }
283
-
284
- function handleDayCellClick(ms: number, e: Event) {
285
- const target = e.target as HTMLElement;
286
- if (target.closest('.wg-ev')) return;
287
- if (readOnly || !oneventcreate) return;
288
- // Check disabled dates
289
- if (disabledSet.has(ms)) return;
290
- // Check blocked slots (all-day block check: if entire day is blocked, prevent)
291
- const durMin = minDuration ? Math.max(60, minDuration) : 60;
292
- const start = new Date(ms + 9 * HOUR_MS);
293
- const end = new Date(start.getTime() + durMin * 60_000);
294
- oneventcreate({ start, end });
295
- }
296
-
297
- // ─── Event drag-to-move ───────────────────────────────────────
298
- const DRAG_THRESHOLD = 8;
299
- let evDragStartX = 0;
300
- let evDragStartY = 0;
301
- let evDragStarted = false;
302
- let evDragging = $state(false);
303
- let evDragId = $state<string | null>(null);
304
- let evDragEvent: TimelineEvent | null = null;
305
-
306
- const dragPreviewEvent = $derived.by(() => {
307
- const payload = drag?.active && drag.mode === 'move' ? drag.payload : null;
308
- if (!payload?.eventId) return null;
309
- const ev = events.find((event) => event.id === payload.eventId);
310
- if (!ev) return null;
311
- return { ...ev, start: payload.start, end: payload.end };
312
- });
313
-
314
- function isDraggedEvent(eventId: string): boolean {
315
- return dragPreviewEvent?.id === eventId;
316
- }
317
-
318
- function timedEventsForDay(day: DayCell): TimelineEvent[] {
319
- if (!dragPreviewEvent) return day.events;
320
- return day.events.filter((ev) => ev.id !== dragPreviewEvent.id);
321
- }
322
-
323
- // Crossfade keys for the previews, snapshotted at render time. Transition
324
- // params are evaluated lazily at unmount — after the drag payload is
325
- // already null — so they must never read the reactive preview directly.
326
- // Plain Map (not $state): written during render, read only by transitions.
327
- const previewKeySnapshot = new Map<number | 'timed', string>();
328
-
329
- function dragPreviewTimedForDay(dayMs: number): TimelineEvent | null {
330
- const ev = dragPreviewEvent;
331
- if (!ev || isAllDay(ev) || isMultiDay(ev)) return null;
332
- const dayEnd = dayMs + DAY_MS;
333
- const hit = ev.start.getTime() < dayEnd && ev.end.getTime() > dayMs;
334
- if (hit) previewKeySnapshot.set('timed', ev.id);
335
- return hit ? ev : null;
336
- }
337
-
338
- function dragPreviewSegmentForDay(dayMs: number): DaySegment | null {
339
- const ev = dragPreviewEvent;
340
- if (!ev || (!isAllDay(ev) && !isMultiDay(ev))) return null;
341
- const seg = segmentForDay(ev, dayMs);
342
- if (seg) previewKeySnapshot.set(dayMs, `${ev.id}:${seg.dayIndex}`);
343
- return seg;
344
- }
345
-
346
- function getCellWidth(): number {
347
- const cell = el?.querySelector('.wg-cell');
348
- return cell ? cell.getBoundingClientRect().width : 100;
349
- }
350
-
351
- function getRowHeight(): number {
352
- const row = el?.querySelector('.wg-week');
353
- return row ? row.getBoundingClientRect().height + 24 : 200; // 24 ≈ vertical margin between weeks
354
- }
355
-
356
- function onEventPointerDown(e: PointerEvent, ev: TimelineEvent) {
357
- if (e.button !== 0 || !drag || readOnly || ev.data?.readOnly) return;
358
- e.stopPropagation();
359
- evDragStartX = e.clientX;
360
- evDragStartY = e.clientY;
361
- evDragStarted = false;
362
- evDragId = ev.id;
363
- evDragEvent = ev;
364
-
365
- window.addEventListener('pointermove', onEvWindowPointerMove);
366
- window.addEventListener('pointerup', onEvWindowPointerUp, { once: true });
367
- window.addEventListener('pointercancel', onEvWindowPointerCancel, { once: true });
368
- }
369
-
370
- function onEvWindowPointerMove(e: PointerEvent) {
371
- const ev = evDragEvent;
372
- if (!drag || !ev || evDragId !== ev.id) return;
373
- const dx = e.clientX - evDragStartX;
374
- const dy = e.clientY - evDragStartY;
375
- if (!evDragStarted && Math.abs(dx) + Math.abs(dy) < DRAG_THRESHOLD) return;
376
-
377
- if (!evDragStarted) {
378
- evDragStarted = true;
379
- evDragging = true;
380
- drag.beginMove(ev.id, ev.start, ev.end);
381
- }
382
-
383
- const cellW = getCellWidth();
384
- const rowH = getRowHeight();
385
- const dayOffset = Math.round(dx / cellW);
386
- const weekOffset = Math.round(dy / rowH);
387
- const deltaMs = (dayOffset + weekOffset * 7) * DAY_MS;
388
- drag.updatePointer(
389
- new Date(ev.start.getTime() + deltaMs),
390
- new Date(ev.end.getTime() + deltaMs),
391
- );
392
- }
393
-
394
- function cleanupEvDrag() {
395
- window.removeEventListener('pointermove', onEvWindowPointerMove);
396
- window.removeEventListener('pointerup', onEvWindowPointerUp);
397
- window.removeEventListener('pointercancel', onEvWindowPointerCancel);
398
- evDragStarted = false;
399
- evDragging = false;
400
- evDragId = null;
401
- evDragEvent = null;
402
- }
403
-
404
- function onEvWindowPointerUp() {
405
- if (!drag) { cleanupEvDrag(); return; }
406
- if (!evDragStarted) {
407
- if (evDragEvent) oneventclick?.(evDragEvent);
408
- } else {
409
- commitDragCtx?.();
410
- }
411
- cleanupEvDrag();
412
- }
413
-
414
- function onEvWindowPointerCancel() {
415
- if (drag && evDragStarted) drag.cancel();
416
- cleanupEvDrag();
417
- }
11
+ <script lang="ts">import { onMount, tick, untrack } from "svelte";
12
+ import { flip } from "svelte/animate";
13
+ import { crossfade } from "svelte/transition";
14
+ import { prefersReducedMotion } from "svelte/motion";
15
+ import { useCalendarContext } from "../shared/context.svelte.js";
16
+ import EventContent from "../shared/EventContent.svelte";
17
+ import { createClock } from "../../core/clock.svelte.js";
18
+ import { DAY_MS, HOUR_MS, sod } from "../../core/time.js";
19
+ import { startOfWeek as sowFn, fractionalHour, isAllDay, isMultiDay, segmentForDay } from "../../core/time.js";
20
+ import { weekdayShort, monthLong, fmtTime as _fmtTime, getLabels } from "../../core/locale.js";
21
+ const L = $derived(getLabels());
22
+ let {
23
+ mondayStart = true,
24
+ locale,
25
+ height = 520,
26
+ events = [],
27
+ style = "",
28
+ focusDate,
29
+ oneventclick,
30
+ oneventcreate,
31
+ selectedEventId = null,
32
+ readOnly = false
33
+ } = $props();
34
+ const ctx = useCalendarContext();
35
+ const clock = createClock(ctx.timezone);
36
+ const ANIM = $derived(prefersReducedMotion.current ? 0 : 180);
37
+ const [previewSend, previewReceive] = crossfade({ duration: () => prefersReducedMotion.current ? 0 : 160 });
38
+ const drag = $derived(ctx.drag);
39
+ const commitDragCtx = $derived(ctx.commitDrag);
40
+ const viewState = $derived(ctx.viewState);
41
+ const loadRangeCtx = $derived(ctx.loadRange);
42
+ const equalDays = $derived(ctx.equalDays);
43
+ const showDates = $derived(ctx.showDates);
44
+ const hideDays = $derived(ctx.hideDays);
45
+ const blockedSlots = $derived(ctx.blockedSlots);
46
+ const dayHeaderSnippet = $derived(ctx.dayHeaderSnippet);
47
+ const minDuration = $derived(ctx.minDuration);
48
+ const autoHeight = $derived(ctx.autoHeight);
49
+ const oneventhover = $derived(ctx.oneventhover);
50
+ const disabledSet = $derived(ctx.disabledSet);
51
+ const INITIAL_BUFFER = 52;
52
+ const EXTEND_BY = 26;
53
+ const EDGE_PX = 200;
54
+ let bufferBefore = $state(INITIAL_BUFFER);
55
+ let bufferAfter = $state(INITIAL_BUFFER);
56
+ const MAX_EVENTS_SHOWN = 5;
57
+ const _initMs = untrack(() => sod(focusDate?.getTime() ?? Date.now()));
58
+ let internalFocusMs = $state(_initMs);
59
+ let lastExternalMs = _initMs;
60
+ let el;
61
+ function scrollWeekIntoContainer(targetMs, behavior = "auto") {
62
+ if (!el) return;
63
+ let target = null;
64
+ if (targetMs !== void 0) {
65
+ const rows = el.querySelectorAll("[data-week]");
66
+ for (const row of rows) {
67
+ const weekMs = Number(row.dataset.week);
68
+ if (weekMs <= targetMs && targetMs < weekMs + customDays * DAY_MS) {
69
+ target = row;
70
+ break;
71
+ }
72
+ }
73
+ }
74
+ if (!target) target = el.querySelector(".wg-week--current");
75
+ if (!target) return;
76
+ const targetTop = target.offsetTop - (el.clientHeight - target.offsetHeight) / 2;
77
+ el.scrollTo({ top: Math.max(0, targetTop), behavior });
78
+ }
79
+ const todayMs = $derived(clock.today);
80
+ const customDays = $derived(viewState?.dayCount ?? 7);
81
+ const anchorPeriodStart = $derived(
82
+ customDays === 7 ? sowFn(internalFocusMs, mondayStart) : sod(internalFocusMs)
83
+ );
84
+ $effect(() => {
85
+ if (!loadRangeCtx) return;
86
+ const rangeStart = new Date(anchorPeriodStart - bufferBefore * customDays * DAY_MS);
87
+ const rangeEnd = new Date(anchorPeriodStart + (bufferAfter + 1) * customDays * DAY_MS);
88
+ loadRangeCtx.set({ start: rangeStart, end: rangeEnd });
89
+ return () => loadRangeCtx.set(null);
90
+ });
91
+ const weeks = $derived.by(() => {
92
+ const result = [];
93
+ for (let w = -bufferBefore; w <= bufferAfter; w++) {
94
+ const periodStart = anchorPeriodStart + w * customDays * DAY_MS;
95
+ const isCurrent = todayMs >= periodStart && todayMs < periodStart + customDays * DAY_MS;
96
+ const days = [];
97
+ for (let d = 0; d < customDays; d++) {
98
+ const ms = periodStart + d * DAY_MS;
99
+ const date = new Date(ms);
100
+ const dayNum = date.getDate();
101
+ const dow = date.getDay();
102
+ const isWeekend = dow === 0 || dow === 6;
103
+ const isToday = ms === todayMs;
104
+ const isPast = equalDays ? false : ms < todayMs;
105
+ const isFirstOfMonth = dayNum === 1;
106
+ const monthLabel2 = d === 0 || isFirstOfMonth ? monthLong(ms, locale).toUpperCase() : null;
107
+ const dayEnd = ms + DAY_MS;
108
+ const dayEventsAll = events.filter((ev) => ev.start.getTime() < dayEnd && ev.end.getTime() > ms).sort((a, b) => a.start.getTime() - b.start.getTime());
109
+ const timedEvents = [];
110
+ const allDaySegments = [];
111
+ for (const ev of dayEventsAll) {
112
+ if (isAllDay(ev) || isMultiDay(ev)) {
113
+ const seg = segmentForDay(ev, ms);
114
+ if (seg) allDaySegments.push(seg);
115
+ } else {
116
+ timedEvents.push(ev);
117
+ }
118
+ }
119
+ days.push({ ms, dayNum, isToday, isPast, isWeekend, isFirstOfMonth, monthLabel: monthLabel2, events: timedEvents, allDaySegments });
120
+ }
121
+ const startDate = new Date(periodStart);
122
+ const showMonth = customDays === 7 ? startDate.getDate() <= 7 : startDate.getDate() <= customDays;
123
+ const monthLabel = showMonth ? monthLong(periodStart, locale).toUpperCase() : null;
124
+ result.push({ weekStart: periodStart, isCurrent, monthLabel, days });
125
+ }
126
+ if (hideDays?.length) {
127
+ for (const row of result) {
128
+ row.days = row.days.filter((d) => {
129
+ const isoDay = new Date(d.ms).getDay();
130
+ const iso = isoDay === 0 ? 7 : isoDay;
131
+ return !hideDays.includes(iso);
132
+ });
133
+ }
134
+ }
135
+ return result;
136
+ });
137
+ function fmtAmPm(d) {
138
+ return _fmtTime(d, locale);
139
+ }
140
+ function fmtNowTime(tick2) {
141
+ return _fmtTime(new Date(tick2), locale);
142
+ }
143
+ const nowFrac = $derived(fractionalHour(clock.tick) / 24);
144
+ onMount(async () => {
145
+ await tick();
146
+ scrollWeekIntoContainer();
147
+ });
148
+ $effect(() => {
149
+ const ext = focusDate ? sod(focusDate.getTime()) : clock.today;
150
+ if (ext !== lastExternalMs) {
151
+ lastExternalMs = ext;
152
+ internalFocusMs = ext;
153
+ tick().then(() => scrollWeekIntoContainer(ext));
154
+ }
155
+ });
156
+ function syncFocusFromScroll() {
157
+ if (!el || !viewState) return;
158
+ const centerY = el.scrollTop + el.clientHeight / 2;
159
+ const rows = el.querySelectorAll("[data-week]");
160
+ for (const row of rows) {
161
+ if (row.offsetTop + row.offsetHeight >= centerY) {
162
+ const ms = Number(row.dataset.week);
163
+ if (Number.isFinite(ms) && ms !== lastExternalMs) {
164
+ lastExternalMs = ms;
165
+ viewState.setFocusDate(new Date(ms));
166
+ }
167
+ return;
168
+ }
169
+ }
170
+ }
171
+ let extending = false;
172
+ function handleUserScroll() {
173
+ syncFocusFromScroll();
174
+ if (!el || extending) return;
175
+ if (el.scrollTop < EDGE_PX) {
176
+ extending = true;
177
+ const oldHeight = el.scrollHeight;
178
+ bufferBefore += EXTEND_BY;
179
+ tick().then(() => {
180
+ el.scrollTop += el.scrollHeight - oldHeight;
181
+ extending = false;
182
+ });
183
+ } else {
184
+ const bottomRemaining = el.scrollHeight - el.clientHeight - el.scrollTop;
185
+ if (bottomRemaining < EDGE_PX) {
186
+ bufferAfter += EXTEND_BY;
187
+ }
188
+ }
189
+ }
190
+ function handleDayCellClick(ms, e) {
191
+ const target = e.target;
192
+ if (target.closest(".wg-ev")) return;
193
+ if (readOnly || !oneventcreate) return;
194
+ if (disabledSet.has(ms)) return;
195
+ const durMin = minDuration ? Math.max(60, minDuration) : 60;
196
+ const start = new Date(ms + 9 * HOUR_MS);
197
+ const end = new Date(start.getTime() + durMin * 6e4);
198
+ oneventcreate({ start, end });
199
+ }
200
+ const DRAG_THRESHOLD = 8;
201
+ let evDragStartX = 0;
202
+ let evDragStartY = 0;
203
+ let evDragStarted = false;
204
+ let evDragging = $state(false);
205
+ let evDragId = $state(null);
206
+ let evDragEvent = null;
207
+ const dragPreviewEvent = $derived.by(() => {
208
+ const payload = drag?.active && drag.mode === "move" ? drag.payload : null;
209
+ if (!payload?.eventId) return null;
210
+ const ev = events.find((event) => event.id === payload.eventId);
211
+ if (!ev) return null;
212
+ return { ...ev, start: payload.start, end: payload.end };
213
+ });
214
+ function isDraggedEvent(eventId) {
215
+ return dragPreviewEvent?.id === eventId;
216
+ }
217
+ function timedEventsForDay(day) {
218
+ if (!dragPreviewEvent) return day.events;
219
+ return day.events.filter((ev) => ev.id !== dragPreviewEvent.id);
220
+ }
221
+ const previewKeySnapshot = /* @__PURE__ */ new Map();
222
+ function dragPreviewTimedForDay(dayMs) {
223
+ const ev = dragPreviewEvent;
224
+ if (!ev || isAllDay(ev) || isMultiDay(ev)) return null;
225
+ const dayEnd = dayMs + DAY_MS;
226
+ const hit = ev.start.getTime() < dayEnd && ev.end.getTime() > dayMs;
227
+ if (hit) previewKeySnapshot.set("timed", ev.id);
228
+ return hit ? ev : null;
229
+ }
230
+ function dragPreviewSegmentForDay(dayMs) {
231
+ const ev = dragPreviewEvent;
232
+ if (!ev || !isAllDay(ev) && !isMultiDay(ev)) return null;
233
+ const seg = segmentForDay(ev, dayMs);
234
+ if (seg) previewKeySnapshot.set(dayMs, `${ev.id}:${seg.dayIndex}`);
235
+ return seg;
236
+ }
237
+ function getCellWidth() {
238
+ const cell = el?.querySelector(".wg-cell");
239
+ return cell ? cell.getBoundingClientRect().width : 100;
240
+ }
241
+ function getRowHeight() {
242
+ const row = el?.querySelector(".wg-week");
243
+ return row ? row.getBoundingClientRect().height + 24 : 200;
244
+ }
245
+ function onEventPointerDown(e, ev) {
246
+ if (e.button !== 0 || !drag || readOnly || ev.data?.readOnly) return;
247
+ e.stopPropagation();
248
+ evDragStartX = e.clientX;
249
+ evDragStartY = e.clientY;
250
+ evDragStarted = false;
251
+ evDragId = ev.id;
252
+ evDragEvent = ev;
253
+ window.addEventListener("pointermove", onEvWindowPointerMove);
254
+ window.addEventListener("pointerup", onEvWindowPointerUp, { once: true });
255
+ window.addEventListener("pointercancel", onEvWindowPointerCancel, { once: true });
256
+ }
257
+ function onEvWindowPointerMove(e) {
258
+ const ev = evDragEvent;
259
+ if (!drag || !ev || evDragId !== ev.id) return;
260
+ const dx = e.clientX - evDragStartX;
261
+ const dy = e.clientY - evDragStartY;
262
+ if (!evDragStarted && Math.abs(dx) + Math.abs(dy) < DRAG_THRESHOLD) return;
263
+ if (!evDragStarted) {
264
+ evDragStarted = true;
265
+ evDragging = true;
266
+ drag.beginMove(ev.id, ev.start, ev.end);
267
+ }
268
+ const cellW = getCellWidth();
269
+ const rowH = getRowHeight();
270
+ const dayOffset = Math.round(dx / cellW);
271
+ const weekOffset = Math.round(dy / rowH);
272
+ const deltaMs = (dayOffset + weekOffset * 7) * DAY_MS;
273
+ drag.updatePointer(
274
+ new Date(ev.start.getTime() + deltaMs),
275
+ new Date(ev.end.getTime() + deltaMs)
276
+ );
277
+ }
278
+ function cleanupEvDrag() {
279
+ window.removeEventListener("pointermove", onEvWindowPointerMove);
280
+ window.removeEventListener("pointerup", onEvWindowPointerUp);
281
+ window.removeEventListener("pointercancel", onEvWindowPointerCancel);
282
+ evDragStarted = false;
283
+ evDragging = false;
284
+ evDragId = null;
285
+ evDragEvent = null;
286
+ }
287
+ function onEvWindowPointerUp() {
288
+ if (!drag) {
289
+ cleanupEvDrag();
290
+ return;
291
+ }
292
+ if (!evDragStarted) {
293
+ if (evDragEvent) oneventclick?.(evDragEvent);
294
+ } else {
295
+ commitDragCtx?.();
296
+ }
297
+ cleanupEvDrag();
298
+ }
299
+ function onEvWindowPointerCancel() {
300
+ if (drag && evDragStarted) drag.cancel();
301
+ cleanupEvDrag();
302
+ }
418
303
  </script>
419
304
 
420
305
  {#snippet allDaySegmentContent(seg: DaySegment)}
@@ -430,11 +315,13 @@
430
315
  {/snippet}
431
316
 
432
317
  {#snippet timedEventContent(ev: TimelineEvent)}
433
- <span class="wg-ev-time">{fmtAmPm(ev.start)}</span>
434
- <span class="wg-ev-title">{ev.title}</span>
435
- {#if ev.location}
436
- <span class="wg-ev-loc">{ev.location}</span>
437
- {/if}
318
+ <EventContent event={ev}>
319
+ <span class="wg-ev-time">{fmtAmPm(ev.start)}</span>
320
+ <span class="wg-ev-title">{ev.title}</span>
321
+ {#if ev.location}
322
+ <span class="wg-ev-loc">{ev.location}</span>
323
+ {/if}
324
+ </EventContent>
438
325
  {/snippet}
439
326
 
440
327
  <div class="wg" class:wg--auto={autoHeight} style={style || undefined} style:height={autoHeight ? undefined : (height ? `${height}px` : '100%')}>