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); + }); -
+