fix(canvas): focus-visible rings across all tabs components

Fix all interactive buttons across 9 tabs files — add or upgrade
focus-visible rings (WCAG 2.4.7). Also normalize weak /60 and /40
opacity rings to full-color variants.

Files fixed:
- EventsTab.tsx: Refresh, event row expand
- TracesTab.tsx: Refresh, trace row expand
- ExternalConnectionSection.tsx: Show info, Rotate creds (red),
  dialog Cancel (accent), dialog Rotate (red)
- ScheduleTab.tsx: Create/Update, Cancel
- ConfigTab.tsx: Save, Cancel, Edit Agent Card, Apply env template,
  Save & Restart, Save, Reload
- MemoryTab.tsx: Awareness expand/collapse, awareness Open, expand
  (collapsed), Hide/Show Advanced, Refresh, + Add, Save, Cancel,
  memory row expand, Save (edit), Cancel (edit), Edit, Delete,
  Show
- ChannelsTab.tsx: Connect Channel
- SkillsTab.tsx: Retry registry load
- AttachmentImage.tsx: image preview button

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Molecule AI · core-uiux 2026-05-10 18:27:44 +00:00 committed by Molecule AI Core-FE
parent 0035b04e2e
commit 317c445839
9 changed files with 34 additions and 34 deletions

View File

