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
+10
View File
@@ -38,6 +38,12 @@ const tinted = {
hover: tint("--accent-hover"),
muted: "color-mix(in srgb, rgb(var(--accent)) 15%, rgb(var(--tint-base)))",
},
// The fill behind white text (`.btn-primary`, Switch on, Checkbox tick):
// `--accent` is a TEXT colour, too light in dark themes to carry white.
"accent-solid": {
DEFAULT: tint("--accent-solid"),
hover: tint("--accent-solid-hover"),
},
status: {
success: tint("--status-success"),
danger: tint("--status-danger"),
@@ -69,6 +75,10 @@ export default {
hover: "rgb(var(--accent-hover) / <alpha-value>)",
muted: "rgb(var(--accent) / 0.15)",
},
"accent-solid": {
DEFAULT: "rgb(var(--accent-solid) / <alpha-value>)",
hover: "rgb(var(--accent-solid-hover) / <alpha-value>)",
},
fg: {
primary: "rgb(var(--fg-primary) / <alpha-value>)",
secondary: "rgb(var(--fg-secondary) / <alpha-value>)",