@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
|
@@ -19,8 +19,8 @@ import { useCalendarContext } from "../shared/context.svelte.js";
|
|
|
19
19
|
import EventContent from "../shared/EventContent.svelte";
|
|
20
20
|
import { createClock } from "../../core/clock.svelte.js";
|
|
21
21
|
import { fitParts } from "../../text-fit.js";
|
|
22
|
-
import {
|
|
23
|
-
import { startOfWeek as sowFn, isAllDay,
|
|
22
|
+
import { HOUR_MS, sod, addDaysMs, diffDays } from "../../core/time.js";
|
|
23
|
+
import { startOfWeek as sowFn, isAllDay, segmentForDay } from "../../core/time.js";
|
|
24
24
|
import { fmtH, fmtTime, fmtDuration, weekdayShort, weekdayLong } from "../../core/locale.js";
|
|
25
25
|
let { mode = "week", mondayStart = true, locale, height = 520, events = [], style = "", focusDate, oneventclick, oneventcreate, onexternaldrop, selectedEventId = null, readOnly = false, visibleHours } = $props();
|
|
26
26
|
// ── Context ────────────────────────────────────────
|
|
@@ -41,6 +41,42 @@ const autoHeight = $derived(ctx.autoHeight);
|
|
|
41
41
|
const oneventhover = $derived(ctx.oneventhover);
|
|
42
42
|
const disabledSet = $derived(ctx.disabledSet);
|
|
43
43
|
const SNAP_MS = $derived(ctx.snapInterval * 6e4);
|
|
44
|
+
// ── Wall-clock time axis ───────────────────────────
|
|
45
|
+
// The grid is 24 wall-clock rows per day. A DST day is 23 or 25 hours
|
|
46
|
+
// long, so `dayMs + h * HOUR_MS` lands an hour off after the change:
|
|
47
|
+
// every hour ↔ instant conversion goes through the local calendar instead.
|
|
48
|
+
/** Fractional wall-clock hour `hour` on the day starting at `dayMs` → epoch ms. */
|
|
49
|
+
function atHour(dayMs, hour) {
|
|
50
|
+
// MakeTime sums the fields in local time before converting, so the
|
|
51
|
+
// milliseconds may exceed a day (24 → next midnight) or be negative.
|
|
52
|
+
return new Date(dayMs).setHours(0, 0, 0, Math.round(hour * HOUR_MS));
|
|
53
|
+
}
|
|
54
|
+
/** Epoch ms → fractional wall-clock hour counted from the day at `dayMs`
|
|
55
|
+
* (24 = the next midnight; calendar days, not 24 h steps). */
|
|
56
|
+
function hourOf(ms, dayMs) {
|
|
57
|
+
const d = new Date(ms);
|
|
58
|
+
return diffDays(ms, dayMs) * 24 + d.getHours() + d.getMinutes() / 60 + d.getSeconds() / 3600 + d.getMilliseconds() / HOUR_MS;
|
|
59
|
+
}
|
|
60
|
+
/** Snap a fractional hour to the snap interval, in wall-clock minutes of
|
|
61
|
+
* the local day (not UTC multiples — those are off in +05:30 zones). */
|
|
62
|
+
function snapHour(hour, how) {
|
|
63
|
+
const step = Math.max(1, ctx.snapInterval);
|
|
64
|
+
// Epsilon: 10:15 must not floor to 10:00 through float noise.
|
|
65
|
+
const units = hour * 60 / step;
|
|
66
|
+
const n = how === "floor" ? Math.floor(units + 1e-9) : Math.round(units);
|
|
67
|
+
return n * step / 60;
|
|
68
|
+
}
|
|
69
|
+
/** Snap an instant, relative to its own local day. */
|
|
70
|
+
function snapMs(ms, how) {
|
|
71
|
+
const day = sod(ms);
|
|
72
|
+
return atHour(day, snapHour(hourOf(ms, day), how));
|
|
73
|
+
}
|
|
74
|
+
/** Timed events at least a (wall-clock) day long, and all-day ones, sit in
|
|
75
|
+
* the all-day strip; a shorter overnight event is drawn as a clamped
|
|
76
|
+
* segment on each day it touches. */
|
|
77
|
+
function inAllDayStrip(ev) {
|
|
78
|
+
return isAllDay(ev) || ev.end.getTime() >= addDaysMs(ev.start.getTime(), 1);
|
|
79
|
+
}
|
|
44
80
|
// ── Config ─────────────────────────────────────────
|
|
45
81
|
const HOUR_H = 48;
|
|
46
82
|
const GUTTER_W = 48;
|
|
@@ -97,8 +133,8 @@ const innerMinWidth = $derived(GUTTER_W + dayCols.length * MIN_COL_W);
|
|
|
97
133
|
// ── Load range: visible week ±7 days ───────────────
|
|
98
134
|
$effect(() => {
|
|
99
135
|
if (!loadRangeCtx) return;
|
|
100
|
-
const rangeStart = new Date(weekStartMs -
|
|
101
|
-
const rangeEnd = new Date(weekEndMs
|
|
136
|
+
const rangeStart = new Date(addDaysMs(weekStartMs, -7));
|
|
137
|
+
const rangeEnd = new Date(addDaysMs(weekEndMs, 7));
|
|
102
138
|
loadRangeCtx.set({
|
|
103
139
|
start: rangeStart,
|
|
104
140
|
end: rangeEnd
|
|
@@ -111,7 +147,7 @@ const allDayByDay = $derived.by(() => {
|
|
|
111
147
|
for (const day of dayCols) {
|
|
112
148
|
const segs = [];
|
|
113
149
|
for (const ev of events) {
|
|
114
|
-
if (!
|
|
150
|
+
if (!inAllDayStrip(ev)) continue;
|
|
115
151
|
const seg = segmentForDay(ev, day.ms);
|
|
116
152
|
if (seg) segs.push(seg);
|
|
117
153
|
}
|
|
@@ -131,12 +167,12 @@ const layoutByDay = $derived.by(() => {
|
|
|
131
167
|
const rsP = drag?.active && (drag.mode === "resize-start" || drag.mode === "resize-end") ? drag.payload : null;
|
|
132
168
|
const map = new Map();
|
|
133
169
|
for (const day of dayCols) {
|
|
134
|
-
const dayEnd = day.ms
|
|
135
|
-
const bandStart = day.ms
|
|
136
|
-
const bandEnd = day.ms
|
|
170
|
+
const dayEnd = addDaysMs(day.ms, 1);
|
|
171
|
+
const bandStart = atHour(day.ms, startHour);
|
|
172
|
+
const bandEnd = atHour(day.ms, endHour);
|
|
137
173
|
const infos = [];
|
|
138
174
|
for (const ev of events) {
|
|
139
|
-
if (
|
|
175
|
+
if (inAllDayStrip(ev)) continue;
|
|
140
176
|
// The event being moved stays laned at its ORIGINAL slot, dimmed:
|
|
141
177
|
// the ghost shows where it is going, the faded block where it came
|
|
142
178
|
// from, and neither the drop target nor its neighbours re-lane
|
|
@@ -144,15 +180,44 @@ const layoutByDay = $derived.by(() => {
|
|
|
144
180
|
const isResizing = rsP?.eventId === ev.id;
|
|
145
181
|
const s0 = isResizing ? rsP.start.getTime() : ev.start.getTime();
|
|
146
182
|
const e0 = isResizing ? rsP.end.getTime() : ev.end.getTime();
|
|
183
|
+
// Inverted events are bad data; skip them.
|
|
184
|
+
if (e0 < s0) continue;
|
|
185
|
+
const hasStart = s0 >= day.ms && s0 < dayEnd;
|
|
186
|
+
const hasEnd = e0 > day.ms && e0 <= dayEnd;
|
|
187
|
+
if (e0 === s0) {
|
|
188
|
+
// Zero-length (a deadline, a marker): drawn at its start with the
|
|
189
|
+
// minimum block height, when that start is inside the band.
|
|
190
|
+
if (!hasStart || s0 < bandStart || s0 >= bandEnd) continue;
|
|
191
|
+
const laneEnd = Math.min(bandEnd, s0 + HOUR_MS / 2);
|
|
192
|
+
infos.push({
|
|
193
|
+
ev,
|
|
194
|
+
startMs: s0,
|
|
195
|
+
endMs: laneEnd,
|
|
196
|
+
drawEndMs: s0,
|
|
197
|
+
hasStart,
|
|
198
|
+
hasEnd: true,
|
|
199
|
+
isResizing,
|
|
200
|
+
isMoving: ev.id === movingId,
|
|
201
|
+
col: 0,
|
|
202
|
+
totalCols: 1
|
|
203
|
+
});
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
147
206
|
if (s0 >= dayEnd || e0 <= day.ms) continue;
|
|
148
207
|
const sMs = Math.max(s0, bandStart);
|
|
149
208
|
const eMs = Math.min(e0, bandEnd);
|
|
150
209
|
// Entirely outside the visible hour band — skip, don't paint a sliver
|
|
151
210
|
if (eMs <= sMs) continue;
|
|
211
|
+
// A grip only where the event really starts / ends: an edge cut by the
|
|
212
|
+
// visible band (a 21:00–22:30 class in a 6–22 grid) is not its end,
|
|
213
|
+
// and dragging it would rewrite the class's length to the band.
|
|
152
214
|
infos.push({
|
|
153
215
|
ev,
|
|
154
216
|
startMs: sMs,
|
|
155
217
|
endMs: eMs,
|
|
218
|
+
drawEndMs: eMs,
|
|
219
|
+
hasStart: hasStart && s0 >= bandStart,
|
|
220
|
+
hasEnd: hasEnd && e0 <= bandEnd,
|
|
156
221
|
isResizing,
|
|
157
222
|
isMoving: ev.id === movingId,
|
|
158
223
|
col: 0,
|
|
@@ -201,12 +266,14 @@ const layoutByDay = $derived.by(() => {
|
|
|
201
266
|
}
|
|
202
267
|
map.set(day.ms, infos.map((inf) => ({
|
|
203
268
|
ev: inf.ev,
|
|
204
|
-
top: ((inf.startMs
|
|
205
|
-
height: Math.max(24, (inf.
|
|
269
|
+
top: (hourOf(inf.startMs, day.ms) - startHour) * HOUR_H,
|
|
270
|
+
height: Math.max(24, (hourOf(inf.drawEndMs, day.ms) - hourOf(inf.startMs, day.ms)) * HOUR_H),
|
|
206
271
|
col: inf.col,
|
|
207
272
|
totalCols: inf.totalCols,
|
|
208
273
|
isResizing: inf.isResizing,
|
|
209
|
-
isMoving: inf.isMoving
|
|
274
|
+
isMoving: inf.isMoving,
|
|
275
|
+
hasStart: inf.hasStart,
|
|
276
|
+
hasEnd: inf.hasEnd
|
|
210
277
|
})));
|
|
211
278
|
}
|
|
212
279
|
return map;
|
|
@@ -220,7 +287,7 @@ const nowIds = $derived.by(() => {
|
|
|
220
287
|
}
|
|
221
288
|
return s;
|
|
222
289
|
});
|
|
223
|
-
const nowFracHour = $derived((clock.tick
|
|
290
|
+
const nowFracHour = $derived(hourOf(clock.tick, clock.today));
|
|
224
291
|
/** Y offset of the now-line, or null when outside visibleHours */
|
|
225
292
|
const nowY = $derived.by(() => {
|
|
226
293
|
if (nowFracHour < startHour || nowFracHour > endHour) return null;
|
|
@@ -291,13 +358,18 @@ function cancelFrame() {
|
|
|
291
358
|
function colsRect() {
|
|
292
359
|
return rectCache ?? colsEl.getBoundingClientRect();
|
|
293
360
|
}
|
|
294
|
-
/** Pointer X → index into dayCols (clamped)
|
|
361
|
+
/** Pointer X → index into dayCols (clamped). Under `dir="rtl"` the flex
|
|
362
|
+
* row runs right-to-left, so the first day is the rightmost column. */
|
|
295
363
|
function pointerDayIndex(clientX) {
|
|
296
364
|
const r = colsRect();
|
|
297
365
|
const n = dayCols.length;
|
|
298
366
|
if (n === 0) return 0;
|
|
299
367
|
const w = r.width / n;
|
|
300
|
-
|
|
368
|
+
const i = Math.max(0, Math.min(n - 1, Math.floor((clientX - r.left) / w)));
|
|
369
|
+
return isRtl() ? n - 1 - i : i;
|
|
370
|
+
}
|
|
371
|
+
function isRtl() {
|
|
372
|
+
return !!colsEl && getComputedStyle(colsEl).direction === "rtl";
|
|
301
373
|
}
|
|
302
374
|
/** Pointer Y → fractional hour (unclamped) */
|
|
303
375
|
function pointerHour(clientY) {
|
|
@@ -307,11 +379,11 @@ function pointerHour(clientY) {
|
|
|
307
379
|
function pointerTimeMs(clientX, clientY) {
|
|
308
380
|
const dayMs = dayCols[pointerDayIndex(clientX)]?.ms ?? weekStartMs;
|
|
309
381
|
const hour = Math.min(Math.max(pointerHour(clientY), startHour), endHour);
|
|
310
|
-
return dayMs
|
|
382
|
+
return atHour(dayMs, hour);
|
|
311
383
|
}
|
|
312
384
|
/** Clamp a timestamp into the visible band of a specific day */
|
|
313
385
|
function clampToDayBand(ms, dayMs) {
|
|
314
|
-
return Math.max(dayMs
|
|
386
|
+
return Math.max(atHour(dayMs, startHour), Math.min(atHour(dayMs, endHour), ms));
|
|
315
387
|
}
|
|
316
388
|
// ── Blocked slot helpers ───────────────────────────
|
|
317
389
|
function isBlockedAt(dayMs, hour) {
|
|
@@ -324,7 +396,7 @@ function isBlockedAt(dayMs, hour) {
|
|
|
324
396
|
});
|
|
325
397
|
}
|
|
326
398
|
function blockedRangeLabel(dayMs, slotStart, slotEnd) {
|
|
327
|
-
return `${fmtTime(new Date(dayMs
|
|
399
|
+
return `${fmtTime(new Date(atHour(dayMs, slotStart)), locale)} – ${fmtTime(new Date(atHour(dayMs, slotEnd)), locale)}`;
|
|
328
400
|
}
|
|
329
401
|
// ── Status label (aria) ────────────────────────────
|
|
330
402
|
// ─── Block parts ────────────────────────────────
|
|
@@ -374,14 +446,16 @@ function ghostForDay(dayMs) {
|
|
|
374
446
|
if (mode !== "move" && mode !== "create") return null;
|
|
375
447
|
const s = drag.payload.start.getTime();
|
|
376
448
|
const e = drag.payload.end.getTime();
|
|
377
|
-
const bandS = dayMs
|
|
378
|
-
const bandE = dayMs
|
|
449
|
+
const bandS = atHour(dayMs, startHour);
|
|
450
|
+
const bandE = atHour(dayMs, endHour);
|
|
379
451
|
const cs = Math.max(s, bandS);
|
|
380
452
|
const ce = Math.min(e, bandE);
|
|
381
|
-
|
|
453
|
+
// A zero-length event (moved marker) shows at its start, like its block
|
|
454
|
+
const zero = e === s;
|
|
455
|
+
if (zero ? s < bandS || s >= bandE : ce <= cs) return null;
|
|
382
456
|
return {
|
|
383
|
-
top: ((cs
|
|
384
|
-
height: Math.max(12, (ce - cs)
|
|
457
|
+
top: (hourOf(cs, dayMs) - startHour) * HOUR_H,
|
|
458
|
+
height: Math.max(zero ? 24 : 12, (hourOf(ce, dayMs) - hourOf(cs, dayMs)) * HOUR_H),
|
|
385
459
|
start: drag.payload.start,
|
|
386
460
|
end: drag.payload.end,
|
|
387
461
|
create: mode === "create",
|
|
@@ -393,7 +467,7 @@ function ghostForDay(dayMs) {
|
|
|
393
467
|
const axisGhost = $derived.by(() => {
|
|
394
468
|
if (!drag?.active || !drag.payload) return null;
|
|
395
469
|
const s = drag.payload.start.getTime();
|
|
396
|
-
const day = dayCols.find((d) => s >= d.ms && s < d.ms
|
|
470
|
+
const day = dayCols.find((d) => s >= d.ms && s < addDaysMs(d.ms, 1));
|
|
397
471
|
return day ? ghostForDay(day.ms) : null;
|
|
398
472
|
});
|
|
399
473
|
// ── Drag-to-create (mouse: sweep; touch: long-press) ──
|
|
@@ -407,6 +481,8 @@ let suppressColsClick = false;
|
|
|
407
481
|
let crStartX = 0;
|
|
408
482
|
let crStartY = 0;
|
|
409
483
|
let crAnchorMs = 0;
|
|
484
|
+
/** The press's wall-clock hour (clamped into the band) — snapped on start. */
|
|
485
|
+
let crAnchorHour = 0;
|
|
410
486
|
let crDayMs = 0;
|
|
411
487
|
let crStarted = false;
|
|
412
488
|
let longPressTimer = null;
|
|
@@ -430,7 +506,7 @@ function clearLongPress() {
|
|
|
430
506
|
function startColsCreate() {
|
|
431
507
|
if (!drag) return;
|
|
432
508
|
crStarted = true;
|
|
433
|
-
crAnchorMs = clampToDayBand(
|
|
509
|
+
crAnchorMs = clampToDayBand(atHour(crDayMs, snapHour(crAnchorHour, "floor")), crDayMs);
|
|
434
510
|
drag.beginCreate(new Date(crAnchorMs), new Date(crAnchorMs + SNAP_MS));
|
|
435
511
|
addTouchScrollBlock();
|
|
436
512
|
}
|
|
@@ -447,8 +523,8 @@ function onColsDrop(e) {
|
|
|
447
523
|
const day = dayCols[pointerDayIndex(e.clientX)];
|
|
448
524
|
if (!day || day.isDisabled) return;
|
|
449
525
|
e.preventDefault();
|
|
450
|
-
const
|
|
451
|
-
const snapped = day.ms
|
|
526
|
+
const hour = Math.min(Math.max(pointerHour(e.clientY), startHour), endHour);
|
|
527
|
+
const snapped = clampToDayBand(atHour(day.ms, snapHour(hour, "floor")), day.ms);
|
|
452
528
|
onexternaldrop({
|
|
453
529
|
start: new Date(snapped),
|
|
454
530
|
dataTransfer: e.dataTransfer
|
|
@@ -464,7 +540,8 @@ function onColsPointerDown(e) {
|
|
|
464
540
|
crStartX = e.clientX;
|
|
465
541
|
crStartY = e.clientY;
|
|
466
542
|
crDayMs = day.ms;
|
|
467
|
-
|
|
543
|
+
crAnchorHour = Math.min(Math.max(pointerHour(e.clientY), startHour), endHour);
|
|
544
|
+
crAnchorMs = atHour(day.ms, crAnchorHour);
|
|
468
545
|
crStarted = false;
|
|
469
546
|
if (e.pointerType === "touch") {
|
|
470
547
|
longPressTimer = setTimeout(() => {
|
|
@@ -489,8 +566,7 @@ const onColsCreateMove = perFrame((e) => {
|
|
|
489
566
|
if (Math.abs(e.clientY - crStartY) < CREATE_THRESHOLD) return;
|
|
490
567
|
startColsCreate();
|
|
491
568
|
}
|
|
492
|
-
const
|
|
493
|
-
const snapped = clampToDayBand(Math.round(raw / SNAP_MS) * SNAP_MS, crDayMs);
|
|
569
|
+
const snapped = clampToDayBand(atHour(crDayMs, snapHour(pointerHour(e.clientY), "round")), crDayMs);
|
|
494
570
|
drag.updatePointer(new Date(Math.min(crAnchorMs, snapped)), new Date(Math.max(crAnchorMs + SNAP_MS, snapped)));
|
|
495
571
|
});
|
|
496
572
|
function cleanupColsCreate() {
|
|
@@ -538,8 +614,10 @@ function handleColsClick(e) {
|
|
|
538
614
|
if (!day || day.isDisabled) return;
|
|
539
615
|
const hour = Math.min(Math.max(pointerHour(e.clientY), startHour), endHour);
|
|
540
616
|
if (isBlockedAt(day.ms, hour)) return;
|
|
541
|
-
const startMs = clampToDayBand(
|
|
542
|
-
|
|
617
|
+
const startMs = clampToDayBand(atHour(day.ms, snapHour(hour, "floor")), day.ms);
|
|
618
|
+
// An hour, or the minimum duration when that is longer. Blocked-slot and
|
|
619
|
+
// max-duration validation is Calendar's (ctx.oneventcreate), not ours.
|
|
620
|
+
const durMin = Math.max(60, minDuration ?? 0);
|
|
543
621
|
oneventcreate({
|
|
544
622
|
start: new Date(startMs),
|
|
545
623
|
end: new Date(startMs + durMin * 6e4)
|
|
@@ -583,7 +661,7 @@ const onEvMove = perFrame((e) => {
|
|
|
583
661
|
}
|
|
584
662
|
const duration = ev.end.getTime() - ev.start.getTime();
|
|
585
663
|
const raw = pointerTimeMs(e.clientX, e.clientY) - evGrabOffsetMs;
|
|
586
|
-
const snapped =
|
|
664
|
+
const snapped = snapMs(raw, "round");
|
|
587
665
|
drag.updatePointer(new Date(snapped), new Date(snapped + duration));
|
|
588
666
|
});
|
|
589
667
|
function cleanupEvDrag() {
|
|
@@ -619,7 +697,10 @@ let rsStartY = 0;
|
|
|
619
697
|
let rsStarted = false;
|
|
620
698
|
let rsEdge = "end";
|
|
621
699
|
let rsEvent = null;
|
|
622
|
-
|
|
700
|
+
/** The day column whose segment holds the grabbed grip — an overnight
|
|
701
|
+
* event's end grip sits on the next day. */
|
|
702
|
+
let rsDayMs = 0;
|
|
703
|
+
function onResizePointerDown(e, ev, edge, dayMs) {
|
|
623
704
|
if (e.button !== 0 || !drag || readOnly || ev.data?.readOnly) return;
|
|
624
705
|
e.stopPropagation();
|
|
625
706
|
evAnchor = e.currentTarget.parentElement?.getBoundingClientRect();
|
|
@@ -627,6 +708,7 @@ function onResizePointerDown(e, ev, edge) {
|
|
|
627
708
|
rsStarted = false;
|
|
628
709
|
rsEdge = edge;
|
|
629
710
|
rsEvent = ev;
|
|
711
|
+
rsDayMs = dayMs;
|
|
630
712
|
window.addEventListener("pointermove", onResizeMove);
|
|
631
713
|
window.addEventListener("pointerup", onResizeUp, { once: true });
|
|
632
714
|
window.addEventListener("pointercancel", onResizeCancel, { once: true });
|
|
@@ -640,9 +722,7 @@ const onResizeMove = perFrame((e) => {
|
|
|
640
722
|
drag.beginResize(ev.id, rsEdge, ev.start, ev.end);
|
|
641
723
|
addTouchScrollBlock();
|
|
642
724
|
}
|
|
643
|
-
const
|
|
644
|
-
const raw = evDayMs + pointerHour(e.clientY) * HOUR_MS;
|
|
645
|
-
const snapped = clampToDayBand(Math.round(raw / SNAP_MS) * SNAP_MS, evDayMs);
|
|
725
|
+
const snapped = clampToDayBand(atHour(rsDayMs, snapHour(pointerHour(e.clientY), "round")), rsDayMs);
|
|
646
726
|
if (rsEdge === "end") {
|
|
647
727
|
const end = Math.max(snapped, ev.start.getTime() + SNAP_MS);
|
|
648
728
|
drag.updatePointer(ev.start, new Date(end));
|
|
@@ -679,6 +759,18 @@ function onResizeCancel() {
|
|
|
679
759
|
if (drag && rsStarted) drag.cancel();
|
|
680
760
|
cleanupResize();
|
|
681
761
|
}
|
|
762
|
+
// ── Unmount mid-gesture ────────────────────────────
|
|
763
|
+
// A view switch (or the host unmounting the calendar) can land between
|
|
764
|
+
// pointerdown and pointerup: tear down the window listeners, the
|
|
765
|
+
// long-press timer, the pending frame and the touch-scroll blocker, and
|
|
766
|
+
// drop the half-made drag rather than leave it live in the shared context.
|
|
767
|
+
$effect(() => () => {
|
|
768
|
+
const live = crStarted || evDragStarted || rsStarted;
|
|
769
|
+
cleanupColsCreate();
|
|
770
|
+
cleanupEvDrag();
|
|
771
|
+
cleanupResize();
|
|
772
|
+
if (live && drag?.active) drag.cancel();
|
|
773
|
+
});
|
|
682
774
|
// ── Escape cancels any in-flight drag ──────────────
|
|
683
775
|
function onWindowKeydown(e) {
|
|
684
776
|
if (e.key !== "Escape" || !drag?.active) return;
|
|
@@ -852,7 +944,7 @@ function onWindowKeydown(e) {
|
|
|
852
944
|
style:top="{(s - startHour) * HOUR_H}px"
|
|
853
945
|
style:height="{(e - s) * HOUR_H}px"
|
|
854
946
|
title="{slot.label ? `${slot.label}, ` : ''}{range}"
|
|
855
|
-
aria-label="{slot.label ||
|
|
947
|
+
aria-label="{slot.label || L.unavailable}, {range}"
|
|
856
948
|
>
|
|
857
949
|
{#if slot.label}
|
|
858
950
|
<span class="tw-blocked-lb">{slot.label}</span>
|
|
@@ -909,16 +1001,20 @@ function onWindowKeydown(e) {
|
|
|
909
1001
|
<span class="tw-ev-live" aria-hidden="true"></span>
|
|
910
1002
|
{/if}
|
|
911
1003
|
{#if !readOnly && !p.ev.data?.readOnly}
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
1004
|
+
{#if p.hasStart}
|
|
1005
|
+
<span
|
|
1006
|
+
class="tw-ev-handle tw-ev-handle--start"
|
|
1007
|
+
aria-hidden="true"
|
|
1008
|
+
onpointerdown={(e) => onResizePointerDown(e, p.ev, 'start', day.ms)}
|
|
1009
|
+
></span>
|
|
1010
|
+
{/if}
|
|
1011
|
+
{#if p.hasEnd}
|
|
1012
|
+
<span
|
|
1013
|
+
class="tw-ev-handle tw-ev-handle--end"
|
|
1014
|
+
aria-hidden="true"
|
|
1015
|
+
onpointerdown={(e) => onResizePointerDown(e, p.ev, 'end', day.ms)}
|
|
1016
|
+
></span>
|
|
1017
|
+
{/if}
|
|
922
1018
|
{/if}
|
|
923
1019
|
</div>
|
|
924
1020
|
{/each}
|
|
@@ -987,7 +1083,11 @@ function onWindowKeydown(e) {
|
|
|
987
1083
|
min-height: 0;
|
|
988
1084
|
overflow-y: auto;
|
|
989
1085
|
overflow-x: auto;
|
|
990
|
-
|
|
1086
|
+
/* Horizontal stays contained (a sideways swipe must not trigger the
|
|
1087
|
+
browser's back navigation); vertical chains to the page, so a wheel
|
|
1088
|
+
over a page-embedded calendar scrolls on once the grid ends. */
|
|
1089
|
+
overscroll-behavior-x: contain;
|
|
1090
|
+
overscroll-behavior-y: auto;
|
|
991
1091
|
scrollbar-width: thin;
|
|
992
1092
|
scrollbar-color: var(--dt-scrollbar, rgba(0, 0, 0, 0.1)) transparent;
|
|
993
1093
|
}
|
|
@@ -1061,7 +1161,7 @@ function onWindowKeydown(e) {
|
|
|
1061
1161
|
}
|
|
1062
1162
|
.tw-hd-num--today {
|
|
1063
1163
|
background: var(--dt-accent, #2563eb);
|
|
1064
|
-
color: var(--dt-accent-fg, #ffffff);
|
|
1164
|
+
color: var(--dt-accent-fg, var(--dt-btn-text, #ffffff));
|
|
1065
1165
|
font-weight: 700;
|
|
1066
1166
|
}
|
|
1067
1167
|
|
|
@@ -6,7 +6,7 @@ import type { ViewState } from '../../engine/view-state.svelte.js';
|
|
|
6
6
|
export interface CalendarContext {
|
|
7
7
|
readonly viewState: ViewState | undefined;
|
|
8
8
|
readonly drag: DragState | undefined;
|
|
9
|
-
readonly commitDrag: (() => void) | undefined;
|
|
9
|
+
readonly commitDrag: (() => Promise<void>) | undefined;
|
|
10
10
|
readonly snapInterval: number;
|
|
11
11
|
readonly minColumnWidth: number;
|
|
12
12
|
readonly equalDays: boolean;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nomideusz/svelte-calendar",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "A themeable Svelte 5 calendar
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A themeable Svelte 5 calendar: day, week, scrolling-week and month views, agenda lists, touch-first mobile layouts, drag-and-drop, timezones, a headless API and an embeddable web component.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
7
7
|
"./widget/widget.js"
|
|
@@ -15,13 +15,18 @@
|
|
|
15
15
|
"svelte": "./dist/index.js",
|
|
16
16
|
"default": "./dist/index.js"
|
|
17
17
|
},
|
|
18
|
-
"./widget":
|
|
18
|
+
"./widget": {
|
|
19
|
+
"types": "./widget/widget.d.ts",
|
|
20
|
+
"default": "./widget/widget.js"
|
|
21
|
+
},
|
|
22
|
+
"./package.json": "./package.json"
|
|
19
23
|
},
|
|
20
24
|
"files": [
|
|
21
25
|
"dist",
|
|
22
26
|
"widget",
|
|
23
27
|
"!dist/**/*.test.*",
|
|
24
|
-
"!dist/**/*.spec.*"
|
|
28
|
+
"!dist/**/*.spec.*",
|
|
29
|
+
"!dist/widget"
|
|
25
30
|
],
|
|
26
31
|
"imports": {
|
|
27
32
|
"#lib": "./src/lib",
|
|
@@ -33,7 +38,7 @@
|
|
|
33
38
|
"package": "svelte-kit sync && svelte-package",
|
|
34
39
|
"package:watch": "svelte-kit sync && svelte-package --watch",
|
|
35
40
|
"build:widget": "vite build --config vite.config.widget.ts",
|
|
36
|
-
"prepublishOnly": "npm run package",
|
|
41
|
+
"prepublishOnly": "npm run package && npm run build:widget",
|
|
37
42
|
"preview": "vite preview",
|
|
38
43
|
"prepare": "svelte-kit sync && svelte-package",
|
|
39
44
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
@@ -42,7 +47,7 @@
|
|
|
42
47
|
"test:watch": "vitest"
|
|
43
48
|
},
|
|
44
49
|
"peerDependencies": {
|
|
45
|
-
"svelte": "^5.
|
|
50
|
+
"svelte": "^5.29.0"
|
|
46
51
|
},
|
|
47
52
|
"devDependencies": {
|
|
48
53
|
"@sveltejs/adapter-auto": "8.0.0-next.4",
|
|
@@ -67,17 +72,27 @@
|
|
|
67
72
|
},
|
|
68
73
|
"keywords": [
|
|
69
74
|
"svelte",
|
|
75
|
+
"svelte5",
|
|
70
76
|
"calendar",
|
|
77
|
+
"event-calendar",
|
|
71
78
|
"scheduler",
|
|
72
79
|
"timeline",
|
|
73
|
-
"
|
|
80
|
+
"agenda",
|
|
74
81
|
"day",
|
|
82
|
+
"week",
|
|
83
|
+
"month",
|
|
75
84
|
"booking",
|
|
76
|
-
"
|
|
85
|
+
"drag-and-drop",
|
|
86
|
+
"timezone",
|
|
87
|
+
"headless",
|
|
88
|
+
"web-component"
|
|
77
89
|
],
|
|
78
90
|
"repository": {
|
|
79
91
|
"type": "git",
|
|
80
|
-
"url": "https://github.com/nomideusz/svelte-calendar"
|
|
92
|
+
"url": "git+https://github.com/nomideusz/svelte-calendar.git"
|
|
93
|
+
},
|
|
94
|
+
"bugs": {
|
|
95
|
+
"url": "https://github.com/nomideusz/svelte-calendar/issues"
|
|
81
96
|
},
|
|
82
97
|
"homepage": "https://svelte-calendar.xyz/"
|
|
83
98
|
}
|