@nomideusz/svelte-calendar 0.20.1 → 1.0.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 +502 -267
- package/dist/adapters/composite.d.ts +12 -1
- package/dist/adapters/composite.js +109 -47
- package/dist/adapters/errors.d.ts +22 -0
- package/dist/adapters/errors.js +32 -0
- package/dist/adapters/index.d.ts +1 -0
- package/dist/adapters/index.js +1 -0
- package/dist/adapters/jmap.js +26 -17
- package/dist/adapters/mapped.d.ts +2 -2
- package/dist/adapters/mapped.js +18 -9
- package/dist/adapters/memory.d.ts +1 -1
- package/dist/adapters/memory.js +5 -3
- package/dist/adapters/recurring.d.ts +1 -1
- package/dist/adapters/recurring.js +43 -25
- package/dist/adapters/rest.d.ts +2 -2
- package/dist/adapters/rest.js +9 -4
- package/dist/calendar/Calendar.svelte +122 -78
- package/dist/calendar/Calendar.svelte.d.ts +69 -14
- package/dist/calendar/index.d.ts +2 -2
- package/dist/calendar/index.js +1 -1
- package/dist/core/clock.svelte.js +3 -2
- package/dist/core/locale.d.ts +11 -1
- package/dist/core/locale.js +16 -16
- package/dist/core/time.d.ts +9 -0
- package/dist/core/time.js +12 -5
- package/dist/core/timezone.d.ts +7 -3
- package/dist/core/timezone.js +10 -4
- package/dist/engine/event-store.svelte.js +94 -57
- package/dist/engine/view-state.svelte.d.ts +5 -1
- package/dist/engine/view-state.svelte.js +8 -6
- package/dist/headless/create-agenda.svelte.d.ts +8 -1
- package/dist/headless/create-agenda.svelte.js +36 -18
- package/dist/headless/create-calendar.svelte.js +57 -21
- package/dist/headless/create-range-agenda.svelte.d.ts +7 -0
- package/dist/headless/create-range-agenda.svelte.js +38 -17
- package/dist/headless/index.d.ts +1 -0
- package/dist/index.d.ts +10 -7
- package/dist/index.js +8 -5
- package/dist/primitives/DayHeader.svelte +3 -2
- package/dist/primitives/DayHeader.svelte.d.ts +1 -1
- package/dist/primitives/EmptySlot.svelte +5 -2
- package/dist/primitives/EventBlock.svelte +21 -14
- package/dist/primitives/FloatingPanel.svelte +110 -15
- package/dist/primitives/FloatingPanel.svelte.d.ts +7 -5
- package/dist/primitives/NowIndicator.svelte +11 -4
- package/dist/primitives/TimeGutter.svelte +10 -6
- package/dist/primitives/index.d.ts +1 -0
- package/dist/text-fit.d.ts +8 -2
- package/dist/views/agenda/AgendaDay.svelte +43 -35
- package/dist/views/agenda/AgendaWeek.svelte +75 -38
- package/dist/views/index.d.ts +2 -0
- package/dist/views/index.js +2 -0
- package/dist/views/mobile/MobileDay.svelte +99 -42
- package/dist/views/mobile/MobileWeek.svelte +31 -15
- package/dist/views/mobile/swipe.js +6 -1
- package/dist/views/month/MonthGrid.svelte +33 -21
- package/dist/views/month/MonthGrid.svelte.d.ts +1 -1
- package/dist/views/planner/PlannerScroll.svelte +438 -164
- package/dist/views/planner/PlannerWeek.svelte +150 -50
- package/dist/views/shared/context.svelte.d.ts +1 -1
- package/package.json +24 -9
- package/widget/widget.js +10000 -9520
- package/dist/assets/favicon.svg +0 -9
- package/dist/widget/CalendarWidget.svelte +0 -114
- package/dist/widget/CalendarWidget.svelte.d.ts +0 -37
- package/dist/widget/index.d.ts +0 -1
- package/dist/widget/index.js +0 -1
- package/dist/widget/widget.d.ts +0 -10
- package/dist/widget/widget.js +0 -96
|
@@ -9,11 +9,12 @@
|
|
|
9
9
|
* Answers: "What's coming up and when do I need to be ready?"
|
|
10
10
|
*/
|
|
11
11
|
import { createClock } from "../../core/clock.svelte.js";
|
|
12
|
-
import { sod,
|
|
12
|
+
import { sod, addDaysMs, startOfWeek, dayNum, isAllDay, isMultiDay, segmentForDay } from "../../core/time.js";
|
|
13
13
|
import { weekdayLong, monthLong } from "../../core/locale.js";
|
|
14
14
|
import { useCalendarContext } from "../shared/context.svelte.js";
|
|
15
15
|
import EventContent from "../shared/EventContent.svelte";
|
|
16
16
|
import { fmtTime, duration, timeUntilMs, progress, groupIntoSlots } from "../shared/format.js";
|
|
17
|
+
import { createSwipe } from "../mobile/swipe.js";
|
|
17
18
|
const ctx = useCalendarContext();
|
|
18
19
|
const L = $derived(ctx.labels);
|
|
19
20
|
let { mondayStart = true, locale, height, events = [], style = "", focusDate, oneventclick, selectedEventId = null } = $props();
|
|
@@ -34,6 +35,28 @@ const disabledSet = $derived(ctx.disabledSet);
|
|
|
34
35
|
function clickDay(ms) {
|
|
35
36
|
ondayclick?.(new Date(ms));
|
|
36
37
|
}
|
|
38
|
+
/** Keyboard activation for the day header / empty-day row (role="button").
|
|
39
|
+
* Keys aimed at a control inside a custom dayHeader snippet stay its own. */
|
|
40
|
+
function dayKeydown(e, ms) {
|
|
41
|
+
if (e.target !== e.currentTarget) return;
|
|
42
|
+
if (e.key !== "Enter" && e.key !== " ") return;
|
|
43
|
+
e.preventDefault();
|
|
44
|
+
clickDay(ms);
|
|
45
|
+
}
|
|
46
|
+
// ── Status suffix (aria) ────────────────────────────
|
|
47
|
+
function statusText(ev) {
|
|
48
|
+
if (ev.status === "cancelled") return ` (${L.cancelled})`;
|
|
49
|
+
if (ev.status === "tentative") return ` (${L.tentative})`;
|
|
50
|
+
if (ev.status === "full") return ` (${L.full})`;
|
|
51
|
+
if (ev.status === "limited") return ` (${L.limited})`;
|
|
52
|
+
return "";
|
|
53
|
+
}
|
|
54
|
+
/** All-day events and timed ones at least a (wall-clock) day long go to
|
|
55
|
+
* the all-day chips; a shorter overnight event is listed with its time on
|
|
56
|
+
* each day it touches. */
|
|
57
|
+
function inAllDayStrip(ev) {
|
|
58
|
+
return isAllDay(ev) || ev.end.getTime() >= addDaysMs(ev.start.getTime(), 1);
|
|
59
|
+
}
|
|
37
60
|
// ── Day-to-day card drag (timetable columns) ────────
|
|
38
61
|
// This view has no hour axis, so a drag can only choose a DAY: the card
|
|
39
62
|
// keeps its time of day. Wired through the same oneventmove the planner
|
|
@@ -62,34 +85,24 @@ function onDayDrop(e, day) {
|
|
|
62
85
|
if (!ev) return;
|
|
63
86
|
// Same-day drop is a no-op, not a zero-minute "move" for the host to reject.
|
|
64
87
|
if (sod(ev.start.getTime()) === day.ms) return;
|
|
65
|
-
|
|
88
|
+
// Same wall-clock time on the target day (a fixed ms offset from
|
|
89
|
+
// midnight is an hour off when a DST change lies between the two).
|
|
90
|
+
const s = ev.start;
|
|
91
|
+
const start = new Date(new Date(day.ms).setHours(s.getHours(), s.getMinutes(), s.getSeconds(), s.getMilliseconds()));
|
|
66
92
|
const end = new Date(start.getTime() + (ev.end.getTime() - ev.start.getTime()));
|
|
67
93
|
oneventmove?.(ev, start, end);
|
|
68
94
|
}
|
|
69
95
|
// ── Swipe navigation (mobile, touch only) ──────────
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
swipeStartY = e.clientY;
|
|
79
|
-
}
|
|
80
|
-
function onPointerUp(e) {
|
|
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();
|
|
96
|
+
// Touch events, not pointer events: once the browser claims a touch for
|
|
97
|
+
// panning it sends pointercancel and the pointerup never arrives.
|
|
98
|
+
const swipe = createSwipe({
|
|
99
|
+
disabled: () => !isMobile,
|
|
100
|
+
onmove: () => {},
|
|
101
|
+
onend: (dir) => {
|
|
102
|
+
if (dir > 0) viewState?.prev();
|
|
103
|
+
else if (dir < 0) viewState?.next();
|
|
88
104
|
}
|
|
89
|
-
}
|
|
90
|
-
function onPointerCancel() {
|
|
91
|
-
swipeActive = false;
|
|
92
|
-
}
|
|
105
|
+
});
|
|
93
106
|
// ── Compact-day disclosure ("+N more" → expand) ─────
|
|
94
107
|
let expandedDays = $state([]);
|
|
95
108
|
function toggleDayExpand(ms) {
|
|
@@ -115,14 +128,14 @@ const customDays = $derived(viewState?.dayCount ?? 7);
|
|
|
115
128
|
const weekDays = $derived.by(() => {
|
|
116
129
|
const now = clock.tick;
|
|
117
130
|
const todayMs = clock.today;
|
|
118
|
-
const tomorrowMs = todayMs
|
|
131
|
+
const tomorrowMs = addDaysMs(todayMs, 1);
|
|
119
132
|
const out = [];
|
|
120
133
|
for (let i = 0; i < customDays; i++) {
|
|
121
|
-
const ms = weekStartMs
|
|
122
|
-
const dEnd = ms
|
|
134
|
+
const ms = addDaysMs(weekStartMs, i);
|
|
135
|
+
const dEnd = addDaysMs(ms, 1);
|
|
123
136
|
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(
|
|
125
|
-
const timedEvts = dayEvts.filter((ev) => !
|
|
137
|
+
const allDayEvts = dayEvts.filter(inAllDayStrip);
|
|
138
|
+
const timedEvts = dayEvts.filter((ev) => !inAllDayStrip(ev));
|
|
126
139
|
const totalMinutes = timedEvts.reduce((sum, ev) => {
|
|
127
140
|
const s = Math.max(ev.start.getTime(), ms);
|
|
128
141
|
const e = Math.min(ev.end.getTime(), dEnd);
|
|
@@ -186,7 +199,7 @@ const weekDays = $derived.by(() => {
|
|
|
186
199
|
class:ag-card--full={ev.status === 'full'}
|
|
187
200
|
class:ag-card--limited={ev.status === 'limited'}
|
|
188
201
|
style:--ev-color={ev.color || 'var(--dt-accent)'}
|
|
189
|
-
aria-label="{ev.title}{
|
|
202
|
+
aria-label="{ev.title}{statusText(ev)}, {fmt(ev.start)} – {fmt(ev.end)}, {duration(ev)}"
|
|
190
203
|
class:ag-card--drag={dragId === String(ev.id)}
|
|
191
204
|
draggable={canDrag(ev)}
|
|
192
205
|
ondragstart={(e) => onCardDragStart(e, ev)}
|
|
@@ -243,7 +256,7 @@ const weekDays = $derived.by(() => {
|
|
|
243
256
|
class:ag-compact--full={ev.status === 'full'}
|
|
244
257
|
class:ag-compact--limited={ev.status === 'limited'}
|
|
245
258
|
style:--ev-color={ev.color || 'var(--dt-accent)'}
|
|
246
|
-
aria-label="{ev.title}{done ? `, ${L.completed}` : ''}, {fmt(ev.start)}, {duration(ev)}"
|
|
259
|
+
aria-label="{ev.title}{statusText(ev)}{done ? `, ${L.completed}` : ''}, {fmt(ev.start)}, {duration(ev)}"
|
|
247
260
|
onclick={() => handleClick(ev)}
|
|
248
261
|
onpointerenter={() => oneventhover?.(ev)}
|
|
249
262
|
>
|
|
@@ -283,7 +296,7 @@ const weekDays = $derived.by(() => {
|
|
|
283
296
|
</button>
|
|
284
297
|
{/snippet}
|
|
285
298
|
|
|
286
|
-
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
299
|
+
<!-- svelte-ignore a11y_no_static_element_interactions (touch swipe only; no pointer/keyboard role) -->
|
|
287
300
|
<div
|
|
288
301
|
class="ag ag--week"
|
|
289
302
|
class:ag--mobile={isMobile}
|
|
@@ -291,9 +304,10 @@ const weekDays = $derived.by(() => {
|
|
|
291
304
|
class:ag--cols={cols}
|
|
292
305
|
style={style || undefined}
|
|
293
306
|
style:height={height ? `${height}px` : undefined}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
307
|
+
ontouchstart={swipe.ontouchstart}
|
|
308
|
+
ontouchmove={swipe.ontouchmove}
|
|
309
|
+
ontouchend={swipe.ontouchend}
|
|
310
|
+
ontouchcancel={swipe.ontouchcancel}
|
|
297
311
|
>
|
|
298
312
|
<div class="ag-body" role="list" aria-label={L.weekAhead} style:--ag-cols={weekDays.length}>
|
|
299
313
|
{#each weekDays as day (day.ms)}
|
|
@@ -301,11 +315,15 @@ const weekDays = $derived.by(() => {
|
|
|
301
315
|
{#if day.tier === 'past'}
|
|
302
316
|
<!-- Past day: single collapsed line -->
|
|
303
317
|
<div class="ag-wday ag-wday--past" class:ag-wday--disabled={disabledSet.has(day.ms)} data-day={day.ms} role="listitem">
|
|
304
|
-
<!--
|
|
318
|
+
<!-- role="button" exactly when ondayclick is set (Calendar always sets it) -->
|
|
319
|
+
<!-- svelte-ignore a11y_no_static_element_interactions, a11y_no_noninteractive_tabindex -->
|
|
305
320
|
<div
|
|
306
321
|
class="ag-wday-head"
|
|
307
322
|
class:ag-wday-head--click={!!ondayclick}
|
|
323
|
+
role={ondayclick ? 'button' : undefined}
|
|
324
|
+
tabindex={ondayclick ? 0 : undefined}
|
|
308
325
|
onclick={ondayclick ? () => clickDay(day.ms) : undefined}
|
|
326
|
+
onkeydown={ondayclick ? (e) => dayKeydown(e, day.ms) : undefined}
|
|
309
327
|
>
|
|
310
328
|
<div class="ag-wday-head-left">
|
|
311
329
|
<span class="ag-wday-name">{day.dayName}</span>
|
|
@@ -353,11 +371,15 @@ const weekDays = $derived.by(() => {
|
|
|
353
371
|
ondrop={(e) => onDayDrop(e, day)}
|
|
354
372
|
>
|
|
355
373
|
<!-- Day header -->
|
|
356
|
-
<!--
|
|
374
|
+
<!-- role="button" exactly when ondayclick is set (Calendar always sets it) -->
|
|
375
|
+
<!-- svelte-ignore a11y_no_static_element_interactions, a11y_no_noninteractive_tabindex -->
|
|
357
376
|
<div
|
|
358
377
|
class="ag-wday-head"
|
|
359
378
|
class:ag-wday-head--click={!!ondayclick}
|
|
379
|
+
role={ondayclick ? 'button' : undefined}
|
|
380
|
+
tabindex={ondayclick ? 0 : undefined}
|
|
360
381
|
onclick={ondayclick ? () => clickDay(day.ms) : undefined}
|
|
382
|
+
onkeydown={ondayclick ? (e) => dayKeydown(e, day.ms) : undefined}
|
|
361
383
|
>
|
|
362
384
|
<div class="ag-wday-head-left">
|
|
363
385
|
{#if day.isToday}
|
|
@@ -400,11 +422,14 @@ const weekDays = $derived.by(() => {
|
|
|
400
422
|
{/if}
|
|
401
423
|
|
|
402
424
|
{#if day.events.length === 0}
|
|
403
|
-
<!-- svelte-ignore
|
|
425
|
+
<!-- svelte-ignore a11y_no_static_element_interactions, a11y_no_noninteractive_tabindex -->
|
|
404
426
|
<div
|
|
405
427
|
class="ag-wday-empty"
|
|
406
428
|
class:ag-wday-empty--click={!!ondayclick}
|
|
429
|
+
role={ondayclick ? 'button' : undefined}
|
|
430
|
+
tabindex={ondayclick ? 0 : undefined}
|
|
407
431
|
onclick={ondayclick ? () => clickDay(day.ms) : undefined}
|
|
432
|
+
onkeydown={ondayclick ? (e) => dayKeydown(e, day.ms) : undefined}
|
|
408
433
|
>{L.noEvents}</div>
|
|
409
434
|
{:else if compact && !cols}
|
|
410
435
|
<!-- Compact: minimal dot + time + title rows for all days.
|
|
@@ -772,6 +797,12 @@ const weekDays = $derived.by(() => {
|
|
|
772
797
|
.ag-wday-empty--click {
|
|
773
798
|
cursor: pointer;
|
|
774
799
|
}
|
|
800
|
+
.ag-wday-head--click:focus-visible,
|
|
801
|
+
.ag-wday-empty--click:focus-visible {
|
|
802
|
+
outline: 2px solid var(--dt-accent, #2563eb);
|
|
803
|
+
outline-offset: -2px;
|
|
804
|
+
border-radius: 4px;
|
|
805
|
+
}
|
|
775
806
|
.ag-wday-head {
|
|
776
807
|
display: flex;
|
|
777
808
|
justify-content: space-between;
|
|
@@ -1119,6 +1150,12 @@ const weekDays = $derived.by(() => {
|
|
|
1119
1150
|
}
|
|
1120
1151
|
|
|
1121
1152
|
/* ═══ Mobile adaptations ═══ */
|
|
1153
|
+
/* Horizontal swipes are ours (week navigation); the browser keeps
|
|
1154
|
+
vertical scrolling and pinch-zoom. */
|
|
1155
|
+
.ag--mobile,
|
|
1156
|
+
.ag--mobile .ag-body {
|
|
1157
|
+
touch-action: pan-y pinch-zoom;
|
|
1158
|
+
}
|
|
1122
1159
|
.ag--mobile .ag-wday-head {
|
|
1123
1160
|
padding: 12px 16px;
|
|
1124
1161
|
}
|
package/dist/views/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { default as Planner } from './planner/Planner.svelte';
|
|
2
|
+
export { default as PlannerScroll } from './planner/PlannerScroll.svelte';
|
|
2
3
|
export { default as Agenda } from './agenda/Agenda.svelte';
|
|
3
4
|
export { default as Mobile } from './mobile/Mobile.svelte';
|
|
5
|
+
export { default as MonthGrid } from './month/MonthGrid.svelte';
|
package/dist/views/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { default as Planner } from './planner/Planner.svelte';
|
|
2
|
+
export { default as PlannerScroll } from './planner/PlannerScroll.svelte';
|
|
2
3
|
export { default as Agenda } from './agenda/Agenda.svelte';
|
|
3
4
|
export { default as Mobile } from './mobile/Mobile.svelte';
|
|
5
|
+
export { default as MonthGrid } from './month/MonthGrid.svelte';
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { useCalendarContext } from "../shared/context.svelte.js";
|
|
10
10
|
import EventContent from "../shared/EventContent.svelte";
|
|
11
11
|
import { createClock } from "../../core/clock.svelte.js";
|
|
12
|
-
import {
|
|
12
|
+
import { HOUR_MS, sod, addDaysMs, diffDays, isAllDay, segmentForDay } from "../../core/time.js";
|
|
13
13
|
import { fmtH, fmtTime } from "../../core/locale.js";
|
|
14
14
|
import { createSwipe } from "./swipe.js";
|
|
15
15
|
let { height = null, events = [], style = "", locale, focusDate, oneventclick, oneventcreate, selectedEventId = null, readOnly = false, visibleHours } = $props();
|
|
@@ -27,6 +27,36 @@ const drag = $derived(ctx.drag);
|
|
|
27
27
|
const commitDragCtx = $derived(ctx.commitDrag);
|
|
28
28
|
const SNAP_MS = $derived(ctx.snapInterval * 6e4);
|
|
29
29
|
const clock = createClock(ctx.timezone);
|
|
30
|
+
// ── Wall-clock time axis ───────────────────────────
|
|
31
|
+
// The grid is 24 wall-clock rows. A DST day is 23 or 25 hours long, so
|
|
32
|
+
// `dayMs + h * HOUR_MS` lands an hour off after the change: every
|
|
33
|
+
// hour ↔ instant conversion goes through the local calendar instead.
|
|
34
|
+
/** Fractional wall-clock hour `hour` on the day starting at `dayMs` → epoch ms. */
|
|
35
|
+
function atHour(day, hour) {
|
|
36
|
+
// MakeTime sums the fields in local time, so 24 → next midnight.
|
|
37
|
+
return new Date(day).setHours(0, 0, 0, Math.round(hour * HOUR_MS));
|
|
38
|
+
}
|
|
39
|
+
/** Epoch ms → fractional wall-clock hour counted from the day at `day`
|
|
40
|
+
* (24 = the next midnight; calendar days, not 24 h steps). */
|
|
41
|
+
function hourOf(ms, day) {
|
|
42
|
+
const d = new Date(ms);
|
|
43
|
+
return diffDays(ms, day) * 24 + d.getHours() + d.getMinutes() / 60 + d.getSeconds() / 3600 + d.getMilliseconds() / HOUR_MS;
|
|
44
|
+
}
|
|
45
|
+
/** Snap a fractional hour to the snap interval in wall-clock minutes of
|
|
46
|
+
* the local day (not UTC multiples — those are off in +05:30 zones). */
|
|
47
|
+
function snapHour(hour, how) {
|
|
48
|
+
const step = Math.max(1, ctx.snapInterval);
|
|
49
|
+
const units = hour * 60 / step;
|
|
50
|
+
// Epsilon: 10:15 must not floor to 10:00 (or ceil to 10:30) through float noise.
|
|
51
|
+
const n = how === "floor" ? Math.floor(units + 1e-9) : how === "ceil" ? Math.ceil(units - 1e-9) : Math.round(units);
|
|
52
|
+
return n * step / 60;
|
|
53
|
+
}
|
|
54
|
+
/** Timed events at least a (wall-clock) day long, and all-day ones, sit in
|
|
55
|
+
* the all-day strip; a shorter overnight event is drawn in the grid,
|
|
56
|
+
* clipped to this day. */
|
|
57
|
+
function inAllDayStrip(ev) {
|
|
58
|
+
return isAllDay(ev) || ev.end.getTime() >= addDaysMs(ev.start.getTime(), 1);
|
|
59
|
+
}
|
|
30
60
|
// ── Config ─────────────────────────────────────────
|
|
31
61
|
const HOUR_HEIGHT = 64;
|
|
32
62
|
const GUTTER_W = 40;
|
|
@@ -36,14 +66,14 @@ const hourCount = $derived(Math.max(1, endHour - startHour));
|
|
|
36
66
|
const gridHeight = $derived(hourCount * HOUR_HEIGHT);
|
|
37
67
|
// ── Day state ──────────────────────────────────────
|
|
38
68
|
const dayMs = $derived(focusDate ? sod(focusDate.getTime()) : clock.today);
|
|
39
|
-
const dayEnd = $derived(dayMs
|
|
69
|
+
const dayEnd = $derived(addDaysMs(dayMs, 1));
|
|
40
70
|
const isToday = $derived(dayMs === clock.today);
|
|
41
71
|
const isDisabled = $derived(disabledSet.has(dayMs));
|
|
42
72
|
// ── Load range ─────────────────────────────────────
|
|
43
73
|
$effect(() => {
|
|
44
74
|
if (!loadRangeCtx) return;
|
|
45
|
-
const rangeStart = new Date(dayMs -
|
|
46
|
-
const rangeEnd = new Date(dayMs
|
|
75
|
+
const rangeStart = new Date(addDaysMs(dayMs, -2));
|
|
76
|
+
const rangeEnd = new Date(addDaysMs(dayMs, 3));
|
|
47
77
|
loadRangeCtx.set({
|
|
48
78
|
start: rangeStart,
|
|
49
79
|
end: rangeEnd
|
|
@@ -51,11 +81,11 @@ $effect(() => {
|
|
|
51
81
|
return () => loadRangeCtx.set(null);
|
|
52
82
|
});
|
|
53
83
|
// ── Events partition ───────────────────────────────
|
|
54
|
-
const timedEvents = $derived(events.filter((ev) => !
|
|
84
|
+
const timedEvents = $derived(events.filter((ev) => !inAllDayStrip(ev) && ev.start.getTime() < dayEnd && ev.end.getTime() > dayMs).sort((a, b) => a.start.getTime() - b.start.getTime()));
|
|
55
85
|
const allDayEvents = $derived.by(() => {
|
|
56
86
|
const segs = [];
|
|
57
87
|
for (const ev of events) {
|
|
58
|
-
if (!
|
|
88
|
+
if (!inAllDayStrip(ev)) continue;
|
|
59
89
|
const seg = segmentForDay(ev, dayMs);
|
|
60
90
|
if (seg) segs.push(seg);
|
|
61
91
|
}
|
|
@@ -82,27 +112,38 @@ const positionedEvents = $derived.by(() => {
|
|
|
82
112
|
}
|
|
83
113
|
}
|
|
84
114
|
// Overlap grouping
|
|
85
|
-
const
|
|
115
|
+
const bandStart = atHour(dayMs, startHour);
|
|
116
|
+
const bandEnd = atHour(dayMs, endHour);
|
|
117
|
+
const infos = sorted.flatMap((ev) => {
|
|
86
118
|
// While resizing, this event tracks the tentative drag payload
|
|
87
119
|
const resizing = rsP?.eventId === ev.id;
|
|
88
120
|
const evStart = resizing ? rsP.start : ev.start;
|
|
89
121
|
const evEnd = resizing ? rsP.end : ev.end;
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
|
|
122
|
+
const s0 = evStart.getTime();
|
|
123
|
+
const e0 = evEnd.getTime();
|
|
124
|
+
const sMs = Math.max(s0, bandStart);
|
|
125
|
+
const eMs = Math.min(e0, bandEnd);
|
|
126
|
+
// Outside the visible hours: nothing to draw (a zero-length marker
|
|
127
|
+
// counts only when it sits inside them).
|
|
128
|
+
if (eMs < sMs || eMs === sMs && (s0 !== e0 || s0 < bandStart || s0 >= bandEnd)) return [];
|
|
129
|
+
const topH = hourOf(sMs, dayMs) - startHour;
|
|
130
|
+
const botH = hourOf(eMs, dayMs) - startHour;
|
|
131
|
+
return [{
|
|
95
132
|
ev,
|
|
96
133
|
top: topH * HOUR_HEIGHT,
|
|
97
134
|
height: Math.max(24, (botH - topH) * HOUR_HEIGHT),
|
|
98
135
|
isCurrent: ev.start.getTime() <= now && ev.end.getTime() > now,
|
|
99
136
|
isNext: ev.id === nextEventId,
|
|
100
137
|
isResizing: resizing,
|
|
138
|
+
// Grips only on the edges that are the event's own — not where the
|
|
139
|
+
// day or the visible hours cut it.
|
|
140
|
+
hasStart: s0 >= Math.max(dayMs, bandStart),
|
|
141
|
+
hasEnd: e0 <= Math.min(dayEnd, bandEnd),
|
|
101
142
|
startMs: sMs,
|
|
102
143
|
endMs: eMs,
|
|
103
144
|
col: 0,
|
|
104
145
|
totalCols: 1
|
|
105
|
-
};
|
|
146
|
+
}];
|
|
106
147
|
});
|
|
107
148
|
// Assign columns for overlapping events
|
|
108
149
|
const par = infos.map((_, i) => i);
|
|
@@ -151,6 +192,8 @@ const positionedEvents = $derived.by(() => {
|
|
|
151
192
|
isCurrent: info.isCurrent,
|
|
152
193
|
isNext: info.isNext,
|
|
153
194
|
isResizing: info.isResizing,
|
|
195
|
+
hasStart: info.hasStart,
|
|
196
|
+
hasEnd: info.hasEnd,
|
|
154
197
|
col: info.col,
|
|
155
198
|
totalCols: info.totalCols
|
|
156
199
|
}));
|
|
@@ -158,7 +201,7 @@ const positionedEvents = $derived.by(() => {
|
|
|
158
201
|
// ── Now indicator ──────────────────────────────────
|
|
159
202
|
const nowOffset = $derived.by(() => {
|
|
160
203
|
if (!isToday) return -1;
|
|
161
|
-
const h = (clock.tick
|
|
204
|
+
const h = hourOf(clock.tick, dayMs) - startHour;
|
|
162
205
|
if (h < 0 || h > hourCount) return -1;
|
|
163
206
|
return h * HOUR_HEIGHT;
|
|
164
207
|
});
|
|
@@ -184,8 +227,14 @@ function statusText(ev) {
|
|
|
184
227
|
let swipeOffset = $state(0);
|
|
185
228
|
let swipeAnimate = $state(false);
|
|
186
229
|
const swipe = createSwipe({
|
|
187
|
-
|
|
230
|
+
// A pending long-press must not block the swipe: pointerdown arms it
|
|
231
|
+
// before touchstart, so the swipe would never track. A horizontal move
|
|
232
|
+
// past the long-press tolerance cancels the pending create instead.
|
|
233
|
+
disabled: () => !!drag?.active || mbCreateStarted || mbRsStarted,
|
|
188
234
|
onmove: (dx) => {
|
|
235
|
+
if (longPressTimer !== null && !mbCreateStarted && Math.abs(dx) > LONG_PRESS_TOLERANCE) {
|
|
236
|
+
cleanupGridCreate();
|
|
237
|
+
}
|
|
189
238
|
swipeAnimate = false;
|
|
190
239
|
swipeOffset = dx;
|
|
191
240
|
},
|
|
@@ -211,9 +260,9 @@ function handleGridClick(e) {
|
|
|
211
260
|
}
|
|
212
261
|
if (!oneventcreate || readOnly || isDisabled) return;
|
|
213
262
|
if (e.target.closest(".mb-event")) return;
|
|
214
|
-
const
|
|
215
|
-
if (isBlockedAt(
|
|
216
|
-
const startMs = clampToDay(
|
|
263
|
+
const hour = gridHour(e.clientY);
|
|
264
|
+
if (isBlockedAt(hour)) return;
|
|
265
|
+
const startMs = clampToDay(atHour(dayMs, snapHour(hour, "floor")));
|
|
217
266
|
const durMin = minDuration ?? 60;
|
|
218
267
|
oneventcreate({
|
|
219
268
|
start: new Date(startMs),
|
|
@@ -226,9 +275,9 @@ function onGridKeydown(e) {
|
|
|
226
275
|
if (e.key !== "Enter" && e.key !== " ") return;
|
|
227
276
|
if (!oneventcreate || readOnly || isDisabled) return;
|
|
228
277
|
e.preventDefault();
|
|
229
|
-
const
|
|
230
|
-
const startMs = clampToDay(
|
|
231
|
-
if (isBlockedAt((startMs
|
|
278
|
+
const rawHour = isToday ? hourOf(clock.tick, dayMs) : startHour;
|
|
279
|
+
const startMs = clampToDay(atHour(dayMs, snapHour(rawHour, "ceil")));
|
|
280
|
+
if (isBlockedAt(hourOf(startMs, dayMs))) return;
|
|
232
281
|
const durMin = minDuration ?? 60;
|
|
233
282
|
oneventcreate({
|
|
234
283
|
start: new Date(startMs),
|
|
@@ -265,20 +314,23 @@ function clearLongPress() {
|
|
|
265
314
|
longPressTimer = null;
|
|
266
315
|
}
|
|
267
316
|
}
|
|
268
|
-
/** Pointer Y →
|
|
269
|
-
function
|
|
317
|
+
/** Pointer Y → fractional wall-clock hour of this day (accounts for scroll). */
|
|
318
|
+
function gridHour(clientY) {
|
|
270
319
|
const rect = gridEl.getBoundingClientRect();
|
|
271
320
|
const y = clientY - rect.top + gridEl.scrollTop;
|
|
272
|
-
return
|
|
321
|
+
return startHour + y / HOUR_HEIGHT;
|
|
322
|
+
}
|
|
323
|
+
/** Pointer Y → wall-clock time snapped to the interval, clamped to the band. */
|
|
324
|
+
function gridSnapped(clientY, how) {
|
|
325
|
+
return clampToDay(atHour(dayMs, snapHour(gridHour(clientY), how)));
|
|
273
326
|
}
|
|
274
327
|
/** Clamp a timestamp into the visible hour range of this day. */
|
|
275
328
|
function clampToDay(ms) {
|
|
276
|
-
return Math.max(dayMs
|
|
329
|
+
return Math.max(atHour(dayMs, startHour), Math.min(atHour(dayMs, endHour), ms));
|
|
277
330
|
}
|
|
278
331
|
function startGridCreate() {
|
|
279
332
|
if (!drag) return;
|
|
280
333
|
mbCreateStarted = true;
|
|
281
|
-
mbCreateAnchorMs = clampToDay(Math.floor(mbCreateAnchorMs / SNAP_MS) * SNAP_MS);
|
|
282
334
|
drag.beginCreate(new Date(mbCreateAnchorMs), new Date(mbCreateAnchorMs + SNAP_MS));
|
|
283
335
|
addTouchScrollBlock();
|
|
284
336
|
}
|
|
@@ -287,7 +339,7 @@ function onGridPointerDown(e) {
|
|
|
287
339
|
if (e.target.closest(".mb-event")) return;
|
|
288
340
|
mbCreateStartX = e.clientX;
|
|
289
341
|
mbCreateStartY = e.clientY;
|
|
290
|
-
mbCreateAnchorMs =
|
|
342
|
+
mbCreateAnchorMs = gridSnapped(e.clientY, "floor");
|
|
291
343
|
mbCreateStarted = false;
|
|
292
344
|
if (e.pointerType === "touch") {
|
|
293
345
|
longPressTimer = setTimeout(() => {
|
|
@@ -312,7 +364,7 @@ function onGridCreateMove(e) {
|
|
|
312
364
|
if (Math.abs(e.clientY - mbCreateStartY) < CREATE_THRESHOLD) return;
|
|
313
365
|
startGridCreate();
|
|
314
366
|
}
|
|
315
|
-
const snapped =
|
|
367
|
+
const snapped = gridSnapped(e.clientY, "round");
|
|
316
368
|
drag.updatePointer(new Date(Math.min(mbCreateAnchorMs, snapped)), new Date(Math.max(mbCreateAnchorMs + SNAP_MS, snapped)));
|
|
317
369
|
}
|
|
318
370
|
function cleanupGridCreate() {
|
|
@@ -370,7 +422,7 @@ function onResizeMove(e) {
|
|
|
370
422
|
drag.beginResize(ev.id, mbRsEdge, ev.start, ev.end);
|
|
371
423
|
addTouchScrollBlock();
|
|
372
424
|
}
|
|
373
|
-
const snapped =
|
|
425
|
+
const snapped = gridSnapped(e.clientY, "round");
|
|
374
426
|
if (mbRsEdge === "end") {
|
|
375
427
|
const end = Math.max(snapped, ev.start.getTime() + SNAP_MS);
|
|
376
428
|
drag.updatePointer(ev.start, new Date(end));
|
|
@@ -562,24 +614,29 @@ $effect(() => {
|
|
|
562
614
|
<span class="mb-ev-next-badge">{L.upNext}</span>
|
|
563
615
|
{/if}
|
|
564
616
|
{#if !readOnly && !p.ev.data?.readOnly}
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
617
|
+
{#if p.hasStart}
|
|
618
|
+
<span
|
|
619
|
+
class="mb-ev-handle mb-ev-handle--start"
|
|
620
|
+
aria-hidden="true"
|
|
621
|
+
onpointerdown={(e) => onResizePointerDown(e, p.ev, 'start')}
|
|
622
|
+
></span>
|
|
623
|
+
{/if}
|
|
624
|
+
{#if p.hasEnd}
|
|
625
|
+
<span
|
|
626
|
+
class="mb-ev-handle mb-ev-handle--end"
|
|
627
|
+
aria-hidden="true"
|
|
628
|
+
onpointerdown={(e) => onResizePointerDown(e, p.ev, 'end')}
|
|
629
|
+
></span>
|
|
630
|
+
{/if}
|
|
575
631
|
{/if}
|
|
576
632
|
</button>
|
|
577
633
|
{/each}
|
|
578
634
|
|
|
579
635
|
<!-- Drag-to-create ghost -->
|
|
580
636
|
{#if !readOnly && drag?.active && drag.mode === 'create' && drag.payload}
|
|
581
|
-
{@const
|
|
582
|
-
{@const
|
|
637
|
+
{@const gTopH = hourOf(drag.payload.start.getTime(), dayMs)}
|
|
638
|
+
{@const gTop = (gTopH - startHour) * HOUR_HEIGHT}
|
|
639
|
+
{@const gH = Math.max(12, (hourOf(drag.payload.end.getTime(), dayMs) - gTopH) * HOUR_HEIGHT)}
|
|
583
640
|
<div class="mb-create-ghost" style:top="{gTop}px" style:height="{gH}px" aria-hidden="true">
|
|
584
641
|
<span class="mb-create-ghost-time">
|
|
585
642
|
{fmtTime(drag.payload.start, locale)} – {fmtTime(drag.payload.end, locale)}
|
|
@@ -613,7 +670,7 @@ $effect(() => {
|
|
|
613
670
|
overflow: hidden;
|
|
614
671
|
background: var(--dt-bg, #fff);
|
|
615
672
|
-webkit-tap-highlight-color: transparent;
|
|
616
|
-
touch-action: pan-y;
|
|
673
|
+
touch-action: pan-y pinch-zoom;
|
|
617
674
|
}
|
|
618
675
|
.mb--auto { overflow: visible; }
|
|
619
676
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<script lang="ts">import { useCalendarContext } from "../shared/context.svelte.js";
|
|
9
9
|
import EventContent from "../shared/EventContent.svelte";
|
|
10
10
|
import { createClock } from "../../core/clock.svelte.js";
|
|
11
|
-
import {
|
|
11
|
+
import { sod, addDaysMs, isAllDay } from "../../core/time.js";
|
|
12
12
|
import { startOfWeek as sowFn } from "../../core/time.js";
|
|
13
13
|
import { fmtTime as _fmtTime, weekdayShort } from "../../core/locale.js";
|
|
14
14
|
import { createSwipe } from "./swipe.js";
|
|
@@ -35,8 +35,8 @@ const weekStart = $derived(customDays === 7 ? sowFn(focusMs, mondayStart) : sod(
|
|
|
35
35
|
// ── Load range ─────────────────────────────────────
|
|
36
36
|
$effect(() => {
|
|
37
37
|
if (!loadRangeCtx) return;
|
|
38
|
-
const rangeStart = new Date(weekStart -
|
|
39
|
-
const rangeEnd = new Date(weekStart
|
|
38
|
+
const rangeStart = new Date(addDaysMs(weekStart, -7));
|
|
39
|
+
const rangeEnd = new Date(addDaysMs(weekStart, customDays + 7));
|
|
40
40
|
loadRangeCtx.set({
|
|
41
41
|
start: rangeStart,
|
|
42
42
|
end: rangeEnd
|
|
@@ -47,7 +47,7 @@ const dayCells = $derived.by(() => {
|
|
|
47
47
|
const result = [];
|
|
48
48
|
const hideSet = new Set(hideDays ?? []);
|
|
49
49
|
for (let i = 0; i < customDays; i++) {
|
|
50
|
-
const ms = weekStart
|
|
50
|
+
const ms = addDaysMs(weekStart, i);
|
|
51
51
|
const d = new Date(ms);
|
|
52
52
|
const jsDay = d.getDay();
|
|
53
53
|
const isoDay = jsDay === 0 ? 7 : jsDay;
|
|
@@ -56,9 +56,9 @@ const dayCells = $derived.by(() => {
|
|
|
56
56
|
const isPast = equalDays ? false : ms < todayMs;
|
|
57
57
|
const isWeekend = jsDay === 0 || jsDay === 6;
|
|
58
58
|
const isDisabled = disabledSet.has(ms);
|
|
59
|
-
const dayEnd = ms
|
|
59
|
+
const dayEnd = addDaysMs(ms, 1);
|
|
60
60
|
const dayEvents = events.filter((ev) => ev.start.getTime() < dayEnd && ev.end.getTime() > ms).sort((a, b) => a.start.getTime() - b.start.getTime());
|
|
61
|
-
const allDayCount = dayEvents.filter(
|
|
61
|
+
const allDayCount = dayEvents.filter(inAllDayStrip).length;
|
|
62
62
|
result.push({
|
|
63
63
|
ms,
|
|
64
64
|
dayNum: d.getDate(),
|
|
@@ -86,10 +86,22 @@ function toggleExpand(ms) {
|
|
|
86
86
|
function fmtTime(d) {
|
|
87
87
|
return _fmtTime(d, locale);
|
|
88
88
|
}
|
|
89
|
+
/** All-day events and timed ones at least a (wall-clock) day long read as
|
|
90
|
+
* "All day"; a shorter overnight event keeps its time on both days. */
|
|
91
|
+
function inAllDayStrip(ev) {
|
|
92
|
+
return isAllDay(ev) || ev.end.getTime() >= addDaysMs(ev.start.getTime(), 1);
|
|
93
|
+
}
|
|
89
94
|
function evTimeLabel(ev) {
|
|
90
|
-
if (
|
|
95
|
+
if (inAllDayStrip(ev)) return L.allDay;
|
|
91
96
|
return `${fmtTime(ev.start)} – ${fmtTime(ev.end)}`;
|
|
92
97
|
}
|
|
98
|
+
/** Visible time on a day row: the start, or "until 01:00" on the day an
|
|
99
|
+
* overnight event runs into. */
|
|
100
|
+
function evRowTime(ev, dayMs) {
|
|
101
|
+
if (inAllDayStrip(ev)) return L.allDay;
|
|
102
|
+
if (ev.start.getTime() < dayMs) return `${L.until} ${fmtTime(ev.end)}`;
|
|
103
|
+
return fmtTime(ev.start);
|
|
104
|
+
}
|
|
93
105
|
function statusText(ev) {
|
|
94
106
|
if (ev.status === "cancelled") return ` (${L.cancelled})`;
|
|
95
107
|
if (ev.status === "tentative") return ` (${L.tentative})`;
|
|
@@ -98,6 +110,13 @@ function statusText(ev) {
|
|
|
98
110
|
return "";
|
|
99
111
|
}
|
|
100
112
|
// ── Touch swipe ────────────────────────────────────
|
|
113
|
+
let listEl = $state(null);
|
|
114
|
+
// A new week starts at its first day: the list's scroll offset belongs to
|
|
115
|
+
// the week that was showing (swipe, header nav, or a host date change).
|
|
116
|
+
$effect(() => {
|
|
117
|
+
void weekStart;
|
|
118
|
+
if (listEl) listEl.scrollTop = 0;
|
|
119
|
+
});
|
|
101
120
|
let swipeOffset = $state(0);
|
|
102
121
|
let swipeAnimate = $state(false);
|
|
103
122
|
const swipe = createSwipe({
|
|
@@ -155,6 +174,7 @@ function handleDayKeydown(e, dayMs) {
|
|
|
155
174
|
<!-- Vertical day list -->
|
|
156
175
|
<div
|
|
157
176
|
class="mw-list"
|
|
177
|
+
bind:this={listEl}
|
|
158
178
|
class:mw-list--animate={swipeAnimate}
|
|
159
179
|
style:transform={swipeOffset !== 0 ? `translateX(${swipeOffset}px)` : undefined}
|
|
160
180
|
role="list"
|
|
@@ -195,8 +215,8 @@ function handleDayKeydown(e, dayMs) {
|
|
|
195
215
|
type="button"
|
|
196
216
|
class="mw-ev"
|
|
197
217
|
class:mw-ev--selected={selectedEventId === ev.id}
|
|
198
|
-
class:mw-ev--allday={
|
|
199
|
-
class:mw-ev--current={!
|
|
218
|
+
class:mw-ev--allday={inAllDayStrip(ev)}
|
|
219
|
+
class:mw-ev--current={!inAllDayStrip(ev) && ev.start.getTime() <= clock.tick && ev.end.getTime() > clock.tick}
|
|
200
220
|
class:mw-ev--cancelled={ev.status === 'cancelled'}
|
|
201
221
|
class:mw-ev--tentative={ev.status === 'tentative'}
|
|
202
222
|
class:mw-ev--full={ev.status === 'full'}
|
|
@@ -211,11 +231,7 @@ function handleDayKeydown(e, dayMs) {
|
|
|
211
231
|
<div class="mw-ev-body">
|
|
212
232
|
<EventContent event={ev}>
|
|
213
233
|
<span class="mw-ev-title">{ev.title}</span>
|
|
214
|
-
{
|
|
215
|
-
<span class="mw-ev-time">{L.allDay}</span>
|
|
216
|
-
{:else}
|
|
217
|
-
<span class="mw-ev-time">{fmtTime(ev.start)}</span>
|
|
218
|
-
{/if}
|
|
234
|
+
<span class="mw-ev-time">{evRowTime(ev, cell.ms)}</span>
|
|
219
235
|
</EventContent>
|
|
220
236
|
</div>
|
|
221
237
|
</button>
|
|
@@ -251,7 +267,7 @@ function handleDayKeydown(e, dayMs) {
|
|
|
251
267
|
overflow: hidden;
|
|
252
268
|
background: var(--dt-bg, #fff);
|
|
253
269
|
-webkit-tap-highlight-color: transparent;
|
|
254
|
-
touch-action: pan-y;
|
|
270
|
+
touch-action: pan-y pinch-zoom;
|
|
255
271
|
}
|
|
256
272
|
.mw--auto { overflow: visible; }
|
|
257
273
|
|
|
@@ -22,6 +22,11 @@ export function createSwipe(cb) {
|
|
|
22
22
|
}
|
|
23
23
|
return {
|
|
24
24
|
ontouchstart(e) {
|
|
25
|
+
// A second finger (pinch-zoom) is never a swipe.
|
|
26
|
+
if (e.touches.length > 1) {
|
|
27
|
+
abandon();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
25
30
|
if (cb.disabled?.()) {
|
|
26
31
|
tracking = false;
|
|
27
32
|
return;
|
|
@@ -35,7 +40,7 @@ export function createSwipe(cb) {
|
|
|
35
40
|
ontouchmove(e) {
|
|
36
41
|
if (!tracking)
|
|
37
42
|
return;
|
|
38
|
-
if (cb.disabled?.()) {
|
|
43
|
+
if (e.touches.length > 1 || cb.disabled?.()) {
|
|
39
44
|
abandon();
|
|
40
45
|
return;
|
|
41
46
|
}
|