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