Mercurial
view design_system/src/catalog.js @ 258:60a876c4587a
[ui] Add semantic primitive ownership
Build a layered Zenbu token and sizing system, make authored controls use native-underneath primitives, migrate mrjunejune without imposing visual surfaces, and document/enforce HTML ownership in the catalog and wiki.
Co-authored-by: Copilot <[email protected]>
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Wed, 05 Aug 2026 05:25:40 -0700 |
| parents | 2b6e732087ff |
| children |
line wrap: on
line source
import "./components/index.js"; import { componentCatalog } from "./component_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"], ["/components/field", "field"], ["/components/notifications", "notifications"], ["/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]")) { page.hidden = page.dataset.catalogPage !== pageName; } for (const link of document.querySelectorAll( ".catalog-sidebar [data-catalog-link]", )) { const active = new URL(link.href).pathname === pathname; if (active) link.setAttribute("aria-current", "page"); else link.removeAttribute("aria-current"); } const heading = document.querySelector( `[data-catalog-page="${pageName}"] h1`, ); if (heading) document.title = `${heading.textContent} ยท Zenbu UI`; } window.addEventListener("DOMContentLoaded", () => { installComponentCatalog(); installIconCatalog(); installComponentSearch(); const root = document.documentElement; const systemTheme = matchMedia("(prefers-color-scheme: dark)"); const storedTheme = localStorage.getItem("zen-theme"); if (storedTheme) root.dataset.zenTheme = storedTheme; const themeToggle = document.querySelector("#themeToggle"); const isDark = () => { if (root.dataset.zenTheme) { return root.dataset.zenTheme === "dark"; } return systemTheme.matches; }; const updateThemeButton = () => { const dark = isDark(); themeToggle?.setAttribute("aria-pressed", String(dark)); if (themeToggle) { themeToggle.textContent = dark ? "Use light theme" : "Use dark theme"; } }; updateThemeButton(); themeToggle?.addEventListener("click", () => { const next = isDark() ? "light" : "dark"; root.dataset.zenTheme = next; localStorage.setItem("zen-theme", next); updateThemeButton(); }); systemTheme.addEventListener("change", () => { if (!root.dataset.zenTheme) updateThemeButton(); }); document.addEventListener("click", event => { const notificationDemo = event.target.closest( "[data-notification-demo]", ); if (notificationDemo) { const scope = notificationDemo.closest("zen-notifications"); if (!scope) return; const tone = notificationDemo.dataset.tone || "info"; const baseId = notificationDemo.dataset.notificationId || tone; const count = notificationDemo.dataset.burst === "true" ? 5 : 1; for (let index = 0; index < count; index++) { notificationDemo.dispatchEvent(new CustomEvent("zen-notify", { bubbles: true, composed: false, detail: { version: 1, id: count > 1 ? `${baseId}-${Date.now()}-${index}` : baseId, tone, message: notificationDemo.dataset.message || "Notification", description: notificationDemo.dataset.description, announcement: tone === "error" ? "assertive" : "polite", durationMs: 5000, persistent: notificationDemo.dataset.persistent === "true", action: notificationDemo.dataset.action === "true" ? { token: `catalog-action-${Date.now()}`, label: "Retry" } : undefined, }, })); } return; } const dismissDemo = event.target.closest( "[data-notification-dismiss-demo]", ); if (dismissDemo) { dismissDemo.dispatchEvent(new CustomEvent( "zen-dismiss-notification", { bubbles: true, composed: false, detail: { version: 1, id: dismissDemo.dataset.notificationDismissDemo, }, }, )); return; } const link = event.target.closest("a[data-catalog-link]"); if (!link || link.origin !== location.origin || event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || link.hasAttribute("download") || link.target) return; event.preventDefault(); history.pushState({}, "", link.href); showPage(location.pathname); document.querySelector("#catalogMain")?.focus(); }); window.addEventListener("popstate", () => showPage(location.pathname)); showPage(location.pathname); });