@ -370,7 +370,7 @@ export function ChannelsTab({ workspaceId }: Props) {
// Was bg-accent-strong hover:bg-accent — accent is the
// LIGHTER variant; same AA contrast trap fixed in
// ScheduleTab/MemoryTab/OnboardingWizard.
className="w-full text-xs py-1.5 rounded bg-accent hover:bg-accent-strong text-white transition focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/60 focus-visible:ring-offset-2 focus-visible:ring-offset-surface"
className="w-full text-xs py-1.5 rounded bg-accent hover:bg-accent-strong text-white transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-surface"
>
Connect Channel
</button>

View File

@ -83,11 +83,11 @@ function AgentCardSection({ workspaceId }: { workspaceId: string }) {
{error && <div className="px-2 py-1 bg-red-900/30 border border-red-800 rounded text-[10px] text-bad">{error}</div>}
<div className="flex gap-2">
<button type="button" onClick={handleSave} disabled={saving}
className="px-2 py-1 bg-accent hover:bg-accent-strong text-[10px] rounded text-white disabled:opacity-50 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/60 focus-visible:ring-offset-1 focus-visible:ring-offset-surface">
className="px-2 py-1 bg-accent hover:bg-accent-strong text-[10px] rounded text-white disabled:opacity-50 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1 focus-visible:ring-offset-surface">
{saving ? "Saving..." : "Save"}
</button>
<button type="button" onClick={() => setEditing(false)}
className="px-2 py-1 bg-surface-card hover:bg-surface-elevated hover:text-ink text-[10px] rounded text-ink-mid transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 focus-visible:ring-offset-1 focus-visible:ring-offset-surface">Cancel</button>
className="px-2 py-1 bg-surface-card hover:bg-surface-elevated hover:text-ink text-[10px] rounded text-ink-mid transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1 focus-visible:ring-offset-surface">Cancel</button>
</div>
</div>
) : (
@ -101,7 +101,7 @@ function AgentCardSection({ workspaceId }: { workspaceId: string }) {
)}
{success && <div className="mt-2 px-2 py-1 bg-green-900/30 border border-green-800 rounded text-[10px] text-good">Updated</div>}
<button type="button" onClick={() => { setDraft(JSON.stringify(card || {}, null, 2)); setEditing(true); setError(null); setSuccess(false); }}
className="mt-2 text-[10px] text-accent hover:text-accent">Edit Agent Card</button>
className="mt-2 text-[10px] text-accent hover:text-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1">Edit Agent Card</button>
</div>
)}
</Section>
@ -876,7 +876,7 @@ export function ConfigTab({ workspaceId }: Props) {
<button
type="button"
onClick={() => updateNested("runtime_config" as keyof ConfigData, "required_env", currentModelSpec.required_env)}
className="text-accent hover:text-accent underline"
className="text-accent hover:text-accent underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1"
>
Apply
</button>
@ -1016,7 +1016,7 @@ export function ConfigTab({ workspaceId }: Props) {
onClick={() => handleSave(true)}
disabled={!isDirty || saving}
// Same accent-LIGHTER fix shipped on every other tab.
className="px-3 py-1.5 bg-accent hover:bg-accent-strong text-xs rounded text-white disabled:opacity-30 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/60 focus-visible:ring-offset-1 focus-visible:ring-offset-surface"
className="px-3 py-1.5 bg-accent hover:bg-accent-strong text-xs rounded text-white disabled:opacity-30 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1 focus-visible:ring-offset-surface"
>
{saving ? "Restarting..." : "Save & Restart"}
</button>
@ -1024,14 +1024,14 @@ export function ConfigTab({ workspaceId }: Props) {
type="button"
onClick={() => handleSave(false)}
disabled={!isDirty || saving}
className="px-3 py-1.5 bg-surface-card hover:bg-surface-card text-xs rounded text-ink-mid disabled:opacity-30 transition-colors"
className="px-3 py-1.5 bg-surface-card hover:bg-surface-card text-xs rounded text-ink-mid disabled:opacity-30 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1"
>
Save
</button>
<button
type="button"
onClick={loadConfig}
className="px-3 py-1.5 bg-surface-card hover:bg-surface-card text-xs rounded text-ink-mid ml-auto"
className="px-3 py-1.5 bg-surface-card hover:bg-surface-card text-xs rounded text-ink-mid ml-auto focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1"
>
Reload
</button>

View File

@ -75,7 +75,7 @@ export function EventsTab({ workspaceId }: Props) {
// Was hover:bg-surface-card on top of bg-surface-card — silent
// no-op hover. Lift to surface-elevated, matching the Cancel
// pattern from ConfirmDialog.
className="px-2 py-1 bg-surface-card hover:bg-surface-elevated hover:text-ink text-[10px] rounded text-ink-mid transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/50"
className="px-2 py-1 bg-surface-card hover:bg-surface-elevated hover:text-ink text-[10px] rounded text-ink-mid transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1"
>
Refresh
</button>
@ -106,7 +106,7 @@ export function EventsTab({ workspaceId }: Props) {
// toggles or what it controls.
aria-expanded={isOpen}
aria-controls={panelId}
className="w-full flex items-center gap-2 px-3 py-2 text-left rounded-t hover:bg-surface-elevated/40 focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-accent/50 transition-colors"
className="w-full flex items-center gap-2 px-3 py-2 text-left rounded-t hover:bg-surface-elevated/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1 transition-colors"
>
<span
className={`text-xs font-mono ${

View File

@ -87,7 +87,7 @@ export function ExternalConnectionSection({ workspaceId }: Props) {
type="button"
onClick={showConnection}
disabled={busy !== null}
className="px-3 py-1.5 bg-surface-card hover:bg-surface-card text-xs rounded text-ink-mid disabled:opacity-30 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/60"
className="px-3 py-1.5 bg-surface-card hover:bg-surface-card text-xs rounded text-ink-mid disabled:opacity-30 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1"
>
{busy === "show" ? "Loading…" : "Show connection info"}
</button>
@ -95,7 +95,7 @@ export function ExternalConnectionSection({ workspaceId }: Props) {
type="button"
onClick={() => setConfirmRotate(true)}
disabled={busy !== null}
className="px-3 py-1.5 bg-red-900/30 hover:bg-red-900/50 border border-red-800/60 text-xs rounded text-bad disabled:opacity-30 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-red-600/60"
className="px-3 py-1.5 bg-red-900/30 hover:bg-red-900/50 border border-red-800/60 text-xs rounded text-bad disabled:opacity-30 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500 focus-visible:ring-offset-1"
>
{busy === "rotate" ? "Rotating…" : "Rotate credentials"}
</button>
@ -124,14 +124,14 @@ export function ExternalConnectionSection({ workspaceId }: Props) {
<button
type="button"
onClick={() => setConfirmRotate(false)}
className="px-3 py-1.5 bg-surface-card text-xs rounded text-ink-mid"
className="px-3 py-1.5 bg-surface-card text-xs rounded text-ink-mid focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1"
>
Cancel
</button>
<button
type="button"
onClick={doRotate}
className="px-3 py-1.5 bg-red-700 hover:bg-red-600 text-xs rounded text-white"
className="px-3 py-1.5 bg-red-700 hover:bg-red-600 text-xs rounded text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500 focus-visible:ring-offset-1"
>
Rotate
</button>

View File

@ -205,14 +205,14 @@ export function MemoryTab({ workspaceId }: Props) {
<button
type="button"
onClick={() => setShowAwareness((prev) => !prev)}
className="shrink-0 px-2 py-1 bg-surface-card hover:bg-surface-elevated text-[10px] rounded text-ink"
className="shrink-0 px-2 py-1 bg-surface-card hover:bg-surface-elevated text-[10px] rounded text-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1"
>
{showAwareness ? "Collapse" : "Expand"}
</button>
<button
type="button"
onClick={openAwareness}
className="shrink-0 px-2 py-1 bg-surface-card hover:bg-surface-elevated text-[10px] rounded text-ink"
className="shrink-0 px-2 py-1 bg-surface-card hover:bg-surface-elevated text-[10px] rounded text-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1"
>
Open
</button>
@ -245,7 +245,7 @@ export function MemoryTab({ workspaceId }: Props) {
<button
type="button"
onClick={() => setShowAwareness(true)}
className="shrink-0 px-2 py-1 bg-accent hover:bg-accent-strong text-[10px] rounded text-white"
className="shrink-0 px-2 py-1 bg-accent hover:bg-accent-strong text-[10px] rounded text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1"
>
Expand
</button>
@ -280,21 +280,21 @@ export function MemoryTab({ workspaceId }: Props) {
<button
type="button"
onClick={() => setShowAdvanced((prev) => !prev)}
className="px-2 py-1 bg-surface-card hover:bg-surface-elevated text-[10px] rounded text-ink-mid"
className="px-2 py-1 bg-surface-card hover:bg-surface-elevated text-[10px] rounded text-ink-mid focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1"
>
{showAdvanced ? "Hide Advanced" : "Advanced"}
</button>
<button
type="button"
onClick={loadMemory}
className="px-2 py-1 bg-surface-card hover:bg-surface-elevated text-[10px] rounded text-ink-mid"
className="px-2 py-1 bg-surface-card hover:bg-surface-elevated text-[10px] rounded text-ink-mid focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1"
>
Refresh
</button>
<button
type="button"
onClick={() => { setShowAdd(!showAdd); if (!showAdd) setShowAdvanced(true); }}
className="px-2 py-1 bg-accent hover:bg-accent-strong text-[10px] rounded text-white"
className="px-2 py-1 bg-accent hover:bg-accent-strong text-[10px] rounded text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1"
>
+ Add
</button>
@ -330,7 +330,7 @@ export function MemoryTab({ workspaceId }: Props) {
<button
type="button"
onClick={handleAdd}
className="px-3 py-1 bg-accent hover:bg-accent-strong text-xs rounded text-white"
className="px-3 py-1 bg-accent hover:bg-accent-strong text-xs rounded text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1"
>
Save
</button>
@ -340,7 +340,7 @@ export function MemoryTab({ workspaceId }: Props) {
setShowAdd(false);
setError(null);
}}
className="px-3 py-1 bg-surface-card hover:bg-surface-elevated text-xs rounded text-ink-mid"
className="px-3 py-1 bg-surface-card hover:bg-surface-elevated text-xs rounded text-ink-mid focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1"
>
Cancel
</button>
@ -358,7 +358,7 @@ export function MemoryTab({ workspaceId }: Props) {
<button
type="button"
onClick={() => setExpanded(expanded === entry.key ? null : entry.key)}
className="w-full flex items-center justify-between px-3 py-2 text-left"
className="w-full flex items-center justify-between px-3 py-2 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1"
aria-expanded={expanded === entry.key}
>
<span className="text-xs font-mono text-accent">{entry.key}</span>
@ -401,14 +401,14 @@ export function MemoryTab({ workspaceId }: Props) {
<button
type="button"
onClick={() => handleEditSave(entry)}
className="px-3 py-1 bg-accent hover:bg-accent-strong text-xs rounded text-white"
className="px-3 py-1 bg-accent hover:bg-accent-strong text-xs rounded text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1"
>
Save
</button>
<button
type="button"
onClick={cancelEdit}
className="px-3 py-1 bg-surface-card hover:bg-surface-elevated text-xs rounded text-ink-mid"
className="px-3 py-1 bg-surface-card hover:bg-surface-elevated text-xs rounded text-ink-mid focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1"
>
Cancel
</button>
@ -428,7 +428,7 @@ export function MemoryTab({ workspaceId }: Props) {
<button
type="button"
onClick={() => beginEdit(entry)}
className="text-[10px] text-ink-mid hover:bg-surface-elevated rounded px-1 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/60"
className="text-[10px] text-ink-mid hover:bg-surface-elevated rounded px-1 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1"
>
Edit
</button>
@ -436,7 +436,7 @@ export function MemoryTab({ workspaceId }: Props) {
<button
type="button"
onClick={() => handleDelete(entry.key)}
className="text-[10px] text-bad hover:bg-red-950/40 rounded px-1 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-red-500/60"
className="text-[10px] text-bad hover:bg-red-950/40 rounded px-1 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500 focus-visible:ring-offset-1"
>
Delete
</button>
@ -459,7 +459,7 @@ export function MemoryTab({ workspaceId }: Props) {
<button
type="button"
onClick={() => setShowAdvanced(true)}
className="shrink-0 px-2 py-1 bg-accent hover:bg-accent-strong text-[10px] rounded text-white"
className="shrink-0 px-2 py-1 bg-accent hover:bg-accent-strong text-[10px] rounded text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1"
>
Show
</button>

View File

@ -276,7 +276,7 @@ export function ScheduleTab({ workspaceId }: Props) {
// LIGHTER variant, so this hovered lighter on white text
// and dropped contrast below AA. Same trap fixed in
// OnboardingWizard, ConfirmDialog, ApprovalBanner.
className="text-[11px] px-3 py-1 bg-accent text-white rounded hover:bg-accent-strong disabled:opacity-40 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/60 focus-visible:ring-offset-1 focus-visible:ring-offset-surface"
className="text-[11px] px-3 py-1 bg-accent text-white rounded hover:bg-accent-strong disabled:opacity-40 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1 focus-visible:ring-offset-surface"
>
{editId ? "Update" : "Create"}
</button>
@ -285,7 +285,7 @@ export function ScheduleTab({ workspaceId }: Props) {
onClick={resetForm}
// Was hover:bg-surface-card on top of bg-surface-card —
// silent no-op hover. Lift to surface-elevated.
className="text-[11px] px-3 py-1 bg-surface-card text-ink-mid rounded hover:bg-surface-elevated hover:text-ink transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 focus-visible:ring-offset-1 focus-visible:ring-offset-surface"
className="text-[11px] px-3 py-1 bg-surface-card text-ink-mid rounded hover:bg-surface-elevated hover:text-ink transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1 focus-visible:ring-offset-surface"
>
Cancel
</button>

View File

@ -479,7 +479,7 @@ export function SkillsTab({ workspaceId, data }: Props) {
<button
type="button"
onClick={() => loadRegistry(true)}
className="text-[10px] text-violet-300 hover:text-violet-200 underline-offset-2 hover:underline"
className="text-[10px] text-violet-300 hover:text-violet-200 underline-offset-2 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1"
>
{registryLoading ? "Loading… click to retry" : "Retry"}
</button>

View File

@ -60,7 +60,7 @@ export function TracesTab({ workspaceId }: Props) {
onClick={loadTraces}
// Added focus-visible ring; previous version was hover-only,
// invisible to keyboard users.
className="text-[10px] text-ink-mid hover:text-ink-mid rounded-sm px-1 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/50"
className="text-[10px] text-ink-mid hover:text-ink-mid rounded-sm px-1 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1"
>
Refresh
</button>
@ -98,7 +98,7 @@ export function TracesTab({ workspaceId }: Props) {
// panel. Same pattern shipped on EventsTab.
aria-expanded={isOpen}
aria-controls={panelId}
className="w-full px-3 py-2 flex items-center gap-2 text-left hover:bg-surface-card/60 focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-accent/50 transition-colors"
className="w-full px-3 py-2 flex items-center gap-2 text-left hover:bg-surface-card/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1 transition-colors"
>
{/* Status dot uses semantic bad/good tokens was hardcoded
bg-red-400 / bg-emerald-400 which doesn't pin to the

View File

@ -143,7 +143,7 @@ export function AttachmentImage({ workspaceId, attachment, onDownload, tone }: P
type="button"
onClick={() => setOpen(true)}
title={`Preview ${attachment.name}`}
className={`group relative inline-block max-w-full rounded-lg overflow-hidden border focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/60 ${
className={`group relative inline-block max-w-full rounded-lg overflow-hidden border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1 ${
tone === "user" ? "border-blue-400/30" : "border-line/50"
}`}
aria-label={`Open ${attachment.name} preview`}