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:
@@ -50,6 +50,7 @@ export function AppShell() {
|
||||
[sessions],
|
||||
);
|
||||
const activeList = sessions.filter((s) => !s.archived);
|
||||
const ungroupedActive = activeList.filter((s) => !s.group);
|
||||
const archivedList = sessions.filter((s) => s.archived);
|
||||
const refresh = () => queryClient.invalidateQueries({ queryKey: ["sessions"] });
|
||||
|
||||
@@ -227,17 +228,19 @@ export function AppShell() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* L1 — rail title */}
|
||||
<div className="px-4 pb-1.5 pt-1">
|
||||
<span className="text-[0.7rem] font-bold uppercase tracking-[0.12em] text-primary">
|
||||
<span className="text-[0.82rem] font-bold uppercase tracking-[0.18em] text-primary">
|
||||
Sessions
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto px-2 pb-4">
|
||||
{/* L2 — section toggle */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveOpen((v) => !v)}
|
||||
aria-expanded={activeOpen}
|
||||
className="flex w-full items-center gap-1 px-1 pb-1 pt-1 text-left text-[0.7rem] font-bold uppercase tracking-wide text-primary hover:text-primary/80"
|
||||
className="flex w-full items-center gap-1 px-1 pb-1 pt-1 text-left text-[0.7rem] font-semibold uppercase tracking-[0.1em] text-foreground/65 hover:text-foreground"
|
||||
>
|
||||
<span className="select-none">{activeOpen ? "▾" : "▸"}</span>
|
||||
<span>Active sessions</span>
|
||||
@@ -247,11 +250,12 @@ export function AppShell() {
|
||||
{groups.map((g) => (
|
||||
<div key={g}>
|
||||
<div className="group/gh flex items-center gap-1">
|
||||
{/* L2 — group header (sub-level of the section) */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCollapsedGroups((c) => ({ ...c, [g]: !c[g] }))}
|
||||
aria-expanded={!collapsedGroups[g]}
|
||||
className="flex min-w-0 flex-1 items-center gap-1 px-1 pb-1 text-left text-[0.7rem] font-semibold uppercase tracking-wide text-muted-foreground hover:text-foreground"
|
||||
className="flex min-w-0 flex-1 items-center gap-1 px-1 pb-1 text-left text-[0.66rem] font-semibold uppercase tracking-[0.06em] text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<span className="select-none">{collapsedGroups[g] ? "▸" : "▾"}</span>
|
||||
<span className="truncate">{g}</span>
|
||||
@@ -275,25 +279,26 @@ export function AppShell() {
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div>
|
||||
{groups.length > 0 && (
|
||||
<p className="px-1 pb-1 text-[0.7rem] font-semibold uppercase tracking-wide text-muted-foreground">No group</p>
|
||||
)}
|
||||
{/* Ungrouped sessions list directly after the groups — no "No group" label.
|
||||
When there are no groups at all, still render it so its empty state can
|
||||
teach first-time users. */}
|
||||
{(ungroupedActive.length > 0 || groups.length === 0) && (
|
||||
<NavSessions
|
||||
sessions={activeList.filter((s) => !s.group)}
|
||||
sessions={ungroupedActive}
|
||||
activeSessionId={activeSessionId}
|
||||
onOpenPanel={openPanel}
|
||||
menuFor={menuFor}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* L2 — section toggle */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setArchiveOpen((v) => !v)}
|
||||
aria-expanded={archiveOpen}
|
||||
className="mt-2 flex w-full items-center gap-1 px-1 pb-1 pt-1 text-left text-[0.7rem] font-bold uppercase tracking-wide text-muted-foreground hover:text-foreground"
|
||||
className="mt-2 flex w-full items-center gap-1 px-1 pb-1 pt-1 text-left text-[0.7rem] font-semibold uppercase tracking-[0.1em] text-foreground/65 hover:text-foreground"
|
||||
>
|
||||
<span className="select-none">{archiveOpen ? "▾" : "▸"}</span>
|
||||
<span>Archive ({archivedList.length})</span>
|
||||
|
||||
Reference in New Issue
Block a user