Skip to content

Commit c3f7c6e

Browse files
committed
Fit the empty Toolkits grid to the viewport
Each ToolkitSection shelf reserved a fixed ~3-row min-height, so the cloud grid's Workspace + Personal stack overflowed the viewport and scrolled even with zero toolkits (and padded short shelves with dead space). Size sections to content instead; the skeleton matches, and a new selfhost scenario pins the empty grid to its scroll boundary.
1 parent 9c67fd5 commit c3f7c6e

3 files changed

Lines changed: 63 additions & 9 deletions

File tree

.changeset/toolkits-empty-grid.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"executor": patch
3+
---
4+
5+
Prevent the empty Toolkits page from scrolling past its visible add cards.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// An empty Toolkits grid must fit the viewport: with no toolkits in either
2+
// scope, both the Workspace and Personal add cards sit above the fold and the
3+
// grid does not scroll. Regression: each shelf reserved a fixed ~3-row
4+
// min-height, so two empty shelves stacked past the viewport and the page
5+
// scrolled with nothing to see.
6+
import { expect } from "@effect/vitest";
7+
import { Effect } from "effect";
8+
9+
import { scenario } from "../src/scenario";
10+
import { Browser, Target } from "../src/services";
11+
import { visit } from "../src/surfaces/browser";
12+
13+
scenario(
14+
"Toolkits · empty grid fits the viewport without scrolling",
15+
{ timeout: 120_000 },
16+
Effect.gen(function* () {
17+
const target = yield* Target;
18+
const browser = yield* Browser;
19+
const identity = yield* target.newIdentity();
20+
21+
yield* browser.session(identity, async ({ page, step }) => {
22+
await step("Open the Toolkits page with no toolkits in either scope", async () => {
23+
await visit(page, "/default/toolkits/");
24+
await page.getByRole("heading", { name: "Toolkits", level: 1 }).waitFor();
25+
await page.getByRole("heading", { name: "Workspace" }).waitFor();
26+
await page.getByRole("heading", { name: "Personal" }).waitFor();
27+
await page.getByRole("button", { name: "Add workspace toolkit" }).waitFor();
28+
await page.getByRole("button", { name: "Add personal toolkit" }).waitFor();
29+
await page.locator('main [data-slot="skeleton"]').first().waitFor({ state: "detached" });
30+
});
31+
32+
await step("The empty grid does not overflow its scroll container", async () => {
33+
// Walk up from the Personal add card to its nearest scrollable
34+
// ancestor; on an empty grid that ancestor must have nothing to
35+
// scroll. This is the user-visible contract — both add cards are
36+
// reachable without scrolling — measured at the scroll boundary.
37+
const overflow = await page.evaluate(() => {
38+
const addCard = [...document.querySelectorAll("button")].find(
39+
(button) => button.getAttribute("aria-label") === "Add personal toolkit",
40+
);
41+
let node: HTMLElement | null = addCard ?? null;
42+
while (node && node.scrollHeight <= node.clientHeight + 1) {
43+
node = node.parentElement;
44+
}
45+
if (!node) return null;
46+
return {
47+
tag: node.tagName,
48+
scrollHeight: node.scrollHeight,
49+
clientHeight: node.clientHeight,
50+
};
51+
});
52+
expect(overflow, "no scrollable ancestor overflows for an empty grid").toBeNull();
53+
});
54+
});
55+
}),
56+
);

packages/plugins/toolkits/src/page.tsx

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,6 @@ const toolkitByRouteSlug = (
181181
};
182182

183183
const toolkitCardStyle = { minHeight: "9rem" };
184-
const toolkitShelfStyle = { minHeight: "28.5rem" };
185184
const toolkitGridContainerStyle = { maxWidth: "80rem" };
186185
const toolkitToolTreeStyle = { width: "24rem" };
187186

@@ -636,10 +635,7 @@ function ToolkitSection(props: {
636635
</div>
637636
) : null}
638637

639-
<div
640-
className="grid content-start grid-cols-1 gap-3 sm:grid-cols-2 md:grid-cols-3"
641-
style={toolkitShelfStyle}
642-
>
638+
<div className="grid content-start grid-cols-1 gap-3 sm:grid-cols-2 md:grid-cols-3">
643639
{rows.map((toolkit) => (
644640
<ToolkitTile key={toolkit.id} showOwnerLabels={props.showOwnerLabels} toolkit={toolkit} />
645641
))}
@@ -1204,10 +1200,7 @@ function ToolkitSectionSkeleton(props: { title?: string }) {
12041200
</div>
12051201
) : null}
12061202

1207-
<div
1208-
className="grid content-start grid-cols-1 gap-3 sm:grid-cols-2 md:grid-cols-3"
1209-
style={toolkitShelfStyle}
1210-
>
1203+
<div className="grid content-start grid-cols-1 gap-3 sm:grid-cols-2 md:grid-cols-3">
12111204
<ToolkitTileSkeleton />
12121205
</div>
12131206
</section>

0 commit comments

Comments
 (0)