From b935e48449b7f095de2b0ae189b71931cfbfcf2b Mon Sep 17 00:00:00 2001 From: Molecule AI Core-UIUX Date: Sun, 17 May 2026 17:55:45 +0000 Subject: [PATCH 1/8] 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 3dbaf4bcc8ed62ef90f91574e048bdd99808c96d Mon Sep 17 00:00:00 2001 From: Molecule AI Core-UIUX Date: Sun, 17 May 2026 18:13:48 +0000 Subject: [PATCH 2/8] 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 a9c18e066d655742f538baa14c3cac7987462c22 Mon Sep 17 00:00:00 2001 From: Molecule AI Core-UIUX Date: Sun, 17 May 2026 18:41:41 +0000 Subject: [PATCH 3/8] 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 03540edaa48b7646db2a3dd75273caaf2a87b840 Mon Sep 17 00:00:00 2001 From: Molecule AI Core-UIUX Date: Sun, 17 May 2026 18:42:44 +0000 Subject: [PATCH 4/8] 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 8772fa5433f714601d030c2349a08af9f4ee6bf4 Mon Sep 17 00:00:00 2001 From: Molecule AI Core-UIUX Date: Sun, 17 May 2026 18:43:57 +0000 Subject: [PATCH 5/8] 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 a338fa380704cd579f0d28a00240d1886b69e211 Mon Sep 17 00:00:00 2001 From: Molecule AI Core-UIUX Date: Sun, 17 May 2026 18:55:24 +0000 Subject: [PATCH 6/8] fix(canvas-mobile): set chat textarea font-size to 16px to prevent iOS zoom iOS Safari zooms in when a text input receives focus if its font-size is below 16px. Change the MobileChat composer textarea from 14.5px to 16px to prevent this unwanted page zoom on iOS devices. Fixes #1434. Co-Authored-By: Claude Opus 4.7 --- canvas/src/components/mobile/MobileChat.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/canvas/src/components/mobile/MobileChat.tsx b/canvas/src/components/mobile/MobileChat.tsx index e17f04eed..e361619b4 100644 --- a/canvas/src/components/mobile/MobileChat.tsx +++ b/canvas/src/components/mobile/MobileChat.tsx @@ -735,7 +735,9 @@ export function MobileChat({ border: "none", outline: "none", background: "transparent", - fontSize: 14.5, + // 16px minimum prevents iOS from zooming the page when the + // textarea receives focus (iOS triggers zoom for font-size < 16). + fontSize: 16, lineHeight: 1.4, color: p.text, padding: "6px 0", -- 2.52.0 From 977a3a7c9c5c0ab7f7b1c1b17c79e824e190d8ea Mon Sep 17 00:00:00 2001 From: Molecule AI Core-UIUX Date: Sun, 17 May 2026 19:11:11 +0000 Subject: [PATCH 7/8] fix(canvas/mobile): add ARIA radio role + keyboard nav to MobileComms filter buttons Add role="radiogroup", role="radio", aria-checked, tabIndex roving, and arrow/Home/End keyboard navigation to the All/Errors filter buttons in MobileComms.tsx. Screen readers now announce these as a radio group rather than unlabeled toggle buttons. The count span is aria-hidden since it repeats the label context. Co-Authored-By: Claude Opus 4.7 --- canvas/src/components/mobile/MobileComms.tsx | 39 ++++++++++++++++---- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/canvas/src/components/mobile/MobileComms.tsx b/canvas/src/components/mobile/MobileComms.tsx index 2722d9cd0..1427eb749 100644 --- a/canvas/src/components/mobile/MobileComms.tsx +++ b/canvas/src/components/mobile/MobileComms.tsx @@ -205,7 +205,11 @@ export function MobileComms({ dark }: { dark: boolean }) {

-
+
{( [ { id: "all", label: "All", n: items.length }, @@ -216,8 +220,33 @@ export function MobileComms({ dark }: { dark: boolean }) { return ( -- 2.52.0 From 80f949a5289fc9de9f3178654e329ad1baf92c5c Mon Sep 17 00:00:00 2001 From: Molecule AI Core-UIUX Date: Sun, 17 May 2026 20:57:21 +0000 Subject: [PATCH 8/8] fix(canvas): add missing focus-visible to settings buttons (WCAG 2.4.7) - settings-panel.css: add focus-visible to secrets-tab__clear-search, delete-dialog__confirm-btn, delete-dialog__cancel-btn, secrets-tab__add-btn, secrets-tab__refresh-btn - globals.css: add global button:focus-visible and a:focus-visible fallback so Tailwind buttons in TokensTab / OrgTokensTab are keyboard-navigable Co-Authored-By: Claude Opus 4.7 --- canvas/src/app/globals.css | 13 +++++++++++++ canvas/src/styles/settings-panel.css | 25 +++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/canvas/src/app/globals.css b/canvas/src/app/globals.css index 7f93dc53b..2c44ab164 100644 --- a/canvas/src/app/globals.css +++ b/canvas/src/app/globals.css @@ -287,4 +287,17 @@ body { outline: 2px solid var(--accent, #3b5bdb); outline-offset: 2px; } + + /* Global focus-visible fallback for Tailwind buttons (WCAG 2.4.7). + Specific component rules (e.g. .settings-panel__close:focus-visible) + override this via higher specificity. */ + button:focus-visible { + outline: 2px solid var(--accent, #3b5bdb); + outline-offset: 2px; + } + + a:focus-visible { + outline: 2px solid var(--accent, #3b5bdb); + outline-offset: 2px; + } } diff --git a/canvas/src/styles/settings-panel.css b/canvas/src/styles/settings-panel.css index 5d4be4514..cd7a2b6a0 100644 --- a/canvas/src/styles/settings-panel.css +++ b/canvas/src/styles/settings-panel.css @@ -411,6 +411,11 @@ color: #f4f4f5; } +.secrets-tab__add-btn:focus-visible { + outline: var(--focus-ring); + outline-offset: var(--focus-ring-offset); +} + /* ── Shared UI ─────────────────────────────────────── */ .key-value-field { @@ -585,6 +590,11 @@ background: #1e40af; } +.secrets-tab__refresh-btn:focus-visible { + outline: var(--focus-ring); + outline-offset: var(--focus-ring-offset); +} + .secrets-tab__no-results { text-align: center; padding: 24px; @@ -601,6 +611,11 @@ font-size: 14px; } +.secrets-tab__clear-search:focus-visible { + outline: var(--focus-ring); + outline-offset: var(--focus-ring-offset); +} + /* ── Delete confirmation dialog ────────────────────── */ .delete-dialog__overlay { @@ -661,6 +676,16 @@ .delete-dialog__confirm-btn:disabled { opacity: 0.4; cursor: not-allowed; } +.delete-dialog__confirm-btn:focus-visible { + outline: var(--focus-ring); + outline-offset: var(--focus-ring-offset); +} + +.delete-dialog__cancel-btn:focus-visible { + outline: var(--focus-ring); + outline-offset: var(--focus-ring-offset); +} + /* ── Unsaved changes guard ─────────────────────────── */ .guard-dialog__overlay { -- 2.52.0