@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.
Files changed (69) hide show
  1. package/README.md +502 -267
  2. package/dist/adapters/composite.d.ts +12 -1
  3. package/dist/adapters/composite.js +109 -47
  4. package/dist/adapters/errors.d.ts +22 -0
  5. package/dist/adapters/errors.js +32 -0
  6. package/dist/adapters/index.d.ts +1 -0
  7. package/dist/adapters/index.js +1 -0
  8. package/dist/adapters/jmap.js +26 -17
  9. package/dist/adapters/mapped.d.ts +2 -2
  10. package/dist/adapters/mapped.js +18 -9
  11. package/dist/adapters/memory.d.ts +1 -1
  12. package/dist/adapters/memory.js +5 -3
  13. package/dist/adapters/recurring.d.ts +1 -1
  14. package/dist/adapters/recurring.js +43 -25
  15. package/dist/adapters/rest.d.ts +2 -2
  16. package/dist/adapters/rest.js +9 -4
  17. package/dist/calendar/Calendar.svelte +122 -78
  18. package/dist/calendar/Calendar.svelte.d.ts +69 -14
  19. package/dist/calendar/index.d.ts +2 -2
  20. package/dist/calendar/index.js +1 -1
  21. package/dist/core/clock.svelte.js +3 -2
  22. package/dist/core/locale.d.ts +11 -1
  23. package/dist/core/locale.js +16 -16
  24. package/dist/core/time.d.ts +9 -0
  25. package/dist/core/time.js +12 -5
  26. package/dist/core/timezone.d.ts +7 -3
  27. package/dist/core/timezone.js +10 -4
  28. package/dist/engine/event-store.svelte.js +94 -57
  29. package/dist/engine/view-state.svelte.d.ts +5 -1
  30. package/dist/engine/view-state.svelte.js +8 -6
  31. package/dist/headless/create-agenda.svelte.d.ts +8 -1
  32. package/dist/headless/create-agenda.svelte.js +36 -18
  33. package/dist/headless/create-calendar.svelte.js +57 -21
  34. package/dist/headless/create-range-agenda.svelte.d.ts +7 -0
  35. package/dist/headless/create-range-agenda.svelte.js +38 -17
  36. package/dist/headless/index.d.ts +1 -0
  37. package/dist/index.d.ts +10 -7
  38. package/dist/index.js +8 -5
  39. package/dist/primitives/DayHeader.svelte +3 -2
  40. package/dist/primitives/DayHeader.svelte.d.ts +1 -1
  41. package/dist/primitives/EmptySlot.svelte +5 -2
  42. package/dist/primitives/EventBlock.svelte +21 -14
  43. package/dist/primitives/FloatingPanel.svelte +110 -15
  44. package/dist/primitives/FloatingPanel.svelte.d.ts +7 -5
  45. package/dist/primitives/NowIndicator.svelte +11 -4
  46. package/dist/primitives/TimeGutter.svelte +10 -6
  47. package/dist/primitives/index.d.ts +1 -0
  48. package/dist/text-fit.d.ts +8 -2
  49. package/dist/views/agenda/AgendaDay.svelte +43 -35
  50. package/dist/views/agenda/AgendaWeek.svelte +75 -38
  51. package/dist/views/index.d.ts +2 -0
  52. package/dist/views/index.js +2 -0
  53. package/dist/views/mobile/MobileDay.svelte +99 -42
  54. package/dist/views/mobile/MobileWeek.svelte +31 -15
  55. package/dist/views/mobile/swipe.js +6 -1
  56. package/dist/views/month/MonthGrid.svelte +33 -21
  57. package/dist/views/month/MonthGrid.svelte.d.ts +1 -1
  58. package/dist/views/planner/PlannerScroll.svelte +438 -164
  59. package/dist/views/planner/PlannerWeek.svelte +150 -50
  60. package/dist/views/shared/context.svelte.d.ts +1 -1
  61. package/package.json +24 -9
  62. package/widget/widget.js +10000 -9520
  63. package/dist/assets/favicon.svg +0 -9
  64. package/dist/widget/CalendarWidget.svelte +0 -114
  65. package/dist/widget/CalendarWidget.svelte.d.ts +0 -37
  66. package/dist/widget/index.d.ts +0 -1
  67. package/dist/widget/index.js +0 -1
  68. package/dist/widget/widget.d.ts +0 -10
  69. 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 { DAY_MS, HOUR_MS, sod, addDaysMs } from "../../core/time.js";
