Mercurial
comparison 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 |
comparison
equal
deleted
inserted
replaced
| 253:fdf3816959cb | 254:2b6e732087ff |
|---|---|
| 1 import "./components/index.js"; | 1 import "./components/index.js"; |
| 2 import { componentCatalog } from "./catalog.js"; | |
| 3 import { ICON_NAMES } from "./components/icon.js"; | |
| 4 | |
| 5 const stackComponent = { | |
| 6 name: "Stack", | |
| 7 slug: "stack", | |
| 8 element: "zen-stack", | |
| 9 group: "Layout", | |
| 10 description: "A Zenbu-specific flex layout primitive with tokenized gaps.", | |
| 11 existing: true, | |
| 12 }; | |
| 13 const displayedComponents = [...componentCatalog, stackComponent]; | |
| 2 | 14 |
| 3 const pages = new Map([ | 15 const pages = new Map([ |
| 4 ["/", "overview"], | 16 ["/", "overview"], |
| 5 ["/components", "overview"], | 17 ["/components", "overview"], |
| 6 ["/tokens", "tokens"], | 18 ["/tokens", "tokens"], |
| 19 ["/icons", "icons"], | |
| 7 ["/components/button", "button"], | 20 ["/components/button", "button"], |
| 8 ["/components/card", "card"], | 21 ["/components/card", "card"], |
| 9 ["/components/alert", "alert"], | 22 ["/components/alert", "alert"], |
| 10 ["/components/field", "field"], | 23 ["/components/field", "field"], |
| 11 ["/components/notifications", "notifications"], | 24 ["/components/notifications", "notifications"], |
| 12 ["/components/stack", "stack"], | 25 ["/components/stack", "stack"], |
| 13 ]); | 26 ]); |
| 27 | |
| 28 for (const component of componentCatalog) { | |
| 29 pages.set(`/components/${component.slug}`, component.slug); | |
| 30 } | |
| 31 | |
| 32 function createCatalogLink(component) { | |
| 33 const link = document.createElement("a"); | |
| 34 link.href = `/components/${component.slug}`; | |
| 35 link.dataset.catalogLink = ""; | |
| 36 link.dataset.zenElement = component.element; | |
| 37 link.textContent = component.name; | |
| 38 return link; | |
| 39 } | |
| 40 | |
| 41 function createComponentCard(component) { | |
| 42 const link = createCatalogLink(component); | |
| 43 const card = document.createElement("zen-card"); | |
| 44 card.setAttribute("interactive", ""); | |
| 45 const article = document.createElement("article"); | |
| 46 const heading = document.createElement("h3"); | |
| 47 heading.textContent = component.name; | |
| 48 const description = document.createElement("p"); | |
| 49 description.textContent = component.description; | |
| 50 const element = document.createElement("code"); | |
| 51 element.textContent = component.element; | |
| 52 article.append(heading, description, element); | |
| 53 card.append(article); | |
| 54 link.replaceChildren(card); | |
| 55 return link; | |
| 56 } | |
| 57 | |
| 58 function createCatalogPage(component) { | |
| 59 const section = document.createElement("section"); | |
| 60 section.className = "catalog-page"; | |
| 61 section.dataset.catalogPage = component.slug; | |
| 62 section.hidden = true; | |
| 63 | |
| 64 const header = document.createElement("header"); | |
| 65 header.className = "page-heading"; | |
| 66 const eyebrow = document.createElement("p"); | |
| 67 eyebrow.className = "eyebrow"; | |
| 68 eyebrow.textContent = component.group; | |
| 69 const heading = document.createElement("h1"); | |
| 70 heading.textContent = component.name; | |
| 71 const description = document.createElement("p"); | |
| 72 description.textContent = component.description; | |
| 73 header.append(eyebrow, heading, description); | |
| 74 | |
| 75 const story = document.createElement("zen-story"); | |
| 76 story.setAttribute("name", `${component.name} example`); | |
| 77 const template = document.createElement("template"); | |
| 78 template.innerHTML = component.markup; | |
| 79 story.append(template); | |
| 80 section.append(header, story); | |
| 81 return section; | |
| 82 } | |
| 83 | |
| 84 function installComponentCatalog() { | |
| 85 const navigation = document.querySelector( | |
| 86 '.catalog-nav[aria-label="Components"]', | |
| 87 ); | |
| 88 const componentGrid = document.querySelector(".component-grid"); | |
| 89 const main = document.querySelector("#catalogMain"); | |
| 90 if (!navigation || !componentGrid || !main) return; | |
| 91 | |
| 92 navigation.replaceChildren( | |
| 93 ...displayedComponents.map(createCatalogLink), | |
| 94 ); | |
| 95 componentGrid.replaceChildren( | |
| 96 ...displayedComponents.map(createComponentCard), | |
| 97 ); | |
| 98 | |
| 99 for (const component of componentCatalog) { | |
| 100 if (component.existing) continue; | |
| 101 main.append(createCatalogPage(component)); | |
| 102 } | |
| 103 } | |
| 104 | |
| 105 function installIconCatalog() { | |
| 106 const grid = document.querySelector("#iconGrid"); | |
| 107 if (!grid) return; | |
| 108 const items = ICON_NAMES.map(name => { | |
| 109 const figure = document.createElement("figure"); | |
| 110 const icon = document.createElement("zen-icon"); | |
| 111 icon.setAttribute("name", name); | |
| 112 icon.setAttribute("size", "2rem"); | |
| 113 const caption = document.createElement("figcaption"); | |
| 114 const code = document.createElement("code"); | |
| 115 code.textContent = name; | |
| 116 caption.append(code); | |
| 117 figure.append(icon, caption); | |
| 118 return figure; | |
| 119 }); | |
| 120 grid.replaceChildren(...items); | |
| 121 } | |
| 122 | |
| 123 function installComponentSearch() { | |
| 124 const input = document.querySelector("#componentSearch"); | |
| 125 const status = document.querySelector("#componentSearchStatus"); | |
| 126 if (!input || !status) return; | |
| 127 const links = [ | |
| 128 ...document.querySelectorAll( | |
| 129 '.catalog-nav[aria-label="Components"] a', | |
| 130 ), | |
| 131 ]; | |
| 132 const cards = [...document.querySelectorAll(".component-grid > a")]; | |
| 133 const descriptions = new Map( | |
| 134 displayedComponents.map(component => [ | |
| 135 component.slug, | |
| 136 [ | |
| 137 component.name, | |
| 138 component.element, | |
| 139 component.group, | |
| 140 component.description, | |
| 141 ].join(" ").toLocaleLowerCase(), | |
| 142 ]), | |
| 143 ); | |
| 144 const filter = () => { | |
| 145 const query = input.value.trim().toLocaleLowerCase(); | |
| 146 let matches = 0; | |
| 147 for (const link of links) { | |
| 148 const slug = new URL(link.href).pathname.split("/").pop(); | |
| 149 const visible = !query || descriptions.get(slug)?.includes(query); | |
| 150 link.hidden = !visible; | |
| 151 if (visible) matches++; | |
| 152 } | |
| 153 for (const card of cards) { | |
| 154 const slug = new URL(card.href).pathname.split("/").pop(); | |
| 155 card.hidden = Boolean(query) && | |
| 156 !descriptions.get(slug)?.includes(query); | |
| 157 } | |
| 158 status.textContent = query | |
| 159 ? `${matches} component${matches === 1 ? "" : "s"} found` | |
| 160 : `${links.length} components`; | |
| 161 }; | |
| 162 | |
| 163 input.addEventListener("input", filter); | |
| 164 input.addEventListener("keydown", event => { | |
| 165 if (event.key !== "Escape" || !input.value) return; | |
| 166 input.value = ""; | |
| 167 filter(); | |
| 168 }); | |
| 169 document.addEventListener("keydown", event => { | |
| 170 const target = event.target; | |
| 171 const typing = target instanceof HTMLElement && | |
| 172 target.matches("input, textarea, select, [contenteditable]"); | |
| 173 if (event.key !== "/" || typing || event.metaKey || event.ctrlKey || | |
| 174 event.altKey) return; | |
| 175 event.preventDefault(); | |
| 176 input.focus(); | |
| 177 input.select(); | |
| 178 }); | |
| 179 filter(); | |
| 180 } | |
| 14 | 181 |
| 15 function showPage(pathname) { | 182 function showPage(pathname) { |
| 16 const pageName = pages.get(pathname) || "overview"; | 183 const pageName = pages.get(pathname) || "overview"; |
| 17 for (const page of document.querySelectorAll("[data-catalog-page]")) { | 184 for (const page of document.querySelectorAll("[data-catalog-page]")) { |
| 18 page.hidden = page.dataset.catalogPage !== pageName; | 185 page.hidden = page.dataset.catalogPage !== pageName; |
| 29 ); | 196 ); |
| 30 if (heading) document.title = `${heading.textContent} · Zenbu UI`; | 197 if (heading) document.title = `${heading.textContent} · Zenbu UI`; |
| 31 } | 198 } |
| 32 | 199 |
| 33 window.addEventListener("DOMContentLoaded", () => { | 200 window.addEventListener("DOMContentLoaded", () => { |
| 201 installComponentCatalog(); | |
| 202 installIconCatalog(); | |
| 203 installComponentSearch(); | |
| 34 const root = document.documentElement; | 204 const root = document.documentElement; |
| 35 const systemTheme = matchMedia("(prefers-color-scheme: dark)"); | 205 const systemTheme = matchMedia("(prefers-color-scheme: dark)"); |
| 36 const storedTheme = localStorage.getItem("zen-theme"); | 206 const storedTheme = localStorage.getItem("zen-theme"); |
| 37 if (storedTheme) root.dataset.zenTheme = storedTheme; | 207 if (storedTheme) root.dataset.zenTheme = storedTheme; |
| 38 | 208 |