diff --git a/website/scripts/page-width-regression.mjs b/website/scripts/page-width-regression.mjs index 861ba58a6e..f9525ec508 100644 --- a/website/scripts/page-width-regression.mjs +++ b/website/scripts/page-width-regression.mjs @@ -15,9 +15,41 @@ const frameSelector = [ '[class*="sectionInner"]', '[class*="_catalog_"]', '[class*="catalogHeader"]', '[class*="recommendedInner"]', '[class*="ctaFrameInner"]', '#resources [class*="Grid-module__Grid___"]', + '[class*="_wideFrame_"]', ].join(", "); +// Catalogs beside their filter rail. 6 per page never splits, so these also run +// at 50 per page, where the wide grid below the filters is a frame too. +const splitRoutes = new Set([ + "agents/", "instructions/", "skills/", "plugins/", "extensions/", "learning-hub/", +]); const browser = await chromium.launch(); +async function checkFrames(page, colorScheme, width, route) { + const frames = await page.locator(frameSelector).evaluateAll(elements => + elements.map(element => { + const box = element.getBoundingClientRect(); + return { name: element.className, x: box.x, right: box.right, width: box.width }; + }), + ); + assert.ok(frames.length > 0, `${route}: content frames found`); + const header = await page.getByRole("navigation", { name: "Primary", exact: true }).boundingBox(); + const expected = Math.min(1280, header.width); + for (const frame of frames) { + const label = `${colorScheme} ${width}px ${route || "homepage"} ${frame.name}`; + assert.ok(frame.width <= 1281, `${label}: maximum width is 1280px, got ${frame.width}`); + assert.ok(frame.x >= -1 && frame.right <= width + 1, `${label}: within viewport`); + if (width >= 768) { + assert.ok(Math.abs(frame.width - expected) <= 1, `${label}: matches header width ${expected}`); + assert.ok(Math.abs(frame.x - header.x) <= 1, `${label}: aligns with header`); + } + } + if (width >= 1600) { + assert.ok(Math.abs(header.width - 1280) <= 1, `${route}: desktop frame fills the 1280px cap`); + } + assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth), false, + `${route}: no horizontal overflow`); +} + try { for (const colorScheme of ["light", "dark"]) { const page = await browser.newPage({ colorScheme, reducedMotion: "reduce" }); @@ -28,29 +60,16 @@ try { assert.equal(response.status(), 200, route); await page.waitForFunction(() => !document.querySelector("astro-island[ssr]")); await page.addStyleTag({ content: "html { scrollbar-gutter: stable; }" }); - const frames = await page.locator(frameSelector).evaluateAll(elements => - elements.map(element => { - const box = element.getBoundingClientRect(); - return { name: element.className, x: box.x, right: box.right, width: box.width }; - }), - ); - assert.ok(frames.length > 0, `${route}: content frames found`); - const header = await page.getByRole("navigation", { name: "Primary", exact: true }).boundingBox(); - const expected = Math.min(1280, header.width); - for (const frame of frames) { - const label = `${colorScheme} ${width}px ${route || "homepage"} ${frame.name}`; - assert.ok(frame.width <= 1281, `${label}: maximum width is 1280px, got ${frame.width}`); - assert.ok(frame.x >= -1 && frame.right <= width + 1, `${label}: within viewport`); - if (width >= 768) { - assert.ok(Math.abs(frame.width - expected) <= 1, `${label}: matches header width ${expected}`); - assert.ok(Math.abs(frame.x - header.x) <= 1, `${label}: aligns with header`); - } - } - if (width >= 1600) { - assert.ok(Math.abs(header.width - 1280) <= 1, `${route}: desktop frame fills the 1280px cap`); + await checkFrames(page, colorScheme, width, route); + // The rail stacks above the cards below 1012px, so nothing splits there. + if (width >= 1024 && splitRoutes.has(route)) { + const trigger = page.locator('summary[aria-label^="Resource page size"]'); + await trigger.click(); + await page.locator("details", { has: trigger }) + .getByRole("menuitemradio", { name: "50", exact: true }).click(); + await page.locator('[class*="_wideFrame_"]').waitFor(); + await checkFrames(page, colorScheme, width, `${route} at 50 per page`); } - assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth), false, - `${route}: no horizontal overflow`); } console.log(`PASS ${colorScheme} ${width}px: ${routes.length} page layouts share the 1280px cap`); } diff --git a/website/src/components/brand/AgentsCatalog.tsx b/website/src/components/brand/AgentsCatalog.tsx index 04c1871305..81e4bed8ac 100644 --- a/website/src/components/brand/AgentsCatalog.tsx +++ b/website/src/components/brand/AgentsCatalog.tsx @@ -31,6 +31,7 @@ import { daysSince, toggleValue, updatedBuckets, updatedBucketOf } from "./catal import { pageHref } from "./pageHref"; import { downloadFile } from "./resourceActions"; import { useCatalogPageFocus } from "./useCatalogPageFocus"; +import { useCatalogSplit } from "./useCatalogSplit"; import type { SearchItem } from "./searchIndex"; import styles from "./styles/agents.module.css"; @@ -201,6 +202,69 @@ export function AgentsCatalog({ const page = Math.min(currentPage, pageCount); const visibleAgents = sortedAgents.slice((page - 1) * pageSize, page * pageSize); + const { filterNavRef, sideGridRef, wideGridRef, beside, below } = + useCatalogSplit(visibleAgents, pageSize); + + const renderAgentCard = (agent: AgentItem) => ( + + + {agent.title} + + + {agent.description} + + + +
+ + + Install + + + + VS Code + + + VS Code Insiders + + + + + +
+
+ ); + return ( -