23
- import { startOfWeek as sowFn, isAllDay, isMultiDay, segmentForDay } from "../../core/time.js";
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 - 7 * DAY_MS);
101
- const rangeEnd = new Date(weekEndMs + 7 * DAY_MS);
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 (!isAllDay(ev) && !isMultiDay(ev)) continue;
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 + DAY_MS;
135
- const bandStart = day.ms + startHour * HOUR_MS;
136
- const bandEnd = day.ms + endHour * HOUR_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 (isAllDay(ev) || isMultiDay(ev)) continue;
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 - day.ms) / HOUR_MS - startHour) * HOUR_H,
205
- height: Math.max(24, (inf.endMs - inf.startMs) / HOUR_MS * HOUR_H),
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 - clock.today) / HOUR_MS);
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
- return Math.max(0, Math.min(n - 1, Math.floor((clientX - r.left) / w)));
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 + hour * HOUR_MS;
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 + startHour * HOUR_MS, Math.min(dayMs + endHour * HOUR_MS, ms));
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 + slotStart * HOUR_MS), locale)} – ${fmtTime(new Date(dayMs + slotEnd * HOUR_MS), locale)}`;
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 + startHour * HOUR_MS;
378
- const bandE = dayMs + endHour * HOUR_MS;
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
- if (ce <= cs) return null;
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 - dayMs) / HOUR_MS - startHour) * HOUR_H,
384
- height: Math.max(12, (ce - cs) / HOUR_MS * HOUR_H),
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 + 24 * HOUR_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(Math.floor(crAnchorMs / SNAP_MS) * SNAP_MS, crDayMs);
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 raw = clampToDayBand(day.ms + pointerHour(e.clientY) * HOUR_MS, day.ms);
451
- const snapped = day.ms + Math.floor((raw - day.ms) / SNAP_MS) * SNAP_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
- crAnchorMs = clampToDayBand(day.ms + Math.max(pointerHour(e.clientY), startHour) * HOUR_MS, day.ms);
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 raw = crDayMs + pointerHour(e.clientY) * HOUR_MS;
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(Math.floor((day.ms + hour * HOUR_MS) / SNAP_MS) * SNAP_MS, day.ms);
542
- const durMin = minDuration ?? 60;
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 = Math.round(raw / SNAP_MS) * SNAP_MS;
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
- function onResizePointerDown(e, ev, edge) {
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 evDayMs = sod(ev.start.getTime());
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 || 'Unavailable'}, {range}"
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
- <span
913
- class="tw-ev-handle tw-ev-handle--start"
914
- aria-hidden="true"
915
- onpointerdown={(e) => onResizePointerDown(e, p.ev, 'start')}
916
- ></span>
917
- <span
918
- class="tw-ev-handle tw-ev-handle--end"
919
- aria-hidden="true"
920
- onpointerdown={(e) => onResizePointerDown(e, p.ev, 'end')}
921
- ></span>
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
- overscroll-behavior: contain;
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.20.1",
4
- "description": "A themeable Svelte 5 calendar with Day and Week views — Planner and Agenda.",
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": "./widget/widget.js"
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.0.0"
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
- "week",
80
+ "agenda",
74
81
  "day",
82
+ "week",
83
+ "month",
75
84
  "booking",
76
- "svelte5"
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
  }