From 642c5b73222816dc31b6ee231f00c4933dc28775 Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Fri, 22 May 2026 08:25:14 +0200 Subject: [PATCH 1/3] =?UTF-8?q?feat(ui):=20accessibility=20pass=20?= =?UTF-8?q?=E2=80=94=20WCAG=202.2=20AA=20for=20login/lobby/shell=20(F2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the a11y foundation and bring login, lobby, and the in-game shell to WCAG 2.2 AA: - Primitives: .sr-only + .skip-link (base.css), trapFocus (modal focus trap + restore) and restoreFocus (menu focus restore) actions, the --color-focus visible ring. - In-game shell: skip link + focusable main landmark; WAI-ARIA sidebar tabs (roving tabindex, arrow/Home/End, tabpanel wiring); menu Escape + focus restore (account / view / turn-navigator / map-toggles / bottom-tabs); mail compose as a role=dialog modal with a focus trap. - login / lobby / lobby-create: skip link + main landmark, field labels, role=alert / role=status live regions. - Map canvas: aria-label naming it a visual overview, with its data reachable by keyboard via the sidebar inspector and tables (accessible alternative; in-canvas keyboard nav deferred). Gates (chromium-desktop): tests/e2e/a11y-axe.spec.ts scans every top-level view for WCAG 2.2 AA violations (zero); a11y-keyboard.spec.ts covers the skip link, menu Escape+restore, and tab roving. Adds @axe-core/playwright. Docs: ui/docs/a11y.md (+ index). Marks F1 and F2 done in ui/PLAN-finalize.md. Co-Authored-By: Claude Opus 4.7 (1M context) --- ui/PLAN-finalize.md | 22 ++++- ui/docs/README.md | 3 + ui/docs/a11y.md | 77 +++++++++++++++++ ui/frontend/package.json | 1 + ui/frontend/src/lib/a11y/focus-trap.ts | 70 +++++++++++++++ ui/frontend/src/lib/a11y/restore-focus.ts | 34 ++++++++ .../src/lib/active-view/mail/compose.svelte | 39 ++++++--- .../src/lib/active-view/map-toggles.svelte | 8 +- ui/frontend/src/lib/active-view/map.svelte | 7 +- .../src/lib/header/account-menu.svelte | 8 +- .../src/lib/header/turn-navigator.svelte | 8 +- ui/frontend/src/lib/header/view-menu.svelte | 8 +- ui/frontend/src/lib/i18n/locales/en.ts | 3 + ui/frontend/src/lib/i18n/locales/ru.ts | 3 + .../src/lib/sidebar/bottom-tabs.svelte | 8 +- ui/frontend/src/lib/sidebar/sidebar.svelte | 8 +- ui/frontend/src/lib/sidebar/tab-bar.svelte | 47 +++++++++- ui/frontend/src/lib/theme/base.css | 38 ++++++++ .../src/routes/games/[id]/+layout.svelte | 11 ++- ui/frontend/src/routes/lobby/+page.svelte | 11 +-- .../src/routes/lobby/create/+page.svelte | 3 +- ui/frontend/src/routes/login/+page.svelte | 17 +--- ui/frontend/tests/e2e/a11y-axe.spec.ts | 86 +++++++++++++++++++ ui/frontend/tests/e2e/a11y-keyboard.spec.ts | 66 ++++++++++++++ ui/pnpm-lock.yaml | 19 ++++ 25 files changed, 559 insertions(+), 46 deletions(-) create mode 100644 ui/docs/a11y.md create mode 100644 ui/frontend/src/lib/a11y/focus-trap.ts create mode 100644 ui/frontend/src/lib/a11y/restore-focus.ts create mode 100644 ui/frontend/tests/e2e/a11y-axe.spec.ts create mode 100644 ui/frontend/tests/e2e/a11y-keyboard.spec.ts diff --git a/ui/PLAN-finalize.md b/ui/PLAN-finalize.md index c40f2bc..251d640 100644 --- a/ui/PLAN-finalize.md +++ b/ui/PLAN-finalize.md @@ -19,7 +19,15 @@ being marked done. --- -## F1 — Visual design system +## F1 — Visual design system — done + +Merged to `development` via PR #26 (2026-05-22): a shared design-token +system (`ui/frontend/src/lib/theme/`), light/dark theming with a picker +and pre-paint guard, and the whole UI migrated onto the tokens. +Documented literal exceptions: the battle-scene data-viz palette, overlay +scrims, and directional/deliberate drop shadows. The default theme is +dark; flipping it to `system` is an available follow-up. Tokens and +conventions live in `ui/docs/design-system.md`. Goal: replace the ad-hoc per-component styling (inline hex colors like `#0a0e1a`, one-off spacing) with a shared design language so every view @@ -36,7 +44,17 @@ Acceptance: no literal theme colors left in component ` diff --git a/ui/frontend/src/lib/active-view/map-toggles.svelte b/ui/frontend/src/lib/active-view/map-toggles.svelte index 9e01036..2b61bda 100644 --- a/ui/frontend/src/lib/active-view/map-toggles.svelte +++ b/ui/frontend/src/lib/active-view/map-toggles.svelte @@ -14,6 +14,7 @@ bottom-tabs bar. -
- {#each tabs as tab (tab.id)} +
+ {#each tabs as tab, i (tab.id)} diff --git a/ui/frontend/src/lib/theme/base.css b/ui/frontend/src/lib/theme/base.css index fe31674..44e5fb4 100644 --- a/ui/frontend/src/lib/theme/base.css +++ b/ui/frontend/src/lib/theme/base.css @@ -27,3 +27,41 @@ body { ::selection { background: var(--color-accent-subtle); } + +/* + * Visually-hidden content that stays available to assistive tech. Use + * for labels/announcements a sighted user gets from layout or icons. + */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +/* + * Skip link: visually hidden until focused, then slides into the + * top-left so a keyboard user can jump straight to the main content. + * Each layout renders one as its first focusable element, targeting a + * `tabindex="-1"` main region. + */ +.skip-link { + position: absolute; + left: var(--space-2); + top: -4rem; + z-index: 100; + padding: var(--space-2) var(--space-3); + background: var(--color-surface-overlay); + color: var(--color-text); + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + transition: top 120ms ease; +} +.skip-link:focus-visible { + top: var(--space-2); +} diff --git a/ui/frontend/src/routes/games/[id]/+layout.svelte b/ui/frontend/src/routes/games/[id]/+layout.svelte index 591ab86..c2ad782 100644 --- a/ui/frontend/src/routes/games/[id]/+layout.svelte +++ b/ui/frontend/src/routes/games/[id]/+layout.svelte @@ -46,6 +46,7 @@ fresh. import { onDestroy, onMount, setContext, untrack } from "svelte"; import { goto } from "$app/navigation"; import { page } from "$app/state"; + import { i18n } from "$lib/i18n/index.svelte"; import Header from "$lib/header/header.svelte"; import HistoryBanner from "$lib/header/history-banner.svelte"; import Sidebar from "$lib/sidebar/sidebar.svelte"; @@ -528,6 +529,9 @@ fresh.
+
-
+
{#if effectiveTool === "calc"} {:else if effectiveTool === "order"} diff --git a/ui/frontend/src/routes/lobby/+page.svelte b/ui/frontend/src/routes/lobby/+page.svelte index 35f198f..77cd66d 100644 --- a/ui/frontend/src/routes/lobby/+page.svelte +++ b/ui/frontend/src/routes/lobby/+page.svelte @@ -265,7 +265,8 @@ }); -
+ +

{i18n.t("lobby.title")}

@@ -297,7 +298,7 @@

{i18n.t("lobby.section.my_games")}

{#if listsLoading} -

{i18n.t("lobby.list_loading")}

+

{i18n.t("lobby.list_loading")}

{:else if myGames.length === 0}

{i18n.t("lobby.my_games.empty")}

{:else} @@ -323,7 +324,7 @@

{i18n.t("lobby.section.invitations")}

{#if listsLoading} -

{i18n.t("lobby.list_loading")}

+

{i18n.t("lobby.list_loading")}

{:else if invitations.length === 0}

{i18n.t("lobby.invitations.empty")}

{:else} @@ -357,7 +358,7 @@

{i18n.t("lobby.section.applications")}

{#if listsLoading} -

{i18n.t("lobby.list_loading")}

+

{i18n.t("lobby.list_loading")}

{:else if applications.length === 0}

{i18n.t("lobby.applications.empty")}

{:else} @@ -416,7 +417,7 @@

{i18n.t("lobby.section.public_games")}

{#if listsLoading} -

{i18n.t("lobby.list_loading")}

+

{i18n.t("lobby.list_loading")}

{:else if publicGames.length === 0}

{i18n.t("lobby.public_games.empty")}

{:else} diff --git a/ui/frontend/src/routes/lobby/create/+page.svelte b/ui/frontend/src/routes/lobby/create/+page.svelte index 9b169d1..0aa13d0 100644 --- a/ui/frontend/src/routes/lobby/create/+page.svelte +++ b/ui/frontend/src/routes/lobby/create/+page.svelte @@ -125,7 +125,8 @@ }); -
+ +

{i18n.t("lobby.create.title")}

{#if configError !== null}

{configError}

diff --git a/ui/frontend/src/routes/login/+page.svelte b/ui/frontend/src/routes/login/+page.svelte index 482c5b5..23bacd3 100644 --- a/ui/frontend/src/routes/login/+page.svelte +++ b/ui/frontend/src/routes/login/+page.svelte @@ -133,7 +133,8 @@ } -
+ +

{i18n.t("login.title")}

@@ -166,7 +167,7 @@ stroke-width="1.5" /> -