feat(ui): improve visual polish, responsiveness, and accessibility (#58)

* feat(ui): improve visual polish, responsiveness, and UX consistency

- GatewayConnectScreen: replace hardcoded text-white* with semantic
  foreground/muted-foreground tokens so the connect form is readable
  in both light and dark modes
- HeaderBar: show gateway status chip for "connected" state in addition
  to "connecting", giving users clear visual feedback once connected
- FleetSidebar: add aria-label and aria-pressed to agent row buttons
  for screen-reader accessibility
- HQSidebar: add role=tablist/tab/tabpanel, aria-selected, aria-controls,
  and aria-labelledby to the headquarters panel tabs
- OfficePage: replace Suspense fallback={null} with a themed spinner
  so users see feedback instead of a blank screen during initial load

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(office): widen agent nameplate to prevent name truncation

- Expand background plane from 0.68 to 1.1 width
- Increase maxWidth from 0.56 to 1.0
- Slightly reduce fontSize 0.1 to 0.09 for better fit
- Add whiteSpace nowrap to prevent wrapping
- Truncate names >14 chars with ellipsis for very long agent names

---------

Co-authored-by: Claw3D UI Bot <ui-improvements@claw3d.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Yusup Supriyadi
2026-03-24 22:55:19 +07:00
committed by GitHub
parent c9789c2148
commit 994c8b06b5
6 changed files with 71 additions and 34 deletions
+18 -1
View File
@@ -10,12 +10,29 @@ const readDebugFlag = (value: string | undefined): boolean => {
return ENABLED_RE.test(normalized);
};
function OfficeLoadingFallback() {
return (
<div
className="flex h-full w-full items-center justify-center bg-background"
aria-label="Loading office"
role="status"
>
<div className="flex flex-col items-center gap-3">
<div className="h-8 w-8 animate-spin rounded-full border-2 border-border border-t-primary" />
<p className="font-mono text-[11px] tracking-[0.08em] text-muted-foreground">
Loading
</p>
</div>
</div>
);
}
export default function OfficePage() {
const showOpenClawConsole = readDebugFlag(process.env.DEBUG);
return (
<AgentStoreProvider>
<Suspense fallback={null}>
<Suspense fallback={<OfficeLoadingFallback />}>
<OfficeScreen showOpenClawConsole={showOpenClawConsole} />
</Suspense>
</AgentStoreProvider>
@@ -126,6 +126,8 @@ export const FleetSidebar = ({
}}
type="button"
data-testid={`fleet-agent-row-${agent.agentId}`}
aria-label={`Select agent: ${agent.name}`}
aria-pressed={selected}
className={`group relative ui-card flex w-full items-center gap-3 overflow-hidden border px-3 py-3 text-left transition-colors ${
selected
? "ui-card-selected"
@@ -85,7 +85,7 @@ export const GatewayConnectScreen = ({
const commandField = (
<div className="space-y-1.5">
<div className="ui-command-surface flex items-center gap-2 rounded-md px-3 py-2">
<code className="min-w-0 flex-1 overflow-x-auto whitespace-nowrap font-mono text-[12px] text-white">
<code className="min-w-0 flex-1 overflow-x-auto whitespace-nowrap font-mono text-[12px] text-[var(--command-fg)]">
{localGatewayCommand}
</code>
<button
@@ -99,12 +99,12 @@ export const GatewayConnectScreen = ({
</button>
</div>
{copyStatus === "copied" ? (
<p className="text-xs text-white/80">Copied</p>
<p className="text-xs text-muted-foreground">Copied</p>
) : copyStatus === "failed" ? (
<p className="ui-text-danger text-xs">Could not copy command.</p>
) : (
<p className="text-xs leading-snug text-white/80">
In a source checkout, use <span className="font-mono text-white">{localGatewayCommandPnpm}</span>.
<p className="text-xs leading-snug text-muted-foreground">
In a source checkout, use <span className="font-mono text-foreground">{localGatewayCommandPnpm}</span>.
</p>
)}
</div>
@@ -112,7 +112,7 @@ export const GatewayConnectScreen = ({
const remoteForm = (
<div className="mt-2.5 flex flex-col gap-3">
<label className="flex flex-col gap-1 text-[11px] font-medium text-white/90">
<label className="flex flex-col gap-1 text-[11px] font-medium text-foreground/90">
Upstream URL
<input
className="ui-input h-10 rounded-md px-4 font-sans text-sm text-foreground outline-none"
@@ -124,14 +124,14 @@ export const GatewayConnectScreen = ({
/>
</label>
<div className="space-y-0.5 text-xs text-white/80">
<p className="font-medium text-white">Using Tailscale?</p>
<div className="space-y-0.5 text-xs text-muted-foreground">
<p className="font-medium text-foreground">Using Tailscale?</p>
<p>
URL: <span className="font-mono">wss://&lt;your-tailnet-host&gt;</span>
</p>
</div>
<label className="flex flex-col gap-1 text-[11px] font-medium text-white/90">
<label className="flex flex-col gap-1 text-[11px] font-medium text-foreground/90">
Upstream token
<div className="relative">
<input
@@ -144,7 +144,7 @@ export const GatewayConnectScreen = ({
/>
<button
type="button"
className="ui-btn-icon absolute inset-y-0 right-1 my-auto h-8 w-8 border-transparent bg-transparent text-white/70 hover:bg-transparent hover:text-white"
className="ui-btn-icon absolute inset-y-0 right-1 my-auto h-8 w-8 border-transparent bg-transparent text-muted-foreground hover:bg-transparent hover:text-foreground"
aria-label={showToken ? "Hide token" : "Show token"}
onClick={() => setShowToken((prev) => !prev)}
>
@@ -167,19 +167,19 @@ export const GatewayConnectScreen = ({
</button>
{status === "connecting" ? (
<p className="inline-flex items-center gap-1.5 text-xs text-white/80">
<p className="inline-flex items-center gap-1.5 text-xs text-muted-foreground">
<Loader2 className="h-3.5 w-3.5 animate-spin" />
Connecting
</p>
) : null}
{error ? <p className="ui-text-danger text-xs leading-snug">{error}</p> : null}
{showApprovalHint ? (
<div className="rounded-md border border-white/10 bg-white/5 px-3 py-3 text-xs text-white/85">
<div className="rounded-md border border-border bg-muted/40 px-3 py-3 text-xs text-muted-foreground">
<p className="leading-snug">
If the first connection attempt did not work, go to your OpenClaw computer and approve this
device:
</p>
<code className="mt-2 block overflow-x-auto whitespace-nowrap rounded-md bg-black/30 px-2.5 py-2 font-mono text-[11px] text-white">
<code className="mt-2 block overflow-x-auto whitespace-nowrap rounded-md bg-[var(--command-bg)] px-2.5 py-2 font-mono text-[11px] text-[var(--command-fg)]">
openclaw devices approve --latest
</code>
</div>
@@ -198,26 +198,26 @@ export const GatewayConnectScreen = ({
className={`h-2.5 w-2.5 ${statusDotClass}`}
/>
)}
<p className="text-sm font-semibold text-white">{statusCopy}</p>
<p className="text-sm font-semibold text-foreground">{statusCopy}</p>
</div>
</div>
<div className="ui-card px-4 py-5 sm:px-6">
<div>
<p className="font-mono text-[10px] font-medium tracking-[0.06em] text-white/80">
<p className="font-mono text-[10px] font-medium tracking-[0.06em] text-muted-foreground">
Remote gateway (recommended)
</p>
<p className="mt-2 text-sm text-white/90">Default: enter your URL and token to connect.</p>
<p className="mt-2 text-sm text-foreground/90">Default: enter your URL and token to connect.</p>
</div>
{remoteForm}
</div>
<div className="ui-card px-4 py-4 sm:px-6 sm:py-5">
<div className="space-y-1.5">
<p className="font-mono text-[10px] font-semibold tracking-[0.06em] text-white/80">
<p className="font-mono text-[10px] font-semibold tracking-[0.06em] text-muted-foreground">
Run locally (optional)
</p>
<p className="text-sm text-white/90">
<p className="text-sm text-foreground/90">
Start a local gateway process on this machine, then connect.
</p>
</div>
@@ -226,15 +226,15 @@ export const GatewayConnectScreen = ({
{localGatewayDefaults ? (
<div className="ui-input rounded-md px-3 py-3">
<div className="space-y-2">
<p className="text-xs text-white/80">
<p className="text-xs text-muted-foreground">
Use token from <span className="font-mono">~/.openclaw/openclaw.json</span>.
</p>
<p className="font-mono text-[11px] text-white">
<p className="font-mono text-[11px] text-foreground">
{localGatewayDefaults.url}
</p>
<button
type="button"
className="ui-btn-secondary h-9 w-full px-3 text-xs font-semibold tracking-[0.05em] text-white"
className="ui-btn-secondary h-9 w-full px-3 text-xs font-semibold tracking-[0.05em]"
onClick={onUseLocalDefaults}
>
Use local defaults
+6 -5
View File
@@ -42,13 +42,14 @@ export const HeaderBar = ({
<div aria-hidden="true" />
<p className="truncate text-sm font-semibold tracking-[0.01em] text-foreground">Claw3D</p>
<div className="flex items-center justify-end gap-1">
{status === "connecting" ? (
{status !== "disconnected" ? (
<span
className={`ui-chip px-2 py-0.5 font-mono text-[9px] font-semibold tracking-[0.08em] ${resolveGatewayStatusBadgeClass("connecting")}`}
data-testid="gateway-connecting-indicator"
data-status="connecting"
className={`ui-chip px-2 py-0.5 font-mono text-[9px] font-semibold tracking-[0.08em] ${resolveGatewayStatusBadgeClass(status)}`}
data-testid="gateway-status-indicator"
data-status={status}
aria-label={`Gateway ${status}`}
>
Connecting
{status === "connecting" ? "Connecting" : "Connected"}
</span>
) : null}
<ThemeToggle />
+18 -3
View File
@@ -137,7 +137,11 @@ export function HQSidebar({
</div>
{!railOnly ? (
<div className="grid grid-cols-3 border-b border-cyan-500/15">
<div
role="tablist"
aria-label="Headquarters panels"
className="grid grid-cols-3 border-b border-cyan-500/15"
>
{PRIMARY_TABS.map((tab) => {
const isActive = tab === activeTab;
const showBadge = tab === "inbox" && inboxCount > 0;
@@ -145,6 +149,10 @@ export function HQSidebar({
<button
key={tab}
type="button"
role="tab"
aria-selected={isActive}
aria-controls={`hq-panel-${tab}`}
id={`hq-tab-${tab}`}
onClick={() => onTabChange(tab)}
className={`flex items-center justify-center gap-1 border-r border-cyan-500/10 px-2 py-2.5 font-mono text-[11px] uppercase tracking-[0.18em] transition-colors last:border-r-0 ${
isActive
@@ -154,7 +162,7 @@ export function HQSidebar({
>
<span>{TAB_LABELS[tab]}</span>
{showBadge ? (
<span className="rounded bg-cyan-500/15 px-1.5 py-0.5 text-[10px] text-cyan-300">
<span className="rounded bg-cyan-500/15 px-1.5 py-0.5 text-[10px] text-cyan-300" aria-label={`${inboxCount} unread`}>
{inboxCount}
</span>
) : null}
@@ -164,7 +172,14 @@ export function HQSidebar({
</div>
) : null}
<div className="min-h-0 flex-1 overflow-hidden">{activePanel}</div>
<div
role="tabpanel"
id={`hq-panel-${activeTab}`}
aria-labelledby={`hq-tab-${activeTab}`}
className="min-h-0 flex-1 overflow-hidden"
>
{activePanel}
</div>
</div>
) : null}
</aside>
@@ -349,23 +349,25 @@ export function DeskNameplates({
return (
<Billboard key={`nameplate-${index}`} position={[wx, 0.55, wz]}>
<mesh position={[0, 0, -0.001]}>
<planeGeometry args={[0.68, 0.18]} />
<planeGeometry args={[1.1, 0.18]} />
<meshBasicMaterial color="#0a0804" transparent opacity={0.75} />
</mesh>
<mesh position={[-0.32, 0, 0]}>
<mesh position={[-0.52, 0, 0]}>
<planeGeometry args={[0.04, 0.18]} />
<meshBasicMaterial color={agent.color} />
</mesh>
<Text
position={[0.02, 0, 0.001]}
fontSize={0.1}
fontSize={0.09}
color="#c8a860"
anchorX="center"
anchorY="middle"
maxWidth={0.56}
maxWidth={1.0}
font={undefined}
overflowWrap="break-word"
whiteSpace="nowrap"
>
{agent.name}
{agent.name.length > 14 ? agent.name.slice(0, 13) + "…" : agent.name}
</Text>
</Billboard>
);