fix(design-system): simplify catalog links and nest component navigation

Signed-off-by: Arjun Mahanti <arjun@squareup.com>
This commit is contained in:
Arjun Mahanti
2026-09-09 11:46:28 -07:00
parent 8e786de355
commit b3b7de52bc
6 changed files with 67 additions and 46 deletions

View File

@@ -15,14 +15,17 @@ Open `http://localhost:1430/design`. Use another port when needed:
## Focused component pages
Click a component heading or **Explore states** in the catalog to open
Click a section’s eyebrow link (the category followed by →) in the catalog to open
`/design/components/<id>` (for example, `/design/components/input`). Every catalog
entry has a dedicated URL with an isolated live playground, component-specific
state guidance, and its example module. Fields, checkboxes, switches, tabs,
state guidance, and its example module. Source is shown only on these focused pages. Fields, checkboxes, switches, tabs,
progress, and meters also include side-by-side state samples using the production
components. Reset example restores both the playground and its state samples.
Use the component picker or Previous / Next links to move through the catalog.
Every component is also indented beneath All components in the navigation, like
Token table beneath Color. The navigation list scrolls independently so theme and
density controls remain available. Use these links, the component picker, or
Previous / Next links to move through the catalog.
All components returns to that component's original catalog anchor. Direct links
and refreshes work with the same SPA fallback as the rest of the site; unknown
component IDs have a recovery link. Theme and density preferences are shared.

View File

