From 56dc04e724d35b6bd988f46e06f273aacc9101d0 Mon Sep 17 00:00:00 2001 From: Molecule AI Core-UIUX Date: Sun, 17 May 2026 17:55:45 +0000 Subject: [PATCH 1/7] fix(canvas/mobile): WCAG 2.4.7 focus-visible rings on all mobile canvas buttons Adds keyboard focus indicators (focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset) to every keyboard-navigable button across the mobile canvas layer. Buttons using inline style props now carry a Tailwind className pair that shows the ring on keyboard focus without affecting mouse/touch users. Components fixed: - MobileChat: Back, More, tab-switch, Retry, Remove file, Attach, Send - MobileHome: Spawn FAB - MobileSpawn: Close, template select, tier select, Spawn agent - MobileMe: Accent swatches, Theme/Density segmented controls - MobileDetail: Back, More, tab-switch, Open chat CTA - MobileComms: Filter chips (All, Errors) - components.tsx: AgentCard, FilterChips Refs #1384 Co-Authored-By: Claude Opus 4.7 --- canvas/src/components/mobile/MobileChat.tsx | 7 +++++++ canvas/src/components/mobile/MobileComms.tsx | 1 + canvas/src/components/mobile/MobileDetail.tsx | 5 ++++- canvas/src/components/mobile/MobileHome.tsx | 1 + canvas/src/components/mobile/MobileMe.tsx | 2 ++ canvas/src/components/mobile/MobileSpawn.tsx | 4 ++++ canvas/src/components/mobile/components.tsx | 2 ++ 7 files changed, 21 insertions(+), 1 deletion(-) diff --git a/canvas/src/components/mobile/MobileChat.tsx b/canvas/src/components/mobile/MobileChat.tsx index 375bd37a8..c05c7bf2c 100644 --- a/canvas/src/components/mobile/MobileChat.tsx +++ b/canvas/src/components/mobile/MobileChat.tsx @@ -339,6 +339,7 @@ export function MobileChat({ type="button" onClick={onBack} aria-label="Back" + className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1" style={{ width: 36, height: 36, @@ -385,6 +386,7 @@ export function MobileChat({ - @@ -183,6 +184,7 @@ export function MobileDetail({ key={t.id} type="button" onClick={() => setTab(t.id)} + className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1" style={{ padding: "8px 14px", borderRadius: 999, @@ -215,6 +217,7 @@ export function MobileDetail({ type="button" onClick={onChat} data-testid="mobile-chat-cta" + className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2" style={{ width: "100%", height: 52, diff --git a/canvas/src/components/mobile/MobileHome.tsx b/canvas/src/components/mobile/MobileHome.tsx index 271fa511f..2569ada91 100644 --- a/canvas/src/components/mobile/MobileHome.tsx +++ b/canvas/src/components/mobile/MobileHome.tsx @@ -183,6 +183,7 @@ export function MobileHome({ type="button" onClick={onSpawn} aria-label="Spawn new agent" + className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2" style={{ position: "absolute", right: 24, diff --git a/canvas/src/components/mobile/MobileMe.tsx b/canvas/src/components/mobile/MobileMe.tsx index c1735083d..ca859e9be 100644 --- a/canvas/src/components/mobile/MobileMe.tsx +++ b/canvas/src/components/mobile/MobileMe.tsx @@ -83,6 +83,7 @@ export function MobileMe({ type="button" onClick={() => setAccent(c)} aria-label={`Set accent ${c}`} + className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1" style={{ width: 36, height: 36, @@ -173,6 +174,7 @@ function SegmentedRow({ key={o.id} type="button" onClick={() => onChange(o.id)} + className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1" style={{ flex: 1, padding: "10px 8px", diff --git a/canvas/src/components/mobile/MobileSpawn.tsx b/canvas/src/components/mobile/MobileSpawn.tsx index 7ee62e89d..752b37f94 100644 --- a/canvas/src/components/mobile/MobileSpawn.tsx +++ b/canvas/src/components/mobile/MobileSpawn.tsx @@ -148,6 +148,7 @@ export function MobileSpawn({ dark, onClose }: { dark: boolean; onClose: () => v type="button" onClick={onClose} aria-label="Close" + className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1" style={{ width: 32, height: 32, @@ -214,6 +215,7 @@ export function MobileSpawn({ dark, onClose }: { dark: boolean; onClose: () => v setTplId(t.id); setTier(tCode); }} + className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1" style={{ background: on ? dark @@ -330,6 +332,7 @@ export function MobileSpawn({ dark, onClose }: { dark: boolean; onClose: () => v key={t} type="button" onClick={() => setTier(t)} + className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1" style={{ flex: 1, padding: "10px 8px", @@ -377,6 +380,7 @@ export function MobileSpawn({ dark, onClose }: { dark: boolean; onClose: () => v type="button" onClick={handleSpawn} disabled={busy || !tplId || templates.length === 0} + className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2" style={{ width: "100%", height: 52, diff --git a/canvas/src/components/mobile/components.tsx b/canvas/src/components/mobile/components.tsx index 592604a52..4d06ca210 100644 --- a/canvas/src/components/mobile/components.tsx +++ b/canvas/src/components/mobile/components.tsx @@ -291,6 +291,7 @@ export function AgentCard({ data-testid="workspace-card" aria-label={`${agent.name}, status: ${agent.status}, tier ${agent.tier}${agent.remote ? ", remote" : ""}`} onClick={onClick} + className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1" style={{ display: "block", width: "100%", @@ -444,6 +445,7 @@ export function FilterChips({ type="button" aria-checked={on} onClick={() => onChange(o.id)} + className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1" style={{ display: "inline-flex", alignItems: "center", -- 2.52.0 From 70d3ed690fbb0223b7298ad49f311af2be23ea59 Mon Sep 17 00:00:00 2001 From: Molecule AI Core-UIUX Date: Sun, 17 May 2026 18:13:48 +0000 Subject: [PATCH 2/7] fix(canvas/mobile): WCAG 2.4.7 focus-visible + aria-label on MobileCanvas buttons Adds focus-visible ring + aria-label to the three inline-styled buttons in MobileCanvas.tsx: - Reset zoom: focus-visible ring - Agent node: aria-label="Open {name}" + focus-visible ring (was missing both) - Spawn FAB: focus-visible ring Also adds MobileCanvas to the PR scope; refs #1395. Co-Authored-By: Claude Opus 4.7 --- canvas/src/components/mobile/MobileCanvas.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/canvas/src/components/mobile/MobileCanvas.tsx b/canvas/src/components/mobile/MobileCanvas.tsx index acdaa1689..53a462c4c 100644 --- a/canvas/src/components/mobile/MobileCanvas.tsx +++ b/canvas/src/components/mobile/MobileCanvas.tsx @@ -205,6 +205,7 @@ export function MobileCanvas({ type="button" onClick={resetView} aria-label="Reset zoom" + className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1" style={{ position: "absolute", right: 14, @@ -272,6 +273,8 @@ export function MobileCanvas({ key={l.agent.id} type="button" onClick={() => onOpen(l.agent.id)} + aria-label={`Open ${l.agent.name}`} + className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1" style={{ position: "absolute", left: `${l.x}%`, @@ -376,6 +379,7 @@ export function MobileCanvas({ type="button" onClick={onSpawn} aria-label="Spawn new agent" + className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2" style={{ position: "absolute", right: 24, -- 2.52.0 From ce697360dc314983da017d672159281b5169728d Mon Sep 17 00:00:00 2001 From: Molecule AI Core-UIUX Date: Sun, 17 May 2026 18:41:41 +0000 Subject: [PATCH 3/7] fix(canvas/mobile): add ARIA tab pattern + keyboard nav to MobileChat sub-tabs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add role="tab", aria-selected, tabIndex roving, and arrow/Home/End keyboard navigation to the "My Chat / Agent Comms" tab buttons in MobileChat.tsx — matching the WCAG 2.1.1 pattern already used in the bottom TabBar. Without ARIA roles, screen readers treat these as plain buttons with no tab-group semantics. Co-Authored-By: Claude Opus 4.7 --- canvas/src/components/mobile/MobileChat.tsx | 86 ++++++++++++++------- 1 file changed, 56 insertions(+), 30 deletions(-) diff --git a/canvas/src/components/mobile/MobileChat.tsx b/canvas/src/components/mobile/MobileChat.tsx index c05c7bf2c..e17f04eed 100644 --- a/canvas/src/components/mobile/MobileChat.tsx +++ b/canvas/src/components/mobile/MobileChat.tsx @@ -404,36 +404,62 @@ export function MobileChat({ {/* Sub-tabs */} -
- {( - [ - { id: "my", label: "My Chat" }, - { id: "a2a", label: "Agent Comms" }, - ] as const - ).map((t) => { - const on = tab === t.id; - return ( - - ); - })} -
+ {( + [ + { id: "my", label: "My Chat" }, + { id: "a2a", label: "Agent Comms" }, + ] as const + ).map((t) => { + const on = tab === t.id; + return ( + + ); + })} {/* Messages */} -- 2.52.0 From ed778fa66855cb1b9545a3df1632c03cee4657a9 Mon Sep 17 00:00:00 2001 From: Molecule AI Core-UIUX Date: Sun, 17 May 2026 18:42:44 +0000 Subject: [PATCH 4/7] fix(canvas/mobile): add ARIA tab roles + keyboard nav to MobileDetail tabs Add role="tablist", role="tab", aria-selected, tabIndex roving, and arrow/Home/End keyboard navigation to the Overview/Activity/Config/Memory tab buttons in MobileDetail.tsx. Matches the pattern already applied to the bottom TabBar and the MobileChat sub-tabs. Co-Authored-By: Claude Opus 4.7 --- canvas/src/components/mobile/MobileDetail.tsx | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/canvas/src/components/mobile/MobileDetail.tsx b/canvas/src/components/mobile/MobileDetail.tsx index 06f7d973b..0e18e7063 100644 --- a/canvas/src/components/mobile/MobileDetail.tsx +++ b/canvas/src/components/mobile/MobileDetail.tsx @@ -169,6 +169,8 @@ export function MobileDetail({ {/* Tabs */}
setTab(t.id)} + onKeyDown={(e) => { + const idx = TABS.findIndex((x) => x.id === t.id); + let nextIdx: number | null = null; + if (e.key === "ArrowRight" || e.key === "ArrowDown") { + nextIdx = (idx + 1) % TABS.length; + } else if (e.key === "ArrowLeft" || e.key === "ArrowUp") { + nextIdx = (idx - 1 + TABS.length) % TABS.length; + } else if (e.key === "Home") { + nextIdx = 0; + } else if (e.key === "End") { + nextIdx = TABS.length - 1; + } + if (nextIdx !== null) { + e.preventDefault(); + setTab(TABS[nextIdx]!.id); + setTimeout(() => { + const btns = document.querySelectorAll('[role="tab"]'); + (btns[nextIdx!] as HTMLButtonElement | null)?.focus(); + }, 0); + } + }} className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1" style={{ padding: "8px 14px", -- 2.52.0 From 376f62a4e9a0167f168e038206737167cce7dc54 Mon Sep 17 00:00:00 2001 From: Molecule AI Core-UIUX Date: Sun, 17 May 2026 18:43:57 +0000 Subject: [PATCH 5/7] fix(canvas/mobile): add aria-hidden to decorative check icon in MobileSpawn MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The check-mark span inside the selected-tier button is purely decorative visual feedback — screen readers already get the tier name from the button text. Mark it aria-hidden to avoid redundant announcements. Co-Authored-By: Claude Opus 4.7 --- canvas/src/components/mobile/MobileSpawn.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/canvas/src/components/mobile/MobileSpawn.tsx b/canvas/src/components/mobile/MobileSpawn.tsx index 752b37f94..fd66c783f 100644 --- a/canvas/src/components/mobile/MobileSpawn.tsx +++ b/canvas/src/components/mobile/MobileSpawn.tsx @@ -288,7 +288,7 @@ export function MobileSpawn({ dark, onClose }: { dark: boolean; onClose: () => v justifyContent: "center", }} > - {Icons.check({ size: 10, sw: 2.5 })} + )} -- 2.52.0 From e5f6c31eac682de01e1a89743b0ad3a54a3b4a45 Mon Sep 17 00:00:00 2001 From: Molecule AI Core-UIUX Date: Sun, 17 May 2026 20:16:40 +0000 Subject: [PATCH 6/7] fix(canvas/mobile): wrap decorative icons in aria-hidden in MobileChat/MobileDetail/MobileSpawn Add aria-hidden to decorative back/more icons inside aria-labeled buttons. Fixes: attach icon in MobileChat composer was missing aria-hidden. Matches the aria-hidden pattern already applied to MobileSpawn's check icon. Co-Authored-By: Claude Opus 4.7 --- canvas/src/components/mobile/MobileChat.tsx | 6 +++--- canvas/src/components/mobile/MobileDetail.tsx | 4 ++-- canvas/src/components/mobile/MobileSpawn.tsx | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/canvas/src/components/mobile/MobileChat.tsx b/canvas/src/components/mobile/MobileChat.tsx index e17f04eed..24ec0e594 100644 --- a/canvas/src/components/mobile/MobileChat.tsx +++ b/canvas/src/components/mobile/MobileChat.tsx @@ -353,7 +353,7 @@ export function MobileChat({ justifyContent: "center", }} > - {Icons.back({ size: 18 })} +
@@ -400,7 +400,7 @@ export function MobileChat({ justifyContent: "center", }} > - {Icons.more({ size: 18 })} +
{/* Sub-tabs */} @@ -706,7 +706,7 @@ export function MobileChat({ opacity: !reachable || sending || uploading ? 0.4 : 1, }} > - {Icons.attach({ size: 16 })} +