@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.
@@ -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
- oneventclick?: (event: TimelineEvent) => void;
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;
@@ -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
- if (e.key === "Enter" || e.key === " ") {
19
- e.preventDefault();
20
- onclick?.({ start, end });
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
- const t = event.title;
31
- const time = `${fmtTime(event.start)} to ${fmtTime(event.end)}`;
32
- const dur = fmtDuration(event.start, event.end);
33
- const loc = event.location ? `, ${event.location}` : "";
34
- const statusStr = isCancelled ? ", cancelled" : isTentative ? ", tentative" : isFull ? ", full" : isLimited ? ", limited" : "";
35
- const activeStr = active ? `, ${L.happeningNow}` : past ? `, ${L.past}` : "";
36
- return `${t}${loc}, ${time}, ${dur}${statusStr}${activeStr}`;
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
- if (e.key === "Enter" || e.key === " ") {
40
- e.preventDefault();
41
- onclick?.(event);
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
- mode = "line",
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';
@@ -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">import { createClock } from "../../core/clock.svelte.js";
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
- if (!isMobile || e.pointerType !== "touch") return;
35
- swipeActive = true;
36
- swipeStartX = e.clientX;
37
- swipeStartY = e.clientY;
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
- if (!swipeActive || e.pointerType !== "touch") return;
41
- swipeActive = false;
42
- const dx = e.clientX - swipeStartX;
43
- const dy = e.clientY - swipeStartY;
44
- if (Math.abs(dx) > SWIPE_THRESHOLD && Math.abs(dx) > Math.abs(dy) * 1.4) {
45
- if (dx > 0) viewState?.prev();
46
- else viewState?.next();
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
- swipeActive = false;
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
- oneventclick?.(ev);
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
- return events.filter((ev) => ev.start.getTime() < dayEnd && ev.end.getTime() > dayMs).sort((a, b) => a.start.getTime() - b.start.getTime());
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
- const now = clock.tick;
70
- const past = [];
71
- const current = [];
72
- const upcoming = [];
73
- for (const ev of timedDayEvents) {
74
- const s = ev.start.getTime();
75
- const e = ev.end.getTime();
76
- if (e <= now) past.push(ev);
77
- else if (s <= now && e > now) current.push(ev);
78
- else upcoming.push(ev);
79
- }
80
- return { past, current, upcomingSlots: groupIntoSlots(upcoming), totalUp: upcoming.length };
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
- const all = [];
84
- for (const slot of dayCat.upcomingSlots) {
85
- for (const ev of slot.events) all.push(ev);
86
- }
87
- return all;
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