@nomideusz/svelte-calendar 0.14.1 → 0.15.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/dist/calendar/Calendar.svelte +450 -372
- package/dist/calendar/Calendar.svelte.d.ts +2 -1
- package/dist/headless/types.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/primitives/DayHeader.svelte +1 -7
- package/dist/primitives/EmptySlot.svelte +8 -10
- package/dist/primitives/EventBlock.svelte +12 -22
- package/dist/primitives/FloatingPanel.svelte +191 -0
- package/dist/primitives/FloatingPanel.svelte.d.ts +23 -0
- package/dist/primitives/NowIndicator.svelte +2 -13
- package/dist/primitives/TimeGutter.svelte +1 -7
- package/dist/primitives/index.d.ts +1 -0
- package/dist/primitives/index.js +1 -0
- package/dist/views/agenda/AgendaDay.svelte +68 -45
- package/dist/views/agenda/AgendaWeek.svelte +120 -105
- package/dist/views/mobile/MobileDay.svelte +313 -276
- package/dist/views/mobile/MobileWeek.svelte +91 -84
- package/dist/views/month/MonthGrid.svelte +101 -104
- package/dist/views/planner/PlannerWeek.svelte +475 -407
- package/dist/views/planner/PlannerWeek.svelte.d.ts +1 -1
- package/dist/widget/CalendarWidget.svelte +64 -83
- package/package.json +17 -10
- package/widget/widget.js +12723 -12203
|
@@ -123,7 +123,8 @@ interface Props {
|
|
|
123
123
|
* Receives context: { prev, next, goToday, isViewOnToday, focusDate, mode }.
|
|
124
124
|
*/
|
|
125
125
|
navigation?: Snippet<[import('../headless/types.js').NavigationContext]>;
|
|
126
|
-
|
|
126
|
+
/** `anchor` is the clicked block's viewport rect where a view has one (planner) — for positioning a FloatingPanel. */
|
|
127
|
+
oneventclick?: (event: TimelineEvent, anchor?: DOMRect) => void;
|
|
127
128
|
oneventcreate?: (range: {
|
|
128
129
|
start: Date;
|
|
129
130
|
end: Date;
|
package/dist/headless/types.d.ts
CHANGED
|
@@ -46,7 +46,7 @@ export interface HeadlessCalendarOptions {
|
|
|
46
46
|
minDuration?: number;
|
|
47
47
|
/** Maximum event duration in minutes */
|
|
48
48
|
maxDuration?: number;
|
|
49
|
-
oneventclick?: (event: TimelineEvent) => void;
|
|
49
|
+
oneventclick?: (event: TimelineEvent, anchor?: DOMRect) => void;
|
|
50
50
|
oneventcreate?: (range: {
|
|
51
51
|
start: Date;
|
|
52
52
|
end: Date;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { NowIndicator, EventBlock, TimeGutter, DayHeader, EmptySlot, } from './primitives/index.js';
|
|
1
|
+
export { NowIndicator, EventBlock, TimeGutter, DayHeader, EmptySlot, FloatingPanel, } from './primitives/index.js';
|
|
2
2
|
export { Calendar } from './calendar/index.js';
|
|
3
3
|
export { Planner, Agenda, Mobile } from './views/index.js';
|
|
4
4
|
export { default as MonthGrid } from './views/month/MonthGrid.svelte';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// ─── Primitives ─────────────────────────────────────────
|
|
2
|
-
export { NowIndicator, EventBlock, TimeGutter, DayHeader, EmptySlot, } from './primitives/index.js';
|
|
2
|
+
export { NowIndicator, EventBlock, TimeGutter, DayHeader, EmptySlot, FloatingPanel, } from './primitives/index.js';
|
|
3
3
|
// ─── Calendar shell ─────────────────────────────────────
|
|
4
4
|
export { Calendar } from './calendar/index.js';
|
|
5
5
|
// Raw view components — compose your own shell around the engine if needed
|
|
@@ -6,13 +6,7 @@
|
|
|
6
6
|
- Short/long weekday + date number for week views
|
|
7
7
|
-->
|
|
8
8
|
<script lang="ts">import { weekdayShort, weekdayLong, monthShort, fmtDay } from "../core/locale.js";
|
|
9
|
-
let {
|
|
10
|
-
dayMs,
|
|
11
|
-
todayMs = Date.now(),
|
|
12
|
-
format = "short",
|
|
13
|
-
isToday = false,
|
|
14
|
-
isPast = false
|
|
15
|
-
} = $props();
|
|
9
|
+
let { dayMs, todayMs = Date.now(), format = "short", isToday = false, isPast = false } = $props();
|
|
16
10
|
const dayNum = $derived(new Date(dayMs).getDate());
|
|
17
11
|
</script>
|
|
18
12
|
|
|
@@ -7,18 +7,16 @@
|
|
|
7
7
|
-->
|
|
8
8
|
<script lang="ts">import { fmtTime, fmtDuration, getLabels } from "../core/locale.js";
|
|
9
9
|
const L = $derived(getLabels());
|
|
10
|
-
let {
|
|
11
|
-
start,
|
|
12
|
-
end,
|
|
13
|
-
onclick,
|
|
14
|
-
orientation = "vertical"
|
|
15
|
-
} = $props();
|
|
10
|
+
let { start, end, onclick, orientation = "vertical" } = $props();
|
|
16
11
|
const dur = $derived(`${fmtDuration(start, end)} ${L.free}`);
|
|
17
12
|
function handleKeydown(e) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
13
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
14
|
+
e.preventDefault();
|
|
15
|
+
onclick?.({
|
|
16
|
+
start,
|
|
17
|
+
end
|
|
18
|
+
});
|
|
19
|
+
}
|
|
22
20
|
}
|
|
23
21
|
</script>
|
|
24
22
|
|
|
@@ -10,36 +10,26 @@
|
|
|
10
10
|
-->
|
|
11
11
|
<script lang="ts">import { fmtTime, fmtDuration, getLabels } from "../core/locale.js";
|
|
12
12
|
const L = $derived(getLabels());
|
|
13
|
-
let {
|
|
14
|
-
event,
|
|
15
|
-
variant = "chip",
|
|
16
|
-
active = false,
|
|
17
|
-
past = false,
|
|
18
|
-
showTime = false,
|
|
19
|
-
showDuration = false,
|
|
20
|
-
editable = false,
|
|
21
|
-
onclick,
|
|
22
|
-
children
|
|
23
|
-
} = $props();
|
|
13
|
+
let { event, variant = "chip", active = false, past = false, showTime = false, showDuration = false, editable = false, onclick, children } = $props();
|
|
24
14
|
const accentColor = $derived(event.color || "var(--dt-accent, #2563eb)");
|
|
25
15
|
const isCancelled = $derived(event.status === "cancelled");
|
|
26
16
|
const isTentative = $derived(event.status === "tentative");
|
|
27
17
|
const isFull = $derived(event.status === "full");
|
|
28
18
|
const isLimited = $derived(event.status === "limited");
|
|
29
19
|
const ariaLabel = $derived.by(() => {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
20
|
+
const t = event.title;
|
|
21
|
+
const time = `${fmtTime(event.start)} to ${fmtTime(event.end)}`;
|
|
22
|
+
const dur = fmtDuration(event.start, event.end);
|
|
23
|
+
const loc = event.location ? `, ${event.location}` : "";
|
|
24
|
+
const statusStr = isCancelled ? ", cancelled" : isTentative ? ", tentative" : isFull ? ", full" : isLimited ? ", limited" : "";
|
|
25
|
+
const activeStr = active ? `, ${L.happeningNow}` : past ? `, ${L.past}` : "";
|
|
26
|
+
return `${t}${loc}, ${time}, ${dur}${statusStr}${activeStr}`;
|
|
37
27
|
});
|
|
38
28
|
function handleKeydown(e) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
29
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
30
|
+
e.preventDefault();
|
|
31
|
+
onclick?.(event);
|
|
32
|
+
}
|
|
43
33
|
}
|
|
44
34
|
</script>
|
|
45
35
|
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
<script module lang="ts">// Phone sheet scroll-lock: nested panels each take a lock; the page behind
|
|
2
|
+
// unlocks when the last one closes.
|
|
3
|
+
let sheetLocks = 0;
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<script lang="ts">let { title = "", anchor = {
|
|
7
|
+
x: 24,
|
|
8
|
+
y: 24
|
|
9
|
+
}, width = 440, theme = "", closeLabel = "Close", closeOnOutside = true, onclose, children } = $props();
|
|
10
|
+
const GAP = 10;
|
|
11
|
+
const EDGE = 8;
|
|
12
|
+
let el = $state(null);
|
|
13
|
+
// Placed by the effect below once the panel has a size to clamp against.
|
|
14
|
+
let left = $state(EDGE);
|
|
15
|
+
let top = $state(EDGE);
|
|
16
|
+
const clamp = (v, size, max) => Math.max(EDGE, Math.min(v, max - size - EDGE));
|
|
17
|
+
function place(px, py) {
|
|
18
|
+
if (!el) return;
|
|
19
|
+
left = clamp(px, el.offsetWidth, window.innerWidth);
|
|
20
|
+
top = clamp(py, el.offsetHeight, window.innerHeight);
|
|
21
|
+
}
|
|
22
|
+
// Beside the anchor: to its right, else to its left, else over it.
|
|
23
|
+
function beside(a) {
|
|
24
|
+
if (!el) return;
|
|
25
|
+
const w = el.offsetWidth;
|
|
26
|
+
const aw = a.width ?? 0;
|
|
27
|
+
const right = a.x + aw + GAP;
|
|
28
|
+
const px = right + w + EDGE <= window.innerWidth ? right : a.x - GAP - w >= EDGE ? a.x - GAP - w : a.x;
|
|
29
|
+
place(px, a.y);
|
|
30
|
+
}
|
|
31
|
+
// A new anchor (the next click) re-places the panel; each run focuses it.
|
|
32
|
+
$effect(() => {
|
|
33
|
+
beside(anchor);
|
|
34
|
+
el?.focus({ preventScroll: true });
|
|
35
|
+
});
|
|
36
|
+
// In sheet mode the page behind must not scroll — drags on the sheet
|
|
37
|
+
// chrome, or reaching the end of the sheet body, would otherwise reach it.
|
|
38
|
+
$effect(() => {
|
|
39
|
+
if (typeof window === "undefined") return;
|
|
40
|
+
if (!window.matchMedia("(max-width: 640px)").matches) return;
|
|
41
|
+
document.body.classList.add("fp-sheet-lock");
|
|
42
|
+
document.documentElement.classList.add("fp-sheet-lock");
|
|
43
|
+
sheetLocks++;
|
|
44
|
+
return () => {
|
|
45
|
+
sheetLocks--;
|
|
46
|
+
if (sheetLocks === 0) {
|
|
47
|
+
document.body.classList.remove("fp-sheet-lock");
|
|
48
|
+
document.documentElement.classList.remove("fp-sheet-lock");
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
let grab = null;
|
|
53
|
+
function down(e) {
|
|
54
|
+
if (e.button !== 0 || e.target.closest("button")) return;
|
|
55
|
+
grab = {
|
|
56
|
+
dx: e.clientX - left,
|
|
57
|
+
dy: e.clientY - top
|
|
58
|
+
};
|
|
59
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
60
|
+
}
|
|
61
|
+
function move(e) {
|
|
62
|
+
if (grab) place(e.clientX - grab.dx, e.clientY - grab.dy);
|
|
63
|
+
}
|
|
64
|
+
function up() {
|
|
65
|
+
grab = null;
|
|
66
|
+
}
|
|
67
|
+
// The outside pointerdown is swallowed here, before any calendar handler,
|
|
68
|
+
// and so is the click it would synthesize — one tap closes, the next acts.
|
|
69
|
+
let swallowClick = false;
|
|
70
|
+
function outsideDown(e) {
|
|
71
|
+
if (!closeOnOutside || !el || el.contains(e.target)) return;
|
|
72
|
+
e.stopPropagation();
|
|
73
|
+
e.preventDefault();
|
|
74
|
+
swallowClick = true;
|
|
75
|
+
onclose?.();
|
|
76
|
+
}
|
|
77
|
+
function outsideClick(e) {
|
|
78
|
+
if (!swallowClick) return;
|
|
79
|
+
swallowClick = false;
|
|
80
|
+
e.stopPropagation();
|
|
81
|
+
e.preventDefault();
|
|
82
|
+
}
|
|
83
|
+
export {};
|
|
84
|
+
</script>
|
|
85
|
+
|
|
86
|
+
<svelte:window
|
|
87
|
+
onkeydown={(e) => e.key === 'Escape' && onclose?.()}
|
|
88
|
+
onpointerdowncapture={outsideDown}
|
|
89
|
+
onclickcapture={outsideClick}
|
|
90
|
+
/>
|
|
91
|
+
|
|
92
|
+
<div
|
|
93
|
+
class="fp"
|
|
94
|
+
role="dialog"
|
|
95
|
+
aria-label={title}
|
|
96
|
+
tabindex="-1"
|
|
97
|
+
bind:this={el}
|
|
98
|
+
style="{theme}; left: {left}px; top: {top}px; width: {width}px"
|
|
99
|
+
>
|
|
100
|
+
<div class="fp-head" role="presentation" onpointerdown={down} onpointermove={move} onpointerup={up} onpointercancel={up}>
|
|
101
|
+
<span class="fp-title">{title}</span>
|
|
102
|
+
<button class="fp-close" type="button" aria-label={closeLabel} onclick={onclose}>
|
|
103
|
+
<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true">
|
|
104
|
+
<path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" />
|
|
105
|
+
</svg>
|
|
106
|
+
</button>
|
|
107
|
+
</div>
|
|
108
|
+
<div class="fp-body">{@render children()}</div>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<style>
|
|
112
|
+
.fp {
|
|
113
|
+
position: fixed;
|
|
114
|
+
z-index: 60;
|
|
115
|
+
max-width: calc(100vw - 16px);
|
|
116
|
+
max-height: calc(100vh - 16px);
|
|
117
|
+
display: flex;
|
|
118
|
+
flex-direction: column;
|
|
119
|
+
background: var(--dt-surface, #fff);
|
|
120
|
+
color: var(--dt-text, #111);
|
|
121
|
+
border: 1px solid var(--dt-border, #e2e2e2);
|
|
122
|
+
border-radius: var(--dt-radius, 8px);
|
|
123
|
+
box-shadow: var(--dt-shadow, 0 16px 48px rgba(0, 0, 0, 0.18));
|
|
124
|
+
font-family: var(--dt-sans, system-ui, sans-serif);
|
|
125
|
+
outline: none;
|
|
126
|
+
}
|
|
127
|
+
.fp-head {
|
|
128
|
+
display: flex;
|
|
129
|
+
align-items: center;
|
|
130
|
+
gap: 8px;
|
|
131
|
+
padding: 8px 8px 8px 14px;
|
|
132
|
+
border-bottom: 1px solid var(--dt-border, #e2e2e2);
|
|
133
|
+
cursor: grab;
|
|
134
|
+
user-select: none;
|
|
135
|
+
touch-action: none;
|
|
136
|
+
}
|
|
137
|
+
.fp-head:active {
|
|
138
|
+
cursor: grabbing;
|
|
139
|
+
}
|
|
140
|
+
.fp-title {
|
|
141
|
+
flex: 1;
|
|
142
|
+
min-width: 0;
|
|
143
|
+
overflow: hidden;
|
|
144
|
+
text-overflow: ellipsis;
|
|
145
|
+
white-space: nowrap;
|
|
146
|
+
font-weight: 600;
|
|
147
|
+
}
|
|
148
|
+
.fp-close {
|
|
149
|
+
display: inline-grid;
|
|
150
|
+
place-items: center;
|
|
151
|
+
width: 32px;
|
|
152
|
+
height: 32px;
|
|
153
|
+
border: 0;
|
|
154
|
+
border-radius: 6px;
|
|
155
|
+
background: transparent;
|
|
156
|
+
color: var(--dt-text-2, #555);
|
|
157
|
+
cursor: pointer;
|
|
158
|
+
}
|
|
159
|
+
.fp-close:hover {
|
|
160
|
+
background: var(--dt-hover, rgba(0, 0, 0, 0.06));
|
|
161
|
+
color: var(--dt-text, #111);
|
|
162
|
+
}
|
|
163
|
+
.fp-body {
|
|
164
|
+
/* A flex child defaults to min-height:auto and never shrinks below its
|
|
165
|
+
content — so a tall form ran past the panel's max-height, into the
|
|
166
|
+
part of a fixed element nothing can scroll to. This is what lets the
|
|
167
|
+
body scroll instead. */
|
|
168
|
+
flex: 1 1 auto;
|
|
169
|
+
min-height: 0;
|
|
170
|
+
overflow: auto;
|
|
171
|
+
overscroll-behavior: contain;
|
|
172
|
+
}
|
|
173
|
+
:global(.fp-sheet-lock) {
|
|
174
|
+
overflow: hidden !important;
|
|
175
|
+
}
|
|
176
|
+
@media (max-width: 640px) {
|
|
177
|
+
.fp {
|
|
178
|
+
left: 0 !important;
|
|
179
|
+
right: 0;
|
|
180
|
+
top: auto !important;
|
|
181
|
+
bottom: 0;
|
|
182
|
+
width: auto !important;
|
|
183
|
+
max-width: none;
|
|
184
|
+
max-height: 85vh;
|
|
185
|
+
border-radius: var(--dt-radius, 12px) var(--dt-radius, 12px) 0 0;
|
|
186
|
+
}
|
|
187
|
+
.fp-head {
|
|
188
|
+
cursor: default;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
</style>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type Anchor = {
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
width?: number;
|
|
6
|
+
height?: number;
|
|
7
|
+
};
|
|
8
|
+
type $$ComponentProps = {
|
|
9
|
+
title?: string;
|
|
10
|
+
/** What to open beside: the clicked block's viewport rect, or a bare point. */
|
|
11
|
+
anchor?: Anchor;
|
|
12
|
+
width?: number;
|
|
13
|
+
/** The same `--dt-*` theme string a Calendar takes. */
|
|
14
|
+
theme?: string;
|
|
15
|
+
closeLabel?: string;
|
|
16
|
+
/** A pointerdown outside closes the panel and goes no further. */
|
|
17
|
+
closeOnOutside?: boolean;
|
|
18
|
+
onclose?: () => void;
|
|
19
|
+
children: Snippet;
|
|
20
|
+
};
|
|
21
|
+
declare const FloatingPanel: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
22
|
+
type FloatingPanel = ReturnType<typeof FloatingPanel>;
|
|
23
|
+
export default FloatingPanel;
|
|
@@ -13,19 +13,8 @@
|
|
|
13
13
|
-->
|
|
14
14
|
<script lang="ts">import { getLabels } from "../core/locale.js";
|
|
15
15
|
const L = $derived(getLabels());
|
|
16
|
-
let {
|
|
17
|
-
|
|
18
|
-
position = 0,
|
|
19
|
-
orientation = "vertical",
|
|
20
|
-
time = "",
|
|
21
|
-
seconds = "",
|
|
22
|
-
showLabel = true,
|
|
23
|
-
color,
|
|
24
|
-
children
|
|
25
|
-
} = $props();
|
|
26
|
-
const posStyle = $derived(
|
|
27
|
-
orientation === "vertical" ? `left: ${position}px` : `top: ${position}px`
|
|
28
|
-
);
|
|
16
|
+
let { mode = "line", position = 0, orientation = "vertical", time = "", seconds = "", showLabel = true, color, children } = $props();
|
|
17
|
+
const posStyle = $derived(orientation === "vertical" ? `left: ${position}px` : `top: ${position}px`);
|
|
29
18
|
const colorVar = $derived(color ? `--ni-color: ${color}` : "");
|
|
30
19
|
</script>
|
|
31
20
|
|
|
@@ -7,13 +7,7 @@
|
|
|
7
7
|
-->
|
|
8
8
|
<script lang="ts">import { HOURS } from "../core/time.js";
|
|
9
9
|
import { fmtH } from "../core/locale.js";
|
|
10
|
-
let {
|
|
11
|
-
orientation = "horizontal",
|
|
12
|
-
hourSize = 120,
|
|
13
|
-
halfHour = true,
|
|
14
|
-
hours = HOURS,
|
|
15
|
-
formatHour = fmtH
|
|
16
|
-
} = $props();
|
|
10
|
+
let { orientation = "horizontal", hourSize = 120, halfHour = true, hours = HOURS, formatHour = fmtH } = $props();
|
|
17
11
|
</script>
|
|
18
12
|
|
|
19
13
|
{#if orientation === 'horizontal'}
|
|
@@ -3,3 +3,4 @@ export { default as EventBlock } from './EventBlock.svelte';
|
|
|
3
3
|
export { default as TimeGutter } from './TimeGutter.svelte';
|
|
4
4
|
export { default as DayHeader } from './DayHeader.svelte';
|
|
5
5
|
export { default as EmptySlot } from './EmptySlot.svelte';
|
|
6
|
+
export { default as FloatingPanel } from './FloatingPanel.svelte';
|
package/dist/primitives/index.js
CHANGED
|
@@ -4,3 +4,4 @@ export { default as EventBlock } from './EventBlock.svelte';
|
|
|
4
4
|
export { default as TimeGutter } from './TimeGutter.svelte';
|
|
5
5
|
export { default as DayHeader } from './DayHeader.svelte';
|
|
6
6
|
export { default as EmptySlot } from './EmptySlot.svelte';
|
|
7
|
+
export { default as FloatingPanel } from './FloatingPanel.svelte';
|
|
@@ -1,4 +1,17 @@
|
|
|
1
|
-
<script lang="ts"
|
|
1
|
+
<script lang="ts">/**
|
|
2
|
+
* AgendaDay — single-day agenda view.
|
|
3
|
+
*
|
|
4
|
+
* Today ("The Queue"):
|
|
5
|
+
* 3-column layout: Done | Now | Up next (hero).
|
|
6
|
+
* Answers: "What's coming up next?"
|
|
7
|
+
*
|
|
8
|
+
* Past day ("The Log"):
|
|
9
|
+
* Quiet chronological record of completed events.
|
|
10
|
+
*
|
|
11
|
+
* Future day ("The Plan"):
|
|
12
|
+
* Clean numbered schedule list.
|
|
13
|
+
*/
|
|
14
|
+
import { createClock } from "../../core/clock.svelte.js";
|
|
2
15
|
import { sod, DAY_MS, dayNum, isAllDay, isMultiDay } from "../../core/time.js";
|
|
3
16
|
import { weekdayLong, monthLong } from "../../core/locale.js";
|
|
4
17
|
import { useCalendarContext } from "../shared/context.svelte.js";
|
|
@@ -7,91 +20,101 @@ import { fmtTime, duration, timeUntilMs, progress, groupIntoSlots } from "../sha
|
|
|
7
20
|
const ctx = useCalendarContext();
|
|
8
21
|
const L = $derived(ctx.labels);
|
|
9
22
|
const emptySnippet = $derived(ctx.emptySnippet);
|
|
10
|
-
let {
|
|
11
|
-
locale,
|
|
12
|
-
height,
|
|
13
|
-
events = [],
|
|
14
|
-
style = "",
|
|
15
|
-
focusDate,
|
|
16
|
-
oneventclick,
|
|
17
|
-
selectedEventId = null
|
|
18
|
-
} = $props();
|
|
23
|
+
let { locale, height, events = [], style = "", focusDate, oneventclick, selectedEventId = null } = $props();
|
|
19
24
|
const clock = createClock(ctx.timezone);
|
|
20
25
|
const viewState = $derived(ctx.viewState);
|
|
21
26
|
const equalDays = $derived(ctx.equalDays);
|
|
22
27
|
const showDates = $derived(ctx.showDates);
|
|
28
|
+
// Calendar's chrome date label renders exactly when showDates is on and
|
|
29
|
+
// already names this day — drop the in-view header so the date never
|
|
30
|
+
// appears twice. Chromeless (showDates=false) hosts keep it as the only
|
|
31
|
+
// label for swipe navigation.
|
|
23
32
|
const hideDayHead = $derived(showDates && !!viewState);
|
|
24
33
|
const isMobile = $derived(ctx.isMobile);
|
|
25
34
|
const autoHeight = $derived(ctx.autoHeight);
|
|
26
35
|
const compact = $derived(ctx.compact);
|
|
27
36
|
const oneventhover = $derived(ctx.oneventhover);
|
|
28
37
|
const disabledSet = $derived(ctx.disabledSet);
|
|
38
|
+
// ── Swipe navigation (mobile, touch only) ──────────
|
|
29
39
|
let swipeStartX = 0;
|
|
30
40
|
let swipeStartY = 0;
|
|
31
41
|
let swipeActive = false;
|
|
32
42
|
const SWIPE_THRESHOLD = 50;
|
|
33
43
|
function onPointerDown(e) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
44
|
+
if (!isMobile || e.pointerType !== "touch") return;
|
|
45
|
+
swipeActive = true;
|
|
46
|
+
swipeStartX = e.clientX;
|
|
47
|
+
swipeStartY = e.clientY;
|
|
38
48
|
}
|
|
39
49
|
function onPointerUp(e) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
if (!swipeActive || e.pointerType !== "touch") return;
|
|
51
|
+
swipeActive = false;
|
|
52
|
+
const dx = e.clientX - swipeStartX;
|
|
53
|
+
const dy = e.clientY - swipeStartY;
|
|
54
|
+
if (Math.abs(dx) > SWIPE_THRESHOLD && Math.abs(dx) > Math.abs(dy) * 1.4) {
|
|
55
|
+
if (dx > 0) viewState?.prev();
|
|
56
|
+
else viewState?.next();
|
|
57
|
+
}
|
|
48
58
|
}
|
|
49
59
|
function onPointerCancel() {
|
|
50
|
-
|
|
60
|
+
swipeActive = false;
|
|
51
61
|
}
|
|
62
|
+
// ── Format helpers (delegated to shared/format.ts) ──
|
|
52
63
|
const fmt = (d) => fmtTime(d, locale);
|
|
53
64
|
const eta = (ms) => timeUntilMs(ms, clock.tick, L);
|
|
54
65
|
const prog = (ev) => progress(ev, clock.tick);
|
|
66
|
+
// ── Event handlers ──────────────────────────────────
|
|
55
67
|
function handleClick(ev) {
|
|
56
|
-
|
|
68
|
+
oneventclick?.(ev);
|
|
57
69
|
}
|
|
70
|
+
// ── Day derivations ─────────────────────────────────
|
|
58
71
|
const dayMs = $derived(focusDate ? sod(focusDate.getTime()) : clock.today);
|
|
59
72
|
const dayEnd = $derived(dayMs + DAY_MS);
|
|
60
73
|
const isToday = $derived(dayMs === clock.today);
|
|
61
74
|
const isTomorrow = $derived(dayMs === clock.today + DAY_MS);
|
|
62
75
|
const isPastDay = $derived(equalDays ? false : dayMs < clock.today);
|
|
76
|
+
/** All events for this day, sorted chronologically */
|
|
63
77
|
const dayEvents = $derived.by(() => {
|
|
64
|
-
|
|
78
|
+
return events.filter((ev) => ev.start.getTime() < dayEnd && ev.end.getTime() > dayMs).sort((a, b) => a.start.getTime() - b.start.getTime());
|
|
65
79
|
});
|
|
80
|
+
/** All-day / multi-day events shown in a separate strip */
|
|
66
81
|
const allDayBanner = $derived(dayEvents.filter((ev) => isAllDay(ev) || isMultiDay(ev)));
|
|
82
|
+
/** Timed events (non-all-day) for normal slot rendering */
|
|
67
83
|
const timedDayEvents = $derived(dayEvents.filter((ev) => !isAllDay(ev) && !isMultiDay(ev)));
|
|
68
84
|
const dayCat = $derived.by(() => {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
85
|
+
const now = clock.tick;
|
|
86
|
+
const past = [];
|
|
87
|
+
const current = [];
|
|
88
|
+
const upcoming = [];
|
|
89
|
+
for (const ev of timedDayEvents) {
|
|
90
|
+
const s = ev.start.getTime();
|
|
91
|
+
const e = ev.end.getTime();
|
|
92
|
+
if (e <= now) past.push(ev);
|
|
93
|
+
else if (s <= now && e > now) current.push(ev);
|
|
94
|
+
else upcoming.push(ev);
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
past,
|
|
98
|
+
current,
|
|
99
|
+
upcomingSlots: groupIntoSlots(upcoming),
|
|
100
|
+
totalUp: upcoming.length
|
|
101
|
+
};
|
|
81
102
|
});
|
|
103
|
+
/** Flat list of ALL upcoming events for the "Up next" column. The first
|
|
104
|
+
* few get card treatment; the rest render as compact rows — busy days
|
|
105
|
+
* (a city-wide feed) would otherwise drown the queue. */
|
|
82
106
|
const upcomingNext = $derived.by(() => {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
107
|
+
const all = [];
|
|
108
|
+
for (const slot of dayCat.upcomingSlots) {
|
|
109
|
+
for (const ev of slot.events) all.push(ev);
|
|
110
|
+
}
|
|
111
|
+
return all;
|
|
88
112
|
});
|
|
89
113
|
const UPCOMING_CARDS = 4;
|
|
114
|
+
/** Done list collapses past this many items (most recent stay visible). */
|
|
90
115
|
const DONE_VISIBLE = 3;
|
|
91
116
|
let showAllDone = $state(false);
|
|
92
|
-
const visibleDone = $derived(
|
|
93
|
-
showAllDone ? dayCat.past : dayCat.past.slice(-DONE_VISIBLE)
|
|
94
|
-
);
|
|
117
|
+
const visibleDone = $derived(showAllDone ? dayCat.past : dayCat.past.slice(-DONE_VISIBLE));
|
|
95
118
|
const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.length - DONE_VISIBLE));
|
|
96
119
|
</script>
|
|
97
120
|
|