diff --git a/src/lib/components/CompletionSparkline.svelte b/src/lib/components/CompletionSparkline.svelte index ca72bdc..264ae42 100644 --- a/src/lib/components/CompletionSparkline.svelte +++ b/src/lib/components/CompletionSparkline.svelte @@ -3,6 +3,13 @@ // Zero-days render as a 1 px baseline stub (never a gap, never zero // height). The component self-hides when all bars are zero. Caller // still guards on the user's settings toggle. + // + // Phase 9 polish: friendlier aria-label (sentence-form summary, not a + // numeric list), per-bar tooltips with absolute date + count, + // and a 30 ms-stagger entrance animation that respects + // prefers-reduced-motion. Tabindex deliberately omitted: SVGs with + // role="img" are reachable by screen-reader graphic navigation + // without being part of the keyboard-tab order. import type { DailyCompletionCount } from "$lib/types/app"; interface Props { @@ -22,10 +29,12 @@ (width - BAR_GAP * Math.max(0, data.length - 1)) / Math.max(1, data.length), ); + let total = $derived(data.reduce((sum, d) => sum + d.count, 0)); + let today = $derived(data.at(-1)?.count ?? 0); + let ariaLabel = $derived.by(() => { if (data.length === 0) return ""; - const nums = data.map((d) => d.count).join(", "); - return `Tasks completed over the last ${data.length} days: ${nums}`; + return `${today} completed today. ${total} total over the last ${data.length} days.`; }); let hasAnyCompletion = $derived(data.some((d) => d.count > 0)); @@ -53,7 +62,31 @@ fill="currentColor" opacity={d.count === 0 ? 0.35 : 0.85} rx="1" - /> + class="bar" + style={`--bar-delay: ${i * 30}ms`} + > + <title>{d.day}: {d.count} {d.count === 1 ? "task" : "tasks"} + {/each} {/if} + + diff --git a/src/lib/pages/TasksPage.svelte b/src/lib/pages/TasksPage.svelte index d2a4eb6..e6e7eda 100644 --- a/src/lib/pages/TasksPage.svelte +++ b/src/lib/pages/TasksPage.svelte @@ -298,7 +298,7 @@ > {#if todayCount() > 0} {todayCount()} today @@ -689,3 +689,27 @@ + +