From dd45f10cd464bddb1dddc37d13c034ad4967c6cc Mon Sep 17 00:00:00 2001 From: Jake Date: Sat, 25 Apr 2026 00:17:53 +0100 Subject: [PATCH] polish(phase9): sparkline + badge motion and a11y Sparkline: friendlier aria-label ("3 completed today. 14 total over the last 7 days." rather than a numeric list), per-bar tooltips with absolute date + count, 30 ms stagger entrance via scaleY animation. Badge: 180 ms opacity + translateY entrance on mount; conditional render means each new badge re-fires the animation. Both animations respect prefers-reduced-motion. The earlier draft tabindex=0 on the SVG was correctly flagged by svelte-check as noninteractive_tabindex; SVG role="img" + aria-label is sufficient for SR navigation without putting it in the keyboard tab order. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --- src/lib/components/CompletionSparkline.svelte | 39 +++++++++++++++++-- src/lib/pages/TasksPage.svelte | 26 ++++++++++++- 2 files changed, 61 insertions(+), 4 deletions(-) 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 <title> 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 @@ + +