forked from molecule-ai/molecule-core
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:
parent
7fc2da2146
commit
3daa9083b8
@ -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
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -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"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user