@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
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
Large touch targets, swipe left/right to change day.
|
|
6
6
|
Events positioned absolutely within hour lanes.
|
|
7
7
|
-->
|
|
8
|
-
<script lang="ts">import {
|
|
8
|
+
<script lang="ts">import { untrack } from "svelte";
|
|
9
9
|
import { useCalendarContext } from "../shared/context.svelte.js";
|
|
10
10
|
import EventContent from "../shared/EventContent.svelte";
|
|
11
11
|
import { createClock } from "../../core/clock.svelte.js";
|
|
12
12
|
import { DAY_MS, HOUR_MS, sod, isAllDay, isMultiDay, segmentForDay } from "../../core/time.js";
|
|
13
|
-
import { fmtH, fmtTime
|
|
14
|
-
|
|
13
|
+
import { fmtH, fmtTime } from "../../core/locale.js";
|
|
14
|
+
import { createSwipe } from "./swipe.js";
|
|
15
15
|
let {
|
|
16
16
|
height = null,
|
|
17
17
|
events = [],
|
|
@@ -25,6 +25,7 @@ let {
|
|
|
25
25
|
visibleHours
|
|
26
26
|
} = $props();
|
|
27
27
|
const ctx = useCalendarContext();
|
|
28
|
+
const L = $derived(ctx.labels);
|
|
28
29
|
const viewState = $derived(ctx.viewState);
|
|
29
30
|
const autoHeight = $derived(ctx.autoHeight);
|
|
30
31
|
const oneventhover = $derived(ctx.oneventhover);
|
|
@@ -45,7 +46,6 @@ const gridHeight = $derived(hourCount * HOUR_HEIGHT);
|
|
|
45
46
|
const dayMs = $derived(focusDate ? sod(focusDate.getTime()) : clock.today);
|
|
46
47
|
const dayEnd = $derived(dayMs + DAY_MS);
|
|
47
48
|
const isToday = $derived(dayMs === clock.today);
|
|
48
|
-
const isPast = $derived(dayMs < clock.today);
|
|
49
49
|
const isDisabled = $derived(disabledSet.has(dayMs));
|
|
50
50
|
$effect(() => {
|
|
51
51
|
if (!loadRangeCtx) return;
|
|
@@ -66,6 +66,10 @@ const allDayEvents = $derived.by(() => {
|
|
|
66
66
|
}
|
|
67
67
|
return segs;
|
|
68
68
|
});
|
|
69
|
+
const isEmpty = $derived(timedEvents.length === 0 && allDayEvents.length === 0);
|
|
70
|
+
const ALLDAY_MAX = 3;
|
|
71
|
+
let allDayExpanded = $state(false);
|
|
72
|
+
const visibleAllDay = $derived(allDayExpanded ? allDayEvents : allDayEvents.slice(0, ALLDAY_MAX));
|
|
69
73
|
const positionedEvents = $derived.by(() => {
|
|
70
74
|
const now = clock.tick;
|
|
71
75
|
const sorted = [...timedEvents];
|
|
@@ -166,45 +170,33 @@ function isBlockedAt(hour) {
|
|
|
166
170
|
return hour >= slot.start && hour < slot.end;
|
|
167
171
|
});
|
|
168
172
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
function onTouchStart(e) {
|
|
176
|
-
const t = e.touches[0];
|
|
177
|
-
touchStartX = t.clientX;
|
|
178
|
-
touchStartY = t.clientY;
|
|
179
|
-
swiping = true;
|
|
180
|
-
swipeOffset = 0;
|
|
181
|
-
}
|
|
182
|
-
function onTouchMove(e) {
|
|
183
|
-
if (!swiping) return;
|
|
184
|
-
const t = e.touches[0];
|
|
185
|
-
const dx = t.clientX - touchStartX;
|
|
186
|
-
const dy = t.clientY - touchStartY;
|
|
187
|
-
if (Math.abs(dy) > Math.abs(dx) * 0.8) {
|
|
188
|
-
swiping = false;
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
swipeOffset = dx;
|
|
173
|
+
function statusText(ev) {
|
|
174
|
+
if (ev.status === "cancelled") return ` (${L.cancelled})`;
|
|
175
|
+
if (ev.status === "tentative") return ` (${L.tentative})`;
|
|
176
|
+
if (ev.status === "full") return ` (${L.full})`;
|
|
177
|
+
if (ev.status === "limited") return ` (${L.limited})`;
|
|
178
|
+
return "";
|
|
192
179
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
180
|
+
let swipeOffset = $state(0);
|
|
181
|
+
let swipeAnimate = $state(false);
|
|
182
|
+
const swipe = createSwipe({
|
|
183
|
+
disabled: () => !!drag?.active || mbCreateStarted || mbRsStarted || longPressTimer !== null,
|
|
184
|
+
onmove: (dx) => {
|
|
185
|
+
swipeAnimate = false;
|
|
186
|
+
swipeOffset = dx;
|
|
187
|
+
},
|
|
188
|
+
onend: (dir) => {
|
|
189
|
+
if (dir !== 0) {
|
|
190
|
+
swipeAnimate = false;
|
|
191
|
+
swipeOffset = 0;
|
|
192
|
+
if (dir > 0) viewState?.prev();
|
|
193
|
+
else viewState?.next();
|
|
201
194
|
} else {
|
|
202
|
-
|
|
195
|
+
swipeAnimate = true;
|
|
196
|
+
swipeOffset = 0;
|
|
203
197
|
}
|
|
204
198
|
}
|
|
205
|
-
|
|
206
|
-
swiping = false;
|
|
207
|
-
}
|
|
199
|
+
});
|
|
208
200
|
function handleGridClick(e) {
|
|
209
201
|
if (suppressGridClick) {
|
|
210
202
|
suppressGridClick = false;
|
|
@@ -212,22 +204,46 @@ function handleGridClick(e) {
|
|
|
212
204
|
}
|
|
213
205
|
if (!oneventcreate || readOnly || isDisabled) return;
|
|
214
206
|
if (e.target.closest(".mb-event")) return;
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
const
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
207
|
+
const tMs = gridTimeMs(e.clientY);
|
|
208
|
+
if (isBlockedAt((tMs - dayMs) / HOUR_MS)) return;
|
|
209
|
+
const startMs = clampToDay(Math.floor(tMs / SNAP_MS) * SNAP_MS);
|
|
210
|
+
const durMin = minDuration ?? 60;
|
|
211
|
+
oneventcreate({ start: new Date(startMs), end: new Date(startMs + durMin * 6e4) });
|
|
212
|
+
}
|
|
213
|
+
function onGridKeydown(e) {
|
|
214
|
+
if (e.key !== "Enter" && e.key !== " ") return;
|
|
215
|
+
if (!oneventcreate || readOnly || isDisabled) return;
|
|
216
|
+
e.preventDefault();
|
|
217
|
+
const raw = isToday ? clock.tick : dayMs + startHour * HOUR_MS;
|
|
218
|
+
const startMs = clampToDay(Math.ceil(raw / SNAP_MS) * SNAP_MS);
|
|
219
|
+
if (isBlockedAt((startMs - dayMs) / HOUR_MS)) return;
|
|
220
|
+
const durMin = minDuration ?? 60;
|
|
221
|
+
oneventcreate({ start: new Date(startMs), end: new Date(startMs + durMin * 6e4) });
|
|
225
222
|
}
|
|
226
223
|
const CREATE_THRESHOLD = 4;
|
|
224
|
+
const LONG_PRESS_MS = 350;
|
|
225
|
+
const LONG_PRESS_TOLERANCE = 8;
|
|
227
226
|
let suppressGridClick = false;
|
|
227
|
+
let mbCreateStartX = 0;
|
|
228
228
|
let mbCreateStartY = 0;
|
|
229
229
|
let mbCreateAnchorMs = 0;
|
|
230
230
|
let mbCreateStarted = false;
|
|
231
|
+
let longPressTimer = null;
|
|
232
|
+
function blockTouchScroll(e) {
|
|
233
|
+
e.preventDefault();
|
|
234
|
+
}
|
|
235
|
+
function addTouchScrollBlock() {
|
|
236
|
+
window.addEventListener("touchmove", blockTouchScroll, { passive: false });
|
|
237
|
+
}
|
|
238
|
+
function removeTouchScrollBlock() {
|
|
239
|
+
window.removeEventListener("touchmove", blockTouchScroll);
|
|
240
|
+
}
|
|
241
|
+
function clearLongPress() {
|
|
242
|
+
if (longPressTimer !== null) {
|
|
243
|
+
clearTimeout(longPressTimer);
|
|
244
|
+
longPressTimer = null;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
231
247
|
function gridTimeMs(clientY) {
|
|
232
248
|
const rect = gridEl.getBoundingClientRect();
|
|
233
249
|
const y = clientY - rect.top + gridEl.scrollTop;
|
|
@@ -236,12 +252,26 @@ function gridTimeMs(clientY) {
|
|
|
236
252
|
function clampToDay(ms) {
|
|
237
253
|
return Math.max(dayMs + startHour * HOUR_MS, Math.min(dayMs + endHour * HOUR_MS, ms));
|
|
238
254
|
}
|
|
255
|
+
function startGridCreate() {
|
|
256
|
+
if (!drag) return;
|
|
257
|
+
mbCreateStarted = true;
|
|
258
|
+
mbCreateAnchorMs = clampToDay(Math.floor(mbCreateAnchorMs / SNAP_MS) * SNAP_MS);
|
|
259
|
+
drag.beginCreate(new Date(mbCreateAnchorMs), new Date(mbCreateAnchorMs + SNAP_MS));
|
|
260
|
+
addTouchScrollBlock();
|
|
261
|
+
}
|
|
239
262
|
function onGridPointerDown(e) {
|
|
240
263
|
if (e.button !== 0 || !drag || !oneventcreate || readOnly || isDisabled) return;
|
|
241
264
|
if (e.target.closest(".mb-event")) return;
|
|
265
|
+
mbCreateStartX = e.clientX;
|
|
242
266
|
mbCreateStartY = e.clientY;
|
|
243
267
|
mbCreateAnchorMs = gridTimeMs(e.clientY);
|
|
244
268
|
mbCreateStarted = false;
|
|
269
|
+
if (e.pointerType === "touch") {
|
|
270
|
+
longPressTimer = setTimeout(() => {
|
|
271
|
+
longPressTimer = null;
|
|
272
|
+
startGridCreate();
|
|
273
|
+
}, LONG_PRESS_MS);
|
|
274
|
+
}
|
|
245
275
|
window.addEventListener("pointermove", onGridCreateMove);
|
|
246
276
|
window.addEventListener("pointerup", onGridCreateUp, { once: true });
|
|
247
277
|
window.addEventListener("pointercancel", onGridCreateCancel, { once: true });
|
|
@@ -249,10 +279,14 @@ function onGridPointerDown(e) {
|
|
|
249
279
|
function onGridCreateMove(e) {
|
|
250
280
|
if (!drag) return;
|
|
251
281
|
if (!mbCreateStarted) {
|
|
282
|
+
if (longPressTimer !== null) {
|
|
283
|
+
const moved = Math.hypot(e.clientX - mbCreateStartX, e.clientY - mbCreateStartY);
|
|
284
|
+
if (moved > LONG_PRESS_TOLERANCE) cleanupGridCreate();
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (e.pointerType === "touch") return;
|
|
252
288
|
if (Math.abs(e.clientY - mbCreateStartY) < CREATE_THRESHOLD) return;
|
|
253
|
-
|
|
254
|
-
mbCreateAnchorMs = clampToDay(Math.floor(mbCreateAnchorMs / SNAP_MS) * SNAP_MS);
|
|
255
|
-
drag.beginCreate(new Date(mbCreateAnchorMs), new Date(mbCreateAnchorMs + SNAP_MS));
|
|
289
|
+
startGridCreate();
|
|
256
290
|
}
|
|
257
291
|
const snapped = clampToDay(Math.round(gridTimeMs(e.clientY) / SNAP_MS) * SNAP_MS);
|
|
258
292
|
drag.updatePointer(
|
|
@@ -261,6 +295,8 @@ function onGridCreateMove(e) {
|
|
|
261
295
|
);
|
|
262
296
|
}
|
|
263
297
|
function cleanupGridCreate() {
|
|
298
|
+
clearLongPress();
|
|
299
|
+
removeTouchScrollBlock();
|
|
264
300
|
window.removeEventListener("pointermove", onGridCreateMove);
|
|
265
301
|
window.removeEventListener("pointerup", onGridCreateUp);
|
|
266
302
|
window.removeEventListener("pointercancel", onGridCreateCancel);
|
|
@@ -280,6 +316,9 @@ function onGridCreateCancel() {
|
|
|
280
316
|
if (drag && mbCreateStarted) drag.cancel();
|
|
281
317
|
cleanupGridCreate();
|
|
282
318
|
}
|
|
319
|
+
function onGridContextMenu(e) {
|
|
320
|
+
if (mbCreateStarted || longPressTimer !== null) e.preventDefault();
|
|
321
|
+
}
|
|
283
322
|
let suppressEventClick = false;
|
|
284
323
|
let mbRsStartY = 0;
|
|
285
324
|
let mbRsStarted = false;
|
|
@@ -303,6 +342,7 @@ function onResizeMove(e) {
|
|
|
303
342
|
if (Math.abs(e.clientY - mbRsStartY) < CREATE_THRESHOLD) return;
|
|
304
343
|
mbRsStarted = true;
|
|
305
344
|
drag.beginResize(ev.id, mbRsEdge, ev.start, ev.end);
|
|
345
|
+
addTouchScrollBlock();
|
|
306
346
|
}
|
|
307
347
|
const snapped = clampToDay(Math.round(gridTimeMs(e.clientY) / SNAP_MS) * SNAP_MS);
|
|
308
348
|
if (mbRsEdge === "end") {
|
|
@@ -314,6 +354,7 @@ function onResizeMove(e) {
|
|
|
314
354
|
}
|
|
315
355
|
}
|
|
316
356
|
function cleanupResize() {
|
|
357
|
+
removeTouchScrollBlock();
|
|
317
358
|
window.removeEventListener("pointermove", onResizeMove);
|
|
318
359
|
window.removeEventListener("pointerup", onResizeUp);
|
|
319
360
|
window.removeEventListener("pointercancel", onResizeCancel);
|
|
@@ -337,11 +378,13 @@ function onResizeCancel() {
|
|
|
337
378
|
cleanupResize();
|
|
338
379
|
}
|
|
339
380
|
let gridEl;
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
381
|
+
$effect(() => {
|
|
382
|
+
void dayMs;
|
|
383
|
+
const el = gridEl;
|
|
384
|
+
if (!el) return;
|
|
385
|
+
untrack(() => {
|
|
386
|
+
if (nowOffset >= 0) el.scrollTop = Math.max(0, nowOffset - 120);
|
|
387
|
+
});
|
|
345
388
|
});
|
|
346
389
|
</script>
|
|
347
390
|
|
|
@@ -352,147 +395,177 @@ onMount(() => {
|
|
|
352
395
|
style:height={autoHeight ? undefined : (height ? `${height}px` : '100%')}
|
|
353
396
|
role="region"
|
|
354
397
|
aria-label={L.dayPlanner}
|
|
355
|
-
ontouchstart={
|
|
356
|
-
ontouchmove={
|
|
357
|
-
ontouchend={
|
|
398
|
+
ontouchstart={swipe.ontouchstart}
|
|
399
|
+
ontouchmove={swipe.ontouchmove}
|
|
400
|
+
ontouchend={swipe.ontouchend}
|
|
358
401
|
>
|
|
359
|
-
<!-- All-day events bar -->
|
|
360
|
-
{#if allDayEvents.length > 0}
|
|
361
|
-
<div class="mb-allday">
|
|
362
|
-
{#each allDayEvents.slice(0, 3) as seg (seg.ev.id)}
|
|
363
|
-
<button
|
|
364
|
-
class="mb-allday-chip"
|
|
365
|
-
class:mb-allday-chip--selected={selectedEventId === seg.ev.id}
|
|
366
|
-
style:--ev-color={seg.ev.color ?? 'var(--dt-accent)'}
|
|
367
|
-
onclick={() => oneventclick?.(seg.ev)}
|
|
368
|
-
>
|
|
369
|
-
<span class="mb-allday-dot"></span>
|
|
370
|
-
<span class="mb-allday-title">{seg.ev.title}</span>
|
|
371
|
-
{#if seg.totalDays > 1}
|
|
372
|
-
<span class="mb-allday-span">{seg.dayIndex}/{seg.totalDays}</span>
|
|
373
|
-
{/if}
|
|
374
|
-
</button>
|
|
375
|
-
{/each}
|
|
376
|
-
{#if allDayEvents.length > 3}
|
|
377
|
-
<span class="mb-allday-more">{L.nMore(allDayEvents.length - 3)}</span>
|
|
378
|
-
{/if}
|
|
379
|
-
</div>
|
|
380
|
-
{/if}
|
|
381
|
-
|
|
382
|
-
<!-- Scrollable time grid -->
|
|
383
402
|
<div
|
|
384
|
-
class="mb-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
onpointerdown={onGridPointerDown}
|
|
388
|
-
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') handleGridClick(e as unknown as MouseEvent); }}
|
|
389
|
-
role="grid"
|
|
390
|
-
tabindex="-1"
|
|
403
|
+
class="mb-swipe"
|
|
404
|
+
class:mb-swipe--animate={swipeAnimate}
|
|
405
|
+
style:transform={swipeOffset !== 0 ? `translateX(${swipeOffset}px)` : undefined}
|
|
391
406
|
>
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
{
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
{#if slot?.label}
|
|
408
|
-
<span class="mb-blocked-label">{slot.label}</span>
|
|
409
|
-
{/if}
|
|
410
|
-
{/if}
|
|
411
|
-
</div>
|
|
412
|
-
{/each}
|
|
413
|
-
|
|
414
|
-
<!-- Now line -->
|
|
415
|
-
{#if nowOffset >= 0}
|
|
416
|
-
<div class="mb-now" style:top="{nowOffset}px">
|
|
417
|
-
<span class="mb-now-label">{clock.hm}</span>
|
|
418
|
-
<div class="mb-now-line"></div>
|
|
419
|
-
</div>
|
|
420
|
-
{/if}
|
|
421
|
-
|
|
422
|
-
<!-- Events -->
|
|
423
|
-
{#each positionedEvents as p (p.ev.id)}
|
|
424
|
-
<button
|
|
425
|
-
class="mb-event"
|
|
426
|
-
class:mb-event--selected={selectedEventId === p.ev.id}
|
|
427
|
-
class:mb-event--current={p.isCurrent}
|
|
428
|
-
class:mb-event--next={p.isNext}
|
|
429
|
-
class:mb-event--cancelled={p.ev.status === 'cancelled'}
|
|
430
|
-
class:mb-event--tentative={p.ev.status === 'tentative'}
|
|
431
|
-
class:mb-event--full={p.ev.status === 'full'}
|
|
432
|
-
class:mb-event--limited={p.ev.status === 'limited'}
|
|
433
|
-
class:mb-event--resizing={p.isResizing}
|
|
434
|
-
style:top="{p.top}px"
|
|
435
|
-
style:height="{p.height}px"
|
|
436
|
-
style:left={p.left}
|
|
437
|
-
style:width={p.width}
|
|
438
|
-
style:--ev-color={p.ev.color ?? 'var(--dt-accent)'}
|
|
439
|
-
onclick={(e) => { e.stopPropagation(); if (suppressEventClick) { suppressEventClick = false; return; } oneventclick?.(p.ev); }}
|
|
440
|
-
onpointerenter={() => oneventhover?.(p.ev)}
|
|
441
|
-
aria-label="{p.ev.title}{p.ev.status === 'cancelled' ? ' (cancelled)' : ''}{p.ev.status === 'tentative' ? ' (tentative)' : ''}{p.ev.status === 'full' ? ' (full)' : ''}{p.ev.status === 'limited' ? ' (limited)' : ''}{p.isCurrent ? `, ${L.inProgress}` : ''}{p.isNext ? `, ${L.upNext}` : ''}"
|
|
442
|
-
>
|
|
443
|
-
<div class="mb-ev-stripe"></div>
|
|
444
|
-
<div class="mb-ev-body">
|
|
445
|
-
<EventContent event={p.ev}>
|
|
446
|
-
<span class="mb-ev-title">{p.ev.title}</span>
|
|
447
|
-
{#if p.height > 32}
|
|
448
|
-
<span class="mb-ev-time">{fmtTime(p.ev.start, locale)} – {fmtTime(p.ev.end, locale)}</span>
|
|
407
|
+
<!-- All-day events bar -->
|
|
408
|
+
{#if allDayEvents.length > 0}
|
|
409
|
+
<div class="mb-allday" class:mb-allday--expanded={allDayExpanded}>
|
|
410
|
+
{#each visibleAllDay as seg (seg.ev.id)}
|
|
411
|
+
<button
|
|
412
|
+
type="button"
|
|
413
|
+
class="mb-allday-chip"
|
|
414
|
+
class:mb-allday-chip--selected={selectedEventId === seg.ev.id}
|
|
415
|
+
style:--ev-color={seg.ev.color ?? 'var(--dt-accent)'}
|
|
416
|
+
onclick={() => oneventclick?.(seg.ev)}
|
|
417
|
+
>
|
|
418
|
+
<span class="mb-allday-dot"></span>
|
|
419
|
+
<span class="mb-allday-title">{seg.ev.title}</span>
|
|
420
|
+
{#if seg.totalDays > 1}
|
|
421
|
+
<span class="mb-allday-span">{seg.dayIndex}/{seg.totalDays}</span>
|
|
449
422
|
{/if}
|
|
450
|
-
|
|
451
|
-
|
|
423
|
+
</button>
|
|
424
|
+
{/each}
|
|
425
|
+
{#if allDayEvents.length > ALLDAY_MAX}
|
|
426
|
+
<button
|
|
427
|
+
type="button"
|
|
428
|
+
class="mb-allday-more"
|
|
429
|
+
aria-expanded={allDayExpanded}
|
|
430
|
+
onclick={() => { allDayExpanded = !allDayExpanded; }}
|
|
431
|
+
>
|
|
432
|
+
{allDayExpanded ? L.showLess : L.nMore(allDayEvents.length - ALLDAY_MAX)}
|
|
433
|
+
</button>
|
|
434
|
+
{/if}
|
|
435
|
+
</div>
|
|
436
|
+
{/if}
|
|
437
|
+
|
|
438
|
+
<!-- Scrollable time grid -->
|
|
439
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex, a11y_no_noninteractive_element_interactions -->
|
|
440
|
+
<div
|
|
441
|
+
class="mb-grid"
|
|
442
|
+
bind:this={gridEl}
|
|
443
|
+
onclick={handleGridClick}
|
|
444
|
+
onpointerdown={onGridPointerDown}
|
|
445
|
+
onkeydown={onGridKeydown}
|
|
446
|
+
oncontextmenu={onGridContextMenu}
|
|
447
|
+
role="region"
|
|
448
|
+
aria-label={L.scrollableDayPlanner}
|
|
449
|
+
tabindex="0"
|
|
450
|
+
>
|
|
451
|
+
<div class="mb-grid-inner" style:height="{gridHeight}px">
|
|
452
|
+
<!-- Hour lanes -->
|
|
453
|
+
{#each { length: hourCount } as _, h}
|
|
454
|
+
{@const hour = startHour + h}
|
|
455
|
+
{@const blocked = isBlockedAt(hour)}
|
|
456
|
+
<div
|
|
457
|
+
class="mb-hour"
|
|
458
|
+
class:mb-hour--blocked={blocked}
|
|
459
|
+
style:top="{h * HOUR_HEIGHT}px"
|
|
460
|
+
style:height="{HOUR_HEIGHT}px"
|
|
461
|
+
>
|
|
462
|
+
<div class="mb-hour-label">{fmtH(hour, locale)}</div>
|
|
463
|
+
<div class="mb-hour-line"></div>
|
|
464
|
+
{#if blocked && blockedSlots}
|
|
465
|
+
{@const slot = blockedSlots.find(s => (!s.day || s.day === (new Date(dayMs).getDay() === 0 ? 7 : new Date(dayMs).getDay())) && hour >= s.start && hour < s.end)}
|
|
466
|
+
{#if slot?.label}
|
|
467
|
+
<span class="mb-blocked-label">{slot.label}</span>
|
|
468
|
+
{/if}
|
|
452
469
|
{/if}
|
|
453
|
-
|
|
454
|
-
|
|
470
|
+
</div>
|
|
471
|
+
{/each}
|
|
472
|
+
|
|
473
|
+
<!-- Now line -->
|
|
474
|
+
{#if nowOffset >= 0}
|
|
475
|
+
<div class="mb-now" style:top="{nowOffset}px">
|
|
476
|
+
<span class="mb-now-label">{clock.hm}</span>
|
|
477
|
+
<div class="mb-now-line"></div>
|
|
478
|
+
</div>
|
|
479
|
+
{/if}
|
|
480
|
+
|
|
481
|
+
<!-- Events -->
|
|
482
|
+
{#each positionedEvents as p (p.ev.id)}
|
|
483
|
+
<button
|
|
484
|
+
type="button"
|
|
485
|
+
class="mb-event"
|
|
486
|
+
class:mb-event--selected={selectedEventId === p.ev.id}
|
|
487
|
+
class:mb-event--current={p.isCurrent}
|
|
488
|
+
class:mb-event--next={p.isNext}
|
|
489
|
+
class:mb-event--cancelled={p.ev.status === 'cancelled'}
|
|
490
|
+
class:mb-event--tentative={p.ev.status === 'tentative'}
|
|
491
|
+
class:mb-event--full={p.ev.status === 'full'}
|
|
492
|
+
class:mb-event--limited={p.ev.status === 'limited'}
|
|
493
|
+
class:mb-event--resizing={p.isResizing}
|
|
494
|
+
class:mb-event--short={p.height < 44}
|
|
495
|
+
style:top="{p.top}px"
|
|
496
|
+
style:height="{p.height}px"
|
|
497
|
+
style:left={p.left}
|
|
498
|
+
style:width={p.width}
|
|
499
|
+
style:--ev-color={p.ev.color ?? 'var(--dt-accent)'}
|
|
500
|
+
onclick={(e) => { e.stopPropagation(); if (suppressEventClick) { suppressEventClick = false; return; } oneventclick?.(p.ev); }}
|
|
501
|
+
onpointerenter={() => oneventhover?.(p.ev)}
|
|
502
|
+
aria-label="{p.ev.title}{statusText(p.ev)}, {fmtTime(p.ev.start, locale)} – {fmtTime(p.ev.end, locale)}{p.isCurrent ? `, ${L.inProgress}` : ''}{p.isNext ? `, ${L.upNext}` : ''}"
|
|
503
|
+
>
|
|
504
|
+
<div class="mb-ev-stripe"></div>
|
|
505
|
+
<div class="mb-ev-body">
|
|
506
|
+
<EventContent event={p.ev}>
|
|
507
|
+
<span class="mb-ev-title">{p.ev.title}</span>
|
|
508
|
+
{#if p.height > 32}
|
|
509
|
+
<span class="mb-ev-time">{fmtTime(p.ev.start, locale)} – {fmtTime(p.ev.end, locale)}</span>
|
|
510
|
+
{/if}
|
|
511
|
+
{#if p.ev.subtitle && p.height > 48}
|
|
512
|
+
<span class="mb-ev-sub">{p.ev.subtitle}</span>
|
|
513
|
+
{/if}
|
|
514
|
+
{#if p.ev.location && p.height > 56}
|
|
515
|
+
<span class="mb-ev-loc">{p.ev.location}</span>
|
|
516
|
+
{/if}
|
|
517
|
+
{#if p.ev.tags?.length && p.height > 56}
|
|
518
|
+
<div class="mb-ev-tags">
|
|
519
|
+
{#each p.ev.tags as tag}
|
|
520
|
+
<span class="mb-ev-tag">{tag}</span>
|
|
521
|
+
{/each}
|
|
522
|
+
</div>
|
|
523
|
+
{/if}
|
|
524
|
+
</EventContent>
|
|
525
|
+
</div>
|
|
526
|
+
{#if p.isCurrent}
|
|
527
|
+
<span class="mb-ev-live"></span>
|
|
528
|
+
{:else if p.isNext}
|
|
529
|
+
<span class="mb-ev-next-badge">{L.upNext}</span>
|
|
455
530
|
{/if}
|
|
456
|
-
{#if p.ev.
|
|
457
|
-
<
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
{
|
|
461
|
-
|
|
531
|
+
{#if !readOnly && !p.ev.data?.readOnly}
|
|
532
|
+
<span
|
|
533
|
+
class="mb-ev-handle mb-ev-handle--start"
|
|
534
|
+
aria-hidden="true"
|
|
535
|
+
onpointerdown={(e) => onResizePointerDown(e, p.ev, 'start')}
|
|
536
|
+
></span>
|
|
537
|
+
<span
|
|
538
|
+
class="mb-ev-handle mb-ev-handle--end"
|
|
539
|
+
aria-hidden="true"
|
|
540
|
+
onpointerdown={(e) => onResizePointerDown(e, p.ev, 'end')}
|
|
541
|
+
></span>
|
|
462
542
|
{/if}
|
|
463
|
-
|
|
543
|
+
</button>
|
|
544
|
+
{/each}
|
|
545
|
+
|
|
546
|
+
<!-- Drag-to-create ghost -->
|
|
547
|
+
{#if !readOnly && drag?.active && drag.mode === 'create' && drag.payload}
|
|
548
|
+
{@const gTop = ((drag.payload.start.getTime() - dayMs) / HOUR_MS - startHour) * HOUR_HEIGHT}
|
|
549
|
+
{@const gH = Math.max(12, ((drag.payload.end.getTime() - drag.payload.start.getTime()) / HOUR_MS) * HOUR_HEIGHT)}
|
|
550
|
+
<div class="mb-create-ghost" style:top="{gTop}px" style:height="{gH}px" aria-hidden="true">
|
|
551
|
+
<span class="mb-create-ghost-time">
|
|
552
|
+
{fmtTime(drag.payload.start, locale)} – {fmtTime(drag.payload.end, locale)}
|
|
553
|
+
</span>
|
|
464
554
|
</div>
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
{:else if p.isNext}
|
|
468
|
-
<span class="mb-ev-next-badge">{L.upNext}</span>
|
|
469
|
-
{/if}
|
|
470
|
-
{#if !readOnly && !p.ev.data?.readOnly}
|
|
471
|
-
<span
|
|
472
|
-
class="mb-ev-handle mb-ev-handle--start"
|
|
473
|
-
aria-hidden="true"
|
|
474
|
-
onpointerdown={(e) => onResizePointerDown(e, p.ev, 'start')}
|
|
475
|
-
></span>
|
|
476
|
-
<span
|
|
477
|
-
class="mb-ev-handle mb-ev-handle--end"
|
|
478
|
-
aria-hidden="true"
|
|
479
|
-
onpointerdown={(e) => onResizePointerDown(e, p.ev, 'end')}
|
|
480
|
-
></span>
|
|
481
|
-
{/if}
|
|
482
|
-
</button>
|
|
483
|
-
{/each}
|
|
484
|
-
|
|
485
|
-
<!-- Drag-to-create ghost -->
|
|
486
|
-
{#if !readOnly && drag?.active && drag.mode === 'create' && drag.payload}
|
|
487
|
-
{@const gTop = ((drag.payload.start.getTime() - dayMs) / HOUR_MS - startHour) * HOUR_HEIGHT}
|
|
488
|
-
{@const gH = Math.max(12, ((drag.payload.end.getTime() - drag.payload.start.getTime()) / HOUR_MS) * HOUR_HEIGHT)}
|
|
489
|
-
<div class="mb-create-ghost" style:top="{gTop}px" style:height="{gH}px" aria-hidden="true">
|
|
490
|
-
<span class="mb-create-ghost-time">
|
|
491
|
-
{fmtTime(drag.payload.start, locale)} – {fmtTime(drag.payload.end, locale)}
|
|
492
|
-
</span>
|
|
493
|
-
</div>
|
|
494
|
-
{/if}
|
|
555
|
+
{/if}
|
|
556
|
+
</div>
|
|
495
557
|
</div>
|
|
558
|
+
|
|
559
|
+
<!-- Empty state — overlaid, taps pass through to the grid -->
|
|
560
|
+
{#if isEmpty}
|
|
561
|
+
<div class="mb-empty">
|
|
562
|
+
{#if ctx.emptySnippet}
|
|
563
|
+
{@render ctx.emptySnippet()}
|
|
564
|
+
{:else}
|
|
565
|
+
<span class="mb-empty-text">{L.nothingScheduled}</span>
|
|
566
|
+
{/if}
|
|
567
|
+
</div>
|
|
568
|
+
{/if}
|
|
496
569
|
</div>
|
|
497
570
|
</div>
|
|
498
571
|
|
|
@@ -507,9 +580,25 @@ onMount(() => {
|
|
|
507
580
|
overflow: hidden;
|
|
508
581
|
background: var(--dt-bg, #fff);
|
|
509
582
|
-webkit-tap-highlight-color: transparent;
|
|
583
|
+
touch-action: pan-y;
|
|
510
584
|
}
|
|
511
585
|
.mb--auto { overflow: visible; }
|
|
512
586
|
|
|
587
|
+
/* ─── Swipe wrapper (follows the finger) ─────────── */
|
|
588
|
+
.mb-swipe {
|
|
589
|
+
flex: 1;
|
|
590
|
+
min-height: 0;
|
|
591
|
+
display: flex;
|
|
592
|
+
flex-direction: column;
|
|
593
|
+
position: relative;
|
|
594
|
+
}
|
|
595
|
+
.mb-swipe--animate {
|
|
596
|
+
transition: transform 180ms ease;
|
|
597
|
+
}
|
|
598
|
+
@media (prefers-reduced-motion: reduce) {
|
|
599
|
+
.mb-swipe--animate { transition: none; }
|
|
600
|
+
}
|
|
601
|
+
|
|
513
602
|
/* ─── All-day bar ────────────────────────────────── */
|
|
514
603
|
.mb-allday {
|
|
515
604
|
display: flex;
|
|
@@ -522,12 +611,17 @@ onMount(() => {
|
|
|
522
611
|
align-items: center;
|
|
523
612
|
}
|
|
524
613
|
.mb-allday::-webkit-scrollbar { display: none; }
|
|
614
|
+
.mb-allday--expanded {
|
|
615
|
+
flex-wrap: wrap;
|
|
616
|
+
overflow-x: visible;
|
|
617
|
+
}
|
|
525
618
|
|
|
526
619
|
.mb-allday-chip {
|
|
527
620
|
display: flex;
|
|
528
621
|
align-items: center;
|
|
529
622
|
gap: 4px;
|
|
530
623
|
padding: 4px 8px;
|
|
624
|
+
min-height: 32px;
|
|
531
625
|
border-radius: 5px;
|
|
532
626
|
background: color-mix(in srgb, var(--ev-color) 12%, var(--dt-surface, #f9fafb));
|
|
533
627
|
border: none;
|
|
@@ -536,6 +630,17 @@ onMount(() => {
|
|
|
536
630
|
transition: background 120ms;
|
|
537
631
|
-webkit-tap-highlight-color: transparent;
|
|
538
632
|
max-width: 160px;
|
|
633
|
+
position: relative;
|
|
634
|
+
}
|
|
635
|
+
/* Hit-slop: 44px effective touch target */
|
|
636
|
+
.mb-allday-chip::before {
|
|
637
|
+
content: '';
|
|
638
|
+
position: absolute;
|
|
639
|
+
left: 0;
|
|
640
|
+
right: 0;
|
|
641
|
+
top: 50%;
|
|
642
|
+
transform: translateY(-50%);
|
|
643
|
+
height: 44px;
|
|
539
644
|
}
|
|
540
645
|
.mb-allday-chip:active {
|
|
541
646
|
background: color-mix(in srgb, var(--ev-color) 22%, var(--dt-surface, #f9fafb));
|
|
@@ -543,6 +648,10 @@ onMount(() => {
|
|
|
543
648
|
.mb-allday-chip--selected {
|
|
544
649
|
box-shadow: 0 0 0 1.5px var(--ev-color);
|
|
545
650
|
}
|
|
651
|
+
.mb-allday-chip:focus-visible {
|
|
652
|
+
outline: none;
|
|
653
|
+
box-shadow: 0 0 0 2px var(--dt-accent, #2563eb);
|
|
654
|
+
}
|
|
546
655
|
|
|
547
656
|
.mb-allday-dot {
|
|
548
657
|
width: 6px;
|
|
@@ -553,7 +662,7 @@ onMount(() => {
|
|
|
553
662
|
}
|
|
554
663
|
|
|
555
664
|
.mb-allday-title {
|
|
556
|
-
font: 500
|
|
665
|
+
font: 500 12px/1 var(--dt-sans, system-ui, sans-serif);
|
|
557
666
|
color: var(--dt-text, rgba(0, 0, 0, 0.87));
|
|
558
667
|
white-space: nowrap;
|
|
559
668
|
max-width: 100px;
|
|
@@ -562,16 +671,35 @@ onMount(() => {
|
|
|
562
671
|
}
|
|
563
672
|
|
|
564
673
|
.mb-allday-span {
|
|
565
|
-
font: 400
|
|
566
|
-
color: var(--dt-text-
|
|
674
|
+
font: 400 11px/1 var(--dt-sans, system-ui, sans-serif);
|
|
675
|
+
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
567
676
|
}
|
|
568
677
|
|
|
569
678
|
.mb-allday-more {
|
|
570
|
-
font: 500
|
|
679
|
+
font: 500 12px/1 var(--dt-sans, system-ui, sans-serif);
|
|
571
680
|
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
572
681
|
white-space: nowrap;
|
|
573
682
|
flex-shrink: 0;
|
|
574
|
-
padding: 0
|
|
683
|
+
padding: 0 6px;
|
|
684
|
+
min-height: 32px;
|
|
685
|
+
border: none;
|
|
686
|
+
background: transparent;
|
|
687
|
+
cursor: pointer;
|
|
688
|
+
position: relative;
|
|
689
|
+
-webkit-tap-highlight-color: transparent;
|
|
690
|
+
}
|
|
691
|
+
.mb-allday-more::before {
|
|
692
|
+
content: '';
|
|
693
|
+
position: absolute;
|
|
694
|
+
left: 0;
|
|
695
|
+
right: 0;
|
|
696
|
+
top: 50%;
|
|
697
|
+
transform: translateY(-50%);
|
|
698
|
+
height: 44px;
|
|
699
|
+
}
|
|
700
|
+
.mb-allday-more:focus-visible {
|
|
701
|
+
outline: none;
|
|
702
|
+
box-shadow: 0 0 0 2px var(--dt-accent, #2563eb);
|
|
575
703
|
}
|
|
576
704
|
|
|
577
705
|
/* ─── Grid ───────────────────────────────────────── */
|
|
@@ -579,6 +707,7 @@ onMount(() => {
|
|
|
579
707
|
flex: 1;
|
|
580
708
|
overflow-y: auto;
|
|
581
709
|
overflow-x: hidden;
|
|
710
|
+
overscroll-behavior: contain;
|
|
582
711
|
-webkit-overflow-scrolling: touch;
|
|
583
712
|
scrollbar-width: thin;
|
|
584
713
|
scrollbar-color: var(--dt-scrollbar, rgba(0, 0, 0, 0.1)) transparent;
|
|
@@ -586,12 +715,31 @@ onMount(() => {
|
|
|
586
715
|
padding-top: 8px;
|
|
587
716
|
}
|
|
588
717
|
.mb--auto .mb-grid { overflow-y: visible; }
|
|
718
|
+
.mb-grid:focus-visible {
|
|
719
|
+
outline: none;
|
|
720
|
+
box-shadow: inset 0 0 0 2px var(--dt-accent, #2563eb);
|
|
721
|
+
}
|
|
589
722
|
|
|
590
723
|
.mb-grid-inner {
|
|
591
724
|
position: relative;
|
|
592
725
|
min-width: 100%;
|
|
593
726
|
}
|
|
594
727
|
|
|
728
|
+
/* ─── Empty state ────────────────────────────────── */
|
|
729
|
+
.mb-empty {
|
|
730
|
+
position: absolute;
|
|
731
|
+
inset: 0;
|
|
732
|
+
display: flex;
|
|
733
|
+
align-items: center;
|
|
734
|
+
justify-content: center;
|
|
735
|
+
pointer-events: none;
|
|
736
|
+
z-index: 4;
|
|
737
|
+
}
|
|
738
|
+
.mb-empty-text {
|
|
739
|
+
font: 500 13px/1.4 var(--dt-sans, system-ui, sans-serif);
|
|
740
|
+
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
741
|
+
}
|
|
742
|
+
|
|
595
743
|
/* ─── Hour row ───────────────────────────────────── */
|
|
596
744
|
.mb-hour {
|
|
597
745
|
position: absolute;
|
|
@@ -634,8 +782,8 @@ onMount(() => {
|
|
|
634
782
|
left: 44px;
|
|
635
783
|
top: 50%;
|
|
636
784
|
transform: translateY(-50%);
|
|
637
|
-
font: 500
|
|
638
|
-
color: var(--dt-text-
|
|
785
|
+
font: 500 10px/1 var(--dt-sans, system-ui, sans-serif);
|
|
786
|
+
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
639
787
|
text-transform: uppercase;
|
|
640
788
|
letter-spacing: 0.04em;
|
|
641
789
|
}
|
|
@@ -699,6 +847,24 @@ onMount(() => {
|
|
|
699
847
|
.mb-event:active {
|
|
700
848
|
background: color-mix(in srgb, var(--ev-color) 20%, var(--dt-surface, #f9fafb));
|
|
701
849
|
}
|
|
850
|
+
/* Short blocks keep their duration-proportional height, but get a 44px
|
|
851
|
+
transparent hit-slop so taps still land. */
|
|
852
|
+
.mb-event--short {
|
|
853
|
+
overflow: visible;
|
|
854
|
+
}
|
|
855
|
+
.mb-event--short::after {
|
|
856
|
+
content: '';
|
|
857
|
+
position: absolute;
|
|
858
|
+
left: 0;
|
|
859
|
+
right: 0;
|
|
860
|
+
top: 50%;
|
|
861
|
+
transform: translateY(-50%);
|
|
862
|
+
height: 44px;
|
|
863
|
+
}
|
|
864
|
+
.mb-event--short .mb-ev-body {
|
|
865
|
+
padding-top: 2px;
|
|
866
|
+
padding-bottom: 2px;
|
|
867
|
+
}
|
|
702
868
|
.mb-event--selected {
|
|
703
869
|
box-shadow: 0 0 0 2px var(--ev-color),
|
|
704
870
|
0 2px 12px color-mix(in srgb, var(--ev-color) 25%, transparent);
|
|
@@ -710,22 +876,32 @@ onMount(() => {
|
|
|
710
876
|
background: color-mix(in srgb, var(--ev-color) 8%, var(--dt-surface, #f9fafb));
|
|
711
877
|
border: 1px dashed color-mix(in srgb, var(--ev-color) 35%, transparent);
|
|
712
878
|
}
|
|
879
|
+
/* Status treatments: token-level dims + a non-opacity signal
|
|
880
|
+
(strikethrough / border style) — never a bare opacity on the block. */
|
|
713
881
|
.mb-event--cancelled {
|
|
714
|
-
|
|
882
|
+
background: color-mix(in srgb, var(--ev-color) 5%, var(--dt-surface, #f9fafb));
|
|
715
883
|
}
|
|
716
884
|
.mb-event--cancelled .mb-ev-title {
|
|
717
885
|
text-decoration: line-through;
|
|
886
|
+
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
887
|
+
}
|
|
888
|
+
.mb-event--cancelled .mb-ev-stripe {
|
|
889
|
+
opacity: 0.45; /* decorative bar only */
|
|
718
890
|
}
|
|
719
891
|
.mb-event--tentative {
|
|
720
|
-
|
|
721
|
-
border: 1px dashed color-mix(in srgb, var(--ev-color)
|
|
892
|
+
background: color-mix(in srgb, var(--ev-color) 6%, var(--dt-surface, #f9fafb));
|
|
893
|
+
border: 1px dashed color-mix(in srgb, var(--ev-color) 45%, transparent);
|
|
722
894
|
}
|
|
723
895
|
.mb-event--full {
|
|
724
|
-
|
|
896
|
+
background: color-mix(in srgb, var(--ev-color) 6%, var(--dt-surface, #f9fafb));
|
|
897
|
+
border: 1px solid color-mix(in srgb, var(--ev-color) 30%, transparent);
|
|
898
|
+
}
|
|
899
|
+
.mb-event--full .mb-ev-title {
|
|
900
|
+
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
725
901
|
}
|
|
726
902
|
.mb-event--limited {
|
|
727
|
-
|
|
728
|
-
border: 1px dashed color-mix(in srgb, var(--ev-color)
|
|
903
|
+
background: color-mix(in srgb, var(--ev-color) 8%, var(--dt-surface, #f9fafb));
|
|
904
|
+
border: 1px dashed color-mix(in srgb, var(--ev-color) 45%, transparent);
|
|
729
905
|
}
|
|
730
906
|
.mb-event--resizing {
|
|
731
907
|
z-index: 50;
|
|
@@ -745,6 +921,17 @@ onMount(() => {
|
|
|
745
921
|
}
|
|
746
922
|
.mb-ev-handle--start { top: 0; }
|
|
747
923
|
.mb-ev-handle--end { bottom: 0; }
|
|
924
|
+
/* Hit-slop: ≥24px effective, extending inward so the block's
|
|
925
|
+
overflow clipping can't cut it off. */
|
|
926
|
+
.mb-ev-handle::before {
|
|
927
|
+
content: '';
|
|
928
|
+
position: absolute;
|
|
929
|
+
left: 0;
|
|
930
|
+
right: 0;
|
|
931
|
+
height: 24px;
|
|
932
|
+
}
|
|
933
|
+
.mb-ev-handle--start::before { top: 0; }
|
|
934
|
+
.mb-ev-handle--end::before { bottom: 0; }
|
|
748
935
|
.mb-ev-handle::after {
|
|
749
936
|
content: '';
|
|
750
937
|
position: absolute;
|
|
@@ -760,8 +947,13 @@ onMount(() => {
|
|
|
760
947
|
.mb-ev-handle--start::after { top: 2px; }
|
|
761
948
|
.mb-ev-handle--end::after { bottom: 2px; }
|
|
762
949
|
.mb-event:hover .mb-ev-handle::after,
|
|
950
|
+
.mb-event:focus-within .mb-ev-handle::after,
|
|
763
951
|
.mb-event--resizing .mb-ev-handle::after,
|
|
764
952
|
.mb-event--selected .mb-ev-handle::after { opacity: 0.55; }
|
|
953
|
+
/* Touch devices have no hover — show the handles persistently. */
|
|
954
|
+
@media (hover: none) {
|
|
955
|
+
.mb-ev-handle::after { opacity: 0.55; }
|
|
956
|
+
}
|
|
765
957
|
|
|
766
958
|
/* ─── Drag-to-create ghost ───────────────────────── */
|
|
767
959
|
.mb-create-ghost {
|
|
@@ -778,7 +970,7 @@ onMount(() => {
|
|
|
778
970
|
pointer-events: none;
|
|
779
971
|
}
|
|
780
972
|
.mb-create-ghost-time {
|
|
781
|
-
font: 600
|
|
973
|
+
font: 600 11px/1 var(--dt-mono, ui-monospace, monospace);
|
|
782
974
|
color: var(--dt-accent, #2563eb);
|
|
783
975
|
padding: 4px 8px;
|
|
784
976
|
white-space: nowrap;
|
|
@@ -802,7 +994,7 @@ onMount(() => {
|
|
|
802
994
|
}
|
|
803
995
|
|
|
804
996
|
.mb-ev-title {
|
|
805
|
-
font: 600
|
|
997
|
+
font: 600 15px/1.2 var(--dt-sans, system-ui, sans-serif);
|
|
806
998
|
color: var(--dt-text, rgba(0, 0, 0, 0.87));
|
|
807
999
|
white-space: nowrap;
|
|
808
1000
|
overflow: hidden;
|
|
@@ -810,21 +1002,21 @@ onMount(() => {
|
|
|
810
1002
|
}
|
|
811
1003
|
|
|
812
1004
|
.mb-ev-time {
|
|
813
|
-
font: 400
|
|
1005
|
+
font: 400 12px/1 var(--dt-mono, ui-monospace, monospace);
|
|
814
1006
|
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
815
1007
|
}
|
|
816
1008
|
|
|
817
1009
|
.mb-ev-sub {
|
|
818
|
-
font: 400
|
|
819
|
-
color: var(--dt-text-
|
|
1010
|
+
font: 400 12px/1.1 var(--dt-sans, system-ui, sans-serif);
|
|
1011
|
+
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
820
1012
|
white-space: nowrap;
|
|
821
1013
|
overflow: hidden;
|
|
822
1014
|
text-overflow: ellipsis;
|
|
823
1015
|
}
|
|
824
1016
|
|
|
825
1017
|
.mb-ev-loc {
|
|
826
|
-
font: 400
|
|
827
|
-
color: var(--dt-text-
|
|
1018
|
+
font: 400 11px/1 var(--dt-sans, system-ui, sans-serif);
|
|
1019
|
+
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
828
1020
|
white-space: nowrap;
|
|
829
1021
|
overflow: hidden;
|
|
830
1022
|
text-overflow: ellipsis;
|
|
@@ -837,7 +1029,7 @@ onMount(() => {
|
|
|
837
1029
|
}
|
|
838
1030
|
|
|
839
1031
|
.mb-ev-tag {
|
|
840
|
-
font: 500
|
|
1032
|
+
font: 500 11px/1 var(--dt-sans, system-ui, sans-serif);
|
|
841
1033
|
color: var(--ev-color, var(--dt-accent));
|
|
842
1034
|
background: color-mix(in srgb, var(--ev-color, var(--dt-accent)) 15%, transparent);
|
|
843
1035
|
padding: 2px 5px;
|
|
@@ -855,11 +1047,14 @@ onMount(() => {
|
|
|
855
1047
|
background: var(--ev-color, var(--dt-accent));
|
|
856
1048
|
animation: mb-pulse 2s ease-in-out infinite;
|
|
857
1049
|
}
|
|
1050
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1051
|
+
.mb-ev-live { animation: none; }
|
|
1052
|
+
}
|
|
858
1053
|
.mb-ev-next-badge {
|
|
859
1054
|
position: absolute;
|
|
860
1055
|
top: 4px;
|
|
861
1056
|
right: 4px;
|
|
862
|
-
font: 600
|
|
1057
|
+
font: 600 10px/1 var(--dt-sans, system-ui, sans-serif);
|
|
863
1058
|
text-transform: uppercase;
|
|
864
1059
|
letter-spacing: 0.06em;
|
|
865
1060
|
color: var(--ev-color, var(--dt-accent));
|
|
@@ -876,7 +1071,7 @@ onMount(() => {
|
|
|
876
1071
|
|
|
877
1072
|
/* ─── Focus ──────────────────────────────────────── */
|
|
878
1073
|
.mb-event:focus-visible {
|
|
879
|
-
outline:
|
|
880
|
-
|
|
1074
|
+
outline: none;
|
|
1075
|
+
box-shadow: 0 0 0 2px var(--dt-accent, #2563eb);
|
|
881
1076
|
}
|
|
882
1077
|
</style>
|