@nomideusz/svelte-calendar 0.21.0 → 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 -269
  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 +37 -19
  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 +12 -12
  24. package/dist/core/time.d.ts +9 -0
  25. package/dist/core/time.js +8 -0
  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 +93 -56
  29. package/dist/engine/view-state.svelte.d.ts +5 -1
  30. package/dist/engine/view-state.svelte.js +5 -3
  31. package/dist/headless/create-agenda.svelte.d.ts +8 -1
  32. package/dist/headless/create-agenda.svelte.js +32 -14
  33. package/dist/headless/create-calendar.svelte.js +53 -17
  34. package/dist/headless/create-range-agenda.svelte.d.ts +7 -0
  35. package/dist/headless/create-range-agenda.svelte.js +33 -12
  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 +41 -33
  50. package/dist/views/agenda/AgendaWeek.svelte +71 -34
  51. package/dist/views/index.d.ts +2 -0
  52. package/dist/views/index.js +2 -0
  53. package/dist/views/mobile/MobileDay.svelte +98 -41
  54. package/dist/views/mobile/MobileWeek.svelte +29 -13
  55. package/dist/views/mobile/swipe.js +6 -1
  56. package/dist/views/month/MonthGrid.svelte +29 -17
  57. package/dist/views/month/MonthGrid.svelte.d.ts +1 -1
  58. package/dist/views/planner/PlannerScroll.svelte +183 -51
  59. package/dist/views/planner/PlannerWeek.svelte +148 -48
  60. package/dist/views/shared/context.svelte.d.ts +1 -1
  61. package/package.json +24 -9
  62. package/widget/widget.js +7983 -7688
  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
@@ -12,11 +12,12 @@
12
12
  * Clean numbered schedule list.
13
13
  */
14
14
  import { createClock } from "../../core/clock.svelte.js";
15
- import { sod, addDaysMs, dayNum, isAllDay, isMultiDay } from "../../core/time.js";
15
+ import { sod, addDaysMs, dayNum, isAllDay } from "../../core/time.js";
16
16
  import { weekdayLong, monthLong } from "../../core/locale.js";
17
17
  import { useCalendarContext } from "../shared/context.svelte.js";
18
18
  import EventContent from "../shared/EventContent.svelte";
19
19
  import { fmtTime, duration, timeUntilMs, progress, groupIntoSlots } from "../shared/format.js";
20
+ import { createSwipe } from "../mobile/swipe.js";
20
21
  const ctx = useCalendarContext();
21
22
  const L = $derived(ctx.labels);
22
23
  const emptySnippet = $derived(ctx.emptySnippet);
@@ -36,28 +37,28 @@ const compact = $derived(ctx.compact);
36
37
  const oneventhover = $derived(ctx.oneventhover);
37
38
  const disabledSet = $derived(ctx.disabledSet);
38
39
  // ── Swipe navigation (mobile, touch only) ──────────
