mirror of
https://github.com/block/buzz.git
synced 2026-11-02 07:22:55 +00:00
fix(design-system): simplify catalog links and nest component navigation
Signed-off-by: Arjun Mahanti <arjun@squareup.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 ({
|
||||
|
||||
Reference in New Issue
Block a user