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 <title>
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>
This commit is contained in:
2026-04-25 00:17:53 +01:00
parent 3bc34d2873
commit dd45f10cd4
2 changed files with 61 additions and 4 deletions

View File

@@ -298,7 +298,7 @@
>
{#if todayCount() > 0}
<span
class="text-[11px] text-text-tertiary"
class="text-[11px] text-text-tertiary badge-today"
aria-label={`${todayCount()} ${todayCount() === 1 ? "task" : "tasks"} completed today`}
>
{todayCount()} today
@@ -689,3 +689,27 @@
</div>
</div>
</div>
<style>
/* Phase 9 polish: gentle entrance animation when the today-count
badge mounts (it is conditionally rendered, so each new render
re-fires the animation). prefers-reduced-motion disables. */
:global(.badge-today) {
animation: badge-pop 180ms ease both;
}
@media (prefers-reduced-motion: reduce) {
:global(.badge-today) {
animation: none;
}
}
@keyframes badge-pop {
from {
opacity: 0;
transform: translateY(2px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>