@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
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
Emits: onclick, ondragstart, onresize (for future interaction wiring)
|
|
10
10
|
-->
|
|
11
11
|
<script lang="ts">import { fmtTime, fmtDuration, getLabels } from "../core/locale.js";
|
|
12
|
-
|
|
12
|
+
import { getContext } from "svelte";
|
|
13
|
+
// Inside a Calendar: its per-instance labels; standalone: the global ones.
|
|
14
|
+
const calendarCtx = getContext("calendar");
|
|
15
|
+
const L = $derived(calendarCtx?.labels ?? getLabels());
|
|
13
16
|
let { event, variant = "chip", active = false, past = false, showTime = false, showDuration = false, editable = false, onclick, children } = $props();
|
|
14
17
|
const accentColor = $derived(event.color || "var(--dt-accent, #2563eb)");
|
|
15
18
|
const isCancelled = $derived(event.status === "cancelled");
|
|
@@ -18,10 +21,11 @@ const isFull = $derived(event.status === "full");
|
|
|
18
21
|
const isLimited = $derived(event.status === "limited");
|
|
19
22
|
const ariaLabel = $derived.by(() => {
|
|
20
23
|
const t = event.title;
|
|
21
|
-
const time = `${fmtTime(event.start)}
|
|
24
|
+
const time = `${fmtTime(event.start)} – ${fmtTime(event.end)}`;
|
|
22
25
|
const dur = fmtDuration(event.start, event.end);
|
|
23
26
|
const loc = event.location ? `, ${event.location}` : "";
|
|
24
|
-
const
|
|
27
|
+
const status = isCancelled ? L.cancelled : isTentative ? L.tentative : isFull ? L.full : isLimited ? L.limited : "";
|
|
28
|
+
const statusStr = status ? `, ${status}` : "";
|
|
25
29
|
const activeStr = active ? `, ${L.happeningNow}` : past ? `, ${L.past}` : "";
|
|
26
30
|
return `${t}${loc}, ${time}, ${dur}${statusStr}${activeStr}`;
|
|
27
31
|
});
|
|
@@ -63,11 +67,7 @@ function handleKeydown(e) {
|
|
|
63
67
|
{/each}
|
|
64
68
|
</div>
|
|
65
69
|
{/if}
|
|
66
|
-
{
|
|
67
|
-
{#if isTentative}<span class="eb-status-badge eb-tentative-badge">Tentative</span>{/if}
|
|
68
|
-
{#if isFull}<span class="eb-status-badge eb-full-badge">Full</span>{/if}
|
|
69
|
-
{#if isLimited}<span class="eb-status-badge eb-limited-badge">Limited</span>{/if}
|
|
70
|
-
{#if active}<span class="eb-live-badge">{L.now}</span>{/if}
|
|
70
|
+
{@render badges()}
|
|
71
71
|
</div>
|
|
72
72
|
{:else}
|
|
73
73
|
<!-- row -->
|
|
@@ -90,13 +90,20 @@ function handleKeydown(e) {
|
|
|
90
90
|
{/each}
|
|
91
91
|
</span>
|
|
92
92
|
{/if}
|
|
93
|
-
{
|
|
94
|
-
{#if isFull}<span class="eb-status-badge eb-full-badge">Full</span>{/if}
|
|
95
|
-
{#if isLimited}<span class="eb-status-badge eb-limited-badge">Limited</span>{/if}
|
|
96
|
-
{#if active}<span class="eb-live-badge">{L.now}</span>{/if}
|
|
93
|
+
{@render badges()}
|
|
97
94
|
{/if}
|
|
98
95
|
{/snippet}
|
|
99
96
|
|
|
97
|
+
<!-- Status badges: the same set in card and row. The badge CSS uppercases
|
|
98
|
+
the (lower-case) status labels. -->
|
|
99
|
+
{#snippet badges()}
|
|
100
|
+
{#if isCancelled}<span class="eb-status-badge eb-cancelled-badge">{L.cancelled}</span>{/if}
|
|
101
|
+
{#if isTentative}<span class="eb-status-badge eb-tentative-badge">{L.tentative}</span>{/if}
|
|
102
|
+
{#if isFull}<span class="eb-status-badge eb-full-badge">{L.full}</span>{/if}
|
|
103
|
+
{#if isLimited}<span class="eb-status-badge eb-limited-badge">{L.limited}</span>{/if}
|
|
104
|
+
{#if active}<span class="eb-live-badge">{L.now}</span>{/if}
|
|
105
|
+
{/snippet}
|
|
106
|
+
|
|
100
107
|
{#if onclick}
|
|
101
108
|
<div
|
|
102
109
|
class="eb eb-{variant}"
|
|
@@ -331,8 +338,8 @@ function handleKeydown(e) {
|
|
|
331
338
|
background: color-mix(in srgb, var(--_color) 12%, transparent);
|
|
332
339
|
}
|
|
333
340
|
.eb-full-badge {
|
|
334
|
-
color: #2563eb;
|
|
335
|
-
background: color-mix(in srgb, #2563eb 15%, transparent);
|
|
341
|
+
color: var(--dt-accent, #2563eb);
|
|
342
|
+
background: color-mix(in srgb, var(--dt-accent, #2563eb) 15%, transparent);
|
|
336
343
|
}
|
|
337
344
|
.eb-limited-badge {
|
|
338
345
|
color: var(--_color);
|
|
@@ -1,9 +1,48 @@
|
|
|
1
1
|
<script module lang="ts">// Phone sheet scroll-lock: nested panels each take a lock; the page behind
|
|
2
2
|
// unlocks when the last one closes.
|
|
3
3
|
let sheetLocks = 0;
|
|
4
|
+
// Open panels, oldest first. Escape and the outside tap belong to the
|
|
5
|
+
// topmost one only, so a nested panel closes without its parent.
|
|
6
|
+
const openPanels = [];
|
|
7
|
+
// Events a panel already acted on — the next panel down must not act on
|
|
8
|
+
// the same keydown/pointerdown once the top one unmounts mid-dispatch.
|
|
9
|
+
const handled = new WeakSet();
|
|
10
|
+
/**
|
|
11
|
+
* Spend the click a closing tap synthesizes. `onclose` usually unmounts the
|
|
12
|
+
* panel before that click arrives, so the listener lives on `window`,
|
|
13
|
+
* outside the component: it eats the next click (capture phase, before any
|
|
14
|
+
* calendar handler) and removes itself then, on the next pointerdown, on a
|
|
15
|
+
* pointercancel (no click follows), or after a timeout.
|
|
16
|
+
*/
|
|
17
|
+
function swallowNextClick() {
|
|
18
|
+
const eat = (e) => {
|
|
19
|
+
e.stopPropagation();
|
|
20
|
+
e.preventDefault();
|
|
21
|
+
done();
|
|
22
|
+
};
|
|
23
|
+
const done = () => {
|
|
24
|
+
clearTimeout(timer);
|
|
25
|
+
window.removeEventListener("click", eat, true);
|
|
26
|
+
window.removeEventListener("pointerdown", done, true);
|
|
27
|
+
window.removeEventListener("pointercancel", done, true);
|
|
28
|
+
};
|
|
29
|
+
const timer = setTimeout(done, 1500);
|
|
30
|
+
window.addEventListener("click", eat, true);
|
|
31
|
+
window.addEventListener("pointercancel", done, true);
|
|
32
|
+
// Armed after this pointerdown has finished dispatching.
|
|
33
|
+
setTimeout(() => window.addEventListener("pointerdown", done, true), 0);
|
|
34
|
+
}
|
|
35
|
+
export {};
|
|
4
36
|
</script>
|
|
5
37
|
|
|
6
|
-
<script lang="ts"
|
|
38
|
+
<script lang="ts">// A non-modal window that floats over the calendar: opened beside the
|
|
39
|
+
// block that was clicked (right of it, left when the right edge is near,
|
|
40
|
+
// clamped to the viewport), dragged by its header, closed by Escape, its
|
|
41
|
+
// own button, or a click anywhere outside — that outside click is spent on
|
|
42
|
+
// closing, so a block under it is not opened by the same tap. Below 640px
|
|
43
|
+
// it is a bottom sheet instead.
|
|
44
|
+
import { untrack } from "svelte";
|
|
45
|
+
let { title = "", anchor = {
|
|
7
46
|
x: 24,
|
|
8
47
|
y: 24
|
|
9
48
|
}, width = 440, theme = "", closeLabel = "Close", closeOnOutside = true, onclose, children } = $props();
|
|
@@ -28,11 +67,66 @@ function beside(a) {
|
|
|
28
67
|
const px = right + w + EDGE <= window.innerWidth ? right : a.x - GAP - w >= EDGE ? a.x - GAP - w : a.x;
|
|
29
68
|
place(px, a.y);
|
|
30
69
|
}
|
|
70
|
+
// Where focus goes back to when the panel closes: whatever held it when
|
|
71
|
+
// the panel opened (or when a new anchor re-focused it).
|
|
72
|
+
let returnFocus = null;
|
|
73
|
+
// Set once the user drags the panel: from then on a size change re-clamps
|
|
74
|
+
// it where it is instead of jumping back beside the anchor.
|
|
75
|
+
let dragged = false;
|
|
31
76
|
// A new anchor (the next click) re-places the panel; each run focuses it.
|
|
32
77
|
$effect(() => {
|
|
78
|
+
dragged = false;
|
|
33
79
|
beside(anchor);
|
|
80
|
+
const active = document.activeElement;
|
|
81
|
+
if (active instanceof HTMLElement && active !== document.body && !el?.contains(active)) {
|
|
82
|
+
returnFocus = active;
|
|
83
|
+
}
|
|
34
84
|
el?.focus({ preventScroll: true });
|
|
35
85
|
});
|
|
86
|
+
// Content that grows (or shrinks) after opening — a form section, loaded
|
|
87
|
+
// data — must stay inside the viewport; so must a resized window.
|
|
88
|
+
$effect(() => {
|
|
89
|
+
if (!el) return;
|
|
90
|
+
const reclamp = () => {
|
|
91
|
+
if (dragged) place(left, top);
|
|
92
|
+
else beside(anchor);
|
|
93
|
+
};
|
|
94
|
+
const ro = new ResizeObserver(reclamp);
|
|
95
|
+
ro.observe(el);
|
|
96
|
+
window.addEventListener("resize", reclamp);
|
|
97
|
+
return () => {
|
|
98
|
+
ro.disconnect();
|
|
99
|
+
window.removeEventListener("resize", reclamp);
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
// Topmost-panel stack + focus restore on close.
|
|
103
|
+
const myId = Symbol("floating-panel");
|
|
104
|
+
const isTopmost = () => openPanels[openPanels.length - 1] === myId;
|
|
105
|
+
$effect(() => {
|
|
106
|
+
const id = myId;
|
|
107
|
+
openPanels.push(id);
|
|
108
|
+
const panel = untrack(() => el);
|
|
109
|
+
return () => {
|
|
110
|
+
const i = openPanels.indexOf(id);
|
|
111
|
+
if (i !== -1) openPanels.splice(i, 1);
|
|
112
|
+
// Only pull focus back when it would otherwise be lost (inside the
|
|
113
|
+
// closing panel, or dropped to <body>) — not from where the user
|
|
114
|
+
// has since moved it.
|
|
115
|
+
const active = document.activeElement;
|
|
116
|
+
const lost = !active || active === document.body || !!panel && panel.contains(active);
|
|
117
|
+
const target = returnFocus;
|
|
118
|
+
if (lost && target && target.isConnected) {
|
|
119
|
+
target.focus({ preventScroll: true });
|
|
120
|
+
} else if (lost && target) {
|
|
121
|
+
// The opener may be re-rendered by the close (a view refresh);
|
|
122
|
+
// try once more after the DOM settles.
|
|
123
|
+
queueMicrotask(() => {
|
|
124
|
+
const a = document.activeElement;
|
|
125
|
+
if ((!a || a === document.body) && target.isConnected) target.focus({ preventScroll: true });
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
});
|
|
36
130
|
// In sheet mode the page behind must not scroll — drags on the sheet
|
|
37
131
|
// chrome, or reaching the end of the sheet body, would otherwise reach it.
|
|
38
132
|
$effect(() => {
|
|
@@ -59,35 +153,36 @@ function down(e) {
|
|
|
59
153
|
e.currentTarget.setPointerCapture(e.pointerId);
|
|
60
154
|
}
|
|
61
155
|
function move(e) {
|
|
62
|
-
if (grab)
|
|
156
|
+
if (!grab) return;
|
|
157
|
+
dragged = true;
|
|
158
|
+
place(e.clientX - grab.dx, e.clientY - grab.dy);
|
|
63
159
|
}
|
|
64
160
|
function up() {
|
|
65
161
|
grab = null;
|
|
66
162
|
}
|
|
67
163
|
// The outside pointerdown is swallowed here, before any calendar handler,
|
|
68
164
|
// and so is the click it would synthesize — one tap closes, the next acts.
|
|
69
|
-
|
|
165
|
+
// With nested panels only the topmost reacts: a tap outside it (even on
|
|
166
|
+
// the panel beneath) closes it alone.
|
|
70
167
|
function outsideDown(e) {
|
|
71
168
|
if (!closeOnOutside || !el || el.contains(e.target)) return;
|
|
169
|
+
if (handled.has(e) || !isTopmost()) return;
|
|
170
|
+
handled.add(e);
|
|
72
171
|
e.stopPropagation();
|
|
73
172
|
e.preventDefault();
|
|
74
|
-
|
|
173
|
+
swallowNextClick();
|
|
75
174
|
onclose?.();
|
|
76
175
|
}
|
|
77
|
-
function
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
e.
|
|
81
|
-
|
|
176
|
+
function onKeydown(e) {
|
|
177
|
+
// defaultPrevented: a control inside the panel (a combobox, a menu)
|
|
178
|
+
// already used this Escape to close itself.
|
|
179
|
+
if (e.key !== "Escape" || e.defaultPrevented || handled.has(e) || !isTopmost()) return;
|
|
180
|
+
handled.add(e);
|
|
181
|
+
onclose?.();
|
|
82
182
|
}
|
|
83
|
-
export {};
|
|
84
183
|
</script>
|
|
85
184
|
|
|
86
|
-
<svelte:window
|
|
87
|
-
onkeydown={(e) => e.key === 'Escape' && onclose?.()}
|
|
88
|
-
onpointerdowncapture={outsideDown}
|
|
89
|
-
onclickcapture={outsideClick}
|
|
90
|
-
/>
|
|
185
|
+
<svelte:window onkeydown={onKeydown} onpointerdowncapture={outsideDown} />
|
|
91
186
|
|
|
92
187
|
<div
|
|
93
188
|
class="fp"
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
|
-
|
|
2
|
+
/** What a FloatingPanel opens beside: a viewport rect (a clicked block's `getBoundingClientRect()`), or a bare point. */
|
|
3
|
+
export type FloatingPanelAnchor = {
|
|
3
4
|
x: number;
|
|
4
5
|
y: number;
|
|
5
6
|
width?: number;
|
|
6
7
|
height?: number;
|
|
7
8
|
};
|
|
8
|
-
|
|
9
|
+
export interface FloatingPanelProps {
|
|
9
10
|
title?: string;
|
|
10
11
|
/** What to open beside: the clicked block's viewport rect, or a bare point. */
|
|
11
|
-
anchor?:
|
|
12
|
+
anchor?: FloatingPanelAnchor;
|
|
13
|
+
/** Panel width in px (default 440). Below 640px viewport width it is a bottom sheet. */
|
|
12
14
|
width?: number;
|
|
13
15
|
/** The same `--dt-*` theme string a Calendar takes. */
|
|
14
16
|
theme?: string;
|
|
@@ -17,7 +19,7 @@ type $$ComponentProps = {
|
|
|
17
19
|
closeOnOutside?: boolean;
|
|
18
20
|
onclose?: () => void;
|
|
19
21
|
children: Snippet;
|
|
20
|
-
}
|
|
21
|
-
declare const FloatingPanel: import("svelte").Component
|
|
22
|
+
}
|
|
23
|
+
declare const FloatingPanel: import("svelte").Component<FloatingPanelProps, {}, "">;
|
|
22
24
|
type FloatingPanel = ReturnType<typeof FloatingPanel>;
|
|
23
25
|
export default FloatingPanel;
|
|
@@ -12,14 +12,19 @@
|
|
|
12
12
|
<NowIndicator mode="badge" />
|
|
13
13
|
-->
|
|
14
14
|
<script lang="ts">import { getLabels } from "../core/locale.js";
|
|
15
|
-
|
|
15
|
+
import { getContext } from "svelte";
|
|
16
|
+
// Inside a Calendar: its per-instance labels; standalone: the global ones.
|
|
17
|
+
const calendarCtx = getContext("calendar");
|
|
18
|
+
const L = $derived(calendarCtx?.labels ?? getLabels());
|
|
16
19
|
let { mode = "line", position = 0, orientation = "vertical", time = "", seconds = "", showLabel = true, color, children } = $props();
|
|
17
20
|
const posStyle = $derived(orientation === "vertical" ? `left: ${position}px` : `top: ${position}px`);
|
|
18
21
|
const colorVar = $derived(color ? `--ni-color: ${color}` : "");
|
|
19
22
|
</script>
|
|
20
23
|
|
|
21
24
|
{#if mode === 'badge'}
|
|
22
|
-
|
|
25
|
+
<!-- Plain text, not a live region: the time it carries changes every
|
|
26
|
+
second, and a status role would read each change aloud. -->
|
|
27
|
+
<span class="ni-badge" style={colorVar}>
|
|
23
28
|
{#if children}
|
|
24
29
|
{@render children()}
|
|
25
30
|
{:else}
|
|
@@ -27,7 +32,9 @@ const colorVar = $derived(color ? `--ni-color: ${color}` : "");
|
|
|
27
32
|
{/if}
|
|
28
33
|
</span>
|
|
29
34
|
{:else if mode === 'dot'}
|
|
30
|
-
|
|
35
|
+
<!-- Decorative: the visible time is the grid's own; hidden from AT so a
|
|
36
|
+
ticking label is never announced. -->
|
|
37
|
+
<div class="ni ni-dot {orientation}" style="{posStyle}; {colorVar}" aria-hidden="true">
|
|
31
38
|
<div class="ni-dot-circle"></div>
|
|
32
39
|
{#if showLabel && time}
|
|
33
40
|
<div class="ni-label">
|
|
@@ -37,7 +44,7 @@ const colorVar = $derived(color ? `--ni-color: ${color}` : "");
|
|
|
37
44
|
{/if}
|
|
38
45
|
</div>
|
|
39
46
|
{:else}
|
|
40
|
-
<div class="ni ni-line {orientation}" style="{posStyle}; {colorVar}"
|
|
47
|
+
<div class="ni ni-line {orientation}" style="{posStyle}; {colorVar}" aria-hidden="true">
|
|
41
48
|
<div class="ni-line-bar"></div>
|
|
42
49
|
{#if showLabel && time}
|
|
43
50
|
<div class="ni-label">
|
|
@@ -8,23 +8,26 @@
|
|
|
8
8
|
<script lang="ts">import { HOURS } from "../core/time.js";
|
|
9
9
|
import { fmtH } from "../core/locale.js";
|
|
10
10
|
let { orientation = "horizontal", hourSize = 120, halfHour = true, hours = HOURS, formatHour = fmtH } = $props();
|
|
11
|
+
// Labels sit relative to the first hour shown: hours [6…21] start at 0px,
|
|
12
|
+
// not 6 hours down.
|
|
13
|
+
const firstHour = $derived(hours[0] ?? 0);
|
|
11
14
|
</script>
|
|
12
15
|
|
|
13
16
|
{#if orientation === 'horizontal'}
|
|
14
|
-
<div class="tg tg-h" style="--tg-hour: {hourSize}px">
|
|
17
|
+
<div class="tg tg-h" style="--tg-hour: {hourSize}px; --tg-count: {hours.length}">
|
|
15
18
|
{#each hours as h}
|
|
16
|
-
<div class="tg-tick" style="left: {h * hourSize}px">
|
|
19
|
+
<div class="tg-tick" style="left: {(h - firstHour) * hourSize}px">
|
|
17
20
|
<span class="tg-label">{formatHour(h)}</span>
|
|
18
21
|
</div>
|
|
19
22
|
{#if halfHour}
|
|
20
|
-
<div class="tg-tick tg-tick-half" style="left: {(h + 0.5) * hourSize}px"></div>
|
|
23
|
+
<div class="tg-tick tg-tick-half" style="left: {(h - firstHour + 0.5) * hourSize}px"></div>
|
|
21
24
|
{/if}
|
|
22
25
|
{/each}
|
|
23
26
|
</div>
|
|
24
27
|
{:else}
|
|
25
|
-
<div class="tg tg-v" style="--tg-hour: {hourSize}px">
|
|
28
|
+
<div class="tg tg-v" style="--tg-hour: {hourSize}px; --tg-count: {hours.length}">
|
|
26
29
|
{#each hours as h}
|
|
27
|
-
<div class="tg-row" style="top: {h * hourSize}px; height: {hourSize}px">
|
|
30
|
+
<div class="tg-row" style="top: {(h - firstHour) * hourSize}px; height: {hourSize}px">
|
|
28
31
|
<span class="tg-label">{formatHour(h)}</span>
|
|
29
32
|
</div>
|
|
30
33
|
{/each}
|
|
@@ -41,7 +44,7 @@ let { orientation = "horizontal", hourSize = 120, halfHour = true, hours = HOURS
|
|
|
41
44
|
/* ── Horizontal ── */
|
|
42
45
|
.tg-h {
|
|
43
46
|
height: 100%;
|
|
44
|
-
width: calc(24 * var(--tg-hour));
|
|
47
|
+
width: calc(var(--tg-count, 24) * var(--tg-hour));
|
|
45
48
|
}
|
|
46
49
|
.tg-tick {
|
|
47
50
|
position: absolute;
|
|
@@ -66,6 +69,7 @@ let { orientation = "horizontal", hourSize = 120, halfHour = true, hours = HOURS
|
|
|
66
69
|
/* ── Vertical ── */
|
|
67
70
|
.tg-v {
|
|
68
71
|
width: 48px;
|
|
72
|
+
height: calc(var(--tg-count, 24) * var(--tg-hour));
|
|
69
73
|
flex-shrink: 0;
|
|
70
74
|
}
|
|
71
75
|
.tg-row {
|
|
@@ -4,3 +4,4 @@ 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
6
|
export { default as FloatingPanel } from './FloatingPanel.svelte';
|
|
7
|
+
export type { FloatingPanelAnchor, FloatingPanelProps } from './FloatingPanel.svelte';
|
package/dist/text-fit.d.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import { type PrepareOptions } from '@chenglou/pretext';
|
|
2
1
|
import type { Attachment } from 'svelte/attachments';
|
|
3
|
-
|
|
2
|
+
/** How text wraps when measured — the CSS properties of the same names. */
|
|
3
|
+
export interface TextLayoutOptions {
|
|
4
|
+
whiteSpace?: 'normal' | 'pre-wrap';
|
|
5
|
+
wordBreak?: 'normal' | 'keep-all';
|
|
6
|
+
/** Extra px between characters */
|
|
7
|
+
letterSpacing?: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function lineCount(text: string, font: string, width: number, options?: TextLayoutOptions): number;
|
|
4
10
|
export declare function fits(text: string, font: string, width: number, lines?: number): boolean;
|
|
5
11
|
/** Height the text will take at `width`, before it is in the DOM. */
|
|
6
12
|
export declare function textHeight(text: string, font: string, width: number, lineHeight: number): number;
|
|
@@ -12,11 +12,12 @@
|
|
|
12
12
|
* Clean numbered schedule list.
|
|
13
13
|
*/
|
|
14
14
|
import { createClock } from "../../core/clock.svelte.js";
|
|
15
|
-
import { sod,
|
|
15
|
+
import { sod, addDaysMs, dayNum, isAllDay } from "../../core/time.js";
|
|
16
16
|
import { weekdayLong, monthLong } from "../../core/locale.js";
|
|
17
17
|
import { useCalendarContext } from "../shared/context.svelte.js";
|
|
18
18
|
import EventContent from "../shared/EventContent.svelte";
|
|
19
19
|
import { fmtTime, duration, timeUntilMs, progress, groupIntoSlots } from "../shared/format.js";
|
|
20
|
+
import { createSwipe } from "../mobile/swipe.js";
|
|
20
21
|
const ctx = useCalendarContext();
|
|
21
22
|
const L = $derived(ctx.labels);
|
|
22
23
|
const emptySnippet = $derived(ctx.emptySnippet);
|
|
@@ -36,28 +37,28 @@ const compact = $derived(ctx.compact);
|
|
|
36
37
|
const oneventhover = $derived(ctx.oneventhover);
|
|
37
38
|
const disabledSet = $derived(ctx.disabledSet);
|
|
38
39
|
// ── Swipe navigation (mobile, touch only) ──────────
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
swipeStartY = e.clientY;
|
|
48
|
-
}
|
|
49
|
-
function onPointerUp(e) {
|
|
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();
|
|
40
|
+
// Touch events, not pointer events: once the browser claims a touch for
|
|
41
|
+
// panning it sends pointercancel and the pointerup never arrives.
|
|
42
|
+
const swipe = createSwipe({
|
|
43
|
+
disabled: () => !isMobile,
|
|
44
|
+
onmove: () => {},
|
|
45
|
+
onend: (dir) => {
|
|
46
|
+
if (dir > 0) viewState?.prev();
|
|
47
|
+
else if (dir < 0) viewState?.next();
|
|
57
48
|
}
|
|
49
|
+
});
|
|
50
|
+
// ── Status suffix (aria) ────────────────────────────
|
|
51
|
+
function statusText(ev) {
|
|
52
|
+
if (ev.status === "cancelled") return ` (${L.cancelled})`;
|
|
53
|
+
if (ev.status === "tentative") return ` (${L.tentative})`;
|
|
54
|
+
if (ev.status === "full") return ` (${L.full})`;
|
|
55
|
+
if (ev.status === "limited") return ` (${L.limited})`;
|
|
56
|
+
return "";
|
|
58
57
|
}
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
/** All-day events and timed ones at least a (wall-clock) day long go to
|
|
59
|
+
* the all-day strip; a shorter overnight event is listed with its time. */
|
|
60
|
+
function inAllDayStrip(ev) {
|
|
61
|
+
return isAllDay(ev) || ev.end.getTime() >= addDaysMs(ev.start.getTime(), 1);
|
|
61
62
|
}
|
|
62
63
|
// ── Format helpers (delegated to shared/format.ts) ──
|
|
63
64
|
const fmt = (d) => fmtTime(d, locale);
|
|
@@ -69,18 +70,18 @@ function handleClick(ev) {
|
|
|
69
70
|
}
|
|
70
71
|
// ── Day derivations ─────────────────────────────────
|
|
71
72
|
const dayMs = $derived(focusDate ? sod(focusDate.getTime()) : clock.today);
|
|
72
|
-
const dayEnd = $derived(dayMs
|
|
73
|
+
const dayEnd = $derived(addDaysMs(dayMs, 1));
|
|
73
74
|
const isToday = $derived(dayMs === clock.today);
|
|
74
|
-
const isTomorrow = $derived(dayMs === clock.today
|
|
75
|
+
const isTomorrow = $derived(dayMs === addDaysMs(clock.today, 1));
|
|
75
76
|
const isPastDay = $derived(equalDays ? false : dayMs < clock.today);
|
|
76
77
|
/** All events for this day, sorted chronologically */
|
|
77
78
|
const dayEvents = $derived.by(() => {
|
|
78
79
|
return events.filter((ev) => ev.start.getTime() < dayEnd && ev.end.getTime() > dayMs).sort((a, b) => a.start.getTime() - b.start.getTime());
|
|
79
80
|
});
|
|
80
81
|
/** All-day / multi-day events shown in a separate strip */
|
|
81
|
-
const allDayBanner = $derived(dayEvents.filter(
|
|
82
|
+
const allDayBanner = $derived(dayEvents.filter(inAllDayStrip));
|
|
82
83
|
/** Timed events (non-all-day) for normal slot rendering */
|
|
83
|
-
const timedDayEvents = $derived(dayEvents.filter((ev) => !
|
|
84
|
+
const timedDayEvents = $derived(dayEvents.filter((ev) => !inAllDayStrip(ev)));
|
|
84
85
|
const dayCat = $derived.by(() => {
|
|
85
86
|
const now = clock.tick;
|
|
86
87
|
const past = [];
|
|
@@ -118,7 +119,7 @@ const visibleDone = $derived(showAllDone ? dayCat.past : dayCat.past.slice(-DONE
|
|
|
118
119
|
const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.length - DONE_VISIBLE));
|
|
119
120
|
</script>
|
|
120
121
|
|
|
121
|
-
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
122
|
+
<!-- svelte-ignore a11y_no_static_element_interactions (touch swipe only; no pointer/keyboard role) -->
|
|
122
123
|
<div
|
|
123
124
|
class="ag ag--day"
|
|
124
125
|
class:ag--disabled={disabledSet.has(dayMs)}
|
|
@@ -126,9 +127,10 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
|
|
|
126
127
|
class:ag--auto={autoHeight}
|
|
127
128
|
style={style || undefined}
|
|
128
129
|
style:height={height ? `${height}px` : undefined}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
130
|
+
ontouchstart={swipe.ontouchstart}
|
|
131
|
+
ontouchmove={swipe.ontouchmove}
|
|
132
|
+
ontouchend={swipe.ontouchend}
|
|
133
|
+
ontouchcancel={swipe.ontouchcancel}
|
|
132
134
|
>
|
|
133
135
|
<div class="ag-body" role="group" aria-label={L.todaysLineup}>
|
|
134
136
|
<!-- ─── In-view date header (only when the chrome doesn't label the day) ─── -->
|
|
@@ -154,7 +156,7 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
|
|
|
154
156
|
class="ag-allday-chip"
|
|
155
157
|
class:ag-allday-chip--selected={selectedEventId === ev.id}
|
|
156
158
|
style:--ev-color={ev.color || 'var(--dt-accent)'}
|
|
157
|
-
aria-label="{ev.title}, {L.allDay}"
|
|
159
|
+
aria-label="{ev.title}{statusText(ev)}, {L.allDay}"
|
|
158
160
|
onclick={() => handleClick(ev)}
|
|
159
161
|
onpointerenter={() => oneventhover?.(ev)}
|
|
160
162
|
>
|
|
@@ -182,7 +184,7 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
|
|
|
182
184
|
class:ag-compact-row--cancelled={ev.status === 'cancelled'}
|
|
183
185
|
class:ag-compact-row--tentative={ev.status === 'tentative'}
|
|
184
186
|
style:--ev-color={ev.color || 'var(--dt-accent)'}
|
|
185
|
-
aria-label="{ev.title}, {fmt(ev.start)}, {duration(ev)}"
|
|
187
|
+
aria-label="{ev.title}{statusText(ev)}, {fmt(ev.start)}, {duration(ev)}"
|
|
186
188
|
onclick={() => handleClick(ev)}
|
|
187
189
|
onpointerenter={() => oneventhover?.(ev)}
|
|
188
190
|
>
|
|
@@ -277,7 +279,7 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
|
|
|
277
279
|
class="ag-compact-row ag-compact-row--queue"
|
|
278
280
|
class:ag-compact-row--selected={selectedEventId === ev.id}
|
|
279
281
|
style:--ev-color={ev.color || 'var(--dt-accent)'}
|
|
280
|
-
aria-label="{ev.title}, {fmt(ev.start)}, {duration(ev)}"
|
|
282
|
+
aria-label="{ev.title}{statusText(ev)}, {fmt(ev.start)}, {duration(ev)}"
|
|
281
283
|
onclick={() => handleClick(ev)}
|
|
282
284
|
onpointerenter={() => oneventhover?.(ev)}
|
|
283
285
|
>
|
|
@@ -298,7 +300,7 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
|
|
|
298
300
|
class:ag-card--hero={i === 0}
|
|
299
301
|
class:ag-card--selected={selectedEventId === ev.id}
|
|
300
302
|
style:--ev-color={ev.color || 'var(--dt-accent)'}
|
|
301
|
-
aria-label="{ev.title}, {fmt(ev.start)}, {duration(ev)}"
|
|
303
|
+
aria-label="{ev.title}{statusText(ev)}, {fmt(ev.start)}, {duration(ev)}"
|
|
302
304
|
onclick={() => handleClick(ev)}
|
|
303
305
|
onpointerenter={() => oneventhover?.(ev)}
|
|
304
306
|
>
|
|
@@ -345,7 +347,7 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
|
|
|
345
347
|
class="ag-log-row"
|
|
346
348
|
class:ag-log-row--selected={selectedEventId === ev.id}
|
|
347
349
|
style:--ev-color={ev.color || 'var(--dt-accent)'}
|
|
348
|
-
aria-label="{ev.title}, {fmt(ev.start)}
|
|
350
|
+
aria-label="{ev.title}{statusText(ev)}, {fmt(ev.start)} – {fmt(ev.end)}"
|
|
349
351
|
onclick={() => handleClick(ev)}
|
|
350
352
|
onpointerenter={() => oneventhover?.(ev)}
|
|
351
353
|
>
|
|
@@ -378,7 +380,7 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
|
|
|
378
380
|
class:ag-card--full={ev.status === 'full'}
|
|
379
381
|
class:ag-card--limited={ev.status === 'limited'}
|
|
380
382
|
style:--ev-color={ev.color || 'var(--dt-accent)'}
|
|
381
|
-
aria-label="{ev.title}{
|
|
383
|
+
aria-label="{ev.title}{statusText(ev)}, {fmt(ev.start)} – {fmt(ev.end)}, {duration(ev)}"
|
|
382
384
|
onclick={() => handleClick(ev)}
|
|
383
385
|
onpointerenter={() => oneventhover?.(ev)}
|
|
384
386
|
>
|
|
@@ -767,6 +769,12 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
|
|
|
767
769
|
}
|
|
768
770
|
/* Mobile: stack queue columns vertically — "Up next" (hero) first,
|
|
769
771
|
Now/Done status column second */
|
|
772
|
+
/* Horizontal swipes are ours (day navigation); the browser keeps
|
|
773
|
+
vertical scrolling and pinch-zoom. */
|
|
774
|
+
.ag--mobile,
|
|
775
|
+
.ag--mobile .ag-body {
|
|
776
|
+
touch-action: pan-y pinch-zoom;
|
|
777
|
+
}
|
|
770
778
|
.ag--mobile .ag-q {
|
|
771
779
|
grid-template-columns: 1fr;
|
|
772
780
|
min-height: auto;
|