From cfbb02c19df32efd60d84ec493a8b56259c4a7a6 Mon Sep 17 00:00:00 2001 From: Dan Barua Date: Sun, 11 Oct 2026 23:14:29 +0100 Subject: [PATCH 1/2] The top bar starts with the LabKit name as the link home; Collections gives Items room - The mark and name are one button that opens the workspaces. The sidebar toggle follows it, borderless, and the theme button is borderless and has no background. - The Back, Forward, home and top-bar filter controls are removed. The filter now sits in the Items pane and narrows its items. - In Record Explorer, Collections takes at most 40% of the height and scrolls beyond that, so Items keeps the rest. Co-Authored-By: Claude Sonnet 5.5 --- packages/app-web/docs/architecture.md | 7 +- packages/app-web/e2e/app.e2e.ts | 56 +++++++-- .../app-web/src/ui/trace/TraceConsole.tsx | 113 ++++-------------- packages/app-web/src/ui/trace/trace.css | 93 ++++++-------- 4 files changed, 108 insertions(+), 161 deletions(-) diff --git a/packages/app-web/docs/architecture.md b/packages/app-web/docs/architecture.md index 5a26b113..f66e7d76 100644 --- a/packages/app-web/docs/architecture.md +++ b/packages/app-web/docs/architecture.md @@ -121,10 +121,13 @@ Text that is not UTF-8, or a binary file of a text type, gets the note. | Column | Shows | |--------|-------| -| Left | The sidebar, in three sections that fold: File Explorer, Source Explorer, and Record Explorer, which lists two collections: the one picked (`?list=`) and the collection it is listed in. | +| Left | The sidebar, in three sections that fold: File Explorer, Source Explorer, and Record Explorer, which lists two collections: the one picked (`?list=`) and the collection it is listed in. Collections takes at most 40% of Record Explorer and scrolls beyond that; Items has the rest and a filter box that narrows the listed items. | | Centre | The graph, a card that folds: the resources opened in the workspace since its last reset and what they relate to, on a 3D canvas (`GraphView`). Clicking a node opens its resource. Below it, the open resource. Tabs: Overview (properties, then a card for each related resource, outbound then inbound), Debug, drawn as a wrench (the response; the events of the record, or of the acts that ran against the commit, each a line with chips for the records it names; and the commits those acts ran against). `?tab=` names the tab. The tabs and Reload sit at the right of the line with the resource's name. | -The button at the left of the top bar shows or hides the sidebar: +The top bar starts with the LabKit mark and name, which are one button that opens the list of +workspaces. The button after it shows or hides the sidebar. The theme button at the right has no +border or background. The browser's own Back and Forward buttons step through the pages opened. The +sidebar toggle works like this: - At full width, the sidebar is shown or hidden in place, and the browser keeps the choice for the next visit. diff --git a/packages/app-web/e2e/app.e2e.ts b/packages/app-web/e2e/app.e2e.ts index a2578893..268104ad 100644 --- a/packages/app-web/e2e/app.e2e.ts +++ b/packages/app-web/e2e/app.e2e.ts @@ -64,13 +64,53 @@ test.describe("getting around", () => { await expect(item(page, "Q_1")).toHaveClass(/active/); }); - test("the home button leads back to the workspaces", async ({ page }) => { + test("the LabKit name in the top bar leads back to the workspaces", async ({ page }) => { await page.goto("/app/workspace/alpha/Q_1"); - await page.getByRole("button", { name: "Entry point" }).click(); + await page.getByRole("button", { name: "LabKit" }).click(); await expect(page).toHaveURL(/\/app\/$/); await expect(page.getByText("Pick a type on the left")).toBeVisible(); }); + test("the top bar has the LabKit name first, then the sidebar toggle, and no history or filter controls", async ({ + page, + }) => { + await page.goto("/app/workspace/alpha/Q_1"); + const bar = page.locator(".topbar"); + const brand = bar.getByRole("button", { name: "LabKit" }); + const toggle = bar.getByRole("button", { name: "Show or hide the sidebar" }); + const x = async (locator: import("@playwright/test").Locator) => + (await locator.boundingBox())?.x ?? -1; + expect(await x(brand)).toBeLessThan(await x(toggle)); + await expect(bar.getByRole("button", { name: "Back" })).toHaveCount(0); + await expect(bar.getByRole("button", { name: "Forward" })).toHaveCount(0); + await expect(bar.getByRole("button", { name: "Entry point" })).toHaveCount(0); + await expect(bar.getByRole("textbox")).toHaveCount(0); + await expect(bar).not.toContainText("Trace Console"); + }); + + test("the Items filter narrows the listed items", async ({ page }) => { + await page.goto(`/app/workspace/alpha/Q_1?${list("/workspace/alpha/question")}`); + await expect(item(page, "Q_2")).toBeVisible(); + await page.getByRole("textbox", { name: "Filter the items" }).fill("Q_2"); + await expect(item(page, "Q_2")).toBeVisible(); + await expect(item(page, "Q_1")).toHaveCount(0); + }); + + test("in a short window, Collections takes at most 40% of Record Explorer and Items keeps the rest", async ({ + page, + }) => { + await page.setViewportSize({ width: 1280, height: 520 }); + await page.goto(`/app/workspace/alpha/Q_1?${list("/workspace/alpha/question")}`); + const panes = page.locator("aside.sidebar .explorer-body .sidebar-section"); + await expect(panes).toHaveCount(2); + const body = (await page.locator("aside.sidebar .explorer-body").boundingBox())?.height ?? 0; + const collections = (await panes.nth(0).boundingBox())?.height ?? 0; + const items = (await panes.nth(1).boundingBox())?.height ?? 0; + expect(collections).toBeLessThanOrEqual(0.4 * body + 2); + expect(items).toBeGreaterThanOrEqual(120); + expect(items).toBeGreaterThan(collections); + }); + test("a deep link renders directly, and survives a reload", async ({ page }) => { await page.goto("/app/workspace/alpha/LOE_1"); await expect(header(page)).toHaveText("LOE_1"); @@ -637,28 +677,24 @@ test.describe("an open resource", () => { expect(await page.evaluate(() => (window as unknown as { kept?: boolean }).kept)).toBe(true); }); - test("back and forward, the browser's or the page's, step through what was opened", async ({ - page, - }) => { + test("the browser's back and forward step through what was opened", async ({ page }) => { await page.goto("/app/workspace/alpha/Q_1"); await page.locator(".card-head", { hasText: "LOE_1" }).click(); await expect(header(page)).toHaveText("LOE_1"); await page.goBack(); await expect(header(page)).toHaveText("Q_1"); - await page.getByRole("button", { name: "Forward" }).click(); + await page.goForward(); await expect(header(page)).toHaveText("LOE_1"); - await expect(page.getByRole("button", { name: "Forward" })).toBeDisabled(); - await page.getByRole("button", { name: "Back" }).click(); - await expect(header(page)).toHaveText("Q_1"); }); test("opening the resource that is open adds no history entry", async ({ page }) => { await page.goto(`/app/workspace/alpha/Q_1?${list("/workspace/alpha/question")}`); await expect(header(page)).toHaveText("Q_1"); + const entries = await page.evaluate(() => history.length); await item(page, "Q_1").click(); await expect(header(page)).toHaveText("Q_1"); - await expect(page.getByRole("button", { name: "Back" })).toBeDisabled(); + expect(await page.evaluate(() => history.length)).toBe(entries); }); test("while its response is on its way, the page keeps its layout with placeholder rows", async ({ diff --git a/packages/app-web/src/ui/trace/TraceConsole.tsx b/packages/app-web/src/ui/trace/TraceConsole.tsx index 317510ac..954781c0 100644 --- a/packages/app-web/src/ui/trace/TraceConsole.tsx +++ b/packages/app-web/src/ui/trace/TraceConsole.tsx @@ -23,10 +23,8 @@ import { FlagCheckeredIcon, FolderIcon, GitCommitIcon, - HouseIcon, type Icon, IconContext, - MagnifyingGlassIcon, PauseIcon, PlayIcon, SealCheckIcon, @@ -36,7 +34,7 @@ import { } from "@phosphor-icons/react"; import markUrl from "@labkit/design/mark.svg"; import "@labkit/design/tokens.css"; -import { Link, useNavigate, useRouter, useRouterState } from "@tanstack/react-router"; +import { Link, useNavigate, useRouterState } from "@tanstack/react-router"; import { type CSSProperties, type ReactNode, @@ -107,34 +105,6 @@ function Spinner({ children }: { children: ReactNode }) { ); } -/** - * The history entries this page has seen, by their position in the browser's history, which enable - * or disable the Back and Forward buttons. The browser does not let a page read its history, so - * the page keeps the path it showed at each position: an entry is replaced when the page arrives at - * its position again by a new navigation, and the entries after it are dropped, as the browser - * drops them. The position and the path are read from one location, so a render between two - * navigations never pairs one's position with the other's path. - */ -function useTrail(): { trail: string[]; at: number } { - const here = useRouterState({ - select: (s) => - `${(s.location.state as { __TSR_index?: number }).__TSR_index ?? 0} ${s.location.pathname}`, - }); - const space = here.indexOf(" "); - const at = Number(here.slice(0, space)); - const path = here.slice(space + 1); - const [trail, setTrail] = useState([]); - useEffect(() => { - setTrail((t) => { - if (t[at] === path) return t; - const next = t.slice(0, at); - next[at] = path; - return next; - }); - }, [at, path]); - return { trail, at }; -} - /** * The resources the reader has opened since the graph was last reset, in the order they were first * opened. The graph draws these and what they relate to. `generation` counts the resets, so the @@ -186,9 +156,7 @@ export interface TraceConsoleProps { export function TraceConsole({ path, list, tab }: TraceConsoleProps) { const navigate = useNavigate(); - const router = useRouter(); const [theme, setTheme] = useSavedTheme(); - const [term, setTerm] = useState(""); const [drawer, setDrawer] = useState<"items" | undefined>(); // At full width the sidebar is shown or hidden, as this browser last left it; in a narrow // window it is a drawer over the page, closed until opened. @@ -244,10 +212,6 @@ export function TraceConsole({ path, list, tab }: TraceConsoleProps) { void navigate({ to: "/$", params: { _splat: "" }, search: {} }); }; - const { trail, at } = useTrail(); - const canBack = trail.slice(0, at).some((e) => e !== undefined); - const canForward = trail.slice(at + 1).some((e) => e !== undefined); - const listKey = openIsCollection ? path : (list ?? ENTRY); const prose = proseFor(held, path, open); @@ -255,6 +219,10 @@ export function TraceConsole({ path, list, tab }: TraceConsoleProps) {
+ -
- - Trace Console - live · {window.location.host} -
-
- - - -
+ live · {window.location.host} {import.meta.env.DEV ? : null} -
-
@@ -338,7 +262,6 @@ export function TraceConsole({ path, list, tab }: TraceConsoleProps) { root={listKey === ENTRY} openKey={path} held={held} - term={term} onOpen={open} onList={pickList} /> @@ -480,7 +403,6 @@ function Lists({ root, openKey, held, - term, onOpen, onList, }: { @@ -491,10 +413,11 @@ function Lists({ root: boolean; openKey: string; held: HeldIndex; - term: string; onOpen: (key: string) => void; onList: (key: string) => void; }) { + // Narrows the Items pane to the items whose text holds it. + const [term, setTerm] = useState(""); const listEntry = useDocument(requestPath(listKey)); // Until the picked collection's first page arrives, the collection it was picked from stays the // upper one, so the upper pane does not show the picked collection and then swap back. @@ -652,16 +575,13 @@ function Lists({
{upperKey}
{lowerRow === undefined ? null : ( - + )}
+ {lowerKey === undefined ? null : ( + setTerm(e.target.value)} + /> + )}
{lowerKey ?? ""}
{lowerKey === undefined ? ( diff --git a/packages/app-web/src/ui/trace/trace.css b/packages/app-web/src/ui/trace/trace.css index cd3211b5..8a74b29b 100644 --- a/packages/app-web/src/ui/trace/trace.css +++ b/packages/app-web/src/ui/trace/trace.css @@ -214,12 +214,22 @@ border-bottom: 1px solid var(--line); background: var(--panel); } +/* The brand is the link to the workspaces. */ .trace .brand { display: flex; align-items: center; gap: 8px; + padding: 4px 6px; + border: 0; + border-radius: 6px; + background: none; + color: var(--text); + font-size: inherit; white-space: nowrap; } +.trace .brand:hover { + background: var(--panel-2); +} .trace .brand-mark { width: 21px; height: 23.6px; @@ -238,41 +248,18 @@ .trace .hamburger { display: flex; background: none; - border: 1px solid var(--line); + border: 0; border-radius: 6px; - width: 32px; - height: 32px; + width: 30px; + height: 30px; padding: 0; align-items: center; justify-content: center; - color: var(--text); -} - -.trace .nav-controls { - display: flex; - align-items: center; - gap: 4px; + color: var(--text-dim); } -.trace .nav-btn { +.trace .hamburger:hover { background: var(--panel-2); - border: 1px solid var(--line); - border-radius: 6px; - width: 28px; - height: 28px; - padding: 0; - display: flex; - align-items: center; - justify-content: center; color: var(--text); - font-size: 14px; -} -.trace .nav-btn:disabled { - opacity: 0.35; - cursor: default; -} -.trace .nav-btn:not(:disabled):hover { - border-color: var(--accent); - color: var(--accent); } .trace .topbar-gap { @@ -293,41 +280,31 @@ color: var(--accent); } -.trace .search-wrap { - position: relative; - flex: 0 0 220px; -} -.trace .search-wrap input { - width: 100%; - background: var(--panel-2); - border: 1px solid var(--line); - border-radius: 7px; - padding: 6px 10px 6px 28px; - color: var(--text); -} -.trace .search-wrap input::placeholder { - color: var(--text-faint); -} -.trace .search-wrap .icon { - position: absolute; - left: 8px; - top: 50%; - transform: translateY(-50%); - color: var(--text-faint); -} - .trace .theme-btn { display: inline-flex; align-items: center; - background: var(--panel-2); - border: 1px solid var(--line); + background: none; + border: 0; border-radius: 6px; - padding: 5px 9px; + padding: 6px 8px; color: var(--text-dim); } .trace .theme-btn:hover { + background: var(--panel-2); color: var(--text); - border-color: var(--accent); +} +.trace .items-filter { + width: 100%; + margin-bottom: 6px; + background: var(--panel-2); + border: 1px solid var(--line); + border-radius: 6px; + padding: 4px 8px; + color: var(--text); + font-size: 12.5px; +} +.trace .items-filter::placeholder { + color: var(--text-faint); } /* ---------- body layout ---------- */ @@ -706,10 +683,11 @@ .trace .explorer-body .sidebar-section:last-child { border-bottom: 0; } -/* In a short Record Explorer, Collections scrolls rather than pushing Items out of the sidebar, - and Items keeps room for a few rows. */ +/* In Record Explorer, Collections takes at most 40% of the height and scrolls beyond that, so + Items, which the reader works in more, keeps the rest and room for a few rows. */ .trace .explorer-body .sidebar-section:not(.grow) { flex: 0 1 auto; + max-height: 40%; min-height: 72px; overflow-y: auto; overflow-x: hidden; @@ -1419,7 +1397,6 @@ /* ---------- narrow windows: the side column becomes a drawer ---------- */ @media (max-width: 860px) { - .trace .search-wrap, .trace .dev-links { display: none; } From 8699b18bb0f22513db7158563bf80bdea2548f4b Mon Sep 17 00:00:00 2001 From: Dan Barua Date: Sun, 11 Oct 2026 23:20:57 +0100 Subject: [PATCH 2/2] The top bar no longer shows "live" and the host address The browser's address bar already shows the host. Co-Authored-By: Claude Sonnet 5.5 --- packages/app-web/src/ui/trace/TraceConsole.tsx | 1 - packages/app-web/src/ui/trace/trace.css | 4 ---- 2 files changed, 5 deletions(-) diff --git a/packages/app-web/src/ui/trace/TraceConsole.tsx b/packages/app-web/src/ui/trace/TraceConsole.tsx index 954781c0..f0b457f4 100644 --- a/packages/app-web/src/ui/trace/TraceConsole.tsx +++ b/packages/app-web/src/ui/trace/TraceConsole.tsx @@ -236,7 +236,6 @@ export function TraceConsole({ path, list, tab }: TraceConsoleProps) { >