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");