@nomideusz/svelte-calendar 0.9.0 → 0.10.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 +167 -59
- 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 +259 -110
- package/dist/views/agenda/AgendaWeek.svelte +190 -65
- package/dist/views/mobile/MobileDay.svelte +408 -213
- package/dist/views/mobile/MobileWeek.svelte +136 -74
- package/dist/views/mobile/swipe.d.ts +28 -0
- package/dist/views/mobile/swipe.js +64 -0
- package/dist/views/month/MonthGrid.svelte +166 -30
- 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 +3263 -2727
- 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,17 @@ 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}
|
|
149
146
|
>
|
|
150
147
|
<!-- Vertical day list -->
|
|
151
|
-
<div
|
|
148
|
+
<div
|
|
149
|
+
class="mw-list"
|
|
150
|
+
class:mw-list--animate={swipeAnimate}
|
|
151
|
+
style:transform={swipeOffset !== 0 ? `translateX(${swipeOffset}px)` : undefined}
|
|
152
|
+
role="list"
|
|
153
|
+
>
|
|
152
154
|
{#each dayCells as cell (cell.ms)}
|
|
153
155
|
<div
|
|
154
156
|
class="mw-row"
|
|
@@ -157,13 +159,15 @@ function handleDayKeydown(e, dayMs) {
|
|
|
157
159
|
class:mw-row--weekend={cell.isWeekend}
|
|
158
160
|
class:mw-row--disabled={cell.isDisabled}
|
|
159
161
|
role="listitem"
|
|
162
|
+
aria-current={cell.isToday ? 'date' : undefined}
|
|
160
163
|
>
|
|
161
164
|
<button
|
|
165
|
+
type="button"
|
|
162
166
|
class="mw-row-target"
|
|
163
167
|
disabled={cell.isDisabled}
|
|
164
168
|
onclick={() => handleDayTap(cell.ms)}
|
|
165
169
|
onkeydown={(e) => handleDayKeydown(e, cell.ms)}
|
|
166
|
-
aria-label="{cell.dayName} {cell.dayNum}"
|
|
170
|
+
aria-label="{cell.dayName} {cell.dayNum}{cell.isToday ? `, ${L.today}` : ''}"
|
|
167
171
|
></button>
|
|
168
172
|
<!-- Date column -->
|
|
169
173
|
<div class="mw-date">
|
|
@@ -178,7 +182,7 @@ function handleDayKeydown(e, dayMs) {
|
|
|
178
182
|
{#if cell.events.length === 0}
|
|
179
183
|
<span class="mw-empty">{L.noEvents}</span>
|
|
180
184
|
{:else}
|
|
181
|
-
{#each cell.events.slice(0, MAX_EVENTS) as ev (ev.id)}
|
|
185
|
+
{#each (expandedDays.has(cell.ms) ? cell.events : cell.events.slice(0, MAX_EVENTS)) as ev (ev.id)}
|
|
182
186
|
<button
|
|
183
187
|
type="button"
|
|
184
188
|
class="mw-ev"
|
|
@@ -193,6 +197,7 @@ function handleDayKeydown(e, dayMs) {
|
|
|
193
197
|
onclick={(e) => { e.stopPropagation(); oneventclick?.(ev); }}
|
|
194
198
|
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); oneventclick?.(ev); } }}
|
|
195
199
|
onpointerenter={() => oneventhover?.(ev)}
|
|
200
|
+
aria-label="{ev.title}{statusText(ev)}, {evTimeLabel(ev)}"
|
|
196
201
|
>
|
|
197
202
|
<span class="mw-ev-stripe"></span>
|
|
198
203
|
<div class="mw-ev-body">
|
|
@@ -208,7 +213,14 @@ function handleDayKeydown(e, dayMs) {
|
|
|
208
213
|
</button>
|
|
209
214
|
{/each}
|
|
210
215
|
{#if cell.totalCount > MAX_EVENTS}
|
|
211
|
-
<
|
|
216
|
+
<button
|
|
217
|
+
type="button"
|
|
218
|
+
class="mw-ev-more"
|
|
219
|
+
aria-expanded={expandedDays.has(cell.ms)}
|
|
220
|
+
onclick={() => toggleExpand(cell.ms)}
|
|
221
|
+
>
|
|
222
|
+
{expandedDays.has(cell.ms) ? L.showLess : L.nMore(cell.totalCount - MAX_EVENTS)}
|
|
223
|
+
</button>
|
|
212
224
|
{/if}
|
|
213
225
|
{/if}
|
|
214
226
|
</div>
|
|
@@ -231,6 +243,7 @@ function handleDayKeydown(e, dayMs) {
|
|
|
231
243
|
overflow: hidden;
|
|
232
244
|
background: var(--dt-bg, #fff);
|
|
233
245
|
-webkit-tap-highlight-color: transparent;
|
|
246
|
+
touch-action: pan-y;
|
|
234
247
|
}
|
|
235
248
|
.mw--auto { overflow: visible; }
|
|
236
249
|
|
|
@@ -238,11 +251,18 @@ function handleDayKeydown(e, dayMs) {
|
|
|
238
251
|
.mw-list {
|
|
239
252
|
flex: 1;
|
|
240
253
|
overflow-y: auto;
|
|
254
|
+
overscroll-behavior: contain;
|
|
241
255
|
-webkit-overflow-scrolling: touch;
|
|
242
256
|
scrollbar-width: thin;
|
|
243
257
|
scrollbar-color: var(--dt-scrollbar, rgba(0, 0, 0, 0.1)) transparent;
|
|
244
258
|
}
|
|
245
259
|
.mw--auto .mw-list { overflow-y: visible; }
|
|
260
|
+
.mw-list--animate {
|
|
261
|
+
transition: transform 180ms ease;
|
|
262
|
+
}
|
|
263
|
+
@media (prefers-reduced-motion: reduce) {
|
|
264
|
+
.mw-list--animate { transition: none; }
|
|
265
|
+
}
|
|
246
266
|
|
|
247
267
|
/* ─── Day row ────────────────────────────────────── */
|
|
248
268
|
.mw-row {
|
|
@@ -268,8 +288,13 @@ function handleDayKeydown(e, dayMs) {
|
|
|
268
288
|
.mw-row--today {
|
|
269
289
|
background: color-mix(in srgb, var(--dt-accent, #2563eb) 4%, transparent);
|
|
270
290
|
}
|
|
291
|
+
/* Token-based dim (not subtree opacity) so past rows stay legible/tappable */
|
|
271
292
|
.mw-row--past {
|
|
272
|
-
|
|
293
|
+
background: color-mix(in srgb, var(--dt-text, rgba(0, 0, 0, 0.87)) 3%, transparent);
|
|
294
|
+
}
|
|
295
|
+
.mw-row--past :global(.mw-ev-title),
|
|
296
|
+
.mw-row--past .mw-day-num {
|
|
297
|
+
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
273
298
|
}
|
|
274
299
|
.mw-row--disabled {
|
|
275
300
|
background-image: repeating-linear-gradient(
|
|
@@ -294,8 +319,8 @@ function handleDayKeydown(e, dayMs) {
|
|
|
294
319
|
cursor: default;
|
|
295
320
|
}
|
|
296
321
|
.mw-row-target:focus-visible {
|
|
297
|
-
outline:
|
|
298
|
-
|
|
322
|
+
outline: none;
|
|
323
|
+
box-shadow: inset 0 0 0 2px var(--dt-accent, #2563eb);
|
|
299
324
|
}
|
|
300
325
|
|
|
301
326
|
/* ─── Date column ────────────────────────────────── */
|
|
@@ -312,10 +337,10 @@ function handleDayKeydown(e, dayMs) {
|
|
|
312
337
|
}
|
|
313
338
|
|
|
314
339
|
.mw-day-name {
|
|
315
|
-
font: 600
|
|
340
|
+
font: 600 11px/1 var(--dt-sans, system-ui, sans-serif);
|
|
316
341
|
letter-spacing: 0.06em;
|
|
317
342
|
text-transform: uppercase;
|
|
318
|
-
color: var(--dt-text-
|
|
343
|
+
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
319
344
|
}
|
|
320
345
|
.mw-day-name--today {
|
|
321
346
|
color: var(--dt-accent, #2563eb);
|
|
@@ -338,6 +363,8 @@ function handleDayKeydown(e, dayMs) {
|
|
|
338
363
|
}
|
|
339
364
|
|
|
340
365
|
/* ─── Events column ──────────────────────────────── */
|
|
366
|
+
/* pointer-events pass through to the full-row target underneath;
|
|
367
|
+
only the chips (and "+N more") re-capture them. */
|
|
341
368
|
.mw-events {
|
|
342
369
|
flex: 1;
|
|
343
370
|
min-width: 0;
|
|
@@ -346,12 +373,12 @@ function handleDayKeydown(e, dayMs) {
|
|
|
346
373
|
gap: 4px;
|
|
347
374
|
position: relative;
|
|
348
375
|
z-index: 2;
|
|
376
|
+
pointer-events: none;
|
|
349
377
|
}
|
|
350
378
|
|
|
351
379
|
.mw-empty {
|
|
352
|
-
font: 400
|
|
353
|
-
color: var(--dt-text-
|
|
354
|
-
font-style: italic;
|
|
380
|
+
font: 400 13px/1 var(--dt-sans, system-ui, sans-serif);
|
|
381
|
+
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
355
382
|
}
|
|
356
383
|
|
|
357
384
|
/* ─── Event chip ─────────────────────────────────── */
|
|
@@ -359,6 +386,7 @@ function handleDayKeydown(e, dayMs) {
|
|
|
359
386
|
display: flex;
|
|
360
387
|
align-items: center;
|
|
361
388
|
gap: 0;
|
|
389
|
+
min-height: 44px;
|
|
362
390
|
border-radius: 6px;
|
|
363
391
|
background: color-mix(in srgb, var(--ev-color) 10%, var(--dt-surface, #f9fafb));
|
|
364
392
|
overflow: hidden;
|
|
@@ -368,6 +396,7 @@ function handleDayKeydown(e, dayMs) {
|
|
|
368
396
|
border: none;
|
|
369
397
|
text-align: left;
|
|
370
398
|
padding: 0;
|
|
399
|
+
pointer-events: auto;
|
|
371
400
|
}
|
|
372
401
|
.mw-ev:active {
|
|
373
402
|
background: color-mix(in srgb, var(--ev-color) 20%, var(--dt-surface, #f9fafb));
|
|
@@ -381,22 +410,32 @@ function handleDayKeydown(e, dayMs) {
|
|
|
381
410
|
.mw-ev--allday {
|
|
382
411
|
background: color-mix(in srgb, var(--ev-color) 14%, var(--dt-surface, #f9fafb));
|
|
383
412
|
}
|
|
413
|
+
/* Status treatments: token-level dims + a non-opacity signal
|
|
414
|
+
(strikethrough / border style) — never a bare opacity on the chip. */
|
|
384
415
|
.mw-ev--cancelled {
|
|
385
|
-
|
|
416
|
+
background: color-mix(in srgb, var(--ev-color) 5%, var(--dt-surface, #f9fafb));
|
|
386
417
|
}
|
|
387
418
|
.mw-ev--cancelled .mw-ev-title {
|
|
388
419
|
text-decoration: line-through;
|
|
420
|
+
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
421
|
+
}
|
|
422
|
+
.mw-ev--cancelled .mw-ev-stripe {
|
|
423
|
+
opacity: 0.45; /* decorative bar only */
|
|
389
424
|
}
|
|
390
425
|
.mw-ev--tentative {
|
|
391
|
-
|
|
392
|
-
border: 1px dashed color-mix(in srgb, var(--ev-color)
|
|
426
|
+
background: color-mix(in srgb, var(--ev-color) 6%, var(--dt-surface, #f9fafb));
|
|
427
|
+
border: 1px dashed color-mix(in srgb, var(--ev-color) 45%, transparent);
|
|
393
428
|
}
|
|
394
429
|
.mw-ev--full {
|
|
395
|
-
|
|
430
|
+
background: color-mix(in srgb, var(--ev-color) 6%, var(--dt-surface, #f9fafb));
|
|
431
|
+
border: 1px solid color-mix(in srgb, var(--ev-color) 30%, transparent);
|
|
432
|
+
}
|
|
433
|
+
.mw-ev--full .mw-ev-title {
|
|
434
|
+
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
396
435
|
}
|
|
397
436
|
.mw-ev--limited {
|
|
398
|
-
|
|
399
|
-
border: 1px dashed color-mix(in srgb, var(--ev-color)
|
|
437
|
+
background: color-mix(in srgb, var(--ev-color) 8%, var(--dt-surface, #f9fafb));
|
|
438
|
+
border: 1px dashed color-mix(in srgb, var(--ev-color) 45%, transparent);
|
|
400
439
|
}
|
|
401
440
|
|
|
402
441
|
.mw-ev-stripe {
|
|
@@ -417,7 +456,7 @@ function handleDayKeydown(e, dayMs) {
|
|
|
417
456
|
}
|
|
418
457
|
|
|
419
458
|
.mw-ev-title {
|
|
420
|
-
font: 500
|
|
459
|
+
font: 500 15px/1.2 var(--dt-sans, system-ui, sans-serif);
|
|
421
460
|
color: var(--dt-text, rgba(0, 0, 0, 0.87));
|
|
422
461
|
white-space: nowrap;
|
|
423
462
|
overflow: hidden;
|
|
@@ -427,16 +466,39 @@ function handleDayKeydown(e, dayMs) {
|
|
|
427
466
|
}
|
|
428
467
|
|
|
429
468
|
.mw-ev-time {
|
|
430
|
-
font: 400
|
|
431
|
-
color: var(--dt-text-
|
|
469
|
+
font: 400 12px/1 var(--dt-mono, ui-monospace, monospace);
|
|
470
|
+
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
432
471
|
white-space: nowrap;
|
|
433
472
|
flex-shrink: 0;
|
|
434
473
|
}
|
|
435
474
|
|
|
436
475
|
.mw-ev-more {
|
|
437
|
-
font:
|
|
438
|
-
color: var(--dt-text-
|
|
439
|
-
padding: 2px
|
|
476
|
+
font: 500 12px/1 var(--dt-sans, system-ui, sans-serif);
|
|
477
|
+
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
478
|
+
padding: 2px 4px;
|
|
479
|
+
min-height: 32px;
|
|
480
|
+
border: none;
|
|
481
|
+
background: transparent;
|
|
482
|
+
cursor: pointer;
|
|
483
|
+
text-align: left;
|
|
484
|
+
align-self: flex-start;
|
|
485
|
+
position: relative;
|
|
486
|
+
pointer-events: auto;
|
|
487
|
+
-webkit-tap-highlight-color: transparent;
|
|
488
|
+
}
|
|
489
|
+
/* Hit-slop: 44px effective touch target */
|
|
490
|
+
.mw-ev-more::before {
|
|
491
|
+
content: '';
|
|
492
|
+
position: absolute;
|
|
493
|
+
left: 0;
|
|
494
|
+
right: 0;
|
|
495
|
+
top: 50%;
|
|
496
|
+
transform: translateY(-50%);
|
|
497
|
+
height: 44px;
|
|
498
|
+
}
|
|
499
|
+
.mw-ev-more:focus-visible {
|
|
500
|
+
outline: none;
|
|
501
|
+
box-shadow: 0 0 0 2px var(--dt-accent, #2563eb);
|
|
440
502
|
}
|
|
441
503
|
|
|
442
504
|
/* ─── Chevron ────────────────────────────────────── */
|
|
@@ -450,7 +512,7 @@ function handleDayKeydown(e, dayMs) {
|
|
|
450
512
|
|
|
451
513
|
/* ─── Focus ──────────────────────────────────────── */
|
|
452
514
|
.mw-ev:focus-visible {
|
|
453
|
-
outline:
|
|
454
|
-
|
|
515
|
+
outline: none;
|
|
516
|
+
box-shadow: 0 0 0 2px var(--dt-accent, #2563eb);
|
|
455
517
|
}
|
|
456
518
|
</style>
|
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
}
|
|
28
|
+
export declare function createSwipe(cb: SwipeCallbacks): SwipeHandlers;
|
|
@@ -0,0 +1,64 @@
|
|
|
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
|
+
};
|
|
64
|
+
}
|