feat(a11y): contrast tokens, accent-solid, responsive drawer, keyboard menus

- Contrast: --fg-muted reaches 4.5:1 on every surface in all nine themes
  (dark default was 2.55:1); status chips and accent text clear 4.5:1 on
  their own tints. New --accent-solid(/-hover) + tailwind `accent-solid`
  is the fill behind white text (.btn-primary, Switch on, Checkbox tick);
  --accent is now a text colour. Switch off / unchecked Checkbox get a
  >=3:1 edge.
- <html lang> follows the chosen language; a global prefers-reduced-motion
  rule; index.html body uses a token instead of text-gray-200.
- Layout: the sidebar starts collapsed below lg and becomes an off-canvas
  drawer below md (menu button, backdrop, Escape, useDialog).
- ContextMenu: focus moves in, arrows/Home/End, Escape/Tab close, focus
  returns, a keyboard-opened menu anchors to its target. Select is a real
  listbox button (aria-expanded, activedescendant, type-ahead). PageTabs
  implements APG tabs with a tabpanel. New .btn-secondary/.btn-danger/
  .btn-icon and a .select class for native selects, all with focus rings.
- StatusBar announces connection and "N need you" politely; Sidebar's
  connection modal uses useDialog; EmptyState/Skeleton/AiToolIcon a11y.
- api.ts request(): a non-JSON 200 throws a translated EBADRESPONSE
  instead of "Unexpected token '<'".
This commit is contained in:
2026-10-05 13:22:00 +07:00
parent 17a5c7470d
commit 8fb703c6db
26 changed files with 917 additions and 167 deletions
+16 -3
View File
@@ -102,8 +102,15 @@ export function StatusBar({
<footer
data-testid="status-bar"
style={{ left }}
className="fixed bottom-0 right-0 z-20 flex h-6 items-center gap-4 border-t border-border bg-surface-1 px-3 text-xs text-fg-muted transition-[left] duration-200"
className="fixed bottom-0 right-0 z-20 flex h-6 items-center gap-4 overflow-hidden whitespace-nowrap border-t border-border bg-surface-1 px-3 text-xs text-fg-muted transition-[left] duration-200"
>
{/* What a screen reader is told without looking: the connection dropping
or coming back, and agents waiting on the user. Polite, and nothing
that ticks on its own (costs, running counts) — those stay silent. */}
<span role="status" className="sr-only" data-testid="status-bar-live">
{wsConnected ? t("live") : t("disconnected")}
{needYou > 0 ? ` · ${t("statusBar.needYou", { count: needYou })}` : ""}
</span>
<button
type="button"
onClick={onConnectionClick}
@@ -112,7 +119,11 @@ export function StatusBar({
wsConnected ? "text-status-success" : ""
}`}
>
{wsConnected ? <Wifi className="h-3 w-3" /> : <WifiOff className="h-3 w-3" />}
{wsConnected ? (
<Wifi className="h-3 w-3" aria-hidden />
) : (
<WifiOff className="h-3 w-3" aria-hidden />
)}
{wsConnected ? t("live") : t("disconnected")}
<span className="text-fg-muted">{`v${__APP_VERSION__}`}</span>
{updateAvailable && (
@@ -120,7 +131,9 @@ export function StatusBar({
data-testid="status-bar-update"
title={t("updateAvailable")}
className="h-1.5 w-1.5 rounded-full bg-status-success"
/>
>
<span className="sr-only">{t("updateAvailable")}</span>
</span>
)}
</button>
<span data-testid="status-bar-counts">