From 4cbf4c5bb86756c90febea0931c806490584b89f Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 7 Feb 2026 12:10:10 +0200 Subject: [PATCH] Add WCAG 2.1 Level AA accessibility across all components A break timer designed to prevent RSI should be usable by people who already live with disabilities. This overhaul adds comprehensive accessibility without changing the visual design. Changes across 17 source files: - Global focus-visible outlines, sr-only utility, forced-colors support - prefers-reduced-motion kills all CSS animations AND JS Web Animations - All text upgraded to 4.5:1+ contrast ratio (WCAG AA) - Keyboard navigation for ColorPicker, Stepper, TimeSpinner - Screen reader: aria-live status regions, progressbar roles, labeled controls, sr-only chart data table, focus management on view changes - Focus trap on break screen, aria-hidden on decorative elements - Descriptive labels on all 25+ toggle/stepper instances in Settings - README updated with accessibility section and WCAG badge --- README.md | 20 +++- src-tauri/Cargo.lock | 113 ++-------------------- src/App.svelte | 26 ++++- src/app.css | 50 +++++++++- src/lib/components/BackgroundBlobs.svelte | 26 +++-- src/lib/components/BreakScreen.svelte | 68 +++++++++---- src/lib/components/ColorPicker.svelte | 60 ++++++++++-- src/lib/components/Dashboard.svelte | 43 +++++--- src/lib/components/FontSelector.svelte | 10 +- src/lib/components/MiniTimer.svelte | 7 +- src/lib/components/Settings.svelte | 103 ++++++++++++-------- src/lib/components/StatsView.svelte | 56 ++++++++--- src/lib/components/Stepper.svelte | 14 ++- src/lib/components/TimeSpinner.svelte | 27 ++++++ src/lib/components/TimerRing.svelte | 12 +++ src/lib/components/Titlebar.svelte | 5 +- src/lib/components/ToggleSwitch.svelte | 7 +- src/lib/utils/animate.ts | 38 ++++++++ 18 files changed, 459 insertions(+), 226 deletions(-) diff --git a/README.md b/README.md index 20e5d99..2f4cc47 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ Svelte 5 Rust Tailwind v4 + WCAG 2.1 AA

@@ -239,6 +240,23 @@ Native Windows toast notifications for:
+### ♿ Accessibility + +Core Cooldown targets **WCAG 2.1 Level AA** compliance. A break timer for preventing repetitive strain injury should be usable by everyone - including those who already live with disabilities. + +| | Feature | Description | +|:--|:--------|:------------| +| ⌨️ | **Full keyboard navigation** | Every control reachable and operable via keyboard alone. Arrow keys adjust color pickers, steppers, and time spinners. Tab/Shift+Tab cycles through all interactive elements. | +| 🔍 | **Visible focus indicators** | Global `:focus-visible` outlines on all interactive elements - no hidden or suppressed focus rings | +| 🗣️ | **Screen reader support** | `aria-live` regions announce timer state changes, break activities, and status updates. Progress rings use `role="progressbar"` with value text. Stats chart has a screen-reader-accessible data table. | +| 🎯 | **Focus management** | View transitions move focus to the new view's heading. Break screen traps focus to prevent interaction with obscured content. | +| 🎨 | **Color contrast** | All text meets 4.5:1 minimum contrast ratio against dark backgrounds (WCAG AA) | +| 🖥️ | **Windows High Contrast** | `forced-colors: active` media query maps all theme tokens to system colors | +| 🐢 | **Reduced motion** | `prefers-reduced-motion` disables all CSS animations/transitions *and* all JavaScript-driven Web Animations API effects. No functionality lost - just calmer. | +| 🏷️ | **Descriptive labels** | All toggle switches, steppers, buttons, and form controls have descriptive accessible names instead of generic labels | + +
+ --- ## 📦 Portability @@ -511,7 +529,7 @@ No contribution agreements to sign. No corporate CLAs. No licensing traps. Every - 🐛 Report bugs or rough edges - 🧘 Suggest new break activities (especially with physiotherapy or ergonomics knowledge) -- ♿ Improve accessibility +- ♿ Improve accessibility (WCAG 2.1 AA foundation is in place - help us push further) - 🐧 Port idle detection to macOS/Linux - 🌍 Translate the interface - 💌 Share it with someone who needs it diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 01d58fe..27efbd5 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -480,11 +480,10 @@ dependencies = [ [[package]] name = "core-cooldown" -version = "0.1.0" +version = "0.1.1" dependencies = [ "anyhow", "chrono", - "dirs 5.0.1", "serde", "serde_json", "tauri", @@ -683,34 +682,13 @@ dependencies = [ "crypto-common", ] -[[package]] -name = "dirs" -version = "5.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" -dependencies = [ - "dirs-sys 0.4.1", -] - [[package]] name = "dirs" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ - "dirs-sys 0.5.0", -] - -[[package]] -name = "dirs-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" -dependencies = [ - "libc", - "option-ext", - "redox_users 0.4.6", - "windows-sys 0.48.0", + "dirs-sys", ] [[package]] @@ -721,7 +699,7 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", - "redox_users 0.5.2", + "redox_users", "windows-sys 0.61.2", ] @@ -2936,17 +2914,6 @@ dependencies = [ "bitflags 2.10.0", ] -[[package]] -name = "redox_users" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" -dependencies = [ - "getrandom 0.2.17", - "libredox", - "thiserror 1.0.69", -] - [[package]] name = "redox_users" version = "0.5.2" @@ -3637,7 +3604,7 @@ dependencies = [ "anyhow", "bytes", "cookie", - "dirs 6.0.0", + "dirs", "dunce", "embed_plist", "getrandom 0.3.4", @@ -3687,7 +3654,7 @@ checksum = "ca7bd893329425df750813e95bd2b643d5369d929438da96d5bbb7cc2c918f74" dependencies = [ "anyhow", "cargo_toml", - "dirs 6.0.0", + "dirs", "glob", "heck 0.5.0", "json-patch", @@ -4238,7 +4205,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5e85aa143ceb072062fc4d6356c1b520a51d636e7bc8e77ec94be3608e5e80c" dependencies = [ "crossbeam-channel", - "dirs 6.0.0", + "dirs", "libappindicator", "muda", "objc2", @@ -4812,15 +4779,6 @@ dependencies = [ "windows-targets 0.42.2", ] -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - [[package]] name = "windows-sys" version = "0.59.0" @@ -4863,21 +4821,6 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - [[package]] name = "windows-targets" version = "0.52.6" @@ -4935,12 +4878,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -4959,12 +4896,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -4983,12 +4914,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -5019,12 +4944,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -5043,12 +4962,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -5067,12 +4980,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -5091,12 +4998,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -5159,7 +5060,7 @@ dependencies = [ "block2", "cookie", "crossbeam-channel", - "dirs 6.0.0", + "dirs", "dpi", "dunce", "gdkx11", diff --git a/src/App.svelte b/src/App.svelte index 91c25fa..2c656f5 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -52,10 +52,28 @@ }; }); - // Transition parameters - const DURATION = 700; + // Reduced motion preference + let reducedMotion = $state(window.matchMedia("(prefers-reduced-motion: reduce)").matches); + $effect(() => { + const mq = window.matchMedia("(prefers-reduced-motion: reduce)"); + const handler = (e: MediaQueryListEvent) => { reducedMotion = e.matches; }; + mq.addEventListener("change", handler); + return () => mq.removeEventListener("change", handler); + }); + + // Transition parameters — zero when reduced motion active + const DURATION = $derived(reducedMotion ? 0 : 700); const easing = cubicOut; + // Focus management: move focus to new view's heading on view change + $effect(() => { + const _view = effectiveView; + requestAnimationFrame(() => { + const heading = document.querySelector("h1[tabindex='-1']") as HTMLElement | null; + heading?.focus({ preventScroll: true }); + }); + }); + // When fullscreen_mode is OFF, the separate break window handles breaks, // so the main window should keep showing whatever view it was on (dashboard). const effectiveView = $derived( @@ -65,7 +83,7 @@ ); -

