- lib/error/: classify any caught error into a stable ErrorKind from the
transport signal (HTTP status / Connect Code / fetch TypeError /
navigator.onLine); map to translated error.* messages via reportError
(sticky Retry toast for retryable kinds) or errorMessageKey (inline).
Mail compose now surfaces the translated 403/error inline.
- lib/ui/view-state.svelte: shared loading/empty/error placeholder with
the right live-region role + optional action; entity tables
(races/sciences/ship-classes) migrated, rest adopt incrementally.
- map/selection-ring.ts: accent ring around the selected planet, fed into
the map buildExtras alongside the reach circles.
- lib/ui/sheet-dismiss.ts: tap-outside + drag-handle swipe-down dismissal
for the planet/ship-group bottom-sheets (hand-rolled pointer events).
Tests: error, view-state, selection-ring, sheet-dismiss (761 total).
Docs: ui/docs/error-state-ux.md (+ index); F4 marked done.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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) <noreply@anthropic.com>
Tokenize every remaining component <style> — calculator, order tab,
inspectors, tables, report sections, lobby, auth, mail, battle viewer,
toasts, map overlays. A scripted pass handled the unambiguous core
palette (text/bg/surface/border/accent/danger/muted), the rest were
mapped to the semantic/grey tokens by role.
Remaining colour literals are the documented exceptions only: the
battle-scene SVG data-visualisation palette (fixed dark, like the WebGL
map canvas), overlay scrims (modal / map-canvas), and directional or
deliberate drop shadows. The default theme stays dark until light
coherence is signed off across the views.
Updates ui/docs/design-system.md (migration status + exceptions).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two issues surfaced once the long-lived dev environment finally
reached the diplomail view:
1. `/sent` returns one row per recipient for broadcast and admin
fan-outs (so the admin tooling can render the materialised
audience). The list pane fed all rows into the stand-alone
bucket, so the `{#each entries as e (entryKey(e))}` key in
`thread-list.svelte` collapsed to the same `standalone:${id}`
for every recipient and Svelte 5 aborted the render with
`each_key_duplicate`. Dedupe stand-alones by `message_id` in
`buildEntries`.
2. The compose dialog exposed an `admin` kind toggle gated on
"owner of game". That was a Phase 28 plan decision, but admin
compose is an operator tool (server admin), not an in-game
action — every game owner should not be able to broadcast
admin notifications. Drop the admin option, the audience
sub-toggles, and the admin path through `submit`. The
`MailStore.composeAdmin` wrapper and the backend RPC stay so
the future admin UI can call them.
Vitest covers the fan-out dedup with three rows sharing one
`message_id` collapsing to a single stand-alone entry.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Step 6 — mail active view + subcomponents.
- `lib/active-view/mail.svelte` replaces the Phase 10 stub with the
list / detail layout: two-pane on desktop, one-pane stack on
mobile (CSS media query, no separate route).
- `lib/active-view/mail/thread-list.svelte` renders per-race
threads collapsed to their last message plus stand-alone
system / admin / outgoing-broadcast items, with unread badges.
- `lib/active-view/mail/thread-pane.svelte` is the chat-style
transcript for one race; bodies render through `textContent`,
per-message Show original / translation toggles flip the
rendering when a translated body is present, and a persistent
reply box at the bottom calls `mailStore.composePersonal`.
- `lib/active-view/mail/system-item-pane.svelte` renders one
stand-alone item read-only with the same translation toggle.
- `lib/active-view/mail/compose.svelte` is the compose dialog:
recipient race picker fed from `report.races[]`, kind toggle
(personal / broadcast / admin), admin sub-toggle for target
user / all and recipient-scope picker. Server-side enforces
paid-tier and owner gating; the UI surfaces 403 inline.
- `lib/active-view/mail/system-titles.ts` keeps the keyword →
i18n-title mapping for lifecycle-hook system mail so both the
list and the detail pane pick the same canonical title.
Step 9 — i18n strings (en + ru).
`game.mail.*`, `game.view.mail.badge`, `game.events.mail_new.*`,
`game.mail.system.*` keys added in lockstep across both locales
covering compose labels / validation copy / per-system titles /
translation toggle / reply / delete affordances.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>