@nomideusz/svelte-calendar 0.20.0 → 0.21.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 +3 -1
- package/dist/adapters/recurring.js +9 -9
- package/dist/core/locale.js +4 -4
- package/dist/core/time.js +4 -5
- package/dist/engine/event-store.svelte.js +2 -2
- package/dist/engine/view-state.svelte.js +3 -3
- package/dist/headless/create-agenda.svelte.js +6 -6
- package/dist/headless/create-calendar.svelte.js +4 -4
- package/dist/headless/create-range-agenda.svelte.js +6 -6
- package/dist/views/agenda/AgendaDay.svelte +3 -3
- package/dist/views/agenda/AgendaWeek.svelte +4 -4
- package/dist/views/mobile/MobileDay.svelte +2 -2
- package/dist/views/mobile/MobileWeek.svelte +3 -3
- package/dist/views/month/MonthGrid.svelte +5 -5
- package/dist/views/planner/PlannerScroll.svelte +285 -140
- package/dist/views/planner/PlannerWeek.svelte +2 -2
- package/package.json +1 -1
- package/widget/widget.js +425 -240
|
@@ -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,7 +20,7 @@ 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 { DAY_MS, HOUR_MS, sod } from "../../core/time.js";
|
|
23
|
+
import { DAY_MS, HOUR_MS, sod, addDaysMs } from "../../core/time.js";
|
|
22
24
|
import { startOfWeek as sowFn, isAllDay, isMultiDay, segmentForDay } from "../../core/time.js";
|
|
23
25
|
import { weekdayShort, monthLong, fmtTime as _fmtTime, getLabels } from "../../core/locale.js";
|
|
24
26
|
import { createChipFit } from "../shared/chip-fit.svelte.js";
|
|
@@ -61,12 +63,58 @@ const MAX_EVENTS_SHOWN = 5;
|
|
|
61
63
|
// mount re-centres once the events have landed.
|
|
62
64
|
const CHIP_H = 22;
|
|
63
65
|
const CHIP_GAP = 3;
|
|
64
|
-
|
|
66
|
+
/** The day's hours at their floor height — a quiet week is short, a busy
|
|
67
|
+
* day grows its row. Chips sit on this axis by start time. */
|
|
68
|
+
const TIME_H = 120;
|
|
65
69
|
const ROW_MARGIN = 12;
|
|
66
70
|
/** Distance between two rows' tops; only a fallback now. */
|
|
67
71
|
function rowPitch() {
|
|
68
72
|
const rows = el?.querySelectorAll("[data-week]");
|
|
69
|
-
return rows && rows.length > 1 ? rows[1].offsetTop - rows[0].offsetTop :
|
|
73
|
+
return rows && rows.length > 1 ? rows[1].offsetTop - rows[0].offsetTop : TIME_H + ROW_MARGIN;
|
|
74
|
+
}
|
|
75
|
+
// ─── Time axis ──────────────────────────────────
|
|
76
|
+
// Without visibleHours the axis spans the hours the loaded events start in:
|
|
77
|
+
// a timetable of 7:00–21:00 classes spread over 24 hours bunches them up.
|
|
78
|
+
const eventHours = $derived.by(() => {
|
|
79
|
+
let lo = 24;
|
|
80
|
+
let hi = -1;
|
|
81
|
+
for (const ev of events) {
|
|
82
|
+
if (isAllDay(ev) || isMultiDay(ev)) continue;
|
|
83
|
+
const h = ev.start.getHours() + ev.start.getMinutes() / 60;
|
|
84
|
+
lo = Math.min(lo, h);
|
|
85
|
+
hi = Math.max(hi, h);
|
|
86
|
+
}
|
|
87
|
+
return hi < 0 ? [0, 24] : [Math.floor(lo), Math.min(24, Math.floor(hi) + 1)];
|
|
88
|
+
});
|
|
89
|
+
const startHour = $derived(visibleHours?.[0] ?? eventHours[0]);
|
|
90
|
+
const endHour = $derived(visibleHours?.[1] ?? eventHours[1]);
|
|
91
|
+
/** Where a chip starting this far into the day sits, px from the axis top.
|
|
92
|
+
* A chip at the last hour still ends inside the floor height. */
|
|
93
|
+
function timeTop(msIntoDay) {
|
|
94
|
+
const f = (msIntoDay / HOUR_MS - startHour) / Math.max(1, endHour - startHour);
|
|
95
|
+
return Math.min(Math.max(f, 0), 1) * (TIME_H - CHIP_H);
|
|
96
|
+
}
|
|
97
|
+
/** Each chip at its start time, or just under the chip before it when that
|
|
98
|
+
* one is in the way: the order is time, the height is time, nothing overlaps. */
|
|
99
|
+
function chipTops(list, dayMs) {
|
|
100
|
+
const tops = [];
|
|
101
|
+
let min = 0;
|
|
102
|
+
for (const ev of list) {
|
|
103
|
+
const top = Math.max(timeTop(ev.start.getTime() - dayMs), min);
|
|
104
|
+
tops.push(top);
|
|
105
|
+
min = top + CHIP_H + CHIP_GAP;
|
|
106
|
+
}
|
|
107
|
+
return tops;
|
|
108
|
+
}
|
|
109
|
+
/** The now-line: at the clock's height (pinned to the axis ends outside
|
|
110
|
+
* it), but never above a chip that has already started — pushed-down
|
|
111
|
+
* chips would read as still to come. */
|
|
112
|
+
function nowTop(list, tops, dayMs) {
|
|
113
|
+
let y = timeTop(clock.tick - dayMs);
|
|
114
|
+
list.forEach((ev, i) => {
|
|
115
|
+
if (ev.start.getTime() <= clock.tick) y = Math.max(y, tops[i] + CHIP_H + CHIP_GAP / 2);
|
|
116
|
+
});
|
|
117
|
+
return y;
|
|
70
118
|
}
|
|
71
119
|
const _initMs = untrack(() => sod(focusDate?.getTime() ?? Date.now()));
|
|
72
120
|
let internalFocusMs = $state(_initMs);
|
|
@@ -80,7 +128,7 @@ function scrollWeekIntoContainer(targetMs, behavior = "auto") {
|
|
|
80
128
|
const rows = el.querySelectorAll("[data-week]");
|
|
81
129
|
for (const row of rows) {
|
|
82
130
|
const weekMs = Number(row.dataset.week);
|
|
83
|
-
if (weekMs <= targetMs && targetMs < weekMs
|
|
131
|
+
if (weekMs <= targetMs && targetMs < addDaysMs(weekMs, customDays)) {
|
|
84
132
|
target = row;
|
|
85
133
|
break;
|
|
86
134
|
}
|
|
@@ -104,8 +152,8 @@ const anchorPeriodStart = $derived(customDays === 7 ? sowFn(internalFocusMs, mon
|
|
|
104
152
|
// what range we need. Calendar's single $effect handles loading.
|
|
105
153
|
$effect(() => {
|
|
106
154
|
if (!loadRangeCtx) return;
|
|
107
|
-
const rangeStart = new Date(anchorPeriodStart -
|
|
108
|
-
const rangeEnd = new Date(anchorPeriodStart
|
|
155
|
+
const rangeStart = new Date(addDaysMs(anchorPeriodStart, -bufferBefore * customDays));
|
|
156
|
+
const rangeEnd = new Date(addDaysMs(anchorPeriodStart, (bufferAfter + 1) * customDays));
|
|
109
157
|
loadRangeCtx.set({
|
|
110
158
|
start: rangeStart,
|
|
111
159
|
end: rangeEnd
|
|
@@ -115,20 +163,20 @@ $effect(() => {
|
|
|
115
163
|
const weeks = $derived.by(() => {
|
|
116
164
|
const result = [];
|
|
117
165
|
for (let w = -bufferBefore; w <= bufferAfter; w++) {
|
|
118
|
-
|
|
119
|
-
|
|
166
|
+
// Calendar days, not 24 h steps: past a DST change a sum of DAY_MS
|
|
167
|
+
// lands at 23:00 the day before, and every later row shifts a day.
|
|
168
|
+
const periodStart = addDaysMs(anchorPeriodStart, w * customDays);
|
|
169
|
+
const isCurrent = todayMs >= periodStart && todayMs < addDaysMs(periodStart, customDays);
|
|
120
170
|
const days = [];
|
|
121
171
|
for (let d = 0; d < customDays; d++) {
|
|
122
|
-
const ms = periodStart
|
|
172
|
+
const ms = addDaysMs(periodStart, d);
|
|
123
173
|
const date = new Date(ms);
|
|
124
174
|
const dayNum = date.getDate();
|
|
125
175
|
const dow = date.getDay();
|
|
126
176
|
const isWeekend = dow === 0 || dow === 6;
|
|
127
177
|
const isToday = ms === todayMs;
|
|
128
178
|
const isPast = equalDays ? false : ms < todayMs;
|
|
129
|
-
const
|
|
130
|
-
const monthLabel = d === 0 || isFirstOfMonth ? monthLong(ms, locale).toUpperCase() : null;
|
|
131
|
-
const dayEnd = ms + DAY_MS;
|
|
179
|
+
const dayEnd = addDaysMs(ms, 1);
|
|
132
180
|
const dayEventsAll = events.filter((ev) => ev.start.getTime() < dayEnd && ev.end.getTime() > ms).sort((a, b) => a.start.getTime() - b.start.getTime());
|
|
133
181
|
// Separate all-day / multi-day from timed events
|
|
134
182
|
const timedEvents = [];
|
|
@@ -147,20 +195,16 @@ const weeks = $derived.by(() => {
|
|
|
147
195
|
isToday,
|
|
148
196
|
isPast,
|
|
149
197
|
isWeekend,
|
|
150
|
-
|
|
151
|
-
monthLabel,
|
|
198
|
+
monthLabel: null,
|
|
152
199
|
events: timedEvents,
|
|
153
200
|
allDaySegments
|
|
154
201
|
});
|
|
155
202
|
}
|
|
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
203
|
result.push({
|
|
161
204
|
weekStart: periodStart,
|
|
162
205
|
isCurrent,
|
|
163
|
-
monthLabel,
|
|
206
|
+
monthLabel: "",
|
|
207
|
+
monthStart: false,
|
|
164
208
|
days
|
|
165
209
|
});
|
|
166
210
|
}
|
|
@@ -175,6 +219,24 @@ const weeks = $derived.by(() => {
|
|
|
175
219
|
});
|
|
176
220
|
}
|
|
177
221
|
}
|
|
222
|
+
// Month labels, from the days actually shown — a hidden 1st hands its
|
|
223
|
+
// label to the next shown day. The gutter names the row's month; a month
|
|
224
|
+
// that begins after the row's first day is named where it begins.
|
|
225
|
+
for (const row of result) {
|
|
226
|
+
let prev = new Date(addDaysMs(row.weekStart, -1)).getMonth();
|
|
227
|
+
row.days.forEach((day, i) => {
|
|
228
|
+
const date = new Date(day.ms);
|
|
229
|
+
const starts = date.getMonth() !== prev;
|
|
230
|
+
prev = date.getMonth();
|
|
231
|
+
const label = monthLong(day.ms, locale).toUpperCase() + (starts ? ` ${date.getFullYear()}` : "");
|
|
232
|
+
if (i === 0) {
|
|
233
|
+
row.monthLabel = label;
|
|
234
|
+
row.monthStart = starts;
|
|
235
|
+
} else if (starts) {
|
|
236
|
+
day.monthLabel = label;
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
}
|
|
178
240
|
return result;
|
|
179
241
|
});
|
|
180
242
|
// ─── Format helpers ─────────────────────────────────
|
|
@@ -385,7 +447,7 @@ const previewKeySnapshot = new Map();
|
|
|
385
447
|
function dragPreviewTimedForDay(dayMs) {
|
|
386
448
|
const ev = dragPreviewEvent;
|
|
387
449
|
if (!ev || isAllDay(ev) || isMultiDay(ev)) return null;
|
|
388
|
-
const dayEnd = dayMs
|
|
450
|
+
const dayEnd = addDaysMs(dayMs, 1);
|
|
389
451
|
const hit = ev.start.getTime() < dayEnd && ev.end.getTime() > dayMs;
|
|
390
452
|
if (hit) previewKeySnapshot.set("timed", ev.id);
|
|
391
453
|
return hit ? ev : null;
|
|
@@ -398,10 +460,13 @@ function dragPreviewSegmentForDay(dayMs) {
|
|
|
398
460
|
return seg;
|
|
399
461
|
}
|
|
400
462
|
// ─── Chip labels ────────────────────────────────
|
|
401
|
-
// A chip is one line and holds more than fits
|
|
402
|
-
//
|
|
403
|
-
//
|
|
404
|
-
//
|
|
463
|
+
// A chip is one line and holds more than fits: it gives up the room, and
|
|
464
|
+
// only the room. The time stays whatever happens — this is a schedule, and
|
|
465
|
+
// "18:00 Hatha dla p…" answers more than an untruncated title does. (The
|
|
466
|
+
// time was briefly droppable, to buy the title room to fit whole. It
|
|
467
|
+
// usually bought nothing: a title long enough to push the time out was
|
|
468
|
+
// long enough to be cut anyway, so the chip lost its time for no gain.)
|
|
469
|
+
// createChipFit measures, fitParts decides.
|
|
405
470
|
const CHIP_PAD_X = 12;
|
|
406
471
|
const CHIP_GAP_X = 5;
|
|
407
472
|
const fit = createChipFit({
|
|
@@ -412,29 +477,31 @@ const fit = createChipFit({
|
|
|
412
477
|
room: ".wg-probe .wg-ev-loc"
|
|
413
478
|
}
|
|
414
479
|
});
|
|
415
|
-
|
|
480
|
+
/** A cell that carries a month label is this much narrower (.wg-cell--month). */
|
|
481
|
+
const MONTH_INSET = 22;
|
|
482
|
+
function chipParts(ev, inset = 0) {
|
|
416
483
|
return fit.parts([
|
|
417
|
-
{
|
|
418
|
-
key: "title",
|
|
419
|
-
text: ev.title,
|
|
420
|
-
font: fit.fonts.title,
|
|
421
|
-
priority: 0
|
|
422
|
-
},
|
|
423
484
|
{
|
|
424
485
|
key: "time",
|
|
425
486
|
text: fmtAmPm(ev.start),
|
|
426
487
|
font: fit.fonts.time,
|
|
427
|
-
priority:
|
|
488
|
+
priority: 0,
|
|
428
489
|
extra: CHIP_GAP_X
|
|
429
490
|
},
|
|
491
|
+
{
|
|
492
|
+
key: "title",
|
|
493
|
+
text: ev.title,
|
|
494
|
+
font: fit.fonts.title,
|
|
495
|
+
priority: 0
|
|
496
|
+
},
|
|
430
497
|
{
|
|
431
498
|
key: "room",
|
|
432
499
|
text: ev.location ?? "",
|
|
433
500
|
font: fit.fonts.room,
|
|
434
|
-
priority:
|
|
501
|
+
priority: 1,
|
|
435
502
|
extra: CHIP_GAP_X
|
|
436
503
|
}
|
|
437
|
-
], CHIP_PAD_X);
|
|
504
|
+
], CHIP_PAD_X + inset);
|
|
438
505
|
}
|
|
439
506
|
function getCellWidth() {
|
|
440
507
|
const cell = el?.querySelector(".wg-cell");
|
|
@@ -477,8 +544,8 @@ function updateDragFromPointer() {
|
|
|
477
544
|
const nowWeekMs = weekMsAtY(evLastY);
|
|
478
545
|
// A vertical row step spans one period (customDays), not always 7 days
|
|
479
546
|
const weekOffset = evStartWeekMs && nowWeekMs ? Math.round((nowWeekMs - evStartWeekMs) / (customDays * DAY_MS)) : 0;
|
|
480
|
-
const
|
|
481
|
-
drag.updatePointer(new Date(ev.start.getTime()
|
|
547
|
+
const deltaDays = dayOffset + weekOffset * customDays;
|
|
548
|
+
drag.updatePointer(new Date(addDaysMs(ev.start.getTime(), deltaDays)), new Date(addDaysMs(ev.end.getTime(), deltaDays)));
|
|
482
549
|
}
|
|
483
550
|
// ─── Auto-scroll under a drag ───────────────────────
|
|
484
551
|
// Weeks the target is in may be off-screen — without this, moving an event
|
|
@@ -574,15 +641,15 @@ function onCellKeydown(e, ms) {
|
|
|
574
641
|
return;
|
|
575
642
|
}
|
|
576
643
|
let step = 0;
|
|
577
|
-
if (e.key === "ArrowRight") step =
|
|
578
|
-
else if (e.key === "ArrowLeft") step = -
|
|
579
|
-
else if (e.key === "ArrowDown") step = customDays
|
|
580
|
-
else if (e.key === "ArrowUp") step = -customDays
|
|
644
|
+
if (e.key === "ArrowRight") step = 1;
|
|
645
|
+
else if (e.key === "ArrowLeft") step = -1;
|
|
646
|
+
else if (e.key === "ArrowDown") step = customDays;
|
|
647
|
+
else if (e.key === "ArrowUp") step = -customDays;
|
|
581
648
|
if (step === 0) return;
|
|
582
649
|
e.preventDefault();
|
|
583
650
|
// Walk in the step direction until a rendered cell is found
|
|
584
651
|
// (skips hidden days; bails at the buffer edge).
|
|
585
|
-
let target = ms
|
|
652
|
+
let target = addDaysMs(ms, step);
|
|
586
653
|
for (let i = 0; i < 7; i++) {
|
|
587
654
|
const cell = el?.querySelector(`[data-day="${target}"]`);
|
|
588
655
|
if (cell) {
|
|
@@ -591,7 +658,7 @@ function onCellKeydown(e, ms) {
|
|
|
591
658
|
cell.scrollIntoView({ block: "nearest" });
|
|
592
659
|
return;
|
|
593
660
|
}
|
|
594
|
-
target
|
|
661
|
+
target = addDaysMs(target, step < 0 ? -1 : 1);
|
|
595
662
|
}
|
|
596
663
|
}
|
|
597
664
|
</script>
|
|
@@ -608,8 +675,8 @@ function onCellKeydown(e, ms) {
|
|
|
608
675
|
{/if}
|
|
609
676
|
{/snippet}
|
|
610
677
|
|
|
611
|
-
{#snippet timedEventContent(ev: TimelineEvent)}
|
|
612
|
-
{@const parts = chipParts(ev)}
|
|
678
|
+
{#snippet timedEventContent(ev: TimelineEvent, inset: number)}
|
|
679
|
+
{@const parts = chipParts(ev, inset)}
|
|
613
680
|
<EventContent event={ev}>
|
|
614
681
|
{#if parts.time}<span class="wg-ev-time">{fmtAmPm(ev.start)}</span>{/if}
|
|
615
682
|
<span class="wg-ev-title">{ev.title}</span>
|
|
@@ -621,7 +688,7 @@ function onCellKeydown(e, ms) {
|
|
|
621
688
|
|
|
622
689
|
<svelte:window onkeydown={onWindowKeydown} />
|
|
623
690
|
|
|
624
|
-
<div class="wg" class:wg--auto={autoHeight} style={style || undefined} style:height={autoHeight ? undefined : (height ? `${height}px` : '100%')} style:--wg-
|
|
691
|
+
<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">
|
|
625
692
|
<div
|
|
626
693
|
class="wg-body"
|
|
627
694
|
bind:this={el}
|
|
@@ -638,6 +705,8 @@ function onCellKeydown(e, ms) {
|
|
|
638
705
|
</div>
|
|
639
706
|
{#each weeks as week (week.weekStart)}
|
|
640
707
|
<div class="wg-week" class:wg-week--current={week.isCurrent} data-week={week.weekStart} role="presentation">
|
|
708
|
+
<!-- Each cell's aria-label already names its date -->
|
|
709
|
+
<div class="wg-month" class:wg-month--start={week.monthStart} aria-hidden="true">{week.monthLabel}</div>
|
|
641
710
|
<div class="wg-week-body" role="presentation">
|
|
642
711
|
<!-- Day columns (header inside each cell) -->
|
|
643
712
|
<div class="wg-days" role="row">
|
|
@@ -649,8 +718,13 @@ function onCellKeydown(e, ms) {
|
|
|
649
718
|
{@const hiddenCount = Math.max(0, visibleTimedEvents.length - timedCap)}
|
|
650
719
|
{@const previewTimedEvent = dragPreviewTimedForDay(day.ms)}
|
|
651
720
|
{@const previewSegment = dragPreviewSegmentForDay(day.ms)}
|
|
721
|
+
{@const shownTimed = visibleTimedEvents.slice(0, isExpanded ? visibleTimedEvents.length : timedCap)}
|
|
722
|
+
{@const tops = chipTops(shownTimed, day.ms)}
|
|
723
|
+
{@const nowY = day.isToday ? nowTop(shownTimed, tops, day.ms) : null}
|
|
724
|
+
{@const inset = day.monthLabel ? MONTH_INSET : 0}
|
|
652
725
|
<div
|
|
653
726
|
class="wg-cell"
|
|
727
|
+
class:wg-cell--month={!!day.monthLabel}
|
|
654
728
|
class:wg-cell--today={day.isToday}
|
|
655
729
|
class:wg-cell--past={day.isPast}
|
|
656
730
|
class:wg-cell--weekend={day.isWeekend}
|
|
@@ -668,14 +742,15 @@ function onCellKeydown(e, ms) {
|
|
|
668
742
|
onfocus={() => { focusedCellMs = day.ms; }}
|
|
669
743
|
onkeydown={(e) => onCellKeydown(e, day.ms)}
|
|
670
744
|
>
|
|
671
|
-
|
|
745
|
+
{#if day.monthLabel}
|
|
746
|
+
<div class="wg-month wg-month--start wg-month--inset" aria-hidden="true">{day.monthLabel}</div>
|
|
747
|
+
{/if}
|
|
748
|
+
<!-- Day label in top-right corner: "MON 23", a pill today -->
|
|
672
749
|
<div class="wg-cell-hd" class:wg-cell-hd--today={day.isToday}>
|
|
750
|
+
<span class="wg-day-wd">{weekdayShort(day.ms, locale)}</span>
|
|
673
751
|
{#if showDates}
|
|
674
|
-
<span class="wg-day-num"
|
|
675
|
-
{day.dayNum}
|
|
676
|
-
</span>
|
|
752
|
+
<span class="wg-day-num">{day.dayNum}</span>
|
|
677
753
|
{/if}
|
|
678
|
-
<span class="wg-day-wd">{weekdayShort(day.ms, locale)}</span>
|
|
679
754
|
</div>
|
|
680
755
|
|
|
681
756
|
<!-- Custom day header snippet -->
|
|
@@ -685,27 +760,89 @@ function onCellKeydown(e, ms) {
|
|
|
685
760
|
</div>
|
|
686
761
|
{/if}
|
|
687
762
|
|
|
688
|
-
<!--
|
|
689
|
-
|
|
690
|
-
{
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
{#
|
|
694
|
-
{
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
763
|
+
<!-- Timed events, on the day's time axis -->
|
|
764
|
+
<div class="wg-cell-events">
|
|
765
|
+
{#if blockedSlots?.length}
|
|
766
|
+
{@const jsDay = new Date(day.ms).getDay()}
|
|
767
|
+
{@const isoDay = jsDay === 0 ? 7 : jsDay}
|
|
768
|
+
{#each blockedSlots as slot, i (i)}
|
|
769
|
+
{#if (!slot.day || slot.day === isoDay) && slot.end > startHour && slot.start < endHour}
|
|
770
|
+
{@const slotRange = `${_fmtTime(new Date(day.ms + slot.start * HOUR_MS), locale)} – ${_fmtTime(new Date(day.ms + slot.end * HOUR_MS), locale)}`}
|
|
771
|
+
{@const bandTop = timeTop(slot.start * HOUR_MS)}
|
|
772
|
+
{@const bandH = Math.max(14, timeTop(slot.end * HOUR_MS) - bandTop)}
|
|
773
|
+
<!-- A chip over the band would cut its label in half: the title keeps it -->
|
|
774
|
+
{@const covered = tops.some((t) => t < bandTop + bandH && t + CHIP_H > bandTop)}
|
|
775
|
+
<div
|
|
776
|
+
class="wg-blocked"
|
|
777
|
+
style:top="{bandTop}px"
|
|
778
|
+
style:height="{bandH}px"
|
|
779
|
+
title="{slot.label ? `${slot.label}, ` : ''}{slotRange}"
|
|
780
|
+
aria-label="{slot.label || 'Unavailable'}, {slotRange}"
|
|
781
|
+
>
|
|
782
|
+
{#if slot.label && !covered}
|
|
783
|
+
<span class="wg-blocked-label">{slot.label}</span>
|
|
784
|
+
{/if}
|
|
785
|
+
</div>
|
|
786
|
+
{/if}
|
|
787
|
+
{/each}
|
|
788
|
+
{/if}
|
|
789
|
+
{#each shownTimed as ev, i (ev.id)}
|
|
790
|
+
<!-- send/receive keyed by event id pair the card with the drag ghost:
|
|
791
|
+
drag start morphs card → ghost, drop morphs ghost → placed card -->
|
|
792
|
+
<div
|
|
793
|
+
animate:flip={{ duration: ANIM }}
|
|
794
|
+
in:previewReceive={{ key: ev.id }}
|
|
795
|
+
out:previewSend={{ key: ev.id }}
|
|
796
|
+
class="wg-ev"
|
|
797
|
+
class:wg-ev--selected={selectedEventId === ev.id}
|
|
798
|
+
class:wg-ev--current={ev.start.getTime() <= clock.tick && ev.end.getTime() > clock.tick}
|
|
799
|
+
class:wg-ev--dragging={evDragging && evDragId === ev.id}
|
|
800
|
+
class:wg-ev--readonly={ev.data?.readOnly}
|
|
801
|
+
class:wg-ev--cancelled={ev.status === 'cancelled'}
|
|
802
|
+
class:wg-ev--tentative={ev.status === 'tentative'}
|
|
803
|
+
class:wg-ev--full={ev.status === 'full'}
|
|
804
|
+
class:wg-ev--limited={ev.status === 'limited'}
|
|
805
|
+
style:--ev-color={ev.color ?? 'var(--dt-accent)'}
|
|
806
|
+
style:margin-top="{tops[i] - (i ? tops[i - 1] + CHIP_H : 0)}px"
|
|
807
|
+
role="button"
|
|
808
|
+
tabindex="0"
|
|
809
|
+
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})` : ''}"
|
|
810
|
+
onpointerdown={(e) => onEventPointerDown(e, ev)}
|
|
811
|
+
onpointerenter={() => oneventhover?.(ev)}
|
|
812
|
+
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); oneventclick?.(ev, e.currentTarget.getBoundingClientRect()); } }}
|
|
813
|
+
>
|
|
814
|
+
{@render timedEventContent(ev, inset)}
|
|
815
|
+
</div>
|
|
705
816
|
{/each}
|
|
706
|
-
|
|
817
|
+
{#if hiddenCount > 0}
|
|
818
|
+
<button
|
|
819
|
+
type="button"
|
|
820
|
+
class="wg-ev-more"
|
|
821
|
+
aria-expanded={isExpanded}
|
|
822
|
+
onclick={(e) => { e.stopPropagation(); expandedCells[day.ms] = !isExpanded; }}
|
|
823
|
+
>{isExpanded ? L.showLess : L.nMore(hiddenCount)}</button>
|
|
824
|
+
{/if}
|
|
825
|
+
{#if previewTimedEvent}
|
|
826
|
+
<!-- The ghost floats at its own time; it pushes nothing aside -->
|
|
827
|
+
<div
|
|
828
|
+
class="wg-ev wg-ev--drag-preview"
|
|
829
|
+
style:--ev-color={previewTimedEvent.color ?? 'var(--dt-accent)'}
|
|
830
|
+
style:top="{timeTop(previewTimedEvent.start.getTime() - day.ms)}px"
|
|
831
|
+
aria-hidden="true"
|
|
832
|
+
in:previewReceive={{ key: previewKeySnapshot.get('timed') ?? '' }}
|
|
833
|
+
out:previewSend={{ key: previewKeySnapshot.get('timed') ?? '' }}
|
|
834
|
+
>
|
|
835
|
+
{@render timedEventContent(previewTimedEvent, inset)}
|
|
836
|
+
</div>
|
|
837
|
+
{/if}
|
|
838
|
+
{#if nowY !== null}
|
|
839
|
+
<div class="wg-now" style:top="{nowY}px" aria-hidden="true">
|
|
840
|
+
<span class="wg-now-time">{fmtAmPm(new Date(clock.tick))}</span>
|
|
841
|
+
</div>
|
|
842
|
+
{/if}
|
|
843
|
+
</div>
|
|
707
844
|
|
|
708
|
-
<!-- All-day / multi-day events -->
|
|
845
|
+
<!-- All-day / multi-day events, under the day (as Hey) -->
|
|
709
846
|
{#if visibleAllDaySegments.length > 0 || previewSegment}
|
|
710
847
|
<div class="wg-allday">
|
|
711
848
|
{#each visibleAllDaySegments as seg (seg.ev.id)}
|
|
@@ -746,56 +883,6 @@ function onCellKeydown(e, ms) {
|
|
|
746
883
|
{/if}
|
|
747
884
|
</div>
|
|
748
885
|
{/if}
|
|
749
|
-
|
|
750
|
-
<!-- Timed events -->
|
|
751
|
-
<div class="wg-cell-events">
|
|
752
|
-
{#each visibleTimedEvents.slice(0, isExpanded ? visibleTimedEvents.length : timedCap) as ev (ev.id)}
|
|
753
|
-
<!-- send/receive keyed by event id pair the card with the drag ghost:
|
|
754
|
-
drag start morphs card → ghost, drop morphs ghost → placed card -->
|
|
755
|
-
<div
|
|
756
|
-
animate:flip={{ duration: ANIM }}
|
|
757
|
-
in:previewReceive={{ key: ev.id }}
|
|
758
|
-
out:previewSend={{ key: ev.id }}
|
|
759
|
-
class="wg-ev"
|
|
760
|
-
class:wg-ev--selected={selectedEventId === ev.id}
|
|
761
|
-
class:wg-ev--current={ev.start.getTime() <= clock.tick && ev.end.getTime() > clock.tick}
|
|
762
|
-
class:wg-ev--dragging={evDragging && evDragId === ev.id}
|
|
763
|
-
class:wg-ev--readonly={ev.data?.readOnly}
|
|
764
|
-
class:wg-ev--cancelled={ev.status === 'cancelled'}
|
|
765
|
-
class:wg-ev--tentative={ev.status === 'tentative'}
|
|
766
|
-
class:wg-ev--full={ev.status === 'full'}
|
|
767
|
-
class:wg-ev--limited={ev.status === 'limited'}
|
|
768
|
-
style:--ev-color={ev.color ?? 'var(--dt-accent)'}
|
|
769
|
-
role="button"
|
|
770
|
-
tabindex="0"
|
|
771
|
-
aria-label="{ev.title}, {fmtAmPm(ev.start)} – {fmtAmPm(ev.end)}{ev.status === 'cancelled' ? ` (cancelled)` : ''}{ev.status === 'tentative' ? ` (tentative)` : ''}{ev.status === 'full' ? ` (full)` : ''}{ev.status === 'limited' ? ` (limited)` : ''}{ev.start.getTime() <= clock.tick && ev.end.getTime() > clock.tick ? ` (${L.inProgress})` : ''}"
|
|
772
|
-
onpointerdown={(e) => onEventPointerDown(e, ev)}
|
|
773
|
-
onpointerenter={() => oneventhover?.(ev)}
|
|
774
|
-
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); oneventclick?.(ev, e.currentTarget.getBoundingClientRect()); } }}
|
|
775
|
-
>
|
|
776
|
-
{@render timedEventContent(ev)}
|
|
777
|
-
</div>
|
|
778
|
-
{/each}
|
|
779
|
-
{#if previewTimedEvent}
|
|
780
|
-
<div
|
|
781
|
-
class="wg-ev wg-ev--drag-preview"
|
|
782
|
-
style:--ev-color={previewTimedEvent.color ?? 'var(--dt-accent)'}
|
|
783
|
-
aria-hidden="true"
|
|
784
|
-
in:previewReceive={{ key: previewKeySnapshot.get('timed') ?? '' }}
|
|
785
|
-
out:previewSend={{ key: previewKeySnapshot.get('timed') ?? '' }}
|
|
786
|
-
>
|
|
787
|
-
{@render timedEventContent(previewTimedEvent)}
|
|
788
|
-
</div>
|
|
789
|
-
{/if}
|
|
790
|
-
</div>
|
|
791
|
-
{#if hiddenCount > 0}
|
|
792
|
-
<button
|
|
793
|
-
type="button"
|
|
794
|
-
class="wg-ev-more"
|
|
795
|
-
aria-expanded={isExpanded}
|
|
796
|
-
onclick={(e) => { e.stopPropagation(); expandedCells[day.ms] = !isExpanded; }}
|
|
797
|
-
>{isExpanded ? L.showLess : L.nMore(hiddenCount)}</button>
|
|
798
|
-
{/if}
|
|
799
886
|
</div>
|
|
800
887
|
{/each}
|
|
801
888
|
</div>
|
|
@@ -884,7 +971,6 @@ function onCellKeydown(e, ms) {
|
|
|
884
971
|
display: flex;
|
|
885
972
|
flex-direction: column;
|
|
886
973
|
min-width: 90px;
|
|
887
|
-
min-height: var(--wg-row-min, 170px);
|
|
888
974
|
box-sizing: border-box;
|
|
889
975
|
padding: 4px 4px 8px;
|
|
890
976
|
border-right: 1px solid var(--dt-border, rgba(0, 0, 0, 0.08));
|
|
@@ -893,6 +979,34 @@ function onCellKeydown(e, ms) {
|
|
|
893
979
|
}
|
|
894
980
|
|
|
895
981
|
.wg-cell:last-child { border-right: none; }
|
|
982
|
+
/* The inset is on the contents, not the cell's padding: flex adds padding
|
|
983
|
+
to a basis-0 cell, and that one column would come out wider. */
|
|
984
|
+
.wg-cell--month .wg-cell-events,
|
|
985
|
+
.wg-cell--month .wg-allday { margin-left: var(--wg-month-inset, 22px); }
|
|
986
|
+
|
|
987
|
+
/* ─── Month labels (vertical, read bottom-to-top) ─── */
|
|
988
|
+
.wg-month {
|
|
989
|
+
flex: 0 0 22px;
|
|
990
|
+
writing-mode: vertical-rl;
|
|
991
|
+
transform: rotate(180deg);
|
|
992
|
+
text-align: end; /* the rotation puts the end at the top */
|
|
993
|
+
padding: 6px 0;
|
|
994
|
+
font: 700 11px / 22px var(--dt-sans, system-ui, sans-serif);
|
|
995
|
+
letter-spacing: 0.08em;
|
|
996
|
+
white-space: nowrap;
|
|
997
|
+
overflow: hidden;
|
|
998
|
+
color: var(--dt-text-3, rgba(0, 0, 0, 0.38));
|
|
999
|
+
}
|
|
1000
|
+
.wg-week--current .wg-month,
|
|
1001
|
+
.wg-month--start {
|
|
1002
|
+
color: var(--dt-text, rgba(0, 0, 0, 0.87));
|
|
1003
|
+
}
|
|
1004
|
+
.wg-month--inset {
|
|
1005
|
+
position: absolute;
|
|
1006
|
+
top: 0;
|
|
1007
|
+
left: 2px;
|
|
1008
|
+
pointer-events: none;
|
|
1009
|
+
}
|
|
896
1010
|
.wg-cell:hover { background: var(--dt-hover, rgba(0, 0, 0, 0.015)); }
|
|
897
1011
|
|
|
898
1012
|
.wg-cell--today { background: var(--dt-today-bg, rgba(37, 99, 235, 0.04)); }
|
|
@@ -930,9 +1044,15 @@ function onCellKeydown(e, ms) {
|
|
|
930
1044
|
}
|
|
931
1045
|
|
|
932
1046
|
/* ─── Blocked slot indicator ─────────────────────── */
|
|
1047
|
+
/* A band on the time axis, behind the chips */
|
|
933
1048
|
.wg-blocked {
|
|
1049
|
+
position: absolute;
|
|
1050
|
+
left: 0;
|
|
1051
|
+
right: 0;
|
|
1052
|
+
box-sizing: border-box;
|
|
1053
|
+
overflow: hidden;
|
|
934
1054
|
display: flex;
|
|
935
|
-
align-items:
|
|
1055
|
+
align-items: flex-start;
|
|
936
1056
|
gap: 3px;
|
|
937
1057
|
padding: 2px 4px;
|
|
938
1058
|
border-radius: 3px;
|
|
@@ -943,8 +1063,6 @@ function onCellKeydown(e, ms) {
|
|
|
943
1063
|
transparent 3px,
|
|
944
1064
|
transparent 6px
|
|
945
1065
|
);
|
|
946
|
-
margin-bottom: 2px;
|
|
947
|
-
min-height: 14px;
|
|
948
1066
|
}
|
|
949
1067
|
|
|
950
1068
|
.wg-blocked-label {
|
|
@@ -962,12 +1080,13 @@ function onCellKeydown(e, ms) {
|
|
|
962
1080
|
|
|
963
1081
|
/* ─── Cell header (day label top-right) ──────────── */
|
|
964
1082
|
.wg-cell-hd {
|
|
1083
|
+
align-self: flex-end;
|
|
965
1084
|
display: flex;
|
|
966
|
-
align-items:
|
|
967
|
-
justify-content: flex-end;
|
|
1085
|
+
align-items: baseline;
|
|
968
1086
|
gap: 4px;
|
|
969
|
-
padding:
|
|
970
|
-
margin
|
|
1087
|
+
padding: 3px 7px;
|
|
1088
|
+
margin: 1px 0 2px;
|
|
1089
|
+
border-radius: 999px;
|
|
971
1090
|
}
|
|
972
1091
|
|
|
973
1092
|
.wg-day-wd {
|
|
@@ -981,11 +1100,6 @@ function onCellKeydown(e, ms) {
|
|
|
981
1100
|
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
982
1101
|
}
|
|
983
1102
|
|
|
984
|
-
.wg-cell-hd--today .wg-day-wd {
|
|
985
|
-
color: var(--dt-accent, #2563eb);
|
|
986
|
-
font-weight: 600;
|
|
987
|
-
}
|
|
988
|
-
|
|
989
1103
|
.wg-day-num {
|
|
990
1104
|
font: 700 14px / 1 var(--dt-sans, system-ui, sans-serif);
|
|
991
1105
|
color: var(--dt-text, rgba(0, 0, 0, 0.87));
|
|
@@ -995,9 +1109,13 @@ function onCellKeydown(e, ms) {
|
|
|
995
1109
|
color: var(--dt-text, rgba(0, 0, 0, 0.87));
|
|
996
1110
|
}
|
|
997
1111
|
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1112
|
+
/* Today's pill — after the week rules above, which it must beat */
|
|
1113
|
+
.wg-cell-hd--today {
|
|
1114
|
+
background: var(--dt-accent, #2563eb);
|
|
1115
|
+
}
|
|
1116
|
+
.wg-cell-hd--today .wg-day-wd,
|
|
1117
|
+
.wg-cell-hd--today .wg-day-num {
|
|
1118
|
+
color: var(--dt-btn-text, #fff);
|
|
1001
1119
|
}
|
|
1002
1120
|
|
|
1003
1121
|
/* ─── All-day / multi-day events ─────────────────── */
|
|
@@ -1005,7 +1123,7 @@ function onCellKeydown(e, ms) {
|
|
|
1005
1123
|
display: flex;
|
|
1006
1124
|
flex-direction: column;
|
|
1007
1125
|
gap: var(--wg-chip-gap, 3px);
|
|
1008
|
-
margin-
|
|
1126
|
+
margin-top: var(--wg-chip-gap, 3px);
|
|
1009
1127
|
flex-shrink: 0;
|
|
1010
1128
|
}
|
|
1011
1129
|
|
|
@@ -1082,14 +1200,39 @@ function onCellKeydown(e, ms) {
|
|
|
1082
1200
|
}
|
|
1083
1201
|
|
|
1084
1202
|
/* ─── Events ─────────────────────────────────────── */
|
|
1203
|
+
/* The day's time axis: chips carry their own margin-top (chipTops), and
|
|
1204
|
+
the area fills the cell, so the all-day bars line up at the bottom. */
|
|
1085
1205
|
.wg-cell-events {
|
|
1086
1206
|
position: relative;
|
|
1087
1207
|
display: flex;
|
|
1088
1208
|
flex-direction: column;
|
|
1089
|
-
|
|
1209
|
+
flex: 1 0 auto;
|
|
1210
|
+
min-height: var(--wg-time-h, 100px);
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
.wg-now {
|
|
1214
|
+
position: absolute;
|
|
1215
|
+
left: 0;
|
|
1216
|
+
right: 0;
|
|
1217
|
+
z-index: 2;
|
|
1218
|
+
display: flex;
|
|
1219
|
+
align-items: center;
|
|
1220
|
+
gap: 3px;
|
|
1221
|
+
transform: translateY(-50%);
|
|
1222
|
+
pointer-events: none;
|
|
1223
|
+
}
|
|
1224
|
+
.wg-now::after {
|
|
1225
|
+
content: '';
|
|
1226
|
+
flex: 1;
|
|
1227
|
+
border-top: 1px dashed var(--dt-accent, #2563eb);
|
|
1228
|
+
}
|
|
1229
|
+
.wg-now-time {
|
|
1230
|
+
font: 500 9px / 1 var(--dt-sans, system-ui, sans-serif);
|
|
1231
|
+
color: var(--dt-accent, #2563eb);
|
|
1090
1232
|
}
|
|
1091
1233
|
|
|
1092
1234
|
.wg-ev {
|
|
1235
|
+
position: relative; /* above the blocked bands */
|
|
1093
1236
|
display: flex;
|
|
1094
1237
|
align-items: center;
|
|
1095
1238
|
flex-wrap: nowrap;
|
|
@@ -1110,7 +1253,9 @@ function onCellKeydown(e, ms) {
|
|
|
1110
1253
|
}
|
|
1111
1254
|
|
|
1112
1255
|
.wg-ev--drag-preview {
|
|
1113
|
-
position:
|
|
1256
|
+
position: absolute;
|
|
1257
|
+
left: 0;
|
|
1258
|
+
right: 0;
|
|
1114
1259
|
z-index: 8;
|
|
1115
1260
|
opacity: 0.95;
|
|
1116
1261
|
pointer-events: none;
|