view design_system/src/catalog.js @ 273:e02e2036ef84 default tip

add Layer 2 JRPG component system Add reusable content and window modals, an isolated component sandbox, shared cyberpunk scroll areas, production-safe cache freshness, and server-rendered JRPG panel state. Co-authored-by: Copilot <[email protected]>
author MrJuneJune <me@mrjunejune.com>
date Sat, 08 Aug 2026 02:08:08 -0700
parents 60a876c4587a
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);
});