39
- let swipeStartX = 0;
40
- let swipeStartY = 0;
41
- let swipeActive = false;
42
- const SWIPE_THRESHOLD = 50;
43
- function onPointerDown(e) {
44
- if (!isMobile || e.pointerType !== "touch") return;
45
- swipeActive = true;
46
- swipeStartX = e.clientX;
47
- swipeStartY = e.clientY;
48
- }
49
- function onPointerUp(e) {
50
- if (!swipeActive || e.pointerType !== "touch") return;
51
- swipeActive = false;
52
- const dx = e.clientX - swipeStartX;
53
- const dy = e.clientY - swipeStartY;
54
- if (Math.abs(dx) > SWIPE_THRESHOLD && Math.abs(dx) > Math.abs(dy) * 1.4) {
55
- if (dx > 0) viewState?.prev();
56
- else viewState?.next();
40
+ // Touch events, not pointer events: once the browser claims a touch for
41
+ // panning it sends pointercancel and the pointerup never arrives.
42
+ const swipe = createSwipe({
43
+ disabled: () => !isMobile,
44
+ onmove: () => {},
45
+ onend: (dir) => {
46
+ if (dir > 0) viewState?.prev();
47
+ else if (dir < 0) viewState?.next();
57
48
  }
49
+ });
50
+ // ── Status suffix (aria) ────────────────────────────
51
+ function statusText(ev) {
52
+ if (ev.status === "cancelled") return ` (${L.cancelled})`;
53
+ if (ev.status === "tentative") return ` (${L.tentative})`;
54
+ if (ev.status === "full") return ` (${L.full})`;
55
+ if (ev.status === "limited") return ` (${L.limited})`;
56
+ return "";
58
57
  }
59
- function onPointerCancel() {
60
- swipeActive = false;
58
+ /** All-day events and timed ones at least a (wall-clock) day long go to
59
+ * the all-day strip; a shorter overnight event is listed with its time. */
60
+ function inAllDayStrip(ev) {
61
+ return isAllDay(ev) || ev.end.getTime() >= addDaysMs(ev.start.getTime(), 1);
61
62
  }
62
63
  // ── Format helpers (delegated to shared/format.ts) ──
63
64
  const fmt = (d) => fmtTime(d, locale);
@@ -78,9 +79,9 @@ const dayEvents = $derived.by(() => {
78
79
  return events.filter((ev) => ev.start.getTime() < dayEnd && ev.end.getTime() > dayMs).sort((a, b) => a.start.getTime() - b.start.getTime());
79
80
  });
80
81
  /** All-day / multi-day events shown in a separate strip */
81
- const allDayBanner = $derived(dayEvents.filter((ev) => isAllDay(ev) || isMultiDay(ev)));
82
+ const allDayBanner = $derived(dayEvents.filter(inAllDayStrip));
82
83
  /** Timed events (non-all-day) for normal slot rendering */
83
- const timedDayEvents = $derived(dayEvents.filter((ev) => !isAllDay(ev) && !isMultiDay(ev)));
84
+ const timedDayEvents = $derived(dayEvents.filter((ev) => !inAllDayStrip(ev)));
84
85
  const dayCat = $derived.by(() => {
85
86
  const now = clock.tick;
86
87
  const past = [];
@@ -118,7 +119,7 @@ const visibleDone = $derived(showAllDone ? dayCat.past : dayCat.past.slice(-DONE
118
119
  const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.length - DONE_VISIBLE));
119
120
  </script>
120
121
 
121
- <!-- svelte-ignore a11y_no_static_element_interactions -->
122
+ <!-- svelte-ignore a11y_no_static_element_interactions (touch swipe only; no pointer/keyboard role) -->
122
123
  <div
123
124
  class="ag ag--day"
124
125
  class:ag--disabled={disabledSet.has(dayMs)}
@@ -126,9 +127,10 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
126
127
  class:ag--auto={autoHeight}
127
128
  style={style || undefined}
128
129
  style:height={height ? `${height}px` : undefined}
129
- onpointerdown={onPointerDown}
130
- onpointerup={onPointerUp}
131
- onpointercancel={onPointerCancel}
130
+ ontouchstart={swipe.ontouchstart}
131
+ ontouchmove={swipe.ontouchmove}
132
+ ontouchend={swipe.ontouchend}
133
+ ontouchcancel={swipe.ontouchcancel}
132
134
  >
133
135
  <div class="ag-body" role="group" aria-label={L.todaysLineup}>
134
136
  <!-- ─── In-view date header (only when the chrome doesn't label the day) ─── -->
@@ -154,7 +156,7 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
154
156
  class="ag-allday-chip"
155
157
  class:ag-allday-chip--selected={selectedEventId === ev.id}
156
158
  style:--ev-color={ev.color || 'var(--dt-accent)'}
157
- aria-label="{ev.title}, {L.allDay}"
159
+ aria-label="{ev.title}{statusText(ev)}, {L.allDay}"
158
160
  onclick={() => handleClick(ev)}
159
161
  onpointerenter={() => oneventhover?.(ev)}
160
162
  >
@@ -182,7 +184,7 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
182
184
  class:ag-compact-row--cancelled={ev.status === 'cancelled'}
183
185
  class:ag-compact-row--tentative={ev.status === 'tentative'}
184
186
  style:--ev-color={ev.color || 'var(--dt-accent)'}
185
- aria-label="{ev.title}, {fmt(ev.start)}, {duration(ev)}"
187
+ aria-label="{ev.title}{statusText(ev)}, {fmt(ev.start)}, {duration(ev)}"
186
188
  onclick={() => handleClick(ev)}
187
189
  onpointerenter={() => oneventhover?.(ev)}
188
190
  >
@@ -277,7 +279,7 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
277
279
  class="ag-compact-row ag-compact-row--queue"
278
280
  class:ag-compact-row--selected={selectedEventId === ev.id}
279
281
  style:--ev-color={ev.color || 'var(--dt-accent)'}
280
- aria-label="{ev.title}, {fmt(ev.start)}, {duration(ev)}"
282
+ aria-label="{ev.title}{statusText(ev)}, {fmt(ev.start)}, {duration(ev)}"
281
283
  onclick={() => handleClick(ev)}
282
284
  onpointerenter={() => oneventhover?.(ev)}
283
285
  >
@@ -298,7 +300,7 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
298
300
  class:ag-card--hero={i === 0}
299
301
  class:ag-card--selected={selectedEventId === ev.id}
300
302
  style:--ev-color={ev.color || 'var(--dt-accent)'}
301
- aria-label="{ev.title}, {fmt(ev.start)}, {duration(ev)}"
303
+ aria-label="{ev.title}{statusText(ev)}, {fmt(ev.start)}, {duration(ev)}"
302
304
  onclick={() => handleClick(ev)}
303
305
  onpointerenter={() => oneventhover?.(ev)}
304
306
  >
@@ -345,7 +347,7 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
345
347
  class="ag-log-row"
346
348
  class:ag-log-row--selected={selectedEventId === ev.id}
347
349
  style:--ev-color={ev.color || 'var(--dt-accent)'}
348
- aria-label="{ev.title}, {fmt(ev.start)} to {fmt(ev.end)}"
350
+ aria-label="{ev.title}{statusText(ev)}, {fmt(ev.start)} – {fmt(ev.end)}"
349
351
  onclick={() => handleClick(ev)}
350
352
  onpointerenter={() => oneventhover?.(ev)}
351
353
  >
@@ -378,7 +380,7 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
378
380
  class:ag-card--full={ev.status === 'full'}
379
381
  class:ag-card--limited={ev.status === 'limited'}
380
382
  style:--ev-color={ev.color || 'var(--dt-accent)'}
381
- aria-label="{ev.title}{ev.status === 'cancelled' ? ' (cancelled)' : ''}{ev.status === 'tentative' ? ' (tentative)' : ''}{ev.status === 'full' ? ' (full)' : ''}{ev.status === 'limited' ? ' (limited)' : ''}, {fmt(ev.start)} to {fmt(ev.end)}, {duration(ev)}"
383
+ aria-label="{ev.title}{statusText(ev)}, {fmt(ev.start)} – {fmt(ev.end)}, {duration(ev)}"
382
384
  onclick={() => handleClick(ev)}
383
385
  onpointerenter={() => oneventhover?.(ev)}
384
386
  >
@@ -767,6 +769,12 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
767
769
  }
768
770
  /* Mobile: stack queue columns vertically — "Up next" (hero) first,
769
771
  Now/Done status column second */
772
+ /* Horizontal swipes are ours (day navigation); the browser keeps
773
+ vertical scrolling and pinch-zoom. */
774
+ .ag--mobile,
775
+ .ag--mobile .ag-body {
776
+ touch-action: pan-y pinch-zoom;
777
+ }
770
778
  .ag--mobile .ag-q {
771
779
  grid-template-columns: 1fr;
772
780
  min-height: auto;
@@ -14,6 +14,7 @@ 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
- const start = new Date(day.ms + (ev.start.getTime() - sod(ev.start.getTime())));
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
- let swipeStartX = 0;
71
- let swipeStartY = 0;
72
- let swipeActive = false;
73
- const SWIPE_THRESHOLD = 50;
74
- function onPointerDown(e) {
75
- if (!isMobile || e.pointerType !== "touch") return;
76
- swipeActive = true;
77
- swipeStartX = e.clientX;
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) {
@@ -121,8 +134,8 @@ const weekDays = $derived.by(() => {
121
134
  const ms = addDaysMs(weekStartMs, i);
122
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((ev) => isAllDay(ev) || isMultiDay(ev));
125
- const timedEvts = dayEvts.filter((ev) => !isAllDay(ev) && !isMultiDay(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}{ev.status === 'cancelled' ? ' (cancelled)' : ''}{ev.status === 'tentative' ? ' (tentative)' : ''}{ev.status === 'full' ? ' (full)' : ''}{ev.status === 'limited' ? ' (limited)' : ''}, {fmt(ev.start)} to {fmt(ev.end)}, {duration(ev)}"
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
- onpointerdown={onPointerDown}
295
- onpointerup={onPointerUp}
296
- onpointercancel={onPointerCancel}
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
- <!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
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
- <!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
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 a11y_click_events_have_key_events a11y_no_static_element_interactions -->
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
  }
@@ -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';
@@ -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';