+
{#if $config.background_blobs_enabled} {/if} @@ -115,4 +133,4 @@
{/if} - + diff --git a/src/app.css b/src/app.css index 7ed789e..6ac3e48 100644 --- a/src/app.css +++ b/src/app.css @@ -14,7 +14,7 @@ --color-warning: #f0a500; --color-danger: #f85149; --color-text-pri: #ffffff; - --color-text-sec: #777777; + --color-text-sec: #8a8a8a; --color-text-dim: #3a3a3a; --color-caption-bg: #050505; } @@ -55,3 +55,51 @@ body { ::-webkit-scrollbar-thumb:hover { background: #333; } + +/* ── Accessibility: Screen-reader only ── */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} + +/* ── Accessibility: Focus indicators ── */ +:focus-visible { + outline: 2px solid var(--color-accent); + outline-offset: 2px; +} + +/* ── Accessibility: Reduced motion ── */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.001ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.001ms !important; + scroll-behavior: auto !important; + } +} + +/* ── Accessibility: Windows High Contrast ── */ +@media (forced-colors: active) { + :root { + --color-bg: Canvas; + --color-surface: Canvas; + --color-card: Canvas; + --color-card-lt: Canvas; + --color-border: ButtonBorder; + --color-accent: Highlight; + --color-accent-lt: Highlight; + --color-text-pri: CanvasText; + --color-text-sec: CanvasText; + --color-text-dim: GrayText; + --color-success: Highlight; + --color-warning: Highlight; + --color-danger: LinkText; + } +} diff --git a/src/lib/components/BackgroundBlobs.svelte b/src/lib/components/BackgroundBlobs.svelte index 42a8ce1..2b89a92 100644 --- a/src/lib/components/BackgroundBlobs.svelte +++ b/src/lib/components/BackgroundBlobs.svelte @@ -5,9 +5,17 @@ } let { accentColor, breakColor }: Props = $props(); + + let reducedMotion = $state(window.matchMedia("(prefers-reduced-motion: reduce)").matches); + $effect(() => { + const mq = window.matchMedia("(prefers-reduced-motion: reduce)"); + const handler = (e: MediaQueryListEvent) => { reducedMotion = e.matches; }; + mq.addEventListener("change", handler); + return () => mq.removeEventListener("change", handler); + }); -
+