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
+9 -6
View File
@@ -82,10 +82,13 @@ export interface CheckboxProps {
title?: string;
}
// Fill + tick per tone. Accent fills with `accent-solid` under a white tick.
// The status colours are TEXT shades — light in dark mode, dark in light mode —
// so their tick takes the page colour (surface-0), which contrasts both ways.
const TONE = {
accent: "bg-accent border-accent",
danger: "bg-status-danger border-status-danger",
warning: "bg-status-warning border-status-warning",
accent: "bg-accent-solid border-accent-solid text-white",
danger: "bg-status-danger border-status-danger text-surface-0",
warning: "bg-status-warning border-status-warning text-surface-0",
} as const;
/**
@@ -114,11 +117,11 @@ export function Checkbox({
className={`group inline-flex items-center gap-2 text-left disabled:cursor-not-allowed disabled:opacity-50 focus-visible:outline-none ${className ?? ""}`}
>
<span
className={`flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-[5px] border transition-colors group-focus-visible:ring-2 group-focus-visible:ring-accent/40 ${
checked ? TONE[tone] : "bg-surface-2 border-border group-hover:border-border-light"
className={`flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-[5px] border transition-colors group-focus-visible:ring-2 group-focus-visible:ring-accent/60 ${
checked ? TONE[tone] : "bg-surface-2 border-fg-muted group-hover:border-fg-secondary"
}`}
>
{checked && <Check className="h-3 w-3 text-white" strokeWidth={3} />}
{checked && <Check className="h-3 w-3" strokeWidth={3} aria-hidden />}
</span>
{label != null && (
<span