mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-07-01 09:29:38 +02:00
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:
parent
77a5fa5044
commit
d097b790ff
24 changed files with 1360 additions and 17 deletions
1210
ts/bun.lock
Normal file
1210
ts/bun.lock
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1 @@
|
|||
Alice Johnson is a senior engineer at Acme Corporation. Acme develops CloudSync, a cloud storage platform. CloudSync uses Amazon Web Services for hosting.
|
||||
BIN
ts/data/librarian/docs/2b954221-169b-4297-9d0b-5733676bdd19.bin
Normal file
BIN
ts/data/librarian/docs/2b954221-169b-4297-9d0b-5733676bdd19.bin
Normal file
Binary file not shown.
|
|
@ -0,0 +1 @@
|
|||
Bob Chen is the CTO of Acme Corporation. Alice reports to Bob. CloudSync was launched in 2024 and competes with Dropbox.
|
||||
BIN
ts/data/librarian/docs/5c884d9d-7b96-48c2-826c-7ca21314edf8.bin
Normal file
BIN
ts/data/librarian/docs/5c884d9d-7b96-48c2-826c-7ca21314edf8.bin
Normal file
Binary file not shown.
|
|
@ -0,0 +1 @@
|
|||
Alice Johnson is a senior engineer at Acme Corporation. Acme develops CloudSync, a cloud storage platform. CloudSync uses Amazon Web Services for hosting.
|
||||
|
|
@ -0,0 +1 @@
|
|||
Bob Chen is the CTO of Acme Corporation. Alice reports to Bob. CloudSync was launched in 2024 and competes with Dropbox.
|
||||
|
|
@ -0,0 +1 @@
|
|||
The Eiffel Tower is a wrought-iron lattice tower in Paris, France
|
||||
88
ts/data/librarian/librarian-state.json
Normal file
88
ts/data/librarian/librarian-state.json
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
{
|
||||
"documents": {
|
||||
"d2871466-6dbd-46f3-b548-8cffc77c466f": {
|
||||
"kind": "text/plain",
|
||||
"title": "Test Knowledge",
|
||||
"comments": "Test doc",
|
||||
"user": "test",
|
||||
"tags": [
|
||||
"test"
|
||||
],
|
||||
"documentType": "source",
|
||||
"id": "d2871466-6dbd-46f3-b548-8cffc77c466f",
|
||||
"time": 1775544601926
|
||||
},
|
||||
"2b954221-169b-4297-9d0b-5733676bdd19": {
|
||||
"id": "2b954221-169b-4297-9d0b-5733676bdd19",
|
||||
"time": 1775545730516,
|
||||
"kind": "application/pdf",
|
||||
"title": "Acme Corporation Test Document",
|
||||
"comments": "End-to-end pipeline test",
|
||||
"user": "test",
|
||||
"tags": [
|
||||
"test",
|
||||
"pipeline"
|
||||
],
|
||||
"documentType": "source"
|
||||
},
|
||||
"09b14acd-6274-48f3-a468-1ef78ec71147": {
|
||||
"id": "09b14acd-6274-48f3-a468-1ef78ec71147",
|
||||
"user": "test",
|
||||
"kind": "text/plain",
|
||||
"title": "Page 1",
|
||||
"parentId": "2b954221-169b-4297-9d0b-5733676bdd19",
|
||||
"documentType": "page",
|
||||
"time": 1775545736563,
|
||||
"comments": "",
|
||||
"tags": []
|
||||
},
|
||||
"b7bc3c30-65fc-424b-bb8b-6dfeca739250": {
|
||||
"id": "b7bc3c30-65fc-424b-bb8b-6dfeca739250",
|
||||
"user": "test",
|
||||
"kind": "text/plain",
|
||||
"title": "Page 2",
|
||||
"parentId": "2b954221-169b-4297-9d0b-5733676bdd19",
|
||||
"documentType": "page",
|
||||
"time": 1775545738656,
|
||||
"comments": "",
|
||||
"tags": []
|
||||
},
|
||||
"5c884d9d-7b96-48c2-826c-7ca21314edf8": {
|
||||
"id": "5c884d9d-7b96-48c2-826c-7ca21314edf8",
|
||||
"time": 1775545966287,
|
||||
"kind": "application/pdf",
|
||||
"title": "Acme Corporation Test Document",
|
||||
"comments": "End-to-end pipeline test",
|
||||
"user": "test",
|
||||
"tags": [
|
||||
"test",
|
||||
"pipeline"
|
||||
],
|
||||
"documentType": "source"
|
||||
},
|
||||
"897f90a7-58f4-4466-933c-adf6f8d904ca": {
|
||||
"id": "897f90a7-58f4-4466-933c-adf6f8d904ca",
|
||||
"user": "test",
|
||||
"kind": "text/plain",
|
||||
"title": "Page 1",
|
||||
"parentId": "5c884d9d-7b96-48c2-826c-7ca21314edf8",
|
||||
"documentType": "page",
|
||||
"time": 1775545972298,
|
||||
"comments": "",
|
||||
"tags": []
|
||||
},
|
||||
"35befdd5-50f2-4432-a7da-b7d95fcc6ab3": {
|
||||
"id": "35befdd5-50f2-4432-a7da-b7d95fcc6ab3",
|
||||
"user": "test",
|
||||
"kind": "text/plain",
|
||||
"title": "Page 2",
|
||||
"parentId": "5c884d9d-7b96-48c2-826c-7ca21314edf8",
|
||||
"documentType": "page",
|
||||
"time": 1775545974301,
|
||||
"comments": "",
|
||||
"tags": []
|
||||
}
|
||||
},
|
||||
"processing": {},
|
||||
"collections": []
|
||||
}
|
||||
BIN
ts/data/test.pdf
Normal file
BIN
ts/data/test.pdf
Normal file
Binary file not shown.
|
|
@ -52,7 +52,7 @@ export function RootLayout() {
|
|||
|
||||
{/* Connection lost banner */}
|
||||
{isDisconnected && (
|
||||
<div className="flex items-center gap-2 border-b border-warning/30 bg-warning/10 px-4 py-2 text-xs text-warning">
|
||||
<div role="alert" className="flex items-center gap-2 border-b border-warning/30 bg-warning/10 px-4 py-2 text-xs text-warning">
|
||||
<WifiOff className="h-3.5 w-3.5" />
|
||||
<span>Connection lost. Attempting to reconnect...</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ interface NavItemProps {
|
|||
|
||||
function NavItem({ to, icon: Icon, label }: NavItemProps) {
|
||||
return (
|
||||
<NavLink to={to} className="w-full">
|
||||
<NavLink to={to} className="w-full rounded-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:ring-offset-1 focus-visible:ring-offset-surface-50">
|
||||
{({ isActive }) => (
|
||||
<div
|
||||
className={cn(
|
||||
|
|
@ -72,7 +72,7 @@ function ConnectionBadge() {
|
|||
? "text-warning"
|
||||
: isConnected
|
||||
? "text-success"
|
||||
: "text-fg-subtle",
|
||||
: "text-fg-muted",
|
||||
)}
|
||||
>
|
||||
<span
|
||||
|
|
@ -148,7 +148,7 @@ function FlowSelectorDropdown() {
|
|||
|
||||
export function Sidebar() {
|
||||
return (
|
||||
<aside className="flex h-screen w-sidebar shrink-0 flex-col border-r border-border bg-surface-50">
|
||||
<aside aria-label="Sidebar" className="flex h-screen w-sidebar shrink-0 flex-col border-r border-border bg-surface-50">
|
||||
{/* Logo area */}
|
||||
<div className="flex h-14 items-center gap-2 px-4">
|
||||
<TestTube2 className="h-5 w-5 text-brand-500" />
|
||||
|
|
@ -167,7 +167,7 @@ export function Sidebar() {
|
|||
<div className="mx-3 border-t border-border" />
|
||||
|
||||
{/* Navigation links */}
|
||||
<nav className="flex flex-1 flex-col gap-0.5 overflow-y-auto px-2 py-3">
|
||||
<nav aria-label="Main navigation" className="flex flex-1 flex-col gap-0.5 overflow-y-auto px-2 py-3">
|
||||
<NavItem to="/chat" icon={MessageSquareText} label="Chat" />
|
||||
<NavItem to="/library" icon={LibraryBig} label="Library" />
|
||||
<NavItem to="/graph" icon={Rotate3d} label="Graph" />
|
||||
|
|
|
|||
|
|
@ -35,6 +35,17 @@ export function Dialog({
|
|||
const titleId = useId();
|
||||
const dialogRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Save the element that triggered the dialog so we can restore focus on close
|
||||
const triggerRef = useRef<HTMLElement | null>(null);
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
triggerRef.current = document.activeElement as HTMLElement | null;
|
||||
} else if (triggerRef.current) {
|
||||
triggerRef.current.focus();
|
||||
triggerRef.current = null;
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
// Close on Escape key
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
|
|
@ -132,7 +143,7 @@ export function Dialog({
|
|||
<button
|
||||
onClick={onClose}
|
||||
aria-label="Close dialog"
|
||||
className="rounded-md p-1 text-fg-subtle hover:bg-surface-200 hover:text-fg"
|
||||
className="rounded-md p-1 text-fg-subtle hover:bg-surface-200 hover:text-fg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -130,6 +130,6 @@ html.light {
|
|||
|
||||
/* Semantic colors stay vivid but slightly darker for contrast */
|
||||
--color-success: #16a34a;
|
||||
--color-warning: #ca8a04;
|
||||
--color-error: #dc2626;
|
||||
--color-warning: #854d0e;
|
||||
--color-error: #b91c1c;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue