diff --git a/.changeset/toolkits-empty-grid.md b/.changeset/toolkits-empty-grid.md new file mode 100644 index 0000000000..fa868f391f --- /dev/null +++ b/.changeset/toolkits-empty-grid.md @@ -0,0 +1,5 @@ +--- +"executor": patch +--- + +Prevent the empty Toolkits page from scrolling past its visible add cards. diff --git a/e2e/selfhost/toolkits-empty-grid.test.ts b/e2e/selfhost/toolkits-empty-grid.test.ts new file mode 100644 index 0000000000..41fe93091b --- /dev/null +++ b/e2e/selfhost/toolkits-empty-grid.test.ts @@ -0,0 +1,56 @@ +// An empty Toolkits grid must fit the viewport: with no toolkits in either +// scope, both the Workspace and Personal add cards sit above the fold and the +// grid does not scroll. Regression: each shelf reserved a fixed ~3-row +// min-height, so two empty shelves stacked past the viewport and the page +// scrolled with nothing to see. +import { expect } from "@effect/vitest"; +import { Effect } from "effect"; + +import { scenario } from "../src/scenario"; +import { Browser, Target } from "../src/services"; +import { visit } from "../src/surfaces/browser"; + +scenario( + "Toolkits · empty grid fits the viewport without scrolling", + { timeout: 120_000 }, + Effect.gen(function* () { + const target = yield* Target; + const browser = yield* Browser; + const identity = yield* target.newIdentity(); + + yield* browser.session(identity, async ({ page, step }) => { + await step("Open the Toolkits page with no toolkits in either scope", async () => { + await visit(page, "/default/toolkits/"); + await page.getByRole("heading", { name: "Toolkits", level: 1 }).waitFor(); + await page.getByRole("heading", { name: "Workspace" }).waitFor(); + await page.getByRole("heading", { name: "Personal" }).waitFor(); + await page.getByRole("button", { name: "Add workspace toolkit" }).waitFor(); + await page.getByRole("button", { name: "Add personal toolkit" }).waitFor(); + await page.locator('main [data-slot="skeleton"]').first().waitFor({ state: "detached" }); + }); + + await step("The empty grid does not overflow its scroll container", async () => { + // Walk up from the Personal add card to its nearest scrollable + // ancestor; on an empty grid that ancestor must have nothing to + // scroll. This is the user-visible contract — both add cards are + // reachable without scrolling — measured at the scroll boundary. + const overflow = await page.evaluate(() => { + const addCard = [...document.querySelectorAll("button")].find( + (button) => button.getAttribute("aria-label") === "Add personal toolkit", + ); + let node: HTMLElement | null = addCard ?? null; + while (node && node.scrollHeight <= node.clientHeight + 1) { + node = node.parentElement; + } + if (!node) return null; + return { + tag: node.tagName, + scrollHeight: node.scrollHeight, + clientHeight: node.clientHeight, + }; + }); + expect(overflow, "no scrollable ancestor overflows for an empty grid").toBeNull(); + }); + }); + }), +); diff --git a/packages/plugins/toolkits/src/page.tsx b/packages/plugins/toolkits/src/page.tsx index 9e78eaddbb..5d1a9f2a3e 100644 --- a/packages/plugins/toolkits/src/page.tsx +++ b/packages/plugins/toolkits/src/page.tsx @@ -181,7 +181,6 @@ const toolkitByRouteSlug = ( }; const toolkitCardStyle = { minHeight: "9rem" }; -const toolkitShelfStyle = { minHeight: "28.5rem" }; const toolkitGridContainerStyle = { maxWidth: "80rem" }; const toolkitToolTreeStyle = { width: "24rem" }; @@ -636,10 +635,7 @@ function ToolkitSection(props: { ) : null} -
+
{rows.map((toolkit) => ( ))} @@ -1204,10 +1200,7 @@ function ToolkitSectionSkeleton(props: { title?: string }) {
) : null} -
+