@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
|
@@ -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,181 @@ 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}
|
|
401
|
+
ontouchcancel={swipe.ontouchcancel}
|
|
358
402
|
>
|
|
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
403
|
<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"
|
|
404
|
+
class="mb-swipe"
|
|
405
|
+
class:mb-swipe--animate={swipeAnimate}
|
|
406
|
+
style:transform={swipeOffset !== 0 ? `translateX(${swipeOffset}px)` : undefined}
|
|
391
407
|
>
|
|
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>
|
|
408
|
+
<!-- All-day events bar -->
|
|
409
|
+
{#if allDayEvents.length > 0}
|
|
410
|
+
<div class="mb-allday" class:mb-allday--expanded={allDayExpanded}>
|
|
411
|
+
{#each visibleAllDay as seg (seg.ev.id)}
|
|
412
|
+
<button
|
|
413
|
+
type="button"
|
|
414
|
+
class="mb-allday-chip"
|
|
415
|
+
class:mb-allday-chip--selected={selectedEventId === seg.ev.id}
|
|
416
|
+
style:--ev-color={seg.ev.color ?? 'var(--dt-accent)'}
|
|
417
|
+
onclick={() => oneventclick?.(seg.ev)}
|
|
418
|
+
>
|
|
419
|
+
<span class="mb-allday-dot"></span>
|
|
420
|
+
<span class="mb-allday-title">{seg.ev.title}</span>
|
|
421
|
+
{#if seg.totalDays > 1}
|
|
422
|
+
<span class="mb-allday-span">{seg.dayIndex}/{seg.totalDays}</span>
|
|
449
423
|
{/if}
|
|
450
|
-
|
|
451
|
-
|
|
424
|
+
</button>
|
|
425
|
+
{/each}
|
|
426
|
+
{#if allDayEvents.length > ALLDAY_MAX}
|
|
427
|
+
<button
|
|
428
|
+
type="button"
|
|
429
|
+
class="mb-allday-more"
|
|
430
|
+
aria-expanded={allDayExpanded}
|
|
431
|
+
onclick={() => { allDayExpanded = !allDayExpanded; }}
|
|
432
|
+
>
|
|
433
|
+
{allDayExpanded ? L.showLess : L.nMore(allDayEvents.length - ALLDAY_MAX)}
|
|
434
|
+
</button>
|
|
435
|
+
{/if}
|
|
436
|
+
</div>
|
|
437
|
+
{/if}
|
|
438
|
+
|
|
439
|
+
<!-- Scrollable time grid -->
|
|
440
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex, a11y_no_noninteractive_element_interactions -->
|
|
441
|
+
<div
|
|
442
|
+
class="mb-grid"
|
|
443
|
+
bind:this={gridEl}
|
|
444
|
+
onclick={handleGridClick}
|
|
445
|
+
onpointerdown={onGridPointerDown}
|
|
446
|
+
onkeydown={onGridKeydown}
|
|
447
|
+
oncontextmenu={onGridContextMenu}
|
|
448
|
+
role="region"
|
|
449
|
+
aria-label={L.scrollableDayPlanner}
|
|
450
|
+
tabindex="0"
|
|
451
|
+
>
|
|
452
|
+
<div class="mb-grid-inner" style:height="{gridHeight}px">
|
|
453
|
+
<!-- Hour lanes -->
|
|
454
|
+
{#each { length: hourCount } as _, h}
|
|
455
|
+
{@const hour = startHour + h}
|
|
456
|
+
{@const blocked = isBlockedAt(hour)}
|
|
457
|
+
<div
|
|
458
|
+
class="mb-hour"
|
|
459
|
+
class:mb-hour--blocked={blocked}
|
|
460
|
+
style:top="{h * HOUR_HEIGHT}px"
|
|
461
|
+
style:height="{HOUR_HEIGHT}px"
|
|
462
|
+
>
|
|
463
|
+
<div class="mb-hour-label">{fmtH(hour, locale)}</div>
|
|
464
|
+
<div class="mb-hour-line"></div>
|
|
465
|
+
{#if blocked && blockedSlots}
|
|
466
|
+
{@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)}
|
|
467
|
+
{#if slot?.label}
|
|
468
|
+
<span class="mb-blocked-label">{slot.label}</span>
|
|
469
|
+
{/if}
|
|
452
470
|
{/if}
|
|
453
|
-
|
|
454
|
-
|
|
471
|
+
</div>
|
|
472
|
+
{/each}
|
|
473
|
+
|
|
474
|
+
<!-- Now line -->
|
|
475
|
+
{#if nowOffset >= 0}
|
|
476
|
+
<div class="mb-now" style:top="{nowOffset}px">
|
|
477
|
+
<span class="mb-now-label">{clock.hm}</span>
|
|
478
|
+
<div class="mb-now-line"></div>
|
|
479
|
+
</div>
|
|
480
|
+
{/if}
|
|
481
|
+
|
|
482
|
+
<!-- Events -->
|
|
483
|
+
{#each positionedEvents as p (p.ev.id)}
|
|
484
|
+
<button
|
|
485
|
+
type="button"
|
|
486
|
+
class="mb-event"
|
|
487
|
+
class:mb-event--selected={selectedEventId === p.ev.id}
|
|
488
|
+
class:mb-event--current={p.isCurrent}
|
|
489
|
+
class:mb-event--next={p.isNext}
|
|
490
|
+
class:mb-event--cancelled={p.ev.status === 'cancelled'}
|
|
491
|
+
class:mb-event--tentative={p.ev.status === 'tentative'}
|
|
492
|
+
class:mb-event--full={p.ev.status === 'full'}
|
|
493
|
+
class:mb-event--limited={p.ev.status === 'limited'}
|
|
494
|
+
class:mb-event--resizing={p.isResizing}
|
|
495
|
+
class:mb-event--short={p.height < 44}
|
|
496
|
+
style:top="{p.top}px"
|
|
497
|
+
style:height="{p.height}px"
|
|
498
|
+
style:left={p.left}
|
|
499
|
+
style:width={p.width}
|
|
500
|
+
style:--ev-color={p.ev.color ?? 'var(--dt-accent)'}
|
|
501
|
+
onclick={(e) => { e.stopPropagation(); if (suppressEventClick) { suppressEventClick = false; return; } oneventclick?.(p.ev); }}
|
|
502
|
+
onpointerenter={() => oneventhover?.(p.ev)}
|
|
503
|
+
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}` : ''}"
|
|
504
|
+
>
|
|
505
|
+
<div class="mb-ev-stripe"></div>
|
|
506
|
+
<div class="mb-ev-body">
|
|
507
|
+
<EventContent event={p.ev}>
|
|
508
|
+
<!-- Line thresholds must track real content height (title 18 +
|
|
509
|
+
time 13 + sub 14 + loc 12 + tags 19 + padding 8) — showing
|
|
510
|
+
a line the block can't hold clips text at both ends. -->
|
|
511
|
+
<span class="mb-ev-title">{p.ev.title}</span>
|
|
512
|
+
{#if p.height > 32}
|
|
513
|
+
<span class="mb-ev-time">{fmtTime(p.ev.start, locale)} – {fmtTime(p.ev.end, locale)}</span>
|
|
514
|
+
{/if}
|
|
515
|
+
{#if p.ev.subtitle && p.height > 56}
|
|
516
|
+
<span class="mb-ev-sub">{p.ev.subtitle}</span>
|
|
517
|
+
{/if}
|
|
518
|
+
{#if p.ev.location && p.height > 72}
|
|
519
|
+
<span class="mb-ev-loc">{p.ev.location}</span>
|
|
520
|
+
{/if}
|
|
521
|
+
{#if p.ev.tags?.length && p.height > 88}
|
|
522
|
+
<div class="mb-ev-tags">
|
|
523
|
+
{#each p.ev.tags as tag}
|
|
524
|
+
<span class="mb-ev-tag">{tag}</span>
|
|
525
|
+
{/each}
|
|
526
|
+
</div>
|
|
527
|
+
{/if}
|
|
528
|
+
</EventContent>
|
|
529
|
+
</div>
|
|
530
|
+
{#if p.isCurrent}
|
|
531
|
+
<span class="mb-ev-live"></span>
|
|
532
|
+
{:else if p.isNext}
|
|
533
|
+
<span class="mb-ev-next-badge">{L.upNext}</span>
|
|
455
534
|
{/if}
|
|
456
|
-
{#if p.ev.
|
|
457
|
-
<
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
{
|
|
461
|
-
|
|
535
|
+
{#if !readOnly && !p.ev.data?.readOnly}
|
|
536
|
+
<span
|
|
537
|
+
class="mb-ev-handle mb-ev-handle--start"
|
|
538
|
+
aria-hidden="true"
|
|
539
|
+
onpointerdown={(e) => onResizePointerDown(e, p.ev, 'start')}
|
|
540
|
+
></span>
|
|
541
|
+
<span
|
|
542
|
+
class="mb-ev-handle mb-ev-handle--end"
|
|
543
|
+
aria-hidden="true"
|
|
544
|
+
onpointerdown={(e) => onResizePointerDown(e, p.ev, 'end')}
|
|
545
|
+
></span>
|
|
462
546
|
{/if}
|
|
463
|
-
|
|
547
|
+
</button>
|
|
548
|
+
{/each}
|
|
549
|
+
|
|
550
|
+
<!-- Drag-to-create ghost -->
|
|
551
|
+
{#if !readOnly && drag?.active && drag.mode === 'create' && drag.payload}
|
|
552
|
+
{@const gTop = ((drag.payload.start.getTime() - dayMs) / HOUR_MS - startHour) * HOUR_HEIGHT}
|
|
553
|
+
{@const gH = Math.max(12, ((drag.payload.end.getTime() - drag.payload.start.getTime()) / HOUR_MS) * HOUR_HEIGHT)}
|
|
554
|
+
<div class="mb-create-ghost" style:top="{gTop}px" style:height="{gH}px" aria-hidden="true">
|
|
555
|
+
<span class="mb-create-ghost-time">
|
|
556
|
+
{fmtTime(drag.payload.start, locale)} – {fmtTime(drag.payload.end, locale)}
|
|
557
|
+
</span>
|
|
464
558
|
</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}
|
|
559
|
+
{/if}
|
|
560
|
+
</div>
|
|
495
561
|
</div>
|
|
562
|
+
|
|
563
|
+
<!-- Empty state — overlaid, taps pass through to the grid -->
|
|
564
|
+
{#if isEmpty}
|
|
565
|
+
<div class="mb-empty">
|
|
566
|
+
{#if ctx.emptySnippet}
|
|
567
|
+
{@render ctx.emptySnippet()}
|
|
568
|
+
{:else}
|
|
569
|
+
<span class="mb-empty-text">{L.nothingScheduled}</span>
|
|
570
|
+
{/if}
|
|
571
|
+
</div>
|
|
572
|
+
{/if}
|
|
496
573
|
</div>
|
|
497
574
|
</div>
|
|
498
575
|
|
|
@@ -507,9 +584,25 @@ onMount(() => {
|
|
|
507
584
|
overflow: hidden;
|
|
508
585
|
background: var(--dt-bg, #fff);
|
|
509
586
|
-webkit-tap-highlight-color: transparent;
|
|
587
|
+
touch-action: pan-y;
|
|
510
588
|
}
|
|
511
589
|
.mb--auto { overflow: visible; }
|
|
512
590
|
|
|
591
|
+
/* ─── Swipe wrapper (follows the finger) ─────────── */
|
|
592
|
+
.mb-swipe {
|
|
593
|
+
flex: 1;
|
|
594
|
+
min-height: 0;
|
|
595
|
+
display: flex;
|
|
596
|
+
flex-direction: column;
|
|
597
|
+
position: relative;
|
|
598
|
+
}
|
|
599
|
+
.mb-swipe--animate {
|
|
600
|
+
transition: transform 180ms ease;
|
|
601
|
+
}
|
|
602
|
+
@media (prefers-reduced-motion: reduce) {
|
|
603
|
+
.mb-swipe--animate { transition: none; }
|
|
604
|
+
}
|
|
605
|
+
|
|
513
606
|
/* ─── All-day bar ────────────────────────────────── */
|
|
514
607
|
.mb-allday {
|
|
515
608
|
display: flex;
|
|
@@ -522,12 +615,17 @@ onMount(() => {
|
|
|
522
615
|
align-items: center;
|
|
523
616
|
}
|
|
524
617
|
.mb-allday::-webkit-scrollbar { display: none; }
|
|
618
|
+
.mb-allday--expanded {
|
|
619
|
+
flex-wrap: wrap;
|
|
620
|
+
overflow-x: visible;
|
|
621
|
+
}
|
|
525
622
|
|
|
526
623
|
.mb-allday-chip {
|
|
527
624
|
display: flex;
|
|
528
625
|
align-items: center;
|
|
529
626
|
gap: 4px;
|
|
530
627
|
padding: 4px 8px;
|
|
628
|
+
min-height: 32px;
|
|
531
629
|
border-radius: 5px;
|
|
532
630
|
background: color-mix(in srgb, var(--ev-color) 12%, var(--dt-surface, #f9fafb));
|
|
533
631
|
border: none;
|
|
@@ -536,6 +634,17 @@ onMount(() => {
|
|
|
536
634
|
transition: background 120ms;
|
|
537
635
|
-webkit-tap-highlight-color: transparent;
|
|
538
636
|
max-width: 160px;
|
|
637
|
+
position: relative;
|
|
638
|
+
}
|
|
639
|
+
/* Hit-slop: 44px effective touch target */
|
|
640
|
+
.mb-allday-chip::before {
|
|
641
|
+
content: '';
|
|
642
|
+
position: absolute;
|
|
643
|
+
left: 0;
|
|
644
|
+
right: 0;
|
|
645
|
+
top: 50%;
|
|
646
|
+
transform: translateY(-50%);
|
|
647
|
+
height: 44px;
|
|
539
648
|
}
|
|
540
649
|
.mb-allday-chip:active {
|
|
541
650
|
background: color-mix(in srgb, var(--ev-color) 22%, var(--dt-surface, #f9fafb));
|
|
@@ -543,6 +652,10 @@ onMount(() => {
|
|
|
543
652
|
.mb-allday-chip--selected {
|
|
544
653
|
box-shadow: 0 0 0 1.5px var(--ev-color);
|
|
545
654
|
}
|
|
655
|
+
.mb-allday-chip:focus-visible {
|
|
656
|
+
outline: none;
|
|
657
|
+
box-shadow: 0 0 0 2px var(--dt-accent, #2563eb);
|
|
658
|
+
}
|
|
546
659
|
|
|
547
660
|
.mb-allday-dot {
|
|
548
661
|
width: 6px;
|
|
@@ -553,7 +666,7 @@ onMount(() => {
|
|
|
553
666
|
}
|
|
554
667
|
|
|
555
668
|
.mb-allday-title {
|
|
556
|
-
font: 500
|
|
669
|
+
font: 500 12px/1 var(--dt-sans, system-ui, sans-serif);
|
|
557
670
|
color: var(--dt-text, rgba(0, 0, 0, 0.87));
|
|
558
671
|
white-space: nowrap;
|
|
559
672
|
max-width: 100px;
|
|
@@ -562,16 +675,35 @@ onMount(() => {
|
|
|
562
675
|
}
|
|
563
676
|
|
|
564
677
|
.mb-allday-span {
|
|
565
|
-
font: 400
|
|
566
|
-
color: var(--dt-text-
|
|
678
|
+
font: 400 11px/1 var(--dt-sans, system-ui, sans-serif);
|
|
679
|
+
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
567
680
|
}
|
|
568
681
|
|
|
569
682
|
.mb-allday-more {
|
|
570
|
-
font: 500
|
|
683
|
+
font: 500 12px/1 var(--dt-sans, system-ui, sans-serif);
|
|
571
684
|
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
572
685
|
white-space: nowrap;
|
|
573
686
|
flex-shrink: 0;
|
|
574
|
-
padding: 0
|
|
687
|
+
padding: 0 6px;
|
|
688
|
+
min-height: 32px;
|
|
689
|
+
border: none;
|
|
690
|
+
background: transparent;
|
|
691
|
+
cursor: pointer;
|
|
692
|
+
position: relative;
|
|
693
|
+
-webkit-tap-highlight-color: transparent;
|
|
694
|
+
}
|
|
695
|
+
.mb-allday-more::before {
|
|
696
|
+
content: '';
|
|
697
|
+
position: absolute;
|
|
698
|
+
left: 0;
|
|
699
|
+
right: 0;
|
|
700
|
+
top: 50%;
|
|
701
|
+
transform: translateY(-50%);
|
|
702
|
+
height: 44px;
|
|
703
|
+
}
|
|
704
|
+
.mb-allday-more:focus-visible {
|
|
705
|
+
outline: none;
|
|
706
|
+
box-shadow: 0 0 0 2px var(--dt-accent, #2563eb);
|
|
575
707
|
}
|
|
576
708
|
|
|
577
709
|
/* ─── Grid ───────────────────────────────────────── */
|
|
@@ -579,6 +711,7 @@ onMount(() => {
|
|
|
579
711
|
flex: 1;
|
|
580
712
|
overflow-y: auto;
|
|
581
713
|
overflow-x: hidden;
|
|
714
|
+
overscroll-behavior: contain;
|
|
582
715
|
-webkit-overflow-scrolling: touch;
|
|
583
716
|
scrollbar-width: thin;
|
|
584
717
|
scrollbar-color: var(--dt-scrollbar, rgba(0, 0, 0, 0.1)) transparent;
|
|
@@ -586,12 +719,31 @@ onMount(() => {
|
|
|
586
719
|
padding-top: 8px;
|
|
587
720
|
}
|
|
588
721
|
.mb--auto .mb-grid { overflow-y: visible; }
|
|
722
|
+
.mb-grid:focus-visible {
|
|
723
|
+
outline: none;
|
|
724
|
+
box-shadow: inset 0 0 0 2px var(--dt-accent, #2563eb);
|
|
725
|
+
}
|
|
589
726
|
|
|
590
727
|
.mb-grid-inner {
|
|
591
728
|
position: relative;
|
|
592
729
|
min-width: 100%;
|
|
593
730
|
}
|
|
594
731
|
|
|
732
|
+
/* ─── Empty state ────────────────────────────────── */
|
|
733
|
+
.mb-empty {
|
|
734
|
+
position: absolute;
|
|
735
|
+
inset: 0;
|
|
736
|
+
display: flex;
|
|
737
|
+
align-items: center;
|
|
738
|
+
justify-content: center;
|
|
739
|
+
pointer-events: none;
|
|
740
|
+
z-index: 4;
|
|
741
|
+
}
|
|
742
|
+
.mb-empty-text {
|
|
743
|
+
font: 500 13px/1.4 var(--dt-sans, system-ui, sans-serif);
|
|
744
|
+
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
745
|
+
}
|
|
746
|
+
|
|
595
747
|
/* ─── Hour row ───────────────────────────────────── */
|
|
596
748
|
.mb-hour {
|
|
597
749
|
position: absolute;
|
|
@@ -603,6 +755,9 @@ onMount(() => {
|
|
|
603
755
|
|
|
604
756
|
.mb-hour-label {
|
|
605
757
|
width: 40px;
|
|
758
|
+
/* border-box keeps the label inside the 40px gutter that events
|
|
759
|
+
start at — content-box pushed digits flush under the event edge */
|
|
760
|
+
box-sizing: border-box;
|
|
606
761
|
flex-shrink: 0;
|
|
607
762
|
font: 500 11px/1 var(--dt-mono, ui-monospace, monospace);
|
|
608
763
|
color: var(--dt-text-3, rgba(0, 0, 0, 0.38));
|
|
@@ -634,8 +789,8 @@ onMount(() => {
|
|
|
634
789
|
left: 44px;
|
|
635
790
|
top: 50%;
|
|
636
791
|
transform: translateY(-50%);
|
|
637
|
-
font: 500
|
|
638
|
-
color: var(--dt-text-
|
|
792
|
+
font: 500 10px/1 var(--dt-sans, system-ui, sans-serif);
|
|
793
|
+
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
639
794
|
text-transform: uppercase;
|
|
640
795
|
letter-spacing: 0.04em;
|
|
641
796
|
}
|
|
@@ -653,6 +808,7 @@ onMount(() => {
|
|
|
653
808
|
|
|
654
809
|
.mb-now-label {
|
|
655
810
|
width: 40px;
|
|
811
|
+
box-sizing: border-box;
|
|
656
812
|
flex-shrink: 0;
|
|
657
813
|
text-align: right;
|
|
658
814
|
padding-right: 6px;
|
|
@@ -699,6 +855,24 @@ onMount(() => {
|
|
|
699
855
|
.mb-event:active {
|
|
700
856
|
background: color-mix(in srgb, var(--ev-color) 20%, var(--dt-surface, #f9fafb));
|
|
701
857
|
}
|
|
858
|
+
/* Short blocks keep their duration-proportional height, but get a 44px
|
|
859
|
+
transparent hit-slop so taps still land. */
|
|
860
|
+
.mb-event--short {
|
|
861
|
+
overflow: visible;
|
|
862
|
+
}
|
|
863
|
+
.mb-event--short::after {
|
|
864
|
+
content: '';
|
|
865
|
+
position: absolute;
|
|
866
|
+
left: 0;
|
|
867
|
+
right: 0;
|
|
868
|
+
top: 50%;
|
|
869
|
+
transform: translateY(-50%);
|
|
870
|
+
height: 44px;
|
|
871
|
+
}
|
|
872
|
+
.mb-event--short .mb-ev-body {
|
|
873
|
+
padding-top: 2px;
|
|
874
|
+
padding-bottom: 2px;
|
|
875
|
+
}
|
|
702
876
|
.mb-event--selected {
|
|
703
877
|
box-shadow: 0 0 0 2px var(--ev-color),
|
|
704
878
|
0 2px 12px color-mix(in srgb, var(--ev-color) 25%, transparent);
|
|
@@ -710,22 +884,32 @@ onMount(() => {
|
|
|
710
884
|
background: color-mix(in srgb, var(--ev-color) 8%, var(--dt-surface, #f9fafb));
|
|
711
885
|
border: 1px dashed color-mix(in srgb, var(--ev-color) 35%, transparent);
|
|
712
886
|
}
|
|
887
|
+
/* Status treatments: token-level dims + a non-opacity signal
|
|
888
|
+
(strikethrough / border style) — never a bare opacity on the block. */
|
|
713
889
|
.mb-event--cancelled {
|
|
714
|
-
|
|
890
|
+
background: color-mix(in srgb, var(--ev-color) 5%, var(--dt-surface, #f9fafb));
|
|
715
891
|
}
|
|
716
892
|
.mb-event--cancelled .mb-ev-title {
|
|
717
893
|
text-decoration: line-through;
|
|
894
|
+
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
895
|
+
}
|
|
896
|
+
.mb-event--cancelled .mb-ev-stripe {
|
|
897
|
+
opacity: 0.45; /* decorative bar only */
|
|
718
898
|
}
|
|
719
899
|
.mb-event--tentative {
|
|
720
|
-
|
|
721
|
-
border: 1px dashed color-mix(in srgb, var(--ev-color)
|
|
900
|
+
background: color-mix(in srgb, var(--ev-color) 6%, var(--dt-surface, #f9fafb));
|
|
901
|
+
border: 1px dashed color-mix(in srgb, var(--ev-color) 45%, transparent);
|
|
722
902
|
}
|
|
723
903
|
.mb-event--full {
|
|
724
|
-
|
|
904
|
+
background: color-mix(in srgb, var(--ev-color) 6%, var(--dt-surface, #f9fafb));
|
|
905
|
+
border: 1px solid color-mix(in srgb, var(--ev-color) 30%, transparent);
|
|
906
|
+
}
|
|
907
|
+
.mb-event--full .mb-ev-title {
|
|
908
|
+
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
725
909
|
}
|
|
726
910
|
.mb-event--limited {
|
|
727
|
-
|
|
728
|
-
border: 1px dashed color-mix(in srgb, var(--ev-color)
|
|
911
|
+
background: color-mix(in srgb, var(--ev-color) 8%, var(--dt-surface, #f9fafb));
|
|
912
|
+
border: 1px dashed color-mix(in srgb, var(--ev-color) 45%, transparent);
|
|
729
913
|
}
|
|
730
914
|
.mb-event--resizing {
|
|
731
915
|
z-index: 50;
|
|
@@ -745,6 +929,17 @@ onMount(() => {
|
|
|
745
929
|
}
|
|
746
930
|
.mb-ev-handle--start { top: 0; }
|
|
747
931
|
.mb-ev-handle--end { bottom: 0; }
|
|
932
|
+
/* Hit-slop: ≥24px effective, extending inward so the block's
|
|
933
|
+
overflow clipping can't cut it off. */
|
|
934
|
+
.mb-ev-handle::before {
|
|
935
|
+
content: '';
|
|
936
|
+
position: absolute;
|
|
937
|
+
left: 0;
|
|
938
|
+
right: 0;
|
|
939
|
+
height: 24px;
|
|
940
|
+
}
|
|
941
|
+
.mb-ev-handle--start::before { top: 0; }
|
|
942
|
+
.mb-ev-handle--end::before { bottom: 0; }
|
|
748
943
|
.mb-ev-handle::after {
|
|
749
944
|
content: '';
|
|
750
945
|
position: absolute;
|
|
@@ -760,8 +955,13 @@ onMount(() => {
|
|
|
760
955
|
.mb-ev-handle--start::after { top: 2px; }
|
|
761
956
|
.mb-ev-handle--end::after { bottom: 2px; }
|
|
762
957
|
.mb-event:hover .mb-ev-handle::after,
|
|
958
|
+
.mb-event:focus-within .mb-ev-handle::after,
|
|
763
959
|
.mb-event--resizing .mb-ev-handle::after,
|
|
764
960
|
.mb-event--selected .mb-ev-handle::after { opacity: 0.55; }
|
|
961
|
+
/* Touch devices have no hover — show the handles persistently. */
|
|
962
|
+
@media (hover: none) {
|
|
963
|
+
.mb-ev-handle::after { opacity: 0.55; }
|
|
964
|
+
}
|
|
765
965
|
|
|
766
966
|
/* ─── Drag-to-create ghost ───────────────────────── */
|
|
767
967
|
.mb-create-ghost {
|
|
@@ -778,7 +978,7 @@ onMount(() => {
|
|
|
778
978
|
pointer-events: none;
|
|
779
979
|
}
|
|
780
980
|
.mb-create-ghost-time {
|
|
781
|
-
font: 600
|
|
981
|
+
font: 600 11px/1 var(--dt-mono, ui-monospace, monospace);
|
|
782
982
|
color: var(--dt-accent, #2563eb);
|
|
783
983
|
padding: 4px 8px;
|
|
784
984
|
white-space: nowrap;
|
|
@@ -802,7 +1002,7 @@ onMount(() => {
|
|
|
802
1002
|
}
|
|
803
1003
|
|
|
804
1004
|
.mb-ev-title {
|
|
805
|
-
font: 600
|
|
1005
|
+
font: 600 15px/1.2 var(--dt-sans, system-ui, sans-serif);
|
|
806
1006
|
color: var(--dt-text, rgba(0, 0, 0, 0.87));
|
|
807
1007
|
white-space: nowrap;
|
|
808
1008
|
overflow: hidden;
|
|
@@ -810,21 +1010,21 @@ onMount(() => {
|
|
|
810
1010
|
}
|
|
811
1011
|
|
|
812
1012
|
.mb-ev-time {
|
|
813
|
-
font: 400
|
|
1013
|
+
font: 400 12px/1 var(--dt-mono, ui-monospace, monospace);
|
|
814
1014
|
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
815
1015
|
}
|
|
816
1016
|
|
|
817
1017
|
.mb-ev-sub {
|
|
818
|
-
font: 400
|
|
819
|
-
color: var(--dt-text-
|
|
1018
|
+
font: 400 12px/1.1 var(--dt-sans, system-ui, sans-serif);
|
|
1019
|
+
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
820
1020
|
white-space: nowrap;
|
|
821
1021
|
overflow: hidden;
|
|
822
1022
|
text-overflow: ellipsis;
|
|
823
1023
|
}
|
|
824
1024
|
|
|
825
1025
|
.mb-ev-loc {
|
|
826
|
-
font: 400
|
|
827
|
-
color: var(--dt-text-
|
|
1026
|
+
font: 400 11px/1 var(--dt-sans, system-ui, sans-serif);
|
|
1027
|
+
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
828
1028
|
white-space: nowrap;
|
|
829
1029
|
overflow: hidden;
|
|
830
1030
|
text-overflow: ellipsis;
|
|
@@ -837,7 +1037,7 @@ onMount(() => {
|
|
|
837
1037
|
}
|
|
838
1038
|
|
|
839
1039
|
.mb-ev-tag {
|
|
840
|
-
font: 500
|
|
1040
|
+
font: 500 11px/1 var(--dt-sans, system-ui, sans-serif);
|
|
841
1041
|
color: var(--ev-color, var(--dt-accent));
|
|
842
1042
|
background: color-mix(in srgb, var(--ev-color, var(--dt-accent)) 15%, transparent);
|
|
843
1043
|
padding: 2px 5px;
|
|
@@ -855,11 +1055,14 @@ onMount(() => {
|
|
|
855
1055
|
background: var(--ev-color, var(--dt-accent));
|
|
856
1056
|
animation: mb-pulse 2s ease-in-out infinite;
|
|
857
1057
|
}
|
|
1058
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1059
|
+
.mb-ev-live { animation: none; }
|
|
1060
|
+
}
|
|
858
1061
|
.mb-ev-next-badge {
|
|
859
1062
|
position: absolute;
|
|
860
1063
|
top: 4px;
|
|
861
1064
|
right: 4px;
|
|
862
|
-
font: 600
|
|
1065
|
+
font: 600 10px/1 var(--dt-sans, system-ui, sans-serif);
|
|
863
1066
|
text-transform: uppercase;
|
|
864
1067
|
letter-spacing: 0.06em;
|
|
865
1068
|
color: var(--ev-color, var(--dt-accent));
|
|
@@ -876,7 +1079,7 @@ onMount(() => {
|
|
|
876
1079
|
|
|
877
1080
|
/* ─── Focus ──────────────────────────────────────── */
|
|
878
1081
|
.mb-event:focus-visible {
|
|
879
|
-
outline:
|
|
880
|
-
|
|
1082
|
+
outline: none;
|
|
1083
|
+
box-shadow: 0 0 0 2px var(--dt-accent, #2563eb);
|
|
881
1084
|
}
|
|
882
1085
|
</style>
|