feat(gamification): today count + sparkline on Tasks header

Badge renders when today's count > 0. Sparkline renders when the
setting is enabled and any of the last 7 days has a completion.
Wrapped in a narrow aria-live region so increments announce without
re-reading the rest of the header.

Fix: converted todayCount from $derived module export to a getter
function (Svelte 5 derived_invalid_export constraint).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-24 20:45:47 +01:00
parent 54ddd41265
commit 3cadbb0f82
2 changed files with 34 additions and 5 deletions

View File

@@ -8,8 +8,10 @@
taskLists, addTaskList, renameTaskList, deleteTaskList,
settings, saveSettings,
} from "$lib/stores/page.svelte.js";
import { recentCompletions, todayCount } from "$lib/stores/completionStats.svelte";
import WipTaskList from '$lib/components/WipTaskList.svelte';
import EmptyState from '$lib/components/EmptyState.svelte';
import CompletionSparkline from "$lib/components/CompletionSparkline.svelte";
import EnergyChip from '$lib/components/EnergyChip.svelte';
import { SquareCheck, Search, ExternalLink, ChevronLeft, ArrowUpDown, Plus, X, ChevronRight, Zap } from 'lucide-svelte';
import Card from "$lib/components/Card.svelte";
@@ -284,7 +286,31 @@
<!-- Header -->
<div class="flex items-center px-7 pt-6 pb-2">
<div>
<h2 class="text-xl font-display text-text italic">Tasks</h2>
<div class="flex items-baseline gap-3">
<h2 class="text-xl font-display text-text italic">Tasks</h2>
<!-- Phase 8 badge + sparkline. aria-live scoped to just this
wrapper so screen readers announce completions without
re-reading the rest of the header. -->
<div
class="flex items-center gap-2"
aria-live="polite"
>
{#if todayCount() > 0}
<span
class="text-[11px] text-text-tertiary"
aria-label={`${todayCount()} ${todayCount() === 1 ? "task" : "tasks"} completed today`}
>
{todayCount()} today
</span>
{/if}
{#if settings.showMomentumSparkline}
<CompletionSparkline data={recentCompletions} />
{/if}
</div>
</div>
<p class="text-[11px] text-text-tertiary mt-1">Add tasks manually. Automatic extraction from your transcripts is coming.</p>
</div>
<div class="flex-1"></div>

View File

@@ -21,10 +21,13 @@ const DAYS = 7;
export const recentCompletions = $state<DailyCompletionCount[]>([]);
// Reactive derived value. Svelte 5 template consumers read it as a
// plain property (no parens) and it recomputes when recentCompletions
// changes.
export const todayCount = $derived(recentCompletions.at(-1)?.count ?? 0);
// Reactive getter. Svelte 5 does not allow exporting $derived at module
// level ("derived_invalid_export"), so we expose a function instead.
// Template consumers call todayCount() in reactive contexts and it
// recomputes when recentCompletions changes.
export function todayCount(): number {
return recentCompletions.at(-1)?.count ?? 0;
}
export async function refresh(): Promise<void> {
if (!hasTauriRuntime()) return;