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:
@@ -74,6 +74,18 @@
|
||||
let breathCountdown = $state(4);
|
||||
let breathScale = $state(0.6);
|
||||
|
||||
// Only announce phase name changes (not countdown ticks) to screen readers
|
||||
let breathAnnouncement = $state("");
|
||||
let lastBreathPhase = $state("");
|
||||
$effect(() => {
|
||||
// Extract just the phase name (e.g., "Inhale" from "Inhale 4")
|
||||
const phaseName = breathPhase?.split(' ')[0] ?? "";
|
||||
if (phaseName && phaseName !== lastBreathPhase) {
|
||||
lastBreathPhase = phaseName;
|
||||
breathAnnouncement = phaseName;
|
||||
}
|
||||
});
|
||||
|
||||
// Map raw 0.6–1.0 scale to 0.9–1.6 range for visible breathing text
|
||||
const textScale = $derived(0.9 + (breathScale - 0.6) * (0.7 / 0.4));
|
||||
|
||||
@@ -169,11 +181,11 @@
|
||||
<span
|
||||
class="block mt-1.5 text-[9px] tracking-wider uppercase text-center font-medium"
|
||||
style="transform: scale({textScale}); color: {breathColor}; opacity: {0.5 + breathT * 0.5}; transition: transform 0.15s ease-out, opacity 0.15s ease-out, color 0.15s ease-out;"
|
||||
aria-live="polite" aria-atomic="true"
|
||||
aria-label="Breathing guide: {breathPhase} {breathCountdown > 0 ? breathCountdown + ' seconds' : ''}"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{breathPhase}{breathCountdown > 0 ? ` ${breathCountdown}s` : ""}
|
||||
</span>
|
||||
<span class="sr-only" aria-live="polite" aria-atomic="true">{breathAnnouncement}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</TimerRing>
|
||||
@@ -185,16 +197,16 @@
|
||||
<h2 class="text-[17px] font-medium text-white mb-1.5" tabindex="-1">
|
||||
{$timer.breakTitle}
|
||||
</h2>
|
||||
<p class="text-[12px] leading-relaxed text-[#8a8a8a] mb-4 max-w-[240px]">
|
||||
<p class="text-[12px] leading-relaxed text-text-sec mb-4 max-w-[240px]">
|
||||
{$timer.breakMessage}
|
||||
</p>
|
||||
|
||||
{#if $config.show_break_activities}
|
||||
<div class="rounded-xl border border-[#ffffff08] bg-[#ffffff06] px-4 py-2.5 mb-4 max-w-[260px]">
|
||||
<div class="text-[9px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase mb-1">
|
||||
<div class="text-[9px] font-medium tracking-[0.15em] text-text-sec uppercase mb-1">
|
||||
{getCategoryLabel(currentActivity.category)}
|
||||
</div>
|
||||
<p class="text-[12px] leading-relaxed text-[#8a8a8a]" aria-live="polite">
|
||||
<p class="text-[12px] leading-relaxed text-text-sec" aria-live="polite">
|
||||
{currentActivity.text}
|
||||
</p>
|
||||
</div>
|
||||
@@ -205,7 +217,7 @@
|
||||
<button
|
||||
use:pressable
|
||||
class="rounded-full border border-[#333] px-5 py-2 text-[11px]
|
||||
tracking-wider text-[#8a8a8a] uppercase
|
||||
tracking-wider text-text-sec uppercase
|
||||
transition-colors duration-200
|
||||
hover:border-[#444] hover:text-[#ccc]"
|
||||
onclick={cancelBreak}
|
||||
@@ -226,10 +238,15 @@
|
||||
{/if}
|
||||
</div>
|
||||
{#if $config.snooze_limit > 0}
|
||||
<p class="mt-2 text-[9px] text-[#8a8a8a]">
|
||||
<p class="mt-2 text-[9px] text-text-sec">
|
||||
{$timer.snoozesUsed}/{$config.snooze_limit} snoozes used
|
||||
</p>
|
||||
{/if}
|
||||
{:else}
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||
<span tabindex="0" class="sr-only" aria-live="polite">
|
||||
Break in progress, please wait
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -302,11 +319,11 @@
|
||||
class:text-[10px]={!isModal}
|
||||
class:text-[9px]={isModal}
|
||||
style="transform: scale({textScale}); color: {breathColor}; opacity: {0.5 + breathT * 0.5}; transition: transform 0.15s ease-out, opacity 0.15s ease-out, color 0.15s ease-out;"
|
||||
aria-live="polite" aria-atomic="true"
|
||||
aria-label="Breathing guide: {breathPhase} {breathCountdown > 0 ? breathCountdown + ' seconds' : ''}"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{breathPhase}{breathCountdown > 0 ? ` ${breathCountdown}s` : ""}
|
||||
</span>
|
||||
<span class="sr-only" aria-live="polite" aria-atomic="true">{breathAnnouncement}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</TimerRing>
|
||||
@@ -328,7 +345,7 @@
|
||||
</h2>
|
||||
|
||||
<p
|
||||
class="max-w-[300px] text-center text-[13px] leading-relaxed text-[#8a8a8a]"
|
||||
class="max-w-[300px] text-center text-[13px] leading-relaxed text-text-sec"
|
||||
class:mb-4={$config.show_break_activities}
|
||||
class:mb-8={!$config.show_break_activities}
|
||||
use:fadeIn={{ delay: 0.35, y: 10 }}
|
||||
@@ -341,10 +358,10 @@
|
||||
class="mb-8 mx-auto max-w-[320px] rounded-2xl border border-[#1a1a1a] bg-[#0a0a0a] px-5 py-3.5 text-center"
|
||||
use:fadeIn={{ delay: 0.4, y: 10 }}
|
||||
>
|
||||
<div class="mb-1.5 text-[10px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
|
||||
<div class="mb-1.5 text-[10px] font-medium tracking-[0.15em] text-text-sec uppercase">
|
||||
{getCategoryLabel(currentActivity.category)}
|
||||
</div>
|
||||
<p class="text-[13px] leading-relaxed text-[#8a8a8a]" aria-live="polite">
|
||||
<p class="text-[13px] leading-relaxed text-text-sec" aria-live="polite">
|
||||
{currentActivity.text}
|
||||
</p>
|
||||
</div>
|
||||
@@ -354,8 +371,8 @@
|
||||
<div class="flex items-center gap-3" use:fadeIn={{ delay: 0.45, y: 10 }}>
|
||||
<button
|
||||
use:pressable
|
||||
class="rounded-full border border-[#222] px-6 py-2.5 text-[12px]
|
||||
tracking-wider text-[#8a8a8a] uppercase
|
||||
class="rounded-full border border-border px-6 py-2.5 text-[12px]
|
||||
tracking-wider text-text-sec uppercase
|
||||
transition-colors duration-200
|
||||
hover:border-[#333] hover:text-[#ccc]"
|
||||
onclick={cancelBreak}
|
||||
@@ -376,10 +393,15 @@
|
||||
{/if}
|
||||
</div>
|
||||
{#if $config.snooze_limit > 0}
|
||||
<p class="mt-3 text-[10px] text-[#8a8a8a]">
|
||||
<p class="mt-3 text-[10px] text-text-sec">
|
||||
{$timer.snoozesUsed}/{$config.snooze_limit} snoozes used
|
||||
</p>
|
||||
{/if}
|
||||
{:else}
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||
<span tabindex="0" class="sr-only" aria-live="polite">
|
||||
Break in progress, please wait
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<!-- Bottom progress bar for modal -->
|
||||
|
||||
Reference in New Issue
Block a user