fix(canvas): UIUX Cycle 15 dark-theme & a11y sweep (C1-C5, A1-A4, F1, M1)

- C4: OnboardingWizard skip button — aria-label + text-zinc-400 (was zinc-600)
- A1+M1: CommunicationOverlay — aria-label on both icon buttons, aria-hidden
  on decorative arrow glyphs (↗↙ toggle, ✕ close, → comms rows)
- A2: ChatTab sub-tab bar — ARIA roving tabIndex + ArrowLeft/ArrowRight
  keyboard navigation (role=tablist/tab already present)
- A4: SearchDialog search input — focus-visible:ring-2 ring-blue-500 replaces
  bare focus:outline-none so keyboard focus is visible
- F1: AuthGate loading state — zinc-950 full-screen backdrop instead of null
  (prevents white flash on SaaS tenant load)
- A3: SidePanel tab bar — wrap in relative container + right-edge fade
  gradient so truncated tabs are visually signalled

C2 (settings-panel.css input backgrounds) and C3 (Canvas.tsx colorMode="dark")
were already in place; verified by code audit before this commit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Canvas Agent 2026-04-16 10:35:32 +00:00
parent 7fc2da2146
commit 3daa9083b8
6 changed files with 31 additions and 13 deletions

View File

@ -56,8 +56,9 @@ export function AuthGate({ children }: { children: ReactNode }) {
}, [state]);
if (state.kind === "loading") {
// Minimal placeholder; canvas has its own loading UI downstream.
return null;
// Zinc-950 backdrop matches the canvas background so the browser
// never paints a white flash while the session round-trip resolves.
return <div className="fixed inset-0 bg-zinc-950" aria-hidden="true" />;
}
if (state.kind === "anonymous" && !state.skipRedirect) {
// Redirect already firing from the effect above; render nothing in

View File

@ -99,10 +99,10 @@ export function CommunicationOverlay() {
return (
<button
onClick={() => setVisible(true)}
aria-label="Show communications panel"
className="fixed top-16 right-4 z-30 px-3 py-1.5 bg-zinc-900/90 border border-zinc-700/50 rounded-lg text-[10px] text-zinc-400 hover:text-zinc-200 transition-colors"
title="Show communications"
>
{comms.length > 0 ? `${comms.length} comms` : "Communications"}
<span aria-hidden="true"> </span>{comms.length > 0 ? `${comms.length} comms` : "Communications"}
</button>
);
}
@ -111,13 +111,14 @@ export function CommunicationOverlay() {
<div className="fixed top-16 right-4 z-30 w-[320px] max-h-[400px] bg-zinc-900/95 border border-zinc-700/50 rounded-xl shadow-xl shadow-black/30 backdrop-blur-sm overflow-hidden">
<div className="flex items-center justify-between px-3 py-2 border-b border-zinc-800/60">
<div className="text-[10px] font-semibold text-zinc-400 uppercase tracking-wider">
Communications ({comms.length})
<span aria-hidden="true"> </span>Communications ({comms.length})
</div>
<button
onClick={() => setVisible(false)}
aria-label="Close communications panel"
className="text-zinc-500 hover:text-zinc-300 text-xs"
>
<span aria-hidden="true"></span>
</button>
</div>
@ -141,11 +142,11 @@ export function CommunicationOverlay() {
>
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-1.5 min-w-0">
<span className={typeColor}>{typeIcon}</span>
<span className={typeColor} aria-hidden="true">{typeIcon}</span>
<span className="text-zinc-300 font-medium truncate">
{c.sourceName}
</span>
<span className="text-zinc-400"></span>
<span className="text-zinc-400" aria-hidden="true"></span>
<span className="text-zinc-300 truncate">{c.targetName}</span>
</div>
<div className="flex items-center gap-1 shrink-0">

View File

@ -138,7 +138,8 @@ export function OnboardingWizard() {
</span>
<button
onClick={dismiss}
className="text-[10px] text-zinc-600 hover:text-zinc-400 transition-colors"
aria-label="Skip onboarding guide"
className="text-[10px] text-zinc-400 hover:text-zinc-200 transition-colors"
>
Skip guide
</button>

View File

@ -112,7 +112,7 @@ export function SearchDialog() {
onChange={(e) => setQuery(e.target.value)}
onKeyDown={handleInputKeyDown}
placeholder="Search workspaces..."
className="flex-1 bg-transparent text-sm text-zinc-100 placeholder-zinc-400 focus:outline-none"
className="flex-1 bg-transparent text-sm text-zinc-100 placeholder-zinc-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus:outline-none rounded"
/>
<kbd className="text-[9px] text-zinc-400 bg-zinc-800/60 px-1.5 py-0.5 rounded border border-zinc-700/40">ESC</kbd>
</div>

View File

@ -137,11 +137,14 @@ export function SidePanel() {
</div>
</div>
{/* Tabs */}
{/* Tabs — relative wrapper lets the fade gradient position against the scroll container */}
<div className="relative border-b border-zinc-800/40">
{/* Right-edge fade: signals more tabs are hidden off-screen when the bar overflows */}
<div className="pointer-events-none absolute inset-y-0 right-0 w-8 bg-gradient-to-l from-zinc-950 to-transparent z-10" aria-hidden="true" />
<div
role="tablist"
aria-label="Workspace panel tabs"
className="flex border-b border-zinc-800/40 overflow-x-auto bg-zinc-900/20 px-1"
className="flex overflow-x-auto bg-zinc-900/20 px-1"
onKeyDown={(e) => {
const idx = TABS.findIndex((t) => t.id === panelTab);
let next: number | null = null;
@ -175,6 +178,7 @@ export function SidePanel() {
</button>
))}
</div>
</div>
{/* Needs Restart Banner */}
{node.data.needsRestart && !node.data.currentTask && selectedNodeId && (

View File

@ -98,11 +98,21 @@ export function ChatTab({ workspaceId, data }: Props) {
return (
<div className="flex flex-col h-full">
{/* Sub-tab bar — role="tablist" so screen readers expose tab context */}
<div role="tablist" className="flex border-b border-zinc-800/40 bg-zinc-900/30 px-2 shrink-0">
<div
role="tablist"
className="flex border-b border-zinc-800/40 bg-zinc-900/30 px-2 shrink-0"
onKeyDown={(e) => {
const tabs: ChatSubTab[] = ["my-chat", "agent-comms"];
const idx = tabs.indexOf(subTab);
if (e.key === "ArrowRight") { e.preventDefault(); setSubTab(tabs[(idx + 1) % tabs.length]); }
else if (e.key === "ArrowLeft") { e.preventDefault(); setSubTab(tabs[(idx - 1 + tabs.length) % tabs.length]); }
}}
>
<button
role="tab"
aria-selected={subTab === "my-chat"}
aria-controls="chat-panel-my-chat"
tabIndex={subTab === "my-chat" ? 0 : -1}
onClick={() => setSubTab("my-chat")}
className={`px-3 py-1.5 text-[10px] font-medium transition-colors ${
subTab === "my-chat"
@ -116,6 +126,7 @@ export function ChatTab({ workspaceId, data }: Props) {
role="tab"
aria-selected={subTab === "agent-comms"}
aria-controls="chat-panel-agent-comms"
tabIndex={subTab === "agent-comms" ? 0 : -1}
onClick={() => setSubTab("agent-comms")}
className={`px-3 py-1.5 text-[10px] font-medium transition-colors ${
subTab === "agent-comms"