From 3f62a594aea137d92db1227501c379de39aded60 Mon Sep 17 00:00:00 2001 From: dovvnloading Date: Sun, 16 Aug 2026 09:28:53 -0400 Subject: [PATCH 1/2] Redesign the app bar with a fixed three-column layout and grouped controls The top bar was a single unbroken flex row of buttons carried over from the Qt port, with no visual grouping and a connection badge that only stayed anchored to the window edge by consuming whatever space was left over from a sibling flex item. At narrow widths, or once enough buttons were present, the badge would end up pressed against the last toolbar button instead of the window edge. The header is now a three-column grid - brand, toolbar, connection status - so each region has a declared track and cannot encroach on the others. Toolbar actions are organized into labeled groups (Session, History, Viewport, Arrange, Panels, Workspace Tools, Settings) with consistent internal spacing and a shared surface per group, replacing the undifferentiated run of buttons. Frequent, app-specific actions keep text labels; universally recognizable actions (undo/redo, zoom controls, the workspace-tools cluster) become icon buttons with the same wording carried in their tooltip and accessible name. The existing overflow-menu mechanism (container queries collapsing by width tier) now collapses whole groups instead of individual buttons, so a cluster of related actions is never left as a partial fragment at in-between widths. The Global Search action, which previously had no overflow-menu counterpart, now has one - it would otherwise have become unreachable once its tier collapsed. The connection-status indicator only renders when the connection is degraded. A permanently visible "connected" badge reports the expected state on every frame and adds visual noise without carrying information; the reconnecting state does carry real information (intents are queued or refused while it shows), so that state alone is surfaced. Geometry was verified across six viewport widths (560px-1600px): zero region overlap, a constant 47px row height, uniform 26px control height, and no document horizontal overflow at any width. Two icon glyphs (Settings, Knowledge) were redrawn after an initial render was ambiguous at 15px. Co-Authored-By: Claude Sonnet 5 --- web_ui/src/app/App.tsx | 35 +- web_ui/src/app/chrome/AppBar.test.tsx | 34 +- web_ui/src/app/chrome/AppBar.tsx | 674 +++++++++++++++----------- web_ui/src/app/chrome/AppBarIcon.tsx | 170 +++++++ web_ui/src/app/styles.css | 358 +++++++++----- 5 files changed, 834 insertions(+), 437 deletions(-) create mode 100644 web_ui/src/app/chrome/AppBarIcon.tsx diff --git a/web_ui/src/app/App.tsx b/web_ui/src/app/App.tsx index 13582b3..18d4fb5 100644 --- a/web_ui/src/app/App.tsx +++ b/web_ui/src/app/App.tsx @@ -400,12 +400,39 @@ function App() {
{announcement}
+ {/* Three declared grid tracks - brand, toolbar, status - so the + regions of a permanently-visible bar have fixed homes and + cannot encroach on one another. See .app-topbar in styles.css + and AppBar.tsx's own layout contract. */}
- Graphlink +
+ + Graphlink +
- - {connectionBadgeLabel(status)} - + {/* Exception-only: a healthy connection shows nothing at all. + A permanent "connected" badge reports the expected state on + every frame and carries no information. The degraded states + do carry information - "reconnecting" is the app's own + answer to "why did my click do nothing", since intents are + queued or refused while it shows (see connectionBadge.ts) - + so those still surface, and only those. */} +
+ {status !== "open" && ( + + + )} +
diff --git a/web_ui/src/app/chrome/AppBar.test.tsx b/web_ui/src/app/chrome/AppBar.test.tsx index fb8e795..bfaa2a8 100644 --- a/web_ui/src/app/chrome/AppBar.test.tsx +++ b/web_ui/src/app/chrome/AppBar.test.tsx @@ -197,19 +197,29 @@ describe("AppBar", () => { await user.click(screen.getByRole("button", { name: "More toolbar actions" })); const menu = screen.getByRole("dialog"); + // Tiers live on the GROUP wrapper, not the individual button: the + // bar collapses whole clusters so related actions stay together at + // every width instead of leaving fragments behind (see AppBar.tsx). + // The contract this pins is unchanged though - an inline action and + // its overflow duplicate must always collapse at the same tier. const pairs: [string, string][] = [ - ["Export PNG", "1"], - ["Pins", "2"], + ["Undo", "1"], + ["Redo", "1"], + ["Zoom In", "1"], + ["Zoom Out", "1"], + ["Reset", "1"], + ["Fit All", "1"], ["Organize", "2"], - ["View", "2"], - ["Plugins", "2"], - ["Zoom In", "3"], - ["Zoom Out", "3"], - ["Reset", "3"], - ["Fit All", "3"], - ["About", "1"], - ["Help", "1"], - ["Diagnostics", "1"], + ["Pins", "2"], + ["Export PNG", "2"], + ["View", "3"], + ["Plugins", "3"], + ["Global Search", "4"], + ["Knowledge", "4"], + ["Builder", "4"], + ["Diagnostics", "4"], + ["Help", "4"], + ["About", "4"], ]; for (const [label, tier] of pairs) { // Every duplicated pair shares its exact label except Plugins (the @@ -222,7 +232,7 @@ describe("AppBar", () => { const matches = screen.getAllByRole("button", { name: new RegExp(label) }); const inline = matches.find((el) => !menu.contains(el)); const overflowItem = matches.find((el) => menu.contains(el)); - expect(inline).toHaveAttribute("data-tier", tier); + expect(inline?.closest(".appbar-group")).toHaveAttribute("data-tier", tier); expect(overflowItem).toHaveAttribute("data-tier", tier); } diff --git a/web_ui/src/app/chrome/AppBar.tsx b/web_ui/src/app/chrome/AppBar.tsx index 0eadbae..19e849f 100644 --- a/web_ui/src/app/chrome/AppBar.tsx +++ b/web_ui/src/app/chrome/AppBar.tsx @@ -4,10 +4,36 @@ import { exportCanvasAsPng } from "../canvas/exportCanvasPng"; import { motionDuration } from "../reducedMotion"; import type { SceneStore } from "../canvas/sceneStore"; import { Popover, useOverlays } from "../overlays/overlays"; +import { AppBarIcon, type AppBarIconName } from "./AppBarIcon"; /** * The app bar (Qt-removal plan R2) - the toolbar island's SPA successor. * + * LAYOUT CONTRACT. This bar is on screen at all times, so its geometry is + * fixed rather than emergent: + * + * - `.app-topbar` (styles.css) is a THREE-COLUMN GRID - brand, this + * toolbar, connection status - so those three regions occupy declared + * tracks and cannot encroach on one another. The previous layout was one + * flex row in which the status badge sat outside the toolbar and relied + * on `margin-left: auto` against a `flex: 1` sibling that had already + * eaten the free space, which is why it ended up jammed against the last + * button instead of anchored to the window edge. + * - The row has a FIXED height and every control a fixed height, so the bar + * never changes size with its contents. + * - Related actions live in `.appbar-group` containers with uniform inner + * spacing and a shared surface. Grouping is what carries the visual + * organisation; the old bar was one undifferentiated run of twenty text + * buttons separated by ad-hoc 1px rules. + * + * ICONS vs LABELS. Frequent, app-specific verbs (Library, Save, Organize, + * View, Plugins) keep text - they are the vocabulary of the product and + * nothing draws them unambiguously. Universally-recognised mechanics (undo, + * redo, the four viewport controls) and the utility surfaces on the right + * become icons with `title` + `aria-label` carrying the exact same wording + * they had as text, which is what keeps them findable by keyboard, by + * screen reader, and by every existing test. + * * Intent routing, surface by surface, against the ToolbarBridge @Slot list: * - zoomIn/zoomOut/resetZoom/fitAll -> React Flow viewport ops (they were * pure ChatView viewport calls; the viewport lives HERE now) @@ -26,66 +52,112 @@ import { Popover, useOverlays } from "../overlays/overlays"; * * R8a (UI/UX issue list finding #8): the provider-mode