fix: improve console workspace usability

This commit is contained in:
lda
2026-08-12 02:32:51 +07:00 Verified
parent b94b77cd39
commit b24ba6a92d
8 changed files with 70 additions and 42 deletions
+21 -31
View File
@@ -270,13 +270,14 @@ tbody tr:hover {
overflow: hidden;
}
/* The workspace keeps connection, navigation, content, and receipts visible together. */
/* Operation receipts belong to the route that produced them; the shell reserves
its width for navigation and the active workflow surface. */
.console-workspace {
display: grid;
grid-template-columns: minmax(9rem, 13rem) minmax(0, 1fr) minmax(18rem, 24rem);
grid-template-columns: minmax(9rem, 13rem) minmax(0, 1fr);
grid-template-areas:
"header header header"
"nav main evidence";
"header header"
"nav main";
gap: 0 1rem;
min-height: 100vh;
padding: 1rem;
@@ -379,23 +380,6 @@ tbody tr:hover {
min-width: 0;
}
.console-workspace__evidence {
grid-area: evidence;
align-self: start;
min-width: 0;
position: sticky;
top: 1rem;
max-height: calc(100vh - 2rem);
overflow: auto;
margin: 0;
}
.console-workspace__evidence h2 {
position: sticky;
top: 0;
background: var(--color-surface);
}
.workspace-route-pending {
min-height: 18rem;
margin: 0;
@@ -1054,9 +1038,13 @@ tbody tr:hover {
}
.capability-discovery__results,
.capability-discovery__detail {
.capability-discovery__detail,
.capability-playground {
min-width: 0;
margin: 0;
max-height: calc(100vh - 10rem);
overflow-y: auto;
overscroll-behavior: contain;
}
.capability-discovery__section-heading {
@@ -1803,6 +1791,8 @@ tbody tr:hover {
.authoring-graph {
overflow: hidden;
display: grid;
grid-template-rows: auto minmax(20rem, 1fr) minmax(0, auto);
}
.authoring-graph__heading {
@@ -1820,11 +1810,15 @@ tbody tr:hover {
}
.authoring-graph .workflow-graph {
height: 34rem;
height: 100%;
border-color: var(--color-border);
}
.authoring-graph__routes {
min-height: 0;
max-height: 12rem;
overflow-y: auto;
overscroll-behavior: contain;
margin-top: 0.75rem;
padding-top: 0.65rem;
border-top: 1px solid var(--color-border);
@@ -2002,13 +1996,11 @@ tbody tr:nth-child(10) { animation-delay: 270ms; }
grid-template-areas:
"header"
"nav"
"main"
"evidence";
"main";
gap: 1rem;
}
.console-workspace__nav,
.console-workspace__evidence {
.console-workspace__nav {
position: static;
}
@@ -2034,10 +2026,6 @@ tbody tr:nth-child(10) { animation-delay: 270ms; }
border-bottom-color: var(--color-signal-green);
}
.console-workspace__evidence {
max-height: none;
}
.capability-discovery__panes {
grid-template-columns: 1fr;
}
@@ -2048,10 +2036,12 @@ tbody tr:nth-child(10) { animation-delay: 270ms; }
.capability-discovery__detail {
position: static;
max-height: 70dvh;
}
.capability-playground {
position: static;
max-height: 70dvh;
}
.capability-discovery__filters {
@@ -3,13 +3,16 @@ import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it } from "vitest";
import { cleanup } from "@testing-library/react";
import { MemoryRouter } from "react-router-dom";
import { readFileSync } from "node:fs";
import { initialState } from "../app/state.js";
import { ConsoleShell } from "./ConsoleShell.js";
const globalStyles = readFileSync("src/styles/global.css", "utf8");
afterEach(() => cleanup());
describe("ConsoleShell", () => {
it("renders the connection header, lifecycle rail, main content, and evidence surface", () => {
it("renders the connection header, lifecycle rail, and main content without a global evidence panel", () => {
render(
<MemoryRouter initialEntries={["/console/discover"]}>
<ConsoleShell
@@ -27,13 +30,21 @@ describe("ConsoleShell", () => {
expect(screen.getByRole("main", { name: "Console workspace" })).toHaveTextContent(
"Discover content",
);
expect(screen.getByRole("complementary", { name: "Operation evidence" })).toBeInTheDocument();
expect(screen.queryByRole("complementary", { name: "Operation evidence" })).toBeNull();
expect(screen.getByRole("link", { name: "Discover" })).toHaveAttribute(
"aria-current",
"page",
);
});
it("reserves the workspace width for navigation and route content", () => {
expect(globalStyles).toMatch(
/\.console-workspace\s*\{[\s\S]*?grid-template-columns:\s*minmax\(9rem, 13rem\)\s+minmax\(0, 1fr\)/,
);
expect(globalStyles).toContain('"nav main"');
expect(globalStyles).not.toContain('"nav main evidence"');
});
it("renders all lifecycle links with route destinations", () => {
render(
<MemoryRouter>
@@ -2,7 +2,6 @@ import type { ReactNode } from "react";
import { NavLink } from "react-router-dom";
import type { ConnectionState } from "../app/state.js";
import { ConnectionHeader } from "../components/ConnectionHeader.js";
import { EvidenceLedger } from "./EvidenceLedger.js";
type Props = {
readonly connection: ConnectionState;
@@ -59,9 +58,5 @@ export const ConsoleShell = ({
<main id="console-workspace-main" aria-label="Console workspace" tabIndex={-1}>
{children}
</main>
<aside aria-label="Operation evidence" className="console-workspace__evidence">
<h2>Operation evidence</h2>
<EvidenceLedger records={connection.evidence} />
</aside>
</div>
);
@@ -149,13 +149,13 @@ describe("ConsoleWorkspace", () => {
expect(screen.getByTestId("executor-state")).toHaveTextContent("available");
expect(screen.getByTestId("write-executor-state")).toHaveTextContent("available");
expect(screen.getByTestId("executor-stable")).toHaveTextContent("yes");
expect(screen.getAllByText("Health check")).toHaveLength(1);
expect(screen.getByTestId("evidence-ids")).toHaveTextContent("workflow.health-0");
expect(mockedCallOperation).not.toHaveBeenCalled();
await userEvent.click(screen.getByRole("button", { name: "Navigate to drafts" }));
expect(await screen.findByText("Drafts route")).toBeInTheDocument();
expect(screen.getAllByText("Health check")).toHaveLength(1);
expect(screen.getByTestId("evidence-ids")).toHaveTextContent("workflow.health-0");
expect(screen.getByTestId("connected-target")).toHaveTextContent("http://one.example/rpc");
});
@@ -273,6 +273,6 @@ describe("ConsoleWorkspace", () => {
"http://two.example/rpc",
);
});
expect(screen.getAllByText("Health check")).toHaveLength(1);
expect(screen.getByTestId("evidence-ids")).toHaveTextContent("workflow.health-0");
});
});
@@ -188,6 +188,21 @@ describe("DraftWorkbench", () => {
expect(within(inspector as HTMLElement).getByText("demo.collect")).toBeInTheDocument();
});
it("opens the context inspector when a route outcome is selected on mobile", async () => {
setViewport(390);
const user = userEvent.setup();
const { container } = render(<DraftWorkbench draft={workspace} />);
const routes = screen.getByLabelText("Route outcomes");
await user.click(within(routes).getByRole("button", { name: /collect.*ok/i }));
const inspector = container.querySelector("#draft-workbench-inspector");
expect(inspector).toHaveAttribute("open", "");
expect(within(inspector as HTMLElement).getByRole("heading", { name: "Route inspector" })).toBeInTheDocument();
expect(within(inspector as HTMLElement).getByRole("textbox", { name: "Source step" })).toHaveValue("collect");
expect(within(inspector as HTMLElement).getByRole("textbox", { name: "Outcome" })).toHaveValue("ok");
});
it("keeps a dirty inspector form mounted and intact across mobile close and reopen", async () => {
setViewport(390);
mockedUseAuthoringCapabilityDetail.mockReturnValue({
@@ -164,8 +164,11 @@ export const DraftWorkbench = ({
(nextSelection: WorkbenchSelection): void => {
controller.select(nextSelection);
onSelectionChange?.(nextSelection);
if (isMobile && (nextSelection.kind === "edge" || nextSelection.kind === "node")) {
setOpenSheet("inspector");
}
},
[controller, onSelectionChange],
[controller, isMobile, onSelectionChange],
);
return (
@@ -1,4 +1,5 @@
import { cleanup, render, screen, within } from "@testing-library/react";
import { readFileSync } from "node:fs";
import userEvent from "@testing-library/user-event";
import { MemoryRouter, Route, Routes, useLocation } from "react-router-dom";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
@@ -33,6 +34,7 @@ vi.mock("../domain/draft-authoring-client.js", () => ({
const mockedUseCapabilityDiscovery = vi.mocked(useCapabilityDiscovery);
const mockedUseDraftWorkspace = vi.mocked(useDraftWorkspace);
const globalStyles = readFileSync("src/styles/global.css", "utf8");
const mockedUseConsoleWorkspace = vi.mocked(useConsoleWorkspace);
const mockedCreateDraftAuthoringClient = vi.mocked(createDraftAuthoringClient);
@@ -335,4 +337,10 @@ describe("DiscoverRoute", () => {
),
).toBeInTheDocument();
});
it("keeps both discovery panes scrollable within the viewport", () => {
expect(globalStyles).toMatch(
/\.capability-discovery__results,\s*\.capability-discovery__detail,\s*\.capability-playground\s*\{[\s\S]*?max-height:\s*calc\(100vh - 10rem\);[\s\S]*?overflow-y:\s*auto/,
);
});
});
@@ -150,6 +150,12 @@ describe("DraftDetailRoute", () => {
);
});
it("keeps route outcomes scrollable inside the bounded graph panel", () => {
expect(globalStyles).toMatch(
/\.authoring-graph__routes\s*\{[\s\S]*?min-height:\s*0;[\s\S]*?overflow-y:\s*auto/,
);
});
it("explains when the full draft document was not returned", () => {
mockedUseDraftWorkspace.mockReturnValue(
controller({ selected: workspace({ draft: null }) }),