@nomideusz/svelte-calendar 0.6.3 → 0.6.5
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 +192 -10
- package/dist/adapters/composite.d.ts +15 -0
- package/dist/adapters/composite.js +65 -0
- package/dist/adapters/index.d.ts +5 -1
- package/dist/adapters/index.js +2 -0
- package/dist/adapters/mapped.d.ts +170 -0
- package/dist/adapters/mapped.js +343 -0
- package/dist/adapters/recurring.d.ts +4 -0
- package/dist/adapters/recurring.js +5 -10
- package/dist/adapters/types.d.ts +11 -2
- package/dist/assets/favicon.svg +9 -1
- package/dist/calendar/Calendar.svelte +161 -90
- package/dist/calendar/Calendar.svelte.d.ts +12 -0
- package/dist/core/index.d.ts +3 -1
- package/dist/core/index.js +2 -0
- package/dist/core/measure.d.ts +104 -0
- package/dist/core/measure.js +195 -0
- package/dist/core/time.js +2 -2
- package/dist/core/types.d.ts +32 -0
- package/dist/engine/event-store.svelte.js +22 -7
- package/dist/headless/create-agenda.svelte.d.ts +67 -0
- package/dist/headless/create-agenda.svelte.js +139 -0
- package/dist/headless/create-calendar.svelte.d.ts +2 -0
- package/dist/headless/create-calendar.svelte.js +380 -0
- package/dist/headless/index.d.ts +4 -0
- package/dist/headless/index.js +2 -0
- package/dist/headless/types.d.ts +219 -0
- package/dist/headless/types.js +1 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.js +4 -2
- package/dist/primitives/DayHeader.svelte +6 -6
- package/dist/primitives/EmptySlot.svelte +3 -3
- package/dist/primitives/EventBlock.svelte +99 -14
- package/dist/primitives/NowIndicator.svelte +5 -5
- package/dist/views/agenda/AgendaDay.svelte +215 -116
- package/dist/views/agenda/AgendaWeek.svelte +166 -113
- package/dist/views/mobile/MobileDay.svelte +45 -21
- package/dist/views/mobile/MobileWeek.svelte +85 -35
- package/dist/views/planner/PlannerDay.svelte +116 -60
- package/dist/views/planner/PlannerWeek.svelte +228 -166
- package/dist/views/shared/context.svelte.d.ts +47 -0
- package/dist/views/shared/context.svelte.js +47 -0
- package/dist/views/shared/format.d.ts +19 -0
- package/dist/views/shared/format.js +46 -0
- package/dist/views/shared/index.d.ts +4 -0
- package/dist/views/shared/index.js +2 -0
- package/package.json +9 -2
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
• Generous whitespace, thin dividers, minimal chrome.
|
|
10
10
|
-->
|
|
11
11
|
<script lang="ts">
|
|
12
|
-
import {
|
|
12
|
+
import { onMount, tick, untrack } from 'svelte';
|
|
13
|
+
import { useCalendarContext } from '../shared/context.svelte.js';
|
|
13
14
|
import { createClock } from '../../core/clock.svelte.js';
|
|
14
15
|
import type { TimelineEvent, BlockedSlot } from '../../core/types.js';
|
|
15
16
|
import type { DragState } from '../../engine/drag.svelte.js';
|
|
@@ -48,58 +49,56 @@
|
|
|
48
49
|
readOnly = false,
|
|
49
50
|
}: Props = $props();
|
|
50
51
|
|
|
51
|
-
|
|
52
|
-
const drag = getContext<DragState>('calendar:drag') as DragState | undefined;
|
|
53
|
-
const commitDragCtx = getContext<() => void>('calendar:commitDrag') as (() => void) | undefined;
|
|
54
|
-
const viewState = getContext<ViewState>('calendar:viewState') as ViewState | undefined;
|
|
55
|
-
const loadRangeCtx = getContext<{ current: { start: Date; end: Date } | null; set: (r: { start: Date; end: Date } | null) => void }>('calendar:loadRange') as { current: { start: Date; end: Date } | null; set: (r: { start: Date; end: Date } | null) => void } | undefined;
|
|
56
|
-
|
|
52
|
+
const ctx = useCalendarContext();
|
|
57
53
|
const clock = createClock();
|
|
58
|
-
const
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
const showNav = $derived(
|
|
63
|
-
const equalDays = $derived(
|
|
64
|
-
const showDates = $derived(
|
|
65
|
-
const hideDays = $derived(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const oneventhover = $derived(callbacksCtx?.oneventhover);
|
|
80
|
-
const disabledDates = $derived(disabledDatesCtx?.current);
|
|
81
|
-
const disabledSet = $derived(new Set(disabledDates?.map(d => sod(d.getTime())) ?? []));
|
|
82
|
-
|
|
83
|
-
// ─── Infinite-scroll config ────────────────────────
|
|
84
|
-
const BUFFER_WEEKS = 6; // weeks rendered on each side of anchor
|
|
85
|
-
const EDGE_WEEKS = 2; // rebase when this close to an edge
|
|
86
|
-
const SHIFT_WEEKS = 3; // weeks to shift per rebase
|
|
54
|
+
const drag = $derived(ctx.drag);
|
|
55
|
+
const commitDragCtx = $derived(ctx.commitDrag);
|
|
56
|
+
const viewState = $derived(ctx.viewState);
|
|
57
|
+
const loadRangeCtx = $derived(ctx.loadRange);
|
|
58
|
+
const showNav = $derived(ctx.showNav);
|
|
59
|
+
const equalDays = $derived(ctx.equalDays);
|
|
60
|
+
const showDates = $derived(ctx.showDates);
|
|
61
|
+
const hideDays = $derived(ctx.hideDays);
|
|
62
|
+
const blockedSlots = $derived(ctx.blockedSlots);
|
|
63
|
+
const dayHeaderSnippet = $derived(ctx.dayHeaderSnippet);
|
|
64
|
+
const minDuration = $derived(ctx.minDuration);
|
|
65
|
+
const autoHeight = $derived(ctx.autoHeight);
|
|
66
|
+
const oneventhover = $derived(ctx.oneventhover);
|
|
67
|
+
const disabledSet = $derived(ctx.disabledSet);
|
|
68
|
+
|
|
69
|
+
// ─── Buffer config ─────────────────────────────
|
|
70
|
+
const INITIAL_BUFFER = 52; // ±1 year on load
|
|
71
|
+
const EXTEND_BY = 26; // +6 months when user hits edge
|
|
72
|
+
const EDGE_PX = 200; // extend when this close to scroll edge
|
|
73
|
+
let bufferBefore = $state(INITIAL_BUFFER);
|
|
74
|
+
let bufferAfter = $state(INITIAL_BUFFER);
|
|
87
75
|
const MAX_EVENTS_SHOWN = 5;
|
|
88
76
|
|
|
89
|
-
// Internal anchor drives layout; syncs bidirectionally with focusDate.
|
|
90
77
|
const _initMs = untrack(() => sod(focusDate?.getTime() ?? Date.now()));
|
|
91
78
|
let internalFocusMs = $state(_initMs);
|
|
92
79
|
let lastExternalMs = _initMs;
|
|
93
|
-
let rebasing = false;
|
|
94
80
|
|
|
95
81
|
let el: HTMLDivElement;
|
|
96
82
|
let scrolled = $state(false);
|
|
97
83
|
|
|
98
|
-
function
|
|
84
|
+
function scrollWeekIntoContainer(targetMs?: number, behavior: ScrollBehavior = 'auto') {
|
|
99
85
|
if (!el) return;
|
|
100
|
-
|
|
101
|
-
if (
|
|
102
|
-
|
|
86
|
+
let target: HTMLElement | null = null;
|
|
87
|
+
if (targetMs !== undefined) {
|
|
88
|
+
// Find the week row containing this date
|
|
89
|
+
const rows = el.querySelectorAll<HTMLElement>('[data-week]');
|
|
90
|
+
for (const row of rows) {
|
|
91
|
+
const weekMs = Number(row.dataset.week);
|
|
92
|
+
if (weekMs <= targetMs && targetMs < weekMs + customDays * DAY_MS) {
|
|
93
|
+
target = row;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Fall back to current week
|
|
99
|
+
if (!target) target = el.querySelector<HTMLElement>('.wg-week--current');
|
|
100
|
+
if (!target) return;
|
|
101
|
+
const targetTop = target.offsetTop - (el.clientHeight - target.offsetHeight) / 2;
|
|
103
102
|
el.scrollTo({ top: Math.max(0, targetTop), behavior });
|
|
104
103
|
}
|
|
105
104
|
|
|
@@ -117,8 +116,8 @@
|
|
|
117
116
|
// what range we need. Calendar's single $effect handles loading.
|
|
118
117
|
$effect(() => {
|
|
119
118
|
if (!loadRangeCtx) return;
|
|
120
|
-
const rangeStart = new Date(anchorPeriodStart -
|
|
121
|
-
const rangeEnd = new Date(anchorPeriodStart + (
|
|
119
|
+
const rangeStart = new Date(anchorPeriodStart - bufferBefore * customDays * DAY_MS);
|
|
120
|
+
const rangeEnd = new Date(anchorPeriodStart + (bufferAfter + 1) * customDays * DAY_MS);
|
|
122
121
|
loadRangeCtx.set({ start: rangeStart, end: rangeEnd });
|
|
123
122
|
return () => loadRangeCtx.set(null);
|
|
124
123
|
});
|
|
@@ -147,7 +146,7 @@
|
|
|
147
146
|
const weeks = $derived.by(() => {
|
|
148
147
|
const result: WeekRow[] = [];
|
|
149
148
|
|
|
150
|
-
for (let w = -
|
|
149
|
+
for (let w = -bufferBefore; w <= bufferAfter; w++) {
|
|
151
150
|
const periodStart = anchorPeriodStart + w * customDays * DAY_MS;
|
|
152
151
|
const isCurrent = todayMs >= periodStart && todayMs < periodStart + customDays * DAY_MS;
|
|
153
152
|
const days: DayCell[] = [];
|
|
@@ -223,88 +222,20 @@
|
|
|
223
222
|
// ─── Scroll to current week on mount ────────────────
|
|
224
223
|
onMount(async () => {
|
|
225
224
|
await tick();
|
|
226
|
-
|
|
225
|
+
scrollWeekIntoContainer();
|
|
227
226
|
});
|
|
228
227
|
|
|
229
|
-
// ───
|
|
230
|
-
|
|
231
|
-
/** Detect external focusDate changes (from nav arrows). */
|
|
228
|
+
// ─── External navigation (arrows, goToday) ──────────
|
|
232
229
|
$effect(() => {
|
|
233
230
|
const ext = focusDate ? sod(focusDate.getTime()) : clock.today;
|
|
234
|
-
if (ext !== lastExternalMs
|
|
231
|
+
if (ext !== lastExternalMs) {
|
|
235
232
|
lastExternalMs = ext;
|
|
236
233
|
internalFocusMs = ext;
|
|
237
|
-
tick().then(() =>
|
|
234
|
+
tick().then(() => scrollWeekIntoContainer(ext, 'smooth'));
|
|
238
235
|
}
|
|
239
236
|
});
|
|
240
237
|
|
|
241
|
-
|
|
242
|
-
function checkVerticalEdges() {
|
|
243
|
-
if (!el || !viewState || rebasing) return;
|
|
244
|
-
const rows = el.querySelectorAll<HTMLElement>('[data-week]');
|
|
245
|
-
if (!rows.length) return;
|
|
246
|
-
|
|
247
|
-
const avgH = el.scrollHeight / rows.length;
|
|
248
|
-
const threshold = avgH * EDGE_WEEKS;
|
|
249
|
-
const maxScroll = el.scrollHeight - el.clientHeight;
|
|
250
|
-
|
|
251
|
-
if (el.scrollTop < threshold) {
|
|
252
|
-
rebaseWeeks(-1);
|
|
253
|
-
} else if (maxScroll > 0 && maxScroll - el.scrollTop < threshold) {
|
|
254
|
-
rebaseWeeks(1);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/** Shift the rendered week window by SHIFT_WEEKS in the given direction. */
|
|
259
|
-
function rebaseWeeks(direction: number) {
|
|
260
|
-
if (rebasing) return;
|
|
261
|
-
rebasing = true;
|
|
262
|
-
|
|
263
|
-
// Save an anchor: first visible week row and its visual offset.
|
|
264
|
-
const rows = el.querySelectorAll<HTMLElement>('[data-week]');
|
|
265
|
-
let anchorWeekMs = 0;
|
|
266
|
-
let anchorTop = 0;
|
|
267
|
-
for (const row of rows) {
|
|
268
|
-
const top = row.offsetTop - el.scrollTop;
|
|
269
|
-
if (top + row.offsetHeight > 0) {
|
|
270
|
-
anchorWeekMs = Number(row.dataset.week);
|
|
271
|
-
anchorTop = top;
|
|
272
|
-
break;
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const shift = SHIFT_WEEKS * direction;
|
|
277
|
-
internalFocusMs += shift * customDays * DAY_MS;
|
|
278
|
-
lastExternalMs = internalFocusMs;
|
|
279
|
-
viewState?.setFocusDate(new Date(internalFocusMs));
|
|
280
|
-
|
|
281
|
-
tick().then(() => {
|
|
282
|
-
// Restore the anchor week to its previous visual position.
|
|
283
|
-
const anchor = el.querySelector<HTMLElement>(`[data-week="${anchorWeekMs}"]`);
|
|
284
|
-
if (anchor) {
|
|
285
|
-
el.scrollTop = anchor.offsetTop - anchorTop;
|
|
286
|
-
}
|
|
287
|
-
rebasing = false;
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
/** Push the currently visible centre week to viewState. */
|
|
292
|
-
function syncFocusFromScroll() {
|
|
293
|
-
if (!el || !viewState || rebasing) return;
|
|
294
|
-
const centerY = el.scrollTop + el.clientHeight / 2;
|
|
295
|
-
const rows = el.querySelectorAll<HTMLElement>('[data-week]');
|
|
296
|
-
for (const row of rows) {
|
|
297
|
-
if (row.offsetTop + row.offsetHeight >= centerY) {
|
|
298
|
-
const ms = Number(row.dataset.week);
|
|
299
|
-
if (ms && ms !== lastExternalMs) {
|
|
300
|
-
lastExternalMs = ms;
|
|
301
|
-
viewState.setFocusDate(new Date(ms));
|
|
302
|
-
}
|
|
303
|
-
break;
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
238
|
+
// ─── Scroll state ─────────────────────────────────────
|
|
308
239
|
function isCurrentWeekVisible(): boolean {
|
|
309
240
|
if (!el) return false;
|
|
310
241
|
const current = el.querySelector<HTMLElement>('.wg-week--current');
|
|
@@ -314,14 +245,26 @@
|
|
|
314
245
|
return bottom > 0 && top < el.clientHeight;
|
|
315
246
|
}
|
|
316
247
|
|
|
317
|
-
let
|
|
248
|
+
let extending = false;
|
|
318
249
|
|
|
319
250
|
function handleUserScroll() {
|
|
320
|
-
if (suppressScroll) return;
|
|
321
251
|
scrolled = !isCurrentWeekVisible();
|
|
322
|
-
if (!
|
|
323
|
-
|
|
324
|
-
|
|
252
|
+
if (!el || extending) return;
|
|
253
|
+
// Extend buffer when user scrolls near the edge
|
|
254
|
+
if (el.scrollTop < EDGE_PX) {
|
|
255
|
+
extending = true;
|
|
256
|
+
const oldHeight = el.scrollHeight;
|
|
257
|
+
bufferBefore += EXTEND_BY;
|
|
258
|
+
// After DOM updates, compensate scroll for prepended content
|
|
259
|
+
tick().then(() => {
|
|
260
|
+
el.scrollTop += el.scrollHeight - oldHeight;
|
|
261
|
+
extending = false;
|
|
262
|
+
});
|
|
263
|
+
} else {
|
|
264
|
+
const bottomRemaining = el.scrollHeight - el.clientHeight - el.scrollTop;
|
|
265
|
+
if (bottomRemaining < EDGE_PX) {
|
|
266
|
+
bufferAfter += EXTEND_BY;
|
|
267
|
+
}
|
|
325
268
|
}
|
|
326
269
|
}
|
|
327
270
|
|
|
@@ -329,15 +272,9 @@
|
|
|
329
272
|
internalFocusMs = clock.today;
|
|
330
273
|
lastExternalMs = clock.today;
|
|
331
274
|
viewState?.goToday();
|
|
332
|
-
suppressScroll = true;
|
|
333
275
|
scrolled = false;
|
|
334
276
|
tick().then(() => {
|
|
335
|
-
|
|
336
|
-
// Keep suppressed until smooth scroll finishes, then recheck
|
|
337
|
-
setTimeout(() => {
|
|
338
|
-
suppressScroll = false;
|
|
339
|
-
scrolled = !isCurrentWeekVisible();
|
|
340
|
-
}, 600);
|
|
277
|
+
scrollWeekIntoContainer(clock.today, 'smooth');
|
|
341
278
|
});
|
|
342
279
|
}
|
|
343
280
|
|
|
@@ -363,6 +300,36 @@
|
|
|
363
300
|
let evDragId = $state<string | null>(null);
|
|
364
301
|
let evDragEvent: TimelineEvent | null = null;
|
|
365
302
|
|
|
303
|
+
const dragPreviewEvent = $derived.by(() => {
|
|
304
|
+
const payload = drag?.active && drag.mode === 'move' ? drag.payload : null;
|
|
305
|
+
if (!payload?.eventId) return null;
|
|
306
|
+
const ev = events.find((event) => event.id === payload.eventId);
|
|
307
|
+
if (!ev) return null;
|
|
308
|
+
return { ...ev, start: payload.start, end: payload.end };
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
function isDraggedEvent(eventId: string): boolean {
|
|
312
|
+
return dragPreviewEvent?.id === eventId;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function timedEventsForDay(day: DayCell): TimelineEvent[] {
|
|
316
|
+
if (!dragPreviewEvent) return day.events;
|
|
317
|
+
return day.events.filter((ev) => ev.id !== dragPreviewEvent.id);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function dragPreviewTimedForDay(dayMs: number): TimelineEvent | null {
|
|
321
|
+
const ev = dragPreviewEvent;
|
|
322
|
+
if (!ev || isAllDay(ev) || isMultiDay(ev)) return null;
|
|
323
|
+
const dayEnd = dayMs + DAY_MS;
|
|
324
|
+
return ev.start.getTime() < dayEnd && ev.end.getTime() > dayMs ? ev : null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function dragPreviewSegmentForDay(dayMs: number): DaySegment | null {
|
|
328
|
+
const ev = dragPreviewEvent;
|
|
329
|
+
if (!ev || (!isAllDay(ev) && !isMultiDay(ev))) return null;
|
|
330
|
+
return segmentForDay(ev, dayMs);
|
|
331
|
+
}
|
|
332
|
+
|
|
366
333
|
function getCellWidth(): number {
|
|
367
334
|
const cell = el?.querySelector('.wg-cell');
|
|
368
335
|
return cell ? cell.getBoundingClientRect().width : 100;
|
|
@@ -374,7 +341,7 @@
|
|
|
374
341
|
}
|
|
375
342
|
|
|
376
343
|
function onEventPointerDown(e: PointerEvent, ev: TimelineEvent) {
|
|
377
|
-
if (e.button !== 0 || !drag || readOnly) return;
|
|
344
|
+
if (e.button !== 0 || !drag || readOnly || ev.data?.readOnly) return;
|
|
378
345
|
e.stopPropagation();
|
|
379
346
|
evDragStartX = e.clientX;
|
|
380
347
|
evDragStartY = e.clientY;
|
|
@@ -437,6 +404,26 @@
|
|
|
437
404
|
}
|
|
438
405
|
</script>
|
|
439
406
|
|
|
407
|
+
{#snippet allDaySegmentContent(seg: DaySegment)}
|
|
408
|
+
{#if seg.isStart}
|
|
409
|
+
<span class="wg-ad-title">{seg.ev.title}</span>
|
|
410
|
+
{:else}
|
|
411
|
+
<span class="wg-ad-cont" aria-hidden="true">◂</span>
|
|
412
|
+
<span class="wg-ad-title">{seg.ev.title}</span>
|
|
413
|
+
{/if}
|
|
414
|
+
{#if !seg.isEnd && seg.totalDays > 1}
|
|
415
|
+
<span class="wg-ad-arrow" aria-hidden="true">▸</span>
|
|
416
|
+
{/if}
|
|
417
|
+
{/snippet}
|
|
418
|
+
|
|
419
|
+
{#snippet timedEventContent(ev: TimelineEvent)}
|
|
420
|
+
<span class="wg-ev-time">{fmtAmPm(ev.start)}</span>
|
|
421
|
+
<span class="wg-ev-title">{ev.title}</span>
|
|
422
|
+
{#if ev.location}
|
|
423
|
+
<span class="wg-ev-loc">{ev.location}</span>
|
|
424
|
+
{/if}
|
|
425
|
+
{/snippet}
|
|
426
|
+
|
|
440
427
|
<div class="wg" class:wg--auto={autoHeight} style={style || undefined} style:height={autoHeight ? undefined : (height ? `${height}px` : '100%')}>
|
|
441
428
|
<div
|
|
442
429
|
class="wg-body"
|
|
@@ -451,11 +438,17 @@
|
|
|
451
438
|
<!-- Day columns (header inside each cell) -->
|
|
452
439
|
<div class="wg-days">
|
|
453
440
|
{#each week.days as day (day.ms)}
|
|
441
|
+
{@const visibleAllDaySegments = day.allDaySegments.filter((seg) => !isDraggedEvent(seg.ev.id))}
|
|
442
|
+
{@const visibleTimedEvents = timedEventsForDay(day)}
|
|
443
|
+
{@const previewTimedEvent = dragPreviewTimedForDay(day.ms)}
|
|
444
|
+
{@const previewSegment = dragPreviewSegmentForDay(day.ms)}
|
|
454
445
|
<div
|
|
455
446
|
class="wg-cell"
|
|
456
447
|
class:wg-cell--today={day.isToday}
|
|
457
448
|
class:wg-cell--past={day.isPast}
|
|
458
|
-
class:wg-cell--weekend={day.isWeekend}
|
|
449
|
+
class:wg-cell--weekend={day.isWeekend}
|
|
450
|
+
class:wg-cell--disabled={disabledSet.has(day.ms)}
|
|
451
|
+
role="gridcell"
|
|
459
452
|
tabindex="0"
|
|
460
453
|
aria-label="{new Date(day.ms).toLocaleDateString(locale ?? 'en-US', { weekday: 'long', month: 'short', day: 'numeric' })}{day.isToday ? ` (${L.today.toLowerCase()})` : ''}, {L.nEvents(day.events.length)}"
|
|
461
454
|
onclick={(e) => handleDayCellClick(day.ms, e)}
|
|
@@ -498,9 +491,9 @@
|
|
|
498
491
|
{/if}
|
|
499
492
|
|
|
500
493
|
<!-- All-day / multi-day events -->
|
|
501
|
-
{#if
|
|
494
|
+
{#if visibleAllDaySegments.length > 0 || previewSegment}
|
|
502
495
|
<div class="wg-allday">
|
|
503
|
-
{#each
|
|
496
|
+
{#each visibleAllDaySegments as seg (seg.ev.id)}
|
|
504
497
|
<div
|
|
505
498
|
class="wg-ad"
|
|
506
499
|
class:wg-ad--start={seg.isStart}
|
|
@@ -514,41 +507,59 @@
|
|
|
514
507
|
onpointerdown={(e) => onEventPointerDown(e, seg.ev)}
|
|
515
508
|
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); oneventclick?.(seg.ev); } }}
|
|
516
509
|
>
|
|
517
|
-
{
|
|
518
|
-
<span class="wg-ad-title">{seg.ev.title}</span>
|
|
519
|
-
{:else}
|
|
520
|
-
<span class="wg-ad-cont" aria-hidden="true">◂</span>
|
|
521
|
-
<span class="wg-ad-title">{seg.ev.title}</span>
|
|
522
|
-
{/if}
|
|
523
|
-
{#if !seg.isEnd && seg.totalDays > 1}
|
|
524
|
-
<span class="wg-ad-arrow" aria-hidden="true">▸</span>
|
|
525
|
-
{/if}
|
|
510
|
+
{@render allDaySegmentContent(seg)}
|
|
526
511
|
</div>
|
|
527
512
|
{/each}
|
|
513
|
+
{#if previewSegment}
|
|
514
|
+
<div
|
|
515
|
+
class="wg-ad wg-ad--drag-preview"
|
|
516
|
+
class:wg-ad--start={previewSegment.isStart}
|
|
517
|
+
class:wg-ad--end={previewSegment.isEnd}
|
|
518
|
+
class:wg-ad--mid={!previewSegment.isStart && !previewSegment.isEnd}
|
|
519
|
+
style:--ev-color={previewSegment.ev.color ?? 'var(--dt-accent)'}
|
|
520
|
+
aria-hidden="true"
|
|
521
|
+
>
|
|
522
|
+
{@render allDaySegmentContent(previewSegment)}
|
|
523
|
+
</div>
|
|
524
|
+
{/if}
|
|
528
525
|
</div>
|
|
529
526
|
{/if}
|
|
530
527
|
|
|
531
528
|
<!-- Timed events -->
|
|
532
529
|
<div class="wg-cell-events">
|
|
533
|
-
{#each
|
|
530
|
+
{#each visibleTimedEvents.slice(0, MAX_EVENTS_SHOWN) as ev (ev.id)}
|
|
534
531
|
<div
|
|
535
532
|
class="wg-ev"
|
|
536
533
|
class:wg-ev--selected={selectedEventId === ev.id}
|
|
537
534
|
class:wg-ev--current={ev.start.getTime() <= clock.tick && ev.end.getTime() > clock.tick}
|
|
538
535
|
class:wg-ev--dragging={evDragging && evDragId === ev.id}
|
|
536
|
+
class:wg-ev--readonly={ev.data?.readOnly}
|
|
537
|
+
class:wg-ev--cancelled={ev.status === 'cancelled'}
|
|
538
|
+
class:wg-ev--tentative={ev.status === 'tentative'}
|
|
539
|
+
class:wg-ev--full={ev.status === 'full'}
|
|
540
|
+
class:wg-ev--limited={ev.status === 'limited'}
|
|
539
541
|
style:--ev-color={ev.color ?? 'var(--dt-accent)'}
|
|
540
542
|
role="button"
|
|
541
543
|
tabindex="0"
|
|
542
|
-
aria-label="{ev.title}{ev.start.getTime() <= clock.tick && ev.end.getTime() > clock.tick ? ` (${L.inProgress})` : ''}"
|
|
543
|
-
onpointerdown={(e) => onEventPointerDown(e, ev)}
|
|
544
|
+
aria-label="{ev.title}{ev.status === 'cancelled' ? ` (cancelled)` : ''}{ev.status === 'tentative' ? ` (tentative)` : ''}{ev.status === 'full' ? ` (full)` : ''}{ev.status === 'limited' ? ` (limited)` : ''}{ev.start.getTime() <= clock.tick && ev.end.getTime() > clock.tick ? ` (${L.inProgress})` : ''}"
|
|
545
|
+
onpointerdown={(e) => onEventPointerDown(e, ev)}
|
|
546
|
+
onpointerenter={() => oneventhover?.(ev)}
|
|
544
547
|
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); oneventclick?.(ev); } }}
|
|
545
548
|
>
|
|
546
|
-
|
|
547
|
-
<span class="wg-ev-title">{ev.title}</span>
|
|
549
|
+
{@render timedEventContent(ev)}
|
|
548
550
|
</div>
|
|
549
551
|
{/each}
|
|
550
|
-
{#if
|
|
551
|
-
<div
|
|
552
|
+
{#if previewTimedEvent}
|
|
553
|
+
<div
|
|
554
|
+
class="wg-ev wg-ev--drag-preview"
|
|
555
|
+
style:--ev-color={previewTimedEvent.color ?? 'var(--dt-accent)'}
|
|
556
|
+
aria-hidden="true"
|
|
557
|
+
>
|
|
558
|
+
{@render timedEventContent(previewTimedEvent)}
|
|
559
|
+
</div>
|
|
560
|
+
{/if}
|
|
561
|
+
{#if visibleTimedEvents.length > MAX_EVENTS_SHOWN}
|
|
562
|
+
<div class="wg-ev-more">{L.nMore(visibleTimedEvents.length - MAX_EVENTS_SHOWN)}</div>
|
|
552
563
|
{/if}
|
|
553
564
|
</div>
|
|
554
565
|
</div>
|
|
@@ -589,6 +600,8 @@
|
|
|
589
600
|
flex: 1;
|
|
590
601
|
overflow-y: auto;
|
|
591
602
|
overflow-x: hidden;
|
|
603
|
+
box-sizing: border-box;
|
|
604
|
+
padding-top: 48px;
|
|
592
605
|
scrollbar-width: thin;
|
|
593
606
|
scrollbar-color: var(--dt-scrollbar, rgba(0, 0, 0, 0.08)) transparent;
|
|
594
607
|
}
|
|
@@ -612,8 +625,8 @@
|
|
|
612
625
|
|
|
613
626
|
.wg-week--current {
|
|
614
627
|
background: var(--dt-today-bg, rgba(239, 68, 68, 0.02));
|
|
615
|
-
border: 2.5px solid var(--dt-accent, #
|
|
616
|
-
box-shadow: 0 0 0 1px color-mix(in srgb, var(--dt-accent, #
|
|
628
|
+
border: 2.5px solid var(--dt-accent, #2563eb);
|
|
629
|
+
box-shadow: 0 0 0 1px color-mix(in srgb, var(--dt-accent, #2563eb) 15%, transparent);
|
|
617
630
|
}
|
|
618
631
|
|
|
619
632
|
/* ─── Week body ──────────────────────────────────── */
|
|
@@ -659,8 +672,6 @@
|
|
|
659
672
|
|
|
660
673
|
/* ─── Disabled cell ──────────────────────────────── */
|
|
661
674
|
.wg-cell--disabled {
|
|
662
|
-
opacity: 0.35;
|
|
663
|
-
pointer-events: none;
|
|
664
675
|
background: repeating-linear-gradient(
|
|
665
676
|
45deg,
|
|
666
677
|
transparent,
|
|
@@ -723,7 +734,7 @@
|
|
|
723
734
|
}
|
|
724
735
|
|
|
725
736
|
.wg-cell-hd--today .wg-day-wd {
|
|
726
|
-
color: var(--dt-accent, #
|
|
737
|
+
color: var(--dt-accent, #2563eb);
|
|
727
738
|
font-weight: 600;
|
|
728
739
|
}
|
|
729
740
|
|
|
@@ -737,7 +748,7 @@
|
|
|
737
748
|
}
|
|
738
749
|
|
|
739
750
|
.wg-day-num--today {
|
|
740
|
-
color: var(--dt-accent, #
|
|
751
|
+
color: var(--dt-accent, #2563eb);
|
|
741
752
|
font-weight: 900;
|
|
742
753
|
}
|
|
743
754
|
|
|
@@ -774,15 +785,25 @@
|
|
|
774
785
|
gap: 3px;
|
|
775
786
|
padding: 2px 5px;
|
|
776
787
|
border-radius: 3px;
|
|
777
|
-
background: color-mix(in srgb, var(--ev-color) 22%, var(--dt-surface, #
|
|
788
|
+
background: color-mix(in srgb, var(--ev-color) 22%, var(--dt-surface, var(--dt-bg, #ffffff)));
|
|
778
789
|
cursor: pointer;
|
|
779
790
|
overflow: hidden;
|
|
780
791
|
transition: background 0.12s;
|
|
781
792
|
min-height: 18px;
|
|
782
793
|
}
|
|
783
794
|
|
|
795
|
+
.wg-ad--drag-preview {
|
|
796
|
+
position: relative;
|
|
797
|
+
z-index: 8;
|
|
798
|
+
opacity: 0.95;
|
|
799
|
+
pointer-events: none;
|
|
800
|
+
box-shadow: 0 6px 18px color-mix(in srgb, var(--ev-color) 26%, rgba(0, 0, 0, 0.22));
|
|
801
|
+
outline: 1px solid color-mix(in srgb, var(--ev-color) 42%, transparent);
|
|
802
|
+
cursor: grabbing;
|
|
803
|
+
}
|
|
804
|
+
|
|
784
805
|
.wg-ad:hover {
|
|
785
|
-
background: color-mix(in srgb, var(--ev-color) 32%, var(--dt-surface, #
|
|
806
|
+
background: color-mix(in srgb, var(--ev-color) 32%, var(--dt-surface, var(--dt-bg, #ffffff)));
|
|
786
807
|
}
|
|
787
808
|
|
|
788
809
|
.wg-ad--start {
|
|
@@ -842,14 +863,25 @@
|
|
|
842
863
|
gap: 3px 5px;
|
|
843
864
|
padding: 3px 6px;
|
|
844
865
|
border-radius: 4px;
|
|
845
|
-
background: color-mix(in srgb, var(--ev-color) 15%, var(--dt-surface, #
|
|
866
|
+
background: color-mix(in srgb, var(--ev-color) 15%, var(--dt-surface, var(--dt-bg, #ffffff)));
|
|
846
867
|
cursor: pointer;
|
|
847
868
|
overflow: hidden;
|
|
848
869
|
transition: background 0.12s;
|
|
849
870
|
}
|
|
850
871
|
|
|
851
872
|
.wg-ev:hover {
|
|
852
|
-
background: color-mix(in srgb, var(--ev-color) 25%, var(--dt-surface, #
|
|
873
|
+
background: color-mix(in srgb, var(--ev-color) 25%, var(--dt-surface, var(--dt-bg, #ffffff)));
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
.wg-ev--drag-preview {
|
|
877
|
+
position: relative;
|
|
878
|
+
z-index: 8;
|
|
879
|
+
opacity: 0.95;
|
|
880
|
+
pointer-events: none;
|
|
881
|
+
background: color-mix(in srgb, var(--ev-color) 28%, var(--dt-surface, var(--dt-bg, #ffffff)));
|
|
882
|
+
box-shadow: 0 6px 18px color-mix(in srgb, var(--ev-color) 24%, rgba(0, 0, 0, 0.22));
|
|
883
|
+
outline: 1px solid color-mix(in srgb, var(--ev-color) 42%, transparent);
|
|
884
|
+
cursor: grabbing;
|
|
853
885
|
}
|
|
854
886
|
|
|
855
887
|
.wg-ev--selected {
|
|
@@ -857,7 +889,28 @@
|
|
|
857
889
|
}
|
|
858
890
|
|
|
859
891
|
.wg-ev--current {
|
|
860
|
-
background: color-mix(in srgb, var(--ev-color) 22%, var(--dt-surface, #
|
|
892
|
+
background: color-mix(in srgb, var(--ev-color) 22%, var(--dt-surface, var(--dt-bg, #ffffff)));
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
.wg-ev--cancelled {
|
|
896
|
+
opacity: 0.5;
|
|
897
|
+
}
|
|
898
|
+
.wg-ev--cancelled .wg-ev-title {
|
|
899
|
+
text-decoration: line-through;
|
|
900
|
+
}
|
|
901
|
+
.wg-ev--tentative {
|
|
902
|
+
opacity: 0.65;
|
|
903
|
+
border: 1px dashed color-mix(in srgb, var(--ev-color) 40%, transparent);
|
|
904
|
+
}
|
|
905
|
+
.wg-ev--full {
|
|
906
|
+
opacity: 0.55;
|
|
907
|
+
}
|
|
908
|
+
.wg-ev--limited {
|
|
909
|
+
opacity: 0.65;
|
|
910
|
+
border: 1px dashed color-mix(in srgb, var(--ev-color) 40%, transparent);
|
|
911
|
+
}
|
|
912
|
+
.wg-ev--readonly {
|
|
913
|
+
cursor: default;
|
|
861
914
|
}
|
|
862
915
|
|
|
863
916
|
.wg-ev-time {
|
|
@@ -875,6 +928,15 @@
|
|
|
875
928
|
text-overflow: ellipsis;
|
|
876
929
|
}
|
|
877
930
|
|
|
931
|
+
.wg-ev-loc {
|
|
932
|
+
font: 400 9px / 1 var(--dt-sans, system-ui, sans-serif);
|
|
933
|
+
color: var(--dt-text-3, rgba(0, 0, 0, 0.35));
|
|
934
|
+
white-space: nowrap;
|
|
935
|
+
overflow: hidden;
|
|
936
|
+
text-overflow: ellipsis;
|
|
937
|
+
flex-shrink: 1;
|
|
938
|
+
}
|
|
939
|
+
|
|
878
940
|
.wg-ev-more {
|
|
879
941
|
font: 500 10px / 1 var(--dt-sans, system-ui, sans-serif);
|
|
880
942
|
color: var(--dt-text-3, rgba(0, 0, 0, 0.35));
|
|
@@ -894,7 +956,7 @@
|
|
|
894
956
|
z-index: 20;
|
|
895
957
|
display: flex;
|
|
896
958
|
gap: 2px;
|
|
897
|
-
background: color-mix(in srgb, var(--dt-surface, #
|
|
959
|
+
background: color-mix(in srgb, var(--dt-surface, var(--dt-bg, #ffffff)) 85%, transparent);
|
|
898
960
|
backdrop-filter: blur(6px);
|
|
899
961
|
-webkit-backdrop-filter: blur(6px);
|
|
900
962
|
border-radius: 8px;
|
|
@@ -911,7 +973,7 @@
|
|
|
911
973
|
background: transparent;
|
|
912
974
|
color: var(--dt-text-2, rgba(148, 163, 184, 0.55));
|
|
913
975
|
cursor: pointer;
|
|
914
|
-
font: 600 11px / 1 var(--dt-sans,
|
|
976
|
+
font: 600 11px / 1 var(--dt-sans, system-ui, sans-serif);
|
|
915
977
|
padding: 6px 12px;
|
|
916
978
|
border-radius: 6px;
|
|
917
979
|
letter-spacing: 0.04em;
|
|
@@ -925,18 +987,18 @@
|
|
|
925
987
|
color: var(--dt-text, rgba(226, 232, 240, 0.85));
|
|
926
988
|
}
|
|
927
989
|
.wg-nav-pill:focus-visible {
|
|
928
|
-
outline: 2px solid color-mix(in srgb, var(--dt-accent, #
|
|
990
|
+
outline: 2px solid color-mix(in srgb, var(--dt-accent, #2563eb) 55%, transparent);
|
|
929
991
|
outline-offset: 2px;
|
|
930
992
|
}
|
|
931
993
|
|
|
932
994
|
/* ─── Focus-visible ──────────────────────────────── */
|
|
933
995
|
.wg-cell:focus-visible {
|
|
934
|
-
outline: 2px solid var(--dt-accent, #
|
|
996
|
+
outline: 2px solid var(--dt-accent, #2563eb);
|
|
935
997
|
outline-offset: -2px;
|
|
936
998
|
}
|
|
937
999
|
|
|
938
1000
|
.wg-ev:focus-visible {
|
|
939
|
-
outline: 2px solid var(--ev-color, var(--dt-accent, #
|
|
1001
|
+
outline: 2px solid var(--ev-color, var(--dt-accent, #2563eb));
|
|
940
1002
|
outline-offset: 1px;
|
|
941
1003
|
}
|
|
942
1004
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { TimelineEvent, BlockedSlot } from '../../core/types.js';
|
|
3
|
+
import type { DragState } from '../../engine/drag.svelte.js';
|
|
4
|
+
import type { ViewState } from '../../engine/view-state.svelte.js';
|
|
5
|
+
export interface CalendarContext {
|
|
6
|
+
readonly viewState: ViewState | undefined;
|
|
7
|
+
readonly drag: DragState | undefined;
|
|
8
|
+
readonly commitDrag: (() => void) | undefined;
|
|
9
|
+
readonly snapInterval: number;
|
|
10
|
+
readonly showNav: boolean;
|
|
11
|
+
readonly equalDays: boolean;
|
|
12
|
+
readonly showDates: boolean;
|
|
13
|
+
readonly hideDays: number[] | undefined;
|
|
14
|
+
readonly isMobile: boolean;
|
|
15
|
+
readonly autoHeight: boolean;
|
|
16
|
+
readonly compact: boolean;
|
|
17
|
+
readonly readOnly: boolean;
|
|
18
|
+
readonly blockedSlots: BlockedSlot[] | undefined;
|
|
19
|
+
readonly dayHeaderSnippet: Snippet<[{
|
|
20
|
+
date: Date;
|
|
21
|
+
isToday: boolean;
|
|
22
|
+
dayName: string;
|
|
23
|
+
}]> | undefined;
|
|
24
|
+
readonly minDuration: number | undefined;
|
|
25
|
+
readonly maxDuration: number | undefined;
|
|
26
|
+
readonly oneventhover: ((event: TimelineEvent) => void) | undefined;
|
|
27
|
+
readonly disabledDates: Date[] | undefined;
|
|
28
|
+
readonly disabledSet: Set<number>;
|
|
29
|
+
readonly loadRange: {
|
|
30
|
+
current: {
|
|
31
|
+
start: Date;
|
|
32
|
+
end: Date;
|
|
33
|
+
} | null;
|
|
34
|
+
set: (r: {
|
|
35
|
+
start: Date;
|
|
36
|
+
end: Date;
|
|
37
|
+
} | null) => void;
|
|
38
|
+
} | undefined;
|
|
39
|
+
readonly eventSnippet: Snippet<[TimelineEvent]> | undefined;
|
|
40
|
+
readonly emptySnippet: Snippet | undefined;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Read the calendar context.
|
|
44
|
+
* Call at component init time (top-level script).
|
|
45
|
+
* Returns an object with getters that delegate to the raw context.
|
|
46
|
+
*/
|
|
47
|
+
export declare function useCalendarContext(): CalendarContext;
|