@nomideusz/svelte-calendar 0.16.0 → 0.20.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 +7 -2
- package/dist/adapters/composite.js +7 -2
- package/dist/adapters/recurring.d.ts +16 -0
- package/dist/adapters/recurring.js +21 -5
- package/dist/calendar/Calendar.svelte +13 -1
- package/dist/calendar/Calendar.svelte.d.ts +7 -0
- package/dist/engine/view-state.svelte.d.ts +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- package/dist/text-fit.d.ts +30 -0
- package/dist/text-fit.js +65 -8
- package/dist/views/planner/PlannerScroll.svelte +1217 -0
- package/dist/views/planner/PlannerScroll.svelte.d.ts +28 -0
- package/dist/views/planner/PlannerWeek.svelte +98 -13
- package/dist/views/planner/PlannerWeek.svelte.d.ts +7 -0
- package/dist/views/shared/chip-fit.svelte.d.ts +27 -0
- package/dist/views/shared/chip-fit.svelte.js +0 -0
- package/package.json +6 -6
- package/widget/widget.js +13816 -6235
|
@@ -0,0 +1,1217 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
week-scroll — the multi-week vertical scroller (Hey Calendar style).
|
|
3
|
+
|
|
4
|
+
This is the PlannerWeek that 97f7f492 (2026-08-01) replaced with the
|
|
5
|
+
vertical time grid, restored as an optional view beside 'week-planner'.
|
|
6
|
+
|
|
7
|
+
Modelled after Hey Calendar's week view:
|
|
8
|
+
• Weeks stack vertically. Scroll up = past, down = future.
|
|
9
|
+
• Day headers in each week row: "MON 23", "TUE 24", with accent pill for today.
|
|
10
|
+
• Month label in left gutter, vertical bottom-to-top.
|
|
11
|
+
• Events are clean horizontal bars with colour fill, "9AM- 10AM Title" inline.
|
|
12
|
+
• Generous whitespace, thin dividers, minimal chrome.
|
|
13
|
+
-->
|
|
14
|
+
<script lang="ts">import { onMount, tick, untrack } from "svelte";
|
|
15
|
+
import { flip } from "svelte/animate";
|
|
16
|
+
import { crossfade } from "svelte/transition";
|
|
17
|
+
import { prefersReducedMotion } from "svelte/motion";
|
|
18
|
+
import { useCalendarContext } from "../shared/context.svelte.js";
|
|
19
|
+
import EventContent from "../shared/EventContent.svelte";
|
|
20
|
+
import { createClock } from "../../core/clock.svelte.js";
|
|
21
|
+
import { DAY_MS, HOUR_MS, sod } from "../../core/time.js";
|
|
22
|
+
import { startOfWeek as sowFn, isAllDay, isMultiDay, segmentForDay } from "../../core/time.js";
|
|
23
|
+
import { weekdayShort, monthLong, fmtTime as _fmtTime, getLabels } from "../../core/locale.js";
|
|
24
|
+
import { createChipFit } from "../shared/chip-fit.svelte.js";
|
|
25
|
+
const L = $derived(getLabels());
|
|
26
|
+
let { mondayStart = true, locale, height = 520, events = [], style = "", focusDate, oneventclick, oneventcreate, onexternaldrop, selectedEventId = null, readOnly = false, visibleHours } = $props();
|
|
27
|
+
const ctx = useCalendarContext();
|
|
28
|
+
const clock = createClock(ctx.timezone);
|
|
29
|
+
// Drag ghost flies between day cells instead of teleporting.
|
|
30
|
+
// No fallback: without a counterpart (drag start/end) it appears/disappears instantly.
|
|
31
|
+
const [previewSend, previewReceive] = crossfade({ duration: () => prefersReducedMotion.current ? 0 : 160 });
|
|
32
|
+
const drag = $derived(ctx.drag);
|
|
33
|
+
// Cards flip only while a drag is live: a data load must never animate
|
|
34
|
+
// every chip into place (that read as the whole grid jittering).
|
|
35
|
+
const ANIM = $derived(prefersReducedMotion.current || !drag?.active ? 0 : 180);
|
|
36
|
+
const commitDragCtx = $derived(ctx.commitDrag);
|
|
37
|
+
const viewState = $derived(ctx.viewState);
|
|
38
|
+
const loadRangeCtx = $derived(ctx.loadRange);
|
|
39
|
+
const equalDays = $derived(ctx.equalDays);
|
|
40
|
+
const showDates = $derived(ctx.showDates);
|
|
41
|
+
const hideDays = $derived(ctx.hideDays);
|
|
42
|
+
const blockedSlots = $derived(ctx.blockedSlots);
|
|
43
|
+
const dayHeaderSnippet = $derived(ctx.dayHeaderSnippet);
|
|
44
|
+
const minDuration = $derived(ctx.minDuration);
|
|
45
|
+
const autoHeight = $derived(ctx.autoHeight);
|
|
46
|
+
const oneventhover = $derived(ctx.oneventhover);
|
|
47
|
+
const disabledSet = $derived(ctx.disabledSet);
|
|
48
|
+
// ─── Buffer config ─────────────────────────────
|
|
49
|
+
const INITIAL_BUFFER = 8;
|
|
50
|
+
const EXTEND_BY = 8;
|
|
51
|
+
const EDGE_PX = 200;
|
|
52
|
+
let bufferBefore = $state(INITIAL_BUFFER);
|
|
53
|
+
let bufferAfter = $state(INITIAL_BUFFER);
|
|
54
|
+
const MAX_EVENTS_SHOWN = 5;
|
|
55
|
+
// ─── Geometry ───────────────────────────────────
|
|
56
|
+
// A row is as tall as its busiest day: a week with seven classes on Monday
|
|
57
|
+
// is worth the height. It was briefly a constant, which made the scroll
|
|
58
|
+
// arithmetic exact and clipped exactly the days you most need to read.
|
|
59
|
+
// Nothing needs equal rows now — the prepend is compensated by row
|
|
60
|
+
// identity, a drag maps by hit-testing the row under the pointer, and the
|
|
61
|
+
// mount re-centres once the events have landed.
|
|
62
|
+
const CHIP_H = 22;
|
|
63
|
+
const CHIP_GAP = 3;
|
|
64
|
+
const ROW_MIN = 170;
|
|
65
|
+
const ROW_MARGIN = 12;
|
|
66
|
+
/** Distance between two rows' tops; only a fallback now. */
|
|
67
|
+
function rowPitch() {
|
|
68
|
+
const rows = el?.querySelectorAll("[data-week]");
|
|
69
|
+
return rows && rows.length > 1 ? rows[1].offsetTop - rows[0].offsetTop : ROW_MIN + ROW_MARGIN;
|
|
70
|
+
}
|
|
71
|
+
const _initMs = untrack(() => sod(focusDate?.getTime() ?? Date.now()));
|
|
72
|
+
let internalFocusMs = $state(_initMs);
|
|
73
|
+
let lastExternalMs = _initMs;
|
|
74
|
+
let el;
|
|
75
|
+
function scrollWeekIntoContainer(targetMs, behavior = "auto") {
|
|
76
|
+
if (!el) return;
|
|
77
|
+
let target = null;
|
|
78
|
+
if (targetMs !== undefined) {
|
|
79
|
+
// Find the week row containing this date
|
|
80
|
+
const rows = el.querySelectorAll("[data-week]");
|
|
81
|
+
for (const row of rows) {
|
|
82
|
+
const weekMs = Number(row.dataset.week);
|
|
83
|
+
if (weekMs <= targetMs && targetMs < weekMs + customDays * DAY_MS) {
|
|
84
|
+
target = row;
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Fall back to current week
|
|
90
|
+
if (!target) target = el.querySelector(".wg-week--current");
|
|
91
|
+
if (!target) return;
|
|
92
|
+
const targetTop = target.offsetTop - (el.clientHeight - target.offsetHeight) / 2;
|
|
93
|
+
el.scrollTo({
|
|
94
|
+
top: Math.max(0, targetTop),
|
|
95
|
+
behavior
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
// ─── Derived ────────────────────────────────────────
|
|
99
|
+
const todayMs = $derived(clock.today);
|
|
100
|
+
const customDays = $derived(viewState?.dayCount ?? 7);
|
|
101
|
+
const anchorPeriodStart = $derived(customDays === 7 ? sowFn(internalFocusMs, mondayStart) : sod(internalFocusMs));
|
|
102
|
+
// ─── Declare load range for entire visible buffer ────────
|
|
103
|
+
// Instead of calling store.load() directly, we tell Calendar
|
|
104
|
+
// what range we need. Calendar's single $effect handles loading.
|
|
105
|
+
$effect(() => {
|
|
106
|
+
if (!loadRangeCtx) return;
|
|
107
|
+
const rangeStart = new Date(anchorPeriodStart - bufferBefore * customDays * DAY_MS);
|
|
108
|
+
const rangeEnd = new Date(anchorPeriodStart + (bufferAfter + 1) * customDays * DAY_MS);
|
|
109
|
+
loadRangeCtx.set({
|
|
110
|
+
start: rangeStart,
|
|
111
|
+
end: rangeEnd
|
|
112
|
+
});
|
|
113
|
+
return () => loadRangeCtx.set(null);
|
|
114
|
+
});
|
|
115
|
+
const weeks = $derived.by(() => {
|
|
116
|
+
const result = [];
|
|
117
|
+
for (let w = -bufferBefore; w <= bufferAfter; w++) {
|
|
118
|
+
const periodStart = anchorPeriodStart + w * customDays * DAY_MS;
|
|
119
|
+
const isCurrent = todayMs >= periodStart && todayMs < periodStart + customDays * DAY_MS;
|
|
120
|
+
const days = [];
|
|
121
|
+
for (let d = 0; d < customDays; d++) {
|
|
122
|
+
const ms = periodStart + d * DAY_MS;
|
|
123
|
+
const date = new Date(ms);
|
|
124
|
+
const dayNum = date.getDate();
|
|
125
|
+
const dow = date.getDay();
|
|
126
|
+
const isWeekend = dow === 0 || dow === 6;
|
|
127
|
+
const isToday = ms === todayMs;
|
|
128
|
+
const isPast = equalDays ? false : ms < todayMs;
|
|
129
|
+
const isFirstOfMonth = dayNum === 1;
|
|
130
|
+
const monthLabel = d === 0 || isFirstOfMonth ? monthLong(ms, locale).toUpperCase() : null;
|
|
131
|
+
const dayEnd = ms + DAY_MS;
|
|
132
|
+
const dayEventsAll = events.filter((ev) => ev.start.getTime() < dayEnd && ev.end.getTime() > ms).sort((a, b) => a.start.getTime() - b.start.getTime());
|
|
133
|
+
// Separate all-day / multi-day from timed events
|
|
134
|
+
const timedEvents = [];
|
|
135
|
+
const allDaySegments = [];
|
|
136
|
+
for (const ev of dayEventsAll) {
|
|
137
|
+
if (isAllDay(ev) || isMultiDay(ev)) {
|
|
138
|
+
const seg = segmentForDay(ev, ms);
|
|
139
|
+
if (seg) allDaySegments.push(seg);
|
|
140
|
+
} else {
|
|
141
|
+
timedEvents.push(ev);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
days.push({
|
|
145
|
+
ms,
|
|
146
|
+
dayNum,
|
|
147
|
+
isToday,
|
|
148
|
+
isPast,
|
|
149
|
+
isWeekend,
|
|
150
|
+
isFirstOfMonth,
|
|
151
|
+
monthLabel,
|
|
152
|
+
events: timedEvents,
|
|
153
|
+
allDaySegments
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
// Month label: show when first day of period is day 1-7 (for 7-day), or first day of period (for custom)
|
|
157
|
+
const startDate = new Date(periodStart);
|
|
158
|
+
const showMonth = customDays === 7 ? startDate.getDate() <= 7 : startDate.getDate() <= customDays;
|
|
159
|
+
const monthLabel = showMonth ? monthLong(periodStart, locale).toUpperCase() : null;
|
|
160
|
+
result.push({
|
|
161
|
+
weekStart: periodStart,
|
|
162
|
+
isCurrent,
|
|
163
|
+
monthLabel,
|
|
164
|
+
days
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
// Filter hidden days if hideDays is set
|
|
168
|
+
if (hideDays?.length) {
|
|
169
|
+
for (const row of result) {
|
|
170
|
+
row.days = row.days.filter((d) => {
|
|
171
|
+
const isoDay = new Date(d.ms).getDay();
|
|
172
|
+
// Convert JS day (0=Sun) to ISO (7=Sun)
|
|
173
|
+
const iso = isoDay === 0 ? 7 : isoDay;
|
|
174
|
+
return !hideDays.includes(iso);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return result;
|
|
179
|
+
});
|
|
180
|
+
// ─── Format helpers ─────────────────────────────────
|
|
181
|
+
function fmtAmPm(d) {
|
|
182
|
+
return _fmtTime(d, locale);
|
|
183
|
+
}
|
|
184
|
+
// ─── Scroll to current week on mount ────────────────
|
|
185
|
+
onMount(() => {
|
|
186
|
+
tick().then(() => scrollWeekIntoContainer());
|
|
187
|
+
return () => cancelAnimationFrame(syncRaf);
|
|
188
|
+
});
|
|
189
|
+
// Rows grow when their events arrive, which moves every row below them —
|
|
190
|
+
// including the one we centred on an empty grid. Centre once more when the
|
|
191
|
+
// first events land, and never again, so this can't fight a reader who has
|
|
192
|
+
// already taken hold of the scroller.
|
|
193
|
+
let settled = $state(false);
|
|
194
|
+
$effect(() => {
|
|
195
|
+
if (settled || !events.length) return;
|
|
196
|
+
settled = true;
|
|
197
|
+
tick().then(() => {
|
|
198
|
+
if (!touched) scrollWeekIntoContainer(internalFocusMs);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
/** A real reader gesture (wheel or press), as opposed to a scroll we caused
|
|
202
|
+
* ourselves. Keyboard is not watched: the re-centre happens milliseconds
|
|
203
|
+
* after mount, before anyone has reached for a key, and a keydown handler
|
|
204
|
+
* on the grid element would need a tabindex it should not have. */
|
|
205
|
+
let touched = false;
|
|
206
|
+
/** Attached, not bound in markup: a handler on the `role="grid"` element
|
|
207
|
+
* would demand a tabindex the scroller should not carry. */
|
|
208
|
+
function watchTouch(root) {
|
|
209
|
+
const on = () => {
|
|
210
|
+
touched = true;
|
|
211
|
+
};
|
|
212
|
+
root.addEventListener("wheel", on, { passive: true });
|
|
213
|
+
root.addEventListener("pointerdown", on);
|
|
214
|
+
return () => {
|
|
215
|
+
root.removeEventListener("wheel", on);
|
|
216
|
+
root.removeEventListener("pointerdown", on);
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
// ─── External navigation (arrows, goToday) ──────────
|
|
220
|
+
// Only a focus that lands in ANOTHER period than the one the scroll
|
|
221
|
+
// already reported re-anchors the buffer. The scroll sync below writes
|
|
222
|
+
// the centre week back through viewState, and that value comes back
|
|
223
|
+
// here as `focusDate` — compared by day it could differ by hours (zoned
|
|
224
|
+
// dates) and the view re-anchored on itself: the whole buffer rebuilt
|
|
225
|
+
// around the centre week and scrolled to it. That was the big jump.
|
|
226
|
+
const periodOf = (ms) => customDays === 7 ? sowFn(ms, mondayStart) : sod(ms);
|
|
227
|
+
$effect(() => {
|
|
228
|
+
const ext = focusDate ? sod(focusDate.getTime()) : clock.today;
|
|
229
|
+
if (periodOf(ext) !== periodOf(lastExternalMs)) {
|
|
230
|
+
lastExternalMs = ext;
|
|
231
|
+
internalFocusMs = ext;
|
|
232
|
+
bufferBefore = INITIAL_BUFFER;
|
|
233
|
+
bufferAfter = INITIAL_BUFFER;
|
|
234
|
+
tick().then(() => scrollWeekIntoContainer(ext));
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
/** Push the week at the viewport centre to viewState (drives the header label + Today button). */
|
|
238
|
+
function syncFocusFromScroll() {
|
|
239
|
+
if (!el || !viewState) return;
|
|
240
|
+
const centerY = el.scrollTop + el.clientHeight / 2;
|
|
241
|
+
const rows = el.querySelectorAll("[data-week]");
|
|
242
|
+
for (const row of rows) {
|
|
243
|
+
if (row.offsetTop + row.offsetHeight >= centerY) {
|
|
244
|
+
const ms = Number(row.dataset.week);
|
|
245
|
+
if (Number.isFinite(ms) && ms !== lastExternalMs) {
|
|
246
|
+
lastExternalMs = ms;
|
|
247
|
+
viewState.setFocusDate(new Date(ms));
|
|
248
|
+
}
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
let extending = false;
|
|
254
|
+
let syncRaf = 0;
|
|
255
|
+
function handleUserScroll() {
|
|
256
|
+
// syncFocusFromScroll does querySelectorAll + offsetTop reads —
|
|
257
|
+
// rAF-throttle it so scroll ticks stay cheap.
|
|
258
|
+
if (!syncRaf) {
|
|
259
|
+
syncRaf = requestAnimationFrame(() => {
|
|
260
|
+
syncRaf = 0;
|
|
261
|
+
if (!extending) syncFocusFromScroll();
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
if (!el || extending) return;
|
|
265
|
+
// Extend buffer when user scrolls near the edge
|
|
266
|
+
if (el.scrollTop < EDGE_PX) {
|
|
267
|
+
extending = true;
|
|
268
|
+
// Anchor on the first rendered row: after the prepend the same row
|
|
269
|
+
// must sit at the same offset from the viewport top. (The browser's
|
|
270
|
+
// own scroll anchoring is off on .wg-body — two compensations for
|
|
271
|
+
// one insertion was the jump.)
|
|
272
|
+
const first = el.querySelector("[data-week]");
|
|
273
|
+
const key = first?.dataset.week;
|
|
274
|
+
const offset = first ? first.offsetTop - el.scrollTop : 0;
|
|
275
|
+
bufferBefore += EXTEND_BY;
|
|
276
|
+
tick().then(() => {
|
|
277
|
+
const row = key ? el.querySelector(`[data-week="${key}"]`) : null;
|
|
278
|
+
el.scrollTop = row ? row.offsetTop - offset : el.scrollTop + EXTEND_BY * rowPitch();
|
|
279
|
+
extending = false;
|
|
280
|
+
});
|
|
281
|
+
} else {
|
|
282
|
+
const bottomRemaining = el.scrollHeight - el.clientHeight - el.scrollTop;
|
|
283
|
+
if (bottomRemaining < EDGE_PX) {
|
|
284
|
+
bufferAfter += EXTEND_BY;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
/** Where a new thing lands on this day, or null when the day refuses it.
|
|
289
|
+
* A cell is a whole day here — there is no time axis to drop onto — so
|
|
290
|
+
* everything starts at the first visible hour. One rule, so an empty-cell
|
|
291
|
+
* click and a drop from outside can never disagree about a day. */
|
|
292
|
+
function dayDropStart(ms) {
|
|
293
|
+
if (disabledSet.has(ms)) return null;
|
|
294
|
+
const startHour = visibleHours?.[0] ?? 9;
|
|
295
|
+
if (blockedSlots?.length) {
|
|
296
|
+
const jsDay = new Date(ms).getDay();
|
|
297
|
+
const isoDay = jsDay === 0 ? 7 : jsDay;
|
|
298
|
+
const blocked = blockedSlots.some((slot) => {
|
|
299
|
+
if (slot.day && slot.day !== isoDay) return false;
|
|
300
|
+
return startHour >= slot.start && startHour < slot.end;
|
|
301
|
+
});
|
|
302
|
+
if (blocked) return null;
|
|
303
|
+
}
|
|
304
|
+
return new Date(ms + startHour * HOUR_MS);
|
|
305
|
+
}
|
|
306
|
+
function handleDayCellClick(ms, e) {
|
|
307
|
+
const target = e.target;
|
|
308
|
+
if (target.closest(".wg-ev, .wg-ad, .wg-ev-more")) return;
|
|
309
|
+
if (readOnly || !oneventcreate) return;
|
|
310
|
+
const start = dayDropStart(ms);
|
|
311
|
+
if (!start) return;
|
|
312
|
+
const durMin = minDuration ? Math.max(60, minDuration) : 60;
|
|
313
|
+
oneventcreate({
|
|
314
|
+
start,
|
|
315
|
+
end: new Date(start.getTime() + durMin * 6e4)
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
// ─── External drop (HTML5 DnD) ──────────────────────
|
|
319
|
+
// A class chip dragged in from outside the calendar; the cell it is over
|
|
320
|
+
// lights up so the day it would land on is never in doubt.
|
|
321
|
+
let dropDayMs = $state(null);
|
|
322
|
+
function onCellDragOver(e, ms) {
|
|
323
|
+
if (!onexternaldrop || readOnly || !dayDropStart(ms)) return;
|
|
324
|
+
e.preventDefault();
|
|
325
|
+
if (e.dataTransfer) e.dataTransfer.dropEffect = "copy";
|
|
326
|
+
dropDayMs = ms;
|
|
327
|
+
}
|
|
328
|
+
function onCellDragLeave(ms) {
|
|
329
|
+
if (dropDayMs === ms) dropDayMs = null;
|
|
330
|
+
}
|
|
331
|
+
function onCellDrop(e, ms) {
|
|
332
|
+
dropDayMs = null;
|
|
333
|
+
if (!onexternaldrop || readOnly || !e.dataTransfer) return;
|
|
334
|
+
const start = dayDropStart(ms);
|
|
335
|
+
if (!start) return;
|
|
336
|
+
e.preventDefault();
|
|
337
|
+
onexternaldrop({
|
|
338
|
+
start,
|
|
339
|
+
dataTransfer: e.dataTransfer
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
// ─── Event drag-to-move ───────────────────────────────────────
|
|
343
|
+
const DRAG_THRESHOLD = 8;
|
|
344
|
+
let evDragStartX = 0;
|
|
345
|
+
let evDragStartY = 0;
|
|
346
|
+
let evDragStarted = false;
|
|
347
|
+
let evDragging = $state(false);
|
|
348
|
+
let evDragId = $state(null);
|
|
349
|
+
let evDragEvent = null;
|
|
350
|
+
/** The pressed chip's rect, caught before any re-render can move it. */
|
|
351
|
+
let evAnchor;
|
|
352
|
+
/** Whether THIS press may become a move (see onEventPointerDown). */
|
|
353
|
+
let evDragMovable = false;
|
|
354
|
+
let evLastX = 0;
|
|
355
|
+
let evLastY = 0;
|
|
356
|
+
// Frozen at drag start: columns do not reflow mid-drag.
|
|
357
|
+
let evCellW = 100;
|
|
358
|
+
/** The week the press started in — rows differ in height, so the vertical
|
|
359
|
+
* step is the row under the pointer, found by hit-test, not by division.
|
|
360
|
+
* Held as a timestamp: an extension can renumber rows mid-drag. */
|
|
361
|
+
let evStartWeekMs = 0;
|
|
362
|
+
const dragPreviewEvent = $derived.by(() => {
|
|
363
|
+
const payload = drag?.active && drag.mode === "move" ? drag.payload : null;
|
|
364
|
+
if (!payload?.eventId) return null;
|
|
365
|
+
const ev = events.find((event) => event.id === payload.eventId);
|
|
366
|
+
if (!ev) return null;
|
|
367
|
+
return {
|
|
368
|
+
...ev,
|
|
369
|
+
start: payload.start,
|
|
370
|
+
end: payload.end
|
|
371
|
+
};
|
|
372
|
+
});
|
|
373
|
+
function isDraggedEvent(eventId) {
|
|
374
|
+
return dragPreviewEvent?.id === eventId;
|
|
375
|
+
}
|
|
376
|
+
function timedEventsForDay(day) {
|
|
377
|
+
if (!dragPreviewEvent) return day.events;
|
|
378
|
+
return day.events.filter((ev) => ev.id !== dragPreviewEvent.id);
|
|
379
|
+
}
|
|
380
|
+
// Crossfade keys for the previews, snapshotted at render time. Transition
|
|
381
|
+
// params are evaluated lazily at unmount — after the drag payload is
|
|
382
|
+
// already null — so they must never read the reactive preview directly.
|
|
383
|
+
// Plain Map (not $state): written during render, read only by transitions.
|
|
384
|
+
const previewKeySnapshot = new Map();
|
|
385
|
+
function dragPreviewTimedForDay(dayMs) {
|
|
386
|
+
const ev = dragPreviewEvent;
|
|
387
|
+
if (!ev || isAllDay(ev) || isMultiDay(ev)) return null;
|
|
388
|
+
const dayEnd = dayMs + DAY_MS;
|
|
389
|
+
const hit = ev.start.getTime() < dayEnd && ev.end.getTime() > dayMs;
|
|
390
|
+
if (hit) previewKeySnapshot.set("timed", ev.id);
|
|
391
|
+
return hit ? ev : null;
|
|
392
|
+
}
|
|
393
|
+
function dragPreviewSegmentForDay(dayMs) {
|
|
394
|
+
const ev = dragPreviewEvent;
|
|
395
|
+
if (!ev || !isAllDay(ev) && !isMultiDay(ev)) return null;
|
|
396
|
+
const seg = segmentForDay(ev, dayMs);
|
|
397
|
+
if (seg) previewKeySnapshot.set(dayMs, `${ev.id}:${seg.dayIndex}`);
|
|
398
|
+
return seg;
|
|
399
|
+
}
|
|
400
|
+
// ─── Chip labels ────────────────────────────────
|
|
401
|
+
// A chip is one line and holds more than fits. It gives up the room first
|
|
402
|
+
// and the time second, rather than letting CSS cut the title mid-letter;
|
|
403
|
+
// createChipFit measures, fitParts decides. Before the first measurement
|
|
404
|
+
// (SSR, first paint) a chip is its title.
|
|
405
|
+
const CHIP_PAD_X = 12;
|
|
406
|
+
const CHIP_GAP_X = 5;
|
|
407
|
+
const fit = createChipFit({
|
|
408
|
+
slot: ".wg-cell",
|
|
409
|
+
fonts: {
|
|
410
|
+
title: ".wg-probe .wg-ev-title",
|
|
411
|
+
time: ".wg-probe .wg-ev-time",
|
|
412
|
+
room: ".wg-probe .wg-ev-loc"
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
function chipParts(ev) {
|
|
416
|
+
return fit.parts([
|
|
417
|
+
{
|
|
418
|
+
key: "title",
|
|
419
|
+
text: ev.title,
|
|
420
|
+
font: fit.fonts.title,
|
|
421
|
+
priority: 0
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
key: "time",
|
|
425
|
+
text: fmtAmPm(ev.start),
|
|
426
|
+
font: fit.fonts.time,
|
|
427
|
+
priority: 1,
|
|
428
|
+
extra: CHIP_GAP_X
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
key: "room",
|
|
432
|
+
text: ev.location ?? "",
|
|
433
|
+
font: fit.fonts.room,
|
|
434
|
+
priority: 2,
|
|
435
|
+
extra: CHIP_GAP_X
|
|
436
|
+
}
|
|
437
|
+
], CHIP_PAD_X);
|
|
438
|
+
}
|
|
439
|
+
function getCellWidth() {
|
|
440
|
+
const cell = el?.querySelector(".wg-cell");
|
|
441
|
+
return cell ? cell.getBoundingClientRect().width : 100;
|
|
442
|
+
}
|
|
443
|
+
/** The week row under this viewport Y, as its start timestamp. */
|
|
444
|
+
function weekMsAtY(clientY) {
|
|
445
|
+
const rows = el?.querySelectorAll("[data-week]");
|
|
446
|
+
if (!rows?.length) return 0;
|
|
447
|
+
for (const row of rows) {
|
|
448
|
+
if (clientY < row.getBoundingClientRect().bottom) return Number(row.dataset.week);
|
|
449
|
+
}
|
|
450
|
+
return Number(rows[rows.length - 1].dataset.week);
|
|
451
|
+
}
|
|
452
|
+
function onEventPointerDown(e, ev) {
|
|
453
|
+
if (e.button !== 0) return;
|
|
454
|
+
e.stopPropagation();
|
|
455
|
+
evAnchor = e.currentTarget.getBoundingClientRect();
|
|
456
|
+
// Even an event that cannot move — a read-only view, a read-only event,
|
|
457
|
+
// and most of a schedule is read-only occurrences — still goes through
|
|
458
|
+
// the pointerup path, so a plain click opens it. Refusing the press
|
|
459
|
+
// outright made three quarters of the chips dead to the mouse.
|
|
460
|
+
evDragMovable = !!drag && !readOnly && !ev.data?.readOnly;
|
|
461
|
+
evDragStartX = e.clientX;
|
|
462
|
+
evDragStartY = e.clientY;
|
|
463
|
+
evLastX = e.clientX;
|
|
464
|
+
evLastY = e.clientY;
|
|
465
|
+
evDragStarted = false;
|
|
466
|
+
evDragId = ev.id;
|
|
467
|
+
evDragEvent = ev;
|
|
468
|
+
window.addEventListener("pointermove", onEvWindowPointerMove);
|
|
469
|
+
window.addEventListener("pointerup", onEvWindowPointerUp, { once: true });
|
|
470
|
+
window.addEventListener("pointercancel", onEvWindowPointerCancel, { once: true });
|
|
471
|
+
}
|
|
472
|
+
/** Where the dragged event sits for the pointer's current offset. */
|
|
473
|
+
function updateDragFromPointer() {
|
|
474
|
+
const ev = evDragEvent;
|
|
475
|
+
if (!drag || !ev) return;
|
|
476
|
+
const dayOffset = Math.round((evLastX - evDragStartX) / evCellW);
|
|
477
|
+
const nowWeekMs = weekMsAtY(evLastY);
|
|
478
|
+
// A vertical row step spans one period (customDays), not always 7 days
|
|
479
|
+
const weekOffset = evStartWeekMs && nowWeekMs ? Math.round((nowWeekMs - evStartWeekMs) / (customDays * DAY_MS)) : 0;
|
|
480
|
+
const deltaMs = (dayOffset + weekOffset * customDays) * DAY_MS;
|
|
481
|
+
drag.updatePointer(new Date(ev.start.getTime() + deltaMs), new Date(ev.end.getTime() + deltaMs));
|
|
482
|
+
}
|
|
483
|
+
// ─── Auto-scroll under a drag ───────────────────────
|
|
484
|
+
// Weeks the target is in may be off-screen — without this, moving an event
|
|
485
|
+
// a month out means dropping it, scrolling, and dragging again. The week
|
|
486
|
+
// is re-read from whatever row is under the pointer after each step, so
|
|
487
|
+
// the scroll moves the target without any origin bookkeeping.
|
|
488
|
+
const AUTO_EDGE = 56;
|
|
489
|
+
const AUTO_STEP = 14;
|
|
490
|
+
let autoDir = 0;
|
|
491
|
+
let autoRaf = 0;
|
|
492
|
+
function autoScrollTick() {
|
|
493
|
+
autoRaf = 0;
|
|
494
|
+
if (!autoDir || !el) return;
|
|
495
|
+
const before = el.scrollTop;
|
|
496
|
+
el.scrollTop += autoDir * AUTO_STEP;
|
|
497
|
+
if (el.scrollTop !== before) updateDragFromPointer();
|
|
498
|
+
autoRaf = requestAnimationFrame(autoScrollTick);
|
|
499
|
+
}
|
|
500
|
+
function setAutoScroll(clientY) {
|
|
501
|
+
if (!el) return;
|
|
502
|
+
const r = el.getBoundingClientRect();
|
|
503
|
+
const dir = clientY < r.top + AUTO_EDGE ? -1 : clientY > r.bottom - AUTO_EDGE ? 1 : 0;
|
|
504
|
+
if (dir === autoDir) return;
|
|
505
|
+
autoDir = dir;
|
|
506
|
+
if (dir && !autoRaf) autoRaf = requestAnimationFrame(autoScrollTick);
|
|
507
|
+
}
|
|
508
|
+
function stopAutoScroll() {
|
|
509
|
+
autoDir = 0;
|
|
510
|
+
if (autoRaf) cancelAnimationFrame(autoRaf);
|
|
511
|
+
autoRaf = 0;
|
|
512
|
+
}
|
|
513
|
+
function onEvWindowPointerMove(e) {
|
|
514
|
+
const ev = evDragEvent;
|
|
515
|
+
if (!evDragMovable || !drag || !ev || evDragId !== ev.id) return;
|
|
516
|
+
evLastX = e.clientX;
|
|
517
|
+
evLastY = e.clientY;
|
|
518
|
+
const dx = e.clientX - evDragStartX;
|
|
519
|
+
const dy = e.clientY - evDragStartY;
|
|
520
|
+
if (!evDragStarted && Math.abs(dx) + Math.abs(dy) < DRAG_THRESHOLD) return;
|
|
521
|
+
if (!evDragStarted) {
|
|
522
|
+
evDragStarted = true;
|
|
523
|
+
evDragging = true;
|
|
524
|
+
evCellW = getCellWidth();
|
|
525
|
+
evStartWeekMs = weekMsAtY(evDragStartY);
|
|
526
|
+
drag.beginMove(ev.id, ev.start, ev.end);
|
|
527
|
+
}
|
|
528
|
+
setAutoScroll(e.clientY);
|
|
529
|
+
updateDragFromPointer();
|
|
530
|
+
}
|
|
531
|
+
function cleanupEvDrag() {
|
|
532
|
+
window.removeEventListener("pointermove", onEvWindowPointerMove);
|
|
533
|
+
window.removeEventListener("pointerup", onEvWindowPointerUp);
|
|
534
|
+
window.removeEventListener("pointercancel", onEvWindowPointerCancel);
|
|
535
|
+
stopAutoScroll();
|
|
536
|
+
evDragStarted = false;
|
|
537
|
+
evDragging = false;
|
|
538
|
+
evDragId = null;
|
|
539
|
+
evDragEvent = null;
|
|
540
|
+
evAnchor = undefined;
|
|
541
|
+
evDragMovable = false;
|
|
542
|
+
evStartWeekMs = 0;
|
|
543
|
+
}
|
|
544
|
+
function onEvWindowPointerUp() {
|
|
545
|
+
if (!evDragStarted) {
|
|
546
|
+
// A click, not a drag: hand the host the chip's rect so a panel can
|
|
547
|
+
// open beside it rather than in the middle of the screen.
|
|
548
|
+
if (evDragEvent) oneventclick?.(evDragEvent, evAnchor);
|
|
549
|
+
} else if (drag) {
|
|
550
|
+
commitDragCtx?.();
|
|
551
|
+
}
|
|
552
|
+
cleanupEvDrag();
|
|
553
|
+
}
|
|
554
|
+
function onEvWindowPointerCancel() {
|
|
555
|
+
if (drag && evDragStarted) drag.cancel();
|
|
556
|
+
cleanupEvDrag();
|
|
557
|
+
}
|
|
558
|
+
// ─── Escape cancels an in-flight drag ───────────────
|
|
559
|
+
function onWindowKeydown(e) {
|
|
560
|
+
if (e.key !== "Escape" || !drag?.active) return;
|
|
561
|
+
drag.cancel();
|
|
562
|
+
cleanupEvDrag();
|
|
563
|
+
}
|
|
564
|
+
// ─── "+N more" per-cell expansion ───────────────────
|
|
565
|
+
let expandedCells = $state({});
|
|
566
|
+
// ─── Roving tabindex for grid cells ─────────────────
|
|
567
|
+
// One tabbable cell (last focused, else today); arrows move focus.
|
|
568
|
+
let focusedCellMs = $state(null);
|
|
569
|
+
const tabbableCellMs = $derived(focusedCellMs ?? todayMs);
|
|
570
|
+
function onCellKeydown(e, ms) {
|
|
571
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
572
|
+
e.preventDefault();
|
|
573
|
+
handleDayCellClick(ms, e);
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
let step = 0;
|
|
577
|
+
if (e.key === "ArrowRight") step = DAY_MS;
|
|
578
|
+
else if (e.key === "ArrowLeft") step = -DAY_MS;
|
|
579
|
+
else if (e.key === "ArrowDown") step = customDays * DAY_MS;
|
|
580
|
+
else if (e.key === "ArrowUp") step = -customDays * DAY_MS;
|
|
581
|
+
if (step === 0) return;
|
|
582
|
+
e.preventDefault();
|
|
583
|
+
// Walk in the step direction until a rendered cell is found
|
|
584
|
+
// (skips hidden days; bails at the buffer edge).
|
|
585
|
+
let target = ms + step;
|
|
586
|
+
for (let i = 0; i < 7; i++) {
|
|
587
|
+
const cell = el?.querySelector(`[data-day="${target}"]`);
|
|
588
|
+
if (cell) {
|
|
589
|
+
focusedCellMs = target;
|
|
590
|
+
cell.focus();
|
|
591
|
+
cell.scrollIntoView({ block: "nearest" });
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
target += step < 0 ? -DAY_MS : DAY_MS;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
</script>
|
|
598
|
+
|
|
599
|
+
{#snippet allDaySegmentContent(seg: DaySegment)}
|
|
600
|
+
{#if seg.isStart}
|
|
601
|
+
<span class="wg-ad-title">{seg.ev.title}</span>
|
|
602
|
+
{:else}
|
|
603
|
+
<span class="wg-ad-cont" aria-hidden="true">◂</span>
|
|
604
|
+
<span class="wg-ad-title">{seg.ev.title}</span>
|
|
605
|
+
{/if}
|
|
606
|
+
{#if !seg.isEnd && seg.totalDays > 1}
|
|
607
|
+
<span class="wg-ad-arrow" aria-hidden="true">▸</span>
|
|
608
|
+
{/if}
|
|
609
|
+
{/snippet}
|
|
610
|
+
|
|
611
|
+
{#snippet timedEventContent(ev: TimelineEvent)}
|
|
612
|
+
{@const parts = chipParts(ev)}
|
|
613
|
+
<EventContent event={ev}>
|
|
614
|
+
{#if parts.time}<span class="wg-ev-time">{fmtAmPm(ev.start)}</span>{/if}
|
|
615
|
+
<span class="wg-ev-title">{ev.title}</span>
|
|
616
|
+
{#if parts.room}
|
|
617
|
+
<span class="wg-ev-loc">{ev.location}</span>
|
|
618
|
+
{/if}
|
|
619
|
+
</EventContent>
|
|
620
|
+
{/snippet}
|
|
621
|
+
|
|
622
|
+
<svelte:window onkeydown={onWindowKeydown} />
|
|
623
|
+
|
|
624
|
+
<div class="wg" class:wg--auto={autoHeight} style={style || undefined} style:height={autoHeight ? undefined : (height ? `${height}px` : '100%')} style:--wg-row-min="{ROW_MIN}px" style:--wg-chip-h="{CHIP_H}px" style:--wg-chip-gap="{CHIP_GAP}px" style:--wg-row-margin="{ROW_MARGIN}px">
|
|
625
|
+
<div
|
|
626
|
+
class="wg-body"
|
|
627
|
+
bind:this={el}
|
|
628
|
+
{@attach fit.watch}
|
|
629
|
+
{@attach watchTouch}
|
|
630
|
+
onscroll={handleUserScroll}
|
|
631
|
+
role="grid"
|
|
632
|
+
aria-label={L.multiWeekGrid}
|
|
633
|
+
>
|
|
634
|
+
<!-- Font probe: a dropped part cannot report the font it wanted, so the
|
|
635
|
+
three chip fonts are read from here instead of from a live chip. -->
|
|
636
|
+
<div class="wg-probe" aria-hidden="true">
|
|
637
|
+
<span class="wg-ev-time"></span><span class="wg-ev-title"></span><span class="wg-ev-loc"></span>
|
|
638
|
+
</div>
|
|
639
|
+
{#each weeks as week (week.weekStart)}
|
|
640
|
+
<div class="wg-week" class:wg-week--current={week.isCurrent} data-week={week.weekStart} role="presentation">
|
|
641
|
+
<div class="wg-week-body" role="presentation">
|
|
642
|
+
<!-- Day columns (header inside each cell) -->
|
|
643
|
+
<div class="wg-days" role="row">
|
|
644
|
+
{#each week.days as day (day.ms)}
|
|
645
|
+
{@const visibleAllDaySegments = day.allDaySegments.filter((seg) => !isDraggedEvent(seg.ev.id))}
|
|
646
|
+
{@const visibleTimedEvents = timedEventsForDay(day)}
|
|
647
|
+
{@const isExpanded = expandedCells[day.ms] ?? false}
|
|
648
|
+
{@const timedCap = Math.max(0, MAX_EVENTS_SHOWN - visibleAllDaySegments.length)}
|
|
649
|
+
{@const hiddenCount = Math.max(0, visibleTimedEvents.length - timedCap)}
|
|
650
|
+
{@const previewTimedEvent = dragPreviewTimedForDay(day.ms)}
|
|
651
|
+
{@const previewSegment = dragPreviewSegmentForDay(day.ms)}
|
|
652
|
+
<div
|
|
653
|
+
class="wg-cell"
|
|
654
|
+
class:wg-cell--today={day.isToday}
|
|
655
|
+
class:wg-cell--past={day.isPast}
|
|
656
|
+
class:wg-cell--weekend={day.isWeekend}
|
|
657
|
+
class:wg-cell--disabled={disabledSet.has(day.ms)}
|
|
658
|
+
class:wg-cell--expanded={isExpanded}
|
|
659
|
+
class:wg-cell--drop={dropDayMs === day.ms}
|
|
660
|
+
role="gridcell"
|
|
661
|
+
data-day={day.ms}
|
|
662
|
+
tabindex={day.ms === tabbableCellMs ? 0 : -1}
|
|
663
|
+
aria-label="{new Date(day.ms).toLocaleDateString(locale ?? 'en-US', { weekday: 'long', month: 'short', day: 'numeric' })}{day.isToday ? ` (${L.today.toLowerCase()})` : ''}, {L.nEvents(day.events.length + day.allDaySegments.length)}"
|
|
664
|
+
onclick={(e) => handleDayCellClick(day.ms, e)}
|
|
665
|
+
ondragover={(e) => onCellDragOver(e, day.ms)}
|
|
666
|
+
ondragleave={() => onCellDragLeave(day.ms)}
|
|
667
|
+
ondrop={(e) => onCellDrop(e, day.ms)}
|
|
668
|
+
onfocus={() => { focusedCellMs = day.ms; }}
|
|
669
|
+
onkeydown={(e) => onCellKeydown(e, day.ms)}
|
|
670
|
+
>
|
|
671
|
+
<!-- Day label in top-right corner -->
|
|
672
|
+
<div class="wg-cell-hd" class:wg-cell-hd--today={day.isToday}>
|
|
673
|
+
{#if showDates}
|
|
674
|
+
<span class="wg-day-num" class:wg-day-num--today={day.isToday}>
|
|
675
|
+
{day.dayNum}
|
|
676
|
+
</span>
|
|
677
|
+
{/if}
|
|
678
|
+
<span class="wg-day-wd">{weekdayShort(day.ms, locale)}</span>
|
|
679
|
+
</div>
|
|
680
|
+
|
|
681
|
+
<!-- Custom day header snippet -->
|
|
682
|
+
{#if dayHeaderSnippet}
|
|
683
|
+
<div class="wg-cell-custom-header">
|
|
684
|
+
{@render dayHeaderSnippet({ date: new Date(day.ms), isToday: day.isToday, dayName: weekdayShort(day.ms, locale) })}
|
|
685
|
+
</div>
|
|
686
|
+
{/if}
|
|
687
|
+
|
|
688
|
+
<!-- Blocked slots indicator -->
|
|
689
|
+
{#if blockedSlots?.length}
|
|
690
|
+
{@const jsDay = new Date(day.ms).getDay()}
|
|
691
|
+
{@const isoDay = jsDay === 0 ? 7 : jsDay}
|
|
692
|
+
{#each blockedSlots as slot, i (i)}
|
|
693
|
+
{#if !slot.day || slot.day === isoDay}
|
|
694
|
+
{@const slotRange = `${_fmtTime(new Date(day.ms + slot.start * HOUR_MS), locale)} – ${_fmtTime(new Date(day.ms + slot.end * HOUR_MS), locale)}`}
|
|
695
|
+
<div
|
|
696
|
+
class="wg-blocked"
|
|
697
|
+
title="{slot.label ? `${slot.label}, ` : ''}{slotRange}"
|
|
698
|
+
aria-label="{slot.label || 'Unavailable'}, {slotRange}"
|
|
699
|
+
>
|
|
700
|
+
{#if slot.label}
|
|
701
|
+
<span class="wg-blocked-label">{slot.label}</span>
|
|
702
|
+
{/if}
|
|
703
|
+
</div>
|
|
704
|
+
{/if}
|
|
705
|
+
{/each}
|
|
706
|
+
{/if}
|
|
707
|
+
|
|
708
|
+
<!-- All-day / multi-day events -->
|
|
709
|
+
{#if visibleAllDaySegments.length > 0 || previewSegment}
|
|
710
|
+
<div class="wg-allday">
|
|
711
|
+
{#each visibleAllDaySegments as seg (seg.ev.id)}
|
|
712
|
+
<div
|
|
713
|
+
animate:flip={{ duration: ANIM }}
|
|
714
|
+
in:previewReceive={{ key: `${seg.ev.id}:${seg.dayIndex}` }}
|
|
715
|
+
out:previewSend={{ key: `${seg.ev.id}:${seg.dayIndex}` }}
|
|
716
|
+
class="wg-ad"
|
|
717
|
+
class:wg-ad--start={seg.isStart}
|
|
718
|
+
class:wg-ad--end={seg.isEnd}
|
|
719
|
+
class:wg-ad--mid={!seg.isStart && !seg.isEnd}
|
|
720
|
+
class:wg-ad--selected={selectedEventId === seg.ev.id}
|
|
721
|
+
style:--ev-color={seg.ev.color ?? 'var(--dt-accent)'}
|
|
722
|
+
role="button"
|
|
723
|
+
tabindex="0"
|
|
724
|
+
aria-label="{seg.ev.title}{seg.totalDays > 1 ? `, ${L.dayNOfTotal(seg.dayIndex, seg.totalDays)}` : `, ${L.allDay}`}"
|
|
725
|
+
onpointerdown={(e) => onEventPointerDown(e, seg.ev)}
|
|
726
|
+
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); oneventclick?.(seg.ev, e.currentTarget.getBoundingClientRect()); } }}
|
|
727
|
+
>
|
|
728
|
+
{@render allDaySegmentContent(seg)}
|
|
729
|
+
</div>
|
|
730
|
+
{/each}
|
|
731
|
+
{#if previewSegment}
|
|
732
|
+
<!-- key by event id + dayIndex: multi-day previews render one segment
|
|
733
|
+
per cell, and each pairs with its own real-card counterpart -->
|
|
734
|
+
<div
|
|
735
|
+
class="wg-ad wg-ad--drag-preview"
|
|
736
|
+
class:wg-ad--start={previewSegment.isStart}
|
|
737
|
+
class:wg-ad--end={previewSegment.isEnd}
|
|
738
|
+
class:wg-ad--mid={!previewSegment.isStart && !previewSegment.isEnd}
|
|
739
|
+
style:--ev-color={previewSegment.ev.color ?? 'var(--dt-accent)'}
|
|
740
|
+
aria-hidden="true"
|
|
741
|
+
in:previewReceive={{ key: previewKeySnapshot.get(day.ms) ?? '' }}
|
|
742
|
+
out:previewSend={{ key: previewKeySnapshot.get(day.ms) ?? '' }}
|
|
743
|
+
>
|
|
744
|
+
{@render allDaySegmentContent(previewSegment)}
|
|
745
|
+
</div>
|
|
746
|
+
{/if}
|
|
747
|
+
</div>
|
|
748
|
+
{/if}
|
|
749
|
+
|
|
750
|
+
<!-- Timed events -->
|
|
751
|
+
<div class="wg-cell-events">
|
|
752
|
+
{#each visibleTimedEvents.slice(0, isExpanded ? visibleTimedEvents.length : timedCap) as ev (ev.id)}
|
|
753
|
+
<!-- send/receive keyed by event id pair the card with the drag ghost:
|
|
754
|
+
drag start morphs card → ghost, drop morphs ghost → placed card -->
|
|
755
|
+
<div
|
|
756
|
+
animate:flip={{ duration: ANIM }}
|
|
757
|
+
in:previewReceive={{ key: ev.id }}
|
|
758
|
+
out:previewSend={{ key: ev.id }}
|
|
759
|
+
class="wg-ev"
|
|
760
|
+
class:wg-ev--selected={selectedEventId === ev.id}
|
|
761
|
+
class:wg-ev--current={ev.start.getTime() <= clock.tick && ev.end.getTime() > clock.tick}
|
|
762
|
+
class:wg-ev--dragging={evDragging && evDragId === ev.id}
|
|
763
|
+
class:wg-ev--readonly={ev.data?.readOnly}
|
|
764
|
+
class:wg-ev--cancelled={ev.status === 'cancelled'}
|
|
765
|
+
class:wg-ev--tentative={ev.status === 'tentative'}
|
|
766
|
+
class:wg-ev--full={ev.status === 'full'}
|
|
767
|
+
class:wg-ev--limited={ev.status === 'limited'}
|
|
768
|
+
style:--ev-color={ev.color ?? 'var(--dt-accent)'}
|
|
769
|
+
role="button"
|
|
770
|
+
tabindex="0"
|
|
771
|
+
aria-label="{ev.title}, {fmtAmPm(ev.start)} – {fmtAmPm(ev.end)}{ev.status === 'cancelled' ? ` (cancelled)` : ''}{ev.status === 'tentative' ? ` (tentative)` : ''}{ev.status === 'full' ? ` (full)` : ''}{ev.status === 'limited' ? ` (limited)` : ''}{ev.start.getTime() <= clock.tick && ev.end.getTime() > clock.tick ? ` (${L.inProgress})` : ''}"
|
|
772
|
+
onpointerdown={(e) => onEventPointerDown(e, ev)}
|
|
773
|
+
onpointerenter={() => oneventhover?.(ev)}
|
|
774
|
+
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); oneventclick?.(ev, e.currentTarget.getBoundingClientRect()); } }}
|
|
775
|
+
>
|
|
776
|
+
{@render timedEventContent(ev)}
|
|
777
|
+
</div>
|
|
778
|
+
{/each}
|
|
779
|
+
{#if previewTimedEvent}
|
|
780
|
+
<div
|
|
781
|
+
class="wg-ev wg-ev--drag-preview"
|
|
782
|
+
style:--ev-color={previewTimedEvent.color ?? 'var(--dt-accent)'}
|
|
783
|
+
aria-hidden="true"
|
|
784
|
+
in:previewReceive={{ key: previewKeySnapshot.get('timed') ?? '' }}
|
|
785
|
+
out:previewSend={{ key: previewKeySnapshot.get('timed') ?? '' }}
|
|
786
|
+
>
|
|
787
|
+
{@render timedEventContent(previewTimedEvent)}
|
|
788
|
+
</div>
|
|
789
|
+
{/if}
|
|
790
|
+
</div>
|
|
791
|
+
{#if hiddenCount > 0}
|
|
792
|
+
<button
|
|
793
|
+
type="button"
|
|
794
|
+
class="wg-ev-more"
|
|
795
|
+
aria-expanded={isExpanded}
|
|
796
|
+
onclick={(e) => { e.stopPropagation(); expandedCells[day.ms] = !isExpanded; }}
|
|
797
|
+
>{isExpanded ? L.showLess : L.nMore(hiddenCount)}</button>
|
|
798
|
+
{/if}
|
|
799
|
+
</div>
|
|
800
|
+
{/each}
|
|
801
|
+
</div>
|
|
802
|
+
</div>
|
|
803
|
+
</div>
|
|
804
|
+
{/each}
|
|
805
|
+
</div>
|
|
806
|
+
|
|
807
|
+
</div>
|
|
808
|
+
|
|
809
|
+
<style>
|
|
810
|
+
/* ─── Container ──────────────────────────────────── */
|
|
811
|
+
.wg {
|
|
812
|
+
position: relative;
|
|
813
|
+
overflow: hidden;
|
|
814
|
+
display: flex;
|
|
815
|
+
flex-direction: column;
|
|
816
|
+
user-select: none;
|
|
817
|
+
font-variant-numeric: tabular-nums;
|
|
818
|
+
}
|
|
819
|
+
.wg--auto { overflow: visible; }
|
|
820
|
+
|
|
821
|
+
/* ─── Scrollable body ────────────────────────────── */
|
|
822
|
+
.wg-body {
|
|
823
|
+
flex: 1;
|
|
824
|
+
overflow-y: auto;
|
|
825
|
+
/* The prepend is compensated by hand (handleUserScroll); the browser's
|
|
826
|
+
anchoring on top of it moved the content twice. */
|
|
827
|
+
overflow-anchor: none;
|
|
828
|
+
/* Seven columns scroll horizontally at narrow widths instead of squishing */
|
|
829
|
+
overflow-x: auto;
|
|
830
|
+
box-sizing: border-box;
|
|
831
|
+
scrollbar-width: thin;
|
|
832
|
+
scrollbar-color: var(--dt-scrollbar, rgba(0, 0, 0, 0.1)) transparent;
|
|
833
|
+
}
|
|
834
|
+
.wg--auto .wg-body { overflow-y: visible; }
|
|
835
|
+
|
|
836
|
+
.wg-body::-webkit-scrollbar { width: 4px; }
|
|
837
|
+
.wg-body::-webkit-scrollbar-thumb {
|
|
838
|
+
background: var(--dt-scrollbar, rgba(0, 0, 0, 0.1));
|
|
839
|
+
border-radius: 4px;
|
|
840
|
+
}
|
|
841
|
+
.wg-body::-webkit-scrollbar-track { background: transparent; }
|
|
842
|
+
|
|
843
|
+
.wg-probe {
|
|
844
|
+
position: absolute;
|
|
845
|
+
visibility: hidden;
|
|
846
|
+
height: 0;
|
|
847
|
+
overflow: hidden;
|
|
848
|
+
pointer-events: none;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/* ─── Week row ───────────────────────────────────── */
|
|
852
|
+
.wg-week {
|
|
853
|
+
display: flex;
|
|
854
|
+
border-radius: 10px;
|
|
855
|
+
margin: var(--wg-row-margin, 12px) 8px;
|
|
856
|
+
border: 1.5px solid var(--dt-border, rgba(0, 0, 0, 0.08));
|
|
857
|
+
overflow: hidden;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
.wg-week--current {
|
|
861
|
+
background: var(--dt-today-bg, rgba(37, 99, 235, 0.04));
|
|
862
|
+
/* Border width stays constant (no layout shift); emphasis via box-shadow */
|
|
863
|
+
border-color: var(--dt-accent, #2563eb);
|
|
864
|
+
box-shadow: 0 0 0 1.5px color-mix(in srgb, var(--dt-accent, #2563eb) 55%, transparent);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/* ─── Week body ──────────────────────────────────── */
|
|
868
|
+
.wg-week-body {
|
|
869
|
+
flex: 1;
|
|
870
|
+
min-width: 0;
|
|
871
|
+
display: flex;
|
|
872
|
+
flex-direction: column;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/* ─── Day columns ────────────────────────────────── */
|
|
876
|
+
.wg-days {
|
|
877
|
+
display: flex;
|
|
878
|
+
flex: 1;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
.wg-cell {
|
|
882
|
+
flex: 1;
|
|
883
|
+
position: relative;
|
|
884
|
+
display: flex;
|
|
885
|
+
flex-direction: column;
|
|
886
|
+
min-width: 90px;
|
|
887
|
+
min-height: var(--wg-row-min, 170px);
|
|
888
|
+
box-sizing: border-box;
|
|
889
|
+
padding: 4px 4px 8px;
|
|
890
|
+
border-right: 1px solid var(--dt-border, rgba(0, 0, 0, 0.08));
|
|
891
|
+
cursor: pointer;
|
|
892
|
+
transition: background 0.15s;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
.wg-cell:last-child { border-right: none; }
|
|
896
|
+
.wg-cell:hover { background: var(--dt-hover, rgba(0, 0, 0, 0.015)); }
|
|
897
|
+
|
|
898
|
+
.wg-cell--today { background: var(--dt-today-bg, rgba(37, 99, 235, 0.04)); }
|
|
899
|
+
.wg-cell--drop {
|
|
900
|
+
background: color-mix(in srgb, var(--dt-accent, #2563eb) 12%, transparent) !important;
|
|
901
|
+
box-shadow: inset 0 0 0 2px var(--dt-accent, #2563eb);
|
|
902
|
+
}
|
|
903
|
+
.wg-cell--today:hover { background: color-mix(in srgb, var(--dt-accent, #2563eb) 6%, transparent); }
|
|
904
|
+
|
|
905
|
+
/* Dim non-current weeks with a subtle wash + softer header text instead of
|
|
906
|
+
a subtree opacity, so event content keeps full contrast everywhere. */
|
|
907
|
+
.wg-week:not(.wg-week--current) .wg-cell {
|
|
908
|
+
background: color-mix(in srgb, var(--dt-text, rgba(0, 0, 0, 0.87)) 2.5%, transparent);
|
|
909
|
+
}
|
|
910
|
+
.wg-week:not(.wg-week--current) .wg-day-num {
|
|
911
|
+
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
912
|
+
}
|
|
913
|
+
.wg-week--current .wg-cell--past {
|
|
914
|
+
background: color-mix(in srgb, var(--dt-text, rgba(0, 0, 0, 0.87)) 2.5%, transparent);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/* equalDays: when no cells are marked past, all are full brightness */
|
|
918
|
+
|
|
919
|
+
.wg-cell--weekend { background: var(--dt-weekend-bg, rgba(0, 0, 0, 0.012)); }
|
|
920
|
+
|
|
921
|
+
/* ─── Disabled cell ──────────────────────────────── */
|
|
922
|
+
.wg-cell--disabled {
|
|
923
|
+
background: repeating-linear-gradient(
|
|
924
|
+
45deg,
|
|
925
|
+
transparent,
|
|
926
|
+
transparent 6px,
|
|
927
|
+
var(--dt-border, rgba(0, 0, 0, 0.08)) 6px,
|
|
928
|
+
var(--dt-border, rgba(0, 0, 0, 0.08)) 7px
|
|
929
|
+
) !important;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/* ─── Blocked slot indicator ─────────────────────── */
|
|
933
|
+
.wg-blocked {
|
|
934
|
+
display: flex;
|
|
935
|
+
align-items: center;
|
|
936
|
+
gap: 3px;
|
|
937
|
+
padding: 2px 4px;
|
|
938
|
+
border-radius: 3px;
|
|
939
|
+
background: repeating-linear-gradient(
|
|
940
|
+
-45deg,
|
|
941
|
+
color-mix(in srgb, var(--dt-text, rgba(0, 0, 0, 0.87)) 4%, transparent),
|
|
942
|
+
color-mix(in srgb, var(--dt-text, rgba(0, 0, 0, 0.87)) 4%, transparent) 3px,
|
|
943
|
+
transparent 3px,
|
|
944
|
+
transparent 6px
|
|
945
|
+
);
|
|
946
|
+
margin-bottom: 2px;
|
|
947
|
+
min-height: 14px;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
.wg-blocked-label {
|
|
951
|
+
font: 500 10px/1 var(--dt-sans, system-ui, sans-serif);
|
|
952
|
+
color: var(--dt-text-3, rgba(0, 0, 0, 0.38));
|
|
953
|
+
text-transform: uppercase;
|
|
954
|
+
letter-spacing: 0.04em;
|
|
955
|
+
white-space: nowrap;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
/* ─── Custom day header ──────────────────────────── */
|
|
959
|
+
.wg-cell-custom-header {
|
|
960
|
+
padding: 0 4px 2px;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/* ─── Cell header (day label top-right) ──────────── */
|
|
964
|
+
.wg-cell-hd {
|
|
965
|
+
display: flex;
|
|
966
|
+
align-items: center;
|
|
967
|
+
justify-content: flex-end;
|
|
968
|
+
gap: 4px;
|
|
969
|
+
padding: 4px 5px 2px 0;
|
|
970
|
+
margin-bottom: 2px;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
.wg-day-wd {
|
|
974
|
+
font: 400 10px / 1 var(--dt-sans, system-ui, sans-serif);
|
|
975
|
+
letter-spacing: 0.04em;
|
|
976
|
+
text-transform: uppercase;
|
|
977
|
+
color: var(--dt-text-3, rgba(0, 0, 0, 0.38));
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
.wg-week--current .wg-day-wd {
|
|
981
|
+
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
.wg-cell-hd--today .wg-day-wd {
|
|
985
|
+
color: var(--dt-accent, #2563eb);
|
|
986
|
+
font-weight: 600;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
.wg-day-num {
|
|
990
|
+
font: 700 14px / 1 var(--dt-sans, system-ui, sans-serif);
|
|
991
|
+
color: var(--dt-text, rgba(0, 0, 0, 0.87));
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
.wg-week--current .wg-day-num {
|
|
995
|
+
color: var(--dt-text, rgba(0, 0, 0, 0.87));
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
.wg-day-num--today {
|
|
999
|
+
color: var(--dt-accent, #2563eb);
|
|
1000
|
+
font-weight: 900;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/* ─── All-day / multi-day events ─────────────────── */
|
|
1004
|
+
.wg-allday {
|
|
1005
|
+
display: flex;
|
|
1006
|
+
flex-direction: column;
|
|
1007
|
+
gap: var(--wg-chip-gap, 3px);
|
|
1008
|
+
margin-bottom: var(--wg-chip-gap, 3px);
|
|
1009
|
+
flex-shrink: 0;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
.wg-ad {
|
|
1013
|
+
display: flex;
|
|
1014
|
+
align-items: center;
|
|
1015
|
+
gap: 3px;
|
|
1016
|
+
padding: 0 5px;
|
|
1017
|
+
height: var(--wg-chip-h, 22px);
|
|
1018
|
+
box-sizing: border-box;
|
|
1019
|
+
flex-shrink: 0;
|
|
1020
|
+
border-radius: 3px;
|
|
1021
|
+
background: color-mix(in srgb, var(--ev-color) 22%, var(--dt-surface, var(--dt-bg, #ffffff)));
|
|
1022
|
+
cursor: pointer;
|
|
1023
|
+
overflow: hidden;
|
|
1024
|
+
transition: background 0.12s;
|
|
1025
|
+
min-height: 18px;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
.wg-ad--drag-preview {
|
|
1029
|
+
position: relative;
|
|
1030
|
+
z-index: 8;
|
|
1031
|
+
opacity: 0.95;
|
|
1032
|
+
pointer-events: none;
|
|
1033
|
+
box-shadow: 0 6px 18px color-mix(in srgb, var(--ev-color) 26%, rgba(0, 0, 0, 0.22));
|
|
1034
|
+
outline: 1px solid color-mix(in srgb, var(--ev-color) 42%, transparent);
|
|
1035
|
+
cursor: grabbing;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
.wg-ad:hover {
|
|
1039
|
+
background: color-mix(in srgb, var(--ev-color) 32%, var(--dt-surface, var(--dt-bg, #ffffff)));
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
.wg-ad--start {
|
|
1043
|
+
border-left: 2.5px solid var(--ev-color);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
.wg-ad--mid {
|
|
1047
|
+
border-radius: 0;
|
|
1048
|
+
border-left: 1px dashed color-mix(in srgb, var(--ev-color) 40%, transparent);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
.wg-ad--end:not(.wg-ad--start) {
|
|
1052
|
+
border-radius: 0 3px 3px 0;
|
|
1053
|
+
border-left: 1px dashed color-mix(in srgb, var(--ev-color) 40%, transparent);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
.wg-ad--selected {
|
|
1057
|
+
box-shadow: 0 0 0 1.5px var(--ev-color);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
.wg-ad-title {
|
|
1061
|
+
font: 500 10px / 1.1 var(--dt-sans, system-ui, sans-serif);
|
|
1062
|
+
color: var(--dt-text, rgba(0, 0, 0, 0.87));
|
|
1063
|
+
white-space: nowrap;
|
|
1064
|
+
overflow: hidden;
|
|
1065
|
+
text-overflow: ellipsis;
|
|
1066
|
+
flex: 1;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
.wg-ad-cont {
|
|
1070
|
+
font-size: 10px;
|
|
1071
|
+
color: var(--ev-color);
|
|
1072
|
+
flex-shrink: 0;
|
|
1073
|
+
line-height: 1;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
.wg-ad-arrow {
|
|
1077
|
+
font-size: 10px;
|
|
1078
|
+
color: var(--ev-color);
|
|
1079
|
+
flex-shrink: 0;
|
|
1080
|
+
margin-left: auto;
|
|
1081
|
+
line-height: 1;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
/* ─── Events ─────────────────────────────────────── */
|
|
1085
|
+
.wg-cell-events {
|
|
1086
|
+
position: relative;
|
|
1087
|
+
display: flex;
|
|
1088
|
+
flex-direction: column;
|
|
1089
|
+
gap: var(--wg-chip-gap, 3px);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
.wg-ev {
|
|
1093
|
+
display: flex;
|
|
1094
|
+
align-items: center;
|
|
1095
|
+
flex-wrap: nowrap;
|
|
1096
|
+
gap: 0 5px;
|
|
1097
|
+
height: var(--wg-chip-h, 22px);
|
|
1098
|
+
box-sizing: border-box;
|
|
1099
|
+
flex-shrink: 0;
|
|
1100
|
+
padding: 0 6px;
|
|
1101
|
+
border-radius: 4px;
|
|
1102
|
+
background: color-mix(in srgb, var(--ev-color) 15%, var(--dt-surface, var(--dt-bg, #ffffff)));
|
|
1103
|
+
cursor: pointer;
|
|
1104
|
+
overflow: hidden;
|
|
1105
|
+
transition: background 0.12s;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
.wg-ev:hover {
|
|
1109
|
+
background: color-mix(in srgb, var(--ev-color) 25%, var(--dt-surface, var(--dt-bg, #ffffff)));
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
.wg-ev--drag-preview {
|
|
1113
|
+
position: relative;
|
|
1114
|
+
z-index: 8;
|
|
1115
|
+
opacity: 0.95;
|
|
1116
|
+
pointer-events: none;
|
|
1117
|
+
background: color-mix(in srgb, var(--ev-color) 28%, var(--dt-surface, var(--dt-bg, #ffffff)));
|
|
1118
|
+
box-shadow: 0 6px 18px color-mix(in srgb, var(--ev-color) 24%, rgba(0, 0, 0, 0.22));
|
|
1119
|
+
outline: 1px solid color-mix(in srgb, var(--ev-color) 42%, transparent);
|
|
1120
|
+
cursor: grabbing;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
.wg-ev--selected {
|
|
1124
|
+
box-shadow: 0 0 0 1.5px var(--ev-color);
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
.wg-ev--current {
|
|
1128
|
+
background: color-mix(in srgb, var(--ev-color) 22%, var(--dt-surface, var(--dt-bg, #ffffff)));
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
.wg-ev--cancelled {
|
|
1132
|
+
opacity: 0.5;
|
|
1133
|
+
}
|
|
1134
|
+
.wg-ev--cancelled .wg-ev-title {
|
|
1135
|
+
text-decoration: line-through;
|
|
1136
|
+
}
|
|
1137
|
+
.wg-ev--tentative {
|
|
1138
|
+
opacity: 0.65;
|
|
1139
|
+
border: 1px dashed color-mix(in srgb, var(--ev-color) 40%, transparent);
|
|
1140
|
+
}
|
|
1141
|
+
.wg-ev--full {
|
|
1142
|
+
opacity: 0.55;
|
|
1143
|
+
}
|
|
1144
|
+
.wg-ev--limited {
|
|
1145
|
+
opacity: 0.65;
|
|
1146
|
+
border: 1px dashed color-mix(in srgb, var(--ev-color) 40%, transparent);
|
|
1147
|
+
}
|
|
1148
|
+
.wg-ev--readonly {
|
|
1149
|
+
cursor: default;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
.wg-ev-time {
|
|
1153
|
+
font: 400 10px / 1 var(--dt-sans, system-ui, sans-serif);
|
|
1154
|
+
color: var(--dt-text-3, rgba(0, 0, 0, 0.38));
|
|
1155
|
+
flex-shrink: 0;
|
|
1156
|
+
white-space: nowrap;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
.wg-ev-title {
|
|
1160
|
+
font: 500 12px / 1.1 var(--dt-sans, system-ui, sans-serif);
|
|
1161
|
+
color: var(--dt-text, rgba(0, 0, 0, 0.87));
|
|
1162
|
+
white-space: nowrap;
|
|
1163
|
+
overflow: hidden;
|
|
1164
|
+
text-overflow: ellipsis;
|
|
1165
|
+
min-width: 0;
|
|
1166
|
+
flex: 1 1 auto;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
.wg-ev-loc {
|
|
1170
|
+
font: 400 10px / 1 var(--dt-sans, system-ui, sans-serif);
|
|
1171
|
+
color: var(--dt-text-3, rgba(0, 0, 0, 0.38));
|
|
1172
|
+
white-space: nowrap;
|
|
1173
|
+
flex-shrink: 0; /* shown only when measured to fit — never squeezed */
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
.wg-ev-more {
|
|
1177
|
+
/* Real button — reset chrome, keep the quiet-link look */
|
|
1178
|
+
appearance: none;
|
|
1179
|
+
background: none;
|
|
1180
|
+
border: none;
|
|
1181
|
+
border-radius: 3px;
|
|
1182
|
+
text-align: left;
|
|
1183
|
+
align-self: flex-start;
|
|
1184
|
+
font: 500 10px / 1 var(--dt-sans, system-ui, sans-serif);
|
|
1185
|
+
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
1186
|
+
padding: 2px 8px;
|
|
1187
|
+
cursor: pointer;
|
|
1188
|
+
flex-shrink: 0;
|
|
1189
|
+
margin-top: 2px;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
.wg-ev-more:hover {
|
|
1193
|
+
color: var(--dt-text, rgba(0, 0, 0, 0.87));
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
.wg-ev-more:focus-visible {
|
|
1197
|
+
outline: none;
|
|
1198
|
+
box-shadow: 0 0 0 2px var(--dt-accent, #2563eb);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
/* ─── Focus-visible ──────────────────────────────── */
|
|
1202
|
+
.wg-cell:focus-visible {
|
|
1203
|
+
outline: 2px solid var(--dt-accent, #2563eb);
|
|
1204
|
+
outline-offset: -2px;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
.wg-ev:focus-visible {
|
|
1208
|
+
outline: 2px solid var(--ev-color, var(--dt-accent, #2563eb));
|
|
1209
|
+
outline-offset: 1px;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
.wg-ev--dragging {
|
|
1213
|
+
cursor: grabbing;
|
|
1214
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
|
1215
|
+
}
|
|
1216
|
+
</style>
|
|
1217
|
+
|