@nomideusz/svelte-calendar 0.10.0 → 0.12.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 +2 -16
- package/dist/calendar/Calendar.svelte +43 -5
- package/dist/calendar/Calendar.svelte.d.ts +8 -0
- package/dist/core/index.d.ts +0 -2
- package/dist/core/index.js +0 -2
- package/dist/engine/view-state.svelte.d.ts +5 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +1 -1
- package/dist/theme/auto.js +56 -21
- package/dist/theme/presets.d.ts +6 -5
- package/dist/theme/presets.js +2 -3
- package/dist/views/agenda/AgendaDay.svelte +69 -32
- package/dist/views/agenda/AgendaWeek.svelte +220 -83
- package/dist/views/mobile/MobileDay.svelte +11 -3
- package/dist/views/mobile/MobileWeek.svelte +6 -0
- package/dist/views/mobile/swipe.d.ts +1 -0
- package/dist/views/mobile/swipe.js +9 -0
- package/dist/views/month/MonthGrid.svelte +35 -2
- package/dist/views/planner/Planner.svelte +3 -7
- package/dist/views/planner/PlannerWeek.svelte +16 -7
- package/dist/views/planner/PlannerWeek.svelte.d.ts +2 -0
- package/dist/views/shared/context.svelte.d.ts +1 -0
- package/dist/views/shared/context.svelte.js +1 -0
- package/package.json +1 -8
- package/widget/widget.js +1381 -2428
- package/dist/core/measure.d.ts +0 -104
- package/dist/core/measure.js +0 -195
- package/dist/views/planner/PlannerDay.svelte +0 -1279
- package/dist/views/planner/PlannerDay.svelte.d.ts +0 -30
|
@@ -1,1279 +0,0 @@
|
|
|
1
|
-
<!--
|
|
2
|
-
Planner day mode — horizontal filmstrip timeline.
|
|
3
|
-
|
|
4
|
-
Time flows left → right. Past on the left, future on the right.
|
|
5
|
-
The now-line tracks the current time. Drag to scroll through days.
|
|
6
|
-
Events are positioned as horizontal cards with overlap support.
|
|
7
|
-
-->
|
|
8
|
-
<script lang="ts">import { onMount, tick, untrack } from "svelte";
|
|
9
|
-
import { useCalendarContext } from "../shared/context.svelte.js";
|
|
10
|
-
import EventContent from "../shared/EventContent.svelte";
|
|
11
|
-
import { createClock } from "../../core/clock.svelte.js";
|
|
12
|
-
import { createTextMeasure } from "../../core/measure.js";
|
|
13
|
-
import { DAY_MS, HOUR_MS, sod } from "../../core/time.js";
|
|
14
|
-
import { isAllDay, isMultiDay, segmentForDay } from "../../core/time.js";
|
|
15
|
-
import { fmtDuration, fmtH, fmtTime, weekdayShort } from "../../core/locale.js";
|
|
16
|
-
let {
|
|
17
|
-
height = 520,
|
|
18
|
-
events = [],
|
|
19
|
-
style = "",
|
|
20
|
-
locale,
|
|
21
|
-
focusDate,
|
|
22
|
-
oneventclick,
|
|
23
|
-
oneventcreate,
|
|
24
|
-
selectedEventId = null,
|
|
25
|
-
readOnly = false,
|
|
26
|
-
visibleHours
|
|
27
|
-
} = $props();
|
|
28
|
-
const ctx = useCalendarContext();
|
|
29
|
-
const L = $derived(ctx.labels);
|
|
30
|
-
const clock = createClock(ctx.timezone);
|
|
31
|
-
const drag = $derived(ctx.drag);
|
|
32
|
-
const commitDragCtx = $derived(ctx.commitDrag);
|
|
33
|
-
const viewState = $derived(ctx.viewState);
|
|
34
|
-
const loadRangeCtx = $derived(ctx.loadRange);
|
|
35
|
-
const blockedSlots = $derived(ctx.blockedSlots);
|
|
36
|
-
const dayHeaderSnippet = $derived(ctx.dayHeaderSnippet);
|
|
37
|
-
const minDuration = $derived(ctx.minDuration);
|
|
38
|
-
const autoHeight = $derived(ctx.autoHeight);
|
|
39
|
-
const oneventhover = $derived(ctx.oneventhover);
|
|
40
|
-
const disabledSet = $derived(ctx.disabledSet);
|
|
41
|
-
const SNAP_MS = $derived(ctx.snapInterval * 6e4);
|
|
42
|
-
let following = $state(true);
|
|
43
|
-
let scrollDragging = $state(false);
|
|
44
|
-
let wasDragging = false;
|
|
45
|
-
let el;
|
|
46
|
-
let containerW = $state(0);
|
|
47
|
-
let containerH = $state(520);
|
|
48
|
-
let dragStartX = 0;
|
|
49
|
-
let dragScrollStart = 0;
|
|
50
|
-
let rafId = 0;
|
|
51
|
-
const BUFFER_DAYS = 7;
|
|
52
|
-
const EDGE_DAYS = 2;
|
|
53
|
-
const SHIFT_DAYS = 5;
|
|
54
|
-
const _initMs = untrack(() => sod(focusDate?.getTime() ?? Date.now()));
|
|
55
|
-
let internalCenterMs = $state(_initMs);
|
|
56
|
-
let lastExternalMs = _initMs;
|
|
57
|
-
let rebasing = false;
|
|
58
|
-
let visibleDayMs = $state(_initMs);
|
|
59
|
-
const startHour = $derived(visibleHours?.[0] ?? 0);
|
|
60
|
-
const endHour = $derived(visibleHours?.[1] ?? 24);
|
|
61
|
-
const hourCount = $derived(Math.max(1, endHour - startHour));
|
|
62
|
-
const DAY_GAP = 2;
|
|
63
|
-
const count = 1 + 2 * BUFFER_DAYS;
|
|
64
|
-
const origin = $derived(internalCenterMs - BUFFER_DAYS * DAY_MS);
|
|
65
|
-
$effect(() => {
|
|
66
|
-
if (!loadRangeCtx) return;
|
|
67
|
-
const rangeStart = new Date(internalCenterMs - BUFFER_DAYS * DAY_MS);
|
|
68
|
-
const rangeEnd = new Date(internalCenterMs + (BUFFER_DAYS + 1) * DAY_MS);
|
|
69
|
-
loadRangeCtx.set({ start: rangeStart, end: rangeEnd });
|
|
70
|
-
return () => loadRangeCtx.set(null);
|
|
71
|
-
});
|
|
72
|
-
const MIN_HOUR_W = 60;
|
|
73
|
-
const hourWidth = $derived(containerW > 0 ? Math.max(MIN_HOUR_W, containerW / hourCount) : 110);
|
|
74
|
-
const dayWidth = $derived(hourCount * hourWidth);
|
|
75
|
-
const totalWidth = $derived(count * (dayWidth + DAY_GAP));
|
|
76
|
-
const days = $derived.by(() => {
|
|
77
|
-
const result = [];
|
|
78
|
-
for (let i = 0; i < count; i++) {
|
|
79
|
-
const ms = origin + i * DAY_MS;
|
|
80
|
-
result.push({
|
|
81
|
-
ms,
|
|
82
|
-
today: ms === clock.today,
|
|
83
|
-
past: ms < clock.today,
|
|
84
|
-
x: i * (dayWidth + DAY_GAP)
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
return result;
|
|
88
|
-
});
|
|
89
|
-
function timeToPx(ms) {
|
|
90
|
-
const elapsed = ms - origin;
|
|
91
|
-
const dayIndex = Math.floor(elapsed / DAY_MS);
|
|
92
|
-
const hourInDay = (elapsed - dayIndex * DAY_MS) / HOUR_MS;
|
|
93
|
-
const clamped = Math.min(Math.max(hourInDay, startHour), endHour);
|
|
94
|
-
return dayIndex * (dayWidth + DAY_GAP) + (clamped - startHour) * hourWidth;
|
|
95
|
-
}
|
|
96
|
-
function pxToTime(px) {
|
|
97
|
-
const dayStride = dayWidth + DAY_GAP;
|
|
98
|
-
const dayIndex = Math.max(0, Math.min(count - 1, Math.floor(px / dayStride)));
|
|
99
|
-
const localPx = px - dayIndex * dayStride;
|
|
100
|
-
const hour = startHour + localPx / hourWidth;
|
|
101
|
-
return origin + dayIndex * DAY_MS + hour * HOUR_MS;
|
|
102
|
-
}
|
|
103
|
-
const nowPx = $derived(timeToPx(clock.tick));
|
|
104
|
-
const nowHour = $derived((clock.tick - clock.today) / HOUR_MS);
|
|
105
|
-
const nowInBand = $derived(nowHour >= startHour && nowHour < endHour);
|
|
106
|
-
const timedEvents = $derived(events.filter((ev) => !isAllDay(ev) && !isMultiDay(ev)));
|
|
107
|
-
const allDayEvents = $derived.by(() => {
|
|
108
|
-
const segs = [];
|
|
109
|
-
for (const ev of events) {
|
|
110
|
-
if (!isAllDay(ev) && !isMultiDay(ev)) continue;
|
|
111
|
-
const seg = segmentForDay(ev, visibleDayMs);
|
|
112
|
-
if (seg) segs.push(seg);
|
|
113
|
-
}
|
|
114
|
-
return segs;
|
|
115
|
-
});
|
|
116
|
-
const CONTENT_TOP = 56;
|
|
117
|
-
const ALLDAY_H = 24;
|
|
118
|
-
const contentTop = $derived(CONTENT_TOP + (allDayEvents.length > 0 ? ALLDAY_H + 4 : 0));
|
|
119
|
-
const EVENT_GAP = 5;
|
|
120
|
-
const MIN_EVENT_H = 32;
|
|
121
|
-
const measure = createTextMeasure({
|
|
122
|
-
titleFont: "600 13px system-ui, sans-serif",
|
|
123
|
-
secondaryFont: "400 10px system-ui, sans-serif",
|
|
124
|
-
tagFont: "500 8px system-ui, sans-serif",
|
|
125
|
-
titleLineHeight: 16,
|
|
126
|
-
secondaryLineHeight: 13,
|
|
127
|
-
contentGap: 6
|
|
128
|
-
});
|
|
129
|
-
const nowInfo = $derived.by(() => {
|
|
130
|
-
const now = clock.tick;
|
|
131
|
-
const current = /* @__PURE__ */ new Set();
|
|
132
|
-
const todayStart = clock.today;
|
|
133
|
-
const todayEnd = todayStart + DAY_MS;
|
|
134
|
-
let nextId = null;
|
|
135
|
-
let nextStart = Infinity;
|
|
136
|
-
for (const ev of timedEvents) {
|
|
137
|
-
const s = ev.start.getTime();
|
|
138
|
-
const e = ev.end.getTime();
|
|
139
|
-
if (s <= now && e > now) current.add(ev.id);
|
|
140
|
-
else if (s >= todayStart && s < todayEnd && s > now && s < nextStart) {
|
|
141
|
-
nextStart = s;
|
|
142
|
-
nextId = ev.id;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
return { current, nextId };
|
|
146
|
-
});
|
|
147
|
-
const positionedEvents = $derived.by(() => {
|
|
148
|
-
const dragP = drag?.active && (drag.mode === "move" || drag.mode === "resize-start" || drag.mode === "resize-end") ? drag.payload : null;
|
|
149
|
-
const staticEvents = [];
|
|
150
|
-
let draggedEv = null;
|
|
151
|
-
for (const ev of timedEvents) {
|
|
152
|
-
if (dragP?.eventId === ev.id) draggedEv = ev;
|
|
153
|
-
else staticEvents.push(ev);
|
|
154
|
-
}
|
|
155
|
-
const sorted = [...staticEvents].sort((a, b) => a.start.getTime() - b.start.getTime());
|
|
156
|
-
const infos = sorted.map((ev) => {
|
|
157
|
-
const s = ev.start.getTime();
|
|
158
|
-
const e = ev.end.getTime();
|
|
159
|
-
const x = timeToPx(s);
|
|
160
|
-
const xEnd = timeToPx(e);
|
|
161
|
-
return {
|
|
162
|
-
ev,
|
|
163
|
-
x,
|
|
164
|
-
width: Math.max(xEnd - x, 28),
|
|
165
|
-
row: 0,
|
|
166
|
-
groupMaxRow: 1,
|
|
167
|
-
isDragged: false,
|
|
168
|
-
startMs: s,
|
|
169
|
-
endMs: e,
|
|
170
|
-
clippedWidth: xEnd - x
|
|
171
|
-
};
|
|
172
|
-
}).filter((info) => info.clippedWidth > 0);
|
|
173
|
-
const par = infos.map((_, i) => i);
|
|
174
|
-
function find(i) {
|
|
175
|
-
while (par[i] !== i) {
|
|
176
|
-
par[i] = par[par[i]];
|
|
177
|
-
i = par[i];
|
|
178
|
-
}
|
|
179
|
-
return i;
|
|
180
|
-
}
|
|
181
|
-
for (let i = 0; i < infos.length; i++) {
|
|
182
|
-
for (let j = i + 1; j < infos.length; j++) {
|
|
183
|
-
if (infos[j].startMs < infos[i].endMs) par[find(i)] = find(j);
|
|
184
|
-
else break;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
const groups = /* @__PURE__ */ new Map();
|
|
188
|
-
for (let i = 0; i < infos.length; i++) {
|
|
189
|
-
const root = find(i);
|
|
190
|
-
if (!groups.has(root)) groups.set(root, []);
|
|
191
|
-
groups.get(root).push(i);
|
|
192
|
-
}
|
|
193
|
-
for (const [, indices] of groups) {
|
|
194
|
-
const rows = [];
|
|
195
|
-
for (const idx of indices) {
|
|
196
|
-
const inf = infos[idx];
|
|
197
|
-
let row = 0;
|
|
198
|
-
for (let r = 0; r < rows.length; r++) {
|
|
199
|
-
if (rows[r] <= inf.startMs) {
|
|
200
|
-
row = r;
|
|
201
|
-
rows[r] = inf.endMs;
|
|
202
|
-
break;
|
|
203
|
-
}
|
|
204
|
-
row = r + 1;
|
|
205
|
-
}
|
|
206
|
-
if (row >= rows.length) rows.push(inf.endMs);
|
|
207
|
-
infos[idx].row = row;
|
|
208
|
-
}
|
|
209
|
-
for (const idx of indices) infos[idx].groupMaxRow = rows.length;
|
|
210
|
-
}
|
|
211
|
-
const availH = containerH - contentTop - 8;
|
|
212
|
-
const result = infos.map(({ startMs: _s, endMs: _e, clippedWidth: _c, ...info }) => {
|
|
213
|
-
const laneH = Math.max(MIN_EVENT_H, availH / info.groupMaxRow - EVENT_GAP);
|
|
214
|
-
const topPx = contentTop + info.row * (availH / info.groupMaxRow);
|
|
215
|
-
const fit = measure.fitContent({
|
|
216
|
-
title: info.ev.title,
|
|
217
|
-
subtitle: info.ev.subtitle,
|
|
218
|
-
location: info.ev.location,
|
|
219
|
-
time: `${fmtTime(info.ev.start, locale)} \u2013 ${fmtTime(info.ev.end, locale)}`,
|
|
220
|
-
tags: info.ev.tags,
|
|
221
|
-
maxWidth: laneH - 16,
|
|
222
|
-
maxHeight: info.width - 16
|
|
223
|
-
});
|
|
224
|
-
return { ...info, topPx, heightPx: laneH, fit };
|
|
225
|
-
});
|
|
226
|
-
if (draggedEv && dragP) {
|
|
227
|
-
const x = timeToPx(dragP.start.getTime());
|
|
228
|
-
const xEnd = timeToPx(dragP.end.getTime());
|
|
229
|
-
const dragH = Math.max(MIN_EVENT_H, availH - EVENT_GAP);
|
|
230
|
-
const dragW = Math.max(xEnd - x, 28);
|
|
231
|
-
result.push({
|
|
232
|
-
ev: draggedEv,
|
|
233
|
-
x,
|
|
234
|
-
width: dragW,
|
|
235
|
-
row: 0,
|
|
236
|
-
groupMaxRow: 1,
|
|
237
|
-
topPx: contentTop,
|
|
238
|
-
heightPx: dragH,
|
|
239
|
-
isDragged: true,
|
|
240
|
-
fit: measure.fitContent({
|
|
241
|
-
title: draggedEv.title,
|
|
242
|
-
subtitle: draggedEv.subtitle,
|
|
243
|
-
location: draggedEv.location,
|
|
244
|
-
tags: draggedEv.tags,
|
|
245
|
-
maxWidth: dragH - 16,
|
|
246
|
-
maxHeight: dragW - 16
|
|
247
|
-
})
|
|
248
|
-
});
|
|
249
|
-
}
|
|
250
|
-
return result;
|
|
251
|
-
});
|
|
252
|
-
$effect(() => {
|
|
253
|
-
const ext = focusDate ? sod(focusDate.getTime()) : clock.today;
|
|
254
|
-
if (ext !== lastExternalMs && !rebasing) {
|
|
255
|
-
lastExternalMs = ext;
|
|
256
|
-
internalCenterMs = ext;
|
|
257
|
-
visibleDayMs = ext;
|
|
258
|
-
following = ext === clock.today;
|
|
259
|
-
tick().then(() => {
|
|
260
|
-
if (el) {
|
|
261
|
-
const focusX = BUFFER_DAYS * (dayWidth + DAY_GAP);
|
|
262
|
-
el.scrollLeft = focusX + dayWidth / 2 - el.clientWidth / 2;
|
|
263
|
-
}
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
});
|
|
267
|
-
function checkEdges() {
|
|
268
|
-
if (!el || !viewState || rebasing) return;
|
|
269
|
-
const stride = dayWidth + DAY_GAP;
|
|
270
|
-
const threshold = stride * EDGE_DAYS;
|
|
271
|
-
const maxScroll = el.scrollWidth - el.clientWidth;
|
|
272
|
-
if (el.scrollLeft < threshold) {
|
|
273
|
-
rebase(-1);
|
|
274
|
-
} else if (maxScroll > 0 && maxScroll - el.scrollLeft < threshold) {
|
|
275
|
-
rebase(1);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
function rebase(direction) {
|
|
279
|
-
if (rebasing) return;
|
|
280
|
-
rebasing = true;
|
|
281
|
-
const shift = SHIFT_DAYS * direction;
|
|
282
|
-
const stride = dayWidth + DAY_GAP;
|
|
283
|
-
const adj = -shift * stride;
|
|
284
|
-
if (el) el.scrollLeft += adj;
|
|
285
|
-
dragScrollStart += adj;
|
|
286
|
-
internalCenterMs += shift * DAY_MS;
|
|
287
|
-
lastExternalMs = internalCenterMs;
|
|
288
|
-
viewState?.setFocusDate(new Date(internalCenterMs));
|
|
289
|
-
tick().then(() => {
|
|
290
|
-
rebasing = false;
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
function syncFocusFromScroll() {
|
|
294
|
-
if (!el || !viewState || following || rebasing) return;
|
|
295
|
-
const centerX = el.scrollLeft + el.clientWidth / 2;
|
|
296
|
-
const centerDayMs = sod(pxToTime(centerX));
|
|
297
|
-
visibleDayMs = centerDayMs;
|
|
298
|
-
if (centerDayMs !== lastExternalMs) {
|
|
299
|
-
lastExternalMs = centerDayMs;
|
|
300
|
-
viewState.setFocusDate(new Date(centerDayMs));
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
let scrollRaf = 0;
|
|
304
|
-
function handleScroll() {
|
|
305
|
-
if (following || rebasing || scrollRaf) return;
|
|
306
|
-
scrollRaf = requestAnimationFrame(() => {
|
|
307
|
-
scrollRaf = 0;
|
|
308
|
-
if (!el || following || rebasing) return;
|
|
309
|
-
checkEdges();
|
|
310
|
-
syncFocusFromScroll();
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
$effect(() => {
|
|
314
|
-
if (!following) return;
|
|
315
|
-
function frame() {
|
|
316
|
-
if (el && !scrollDragging) {
|
|
317
|
-
if (internalCenterMs !== clock.today) {
|
|
318
|
-
internalCenterMs = clock.today;
|
|
319
|
-
lastExternalMs = clock.today;
|
|
320
|
-
viewState?.goToday();
|
|
321
|
-
}
|
|
322
|
-
visibleDayMs = clock.today;
|
|
323
|
-
const todayD = days.find((d) => d.today);
|
|
324
|
-
if (todayD) {
|
|
325
|
-
el.scrollLeft = todayD.x + dayWidth / 2 - el.clientWidth / 2;
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
rafId = requestAnimationFrame(frame);
|
|
329
|
-
}
|
|
330
|
-
rafId = requestAnimationFrame(frame);
|
|
331
|
-
return () => cancelAnimationFrame(rafId);
|
|
332
|
-
});
|
|
333
|
-
onMount(() => {
|
|
334
|
-
const ro = new ResizeObserver((entries) => {
|
|
335
|
-
for (const entry of entries) {
|
|
336
|
-
containerW = entry.contentRect.width;
|
|
337
|
-
containerH = entry.contentRect.height;
|
|
338
|
-
}
|
|
339
|
-
});
|
|
340
|
-
ro.observe(el);
|
|
341
|
-
return () => {
|
|
342
|
-
cancelAnimationFrame(scrollRaf);
|
|
343
|
-
ro.disconnect();
|
|
344
|
-
};
|
|
345
|
-
});
|
|
346
|
-
const SCROLL_THRESHOLD = 3;
|
|
347
|
-
function onPointerDown(e) {
|
|
348
|
-
if (e.button !== 0) return;
|
|
349
|
-
if (e.target.closest(".fs-event")) return;
|
|
350
|
-
if (readOnly) {
|
|
351
|
-
dragStartX = e.clientX;
|
|
352
|
-
dragScrollStart = el.scrollLeft;
|
|
353
|
-
window.addEventListener("pointermove", onScrollMove);
|
|
354
|
-
window.addEventListener("pointerup", onScrollUp, { once: true });
|
|
355
|
-
window.addEventListener("pointercancel", onScrollUp, { once: true });
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
|
-
onCreatePointerDown(e);
|
|
359
|
-
}
|
|
360
|
-
function onScrollMove(e) {
|
|
361
|
-
const dx = e.clientX - dragStartX;
|
|
362
|
-
if (!scrollDragging && Math.abs(dx) >= SCROLL_THRESHOLD) {
|
|
363
|
-
scrollDragging = true;
|
|
364
|
-
wasDragging = true;
|
|
365
|
-
following = false;
|
|
366
|
-
}
|
|
367
|
-
if (scrollDragging) el.scrollLeft = dragScrollStart - dx;
|
|
368
|
-
}
|
|
369
|
-
function onScrollUp() {
|
|
370
|
-
window.removeEventListener("pointermove", onScrollMove);
|
|
371
|
-
window.removeEventListener("pointerup", onScrollUp);
|
|
372
|
-
window.removeEventListener("pointercancel", onScrollUp);
|
|
373
|
-
scrollDragging = false;
|
|
374
|
-
}
|
|
375
|
-
function isBlockedAt(dayMs, hour) {
|
|
376
|
-
if (!blockedSlots?.length) return false;
|
|
377
|
-
const jsDay = new Date(dayMs).getDay();
|
|
378
|
-
const isoDay = jsDay === 0 ? 7 : jsDay;
|
|
379
|
-
return blockedSlots.some((slot) => {
|
|
380
|
-
if (slot.day && slot.day !== isoDay) return false;
|
|
381
|
-
return hour >= slot.start && hour < slot.end;
|
|
382
|
-
});
|
|
383
|
-
}
|
|
384
|
-
function handleTrackClick(e) {
|
|
385
|
-
if (wasDragging) {
|
|
386
|
-
wasDragging = false;
|
|
387
|
-
return;
|
|
388
|
-
}
|
|
389
|
-
if (!oneventcreate || readOnly) return;
|
|
390
|
-
if (e.target.closest(".fs-event")) return;
|
|
391
|
-
const rect = e.currentTarget.getBoundingClientRect();
|
|
392
|
-
const clickX = e.clientX - rect.left + el.scrollLeft;
|
|
393
|
-
for (const d of days) {
|
|
394
|
-
if (clickX >= d.x && clickX < d.x + dayWidth) {
|
|
395
|
-
if (disabledSet.has(d.ms)) return;
|
|
396
|
-
const frac = (clickX - d.x) / dayWidth;
|
|
397
|
-
const clickHour = startHour + frac * hourCount;
|
|
398
|
-
if (isBlockedAt(d.ms, clickHour)) return;
|
|
399
|
-
const hour = Math.floor(clickHour);
|
|
400
|
-
const durMin = minDuration ? Math.max(60, minDuration) : 60;
|
|
401
|
-
const start = new Date(d.ms + hour * HOUR_MS);
|
|
402
|
-
const end = new Date(start.getTime() + durMin * 6e4);
|
|
403
|
-
oneventcreate({ start, end });
|
|
404
|
-
return;
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
const CREATE_THRESHOLD = 4;
|
|
409
|
-
let createStartX = 0;
|
|
410
|
-
let createAnchorMs = 0;
|
|
411
|
-
let createStarted = false;
|
|
412
|
-
function trackX(e) {
|
|
413
|
-
return e.clientX - el.getBoundingClientRect().left + el.scrollLeft;
|
|
414
|
-
}
|
|
415
|
-
function onCreatePointerDown(e) {
|
|
416
|
-
if (!drag || !oneventcreate) return;
|
|
417
|
-
createStartX = e.clientX;
|
|
418
|
-
createAnchorMs = pxToTime(trackX(e));
|
|
419
|
-
createStarted = false;
|
|
420
|
-
window.addEventListener("pointermove", onCreateMove);
|
|
421
|
-
window.addEventListener("pointerup", onCreateUp, { once: true });
|
|
422
|
-
window.addEventListener("pointercancel", onCreateCancel, { once: true });
|
|
423
|
-
}
|
|
424
|
-
function onCreateMove(e) {
|
|
425
|
-
if (!drag) return;
|
|
426
|
-
if (!createStarted) {
|
|
427
|
-
if (Math.abs(e.clientX - createStartX) < CREATE_THRESHOLD) return;
|
|
428
|
-
if (disabledSet.has(sod(createAnchorMs))) return;
|
|
429
|
-
createStarted = true;
|
|
430
|
-
wasDragging = true;
|
|
431
|
-
createAnchorMs = Math.floor(createAnchorMs / SNAP_MS) * SNAP_MS;
|
|
432
|
-
drag.beginCreate(new Date(createAnchorMs), new Date(createAnchorMs + SNAP_MS));
|
|
433
|
-
}
|
|
434
|
-
const snapped = Math.round(pxToTime(trackX(e)) / SNAP_MS) * SNAP_MS;
|
|
435
|
-
drag.updatePointer(
|
|
436
|
-
new Date(Math.min(createAnchorMs, snapped)),
|
|
437
|
-
new Date(Math.max(createAnchorMs + SNAP_MS, snapped))
|
|
438
|
-
);
|
|
439
|
-
}
|
|
440
|
-
function cleanupCreateDrag() {
|
|
441
|
-
window.removeEventListener("pointermove", onCreateMove);
|
|
442
|
-
window.removeEventListener("pointerup", onCreateUp);
|
|
443
|
-
window.removeEventListener("pointercancel", onCreateCancel);
|
|
444
|
-
createStarted = false;
|
|
445
|
-
}
|
|
446
|
-
function onCreateUp() {
|
|
447
|
-
if (drag && createStarted) {
|
|
448
|
-
commitDragCtx?.();
|
|
449
|
-
setTimeout(() => {
|
|
450
|
-
wasDragging = false;
|
|
451
|
-
}, 0);
|
|
452
|
-
}
|
|
453
|
-
cleanupCreateDrag();
|
|
454
|
-
}
|
|
455
|
-
function onCreateCancel() {
|
|
456
|
-
if (drag && createStarted) {
|
|
457
|
-
drag.cancel();
|
|
458
|
-
setTimeout(() => {
|
|
459
|
-
wasDragging = false;
|
|
460
|
-
}, 0);
|
|
461
|
-
}
|
|
462
|
-
cleanupCreateDrag();
|
|
463
|
-
}
|
|
464
|
-
const DRAG_THRESHOLD = 5;
|
|
465
|
-
let evDragStartX = 0;
|
|
466
|
-
let evDragOriginPx = 0;
|
|
467
|
-
let evDragStarted = false;
|
|
468
|
-
let evDragging = $state(false);
|
|
469
|
-
let evDragId = $state(null);
|
|
470
|
-
let evDragEvent = null;
|
|
471
|
-
function onEventPointerDown(e, ev) {
|
|
472
|
-
if (e.button !== 0 || !drag || readOnly || ev.data?.readOnly) return;
|
|
473
|
-
e.stopPropagation();
|
|
474
|
-
evDragStartX = e.clientX;
|
|
475
|
-
evDragOriginPx = timeToPx(ev.start.getTime());
|
|
476
|
-
evDragStarted = false;
|
|
477
|
-
evDragId = ev.id;
|
|
478
|
-
evDragEvent = ev;
|
|
479
|
-
window.addEventListener("pointermove", onEvMove);
|
|
480
|
-
window.addEventListener("pointerup", onEvUp, { once: true });
|
|
481
|
-
window.addEventListener("pointercancel", onEvCancel, { once: true });
|
|
482
|
-
}
|
|
483
|
-
function onEvMove(e) {
|
|
484
|
-
const ev = evDragEvent;
|
|
485
|
-
if (!drag || !ev || evDragId !== ev.id) return;
|
|
486
|
-
const dx = e.clientX - evDragStartX;
|
|
487
|
-
if (!evDragStarted && Math.abs(dx) < DRAG_THRESHOLD) return;
|
|
488
|
-
if (!evDragStarted) {
|
|
489
|
-
evDragStarted = true;
|
|
490
|
-
evDragging = true;
|
|
491
|
-
drag.beginMove(ev.id, ev.start, ev.end);
|
|
492
|
-
}
|
|
493
|
-
const duration = ev.end.getTime() - ev.start.getTime();
|
|
494
|
-
const raw = pxToTime(evDragOriginPx + dx);
|
|
495
|
-
const snapped = Math.round(raw / SNAP_MS) * SNAP_MS;
|
|
496
|
-
drag.updatePointer(new Date(snapped), new Date(snapped + duration));
|
|
497
|
-
}
|
|
498
|
-
function cleanupEvDrag() {
|
|
499
|
-
window.removeEventListener("pointermove", onEvMove);
|
|
500
|
-
window.removeEventListener("pointerup", onEvUp);
|
|
501
|
-
window.removeEventListener("pointercancel", onEvCancel);
|
|
502
|
-
evDragStarted = false;
|
|
503
|
-
evDragging = false;
|
|
504
|
-
evDragId = null;
|
|
505
|
-
evDragEvent = null;
|
|
506
|
-
}
|
|
507
|
-
function onEvUp() {
|
|
508
|
-
if (!drag) {
|
|
509
|
-
cleanupEvDrag();
|
|
510
|
-
return;
|
|
511
|
-
}
|
|
512
|
-
if (!evDragStarted && evDragEvent) oneventclick?.(evDragEvent);
|
|
513
|
-
else if (evDragStarted) commitDragCtx?.();
|
|
514
|
-
cleanupEvDrag();
|
|
515
|
-
}
|
|
516
|
-
function onEvCancel() {
|
|
517
|
-
if (drag && evDragStarted) drag.cancel();
|
|
518
|
-
cleanupEvDrag();
|
|
519
|
-
}
|
|
520
|
-
let rsStartX = 0;
|
|
521
|
-
let rsStarted = false;
|
|
522
|
-
let rsEdge = "end";
|
|
523
|
-
let rsEvent = null;
|
|
524
|
-
function onResizePointerDown(e, ev, edge) {
|
|
525
|
-
if (e.button !== 0 || !drag || readOnly || ev.data?.readOnly) return;
|
|
526
|
-
e.stopPropagation();
|
|
527
|
-
rsStartX = e.clientX;
|
|
528
|
-
rsStarted = false;
|
|
529
|
-
rsEdge = edge;
|
|
530
|
-
rsEvent = ev;
|
|
531
|
-
window.addEventListener("pointermove", onResizeMove);
|
|
532
|
-
window.addEventListener("pointerup", onResizeUp, { once: true });
|
|
533
|
-
window.addEventListener("pointercancel", onResizeCancel, { once: true });
|
|
534
|
-
}
|
|
535
|
-
function onResizeMove(e) {
|
|
536
|
-
const ev = rsEvent;
|
|
537
|
-
if (!drag || !ev) return;
|
|
538
|
-
if (!rsStarted) {
|
|
539
|
-
if (Math.abs(e.clientX - rsStartX) < CREATE_THRESHOLD) return;
|
|
540
|
-
rsStarted = true;
|
|
541
|
-
drag.beginResize(ev.id, rsEdge, ev.start, ev.end);
|
|
542
|
-
}
|
|
543
|
-
const snapped = Math.round(pxToTime(trackX(e)) / SNAP_MS) * SNAP_MS;
|
|
544
|
-
if (rsEdge === "end") {
|
|
545
|
-
const end = Math.max(snapped, ev.start.getTime() + SNAP_MS);
|
|
546
|
-
drag.updatePointer(ev.start, new Date(end));
|
|
547
|
-
} else {
|
|
548
|
-
const start = Math.min(snapped, ev.end.getTime() - SNAP_MS);
|
|
549
|
-
drag.updatePointer(new Date(start), ev.end);
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
function cleanupResize() {
|
|
553
|
-
window.removeEventListener("pointermove", onResizeMove);
|
|
554
|
-
window.removeEventListener("pointerup", onResizeUp);
|
|
555
|
-
window.removeEventListener("pointercancel", onResizeCancel);
|
|
556
|
-
rsStarted = false;
|
|
557
|
-
rsEvent = null;
|
|
558
|
-
}
|
|
559
|
-
function onResizeUp() {
|
|
560
|
-
if (drag && rsStarted) {
|
|
561
|
-
wasDragging = true;
|
|
562
|
-
commitDragCtx?.();
|
|
563
|
-
setTimeout(() => {
|
|
564
|
-
wasDragging = false;
|
|
565
|
-
}, 0);
|
|
566
|
-
} else if (rsEvent && !rsStarted) {
|
|
567
|
-
oneventclick?.(rsEvent);
|
|
568
|
-
}
|
|
569
|
-
cleanupResize();
|
|
570
|
-
}
|
|
571
|
-
function onResizeCancel() {
|
|
572
|
-
if (drag && rsStarted) drag.cancel();
|
|
573
|
-
cleanupResize();
|
|
574
|
-
}
|
|
575
|
-
function onWindowKeydown(e) {
|
|
576
|
-
if (e.key !== "Escape" || !drag?.active) return;
|
|
577
|
-
drag.cancel();
|
|
578
|
-
cleanupCreateDrag();
|
|
579
|
-
cleanupEvDrag();
|
|
580
|
-
cleanupResize();
|
|
581
|
-
wasDragging = true;
|
|
582
|
-
window.addEventListener(
|
|
583
|
-
"pointerup",
|
|
584
|
-
() => setTimeout(() => {
|
|
585
|
-
wasDragging = false;
|
|
586
|
-
}, 0),
|
|
587
|
-
{ once: true }
|
|
588
|
-
);
|
|
589
|
-
}
|
|
590
|
-
</script>
|
|
591
|
-
|
|
592
|
-
<svelte:window onkeydown={onWindowKeydown} />
|
|
593
|
-
|
|
594
|
-
<div class="fs" class:fs--auto={autoHeight} style={style || undefined} style:height={autoHeight ? undefined : (height ? `${height}px` : '100%')} role="region" aria-label={L.dayPlanner}>
|
|
595
|
-
<div
|
|
596
|
-
class="fs-scroll"
|
|
597
|
-
class:fs-grabbing={scrollDragging}
|
|
598
|
-
class:fs-readonly={readOnly}
|
|
599
|
-
bind:this={el}
|
|
600
|
-
onwheel={(e) => {
|
|
601
|
-
const delta = e.deltaY || e.deltaX;
|
|
602
|
-
if (delta === 0) return;
|
|
603
|
-
// Only capture the wheel when the track can consume the delta in
|
|
604
|
-
// that direction — otherwise let the page scroll normally.
|
|
605
|
-
const maxScroll = el.scrollWidth - el.clientWidth;
|
|
606
|
-
const canConsume = delta < 0 ? el.scrollLeft > 0 : el.scrollLeft < maxScroll - 1;
|
|
607
|
-
if (!canConsume) return;
|
|
608
|
-
e.preventDefault();
|
|
609
|
-
el.scrollLeft += delta;
|
|
610
|
-
following = false;
|
|
611
|
-
}}
|
|
612
|
-
onscroll={handleScroll}
|
|
613
|
-
onpointerdown={onPointerDown}
|
|
614
|
-
role="region"
|
|
615
|
-
aria-label={L.scrollableDayPlanner}
|
|
616
|
-
>
|
|
617
|
-
<!-- Presentational layer: the click handler is a convenience delegate for
|
|
618
|
-
click-to-create on empty canvas; keyboard users create events via the
|
|
619
|
-
host UI, and events themselves are focusable buttons. -->
|
|
620
|
-
<div class="fs-track" style:width="{totalWidth}px" onclick={handleTrackClick} role="presentation">
|
|
621
|
-
{#each days as d (d.ms)}
|
|
622
|
-
<div
|
|
623
|
-
class="fs-day"
|
|
624
|
-
class:fs-today={d.today}
|
|
625
|
-
class:fs-past={d.past}
|
|
626
|
-
class:fs-disabled={disabledSet.has(d.ms)}
|
|
627
|
-
style:left="{d.x}px"
|
|
628
|
-
style:width="{dayWidth}px"
|
|
629
|
-
>
|
|
630
|
-
{#each { length: hourCount } as _, h}
|
|
631
|
-
{@const hour = startHour + h}
|
|
632
|
-
{@const x = h * hourWidth}
|
|
633
|
-
<div class="fs-tick" style:left="{x}px">
|
|
634
|
-
<span class="fs-tick-lb">{fmtH(hour, locale)}</span>
|
|
635
|
-
</div>
|
|
636
|
-
<div class="fs-tick fs-tick--half" style:left="{x + hourWidth * 0.5}px"></div>
|
|
637
|
-
{/each}
|
|
638
|
-
|
|
639
|
-
<!-- Blocked slot overlays -->
|
|
640
|
-
{#if blockedSlots?.length}
|
|
641
|
-
{@const jsDay = new Date(d.ms).getDay()}
|
|
642
|
-
{@const isoDay = jsDay === 0 ? 7 : jsDay}
|
|
643
|
-
{#each blockedSlots as slot}
|
|
644
|
-
{#if !slot.day || slot.day === isoDay}
|
|
645
|
-
{@const s = Math.max(slot.start, startHour)}
|
|
646
|
-
{@const e = Math.min(slot.end, endHour)}
|
|
647
|
-
{#if e > s}
|
|
648
|
-
<div
|
|
649
|
-
class="fs-blocked"
|
|
650
|
-
style:left="{(s - startHour) * hourWidth}px"
|
|
651
|
-
style:width="{(e - s) * hourWidth}px"
|
|
652
|
-
aria-label={slot.label || 'Unavailable'}
|
|
653
|
-
>
|
|
654
|
-
{#if slot.label}
|
|
655
|
-
<span class="fs-blocked-label">{slot.label}</span>
|
|
656
|
-
{/if}
|
|
657
|
-
</div>
|
|
658
|
-
{/if}
|
|
659
|
-
{/if}
|
|
660
|
-
{/each}
|
|
661
|
-
{/if}
|
|
662
|
-
|
|
663
|
-
<!-- Custom day header snippet -->
|
|
664
|
-
{#if dayHeaderSnippet}
|
|
665
|
-
<div class="fs-day-header-custom">
|
|
666
|
-
{@render dayHeaderSnippet({ date: new Date(d.ms), isToday: d.today, dayName: weekdayShort(d.ms, locale) })}
|
|
667
|
-
</div>
|
|
668
|
-
{/if}
|
|
669
|
-
</div>
|
|
670
|
-
{/each}
|
|
671
|
-
|
|
672
|
-
<!-- Now line (hidden when the clock is outside the visible hour band) -->
|
|
673
|
-
{#if nowInBand}
|
|
674
|
-
<div class="fs-now" style:left="{nowPx}px">
|
|
675
|
-
<span class="fs-now-tag">{clock.hm}</span>
|
|
676
|
-
<div class="fs-now-line"></div>
|
|
677
|
-
</div>
|
|
678
|
-
{/if}
|
|
679
|
-
|
|
680
|
-
<!-- Events -->
|
|
681
|
-
{#each positionedEvents as p (p.ev.id)}
|
|
682
|
-
{@const isCurrent = nowInfo.current.has(p.ev.id)}
|
|
683
|
-
{@const isNext = !p.isDragged && p.ev.id === nowInfo.nextId}
|
|
684
|
-
<div
|
|
685
|
-
class="fs-event"
|
|
686
|
-
class:fs-event--selected={selectedEventId === p.ev.id}
|
|
687
|
-
class:fs-event--current={isCurrent}
|
|
688
|
-
class:fs-event--next={isNext}
|
|
689
|
-
class:fs-event--dragging={p.isDragged}
|
|
690
|
-
class:fs-event--resizing={p.isDragged && drag?.mode !== 'move'}
|
|
691
|
-
class:fs-event--readonly={p.ev.data?.readOnly}
|
|
692
|
-
class:fs-event--cancelled={p.ev.status === 'cancelled'}
|
|
693
|
-
class:fs-event--tentative={p.ev.status === 'tentative'}
|
|
694
|
-
class:fs-event--full={p.ev.status === 'full'}
|
|
695
|
-
class:fs-event--limited={p.ev.status === 'limited'}
|
|
696
|
-
style:left="{p.x}px"
|
|
697
|
-
style:width="{p.width}px"
|
|
698
|
-
style:top="{p.topPx}px"
|
|
699
|
-
style:height="{p.heightPx}px"
|
|
700
|
-
style:--ev-color={p.ev.color ?? 'var(--dt-accent)'}
|
|
701
|
-
role="button"
|
|
702
|
-
tabindex="0"
|
|
703
|
-
title={p.ev.title}
|
|
704
|
-
aria-label="{p.ev.title}, {fmtTime(p.ev.start, locale)} – {fmtTime(p.ev.end, locale)}, {fmtDuration(p.ev.start, p.ev.end)}{p.ev.status === 'cancelled' ? ' (cancelled)' : ''}{p.ev.status === 'tentative' ? ' (tentative)' : ''}{p.ev.status === 'full' ? ' (full)' : ''}{p.ev.status === 'limited' ? ' (limited)' : ''}{isCurrent ? ` (${L.inProgress})` : ''}{isNext ? ` (${L.upNext})` : ''}"
|
|
705
|
-
onpointerdown={(e) => onEventPointerDown(e, p.ev)}
|
|
706
|
-
onpointerenter={() => oneventhover?.(p.ev)}
|
|
707
|
-
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); oneventclick?.(p.ev); } }}
|
|
708
|
-
>
|
|
709
|
-
<div class="fs-ev-inner">
|
|
710
|
-
{#if isCurrent}
|
|
711
|
-
<span class="fs-ev-live" aria-hidden="true"></span>
|
|
712
|
-
{:else if isNext}
|
|
713
|
-
<span class="fs-ev-next-badge" aria-hidden="true">{L.upNext}</span>
|
|
714
|
-
{/if}
|
|
715
|
-
<EventContent event={p.ev}>
|
|
716
|
-
{#if p.fit.time}
|
|
717
|
-
<span class="fs-ev-time">{fmtTime(p.ev.start, locale)} – {fmtTime(p.ev.end, locale)}</span>
|
|
718
|
-
{/if}
|
|
719
|
-
<span class="fs-ev-title">{p.ev.title}</span>
|
|
720
|
-
{#if p.ev.subtitle && p.fit.subtitle}
|
|
721
|
-
<span class="fs-ev-sub">{p.ev.subtitle}</span>
|
|
722
|
-
{/if}
|
|
723
|
-
{#if p.ev.location && p.fit.location}
|
|
724
|
-
<span class="fs-ev-loc">{p.ev.location}</span>
|
|
725
|
-
{/if}
|
|
726
|
-
{#if p.ev.tags?.length && p.fit.tags}
|
|
727
|
-
<span class="fs-ev-tags">
|
|
728
|
-
{#each p.ev.tags as tag}
|
|
729
|
-
<span class="fs-ev-tag">{tag}</span>
|
|
730
|
-
{/each}
|
|
731
|
-
</span>
|
|
732
|
-
{/if}
|
|
733
|
-
</EventContent>
|
|
734
|
-
</div>
|
|
735
|
-
{#if !readOnly && !p.ev.data?.readOnly && !p.isDragged}
|
|
736
|
-
<div
|
|
737
|
-
class="fs-ev-handle fs-ev-handle--start"
|
|
738
|
-
aria-hidden="true"
|
|
739
|
-
onpointerdown={(e) => onResizePointerDown(e, p.ev, 'start')}
|
|
740
|
-
></div>
|
|
741
|
-
<div
|
|
742
|
-
class="fs-ev-handle fs-ev-handle--end"
|
|
743
|
-
aria-hidden="true"
|
|
744
|
-
onpointerdown={(e) => onResizePointerDown(e, p.ev, 'end')}
|
|
745
|
-
></div>
|
|
746
|
-
{/if}
|
|
747
|
-
</div>
|
|
748
|
-
{/each}
|
|
749
|
-
|
|
750
|
-
<!-- Drag-to-create ghost -->
|
|
751
|
-
{#if !readOnly && drag?.active && drag.mode === 'create' && drag.payload}
|
|
752
|
-
{@const gx = timeToPx(drag.payload.start.getTime())}
|
|
753
|
-
{@const gw = Math.max(timeToPx(drag.payload.end.getTime()) - gx, 8)}
|
|
754
|
-
{@const gh = Math.max(MIN_EVENT_H, containerH - contentTop - 8 - EVENT_GAP)}
|
|
755
|
-
<div
|
|
756
|
-
class="fs-create-ghost"
|
|
757
|
-
style:left="{gx}px"
|
|
758
|
-
style:width="{gw}px"
|
|
759
|
-
style:top="{contentTop}px"
|
|
760
|
-
style:height="{gh}px"
|
|
761
|
-
aria-hidden="true"
|
|
762
|
-
>
|
|
763
|
-
<span class="fs-create-ghost-time">
|
|
764
|
-
{fmtTime(drag.payload.start, locale)} – {fmtTime(drag.payload.end, locale)}
|
|
765
|
-
</span>
|
|
766
|
-
</div>
|
|
767
|
-
{/if}
|
|
768
|
-
</div>
|
|
769
|
-
</div>
|
|
770
|
-
|
|
771
|
-
<!-- All-day events banner (outside scroll so it stays in viewport) -->
|
|
772
|
-
{#if allDayEvents.length > 0}
|
|
773
|
-
<div class="fs-allday" style:top="{CONTENT_TOP}px">
|
|
774
|
-
{#each allDayEvents as seg (seg.ev.id)}
|
|
775
|
-
<div
|
|
776
|
-
class="fs-ad"
|
|
777
|
-
class:fs-ad--start={seg.isStart}
|
|
778
|
-
class:fs-ad--selected={selectedEventId === seg.ev.id}
|
|
779
|
-
style:--ev-color={seg.ev.color ?? 'var(--dt-accent)'}
|
|
780
|
-
role="button"
|
|
781
|
-
tabindex="0"
|
|
782
|
-
aria-label="{seg.ev.title}{seg.totalDays > 1 ? `, ${L.dayNOfTotal(seg.dayIndex, seg.totalDays)}` : `, ${L.allDay}`}"
|
|
783
|
-
onclick={() => oneventclick?.(seg.ev)}
|
|
784
|
-
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); oneventclick?.(seg.ev); } }}
|
|
785
|
-
>
|
|
786
|
-
<span class="fs-ad-dot" aria-hidden="true"></span>
|
|
787
|
-
<span class="fs-ad-title">{seg.ev.title}</span>
|
|
788
|
-
{#if seg.totalDays > 1}
|
|
789
|
-
<span class="fs-ad-span">{L.day} {seg.dayIndex}/{seg.totalDays}</span>
|
|
790
|
-
{:else}
|
|
791
|
-
<span class="fs-ad-span">{L.allDay}</span>
|
|
792
|
-
{/if}
|
|
793
|
-
</div>
|
|
794
|
-
{/each}
|
|
795
|
-
</div>
|
|
796
|
-
{/if}
|
|
797
|
-
|
|
798
|
-
</div>
|
|
799
|
-
|
|
800
|
-
<style>
|
|
801
|
-
/* ─── Container ──────────────────────────────────── */
|
|
802
|
-
.fs {
|
|
803
|
-
position: relative;
|
|
804
|
-
overflow: hidden;
|
|
805
|
-
user-select: none;
|
|
806
|
-
font-variant-numeric: tabular-nums;
|
|
807
|
-
/* The track is px-sized from the container width; never let it feed
|
|
808
|
-
back into our own intrinsic size (host flex/grid min-width:auto
|
|
809
|
-
would otherwise loop container → hourWidth → track → container). */
|
|
810
|
-
contain: inline-size;
|
|
811
|
-
}
|
|
812
|
-
.fs--auto { overflow: visible; }
|
|
813
|
-
|
|
814
|
-
/* ─── Horizontal scroll ──────────────────────────── */
|
|
815
|
-
.fs-scroll {
|
|
816
|
-
width: 100%;
|
|
817
|
-
height: 100%;
|
|
818
|
-
overflow-x: auto;
|
|
819
|
-
overflow-y: hidden;
|
|
820
|
-
touch-action: pan-x;
|
|
821
|
-
cursor: default;
|
|
822
|
-
scrollbar-width: thin;
|
|
823
|
-
scrollbar-color: var(--dt-scrollbar, rgba(0, 0, 0, 0.1)) transparent;
|
|
824
|
-
}
|
|
825
|
-
/* Auto height grows vertically, but horizontal containment must stay —
|
|
826
|
-
overflow: visible here painted the whole track outside the calendar. */
|
|
827
|
-
.fs--auto .fs-scroll { height: auto; overflow-x: auto; overflow-y: hidden; }
|
|
828
|
-
.fs-scroll::-webkit-scrollbar { height: 5px; }
|
|
829
|
-
.fs-scroll::-webkit-scrollbar-thumb {
|
|
830
|
-
background: var(--dt-scrollbar, rgba(0, 0, 0, 0.1));
|
|
831
|
-
border-radius: 4px;
|
|
832
|
-
}
|
|
833
|
-
.fs-scroll::-webkit-scrollbar-track { background: transparent; }
|
|
834
|
-
.fs-readonly { cursor: grab; }
|
|
835
|
-
.fs-grabbing { cursor: grabbing; }
|
|
836
|
-
|
|
837
|
-
.fs-track {
|
|
838
|
-
position: relative;
|
|
839
|
-
height: 100%;
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
/* ─── Day block ──────────────────────────────────── */
|
|
843
|
-
.fs-day {
|
|
844
|
-
position: absolute;
|
|
845
|
-
top: 0;
|
|
846
|
-
height: 100%;
|
|
847
|
-
border-left: 1px solid var(--dt-border-day, rgba(0, 0, 0, 0.14));
|
|
848
|
-
box-sizing: border-box;
|
|
849
|
-
}
|
|
850
|
-
.fs-today { background: var(--dt-today-bg, color-mix(in srgb, var(--dt-accent, #2563eb) 8%, transparent)); }
|
|
851
|
-
/* Past days: dim via a background wash instead of a subtree opacity so
|
|
852
|
-
event text keeps full contrast. */
|
|
853
|
-
.fs-past { background: color-mix(in srgb, var(--dt-text, rgba(0, 0, 0, 0.87)) 3%, transparent); }
|
|
854
|
-
|
|
855
|
-
/* ─── Disabled day ───────────────────────────────── */
|
|
856
|
-
.fs-disabled {
|
|
857
|
-
opacity: 0.35;
|
|
858
|
-
background: repeating-linear-gradient(
|
|
859
|
-
45deg,
|
|
860
|
-
transparent,
|
|
861
|
-
transparent 6px,
|
|
862
|
-
var(--dt-border, rgba(0, 0, 0, 0.08)) 6px,
|
|
863
|
-
var(--dt-border, rgba(0, 0, 0, 0.08)) 7px
|
|
864
|
-
) !important;
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
/* ─── Blocked slot overlay ───────────────────────── */
|
|
868
|
-
.fs-blocked {
|
|
869
|
-
position: absolute;
|
|
870
|
-
top: 18px;
|
|
871
|
-
bottom: 0;
|
|
872
|
-
z-index: 3;
|
|
873
|
-
background: repeating-linear-gradient(
|
|
874
|
-
-45deg,
|
|
875
|
-
color-mix(in srgb, var(--dt-text, rgba(0, 0, 0, 0.87)) 4%, transparent),
|
|
876
|
-
color-mix(in srgb, var(--dt-text, rgba(0, 0, 0, 0.87)) 4%, transparent) 4px,
|
|
877
|
-
transparent 4px,
|
|
878
|
-
transparent 8px
|
|
879
|
-
);
|
|
880
|
-
border-radius: 4px;
|
|
881
|
-
pointer-events: none;
|
|
882
|
-
display: flex;
|
|
883
|
-
align-items: flex-end;
|
|
884
|
-
justify-content: center;
|
|
885
|
-
padding-bottom: 6px;
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
.fs-blocked-label {
|
|
889
|
-
font: 500 9px/1 var(--dt-sans, system-ui, sans-serif);
|
|
890
|
-
color: var(--dt-text-3, rgba(0, 0, 0, 0.38));
|
|
891
|
-
text-transform: uppercase;
|
|
892
|
-
letter-spacing: 0.04em;
|
|
893
|
-
white-space: nowrap;
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
/* ─── Custom day header ──────────────────────────── */
|
|
897
|
-
.fs-day-header-custom {
|
|
898
|
-
position: absolute;
|
|
899
|
-
top: 16px;
|
|
900
|
-
left: 50%;
|
|
901
|
-
transform: translateX(-50%);
|
|
902
|
-
z-index: 4;
|
|
903
|
-
pointer-events: auto;
|
|
904
|
-
white-space: nowrap;
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
/* ─── Hour ticks ─────────────────────────────────── */
|
|
908
|
-
.fs-tick {
|
|
909
|
-
position: absolute;
|
|
910
|
-
top: 0;
|
|
911
|
-
bottom: 0;
|
|
912
|
-
width: 0;
|
|
913
|
-
}
|
|
914
|
-
.fs-tick::before {
|
|
915
|
-
content: '';
|
|
916
|
-
position: absolute;
|
|
917
|
-
top: 18px;
|
|
918
|
-
bottom: 0;
|
|
919
|
-
width: 1px;
|
|
920
|
-
background: var(--dt-border, rgba(0, 0, 0, 0.08));
|
|
921
|
-
}
|
|
922
|
-
.fs-tick-lb {
|
|
923
|
-
position: absolute;
|
|
924
|
-
top: 2px;
|
|
925
|
-
left: 5px;
|
|
926
|
-
font: 500 10px/1 var(--dt-mono, ui-monospace, monospace);
|
|
927
|
-
color: var(--dt-text-3, rgba(0, 0, 0, 0.38));
|
|
928
|
-
white-space: nowrap;
|
|
929
|
-
pointer-events: none;
|
|
930
|
-
}
|
|
931
|
-
/* Half-hour guide: full-height line at low opacity through the event area */
|
|
932
|
-
.fs-tick--half::before {
|
|
933
|
-
top: 18px;
|
|
934
|
-
bottom: 0;
|
|
935
|
-
opacity: 0.35;
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
/* ─── Now-line ────────────────────────────────────── */
|
|
939
|
-
.fs-now {
|
|
940
|
-
position: absolute;
|
|
941
|
-
top: 0;
|
|
942
|
-
bottom: 0;
|
|
943
|
-
z-index: 10;
|
|
944
|
-
pointer-events: none;
|
|
945
|
-
transform: translateX(-1px);
|
|
946
|
-
}
|
|
947
|
-
.fs-now-line {
|
|
948
|
-
position: absolute;
|
|
949
|
-
top: 0;
|
|
950
|
-
bottom: 0;
|
|
951
|
-
left: 0;
|
|
952
|
-
width: 2px;
|
|
953
|
-
background: var(--dt-accent, #2563eb);
|
|
954
|
-
box-shadow: 0 0 8px var(--dt-glow, rgba(37, 99, 235, 0.25));
|
|
955
|
-
}
|
|
956
|
-
.fs-now-tag {
|
|
957
|
-
position: absolute;
|
|
958
|
-
/* Below the hour-label row (labels sit at top: 2px) so the tag never
|
|
959
|
-
collides with an hour label near hour boundaries. */
|
|
960
|
-
top: 20px;
|
|
961
|
-
left: 8px;
|
|
962
|
-
font: 700 11px/1 var(--dt-mono, ui-monospace, monospace);
|
|
963
|
-
color: var(--dt-accent, #2563eb);
|
|
964
|
-
background: color-mix(in srgb, var(--dt-bg, #ffffff) 92%, var(--dt-accent, #2563eb));
|
|
965
|
-
border: 1px solid var(--dt-accent-dim, rgba(37, 99, 235, 0.12));
|
|
966
|
-
padding: 3px 6px;
|
|
967
|
-
border-radius: 4px;
|
|
968
|
-
white-space: nowrap;
|
|
969
|
-
z-index: 1;
|
|
970
|
-
}
|
|
971
|
-
/* ─── All-day strip ─────────────────────────────── */
|
|
972
|
-
/* The container is a full-width overlay — let clicks pass through it and
|
|
973
|
-
only the chips themselves capture pointer events. */
|
|
974
|
-
.fs-allday {
|
|
975
|
-
position: absolute;
|
|
976
|
-
left: 0;
|
|
977
|
-
right: 0;
|
|
978
|
-
display: flex;
|
|
979
|
-
gap: 6px;
|
|
980
|
-
padding: 0 8px;
|
|
981
|
-
z-index: 7;
|
|
982
|
-
overflow-x: auto;
|
|
983
|
-
scrollbar-width: none;
|
|
984
|
-
pointer-events: none;
|
|
985
|
-
}
|
|
986
|
-
.fs-allday::-webkit-scrollbar { display: none; }
|
|
987
|
-
|
|
988
|
-
.fs-ad {
|
|
989
|
-
display: flex;
|
|
990
|
-
align-items: center;
|
|
991
|
-
gap: 4px;
|
|
992
|
-
padding: 2px 8px;
|
|
993
|
-
border-radius: 4px;
|
|
994
|
-
background: color-mix(in srgb, var(--ev-color) 18%, var(--dt-surface, var(--dt-bg, #ffffff)));
|
|
995
|
-
border-left: 3px solid var(--ev-color);
|
|
996
|
-
white-space: nowrap;
|
|
997
|
-
flex-shrink: 0;
|
|
998
|
-
min-width: 0;
|
|
999
|
-
max-width: 320px;
|
|
1000
|
-
cursor: pointer;
|
|
1001
|
-
transition: background 0.15s;
|
|
1002
|
-
pointer-events: auto;
|
|
1003
|
-
}
|
|
1004
|
-
.fs-ad:hover {
|
|
1005
|
-
background: color-mix(in srgb, var(--ev-color) 28%, var(--dt-surface, var(--dt-bg, #ffffff)));
|
|
1006
|
-
}
|
|
1007
|
-
.fs-ad:focus-visible {
|
|
1008
|
-
outline: none;
|
|
1009
|
-
box-shadow: 0 0 0 2px var(--dt-accent, #2563eb);
|
|
1010
|
-
}
|
|
1011
|
-
.fs-ad--selected {
|
|
1012
|
-
background: color-mix(in srgb, var(--ev-color) 30%, var(--dt-surface, var(--dt-bg, #ffffff)));
|
|
1013
|
-
border-left-width: 4px;
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
.fs-ad-dot {
|
|
1017
|
-
width: 6px;
|
|
1018
|
-
height: 6px;
|
|
1019
|
-
border-radius: 50%;
|
|
1020
|
-
background: var(--ev-color);
|
|
1021
|
-
flex-shrink: 0;
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
.fs-ad-title {
|
|
1025
|
-
font-size: 0.7rem;
|
|
1026
|
-
font-weight: 500;
|
|
1027
|
-
color: var(--dt-text, rgba(0, 0, 0, 0.87));
|
|
1028
|
-
flex: 0 1 auto;
|
|
1029
|
-
min-width: 0;
|
|
1030
|
-
overflow: hidden;
|
|
1031
|
-
text-overflow: ellipsis;
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
.fs-ad-span {
|
|
1035
|
-
font-size: 0.6rem;
|
|
1036
|
-
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
1037
|
-
flex-shrink: 0;
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
/* ─── Events ─────────────────────────────────────── */
|
|
1041
|
-
.fs-event {
|
|
1042
|
-
position: absolute;
|
|
1043
|
-
z-index: 6;
|
|
1044
|
-
border-radius: 6px;
|
|
1045
|
-
/* Editable events are grabbable; touch drags move the event instead of
|
|
1046
|
-
scrolling the strip. */
|
|
1047
|
-
cursor: grab;
|
|
1048
|
-
touch-action: none;
|
|
1049
|
-
background: color-mix(in srgb, var(--ev-color) 22%, var(--dt-surface, var(--dt-bg, #ffffff)));
|
|
1050
|
-
border: 1px solid color-mix(in srgb, var(--ev-color) 40%, transparent);
|
|
1051
|
-
/* Solid stripe at the start edge — matches the week view, keeps the
|
|
1052
|
-
pure tour color visible while the body stays a readable tint. */
|
|
1053
|
-
border-left: 3px solid var(--ev-color);
|
|
1054
|
-
overflow: hidden;
|
|
1055
|
-
display: flex;
|
|
1056
|
-
align-items: center;
|
|
1057
|
-
justify-content: center;
|
|
1058
|
-
/* top/height only: lane reflow animates on drop; left/width stay instant so
|
|
1059
|
-
mount-time width measurement and infinite-scroll rebases don't slide cards */
|
|
1060
|
-
transition: box-shadow 120ms, background 120ms,
|
|
1061
|
-
top 180ms cubic-bezier(0.2, 0.8, 0.2, 1), height 180ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
|
1062
|
-
}
|
|
1063
|
-
.fs-event:hover {
|
|
1064
|
-
background: color-mix(in srgb, var(--ev-color) 32%, var(--dt-surface, var(--dt-bg, #ffffff)));
|
|
1065
|
-
box-shadow: 0 2px 12px color-mix(in srgb, var(--ev-color) 25%, transparent);
|
|
1066
|
-
}
|
|
1067
|
-
.fs-event--selected {
|
|
1068
|
-
box-shadow: 0 0 0 2px var(--ev-color), 0 2px 14px color-mix(in srgb, var(--ev-color) 35%, transparent);
|
|
1069
|
-
}
|
|
1070
|
-
.fs-event--current {
|
|
1071
|
-
background: color-mix(in srgb, var(--ev-color) 22%, var(--dt-surface, var(--dt-bg, #ffffff)));
|
|
1072
|
-
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ev-color) 20%, transparent);
|
|
1073
|
-
}
|
|
1074
|
-
.fs-event--next {
|
|
1075
|
-
border-color: color-mix(in srgb, var(--ev-color) 75%, transparent);
|
|
1076
|
-
}
|
|
1077
|
-
.fs-event--dragging {
|
|
1078
|
-
opacity: 0.85;
|
|
1079
|
-
z-index: 50;
|
|
1080
|
-
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.35);
|
|
1081
|
-
cursor: grabbing;
|
|
1082
|
-
/* fast ease toward the snapped cursor position */
|
|
1083
|
-
transition: left 80ms ease-out, width 80ms ease-out;
|
|
1084
|
-
}
|
|
1085
|
-
.fs-event--resizing {
|
|
1086
|
-
cursor: ew-resize;
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
/* ─── Resize handles ─────────────────────────────── */
|
|
1090
|
-
.fs-ev-handle {
|
|
1091
|
-
position: absolute;
|
|
1092
|
-
top: 0;
|
|
1093
|
-
bottom: 0;
|
|
1094
|
-
width: 6px;
|
|
1095
|
-
z-index: 2;
|
|
1096
|
-
cursor: ew-resize;
|
|
1097
|
-
touch-action: none;
|
|
1098
|
-
}
|
|
1099
|
-
/* Hit-slop: ~20px effective grab zone while the visual stays 6px */
|
|
1100
|
-
.fs-ev-handle::before {
|
|
1101
|
-
content: '';
|
|
1102
|
-
position: absolute;
|
|
1103
|
-
top: 0;
|
|
1104
|
-
bottom: 0;
|
|
1105
|
-
left: -7px;
|
|
1106
|
-
right: -7px;
|
|
1107
|
-
}
|
|
1108
|
-
.fs-ev-handle--start { left: 0; }
|
|
1109
|
-
.fs-ev-handle--end { right: 0; }
|
|
1110
|
-
.fs-ev-handle::after {
|
|
1111
|
-
content: '';
|
|
1112
|
-
position: absolute;
|
|
1113
|
-
top: 20%;
|
|
1114
|
-
bottom: 20%;
|
|
1115
|
-
left: 2px;
|
|
1116
|
-
width: 2px;
|
|
1117
|
-
border-radius: 2px;
|
|
1118
|
-
background: var(--ev-color);
|
|
1119
|
-
opacity: 0;
|
|
1120
|
-
transition: opacity 120ms;
|
|
1121
|
-
}
|
|
1122
|
-
.fs-event:hover .fs-ev-handle::after,
|
|
1123
|
-
.fs-event:focus-within .fs-ev-handle::after { opacity: 0.55; }
|
|
1124
|
-
/* Coarse pointers can't hover — show the grips persistently */
|
|
1125
|
-
@media (hover: none) {
|
|
1126
|
-
.fs-ev-handle::after { opacity: 0.55; }
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
/* ─── Drag-to-create ghost ───────────────────────── */
|
|
1130
|
-
.fs-create-ghost {
|
|
1131
|
-
position: absolute;
|
|
1132
|
-
z-index: 40;
|
|
1133
|
-
border-radius: 6px;
|
|
1134
|
-
background: color-mix(in srgb, var(--dt-accent, #2563eb) 14%, transparent);
|
|
1135
|
-
border: 1px dashed color-mix(in srgb, var(--dt-accent, #2563eb) 60%, transparent);
|
|
1136
|
-
display: flex;
|
|
1137
|
-
align-items: flex-start;
|
|
1138
|
-
justify-content: center;
|
|
1139
|
-
overflow: hidden;
|
|
1140
|
-
pointer-events: none;
|
|
1141
|
-
}
|
|
1142
|
-
.fs-create-ghost-time {
|
|
1143
|
-
font: 600 10px/1 var(--dt-mono, ui-monospace, monospace);
|
|
1144
|
-
color: var(--dt-accent, #2563eb);
|
|
1145
|
-
padding: 6px 4px;
|
|
1146
|
-
white-space: nowrap;
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
@media (prefers-reduced-motion: reduce) {
|
|
1150
|
-
.fs-event,
|
|
1151
|
-
.fs-event--dragging {
|
|
1152
|
-
transition: box-shadow 120ms, background 120ms;
|
|
1153
|
-
}
|
|
1154
|
-
.fs-create-ghost,
|
|
1155
|
-
.fs-ad,
|
|
1156
|
-
.fs-ev-handle::after {
|
|
1157
|
-
transition: none;
|
|
1158
|
-
}
|
|
1159
|
-
}
|
|
1160
|
-
/* Cancelled: strikethrough + secondary text, not a subtree opacity dim */
|
|
1161
|
-
.fs-event--cancelled .fs-ev-title {
|
|
1162
|
-
text-decoration: line-through;
|
|
1163
|
-
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
1164
|
-
}
|
|
1165
|
-
.fs-event--tentative {
|
|
1166
|
-
opacity: 0.65;
|
|
1167
|
-
border: 1px dashed color-mix(in srgb, var(--ev-color) 40%, transparent);
|
|
1168
|
-
}
|
|
1169
|
-
.fs-event--full {
|
|
1170
|
-
opacity: 0.55;
|
|
1171
|
-
}
|
|
1172
|
-
.fs-event--limited {
|
|
1173
|
-
opacity: 0.65;
|
|
1174
|
-
border: 1px dashed color-mix(in srgb, var(--ev-color) 40%, transparent);
|
|
1175
|
-
}
|
|
1176
|
-
.fs-event--readonly,
|
|
1177
|
-
.fs-readonly .fs-event {
|
|
1178
|
-
cursor: default;
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
/* Event inner — vertical text along lane height (day filmstrip) */
|
|
1182
|
-
.fs-ev-inner {
|
|
1183
|
-
writing-mode: vertical-rl;
|
|
1184
|
-
text-orientation: mixed;
|
|
1185
|
-
transform: rotate(180deg);
|
|
1186
|
-
display: flex;
|
|
1187
|
-
flex-direction: row;
|
|
1188
|
-
align-items: center;
|
|
1189
|
-
justify-content: center;
|
|
1190
|
-
gap: 6px;
|
|
1191
|
-
height: 100%;
|
|
1192
|
-
max-width: 100%;
|
|
1193
|
-
overflow: hidden;
|
|
1194
|
-
box-sizing: border-box;
|
|
1195
|
-
padding: 8px 4px;
|
|
1196
|
-
}
|
|
1197
|
-
.fs-ev-live {
|
|
1198
|
-
flex-shrink: 0;
|
|
1199
|
-
width: 7px;
|
|
1200
|
-
height: 7px;
|
|
1201
|
-
border-radius: 50%;
|
|
1202
|
-
background: var(--ev-color);
|
|
1203
|
-
}
|
|
1204
|
-
.fs-ev-next-badge {
|
|
1205
|
-
flex-shrink: 0;
|
|
1206
|
-
font: 600 8px/1 var(--dt-sans, system-ui, sans-serif);
|
|
1207
|
-
text-transform: uppercase;
|
|
1208
|
-
letter-spacing: 0.06em;
|
|
1209
|
-
color: var(--ev-color, var(--dt-accent));
|
|
1210
|
-
background: color-mix(in srgb, var(--ev-color, var(--dt-accent)) 15%, transparent);
|
|
1211
|
-
padding: 2px 5px;
|
|
1212
|
-
border-radius: 3px;
|
|
1213
|
-
white-space: nowrap;
|
|
1214
|
-
}
|
|
1215
|
-
.fs-ev-title {
|
|
1216
|
-
font: 600 13px/1.15 var(--dt-sans, system-ui, sans-serif);
|
|
1217
|
-
color: var(--dt-text, rgba(0, 0, 0, 0.87));
|
|
1218
|
-
overflow: hidden;
|
|
1219
|
-
text-overflow: ellipsis;
|
|
1220
|
-
/* In vertical writing mode line boxes stack as columns — allow a
|
|
1221
|
-
second column before truncating so overlapping (short) cards keep
|
|
1222
|
-
readable names. Full name is in the title tooltip. */
|
|
1223
|
-
display: -webkit-box;
|
|
1224
|
-
-webkit-box-orient: vertical;
|
|
1225
|
-
-webkit-line-clamp: 2;
|
|
1226
|
-
line-clamp: 2;
|
|
1227
|
-
white-space: normal;
|
|
1228
|
-
max-height: 100%;
|
|
1229
|
-
flex-shrink: 0;
|
|
1230
|
-
}
|
|
1231
|
-
.fs-ev-time {
|
|
1232
|
-
font: 400 10px/1 var(--dt-mono, ui-monospace, monospace);
|
|
1233
|
-
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
1234
|
-
white-space: nowrap;
|
|
1235
|
-
flex-shrink: 0;
|
|
1236
|
-
}
|
|
1237
|
-
.fs-ev-sub {
|
|
1238
|
-
font: 400 11px/1 var(--dt-sans, system-ui, sans-serif);
|
|
1239
|
-
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
1240
|
-
white-space: nowrap;
|
|
1241
|
-
overflow: hidden;
|
|
1242
|
-
text-overflow: ellipsis;
|
|
1243
|
-
max-height: 100%;
|
|
1244
|
-
flex-shrink: 0;
|
|
1245
|
-
}
|
|
1246
|
-
.fs-ev-loc {
|
|
1247
|
-
font: 400 10px/1 var(--dt-sans, system-ui, sans-serif);
|
|
1248
|
-
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
1249
|
-
white-space: nowrap;
|
|
1250
|
-
overflow: hidden;
|
|
1251
|
-
text-overflow: ellipsis;
|
|
1252
|
-
max-height: 100%;
|
|
1253
|
-
flex-shrink: 0;
|
|
1254
|
-
}
|
|
1255
|
-
.fs-ev-tags {
|
|
1256
|
-
display: flex;
|
|
1257
|
-
flex-direction: row;
|
|
1258
|
-
gap: 4px;
|
|
1259
|
-
flex-shrink: 0;
|
|
1260
|
-
}
|
|
1261
|
-
.fs-ev-tag {
|
|
1262
|
-
font: 500 8px/1 var(--dt-sans, system-ui, sans-serif);
|
|
1263
|
-
color: var(--ev-color, var(--dt-accent));
|
|
1264
|
-
background: color-mix(in srgb, var(--ev-color, var(--dt-accent)) 18%, transparent);
|
|
1265
|
-
padding: 1px 4px;
|
|
1266
|
-
border-radius: 3px;
|
|
1267
|
-
white-space: nowrap;
|
|
1268
|
-
}
|
|
1269
|
-
|
|
1270
|
-
/* ─── Focus-visible ──────────────────────────────── */
|
|
1271
|
-
/* box-shadow instead of outline: outlines get clipped by the
|
|
1272
|
-
overflow: hidden scroll container. */
|
|
1273
|
-
.fs-event:focus-visible {
|
|
1274
|
-
outline: none;
|
|
1275
|
-
box-shadow: 0 0 0 2px var(--dt-accent, #2563eb);
|
|
1276
|
-
}
|
|
1277
|
-
</style>
|
|
1278
|
-
|
|
1279
|
-
|