3768 lines
131 KiB
JavaScript
3768 lines
131 KiB
JavaScript
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";
|
||
|
||
const exampleRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||
const uiPackageRoot = join(exampleRoot, "node_modules", "@wrnexus", "ui");
|
||
const referencePath = join(uiPackageRoot, "component-reference.json");
|
||
const uiComponentsPath = join(uiPackageRoot, "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"));
|
||
|
||
function writeIfChanged(path, contents) {
|
||
try {
|
||
if (readFileSync(path, "utf8") === contents) return;
|
||
} catch {
|
||
// The generated file does not exist yet.
|
||
}
|
||
writeFileSync(path, contents, "utf8");
|
||
}
|
||
|
||
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("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
||
// 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("{", "{").replaceAll("}", "}");
|
||
const escapeAttribute = (value) =>
|
||
escapeText(value).replaceAll('"', """).replaceAll("'", "'");
|
||
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 === "FileUploadProgress") {
|
||
const configuration = fileUploadProgressConfiguration(variation);
|
||
if (Object.hasOwn(configuration, prop.name)) return configuration[prop.name];
|
||
}
|
||
if (basicFormComponentNames.has(component.name)) {
|
||
const configuration = basicFormConfiguration(component.name, variation);
|
||
if (Object.hasOwn(configuration, prop.name)) return configuration[prop.name];
|
||
return undefined;
|
||
}
|
||
if (component.name === "Accordion") {
|
||
const configuration = accordionConfiguration(variation);
|
||
if (Object.hasOwn(configuration, prop.name)) return configuration[prop.name];
|
||
return undefined;
|
||
}
|
||
if (component.name === "Alert") {
|
||
const configuration = alertConfiguration(variation);
|
||
if (Object.hasOwn(configuration, prop.name)) return configuration[prop.name];
|
||
return undefined;
|
||
}
|
||
if (component.name === "Avatar") {
|
||
const configuration = avatarConfiguration(variation);
|
||
if (Object.hasOwn(configuration, prop.name)) return configuration[prop.name];
|
||
return undefined;
|
||
}
|
||
if (component.name === "AvatarGroup") {
|
||
const configuration = avatarGroupConfiguration(variation);
|
||
if (Object.hasOwn(configuration, prop.name)) return configuration[prop.name];
|
||
return undefined;
|
||
}
|
||
if (component.name === "Badge") {
|
||
const configuration = badgeConfiguration(variation);
|
||
if (Object.hasOwn(configuration, prop.name)) return configuration[prop.name];
|
||
return undefined;
|
||
}
|
||
if (component.name === "Blockquote") {
|
||
const configuration = blockquoteConfiguration(variation);
|
||
if (Object.hasOwn(configuration, prop.name)) return configuration[prop.name];
|
||
return undefined;
|
||
}
|
||
if (component.name === "ButtonGroup") {
|
||
const configuration = buttonGroupConfiguration(variation);
|
||
if (Object.hasOwn(configuration, prop.name)) return configuration[prop.name];
|
||
return undefined;
|
||
}
|
||
if (component.name === "Card") {
|
||
const configuration = cardConfiguration(variation);
|
||
if (Object.hasOwn(configuration, prop.name)) return configuration[prop.name];
|
||
return undefined;
|
||
}
|
||
if (component.name === "Carousel") {
|
||
const configuration = carouselConfiguration(variation);
|
||
if (Object.hasOwn(configuration, prop.name)) return configuration[prop.name];
|
||
return undefined;
|
||
}
|
||
if (component.name === "ChatBubble") {
|
||
const configuration = chatBubbleConfiguration(variation);
|
||
if (Object.hasOwn(configuration, prop.name)) return configuration[prop.name];
|
||
return undefined;
|
||
}
|
||
if (component.name === "Collapse") {
|
||
const configuration = collapseConfiguration(variation);
|
||
if (Object.hasOwn(configuration, prop.name)) return configuration[prop.name];
|
||
return undefined;
|
||
}
|
||
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;
|
||
}
|
||
if (component.name === "ToggleCount") {
|
||
const configuration = toggleCountConfiguration(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") {
|
||
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 componentAttributes(component, variation, overrides = {}) {
|
||
const attributes = [`data-component="${component.mount}"`];
|
||
for (const prop of component.props) {
|
||
const value = Object.hasOwn(overrides, prop.name)
|
||
? overrides[prop.name]
|
||
: sampleValue(prop, component, variation);
|
||
if (value === undefined) continue;
|
||
const encoded =
|
||
typeof value === "object" ? jsonAttribute(value) : escapeAttribute(String(value));
|
||
attributes.push(`${prop.name}="${encoded}"`);
|
||
}
|
||
return attributes;
|
||
}
|
||
|
||
function componentMount(component, variation = 0, overrides = {}) {
|
||
const slotMarkup = component.slots
|
||
.map((slot) =>
|
||
slot === "default"
|
||
? defaultSlotMarkup(component, variation)
|
||
: `<div slot="${escapeAttribute(slot)}" class="showcase-slot"><span class="icon-[lucide--panel-top] size-4" aria-hidden="true"></span><span>${escapeText(displayName(slot))} slot</span></div>`,
|
||
)
|
||
.join("");
|
||
return `<div ${componentAttributes(component, variation, overrides).join(" ")}>${slotMarkup}</div>`;
|
||
}
|
||
|
||
const structuralDefaultSlots = new Set([
|
||
"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 '<button type="button" class="wire-btn wire-btn--secondary">Hover for details</button>';
|
||
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 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("'", "\\'")
|
||
.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 usesSlot = component.slots.some(
|
||
(slot) => slot !== "default" || defaultSlotMarkup(component, variation) !== "",
|
||
);
|
||
const slotMarkup = usesSlot
|
||
? `\n <!-- Add ${component.slots.join(", ")} slot content here -->\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 playgroundValue(prop, component) {
|
||
const sampled = sampleValue(prop, component, 0);
|
||
const value = sampled === undefined ? (defaultValue(prop) ?? "") : sampled;
|
||
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 playgroundStates(component) {
|
||
return component.props
|
||
.map((prop) => {
|
||
const initial = playgroundValue(prop, component);
|
||
return ` state ${playgroundStateName(prop)} = ctx.url.searchParams.get("pg_${prop.name}") ?? ${JSON.stringify(initial)}`;
|
||
})
|
||
.join("\n");
|
||
}
|
||
|
||
function playgroundMount(component) {
|
||
const attributes = [
|
||
`data-component="${component.mount}"`,
|
||
...component.props.map((prop) => `${prop.name}="{${playgroundStateName(prop)}}"`),
|
||
];
|
||
const slotMarkup = component.slots
|
||
.map((slot) =>
|
||
slot === "default"
|
||
? defaultSlotMarkup(component, 0)
|
||
: `<div slot="${escapeAttribute(slot)}" class="showcase-slot">${escapeText(displayName(slot))} slot</div>`,
|
||
)
|
||
.join("");
|
||
return `<div ${attributes.join(" ")}>${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: ["start", "center", "end"],
|
||
alignment: ["start", "center", "end"],
|
||
as: ["", "button", "a"],
|
||
color: ["primary", "secondary", "success", "warning", "danger", "info"],
|
||
direction: ["row", "column"],
|
||
iconposition: ["start", "end"],
|
||
method: ["get", "post"],
|
||
orientation: ["horizontal", "vertical"],
|
||
position: ["top", "right", "bottom", "left", "start", "end"],
|
||
size: ["default", "xs", "sm", "md", "lg", "xl", "icon", "icon-xs", "icon-sm", "icon-lg"],
|
||
target: ["_self", "_blank", "_parent", "_top"],
|
||
variant: ["default", "outline", "secondary", "ghost", "destructive", "link"],
|
||
};
|
||
|
||
function playgroundOptions(prop, component, currentValue) {
|
||
if (prop.type !== "string" || prop.name === "class" || prop.name === "controlClass") return [];
|
||
|
||
const discovered = new Set();
|
||
const add = (value) => {
|
||
if (typeof value === "string" && value.length <= 40) discovered.add(value);
|
||
};
|
||
|
||
const common = commonPlaygroundOptions[prop.name.toLowerCase()] ?? [];
|
||
for (const option of common) add(option);
|
||
if (component.name === "Button" && prop.name === "type") {
|
||
for (const option of ["button", "submit", "reset"]) add(option);
|
||
}
|
||
if (component.name === "Blockquote" && prop.name === "align") {
|
||
discovered.clear();
|
||
for (const option of ["left", "center", "right"]) add(option);
|
||
}
|
||
if (component.name === "Blockquote" && prop.name === "variant") {
|
||
discovered.clear();
|
||
for (const option of ["default", "bordered"]) add(option);
|
||
}
|
||
|
||
const sourcePath = join(uiComponentsPath, `${component.name}.wrn`);
|
||
try {
|
||
const source = readFileSync(sourcePath, "utf8");
|
||
const escapedName = prop.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||
const patterns = [
|
||
new RegExp(`\\b${escapedName}\\s*={2,3}\\s*["']([^"'\\n]+)["']`, "g"),
|
||
new RegExp(`["']([^"'\\n]+)["']\\s*={2,3}\\s*\\b${escapedName}\\b`, "g"),
|
||
];
|
||
for (const pattern of patterns) {
|
||
for (const match of source.matchAll(pattern)) add(match[1]);
|
||
}
|
||
} catch {
|
||
// The generated reference can include virtual components without a same-name source file.
|
||
}
|
||
|
||
if (common.length === 0 && discovered.size < 2) return [];
|
||
|
||
const values = new Set();
|
||
if (typeof currentValue === "string" && currentValue.length <= 40) values.add(currentValue);
|
||
const declaredDefault = defaultValue(prop);
|
||
if (typeof declaredDefault === "string" && declaredDefault.length <= 40)
|
||
values.add(declaredDefault);
|
||
for (const option of discovered) 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);
|
||
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}">
|
||
<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>
|
||
</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 accordionUses = [
|
||
{
|
||
eyebrow: "Basic usage",
|
||
title: "Default accordion",
|
||
description: "Open one section at a time with a clear plus and minus indicator.",
|
||
},
|
||
{
|
||
eyebrow: "Behavior",
|
||
title: "Always open",
|
||
description: "Allow several content sections to remain expanded at the same time.",
|
||
},
|
||
{
|
||
eyebrow: "Composition",
|
||
title: "Nested accordion",
|
||
description: "Place a coordinated secondary accordion inside an expanded parent section.",
|
||
},
|
||
{
|
||
eyebrow: "Indicators",
|
||
title: "Without an indicator",
|
||
description: "Use a minimal text-only accordion when visual indicators are unnecessary.",
|
||
},
|
||
{
|
||
eyebrow: "Indicators",
|
||
title: "Chevron indicator",
|
||
description: "Communicate expanded state with a rotating chevron.",
|
||
},
|
||
{
|
||
eyebrow: "Layout",
|
||
title: "Separated title and indicator",
|
||
description: "Place the title and state indicator at opposite ends of each trigger.",
|
||
},
|
||
{
|
||
eyebrow: "Surface",
|
||
title: "Bordered accordion",
|
||
description: "Group sections inside one bordered surface with dividing lines.",
|
||
},
|
||
{
|
||
eyebrow: "Surface",
|
||
title: "Bordered open content",
|
||
description: "Render each section as an independent rounded card.",
|
||
},
|
||
{
|
||
eyebrow: "States",
|
||
title: "Disabled section",
|
||
description:
|
||
"Disable an individual section while keeping the rest of the accordion interactive.",
|
||
},
|
||
];
|
||
|
||
const alertTitles = [
|
||
"Dark solid alert",
|
||
"Secondary solid alert",
|
||
"Info solid alert",
|
||
"Success solid alert",
|
||
"Danger solid alert",
|
||
"Warning solid alert",
|
||
"Light solid alert",
|
||
"Soft color alert",
|
||
"With supporting text",
|
||
"With a list",
|
||
"With actions",
|
||
"Right-aligned link",
|
||
"Discovery notice",
|
||
"Bordered alert",
|
||
"Dismissible alert",
|
||
];
|
||
|
||
const alertUses = alertTitles.map((title, index) => ({
|
||
eyebrow: index < 7 ? "Solid colors" : index < 11 ? "Content" : "Actions & states",
|
||
title,
|
||
description: `Live ${title.toLowerCase()} with accessible semantics and copyable component code.`,
|
||
}));
|
||
|
||
const avatarTitles = [
|
||
"Extra-small circular avatar",
|
||
"Small circular avatar",
|
||
"Medium circular avatar",
|
||
"Large circular avatar",
|
||
"Extra-large circular avatar",
|
||
"Rounded avatars",
|
||
"Circular avatars with top status",
|
||
"Circular avatars with bottom status",
|
||
"Avatars with brand icons",
|
||
"Placeholder icon",
|
||
"Placeholder initials",
|
||
"Solid color variants",
|
||
"Soft color variants",
|
||
"Outline color variants",
|
||
"White color variants",
|
||
"Avatar with tooltip",
|
||
"Media object",
|
||
];
|
||
|
||
const avatarUses = avatarTitles.map((title, index) => ({
|
||
eyebrow:
|
||
index < 6
|
||
? "Size & shape"
|
||
: index < 8
|
||
? "Status"
|
||
: index < 11
|
||
? "Fallbacks"
|
||
: index < 15
|
||
? "Color variants"
|
||
: "Composition",
|
||
title,
|
||
description:
|
||
index < 5
|
||
? "Render one circular profile image at the documented size."
|
||
: index === 5
|
||
? "Apply softly rounded corners when a circle is not the right fit."
|
||
: index < 8
|
||
? "Place a clear presence indicator at the requested avatar edge."
|
||
: index === 8
|
||
? "Overlay a service or platform icon to identify a linked account."
|
||
: index === 9
|
||
? "Show a generic person icon when no profile image is available."
|
||
: index === 10
|
||
? "Use initials as an accessible text fallback for a missing image."
|
||
: index < 15
|
||
? `Use the ${title.split(" ")[0].toLowerCase()} treatment across semantic avatar colors.`
|
||
: index === 15
|
||
? "Reveal a name or extra context on hover and keyboard focus."
|
||
: "Combine an avatar with supporting text for profile and activity rows.",
|
||
}));
|
||
|
||
function avatarConfiguration(variation) {
|
||
const image = "/avatar-example.svg";
|
||
const configurations = [
|
||
{ src: image, alt: "Example profile portrait", size: "xs" },
|
||
{ src: image, alt: "Example profile portrait", size: "sm" },
|
||
{ src: image, alt: "Example profile portrait", size: "md" },
|
||
{ src: image, alt: "Example profile portrait", size: "lg" },
|
||
{ src: image, alt: "Example profile portrait", size: "xl" },
|
||
{ src: image, alt: "Example rounded profile portrait", shape: "rounded", size: "lg" },
|
||
{
|
||
src: image,
|
||
alt: "Avery Chen",
|
||
size: "lg",
|
||
status: "online",
|
||
statusLabel: "Online",
|
||
statusPosition: "top",
|
||
},
|
||
{
|
||
src: image,
|
||
alt: "Avery Chen",
|
||
size: "lg",
|
||
status: "away",
|
||
statusLabel: "Away",
|
||
statusPosition: "bottom",
|
||
},
|
||
{
|
||
src: image,
|
||
alt: "Linked profile",
|
||
size: "lg",
|
||
badgeIcon: "icon-[lucide--message-square]",
|
||
badgeLabel: "Messaging account",
|
||
},
|
||
{ size: "lg", variant: "soft", alt: "Anonymous profile" },
|
||
{ size: "lg", initials: "AC", alt: "Avery Chen" },
|
||
{ size: "lg", initials: "AC", color: "success", variant: "solid", alt: "Avery Chen" },
|
||
{ size: "lg", initials: "AC", color: "info", variant: "soft", alt: "Avery Chen" },
|
||
{ size: "lg", initials: "AC", color: "danger", variant: "outline", alt: "Avery Chen" },
|
||
{ size: "lg", initials: "AC", variant: "white", alt: "Avery Chen" },
|
||
{
|
||
src: image,
|
||
alt: "Avery Chen",
|
||
size: "lg",
|
||
status: "online",
|
||
tooltip: "Avery Chen · Online",
|
||
name: "Avery Chen",
|
||
},
|
||
{
|
||
src: image,
|
||
name: "Mark Wanner",
|
||
alt: "Mark Wanner",
|
||
size: "lg",
|
||
description: "mark@example.com",
|
||
},
|
||
];
|
||
return {
|
||
size: "md",
|
||
color: "primary",
|
||
variant: "solid",
|
||
shape: "circle",
|
||
...configurations[variation],
|
||
};
|
||
}
|
||
|
||
const avatarGroupUses = [
|
||
{
|
||
eyebrow: "Facepile",
|
||
title: "Overlapping stack",
|
||
description: "Overlap avatars with negative spacing to create a compact team facepile.",
|
||
},
|
||
{
|
||
eyebrow: "Layout",
|
||
title: "Grid layout",
|
||
description: "Arrange members in a grid with equal spacing and a clear overflow count.",
|
||
},
|
||
{
|
||
eyebrow: "Border",
|
||
title: "Custom border color",
|
||
description: "Change the ring color so stacked groups remain distinct on different surfaces.",
|
||
},
|
||
{
|
||
eyebrow: "Tooltip",
|
||
title: "Member tooltips",
|
||
description: "Reveal each member's name on pointer hover or keyboard focus.",
|
||
},
|
||
{
|
||
eyebrow: "Overflow",
|
||
title: "Dropdown overflow",
|
||
description: "Open the overflow counter to inspect members hidden by the visible limit.",
|
||
},
|
||
];
|
||
|
||
function avatarGroupConfiguration(variation) {
|
||
const members = [
|
||
{ src: "/avatar-example.svg", name: "Mark Wanner", alt: "Mark Wanner" },
|
||
{ src: "/avatar-example-2.svg", name: "Maria Guan", alt: "Maria Guan" },
|
||
{ src: "/avatar-example-3.svg", name: "Amil Evara", alt: "Amil Evara" },
|
||
{ initials: "EE", name: "Ebele Egbuna", color: "info" },
|
||
{ initials: "CL", name: "Chris Lynch", color: "success" },
|
||
{ initials: "NS", name: "Nora Singh", color: "warning" },
|
||
{ initials: "JR", name: "Jon Rivera", color: "danger" },
|
||
{ initials: "AK", name: "Amina Khan", color: "secondary" },
|
||
];
|
||
const configurations = [
|
||
{ layout: "stack", size: "lg", maxVisible: 4, items: members.slice(0, 4) },
|
||
{ layout: "grid", size: "md", columns: 3, maxVisible: 7, items: members },
|
||
{
|
||
layout: "stack",
|
||
size: "md",
|
||
maxVisible: 4,
|
||
borderColor: "#2583ff",
|
||
items: members.slice(0, 4),
|
||
},
|
||
{ layout: "stack", size: "md", maxVisible: 4, showTooltips: true, items: members.slice(0, 4) },
|
||
{ layout: "stack", size: "md", maxVisible: 4, showTooltips: true, items: members },
|
||
];
|
||
return {
|
||
color: "primary",
|
||
size: "md",
|
||
variant: "solid",
|
||
shape: "circle",
|
||
...configurations[variation],
|
||
};
|
||
}
|
||
|
||
const badgeUses = [
|
||
{
|
||
eyebrow: "Color variants",
|
||
title: "Solid badge",
|
||
description: "Use a solid fill for a high-emphasis status or category.",
|
||
},
|
||
{
|
||
eyebrow: "Color variants",
|
||
title: "Soft badge",
|
||
description: "Use a softer fill for a lower-contrast label.",
|
||
},
|
||
{
|
||
eyebrow: "Color variants",
|
||
title: "Outline badge",
|
||
description: "Use an outlined treatment for lighter visual emphasis.",
|
||
},
|
||
{
|
||
eyebrow: "Color variants",
|
||
title: "White badge",
|
||
description: "Use a neutral surface with a subtle border and shadow.",
|
||
},
|
||
{
|
||
eyebrow: "Shape",
|
||
title: "Rounded badge",
|
||
description: "Use softly rounded corners instead of fully pilled edges.",
|
||
},
|
||
{
|
||
eyebrow: "Shape",
|
||
title: "Pill badge",
|
||
description: "Use a pill shape for compact labels and filter chips.",
|
||
},
|
||
{
|
||
eyebrow: "Content",
|
||
title: "Truncated badge",
|
||
description: "Constrain long badge text while preserving the full value in the API.",
|
||
},
|
||
{
|
||
eyebrow: "Status",
|
||
title: "Badge with status dot",
|
||
description: "Add a small dot to reinforce status at a glance.",
|
||
},
|
||
{
|
||
eyebrow: "Content",
|
||
title: "Badge with icon",
|
||
description: "Pair an icon with text for states, metrics, or trend changes.",
|
||
},
|
||
{
|
||
eyebrow: "Interaction",
|
||
title: "Dismissible badge",
|
||
description: "Provide a clear remove action for a selected chip.",
|
||
},
|
||
{
|
||
eyebrow: "Content",
|
||
title: "Badge with avatar",
|
||
description: "Combine a profile image, label, and optional remove action.",
|
||
},
|
||
{
|
||
eyebrow: "Composition",
|
||
title: "Badge inside button",
|
||
description: "Place a badge beside a button label to show a count.",
|
||
},
|
||
{
|
||
eyebrow: "Composition",
|
||
title: "Positioned badge",
|
||
description: "Anchor a numeric badge to the corner of an icon button.",
|
||
},
|
||
{
|
||
eyebrow: "Composition",
|
||
title: "Dot badge",
|
||
description: "Use a small dot when a notification does not need a count.",
|
||
},
|
||
{
|
||
eyebrow: "Animation",
|
||
title: "Ping badge",
|
||
description: "Pulse a notification badge while respecting reduced-motion preferences.",
|
||
},
|
||
];
|
||
|
||
function badgeConfiguration(variation) {
|
||
const configurations = [
|
||
{ label: "Active", color: "success", variant: "solid" },
|
||
{ label: "In review", color: "info", variant: "soft" },
|
||
{ label: "Attention", color: "danger", variant: "outline" },
|
||
{ label: "Neutral", color: "dark", variant: "white" },
|
||
{ label: "Category", color: "info", shape: "rounded" },
|
||
{ label: "Pill badge", color: "primary", shape: "pill" },
|
||
{
|
||
label: "This content is a little too long for the available space",
|
||
color: "info",
|
||
truncate: true,
|
||
maxWidth: "10rem",
|
||
shape: "rounded",
|
||
},
|
||
{ label: "Connected", color: "success", variant: "soft", dot: true },
|
||
{
|
||
label: "14.5%",
|
||
color: "success",
|
||
variant: "soft",
|
||
icon: "icon-[lucide--trending-up]",
|
||
},
|
||
{ label: "Design", color: "info", variant: "soft", dismissible: true },
|
||
{
|
||
label: "Christina",
|
||
color: "dark",
|
||
variant: "white",
|
||
avatarSrc: "/avatar-example-2.svg",
|
||
avatarAlt: "Christina",
|
||
dismissible: true,
|
||
},
|
||
{
|
||
label: "5",
|
||
color: "danger",
|
||
size: "xs",
|
||
anchorLabel: "Notifications",
|
||
anchorLabelText: "Open notifications",
|
||
placement: "inline",
|
||
},
|
||
{
|
||
label: "99+",
|
||
color: "danger",
|
||
size: "xs",
|
||
anchorIcon: "icon-[lucide--bell]",
|
||
anchorLabelText: "Open notifications",
|
||
placement: "top-right",
|
||
},
|
||
{
|
||
label: "",
|
||
color: "success",
|
||
dotOnly: true,
|
||
dotLabel: "New notification",
|
||
anchorIcon: "icon-[lucide--bell]",
|
||
anchorLabelText: "Open notifications",
|
||
placement: "top-right",
|
||
},
|
||
{
|
||
label: "9+",
|
||
color: "danger",
|
||
animated: true,
|
||
anchorIcon: "icon-[lucide--shopping-basket]",
|
||
anchorLabelText: "Open cart",
|
||
placement: "top-right",
|
||
},
|
||
];
|
||
return {
|
||
size: "md",
|
||
color: "primary",
|
||
variant: "solid",
|
||
shape: "pill",
|
||
...configurations[variation],
|
||
};
|
||
}
|
||
|
||
function alertConfiguration(variation) {
|
||
const title = alertTitles[variation] ?? alertTitles[0];
|
||
const lower = title.toLowerCase();
|
||
const solidColors = ["dark", "secondary", "info", "success", "danger", "warning", "light"];
|
||
const solidIndex = Math.min(variation, solidColors.length - 1);
|
||
const color =
|
||
variation < solidColors.length
|
||
? solidColors[solidIndex]
|
||
: lower.includes("danger")
|
||
? "danger"
|
||
: lower.includes("warning")
|
||
? "warning"
|
||
: lower.includes("success")
|
||
? "success"
|
||
: "info";
|
||
const supporting = lower.includes("supporting");
|
||
const discovery = lower.includes("discovery");
|
||
return {
|
||
title:
|
||
variation < solidColors.length
|
||
? `${titleCase(color)} alert!`
|
||
: supporting
|
||
? "Successfully updated."
|
||
: lower.includes("list")
|
||
? "A problem occurred while submitting your data."
|
||
: lower.includes("actions")
|
||
? "YouTube would like to send notifications"
|
||
: discovery
|
||
? "New version published"
|
||
: lower.includes("dismissible")
|
||
? "Requires JavaScript"
|
||
: "A new software update is available.",
|
||
description:
|
||
variation < solidColors.length
|
||
? "You should check in on some of those fields below."
|
||
: supporting
|
||
? "You have successfully updated your email preferences."
|
||
: lower.includes("actions")
|
||
? "Notifications may include alerts, sounds, and icon badges. These can be configured in Settings."
|
||
: discovery
|
||
? "A new version of this page was published. Refresh to see the changes."
|
||
: lower.includes("dismissible")
|
||
? "This page requires an optional plugin. You can skip this step if it is already installed."
|
||
: lower.includes("list")
|
||
? ""
|
||
: "See what is new in version 3.0.7.",
|
||
color,
|
||
variant:
|
||
variation < solidColors.length
|
||
? "solid"
|
||
: lower.includes("bordered")
|
||
? "bordered"
|
||
: supporting
|
||
? "accent"
|
||
: "soft",
|
||
showIcon: variation >= solidColors.length,
|
||
items: lower.includes("list")
|
||
? [
|
||
{ label: "This username is already in use" },
|
||
{ label: "Email field cannot be empty" },
|
||
{ label: "Please enter a valid phone number" },
|
||
]
|
||
: [],
|
||
actions: lower.includes("actions")
|
||
? [
|
||
{ label: "Don't allow", href: "#deny" },
|
||
{ label: "Allow", href: "#allow" },
|
||
]
|
||
: [],
|
||
actionLabel: lower.includes("dismissible") ? "Learn more" : "",
|
||
actionHref: lower.includes("dismissible") ? "#learn-more" : "",
|
||
linkLabel: lower.includes("right-aligned") ? "Details" : discovery ? "Refresh" : "",
|
||
linkHref: lower.includes("right-aligned") ? "#details" : discovery ? "#refresh" : "",
|
||
dismissible: lower.includes("dismissible"),
|
||
compact: variation < solidColors.length,
|
||
radius: variation < solidColors.length ? "lg" : lower.includes("discovery") ? "xl" : "md",
|
||
shadow: lower.includes("bordered") ? "none" : lower.includes("discovery") ? "md" : "sm",
|
||
};
|
||
}
|
||
|
||
function blockquoteConfiguration(variation) {
|
||
const configurations = [
|
||
{
|
||
quote:
|
||
"I just wanted to say that I'm very happy with my purchase so far. The documentation is outstanding - clear and detailed.",
|
||
size: "md",
|
||
color: "primary",
|
||
align: "left",
|
||
variant: "default",
|
||
quoteMark: true,
|
||
italic: true,
|
||
},
|
||
{
|
||
quote: "Good design makes the complex understandable and the useful feel effortless.",
|
||
citation: "Josh Grazioso",
|
||
citationTitle: "Product designer",
|
||
size: "sm",
|
||
color: "info",
|
||
align: "center",
|
||
variant: "default",
|
||
quoteMark: true,
|
||
italic: true,
|
||
},
|
||
{
|
||
quote:
|
||
"The strongest product experiences are built when clarity, craft, and empathy reinforce one another.",
|
||
citation: "Josh Grazioso",
|
||
citationTitle: "Source title",
|
||
citationUrl: "#blockquote-demo",
|
||
avatarSrc: "/avatar-example.svg",
|
||
avatarAlt: "Josh Grazioso",
|
||
size: "lg",
|
||
color: "success",
|
||
align: "left",
|
||
variant: "bordered",
|
||
quoteMark: false,
|
||
italic: true,
|
||
},
|
||
];
|
||
return configurations[variation] ?? configurations[0];
|
||
}
|
||
|
||
function accordionConfiguration(variation) {
|
||
const body =
|
||
"This is the item's accordion body. It is hidden by default until the section is opened. Use it for supporting details, explanations, or progressive disclosure.";
|
||
const items = [
|
||
{ value: "first", label: "Accordion #1", content: body },
|
||
{ value: "second", label: "Accordion #2", content: body },
|
||
{ value: "third", label: "Accordion #3", content: body },
|
||
];
|
||
const configurations = [
|
||
{ defaultOpen: ["first"], contentItalic: true },
|
||
{ defaultOpen: ["first", "second"], alwaysOpen: true, contentItalic: true },
|
||
{
|
||
defaultOpen: ["first", "first.sub-first"],
|
||
alwaysOpen: true,
|
||
contentItalic: true,
|
||
items: [
|
||
{
|
||
...items[0],
|
||
children: [
|
||
{ value: "sub-first", label: "Sub accordion #1", content: body },
|
||
{ value: "sub-second", label: "Sub accordion #2", content: body },
|
||
{ value: "sub-third", label: "Sub accordion #3", content: body },
|
||
],
|
||
},
|
||
items[1],
|
||
items[2],
|
||
],
|
||
},
|
||
{ defaultOpen: ["first"], showIndicator: false, contentItalic: true },
|
||
{
|
||
defaultOpen: ["first"],
|
||
indicator: "chevron",
|
||
indicatorPosition: "start",
|
||
variant: "chevron",
|
||
contentItalic: true,
|
||
},
|
||
{
|
||
defaultOpen: ["first"],
|
||
indicator: "chevron",
|
||
indicatorPosition: "end",
|
||
variant: "chevron",
|
||
contentItalic: true,
|
||
},
|
||
{ defaultOpen: ["first"], bordered: true, contentItalic: true },
|
||
{
|
||
defaultOpen: ["second"],
|
||
separated: true,
|
||
indicatorPosition: "end",
|
||
contentItalic: true,
|
||
},
|
||
{
|
||
defaultOpen: ["first"],
|
||
items: [items[0], { ...items[1], disabled: true }, items[2]],
|
||
contentItalic: true,
|
||
},
|
||
];
|
||
|
||
return {
|
||
id: `accordion-example-${variation + 1}`,
|
||
items,
|
||
color: "primary",
|
||
size: "default",
|
||
...configurations[variation],
|
||
};
|
||
}
|
||
|
||
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 buttonGroupUses = [
|
||
{
|
||
eyebrow: "Basic usage",
|
||
title: "Example",
|
||
description: "Group related actions into one compact, visually connected control.",
|
||
},
|
||
{
|
||
eyebrow: "Sizes",
|
||
title: "Small",
|
||
description: "Use small grouped buttons in dense toolbars, tables, and utility surfaces.",
|
||
},
|
||
{
|
||
eyebrow: "Sizes",
|
||
title: "Medium",
|
||
description: "Use the balanced medium size for most product interfaces.",
|
||
},
|
||
{
|
||
eyebrow: "Sizes",
|
||
title: "Large",
|
||
description: "Use large grouped buttons for prominent actions and touch-focused layouts.",
|
||
},
|
||
{
|
||
eyebrow: "Responsive stack",
|
||
title: "Responsive stack",
|
||
description: "Keep actions inline on larger screens and stack them on narrow viewports.",
|
||
},
|
||
{
|
||
eyebrow: "Orientation",
|
||
title: "Vertical stack",
|
||
description: "Stack connected actions vertically while preserving joined borders.",
|
||
},
|
||
{
|
||
eyebrow: "Toolbar",
|
||
title: "Button toolbar",
|
||
description: "Expose a labeled toolbar with selectable formatting actions.",
|
||
},
|
||
];
|
||
|
||
function buttonGroupConfiguration(variation) {
|
||
const actions = [
|
||
{ label: "Profile", value: "profile" },
|
||
{ label: "Settings", value: "settings" },
|
||
{ label: "Messages", value: "messages" },
|
||
];
|
||
const configurations = [
|
||
{ items: actions, value: "profile", selectable: true },
|
||
{ items: actions, size: "sm" },
|
||
{ items: actions, size: "md" },
|
||
{ items: actions, size: "lg" },
|
||
{
|
||
items: [
|
||
{ label: "Save draft", value: "draft" },
|
||
{ label: "Preview", value: "preview" },
|
||
{ label: "Publish", value: "publish" },
|
||
],
|
||
responsive: true,
|
||
},
|
||
{
|
||
items: [
|
||
{ label: "Account", value: "account" },
|
||
{ label: "Security", value: "security" },
|
||
{ label: "Notifications", value: "notifications" },
|
||
],
|
||
orientation: "vertical",
|
||
},
|
||
{
|
||
items: [
|
||
{ label: "Bold", value: "bold", icon: "icon-[lucide--bold]" },
|
||
{ label: "Italic", value: "italic", icon: "icon-[lucide--italic]" },
|
||
{ label: "Underline", value: "underline", icon: "icon-[lucide--underline]" },
|
||
],
|
||
value: "bold",
|
||
selectable: true,
|
||
toolbar: true,
|
||
ariaLabel: "Text formatting",
|
||
},
|
||
];
|
||
|
||
return {
|
||
color: "primary",
|
||
variant: "outline",
|
||
size: "md",
|
||
orientation: "horizontal",
|
||
attached: true,
|
||
...configurations[variation],
|
||
};
|
||
}
|
||
|
||
const cardUses = [
|
||
{
|
||
eyebrow: "Basic usage",
|
||
title: "Default card",
|
||
description: "Use a basic card with media, text content, and a supporting action.",
|
||
},
|
||
{
|
||
eyebrow: "Content",
|
||
title: "Body",
|
||
description: "Use the card body for the main content area.",
|
||
},
|
||
{
|
||
eyebrow: "Content",
|
||
title: "Simple card",
|
||
description: "Use a body-only card when no separate header or footer is needed.",
|
||
},
|
||
{
|
||
eyebrow: "Structure",
|
||
title: "Header and footer",
|
||
description: "Add a header, footer, or both when the card needs clearer structure.",
|
||
},
|
||
{
|
||
eyebrow: "Structure",
|
||
title: "Footer only",
|
||
description: "Separate secondary actions or summary details from the card body.",
|
||
},
|
||
{
|
||
eyebrow: "Sizes",
|
||
title: "Small",
|
||
description: "Use a compact card size for denser lists and side panels.",
|
||
},
|
||
{
|
||
eyebrow: "Sizes",
|
||
title: "Default",
|
||
description: "Use the default card size for most content blocks.",
|
||
},
|
||
{
|
||
eyebrow: "Sizes",
|
||
title: "Large",
|
||
description: "Increase card spacing when content needs more breathing room.",
|
||
},
|
||
{
|
||
eyebrow: "Navigation",
|
||
title: "Navigation",
|
||
description: "Add navigation in the card header for quick section switching.",
|
||
},
|
||
{
|
||
eyebrow: "Navigation",
|
||
title: "Navigation with select on mobile",
|
||
description: "Replace tabs with a select control when horizontal space is limited.",
|
||
},
|
||
{
|
||
eyebrow: "Images",
|
||
title: "Top image",
|
||
description: "Place an image at the top for a classic media-card layout.",
|
||
},
|
||
{
|
||
eyebrow: "Images",
|
||
title: "Bottom image",
|
||
description: "Let content lead before media displayed at the bottom.",
|
||
},
|
||
{
|
||
eyebrow: "Images",
|
||
title: "Image overlays",
|
||
description: "Use the image as a background and layer card content on top.",
|
||
},
|
||
{
|
||
eyebrow: "Animations",
|
||
title: "Image scaling animation on hover",
|
||
description: "Scale card media on hover without changing the card layout.",
|
||
},
|
||
{
|
||
eyebrow: "Animations",
|
||
title: "Transition on hover",
|
||
description: "Animate elevation and position to strengthen the interactive surface.",
|
||
},
|
||
{
|
||
eyebrow: "Card layout",
|
||
title: "Horizontal",
|
||
description: "Place image and content side by side on larger screens.",
|
||
},
|
||
{
|
||
eyebrow: "Card layout",
|
||
title: "Card group",
|
||
description: "Render attached equal-height cards that stack responsively.",
|
||
},
|
||
{
|
||
eyebrow: "Panels",
|
||
title: "Top bordered card",
|
||
description: "Add a colored top border for stronger category or status emphasis.",
|
||
},
|
||
{
|
||
eyebrow: "Panels",
|
||
title: "Card actions",
|
||
description: "Place compact utility actions in a structured card header.",
|
||
},
|
||
{
|
||
eyebrow: "Panels",
|
||
title: "Card with alert",
|
||
description: "Embed an alert inside a card to surface important feedback.",
|
||
},
|
||
{
|
||
eyebrow: "Panels",
|
||
title: "Centered body content",
|
||
description: "Center content for confirmation states and compact summaries.",
|
||
},
|
||
{
|
||
eyebrow: "Panels",
|
||
title: "Empty state",
|
||
description: "Communicate that no data is available with an accessible empty state.",
|
||
},
|
||
{
|
||
eyebrow: "Panels",
|
||
title: "Scrollable body",
|
||
description: "Contain long content inside a fixed-height scrolling card body.",
|
||
},
|
||
];
|
||
|
||
function cardConfiguration(variation) {
|
||
const image = "/card-landscape.svg";
|
||
const description =
|
||
"Some quick example text to build on the card title and make up the bulk of the card's content.";
|
||
const navigation = [
|
||
{ label: "My Account", value: "account" },
|
||
{ label: "Company", value: "company" },
|
||
{ label: "Team Members", value: "team" },
|
||
{ label: "Billing", value: "billing" },
|
||
];
|
||
const base = {
|
||
title: "Card title",
|
||
description,
|
||
actionLabel: "Card link",
|
||
actionHref: "#card-demo",
|
||
size: "md",
|
||
color: "primary",
|
||
};
|
||
const configurations = [
|
||
{ imageSrc: image, imageAlt: "Abstract blue glass shapes", actionLabel: "Go somewhere" },
|
||
{ title: "", description, actionLabel: "" },
|
||
{ subtitle: "Card subtitle" },
|
||
{ header: "Featured", footer: "Last updated 5 mins ago" },
|
||
{ footer: "Last updated 5 mins ago" },
|
||
{ size: "sm" },
|
||
{ size: "md" },
|
||
{ size: "lg" },
|
||
{ navigation: navigation.slice(0, 3), activeNav: "account", align: "center" },
|
||
{ navigation, activeNav: "account", mobileNavigation: true, align: "center" },
|
||
{ imageSrc: image, imageAlt: "Abstract blue glass shapes", footer: "Last updated 5 mins ago" },
|
||
{
|
||
imageSrc: image,
|
||
imageAlt: "Abstract blue glass shapes",
|
||
imagePosition: "bottom",
|
||
footer: "Last updated 5 mins ago",
|
||
},
|
||
{
|
||
imageSrc: image,
|
||
imageAlt: "Abstract blue glass shapes",
|
||
imagePosition: "overlay",
|
||
footer: "Last updated 5 mins ago",
|
||
actionLabel: "",
|
||
},
|
||
{ imageSrc: image, imageAlt: "Abstract blue glass shapes", hover: "image", actionLabel: "" },
|
||
{ imageSrc: image, imageAlt: "Abstract blue glass shapes", hover: "raise", actionLabel: "" },
|
||
{
|
||
imageSrc: image,
|
||
imageAlt: "Abstract blue glass shapes",
|
||
layout: "horizontal",
|
||
footer: "Last updated 5 mins ago",
|
||
actionLabel: "",
|
||
},
|
||
{
|
||
items: [
|
||
{ imageSrc: image, title: "Card title", description, footer: "Updated 5 mins ago" },
|
||
{
|
||
imageSrc: image,
|
||
title: "Card title",
|
||
description: "This card has supporting text below as a natural lead-in.",
|
||
footer: "Updated 5 mins ago",
|
||
},
|
||
{
|
||
imageSrc: image,
|
||
title: "Card title",
|
||
description: `${description} This has even longer content to demonstrate equal height.`,
|
||
footer: "Updated 5 mins ago",
|
||
},
|
||
],
|
||
actionLabel: "",
|
||
},
|
||
{ variant: "top-border" },
|
||
{
|
||
header: "Card actions",
|
||
headerActions: [
|
||
{ label: "Refresh", ariaLabel: "Refresh card", icon: "icon-[lucide--refresh-cw]" },
|
||
{ label: "Expand", ariaLabel: "Expand card", icon: "icon-[lucide--maximize-2]" },
|
||
],
|
||
dismissible: true,
|
||
},
|
||
{
|
||
header: "Featured",
|
||
alertTitle: "Attention needed!",
|
||
alertDescription: "This is an alert box.",
|
||
},
|
||
{ align: "center" },
|
||
{ empty: true, title: "", description: "", actionLabel: "" },
|
||
{
|
||
description: `${description} ${description} ${description} ${description} ${description}`,
|
||
scrollable: true,
|
||
maxHeight: "16rem",
|
||
actionLabel: "",
|
||
},
|
||
];
|
||
|
||
return { ...base, ...configurations[variation] };
|
||
}
|
||
|
||
const carouselUses = [
|
||
{
|
||
eyebrow: "Basic usage",
|
||
title: "Basic usage",
|
||
description:
|
||
"Cycle through images or content slides with accessible previous and next controls.",
|
||
},
|
||
{
|
||
eyebrow: "Navigation",
|
||
title: "Pagination",
|
||
description: "Show the current slide and let users jump directly to any slide.",
|
||
},
|
||
{
|
||
eyebrow: "Playback",
|
||
title: "Autoplay",
|
||
description: "Advance automatically, pausing while users hover or interact with the carousel.",
|
||
},
|
||
{
|
||
eyebrow: "Playback",
|
||
title: "Infinite loop",
|
||
description: "Continue from the last slide back to the first without disabling controls.",
|
||
},
|
||
{
|
||
eyebrow: "Direction",
|
||
title: "RTL",
|
||
description: "Reverse movement and controls for right-to-left interfaces.",
|
||
},
|
||
{
|
||
eyebrow: "Layout",
|
||
title: "Multiple slides",
|
||
description:
|
||
"Keep several slides visible at once for products, articles, or media collections.",
|
||
},
|
||
{
|
||
eyebrow: "Layout",
|
||
title: "Centered",
|
||
description: "Keep a compact active slide centered while adjacent content remains visible.",
|
||
},
|
||
{
|
||
eyebrow: "Pointer interaction",
|
||
title: "Draggable",
|
||
description: "Drag or swipe slides with a pointer while preserving keyboard controls.",
|
||
},
|
||
{
|
||
eyebrow: "Layout",
|
||
title: "Auto height",
|
||
description: "Let each slide determine the carousel height for variable-length content.",
|
||
},
|
||
{
|
||
eyebrow: "Scrolling",
|
||
title: "Snap scrolling",
|
||
description: "Use native horizontal scrolling with mandatory centered snap points.",
|
||
},
|
||
{
|
||
eyebrow: "Status",
|
||
title: "Slide counter",
|
||
description: "Announce and display the current position and total slide count.",
|
||
},
|
||
{
|
||
eyebrow: "Thumbnails",
|
||
title: "Thumbnails (horizontal)",
|
||
description: "Use a horizontal thumbnail rail as an alternate set of slide controls.",
|
||
},
|
||
{
|
||
eyebrow: "Thumbnails",
|
||
title: "Thumbnails (vertical)",
|
||
description: "Place thumbnail navigation beside the carousel on layouts with more room.",
|
||
},
|
||
];
|
||
|
||
function carouselConfiguration(variation) {
|
||
const items = [
|
||
{
|
||
label: "First slide",
|
||
title: "First slide",
|
||
description: "Introduce the collection with a concise, focused message.",
|
||
},
|
||
{
|
||
label: "Second slide",
|
||
title: "Second slide",
|
||
description: "Move through content using buttons, pagination, keyboard, or touch.",
|
||
},
|
||
{
|
||
label: "Third slide",
|
||
title: "Third slide",
|
||
description: "Every interaction exposes useful public lifecycle and navigation events.",
|
||
},
|
||
{
|
||
label: "Fourth slide",
|
||
title: "Fourth slide",
|
||
description: "Flexible for larger collections.",
|
||
},
|
||
{ label: "Fifth slide", title: "Fifth slide", description: "Responsive at every viewport." },
|
||
{ label: "Sixth slide", title: "Sixth slide", description: "Built with accessible semantics." },
|
||
];
|
||
const configurations = [
|
||
{},
|
||
{ showPagination: true },
|
||
{ showPagination: true, isAutoPlay: true, autoplayInterval: 2500 },
|
||
{ showPagination: true, isInfiniteLoop: true },
|
||
{ showPagination: true, isRTL: true, activeIndex: 1 },
|
||
{ showPagination: true, slidesPerView: 3, activeIndex: 3 },
|
||
{ showPagination: true, slidesPerView: 2, isCentered: true },
|
||
{ showPagination: true, slidesPerView: 3, isDraggable: true, activeIndex: 1 },
|
||
{ showPagination: true, isAutoHeight: true },
|
||
{ showPagination: true, slidesPerView: 2, isSnap: true },
|
||
{ showCounter: true },
|
||
{ thumbnails: "horizontal" },
|
||
{ thumbnails: "vertical" },
|
||
];
|
||
|
||
return {
|
||
items,
|
||
title: "",
|
||
description: "",
|
||
ariaLabel: carouselUses[variation].title,
|
||
color: "primary",
|
||
size: "default",
|
||
...configurations[variation],
|
||
};
|
||
}
|
||
|
||
const chatBubbleUses = [
|
||
{
|
||
eyebrow: "Basic usage",
|
||
title: "Conversation",
|
||
description: "Show a basic two-sided conversation with incoming and outgoing messages.",
|
||
},
|
||
{
|
||
eyebrow: "Layout",
|
||
title: "One-sided",
|
||
description: "Use one-sided bubbles for support transcripts or single-direction message feeds.",
|
||
},
|
||
{
|
||
eyebrow: "Identity",
|
||
title: "Messages with avatars",
|
||
description: "Add avatars when people need to identify message ownership at a glance.",
|
||
},
|
||
{
|
||
eyebrow: "Message details",
|
||
title: "Message metadata",
|
||
description: "Add delivery state, timestamps, or recovery actions beneath each message.",
|
||
},
|
||
];
|
||
|
||
function chatBubbleConfiguration(variation) {
|
||
const items = [
|
||
{
|
||
id: "question",
|
||
direction: "incoming",
|
||
author: "Support agent",
|
||
title: "How can we help?",
|
||
text: "You can ask questions like:",
|
||
bullets: [
|
||
"What's WireJS UI?",
|
||
"How many component examples are there?",
|
||
"Is there a PRO version?",
|
||
],
|
||
},
|
||
{
|
||
id: "request",
|
||
direction: "outgoing",
|
||
author: "AZ",
|
||
text: "what's wirejs ui?",
|
||
},
|
||
{
|
||
id: "answer",
|
||
direction: "incoming",
|
||
author: "Support agent",
|
||
text: "WireJS UI is an open-source set of reusable UI components built for responsive applications.",
|
||
links: [
|
||
{ label: "Installation guide", href: "/installation" },
|
||
{ label: "Framework guides", href: "/framework-guides" },
|
||
],
|
||
},
|
||
];
|
||
|
||
if (variation >= 2) {
|
||
items[0].avatarSrc = "/avatar-example.svg";
|
||
items[0].avatarAlt = "Support agent";
|
||
items[0].avatarLabel = "View support agent";
|
||
items[1].avatarFallback = "AZ";
|
||
items[1].avatarLabel = "View AZ";
|
||
items[2].avatarSrc = "/avatar-example.svg";
|
||
items[2].avatarAlt = "Support agent";
|
||
items[2].avatarLabel = "View support agent";
|
||
}
|
||
|
||
if (variation === 3) {
|
||
items[0].status = "Sent";
|
||
items[0].statusIcon = "icon-[lucide--check-check]";
|
||
items[0].timestamp = "10:32";
|
||
items[0].datetime = "2026-07-28T10:32:00";
|
||
items[1].status = "Read";
|
||
items[1].statusIcon = "icon-[lucide--check-check]";
|
||
items[1].timestamp = "10:33";
|
||
items[1].datetime = "2026-07-28T10:33:00";
|
||
items[2].status = "Not sent";
|
||
items[2].statusIcon = "icon-[lucide--circle-alert]";
|
||
items[2].statusTone = "danger";
|
||
items[2].action = "retry";
|
||
items[2].actionLabel = "Retry";
|
||
}
|
||
|
||
return {
|
||
items,
|
||
oneSided: variation === 1,
|
||
showAvatars: variation >= 2,
|
||
showMetadata: variation === 3,
|
||
ariaLabel: chatBubbleUses[variation].title,
|
||
title: "",
|
||
description: "",
|
||
color: "primary",
|
||
size: "default",
|
||
};
|
||
}
|
||
|
||
const collapseUses = [
|
||
{
|
||
eyebrow: "Basic usage",
|
||
title: "Default collapse",
|
||
description: "Use a button trigger to expand and collapse supporting content.",
|
||
},
|
||
{
|
||
eyebrow: "Inline content",
|
||
title: "Read more toggle",
|
||
description: "Reveal additional copy inline with a read more and read less trigger.",
|
||
},
|
||
];
|
||
|
||
function collapseConfiguration(variation) {
|
||
const configurations = [
|
||
{
|
||
mode: "panel",
|
||
items: [
|
||
{
|
||
id: "collapse-default-panel",
|
||
label: "Collapse",
|
||
closeLabel: "Collapse",
|
||
content:
|
||
"This is a collapse body. It is hidden by default until the user expands it, keeping the surrounding interface focused.",
|
||
},
|
||
],
|
||
},
|
||
{
|
||
mode: "inline",
|
||
items: [
|
||
{
|
||
id: "collapse-read-more-panel",
|
||
label: "Read more",
|
||
closeLabel: "Read less",
|
||
preview:
|
||
"WireJS UI is an open-source set of reusable components based on a utility-first styling system.",
|
||
content:
|
||
"This additional collapse body is hidden by default. Expanding it reveals supporting information while preserving a compact reading experience.",
|
||
},
|
||
],
|
||
},
|
||
];
|
||
|
||
return {
|
||
...configurations[variation],
|
||
multiple: false,
|
||
initialOpenIndexes: [],
|
||
ariaLabel: collapseUses[variation].title,
|
||
color: "primary",
|
||
size: "default",
|
||
};
|
||
}
|
||
|
||
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.",
|
||
},
|
||
];
|
||
|
||
const toggleCountUses = [
|
||
{
|
||
eyebrow: "Pricing",
|
||
title: "Monthly and annual pricing",
|
||
description: "Compare plan prices with an aligned segmented control and clear selected state.",
|
||
},
|
||
{
|
||
eyebrow: "Pricing",
|
||
title: "Switch control",
|
||
description: "Use a compact switch between labels while updating every displayed plan value.",
|
||
},
|
||
{
|
||
eyebrow: "Configuration",
|
||
title: "Custom billing periods",
|
||
description: "Configure custom values, labels, currency, suffixes, and plan descriptions.",
|
||
},
|
||
{
|
||
eyebrow: "States",
|
||
title: "Disabled pricing toggle",
|
||
description: "Keep the current comparison visible while preventing billing-period changes.",
|
||
},
|
||
];
|
||
|
||
function toggleCountConfiguration(variation) {
|
||
const sharedItems = [
|
||
{ label: "Startup", monthly: 19, annual: 190 },
|
||
{ label: "Team", monthly: 89, annual: 890 },
|
||
{ label: "Enterprise", monthly: 129, annual: 1290 },
|
||
];
|
||
const configurations = [
|
||
{
|
||
name: "pricing-cycle",
|
||
value: "annual",
|
||
variant: "segmented",
|
||
align: "end",
|
||
items: sharedItems,
|
||
},
|
||
{
|
||
name: "pricing-switch",
|
||
value: "monthly",
|
||
variant: "switch",
|
||
align: "center",
|
||
items: sharedItems,
|
||
},
|
||
{
|
||
name: "contract-length",
|
||
value: "quarterly",
|
||
firstValue: "quarterly",
|
||
firstLabel: "Quarterly",
|
||
secondValue: "yearly",
|
||
secondLabel: "Yearly",
|
||
firstValueKey: "quarterly",
|
||
secondValueKey: "yearly",
|
||
currency: "€",
|
||
suffix: "/seat",
|
||
items: [
|
||
{
|
||
label: "Core",
|
||
description: "For small product teams",
|
||
quarterly: 29,
|
||
yearly: 22,
|
||
},
|
||
{
|
||
label: "Scale",
|
||
description: "For growing organizations",
|
||
quarterly: 79,
|
||
yearly: 62,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
name: "disabled-pricing-cycle",
|
||
value: "annual",
|
||
variant: "segmented",
|
||
align: "end",
|
||
items: sharedItems,
|
||
disabled: true,
|
||
},
|
||
];
|
||
|
||
return {
|
||
color: "primary",
|
||
size: "default",
|
||
...configurations[variation],
|
||
};
|
||
}
|
||
|
||
const basicFormComponentNames = new Set([
|
||
"Checkbox",
|
||
"ColorPicker",
|
||
"FileInput",
|
||
"Input",
|
||
"InputGroup",
|
||
"Radio",
|
||
"RangeSlider",
|
||
"Select",
|
||
"Switch",
|
||
"Textarea",
|
||
"TimePicker",
|
||
]);
|
||
|
||
const basicFormUses = [
|
||
{
|
||
eyebrow: "Basic usage",
|
||
title: "Default field",
|
||
description: "Use a labelled field with a value, placeholder, and supporting helper text.",
|
||
},
|
||
{
|
||
eyebrow: "Appearance",
|
||
title: "Outlined with start icon",
|
||
description: "Combine an outlined treatment with a leading icon.",
|
||
},
|
||
{
|
||
eyebrow: "Appearance",
|
||
title: "Floating label",
|
||
description: "Move the label into the control while preserving an accessible field name.",
|
||
},
|
||
{
|
||
eyebrow: "Appearance",
|
||
title: "Pill and compact size",
|
||
description: "Use pill styling and compact sizing in dense interfaces.",
|
||
},
|
||
{
|
||
eyebrow: "States",
|
||
title: "Read only",
|
||
description: "Display a submitted value without allowing it to be changed.",
|
||
},
|
||
{
|
||
eyebrow: "States",
|
||
title: "Disabled",
|
||
description: "Prevent interaction and communicate that the field is unavailable.",
|
||
},
|
||
{
|
||
eyebrow: "Layout",
|
||
title: "Inline helper text",
|
||
description: "Place the label, control, and supporting text on one responsive line.",
|
||
},
|
||
{
|
||
eyebrow: "Layout",
|
||
title: "Field with corner hint",
|
||
description: "Place optional context opposite the label and keep the control on the next line.",
|
||
},
|
||
{
|
||
eyebrow: "Validation",
|
||
title: "Validated field",
|
||
description:
|
||
"Surface validation through @wrnexus/validation-compatible error targets and accessible state.",
|
||
},
|
||
];
|
||
|
||
const radioUses = [
|
||
{
|
||
eyebrow: "Basic usage",
|
||
title: "Default radio",
|
||
description: "Use a single radio control for one selectable option.",
|
||
},
|
||
{
|
||
eyebrow: "States",
|
||
title: "Disabled",
|
||
description: "Disable an option when it is unavailable.",
|
||
},
|
||
{
|
||
eyebrow: "Groups",
|
||
title: "Inline radio group",
|
||
description: "Arrange compact radio options on one horizontal line.",
|
||
},
|
||
{
|
||
eyebrow: "Groups",
|
||
title: "Vertical radio group",
|
||
description: "Stack radio options for easy scanning.",
|
||
},
|
||
{
|
||
eyebrow: "Content",
|
||
title: "Radios with descriptions",
|
||
description: "Add supporting copy to clarify each choice.",
|
||
},
|
||
{
|
||
eyebrow: "Cards",
|
||
title: "Radio cards",
|
||
description: "Present options as selectable horizontal cards.",
|
||
},
|
||
{
|
||
eyebrow: "Cards",
|
||
title: "Vertical radio cards",
|
||
description: "Stack selectable cards in a vertical layout.",
|
||
},
|
||
{
|
||
eyebrow: "Alignment",
|
||
title: "Right-aligned radio",
|
||
description: "Place controls at the far edge of their labels.",
|
||
},
|
||
{
|
||
eyebrow: "Lists",
|
||
title: "Radio list group",
|
||
description: "Separate options with list dividers.",
|
||
},
|
||
{
|
||
eyebrow: "Lists",
|
||
title: "Horizontal radio list group",
|
||
description: "Combine list styling with a horizontal layout.",
|
||
},
|
||
{
|
||
eyebrow: "Validation",
|
||
title: "Validation states",
|
||
description: "Validate the selected group with @wrnexus/validation.",
|
||
},
|
||
];
|
||
|
||
const checkboxUses = [
|
||
{
|
||
eyebrow: "Basic usage",
|
||
title: "Default checkbox",
|
||
description: "Use a checkbox for an independent yes-or-no choice.",
|
||
},
|
||
{
|
||
eyebrow: "States",
|
||
title: "Indeterminate",
|
||
description: "Represent a partially selected group with the mixed state.",
|
||
},
|
||
{
|
||
eyebrow: "States",
|
||
title: "Disabled",
|
||
description: "Disable a checkbox when its choice is unavailable.",
|
||
},
|
||
{
|
||
eyebrow: "Groups",
|
||
title: "Checkbox group options",
|
||
description: "Render passed options as a named checkbox group.",
|
||
},
|
||
{
|
||
eyebrow: "Content",
|
||
title: "Checkboxes with descriptions",
|
||
description: "Add supporting copy to clarify each choice.",
|
||
},
|
||
{
|
||
eyebrow: "Cards",
|
||
title: "Checkbox cards",
|
||
description: "Present multiple choices as selectable horizontal cards.",
|
||
},
|
||
{
|
||
eyebrow: "Cards",
|
||
title: "Vertical checkbox cards",
|
||
description: "Stack selectable checkbox cards vertically.",
|
||
},
|
||
{
|
||
eyebrow: "Alignment",
|
||
title: "Right-aligned checkbox",
|
||
description: "Place checkbox controls at the far edge of their labels.",
|
||
},
|
||
{
|
||
eyebrow: "Lists",
|
||
title: "Checkbox list group",
|
||
description: "Separate checkbox options with list dividers.",
|
||
},
|
||
{
|
||
eyebrow: "Lists",
|
||
title: "Horizontal checkbox list group",
|
||
description: "Combine list styling with a horizontal layout.",
|
||
},
|
||
{
|
||
eyebrow: "Validation",
|
||
title: "Validation states",
|
||
description: "Validate required checkbox choices with @wrnexus/validation.",
|
||
},
|
||
];
|
||
|
||
const fileUploadProgressUses = [
|
||
{
|
||
eyebrow: "Basic usage",
|
||
title: "Uploading file",
|
||
description: "Show an active upload with file metadata and a cancel action.",
|
||
},
|
||
{
|
||
eyebrow: "Progress",
|
||
title: "Starting upload",
|
||
description: "Communicate that a newly queued file has begun uploading.",
|
||
},
|
||
{
|
||
eyebrow: "Progress",
|
||
title: "Nearly complete",
|
||
description: "Keep progress and transferred size readable near completion.",
|
||
},
|
||
{
|
||
eyebrow: "States",
|
||
title: "Upload complete",
|
||
description: "Confirm a successful upload with a clear completion state.",
|
||
},
|
||
{
|
||
eyebrow: "States",
|
||
title: "Upload failed",
|
||
description: "Explain failure and provide a retry action.",
|
||
},
|
||
{
|
||
eyebrow: "States",
|
||
title: "Upload cancelled",
|
||
description: "Preserve file context and allow a cancelled upload to restart.",
|
||
},
|
||
];
|
||
|
||
function fileUploadProgressConfiguration(variation) {
|
||
const configurations = [
|
||
{
|
||
fileName: "design-system.zip",
|
||
label: "Uploading archive",
|
||
uploadedSize: "8.4 MB",
|
||
fileSize: "20 MB",
|
||
value: 42,
|
||
max: 100,
|
||
status: "uploading",
|
||
},
|
||
{
|
||
fileName: "product-images.tar",
|
||
label: "Preparing upload",
|
||
uploadedSize: "640 KB",
|
||
fileSize: "48 MB",
|
||
value: 2,
|
||
max: 100,
|
||
status: "uploading",
|
||
},
|
||
{
|
||
fileName: "launch-video.mp4",
|
||
label: "Uploading video",
|
||
uploadedSize: "184 MB",
|
||
fileSize: "200 MB",
|
||
value: 92,
|
||
max: 100,
|
||
status: "uploading",
|
||
},
|
||
{
|
||
fileName: "quarterly-report.pdf",
|
||
label: "Uploaded successfully",
|
||
uploadedSize: "4.8 MB",
|
||
fileSize: "4.8 MB",
|
||
value: 100,
|
||
max: 100,
|
||
status: "complete",
|
||
},
|
||
{
|
||
fileName: "customer-export.csv",
|
||
label: "Network connection lost",
|
||
uploadedSize: "12 MB",
|
||
fileSize: "36 MB",
|
||
value: 33,
|
||
max: 100,
|
||
status: "error",
|
||
},
|
||
{
|
||
fileName: "brand-assets.fig",
|
||
label: "Upload cancelled",
|
||
uploadedSize: "16 MB",
|
||
fileSize: "72 MB",
|
||
value: 22,
|
||
max: 100,
|
||
status: "cancelled",
|
||
},
|
||
];
|
||
return configurations[variation] || configurations[0];
|
||
}
|
||
|
||
function basicFormConfiguration(componentName, variation) {
|
||
const validationVariation = componentName === "Radio" || componentName === "Checkbox" ? 10 : 8;
|
||
const common = {
|
||
id: `${baseSlugOf(componentName)}-${variation + 1}`,
|
||
name:
|
||
variation === validationVariation ? "validatedField" : `${baseSlugOf(componentName)}Field`,
|
||
label: displayName(componentName),
|
||
placeholder: `Enter ${displayName(componentName).toLowerCase()}`,
|
||
helperText:
|
||
variation === 0 || variation === 6
|
||
? variation === 6
|
||
? "We'll never share your details."
|
||
: "Helpful context for this field."
|
||
: "",
|
||
cornerHint: variation === 7 ? "Optional" : "",
|
||
variant:
|
||
variation === 1
|
||
? "outlined"
|
||
: variation === 2
|
||
? "floating"
|
||
: variation === 3
|
||
? "pill"
|
||
: "normal",
|
||
icon:
|
||
variation === 1
|
||
? "icon-[lucide--sparkles]"
|
||
: variation === 2
|
||
? "icon-[lucide--circle-user-round]"
|
||
: "",
|
||
iconPosition: variation === 2 ? "end" : "start",
|
||
size: variation === 3 ? "sm" : "default",
|
||
readonly: variation === 4,
|
||
disabled:
|
||
componentName === "Radio"
|
||
? variation === 1
|
||
: componentName === "Checkbox"
|
||
? variation === 2
|
||
: variation === 5,
|
||
inline: variation === 6,
|
||
required: variation === validationVariation,
|
||
error: "",
|
||
color: "primary",
|
||
};
|
||
const byComponent = {
|
||
Checkbox: {
|
||
value: "terms",
|
||
values: variation >= 3 ? ["product"] : [],
|
||
checked: variation === 0,
|
||
indeterminate: variation === 1,
|
||
label: variation < 3 ? "Accept notifications" : "Notification preferences",
|
||
options:
|
||
variation >= 3
|
||
? [
|
||
{
|
||
label: "Product updates",
|
||
value: "product",
|
||
description: variation >= 4 ? "News about features and improvements." : "",
|
||
},
|
||
{
|
||
label: "Account activity",
|
||
value: "account",
|
||
description: variation >= 4 ? "Important security and billing alerts." : "",
|
||
},
|
||
{
|
||
label: "Partner offers",
|
||
value: "partners",
|
||
description: variation >= 4 ? "Relevant offers from trusted partners." : "",
|
||
},
|
||
]
|
||
: [],
|
||
orientation: [3, 5, 9].includes(variation) ? "horizontal" : "vertical",
|
||
card: variation === 5 || variation === 6,
|
||
rightAligned: variation === 7,
|
||
list: variation === 8 || variation === 9,
|
||
helperText: variation === 10 ? "Select at least one notification type." : "",
|
||
},
|
||
ColorPicker: { value: "#2563eb", label: "Brand color" },
|
||
FileInput: {
|
||
accept: "image/png,image/jpeg",
|
||
label: "Profile image",
|
||
placeholder: "Choose an image",
|
||
},
|
||
Input: {
|
||
value: variation === 4 ? "readonly@example.com" : "",
|
||
label: "Email address",
|
||
type: "email",
|
||
autocomplete: "email",
|
||
},
|
||
InputGroup: {
|
||
value: variation === 4 ? "wirejs" : "",
|
||
label: "Workspace",
|
||
startText: "https://",
|
||
endText: ".app",
|
||
actionLabel: variation === 0 ? "Check" : "",
|
||
},
|
||
Radio: {
|
||
value: variation === 0 ? "standard" : "",
|
||
checked: variation === 0,
|
||
label: variation === 0 ? "Standard plan" : "Choose a plan",
|
||
options:
|
||
variation >= 2
|
||
? [
|
||
{
|
||
label: "Starter",
|
||
value: "starter",
|
||
description: variation >= 4 ? "For individuals and small projects." : "",
|
||
},
|
||
{
|
||
label: "Professional",
|
||
value: "professional",
|
||
description: variation >= 4 ? "For growing teams and products." : "",
|
||
},
|
||
{
|
||
label: "Enterprise",
|
||
value: "enterprise",
|
||
description: variation >= 4 ? "For organizations needing advanced controls." : "",
|
||
},
|
||
]
|
||
: [],
|
||
orientation: [2, 5, 9].includes(variation) ? "horizontal" : "vertical",
|
||
card: variation === 5 || variation === 6,
|
||
rightAligned: variation === 7,
|
||
list: variation === 8 || variation === 9,
|
||
helperText: variation === 10 ? "Select one plan to continue." : "",
|
||
},
|
||
RangeSlider: {
|
||
value: 60,
|
||
min: 0,
|
||
max: 100,
|
||
step: 5,
|
||
showBounds: true,
|
||
showSteps: variation === 1,
|
||
marks:
|
||
variation === 1
|
||
? [
|
||
{ value: 0, label: "0" },
|
||
{ value: 25, label: "25" },
|
||
{ value: 50, label: "50" },
|
||
{ value: 75, label: "75" },
|
||
{ value: 100, label: "100" },
|
||
]
|
||
: [],
|
||
label: "Volume",
|
||
},
|
||
Select: {
|
||
value: variation === 4 ? "design" : "",
|
||
label: "Team",
|
||
options: [
|
||
{ label: "Engineering", value: "engineering" },
|
||
{ label: "Design", value: "design" },
|
||
{ label: "Marketing", value: "marketing" },
|
||
],
|
||
},
|
||
Switch: { value: "enabled", checked: variation === 4, label: "Email notifications" },
|
||
Textarea: {
|
||
value: variation === 4 ? "This content is read only." : "",
|
||
label: "Description",
|
||
rows: 4,
|
||
},
|
||
TimePicker: {
|
||
value: variation === 4 ? "09:30" : "",
|
||
label: "Start time",
|
||
min: "08:00",
|
||
max: "18:00",
|
||
},
|
||
};
|
||
return { ...common, ...byComponent[componentName] };
|
||
}
|
||
|
||
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) {
|
||
if (component.name === "FileUploadProgress") return fileUploadProgressUses;
|
||
if (component.name === "Checkbox") return checkboxUses;
|
||
if (component.name === "Radio") return radioUses;
|
||
if (basicFormComponentNames.has(component.name)) return basicFormUses;
|
||
switch (component.name) {
|
||
case "Accordion":
|
||
return accordionUses;
|
||
|
||
case "Alert":
|
||
return alertUses;
|
||
|
||
case "Avatar":
|
||
return avatarUses;
|
||
|
||
case "AvatarGroup":
|
||
return avatarGroupUses;
|
||
|
||
case "Badge":
|
||
return badgeUses;
|
||
|
||
case "Button":
|
||
return buttonUses;
|
||
|
||
case "ButtonGroup":
|
||
return buttonGroupUses;
|
||
|
||
case "Card":
|
||
return cardUses;
|
||
|
||
case "Carousel":
|
||
return carouselUses;
|
||
|
||
case "ChatBubble":
|
||
return chatBubbleUses;
|
||
|
||
case "Collapse":
|
||
return collapseUses;
|
||
|
||
case "AdvancedSelect":
|
||
return advancedSelectUses;
|
||
|
||
case "ComboBox":
|
||
return comboboxUses;
|
||
|
||
case "InputNumber":
|
||
return inputNumberUses;
|
||
|
||
case "ToggleCount":
|
||
return toggleCountUses;
|
||
|
||
case "PinInput":
|
||
return pinInputUses;
|
||
|
||
case "StrongPassword":
|
||
return strongPasswordUses;
|
||
|
||
case "TogglePassword":
|
||
return togglePasswordUses;
|
||
|
||
default:
|
||
return defaultUses;
|
||
}
|
||
}
|
||
|
||
function eventDocumentation(component) {
|
||
const descriptions = {
|
||
action: "Fires when the component's action is selected.",
|
||
avatarClick: "Fires when a message avatar is selected.",
|
||
add: "Fires when an item is added.",
|
||
blur: "Fires when focus leaves the component.",
|
||
cancel: "Fires when the current operation is cancelled.",
|
||
change: "Fires when the component value or selection changes.",
|
||
clear: "Fires after the current value or collection is cleared.",
|
||
click: "Fires when the component is activated by pointer or keyboard.",
|
||
close: "Fires when the component closes.",
|
||
complete: "Fires when the component's operation completes.",
|
||
error: "Fires when the component encounters an error.",
|
||
focus: "Fires when the component receives focus.",
|
||
input: "Fires immediately as the component value changes.",
|
||
load: "Fires when component data or media finishes loading.",
|
||
linkClick: "Fires when a link inside a message is selected.",
|
||
messageClick: "Fires when a message bubble is selected.",
|
||
open: "Fires when the component opens.",
|
||
progress: "Fires as an operation reports progress.",
|
||
search: "Fires when the search query changes or is submitted.",
|
||
select: "Fires when an item or option is selected.",
|
||
success: "Fires when the component operation succeeds.",
|
||
submit: "Fires when the component submits its value.",
|
||
toggle: "Fires when the component toggles between states.",
|
||
};
|
||
const events = component.events.map((name) => [
|
||
name,
|
||
descriptions[name] ?? `Fires when the component emits the ${displayName(name)} event.`,
|
||
]);
|
||
const emptyState =
|
||
'<div class="docs-empty"><span class="icon-[lucide--radio-tower] size-5" aria-hidden="true"></span>No public component events.</div>';
|
||
const eventsList = events.length
|
||
? `<div class="detail-events-list">${events.map(([name, description]) => `<div><code>@${name}</code><span>${description}</span></div>`).join("")}</div>`
|
||
: emptyState;
|
||
const listenerExample = events.length
|
||
? `<section class="demo-code"><header><span><span class="icon-[lucide--radio] size-4" aria-hidden="true"></span>Event listener</span><small>.js</small></header><pre><code>${escapeCode(`const component = document.querySelector('[data-wrn-events]')
|
||
|
||
component.addEventListener('${events[0][0]}', (event) => {
|
||
console.log(event.detail)
|
||
})`)}</code></pre></section>`
|
||
: "";
|
||
return `<section id="events" class="detail-section"><div class="detail-section-heading"><span>Component events</span><h2>Events</h2><p>Public events declared by this component. Custom events bubble from the component root and expose state through <code>event.detail</code>.</p></div><div class="${events.length ? "detail-events-grid" : ""}">${eventsList}${listenerExample}</div></section>`;
|
||
}
|
||
|
||
function detailUseCaseLinks(component, uses) {
|
||
const link = (index) =>
|
||
`<a href="#${slugOf(component.name)}-demo-${index + 1}">${escapeText(uses[index].title)}</a>`;
|
||
|
||
if (component.name === "ButtonGroup") {
|
||
return `<span class="detail-toc-group">Basic usage</span>${link(0)}<span class="detail-toc-group">Sizes</span>${link(1)}${link(2)}${link(3)}${link(4)}${link(5)}${link(6)}`;
|
||
}
|
||
|
||
if (component.name === "Card") {
|
||
return `<span class="detail-toc-group">Basic usage</span>${link(0)}<span class="detail-toc-group">Content</span>${link(1)}${link(2)}<span class="detail-toc-group">Structure</span>${link(3)}${link(4)}<span class="detail-toc-group">Sizes</span>${link(5)}${link(6)}${link(7)}<span class="detail-toc-group">Navigation</span>${link(8)}${link(9)}<span class="detail-toc-group">Images</span>${link(10)}${link(11)}${link(12)}<span class="detail-toc-group">Animations</span>${link(13)}${link(14)}<span class="detail-toc-group">Card layout</span>${link(15)}${link(16)}<span class="detail-toc-group">Panels</span>${link(17)}${link(18)}${link(19)}${link(20)}${link(21)}${link(22)}`;
|
||
}
|
||
if (component.name === "Carousel") {
|
||
return `<span class="detail-toc-group">Basic usage</span>${link(0)}${link(1)}<span class="detail-toc-group">Playback</span>${link(2)}${link(3)}<span class="detail-toc-group">Direction</span>${link(4)}<span class="detail-toc-group">Layouts</span>${link(5)}${link(6)}${link(7)}${link(8)}${link(9)}<span class="detail-toc-group">Status</span>${link(10)}<span class="detail-toc-group">Thumbnails</span>${link(11)}${link(12)}`;
|
||
}
|
||
|
||
return uses
|
||
.map(
|
||
(useCase, variation) =>
|
||
`<a href="#${slugOf(component.name)}-demo-${variation + 1}">${escapeText(useCase.title)}</a>`,
|
||
)
|
||
.join("");
|
||
}
|
||
|
||
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 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, {
|
||
label: `${length}-digit code`,
|
||
name: `pin-length-${length}`,
|
||
length,
|
||
}),
|
||
)
|
||
.join("")}</div>`
|
||
: componentMount(component, variation);
|
||
const snippet = isPinLengthDemo
|
||
? pinLengths
|
||
.map((length) =>
|
||
sourceSnippet(component, variation, {
|
||
label: `${length}-digit code`,
|
||
name: `pinLength${length}`,
|
||
length,
|
||
}),
|
||
)
|
||
.join("\n\n")
|
||
: sourceSnippet(component, variation);
|
||
const validationSchema =
|
||
basicFormComponentNames.has(component.name) &&
|
||
variation === (component.name === "Radio" || component.name === "Checkbox" ? 10 : 8)
|
||
? "basic-form-validation"
|
||
: 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>
|
||
<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>${detailUseCaseLinks(component, uses)}<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">
|
||
<div class="catalog-card-preview">
|
||
<div class="catalog-card-glow" aria-hidden="true"></div>
|
||
<div class="catalog-card-stage">${componentMount(component, 0)}</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,
|
||
);
|
||
|
||
const completedComponents = new Set([
|
||
"Accordion",
|
||
"Alert",
|
||
"Avatar",
|
||
"AvatarGroup",
|
||
"Badge",
|
||
"Blockquote",
|
||
"Button",
|
||
"ButtonGroup",
|
||
"Card",
|
||
"Carousel",
|
||
"ChatBubble",
|
||
"Collapse",
|
||
"Checkbox",
|
||
"DatePicker",
|
||
"DeviceFrame",
|
||
"FileUploadProgress",
|
||
"ColorPicker",
|
||
"FileInput",
|
||
"Input",
|
||
"InputGroup",
|
||
"Radio",
|
||
"RangeSlider",
|
||
"Select",
|
||
"Switch",
|
||
"Textarea",
|
||
"TimePicker",
|
||
"AdvancedSelect",
|
||
"ComboBox",
|
||
"InputNumber",
|
||
"PinInput",
|
||
"StrongPassword",
|
||
"ToggleCount",
|
||
"TogglePassword",
|
||
]);
|
||
const workingComponents = new Set([]);
|
||
|
||
function componentStatus(componentName) {
|
||
if (completedComponents.has(componentName)) return "complete";
|
||
if (workingComponents.has(componentName)) return "working";
|
||
return "pending";
|
||
}
|
||
|
||
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) => {
|
||
const status = componentStatus(component.name);
|
||
return `<a data-docs-component-link href="/components/${slugOf(component.name)}"><span data-docs-component-name>${escapeText(displayName(component.name))}</span><small data-component-status="${status}">${titleCase(status)}</small></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="/blocks">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 });
|
||
writeIfChanged(join(layoutsDir, "showcase.wrn"), showcaseLayout());
|
||
writeIfChanged(join(layoutsDir, "document.wrn"), documentLayout());
|
||
const generatedPages = new Set(["index.wrn", "docs.wrn", "blocks.wrn", "templates.wrn"]);
|
||
const generatedDetailPages = new Set();
|
||
|
||
for (const [category, components] of categoryEntries) {
|
||
generatedPages.add(`${category}.wrn`);
|
||
writeIfChanged(join(pagesDir, `${category}.wrn`), categoryPage(category, components));
|
||
for (const component of components) {
|
||
const detailFile = `${slugOf(component.name)}.wrn`;
|
||
generatedDetailPages.add(detailFile);
|
||
writeIfChanged(join(detailPagesDir, detailFile), detailPage(component, components));
|
||
}
|
||
}
|
||
|
||
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("");
|
||
|
||
writeIfChanged(
|
||
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="/blocks">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="/blocks">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.",
|
||
],
|
||
];
|
||
writeIfChanged(
|
||
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) {
|
||
generatedPages.add(`${slug}.wrn`);
|
||
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.",
|
||
],
|
||
];
|
||
writeIfChanged(
|
||
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="/blocks">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.",
|
||
],
|
||
];
|
||
writeIfChanged(
|
||
join(pagesDir, "blocks.wrn"),
|
||
`page Blocks {
|
||
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.",
|
||
],
|
||
];
|
||
writeIfChanged(
|
||
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="/blocks">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",
|
||
);
|
||
|
||
for (const file of readdirSync(pagesDir).filter((entry) => entry.endsWith(".wrn"))) {
|
||
if (!generatedPages.has(file)) unlinkSync(join(pagesDir, file));
|
||
}
|
||
for (const file of readdirSync(detailPagesDir).filter((entry) => entry.endsWith(".wrn"))) {
|
||
if (!generatedDetailPages.has(file)) unlinkSync(join(detailPagesDir, file));
|
||
}
|
||
|
||
process.stdout.write(
|
||
`Generated ${reference.count} component detail pages, ${totalDemoCount} live demos, and ${categoryEntries.length} category pages.\n`,
|
||
);
|