a11y: Tasks 7-12 - Dashboard, Settings, StatsView, BreakScreen, Celebration

- Dashboard: text-text-sec tokens, nav landmark, toast hover persistence,
  goal progressbar ARIA, pomodoro sr-only text
- Settings: h3→h2 heading hierarchy, section aria-labelledby with ids,
  Working Hours heading added
- StatsView: h3→h2, tablist/tab/tabpanel ARIA pattern, sr-only data tables
  for 30-day chart and heatmap, contrast tokens
- BreakScreen: strict-mode focus safety span, breathing phase-only
  announcements, contrast tokens
- Celebration: JS-controlled hover/focus persistence, dismiss buttons,
  Escape key, removed pointer-events:none
- Titlebar: removed redundant role="banner" on <header>
This commit is contained in:
Your Name
2026-02-18 18:15:06 +02:00
parent 95f684450c
commit acf06c8d32
6 changed files with 443 additions and 216 deletions

View File

@@ -266,8 +266,8 @@
<button
aria-label="Back to dashboard"
use:pressable
class="mr-3 flex h-8 w-8 items-center justify-center rounded-full
text-[#8a8a8a] transition-colors hover:text-white"
class="mr-3 flex h-10 w-10 min-h-[44px] min-w-[44px] items-center justify-center rounded-full
text-text-sec transition-colors hover:text-white"
onclick={goBack}
>
<svg
@@ -294,14 +294,18 @@
</div>
<!-- Tab navigation -->
<div class="flex gap-1 px-5 mb-3" use:fadeIn={{ duration: 0.3, y: 6 }}>
<div class="flex gap-1 px-5 mb-3" role="tablist" aria-label="Statistics time range" use:fadeIn={{ duration: 0.3, y: 6 }}>
{#each [["today", "Today"], ["weekly", "Weekly"], ["monthly", "Monthly"]] as [tab, label]}
<button
use:pressable
class="rounded-lg px-4 py-1.5 text-[11px] tracking-wider uppercase transition-all duration-200
role="tab"
id="tab-{tab}"
aria-selected={activeTab === tab}
aria-controls="tabpanel-{tab}"
class="min-h-[44px] rounded-lg px-4 py-1.5 text-[11px] tracking-wider uppercase transition-all duration-200
{activeTab === tab
? 'bg-[#1a1a1a] text-white'
: 'text-[#8a8a8a] hover:text-white'}"
: 'text-text-sec hover:text-white'}"
onclick={() => activeTab = tab as any}
>
{label}
@@ -314,18 +318,19 @@
<div class="space-y-3">
{#if activeTab === "today"}
<div role="tabpanel" id="tabpanel-today" aria-labelledby="tab-today">
<!-- Today's summary -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0 }}>
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
<h2 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
Today
</h3>
</h2>
<div class="grid grid-cols-2 gap-4">
<div class="text-center">
<div class="text-[28px] font-semibold text-white tabular-nums">
{stats?.todayCompleted ?? 0}
</div>
<div class="text-[11px] text-[#8a8a8a]">Breaks taken</div>
<div class="text-[11px] text-text-sec">Breaks taken</div>
</div>
<div class="text-center">
<div class="text-[28px] font-semibold tabular-nums"
@@ -333,19 +338,19 @@
>
{compliancePercent}%
</div>
<div class="text-[11px] text-[#8a8a8a]">Compliance</div>
<div class="text-[11px] text-text-sec">Compliance</div>
</div>
<div class="text-center">
<div class="text-[28px] font-semibold text-white tabular-nums">
{breakTimeFormatted()}
</div>
<div class="text-[11px] text-[#8a8a8a]">Break time</div>
<div class="text-[11px] text-text-sec">Break time</div>
</div>
<div class="text-center">
<div class="text-[28px] font-semibold text-white tabular-nums">
{stats?.todaySkipped ?? 0}
</div>
<div class="text-[11px] text-[#8a8a8a]">Skipped</div>
<div class="text-[11px] text-text-sec">Skipped</div>
</div>
</div>
</section>
@@ -353,9 +358,9 @@
<!-- F10: Daily goal -->
{#if $config.daily_goal_enabled}
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.04 }}>
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
<h2 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
Daily Goal
</h3>
</h2>
<div class="flex items-center gap-4">
<div class="relative w-16 h-16">
<svg width="64" height="64" viewBox="0 0 64 64" style="transform: rotate(-90deg);">
@@ -375,7 +380,7 @@
<div class="text-[14px] text-white font-medium">
{stats?.dailyGoalProgress ?? 0} / {$config.daily_goal_breaks} breaks
</div>
<div class="text-[11px] text-[#8a8a8a]">
<div class="text-[11px] text-text-sec">
{stats?.dailyGoalMet ? "Goal reached!" : `${$config.daily_goal_breaks - (stats?.dailyGoalProgress ?? 0)} more to go`}
</div>
</div>
@@ -385,26 +390,26 @@
<!-- Streak -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.06 }}>
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
<h2 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
Streak
</h3>
</h2>
<div class="flex items-center justify-between">
<div>
<div class="text-[13px] text-white">Current streak</div>
<div class="text-[11px] text-[#8a8a8a]">Consecutive days with breaks</div>
<div class="text-[11px] text-text-sec">Consecutive days with breaks</div>
</div>
<div class="text-[24px] font-semibold tabular-nums" style="color: {$config.accent_color}">
{stats?.currentStreak ?? 0}
</div>
</div>
<div class="my-4 h-px bg-[#161616]"></div>
<div class="my-4 h-px bg-border"></div>
<div class="flex items-center justify-between">
<div>
<div class="text-[13px] text-white">Best streak</div>
<div class="text-[11px] text-[#8a8a8a]">All-time record</div>
<div class="text-[11px] text-text-sec">All-time record</div>
</div>
<div class="text-[24px] font-semibold text-white tabular-nums">
{stats?.bestStreak ?? 0}
@@ -413,13 +418,13 @@
<!-- F10: Next milestone -->
{#if nextMilestone()}
<div class="my-4 h-px bg-[#161616]"></div>
<div class="my-4 h-px bg-border"></div>
<div class="flex items-center justify-between">
<div>
<div class="text-[13px] text-white">Next milestone</div>
<div class="text-[11px] text-[#8a8a8a]">{nextMilestone()} day streak</div>
<div class="text-[11px] text-text-sec">{nextMilestone()} day streak</div>
</div>
<div class="text-[13px] text-[#8a8a8a] tabular-nums">
<div class="text-[13px] text-text-sec tabular-nums">
{nextMilestone()! - (stats?.currentStreak ?? 0)} days away
</div>
</div>
@@ -428,9 +433,9 @@
<!-- Weekly chart -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.12 }}>
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
<h2 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
Last 7 Days
</h3>
</h2>
<!-- svelte-ignore a11y_no_interactive_element_to_noninteractive_role -->
<canvas
@@ -458,7 +463,7 @@
</table>
{/if}
<div class="mt-3 flex items-center justify-center gap-4 text-[10px] text-[#8a8a8a]">
<div class="mt-3 flex items-center justify-center gap-4 text-[10px] text-text-sec">
<div class="flex items-center gap-1.5">
<div class="h-2 w-2 rounded-sm" style="background: {$config.accent_color}"></div>
Completed
@@ -470,35 +475,37 @@
</div>
</section>
</div>
{:else if activeTab === "weekly"}
<div role="tabpanel" id="tabpanel-weekly" aria-labelledby="tab-weekly">
<!-- Weekly summaries -->
{#each weeklySummaries as week, i}
{@const prevWeek = weeklySummaries[i + 1]}
{@const trend = prevWeek ? week.complianceRate - prevWeek.complianceRate : 0}
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: i * 0.06 }}>
<h3 class="mb-3 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
<h2 class="mb-3 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
Week of {week.weekStart}
</h3>
</h2>
<div class="grid grid-cols-3 gap-3 mb-3">
<div class="text-center">
<div class="text-[22px] font-semibold text-white tabular-nums">{week.totalCompleted}</div>
<div class="text-[10px] text-[#8a8a8a]">Completed</div>
<div class="text-[10px] text-text-sec">Completed</div>
</div>
<div class="text-center">
<div class="text-[22px] font-semibold text-white tabular-nums">{week.totalSkipped}</div>
<div class="text-[10px] text-[#8a8a8a]">Skipped</div>
<div class="text-[10px] text-text-sec">Skipped</div>
</div>
<div class="text-center">
<div class="text-[22px] font-semibold tabular-nums" style="color: {$config.accent_color}">
{Math.round(week.complianceRate * 100)}%
</div>
<div class="text-[10px] text-[#8a8a8a]">Compliance</div>
<div class="text-[10px] text-text-sec">Compliance</div>
</div>
</div>
<div class="flex items-center justify-between text-[11px]">
<span class="text-[#8a8a8a]">Avg {week.avgDailyCompleted.toFixed(1)} breaks/day</span>
<span class="text-text-sec">Avg {week.avgDailyCompleted.toFixed(1)} breaks/day</span>
{#if prevWeek}
<span class="flex items-center gap-1"
style="color: {trend > 0 ? '#3fb950' : trend < 0 ? '#f85149' : '#8a8a8a'};"
@@ -517,12 +524,14 @@
</section>
{/each}
</div>
{:else}
<div role="tabpanel" id="tabpanel-monthly" aria-labelledby="tab-monthly">
<!-- Monthly: 30-day chart -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0 }}>
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
<h2 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
Last 30 Days
</h3>
</h2>
<!-- svelte-ignore a11y_no_interactive_element_to_noninteractive_role -->
<canvas
@@ -532,7 +541,21 @@
aria-label="30-day break history chart"
></canvas>
<div class="mt-3 flex items-center justify-center gap-4 text-[10px] text-[#8a8a8a]">
{#if monthHistory.length > 0}
<table class="sr-only">
<caption>Break history for the last {monthHistory.length} days</caption>
<thead>
<tr><th>Date</th><th>Completed</th><th>Skipped</th></tr>
</thead>
<tbody>
{#each monthHistory as day}
<tr><td>{day.date}</td><td>{day.breaksCompleted}</td><td>{day.breaksSkipped}</td></tr>
{/each}
</tbody>
</table>
{/if}
<div class="mt-3 flex items-center justify-center gap-4 text-[10px] text-text-sec">
<div class="flex items-center gap-1.5">
<div class="h-2 w-2 rounded-sm" style="background: {$config.accent_color}"></div>
Completed
@@ -546,9 +569,9 @@
<!-- Heatmap -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.06 }}>
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
<h2 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
Activity Heatmap
</h3>
</h2>
<div class="flex justify-center">
<!-- svelte-ignore a11y_no_interactive_element_to_noninteractive_role -->
@@ -559,7 +582,21 @@
></canvas>
</div>
<div class="mt-3 flex items-center justify-center gap-2 text-[10px] text-[#8a8a8a]">
{#if monthHistory.length > 0}
<table class="sr-only">
<caption>Activity heatmap for the last {monthHistory.length} days</caption>
<thead>
<tr><th>Date</th><th>Breaks completed</th></tr>
</thead>
<tbody>
{#each monthHistory as day}
<tr><td>{day.date}</td><td>{day.breaksCompleted}</td></tr>
{/each}
</tbody>
</table>
{/if}
<div class="mt-3 flex items-center justify-center gap-2 text-[10px] text-text-sec">
<span>Less</span>
<div class="flex gap-1">
<div class="w-3 h-3 rounded-sm" style="background: #161616;"></div>
@@ -574,35 +611,36 @@
<!-- Monthly totals -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.12 }}>
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
<h2 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
Monthly Summary
</h3>
</h2>
<div class="grid grid-cols-2 gap-4">
<div class="text-center">
<div class="text-[22px] font-semibold text-white tabular-nums">{monthTotalCompleted}</div>
<div class="text-[10px] text-[#8a8a8a]">Total breaks</div>
<div class="text-[10px] text-text-sec">Total breaks</div>
</div>
<div class="text-center">
<div class="text-[22px] font-semibold tabular-nums" style="color: {$config.accent_color}">
{monthAvgCompliance()}%
</div>
<div class="text-[10px] text-[#8a8a8a]">Avg compliance</div>
<div class="text-[10px] text-text-sec">Avg compliance</div>
</div>
<div class="text-center">
<div class="text-[22px] font-semibold text-white tabular-nums">
{Math.floor(monthTotalTime / 60)} min
</div>
<div class="text-[10px] text-[#8a8a8a]">Total break time</div>
<div class="text-[10px] text-text-sec">Total break time</div>
</div>
<div class="text-center">
<div class="text-[22px] font-semibold text-white tabular-nums">
{(monthTotalCompleted / 30).toFixed(1)}
</div>
<div class="text-[10px] text-[#8a8a8a]">Avg daily breaks</div>
<div class="text-[10px] text-text-sec">Avg daily breaks</div>
</div>
</div>
</section>
</div>
{/if}
</div>