@nomideusz/svelte-calendar 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -8
- package/dist/calendar/Calendar.svelte +187 -64
- package/dist/calendar/Calendar.svelte.d.ts +14 -0
- package/dist/core/clock.svelte.d.ts +1 -1
- package/dist/core/clock.svelte.js +8 -4
- package/dist/core/locale.d.ts +15 -1
- package/dist/core/locale.js +9 -2
- package/dist/core/timezone.d.ts +12 -0
- package/dist/core/timezone.js +31 -0
- package/dist/headless/create-calendar.svelte.js +12 -6
- package/dist/headless/types.d.ts +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/theme/auto.js +27 -6
- package/dist/theme/presets.d.ts +4 -4
- package/dist/theme/presets.js +11 -7
- package/dist/views/agenda/AgendaDay.svelte +260 -111
- package/dist/views/agenda/AgendaWeek.svelte +191 -66
- package/dist/views/mobile/MobileDay.svelte +409 -214
- package/dist/views/mobile/MobileWeek.svelte +137 -75
- package/dist/views/mobile/swipe.d.ts +28 -0
- package/dist/views/mobile/swipe.js +64 -0
- package/dist/views/month/MonthGrid.svelte +167 -31
- package/dist/views/planner/PlannerDay.svelte +156 -81
- package/dist/views/planner/PlannerWeek.svelte +1156 -631
- package/dist/views/planner/PlannerWeek.svelte.d.ts +2 -0
- package/dist/views/shared/context.svelte.d.ts +4 -0
- package/dist/views/shared/context.svelte.js +14 -9
- package/dist/views/shared/format.d.ts +2 -1
- package/dist/views/shared/format.js +8 -5
- package/dist/widget/CalendarWidget.svelte +33 -5
- package/dist/widget/CalendarWidget.svelte.d.ts +16 -2
- package/dist/widget/widget.d.ts +9 -0
- package/dist/widget/widget.js +59 -13
- package/package.json +1 -1
- package/widget/widget.js +3830 -2755
- package/widget/svelte-calendar.css +0 -3054
|
@@ -1,24 +1,23 @@
|
|
|
1
1
|
<!--
|
|
2
|
-
Planner week mode —
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
•
|
|
6
|
-
•
|
|
7
|
-
|
|
8
|
-
•
|
|
9
|
-
|
|
2
|
+
Planner week mode — vertical time grid (Google-Calendar-style).
|
|
3
|
+
|
|
4
|
+
• Left time gutter with localized hour labels; N day columns (viewState.dayCount).
|
|
5
|
+
• Sticky day-header row + sticky all-day strip; the grid scrolls vertically.
|
|
6
|
+
• Timed events are absolutely positioned by start/end; overlaps share the
|
|
7
|
+
column via union-find lanes.
|
|
8
|
+
• Drag-to-move (vertical = time, horizontal = day), top/bottom resize
|
|
9
|
+
handles, drag-to-create with ghost preview — all through ctx.drag /
|
|
10
|
+
ctx.commitDrag so Calendar's validation (min/max duration, blocked,
|
|
11
|
+
disabled) applies on drop.
|
|
12
|
+
• Now-line across today's column only; auto-scrolls to the current time.
|
|
10
13
|
-->
|
|
11
|
-
<script lang="ts">import {
|
|
12
|
-
import { flip } from "svelte/animate";
|
|
13
|
-
import { crossfade } from "svelte/transition";
|
|
14
|
-
import { prefersReducedMotion } from "svelte/motion";
|
|
14
|
+
<script lang="ts">import { untrack } from "svelte";
|
|
15
15
|
import { useCalendarContext } from "../shared/context.svelte.js";
|
|
16
16
|
import EventContent from "../shared/EventContent.svelte";
|
|
17
17
|
import { createClock } from "../../core/clock.svelte.js";
|
|
18
|
-
import { DAY_MS, HOUR_MS, sod } from "../../core/time.js";
|
|
19
|
-
import { startOfWeek as sowFn,
|
|
20
|
-
import {
|
|
21
|
-
const L = $derived(getLabels());
|
|
18
|
+
import { DAY_MS, HOUR_MS, sod, addDaysMs } from "../../core/time.js";
|
|
19
|
+
import { startOfWeek as sowFn, isAllDay, isMultiDay, segmentForDay } from "../../core/time.js";
|
|
20
|
+
import { fmtH, fmtTime, fmtDuration, weekdayShort } from "../../core/locale.js";
|
|
22
21
|
let {
|
|
23
22
|
mondayStart = true,
|
|
24
23
|
locale,
|
|
@@ -29,12 +28,12 @@ let {
|
|
|
29
28
|
oneventclick,
|
|
30
29
|
oneventcreate,
|
|
31
30
|
selectedEventId = null,
|
|
32
|
-
readOnly = false
|
|
31
|
+
readOnly = false,
|
|
32
|
+
visibleHours
|
|
33
33
|
} = $props();
|
|
34
34
|
const ctx = useCalendarContext();
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
const [previewSend, previewReceive] = crossfade({ duration: () => prefersReducedMotion.current ? 0 : 160 });
|
|
35
|
+
const L = $derived(ctx.labels);
|
|
36
|
+
const clock = createClock(ctx.timezone);
|
|
38
37
|
const drag = $derived(ctx.drag);
|
|
39
38
|
const commitDragCtx = $derived(ctx.commitDrag);
|
|
40
39
|
const viewState = $derived(ctx.viewState);
|
|
@@ -48,823 +47,1349 @@ const minDuration = $derived(ctx.minDuration);
|
|
|
48
47
|
const autoHeight = $derived(ctx.autoHeight);
|
|
49
48
|
const oneventhover = $derived(ctx.oneventhover);
|
|
50
49
|
const disabledSet = $derived(ctx.disabledSet);
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
let
|
|
61
|
-
|
|
62
|
-
if (!el) return;
|
|
63
|
-
let target = null;
|
|
64
|
-
if (targetMs !== void 0) {
|
|
65
|
-
const rows = el.querySelectorAll("[data-week]");
|
|
66
|
-
for (const row of rows) {
|
|
67
|
-
const weekMs = Number(row.dataset.week);
|
|
68
|
-
if (weekMs <= targetMs && targetMs < weekMs + customDays * DAY_MS) {
|
|
69
|
-
target = row;
|
|
70
|
-
break;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
if (!target) target = el.querySelector(".wg-week--current");
|
|
75
|
-
if (!target) return;
|
|
76
|
-
const targetTop = target.offsetTop - (el.clientHeight - target.offsetHeight) / 2;
|
|
77
|
-
el.scrollTo({ top: Math.max(0, targetTop), behavior });
|
|
78
|
-
}
|
|
50
|
+
const SNAP_MS = $derived(ctx.snapInterval * 6e4);
|
|
51
|
+
const HOUR_H = 48;
|
|
52
|
+
const GUTTER_W = 48;
|
|
53
|
+
const MIN_COL_W = 110;
|
|
54
|
+
const ALLDAY_MAX = 3;
|
|
55
|
+
const startHour = $derived(visibleHours?.[0] ?? 0);
|
|
56
|
+
const endHour = $derived(visibleHours?.[1] ?? 24);
|
|
57
|
+
const hourCount = $derived(Math.max(1, endHour - startHour));
|
|
58
|
+
const gridHeight = $derived(hourCount * HOUR_H);
|
|
59
|
+
let scrollEl;
|
|
60
|
+
let colsEl;
|
|
79
61
|
const todayMs = $derived(clock.today);
|
|
80
62
|
const customDays = $derived(viewState?.dayCount ?? 7);
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
);
|
|
63
|
+
const weekStartMs = $derived.by(() => {
|
|
64
|
+
const r = viewState?.range;
|
|
65
|
+
if (r) return sod(r.start.getTime());
|
|
66
|
+
const f = focusDate?.getTime() ?? todayMs;
|
|
67
|
+
return customDays === 7 ? sowFn(f, mondayStart) : sod(f);
|
|
68
|
+
});
|
|
69
|
+
const weekEndMs = $derived(addDaysMs(weekStartMs, customDays));
|
|
70
|
+
const weekHasToday = $derived(todayMs >= weekStartMs && todayMs < weekEndMs);
|
|
71
|
+
const dayCols = $derived.by(() => {
|
|
72
|
+
const cols = [];
|
|
73
|
+
for (let d = 0; d < customDays; d++) {
|
|
74
|
+
const ms = addDaysMs(weekStartMs, d);
|
|
75
|
+
const date = new Date(ms);
|
|
76
|
+
const dow = date.getDay();
|
|
77
|
+
const isoDay = dow === 0 ? 7 : dow;
|
|
78
|
+
if (hideDays?.includes(isoDay)) continue;
|
|
79
|
+
cols.push({
|
|
80
|
+
ms,
|
|
81
|
+
isToday: ms === todayMs,
|
|
82
|
+
isPast: equalDays ? false : ms < todayMs,
|
|
83
|
+
isWeekend: dow === 0 || dow === 6,
|
|
84
|
+
isDisabled: disabledSet.has(ms),
|
|
85
|
+
isoDay,
|
|
86
|
+
dayNum: date.getDate()
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return cols;
|
|
90
|
+
});
|
|
91
|
+
const innerMinWidth = $derived(GUTTER_W + dayCols.length * MIN_COL_W);
|
|
84
92
|
$effect(() => {
|
|
85
93
|
if (!loadRangeCtx) return;
|
|
86
|
-
const rangeStart = new Date(
|
|
87
|
-
const rangeEnd = new Date(
|
|
94
|
+
const rangeStart = new Date(weekStartMs - 7 * DAY_MS);
|
|
95
|
+
const rangeEnd = new Date(weekEndMs + 7 * DAY_MS);
|
|
88
96
|
loadRangeCtx.set({ start: rangeStart, end: rangeEnd });
|
|
89
97
|
return () => loadRangeCtx.set(null);
|
|
90
98
|
});
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
for (
|
|
94
|
-
const
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
99
|
+
const allDayByDay = $derived.by(() => {
|
|
100
|
+
const map = /* @__PURE__ */ new Map();
|
|
101
|
+
for (const day of dayCols) {
|
|
102
|
+
const segs = [];
|
|
103
|
+
for (const ev of events) {
|
|
104
|
+
if (!isAllDay(ev) && !isMultiDay(ev)) continue;
|
|
105
|
+
const seg = segmentForDay(ev, day.ms);
|
|
106
|
+
if (seg) segs.push(seg);
|
|
107
|
+
}
|
|
108
|
+
if (segs.length) map.set(day.ms, segs);
|
|
109
|
+
}
|
|
110
|
+
return map;
|
|
111
|
+
});
|
|
112
|
+
const hasAllDayRow = $derived(allDayByDay.size > 0);
|
|
113
|
+
let adExpanded = $state({});
|
|
114
|
+
const movingId = $derived(drag?.active && drag.mode === "move" ? drag.payload?.eventId ?? null : null);
|
|
115
|
+
const movingEvent = $derived(movingId ? events.find((e) => e.id === movingId) ?? null : null);
|
|
116
|
+
const layoutByDay = $derived.by(() => {
|
|
117
|
+
const rsP = drag?.active && (drag.mode === "resize-start" || drag.mode === "resize-end") ? drag.payload : null;
|
|
118
|
+
const map = /* @__PURE__ */ new Map();
|
|
119
|
+
for (const day of dayCols) {
|
|
120
|
+
let find = function(i) {
|
|
121
|
+
while (par[i] !== i) {
|
|
122
|
+
par[i] = par[par[i]];
|
|
123
|
+
i = par[i];
|
|
124
|
+
}
|
|
125
|
+
return i;
|
|
126
|
+
};
|
|
127
|
+
const dayEnd = day.ms + DAY_MS;
|
|
128
|
+
const bandStart = day.ms + startHour * HOUR_MS;
|
|
129
|
+
const bandEnd = day.ms + endHour * HOUR_MS;
|
|
130
|
+
const infos = [];
|
|
131
|
+
for (const ev of events) {
|
|
132
|
+
if (isAllDay(ev) || isMultiDay(ev)) continue;
|
|
133
|
+
if (ev.id === movingId) continue;
|
|
134
|
+
const isResizing = rsP?.eventId === ev.id;
|
|
135
|
+
const s0 = isResizing ? rsP.start.getTime() : ev.start.getTime();
|
|
136
|
+
const e0 = isResizing ? rsP.end.getTime() : ev.end.getTime();
|
|
137
|
+
if (s0 >= dayEnd || e0 <= day.ms) continue;
|
|
138
|
+
const sMs = Math.max(s0, bandStart);
|
|
139
|
+
const eMs = Math.min(e0, bandEnd);
|
|
140
|
+
if (eMs <= sMs) continue;
|
|
141
|
+
infos.push({ ev, startMs: sMs, endMs: eMs, isResizing, col: 0, totalCols: 1 });
|
|
142
|
+
}
|
|
143
|
+
infos.sort((a, b) => a.startMs - b.startMs || b.endMs - a.endMs);
|
|
144
|
+
const par = infos.map((_, i) => i);
|
|
145
|
+
for (let i = 0; i < infos.length; i++) {
|
|
146
|
+
for (let j = i + 1; j < infos.length; j++) {
|
|
147
|
+
if (infos[j].startMs < infos[i].endMs) par[find(i)] = find(j);
|
|
148
|
+
else break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const groups = /* @__PURE__ */ new Map();
|
|
152
|
+
for (let i = 0; i < infos.length; i++) {
|
|
153
|
+
const root = find(i);
|
|
154
|
+
if (!groups.has(root)) groups.set(root, []);
|
|
155
|
+
groups.get(root).push(i);
|
|
156
|
+
}
|
|
157
|
+
for (const [, indices] of groups) {
|
|
158
|
+
const lanes = [];
|
|
159
|
+
for (const idx of indices) {
|
|
160
|
+
const inf = infos[idx];
|
|
161
|
+
let lane = 0;
|
|
162
|
+
for (let r = 0; r < lanes.length; r++) {
|
|
163
|
+
if (lanes[r] <= inf.startMs) {
|
|
164
|
+
lane = r;
|
|
165
|
+
lanes[r] = inf.endMs;
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
lane = r + 1;
|
|
117
169
|
}
|
|
170
|
+
if (lane >= lanes.length) lanes.push(inf.endMs);
|
|
171
|
+
infos[idx].col = lane;
|
|
118
172
|
}
|
|
119
|
-
|
|
173
|
+
for (const idx of indices) infos[idx].totalCols = lanes.length;
|
|
120
174
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
175
|
+
map.set(
|
|
176
|
+
day.ms,
|
|
177
|
+
infos.map((inf) => ({
|
|
178
|
+
ev: inf.ev,
|
|
179
|
+
top: ((inf.startMs - day.ms) / HOUR_MS - startHour) * HOUR_H,
|
|
180
|
+
height: Math.max(24, (inf.endMs - inf.startMs) / HOUR_MS * HOUR_H),
|
|
181
|
+
col: inf.col,
|
|
182
|
+
totalCols: inf.totalCols,
|
|
183
|
+
isResizing: inf.isResizing
|
|
184
|
+
}))
|
|
185
|
+
);
|
|
125
186
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
187
|
+
return map;
|
|
188
|
+
});
|
|
189
|
+
const nowIds = $derived.by(() => {
|
|
190
|
+
const now = clock.tick;
|
|
191
|
+
const s = /* @__PURE__ */ new Set();
|
|
192
|
+
for (const ev of events) {
|
|
193
|
+
if (ev.start.getTime() <= now && ev.end.getTime() > now) s.add(ev.id);
|
|
134
194
|
}
|
|
135
|
-
return
|
|
195
|
+
return s;
|
|
136
196
|
});
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
return _fmtTime(new Date(tick2), locale);
|
|
142
|
-
}
|
|
143
|
-
const nowFrac = $derived(fractionalHour(clock.tick) / 24);
|
|
144
|
-
onMount(async () => {
|
|
145
|
-
await tick();
|
|
146
|
-
scrollWeekIntoContainer();
|
|
197
|
+
const nowFracHour = $derived((clock.tick - clock.today) / HOUR_MS);
|
|
198
|
+
const nowY = $derived.by(() => {
|
|
199
|
+
if (nowFracHour < startHour || nowFracHour > endHour) return null;
|
|
200
|
+
return (nowFracHour - startHour) * HOUR_H;
|
|
147
201
|
});
|
|
202
|
+
const weekIsEmpty = $derived(
|
|
203
|
+
!events.some((ev) => ev.start.getTime() < weekEndMs && ev.end.getTime() > weekStartMs)
|
|
204
|
+
);
|
|
148
205
|
$effect(() => {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
206
|
+
void weekStartMs;
|
|
207
|
+
const el = scrollEl;
|
|
208
|
+
if (!el) return;
|
|
209
|
+
untrack(() => {
|
|
210
|
+
let targetHour;
|
|
211
|
+
if (weekHasToday) {
|
|
212
|
+
const clamped = Math.min(Math.max(nowFracHour, startHour), endHour);
|
|
213
|
+
targetHour = Math.max(startHour, clamped - 1);
|
|
214
|
+
} else {
|
|
215
|
+
targetHour = Math.max(startHour, Math.min(8, endHour - 1));
|
|
216
|
+
}
|
|
217
|
+
el.scrollTop = (targetHour - startHour) * HOUR_H;
|
|
218
|
+
});
|
|
155
219
|
});
|
|
156
|
-
function
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
220
|
+
function colsRect() {
|
|
221
|
+
return colsEl.getBoundingClientRect();
|
|
222
|
+
}
|
|
223
|
+
function pointerDayIndex(clientX) {
|
|
224
|
+
const r = colsRect();
|
|
225
|
+
const n = dayCols.length;
|
|
226
|
+
if (n === 0) return 0;
|
|
227
|
+
const w = r.width / n;
|
|
228
|
+
return Math.max(0, Math.min(n - 1, Math.floor((clientX - r.left) / w)));
|
|
229
|
+
}
|
|
230
|
+
function pointerHour(clientY) {
|
|
231
|
+
return startHour + (clientY - colsRect().top) / HOUR_H;
|
|
232
|
+
}
|
|
233
|
+
function pointerTimeMs(clientX, clientY) {
|
|
234
|
+
const dayMs = dayCols[pointerDayIndex(clientX)]?.ms ?? weekStartMs;
|
|
235
|
+
const hour = Math.min(Math.max(pointerHour(clientY), startHour), endHour);
|
|
236
|
+
return dayMs + hour * HOUR_MS;
|
|
237
|
+
}
|
|
238
|
+
function clampToDayBand(ms, dayMs) {
|
|
239
|
+
return Math.max(dayMs + startHour * HOUR_MS, Math.min(dayMs + endHour * HOUR_MS, ms));
|
|
240
|
+
}
|
|
241
|
+
function isBlockedAt(dayMs, hour) {
|
|
242
|
+
if (!blockedSlots?.length) return false;
|
|
243
|
+
const jsDay = new Date(dayMs).getDay();
|
|
244
|
+
const isoDay = jsDay === 0 ? 7 : jsDay;
|
|
245
|
+
return blockedSlots.some((slot) => {
|
|
246
|
+
if (slot.day && slot.day !== isoDay) return false;
|
|
247
|
+
return hour >= slot.start && hour < slot.end;
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
function blockedRangeLabel(dayMs, slotStart, slotEnd) {
|
|
251
|
+
return `${fmtTime(new Date(dayMs + slotStart * HOUR_MS), locale)} \u2013 ${fmtTime(new Date(dayMs + slotEnd * HOUR_MS), locale)}`;
|
|
252
|
+
}
|
|
253
|
+
function statusText(ev) {
|
|
254
|
+
if (ev.status === "cancelled") return ` (${L.cancelled})`;
|
|
255
|
+
if (ev.status === "tentative") return ` (${L.tentative})`;
|
|
256
|
+
if (ev.status === "full") return ` (${L.full})`;
|
|
257
|
+
if (ev.status === "limited") return ` (${L.limited})`;
|
|
258
|
+
return "";
|
|
259
|
+
}
|
|
260
|
+
function ghostForDay(dayMs) {
|
|
261
|
+
if (!drag?.active || !drag.payload) return null;
|
|
262
|
+
const mode = drag.mode;
|
|
263
|
+
if (mode !== "move" && mode !== "create") return null;
|
|
264
|
+
const s = drag.payload.start.getTime();
|
|
265
|
+
const e = drag.payload.end.getTime();
|
|
266
|
+
const bandS = dayMs + startHour * HOUR_MS;
|
|
267
|
+
const bandE = dayMs + endHour * HOUR_MS;
|
|
268
|
+
const cs = Math.max(s, bandS);
|
|
269
|
+
const ce = Math.min(e, bandE);
|
|
270
|
+
if (ce <= cs) return null;
|
|
271
|
+
return {
|
|
272
|
+
top: ((cs - dayMs) / HOUR_MS - startHour) * HOUR_H,
|
|
273
|
+
height: Math.max(12, (ce - cs) / HOUR_MS * HOUR_H),
|
|
274
|
+
start: drag.payload.start,
|
|
275
|
+
end: drag.payload.end,
|
|
276
|
+
create: mode === "create",
|
|
277
|
+
// Only the segment containing the start shows the readout
|
|
278
|
+
showTime: cs === Math.max(s, dayMs)
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
const CREATE_THRESHOLD = 4;
|
|
282
|
+
const LONG_PRESS_MS = 350;
|
|
283
|
+
const LONG_PRESS_TOLERANCE = 8;
|
|
284
|
+
let suppressColsClick = false;
|
|
285
|
+
let crStartX = 0;
|
|
286
|
+
let crStartY = 0;
|
|
287
|
+
let crAnchorMs = 0;
|
|
288
|
+
let crDayMs = 0;
|
|
289
|
+
let crStarted = false;
|
|
290
|
+
let longPressTimer = null;
|
|
291
|
+
function blockTouchScroll(e) {
|
|
292
|
+
e.preventDefault();
|
|
293
|
+
}
|
|
294
|
+
function addTouchScrollBlock() {
|
|
295
|
+
window.addEventListener("touchmove", blockTouchScroll, { passive: false });
|
|
296
|
+
}
|
|
297
|
+
function removeTouchScrollBlock() {
|
|
298
|
+
window.removeEventListener("touchmove", blockTouchScroll);
|
|
299
|
+
}
|
|
300
|
+
function clearLongPress() {
|
|
301
|
+
if (longPressTimer !== null) {
|
|
302
|
+
clearTimeout(longPressTimer);
|
|
303
|
+
longPressTimer = null;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
function startColsCreate() {
|
|
307
|
+
if (!drag) return;
|
|
308
|
+
crStarted = true;
|
|
309
|
+
crAnchorMs = clampToDayBand(Math.floor(crAnchorMs / SNAP_MS) * SNAP_MS, crDayMs);
|
|
310
|
+
drag.beginCreate(new Date(crAnchorMs), new Date(crAnchorMs + SNAP_MS));
|
|
311
|
+
addTouchScrollBlock();
|
|
312
|
+
}
|
|
313
|
+
function onColsPointerDown(e) {
|
|
314
|
+
if (e.button !== 0 || !drag || !oneventcreate || readOnly) return;
|
|
315
|
+
if (e.target.closest(".tw-ev, .tw-ghost")) return;
|
|
316
|
+
const day = dayCols[pointerDayIndex(e.clientX)];
|
|
317
|
+
if (!day || day.isDisabled) return;
|
|
318
|
+
if (isBlockedAt(day.ms, Math.min(Math.max(pointerHour(e.clientY), startHour), endHour))) return;
|
|
319
|
+
crStartX = e.clientX;
|
|
320
|
+
crStartY = e.clientY;
|
|
321
|
+
crDayMs = day.ms;
|
|
322
|
+
crAnchorMs = clampToDayBand(
|
|
323
|
+
day.ms + Math.max(pointerHour(e.clientY), startHour) * HOUR_MS,
|
|
324
|
+
day.ms
|
|
325
|
+
);
|
|
326
|
+
crStarted = false;
|
|
327
|
+
if (e.pointerType === "touch") {
|
|
328
|
+
longPressTimer = setTimeout(() => {
|
|
329
|
+
longPressTimer = null;
|
|
330
|
+
startColsCreate();
|
|
331
|
+
}, LONG_PRESS_MS);
|
|
332
|
+
}
|
|
333
|
+
window.addEventListener("pointermove", onColsCreateMove);
|
|
334
|
+
window.addEventListener("pointerup", onColsCreateUp, { once: true });
|
|
335
|
+
window.addEventListener("pointercancel", onColsCreateCancel, { once: true });
|
|
336
|
+
}
|
|
337
|
+
function onColsCreateMove(e) {
|
|
338
|
+
if (!drag) return;
|
|
339
|
+
if (!crStarted) {
|
|
340
|
+
if (longPressTimer !== null) {
|
|
341
|
+
const moved = Math.hypot(e.clientX - crStartX, e.clientY - crStartY);
|
|
342
|
+
if (moved > LONG_PRESS_TOLERANCE) cleanupColsCreate();
|
|
167
343
|
return;
|
|
168
344
|
}
|
|
345
|
+
if (e.pointerType === "touch") return;
|
|
346
|
+
if (Math.abs(e.clientY - crStartY) < CREATE_THRESHOLD) return;
|
|
347
|
+
startColsCreate();
|
|
348
|
+
}
|
|
349
|
+
const raw = crDayMs + pointerHour(e.clientY) * HOUR_MS;
|
|
350
|
+
const snapped = clampToDayBand(Math.round(raw / SNAP_MS) * SNAP_MS, crDayMs);
|
|
351
|
+
drag.updatePointer(
|
|
352
|
+
new Date(Math.min(crAnchorMs, snapped)),
|
|
353
|
+
new Date(Math.max(crAnchorMs + SNAP_MS, snapped))
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
function cleanupColsCreate() {
|
|
357
|
+
clearLongPress();
|
|
358
|
+
removeTouchScrollBlock();
|
|
359
|
+
window.removeEventListener("pointermove", onColsCreateMove);
|
|
360
|
+
window.removeEventListener("pointerup", onColsCreateUp);
|
|
361
|
+
window.removeEventListener("pointercancel", onColsCreateCancel);
|
|
362
|
+
crStarted = false;
|
|
363
|
+
}
|
|
364
|
+
function onColsCreateUp() {
|
|
365
|
+
if (drag && crStarted) {
|
|
366
|
+
suppressColsClick = true;
|
|
367
|
+
commitDragCtx?.();
|
|
368
|
+
setTimeout(() => {
|
|
369
|
+
suppressColsClick = false;
|
|
370
|
+
}, 0);
|
|
169
371
|
}
|
|
372
|
+
cleanupColsCreate();
|
|
170
373
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
});
|
|
183
|
-
} else {
|
|
184
|
-
const bottomRemaining = el.scrollHeight - el.clientHeight - el.scrollTop;
|
|
185
|
-
if (bottomRemaining < EDGE_PX) {
|
|
186
|
-
bufferAfter += EXTEND_BY;
|
|
187
|
-
}
|
|
374
|
+
function onColsCreateCancel() {
|
|
375
|
+
if (drag && crStarted) drag.cancel();
|
|
376
|
+
cleanupColsCreate();
|
|
377
|
+
}
|
|
378
|
+
function onColsContextMenu(e) {
|
|
379
|
+
if (crStarted || longPressTimer !== null) e.preventDefault();
|
|
380
|
+
}
|
|
381
|
+
function handleColsClick(e) {
|
|
382
|
+
if (suppressColsClick) {
|
|
383
|
+
suppressColsClick = false;
|
|
384
|
+
return;
|
|
188
385
|
}
|
|
386
|
+
if (!oneventcreate || readOnly) return;
|
|
387
|
+
if (e.target.closest(".tw-ev, .tw-ghost")) return;
|
|
388
|
+
const day = dayCols[pointerDayIndex(e.clientX)];
|
|
389
|
+
if (!day || day.isDisabled) return;
|
|
390
|
+
const hour = Math.min(Math.max(pointerHour(e.clientY), startHour), endHour);
|
|
391
|
+
if (isBlockedAt(day.ms, hour)) return;
|
|
392
|
+
const startMs = clampToDayBand(
|
|
393
|
+
Math.floor((day.ms + hour * HOUR_MS) / SNAP_MS) * SNAP_MS,
|
|
394
|
+
day.ms
|
|
395
|
+
);
|
|
396
|
+
const durMin = minDuration ?? 60;
|
|
397
|
+
oneventcreate({ start: new Date(startMs), end: new Date(startMs + durMin * 6e4) });
|
|
189
398
|
}
|
|
190
|
-
|
|
191
|
-
const target = e.target;
|
|
192
|
-
if (target.closest(".wg-ev")) return;
|
|
193
|
-
if (readOnly || !oneventcreate) return;
|
|
194
|
-
if (disabledSet.has(ms)) return;
|
|
195
|
-
const durMin = minDuration ? Math.max(60, minDuration) : 60;
|
|
196
|
-
const start = new Date(ms + 9 * HOUR_MS);
|
|
197
|
-
const end = new Date(start.getTime() + durMin * 6e4);
|
|
198
|
-
oneventcreate({ start, end });
|
|
199
|
-
}
|
|
200
|
-
const DRAG_THRESHOLD = 8;
|
|
399
|
+
const DRAG_THRESHOLD = 5;
|
|
201
400
|
let evDragStartX = 0;
|
|
202
401
|
let evDragStartY = 0;
|
|
402
|
+
let evGrabOffsetMs = 0;
|
|
203
403
|
let evDragStarted = false;
|
|
204
|
-
let
|
|
205
|
-
let evDragId = $state(null);
|
|
404
|
+
let evDragMovable = false;
|
|
206
405
|
let evDragEvent = null;
|
|
207
|
-
const dragPreviewEvent = $derived.by(() => {
|
|
208
|
-
const payload = drag?.active && drag.mode === "move" ? drag.payload : null;
|
|
209
|
-
if (!payload?.eventId) return null;
|
|
210
|
-
const ev = events.find((event) => event.id === payload.eventId);
|
|
211
|
-
if (!ev) return null;
|
|
212
|
-
return { ...ev, start: payload.start, end: payload.end };
|
|
213
|
-
});
|
|
214
|
-
function isDraggedEvent(eventId) {
|
|
215
|
-
return dragPreviewEvent?.id === eventId;
|
|
216
|
-
}
|
|
217
|
-
function timedEventsForDay(day) {
|
|
218
|
-
if (!dragPreviewEvent) return day.events;
|
|
219
|
-
return day.events.filter((ev) => ev.id !== dragPreviewEvent.id);
|
|
220
|
-
}
|
|
221
|
-
const previewKeySnapshot = /* @__PURE__ */ new Map();
|
|
222
|
-
function dragPreviewTimedForDay(dayMs) {
|
|
223
|
-
const ev = dragPreviewEvent;
|
|
224
|
-
if (!ev || isAllDay(ev) || isMultiDay(ev)) return null;
|
|
225
|
-
const dayEnd = dayMs + DAY_MS;
|
|
226
|
-
const hit = ev.start.getTime() < dayEnd && ev.end.getTime() > dayMs;
|
|
227
|
-
if (hit) previewKeySnapshot.set("timed", ev.id);
|
|
228
|
-
return hit ? ev : null;
|
|
229
|
-
}
|
|
230
|
-
function dragPreviewSegmentForDay(dayMs) {
|
|
231
|
-
const ev = dragPreviewEvent;
|
|
232
|
-
if (!ev || !isAllDay(ev) && !isMultiDay(ev)) return null;
|
|
233
|
-
const seg = segmentForDay(ev, dayMs);
|
|
234
|
-
if (seg) previewKeySnapshot.set(dayMs, `${ev.id}:${seg.dayIndex}`);
|
|
235
|
-
return seg;
|
|
236
|
-
}
|
|
237
|
-
function getCellWidth() {
|
|
238
|
-
const cell = el?.querySelector(".wg-cell");
|
|
239
|
-
return cell ? cell.getBoundingClientRect().width : 100;
|
|
240
|
-
}
|
|
241
|
-
function getRowHeight() {
|
|
242
|
-
const row = el?.querySelector(".wg-week");
|
|
243
|
-
return row ? row.getBoundingClientRect().height + 24 : 200;
|
|
244
|
-
}
|
|
245
406
|
function onEventPointerDown(e, ev) {
|
|
246
|
-
if (e.button !== 0
|
|
407
|
+
if (e.button !== 0) return;
|
|
247
408
|
e.stopPropagation();
|
|
409
|
+
evDragMovable = !!drag && !readOnly && !ev.data?.readOnly;
|
|
248
410
|
evDragStartX = e.clientX;
|
|
249
411
|
evDragStartY = e.clientY;
|
|
412
|
+
evGrabOffsetMs = pointerTimeMs(e.clientX, e.clientY) - ev.start.getTime();
|
|
250
413
|
evDragStarted = false;
|
|
251
|
-
evDragId = ev.id;
|
|
252
414
|
evDragEvent = ev;
|
|
253
|
-
window.addEventListener("pointermove",
|
|
254
|
-
window.addEventListener("pointerup",
|
|
255
|
-
window.addEventListener("pointercancel",
|
|
415
|
+
window.addEventListener("pointermove", onEvMove);
|
|
416
|
+
window.addEventListener("pointerup", onEvUp, { once: true });
|
|
417
|
+
window.addEventListener("pointercancel", onEvCancel, { once: true });
|
|
256
418
|
}
|
|
257
|
-
function
|
|
419
|
+
function onEvMove(e) {
|
|
258
420
|
const ev = evDragEvent;
|
|
259
|
-
if (!drag || !ev ||
|
|
260
|
-
const dx = e.clientX - evDragStartX;
|
|
261
|
-
const dy = e.clientY - evDragStartY;
|
|
262
|
-
if (!evDragStarted && Math.abs(dx) + Math.abs(dy) < DRAG_THRESHOLD) return;
|
|
421
|
+
if (!drag || !ev || !evDragMovable) return;
|
|
263
422
|
if (!evDragStarted) {
|
|
423
|
+
const moved = Math.abs(e.clientX - evDragStartX) + Math.abs(e.clientY - evDragStartY);
|
|
424
|
+
if (moved < DRAG_THRESHOLD) return;
|
|
264
425
|
evDragStarted = true;
|
|
265
|
-
evDragging = true;
|
|
266
426
|
drag.beginMove(ev.id, ev.start, ev.end);
|
|
267
427
|
}
|
|
268
|
-
const
|
|
269
|
-
const
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
const deltaMs = (dayOffset + weekOffset * 7) * DAY_MS;
|
|
273
|
-
drag.updatePointer(
|
|
274
|
-
new Date(ev.start.getTime() + deltaMs),
|
|
275
|
-
new Date(ev.end.getTime() + deltaMs)
|
|
276
|
-
);
|
|
428
|
+
const duration = ev.end.getTime() - ev.start.getTime();
|
|
429
|
+
const raw = pointerTimeMs(e.clientX, e.clientY) - evGrabOffsetMs;
|
|
430
|
+
const snapped = Math.round(raw / SNAP_MS) * SNAP_MS;
|
|
431
|
+
drag.updatePointer(new Date(snapped), new Date(snapped + duration));
|
|
277
432
|
}
|
|
278
433
|
function cleanupEvDrag() {
|
|
279
|
-
window.removeEventListener("pointermove",
|
|
280
|
-
window.removeEventListener("pointerup",
|
|
281
|
-
window.removeEventListener("pointercancel",
|
|
434
|
+
window.removeEventListener("pointermove", onEvMove);
|
|
435
|
+
window.removeEventListener("pointerup", onEvUp);
|
|
436
|
+
window.removeEventListener("pointercancel", onEvCancel);
|
|
282
437
|
evDragStarted = false;
|
|
283
|
-
|
|
284
|
-
evDragId = null;
|
|
438
|
+
evDragMovable = false;
|
|
285
439
|
evDragEvent = null;
|
|
286
440
|
}
|
|
287
|
-
function
|
|
288
|
-
if (!
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
if (!evDragStarted) {
|
|
293
|
-
if (evDragEvent) oneventclick?.(evDragEvent);
|
|
294
|
-
} else {
|
|
441
|
+
function onEvUp() {
|
|
442
|
+
if (!evDragStarted && evDragEvent) {
|
|
443
|
+
oneventclick?.(evDragEvent);
|
|
444
|
+
} else if (evDragStarted && drag) {
|
|
445
|
+
suppressColsClick = true;
|
|
295
446
|
commitDragCtx?.();
|
|
447
|
+
setTimeout(() => {
|
|
448
|
+
suppressColsClick = false;
|
|
449
|
+
}, 0);
|
|
296
450
|
}
|
|
297
451
|
cleanupEvDrag();
|
|
298
452
|
}
|
|
299
|
-
function
|
|
453
|
+
function onEvCancel() {
|
|
300
454
|
if (drag && evDragStarted) drag.cancel();
|
|
301
455
|
cleanupEvDrag();
|
|
302
456
|
}
|
|
457
|
+
let rsStartY = 0;
|
|
458
|
+
let rsStarted = false;
|
|
459
|
+
let rsEdge = "end";
|
|
460
|
+
let rsEvent = null;
|
|
461
|
+
function onResizePointerDown(e, ev, edge) {
|
|
462
|
+
if (e.button !== 0 || !drag || readOnly || ev.data?.readOnly) return;
|
|
463
|
+
e.stopPropagation();
|
|
464
|
+
rsStartY = e.clientY;
|
|
465
|
+
rsStarted = false;
|
|
466
|
+
rsEdge = edge;
|
|
467
|
+
rsEvent = ev;
|
|
468
|
+
window.addEventListener("pointermove", onResizeMove);
|
|
469
|
+
window.addEventListener("pointerup", onResizeUp, { once: true });
|
|
470
|
+
window.addEventListener("pointercancel", onResizeCancel, { once: true });
|
|
471
|
+
}
|
|
472
|
+
function onResizeMove(e) {
|
|
473
|
+
const ev = rsEvent;
|
|
474
|
+
if (!drag || !ev) return;
|
|
475
|
+
if (!rsStarted) {
|
|
476
|
+
if (Math.abs(e.clientY - rsStartY) < CREATE_THRESHOLD) return;
|
|
477
|
+
rsStarted = true;
|
|
478
|
+
drag.beginResize(ev.id, rsEdge, ev.start, ev.end);
|
|
479
|
+
addTouchScrollBlock();
|
|
480
|
+
}
|
|
481
|
+
const evDayMs = sod(ev.start.getTime());
|
|
482
|
+
const raw = evDayMs + pointerHour(e.clientY) * HOUR_MS;
|
|
483
|
+
const snapped = clampToDayBand(Math.round(raw / SNAP_MS) * SNAP_MS, evDayMs);
|
|
484
|
+
if (rsEdge === "end") {
|
|
485
|
+
const end = Math.max(snapped, ev.start.getTime() + SNAP_MS);
|
|
486
|
+
drag.updatePointer(ev.start, new Date(end));
|
|
487
|
+
} else {
|
|
488
|
+
const start = Math.min(snapped, ev.end.getTime() - SNAP_MS);
|
|
489
|
+
drag.updatePointer(new Date(start), ev.end);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
function cleanupResize() {
|
|
493
|
+
removeTouchScrollBlock();
|
|
494
|
+
window.removeEventListener("pointermove", onResizeMove);
|
|
495
|
+
window.removeEventListener("pointerup", onResizeUp);
|
|
496
|
+
window.removeEventListener("pointercancel", onResizeCancel);
|
|
497
|
+
rsStarted = false;
|
|
498
|
+
rsEvent = null;
|
|
499
|
+
}
|
|
500
|
+
function onResizeUp() {
|
|
501
|
+
if (drag && rsStarted) {
|
|
502
|
+
suppressColsClick = true;
|
|
503
|
+
commitDragCtx?.();
|
|
504
|
+
setTimeout(() => {
|
|
505
|
+
suppressColsClick = false;
|
|
506
|
+
}, 0);
|
|
507
|
+
} else if (rsEvent && !rsStarted) {
|
|
508
|
+
oneventclick?.(rsEvent);
|
|
509
|
+
}
|
|
510
|
+
cleanupResize();
|
|
511
|
+
}
|
|
512
|
+
function onResizeCancel() {
|
|
513
|
+
if (drag && rsStarted) drag.cancel();
|
|
514
|
+
cleanupResize();
|
|
515
|
+
}
|
|
516
|
+
function onWindowKeydown(e) {
|
|
517
|
+
if (e.key !== "Escape" || !drag?.active) return;
|
|
518
|
+
drag.cancel();
|
|
519
|
+
cleanupColsCreate();
|
|
520
|
+
cleanupEvDrag();
|
|
521
|
+
cleanupResize();
|
|
522
|
+
suppressColsClick = true;
|
|
523
|
+
window.addEventListener(
|
|
524
|
+
"pointerup",
|
|
525
|
+
() => setTimeout(() => {
|
|
526
|
+
suppressColsClick = false;
|
|
527
|
+
}, 0),
|
|
528
|
+
{ once: true }
|
|
529
|
+
);
|
|
530
|
+
}
|
|
303
531
|
</script>
|
|
304
532
|
|
|
305
|
-
{#snippet
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
{
|
|
316
|
-
|
|
317
|
-
{
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
{
|
|
322
|
-
|
|
533
|
+
{#snippet allDayChip(seg: DaySegment)}
|
|
534
|
+
<button
|
|
535
|
+
type="button"
|
|
536
|
+
class="tw-ad"
|
|
537
|
+
class:tw-ad--start={seg.isStart}
|
|
538
|
+
class:tw-ad--end={seg.isEnd}
|
|
539
|
+
class:tw-ad--mid={!seg.isStart && !seg.isEnd}
|
|
540
|
+
class:tw-ad--selected={selectedEventId === seg.ev.id}
|
|
541
|
+
class:tw-ad--cancelled={seg.ev.status === 'cancelled'}
|
|
542
|
+
style:--ev-color={seg.ev.color ?? 'var(--dt-accent)'}
|
|
543
|
+
aria-label="{seg.ev.title}{seg.totalDays > 1 ? `, ${L.dayNOfTotal(seg.dayIndex, seg.totalDays)}` : `, ${L.allDay}`}{statusText(seg.ev)}"
|
|
544
|
+
onclick={() => oneventclick?.(seg.ev)}
|
|
545
|
+
onpointerenter={() => oneventhover?.(seg.ev)}
|
|
546
|
+
>
|
|
547
|
+
{#if !seg.isStart}
|
|
548
|
+
<span class="tw-ad-cont" aria-hidden="true">◂</span>
|
|
549
|
+
{/if}
|
|
550
|
+
<span class="tw-ad-title">{seg.ev.title}</span>
|
|
551
|
+
{#if seg.totalDays > 1}
|
|
552
|
+
<span class="tw-ad-span" aria-hidden="true">{seg.dayIndex}/{seg.totalDays}</span>
|
|
323
553
|
{/if}
|
|
324
|
-
|
|
554
|
+
{#if !seg.isEnd && seg.totalDays > 1}
|
|
555
|
+
<span class="tw-ad-arrow" aria-hidden="true">▸</span>
|
|
556
|
+
{/if}
|
|
557
|
+
</button>
|
|
325
558
|
{/snippet}
|
|
326
559
|
|
|
327
|
-
<
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
>
|
|
357
|
-
<!-- Day label in top-right corner -->
|
|
358
|
-
<div class="wg-cell-hd" class:wg-cell-hd--today={day.isToday}>
|
|
359
|
-
{#if showDates}
|
|
360
|
-
<span class="wg-day-num" class:wg-day-num--today={day.isToday}>
|
|
361
|
-
{day.dayNum}
|
|
362
|
-
</span>
|
|
363
|
-
{/if}
|
|
364
|
-
<span class="wg-day-wd">{weekdayShort(day.ms, locale)}</span>
|
|
560
|
+
<svelte:window onkeydown={onWindowKeydown} />
|
|
561
|
+
|
|
562
|
+
<div
|
|
563
|
+
class="tw"
|
|
564
|
+
class:tw--auto={autoHeight}
|
|
565
|
+
style={style || undefined}
|
|
566
|
+
style:height={autoHeight ? undefined : (height ? `${height}px` : '100%')}
|
|
567
|
+
role="region"
|
|
568
|
+
aria-label={L.weekAhead}
|
|
569
|
+
>
|
|
570
|
+
<div class="tw-scroll" bind:this={scrollEl}>
|
|
571
|
+
<div class="tw-inner" style:min-width="{innerMinWidth}px">
|
|
572
|
+
<!-- Sticky top: day headers + all-day strip -->
|
|
573
|
+
<div class="tw-top">
|
|
574
|
+
<div class="tw-head">
|
|
575
|
+
<div class="tw-corner" style:width="{GUTTER_W}px" aria-hidden="true"></div>
|
|
576
|
+
{#each dayCols as day (day.ms)}
|
|
577
|
+
<div
|
|
578
|
+
class="tw-hd"
|
|
579
|
+
class:tw-hd--today={day.isToday}
|
|
580
|
+
aria-current={day.isToday ? 'date' : undefined}
|
|
581
|
+
>
|
|
582
|
+
<span class="tw-hd-wd">{weekdayShort(day.ms, locale)}</span>
|
|
583
|
+
{#if showDates}
|
|
584
|
+
<span class="tw-hd-num" class:tw-hd-num--today={day.isToday}>{day.dayNum}</span>
|
|
585
|
+
{/if}
|
|
586
|
+
{#if dayHeaderSnippet}
|
|
587
|
+
<div class="tw-hd-custom">
|
|
588
|
+
{@render dayHeaderSnippet({ date: new Date(day.ms), isToday: day.isToday, dayName: weekdayShort(day.ms, locale) })}
|
|
365
589
|
</div>
|
|
590
|
+
{/if}
|
|
591
|
+
</div>
|
|
592
|
+
{/each}
|
|
593
|
+
</div>
|
|
366
594
|
|
|
367
|
-
|
|
368
|
-
|
|
595
|
+
{#if hasAllDayRow}
|
|
596
|
+
<div class="tw-allday">
|
|
597
|
+
<div class="tw-ad-gutter" style:width="{GUTTER_W}px">
|
|
598
|
+
<span class="tw-ad-gutter-lb">{L.allDay}</span>
|
|
599
|
+
</div>
|
|
600
|
+
{#each dayCols as day (day.ms)}
|
|
601
|
+
{@const segs = allDayByDay.get(day.ms) ?? []}
|
|
602
|
+
{@const isExpanded = adExpanded[day.ms] ?? false}
|
|
603
|
+
{@const shown = isExpanded ? segs : segs.slice(0, ALLDAY_MAX)}
|
|
604
|
+
<div class="tw-ad-cell" class:tw-ad-cell--today={day.isToday}>
|
|
605
|
+
{#each shown as seg (seg.ev.id)}
|
|
606
|
+
{@render allDayChip(seg)}
|
|
607
|
+
{/each}
|
|
608
|
+
{#if segs.length > ALLDAY_MAX}
|
|
609
|
+
<button
|
|
610
|
+
type="button"
|
|
611
|
+
class="tw-ad-more"
|
|
612
|
+
aria-expanded={isExpanded}
|
|
613
|
+
onclick={() => { adExpanded[day.ms] = !isExpanded; }}
|
|
614
|
+
>{isExpanded ? L.showLess : L.nMore(segs.length - ALLDAY_MAX)}</button>
|
|
369
615
|
{/if}
|
|
616
|
+
</div>
|
|
617
|
+
{/each}
|
|
618
|
+
</div>
|
|
619
|
+
{/if}
|
|
620
|
+
</div>
|
|
370
621
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
622
|
+
<!-- Grid body -->
|
|
623
|
+
<div class="tw-body" style:height="{gridHeight}px">
|
|
624
|
+
<!-- Time gutter -->
|
|
625
|
+
<div class="tw-gutter" style:width="{GUTTER_W}px" aria-hidden="true">
|
|
626
|
+
{#each { length: hourCount } as _, i}
|
|
627
|
+
{#if i > 0}
|
|
628
|
+
<span class="tw-gutter-lb" style:top="{i * HOUR_H}px">{fmtH(startHour + i, locale)}</span>
|
|
629
|
+
{/if}
|
|
630
|
+
{/each}
|
|
631
|
+
{#if nowY !== null && weekHasToday}
|
|
632
|
+
<span class="tw-gutter-now" style:top="{nowY}px"></span>
|
|
633
|
+
{/if}
|
|
634
|
+
</div>
|
|
377
635
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
636
|
+
<!-- Day columns -->
|
|
637
|
+
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
|
|
638
|
+
<div
|
|
639
|
+
class="tw-cols"
|
|
640
|
+
bind:this={colsEl}
|
|
641
|
+
onclick={handleColsClick}
|
|
642
|
+
onpointerdown={onColsPointerDown}
|
|
643
|
+
oncontextmenu={onColsContextMenu}
|
|
644
|
+
role="presentation"
|
|
645
|
+
>
|
|
646
|
+
<!-- Hour / half-hour guide lines (shared layer behind columns) -->
|
|
647
|
+
<div class="tw-lines" aria-hidden="true">
|
|
648
|
+
{#each { length: hourCount } as _, i}
|
|
649
|
+
<div class="tw-line" style:top="{i * HOUR_H}px"></div>
|
|
650
|
+
<div class="tw-line tw-line--half" style:top="{i * HOUR_H + HOUR_H / 2}px"></div>
|
|
651
|
+
{/each}
|
|
652
|
+
</div>
|
|
392
653
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
>
|
|
413
|
-
{@render allDaySegmentContent(seg)}
|
|
414
|
-
</div>
|
|
415
|
-
{/each}
|
|
416
|
-
{#if previewSegment}
|
|
417
|
-
<!-- key by event id + dayIndex: multi-day previews render one segment
|
|
418
|
-
per cell, and each pairs with its own real-card counterpart -->
|
|
654
|
+
{#each dayCols as day (day.ms)}
|
|
655
|
+
{@const positioned = layoutByDay.get(day.ms) ?? []}
|
|
656
|
+
{@const ghost = ghostForDay(day.ms)}
|
|
657
|
+
<div
|
|
658
|
+
class="tw-col"
|
|
659
|
+
class:tw-col--today={day.isToday}
|
|
660
|
+
class:tw-col--past={day.isPast}
|
|
661
|
+
class:tw-col--weekend={day.isWeekend}
|
|
662
|
+
class:tw-col--disabled={day.isDisabled}
|
|
663
|
+
data-day={day.ms}
|
|
664
|
+
>
|
|
665
|
+
<!-- Blocked slot overlays -->
|
|
666
|
+
{#if blockedSlots?.length}
|
|
667
|
+
{#each blockedSlots as slot}
|
|
668
|
+
{#if !slot.day || slot.day === day.isoDay}
|
|
669
|
+
{@const s = Math.max(slot.start, startHour)}
|
|
670
|
+
{@const e = Math.min(slot.end, endHour)}
|
|
671
|
+
{#if e > s}
|
|
672
|
+
{@const range = blockedRangeLabel(day.ms, slot.start, slot.end)}
|
|
419
673
|
<div
|
|
420
|
-
class="
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
aria-hidden="true"
|
|
426
|
-
in:previewReceive={{ key: previewKeySnapshot.get(day.ms) ?? '' }}
|
|
427
|
-
out:previewSend={{ key: previewKeySnapshot.get(day.ms) ?? '' }}
|
|
674
|
+
class="tw-blocked"
|
|
675
|
+
style:top="{(s - startHour) * HOUR_H}px"
|
|
676
|
+
style:height="{(e - s) * HOUR_H}px"
|
|
677
|
+
title="{slot.label ? `${slot.label}, ` : ''}{range}"
|
|
678
|
+
aria-label="{slot.label || 'Unavailable'}, {range}"
|
|
428
679
|
>
|
|
429
|
-
{
|
|
680
|
+
{#if slot.label}
|
|
681
|
+
<span class="tw-blocked-lb">{slot.label}</span>
|
|
682
|
+
{/if}
|
|
430
683
|
</div>
|
|
431
684
|
{/if}
|
|
432
|
-
|
|
433
|
-
{/
|
|
685
|
+
{/if}
|
|
686
|
+
{/each}
|
|
687
|
+
{/if}
|
|
434
688
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
<
|
|
466
|
-
class="
|
|
467
|
-
|
|
689
|
+
<!-- Timed events -->
|
|
690
|
+
{#each positioned as p (p.ev.id)}
|
|
691
|
+
{@const isCurrent = nowIds.has(p.ev.id)}
|
|
692
|
+
<div
|
|
693
|
+
class="tw-ev"
|
|
694
|
+
class:tw-ev--selected={selectedEventId === p.ev.id}
|
|
695
|
+
class:tw-ev--current={isCurrent}
|
|
696
|
+
class:tw-ev--resizing={p.isResizing}
|
|
697
|
+
class:tw-ev--readonly={p.ev.data?.readOnly}
|
|
698
|
+
class:tw-ev--cancelled={p.ev.status === 'cancelled'}
|
|
699
|
+
class:tw-ev--tentative={p.ev.status === 'tentative'}
|
|
700
|
+
class:tw-ev--full={p.ev.status === 'full'}
|
|
701
|
+
class:tw-ev--limited={p.ev.status === 'limited'}
|
|
702
|
+
class:tw-ev--short={p.height < 44}
|
|
703
|
+
class:tw-ev--compact={p.height < 34}
|
|
704
|
+
style:top="{p.top}px"
|
|
705
|
+
style:height="{p.height}px"
|
|
706
|
+
style:left="calc({(p.col / p.totalCols) * 100}% + 1px)"
|
|
707
|
+
style:width="calc({100 / p.totalCols}% - {p.totalCols > 1 ? 3 : 8}px)"
|
|
708
|
+
style:--ev-color={p.ev.color ?? 'var(--dt-accent)'}
|
|
709
|
+
role="button"
|
|
710
|
+
tabindex="0"
|
|
711
|
+
title={p.ev.title}
|
|
712
|
+
aria-label="{p.ev.title}, {fmtTime(p.ev.start, locale)} – {fmtTime(p.ev.end, locale)}, {fmtDuration(p.ev.start, p.ev.end)}{statusText(p.ev)}{isCurrent ? ` (${L.inProgress})` : ''}"
|
|
713
|
+
onpointerdown={(e) => onEventPointerDown(e, p.ev)}
|
|
714
|
+
onpointerenter={() => oneventhover?.(p.ev)}
|
|
715
|
+
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); oneventclick?.(p.ev); } }}
|
|
716
|
+
>
|
|
717
|
+
<div class="tw-ev-stripe" aria-hidden="true"></div>
|
|
718
|
+
<div class="tw-ev-body">
|
|
719
|
+
<EventContent event={p.ev}>
|
|
720
|
+
<span class="tw-ev-time">{fmtTime(p.ev.start, locale)} – {fmtTime(p.ev.end, locale)}</span>
|
|
721
|
+
<span class="tw-ev-title">{p.ev.title}</span>
|
|
722
|
+
{#if p.ev.location && p.height > 56}
|
|
723
|
+
<span class="tw-ev-loc">{p.ev.location}</span>
|
|
724
|
+
{/if}
|
|
725
|
+
</EventContent>
|
|
726
|
+
</div>
|
|
727
|
+
{#if isCurrent}
|
|
728
|
+
<span class="tw-ev-live" aria-hidden="true"></span>
|
|
729
|
+
{/if}
|
|
730
|
+
{#if !readOnly && !p.ev.data?.readOnly}
|
|
731
|
+
<span
|
|
732
|
+
class="tw-ev-handle tw-ev-handle--start"
|
|
733
|
+
aria-hidden="true"
|
|
734
|
+
onpointerdown={(e) => onResizePointerDown(e, p.ev, 'start')}
|
|
735
|
+
></span>
|
|
736
|
+
<span
|
|
737
|
+
class="tw-ev-handle tw-ev-handle--end"
|
|
468
738
|
aria-hidden="true"
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
>
|
|
472
|
-
{@render timedEventContent(previewTimedEvent)}
|
|
473
|
-
</div>
|
|
739
|
+
onpointerdown={(e) => onResizePointerDown(e, p.ev, 'end')}
|
|
740
|
+
></span>
|
|
474
741
|
{/if}
|
|
475
|
-
|
|
476
|
-
|
|
742
|
+
</div>
|
|
743
|
+
{/each}
|
|
744
|
+
|
|
745
|
+
<!-- Move / create ghost -->
|
|
746
|
+
{#if ghost}
|
|
747
|
+
<div
|
|
748
|
+
class="tw-ghost"
|
|
749
|
+
class:tw-ghost--create={ghost.create}
|
|
750
|
+
style:top="{ghost.top}px"
|
|
751
|
+
style:height="{ghost.height}px"
|
|
752
|
+
style:--ev-color={ghost.create
|
|
753
|
+
? 'var(--dt-accent, #2563eb)'
|
|
754
|
+
: (movingEvent?.color ?? 'var(--dt-accent, #2563eb)')}
|
|
755
|
+
aria-hidden="true"
|
|
756
|
+
>
|
|
757
|
+
{#if ghost.showTime}
|
|
758
|
+
<span class="tw-ghost-time">
|
|
759
|
+
{fmtTime(ghost.start, locale)} – {fmtTime(ghost.end, locale)}
|
|
760
|
+
</span>
|
|
761
|
+
{#if !ghost.create && movingEvent}
|
|
762
|
+
<span class="tw-ghost-title">{movingEvent.title}</span>
|
|
763
|
+
{/if}
|
|
477
764
|
{/if}
|
|
478
765
|
</div>
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
766
|
+
{/if}
|
|
767
|
+
|
|
768
|
+
<!-- Now line (today's column only) -->
|
|
769
|
+
{#if day.isToday && nowY !== null}
|
|
770
|
+
<div class="tw-now" style:top="{nowY}px" aria-label={L.currentTime}>
|
|
771
|
+
<span class="tw-now-dot" aria-hidden="true"></span>
|
|
772
|
+
</div>
|
|
773
|
+
{/if}
|
|
774
|
+
</div>
|
|
775
|
+
{/each}
|
|
482
776
|
</div>
|
|
483
777
|
</div>
|
|
484
|
-
|
|
778
|
+
</div>
|
|
485
779
|
</div>
|
|
486
780
|
|
|
781
|
+
<!-- Empty-week hint — overlaid, pointer events pass through to the grid -->
|
|
782
|
+
{#if weekIsEmpty && ctx.emptySnippet}
|
|
783
|
+
<div class="tw-empty">
|
|
784
|
+
{@render ctx.emptySnippet()}
|
|
785
|
+
</div>
|
|
786
|
+
{/if}
|
|
487
787
|
</div>
|
|
488
788
|
|
|
489
789
|
<style>
|
|
490
790
|
/* ─── Container ──────────────────────────────────── */
|
|
491
|
-
.
|
|
791
|
+
.tw {
|
|
492
792
|
position: relative;
|
|
493
|
-
overflow: hidden;
|
|
494
793
|
display: flex;
|
|
495
794
|
flex-direction: column;
|
|
795
|
+
overflow: hidden;
|
|
496
796
|
user-select: none;
|
|
497
797
|
font-variant-numeric: tabular-nums;
|
|
798
|
+
background: var(--dt-bg, #ffffff);
|
|
799
|
+
-webkit-tap-highlight-color: transparent;
|
|
498
800
|
}
|
|
499
|
-
.
|
|
801
|
+
.tw--auto { overflow: visible; }
|
|
500
802
|
|
|
501
|
-
/* ───
|
|
502
|
-
.
|
|
803
|
+
/* ─── Scroll container ───────────────────────────── */
|
|
804
|
+
.tw-scroll {
|
|
503
805
|
flex: 1;
|
|
806
|
+
min-height: 0;
|
|
504
807
|
overflow-y: auto;
|
|
505
|
-
overflow-x:
|
|
506
|
-
|
|
808
|
+
overflow-x: auto;
|
|
809
|
+
overscroll-behavior: contain;
|
|
507
810
|
scrollbar-width: thin;
|
|
508
811
|
scrollbar-color: var(--dt-scrollbar, rgba(0, 0, 0, 0.1)) transparent;
|
|
509
812
|
}
|
|
510
|
-
.
|
|
511
|
-
|
|
512
|
-
.
|
|
513
|
-
.wg-body::-webkit-scrollbar-thumb {
|
|
813
|
+
.tw--auto .tw-scroll { overflow-y: visible; }
|
|
814
|
+
.tw-scroll::-webkit-scrollbar { width: 5px; height: 5px; }
|
|
815
|
+
.tw-scroll::-webkit-scrollbar-thumb {
|
|
514
816
|
background: var(--dt-scrollbar, rgba(0, 0, 0, 0.1));
|
|
515
817
|
border-radius: 4px;
|
|
516
818
|
}
|
|
517
|
-
.
|
|
819
|
+
.tw-scroll::-webkit-scrollbar-track { background: transparent; }
|
|
518
820
|
|
|
519
|
-
|
|
520
|
-
.wg-week {
|
|
521
|
-
display: flex;
|
|
522
|
-
border-radius: 10px;
|
|
523
|
-
margin: 12px 8px;
|
|
524
|
-
border: 1.5px solid var(--dt-border, rgba(0, 0, 0, 0.08));
|
|
525
|
-
overflow: hidden;
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
.wg-week--current {
|
|
529
|
-
background: var(--dt-today-bg, rgba(37, 99, 235, 0.04));
|
|
530
|
-
border: 2.5px solid var(--dt-accent, #2563eb);
|
|
531
|
-
box-shadow: 0 0 0 1px color-mix(in srgb, var(--dt-accent, #2563eb) 15%, transparent);
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
/* ─── Week body ──────────────────────────────────── */
|
|
535
|
-
.wg-week-body {
|
|
536
|
-
flex: 1;
|
|
537
|
-
min-width: 0;
|
|
821
|
+
.tw-inner {
|
|
538
822
|
display: flex;
|
|
539
823
|
flex-direction: column;
|
|
824
|
+
width: 100%;
|
|
540
825
|
}
|
|
541
826
|
|
|
542
|
-
/* ───
|
|
543
|
-
.
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
flex: 1;
|
|
550
|
-
position: relative;
|
|
551
|
-
min-height: 170px;
|
|
552
|
-
padding: 4px 4px 8px;
|
|
553
|
-
border-right: 1px solid var(--dt-border, rgba(0, 0, 0, 0.08));
|
|
554
|
-
cursor: pointer;
|
|
555
|
-
transition: background 0.15s;
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
.wg-cell:last-child { border-right: none; }
|
|
559
|
-
.wg-cell:hover { background: var(--dt-hover, rgba(0, 0, 0, 0.015)); }
|
|
560
|
-
|
|
561
|
-
.wg-cell--today { background: var(--dt-today-bg, rgba(37, 99, 235, 0.04)); }
|
|
562
|
-
.wg-cell--today:hover { background: color-mix(in srgb, var(--dt-accent, #2563eb) 6%, transparent); }
|
|
563
|
-
|
|
564
|
-
/* Dim all cells by default; current week overrides to full brightness */
|
|
565
|
-
.wg-cell { opacity: 0.55; }
|
|
566
|
-
.wg-cell:hover { opacity: 0.75; }
|
|
567
|
-
.wg-week--current .wg-cell { opacity: 1; }
|
|
568
|
-
.wg-week--current .wg-cell--past { opacity: 0.8; }
|
|
569
|
-
.wg-week--current .wg-cell--past:hover { opacity: 0.9; }
|
|
570
|
-
|
|
571
|
-
/* equalDays: when no cells are marked past, all are full brightness */
|
|
572
|
-
|
|
573
|
-
.wg-cell--weekend { background: var(--dt-weekend-bg, rgba(0, 0, 0, 0.012)); }
|
|
574
|
-
|
|
575
|
-
/* ─── Disabled cell ──────────────────────────────── */
|
|
576
|
-
.wg-cell--disabled {
|
|
577
|
-
background: repeating-linear-gradient(
|
|
578
|
-
45deg,
|
|
579
|
-
transparent,
|
|
580
|
-
transparent 6px,
|
|
581
|
-
var(--dt-border, rgba(0, 0, 0, 0.08)) 6px,
|
|
582
|
-
var(--dt-border, rgba(0, 0, 0, 0.08)) 7px
|
|
583
|
-
) !important;
|
|
827
|
+
/* ─── Sticky top (header + all-day) ──────────────── */
|
|
828
|
+
.tw-top {
|
|
829
|
+
position: sticky;
|
|
830
|
+
top: 0;
|
|
831
|
+
z-index: 30;
|
|
832
|
+
background: var(--dt-bg, #ffffff);
|
|
833
|
+
border-bottom: 1px solid var(--dt-border, rgba(0, 0, 0, 0.08));
|
|
584
834
|
}
|
|
585
835
|
|
|
586
|
-
/* ───
|
|
587
|
-
.
|
|
836
|
+
/* ─── Day header row ─────────────────────────────── */
|
|
837
|
+
.tw-head {
|
|
588
838
|
display: flex;
|
|
589
|
-
align-items: center;
|
|
590
|
-
gap: 3px;
|
|
591
|
-
padding: 2px 4px;
|
|
592
|
-
border-radius: 3px;
|
|
593
|
-
background: repeating-linear-gradient(
|
|
594
|
-
-45deg,
|
|
595
|
-
color-mix(in srgb, var(--dt-text, rgba(0, 0, 0, 0.87)) 4%, transparent),
|
|
596
|
-
color-mix(in srgb, var(--dt-text, rgba(0, 0, 0, 0.87)) 4%, transparent) 3px,
|
|
597
|
-
transparent 3px,
|
|
598
|
-
transparent 6px
|
|
599
|
-
);
|
|
600
|
-
margin-bottom: 2px;
|
|
601
|
-
min-height: 14px;
|
|
602
839
|
}
|
|
603
840
|
|
|
604
|
-
.
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
/* ─── Custom day header ──────────────────────────── */
|
|
613
|
-
.wg-cell-custom-header {
|
|
614
|
-
padding: 0 4px 2px;
|
|
841
|
+
.tw-corner {
|
|
842
|
+
flex-shrink: 0;
|
|
843
|
+
position: sticky;
|
|
844
|
+
left: 0;
|
|
845
|
+
z-index: 2;
|
|
846
|
+
background: var(--dt-bg, #ffffff);
|
|
615
847
|
}
|
|
616
848
|
|
|
617
|
-
|
|
618
|
-
|
|
849
|
+
.tw-hd {
|
|
850
|
+
flex: 1 1 0;
|
|
851
|
+
min-width: 110px;
|
|
619
852
|
display: flex;
|
|
853
|
+
flex-direction: column;
|
|
620
854
|
align-items: center;
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
margin-bottom: 2px;
|
|
855
|
+
gap: 2px;
|
|
856
|
+
padding: 8px 4px 6px;
|
|
857
|
+
border-left: 1px solid var(--dt-border, rgba(0, 0, 0, 0.08));
|
|
625
858
|
}
|
|
626
859
|
|
|
627
|
-
.
|
|
628
|
-
font:
|
|
629
|
-
letter-spacing: 0.
|
|
860
|
+
.tw-hd-wd {
|
|
861
|
+
font: 500 10px/1 var(--dt-sans, system-ui, sans-serif);
|
|
862
|
+
letter-spacing: 0.06em;
|
|
630
863
|
text-transform: uppercase;
|
|
631
864
|
color: var(--dt-text-3, rgba(0, 0, 0, 0.38));
|
|
632
865
|
}
|
|
633
|
-
|
|
634
|
-
.wg-week--current .wg-day-wd {
|
|
635
|
-
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
.wg-cell-hd--today .wg-day-wd {
|
|
866
|
+
.tw-hd--today .tw-hd-wd {
|
|
639
867
|
color: var(--dt-accent, #2563eb);
|
|
640
868
|
font-weight: 600;
|
|
641
869
|
}
|
|
642
870
|
|
|
643
|
-
.
|
|
644
|
-
|
|
871
|
+
.tw-hd-num {
|
|
872
|
+
display: inline-flex;
|
|
873
|
+
align-items: center;
|
|
874
|
+
justify-content: center;
|
|
875
|
+
min-width: 26px;
|
|
876
|
+
height: 26px;
|
|
877
|
+
border-radius: 50%;
|
|
878
|
+
font: 600 14px/1 var(--dt-sans, system-ui, sans-serif);
|
|
645
879
|
color: var(--dt-text, rgba(0, 0, 0, 0.87));
|
|
646
880
|
}
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
color: var(--dt-
|
|
881
|
+
.tw-hd-num--today {
|
|
882
|
+
background: var(--dt-accent, #2563eb);
|
|
883
|
+
color: var(--dt-accent-fg, #ffffff);
|
|
884
|
+
font-weight: 700;
|
|
650
885
|
}
|
|
651
886
|
|
|
652
|
-
.
|
|
653
|
-
|
|
654
|
-
|
|
887
|
+
.tw-hd-custom {
|
|
888
|
+
max-width: 100%;
|
|
889
|
+
overflow: hidden;
|
|
655
890
|
}
|
|
656
891
|
|
|
657
|
-
/* ───
|
|
658
|
-
.
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
top: 4px;
|
|
662
|
-
writing-mode: vertical-rl;
|
|
663
|
-
transform: rotate(180deg);
|
|
664
|
-
font: 800 22px / 1 var(--dt-sans, system-ui, sans-serif);
|
|
665
|
-
letter-spacing: 0.02em;
|
|
666
|
-
text-transform: uppercase;
|
|
667
|
-
color: color-mix(in srgb, var(--dt-text, rgba(0, 0, 0, 0.87)) 4%, transparent);
|
|
668
|
-
pointer-events: none;
|
|
669
|
-
white-space: nowrap;
|
|
892
|
+
/* ─── All-day strip ──────────────────────────────── */
|
|
893
|
+
.tw-allday {
|
|
894
|
+
display: flex;
|
|
895
|
+
border-top: 1px solid var(--dt-border, rgba(0, 0, 0, 0.08));
|
|
670
896
|
}
|
|
671
897
|
|
|
672
|
-
.
|
|
673
|
-
|
|
898
|
+
.tw-ad-gutter {
|
|
899
|
+
flex-shrink: 0;
|
|
900
|
+
position: sticky;
|
|
901
|
+
left: 0;
|
|
902
|
+
z-index: 2;
|
|
903
|
+
background: var(--dt-bg, #ffffff);
|
|
904
|
+
display: flex;
|
|
905
|
+
align-items: flex-start;
|
|
906
|
+
justify-content: flex-end;
|
|
907
|
+
padding: 4px 6px 4px 0;
|
|
908
|
+
}
|
|
909
|
+
.tw-ad-gutter-lb {
|
|
910
|
+
font: 500 10px/1.2 var(--dt-sans, system-ui, sans-serif);
|
|
911
|
+
color: var(--dt-text-3, rgba(0, 0, 0, 0.38));
|
|
912
|
+
text-align: right;
|
|
674
913
|
}
|
|
675
914
|
|
|
676
|
-
|
|
677
|
-
|
|
915
|
+
.tw-ad-cell {
|
|
916
|
+
flex: 1 1 0;
|
|
917
|
+
min-width: 110px;
|
|
678
918
|
display: flex;
|
|
679
919
|
flex-direction: column;
|
|
680
920
|
gap: 2px;
|
|
681
|
-
|
|
921
|
+
padding: 3px 3px 4px;
|
|
922
|
+
border-left: 1px solid var(--dt-border, rgba(0, 0, 0, 0.08));
|
|
682
923
|
}
|
|
924
|
+
.tw-ad-cell--today { background: var(--dt-today-bg, rgba(37, 99, 235, 0.04)); }
|
|
683
925
|
|
|
684
|
-
.
|
|
926
|
+
.tw-ad {
|
|
927
|
+
appearance: none;
|
|
685
928
|
display: flex;
|
|
686
929
|
align-items: center;
|
|
687
930
|
gap: 3px;
|
|
688
|
-
padding: 2px
|
|
931
|
+
padding: 2px 6px;
|
|
932
|
+
min-height: 18px;
|
|
933
|
+
border: none;
|
|
689
934
|
border-radius: 3px;
|
|
690
935
|
background: color-mix(in srgb, var(--ev-color) 22%, var(--dt-surface, var(--dt-bg, #ffffff)));
|
|
936
|
+
border-left: 2.5px solid var(--ev-color);
|
|
691
937
|
cursor: pointer;
|
|
692
938
|
overflow: hidden;
|
|
939
|
+
text-align: left;
|
|
693
940
|
transition: background 0.12s;
|
|
694
|
-
|
|
941
|
+
-webkit-tap-highlight-color: transparent;
|
|
695
942
|
}
|
|
696
|
-
|
|
697
|
-
.wg-ad--drag-preview {
|
|
698
|
-
position: relative;
|
|
699
|
-
z-index: 8;
|
|
700
|
-
opacity: 0.95;
|
|
701
|
-
pointer-events: none;
|
|
702
|
-
box-shadow: 0 6px 18px color-mix(in srgb, var(--ev-color) 26%, rgba(0, 0, 0, 0.22));
|
|
703
|
-
outline: 1px solid color-mix(in srgb, var(--ev-color) 42%, transparent);
|
|
704
|
-
cursor: grabbing;
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
.wg-ad:hover {
|
|
943
|
+
.tw-ad:hover {
|
|
708
944
|
background: color-mix(in srgb, var(--ev-color) 32%, var(--dt-surface, var(--dt-bg, #ffffff)));
|
|
709
945
|
}
|
|
710
|
-
|
|
711
|
-
.
|
|
712
|
-
border-left: 2.5px solid var(--ev-color);
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
.wg-ad--mid {
|
|
716
|
-
border-radius: 0;
|
|
946
|
+
.tw-ad--mid,
|
|
947
|
+
.tw-ad--end:not(.tw-ad--start) {
|
|
717
948
|
border-left: 1px dashed color-mix(in srgb, var(--ev-color) 40%, transparent);
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
.wg-ad--end:not(.wg-ad--start) {
|
|
721
949
|
border-radius: 0 3px 3px 0;
|
|
722
|
-
border-left: 1px dashed color-mix(in srgb, var(--ev-color) 40%, transparent);
|
|
723
950
|
}
|
|
724
|
-
|
|
725
|
-
.wg-ad--selected {
|
|
951
|
+
.tw-ad--selected {
|
|
726
952
|
box-shadow: 0 0 0 1.5px var(--ev-color);
|
|
727
953
|
}
|
|
954
|
+
.tw-ad--cancelled .tw-ad-title {
|
|
955
|
+
text-decoration: line-through;
|
|
956
|
+
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
957
|
+
}
|
|
958
|
+
.tw-ad:focus-visible {
|
|
959
|
+
outline: none;
|
|
960
|
+
box-shadow: 0 0 0 2px var(--dt-accent, #2563eb);
|
|
961
|
+
}
|
|
728
962
|
|
|
729
|
-
.
|
|
730
|
-
font: 500
|
|
963
|
+
.tw-ad-title {
|
|
964
|
+
font: 500 11px/1.2 var(--dt-sans, system-ui, sans-serif);
|
|
731
965
|
color: var(--dt-text, rgba(0, 0, 0, 0.87));
|
|
732
966
|
white-space: nowrap;
|
|
733
967
|
overflow: hidden;
|
|
734
968
|
text-overflow: ellipsis;
|
|
735
969
|
flex: 1;
|
|
970
|
+
min-width: 0;
|
|
736
971
|
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
color: var(--ev-color);
|
|
972
|
+
.tw-ad-span {
|
|
973
|
+
font: 400 10px/1 var(--dt-sans, system-ui, sans-serif);
|
|
974
|
+
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
741
975
|
flex-shrink: 0;
|
|
742
|
-
line-height: 1;
|
|
743
976
|
}
|
|
744
|
-
|
|
745
|
-
.
|
|
746
|
-
font-size:
|
|
977
|
+
.tw-ad-cont,
|
|
978
|
+
.tw-ad-arrow {
|
|
979
|
+
font-size: 10px;
|
|
747
980
|
color: var(--ev-color);
|
|
748
981
|
flex-shrink: 0;
|
|
749
|
-
margin-left: auto;
|
|
750
982
|
line-height: 1;
|
|
751
983
|
}
|
|
984
|
+
.tw-ad-arrow { margin-left: auto; }
|
|
752
985
|
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
986
|
+
.tw-ad-more {
|
|
987
|
+
appearance: none;
|
|
988
|
+
background: none;
|
|
989
|
+
border: none;
|
|
990
|
+
border-radius: 3px;
|
|
991
|
+
text-align: left;
|
|
992
|
+
align-self: flex-start;
|
|
993
|
+
font: 500 10px/1 var(--dt-sans, system-ui, sans-serif);
|
|
994
|
+
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
995
|
+
padding: 2px 6px;
|
|
996
|
+
cursor: pointer;
|
|
997
|
+
-webkit-tap-highlight-color: transparent;
|
|
998
|
+
}
|
|
999
|
+
.tw-ad-more:hover { color: var(--dt-text, rgba(0, 0, 0, 0.87)); }
|
|
1000
|
+
.tw-ad-more:focus-visible {
|
|
1001
|
+
outline: none;
|
|
1002
|
+
box-shadow: 0 0 0 2px var(--dt-accent, #2563eb);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
/* ─── Grid body ──────────────────────────────────── */
|
|
1006
|
+
.tw-body {
|
|
756
1007
|
display: flex;
|
|
757
|
-
|
|
758
|
-
|
|
1008
|
+
position: relative;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
/* ─── Time gutter ────────────────────────────────── */
|
|
1012
|
+
.tw-gutter {
|
|
1013
|
+
flex-shrink: 0;
|
|
1014
|
+
position: sticky;
|
|
1015
|
+
left: 0;
|
|
1016
|
+
z-index: 20;
|
|
1017
|
+
background: var(--dt-bg, #ffffff);
|
|
1018
|
+
border-right: 1px solid var(--dt-border, rgba(0, 0, 0, 0.08));
|
|
759
1019
|
}
|
|
760
1020
|
|
|
761
|
-
.
|
|
1021
|
+
.tw-gutter-lb {
|
|
1022
|
+
position: absolute;
|
|
1023
|
+
right: 6px;
|
|
1024
|
+
transform: translateY(-50%);
|
|
1025
|
+
font: 500 11px/1 var(--dt-mono, ui-monospace, monospace);
|
|
1026
|
+
color: var(--dt-text-3, rgba(0, 0, 0, 0.38));
|
|
1027
|
+
white-space: nowrap;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
.tw-gutter-now {
|
|
1031
|
+
position: absolute;
|
|
1032
|
+
right: -3px;
|
|
1033
|
+
width: 6px;
|
|
1034
|
+
height: 6px;
|
|
1035
|
+
border-radius: 50%;
|
|
1036
|
+
background: var(--dt-accent, #2563eb);
|
|
1037
|
+
transform: translateY(-50%);
|
|
1038
|
+
z-index: 2;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
/* ─── Columns wrapper ────────────────────────────── */
|
|
1042
|
+
.tw-cols {
|
|
1043
|
+
flex: 1;
|
|
762
1044
|
display: flex;
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
gap: 3px 5px;
|
|
766
|
-
padding: 3px 6px;
|
|
767
|
-
border-radius: 4px;
|
|
768
|
-
background: color-mix(in srgb, var(--ev-color) 15%, var(--dt-surface, var(--dt-bg, #ffffff)));
|
|
769
|
-
cursor: pointer;
|
|
770
|
-
overflow: hidden;
|
|
771
|
-
transition: background 0.12s;
|
|
1045
|
+
position: relative;
|
|
1046
|
+
min-width: 0;
|
|
772
1047
|
}
|
|
773
1048
|
|
|
774
|
-
|
|
775
|
-
|
|
1049
|
+
/* ─── Guide lines ────────────────────────────────── */
|
|
1050
|
+
.tw-lines {
|
|
1051
|
+
position: absolute;
|
|
1052
|
+
inset: 0;
|
|
1053
|
+
pointer-events: none;
|
|
1054
|
+
/* Above the columns' background washes, below blocked/events/now */
|
|
1055
|
+
z-index: 1;
|
|
1056
|
+
}
|
|
1057
|
+
.tw-line {
|
|
1058
|
+
position: absolute;
|
|
1059
|
+
left: 0;
|
|
1060
|
+
right: 0;
|
|
1061
|
+
height: 1px;
|
|
1062
|
+
background: var(--dt-border, rgba(0, 0, 0, 0.08));
|
|
776
1063
|
}
|
|
1064
|
+
.tw-line--half { opacity: 0.4; }
|
|
777
1065
|
|
|
778
|
-
|
|
1066
|
+
/* ─── Day column ─────────────────────────────────── */
|
|
1067
|
+
.tw-col {
|
|
1068
|
+
flex: 1 1 0;
|
|
1069
|
+
min-width: 110px;
|
|
779
1070
|
position: relative;
|
|
780
|
-
|
|
781
|
-
|
|
1071
|
+
border-left: 1px solid var(--dt-border, rgba(0, 0, 0, 0.08));
|
|
1072
|
+
box-sizing: border-box;
|
|
1073
|
+
}
|
|
1074
|
+
/* The gutter's right border already bounds the first column */
|
|
1075
|
+
.tw-lines + .tw-col { border-left: none; }
|
|
1076
|
+
|
|
1077
|
+
.tw-col--today { background: var(--dt-today-bg, rgba(37, 99, 235, 0.04)); }
|
|
1078
|
+
/* Dim past days with a wash, never a subtree opacity (event contrast) */
|
|
1079
|
+
.tw-col--past {
|
|
1080
|
+
background: color-mix(in srgb, var(--dt-text, rgba(0, 0, 0, 0.87)) 2.5%, transparent);
|
|
1081
|
+
}
|
|
1082
|
+
.tw-col--weekend:not(.tw-col--today):not(.tw-col--past) {
|
|
1083
|
+
background: var(--dt-weekend-bg, rgba(0, 0, 0, 0.012));
|
|
1084
|
+
}
|
|
1085
|
+
.tw-col--disabled {
|
|
1086
|
+
background: repeating-linear-gradient(
|
|
1087
|
+
45deg,
|
|
1088
|
+
transparent,
|
|
1089
|
+
transparent 6px,
|
|
1090
|
+
var(--dt-border, rgba(0, 0, 0, 0.08)) 6px,
|
|
1091
|
+
var(--dt-border, rgba(0, 0, 0, 0.08)) 7px
|
|
1092
|
+
) !important;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
/* ─── Blocked slot overlay ───────────────────────── */
|
|
1096
|
+
.tw-blocked {
|
|
1097
|
+
position: absolute;
|
|
1098
|
+
left: 0;
|
|
1099
|
+
right: 0;
|
|
1100
|
+
z-index: 2;
|
|
1101
|
+
background: repeating-linear-gradient(
|
|
1102
|
+
-45deg,
|
|
1103
|
+
color-mix(in srgb, var(--dt-text, rgba(0, 0, 0, 0.87)) 4%, transparent),
|
|
1104
|
+
color-mix(in srgb, var(--dt-text, rgba(0, 0, 0, 0.87)) 4%, transparent) 4px,
|
|
1105
|
+
transparent 4px,
|
|
1106
|
+
transparent 8px
|
|
1107
|
+
);
|
|
782
1108
|
pointer-events: none;
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
1109
|
+
display: flex;
|
|
1110
|
+
align-items: flex-start;
|
|
1111
|
+
justify-content: center;
|
|
1112
|
+
overflow: hidden;
|
|
1113
|
+
}
|
|
1114
|
+
.tw-blocked-lb {
|
|
1115
|
+
font: 500 10px/1 var(--dt-sans, system-ui, sans-serif);
|
|
1116
|
+
color: var(--dt-text-3, rgba(0, 0, 0, 0.38));
|
|
1117
|
+
text-transform: uppercase;
|
|
1118
|
+
letter-spacing: 0.04em;
|
|
1119
|
+
white-space: nowrap;
|
|
1120
|
+
padding-top: 4px;
|
|
787
1121
|
}
|
|
788
1122
|
|
|
789
|
-
|
|
790
|
-
|
|
1123
|
+
/* ─── Now line ───────────────────────────────────── */
|
|
1124
|
+
.tw-now {
|
|
1125
|
+
position: absolute;
|
|
1126
|
+
left: 0;
|
|
1127
|
+
right: 0;
|
|
1128
|
+
height: 2px;
|
|
1129
|
+
background: var(--dt-accent, #2563eb);
|
|
1130
|
+
box-shadow: 0 0 6px var(--dt-glow, rgba(37, 99, 235, 0.25));
|
|
1131
|
+
z-index: 12;
|
|
1132
|
+
pointer-events: none;
|
|
1133
|
+
transform: translateY(-1px);
|
|
1134
|
+
}
|
|
1135
|
+
.tw-now-dot {
|
|
1136
|
+
position: absolute;
|
|
1137
|
+
left: -4px;
|
|
1138
|
+
top: -3px;
|
|
1139
|
+
width: 8px;
|
|
1140
|
+
height: 8px;
|
|
1141
|
+
border-radius: 50%;
|
|
1142
|
+
background: var(--dt-accent, #2563eb);
|
|
791
1143
|
}
|
|
792
1144
|
|
|
793
|
-
|
|
1145
|
+
/* ─── Events ─────────────────────────────────────── */
|
|
1146
|
+
.tw-ev {
|
|
1147
|
+
position: absolute;
|
|
1148
|
+
z-index: 6;
|
|
1149
|
+
border-radius: 5px;
|
|
1150
|
+
background: color-mix(in srgb, var(--ev-color) 14%, var(--dt-surface, var(--dt-bg, #ffffff)));
|
|
1151
|
+
display: flex;
|
|
1152
|
+
align-items: stretch;
|
|
1153
|
+
overflow: hidden;
|
|
1154
|
+
cursor: grab;
|
|
1155
|
+
/* Pointer drags move the event, never scroll the grid */
|
|
1156
|
+
touch-action: none;
|
|
1157
|
+
transition: box-shadow 120ms, background 120ms;
|
|
1158
|
+
box-sizing: border-box;
|
|
1159
|
+
min-height: 24px;
|
|
1160
|
+
-webkit-tap-highlight-color: transparent;
|
|
1161
|
+
}
|
|
1162
|
+
.tw-ev:hover {
|
|
1163
|
+
background: color-mix(in srgb, var(--ev-color) 24%, var(--dt-surface, var(--dt-bg, #ffffff)));
|
|
1164
|
+
z-index: 8;
|
|
1165
|
+
}
|
|
1166
|
+
/* Short blocks keep duration-proportional height, but get a 44px
|
|
1167
|
+
transparent hit-slop so clicks/taps still land. */
|
|
1168
|
+
.tw-ev--short { overflow: visible; }
|
|
1169
|
+
.tw-ev--short::after {
|
|
1170
|
+
content: '';
|
|
1171
|
+
position: absolute;
|
|
1172
|
+
left: 0;
|
|
1173
|
+
right: 0;
|
|
1174
|
+
top: 50%;
|
|
1175
|
+
transform: translateY(-50%);
|
|
1176
|
+
height: 44px;
|
|
1177
|
+
}
|
|
1178
|
+
.tw-ev--selected {
|
|
1179
|
+
box-shadow: 0 0 0 2px var(--ev-color),
|
|
1180
|
+
0 2px 12px color-mix(in srgb, var(--ev-color) 25%, transparent);
|
|
1181
|
+
z-index: 9;
|
|
1182
|
+
}
|
|
1183
|
+
.tw-ev--current {
|
|
794
1184
|
background: color-mix(in srgb, var(--ev-color) 22%, var(--dt-surface, var(--dt-bg, #ffffff)));
|
|
795
1185
|
}
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
1186
|
+
.tw-ev--resizing {
|
|
1187
|
+
z-index: 50;
|
|
1188
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25);
|
|
1189
|
+
cursor: ns-resize;
|
|
799
1190
|
}
|
|
800
|
-
|
|
801
|
-
|
|
1191
|
+
/* Status treatments: token-level dims + a non-opacity signal
|
|
1192
|
+
(strikethrough / border style) — consistent with the other views. */
|
|
1193
|
+
.tw-ev--cancelled {
|
|
1194
|
+
background: color-mix(in srgb, var(--ev-color) 5%, var(--dt-surface, var(--dt-bg, #ffffff)));
|
|
802
1195
|
}
|
|
803
|
-
.
|
|
804
|
-
|
|
805
|
-
|
|
1196
|
+
.tw-ev--cancelled .tw-ev-title {
|
|
1197
|
+
text-decoration: line-through;
|
|
1198
|
+
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
806
1199
|
}
|
|
807
|
-
.
|
|
808
|
-
|
|
1200
|
+
.tw-ev--cancelled .tw-ev-stripe { opacity: 0.45; /* decorative bar only */ }
|
|
1201
|
+
.tw-ev--tentative {
|
|
1202
|
+
background: color-mix(in srgb, var(--ev-color) 6%, var(--dt-surface, var(--dt-bg, #ffffff)));
|
|
1203
|
+
border: 1px dashed color-mix(in srgb, var(--ev-color) 45%, transparent);
|
|
809
1204
|
}
|
|
810
|
-
.
|
|
811
|
-
|
|
812
|
-
border: 1px
|
|
1205
|
+
.tw-ev--full {
|
|
1206
|
+
background: color-mix(in srgb, var(--ev-color) 6%, var(--dt-surface, var(--dt-bg, #ffffff)));
|
|
1207
|
+
border: 1px solid color-mix(in srgb, var(--ev-color) 30%, transparent);
|
|
813
1208
|
}
|
|
814
|
-
.
|
|
815
|
-
|
|
1209
|
+
.tw-ev--full .tw-ev-title { color: var(--dt-text-2, rgba(0, 0, 0, 0.54)); }
|
|
1210
|
+
.tw-ev--limited {
|
|
1211
|
+
background: color-mix(in srgb, var(--ev-color) 8%, var(--dt-surface, var(--dt-bg, #ffffff)));
|
|
1212
|
+
border: 1px dashed color-mix(in srgb, var(--ev-color) 45%, transparent);
|
|
816
1213
|
}
|
|
1214
|
+
.tw-ev--readonly { cursor: default; }
|
|
817
1215
|
|
|
818
|
-
.
|
|
819
|
-
|
|
820
|
-
|
|
1216
|
+
.tw-ev-stripe {
|
|
1217
|
+
width: 3px;
|
|
1218
|
+
background: var(--ev-color);
|
|
821
1219
|
flex-shrink: 0;
|
|
1220
|
+
border-radius: 5px 0 0 5px;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
.tw-ev-body {
|
|
1224
|
+
flex: 1;
|
|
1225
|
+
min-width: 0;
|
|
1226
|
+
padding: 3px 6px;
|
|
1227
|
+
display: flex;
|
|
1228
|
+
flex-direction: column;
|
|
1229
|
+
gap: 1px;
|
|
1230
|
+
overflow: hidden;
|
|
1231
|
+
}
|
|
1232
|
+
/* Compact (< ~35min at default zoom): single inline line "9:00 Title" */
|
|
1233
|
+
.tw-ev--compact .tw-ev-body {
|
|
1234
|
+
flex-direction: row;
|
|
1235
|
+
align-items: center;
|
|
1236
|
+
gap: 4px;
|
|
1237
|
+
padding-top: 1px;
|
|
1238
|
+
padding-bottom: 1px;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
.tw-ev-time {
|
|
1242
|
+
font: 400 11px/1.1 var(--dt-mono, ui-monospace, monospace);
|
|
1243
|
+
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
822
1244
|
white-space: nowrap;
|
|
1245
|
+
flex-shrink: 0;
|
|
823
1246
|
}
|
|
1247
|
+
.tw-ev--compact .tw-ev-time { order: 0; }
|
|
824
1248
|
|
|
825
|
-
.
|
|
826
|
-
font:
|
|
1249
|
+
.tw-ev-title {
|
|
1250
|
+
font: 600 12px/1.2 var(--dt-sans, system-ui, sans-serif);
|
|
827
1251
|
color: var(--dt-text, rgba(0, 0, 0, 0.87));
|
|
828
1252
|
white-space: nowrap;
|
|
829
1253
|
overflow: hidden;
|
|
830
1254
|
text-overflow: ellipsis;
|
|
831
1255
|
}
|
|
832
1256
|
|
|
833
|
-
.
|
|
834
|
-
font: 400
|
|
835
|
-
color: var(--dt-text-
|
|
1257
|
+
.tw-ev-loc {
|
|
1258
|
+
font: 400 10px/1.2 var(--dt-sans, system-ui, sans-serif);
|
|
1259
|
+
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
836
1260
|
white-space: nowrap;
|
|
837
1261
|
overflow: hidden;
|
|
838
1262
|
text-overflow: ellipsis;
|
|
839
|
-
flex-shrink: 1;
|
|
840
1263
|
}
|
|
841
1264
|
|
|
842
|
-
.
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
1265
|
+
.tw-ev-live {
|
|
1266
|
+
position: absolute;
|
|
1267
|
+
top: 4px;
|
|
1268
|
+
right: 4px;
|
|
1269
|
+
width: 6px;
|
|
1270
|
+
height: 6px;
|
|
1271
|
+
border-radius: 50%;
|
|
1272
|
+
background: var(--ev-color, var(--dt-accent));
|
|
1273
|
+
animation: tw-pulse 2s ease-in-out infinite;
|
|
1274
|
+
}
|
|
1275
|
+
@keyframes tw-pulse {
|
|
1276
|
+
0%, 100% { opacity: 1; }
|
|
1277
|
+
50% { opacity: 0.4; }
|
|
847
1278
|
}
|
|
848
1279
|
|
|
849
|
-
|
|
850
|
-
|
|
1280
|
+
/* ─── Resize handles ─────────────────────────────── */
|
|
1281
|
+
.tw-ev-handle {
|
|
1282
|
+
position: absolute;
|
|
1283
|
+
left: 0;
|
|
1284
|
+
right: 0;
|
|
1285
|
+
height: 8px;
|
|
1286
|
+
z-index: 2;
|
|
1287
|
+
cursor: ns-resize;
|
|
1288
|
+
touch-action: none;
|
|
1289
|
+
}
|
|
1290
|
+
.tw-ev-handle--start { top: 0; }
|
|
1291
|
+
.tw-ev-handle--end { bottom: 0; }
|
|
1292
|
+
/* Hit-slop: ≥20px effective, extending inward so overflow clipping
|
|
1293
|
+
can't cut it off. */
|
|
1294
|
+
.tw-ev-handle::before {
|
|
1295
|
+
content: '';
|
|
1296
|
+
position: absolute;
|
|
1297
|
+
left: 0;
|
|
1298
|
+
right: 0;
|
|
1299
|
+
height: 20px;
|
|
1300
|
+
}
|
|
1301
|
+
.tw-ev-handle--start::before { top: 0; }
|
|
1302
|
+
.tw-ev-handle--end::before { bottom: 0; }
|
|
1303
|
+
/* Short events: shrink the slop so a move-grab area survives */
|
|
1304
|
+
.tw-ev--short .tw-ev-handle::before { height: 12px; }
|
|
1305
|
+
.tw-ev-handle::after {
|
|
1306
|
+
content: '';
|
|
1307
|
+
position: absolute;
|
|
1308
|
+
left: 50%;
|
|
1309
|
+
transform: translateX(-50%);
|
|
1310
|
+
width: 20px;
|
|
1311
|
+
height: 3px;
|
|
1312
|
+
border-radius: 2px;
|
|
1313
|
+
background: var(--ev-color);
|
|
1314
|
+
opacity: 0;
|
|
1315
|
+
transition: opacity 120ms;
|
|
1316
|
+
}
|
|
1317
|
+
.tw-ev-handle--start::after { top: 1px; }
|
|
1318
|
+
.tw-ev-handle--end::after { bottom: 1px; }
|
|
1319
|
+
.tw-ev:hover .tw-ev-handle::after,
|
|
1320
|
+
.tw-ev:focus-within .tw-ev-handle::after,
|
|
1321
|
+
.tw-ev:focus-visible .tw-ev-handle::after,
|
|
1322
|
+
.tw-ev--resizing .tw-ev-handle::after,
|
|
1323
|
+
.tw-ev--selected .tw-ev-handle::after { opacity: 0.55; }
|
|
1324
|
+
/* Coarse pointers can't hover — show the grips persistently */
|
|
1325
|
+
@media (hover: none) {
|
|
1326
|
+
.tw-ev-handle::after { opacity: 0.55; }
|
|
851
1327
|
}
|
|
852
1328
|
|
|
853
|
-
/* ───
|
|
854
|
-
.
|
|
855
|
-
|
|
856
|
-
|
|
1329
|
+
/* ─── Move / create ghost ────────────────────────── */
|
|
1330
|
+
.tw-ghost {
|
|
1331
|
+
position: absolute;
|
|
1332
|
+
left: 1px;
|
|
1333
|
+
right: 3px;
|
|
1334
|
+
z-index: 40;
|
|
1335
|
+
border-radius: 5px;
|
|
1336
|
+
background: color-mix(in srgb, var(--ev-color) 22%, var(--dt-surface, var(--dt-bg, #ffffff)));
|
|
1337
|
+
outline: 1px solid color-mix(in srgb, var(--ev-color) 45%, transparent);
|
|
1338
|
+
box-shadow: 0 6px 18px color-mix(in srgb, var(--ev-color) 24%, rgba(0, 0, 0, 0.22));
|
|
1339
|
+
display: flex;
|
|
1340
|
+
flex-direction: column;
|
|
1341
|
+
gap: 1px;
|
|
1342
|
+
padding: 3px 6px;
|
|
1343
|
+
overflow: hidden;
|
|
1344
|
+
pointer-events: none;
|
|
1345
|
+
cursor: grabbing;
|
|
1346
|
+
box-sizing: border-box;
|
|
1347
|
+
}
|
|
1348
|
+
.tw-ghost--create {
|
|
1349
|
+
background: color-mix(in srgb, var(--ev-color) 12%, transparent);
|
|
1350
|
+
outline: 1px dashed color-mix(in srgb, var(--ev-color) 60%, transparent);
|
|
1351
|
+
box-shadow: none;
|
|
1352
|
+
}
|
|
1353
|
+
.tw-ghost-time {
|
|
1354
|
+
font: 600 11px/1.1 var(--dt-mono, ui-monospace, monospace);
|
|
1355
|
+
color: var(--ev-color, var(--dt-accent, #2563eb));
|
|
1356
|
+
white-space: nowrap;
|
|
1357
|
+
}
|
|
1358
|
+
.tw-ghost-title {
|
|
1359
|
+
font: 600 12px/1.2 var(--dt-sans, system-ui, sans-serif);
|
|
1360
|
+
color: var(--dt-text, rgba(0, 0, 0, 0.87));
|
|
1361
|
+
white-space: nowrap;
|
|
1362
|
+
overflow: hidden;
|
|
1363
|
+
text-overflow: ellipsis;
|
|
857
1364
|
}
|
|
858
1365
|
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
1366
|
+
/* ─── Empty overlay ──────────────────────────────── */
|
|
1367
|
+
.tw-empty {
|
|
1368
|
+
position: absolute;
|
|
1369
|
+
inset: 0;
|
|
1370
|
+
display: flex;
|
|
1371
|
+
align-items: center;
|
|
1372
|
+
justify-content: center;
|
|
1373
|
+
pointer-events: none;
|
|
1374
|
+
z-index: 4;
|
|
862
1375
|
}
|
|
863
1376
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
1377
|
+
/* ─── Focus-visible ──────────────────────────────── */
|
|
1378
|
+
/* box-shadow instead of outline: outlines get clipped by the
|
|
1379
|
+
overflow: hidden scroll container. */
|
|
1380
|
+
.tw-ev:focus-visible {
|
|
1381
|
+
outline: none;
|
|
1382
|
+
box-shadow: 0 0 0 2px var(--dt-accent, #2563eb);
|
|
1383
|
+
z-index: 9;
|
|
868
1384
|
}
|
|
869
|
-
</style>
|
|
870
1385
|
|
|
1386
|
+
/* ─── Reduced motion ─────────────────────────────── */
|
|
1387
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1388
|
+
.tw-ev,
|
|
1389
|
+
.tw-ad,
|
|
1390
|
+
.tw-ev-handle::after {
|
|
1391
|
+
transition: none;
|
|
1392
|
+
}
|
|
1393
|
+
.tw-ev-live { animation: none; }
|
|
1394
|
+
}
|
|
1395
|
+
</style>
|