@nomideusz/svelte-calendar 0.9.0 → 0.11.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 +15 -7
- package/dist/calendar/Calendar.svelte +202 -60
- package/dist/calendar/Calendar.svelte.d.ts +7 -0
- package/dist/core/locale.d.ts +15 -1
- package/dist/core/locale.js +9 -2
- package/dist/theme/auto.js +27 -6
- package/dist/theme/presets.d.ts +4 -4
- package/dist/theme/presets.js +11 -7
- package/dist/views/agenda/AgendaDay.svelte +327 -126
- package/dist/views/agenda/AgendaWeek.svelte +345 -121
- package/dist/views/mobile/MobileDay.svelte +416 -213
- package/dist/views/mobile/MobileWeek.svelte +142 -74
- package/dist/views/mobile/swipe.d.ts +29 -0
- package/dist/views/mobile/swipe.js +73 -0
- package/dist/views/month/MonthGrid.svelte +200 -31
- package/dist/views/planner/PlannerDay.svelte +155 -80
- package/dist/views/planner/PlannerWeek.svelte +1155 -630
- package/dist/views/planner/PlannerWeek.svelte.d.ts +2 -0
- package/dist/views/shared/context.svelte.d.ts +3 -0
- package/dist/views/shared/context.svelte.js +13 -9
- package/dist/views/shared/format.d.ts +2 -1
- package/dist/views/shared/format.js +8 -5
- package/dist/widget/CalendarWidget.svelte +33 -5
- package/dist/widget/CalendarWidget.svelte.d.ts +16 -2
- package/dist/widget/widget.d.ts +9 -0
- package/dist/widget/widget.js +59 -13
- package/package.json +1 -1
- package/widget/widget.js +3364 -2783
- package/widget/svelte-calendar.css +0 -3054
|
@@ -10,8 +10,8 @@ import EventContent from "../shared/EventContent.svelte";
|
|
|
10
10
|
import { createClock } from "../../core/clock.svelte.js";
|
|
11
11
|
import { DAY_MS, sod, isAllDay, isMultiDay } from "../../core/time.js";
|
|
12
12
|
import { startOfWeek as sowFn } from "../../core/time.js";
|
|
13
|
-
import { fmtTime as _fmtTime, weekdayShort
|
|
14
|
-
|
|
13
|
+
import { fmtTime as _fmtTime, weekdayShort } from "../../core/locale.js";
|
|
14
|
+
import { createSwipe } from "./swipe.js";
|
|
15
15
|
let {
|
|
16
16
|
mondayStart = true,
|
|
17
17
|
locale,
|
|
@@ -20,11 +20,10 @@ let {
|
|
|
20
20
|
style = "",
|
|
21
21
|
focusDate,
|
|
22
22
|
oneventclick,
|
|
23
|
-
|
|
24
|
-
selectedEventId = null,
|
|
25
|
-
readOnly = false
|
|
23
|
+
selectedEventId = null
|
|
26
24
|
} = $props();
|
|
27
25
|
const ctx = useCalendarContext();
|
|
26
|
+
const L = $derived(ctx.labels);
|
|
28
27
|
const viewState = $derived(ctx.viewState);
|
|
29
28
|
const equalDays = $derived(ctx.equalDays);
|
|
30
29
|
const showDates = $derived(ctx.showDates);
|
|
@@ -33,7 +32,6 @@ const autoHeight = $derived(ctx.autoHeight);
|
|
|
33
32
|
const oneventhover = $derived(ctx.oneventhover);
|
|
34
33
|
const disabledSet = $derived(ctx.disabledSet);
|
|
35
34
|
const loadRangeCtx = $derived(ctx.loadRange);
|
|
36
|
-
const minDuration = $derived(ctx.minDuration);
|
|
37
35
|
const clock = createClock(ctx.timezone);
|
|
38
36
|
const MAX_EVENTS = 3;
|
|
39
37
|
const customDays = $derived(viewState?.dayCount ?? 7);
|
|
@@ -80,54 +78,53 @@ const dayCells = $derived.by(() => {
|
|
|
80
78
|
}
|
|
81
79
|
return result;
|
|
82
80
|
});
|
|
81
|
+
let expandedDays = $state(/* @__PURE__ */ new Set());
|
|
82
|
+
function toggleExpand(ms) {
|
|
83
|
+
const next = new Set(expandedDays);
|
|
84
|
+
if (next.has(ms)) next.delete(ms);
|
|
85
|
+
else next.add(ms);
|
|
86
|
+
expandedDays = next;
|
|
87
|
+
}
|
|
83
88
|
function fmtTime(d) {
|
|
84
89
|
return _fmtTime(d, locale);
|
|
85
90
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
let swipeOffset = $state(0);
|
|
90
|
-
const SWIPE_THRESHOLD = 60;
|
|
91
|
-
function onTouchStart(e) {
|
|
92
|
-
const t = e.touches[0];
|
|
93
|
-
touchStartX = t.clientX;
|
|
94
|
-
touchStartY = t.clientY;
|
|
95
|
-
swiping = true;
|
|
96
|
-
swipeOffset = 0;
|
|
91
|
+
function evTimeLabel(ev) {
|
|
92
|
+
if (isAllDay(ev) || isMultiDay(ev)) return L.allDay;
|
|
93
|
+
return `${fmtTime(ev.start)} \u2013 ${fmtTime(ev.end)}`;
|
|
97
94
|
}
|
|
98
|
-
function
|
|
99
|
-
if (
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
swiping = false;
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
swipeOffset = dx;
|
|
95
|
+
function statusText(ev) {
|
|
96
|
+
if (ev.status === "cancelled") return ` (${L.cancelled})`;
|
|
97
|
+
if (ev.status === "tentative") return ` (${L.tentative})`;
|
|
98
|
+
if (ev.status === "full") return ` (${L.full})`;
|
|
99
|
+
if (ev.status === "limited") return ` (${L.limited})`;
|
|
100
|
+
return "";
|
|
108
101
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
102
|
+
let swipeOffset = $state(0);
|
|
103
|
+
let swipeAnimate = $state(false);
|
|
104
|
+
const swipe = createSwipe({
|
|
105
|
+
onmove: (dx) => {
|
|
106
|
+
swipeAnimate = false;
|
|
107
|
+
swipeOffset = dx;
|
|
108
|
+
},
|
|
109
|
+
onend: (dir) => {
|
|
110
|
+
if (dir !== 0) {
|
|
111
|
+
swipeAnimate = false;
|
|
112
|
+
swipeOffset = 0;
|
|
113
|
+
if (dir > 0) viewState?.prev();
|
|
114
|
+
else viewState?.next();
|
|
117
115
|
} else {
|
|
118
|
-
|
|
116
|
+
swipeAnimate = true;
|
|
117
|
+
swipeOffset = 0;
|
|
119
118
|
}
|
|
120
119
|
}
|
|
121
|
-
|
|
122
|
-
swiping = false;
|
|
123
|
-
}
|
|
120
|
+
});
|
|
124
121
|
function handleDayTap(dayMs) {
|
|
125
|
-
if (viewState)
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
122
|
+
if (!viewState) return;
|
|
123
|
+
viewState.setFocusDate(new Date(dayMs));
|
|
124
|
+
const currentView = viewState.view;
|
|
125
|
+
if (!currentView.split("-").includes("week")) return;
|
|
126
|
+
const dayView = currentView.split("-").map((seg) => seg === "week" ? "day" : seg).join("-");
|
|
127
|
+
if (dayView !== currentView) viewState.setView(dayView);
|
|
131
128
|
}
|
|
132
129
|
function handleDayKeydown(e, dayMs) {
|
|
133
130
|
if (e.key !== "Enter" && e.key !== " ") return;
|
|
@@ -143,12 +140,18 @@ function handleDayKeydown(e, dayMs) {
|
|
|
143
140
|
style:height={autoHeight ? undefined : (height ? `${height}px` : '100%')}
|
|
144
141
|
role="region"
|
|
145
142
|
aria-label={L.weekAhead}
|
|
146
|
-
ontouchstart={
|
|
147
|
-
ontouchmove={
|
|
148
|
-
ontouchend={
|
|
143
|
+
ontouchstart={swipe.ontouchstart}
|
|
144
|
+
ontouchmove={swipe.ontouchmove}
|
|
145
|
+
ontouchend={swipe.ontouchend}
|
|
146
|
+
ontouchcancel={swipe.ontouchcancel}
|
|
149
147
|
>
|
|
150
148
|
<!-- Vertical day list -->
|
|
151
|
-
<div
|
|
149
|
+
<div
|
|
150
|
+
class="mw-list"
|
|
151
|
+
class:mw-list--animate={swipeAnimate}
|
|
152
|
+
style:transform={swipeOffset !== 0 ? `translateX(${swipeOffset}px)` : undefined}
|
|
153
|
+
role="list"
|
|
154
|
+
>
|
|
152
155
|
{#each dayCells as cell (cell.ms)}
|
|
153
156
|
<div
|
|
154
157
|
class="mw-row"
|
|
@@ -157,13 +160,15 @@ function handleDayKeydown(e, dayMs) {
|
|
|
157
160
|
class:mw-row--weekend={cell.isWeekend}
|
|
158
161
|
class:mw-row--disabled={cell.isDisabled}
|
|
159
162
|
role="listitem"
|
|
163
|
+
aria-current={cell.isToday ? 'date' : undefined}
|
|
160
164
|
>
|
|
161
165
|
<button
|
|
166
|
+
type="button"
|
|
162
167
|
class="mw-row-target"
|
|
163
168
|
disabled={cell.isDisabled}
|
|
164
169
|
onclick={() => handleDayTap(cell.ms)}
|
|
165
170
|
onkeydown={(e) => handleDayKeydown(e, cell.ms)}
|
|
166
|
-
aria-label="{cell.dayName} {cell.dayNum}"
|
|
171
|
+
aria-label="{cell.dayName} {cell.dayNum}{cell.isToday ? `, ${L.today}` : ''}"
|
|
167
172
|
></button>
|
|
168
173
|
<!-- Date column -->
|
|
169
174
|
<div class="mw-date">
|
|
@@ -178,7 +183,7 @@ function handleDayKeydown(e, dayMs) {
|
|
|
178
183
|
{#if cell.events.length === 0}
|
|
179
184
|
<span class="mw-empty">{L.noEvents}</span>
|
|
180
185
|
{:else}
|
|
181
|
-
{#each cell.events.slice(0, MAX_EVENTS) as ev (ev.id)}
|
|
186
|
+
{#each (expandedDays.has(cell.ms) ? cell.events : cell.events.slice(0, MAX_EVENTS)) as ev (ev.id)}
|
|
182
187
|
<button
|
|
183
188
|
type="button"
|
|
184
189
|
class="mw-ev"
|
|
@@ -193,6 +198,7 @@ function handleDayKeydown(e, dayMs) {
|
|
|
193
198
|
onclick={(e) => { e.stopPropagation(); oneventclick?.(ev); }}
|
|
194
199
|
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); oneventclick?.(ev); } }}
|
|
195
200
|
onpointerenter={() => oneventhover?.(ev)}
|
|
201
|
+
aria-label="{ev.title}{statusText(ev)}, {evTimeLabel(ev)}"
|
|
196
202
|
>
|
|
197
203
|
<span class="mw-ev-stripe"></span>
|
|
198
204
|
<div class="mw-ev-body">
|
|
@@ -208,7 +214,14 @@ function handleDayKeydown(e, dayMs) {
|
|
|
208
214
|
</button>
|
|
209
215
|
{/each}
|
|
210
216
|
{#if cell.totalCount > MAX_EVENTS}
|
|
211
|
-
<
|
|
217
|
+
<button
|
|
218
|
+
type="button"
|
|
219
|
+
class="mw-ev-more"
|
|
220
|
+
aria-expanded={expandedDays.has(cell.ms)}
|
|
221
|
+
onclick={() => toggleExpand(cell.ms)}
|
|
222
|
+
>
|
|
223
|
+
{expandedDays.has(cell.ms) ? L.showLess : L.nMore(cell.totalCount - MAX_EVENTS)}
|
|
224
|
+
</button>
|
|
212
225
|
{/if}
|
|
213
226
|
{/if}
|
|
214
227
|
</div>
|
|
@@ -231,6 +244,7 @@ function handleDayKeydown(e, dayMs) {
|
|
|
231
244
|
overflow: hidden;
|
|
232
245
|
background: var(--dt-bg, #fff);
|
|
233
246
|
-webkit-tap-highlight-color: transparent;
|
|
247
|
+
touch-action: pan-y;
|
|
234
248
|
}
|
|
235
249
|
.mw--auto { overflow: visible; }
|
|
236
250
|
|
|
@@ -238,11 +252,19 @@ function handleDayKeydown(e, dayMs) {
|
|
|
238
252
|
.mw-list {
|
|
239
253
|
flex: 1;
|
|
240
254
|
overflow-y: auto;
|
|
255
|
+
overflow-x: hidden;
|
|
256
|
+
overscroll-behavior: contain;
|
|
241
257
|
-webkit-overflow-scrolling: touch;
|
|
242
258
|
scrollbar-width: thin;
|
|
243
259
|
scrollbar-color: var(--dt-scrollbar, rgba(0, 0, 0, 0.1)) transparent;
|
|
244
260
|
}
|
|
245
261
|
.mw--auto .mw-list { overflow-y: visible; }
|
|
262
|
+
.mw-list--animate {
|
|
263
|
+
transition: transform 180ms ease;
|
|
264
|
+
}
|
|
265
|
+
@media (prefers-reduced-motion: reduce) {
|
|
266
|
+
.mw-list--animate { transition: none; }
|
|
267
|
+
}
|
|
246
268
|
|
|
247
269
|
/* ─── Day row ────────────────────────────────────── */
|
|
248
270
|
.mw-row {
|
|
@@ -250,6 +272,10 @@ function handleDayKeydown(e, dayMs) {
|
|
|
250
272
|
align-items: center;
|
|
251
273
|
gap: 12px;
|
|
252
274
|
position: relative;
|
|
275
|
+
/* border-box: width 100% + padding otherwise overflows the list by
|
|
276
|
+
24px, which iOS turns into a horizontal pan that clips the date
|
|
277
|
+
column off the left edge */
|
|
278
|
+
box-sizing: border-box;
|
|
253
279
|
padding: 10px 12px;
|
|
254
280
|
background: transparent;
|
|
255
281
|
transition: background 120ms;
|
|
@@ -268,8 +294,13 @@ function handleDayKeydown(e, dayMs) {
|
|
|
268
294
|
.mw-row--today {
|
|
269
295
|
background: color-mix(in srgb, var(--dt-accent, #2563eb) 4%, transparent);
|
|
270
296
|
}
|
|
297
|
+
/* Token-based dim (not subtree opacity) so past rows stay legible/tappable */
|
|
271
298
|
.mw-row--past {
|
|
272
|
-
|
|
299
|
+
background: color-mix(in srgb, var(--dt-text, rgba(0, 0, 0, 0.87)) 3%, transparent);
|
|
300
|
+
}
|
|
301
|
+
.mw-row--past :global(.mw-ev-title),
|
|
302
|
+
.mw-row--past .mw-day-num {
|
|
303
|
+
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
273
304
|
}
|
|
274
305
|
.mw-row--disabled {
|
|
275
306
|
background-image: repeating-linear-gradient(
|
|
@@ -294,8 +325,8 @@ function handleDayKeydown(e, dayMs) {
|
|
|
294
325
|
cursor: default;
|
|
295
326
|
}
|
|
296
327
|
.mw-row-target:focus-visible {
|
|
297
|
-
outline:
|
|
298
|
-
|
|
328
|
+
outline: none;
|
|
329
|
+
box-shadow: inset 0 0 0 2px var(--dt-accent, #2563eb);
|
|
299
330
|
}
|
|
300
331
|
|
|
301
332
|
/* ─── Date column ────────────────────────────────── */
|
|
@@ -312,10 +343,10 @@ function handleDayKeydown(e, dayMs) {
|
|
|
312
343
|
}
|
|
313
344
|
|
|
314
345
|
.mw-day-name {
|
|
315
|
-
font: 600
|
|
346
|
+
font: 600 11px/1 var(--dt-sans, system-ui, sans-serif);
|
|
316
347
|
letter-spacing: 0.06em;
|
|
317
348
|
text-transform: uppercase;
|
|
318
|
-
color: var(--dt-text-
|
|
349
|
+
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
319
350
|
}
|
|
320
351
|
.mw-day-name--today {
|
|
321
352
|
color: var(--dt-accent, #2563eb);
|
|
@@ -338,6 +369,8 @@ function handleDayKeydown(e, dayMs) {
|
|
|
338
369
|
}
|
|
339
370
|
|
|
340
371
|
/* ─── Events column ──────────────────────────────── */
|
|
372
|
+
/* pointer-events pass through to the full-row target underneath;
|
|
373
|
+
only the chips (and "+N more") re-capture them. */
|
|
341
374
|
.mw-events {
|
|
342
375
|
flex: 1;
|
|
343
376
|
min-width: 0;
|
|
@@ -346,12 +379,12 @@ function handleDayKeydown(e, dayMs) {
|
|
|
346
379
|
gap: 4px;
|
|
347
380
|
position: relative;
|
|
348
381
|
z-index: 2;
|
|
382
|
+
pointer-events: none;
|
|
349
383
|
}
|
|
350
384
|
|
|
351
385
|
.mw-empty {
|
|
352
|
-
font: 400
|
|
353
|
-
color: var(--dt-text-
|
|
354
|
-
font-style: italic;
|
|
386
|
+
font: 400 13px/1 var(--dt-sans, system-ui, sans-serif);
|
|
387
|
+
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
355
388
|
}
|
|
356
389
|
|
|
357
390
|
/* ─── Event chip ─────────────────────────────────── */
|
|
@@ -359,6 +392,7 @@ function handleDayKeydown(e, dayMs) {
|
|
|
359
392
|
display: flex;
|
|
360
393
|
align-items: center;
|
|
361
394
|
gap: 0;
|
|
395
|
+
min-height: 44px;
|
|
362
396
|
border-radius: 6px;
|
|
363
397
|
background: color-mix(in srgb, var(--ev-color) 10%, var(--dt-surface, #f9fafb));
|
|
364
398
|
overflow: hidden;
|
|
@@ -368,6 +402,7 @@ function handleDayKeydown(e, dayMs) {
|
|
|
368
402
|
border: none;
|
|
369
403
|
text-align: left;
|
|
370
404
|
padding: 0;
|
|
405
|
+
pointer-events: auto;
|
|
371
406
|
}
|
|
372
407
|
.mw-ev:active {
|
|
373
408
|
background: color-mix(in srgb, var(--ev-color) 20%, var(--dt-surface, #f9fafb));
|
|
@@ -381,22 +416,32 @@ function handleDayKeydown(e, dayMs) {
|
|
|
381
416
|
.mw-ev--allday {
|
|
382
417
|
background: color-mix(in srgb, var(--ev-color) 14%, var(--dt-surface, #f9fafb));
|
|
383
418
|
}
|
|
419
|
+
/* Status treatments: token-level dims + a non-opacity signal
|
|
420
|
+
(strikethrough / border style) — never a bare opacity on the chip. */
|
|
384
421
|
.mw-ev--cancelled {
|
|
385
|
-
|
|
422
|
+
background: color-mix(in srgb, var(--ev-color) 5%, var(--dt-surface, #f9fafb));
|
|
386
423
|
}
|
|
387
424
|
.mw-ev--cancelled .mw-ev-title {
|
|
388
425
|
text-decoration: line-through;
|
|
426
|
+
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
427
|
+
}
|
|
428
|
+
.mw-ev--cancelled .mw-ev-stripe {
|
|
429
|
+
opacity: 0.45; /* decorative bar only */
|
|
389
430
|
}
|
|
390
431
|
.mw-ev--tentative {
|
|
391
|
-
|
|
392
|
-
border: 1px dashed color-mix(in srgb, var(--ev-color)
|
|
432
|
+
background: color-mix(in srgb, var(--ev-color) 6%, var(--dt-surface, #f9fafb));
|
|
433
|
+
border: 1px dashed color-mix(in srgb, var(--ev-color) 45%, transparent);
|
|
393
434
|
}
|
|
394
435
|
.mw-ev--full {
|
|
395
|
-
|
|
436
|
+
background: color-mix(in srgb, var(--ev-color) 6%, var(--dt-surface, #f9fafb));
|
|
437
|
+
border: 1px solid color-mix(in srgb, var(--ev-color) 30%, transparent);
|
|
438
|
+
}
|
|
439
|
+
.mw-ev--full .mw-ev-title {
|
|
440
|
+
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
396
441
|
}
|
|
397
442
|
.mw-ev--limited {
|
|
398
|
-
|
|
399
|
-
border: 1px dashed color-mix(in srgb, var(--ev-color)
|
|
443
|
+
background: color-mix(in srgb, var(--ev-color) 8%, var(--dt-surface, #f9fafb));
|
|
444
|
+
border: 1px dashed color-mix(in srgb, var(--ev-color) 45%, transparent);
|
|
400
445
|
}
|
|
401
446
|
|
|
402
447
|
.mw-ev-stripe {
|
|
@@ -417,7 +462,7 @@ function handleDayKeydown(e, dayMs) {
|
|
|
417
462
|
}
|
|
418
463
|
|
|
419
464
|
.mw-ev-title {
|
|
420
|
-
font: 500
|
|
465
|
+
font: 500 15px/1.2 var(--dt-sans, system-ui, sans-serif);
|
|
421
466
|
color: var(--dt-text, rgba(0, 0, 0, 0.87));
|
|
422
467
|
white-space: nowrap;
|
|
423
468
|
overflow: hidden;
|
|
@@ -427,16 +472,39 @@ function handleDayKeydown(e, dayMs) {
|
|
|
427
472
|
}
|
|
428
473
|
|
|
429
474
|
.mw-ev-time {
|
|
430
|
-
font: 400
|
|
431
|
-
color: var(--dt-text-
|
|
475
|
+
font: 400 12px/1 var(--dt-mono, ui-monospace, monospace);
|
|
476
|
+
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
432
477
|
white-space: nowrap;
|
|
433
478
|
flex-shrink: 0;
|
|
434
479
|
}
|
|
435
480
|
|
|
436
481
|
.mw-ev-more {
|
|
437
|
-
font:
|
|
438
|
-
color: var(--dt-text-
|
|
439
|
-
padding: 2px
|
|
482
|
+
font: 500 12px/1 var(--dt-sans, system-ui, sans-serif);
|
|
483
|
+
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
484
|
+
padding: 2px 4px;
|
|
485
|
+
min-height: 32px;
|
|
486
|
+
border: none;
|
|
487
|
+
background: transparent;
|
|
488
|
+
cursor: pointer;
|
|
489
|
+
text-align: left;
|
|
490
|
+
align-self: flex-start;
|
|
491
|
+
position: relative;
|
|
492
|
+
pointer-events: auto;
|
|
493
|
+
-webkit-tap-highlight-color: transparent;
|
|
494
|
+
}
|
|
495
|
+
/* Hit-slop: 44px effective touch target */
|
|
496
|
+
.mw-ev-more::before {
|
|
497
|
+
content: '';
|
|
498
|
+
position: absolute;
|
|
499
|
+
left: 0;
|
|
500
|
+
right: 0;
|
|
501
|
+
top: 50%;
|
|
502
|
+
transform: translateY(-50%);
|
|
503
|
+
height: 44px;
|
|
504
|
+
}
|
|
505
|
+
.mw-ev-more:focus-visible {
|
|
506
|
+
outline: none;
|
|
507
|
+
box-shadow: 0 0 0 2px var(--dt-accent, #2563eb);
|
|
440
508
|
}
|
|
441
509
|
|
|
442
510
|
/* ─── Chevron ────────────────────────────────────── */
|
|
@@ -450,7 +518,7 @@ function handleDayKeydown(e, dayMs) {
|
|
|
450
518
|
|
|
451
519
|
/* ─── Focus ──────────────────────────────────────── */
|
|
452
520
|
.mw-ev:focus-visible {
|
|
453
|
-
outline:
|
|
454
|
-
|
|
521
|
+
outline: none;
|
|
522
|
+
box-shadow: 0 0 0 2px var(--dt-accent, #2563eb);
|
|
455
523
|
}
|
|
456
524
|
</style>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared horizontal-swipe tracker for the mobile views.
|
|
3
|
+
*
|
|
4
|
+
* Tracks touch gestures on a container and reports the live horizontal
|
|
5
|
+
* offset (so the view can follow the finger) plus a commit direction on
|
|
6
|
+
* release. Vertical-dominant movement hands the gesture back to native
|
|
7
|
+
* scrolling immediately.
|
|
8
|
+
*/
|
|
9
|
+
/** Unified swipe commit threshold (px) across mobile day + week views. */
|
|
10
|
+
export declare const SWIPE_THRESHOLD = 50;
|
|
11
|
+
export interface SwipeCallbacks {
|
|
12
|
+
/** Live horizontal offset (px) while the finger is down. Reset to 0 when the gesture is abandoned. */
|
|
13
|
+
onmove: (dx: number) => void;
|
|
14
|
+
/**
|
|
15
|
+
* Gesture finished.
|
|
16
|
+
* `dir` is `1` (swiped right → previous period), `-1` (swiped left → next period),
|
|
17
|
+
* or `0` (below threshold — snap back).
|
|
18
|
+
*/
|
|
19
|
+
onend: (dir: -1 | 0 | 1) => void;
|
|
20
|
+
/** Return true to ignore the gesture entirely (e.g. while a drag-create/resize is active). */
|
|
21
|
+
disabled?: () => boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface SwipeHandlers {
|
|
24
|
+
ontouchstart: (e: TouchEvent) => void;
|
|
25
|
+
ontouchmove: (e: TouchEvent) => void;
|
|
26
|
+
ontouchend: () => void;
|
|
27
|
+
ontouchcancel: () => void;
|
|
28
|
+
}
|
|
29
|
+
export declare function createSwipe(cb: SwipeCallbacks): SwipeHandlers;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared horizontal-swipe tracker for the mobile views.
|
|
3
|
+
*
|
|
4
|
+
* Tracks touch gestures on a container and reports the live horizontal
|
|
5
|
+
* offset (so the view can follow the finger) plus a commit direction on
|
|
6
|
+
* release. Vertical-dominant movement hands the gesture back to native
|
|
7
|
+
* scrolling immediately.
|
|
8
|
+
*/
|
|
9
|
+
/** Unified swipe commit threshold (px) across mobile day + week views. */
|
|
10
|
+
export const SWIPE_THRESHOLD = 50;
|
|
11
|
+
export function createSwipe(cb) {
|
|
12
|
+
let startX = 0;
|
|
13
|
+
let startY = 0;
|
|
14
|
+
let tracking = false;
|
|
15
|
+
let dx = 0;
|
|
16
|
+
function abandon() {
|
|
17
|
+
tracking = false;
|
|
18
|
+
if (dx !== 0) {
|
|
19
|
+
dx = 0;
|
|
20
|
+
cb.onmove(0);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
ontouchstart(e) {
|
|
25
|
+
if (cb.disabled?.()) {
|
|
26
|
+
tracking = false;
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const t = e.touches[0];
|
|
30
|
+
startX = t.clientX;
|
|
31
|
+
startY = t.clientY;
|
|
32
|
+
tracking = true;
|
|
33
|
+
dx = 0;
|
|
34
|
+
},
|
|
35
|
+
ontouchmove(e) {
|
|
36
|
+
if (!tracking)
|
|
37
|
+
return;
|
|
38
|
+
if (cb.disabled?.()) {
|
|
39
|
+
abandon();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const t = e.touches[0];
|
|
43
|
+
const mx = t.clientX - startX;
|
|
44
|
+
const my = t.clientY - startY;
|
|
45
|
+
// Vertical movement dominates → this is a scroll, not a swipe.
|
|
46
|
+
if (Math.abs(my) > Math.abs(mx) * 0.8) {
|
|
47
|
+
abandon();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
dx = mx;
|
|
51
|
+
cb.onmove(dx);
|
|
52
|
+
},
|
|
53
|
+
ontouchend() {
|
|
54
|
+
if (!tracking) {
|
|
55
|
+
cb.onend(0);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
tracking = false;
|
|
59
|
+
const dir = Math.abs(dx) > SWIPE_THRESHOLD ? (dx > 0 ? 1 : -1) : 0;
|
|
60
|
+
dx = 0;
|
|
61
|
+
cb.onend(dir);
|
|
62
|
+
},
|
|
63
|
+
ontouchcancel() {
|
|
64
|
+
// The browser took over the gesture (scroll, edge-swipe, system UI).
|
|
65
|
+
// Without this, the view stays stuck at the last swipe offset.
|
|
66
|
+
if (!tracking)
|
|
67
|
+
return;
|
|
68
|
+
tracking = false;
|
|
69
|
+
dx = 0;
|
|
70
|
+
cb.onend(0);
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|