@nomideusz/svelte-calendar 0.20.1 → 1.0.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 +502 -267
- package/dist/adapters/composite.d.ts +12 -1
- package/dist/adapters/composite.js +109 -47
- package/dist/adapters/errors.d.ts +22 -0
- package/dist/adapters/errors.js +32 -0
- package/dist/adapters/index.d.ts +1 -0
- package/dist/adapters/index.js +1 -0
- package/dist/adapters/jmap.js +26 -17
- package/dist/adapters/mapped.d.ts +2 -2
- package/dist/adapters/mapped.js +18 -9
- package/dist/adapters/memory.d.ts +1 -1
- package/dist/adapters/memory.js +5 -3
- package/dist/adapters/recurring.d.ts +1 -1
- package/dist/adapters/recurring.js +43 -25
- package/dist/adapters/rest.d.ts +2 -2
- package/dist/adapters/rest.js +9 -4
- package/dist/calendar/Calendar.svelte +122 -78
- package/dist/calendar/Calendar.svelte.d.ts +69 -14
- package/dist/calendar/index.d.ts +2 -2
- package/dist/calendar/index.js +1 -1
- package/dist/core/clock.svelte.js +3 -2
- package/dist/core/locale.d.ts +11 -1
- package/dist/core/locale.js +16 -16
- package/dist/core/time.d.ts +9 -0
- package/dist/core/time.js +12 -5
- package/dist/core/timezone.d.ts +7 -3
- package/dist/core/timezone.js +10 -4
- package/dist/engine/event-store.svelte.js +94 -57
- package/dist/engine/view-state.svelte.d.ts +5 -1
- package/dist/engine/view-state.svelte.js +8 -6
- package/dist/headless/create-agenda.svelte.d.ts +8 -1
- package/dist/headless/create-agenda.svelte.js +36 -18
- package/dist/headless/create-calendar.svelte.js +57 -21
- package/dist/headless/create-range-agenda.svelte.d.ts +7 -0
- package/dist/headless/create-range-agenda.svelte.js +38 -17
- package/dist/headless/index.d.ts +1 -0
- package/dist/index.d.ts +10 -7
- package/dist/index.js +8 -5
- package/dist/primitives/DayHeader.svelte +3 -2
- package/dist/primitives/DayHeader.svelte.d.ts +1 -1
- package/dist/primitives/EmptySlot.svelte +5 -2
- package/dist/primitives/EventBlock.svelte +21 -14
- package/dist/primitives/FloatingPanel.svelte +110 -15
- package/dist/primitives/FloatingPanel.svelte.d.ts +7 -5
- package/dist/primitives/NowIndicator.svelte +11 -4
- package/dist/primitives/TimeGutter.svelte +10 -6
- package/dist/primitives/index.d.ts +1 -0
- package/dist/text-fit.d.ts +8 -2
- package/dist/views/agenda/AgendaDay.svelte +43 -35
- package/dist/views/agenda/AgendaWeek.svelte +75 -38
- package/dist/views/index.d.ts +2 -0
- package/dist/views/index.js +2 -0
- package/dist/views/mobile/MobileDay.svelte +99 -42
- package/dist/views/mobile/MobileWeek.svelte +31 -15
- package/dist/views/mobile/swipe.js +6 -1
- package/dist/views/month/MonthGrid.svelte +33 -21
- package/dist/views/month/MonthGrid.svelte.d.ts +1 -1
- package/dist/views/planner/PlannerScroll.svelte +438 -164
- package/dist/views/planner/PlannerWeek.svelte +150 -50
- package/dist/views/shared/context.svelte.d.ts +1 -1
- package/package.json +24 -9
- package/widget/widget.js +10000 -9520
- package/dist/assets/favicon.svg +0 -9
- package/dist/widget/CalendarWidget.svelte +0 -114
- package/dist/widget/CalendarWidget.svelte.d.ts +0 -37
- package/dist/widget/index.d.ts +0 -1
- package/dist/widget/index.js +0 -1
- package/dist/widget/widget.d.ts +0 -10
- package/dist/widget/widget.js +0 -96
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
Modelled after Hey Calendar's week view:
|
|
8
8
|
• Weeks stack vertically. Scroll up = past, down = future.
|
|
9
9
|
• Day headers in each week row: "MON 23", "TUE 24", with accent pill for today.
|
|
10
|
-
• Month label in left gutter, vertical bottom-to-top
|
|
11
|
-
|
|
10
|
+
• Month label in left gutter, vertical bottom-to-top; a month that begins
|
|
11
|
+
mid-week is labelled, with its year, in the column where it begins.
|
|
12
|
+
• Events are clean horizontal bars with colour fill, "9AM- 10AM Title" inline,
|
|
13
|
+
each at the height of its start time in the day.
|
|
12
14
|
• Generous whitespace, thin dividers, minimal chrome.
|
|
13
15
|
-->
|
|
14
16
|
<script lang="ts">import { onMount, tick, untrack } from "svelte";
|
|
@@ -18,13 +20,14 @@ import { prefersReducedMotion } from "svelte/motion";
|
|
|
18
20
|
import { useCalendarContext } from "../shared/context.svelte.js";
|
|
19
21
|
import EventContent from "../shared/EventContent.svelte";
|
|
20
22
|
import { createClock } from "../../core/clock.svelte.js";
|
|
21
|
-
import {
|
|
22
|
-
import { startOfWeek as sowFn, isAllDay,
|
|
23
|
-
import { weekdayShort, monthLong, fmtTime as _fmtTime
|
|
23
|
+
import { HOUR_MS, sod, addDaysMs, diffDays } from "../../core/time.js";
|
|
24
|
+
import { startOfWeek as sowFn, isAllDay, segmentForDay } from "../../core/time.js";
|
|
25
|
+
import { weekdayShort, monthLong, fmtTime as _fmtTime } from "../../core/locale.js";
|
|
24
26
|
import { createChipFit } from "../shared/chip-fit.svelte.js";
|
|
25
|
-
const L = $derived(getLabels());
|
|
26
27
|
let { mondayStart = true, locale, height = 520, events = [], style = "", focusDate, oneventclick, oneventcreate, onexternaldrop, selectedEventId = null, readOnly = false, visibleHours } = $props();
|
|
27
28
|
const ctx = useCalendarContext();
|
|
29
|
+
/** Per-instance labels (Calendar's `labels` prop over the globals). */
|
|
30
|
+
const L = $derived(ctx.labels);
|
|
28
31
|
const clock = createClock(ctx.timezone);
|
|
29
32
|
// Drag ghost flies between day cells instead of teleporting.
|
|
30
33
|
// No fallback: without a counterpart (drag start/end) it appears/disappears instantly.
|
|
@@ -61,12 +64,84 @@ const MAX_EVENTS_SHOWN = 5;
|
|
|
61
64
|
// mount re-centres once the events have landed.
|
|
62
65
|
const CHIP_H = 22;
|
|
63
66
|
const CHIP_GAP = 3;
|
|
64
|
-
|
|
67
|
+
/** The day's hours at their floor height — a quiet week is short, a busy
|
|
68
|
+
* day grows its row. Chips sit on this axis by start time. */
|
|
69
|
+
const TIME_H = 120;
|
|
65
70
|
const ROW_MARGIN = 12;
|
|
66
71
|
/** Distance between two rows' tops; only a fallback now. */
|
|
67
72
|
function rowPitch() {
|
|
68
73
|
const rows = el?.querySelectorAll("[data-week]");
|
|
69
|
-
return rows && rows.length > 1 ? rows[1].offsetTop - rows[0].offsetTop :
|
|
74
|
+
return rows && rows.length > 1 ? rows[1].offsetTop - rows[0].offsetTop : TIME_H + ROW_MARGIN;
|
|
75
|
+
}
|
|
76
|
+
// ─── Wall-clock helpers ─────────────────────────
|
|
77
|
+
// The axis is wall-clock hours. A DST day is 23 or 25 hours long, so
|
|
78
|
+
// `dayMs + h * HOUR_MS` is an hour off after the change: convert through
|
|
79
|
+
// the local calendar instead.
|
|
80
|
+
/** Fractional wall-clock hour `hour` on the day starting at `dayMs` → epoch ms. */
|
|
81
|
+
function atHour(dayMs, hour) {
|
|
82
|
+
// MakeTime sums the fields in local time, so this may pass 24h.
|
|
83
|
+
return new Date(dayMs).setHours(0, 0, 0, Math.round(hour * HOUR_MS));
|
|
84
|
+
}
|
|
85
|
+
/** Epoch ms → fractional wall-clock hour counted from the day at `dayMs`. */
|
|
86
|
+
function hourOf(ms, dayMs) {
|
|
87
|
+
const d = new Date(ms);
|
|
88
|
+
return diffDays(ms, dayMs) * 24 + d.getHours() + d.getMinutes() / 60 + d.getSeconds() / 3600 + d.getMilliseconds() / HOUR_MS;
|
|
89
|
+
}
|
|
90
|
+
/** All-day events and timed ones a (wall-clock) day or longer are bars;
|
|
91
|
+
* a shorter overnight event is a chip on the day it starts. */
|
|
92
|
+
function inAllDayStrip(ev) {
|
|
93
|
+
return isAllDay(ev) || ev.end.getTime() >= addDaysMs(ev.start.getTime(), 1);
|
|
94
|
+
}
|
|
95
|
+
function statusText(ev) {
|
|
96
|
+
if (ev.status === "cancelled") return ` (${L.cancelled})`;
|
|
97
|
+
if (ev.status === "tentative") return ` (${L.tentative})`;
|
|
98
|
+
if (ev.status === "full") return ` (${L.full})`;
|
|
99
|
+
if (ev.status === "limited") return ` (${L.limited})`;
|
|
100
|
+
return "";
|
|
101
|
+
}
|
|
102
|
+
// ─── Time axis ──────────────────────────────────
|
|
103
|
+
// Without visibleHours the axis spans the hours the loaded events start in:
|
|
104
|
+
// a timetable of 7:00–21:00 classes spread over 24 hours bunches them up.
|
|
105
|
+
const eventHours = $derived.by(() => {
|
|
106
|
+
let lo = 24;
|
|
107
|
+
let hi = -1;
|
|
108
|
+
for (const ev of events) {
|
|
109
|
+
if (inAllDayStrip(ev)) continue;
|
|
110
|
+
const h = ev.start.getHours() + ev.start.getMinutes() / 60;
|
|
111
|
+
lo = Math.min(lo, h);
|
|
112
|
+
hi = Math.max(hi, h);
|
|
113
|
+
}
|
|
114
|
+
return hi < 0 ? [0, 24] : [Math.floor(lo), Math.min(24, Math.floor(hi) + 1)];
|
|
115
|
+
});
|
|
116
|
+
const startHour = $derived(visibleHours?.[0] ?? eventHours[0]);
|
|
117
|
+
const endHour = $derived(visibleHours?.[1] ?? eventHours[1]);
|
|
118
|
+
/** Where a chip starting this many wall-clock hours into the day sits, px
|
|
119
|
+
* from the axis top. A chip at the last hour still ends inside the floor height. */
|
|
120
|
+
function timeTop(hoursIntoDay) {
|
|
121
|
+
const f = (hoursIntoDay - startHour) / Math.max(1, endHour - startHour);
|
|
122
|
+
return Math.min(Math.max(f, 0), 1) * (TIME_H - CHIP_H);
|
|
123
|
+
}
|
|
124
|
+
/** Each chip at its start time, or just under the chip before it when that
|
|
125
|
+
* one is in the way: the order is time, the height is time, nothing overlaps. */
|
|
126
|
+
function chipTops(list, dayMs) {
|
|
127
|
+
const tops = [];
|
|
128
|
+
let min = 0;
|
|
129
|
+
for (const ev of list) {
|
|
130
|
+
const top = Math.max(timeTop(hourOf(ev.start.getTime(), dayMs)), min);
|
|
131
|
+
tops.push(top);
|
|
132
|
+
min = top + CHIP_H + CHIP_GAP;
|
|
133
|
+
}
|
|
134
|
+
return tops;
|
|
135
|
+
}
|
|
136
|
+
/** The now-line: at the clock's height (pinned to the axis ends outside
|
|
137
|
+
* it), but never above a chip that has already started — pushed-down
|
|
138
|
+
* chips would read as still to come. */
|
|
139
|
+
function nowTop(list, tops, dayMs) {
|
|
140
|
+
let y = timeTop(hourOf(clock.tick, dayMs));
|
|
141
|
+
list.forEach((ev, i) => {
|
|
142
|
+
if (ev.start.getTime() <= clock.tick) y = Math.max(y, tops[i] + CHIP_H + CHIP_GAP / 2);
|
|
143
|
+
});
|
|
144
|
+
return y;
|
|
70
145
|
}
|
|
71
146
|
const _initMs = untrack(() => sod(focusDate?.getTime() ?? Date.now()));
|
|
72
147
|
let internalFocusMs = $state(_initMs);
|
|
@@ -80,7 +155,7 @@ function scrollWeekIntoContainer(targetMs, behavior = "auto") {
|
|
|
80
155
|
const rows = el.querySelectorAll("[data-week]");
|
|
81
156
|
for (const row of rows) {
|
|
82
157
|
const weekMs = Number(row.dataset.week);
|
|
83
|
-
if (weekMs <= targetMs && targetMs < weekMs
|
|
158
|
+
if (weekMs <= targetMs && targetMs < addDaysMs(weekMs, customDays)) {
|
|
84
159
|
target = row;
|
|
85
160
|
break;
|
|
86
161
|
}
|
|
@@ -104,8 +179,8 @@ const anchorPeriodStart = $derived(customDays === 7 ? sowFn(internalFocusMs, mon
|
|
|
104
179
|
// what range we need. Calendar's single $effect handles loading.
|
|
105
180
|
$effect(() => {
|
|
106
181
|
if (!loadRangeCtx) return;
|
|
107
|
-
const rangeStart = new Date(anchorPeriodStart -
|
|
108
|
-
const rangeEnd = new Date(anchorPeriodStart
|
|
182
|
+
const rangeStart = new Date(addDaysMs(anchorPeriodStart, -bufferBefore * customDays));
|
|
183
|
+
const rangeEnd = new Date(addDaysMs(anchorPeriodStart, (bufferAfter + 1) * customDays));
|
|
109
184
|
loadRangeCtx.set({
|
|
110
185
|
start: rangeStart,
|
|
111
186
|
end: rangeEnd
|
|
@@ -115,29 +190,31 @@ $effect(() => {
|
|
|
115
190
|
const weeks = $derived.by(() => {
|
|
116
191
|
const result = [];
|
|
117
192
|
for (let w = -bufferBefore; w <= bufferAfter; w++) {
|
|
118
|
-
|
|
119
|
-
|
|
193
|
+
// Calendar days, not 24 h steps: past a DST change a sum of DAY_MS
|
|
194
|
+
// lands at 23:00 the day before, and every later row shifts a day.
|
|
195
|
+
const periodStart = addDaysMs(anchorPeriodStart, w * customDays);
|
|
196
|
+
const isCurrent = todayMs >= periodStart && todayMs < addDaysMs(periodStart, customDays);
|
|
120
197
|
const days = [];
|
|
121
198
|
for (let d = 0; d < customDays; d++) {
|
|
122
|
-
const ms = periodStart
|
|
199
|
+
const ms = addDaysMs(periodStart, d);
|
|
123
200
|
const date = new Date(ms);
|
|
124
201
|
const dayNum = date.getDate();
|
|
125
202
|
const dow = date.getDay();
|
|
126
203
|
const isWeekend = dow === 0 || dow === 6;
|
|
127
204
|
const isToday = ms === todayMs;
|
|
128
205
|
const isPast = equalDays ? false : ms < todayMs;
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
//
|
|
206
|
+
const dayEnd = addDaysMs(ms, 1);
|
|
207
|
+
// A zero-length event counts where it sits (even at midnight)
|
|
208
|
+
const dayEventsAll = events.filter((ev) => ev.start.getTime() < dayEnd && (ev.end.getTime() > ms || ev.start.getTime() >= ms)).sort((a, b) => a.start.getTime() - b.start.getTime());
|
|
209
|
+
// Separate all-day / day-long from timed events. A timed event that
|
|
210
|
+
// runs past midnight (22:00–01:00) is a chip on its start day only.
|
|
134
211
|
const timedEvents = [];
|
|
135
212
|
const allDaySegments = [];
|
|
136
213
|
for (const ev of dayEventsAll) {
|
|
137
|
-
if (
|
|
214
|
+
if (inAllDayStrip(ev)) {
|
|
138
215
|
const seg = segmentForDay(ev, ms);
|
|
139
216
|
if (seg) allDaySegments.push(seg);
|
|
140
|
-
} else {
|
|
217
|
+
} else if (ev.start.getTime() >= ms && ev.end.getTime() >= ev.start.getTime()) {
|
|
141
218
|
timedEvents.push(ev);
|
|
142
219
|
}
|
|
143
220
|
}
|
|
@@ -147,20 +224,16 @@ const weeks = $derived.by(() => {
|
|
|
147
224
|
isToday,
|
|
148
225
|
isPast,
|
|
149
226
|
isWeekend,
|
|
150
|
-
|
|
151
|
-
monthLabel,
|
|
227
|
+
monthLabel: null,
|
|
152
228
|
events: timedEvents,
|
|
153
229
|
allDaySegments
|
|
154
230
|
});
|
|
155
231
|
}
|
|
156
|
-
// Month label: show when first day of period is day 1-7 (for 7-day), or first day of period (for custom)
|
|
157
|
-
const startDate = new Date(periodStart);
|
|
158
|
-
const showMonth = customDays === 7 ? startDate.getDate() <= 7 : startDate.getDate() <= customDays;
|
|
159
|
-
const monthLabel = showMonth ? monthLong(periodStart, locale).toUpperCase() : null;
|
|
160
232
|
result.push({
|
|
161
233
|
weekStart: periodStart,
|
|
162
234
|
isCurrent,
|
|
163
|
-
monthLabel,
|
|
235
|
+
monthLabel: "",
|
|
236
|
+
monthStart: false,
|
|
164
237
|
days
|
|
165
238
|
});
|
|
166
239
|
}
|
|
@@ -175,6 +248,24 @@ const weeks = $derived.by(() => {
|
|
|
175
248
|
});
|
|
176
249
|
}
|
|
177
250
|
}
|
|
251
|
+
// Month labels, from the days actually shown — a hidden 1st hands its
|
|
252
|
+
// label to the next shown day. The gutter names the row's month; a month
|
|
253
|
+
// that begins after the row's first day is named where it begins.
|
|
254
|
+
for (const row of result) {
|
|
255
|
+
let prev = new Date(addDaysMs(row.weekStart, -1)).getMonth();
|
|
256
|
+
row.days.forEach((day, i) => {
|
|
257
|
+
const date = new Date(day.ms);
|
|
258
|
+
const starts = date.getMonth() !== prev;
|
|
259
|
+
prev = date.getMonth();
|
|
260
|
+
const label = monthLong(day.ms, locale).toUpperCase() + (starts ? ` ${date.getFullYear()}` : "");
|
|
261
|
+
if (i === 0) {
|
|
262
|
+
row.monthLabel = label;
|
|
263
|
+
row.monthStart = starts;
|
|
264
|
+
} else if (starts) {
|
|
265
|
+
day.monthLabel = label;
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
}
|
|
178
269
|
return result;
|
|
179
270
|
});
|
|
180
271
|
// ─── Format helpers ─────────────────────────────────
|
|
@@ -184,7 +275,15 @@ function fmtAmPm(d) {
|
|
|
184
275
|
// ─── Scroll to current week on mount ────────────────
|
|
185
276
|
onMount(() => {
|
|
186
277
|
tick().then(() => scrollWeekIntoContainer());
|
|
187
|
-
return () =>
|
|
278
|
+
return () => {
|
|
279
|
+
cancelAnimationFrame(syncRaf);
|
|
280
|
+
// Unmounted mid-gesture (a view switch between pointerdown and
|
|
281
|
+
// pointerup): drop the window listeners and the auto-scroll frame,
|
|
282
|
+
// and the half-made drag with them.
|
|
283
|
+
const live = evDragStarted;
|
|
284
|
+
cleanupEvDrag();
|
|
285
|
+
if (live && drag?.active) drag.cancel();
|
|
286
|
+
};
|
|
188
287
|
});
|
|
189
288
|
// Rows grow when their events arrive, which moves every row below them —
|
|
190
289
|
// including the one we centred on an empty grid. Centre once more when the
|
|
@@ -285,31 +384,47 @@ function handleUserScroll() {
|
|
|
285
384
|
}
|
|
286
385
|
}
|
|
287
386
|
}
|
|
288
|
-
/**
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
|
|
292
|
-
|
|
387
|
+
/** The time at a pointer's height on a day's axis — timeTop read backwards,
|
|
388
|
+
* the pointer at the chip's middle. Half hours: the whole day is ~100px.
|
|
389
|
+
* Wall-clock ms into the local day; dayDropStart makes it an instant. */
|
|
390
|
+
function pointerTime(cell, clientY) {
|
|
391
|
+
const area = cell.querySelector(".wg-cell-events");
|
|
392
|
+
if (!area) return startHour * HOUR_MS;
|
|
393
|
+
// The result is wall-clock time into the day (ms), rounded to half hours
|
|
394
|
+
// of the local day — dayDropStart turns it into an instant.
|
|
395
|
+
const y = clientY - area.getBoundingClientRect().top - CHIP_H / 2;
|
|
396
|
+
const f = Math.min(Math.max(y / (TIME_H - CHIP_H), 0), 1);
|
|
397
|
+
const hours = startHour + f * Math.max(1, endHour - startHour);
|
|
398
|
+
return Math.round(hours * 2) * (HOUR_MS / 2);
|
|
399
|
+
}
|
|
400
|
+
/** Where a new thing lands on this day, or null when the day (or that hour
|
|
401
|
+
* of it) refuses it. `at` is the pointer's time into the day; without one
|
|
402
|
+
* (the keyboard) it is the first hour of the axis. One rule, so an
|
|
403
|
+
* empty-cell click and a drop from outside can never disagree. */
|
|
404
|
+
function dayDropStart(ms, at = startHour * HOUR_MS) {
|
|
293
405
|
if (disabledSet.has(ms)) return null;
|
|
294
|
-
const
|
|
406
|
+
const hour = at / HOUR_MS;
|
|
295
407
|
if (blockedSlots?.length) {
|
|
296
408
|
const jsDay = new Date(ms).getDay();
|
|
297
409
|
const isoDay = jsDay === 0 ? 7 : jsDay;
|
|
298
410
|
const blocked = blockedSlots.some((slot) => {
|
|
299
411
|
if (slot.day && slot.day !== isoDay) return false;
|
|
300
|
-
return
|
|
412
|
+
return hour >= slot.start && hour < slot.end;
|
|
301
413
|
});
|
|
302
414
|
if (blocked) return null;
|
|
303
415
|
}
|
|
304
|
-
return new Date(ms
|
|
416
|
+
return new Date(atHour(ms, hour));
|
|
305
417
|
}
|
|
306
418
|
function handleDayCellClick(ms, e) {
|
|
307
419
|
const target = e.target;
|
|
308
420
|
if (target.closest(".wg-ev, .wg-ad, .wg-ev-more")) return;
|
|
309
421
|
if (readOnly || !oneventcreate) return;
|
|
310
|
-
const
|
|
422
|
+
const at = e instanceof MouseEvent ? pointerTime(e.currentTarget, e.clientY) : undefined;
|
|
423
|
+
const start = dayDropStart(ms, at);
|
|
311
424
|
if (!start) return;
|
|
312
|
-
|
|
425
|
+
// An hour, or the minimum duration when that is longer. Blocked-slot and
|
|
426
|
+
// max-duration validation is Calendar's (ctx.oneventcreate), not ours.
|
|
427
|
+
const durMin = Math.max(60, minDuration ?? 0);
|
|
313
428
|
oneventcreate({
|
|
314
429
|
start,
|
|
315
430
|
end: new Date(start.getTime() + durMin * 6e4)
|
|
@@ -317,21 +432,31 @@ function handleDayCellClick(ms, e) {
|
|
|
317
432
|
}
|
|
318
433
|
// ─── External drop (HTML5 DnD) ──────────────────────
|
|
319
434
|
// A class chip dragged in from outside the calendar; the cell it is over
|
|
320
|
-
// lights up
|
|
321
|
-
let
|
|
435
|
+
// lights up and a line on its axis says the time it would land at.
|
|
436
|
+
let drop = $state(null);
|
|
322
437
|
function onCellDragOver(e, ms) {
|
|
323
|
-
if (!onexternaldrop || readOnly
|
|
438
|
+
if (!onexternaldrop || readOnly) return;
|
|
439
|
+
const at = pointerTime(e.currentTarget, e.clientY);
|
|
440
|
+
if (!dayDropStart(ms, at)) {
|
|
441
|
+
drop = null;
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
324
444
|
e.preventDefault();
|
|
325
445
|
if (e.dataTransfer) e.dataTransfer.dropEffect = "copy";
|
|
326
|
-
|
|
446
|
+
if (drop?.day !== ms || drop.at !== at) drop = {
|
|
447
|
+
day: ms,
|
|
448
|
+
at
|
|
449
|
+
};
|
|
327
450
|
}
|
|
328
|
-
function onCellDragLeave(ms) {
|
|
329
|
-
|
|
451
|
+
function onCellDragLeave(e, ms) {
|
|
452
|
+
// Leaving for a chip inside the same cell is not leaving the cell.
|
|
453
|
+
if (e.currentTarget.contains(e.relatedTarget)) return;
|
|
454
|
+
if (drop?.day === ms) drop = null;
|
|
330
455
|
}
|
|
331
456
|
function onCellDrop(e, ms) {
|
|
332
|
-
|
|
457
|
+
drop = null;
|
|
333
458
|
if (!onexternaldrop || readOnly || !e.dataTransfer) return;
|
|
334
|
-
const start = dayDropStart(ms);
|
|
459
|
+
const start = dayDropStart(ms, pointerTime(e.currentTarget, e.clientY));
|
|
335
460
|
if (!start) return;
|
|
336
461
|
e.preventDefault();
|
|
337
462
|
onexternaldrop({
|
|
@@ -359,6 +484,11 @@ let evCellW = 100;
|
|
|
359
484
|
* step is the row under the pointer, found by hit-test, not by division.
|
|
360
485
|
* Held as a timestamp: an extension can renumber rows mid-drag. */
|
|
361
486
|
let evStartWeekMs = 0;
|
|
487
|
+
/** The day cell the press landed in; the drag moves the event by the days
|
|
488
|
+
* between it and the cell now under the pointer. */
|
|
489
|
+
let evPressDayMs = 0;
|
|
490
|
+
/** The grid runs right-to-left (`dir="rtl"`): a rightward drag goes back. */
|
|
491
|
+
let evRtl = false;
|
|
362
492
|
const dragPreviewEvent = $derived.by(() => {
|
|
363
493
|
const payload = drag?.active && drag.mode === "move" ? drag.payload : null;
|
|
364
494
|
if (!payload?.eventId) return null;
|
|
@@ -384,15 +514,16 @@ function timedEventsForDay(day) {
|
|
|
384
514
|
const previewKeySnapshot = new Map();
|
|
385
515
|
function dragPreviewTimedForDay(dayMs) {
|
|
386
516
|
const ev = dragPreviewEvent;
|
|
387
|
-
if (!ev ||
|
|
388
|
-
|
|
389
|
-
const
|
|
517
|
+
if (!ev || inAllDayStrip(ev)) return null;
|
|
518
|
+
// Where the chip would sit: its start day (as in the week rows)
|
|
519
|
+
const dayEnd = addDaysMs(dayMs, 1);
|
|
520
|
+
const hit = ev.start.getTime() >= dayMs && ev.start.getTime() < dayEnd;
|
|
390
521
|
if (hit) previewKeySnapshot.set("timed", ev.id);
|
|
391
522
|
return hit ? ev : null;
|
|
392
523
|
}
|
|
393
524
|
function dragPreviewSegmentForDay(dayMs) {
|
|
394
525
|
const ev = dragPreviewEvent;
|
|
395
|
-
if (!ev || !
|
|
526
|
+
if (!ev || !inAllDayStrip(ev)) return null;
|
|
396
527
|
const seg = segmentForDay(ev, dayMs);
|
|
397
528
|
if (seg) previewKeySnapshot.set(dayMs, `${ev.id}:${seg.dayIndex}`);
|
|
398
529
|
return seg;
|
|
@@ -415,7 +546,9 @@ const fit = createChipFit({
|
|
|
415
546
|
room: ".wg-probe .wg-ev-loc"
|
|
416
547
|
}
|
|
417
548
|
});
|
|
418
|
-
|
|
549
|
+
/** A cell that carries a month label is this much narrower (.wg-cell--month). */
|
|
550
|
+
const MONTH_INSET = 22;
|
|
551
|
+
function chipParts(ev, inset = 0) {
|
|
419
552
|
return fit.parts([
|
|
420
553
|
{
|
|
421
554
|
key: "time",
|
|
@@ -437,7 +570,7 @@ function chipParts(ev) {
|
|
|
437
570
|
priority: 1,
|
|
438
571
|
extra: CHIP_GAP_X
|
|
439
572
|
}
|
|
440
|
-
], CHIP_PAD_X);
|
|
573
|
+
], CHIP_PAD_X + inset);
|
|
441
574
|
}
|
|
442
575
|
function getCellWidth() {
|
|
443
576
|
const cell = el?.querySelector(".wg-cell");
|
|
@@ -468,20 +601,46 @@ function onEventPointerDown(e, ev) {
|
|
|
468
601
|
evDragStarted = false;
|
|
469
602
|
evDragId = ev.id;
|
|
470
603
|
evDragEvent = ev;
|
|
604
|
+
const cellMs = Number(e.currentTarget.closest("[data-day]")?.dataset.day);
|
|
605
|
+
evPressDayMs = Number.isFinite(cellMs) ? cellMs : sod(ev.start.getTime());
|
|
471
606
|
window.addEventListener("pointermove", onEvWindowPointerMove);
|
|
472
607
|
window.addEventListener("pointerup", onEvWindowPointerUp, { once: true });
|
|
473
608
|
window.addEventListener("pointercancel", onEvWindowPointerCancel, { once: true });
|
|
474
609
|
}
|
|
610
|
+
/** The day `cols` rendered columns away from the press cell, in the row
|
|
611
|
+
* whose start is `rowMs`. Columns are counted over the days actually shown
|
|
612
|
+
* (hideDays leaves gaps in the dates, not in the columns); running off a
|
|
613
|
+
* row's end carries into the neighbouring row. */
|
|
614
|
+
function dayAtColumn(rowMs, cols) {
|
|
615
|
+
const startRow = weeks.findIndex((w) => w.weekStart === evStartWeekMs);
|
|
616
|
+
let r = weeks.findIndex((w) => w.weekStart === rowMs);
|
|
617
|
+
if (startRow < 0 || r < 0) return null;
|
|
618
|
+
const pressCol = weeks[startRow].days.findIndex((d) => d.ms === evPressDayMs);
|
|
619
|
+
if (pressCol < 0) return null;
|
|
620
|
+
let col = pressCol + cols;
|
|
621
|
+
while (col < 0 && r > 0) {
|
|
622
|
+
r--;
|
|
623
|
+
col += weeks[r].days.length;
|
|
624
|
+
}
|
|
625
|
+
while (col >= weeks[r].days.length && r < weeks.length - 1) {
|
|
626
|
+
col -= weeks[r].days.length;
|
|
627
|
+
r++;
|
|
628
|
+
}
|
|
629
|
+
const days = weeks[r].days;
|
|
630
|
+
if (!days.length) return null;
|
|
631
|
+
return days[Math.max(0, Math.min(days.length - 1, col))].ms;
|
|
632
|
+
}
|
|
475
633
|
/** Where the dragged event sits for the pointer's current offset. */
|
|
476
634
|
function updateDragFromPointer() {
|
|
477
635
|
const ev = evDragEvent;
|
|
478
636
|
if (!drag || !ev) return;
|
|
479
|
-
const
|
|
637
|
+
const dx = (evLastX - evDragStartX) * (evRtl ? -1 : 1);
|
|
638
|
+
const colOffset = Math.round(dx / evCellW);
|
|
480
639
|
const nowWeekMs = weekMsAtY(evLastY);
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
const
|
|
484
|
-
drag.updatePointer(new Date(ev.start.getTime()
|
|
640
|
+
const target = evStartWeekMs && nowWeekMs ? dayAtColumn(nowWeekMs, colOffset) : null;
|
|
641
|
+
// Without a mapped cell (the rows are mid-rebuild) keep the press day
|
|
642
|
+
const deltaDays = target === null ? 0 : diffDays(target, evPressDayMs);
|
|
643
|
+
drag.updatePointer(new Date(addDaysMs(ev.start.getTime(), deltaDays)), new Date(addDaysMs(ev.end.getTime(), deltaDays)));
|
|
485
644
|
}
|
|
486
645
|
// ─── Auto-scroll under a drag ───────────────────────
|
|
487
646
|
// Weeks the target is in may be off-screen — without this, moving an event
|
|
@@ -526,6 +685,7 @@ function onEvWindowPointerMove(e) {
|
|
|
526
685
|
evDragging = true;
|
|
527
686
|
evCellW = getCellWidth();
|
|
528
687
|
evStartWeekMs = weekMsAtY(evDragStartY);
|
|
688
|
+
evRtl = !!el && getComputedStyle(el).direction === "rtl";
|
|
529
689
|
drag.beginMove(ev.id, ev.start, ev.end);
|
|
530
690
|
}
|
|
531
691
|
setAutoScroll(e.clientY);
|
|
@@ -543,6 +703,8 @@ function cleanupEvDrag() {
|
|
|
543
703
|
evAnchor = undefined;
|
|
544
704
|
evDragMovable = false;
|
|
545
705
|
evStartWeekMs = 0;
|
|
706
|
+
evPressDayMs = 0;
|
|
707
|
+
evRtl = false;
|
|
546
708
|
}
|
|
547
709
|
function onEvWindowPointerUp() {
|
|
548
710
|
if (!evDragStarted) {
|
|
@@ -567,9 +729,28 @@ function onWindowKeydown(e) {
|
|
|
567
729
|
// ─── "+N more" per-cell expansion ───────────────────
|
|
568
730
|
let expandedCells = $state({});
|
|
569
731
|
// ─── Roving tabindex for grid cells ─────────────────
|
|
570
|
-
// One tabbable cell (last focused, else today
|
|
732
|
+
// One tabbable cell (last focused, else today, else the first shown day of
|
|
733
|
+
// the anchor week — today may be a hidden day or outside the buffer, and
|
|
734
|
+
// the grid must never drop out of the tab order); arrows move focus.
|
|
571
735
|
let focusedCellMs = $state(null);
|
|
572
|
-
const tabbableCellMs = $derived(
|
|
736
|
+
const tabbableCellMs = $derived.by(() => {
|
|
737
|
+
let fallback = null;
|
|
738
|
+
let anchorFirst = null;
|
|
739
|
+
let focusedShown = false;
|
|
740
|
+
let todayShown = false;
|
|
741
|
+
for (const w of weeks) {
|
|
742
|
+
if (!w.days.length) continue;
|
|
743
|
+
fallback ??= w.days[0].ms;
|
|
744
|
+
if (w.weekStart === anchorPeriodStart) anchorFirst = w.days[0].ms;
|
|
745
|
+
for (const d of w.days) {
|
|
746
|
+
if (d.ms === focusedCellMs) focusedShown = true;
|
|
747
|
+
if (d.ms === todayMs) todayShown = true;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
if (focusedShown) return focusedCellMs;
|
|
751
|
+
if (todayShown) return todayMs;
|
|
752
|
+
return anchorFirst ?? fallback;
|
|
753
|
+
});
|
|
573
754
|
function onCellKeydown(e, ms) {
|
|
574
755
|
if (e.key === "Enter" || e.key === " ") {
|
|
575
756
|
e.preventDefault();
|
|
@@ -577,15 +758,15 @@ function onCellKeydown(e, ms) {
|
|
|
577
758
|
return;
|
|
578
759
|
}
|
|
579
760
|
let step = 0;
|
|
580
|
-
if (e.key === "ArrowRight") step =
|
|
581
|
-
else if (e.key === "ArrowLeft") step = -
|
|
582
|
-
else if (e.key === "ArrowDown") step = customDays
|
|
583
|
-
else if (e.key === "ArrowUp") step = -customDays
|
|
761
|
+
if (e.key === "ArrowRight") step = 1;
|
|
762
|
+
else if (e.key === "ArrowLeft") step = -1;
|
|
763
|
+
else if (e.key === "ArrowDown") step = customDays;
|
|
764
|
+
else if (e.key === "ArrowUp") step = -customDays;
|
|
584
765
|
if (step === 0) return;
|
|
585
766
|
e.preventDefault();
|
|
586
767
|
// Walk in the step direction until a rendered cell is found
|
|
587
768
|
// (skips hidden days; bails at the buffer edge).
|
|
588
|
-
let target = ms
|
|
769
|
+
let target = addDaysMs(ms, step);
|
|
589
770
|
for (let i = 0; i < 7; i++) {
|
|
590
771
|
const cell = el?.querySelector(`[data-day="${target}"]`);
|
|
591
772
|
if (cell) {
|
|
@@ -594,7 +775,7 @@ function onCellKeydown(e, ms) {
|
|
|
594
775
|
cell.scrollIntoView({ block: "nearest" });
|
|
595
776
|
return;
|
|
596
777
|
}
|
|
597
|
-
target
|
|
778
|
+
target = addDaysMs(target, step < 0 ? -1 : 1);
|
|
598
779
|
}
|
|
599
780
|
}
|
|
600
781
|
</script>
|
|
@@ -611,8 +792,8 @@ function onCellKeydown(e, ms) {
|
|
|
611
792
|
{/if}
|
|
612
793
|
{/snippet}
|
|
613
794
|
|
|
614
|
-
{#snippet timedEventContent(ev: TimelineEvent)}
|
|
615
|
-
{@const parts = chipParts(ev)}
|
|
795
|
+
{#snippet timedEventContent(ev: TimelineEvent, inset: number)}
|
|
796
|
+
{@const parts = chipParts(ev, inset)}
|
|
616
797
|
<EventContent event={ev}>
|
|
617
798
|
{#if parts.time}<span class="wg-ev-time">{fmtAmPm(ev.start)}</span>{/if}
|
|
618
799
|
<span class="wg-ev-title">{ev.title}</span>
|
|
@@ -624,7 +805,7 @@ function onCellKeydown(e, ms) {
|
|
|
624
805
|
|
|
625
806
|
<svelte:window onkeydown={onWindowKeydown} />
|
|
626
807
|
|
|
627
|
-
<div class="wg" class:wg--auto={autoHeight} style={style || undefined} style:height={autoHeight ? undefined : (height ? `${height}px` : '100%')} style:--wg-
|
|
808
|
+
<div class="wg" class:wg--auto={autoHeight} style={style || undefined} style:height={autoHeight ? undefined : (height ? `${height}px` : '100%')} style:--wg-time-h="{TIME_H}px" style:--wg-month-inset="{MONTH_INSET}px" style:--wg-chip-h="{CHIP_H}px" style:--wg-chip-gap="{CHIP_GAP}px" style:--wg-row-margin="{ROW_MARGIN}px">
|
|
628
809
|
<div
|
|
629
810
|
class="wg-body"
|
|
630
811
|
bind:this={el}
|
|
@@ -641,6 +822,8 @@ function onCellKeydown(e, ms) {
|
|
|
641
822
|
</div>
|
|
642
823
|
{#each weeks as week (week.weekStart)}
|
|
643
824
|
<div class="wg-week" class:wg-week--current={week.isCurrent} data-week={week.weekStart} role="presentation">
|
|
825
|
+
<!-- Each cell's aria-label already names its date -->
|
|
826
|
+
<div class="wg-month" class:wg-month--start={week.monthStart} aria-hidden="true">{week.monthLabel}</div>
|
|
644
827
|
<div class="wg-week-body" role="presentation">
|
|
645
828
|
<!-- Day columns (header inside each cell) -->
|
|
646
829
|
<div class="wg-days" role="row">
|
|
@@ -652,33 +835,39 @@ function onCellKeydown(e, ms) {
|
|
|
652
835
|
{@const hiddenCount = Math.max(0, visibleTimedEvents.length - timedCap)}
|
|
653
836
|
{@const previewTimedEvent = dragPreviewTimedForDay(day.ms)}
|
|
654
837
|
{@const previewSegment = dragPreviewSegmentForDay(day.ms)}
|
|
838
|
+
{@const shownTimed = visibleTimedEvents.slice(0, isExpanded ? visibleTimedEvents.length : timedCap)}
|
|
839
|
+
{@const tops = chipTops(shownTimed, day.ms)}
|
|
840
|
+
{@const nowY = day.isToday ? nowTop(shownTimed, tops, day.ms) : null}
|
|
841
|
+
{@const inset = day.monthLabel ? MONTH_INSET : 0}
|
|
655
842
|
<div
|
|
656
843
|
class="wg-cell"
|
|
844
|
+
class:wg-cell--month={!!day.monthLabel}
|
|
657
845
|
class:wg-cell--today={day.isToday}
|
|
658
846
|
class:wg-cell--past={day.isPast}
|
|
659
847
|
class:wg-cell--weekend={day.isWeekend}
|
|
660
848
|
class:wg-cell--disabled={disabledSet.has(day.ms)}
|
|
661
849
|
class:wg-cell--expanded={isExpanded}
|
|
662
|
-
class:wg-cell--drop={
|
|
850
|
+
class:wg-cell--drop={drop?.day === day.ms}
|
|
663
851
|
role="gridcell"
|
|
664
852
|
data-day={day.ms}
|
|
665
853
|
tabindex={day.ms === tabbableCellMs ? 0 : -1}
|
|
666
854
|
aria-label="{new Date(day.ms).toLocaleDateString(locale ?? 'en-US', { weekday: 'long', month: 'short', day: 'numeric' })}{day.isToday ? ` (${L.today.toLowerCase()})` : ''}, {L.nEvents(day.events.length + day.allDaySegments.length)}"
|
|
667
855
|
onclick={(e) => handleDayCellClick(day.ms, e)}
|
|
668
856
|
ondragover={(e) => onCellDragOver(e, day.ms)}
|
|
669
|
-
ondragleave={() => onCellDragLeave(day.ms)}
|
|
857
|
+
ondragleave={(e) => onCellDragLeave(e, day.ms)}
|
|
670
858
|
ondrop={(e) => onCellDrop(e, day.ms)}
|
|
671
859
|
onfocus={() => { focusedCellMs = day.ms; }}
|
|
672
860
|
onkeydown={(e) => onCellKeydown(e, day.ms)}
|
|
673
861
|
>
|
|
674
|
-
|
|
862
|
+
{#if day.monthLabel}
|
|
863
|
+
<div class="wg-month wg-month--start wg-month--inset" aria-hidden="true">{day.monthLabel}</div>
|
|
864
|
+
{/if}
|
|
865
|
+
<!-- Day label in top-right corner: "MON 23", a pill today -->
|
|
675
866
|
<div class="wg-cell-hd" class:wg-cell-hd--today={day.isToday}>
|
|
867
|
+
<span class="wg-day-wd">{weekdayShort(day.ms, locale)}</span>
|
|
676
868
|
{#if showDates}
|
|
677
|
-
<span class="wg-day-num"
|
|
678
|
-
{day.dayNum}
|
|
679
|
-
</span>
|
|
869
|
+
<span class="wg-day-num">{day.dayNum}</span>
|
|
680
870
|
{/if}
|
|
681
|
-
<span class="wg-day-wd">{weekdayShort(day.ms, locale)}</span>
|
|
682
871
|
</div>
|
|
683
872
|
|
|
684
873
|
<!-- Custom day header snippet -->
|
|
@@ -688,27 +877,94 @@ function onCellKeydown(e, ms) {
|
|
|
688
877
|
</div>
|
|
689
878
|
{/if}
|
|
690
879
|
|
|
691
|
-
<!--
|
|
692
|
-
|
|
693
|
-
{
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
{#
|
|
697
|
-
{
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
880
|
+
<!-- Timed events, on the day's time axis -->
|
|
881
|
+
<div class="wg-cell-events">
|
|
882
|
+
{#if blockedSlots?.length}
|
|
883
|
+
{@const jsDay = new Date(day.ms).getDay()}
|
|
884
|
+
{@const isoDay = jsDay === 0 ? 7 : jsDay}
|
|
885
|
+
{#each blockedSlots as slot, i (i)}
|
|
886
|
+
{#if (!slot.day || slot.day === isoDay) && slot.end > startHour && slot.start < endHour}
|
|
887
|
+
{@const slotRange = `${_fmtTime(new Date(atHour(day.ms, slot.start)), locale)} – ${_fmtTime(new Date(atHour(day.ms, slot.end)), locale)}`}
|
|
888
|
+
{@const bandTop = timeTop(slot.start)}
|
|
889
|
+
{@const bandH = Math.max(14, timeTop(slot.end) - bandTop)}
|
|
890
|
+
<!-- A chip over the band would cut its label in half: the title keeps it -->
|
|
891
|
+
{@const covered = tops.some((t) => t < bandTop + bandH && t + CHIP_H > bandTop)}
|
|
892
|
+
<div
|
|
893
|
+
class="wg-blocked"
|
|
894
|
+
style:top="{bandTop}px"
|
|
895
|
+
style:height="{bandH}px"
|
|
896
|
+
title="{slot.label ? `${slot.label}, ` : ''}{slotRange}"
|
|
897
|
+
aria-label="{slot.label || L.unavailable}, {slotRange}"
|
|
898
|
+
>
|
|
899
|
+
{#if slot.label && !covered}
|
|
900
|
+
<span class="wg-blocked-label">{slot.label}</span>
|
|
901
|
+
{/if}
|
|
902
|
+
</div>
|
|
903
|
+
{/if}
|
|
904
|
+
{/each}
|
|
905
|
+
{/if}
|
|
906
|
+
{#each shownTimed as ev, i (ev.id)}
|
|
907
|
+
<!-- send/receive keyed by event id pair the card with the drag ghost:
|
|
908
|
+
drag start morphs card → ghost, drop morphs ghost → placed card -->
|
|
909
|
+
<div
|
|
910
|
+
animate:flip={{ duration: ANIM }}
|
|
911
|
+
in:previewReceive={{ key: ev.id }}
|
|
912
|
+
out:previewSend={{ key: ev.id }}
|
|
913
|
+
class="wg-ev"
|
|
914
|
+
class:wg-ev--selected={selectedEventId === ev.id}
|
|
915
|
+
class:wg-ev--current={ev.start.getTime() <= clock.tick && ev.end.getTime() > clock.tick}
|
|
916
|
+
class:wg-ev--dragging={evDragging && evDragId === ev.id}
|
|
917
|
+
class:wg-ev--readonly={ev.data?.readOnly}
|
|
918
|
+
class:wg-ev--cancelled={ev.status === 'cancelled'}
|
|
919
|
+
class:wg-ev--tentative={ev.status === 'tentative'}
|
|
920
|
+
class:wg-ev--full={ev.status === 'full'}
|
|
921
|
+
class:wg-ev--limited={ev.status === 'limited'}
|
|
922
|
+
style:--ev-color={ev.color ?? 'var(--dt-accent)'}
|
|
923
|
+
style:margin-top="{tops[i] - (i ? tops[i - 1] + CHIP_H : 0)}px"
|
|
924
|
+
role="button"
|
|
925
|
+
tabindex="0"
|
|
926
|
+
aria-label="{ev.title}, {fmtAmPm(ev.start)} – {fmtAmPm(ev.end)}{statusText(ev)}{ev.start.getTime() <= clock.tick && ev.end.getTime() > clock.tick ? ` (${L.inProgress})` : ''}"
|
|
927
|
+
onpointerdown={(e) => onEventPointerDown(e, ev)}
|
|
928
|
+
onpointerenter={() => oneventhover?.(ev)}
|
|
929
|
+
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); oneventclick?.(ev, e.currentTarget.getBoundingClientRect()); } }}
|
|
930
|
+
>
|
|
931
|
+
{@render timedEventContent(ev, inset)}
|
|
932
|
+
</div>
|
|
708
933
|
{/each}
|
|
709
|
-
|
|
934
|
+
{#if hiddenCount > 0}
|
|
935
|
+
<button
|
|
936
|
+
type="button"
|
|
937
|
+
class="wg-ev-more"
|
|
938
|
+
aria-expanded={isExpanded}
|
|
939
|
+
onclick={(e) => { e.stopPropagation(); expandedCells[day.ms] = !isExpanded; }}
|
|
940
|
+
>{isExpanded ? L.showLess : L.nMore(hiddenCount)}</button>
|
|
941
|
+
{/if}
|
|
942
|
+
{#if previewTimedEvent}
|
|
943
|
+
<!-- The ghost floats at its own time; it pushes nothing aside -->
|
|
944
|
+
<div
|
|
945
|
+
class="wg-ev wg-ev--drag-preview"
|
|
946
|
+
style:--ev-color={previewTimedEvent.color ?? 'var(--dt-accent)'}
|
|
947
|
+
style:top="{timeTop(hourOf(previewTimedEvent.start.getTime(), day.ms))}px"
|
|
948
|
+
aria-hidden="true"
|
|
949
|
+
in:previewReceive={{ key: previewKeySnapshot.get('timed') ?? '' }}
|
|
950
|
+
out:previewSend={{ key: previewKeySnapshot.get('timed') ?? '' }}
|
|
951
|
+
>
|
|
952
|
+
{@render timedEventContent(previewTimedEvent, inset)}
|
|
953
|
+
</div>
|
|
954
|
+
{/if}
|
|
955
|
+
{#if drop?.day === day.ms}
|
|
956
|
+
<div class="wg-now wg-drop" style:top="{timeTop(drop.at / HOUR_MS) + CHIP_H / 2}px" aria-hidden="true">
|
|
957
|
+
<span class="wg-now-time">{fmtAmPm(new Date(atHour(day.ms, drop.at / HOUR_MS)))}</span>
|
|
958
|
+
</div>
|
|
959
|
+
{/if}
|
|
960
|
+
{#if nowY !== null}
|
|
961
|
+
<div class="wg-now" style:top="{nowY}px" aria-hidden="true">
|
|
962
|
+
<span class="wg-now-time">{fmtAmPm(new Date(clock.tick))}</span>
|
|
963
|
+
</div>
|
|
964
|
+
{/if}
|
|
965
|
+
</div>
|
|
710
966
|
|
|
711
|
-
<!-- All-day / multi-day events -->
|
|
967
|
+
<!-- All-day / multi-day events, under the day (as Hey) -->
|
|
712
968
|
{#if visibleAllDaySegments.length > 0 || previewSegment}
|
|
713
969
|
<div class="wg-allday">
|
|
714
970
|
{#each visibleAllDaySegments as seg (seg.ev.id)}
|
|
@@ -749,56 +1005,6 @@ function onCellKeydown(e, ms) {
|
|
|
749
1005
|
{/if}
|
|
750
1006
|
</div>
|
|
751
1007
|
{/if}
|
|
752
|
-
|
|
753
|
-
<!-- Timed events -->
|
|
754
|
-
<div class="wg-cell-events">
|
|
755
|
-
{#each visibleTimedEvents.slice(0, isExpanded ? visibleTimedEvents.length : timedCap) as ev (ev.id)}
|
|
756
|
-
<!-- send/receive keyed by event id pair the card with the drag ghost:
|
|
757
|
-
drag start morphs card → ghost, drop morphs ghost → placed card -->
|
|
758
|
-
<div
|
|
759
|
-
animate:flip={{ duration: ANIM }}
|
|
760
|
-
in:previewReceive={{ key: ev.id }}
|
|
761
|
-
out:previewSend={{ key: ev.id }}
|
|
762
|
-
class="wg-ev"
|
|
763
|
-
class:wg-ev--selected={selectedEventId === ev.id}
|
|
764
|
-
class:wg-ev--current={ev.start.getTime() <= clock.tick && ev.end.getTime() > clock.tick}
|
|
765
|
-
class:wg-ev--dragging={evDragging && evDragId === ev.id}
|
|
766
|
-
class:wg-ev--readonly={ev.data?.readOnly}
|
|
767
|
-
class:wg-ev--cancelled={ev.status === 'cancelled'}
|
|
768
|
-
class:wg-ev--tentative={ev.status === 'tentative'}
|
|
769
|
-
class:wg-ev--full={ev.status === 'full'}
|
|
770
|
-
class:wg-ev--limited={ev.status === 'limited'}
|
|
771
|
-
style:--ev-color={ev.color ?? 'var(--dt-accent)'}
|
|
772
|
-
role="button"
|
|
773
|
-
tabindex="0"
|
|
774
|
-
aria-label="{ev.title}, {fmtAmPm(ev.start)} – {fmtAmPm(ev.end)}{ev.status === 'cancelled' ? ` (cancelled)` : ''}{ev.status === 'tentative' ? ` (tentative)` : ''}{ev.status === 'full' ? ` (full)` : ''}{ev.status === 'limited' ? ` (limited)` : ''}{ev.start.getTime() <= clock.tick && ev.end.getTime() > clock.tick ? ` (${L.inProgress})` : ''}"
|
|
775
|
-
onpointerdown={(e) => onEventPointerDown(e, ev)}
|
|
776
|
-
onpointerenter={() => oneventhover?.(ev)}
|
|
777
|
-
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); oneventclick?.(ev, e.currentTarget.getBoundingClientRect()); } }}
|
|
778
|
-
>
|
|
779
|
-
{@render timedEventContent(ev)}
|
|
780
|
-
</div>
|
|
781
|
-
{/each}
|
|
782
|
-
{#if previewTimedEvent}
|
|
783
|
-
<div
|
|
784
|
-
class="wg-ev wg-ev--drag-preview"
|
|
785
|
-
style:--ev-color={previewTimedEvent.color ?? 'var(--dt-accent)'}
|
|
786
|
-
aria-hidden="true"
|
|
787
|
-
in:previewReceive={{ key: previewKeySnapshot.get('timed') ?? '' }}
|
|
788
|
-
out:previewSend={{ key: previewKeySnapshot.get('timed') ?? '' }}
|
|
789
|
-
>
|
|
790
|
-
{@render timedEventContent(previewTimedEvent)}
|
|
791
|
-
</div>
|
|
792
|
-
{/if}
|
|
793
|
-
</div>
|
|
794
|
-
{#if hiddenCount > 0}
|
|
795
|
-
<button
|
|
796
|
-
type="button"
|
|
797
|
-
class="wg-ev-more"
|
|
798
|
-
aria-expanded={isExpanded}
|
|
799
|
-
onclick={(e) => { e.stopPropagation(); expandedCells[day.ms] = !isExpanded; }}
|
|
800
|
-
>{isExpanded ? L.showLess : L.nMore(hiddenCount)}</button>
|
|
801
|
-
{/if}
|
|
802
1008
|
</div>
|
|
803
1009
|
{/each}
|
|
804
1010
|
</div>
|
|
@@ -887,7 +1093,6 @@ function onCellKeydown(e, ms) {
|
|
|
887
1093
|
display: flex;
|
|
888
1094
|
flex-direction: column;
|
|
889
1095
|
min-width: 90px;
|
|
890
|
-
min-height: var(--wg-row-min, 170px);
|
|
891
1096
|
box-sizing: border-box;
|
|
892
1097
|
padding: 4px 4px 8px;
|
|
893
1098
|
border-right: 1px solid var(--dt-border, rgba(0, 0, 0, 0.08));
|
|
@@ -896,6 +1101,34 @@ function onCellKeydown(e, ms) {
|
|
|
896
1101
|
}
|
|
897
1102
|
|
|
898
1103
|
.wg-cell:last-child { border-right: none; }
|
|
1104
|
+
/* The inset is on the contents, not the cell's padding: flex adds padding
|
|
1105
|
+
to a basis-0 cell, and that one column would come out wider. */
|
|
1106
|
+
.wg-cell--month .wg-cell-events,
|
|
1107
|
+
.wg-cell--month .wg-allday { margin-left: var(--wg-month-inset, 22px); }
|
|
1108
|
+
|
|
1109
|
+
/* ─── Month labels (vertical, read bottom-to-top) ─── */
|
|
1110
|
+
.wg-month {
|
|
1111
|
+
flex: 0 0 22px;
|
|
1112
|
+
writing-mode: vertical-rl;
|
|
1113
|
+
transform: rotate(180deg);
|
|
1114
|
+
text-align: end; /* the rotation puts the end at the top */
|
|
1115
|
+
padding: 6px 0;
|
|
1116
|
+
font: 700 11px / 22px var(--dt-sans, system-ui, sans-serif);
|
|
1117
|
+
letter-spacing: 0.08em;
|
|
1118
|
+
white-space: nowrap;
|
|
1119
|
+
overflow: hidden;
|
|
1120
|
+
color: var(--dt-text-3, rgba(0, 0, 0, 0.38));
|
|
1121
|
+
}
|
|
1122
|
+
.wg-week--current .wg-month,
|
|
1123
|
+
.wg-month--start {
|
|
1124
|
+
color: var(--dt-text, rgba(0, 0, 0, 0.87));
|
|
1125
|
+
}
|
|
1126
|
+
.wg-month--inset {
|
|
1127
|
+
position: absolute;
|
|
1128
|
+
top: 0;
|
|
1129
|
+
left: 2px;
|
|
1130
|
+
pointer-events: none;
|
|
1131
|
+
}
|
|
899
1132
|
.wg-cell:hover { background: var(--dt-hover, rgba(0, 0, 0, 0.015)); }
|
|
900
1133
|
|
|
901
1134
|
.wg-cell--today { background: var(--dt-today-bg, rgba(37, 99, 235, 0.04)); }
|
|
@@ -933,9 +1166,15 @@ function onCellKeydown(e, ms) {
|
|
|
933
1166
|
}
|
|
934
1167
|
|
|
935
1168
|
/* ─── Blocked slot indicator ─────────────────────── */
|
|
1169
|
+
/* A band on the time axis, behind the chips */
|
|
936
1170
|
.wg-blocked {
|
|
1171
|
+
position: absolute;
|
|
1172
|
+
left: 0;
|
|
1173
|
+
right: 0;
|
|
1174
|
+
box-sizing: border-box;
|
|
1175
|
+
overflow: hidden;
|
|
937
1176
|
display: flex;
|
|
938
|
-
align-items:
|
|
1177
|
+
align-items: flex-start;
|
|
939
1178
|
gap: 3px;
|
|
940
1179
|
padding: 2px 4px;
|
|
941
1180
|
border-radius: 3px;
|
|
@@ -946,8 +1185,6 @@ function onCellKeydown(e, ms) {
|
|
|
946
1185
|
transparent 3px,
|
|
947
1186
|
transparent 6px
|
|
948
1187
|
);
|
|
949
|
-
margin-bottom: 2px;
|
|
950
|
-
min-height: 14px;
|
|
951
1188
|
}
|
|
952
1189
|
|
|
953
1190
|
.wg-blocked-label {
|
|
@@ -965,12 +1202,13 @@ function onCellKeydown(e, ms) {
|
|
|
965
1202
|
|
|
966
1203
|
/* ─── Cell header (day label top-right) ──────────── */
|
|
967
1204
|
.wg-cell-hd {
|
|
1205
|
+
align-self: flex-end;
|
|
968
1206
|
display: flex;
|
|
969
|
-
align-items:
|
|
970
|
-
justify-content: flex-end;
|
|
1207
|
+
align-items: baseline;
|
|
971
1208
|
gap: 4px;
|
|
972
|
-
padding:
|
|
973
|
-
margin
|
|
1209
|
+
padding: 3px 7px;
|
|
1210
|
+
margin: 1px 0 2px;
|
|
1211
|
+
border-radius: 999px;
|
|
974
1212
|
}
|
|
975
1213
|
|
|
976
1214
|
.wg-day-wd {
|
|
@@ -984,11 +1222,6 @@ function onCellKeydown(e, ms) {
|
|
|
984
1222
|
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
985
1223
|
}
|
|
986
1224
|
|
|
987
|
-
.wg-cell-hd--today .wg-day-wd {
|
|
988
|
-
color: var(--dt-accent, #2563eb);
|
|
989
|
-
font-weight: 600;
|
|
990
|
-
}
|
|
991
|
-
|
|
992
1225
|
.wg-day-num {
|
|
993
1226
|
font: 700 14px / 1 var(--dt-sans, system-ui, sans-serif);
|
|
994
1227
|
color: var(--dt-text, rgba(0, 0, 0, 0.87));
|
|
@@ -998,9 +1231,13 @@ function onCellKeydown(e, ms) {
|
|
|
998
1231
|
color: var(--dt-text, rgba(0, 0, 0, 0.87));
|
|
999
1232
|
}
|
|
1000
1233
|
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1234
|
+
/* Today's pill — after the week rules above, which it must beat */
|
|
1235
|
+
.wg-cell-hd--today {
|
|
1236
|
+
background: var(--dt-accent, #2563eb);
|
|
1237
|
+
}
|
|
1238
|
+
.wg-cell-hd--today .wg-day-wd,
|
|
1239
|
+
.wg-cell-hd--today .wg-day-num {
|
|
1240
|
+
color: var(--dt-btn-text, #fff);
|
|
1004
1241
|
}
|
|
1005
1242
|
|
|
1006
1243
|
/* ─── All-day / multi-day events ─────────────────── */
|
|
@@ -1008,7 +1245,7 @@ function onCellKeydown(e, ms) {
|
|
|
1008
1245
|
display: flex;
|
|
1009
1246
|
flex-direction: column;
|
|
1010
1247
|
gap: var(--wg-chip-gap, 3px);
|
|
1011
|
-
margin-
|
|
1248
|
+
margin-top: var(--wg-chip-gap, 3px);
|
|
1012
1249
|
flex-shrink: 0;
|
|
1013
1250
|
}
|
|
1014
1251
|
|
|
@@ -1085,14 +1322,49 @@ function onCellKeydown(e, ms) {
|
|
|
1085
1322
|
}
|
|
1086
1323
|
|
|
1087
1324
|
/* ─── Events ─────────────────────────────────────── */
|
|
1325
|
+
/* The day's time axis: chips carry their own margin-top (chipTops), and
|
|
1326
|
+
the area fills the cell, so the all-day bars line up at the bottom. */
|
|
1088
1327
|
.wg-cell-events {
|
|
1089
1328
|
position: relative;
|
|
1090
1329
|
display: flex;
|
|
1091
1330
|
flex-direction: column;
|
|
1092
|
-
|
|
1331
|
+
flex: 1 0 auto;
|
|
1332
|
+
min-height: var(--wg-time-h, 100px);
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
.wg-now {
|
|
1336
|
+
position: absolute;
|
|
1337
|
+
left: 0;
|
|
1338
|
+
right: 0;
|
|
1339
|
+
z-index: 2;
|
|
1340
|
+
display: flex;
|
|
1341
|
+
align-items: center;
|
|
1342
|
+
gap: 3px;
|
|
1343
|
+
transform: translateY(-50%);
|
|
1344
|
+
pointer-events: none;
|
|
1345
|
+
}
|
|
1346
|
+
.wg-now::after {
|
|
1347
|
+
content: '';
|
|
1348
|
+
flex: 1;
|
|
1349
|
+
border-top: 1px dashed var(--dt-accent, #2563eb);
|
|
1350
|
+
}
|
|
1351
|
+
.wg-now-time {
|
|
1352
|
+
font: 500 9px / 1 var(--dt-sans, system-ui, sans-serif);
|
|
1353
|
+
color: var(--dt-accent, #2563eb);
|
|
1354
|
+
}
|
|
1355
|
+
/* Where a dragged-in class would land: the now-line's shape, solid */
|
|
1356
|
+
.wg-drop {
|
|
1357
|
+
z-index: 3;
|
|
1358
|
+
}
|
|
1359
|
+
.wg-drop::after {
|
|
1360
|
+
border-top-style: solid;
|
|
1361
|
+
}
|
|
1362
|
+
.wg-drop .wg-now-time {
|
|
1363
|
+
font-weight: 600;
|
|
1093
1364
|
}
|
|
1094
1365
|
|
|
1095
1366
|
.wg-ev {
|
|
1367
|
+
position: relative; /* above the blocked bands */
|
|
1096
1368
|
display: flex;
|
|
1097
1369
|
align-items: center;
|
|
1098
1370
|
flex-wrap: nowrap;
|
|
@@ -1113,7 +1385,9 @@ function onCellKeydown(e, ms) {
|
|
|
1113
1385
|
}
|
|
1114
1386
|
|
|
1115
1387
|
.wg-ev--drag-preview {
|
|
1116
|
-
position:
|
|
1388
|
+
position: absolute;
|
|
1389
|
+
left: 0;
|
|
1390
|
+
right: 0;
|
|
1117
1391
|
z-index: 8;
|
|
1118
1392
|
opacity: 0.95;
|
|
1119
1393
|
pointer-events: none;
|