@@ -8,7 +8,7 @@ export function AppearanceControls() {
const { density, setDensity, saveError } = useDensity();
return (
<div className="mx-3 flex flex-col items-start gap-3">
<div className="mx-3 flex shrink-0 flex-col items-start gap-3">
<Button
size="sm"
variant="secondary"

View File

@@ -66,14 +66,16 @@ export function ComponentsPage() {
className="catalog-entry"
>
<div>
<p className="text-meta text-tertiary">{entry.category}</p>
<Link
to="/design/components/$componentId"
params={{ componentId: entry.id }}
className="catalog-eyebrow text-meta text-tertiary hover:text-primary"
aria-label={`${entry.category}: ${entry.name}`}
>
{entry.category} <span aria-hidden="true">→</span>
</Link>
<h2 id={`${entry.id}-title`} className="text-heading mt-1">
<Link
to="/design/components/$componentId"
params={{ componentId: entry.id }}
>
{entry.name}
</Link>
{entry.name}
</h2>
<p className="text-caption text-secondary mt-2">
{entry.description}
@@ -82,22 +84,6 @@ export function ComponentsPage() {
<div className="catalog-preview">
<entry.Preview />
</div>
<Link
to="/design/components/$componentId"
params={{ componentId: entry.id }}
className="text-label text-accent"
aria-label={`Explore ${entry.name}`}
>
Explore states →
</Link>
<details className="catalog-source">
<summary className="text-caption text-secondary">
View example source
</summary>
<pre className="text-code">
<code>{entry.source}</code>
</pre>
</details>
</section>
))}
</div>

View File

@@ -3,6 +3,7 @@ import { Link, Outlet } from "@tanstack/react-router";
import { Fragment, type ReactNode } from "react";
import { AppearanceControls } from "./AppearanceControls";
import { CATALOG } from "../catalog/registry";
/** A nav entry, optionally with children shown indented beneath it. */
type NavItem = [label: string, to: string, children?: Array<[string, string]>];
@@ -33,7 +34,11 @@ const SECTIONS: Array<{ heading: string; items: NavItem[] }> = [
{
heading: "Components",
items: [
["All components", "/design/components"],
[
"All components",
"/design/components",
CATALOG.map((entry) => [entry.name, `/design/components/${entry.id}`]),
],
["Compositions", "/design/compositions"],
["Open-source guide", "/design/open-source"],
],
@@ -73,9 +78,9 @@ export function DesignSystemLayout() {
<div className="flex min-h-screen flex-col bg-panel lg:flex-row">
<nav
aria-label="Design system"
className="flex shrink-0 flex-col gap-8 px-4 py-8 lg:sticky lg:top-0 lg:h-screen lg:w-[16rem] lg:overflow-y-auto"
className="flex shrink-0 flex-col gap-8 px-4 py-8 lg:sticky lg:top-0 lg:h-screen lg:w-[16rem]"
>
<div className="px-3">
<div className="shrink-0 px-3">
<Link to="/design" className="text-label text-primary">
Buzz Design System
</Link>
@@ -84,7 +89,7 @@ export function DesignSystemLayout() {
</p>
</div>
<div className="flex flex-1 flex-col gap-6 lg:gap-7">
<div className="design-nav-sections flex min-h-0 max-h-80 flex-1 flex-col gap-6 overflow-y-auto lg:max-h-none lg:gap-7">
{SECTIONS.map((section) => (
<div key={section.heading} className="flex flex-col gap-1">
<h2 className="px-3 pb-1.5 text-meta text-tertiary">

View File

@@ -6,15 +6,25 @@ test("every catalog entry links to its own focused component page", async ({
page,
}) => {
await page.goto("/design/components");
const entries = await page
.locator(".catalog-entry h2 a")
.evaluateAll((links) =>
links.map((link) => ({
name: link.textContent ?? "",
href: link.getAttribute("href") ?? "",
})),
);
const entries = await page.locator(".catalog-entry").evaluateAll((links) =>
links.map((link) => ({
name: link.querySelector("h2")?.textContent ?? "",
href: link.querySelector(".catalog-eyebrow")?.getAttribute("href") ?? "",
})),
);
expect(entries).toHaveLength(55);
await expect(page.getByText("Explore states →", { exact: true })).toHaveCount(
0,
);
await expect(page.locator(".catalog-grid .catalog-source")).toHaveCount(0);
for (const eyebrow of await page.locator(".catalog-eyebrow").all()) {
await expect(eyebrow).toContainText("→");
}
const nav = page.getByRole("navigation", {
name: "Design system",
exact: true,
});
await expect(nav.locator('a[href^="/design/components/"]')).toHaveCount(55);
expect(new Set(entries.map((entry) => entry.href)).size).toBe(55);
for (const entry of entries) {
await page.goto(entry.href);
@@ -38,7 +48,7 @@ test("catalog navigation, picker, previous/next, reset, and invalid links work",
}) => {
await page.goto("/design/components");
await page.getByRole("button", { name: "Compact", exact: true }).click();
await page.getByRole("link", { name: "Explore Input", exact: true }).click();
await page.getByRole("link", { name: "Inputs: Input", exact: true }).click();
await expect(page).toHaveURL(/\/design\/components\/input$/);
await expect(page.getByRole("heading", { level: 1 })).toBeFocused();
const input = page.getByRole("textbox", {
@@ -49,6 +59,22 @@ test("catalog navigation, picker, previous/next, reset, and invalid links work",
await expect(input).toHaveCSS("height", "32px");
await page.getByRole("button", { name: "Reset example" }).click();
await expect(input).toHaveValue("");
const nav = page.getByRole("navigation", {
name: "Design system",
exact: true,
});
await expect(
nav.getByRole("link", { name: "Input", exact: true }),
).toHaveAttribute("aria-current", "page");
await expect(
nav.getByRole("link", { name: "All components", exact: true }),
).not.toHaveAttribute("aria-current", "page");
await nav.getByRole("link", { name: "Resizable", exact: true }).click();
await expect(page).toHaveURL(/\/resizable$/);
await expect(
page.getByRole("button", { name: "Compact", exact: true }),
).toBeInViewport();
await nav.getByRole("link", { name: "Input", exact: true }).click();
await page
.getByRole("link", { name: "Next: Textarea", exact: false })
.click();

View File

@@ -7,12 +7,7 @@ test("table cells align to the reading edge and scroll surfaces use thin bars",
const cells = page.locator("#table th, #table td");
for (const cell of await cells.all())
await expect(cell).toHaveCSS("text-align", "start");
for (const selector of [
"html",
"textarea",
"#table .bui-table-scroll",
".catalog-source pre",
]) {
for (const selector of ["html", "textarea", "#table .bui-table-scroll"]) {
await expect(page.locator(selector).first()).toHaveCSS(
"scrollbar-width",
"thin",
@@ -29,6 +24,12 @@ test("table cells align to the reading edge and scroll surfaces use thin bars",
await expect
.poll(() => viewport.evaluate((node) => node.scrollTop))
.toBeGreaterThan(0);
await page.goto("/design/components/input");
await page.getByText("View example module", { exact: true }).click();
await expect(page.locator(".catalog-source pre")).toHaveCSS(
"scrollbar-width",
"thin",
);
});
test("resize grip is one rem with a usable pointer and keyboard target", async ({