feat(frontend): phase progress dots (B) + compact sidebar redesign (C)
B — WorkflowBar renders F1..F8 as colored ring-dots (no phase-name text): green=done, amber=running (subtle pulse), red=error, gray=pending; green connectors lead the active dot, each dot carries data-state. Lightweight error signal: sessionStore gains `phaseError`, set when an info event has level=error during the active phase, cleared on the next ui_request. C — denser single-line session rows (inline status dot + name, py-1), a 3-level type hierarchy (L1 SESSIONS / L2 section+group headers / L3 names), and the "No group" label removed (ungrouped render after the last group, guarded so the empty-state still teaches when there are zero groups). Live-verified with Playwright (all four dot states, sidebar hierarchy, and E's deferred activity-panel check). Frontend 93/93, tsc -b clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -12,8 +12,21 @@ const PHASES = [
|
||||
{ id: "F8", name: "Results" },
|
||||
];
|
||||
|
||||
type DotState = "done" | "running" | "error" | "pending";
|
||||
|
||||
// Each state is a tinted chip; red is reserved strictly for error (the GSD theme's
|
||||
// primary is already red), green = done, amber = the live phase, neutral = pending.
|
||||
const DOT: Record<DotState, string> = {
|
||||
done: "bg-[oklch(var(--success)/0.16)] text-[oklch(0.5_0.13_165)] ring-[oklch(var(--success)/0.32)]",
|
||||
running:
|
||||
"bg-[oklch(var(--warning)/0.24)] text-[oklch(0.46_0.11_79)] ring-[oklch(var(--warning)/0.6)]",
|
||||
error: "bg-destructive/15 text-destructive ring-destructive/45",
|
||||
pending: "bg-transparent text-muted-foreground/70 ring-border",
|
||||
};
|
||||
|
||||
export function WorkflowBar() {
|
||||
const currentPhase = useSessionStore((s) => s.currentPhase);
|
||||
const phaseError = useSessionStore((s) => s.phaseError);
|
||||
const activeIdx = PHASES.findIndex((p) => p.id === currentPhase);
|
||||
|
||||
return (
|
||||
@@ -24,43 +37,48 @@ export function WorkflowBar() {
|
||||
{PHASES.map((p, i) => {
|
||||
const isActive = currentPhase === p.id;
|
||||
const isDone = activeIdx > -1 && i < activeIdx;
|
||||
const state: DotState = isActive
|
||||
? phaseError === p.id
|
||||
? "error"
|
||||
: "running"
|
||||
: isDone
|
||||
? "done"
|
||||
: "pending";
|
||||
const connectorDone = activeIdx > -1 && i <= activeIdx;
|
||||
return (
|
||||
<div key={p.id} className="flex items-center">
|
||||
{i > 0 && (
|
||||
<span
|
||||
aria-hidden
|
||||
className={[
|
||||
"mx-1.5 h-px w-4 shrink-0",
|
||||
isDone || isActive ? "bg-primary/40" : "bg-border",
|
||||
"mx-1 h-px w-3 shrink-0 sm:w-4",
|
||||
connectorDone ? "bg-[oklch(var(--success)/0.4)]" : "bg-border",
|
||||
].join(" ")}
|
||||
/>
|
||||
)}
|
||||
<span
|
||||
data-testid={`phase-${p.id}`}
|
||||
data-active={isActive ? "true" : "false"}
|
||||
title={`${p.id}: ${p.name}`}
|
||||
className={[
|
||||
"inline-flex items-center gap-1.5 whitespace-nowrap rounded-full px-2.5 py-1 text-xs transition-colors",
|
||||
isActive
|
||||
? "bg-primary text-primary-foreground font-semibold shadow-sm shadow-primary/20"
|
||||
: isDone
|
||||
? "text-foreground/70 font-medium"
|
||||
: "text-muted-foreground",
|
||||
].join(" ")}
|
||||
>
|
||||
<span className="relative inline-flex shrink-0">
|
||||
{state === "running" && (
|
||||
<span
|
||||
aria-hidden
|
||||
className="absolute -inset-[3px] animate-pulse rounded-full bg-[oklch(var(--warning)/0.22)]"
|
||||
/>
|
||||
)}
|
||||
<span
|
||||
data-testid={`phase-${p.id}`}
|
||||
data-active={isActive ? "true" : "false"}
|
||||
data-state={state}
|
||||
title={`${p.id}: ${p.name}`}
|
||||
aria-label={`${p.id} ${p.name} — ${state}`}
|
||||
aria-current={isActive ? "step" : undefined}
|
||||
className={[
|
||||
"font-heading text-[0.7rem] font-semibold",
|
||||
isActive
|
||||
? "text-primary-foreground"
|
||||
: isDone
|
||||
? "text-primary/80"
|
||||
: "text-muted-foreground/70",
|
||||
"relative inline-flex size-6 items-center justify-center rounded-full",
|
||||
"text-[0.625rem] font-semibold leading-none tracking-tight tabular-nums",
|
||||
"ring-1 ring-inset transition-colors",
|
||||
DOT[state],
|
||||
].join(" ")}
|
||||
>
|
||||
{p.id}
|
||||
</span>
|
||||
<span className={isActive ? "inline" : "hidden md:inline"}>{p.name}</span>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user