@nomideusz/svelte-calendar 0.4.0 → 0.5.2
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 +331 -568
- package/dist/adapters/recurring.d.ts +45 -8
- package/dist/adapters/recurring.js +227 -49
- package/dist/adapters/rest.js +6 -1
- package/dist/calendar/Calendar.svelte +118 -30
- package/dist/calendar/Calendar.svelte.d.ts +9 -8
- package/dist/calendar/index.d.ts +0 -1
- package/dist/calendar/index.js +0 -1
- package/dist/core/index.d.ts +5 -4
- package/dist/core/index.js +2 -3
- package/dist/core/locale.d.ts +74 -0
- package/dist/core/locale.js +96 -4
- package/dist/core/time.d.ts +30 -0
- package/dist/core/time.js +42 -0
- package/dist/core/types.d.ts +1 -72
- package/dist/core/types.js +1 -8
- package/dist/engine/event-store.svelte.js +2 -2
- package/dist/engine/index.d.ts +1 -1
- package/dist/engine/view-state.svelte.d.ts +11 -6
- package/dist/engine/view-state.svelte.js +5 -4
- package/dist/index.d.ts +6 -7
- package/dist/index.js +4 -4
- package/dist/primitives/EmptySlot.svelte +6 -16
- package/dist/primitives/EventBlock.svelte +6 -19
- package/dist/primitives/NowIndicator.svelte +7 -4
- package/dist/primitives/TimeGutter.svelte +1 -1
- package/dist/theme/index.d.ts +1 -1
- package/dist/theme/index.js +1 -1
- package/dist/theme/presets.d.ts +11 -19
- package/dist/theme/presets.js +25 -111
- package/dist/views/agenda/Agenda.svelte +17 -1516
- package/dist/views/agenda/Agenda.svelte.d.ts +0 -14
- package/dist/views/agenda/AgendaDay.svelte +976 -0
- package/dist/views/agenda/AgendaDay.svelte.d.ts +14 -0
- package/dist/views/agenda/AgendaWeek.svelte +775 -0
- package/dist/views/agenda/AgendaWeek.svelte.d.ts +15 -0
- package/dist/views/agenda/index.d.ts +2 -0
- package/dist/views/agenda/index.js +2 -0
- package/dist/views/index.d.ts +2 -6
- package/dist/views/index.js +2 -6
- package/dist/views/planner/Planner.svelte +17 -0
- package/dist/views/planner/Planner.svelte.d.ts +7 -0
- package/dist/views/planner/PlannerDay.svelte +957 -0
- package/dist/views/{day/DayGrid.svelte.d.ts → planner/PlannerDay.svelte.d.ts} +6 -16
- package/dist/views/planner/PlannerWeek.svelte +840 -0
- package/dist/views/{week/WeekGrid.svelte.d.ts → planner/PlannerWeek.svelte.d.ts} +4 -9
- package/dist/views/planner/index.d.ts +1 -0
- package/dist/views/planner/index.js +1 -0
- package/dist/widget/CalendarWidget.svelte +54 -33
- package/dist/widget/CalendarWidget.svelte.d.ts +1 -1
- package/package.json +70 -65
- package/widget/svelte-calendar.css +1 -0
- package/widget/widget.js +122 -260
- package/dist/calendar/Toolbar.svelte +0 -244
- package/dist/calendar/Toolbar.svelte.d.ts +0 -21
- package/dist/views/day/DayGrid.svelte +0 -1104
- package/dist/views/day/DayTimeline.svelte +0 -533
- package/dist/views/day/DayTimeline.svelte.d.ts +0 -8
- package/dist/views/day/index.d.ts +0 -2
- package/dist/views/day/index.js +0 -2
- package/dist/views/schedule/WeekSchedule.svelte +0 -137
- package/dist/views/schedule/WeekSchedule.svelte.d.ts +0 -40
- package/dist/views/schedule/index.d.ts +0 -1
- package/dist/views/schedule/index.js +0 -2
- package/dist/views/settings/Settings.svelte +0 -585
- package/dist/views/settings/Settings.svelte.d.ts +0 -39
- package/dist/views/settings/index.d.ts +0 -2
- package/dist/views/settings/index.js +0 -1
- package/dist/views/week/WeekGrid.svelte +0 -632
- package/dist/views/week/WeekHeatmap.svelte +0 -461
- package/dist/views/week/WeekHeatmap.svelte.d.ts +0 -9
- package/dist/views/week/index.d.ts +0 -2
- package/dist/views/week/index.js +0 -2
|
@@ -0,0 +1,840 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Planner week mode — Multi-week vertical scroll.
|
|
3
|
+
|
|
4
|
+
Modelled after Hey Calendar's week view:
|
|
5
|
+
• Weeks stack vertically. Scroll up = past, down = future.
|
|
6
|
+
• Day headers in each week row: "MON 23", "TUE 24", with accent pill for today.
|
|
7
|
+
• Month label in left gutter, vertical bottom-to-top.
|
|
8
|
+
• Events are clean horizontal bars with colour fill, "9AM- 10AM Title" inline.
|
|
9
|
+
• Generous whitespace, thin dividers, minimal chrome.
|
|
10
|
+
-->
|
|
11
|
+
<script lang="ts">
|
|
12
|
+
import { getContext, onMount, tick, untrack } from 'svelte';
|
|
13
|
+
import { createClock } from '../../core/clock.svelte.js';
|
|
14
|
+
import type { TimelineEvent } from '../../core/types.js';
|
|
15
|
+
import type { DragState } from '../../engine/drag.svelte.js';
|
|
16
|
+
import type { ViewState } from '../../engine/view-state.svelte.js';
|
|
17
|
+
import type { EventStore } from '../../engine/event-store.svelte.js';
|
|
18
|
+
import { DAY_MS, HOUR_MS, sod } from '../../core/time.js';
|
|
19
|
+
import { startOfWeek as sowFn, fractionalHour, isAllDay, isMultiDay, segmentForDay } from '../../core/time.js';
|
|
20
|
+
import type { DaySegment } from '../../core/time.js';
|
|
21
|
+
import { weekdayShort, monthLong, fmtTime as _fmtTime, getLabels } from '../../core/locale.js';
|
|
22
|
+
|
|
23
|
+
const L = $derived(getLabels());
|
|
24
|
+
|
|
25
|
+
interface Props {
|
|
26
|
+
mondayStart?: boolean;
|
|
27
|
+
locale?: string;
|
|
28
|
+
height?: number | null;
|
|
29
|
+
events?: TimelineEvent[];
|
|
30
|
+
style?: string;
|
|
31
|
+
focusDate?: Date;
|
|
32
|
+
oneventclick?: (event: TimelineEvent) => void;
|
|
33
|
+
oneventcreate?: (range: { start: Date; end: Date }) => void;
|
|
34
|
+
selectedEventId?: string | null;
|
|
35
|
+
readOnly?: boolean;
|
|
36
|
+
[key: string]: unknown;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let {
|
|
40
|
+
mondayStart = true,
|
|
41
|
+
locale,
|
|
42
|
+
height = 520,
|
|
43
|
+
events = [],
|
|
44
|
+
style = '',
|
|
45
|
+
focusDate,
|
|
46
|
+
oneventclick,
|
|
47
|
+
oneventcreate,
|
|
48
|
+
selectedEventId = null,
|
|
49
|
+
readOnly = false,
|
|
50
|
+
}: Props = $props();
|
|
51
|
+
|
|
52
|
+
// ── Drag support (available when inside Calendar) ──
|
|
53
|
+
const drag = getContext<DragState>('calendar:drag') as DragState | undefined;
|
|
54
|
+
const commitDragCtx = getContext<() => void>('calendar:commitDrag') as (() => void) | undefined;
|
|
55
|
+
const viewState = getContext<ViewState>('calendar:viewState') as ViewState | undefined;
|
|
56
|
+
const storeCtx = getContext<{ current: EventStore }>('calendar:store') as { current: EventStore } | undefined;
|
|
57
|
+
|
|
58
|
+
const clock = createClock();
|
|
59
|
+
|
|
60
|
+
// ─── Infinite-scroll config ────────────────────────
|
|
61
|
+
const BUFFER_WEEKS = 6; // weeks rendered on each side of anchor
|
|
62
|
+
const EDGE_WEEKS = 2; // rebase when this close to an edge
|
|
63
|
+
const SHIFT_WEEKS = 3; // weeks to shift per rebase
|
|
64
|
+
const MAX_EVENTS_SHOWN = 5;
|
|
65
|
+
|
|
66
|
+
// Internal anchor drives layout; syncs bidirectionally with focusDate.
|
|
67
|
+
const _initMs = untrack(() => sod(focusDate?.getTime() ?? Date.now()));
|
|
68
|
+
let internalFocusMs = $state(_initMs);
|
|
69
|
+
let lastExternalMs = _initMs;
|
|
70
|
+
let rebasing = false;
|
|
71
|
+
|
|
72
|
+
let el: HTMLDivElement;
|
|
73
|
+
let scrolled = $state(false);
|
|
74
|
+
|
|
75
|
+
function scrollCurrentWeekIntoContainer(behavior: ScrollBehavior = 'auto') {
|
|
76
|
+
if (!el) return;
|
|
77
|
+
const current = el.querySelector<HTMLElement>('.wg-week--current');
|
|
78
|
+
if (!current) return;
|
|
79
|
+
const targetTop = current.offsetTop - (el.clientHeight - current.offsetHeight) / 2;
|
|
80
|
+
el.scrollTo({ top: Math.max(0, targetTop), behavior });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ─── Derived ────────────────────────────────────────
|
|
84
|
+
const todayMs = $derived(clock.today);
|
|
85
|
+
const anchorWeekStart = $derived(sowFn(internalFocusMs, mondayStart));
|
|
86
|
+
|
|
87
|
+
// ─── Preload events for entire visible range ────────
|
|
88
|
+
// Calendar only loads a single week by default. The planner shows
|
|
89
|
+
// BUFFER_WEEKS on each side, so we proactively load the full range
|
|
90
|
+
// so events for non-current weeks are visible immediately.
|
|
91
|
+
$effect(() => {
|
|
92
|
+
const store = storeCtx?.current;
|
|
93
|
+
if (!store) return;
|
|
94
|
+
const rangeStart = new Date(anchorWeekStart - BUFFER_WEEKS * 7 * DAY_MS);
|
|
95
|
+
const rangeEnd = new Date(anchorWeekStart + (BUFFER_WEEKS + 1) * 7 * DAY_MS);
|
|
96
|
+
store.load({ start: rangeStart, end: rangeEnd });
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// ─── Week data ──────────────────────────────────────
|
|
100
|
+
interface WeekRow {
|
|
101
|
+
weekStart: number;
|
|
102
|
+
isCurrent: boolean;
|
|
103
|
+
monthLabel: string | null;
|
|
104
|
+
days: DayCell[];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
interface DayCell {
|
|
108
|
+
ms: number;
|
|
109
|
+
dayNum: number;
|
|
110
|
+
isToday: boolean;
|
|
111
|
+
isPast: boolean;
|
|
112
|
+
isWeekend: boolean;
|
|
113
|
+
isFirstOfMonth: boolean;
|
|
114
|
+
monthLabel: string | null;
|
|
115
|
+
events: TimelineEvent[];
|
|
116
|
+
/** All-day or multi-day event segments for this day */
|
|
117
|
+
allDaySegments: DaySegment[];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const weeks = $derived.by(() => {
|
|
121
|
+
const result: WeekRow[] = [];
|
|
122
|
+
const currentWeekStart = sowFn(todayMs, mondayStart);
|
|
123
|
+
|
|
124
|
+
for (let w = -BUFFER_WEEKS; w <= BUFFER_WEEKS; w++) {
|
|
125
|
+
const weekStart = anchorWeekStart + w * 7 * DAY_MS;
|
|
126
|
+
const isCurrent = weekStart === currentWeekStart;
|
|
127
|
+
const days: DayCell[] = [];
|
|
128
|
+
|
|
129
|
+
const firstMonth = new Date(weekStart).getMonth();
|
|
130
|
+
const lastMonth = new Date(weekStart + 6 * DAY_MS).getMonth();
|
|
131
|
+
|
|
132
|
+
for (let d = 0; d < 7; d++) {
|
|
133
|
+
const ms = weekStart + d * DAY_MS;
|
|
134
|
+
const date = new Date(ms);
|
|
135
|
+
const dayNum = date.getDate();
|
|
136
|
+
const dow = date.getDay();
|
|
137
|
+
const isWeekend = dow === 0 || dow === 6;
|
|
138
|
+
const isToday = ms === todayMs;
|
|
139
|
+
const isPast = ms < todayMs;
|
|
140
|
+
const isFirstOfMonth = dayNum === 1;
|
|
141
|
+
const monthLabel = (d === 0 || isFirstOfMonth)
|
|
142
|
+
? monthLong(ms, locale).toUpperCase()
|
|
143
|
+
: null;
|
|
144
|
+
|
|
145
|
+
const dayEnd = ms + DAY_MS;
|
|
146
|
+
const dayEventsAll = events
|
|
147
|
+
.filter((ev) => ev.start.getTime() < dayEnd && ev.end.getTime() > ms)
|
|
148
|
+
.sort((a, b) => a.start.getTime() - b.start.getTime());
|
|
149
|
+
|
|
150
|
+
// Separate all-day / multi-day from timed events
|
|
151
|
+
const timedEvents: TimelineEvent[] = [];
|
|
152
|
+
const allDaySegments: DaySegment[] = [];
|
|
153
|
+
for (const ev of dayEventsAll) {
|
|
154
|
+
if (isAllDay(ev) || isMultiDay(ev)) {
|
|
155
|
+
const seg = segmentForDay(ev, ms);
|
|
156
|
+
if (seg) allDaySegments.push(seg);
|
|
157
|
+
} else {
|
|
158
|
+
timedEvents.push(ev);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
days.push({ ms, dayNum, isToday, isPast, isWeekend, isFirstOfMonth, monthLabel, events: timedEvents, allDaySegments });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Month label: show when first day of week is day 1-7
|
|
166
|
+
const startDate = new Date(weekStart);
|
|
167
|
+
const showMonth = startDate.getDate() <= 7;
|
|
168
|
+
const monthLabel = showMonth ? monthLong(weekStart, locale).toUpperCase() : null;
|
|
169
|
+
|
|
170
|
+
result.push({ weekStart, isCurrent, monthLabel, days });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return result;
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// ─── Format helpers ─────────────────────────────────
|
|
177
|
+
function fmtAmPm(d: Date): string {
|
|
178
|
+
return _fmtTime(d, locale);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function fmtNowTime(tick: number): string {
|
|
182
|
+
return _fmtTime(new Date(tick), locale);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ─── Now indicator fraction ─────────────────────────
|
|
186
|
+
const nowFrac = $derived(fractionalHour(clock.tick) / 24);
|
|
187
|
+
|
|
188
|
+
// ─── Scroll to current week on mount ────────────────
|
|
189
|
+
onMount(async () => {
|
|
190
|
+
await tick();
|
|
191
|
+
scrollCurrentWeekIntoContainer();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// ─── Infinite-scroll helpers ────────────────────────
|
|
195
|
+
|
|
196
|
+
/** Detect external focusDate changes (from nav arrows). */
|
|
197
|
+
$effect(() => {
|
|
198
|
+
const ext = focusDate ? sod(focusDate.getTime()) : clock.today;
|
|
199
|
+
if (ext !== lastExternalMs && !rebasing) {
|
|
200
|
+
lastExternalMs = ext;
|
|
201
|
+
internalFocusMs = ext;
|
|
202
|
+
tick().then(() => scrollCurrentWeekIntoContainer('smooth'));
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
/** Check if viewport is near top/bottom edge; if so, rebase the window. */
|
|
207
|
+
function checkVerticalEdges() {
|
|
208
|
+
if (!el || !viewState || rebasing) return;
|
|
209
|
+
const rows = el.querySelectorAll<HTMLElement>('[data-week]');
|
|
210
|
+
if (!rows.length) return;
|
|
211
|
+
|
|
212
|
+
const avgH = el.scrollHeight / rows.length;
|
|
213
|
+
const threshold = avgH * EDGE_WEEKS;
|
|
214
|
+
const maxScroll = el.scrollHeight - el.clientHeight;
|
|
215
|
+
|
|
216
|
+
if (el.scrollTop < threshold) {
|
|
217
|
+
rebaseWeeks(-1);
|
|
218
|
+
} else if (maxScroll > 0 && maxScroll - el.scrollTop < threshold) {
|
|
219
|
+
rebaseWeeks(1);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Shift the rendered week window by SHIFT_WEEKS in the given direction. */
|
|
224
|
+
function rebaseWeeks(direction: number) {
|
|
225
|
+
if (rebasing) return;
|
|
226
|
+
rebasing = true;
|
|
227
|
+
|
|
228
|
+
// Save an anchor: first visible week row and its visual offset.
|
|
229
|
+
const rows = el.querySelectorAll<HTMLElement>('[data-week]');
|
|
230
|
+
let anchorWeekMs = 0;
|
|
231
|
+
let anchorTop = 0;
|
|
232
|
+
for (const row of rows) {
|
|
233
|
+
const top = row.offsetTop - el.scrollTop;
|
|
234
|
+
if (top + row.offsetHeight > 0) {
|
|
235
|
+
anchorWeekMs = Number(row.dataset.week);
|
|
236
|
+
anchorTop = top;
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const shift = SHIFT_WEEKS * direction;
|
|
242
|
+
internalFocusMs += shift * 7 * DAY_MS;
|
|
243
|
+
lastExternalMs = internalFocusMs;
|
|
244
|
+
viewState?.setFocusDate(new Date(internalFocusMs));
|
|
245
|
+
|
|
246
|
+
tick().then(() => {
|
|
247
|
+
// Restore the anchor week to its previous visual position.
|
|
248
|
+
const anchor = el.querySelector<HTMLElement>(`[data-week="${anchorWeekMs}"]`);
|
|
249
|
+
if (anchor) {
|
|
250
|
+
el.scrollTop = anchor.offsetTop - anchorTop;
|
|
251
|
+
}
|
|
252
|
+
rebasing = false;
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Push the currently visible centre week to viewState. */
|
|
257
|
+
function syncFocusFromScroll() {
|
|
258
|
+
if (!el || !viewState || rebasing) return;
|
|
259
|
+
const centerY = el.scrollTop + el.clientHeight / 2;
|
|
260
|
+
const rows = el.querySelectorAll<HTMLElement>('[data-week]');
|
|
261
|
+
for (const row of rows) {
|
|
262
|
+
if (row.offsetTop + row.offsetHeight >= centerY) {
|
|
263
|
+
const ms = Number(row.dataset.week);
|
|
264
|
+
if (ms && ms !== lastExternalMs) {
|
|
265
|
+
lastExternalMs = ms;
|
|
266
|
+
viewState.setFocusDate(new Date(ms));
|
|
267
|
+
}
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function isCurrentWeekVisible(): boolean {
|
|
274
|
+
if (!el) return false;
|
|
275
|
+
const current = el.querySelector<HTMLElement>('.wg-week--current');
|
|
276
|
+
if (!current) return false;
|
|
277
|
+
const top = current.offsetTop - el.scrollTop;
|
|
278
|
+
const bottom = top + current.offsetHeight;
|
|
279
|
+
return bottom > 0 && top < el.clientHeight;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
let suppressScroll = false;
|
|
283
|
+
|
|
284
|
+
function handleUserScroll() {
|
|
285
|
+
if (suppressScroll) return;
|
|
286
|
+
scrolled = !isCurrentWeekVisible();
|
|
287
|
+
if (!rebasing) {
|
|
288
|
+
checkVerticalEdges();
|
|
289
|
+
syncFocusFromScroll();
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function jumpToday() {
|
|
294
|
+
internalFocusMs = clock.today;
|
|
295
|
+
lastExternalMs = clock.today;
|
|
296
|
+
viewState?.goToday();
|
|
297
|
+
suppressScroll = true;
|
|
298
|
+
scrolled = false;
|
|
299
|
+
tick().then(() => {
|
|
300
|
+
scrollCurrentWeekIntoContainer('smooth');
|
|
301
|
+
// Keep suppressed until smooth scroll finishes, then recheck
|
|
302
|
+
setTimeout(() => {
|
|
303
|
+
suppressScroll = false;
|
|
304
|
+
scrolled = !isCurrentWeekVisible();
|
|
305
|
+
}, 600);
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function handleDayCellClick(ms: number, e: Event) {
|
|
310
|
+
const target = e.target as HTMLElement;
|
|
311
|
+
if (target.closest('.wg-ev')) return;
|
|
312
|
+
if (readOnly || !oneventcreate) return;
|
|
313
|
+
const start = new Date(ms + 9 * HOUR_MS);
|
|
314
|
+
const end = new Date(ms + 10 * HOUR_MS);
|
|
315
|
+
oneventcreate({ start, end });
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ─── Event drag-to-move ───────────────────────────────────────
|
|
319
|
+
const DRAG_THRESHOLD = 8;
|
|
320
|
+
let evDragStartX = 0;
|
|
321
|
+
let evDragStartY = 0;
|
|
322
|
+
let evDragStarted = false;
|
|
323
|
+
let evDragging = $state(false);
|
|
324
|
+
let evDragId = $state<string | null>(null);
|
|
325
|
+
let evDragEvent: TimelineEvent | null = null;
|
|
326
|
+
|
|
327
|
+
function getCellWidth(): number {
|
|
328
|
+
const cell = el?.querySelector('.wg-cell');
|
|
329
|
+
return cell ? cell.getBoundingClientRect().width : 100;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function getRowHeight(): number {
|
|
333
|
+
const row = el?.querySelector('.wg-week');
|
|
334
|
+
return row ? row.getBoundingClientRect().height + 24 : 200; // 24 ≈ vertical margin between weeks
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function onEventPointerDown(e: PointerEvent, ev: TimelineEvent) {
|
|
338
|
+
if (e.button !== 0 || !drag || readOnly) return;
|
|
339
|
+
e.stopPropagation();
|
|
340
|
+
evDragStartX = e.clientX;
|
|
341
|
+
evDragStartY = e.clientY;
|
|
342
|
+
evDragStarted = false;
|
|
343
|
+
evDragId = ev.id;
|
|
344
|
+
evDragEvent = ev;
|
|
345
|
+
|
|
346
|
+
window.addEventListener('pointermove', onEvWindowPointerMove);
|
|
347
|
+
window.addEventListener('pointerup', onEvWindowPointerUp, { once: true });
|
|
348
|
+
window.addEventListener('pointercancel', onEvWindowPointerCancel, { once: true });
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function onEvWindowPointerMove(e: PointerEvent) {
|
|
352
|
+
const ev = evDragEvent;
|
|
353
|
+
if (!drag || !ev || evDragId !== ev.id) return;
|
|
354
|
+
const dx = e.clientX - evDragStartX;
|
|
355
|
+
const dy = e.clientY - evDragStartY;
|
|
356
|
+
if (!evDragStarted && Math.abs(dx) + Math.abs(dy) < DRAG_THRESHOLD) return;
|
|
357
|
+
|
|
358
|
+
if (!evDragStarted) {
|
|
359
|
+
evDragStarted = true;
|
|
360
|
+
evDragging = true;
|
|
361
|
+
drag.beginMove(ev.id, ev.start, ev.end);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const cellW = getCellWidth();
|
|
365
|
+
const rowH = getRowHeight();
|
|
366
|
+
const dayOffset = Math.round(dx / cellW);
|
|
367
|
+
const weekOffset = Math.round(dy / rowH);
|
|
368
|
+
const deltaMs = (dayOffset + weekOffset * 7) * DAY_MS;
|
|
369
|
+
drag.updatePointer(
|
|
370
|
+
new Date(ev.start.getTime() + deltaMs),
|
|
371
|
+
new Date(ev.end.getTime() + deltaMs),
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function cleanupEvDrag() {
|
|
376
|
+
window.removeEventListener('pointermove', onEvWindowPointerMove);
|
|
377
|
+
window.removeEventListener('pointerup', onEvWindowPointerUp);
|
|
378
|
+
window.removeEventListener('pointercancel', onEvWindowPointerCancel);
|
|
379
|
+
evDragStarted = false;
|
|
380
|
+
evDragging = false;
|
|
381
|
+
evDragId = null;
|
|
382
|
+
evDragEvent = null;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function onEvWindowPointerUp() {
|
|
386
|
+
if (!drag) { cleanupEvDrag(); return; }
|
|
387
|
+
if (!evDragStarted) {
|
|
388
|
+
if (evDragEvent) oneventclick?.(evDragEvent);
|
|
389
|
+
} else {
|
|
390
|
+
commitDragCtx?.();
|
|
391
|
+
}
|
|
392
|
+
cleanupEvDrag();
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function onEvWindowPointerCancel() {
|
|
396
|
+
if (drag && evDragStarted) drag.cancel();
|
|
397
|
+
cleanupEvDrag();
|
|
398
|
+
}
|
|
399
|
+
</script>
|
|
400
|
+
|
|
401
|
+
<div class="wg" style={style || undefined} style:height={height ? `${height}px` : '100%'}>
|
|
402
|
+
<div
|
|
403
|
+
class="wg-body"
|
|
404
|
+
bind:this={el}
|
|
405
|
+
onscroll={handleUserScroll}
|
|
406
|
+
role="grid"
|
|
407
|
+
aria-label={L.multiWeekGrid}
|
|
408
|
+
>
|
|
409
|
+
{#each weeks as week (week.weekStart)}
|
|
410
|
+
<div class="wg-week" class:wg-week--current={week.isCurrent} data-week={week.weekStart}>
|
|
411
|
+
<div class="wg-week-body">
|
|
412
|
+
<!-- Day columns (header inside each cell) -->
|
|
413
|
+
<div class="wg-days">
|
|
414
|
+
{#each week.days as day (day.ms)}
|
|
415
|
+
<div
|
|
416
|
+
class="wg-cell"
|
|
417
|
+
class:wg-cell--today={day.isToday}
|
|
418
|
+
class:wg-cell--past={day.isPast}
|
|
419
|
+
class:wg-cell--weekend={day.isWeekend}
|
|
420
|
+
role="gridcell"
|
|
421
|
+
tabindex="0"
|
|
422
|
+
aria-label="{new Date(day.ms).toLocaleDateString(locale ?? 'en-US', { weekday: 'long', month: 'short', day: 'numeric' })}{day.isToday ? ` (${L.today.toLowerCase()})` : ''}, {L.nEvents(day.events.length)}"
|
|
423
|
+
onclick={(e) => handleDayCellClick(day.ms, e)}
|
|
424
|
+
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleDayCellClick(day.ms, e); } }}
|
|
425
|
+
>
|
|
426
|
+
<!-- Day label in top-right corner -->
|
|
427
|
+
<div class="wg-cell-hd" class:wg-cell-hd--today={day.isToday}>
|
|
428
|
+
<span class="wg-day-num" class:wg-day-num--today={day.isToday}>
|
|
429
|
+
{day.dayNum}
|
|
430
|
+
</span>
|
|
431
|
+
<span class="wg-day-wd">{weekdayShort(day.ms, locale)}</span>
|
|
432
|
+
</div>
|
|
433
|
+
|
|
434
|
+
{#if day.monthLabel}
|
|
435
|
+
<span class="wg-cell-month">{day.monthLabel}</span>
|
|
436
|
+
{/if}
|
|
437
|
+
|
|
438
|
+
<!-- All-day / multi-day events -->
|
|
439
|
+
{#if day.allDaySegments.length > 0}
|
|
440
|
+
<div class="wg-allday">
|
|
441
|
+
{#each day.allDaySegments as seg (seg.ev.id)}
|
|
442
|
+
<div
|
|
443
|
+
class="wg-ad"
|
|
444
|
+
class:wg-ad--start={seg.isStart}
|
|
445
|
+
class:wg-ad--end={seg.isEnd}
|
|
446
|
+
class:wg-ad--mid={!seg.isStart && !seg.isEnd}
|
|
447
|
+
class:wg-ad--selected={selectedEventId === seg.ev.id}
|
|
448
|
+
style:--ev-color={seg.ev.color ?? 'var(--dt-accent)'}
|
|
449
|
+
role="button"
|
|
450
|
+
tabindex="0"
|
|
451
|
+
aria-label="{seg.ev.title}{seg.totalDays > 1 ? `, ${L.dayNOfTotal(seg.dayIndex, seg.totalDays)}` : `, ${L.allDay}`}"
|
|
452
|
+
onpointerdown={(e) => onEventPointerDown(e, seg.ev)}
|
|
453
|
+
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); oneventclick?.(seg.ev); } }}
|
|
454
|
+
>
|
|
455
|
+
{#if seg.isStart}
|
|
456
|
+
<span class="wg-ad-title">{seg.ev.title}</span>
|
|
457
|
+
{:else}
|
|
458
|
+
<span class="wg-ad-cont" aria-hidden="true">◂</span>
|
|
459
|
+
<span class="wg-ad-title">{seg.ev.title}</span>
|
|
460
|
+
{/if}
|
|
461
|
+
{#if !seg.isEnd && seg.totalDays > 1}
|
|
462
|
+
<span class="wg-ad-arrow" aria-hidden="true">▸</span>
|
|
463
|
+
{/if}
|
|
464
|
+
</div>
|
|
465
|
+
{/each}
|
|
466
|
+
</div>
|
|
467
|
+
{/if}
|
|
468
|
+
|
|
469
|
+
<!-- Timed events -->
|
|
470
|
+
<div class="wg-cell-events">
|
|
471
|
+
{#each day.events.slice(0, MAX_EVENTS_SHOWN) as ev (ev.id)}
|
|
472
|
+
<div
|
|
473
|
+
class="wg-ev"
|
|
474
|
+
class:wg-ev--selected={selectedEventId === ev.id}
|
|
475
|
+
class:wg-ev--current={ev.start.getTime() <= clock.tick && ev.end.getTime() > clock.tick}
|
|
476
|
+
class:wg-ev--dragging={evDragging && evDragId === ev.id}
|
|
477
|
+
style:--ev-color={ev.color ?? 'var(--dt-accent)'}
|
|
478
|
+
role="button"
|
|
479
|
+
tabindex="0"
|
|
480
|
+
aria-label="{ev.title}{ev.start.getTime() <= clock.tick && ev.end.getTime() > clock.tick ? ` (${L.inProgress})` : ''}"
|
|
481
|
+
onpointerdown={(e) => onEventPointerDown(e, ev)}
|
|
482
|
+
|
|
483
|
+
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); oneventclick?.(ev); } }}
|
|
484
|
+
>
|
|
485
|
+
<span class="wg-ev-time">{fmtAmPm(ev.start)}</span>
|
|
486
|
+
<span class="wg-ev-title">{ev.title}</span>
|
|
487
|
+
</div>
|
|
488
|
+
{/each}
|
|
489
|
+
{#if day.events.length > MAX_EVENTS_SHOWN}
|
|
490
|
+
<div class="wg-ev-more">{L.nMore(day.events.length - MAX_EVENTS_SHOWN)}</div>
|
|
491
|
+
{/if}
|
|
492
|
+
</div>
|
|
493
|
+
</div>
|
|
494
|
+
{/each}
|
|
495
|
+
</div>
|
|
496
|
+
</div>
|
|
497
|
+
</div>
|
|
498
|
+
{/each}
|
|
499
|
+
</div>
|
|
500
|
+
|
|
501
|
+
{#if scrolled}
|
|
502
|
+
<nav class="wg-nav" aria-label={L.weekNavigation}>
|
|
503
|
+
<button
|
|
504
|
+
class="wg-nav-pill"
|
|
505
|
+
onclick={jumpToday}
|
|
506
|
+
aria-label={L.goToToday}
|
|
507
|
+
>
|
|
508
|
+
{L.today}
|
|
509
|
+
</button>
|
|
510
|
+
</nav>
|
|
511
|
+
{/if}
|
|
512
|
+
</div>
|
|
513
|
+
|
|
514
|
+
<style>
|
|
515
|
+
/* ─── Container ──────────────────────────────────── */
|
|
516
|
+
.wg {
|
|
517
|
+
position: relative;
|
|
518
|
+
overflow: hidden;
|
|
519
|
+
display: flex;
|
|
520
|
+
flex-direction: column;
|
|
521
|
+
user-select: none;
|
|
522
|
+
font-variant-numeric: tabular-nums;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/* ─── Scrollable body ────────────────────────────── */
|
|
526
|
+
.wg-body {
|
|
527
|
+
flex: 1;
|
|
528
|
+
overflow-y: auto;
|
|
529
|
+
overflow-x: hidden;
|
|
530
|
+
scrollbar-width: thin;
|
|
531
|
+
scrollbar-color: var(--dt-scrollbar, rgba(0, 0, 0, 0.08)) transparent;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
.wg-body::-webkit-scrollbar { width: 4px; }
|
|
535
|
+
.wg-body::-webkit-scrollbar-thumb {
|
|
536
|
+
background: var(--dt-scrollbar, rgba(0, 0, 0, 0.1));
|
|
537
|
+
border-radius: 4px;
|
|
538
|
+
}
|
|
539
|
+
.wg-body::-webkit-scrollbar-track { background: transparent; }
|
|
540
|
+
|
|
541
|
+
/* ─── Week row ───────────────────────────────────── */
|
|
542
|
+
.wg-week {
|
|
543
|
+
display: flex;
|
|
544
|
+
border-radius: 10px;
|
|
545
|
+
margin: 12px 8px;
|
|
546
|
+
border: 1.5px solid var(--dt-border, rgba(0, 0, 0, 0.08));
|
|
547
|
+
overflow: hidden;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
.wg-week--current {
|
|
551
|
+
background: var(--dt-today-bg, rgba(239, 68, 68, 0.02));
|
|
552
|
+
border: 2.5px solid var(--dt-accent, #ef4444);
|
|
553
|
+
box-shadow: 0 0 0 1px color-mix(in srgb, var(--dt-accent, #ef4444) 15%, transparent);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/* ─── Week body ──────────────────────────────────── */
|
|
557
|
+
.wg-week-body {
|
|
558
|
+
flex: 1;
|
|
559
|
+
min-width: 0;
|
|
560
|
+
display: flex;
|
|
561
|
+
flex-direction: column;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/* ─── Day columns ────────────────────────────────── */
|
|
565
|
+
.wg-days {
|
|
566
|
+
display: flex;
|
|
567
|
+
flex: 1;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
.wg-cell {
|
|
571
|
+
flex: 1;
|
|
572
|
+
position: relative;
|
|
573
|
+
min-height: 170px;
|
|
574
|
+
padding: 4px 4px 8px;
|
|
575
|
+
border-right: 1px solid var(--dt-border, rgba(0, 0, 0, 0.06));
|
|
576
|
+
cursor: pointer;
|
|
577
|
+
transition: background 0.15s;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
.wg-cell:last-child { border-right: none; }
|
|
581
|
+
.wg-cell:hover { background: var(--dt-hover, rgba(0, 0, 0, 0.015)); }
|
|
582
|
+
|
|
583
|
+
.wg-cell--today { background: var(--dt-today-bg, rgba(239, 68, 68, 0.03)); }
|
|
584
|
+
.wg-cell--today:hover { background: rgba(239, 68, 68, 0.05); }
|
|
585
|
+
|
|
586
|
+
/* Dim all cells by default; current week overrides to full brightness */
|
|
587
|
+
.wg-cell { opacity: 0.55; }
|
|
588
|
+
.wg-cell:hover { opacity: 0.75; }
|
|
589
|
+
.wg-week--current .wg-cell { opacity: 1; }
|
|
590
|
+
.wg-week--current .wg-cell--past { opacity: 0.8; }
|
|
591
|
+
.wg-week--current .wg-cell--past:hover { opacity: 0.9; }
|
|
592
|
+
|
|
593
|
+
.wg-cell--weekend { background: var(--dt-weekend-bg, rgba(0, 0, 0, 0.012)); }
|
|
594
|
+
|
|
595
|
+
/* ─── Cell header (day label top-right) ──────────── */
|
|
596
|
+
.wg-cell-hd {
|
|
597
|
+
display: flex;
|
|
598
|
+
align-items: center;
|
|
599
|
+
justify-content: flex-end;
|
|
600
|
+
gap: 4px;
|
|
601
|
+
padding: 4px 5px 2px 0;
|
|
602
|
+
margin-bottom: 2px;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
.wg-day-wd {
|
|
606
|
+
font: 400 10px / 1 var(--dt-sans, system-ui, sans-serif);
|
|
607
|
+
letter-spacing: 0.04em;
|
|
608
|
+
text-transform: uppercase;
|
|
609
|
+
color: var(--dt-text-3, rgba(0, 0, 0, 0.3));
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
.wg-week--current .wg-day-wd {
|
|
613
|
+
color: var(--dt-text-2, rgba(0, 0, 0, 0.5));
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
.wg-cell-hd--today .wg-day-wd {
|
|
617
|
+
color: var(--dt-accent, #ef4444);
|
|
618
|
+
font-weight: 600;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
.wg-day-num {
|
|
622
|
+
font: 700 14px / 1 var(--dt-sans, system-ui, sans-serif);
|
|
623
|
+
color: var(--dt-text, rgba(0, 0, 0, 0.85));
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
.wg-week--current .wg-day-num {
|
|
627
|
+
color: var(--dt-text, rgba(0, 0, 0, 0.95));
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
.wg-day-num--today {
|
|
631
|
+
color: var(--dt-accent, #ef4444);
|
|
632
|
+
font-weight: 900;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/* ─── Month label (top-left, behind events) ─────── */
|
|
636
|
+
.wg-cell-month {
|
|
637
|
+
position: absolute;
|
|
638
|
+
left: 4px;
|
|
639
|
+
top: 4px;
|
|
640
|
+
writing-mode: vertical-rl;
|
|
641
|
+
transform: rotate(180deg);
|
|
642
|
+
font: 800 22px / 1 var(--dt-sans, system-ui, sans-serif);
|
|
643
|
+
letter-spacing: 0.02em;
|
|
644
|
+
text-transform: uppercase;
|
|
645
|
+
color: color-mix(in srgb, var(--dt-text, rgba(255,255,255,0.85)) 4%, transparent);
|
|
646
|
+
pointer-events: none;
|
|
647
|
+
white-space: nowrap;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
.wg-week--current .wg-cell-month {
|
|
651
|
+
color: color-mix(in srgb, var(--dt-text, rgba(255,255,255,0.85)) 8%, transparent);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/* ─── All-day / multi-day events ─────────────────── */
|
|
655
|
+
.wg-allday {
|
|
656
|
+
display: flex;
|
|
657
|
+
flex-direction: column;
|
|
658
|
+
gap: 2px;
|
|
659
|
+
margin-bottom: 4px;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
.wg-ad {
|
|
663
|
+
display: flex;
|
|
664
|
+
align-items: center;
|
|
665
|
+
gap: 3px;
|
|
666
|
+
padding: 2px 5px;
|
|
667
|
+
border-radius: 3px;
|
|
668
|
+
background: color-mix(in srgb, var(--ev-color) 22%, var(--dt-surface, #10141c));
|
|
669
|
+
cursor: pointer;
|
|
670
|
+
overflow: hidden;
|
|
671
|
+
transition: background 0.12s;
|
|
672
|
+
min-height: 18px;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
.wg-ad:hover {
|
|
676
|
+
background: color-mix(in srgb, var(--ev-color) 32%, var(--dt-surface, #10141c));
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
.wg-ad--start {
|
|
680
|
+
border-left: 2.5px solid var(--ev-color);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
.wg-ad--mid {
|
|
684
|
+
border-radius: 0;
|
|
685
|
+
border-left: 1px dashed color-mix(in srgb, var(--ev-color) 40%, transparent);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
.wg-ad--end:not(.wg-ad--start) {
|
|
689
|
+
border-radius: 0 3px 3px 0;
|
|
690
|
+
border-left: 1px dashed color-mix(in srgb, var(--ev-color) 40%, transparent);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
.wg-ad--selected {
|
|
694
|
+
box-shadow: 0 0 0 1.5px var(--ev-color);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
.wg-ad-title {
|
|
698
|
+
font: 500 10px / 1.1 var(--dt-sans, system-ui, sans-serif);
|
|
699
|
+
color: var(--dt-text, rgba(0, 0, 0, 0.85));
|
|
700
|
+
white-space: nowrap;
|
|
701
|
+
overflow: hidden;
|
|
702
|
+
text-overflow: ellipsis;
|
|
703
|
+
flex: 1;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
.wg-ad-cont {
|
|
707
|
+
font-size: 8px;
|
|
708
|
+
color: var(--ev-color);
|
|
709
|
+
flex-shrink: 0;
|
|
710
|
+
line-height: 1;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
.wg-ad-arrow {
|
|
714
|
+
font-size: 8px;
|
|
715
|
+
color: var(--ev-color);
|
|
716
|
+
flex-shrink: 0;
|
|
717
|
+
margin-left: auto;
|
|
718
|
+
line-height: 1;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/* ─── Events ─────────────────────────────────────── */
|
|
722
|
+
.wg-cell-events {
|
|
723
|
+
position: relative;
|
|
724
|
+
display: flex;
|
|
725
|
+
flex-direction: column;
|
|
726
|
+
gap: 3px;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
.wg-ev {
|
|
730
|
+
display: flex;
|
|
731
|
+
align-items: center;
|
|
732
|
+
flex-wrap: wrap;
|
|
733
|
+
gap: 3px 5px;
|
|
734
|
+
padding: 3px 6px;
|
|
735
|
+
border-radius: 4px;
|
|
736
|
+
background: color-mix(in srgb, var(--ev-color) 15%, var(--dt-surface, #10141c));
|
|
737
|
+
cursor: pointer;
|
|
738
|
+
overflow: hidden;
|
|
739
|
+
transition: background 0.12s;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
.wg-ev:hover {
|
|
743
|
+
background: color-mix(in srgb, var(--ev-color) 25%, var(--dt-surface, #10141c));
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
.wg-ev--selected {
|
|
747
|
+
box-shadow: 0 0 0 1.5px var(--ev-color);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
.wg-ev--current {
|
|
751
|
+
background: color-mix(in srgb, var(--ev-color) 22%, var(--dt-surface, #10141c));
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
.wg-ev-time {
|
|
755
|
+
font: 400 10px / 1 var(--dt-sans, system-ui, sans-serif);
|
|
756
|
+
color: var(--dt-text-3, rgba(0, 0, 0, 0.4));
|
|
757
|
+
flex-shrink: 0;
|
|
758
|
+
white-space: nowrap;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
.wg-ev-title {
|
|
762
|
+
font: 500 12px / 1.1 var(--dt-sans, system-ui, sans-serif);
|
|
763
|
+
color: var(--dt-text, rgba(0, 0, 0, 0.85));
|
|
764
|
+
white-space: nowrap;
|
|
765
|
+
overflow: hidden;
|
|
766
|
+
text-overflow: ellipsis;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
.wg-ev-more {
|
|
770
|
+
font: 500 10px / 1 var(--dt-sans, system-ui, sans-serif);
|
|
771
|
+
color: var(--dt-text-3, rgba(0, 0, 0, 0.35));
|
|
772
|
+
padding: 2px 8px;
|
|
773
|
+
cursor: pointer;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
.wg-ev-more:hover {
|
|
777
|
+
color: var(--dt-text-2, rgba(0, 0, 0, 0.55));
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/* ─── Navigation pill ────────────────────────────── */
|
|
781
|
+
.wg-nav {
|
|
782
|
+
position: absolute;
|
|
783
|
+
top: 22px;
|
|
784
|
+
right: 14px;
|
|
785
|
+
z-index: 20;
|
|
786
|
+
display: flex;
|
|
787
|
+
gap: 2px;
|
|
788
|
+
background: color-mix(in srgb, var(--dt-surface, #10141c) 85%, transparent);
|
|
789
|
+
backdrop-filter: blur(6px);
|
|
790
|
+
-webkit-backdrop-filter: blur(6px);
|
|
791
|
+
border-radius: 8px;
|
|
792
|
+
padding: 2px;
|
|
793
|
+
border: 1px solid var(--dt-border, rgba(148, 163, 184, 0.07));
|
|
794
|
+
animation: wg-nav-in 200ms ease both;
|
|
795
|
+
}
|
|
796
|
+
@keyframes wg-nav-in {
|
|
797
|
+
from { opacity: 0; transform: translateY(-6px); }
|
|
798
|
+
to { opacity: 1; transform: translateY(0); }
|
|
799
|
+
}
|
|
800
|
+
.wg-nav-pill {
|
|
801
|
+
border: none;
|
|
802
|
+
background: transparent;
|
|
803
|
+
color: var(--dt-text-2, rgba(148, 163, 184, 0.55));
|
|
804
|
+
cursor: pointer;
|
|
805
|
+
font: 600 11px / 1 var(--dt-sans, 'Outfit', system-ui, sans-serif);
|
|
806
|
+
padding: 6px 12px;
|
|
807
|
+
border-radius: 6px;
|
|
808
|
+
letter-spacing: 0.04em;
|
|
809
|
+
text-transform: uppercase;
|
|
810
|
+
transition: background 100ms, color 100ms;
|
|
811
|
+
display: inline-flex;
|
|
812
|
+
align-items: center;
|
|
813
|
+
justify-content: center;
|
|
814
|
+
}
|
|
815
|
+
.wg-nav-pill:hover {
|
|
816
|
+
color: var(--dt-text, rgba(226, 232, 240, 0.85));
|
|
817
|
+
}
|
|
818
|
+
.wg-nav-pill:focus-visible {
|
|
819
|
+
outline: 2px solid color-mix(in srgb, var(--dt-accent, #ef4444) 55%, transparent);
|
|
820
|
+
outline-offset: 2px;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/* ─── Focus-visible ──────────────────────────────── */
|
|
824
|
+
.wg-cell:focus-visible {
|
|
825
|
+
outline: 2px solid var(--dt-accent, #ef4444);
|
|
826
|
+
outline-offset: -2px;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
.wg-ev:focus-visible {
|
|
830
|
+
outline: 2px solid var(--ev-color, var(--dt-accent, #ef4444));
|
|
831
|
+
outline-offset: 1px;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
.wg-ev--dragging {
|
|
835
|
+
opacity: 0.6;
|
|
836
|
+
cursor: grabbing;
|
|
837
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
|
838
|
+
}
|
|
839
|
+
</style>
|
|
840
|
+
|