Files
WRNexusJS/examples/component-showcase/scripts/generate-showcase.mjs
T
2026-07-31 16:30:13 +05:30

2014 lines
80 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import process from "node:process";
import { fileURLToPath } from "node:url";
import { eventDescriptionFor, profileFor } from "./showcase-profiles.mjs";
const exampleRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
const workspaceRoot = join(exampleRoot, "..", "..");
const referencePath = join(workspaceRoot, "packages", "ui", "component-reference.json");
const uiComponentsPath = join(workspaceRoot, "packages", "ui", "components");
const pagesDir = join(exampleRoot, "app", "pages");
const detailPagesDir = join(pagesDir, "components");
const layoutsDir = join(exampleRoot, "app", "layouts");
const reference = JSON.parse(readFileSync(referencePath, "utf8"));
const interactiveOverlayComponents = new Set([
"ContextMenu",
"Drawer",
"Dropdown",
"Modal",
"Popover",
"Tooltip",
]);
const overlayVisibilityProps = new Set(["open", "defaultopen", "visible"]);
function isInteractiveOverlay(component) {
return interactiveOverlayComponents.has(component.name);
}
function overlayInteractionLabel(component) {
switch (component.name) {
case "ContextMenu":
return "Right-click or use the trigger button";
case "Tooltip":
return "Hover, focus, or click the trigger";
case "Drawer":
return "Click the trigger to open the drawer";
case "Modal":
return "Click the trigger to open the modal";
case "Dropdown":
return "Click the trigger to open the menu";
case "Popover":
return "Click the trigger to open the popover";
default:
return "Interact with the trigger";
}
}
const displayName = (name) => name.replace(/([a-z0-9])([A-Z])/g, "$1 $2");
const baseSlugOf = (name) => name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
const slugCounts = new Map();
for (const component of reference.components) {
const slug = baseSlugOf(component.name);
slugCounts.set(slug, (slugCounts.get(slug) ?? 0) + 1);
}
const slugOf = (name) => {
const slug = baseSlugOf(name);
if (slugCounts.get(slug) === 1) return slug;
const component = reference.components.find((entry) => entry.name === name);
return `${slug}-${component?.category ?? "component"}`;
};
const titleCase = (value) =>
value
.split("-")
.map((word) => word[0].toUpperCase() + word.slice(1))
.join(" ");
const identifier = (value) =>
value.replace(/(^|[^A-Za-z0-9_])([A-Za-z0-9])/g, (_, _sep, char) => char.toUpperCase());
const escapeText = (value) =>
String(value).replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
// WRN uses braces for reactive expressions. Encode braces inside documentation
// code so hydration keeps them as literal, copyable source text.
const escapeCode = (value) => escapeText(value).replaceAll("{", "&#123;").replaceAll("}", "&#125;");
const escapeAttribute = (value) =>
escapeText(value).replaceAll('"', "&quot;").replaceAll("'", "&#39;");
const jsonAttribute = (value) => escapeAttribute(JSON.stringify(value));
function sampleItem(component, index, variation) {
const number = index + 1;
const labels = ["Primary workflow", "Secondary workflow", "Advanced workflow"];
return {
id: `${slugOf(component.name)}-${variation}-${number}`,
key: `${slugOf(component.name)}-${variation}-${number}`,
value: index === 0 ? "primary" : "secondary",
label: index === 0 ? labels[variation] : "Additional option",
title: index === 0 ? labels[variation] : "Supporting example",
description:
variation === 0
? "A clean default configuration for everyday product interfaces."
: variation === 1
? "A compact configuration designed for dense application layouts."
: "A richer configuration with more supporting information and actions.",
text: index === 0 ? "A configurable sample message." : "Another configurable message.",
href: `#${slugOf(component.name)}-demo`,
actionHref: `#${slugOf(component.name)}-demo`,
actionLabel: variation === 2 ? "Explore workflow" : "View details",
icon:
variation === 0
? "icon-[lucide--sparkles]"
: variation === 1
? "icon-[lucide--zap]"
: "icon-[lucide--circle-check]",
iconClass:
variation === 0
? "icon-[lucide--sparkles]"
: variation === 1
? "icon-[lucide--zap]"
: "icon-[lucide--circle-check]",
variant: variation === 0 ? "primary" : variation === 1 ? "secondary" : "success",
status: variation === 2 ? "Complete" : "Active",
time: variation === 0 ? "09:30" : variation === 1 ? "10:15" : "11:45",
current: index === 0,
selected: index === 0,
checked: index === 0,
disabled: variation === 2 && index === 1,
outgoing: index === 1,
open: true,
number,
count: number * (variation + 2) * 8,
percentage: variation === 0 ? 72 : variation === 1 ? 48 : 91,
color: variation === 0 ? "#7c3aed" : variation === 1 ? "#0284c7" : "#059669",
target: "_self",
ariaLabel: `Open ${labels[variation].toLowerCase()} ${number}`,
items: [
{ label: "Nested option A", value: "nested-a" },
{ label: "Nested option B", value: "nested-b" },
],
links: [
{ label: "Documentation", href: "#documentation" },
{ label: "API reference", href: "#api-reference" },
],
values: ["Included", variation === 2 ? "Unlimited" : "Standard"],
};
}
function sampleArray(prop, component, variation) {
const name = prop.name.toLowerCase();
if (name.includes("chartlabel") || name === "labels")
return variation === 1 ? ["Q1", "Q2", "Q3", "Q4"] : ["Mon", "Tue", "Wed", "Thu", "Fri"];
if (name === "data" || name.includes("series"))
return variation === 0
? [18, 32, 27, 48, 64]
: variation === 1
? [64, 51, 43, 35]
: [24, 46, 72, 88];
if (name === "pages")
return [
{ number: 1, href: "#page-1", current: variation === 0, ellipsis: false },
{ number: 2, href: "#page-2", current: variation !== 0, ellipsis: false },
{ number: 3, href: "#page-3", current: false, ellipsis: false },
];
if (name === "plans")
return [
{ name: "Starter", label: "Starter", value: "starter" },
{ name: "Pro", label: "Pro", value: "pro" },
{ name: "Scale", label: "Scale", value: "scale" },
];
if (name === "features")
return [
{ label: "Team members", values: ["5", "Unlimited", "Unlimited"] },
{ label: "Support", values: ["Email", "Priority", "Dedicated"] },
{ label: "Audit history", values: ["7 days", "90 days", "Custom"] },
];
if (name === "logos")
return ["Acme", "Northstar", "Vertex", "Orbit"].map((logo, index) => ({
name: logo,
alt: `${logo} logo`,
href: `#${slugOf(component.name)}-demo`,
external: false,
src: `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 36'%3E%3Crect width='120' height='36' rx='8' fill='%23${variation === 2 ? "059669" : variation === 1 ? "0284c7" : "7c3aed"}'/%3E%3Ctext x='60' y='23' text-anchor='middle' font-family='Arial' font-size='13' fill='white'%3E${logo}%3C/text%3E%3C/svg%3E`,
order: index,
}));
if (name.includes("navigationcolumns"))
return [
{
title: "Product",
links: [
{ label: "Overview", href: "#overview" },
{ label: "Pricing", href: "#pricing" },
],
},
{
title: "Resources",
links: [
{ label: "Guides", href: "#guides" },
{ label: "Support", href: "#support" },
],
},
];
if (name === "sections")
return [
{
label: "Product",
value: "product",
description: "Product navigation examples.",
items: [
{ label: "Platform", href: "#platform", description: "Explore the platform." },
{ label: "Security", href: "#security", description: "Review security controls." },
],
},
{
label: "Developers",
value: "developers",
description: "Developer resources.",
items: [{ label: "Documentation", href: "#docs", description: "Read the guides." }],
},
];
if (name === "toasts")
return [
{
title: variation === 2 ? "Deployment complete" : "Changes saved",
description:
variation === 2
? "The production release completed successfully."
: "Your settings were saved successfully.",
variant: "success",
icon: "icon-[lucide--circle-check]",
open: true,
dismissible: true,
dismissLabel: "Dismiss notification",
},
];
if (name === "stages")
return [
{
title: "Event received",
description: "A customer event started the flow.",
icon: "icon-[lucide--webhook]",
},
{
title: "Delivery",
description: "Eligible channels are selected.",
icon: "icon-[lucide--send]",
items: [{ label: "Email" }, { label: "SMS" }],
},
];
return [sampleItem(component, 0, variation), sampleItem(component, 1, variation)];
}
function defaultValue(prop) {
if (prop.default === null) return undefined;
if (prop.default === "true") return true;
if (prop.default === "false") return false;
if (/^-?\d+(?:\.\d+)?$/.test(prop.default ?? "")) return Number(prop.default);
if (/^".*"$/.test(prop.default ?? "")) {
try {
return JSON.parse(prop.default);
} catch {
return undefined;
}
}
return undefined;
}
function sampleValue(prop, component, variation) {
const name = prop.name.toLowerCase();
if (component.name === "AdvancedSelect") {
const configuration = advancedSelectConfiguration(variation);
if (Object.hasOwn(configuration, prop.name)) return configuration[prop.name];
return undefined;
}
if (component.name === "ComboBox") {
const configuration = comboboxConfiguration(variation);
if (Object.hasOwn(configuration, prop.name)) return configuration[prop.name];
return undefined;
}
if (component.name === "PinInput") {
const configuration = pinInputConfiguration(variation);
if (Object.hasOwn(configuration, prop.name)) return configuration[prop.name];
return undefined;
}
if (component.name === "StrongPassword") {
const configuration = strongPasswordConfiguration(variation);
if (Object.hasOwn(configuration, prop.name)) return configuration[prop.name];
return undefined;
}
if (component.name === "TogglePassword") {
const configuration = togglePasswordConfiguration(variation);
if (Object.hasOwn(configuration, prop.name)) return configuration[prop.name];
return undefined;
}
if (component.name === "InputNumber") {
const configuration = inputNumberConfiguration(variation);
if (Object.hasOwn(configuration, prop.name)) {
return configuration[prop.name];
}
return undefined;
}
const isArray = prop.default === "[]" || prop.type.includes("[]") || prop.type === "array";
const isObject = prop.default === "{}" || prop.type === "object";
if (isArray) return sampleArray(prop, component, variation);
if (isObject) return sampleItem(component, 0, variation);
if (name === "class") return `showcase-instance showcase-instance--${variation + 1}`;
if (component.name === "Button") {
const configurations = [
{
label: "Save & Submit",
as: "button",
type: "submit",
},
{
label: "Learn more",
as: "a",
href: "#button-demo",
variant: "outline",
size: "sm",
icon: "icon-[lucide--arrow-right]",
iconPosition: "end",
},
{ label: "Upgrade plan", as: "button", variant: "secondary", size: "lg", pill: true },
{
label: "Open settings",
as: "button",
ariaLabel: "Open settings",
variant: "ghost",
size: "icon",
icon: "icon-[lucide--settings]",
},
{
label: "Delete project",
as: "button",
variant: "destructive",
icon: "icon-[lucide--trash-2]",
},
{ label: "Read documentation", as: "a", href: "#button-demo", variant: "link" },
{ label: "Publish", as: "button", loadingLabel: "Publishing…", loading: true },
{ label: "Unavailable action", as: "button", disabled: true, variant: "outline" },
];
const configuration = configurations[variation] ?? configurations[0];
if (Object.hasOwn(configuration, prop.name)) return configuration[prop.name];
return undefined;
}
if (name === "controlclass") return variation === 2 ? "shadow-xl" : "";
if (prop.type === "boolean") {
// Interactive overlays must render closed. A hard-coded open=true prop turns
// the component into controlled mode, so its own click/hover handlers cannot
// close it and multiple overlays block the complete showcase page.
if (isInteractiveOverlay(component) && overlayVisibilityProps.has(name)) return false;
if (name === "compact") return variation === 1;
if (name === "fullwidth") return variation === 2;
if (name === "loading" || name === "disabled") return false;
if (name === "open" || name === "visible" || name === "dismissible" || name.startsWith("show"))
return true;
return defaultValue(prop) ?? variation === 2;
}
if (prop.type === "number") {
if (name === "value") return 36 + variation * 24;
if (name.includes("count") || name.includes("total") || name.includes("page"))
return 3 + variation * 4;
return defaultValue(prop) ?? variation + 1;
}
if (name.includes("arialabel")) return `${displayName(component.name)} ${variation + 1} example`;
if (name === "title" || name.endsWith("title"))
return variation === 0
? `${displayName(component.name)} example`
: variation === 1
? `Compact ${displayName(component.name)}`
: `Advanced ${displayName(component.name)}`;
if (name.includes("description") || name === "summary")
return variation === 0
? `A polished default ${displayName(component.name).toLowerCase()} for a modern application.`
: variation === 1
? "A compact configuration for dashboards and dense product surfaces."
: "A richer configuration demonstrating additional content and hierarchy.";
if (name === "label" || name.endsWith("label"))
return variation === 0
? displayName(component.name)
: variation === 1
? "Compact example"
: "Advanced example";
if (name.includes("placeholder"))
return variation === 2 ? "Search by name, email, or identifier" : "Enter a sample value";
if (name.includes("email")) return variation === 2 ? "team@example.com" : "developer@example.com";
if (name.includes("href") || name === "action") {
if (component.name === "Button" && variation === 0) return "";
return `#${slugOf(component.name)}-demo`;
}
if (name === "icon")
return variation === 0
? ""
: variation === 1
? "icon-[lucide--arrow-right]"
: "icon-[lucide--trash-2]";
if (name === "variant")
if (component.name === "Button")
return variation === 0 ? "primary" : variation === 1 ? "secondary" : "danger";
if (name === "variant")
return variation === 0
? defaultValue(prop) || "primary"
: variation === 1
? "secondary"
: "success";
if (name === "size")
return variation === 0 ? defaultValue(prop) || "md" : variation === 1 ? "sm" : "lg";
if (name === "align") return variation === 1 ? "center" : variation === 2 ? "end" : "start";
if (name === "value") return `sample-${variation + 1}`;
if (name.includes("count") || name.includes("total") || name.includes("page")) return undefined;
if (prop.required) {
return `${displayName(component.name)} sample`;
}
return undefined;
}
function wrnAttribute(name, value) {
if (typeof value === "object" && value !== null) {
const expression = JSON.stringify(value).replaceAll("'", "\\u0027");
return `${name}='${expression}'`;
}
return `${name}="${escapeAttribute(String(value))}"`;
}
function componentAttributes(component, variation, overrides = {}) {
const attributes = [];
for (const prop of component.props) {
const value = Object.hasOwn(overrides, prop.name)
? overrides[prop.name]
: sampleValue(prop, component, variation);
if (value === undefined) continue;
attributes.push(wrnAttribute(prop.name, value));
}
return attributes;
}
function configuredSlotMarkup(component, variation, overrides = {}) {
const configured = overrides.__slots ?? {};
return component.slots
.map((slot) => {
const explicitlyConfigured = Object.hasOwn(configured, slot);
if (slot !== "default" && !explicitlyConfigured) return "";
const custom = configured[slot];
const content = custom ?? (slot === "default" ? defaultSlotMarkup(component, variation) : "");
if (!content) return "";
return slot === "default"
? content
: `<div data-slot="${escapeAttribute(slot)}">${content}</div>`;
})
.join("");
}
function componentMount(component, variation = 0, overrides = {}) {
const attributes = componentAttributes(component, variation, overrides);
const slotMarkup = configuredSlotMarkup(component, variation, overrides);
const publicTag = /^[A-Z][A-Za-z0-9_]*$/.test(component.mount);
if (publicTag) {
const attributesSource = attributes.length ? ` ${attributes.join(" ")}` : "";
return slotMarkup
? `<${component.mount}${attributesSource}>${slotMarkup}</${component.mount}>`
: `<${component.mount}${attributesSource} />`;
}
const attributesSource = attributes.length ? ` ${attributes.join(" ")}` : "";
return `<div data-component="${escapeAttribute(component.mount)}"${attributesSource}>${slotMarkup}</div>`;
}
const structuralDefaultSlots = new Set([
"Accordion",
"AspectRatio",
"AutoGrid",
"card",
"Cluster",
"CodeTabs",
"Container",
"ContentSection",
"FilterBar",
"ScrollArea",
"Section",
"SidebarLayout",
"SplitLayout",
"Stack",
"Sticky",
"StickyLayout",
"Surface",
"Typography",
"VisuallyHidden",
"Well",
"WideContainer",
]);
function defaultSlotMarkup(component, variation) {
if (component.name === "Select")
return '<option value="starter">Starter</option><option value="growth">Growth</option>';
if (component.name === "Table")
return "<thead><tr><th>Name</th><th>Status</th></tr></thead><tbody><tr><td>Production</td><td>Active</td></tr><tr><td>Staging</td><td>Ready</td></tr></tbody>";
if (component.name === "Tooltip") return "";
if (!structuralDefaultSlots.has(component.name)) return "";
return `<div class="showcase-slot"><span class="icon-[lucide--layers-3] size-4" aria-hidden="true"></span><span>${variation === 2 ? "Rich supporting content with additional context." : "Composable content area."}</span></div>`;
}
function indentSource(value, spaces = 2) {
const indent = " ".repeat(spaces);
return String(value)
.trim()
.split("\n")
.map((line) => `${indent}${line}`)
.join("\n");
}
function sourceSnippet(component, variation, overrides = {}) {
const attributes = component.props.flatMap((prop) => {
const value = Object.hasOwn(overrides, prop.name)
? overrides[prop.name]
: sampleValue(prop, component, variation);
if (value === undefined || value === "" || prop.name === "class") return [];
const declaredDefault = defaultValue(prop);
if (
declaredDefault !== undefined &&
typeof value !== "object" &&
String(value) === String(declaredDefault)
)
return [];
if (typeof value === "object") {
const serialized = JSON.stringify(value, null, 2)
.replaceAll("'", "\\u0027")
.replaceAll("\n", "\n ");
return [`${prop.name}='${serialized}'`];
}
return [`${prop.name}="${String(value).replaceAll('"', '\\"')}"`];
});
const formattedAttributes = attributes.length ? `\n ${attributes.join("\n ")}` : "";
const hasPublicComponentTag = /^[A-Z][A-Za-z0-9_]*$/.test(component.mount);
const configured = overrides.__slots ?? {};
const slotParts = component.slots.flatMap((slot) => {
const custom = configured[slot];
const fallback =
slot === "default"
? defaultSlotMarkup(component, variation)
: `<div class="showcase-slot">${displayName(slot)} slot</div>`;
const content = custom ?? fallback;
if (!content) return [];
if (slot === "default") return [indentSource(content, 2)];
return [
` <div data-slot="${slot}">\n${indentSource(content, 4)}\n </div>`,
];
});
const slotMarkup = slotParts.length ? `\n${slotParts.join("\n")}\n` : "";
if (hasPublicComponentTag) {
const source = slotMarkup
? `<${component.mount}${formattedAttributes}>${slotMarkup}</${component.mount}>`
: formattedAttributes
? `<${component.mount}${formattedAttributes}\n/>`
: `<${component.mount} />`;
return escapeCode(source);
}
const mountAttribute = `data-component="${component.mount}"`;
const source = slotMarkup
? `<div\n ${mountAttribute}${formattedAttributes}>${slotMarkup}</div>`
: `<div\n ${mountAttribute}${formattedAttributes}\n></div>`;
return escapeCode(source);
}
function propsTable(component) {
if (component.props.length === 0)
return `<div class="docs-empty"><span class="icon-[lucide--package-open] size-5" aria-hidden="true"></span>No props. Compose this component with slots.</div>`;
return `<div class="docs-table-wrap"><table class="docs-table"><thead><tr><th>Prop</th><th>Type</th><th>Default</th><th>Required</th></tr></thead><tbody>${component.props
.map(
(prop) =>
`<tr><td><code>${escapeCode(prop.name)}</code></td><td>${escapeText(prop.type)}</td><td><code>${escapeCode(prop.default ?? "—")}</code></td><td>${prop.required ? '<span class="docs-required">Yes</span>' : "No"}</td></tr>`,
)
.join("")}</tbody></table></div>`;
}
function playgroundInitialValue(prop, component) {
const profileValue = profileFor(component.name)?.playground?.[prop.name];
if (profileValue !== undefined) return profileValue;
const sampled = sampleValue(prop, component, 0);
return sampled === undefined ? (defaultValue(prop) ?? "") : sampled;
}
function playgroundValue(prop, component) {
const value = playgroundInitialValue(prop, component);
return typeof value === "object" ? JSON.stringify(value, null, 2) : String(value);
}
function playgroundStateName(prop) {
return `playground_${prop.name.replace(/[^A-Za-z0-9_$]/g, "_")}`;
}
function isStructuredProp(prop) {
return (
prop.default === "[]" ||
prop.default === "{}" ||
prop.type === "array" ||
prop.type === "object" ||
prop.type.includes("[]")
);
}
function playgroundStates(component) {
return component.props
.map((prop) => {
const initialValue = playgroundInitialValue(prop, component);
const query = `ctx.url.searchParams.get("pg_${prop.name}")`;
if (prop.type === "boolean") {
const initial = initialValue === true ? "true" : "false";
return ` state ${playgroundStateName(prop)} = (${query} ?? "${initial}") === "true"`;
}
if (prop.type === "number") {
const initial = Number(initialValue || 0);
return ` state ${playgroundStateName(prop)} = Number(${query} ?? ${JSON.stringify(String(initial))})`;
}
if (isStructuredProp(prop)) {
const initial = JSON.stringify(initialValue ?? (prop.default === "{}" ? {} : []));
return ` state ${playgroundStateName(prop)} = JSON.parse(${query} ?? ${JSON.stringify(initial)})`;
}
return ` state ${playgroundStateName(prop)} = ${query} ?? ${JSON.stringify(String(initialValue ?? ""))}`;
})
.join("\n");
}
function playgroundMount(component) {
const attributes = component.props.map(
(prop) => `${prop.name}='{${playgroundStateName(prop)}}'`,
);
const profileSlots = profileFor(component.name)?.demos?.[0]?.slots ?? {};
const slotMarkup = configuredSlotMarkup(component, 0, { __slots: profileSlots });
const publicTag = /^[A-Z][A-Za-z0-9_]*$/.test(component.mount);
if (publicTag) {
const attributesSource = attributes.length ? ` ${attributes.join(" ")}` : "";
return slotMarkup
? `<${component.mount}${attributesSource}>${slotMarkup}</${component.mount}>`
: `<${component.mount}${attributesSource} />`;
}
const attributesSource = attributes.length ? ` ${attributes.join(" ")}` : "";
return `<div data-component="${escapeAttribute(component.mount)}"${attributesSource}>${slotMarkup}</div>`;
}
function playgroundControl(prop, component) {
const value = playgroundValue(prop, component);
const id = `playground-${slugOf(component.name)}-${prop.name}`;
const label = escapeText(displayName(prop.name));
const meta = `<small>${escapeText(prop.type)}${prop.required ? " · required" : ""}</small>`;
if (prop.type === "boolean") {
return `<label class="playground-toggle" for="${id}"><span><strong>${label}</strong>${meta}</span><input id="${id}" name="pg_${escapeAttribute(prop.name)}" type="checkbox" value="true"${value === "true" ? " checked" : ""} data-playground-boolean /><i aria-hidden="true"></i></label>`;
}
const structured =
prop.default === "[]" ||
prop.default === "{}" ||
prop.type === "array" ||
prop.type === "object" ||
prop.type.includes("[]");
if (structured) {
return `<label class="playground-field" for="${id}"><span><strong>${label}</strong>${meta}</span><textarea id="${id}" name="pg_${escapeAttribute(prop.name)}" rows="5" spellcheck="false" data-playground-json>${escapeText(value)}</textarea><em data-playground-error></em></label>`;
}
const options = playgroundOptions(prop, component, value);
if (options.length > 1) {
return `<label class="playground-field" for="${id}"><span><strong>${label}</strong>${meta}</span><select id="${id}" name="pg_${escapeAttribute(prop.name)}">${options
.map(
(option) =>
`<option value="${escapeAttribute(option)}"${option === value ? " selected" : ""}>${escapeText(option || "Auto / default")}</option>`,
)
.join("")}</select></label>`;
}
const inputType =
prop.type === "number"
? "number"
: prop.name.toLowerCase().includes("color") && /^#[0-9a-f]{6}$/i.test(value)
? "color"
: "text";
return `<label class="playground-field" for="${id}"><span><strong>${label}</strong>${meta}</span><input id="${id}" name="pg_${escapeAttribute(prop.name)}" type="${inputType}" value="${escapeAttribute(value)}" /></label>`;
}
const commonPlaygroundOptions = {
align: ["left", "center", "right", "start", "end", "stretch"],
alignment: ["left", "center", "right", "start", "end", "stretch"],
as: ["", "button", "a"],
color: ["primary", "secondary", "success", "warning", "danger", "info"],
direction: ["row", "column"],
gap: ["none", "xs", "sm", "md", "lg", "xl", "2xl"],
iconposition: ["start", "end", "left", "right"],
imageposition: ["top", "left", "right"],
layout: ["content", "split", "stacked", "balanced", "visual"],
maxwidth: ["compact", "lg", "xl", "wide", "2xl", "full"],
method: ["get", "post"],
orientation: ["horizontal", "vertical"],
placement: ["top", "top-start", "top-end", "right", "right-start", "right-end", "bottom", "bottom-start", "bottom-end", "left", "left-start", "left-end"],
position: ["top", "right", "bottom", "left", "start", "end", "center"],
shape: ["round", "rounded", "pill"],
size: ["default", "xs", "sm", "md", "lg", "xl", "icon", "icon-xs", "icon-sm", "icon-lg"],
target: ["_self", "_blank", "_parent", "_top"],
trigger: ["click", "hover", "focus", "manual"],
visualposition: ["left", "right"],
width: ["default", "compact", "wide", "full"],
};
function playgroundOptions(prop, component, currentValue) {
if (prop.type !== "string" || prop.name === "class" || prop.name === "controlClass") return [];
const profileOptions = profileFor(component.name)?.options?.[prop.name] ?? [];
const common = commonPlaygroundOptions[prop.name.toLowerCase()] ?? [];
const componentSpecific =
component.name === "Button" && prop.name === "type"
? ["button", "submit", "reset"]
: [];
const declared = [...profileOptions, ...componentSpecific, ...common];
// Free-form strings such as icon names, labels, URLs, IDs, and descriptions
// must remain text inputs. Implementation comparisons are not a public enum.
if (declared.length === 0) return [];
const values = new Set();
if (typeof currentValue === "string" && currentValue.length <= 120) values.add(currentValue);
const declaredDefault = defaultValue(prop);
if (typeof declaredDefault === "string" && declaredDefault.length <= 120)
values.add(declaredDefault);
for (const option of declared) {
if (typeof option === "string" && option.length <= 120) values.add(option);
}
return [...values];
}
function playgroundSection(component) {
const controls = component.props.map((prop) => playgroundControl(prop, component)).join("");
const publicTag = /^[A-Z][A-Za-z0-9_]*$/.test(component.mount);
const eventNames = component.events.join(",");
return `<section id="playground" class="detail-section playground-section" data-playground data-playground-component="${escapeAttribute(component.mount)}" data-playground-public-tag="${publicTag}" data-playground-has-slot="${component.slots.length > 0}" data-playground-events="${escapeAttribute(eventNames)}">
<div class="detail-section-heading"><span>Interactive playground</span><h2>Configure ${escapeText(displayName(component.name))}</h2><p>Change any prop and inspect the server-rendered component immediately.</p></div>
<div class="playground-workbench">
<div class="playground-preview">
<div class="demo-browser-chrome"><span></span><span></span><span></span><small>Live preview</small></div>
<div class="playground-preview-source" data-playground-preview>${playgroundMount(component)}</div>
<section class="playground-code" aria-label="Current component code">
<header><span><span class="icon-[lucide--code-2] size-4" aria-hidden="true"></span>Component code</span><button type="button" data-playground-copy><span class="icon-[lucide--copy] size-4" aria-hidden="true"></span>Copy</button></header>
<pre><code data-playground-code>${sourceSnippet(component, 0)}</code></pre>
</section>
<div class="playground-status" data-playground-status aria-live="polite"></div>
${component.events.length ? `<div class="playground-event-log" data-playground-event-log aria-live="polite"><strong>Event output</strong><span>Interact with the preview to inspect <code>event.detail</code>.</span></div>` : ""}
</div>
<form class="playground-controls" data-playground-form>
<header><div><span>Component props</span><strong>${component.props.length} controls</strong></div><button type="reset"><span class="icon-[lucide--rotate-ccw] size-4" aria-hidden="true"></span>Reset</button></header>
<div class="playground-fields">${controls || '<p class="playground-empty">This component has no declared props. Its slot content remains composable.</p>'}</div>
</form>
</div>
</section>`;
}
const defaultUses = [
{
eyebrow: "Recommended",
title: "Production default",
description: "Balanced spacing, hierarchy, and content for the most common product workflow.",
},
{
eyebrow: "Dense UI",
title: "Compact application",
description:
"A tighter variation for dashboards, side panels, tables, and operational interfaces.",
},
{
eyebrow: "Extended",
title: "Rich configuration",
description:
"A more expressive variation using additional data, stronger emphasis, and optional states.",
},
];
const buttonUses = [
{
eyebrow: "Default",
title: "Primary action",
description: "The standard high-emphasis action for forms and product workflows.",
},
{
eyebrow: "Outline · Small",
title: "Secondary navigation",
description: "A compact link button with an end icon and restrained emphasis.",
},
{
eyebrow: "Secondary · Large · Pill",
title: "Prominent rounded action",
description: "A large secondary action using the optional pill shape.",
},
{
eyebrow: "Ghost · Icon",
title: "Accessible icon action",
description: "An icon-only control with its accessible name supplied through props.",
},
{
eyebrow: "Destructive",
title: "Dangerous action",
description: "A destructive treatment for irreversible or high-risk operations.",
},
{
eyebrow: "Link",
title: "Inline action",
description: "A low-chrome action that behaves and reads like an inline link.",
},
{
eyebrow: "Loading",
title: "Pending action",
description: "The button owns its busy state, loading label, spinner, and disabled behavior.",
},
{
eyebrow: "Disabled",
title: "Unavailable action",
description: "A visibly and semantically disabled control.",
},
];
const advancedSelectTitles = [
"Default advanced select",
"Grouped options",
"Fixed-position dropdown",
"Placeholder with icon",
"Allow empty selection",
"Multiple selection",
"Multiple selection with optgroups",
"Multiple selection with option templates",
"Multiple selection with counter",
"Multiple selection with conditional counter",
"Counter with option templates",
"Searchable dropdown",
"Minimum search length",
"Search result limit",
"Scroll to selected option",
"Search match mode",
"Search labels and descriptions",
"Tag-style selection",
"Disabled tag selection",
"Option template with icons",
"Option template with avatars",
"Option template with color indicators",
"Advanced select sizes",
"Disabled advanced select",
"Inside a modal",
"Validation states",
"Dynamic validation styling",
"Add or remove options",
"Add or remove options in multiple mode",
"Set a single value programmatically",
"Set multiple values programmatically",
"Remote data source",
"Remote data source (multiple)",
"Remote data tags",
"Option templates with remote data",
"Conditional counter with remote data",
"Avatar template with remote data",
"Preselected values",
"Infinite scroll",
"Inside overflow-hidden containers",
"Destroy and reinitialize",
];
const advancedSelectUses = advancedSelectTitles.map((title, index) => ({
eyebrow: index < 11 ? "Selection" : index < 23 ? "Search & templates" : "Advanced behavior",
title,
description: `Live ${title.toLowerCase()} configuration with copyable component code.`,
}));
function advancedSelectConfiguration(variation) {
const title = advancedSelectTitles[variation] ?? advancedSelectTitles[0];
const lower = title.toLowerCase();
const options = [
{
value: "design",
label: "Design",
description: "Product and visual design",
icon: "icon-[lucide--palette]",
color: "#8b5cf6",
avatar:
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='32' fill='%238b5cf6'/%3E%3Ctext x='32' y='40' text-anchor='middle' font-family='Arial' font-size='24' fill='white'%3ED%3C/text%3E%3C/svg%3E",
},
{
value: "engineering",
label: "Engineering",
description: "Platform and application engineering",
icon: "icon-[lucide--code-2]",
color: "#0ea5e9",
avatar:
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='32' fill='%230ea5e9'/%3E%3Ctext x='32' y='40' text-anchor='middle' font-family='Arial' font-size='24' fill='white'%3EE%3C/text%3E%3C/svg%3E",
},
{
value: "growth",
label: "Growth",
description: "Marketing and customer growth",
icon: "icon-[lucide--trending-up]",
color: "#10b981",
avatar:
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='32' fill='%2310b981'/%3E%3Ctext x='32' y='40' text-anchor='middle' font-family='Arial' font-size='24' fill='white'%3EG%3C/text%3E%3C/svg%3E",
},
{ value: "support", label: "Support", description: "Customer operations", disabled: false },
];
const multiple = lower.includes("multiple") || lower.includes("tags");
const remote = lower.includes("remote") || lower.includes("infinite");
const grouped = lower.includes("group");
return {
label: title,
name: `advanced-select-${variation + 1}`,
value:
multiple || lower.includes("placeholder")
? ""
: lower.includes("preselected")
? "engineering"
: "design",
values: multiple ? ["design", "engineering"] : [],
options: grouped ? [] : options,
groups: grouped
? [
{ label: "Product", options: options.slice(0, 2) },
{ label: "Business", options: options.slice(2) },
]
: [],
placeholder: lower.includes("placeholder") ? "Choose a workspace" : "Choose a team",
placeholderIcon: lower.includes("placeholder") ? "icon-[lucide--building-2]" : "",
multiple,
searchable: lower.includes("search") || remote || variation === 0,
allowEmpty: lower.includes("empty"),
tags: lower.includes("tag"),
disabled: lower.includes("disabled"),
required: lower.includes("validation"),
invalid: false,
validationMessage: "",
minSearchLength: lower.includes("minimum") ? 3 : remote && !lower.includes("infinite") ? 2 : 0,
searchResultLimit: lower.includes("result limit") ? 3 : 0,
searchMode: lower.includes("match mode") ? "startsWith" : "contains",
showCounter: lower.includes("counter"),
maxSelections: lower.includes("conditional") ? 3 : 0,
optionTemplate: lower.includes("avatar")
? "avatar"
: lower.includes("color")
? "color"
: lower.includes("icon") || lower.includes("template")
? "icon"
: "default",
fixed: lower.includes("fixed") || lower.includes("overflow"),
remote,
remoteUrl: remote ? "/api/teams" : "",
infinite: lower.includes("infinite"),
hasMore: lower.includes("infinite"),
size: lower.includes("sizes") ? "lg" : "default",
};
}
const comboboxTitles = [
"Default autocomplete",
"Local HTML options",
"Grouped suggestions",
"Minimum search length",
"Search result limit",
"Starts-with matching",
"Search labels and descriptions",
"Suggestions with icons",
"Suggestions with avatars",
"Suggestions with color indicators",
"Clearable value",
"Custom text values",
"Preselected value",
"Disabled combobox",
"Validation state",
"Remote data with automatic preload",
"Remote search API",
"Remote query parameters",
"Infinite remote suggestions",
"Fixed-position suggestions",
"JavaScript methods",
];
const comboboxUses = comboboxTitles.map((title, index) => ({
eyebrow: index < 7 ? "Autocomplete" : index < 15 ? "Templates & states" : "Remote & API",
title,
description: `Live ${title.toLowerCase()} combobox with editable autocomplete and copyable component code.`,
}));
function comboboxConfiguration(variation) {
const title = comboboxTitles[variation] ?? comboboxTitles[0];
const lower = title.toLowerCase();
const options = [
{
value: "design",
label: "Design",
description: "Product and visual design",
icon: "icon-[lucide--palette]",
color: "#8b5cf6",
avatar:
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='32' fill='%238b5cf6'/%3E%3Ctext x='32' y='40' text-anchor='middle' font-family='Arial' font-size='24' fill='white'%3ED%3C/text%3E%3C/svg%3E",
},
{
value: "engineering",
label: "Engineering",
description: "Platform and application engineering",
icon: "icon-[lucide--code-2]",
color: "#0ea5e9",
avatar:
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='32' fill='%230ea5e9'/%3E%3Ctext x='32' y='40' text-anchor='middle' font-family='Arial' font-size='24' fill='white'%3EE%3C/text%3E%3C/svg%3E",
},
{
value: "growth",
label: "Growth",
description: "Marketing and customer growth",
icon: "icon-[lucide--trending-up]",
color: "#10b981",
avatar:
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='32' fill='%2310b981'/%3E%3Ctext x='32' y='40' text-anchor='middle' font-family='Arial' font-size='24' fill='white'%3EG%3C/text%3E%3C/svg%3E",
},
{
value: "support",
label: "Support",
description: "Customer operations",
icon: "icon-[lucide--life-buoy]",
color: "#f59e0b",
},
];
const remote =
lower.includes("remote") || lower.includes("infinite") || lower.includes("query parameter");
const grouped = lower.includes("grouped");
return {
label: title,
name: `combobox-${variation + 1}`,
value: lower.includes("preselected") ? "engineering" : "",
options: grouped ? [] : options,
groups: grouped
? [
{ label: "Product", options: options.slice(0, 2) },
{ label: "Business", options: options.slice(2) },
]
: [],
placeholder: remote ? "Search remote teams" : "Search teams",
allowCustomValue: lower.includes("custom"),
clearable: true,
disabled: lower.includes("disabled"),
required: lower.includes("validation"),
invalid: false,
validationMessage: "",
minSearchLength: lower.includes("minimum") ? 3 : remote ? 2 : 0,
searchResultLimit: lower.includes("result limit") ? 2 : 0,
searchMode: lower.includes("starts-with") ? "startsWith" : "contains",
optionTemplate: lower.includes("avatar")
? "avatar"
: lower.includes("color")
? "color"
: lower.includes("icon")
? "icon"
: "default",
fixed: lower.includes("fixed"),
remote,
remoteUrl: remote ? "/api/teams" : "",
remoteQueryParam: "q",
remoteAutoLoad: true,
infinite: lower.includes("infinite"),
hasMore: lower.includes("infinite"),
};
}
const pinInputTitles = [
"Default four-digit PIN",
"Different lengths",
"Pre-filled verification code",
"Masked security PIN",
"Alphanumeric regex",
"Grouped code with separator",
"Paste from clipboard",
"Disabled PIN input",
"Read-only code",
"Validation state",
"Supporting help text",
"Large PIN cells",
"Custom placeholder",
"Automatic form submission",
"Completion events",
"JavaScript methods",
];
const pinInputUses = pinInputTitles.map((title, index) => ({
eyebrow: index < 7 ? "Entry behavior" : index < 13 ? "States & design" : "Automation",
title,
description: `Live ${title.toLowerCase()} example with keyboard navigation, filtering, and copyable component code.`,
}));
function pinInputConfiguration(variation) {
const title = pinInputTitles[variation] ?? pinInputTitles[0];
const lower = title.toLowerCase();
const fourDigits = lower.includes("four-digit") || lower.includes("security pin");
return {
label: title,
name: `pin-input-${variation + 1}`,
value: lower.includes("pre-filled") ? "482915" : lower.includes("read-only") ? "739204" : "",
length: fourDigits ? 4 : 6,
pattern: lower.includes("alphanumeric") ? "[A-Z0-9]" : "[0-9]",
inputMode: lower.includes("alphanumeric") ? "text" : "numeric",
placeholder: lower.includes("custom placeholder") ? "•" : "○",
masked: lower.includes("masked"),
disabled: lower.includes("disabled"),
readonly: lower.includes("read-only"),
required: lower.includes("validation"),
invalid: false,
validationMessage: "",
allowPaste: true,
separator: lower.includes("separator") ? "" : "",
groupSize: lower.includes("separator") ? 3 : 0,
helpText: lower.includes("help text")
? "Enter the code sent to your registered email address."
: "",
autoSubmit: lower.includes("automatic"),
size: lower.includes("large") ? "lg" : "default",
};
}
const strongPasswordTitles = [
"Live strength meter",
"Requirements and hint text",
"Requirements in a popover",
"Custom special characters",
"Optional requirements",
"Disabled password strength",
];
const strongPasswordUses = strongPasswordTitles.map((title, index) => ({
eyebrow: index < 3 ? "Strength feedback" : "Configuration",
title,
description: `Live ${title.toLowerCase()} example with accessible scoring feedback and copyable component code.`,
}));
function strongPasswordConfiguration(variation) {
const title = strongPasswordTitles[variation] ?? strongPasswordTitles[0];
const lower = title.toLowerCase();
return {
label: title,
name: `strong-password-${variation + 1}`,
value: lower.includes("live") ? "Northstar!2026" : "",
placeholder: "Create a strong password",
autocomplete: "new-password",
minLength: 8,
specialCharactersSet: lower.includes("custom") ? "@#_-." : "!@#$%^&*()_+-=[]{}|;:,.<>?",
requireLowercase: true,
requireUppercase: true,
requireNumber: true,
requireSpecialCharacter: !lower.includes("optional"),
showRequirements: !lower.includes("live"),
presentation: lower.includes("popover") ? "popover" : "inline",
hintText: lower.includes("hint")
? "Use a unique password you do not use for another account."
: "",
disabled: lower.includes("disabled"),
readonly: false,
required: false,
invalid: false,
validationMessage: "",
};
}
const togglePasswordTitles = [
"Inline visibility button",
"Checkbox-controlled toggle",
"Synchronized password fields",
"Without visibility toggle",
"Supporting help text",
"Required validation",
"Disabled password",
"Read-only password",
"Large password field",
"Toggle events",
];
const togglePasswordUses = togglePasswordTitles.map((title, index) => ({
eyebrow: index < 4 ? "Visibility" : index < 8 ? "Forms & states" : "Design & events",
title,
description: `Live ${title.toLowerCase()} example with accessible visibility controls and copyable component code.`,
}));
function togglePasswordConfiguration(variation) {
const title = togglePasswordTitles[variation] ?? togglePasswordTitles[0];
const lower = title.toLowerCase();
const checkbox = lower.includes("checkbox");
const synchronized = lower.includes("synchronized");
return {
label: synchronized ? "Passwords" : lower.includes("inline") ? "Password" : title,
name: `toggle-password-${variation + 1}`,
value:
lower.includes("inline") || checkbox || lower.includes("read-only") ? "Northstar!2026" : "",
placeholder: "Enter your password",
autocomplete: "current-password",
minlength: lower.includes("validation") ? "8" : "",
maxlength: "128",
pattern: "(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^A-Za-z0-9]).{8,}",
fields: synchronized
? [
{
label: "New password",
name: "new-password",
placeholder: "Enter new password",
autocomplete: "new-password",
},
{
label: "Current password",
name: "current-password",
value: "Northstar!2026",
autocomplete: "current-password",
},
]
: [],
visible: false,
toggleable: !lower.includes("without"),
toggleMode: checkbox ? "checkbox" : "button",
checkboxLabel: "Show password",
disabled: lower.includes("disabled"),
readonly: lower.includes("read-only"),
required: lower.includes("validation"),
helpText: lower.includes("help text")
? "Use at least eight characters and avoid passwords used on other sites."
: "",
validationMessage: "",
invalid: false,
size: lower.includes("large") ? "lg" : "default",
};
}
const inputNumberUses = [
{
eyebrow: "Basic usage",
title: "Default controls",
description: "Use standard decrement and increment buttons alongside a numeric input field.",
},
{
eyebrow: "Basic usage",
title: "Labeled input style",
description:
"Add supporting label text and rounded controls for a more form-like number input.",
},
{
eyebrow: "Layout",
title: "Vertical buttons",
description: "Stack the increment and decrement buttons vertically beside the field.",
},
{
eyebrow: "Layout",
title: "Horizontal buttons",
description: "Place the increment and decrement buttons on opposite sides of the value.",
},
{
eyebrow: "Sizing",
title: "Compact size",
description: "Use a smaller compact control when the available space is limited.",
},
{
eyebrow: "Commerce",
title: "Seat quantity selector",
description: "Pair the control with pricing information for seat counts or plan add-ons.",
},
{
eyebrow: "States",
title: "Disabled input",
description: "Disable manual entry while keeping supported quantity controls available.",
},
{
eyebrow: "States",
title: "Disabled buttons",
description:
"Disable increment and decrement controls while leaving the current value visible.",
},
{
eyebrow: "Behaviour",
title: "Custom step size",
description: "Set a custom step so every button press changes the value by two.",
},
{
eyebrow: "Limits",
title: "Negative values",
description: "Use the min option to permit controlled negative values.",
},
{
eyebrow: "Limits",
title: "Maximum limit",
description: "Set an upper limit and disable incrementing when that limit is reached.",
},
{
eyebrow: "Validation",
title: "Validation states",
description: "Communicate whether the current numerical value is valid.",
},
];
function inputNumberConfiguration(variation) {
const configurations = [
{
name: "default-quantity",
value: 1,
min: 0,
max: 99,
step: 1,
variant: "default",
},
{
name: "labeled-quantity",
label: "Select quantity",
value: 1,
min: 1,
max: 25,
step: 1,
variant: "labeled",
},
{
name: "vertical-quantity",
label: "Select quantity",
value: 1,
min: 0,
max: 20,
variant: "vertical",
},
{
name: "horizontal-quantity",
value: 1,
min: 0,
max: 20,
variant: "horizontal",
},
{
name: "compact-quantity",
value: 0,
min: 0,
max: 10,
variant: "compact",
size: "sm",
},
{
name: "additional-seats",
label: "Additional seats",
description: "$39 monthly",
value: 0,
min: 0,
max: 50,
variant: "seat",
},
{
name: "disabled-input",
value: 10,
inputDisabled: true,
},
{
name: "disabled-buttons",
value: 10,
buttonsDisabled: true,
},
{
name: "custom-step",
value: 0,
min: 0,
max: 20,
step: 2,
},
{
name: "negative-values",
value: -10,
min: -100,
max: 100,
},
{
name: "maximum-limit",
value: 10,
min: 0,
max: 10,
help: "The maximum value is 10.",
},
{
name: "invalid-quantity",
value: 10,
min: 0,
max: 5,
error: "Out of limit",
},
];
return {
color: "primary",
size: "md",
...configurations[variation],
};
}
function demoUses(component) {
const profile = profileFor(component.name);
if (profile?.demos?.length) {
return profile.demos.map(({ eyebrow, title, description }) => ({
eyebrow,
title,
description,
}));
}
switch (component.name) {
case "Button":
return buttonUses;
case "AdvancedSelect":
return advancedSelectUses;
case "ComboBox":
return comboboxUses;
case "InputNumber":
return inputNumberUses;
case "PinInput":
return pinInputUses;
case "StrongPassword":
return strongPasswordUses;
case "TogglePassword":
return togglePasswordUses;
default:
return defaultUses;
}
}
function demoConfiguration(component, variation) {
const demo = profileFor(component.name)?.demos?.[variation];
if (!demo) return {};
return {
...(demo.props ?? {}),
__slots: demo.slots ?? {},
};
}
function eventDocumentation(component) {
if (!component.events.length) return "";
const eventRows = component.events
.map(
(eventName) =>
`<div><code>@${escapeCode(eventName)}</code><span>${escapeText(eventDescriptionFor(component.name, eventName))}</span></div>`,
)
.join("");
const declarativeAttributes = component.events
.map((eventName) => ` @${eventName}='console.log(event.detail)'`)
.join("\n");
const declarativeCode = `<${component.mount}\n${declarativeAttributes}\n/>`;
const selector = `[data-ui-component="${component.name}"], [data-component="${component.mount}"]`;
const listenerCode = `const component = document.querySelector(${JSON.stringify(selector)})\n\n${component.events
.map(
(eventName) =>
`component?.addEventListener(${JSON.stringify(eventName)}, (event) => {\n console.log(${JSON.stringify(eventName)}, event.detail)\n})`,
)
.join("\n\n")}`;
return `<section id="events" class="detail-section"><div class="detail-section-heading"><span>Component events</span><h2>Respond to every public interaction</h2><p>Use declarative <code>@event</code> handlers in <code>.wrn</code> files or subscribe to the bubbling browser events from JavaScript.</p></div><div class="detail-events-grid"><div class="detail-events-list">${eventRows}</div><div class="detail-event-examples"><section class="demo-code"><header><span><span class="icon-[lucide--braces] size-4" aria-hidden="true"></span>Declarative handlers</span><small>.wrn</small></header><pre><code>${escapeCode(declarativeCode)}</code></pre></section><section class="demo-code"><header><span><span class="icon-[lucide--radio] size-4" aria-hidden="true"></span>Browser listeners</span><small>.js</small></header><pre><code>${escapeCode(listenerCode)}</code></pre></section></div></div></section>`;
}
function detailPage(component, categoryComponents) {
const index = categoryComponents.findIndex((item) => item.name === component.name);
const previous = categoryComponents[index - 1];
const next = categoryComponents[index + 1];
const uses = demoUses(component);
const demos = uses
.map((useCase, variation) => {
const configuredDemo = demoConfiguration(component, variation);
const isPinLengthDemo = component.name === "PinInput" && variation === 1;
const pinLengths = [3, 5, 7];
const mount = isPinLengthDemo
? `<div class="pin-length-demo">${pinLengths
.map((length) =>
componentMount(component, variation, {
...configuredDemo,
label: `${length}-digit code`,
name: `pin-length-${length}`,
length,
}),
)
.join("")}</div>`
: componentMount(component, variation, configuredDemo);
const snippet = isPinLengthDemo
? pinLengths
.map((length) =>
sourceSnippet(component, variation, {
...configuredDemo,
label: `${length}-digit code`,
name: `pinLength${length}`,
length,
}),
)
.join("\n\n")
: sourceSnippet(component, variation, configuredDemo);
const validationSchema =
component.name === "AdvancedSelect" && variation === 25
? "advanced-select-validation"
: component.name === "AdvancedSelect" && variation === 26
? "advanced-select-dynamic-validation"
: component.name === "ComboBox" && variation === 14
? "combobox-validation"
: component.name === "PinInput" && variation === 9
? "pin-input-validation"
: component.name === "TogglePassword" && variation === 5
? "toggle-password-validation"
: "";
const preview = validationSchema
? `<form data-schema="${validationSchema}" class="demo-validation-form">${mount}<button type="submit" class="wire-btn wire-btn--variant-default">Validate selection</button></form>`
: mount;
return `
<article class="demo-case">
<header class="demo-case-header">
<div><span class="demo-case-eyebrow">${useCase.eyebrow}</span><h2>${useCase.title}</h2><p>${useCase.description}</p></div>
<span class="demo-case-number">0${variation + 1}</span>
</header>
<div class="demo-workbench">
<div id="${slugOf(component.name)}-demo-${variation + 1}" class="demo-canvas demo-canvas--${variation + 1}">
<div class="demo-browser-chrome"><span></span><span></span><span></span><small>Live preview</small></div>
${isInteractiveOverlay(component) ? `<p class="demo-interaction-hint"><span class="icon-[lucide--mouse-pointer-click] size-4" aria-hidden="true"></span>${escapeText(overlayInteractionLabel(component))}</p>` : ""}
<div class="demo-render">${preview}</div>
</div>
<section class="demo-code" aria-label="${escapeAttribute(useCase.title)} usage">
<header><span><span class="icon-[lucide--code-2] size-4" aria-hidden="true"></span>Component usage</span><small>.wrn</small></header>
<pre><code>${snippet}</code></pre>
</section>
</div>
</article>`;
})
.join("");
return `// Generated by scripts/generate-showcase.mjs. Do not edit directly.
page ${component.name.replace(/[^A-Za-z0-9_]/g, "")}Detail {
layout = "showcase"
${playgroundStates(component)}
seo {
title = "${escapeAttribute(displayName(component.name))}"
description = "${escapeAttribute(component.purpose)}"
}
view {
<nav class="docs-breadcrumbs" aria-label="Breadcrumb">
<a href="/">Components</a><span aria-hidden="true">/</span><a href="/${component.category}">${titleCase(component.category)}</a><span aria-hidden="true">/</span><span>${escapeText(displayName(component.name))}</span>
</nav>
<header class="detail-hero">
<div class="detail-hero-copy">
<span class="showcase-eyebrow">${titleCase(component.category)} component</span>
<h1>${escapeText(displayName(component.name))}</h1>
<p>${escapeText(component.purpose)}</p>
<div class="detail-badges"><span>${component.props.length} props</span><span>${component.slots.length} slots</span><span>${component.events.length} events</span><span>Theme ready</span><span>Responsive</span></div>
</div>
<div class="detail-hero-icon"><span class="icon-[lucide--component] size-10" aria-hidden="true"></span></div>
</header>
${playgroundSection(component)}
<div class="detail-layout">
<main class="detail-content">
<section class="detail-section"><div class="detail-section-heading"><span>Live examples</span><h2>Designed for real product surfaces</h2><p>Compare configurations and resize the browser to check responsive behavior.</p></div><div class="demo-list">${demos}</div></section>
${eventDocumentation(component)}
<section id="api" class="detail-section"><div class="detail-section-heading"><span>Component API</span><h2>Props and configuration</h2><p>All content and behavior shown above is supplied through these props and slots.</p></div>${propsTable(component)}</section>
</main>
<aside class="detail-aside">
<div class="detail-toc"><strong>On this page</strong><a href="#playground">Playground</a>${uses.map((useCase, variation) => `<a href="#${slugOf(component.name)}-demo-${variation + 1}">${escapeText(useCase.title)}</a>`).join("")}${component.events.length ? '<a href="#events">Events</a>' : ""}<a href="#api">Props API</a></div>
</aside>
</div>
<nav class="detail-pagination">
${previous ? `<a href="/components/${slugOf(previous.name)}"><small>Previous</small><strong>← ${escapeText(displayName(previous.name))}</strong></a>` : "<span></span>"}
${next ? `<a href="/components/${slugOf(next.name)}"><small>Next</small><strong>${escapeText(displayName(next.name))} →</strong></a>` : "<span></span>"}
</nav>
}
}
`;
}
function categoryPage(category, components) {
const liveDemoCount = components.reduce(
(total, component) => total + demoUses(component).length,
0,
);
const cards = components
.map(
(component) => `
<article class="catalog-card" data-interactive-preview="${isInteractiveOverlay(component) ? "true" : "false"}">
<div class="catalog-card-preview">
<div class="catalog-card-glow" aria-hidden="true"></div>
<div class="catalog-card-stage">${componentMount(component, 0, demoConfiguration(component, 0))}${isInteractiveOverlay(component) ? `<p class="catalog-interaction-hint"><span class="icon-[lucide--mouse-pointer-click] size-4" aria-hidden="true"></span>${escapeText(overlayInteractionLabel(component))}</p>` : ""}</div>
</div>
<div class="catalog-card-body">
<div><span class="catalog-card-category">${titleCase(category)}</span><h2>${escapeText(displayName(component.name))}</h2><p>${escapeText(component.purpose)}</p></div>
<div class="catalog-card-footer"><span>${component.props.length} props · ${component.slots.length} slots</span><a href="/components/${slugOf(component.name)}">Explore component <span aria-hidden="true">→</span></a></div>
</div>
</article>`,
)
.join("");
return `// Generated by scripts/generate-showcase.mjs. Do not edit directly.
page ${identifier(category)}Showcase {
layout = "showcase"
seo {
title = "${titleCase(category)} components"
description = "${components.length} ${category} components from @wrnexus/ui."
}
view {
<header class="showcase-hero showcase-hero--category">
<span class="showcase-eyebrow">Component category</span>
<h1 class="showcase-title">${titleCase(category)}</h1>
<p class="showcase-description">${components.length} unique, responsive, theme-aware components. Open a component to inspect multiple live configurations and its complete props API.</p>
<div class="category-meta"><span>${components.length} components</span><span>Multiple use cases</span><span>${liveDemoCount} live configurations</span></div>
</header>
<div class="catalog-grid">${cards}</div>
}
}
`;
}
const categories = Object.groupBy(reference.components, (component) => component.category);
const categoryEntries = Object.entries(categories).sort(([left], [right]) =>
left.localeCompare(right),
);
const totalDemoCount = reference.components.reduce(
(total, component) => total + demoUses(component).length,
0,
);
function showcaseLayout() {
const directory = categoryEntries
.map(
([category, components]) => `
<section class="docs-directory-group">
<a class="docs-directory-heading" href="/${category}">${titleCase(category)}</a>
<div>${components
.map(
(component) =>
`<a data-docs-component-link href="/components/${slugOf(component.name)}">${escapeText(displayName(component.name))}</a>`,
)
.join("")}</div>
</section>`,
)
.join("");
return `// Generated by scripts/generate-showcase.mjs. Do not edit directly.
layout Showcase {
view {
<div class="showcase-shell">
<header class="showcase-header">
<div class="showcase-header-start">
<button type="button" class="docs-menu-toggle" data-docs-menu-toggle aria-label="Toggle component directory"><span class="icon-[lucide--menu] size-5" aria-hidden="true"></span></button>
<a class="showcase-brand" href="/"><span class="showcase-brand-mark"><span class="icon-[lucide--blocks] size-4" aria-hidden="true"></span></span>WRNexus UI</a>
<label class="docs-search"><span class="icon-[lucide--search] size-4" aria-hidden="true"></span><input type="search" placeholder="Search documentation..." data-docs-search /><kbd>⌘ K</kbd></label>
</div>
<div class="showcase-header-end">
<nav aria-label="Primary navigation" class="showcase-nav"><a href="/docs">Docs</a><a href="/block-library">Blocks</a><a href="/templates">Templates</a></nav>
<button type="button" class="showcase-theme" data-design-panel-toggle aria-label="Customize design"><span class="icon-[lucide--palette] size-5" aria-hidden="true"></span></button>
</div>
</header>
<aside class="design-panel" data-design-panel aria-label="Design configuration">
<header><div><strong>Customize UI</strong><span>Changes apply to every component</span></div><button type="button" data-design-panel-toggle aria-label="Close customization"><span class="icon-[lucide--x] size-5" aria-hidden="true"></span></button></header>
<label><span>Theme</span><select data-design-setting="style"><option value="default">Default</option><option value="soft">Soft</option><option value="sharp">Sharp</option><option value="glass">Glass</option></select></label>
<label><span>Color</span><select data-design-setting="palette"><option value="violet">Violet</option><option value="blue">Blue</option><option value="emerald">Emerald</option><option value="rose">Rose</option><option value="amber">Amber</option><option value="cyan">Cyan</option><option value="slate">Slate</option></select></label>
<label><span>Mode</span><select data-design-setting="mode"><option value="system">System</option><option value="light">Light</option><option value="dark">Dark</option></select></label>
<label><span>Font</span><select data-design-setting="font"><option value="jakarta">Plus Jakarta Sans</option><option value="system">System UI</option><option value="serif">Instrument Serif</option><option value="mono">JetBrains Mono</option></select></label>
<label><span>Size</span><select data-design-setting="scale"><option value="compact">Compact</option><option value="default">Default</option><option value="comfortable">Comfortable</option><option value="large">Large</option></select></label>
<button class="design-reset" type="button" data-design-reset><span class="icon-[lucide--rotate-ccw] size-4" aria-hidden="true"></span>Reset configuration</button>
</aside>
<div class="docs-shell">
<aside class="docs-directory" data-docs-directory>
<nav aria-label="Documentation directory">
<section class="docs-directory-group docs-directory-guides"><strong>Getting Started</strong><div><a href="/docs">Introduction</a><a href="/installation">Installation</a><a href="/framework-guides">Framework guides</a><a href="/accessibility">Accessibility</a><a href="/resources">Resources</a></div></section>
<section class="docs-directory-group docs-directory-guides"><strong>Customization</strong><div><a href="/dark-mode">Dark Mode</a><a href="/themes">Themes <small>New</small></a><a href="/colors">Colors <small>New</small></a><a href="/fonts">Fonts</a><a href="/sizing">Sizing</a></div></section>
${directory}
</nav>
<p class="docs-directory-empty" data-docs-empty>No components found.</p>
</aside>
<main class="showcase-main"><slot /></main>
</div>
</div>
}
}
`;
}
function documentLayout() {
return `// Generated by scripts/generate-showcase.mjs. Do not edit directly.
layout Document {
props {
cookies = {}
theme = "light"
language = "en"
url = ""
pathname = "/"
}
view {
<html
data-ui-style="{cookies['wrn-ui-style'] || 'default'}"
data-ui-palette="{cookies['wrn-ui-palette'] || 'violet'}"
data-ui-mode="{cookies['wrn-ui-mode'] || 'system'}"
data-ui-font="{cookies['wrn-ui-font'] || 'jakarta'}"
data-ui-scale="{cookies['wrn-ui-scale'] || 'default'}"
>
<head></head>
<body><div id="app"><slot /></div></body>
</html>
}
}
`;
}
mkdirSync(pagesDir, { recursive: true });
mkdirSync(detailPagesDir, { recursive: true });
mkdirSync(layoutsDir, { recursive: true });
writeFileSync(join(layoutsDir, "showcase.wrn"), showcaseLayout(), "utf8");
writeFileSync(join(layoutsDir, "document.wrn"), documentLayout(), "utf8");
for (const file of readdirSync(pagesDir).filter((entry) => entry.endsWith(".wrn"))) {
unlinkSync(join(pagesDir, file));
}
for (const file of readdirSync(detailPagesDir).filter((entry) => entry.endsWith(".wrn"))) {
unlinkSync(join(detailPagesDir, file));
}
for (const [category, components] of categoryEntries) {
writeFileSync(join(pagesDir, `${category}.wrn`), categoryPage(category, components), "utf8");
for (const component of components) {
writeFileSync(
join(detailPagesDir, `${slugOf(component.name)}.wrn`),
detailPage(component, components),
"utf8",
);
}
}
const showcaseManifest = {
generatedFrom: "packages/ui/component-reference.json",
componentCount: reference.count,
categoryCount: categoryEntries.length,
totalDemoCount,
components: reference.components.map((component) => ({
name: component.name,
mount: component.mount,
slug: slugOf(component.name),
category: component.category,
purpose: component.purpose,
demoCount: demoUses(component).length,
propCount: component.props.length,
slots: component.slots,
events: component.events,
profiled: Boolean(profileFor(component.name)),
})),
};
writeFileSync(
join(exampleRoot, "showcase-manifest.json"),
`${JSON.stringify(showcaseManifest, null, 2)}\n`,
"utf8",
);
const categoryCards = categoryEntries
.map(
([category, components], index) =>
`<a class="home-category home-category--${(index % 4) + 1}" href="/${category}"><span class="home-category-index">0${index + 1}</span><div><strong>${titleCase(category)}</strong><p>${components.length} components · ${components.reduce((total, component) => total + demoUses(component).length, 0)} live demos</p></div><span class="icon-[lucide--arrow-up-right] size-5" aria-hidden="true"></span></a>`,
)
.join("");
writeFileSync(
join(pagesDir, "index.wrn"),
`// Generated by scripts/generate-showcase.mjs. Do not edit directly.
page ComponentShowcase {
layout = "showcase"
seo {
title = "WRNexus component system"
description = "Explore every unique @wrnexus/ui component through live, production-quality examples."
}
view {
<div class="page-home">
<header class="showcase-hero showcase-hero--home">
<span class="showcase-eyebrow">The UI foundation for WRNexus</span>
<h1 class="showcase-title">Build modern products<br /><em>at framework speed.</em></h1>
<p class="showcase-description">A complete, accessible component system with ${reference.count} primitives, ready-made blocks, page templates, responsive behavior, and deeply configurable design tokens.</p>
<div class="home-actions"><a class="home-primary-action" href="/docs">Get started <span aria-hidden="true">→</span></a><a class="home-secondary-action" href="/block-library">Explore blocks</a></div>
<div class="home-install"><span>$</span><code>bun add @wrnexus/ui</code><button type="button" data-copy-text="bun add @wrnexus/ui" aria-label="Copy install command"><span class="icon-[lucide--copy] size-4" aria-hidden="true"></span></button></div>
</header>
<section class="home-product-grid"><article><span class="icon-[lucide--component] size-7"></span><strong>${reference.count} components</strong><p>Accessible primitives for every product surface.</p><a href="/base">Browse components →</a></article><article><span class="icon-[lucide--layout-template] size-7"></span><strong>Ready-made blocks</strong><p>Composable sections assembled from WRNexus UI.</p><a href="/block-library">Explore blocks →</a></article><article><span class="icon-[lucide--panels-top-left] size-7"></span><strong>Page templates</strong><p>Complete responsive pages ready to customize.</p><a href="/templates">View templates →</a></article></section>
<section class="showcase-stats">
<div class="showcase-stat"><strong>${reference.count}</strong><span>Unique components</span></div>
<div class="showcase-stat"><strong>${totalDemoCount}</strong><span>Live configurations</span></div>
<div class="showcase-stat"><strong>0</strong><span>Duplicate implementations</span></div>
<div class="showcase-stat"><strong>${categoryEntries.length}</strong><span>Focused categories</span></div>
</section>
<section class="home-categories"><div class="home-section-heading"><span>Explore the system</span><h2>Everything your product needs</h2></div><div class="home-category-grid">${categoryCards}</div></section>
</div>
}
}
`,
"utf8",
);
const docsSections = [
[
"installation",
"Installation",
"Install the package and import the shared stylesheet once in your application.",
"bun add @wrnexus/ui",
],
[
"framework-guides",
"Framework guides",
"WRNexus components use native HTML semantics and framework component mounts.",
],
[
"accessibility",
"Accessibility",
"Keyboard interaction, visible focus states, labels, and reduced-motion support ship by default.",
],
[
"dark-mode",
"Dark mode",
"Choose light, dark, or system mode. The selection persists and updates every preview.",
],
[
"themes",
"Themes",
"Theme presets control radius, surface treatment, borders, shadows, and motion.",
],
[
"colors",
"Colors",
"Semantic primary, secondary, success, warning, danger, and info tokens remain available in every palette.",
],
[
"fonts",
"Fonts",
"Select a font family globally while components continue to inherit typography correctly.",
],
[
"sizing",
"Sizing",
"Compact through large scales adjust the complete component system consistently.",
],
[
"resources",
"Resources",
"Use the component API pages, blocks, and templates as production starting points.",
],
];
writeFileSync(
join(pagesDir, "docs.wrn"),
`page Documentation {
layout = "showcase"
seo { title = "Documentation" description = "Install, configure, and use WRNexus UI." }
view {
<div class="page-docs">
<header class="docs-intro"><span class="showcase-eyebrow">Getting started</span><h1>Build with WRNexus UI</h1><p>Configure one design system and use it across components, blocks, and complete page templates.</p></header>
<section class="guide-section"><h2>Explore the documentation</h2><p>Every Getting Started and Customization topic now has its own page. Choose a guide from the sidebar to continue.</p></section>
</div>
}
}
`,
"utf8",
);
const customizationGuides = new Set(["dark-mode", "themes", "colors", "fonts", "sizing"]);
for (const [slug, title, description, code] of docsSections) {
const section = customizationGuides.has(slug) ? "Customization" : "Getting Started";
const examples =
slug === "colors"
? [
["Brand colors", "Primary and secondary tokens communicate brand hierarchy."],
[
"Status colors",
"Success, warning, danger, and info preserve their semantic meaning in every palette.",
],
[
"Component override",
'Pass color="success" or another semantic color to any component.',
],
]
: slug === "sizing"
? [
["Component size", "Every component accepts xs, sm, default, md, lg, and xl."],
[
"Global density",
"Compact, default, comfortable, and large scale the complete interface.",
],
["Responsive sizing", "Component sizing cooperates with responsive breakpoints."],
]
: slug === "themes"
? [
["Default", "Balanced surfaces, borders, elevation, and radius."],
["Soft and sharp", "Choose friendly rounded surfaces or dense operational styling."],
["Glass", "Use translucent layered surfaces while retaining semantic tokens."],
]
: [
["Overview", description],
["Configuration", "Use the design switcher in the header to test this setting live."],
[
"Component behavior",
"The setting is inherited by components, blocks, templates, and playground previews.",
],
];
writeFileSync(
join(pagesDir, `${slug}.wrn`),
`page ${identifier(slug)}Guide {
layout = "showcase"
seo {
title = "${escapeAttribute(title)}"
description = "${escapeAttribute(description)}"
}
view {
<article class="page-docs guide-page">
<nav class="docs-breadcrumbs" aria-label="Breadcrumb"><a href="/docs">Docs</a><span aria-hidden="true">/</span><span>${escapeText(title)}</span></nav>
<header class="docs-intro"><span class="showcase-eyebrow">${section}</span><h1>${escapeText(title)}</h1><p>${escapeText(description)}</p></header>
${examples.map(([heading, copy]) => `<section class="guide-section"><h2>${escapeText(heading)}</h2><p>${escapeText(copy)}</p></section>`).join("")}
${code ? `<section class="guide-section"><h2>Quick start</h2><pre><code>${escapeCode(code)}</code></pre></section>` : ""}
<nav class="guide-next"><a href="/components/button">Explore components <span aria-hidden="true">→</span></a><a href="/block-library">Browse blocks <span aria-hidden="true">→</span></a></nav>
</article>
}
}
`,
"utf8",
);
}
const blockCards = [
[
"Hero sections",
"Marketing",
"layout-template",
"Hero, actions, product proof, and visual stage.",
],
[
"Feature grids",
"Marketing",
"grid-2x2",
"Responsive benefits and product capability sections.",
],
["Pricing sections", "Marketing", "badge-dollar-sign", "Tier cards, comparison grids, and CTAs."],
[
"Application shells",
"Application UI",
"panel-left",
"Headers, sidebars, content, and command areas.",
],
[
"Dashboard stats",
"Application UI",
"chart-no-axes-combined",
"Metrics, trends, filters, and summaries.",
],
["Checkout panels", "Ecommerce", "shopping-cart", "Cart, address, payment, and order summary."],
[
"Article layouts",
"Blog & Articles",
"newspaper",
"Editorial headers, content, author, and sharing.",
],
[
"Authentication",
"Application UI",
"shield-check",
"Sign in, registration, recovery, and verification.",
],
];
// Keep /blocks for the generated Blocks component category.
writeFileSync(
join(pagesDir, "block-library.wrn"),
`page BlockLibrary {
layout = "showcase"
seo { title = "UI blocks" description = "Complete sections composed from WRNexus UI components." }
view {
<div class="page-marketplace page-blocks">
<nav class="market-tabs"><a href="#all">All blocks</a><a href="#marketing">Marketing</a><a href="#application">Application UI</a><a href="#ecommerce">Ecommerce</a><a href="#blog">Blog & Articles</a></nav>
<header><span class="showcase-eyebrow">Composable sections</span><h1>Ready-to-use UI blocks</h1><p>Complete responsive sections composed exclusively from the components in this library.</p></header>
<div class="market-grid">${blockCards.map(([title, category, icon, description], index) => `<article class="market-card"><div class="market-preview market-preview--${(index % 4) + 1}"><span class="icon-[lucide--${icon}] size-10"></span><div><i></i><i></i><i></i></div></div><div><small>${category}</small><h2>${title}</h2><p>${description}</p><a href="/components/${index % 2 ? "grid" : "container"}">View components →</a></div></article>`).join("")}</div>
</div>
}
}
`,
"utf8",
);
const templates = [
[
"Admin dashboard",
"Dashboard & Admin",
"31 pages",
"Analytics, teams, settings, billing, and operational workflows.",
],
[
"Commerce storefront",
"Ecommerce",
"18 pages",
"Catalog, product, cart, checkout, and customer account pages.",
],
[
"SaaS website",
"Websites",
"12 pages",
"Landing, pricing, about, blog, contact, and legal pages.",
],
[
"Support center",
"Support",
"9 pages",
"Knowledge base, ticketing, status, and customer help experiences.",
],
[
"Finance workspace",
"Finance & Payments",
"14 pages",
"Accounts, transactions, reporting, transfers, and controls.",
],
];
writeFileSync(
join(pagesDir, "templates.wrn"),
`page Templates {
layout = "showcase"
seo { title = "Page templates" description = "Complete pages assembled from WRNexus UI blocks." }
view {
<div class="page-marketplace page-templates">
<nav class="market-tabs"><a href="#all">All templates</a><a href="#admin">Dashboards & Admin</a><a href="#commerce">Ecommerce</a><a href="#websites">Websites</a><a href="#support">Support</a><a href="#finance">Finance</a></nav>
<header><span class="showcase-eyebrow">Complete products</span><h1>Production-ready templates</h1><p>Responsive page systems assembled from WRNexus blocks and components.</p></header>
<div class="template-list">${templates.map(([title, category, count], index) => `<article class="template-card"><div><small>${count}</small><h2>${title}</h2><p>${category} template with consistent navigation, content, forms, and responsive behavior.</p><a href="/block-library">Preview <span>→</span></a></div><div class="template-scenes template-scenes--${index + 1}"><i></i><i></i><i></i></div></article>`).join("")}</div>
</div>
}
}
`,
"utf8",
);
process.stdout.write(
`Generated ${reference.count} component detail pages, ${totalDemoCount} live demos, and ${categoryEntries.length} category pages.\n`,
);