fix: comprehensive a11y and contrast QA pass across workbench

Automated QA loop (6 parallel browser agents, 2 rounds) found and fixed
15 accessibility, contrast, and responsive issues across all 8 pages:

- WCAG contrast: light-mode warning (#854d0e), error (#b91c1c), toggle
  off-state (surface-400), connection badge (fg-muted)
- ARIA: mode selector group+pressed, tab pattern ids+labelledby, nav
  and aside labels, dialog focus-return, alert roles on banners
- Responsive: library header flex-wrap, search/button aria-labels
- Focus: NavLink visible ring, dialog close button ring

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
elpresidank 2026-04-11 23:26:28 -05:00
parent 77a5fa5044
commit d097b790ff
24 changed files with 1360 additions and 17 deletions

View file

@ -251,11 +251,12 @@ export default function ChatPage() {
<div className="flex flex-wrap items-center gap-2">
{/* Mode selector */}
<div className="flex rounded-lg border border-border bg-surface-100 p-0.5">
<div role="group" aria-label="Chat mode" className="flex rounded-lg border border-border bg-surface-100 p-0.5">
{MODES.map((mode) => (
<button
key={mode.value}
onClick={() => setChatMode(mode.value)}
aria-pressed={chatMode === mode.value}
className={cn(
"rounded-md px-3 py-1 text-xs font-medium transition-colors",
chatMode === mode.value

View file

@ -324,6 +324,7 @@ function FlowRow({
}}
className="rounded p-1.5 text-fg-subtle hover:bg-error/10 hover:text-error"
title="Stop flow"
aria-label={`Stop flow ${flow.id}`}
>
<Square className="h-3.5 w-3.5" />
</button>
@ -451,7 +452,7 @@ export default function FlowsPage() {
)}
{error && (
<p className="mb-4 rounded-lg bg-error/10 px-4 py-2 text-sm text-error">
<p role="alert" className="mb-4 rounded-lg bg-error/10 px-4 py-2 text-sm text-error">
{error}
</p>
)}

View file

@ -500,6 +500,7 @@ export default function GraphPage() {
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search nodes..."
aria-label="Search nodes"
className="w-48 rounded-lg border border-border bg-surface-100 py-1.5 pl-8 pr-3 text-xs text-fg placeholder:text-fg-subtle focus:border-brand-500 focus:outline-none focus:ring-1 focus:ring-brand-500"
/>
{searchTerm && (

View file

@ -214,6 +214,7 @@ export default function KnowledgeCoresPage() {
disabled={actionInProgress === id}
className="flex items-center gap-1.5 rounded px-2.5 py-1.5 text-xs font-medium text-brand-400 hover:bg-brand-600/10 disabled:opacity-40"
title="Load core"
aria-label={`Load core ${id}`}
>
{actionInProgress === id ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
@ -227,6 +228,7 @@ export default function KnowledgeCoresPage() {
disabled={actionInProgress === id}
className="flex items-center gap-1.5 rounded px-2.5 py-1.5 text-xs font-medium text-error hover:bg-error/10 disabled:opacity-40"
title="Delete core"
aria-label={`Delete core ${id}`}
>
<Trash2 className="h-3.5 w-3.5" />
Delete

View file

@ -156,6 +156,7 @@ function UploadDialog({
e.stopPropagation();
setFile(null);
}}
aria-label="Remove selected file"
className="ml-1 text-fg-subtle hover:text-fg"
>
<X className="h-3 w-3" />
@ -345,7 +346,7 @@ export default function LibraryPage() {
return (
<div className="flex h-full flex-col">
{/* Header */}
<div className="mb-6 flex items-center justify-between">
<div className="mb-6 flex flex-wrap items-center justify-between gap-2">
<div className="flex items-center gap-3">
<LibraryBig className="h-6 w-6 text-brand-400" />
<h1 className="text-2xl font-bold text-fg">Library</h1>

View file

@ -71,9 +71,9 @@ export default function PromptsPage() {
{/* Tabs */}
<div role="tablist" aria-label="Prompt sections" className="mb-4 flex gap-1 rounded-lg bg-surface-100 p-1">
<button
id="tab-templates"
role="tab"
aria-selected={activeTab === "templates"}
aria-controls="panel-templates"
onClick={() => setActiveTab("templates")}
className={cn(
"flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium transition-colors",
@ -86,9 +86,9 @@ export default function PromptsPage() {
Templates
</button>
<button
id="tab-system"
role="tab"
aria-selected={activeTab === "system"}
aria-controls="panel-system"
onClick={() => setActiveTab("system")}
className={cn(
"flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium transition-colors",
@ -111,7 +111,7 @@ export default function PromptsPage() {
{/* Templates tab */}
{activeTab === "templates" && (
<div id="panel-templates" role="tabpanel" className="flex flex-1 flex-col gap-4 overflow-hidden">
<div id="panel-templates" role="tabpanel" aria-labelledby="tab-templates" className="flex flex-1 flex-col gap-4 overflow-hidden">
{loading && prompts.length === 0 && (
<div className="flex items-center justify-center py-12">
<Loader2 className="mr-2 h-5 w-5 animate-spin text-fg-subtle" />
@ -201,7 +201,7 @@ export default function PromptsPage() {
{/* System Prompt tab */}
{activeTab === "system" && (
<div id="panel-system" role="tabpanel" className="flex flex-1 flex-col overflow-hidden rounded-lg border border-border">
<div id="panel-system" role="tabpanel" aria-labelledby="tab-system" className="flex flex-1 flex-col overflow-hidden rounded-lg border border-border">
<div className="border-b border-border bg-surface-100 px-4 py-3">
<h2 className="text-xs font-medium uppercase tracking-wider text-fg-muted">
System Prompt

View file

@ -342,7 +342,7 @@ export default function SettingsPage() {
onClick={toggleTheme}
className={cn(
"relative inline-flex h-6 w-11 items-center rounded-full transition-colors",
isDark ? "bg-brand-600" : "bg-surface-300",
isDark ? "bg-brand-600" : "bg-surface-400",
)}
>
<span
@ -372,7 +372,7 @@ export default function SettingsPage() {
onClick={() => updateFeatureSwitches({ [key]: !enabled })}
className={cn(
"relative inline-flex h-6 w-11 items-center rounded-full transition-colors",
enabled ? "bg-brand-600" : "bg-surface-300",
enabled ? "bg-brand-600" : "bg-surface-400",
)}
>
<span className={cn(