Mercurial
diff design_system/src/storybook.js @ 254:2b6e732087ff
[ui] Add complete native component catalog
Co-authored-by: Copilot <[email protected]>
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Tue, 04 Aug 2026 15:12:09 -0700 |
| parents | 7a7581f040e8 |
| children |
line wrap: on
line diff
--- a/design_system/src/storybook.js Tue Aug 04 12:21:19 2026 -0700 +++ b/design_system/src/storybook.js Tue Aug 04 15:12:09 2026 -0700 @@ -1,9 +1,22 @@ import "./components/index.js"; +import { componentCatalog } from "./catalog.js"; +import { ICON_NAMES } from "./components/icon.js"; + +const stackComponent = { + name: "Stack", + slug: "stack", + element: "zen-stack", + group: "Layout", + description: "A Zenbu-specific flex layout primitive with tokenized gaps.", + existing: true, +}; +const displayedComponents = [...componentCatalog, stackComponent]; const pages = new Map([ ["/", "overview"], ["/components", "overview"], ["/tokens", "tokens"], + ["/icons", "icons"], ["/components/button", "button"], ["/components/card", "card"], ["/components/alert", "alert"], @@ -12,6 +25,160 @@ ["/components/stack", "stack"], ]); +for (const component of componentCatalog) { + pages.set(`/components/${component.slug}`, component.slug); +} + +function createCatalogLink(component) { + const link = document.createElement("a"); + link.href = `/components/${component.slug}`; + link.dataset.catalogLink = ""; + link.dataset.zenElement = component.element; + link.textContent = component.name; + return link; +} + +function createComponentCard(component) { + const link = createCatalogLink(component); + const card = document.createElement("zen-card"); + card.setAttribute("interactive", ""); + const article = document.createElement("article"); + const heading = document.createElement("h3"); + heading.textContent = component.name; + const description = document.createElement("p"); + description.textContent = component.description; + const element = document.createElement("code"); + element.textContent = component.element; + article.append(heading, description, element); + card.append(article); + link.replaceChildren(card); + return link; +} + +function createCatalogPage(component) { + const section = document.createElement("section"); + section.className = "catalog-page"; + section.dataset.catalogPage = component.slug; + section.hidden = true; + + const header = document.createElement("header"); + header.className = "page-heading"; + const eyebrow = document.createElement("p"); + eyebrow.className = "eyebrow"; + eyebrow.textContent = component.group; + const heading = document.createElement("h1"); + heading.textContent = component.name; + const description = document.createElement("p"); + description.textContent = component.description; + header.append(eyebrow, heading, description); + + const story = document.createElement("zen-story"); + story.setAttribute("name", `${component.name} example`); + const template = document.createElement("template"); + template.innerHTML = component.markup; + story.append(template); + section.append(header, story); + return section; +} + +function installComponentCatalog() { + const navigation = document.querySelector( + '.catalog-nav[aria-label="Components"]', + ); + const componentGrid = document.querySelector(".component-grid"); + const main = document.querySelector("#catalogMain"); + if (!navigation || !componentGrid || !main) return; + + navigation.replaceChildren( + ...displayedComponents.map(createCatalogLink), + ); + componentGrid.replaceChildren( + ...displayedComponents.map(createComponentCard), + ); + + for (const component of componentCatalog) { + if (component.existing) continue; + main.append(createCatalogPage(component)); + } +} + +function installIconCatalog() { + const grid = document.querySelector("#iconGrid"); + if (!grid) return; + const items = ICON_NAMES.map(name => { + const figure = document.createElement("figure"); + const icon = document.createElement("zen-icon"); + icon.setAttribute("name", name); + icon.setAttribute("size", "2rem"); + const caption = document.createElement("figcaption"); + const code = document.createElement("code"); + code.textContent = name; + caption.append(code); + figure.append(icon, caption); + return figure; + }); + grid.replaceChildren(...items); +} + +function installComponentSearch() { + const input = document.querySelector("#componentSearch"); + const status = document.querySelector("#componentSearchStatus"); + if (!input || !status) return; + const links = [ + ...document.querySelectorAll( + '.catalog-nav[aria-label="Components"] a', + ), + ]; + const cards = [...document.querySelectorAll(".component-grid > a")]; + const descriptions = new Map( + displayedComponents.map(component => [ + component.slug, + [ + component.name, + component.element, + component.group, + component.description, + ].join(" ").toLocaleLowerCase(), + ]), + ); + const filter = () => { + const query = input.value.trim().toLocaleLowerCase(); + let matches = 0; + for (const link of links) { + const slug = new URL(link.href).pathname.split("/").pop(); + const visible = !query || descriptions.get(slug)?.includes(query); + link.hidden = !visible; + if (visible) matches++; + } + for (const card of cards) { + const slug = new URL(card.href).pathname.split("/").pop(); + card.hidden = Boolean(query) && + !descriptions.get(slug)?.includes(query); + } + status.textContent = query + ? `${matches} component${matches === 1 ? "" : "s"} found` + : `${links.length} components`; + }; + + input.addEventListener("input", filter); + input.addEventListener("keydown", event => { + if (event.key !== "Escape" || !input.value) return; + input.value = ""; + filter(); + }); + document.addEventListener("keydown", event => { + const target = event.target; + const typing = target instanceof HTMLElement && + target.matches("input, textarea, select, [contenteditable]"); + if (event.key !== "/" || typing || event.metaKey || event.ctrlKey || + event.altKey) return; + event.preventDefault(); + input.focus(); + input.select(); + }); + filter(); +} + function showPage(pathname) { const pageName = pages.get(pathname) || "overview"; for (const page of document.querySelectorAll("[data-catalog-page]")) { @@ -31,6 +198,9 @@ } window.addEventListener("DOMContentLoaded", () => { + installComponentCatalog(); + installIconCatalog(); + installComponentSearch(); const root = document.documentElement; const systemTheme = matchMedia("(prefers-color-scheme: dark)"); const storedTheme = localStorage.getItem("zen-theme");