import { mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import process from "node:process"; import { fileURLToPath } from "node:url"; import prettier from "prettier"; import { generateCatalogPages } from "./generate-catalog-pages.mjs"; import { eventDescriptionFor, profileFor } from "./showcase-profiles.mjs"; const exampleRoot = join(dirname(fileURLToPath(import.meta.url)), ".."); // Standalone deployment: read the reference shipped with the published ui package. const referencePath = join(exampleRoot, "node_modules", "@wrnexus", "ui", "component-reference.json"); const pagesDir = join(exampleRoot, "app", "pages"); const detailPagesDir = join(pagesDir, "components"); const layoutsDir = join(exampleRoot, "app", "layouts"); const reference = JSON.parse(readFileSync(referencePath, "utf8")); const excludedNames = new Set(["AuthForm", "AuthSplitLayout", "PortalDashboard", "PublicPageShell"]); const blockNames = new Set(["CTASection","Hero","HeroActions","SplitHero","FeatureCard","FeatureIconCard","FeatureGrid","MarketingSectionHeader","PageHeader","SectionHeader","AnnouncementBar","Footer","MetricGrid","StatsBar"]); const blockComponents = reference.components.filter(component => blockNames.has(component.name)); reference.components = reference.components.filter(component => !excludedNames.has(component.name) && !blockNames.has(component.name)); reference.count = reference.components.length; const interactiveOverlayComponents = new Set([ "ContextMenu", "Drawer", "Dropdown", "Modal", "Popover", "Tooltip", ]); const overlayVisibilityProps = new Set(["open", "defaultopen", "visible"]); function isInteractiveOverlay(component) { return interactiveOverlayComponents.has(component.name); } function overlayInteractionLabel(component) { switch (component.name) { case "ContextMenu": return "Right-click or use the trigger button"; case "Tooltip": return "Hover, focus, or click the trigger"; case "Drawer": return "Click the trigger to open the drawer"; case "Modal": return "Click the trigger to open the modal"; case "Dropdown": return "Click the trigger to open the menu"; case "Popover": return "Click the trigger to open the popover"; default: return "Interact with the trigger"; } } const displayName = (name) => name.replace(/([a-z0-9])([A-Z])/g, "$1 $2"); const baseSlugOf = (name) => name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase(); const slugCounts = new Map(); for (const component of reference.components) { const slug = baseSlugOf(component.name); slugCounts.set(slug, (slugCounts.get(slug) ?? 0) + 1); } const slugOf = (name) => { const slug = baseSlugOf(name); if (!slugCounts.has(slug) || 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. /* * Code samples: each brace goes in its own element. * * The entity alone is not enough. It survives the compiler, but the BROWSER * decodes it back to a brace, and the client runtime then walks text nodes * looking for {mustache} and evaluates whatever it finds -- so a JSON sample * was silently eaten, leaving "columns=[, , , ]" on the page. A text node can * only be mistaken for a template when it holds a complete pair, so splitting * the braces into their own nodes makes samples immune. A reader sees no * difference. */ const escapeCode = (value) => escapeText(value).replaceAll("{", "{").replaceAll("}", "}"); /* * JSON for a textarea. Entities keep the braces away from the WRN compiler, * which would otherwise read them as interpolation and fail the page build, * while the browser decodes them so the reader still sees valid JSON. The * span trick used for code samples cannot be used here: a textarea shows its * markup literally. */ const escapeTextareaJson = (value) => escapeText(value).replaceAll("{", "{").replaceAll("}", "}"); const escapeAttribute = (value) => escapeText(value).replaceAll('"', """).replaceAll("'", "'"); 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: `/showcase-images/logo-${variation}-${index}.svg`, 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 ?? "") || (prop.default ?? "").startsWith("[")) { try { return JSON.parse(prop.default); } catch { return undefined; } } return undefined; } function sampleValue(prop, component, variation) { const name = prop.name.toLowerCase(); const curated = { ChatBubble: { title:"", description:"", showMetadata:true, showAvatars:false, items:[{text:"Hi Maya, is the new dashboard ready to review?",direction:"incoming",timestamp:"10:24"},{text:"Yes! I have shared the latest version with you.",direction:"outgoing",timestamp:"10:25",receipt:"read"},{text:"Thanks, I will take a look this afternoon.",direction:"incoming",timestamp:"10:26"},{text:"Great. The release notes are attached too.",direction:"outgoing",timestamp:"10:27",receipt:"delivered"},{text:"One more screenshot coming up.",direction:"outgoing",timestamp:"10:28",receipt:"sent"},{text:"Uploading the final export now.",direction:"outgoing",timestamp:"10:29",receipt:"sending"},{text:"Here is the invoice PDF.",direction:"outgoing",timestamp:"10:30",receipt:"failed",status:"Not delivered",actionLabel:"Retry"}] }, FileUploadProgress: { label:"Upload progress",fileName:"brand-guidelines.pdf",fileSize:"4.8 MB",uploadedSize:variation === 0 ? "2.4 MB" : "",value:variation === 2 ? 100 : 50,status:["uploading","error","complete"][variation % 3] }, ListGroup: {title:"Workspace",value:"inbox",items:[{value:"inbox",label:"Inbox",description:"Messages that need your attention",icon:"icon-[lucide--inbox]",badge:"8"},{value:"projects",label:"Projects",description:"Your team's active work",icon:"icon-[lucide--folder]"},{value:"archive",label:"Archive",description:"Completed conversations",icon:"icon-[lucide--archive]"}]}, Rating: {title:"How was your experience?",value:variation === 1 ? 3 : 4,readonly:variation === 2}, StyledIcon: {icon:["icon-[lucide--sparkles]","icon-[lucide--check]","icon-[lucide--shield-check]"][variation % 3],label:"Verified",color:variation === 1 ? "success" : "primary",variant:variation === 2 ? "outline" : "default",size:variation === 1 ? "sm" : "default"}, TreeView: {title:"Project files",defaultExpanded:["src"],items:[{value:"src",label:"src",children:[{value:"pages",label:"pages",children:[{value:"home",label:"index.wrn"}]},{value:"styles",label:"styles.css"}]},{value:"readme",label:"README.md"}]}, InputGroup: {name:"workspace-url",label:"Workspace address",placeholder:"your-team",startText:variation === 0 ? "https://" : "",endText:variation === 0 ? ".example.com" : "",icon:variation === 1 ? "icon-[lucide--search]" : "",actionLabel:variation === 1 ? "Search" : "",value:""}, RangeSlider: {name:"volume",label:"Volume",value:40,showBounds:true,showValue:true,marks:[]}, Chart: {title:"Weekly activity",description:"Completed tasks this week",items:[{label:"Mon",value:24},{label:"Tue",value:38},{label:"Wed",value:29},{label:"Thu",value:52},{label:"Fri",value:45}],height:220,showLegend:variation === 2}, Clipboard: {title:"",description:"",label:"Copy",value:"npm install @wrnexus/ui",iconOnly:variation !== 0}, Confetti: {label:"Celebrate milestone",count:40,duration:1400}, DataMap: {title:"Activity by team",description:"Select a tile to inspect its activity score.",items:[{label:"Design",value:32},{label:"Engineering",value:84},{label:"Support",value:56},{label:"Sales",value:45},{label:"Marketing",value:68},{label:"Operations",value:20}]}, DragAndDrop: {title:"Project priorities",description:"Drag to reorder, or use the arrow buttons.",items:[{label:"Review designs"},{label:"Build the dashboard"},{label:"Test accessibility"},{label:"Prepare release notes"}]}, WysiwygEditor: {title:"Project update",description:"",value:"Share progress, decisions, and next steps with your team.",readonly:variation === 2}, BackToTop: {label:"Back to top"}, Pagination: { label: "Pagination", previousLabel: "Previous", nextLabel: "Next", hrefTemplate: "", }, Avatar: { name: "Aisha Khan", initials: "AK", alt: "Aisha Khan", tooltip: "Aisha Khan · Product designer", description: variation === 2 ? "Product designer" : "", size: variation === 1 ? "sm" : "md", status: variation === 2 ? "online" : "", }, ColorPicker: { name: "brand-color", label: "Brand color", value: ["#8b5cf6", "#14b8a6", "#3b82f6"][variation % 3], }, Kbd: { label: ["⌘ K", "Esc", "Ctrl + Enter"][variation % 3] }, Alert: { title: ["Changes saved", "Scheduled maintenance", "Payment needs attention"][variation % 3], description: [ "Your workspace settings are up to date.", "The dashboard will be unavailable Sunday, 02:00–02:30 UTC.", "Update your payment method to keep your subscription active.", ][variation % 3], color: ["success", "info", "warning"][variation % 3], variant: "soft", shadow: "none", showIcon: true, icon: ["icon-[lucide--circle-check]", "icon-[lucide--info]", "icon-[lucide--triangle-alert]"][ variation % 3 ], dismissible: true, actionLabel: variation === 2 ? "Update payment" : "", actionHref: variation === 2 ? "#payment" : "", }, AvatarGroup: { items: [ { name: "Aisha Khan", initials: "AK", tooltip: "Aisha Khan · Product designer" }, { name: "Daniel Lee", initials: "DL", tooltip: "Daniel Lee · Engineer" }, { name: "Maya Patel", initials: "MP", tooltip: "Maya Patel · Product manager" }, { name: "Oliver Reed", initials: "OR", tooltip: "Oliver Reed · Support" }, { name: "Sofia Chen", initials: "SC", tooltip: "Sofia Chen · Operations" }, ], showTooltips: true, maxVisible: 3, size: variation === 1 ? "sm" : "md", layout: variation === 2 ? "grid" : "stack", }, ButtonGroup: { items: [ { label: "Day", value: "day" }, { label: "Week", value: "week" }, { label: "Month", value: "month" }, ], value: "week", selectable: true, variant: "default", size: variation === 1 ? "sm" : "md", attached: variation !== 2, }, Card: { title: "Team workspace", description: "One place for your projects, files, and conversations.", size: "md", variant: "default", items: [], dismissible: false, }, Carousel: { items: [ { title: "A space to create", description: "Bring your next idea to life.", imageSrc: "/showcase-images/green-hero.svg", }, { title: "Made for teamwork", description: "Keep everyone moving together.", imageSrc: "/showcase-images/blue-mountain.svg", }, { title: "Room to grow", description: "Build something that matters.", imageSrc: "/showcase-images/violet-circle.svg", }, ], framed: variation === 1, showPagination: true, showCounter: true, ariaLabel: "Product highlights", }, }[component.name]; if (curated) return Object.hasOwn(curated, prop.name) ? curated[prop.name] : defaultValue(prop); if (component.name === "Accordion") { const configuration = { items: [ { value: "workspace", label: "How do I create a workspace?", content: "Choose Create workspace, add a name, and invite your team. You can update the details any time in Settings.", }, { value: "billing", label: "Can I change my plan later?", content: "Yes. Open Billing to compare plans and choose the one that suits your team.", }, { value: "security", label: "How is my account protected?", content: "Enable two-step verification and add a recovery method to keep your account secure.", }, ], defaultOpen: ["workspace"], indicator: variation === 1 ? "plus" : "chevron", indicatorPosition: variation === 1 ? "start" : "end", bordered: variation !== 0, separated: variation === 2, multiple: variation === 2, }; if (Object.hasOwn(configuration, prop.name)) return configuration[prop.name]; } if (component.name === "ToggleCount") { const configuration = { value: "monthly", firstValue: "monthly", secondValue: "annual", firstLabel: "Monthly", secondLabel: "Yearly", firstValueKey: "monthly", secondValueKey: "annual", items: [ { title: "Starter", label: "Starter", description: "For individuals getting started.", monthly: 12, annual: 120, }, { title: "Team", label: "Team", description: "For teams working together.", monthly: 29, annual: 290, }, ], }; if (Object.hasOwn(configuration, prop.name)) return configuration[prop.name]; } if (component.name === "Badge") { const configuration = { label: ["Active", "In review", "Verified"][variation % 3], variant: ["soft", "outline", "solid"][variation % 3], color: ["success", "warning", "primary"][variation % 3], icon: variation === 2 ? "icon-[lucide--check]" : "", size: "sm", anchorLabel: "", anchorIcon: "", dismissible: variation === 2, dismissLabel: "Remove badge", dotLabel: "Status", }; if (Object.hasOwn(configuration, prop.name)) return configuration[prop.name]; } if (component.name === "TimePicker" && ["hours", "minutes"].includes(prop.name)) return defaultValue(prop); if (component.name === "TimePicker" && name === "value") return "09:30"; if (component.name === "DatePicker") { if (name === "months") return [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", ].map((label, index) => ({ label, value: String(index + 1).padStart(2, "0") })); if (name === "days") return Array.from({ length: 31 }, (_, index) => index + 1); if (name === "years") return [2024, 2025, 2026, 2027, 2028, 2029, 2030]; } if (component.name === "AdvancedSelect") { const configuration = advancedSelectConfiguration(variation); if (Object.hasOwn(configuration, prop.name)) return configuration[prop.name]; return undefined; } if (component.name === "ComboBox") { const configuration = comboboxConfiguration(variation); if (Object.hasOwn(configuration, prop.name)) return configuration[prop.name]; return undefined; } if (component.name === "PinInput") { const configuration = pinInputConfiguration(variation); if (Object.hasOwn(configuration, prop.name)) return configuration[prop.name]; return undefined; } if (component.name === "StrongPassword") { const configuration = strongPasswordConfiguration(variation); if (Object.hasOwn(configuration, prop.name)) return configuration[prop.name]; return undefined; } if (component.name === "TogglePassword") { const configuration = togglePasswordConfiguration(variation); if (Object.hasOwn(configuration, prop.name)) return configuration[prop.name]; return undefined; } if (component.name === "InputNumber") { const configuration = inputNumberConfiguration(variation); if (Object.hasOwn(configuration, prop.name)) { return configuration[prop.name]; } return undefined; } const isArray = prop.default === "[]" || prop.type.includes("[]") || prop.type === "array"; const isObject = prop.default === "{}" || prop.type === "object"; if (isArray) return sampleArray(prop, component, variation); if (isObject) return sampleItem(component, 0, variation); if (name === "class") return `showcase-instance showcase-instance--${variation + 1}`; if (component.name === "Button") { const configurations = [ { label: "Save & Submit", as: "button", type: "submit", }, { label: "Learn more", as: "a", href: "#button-demo", variant: "outline", size: "sm", icon: "icon-[lucide--arrow-right]", iconPosition: "end", }, { label: "Upgrade plan", as: "button", variant: "secondary", size: "lg", pill: true }, { label: "Open settings", as: "button", ariaLabel: "Open settings", variant: "ghost", size: "icon", icon: "icon-[lucide--settings]", }, { label: "Delete project", as: "button", variant: "destructive", icon: "icon-[lucide--trash-2]", }, { label: "Read documentation", as: "a", href: "#button-demo", variant: "link" }, { label: "Publish", as: "button", loadingLabel: "Publishing…", loading: true }, { label: "Unavailable action", as: "button", disabled: true, variant: "outline" }, ]; const configuration = configurations[variation] ?? configurations[0]; if (Object.hasOwn(configuration, prop.name)) return configuration[prop.name]; return undefined; } if (name === "controlclass") return variation === 2 ? "shadow-xl" : ""; if (prop.type === "boolean") { // Interactive overlays must render closed. A hard-coded open=true prop turns // the component into controlled mode, so its own click/hover handlers cannot // close it and multiple overlays block the complete showcase page. if (isInteractiveOverlay(component) && overlayVisibilityProps.has(name)) return false; if (name === "compact") return variation === 1; if (name === "fullwidth") return variation === 2; if (name === "loading" || name === "disabled") return false; if (name === "open" || name === "visible" || name === "dismissible" || name.startsWith("show")) return true; return defaultValue(prop) ?? variation === 2; } if (prop.type === "number") { if (name === "value") return 36 + variation * 24; if (name.includes("count") || name.includes("total") || name.includes("page")) return 3 + variation * 4; return defaultValue(prop) ?? variation + 1; } if (name.includes("arialabel")) return `${displayName(component.name)} ${variation + 1} example`; if (name === "title" || name.endsWith("title")) return variation === 0 ? `${displayName(component.name)} example` : variation === 1 ? `Compact ${displayName(component.name)}` : `Advanced ${displayName(component.name)}`; if (name.includes("description") || name === "summary") return variation === 0 ? `A polished default ${displayName(component.name).toLowerCase()} for a modern application.` : variation === 1 ? "A compact configuration for dashboards and dense product surfaces." : "A richer configuration demonstrating additional content and hierarchy."; if (name === "label" || name.endsWith("label")) return variation === 0 ? displayName(component.name) : variation === 1 ? "Compact example" : "Advanced example"; if (name.includes("placeholder")) return variation === 2 ? "Search by name, email, or identifier" : "Enter a sample value"; if (name.includes("email")) return variation === 2 ? "team@example.com" : "developer@example.com"; if (name.includes("href") || name === "action") { if (component.name === "Button" && variation === 0) return ""; return `#${slugOf(component.name)}-demo`; } if (name === "icon") return variation === 0 ? "" : variation === 1 ? "icon-[lucide--arrow-right]" : "icon-[lucide--trash-2]"; if (name === "variant") if (component.name === "Button") return variation === 0 ? "primary" : variation === 1 ? "secondary" : "danger"; if (name === "variant") return variation === 0 ? defaultValue(prop) || "primary" : variation === 1 ? "secondary" : "success"; if (name === "size") return variation === 0 ? defaultValue(prop) || "md" : variation === 1 ? "sm" : "lg"; if (name === "align") return variation === 1 ? "center" : variation === 2 ? "end" : "start"; if (name === "value" && component.name === "DatePicker") return `2026-09-${20 + variation}`; 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; } /* * Object props are hoisted into page state rather than written inline. * * A bare {...} attribute is read by the compiler as an interpolation, and * escaping the braces does not help either: prop coercion runs JSON.parse on * the raw attribute, which never sees the entities decoded, so the mount threw * and the demo silently rendered nothing. Binding to state is what the * playground already does, and it is the only form that survives both. * * Arrays start with [ and are unambiguous, so they stay inline. */ const hoistedObjects = []; function resetHoistedObjects() { hoistedObjects.length = 0; } function hoistObjectProp(value) { const name = `demo_obj_${hoistedObjects.length}`; // JSON.parse, not a bare object literal: the parser reads a leading brace // as the start of a block and rejects the rest of the page. const json = JSON.stringify(JSON.stringify(value)); hoistedObjects.push(` state ${name} = JSON.parse(${json})`); return name; } function hoistedObjectStates() { if (!hoistedObjects.length) return ""; // playgroundStates has no trailing newline, so lead with one. return "\n" + hoistedObjects.join("\n"); } function wrnAttribute(name, value) { if (typeof value === "object" && value !== null) { if (!Array.isArray(value)) return `${name}='{${hoistObjectProp(value)}}'`; return `${name}='${JSON.stringify(value).replaceAll("'", "\\u0027")}'`; } return `${name}="${escapeAttribute(String(value))}"`; } function componentAttributes(component, variation, overrides = {}) { const attributes = []; for (const prop of component.props) { const value = Object.hasOwn(overrides, prop.name) ? overrides[prop.name] : sampleValue(prop, component, variation); if (value === undefined) continue; attributes.push(wrnAttribute(prop.name, value)); } return attributes; } function configuredSlotMarkup(component, variation, overrides = {}) { const configured = overrides.__slots ?? {}; if (component.name === "LayoutSplitter" && !Object.keys(configured).length) return '

Project files

Drag the divider to resize these panels.

Editor

Your selected file appears here.

'; return component.slots .map((slot) => { const explicitlyConfigured = Object.hasOwn(configured, slot); if (slot !== "default" && !explicitlyConfigured) return ""; const custom = configured[slot]; const content = custom ?? (slot === "default" ? defaultSlotMarkup(component, variation) : ""); if (!content) return ""; return slot === "default" ? content : `
${content}
`; }) .join(""); } function componentMount(component, variation = 0, overrides = {}) { const attributes = componentAttributes(component, variation, overrides); const slotMarkup = configuredSlotMarkup(component, variation, overrides); const publicTag = /^[A-Z][A-Za-z0-9_]*$/.test(component.mount); if (publicTag) { const attributesSource = attributes.length ? ` ${attributes.join(" ")}` : ""; return slotMarkup ? `<${component.mount}${attributesSource}>${slotMarkup}` : `<${component.mount}${attributesSource} />`; } const attributesSource = attributes.length ? ` ${attributes.join(" ")}` : ""; return `
${slotMarkup}
`; } const structuralDefaultSlots = new Set([ "Accordion", "AspectRatio", "AutoGrid", "card", "Cluster", "CodeTabs", "Container", "ContentSection", "FilterBar", "ScrollArea", "Section", "SidebarLayout", "SplitLayout", "Stack", "Sticky", "StickyLayout", "Surface", "Typography", "VisuallyHidden", "Well", "WideContainer", ]); function defaultSlotMarkup(component, variation) { if (component.name === "Select") return ''; if (component.name === "Table") return "NameStatusProductionActiveStagingReady"; if (component.name === "Tooltip") return ""; if (!structuralDefaultSlots.has(component.name)) return ""; return `
${variation === 2 ? "Rich supporting content with additional context." : "Composable content area."}
`; } function indentSource(value, spaces = 2) { const indent = " ".repeat(spaces); return String(value) .trim() .split("\n") .map((line) => `${indent}${line}`) .join("\n"); } function sourceSnippet(component, variation, overrides = {}) { const declarations = []; 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 []; if (prop.type.includes("[]")) { let data = value; if (typeof data === "string") { try { data = JSON.parse(data); } catch {} } if (Array.isArray(data)) { declarations.push(` state ${prop.name} = ${JSON.stringify(data, null, 2)}`); return [`${prop.name}="{${prop.name}}"`]; } } const declaredDefault = defaultValue(prop); if ( declaredDefault !== undefined && typeof value !== "object" && String(value) === String(declaredDefault) ) return []; if (typeof value === "object") { const serialized = JSON.stringify(value, null, 2) .replaceAll("'", "\\u0027") .replaceAll("\n", "\n "); return [`${prop.name}='${serialized}'`]; } return [`${prop.name}="${String(value).replaceAll('"', '\\"')}"`]; }); const formattedAttributes = attributes.length ? `\n ${attributes.join("\n ")}` : ""; const hasPublicComponentTag = /^[A-Z][A-Za-z0-9_]*$/.test(component.mount); const configured = overrides.__slots ?? {}; const slotParts = component.slots.flatMap((slot) => { const custom = configured[slot]; const fallback = slot === "default" ? defaultSlotMarkup(component, variation) : `
${displayName(slot)} slot
`; const content = custom ?? fallback; if (!content) return []; if (slot === "default") return [indentSource(content, 2)]; return [`
\n${indentSource(content, 4)}\n
`]; }); const slotMarkup = slotParts.length ? `\n${slotParts.join("\n")}\n` : ""; if (hasPublicComponentTag) { const source = slotMarkup ? `<${component.mount}${formattedAttributes}>${slotMarkup}` : formattedAttributes ? `<${component.mount}${formattedAttributes}\n/>` : `<${component.mount} />`; return escapeCode(declarations.length ? `page ${identifier(component.name)}Example {\n${declarations.join("\n")}\n view {\n${indentSource(source,4)}\n }\n}`.replaceAll("\\n", "\n") : source); } const mountAttribute = `data-component="${component.mount}"`; const source = slotMarkup ? `${slotMarkup}` : ``; return escapeCode(source); } function propsTable(component) { if (component.props.length === 0) return `
No props. Compose this component with slots.
`; return `
${component.props .map( (prop) => ``, ) .join("")}
PropTypeDefaultRequired
${escapeCode(prop.name)}${escapeText(prop.type)}${escapeCode(prop.default ?? "—")}${prop.required ? 'Yes' : "No"}
`; } function playgroundInitialValue(prop, component) { const profileValue = profileFor(component.name)?.playground?.[prop.name]; if (profileValue !== undefined) return profileValue; const sampled = sampleValue(prop, component, 0); return sampled === undefined ? (defaultValue(prop) ?? "") : sampled; } function playgroundValue(prop, component) { const value = playgroundInitialValue(prop, component); return typeof value === "object" ? JSON.stringify(value, null, 2) : String(value); } function playgroundStateName(prop) { return `playground_${prop.name.replace(/[^A-Za-z0-9_$]/g, "_")}`; } function isStructuredProp(prop) { return ( (prop.default ?? "").startsWith("[") || prop.default === "[]" || prop.default === "{}" || prop.type === "array" || prop.type === "object" || prop.type.includes("[]") ); } function playgroundStates(component) { return component.props .map((prop) => { const initialValue = playgroundInitialValue(prop, component); const query = `ctx.url.searchParams.get("pg_${prop.name}")`; if (prop.type === "boolean") { const initial = initialValue === true ? "true" : "false"; return ` state ${playgroundStateName(prop)} = (${query} ?? "${initial}") === "true"`; } if (prop.type === "number") { const initial = Number(initialValue || 0); return ` state ${playgroundStateName(prop)} = Number(${query} ?? ${JSON.stringify(String(initial))})`; } if (isStructuredProp(prop)) { const initial = typeof initialValue === "string" && initialValue.trim() ? initialValue : JSON.stringify(initialValue || (prop.default === "{}" ? {} : [])); return ` state ${playgroundStateName(prop)} = JSON.parse(${query} ?? ${JSON.stringify(initial)})`; } return ` state ${playgroundStateName(prop)} = ${query} ?? ${JSON.stringify(String(initialValue ?? ""))}`; }) .join("\n"); } function playgroundMount(component) { const attributes = component.props.map((prop) => `${prop.name}='{${playgroundStateName(prop)}}'`); const profileSlots = profileFor(component.name)?.demos?.[0]?.slots ?? {}; const slotMarkup = configuredSlotMarkup(component, 0, { __slots: profileSlots }); const publicTag = /^[A-Z][A-Za-z0-9_]*$/.test(component.mount); if (publicTag) { const attributesSource = attributes.length ? ` ${attributes.join(" ")}` : ""; return slotMarkup ? `<${component.mount}${attributesSource}>${slotMarkup}` : `<${component.mount}${attributesSource} />`; } const attributesSource = attributes.length ? ` ${attributes.join(" ")}` : ""; return `
${slotMarkup}
`; } 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 = `${escapeText(prop.type)}${prop.required ? " · required" : ""}`; if (prop.type === "boolean") { return ``; } const structured = (prop.default ?? "").startsWith("[") || prop.default === "[]" || prop.default === "{}" || prop.type === "array" || prop.type === "object" || prop.type.includes("[]"); if (structured) { return ``; } const options = playgroundOptions(prop, component, value); if (options.length > 1) { return ``; } const inputType = prop.type === "number" ? "number" : prop.name.toLowerCase().includes("color") && /^#[0-9a-f]{6}$/i.test(value) ? "color" : "text"; return ``; } const commonPlaygroundOptions = { align: ["left", "center", "right", "start", "end", "stretch"], alignment: ["left", "center", "right", "start", "end", "stretch"], as: ["", "button", "a"], color: ["primary", "secondary", "success", "warning", "danger", "info"], direction: ["row", "column"], gap: ["none", "xs", "sm", "md", "lg", "xl", "2xl"], iconposition: ["start", "end", "left", "right"], imageposition: ["top", "left", "right"], layout: ["content", "split", "stacked", "balanced", "visual"], maxwidth: ["compact", "lg", "xl", "wide", "2xl", "full"], method: ["get", "post"], orientation: ["horizontal", "vertical"], placement: [ "top", "top-start", "top-end", "right", "right-start", "right-end", "bottom", "bottom-start", "bottom-end", "left", "left-start", "left-end", ], position: ["top", "right", "bottom", "left", "start", "end", "center"], shape: ["round", "rounded", "pill"], size: ["default", "xs", "sm", "md", "lg", "xl", "icon", "icon-xs", "icon-sm", "icon-lg"], target: ["_self", "_blank", "_parent", "_top"], trigger: ["click", "hover", "focus", "manual"], visualposition: ["left", "right"], width: ["default", "compact", "wide", "full"], }; function playgroundOptions(prop, component, currentValue) { if (prop.type !== "string" || prop.name === "class" || prop.name === "controlClass") return []; const profileOptions = profileFor(component.name)?.options?.[prop.name] ?? []; const common = commonPlaygroundOptions[prop.name.toLowerCase()] ?? []; const componentSpecific = component.name === "Button" && prop.name === "type" ? ["button", "submit", "reset"] : []; const declared = [...profileOptions, ...componentSpecific, ...common]; // Free-form strings such as icon names, labels, URLs, IDs, and descriptions // must remain text inputs. Implementation comparisons are not a public enum. if (declared.length === 0) return []; const values = new Set(); if (typeof currentValue === "string" && currentValue.length <= 120) values.add(currentValue); const declaredDefault = defaultValue(prop); if (typeof declaredDefault === "string" && declaredDefault.length <= 120) values.add(declaredDefault); for (const option of declared) { if (typeof option === "string" && option.length <= 120) values.add(option); } return [...values]; } function playgroundSection(component) { const controls = component.props.map((prop) => playgroundControl(prop, component)).join(""); const publicTag = /^[A-Z][A-Za-z0-9_]*$/.test(component.mount); const eventNames = component.events.join(","); return `
Interactive playground

Configure ${escapeText(displayName(component.name))}

Change any prop and inspect the server-rendered component immediately.

Live preview
${playgroundMount(component)}
Component code
${sourceSnippet(component, 0)}
${component.events.length ? `
Event outputInteract with the preview to inspect the typed payload.
` : ""}
Component props${component.props.length} controls
${controls || '

This component has no declared props. Its slot content remains composable.

'}
`; } const defaultUses = [ { eyebrow: "Recommended", title: "Production default", description: "Balanced spacing, hierarchy, and content for the most common product workflow.", }, { eyebrow: "Dense UI", title: "Compact application", description: "A tighter variation for dashboards, side panels, tables, and operational interfaces.", }, { eyebrow: "Extended", title: "Rich configuration", description: "A more expressive variation using additional data, stronger emphasis, and optional states.", }, ]; const buttonUses = [ { eyebrow: "Default", title: "Primary action", description: "The standard high-emphasis action for forms and product workflows.", }, { eyebrow: "Outline · Small", title: "Secondary navigation", description: "A compact link button with an end icon and restrained emphasis.", }, { eyebrow: "Secondary · Large · Pill", title: "Prominent rounded action", description: "A large secondary action using the optional pill shape.", }, { eyebrow: "Ghost · Icon", title: "Accessible icon action", description: "An icon-only control with its accessible name supplied through props.", }, { eyebrow: "Destructive", title: "Dangerous action", description: "A destructive treatment for irreversible or high-risk operations.", }, { eyebrow: "Link", title: "Inline action", description: "A low-chrome action that behaves and reads like an inline link.", }, { eyebrow: "Loading", title: "Pending action", description: "The button owns its busy state, loading label, spinner, and disabled behavior.", }, { eyebrow: "Disabled", title: "Unavailable action", description: "A visibly and semantically disabled control.", }, ]; const advancedSelectTitles = [ "Default advanced select", "Grouped options", "Fixed-position dropdown", "Placeholder with icon", "Allow empty selection", "Multiple selection", "Multiple selection with optgroups", "Multiple selection with option templates", "Multiple selection with counter", "Multiple selection with conditional counter", "Counter with option templates", "Searchable dropdown", "Minimum search length", "Search result limit", "Scroll to selected option", "Search match mode", "Search labels and descriptions", "Tag-style selection", "Disabled tag selection", "Option template with icons", "Option template with avatars", "Option template with color indicators", "Advanced select sizes", "Disabled advanced select", "Inside a modal", "Validation states", "Dynamic validation styling", "Add or remove options", "Add or remove options in multiple mode", "Set a single value programmatically", "Set multiple values programmatically", "Remote data source", "Remote data source (multiple)", "Remote data tags", "Option templates with remote data", "Conditional counter with remote data", "Avatar template with remote data", "Preselected values", "Infinite scroll", "Inside overflow-hidden containers", "Destroy and reinitialize", ]; const advancedSelectUses = advancedSelectTitles.map((title, index) => ({ eyebrow: index < 11 ? "Selection" : index < 23 ? "Search & templates" : "Advanced behavior", title, description: `Live ${title.toLowerCase()} configuration with copyable component code.`, })); function advancedSelectConfiguration(variation) { const title = advancedSelectTitles[variation] ?? advancedSelectTitles[0]; const lower = title.toLowerCase(); const options = [ { value: "design", label: "Design", description: "Product and visual design", icon: "icon-[lucide--palette]", color: "#8b5cf6", avatar: "/showcase-images/avatar-design.svg", }, { value: "engineering", label: "Engineering", description: "Platform and application engineering", icon: "icon-[lucide--code-2]", color: "#0ea5e9", avatar: "/showcase-images/avatar-engineering.svg", }, { value: "growth", label: "Growth", description: "Marketing and customer growth", icon: "icon-[lucide--trending-up]", color: "#10b981", avatar: "/showcase-images/avatar-growth.svg", }, { 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: "/showcase-images/avatar-design.svg", }, { value: "engineering", label: "Engineering", description: "Platform and application engineering", icon: "icon-[lucide--code-2]", color: "#0ea5e9", avatar: "/showcase-images/avatar-engineering.svg", }, { value: "growth", label: "Growth", description: "Marketing and customer growth", icon: "icon-[lucide--trending-up]", color: "#10b981", avatar: "/showcase-images/avatar-growth.svg", }, { value: "support", label: "Support", description: "Customer operations", icon: "icon-[lucide--life-buoy]", color: "#f59e0b", }, ]; const remote = lower.includes("remote") || lower.includes("infinite") || lower.includes("query parameter"); const grouped = lower.includes("grouped"); return { label: title, name: `combobox-${variation + 1}`, value: lower.includes("preselected") ? "engineering" : "", options: grouped ? [] : options, groups: grouped ? [ { label: "Product", options: options.slice(0, 2) }, { label: "Business", options: options.slice(2) }, ] : [], placeholder: remote ? "Search remote teams" : "Search teams", allowCustomValue: lower.includes("custom"), clearable: true, disabled: lower.includes("disabled"), required: lower.includes("validation"), invalid: false, validationMessage: "", minSearchLength: lower.includes("minimum") ? 3 : remote ? 2 : 0, searchResultLimit: lower.includes("result limit") ? 2 : 0, searchMode: lower.includes("starts-with") ? "startsWith" : "contains", optionTemplate: lower.includes("avatar") ? "avatar" : lower.includes("color") ? "color" : lower.includes("icon") ? "icon" : "default", fixed: lower.includes("fixed"), remote, remoteUrl: remote ? "/api/teams" : "", remoteQueryParam: "q", remoteAutoLoad: true, infinite: lower.includes("infinite"), hasMore: lower.includes("infinite"), }; } const pinInputTitles = [ "Default four-digit PIN", "Different lengths", "Pre-filled verification code", "Masked security PIN", "Alphanumeric regex", "Grouped code with separator", "Paste from clipboard", "Disabled PIN input", "Read-only code", "Validation state", "Supporting help text", "Large PIN cells", "Custom placeholder", "Automatic form submission", "Completion events", "JavaScript methods", ]; const pinInputUses = pinInputTitles.map((title, index) => ({ eyebrow: index < 7 ? "Entry behavior" : index < 13 ? "States & design" : "Automation", title, description: `Live ${title.toLowerCase()} example with keyboard navigation, filtering, and copyable component code.`, })); function pinInputConfiguration(variation) { const title = pinInputTitles[variation] ?? pinInputTitles[0]; const lower = title.toLowerCase(); const fourDigits = lower.includes("four-digit") || lower.includes("security pin"); return { label: title, name: `pin-input-${variation + 1}`, value: lower.includes("pre-filled") ? "482915" : lower.includes("read-only") ? "739204" : "", length: fourDigits ? 4 : 6, pattern: lower.includes("alphanumeric") ? "[A-Z0-9]" : "[0-9]", inputMode: lower.includes("alphanumeric") ? "text" : "numeric", placeholder: lower.includes("custom placeholder") ? "•" : "○", masked: lower.includes("masked"), disabled: lower.includes("disabled"), readonly: lower.includes("read-only"), required: lower.includes("validation"), invalid: false, validationMessage: "", allowPaste: true, separator: lower.includes("separator") ? "–" : "", groupSize: lower.includes("separator") ? 3 : 0, helpText: lower.includes("help text") ? "Enter the code sent to your registered email address." : "", autoSubmit: lower.includes("automatic"), size: lower.includes("large") ? "lg" : "default", }; } const strongPasswordTitles = [ "Live strength meter", "Requirements and hint text", "Requirements in a popover", "Custom special characters", "Optional requirements", "Disabled password strength", ]; const strongPasswordUses = strongPasswordTitles.map((title, index) => ({ eyebrow: index < 3 ? "Strength feedback" : "Configuration", title, description: `Live ${title.toLowerCase()} example with accessible scoring feedback and copyable component code.`, })); function strongPasswordConfiguration(variation) { const title = strongPasswordTitles[variation] ?? strongPasswordTitles[0]; const lower = title.toLowerCase(); return { label: title, name: `strong-password-${variation + 1}`, value: lower.includes("live") ? "Northstar!2026" : "", placeholder: "Create a strong password", autocomplete: "new-password", minLength: 8, specialCharactersSet: lower.includes("custom") ? "@#_-." : "!@#$%^&*()_+-=[]{}|;:,.<>?", requireLowercase: true, requireUppercase: true, requireNumber: true, requireSpecialCharacter: !lower.includes("optional"), showRequirements: !lower.includes("live"), presentation: lower.includes("popover") ? "popover" : "inline", hintText: lower.includes("hint") ? "Use a unique password you do not use for another account." : "", disabled: lower.includes("disabled"), readonly: false, required: false, invalid: false, validationMessage: "", }; } const togglePasswordTitles = [ "Inline visibility button", "Checkbox-controlled toggle", "Synchronized password fields", "Without visibility toggle", "Supporting help text", "Required validation", "Disabled password", "Read-only password", "Large password field", "Toggle events", ]; const togglePasswordUses = togglePasswordTitles.map((title, index) => ({ eyebrow: index < 4 ? "Visibility" : index < 8 ? "Forms & states" : "Design & events", title, description: `Live ${title.toLowerCase()} example with accessible visibility controls and copyable component code.`, })); function togglePasswordConfiguration(variation) { const title = togglePasswordTitles[variation] ?? togglePasswordTitles[0]; const lower = title.toLowerCase(); const checkbox = lower.includes("checkbox"); const synchronized = lower.includes("synchronized"); return { label: synchronized ? "Passwords" : lower.includes("inline") ? "Password" : title, name: `toggle-password-${variation + 1}`, value: lower.includes("inline") || checkbox || lower.includes("read-only") ? "Northstar!2026" : "", placeholder: "Enter your password", autocomplete: "current-password", minlength: lower.includes("validation") ? "8" : "", maxlength: "128", pattern: "(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^A-Za-z0-9]).{8,}", fields: synchronized ? [ { label: "New password", name: "new-password", placeholder: "Enter new password", autocomplete: "new-password", }, { label: "Current password", name: "current-password", value: "Northstar!2026", autocomplete: "current-password", }, ] : [], visible: false, toggleable: !lower.includes("without"), toggleMode: checkbox ? "checkbox" : "button", checkboxLabel: "Show password", disabled: lower.includes("disabled"), readonly: lower.includes("read-only"), required: lower.includes("validation"), helpText: lower.includes("help text") ? "Use at least eight characters and avoid passwords used on other sites." : "", validationMessage: "", invalid: false, size: lower.includes("large") ? "lg" : "default", }; } const inputNumberUses = [ { eyebrow: "Basic usage", title: "Default controls", description: "Use standard decrement and increment buttons alongside a numeric input field.", }, { eyebrow: "Basic usage", title: "Labeled input style", description: "Add supporting label text and rounded controls for a more form-like number input.", }, { eyebrow: "Layout", title: "Vertical buttons", description: "Stack the increment and decrement buttons vertically beside the field.", }, { eyebrow: "Layout", title: "Horizontal buttons", description: "Place the increment and decrement buttons on opposite sides of the value.", }, { eyebrow: "Sizing", title: "Compact size", description: "Use a smaller compact control when the available space is limited.", }, { eyebrow: "Commerce", title: "Seat quantity selector", description: "Pair the control with pricing information for seat counts or plan add-ons.", }, { eyebrow: "States", title: "Disabled input", description: "Disable manual entry while keeping supported quantity controls available.", }, { eyebrow: "States", title: "Disabled buttons", description: "Disable increment and decrement controls while leaving the current value visible.", }, { eyebrow: "Behaviour", title: "Custom step size", description: "Set a custom step so every button press changes the value by two.", }, { eyebrow: "Limits", title: "Negative values", description: "Use the min option to permit controlled negative values.", }, { eyebrow: "Limits", title: "Maximum limit", description: "Set an upper limit and disable incrementing when that limit is reached.", }, { eyebrow: "Validation", title: "Validation states", description: "Communicate whether the current numerical value is valid.", }, ]; function inputNumberConfiguration(variation) { const configurations = [ { name: "default-quantity", value: 1, min: 0, max: 99, step: 1, variant: "default", }, { name: "labeled-quantity", label: "Select quantity", value: 1, min: 1, max: 25, step: 1, variant: "labeled", }, { name: "vertical-quantity", label: "Select quantity", value: 1, min: 0, max: 20, variant: "vertical", }, { name: "horizontal-quantity", value: 1, min: 0, max: 20, variant: "horizontal", }, { name: "compact-quantity", value: 0, min: 0, max: 10, variant: "compact", size: "sm", }, { name: "additional-seats", label: "Additional seats", description: "$39 monthly", value: 0, min: 0, max: 50, variant: "seat", }, { name: "disabled-input", value: 10, inputDisabled: true, }, { name: "disabled-buttons", value: 10, buttonsDisabled: true, }, { name: "custom-step", value: 0, min: 0, max: 20, step: 2, }, { name: "negative-values", value: -10, min: -100, max: 100, }, { name: "maximum-limit", value: 10, min: 0, max: 10, help: "The maximum value is 10.", }, { name: "invalid-quantity", value: 10, min: 0, max: 5, error: "Out of limit", }, ]; return { color: "primary", size: "md", ...configurations[variation], }; } function demoUses(component) { const profile = profileFor(component.name); if (profile?.demos?.length) { // `before` is carried through: it is markup rendered beside the mount for // host components that have no slot to put controls in. return profile.demos.map(({ eyebrow, title, description, before }) => ({ eyebrow, title, description, before, })); } switch (component.name) { case "Button": return buttonUses; case "AdvancedSelect": return advancedSelectUses; case "ComboBox": return comboboxUses; case "InputNumber": return inputNumberUses; case "PinInput": return pinInputUses; case "StrongPassword": return strongPasswordUses; case "TogglePassword": return togglePasswordUses; default: return defaultUses; } } function demoConfiguration(component, variation) { const demo = profileFor(component.name)?.demos?.[variation]; if (!demo) return {}; return { ...(demo.props ?? {}), __slots: demo.slots ?? {}, }; } function eventDocumentation(component) { if (!component.events.length) return ""; const eventRows = component.events .map( (eventName) => `
@${escapeCode(eventName)}${escapeText(eventDescriptionFor(component.name, eventName))}
`, ) .join(""); const declarativeAttributes = component.events .map((eventName) => ` @${eventName}='console.log(payload)'`) .join("\n"); const declarativeCode = `<${component.mount}\n${declarativeAttributes}\n/>`; const selector = `[data-ui-component="${component.name}"], [data-component="${component.mount}"]`; const listenerCode = `import { registerOutputHandler } from "@wrnexus/csr/outputs"\n\nconst component = document.querySelector(${JSON.stringify(selector)})\n\n${component.events .map( (eventName) => `if (component) registerOutputHandler(component, ${JSON.stringify(eventName)}, (payload) => {\n console.log(${JSON.stringify(eventName)}, payload)\n})`, ) .join("\n\n")}`; return `
Component outputs

Receive every typed component output

Use declarative output handlers in .wrn files or register a direct output handler from JavaScript. The canonical API exposes payload and does not require event.detail.

${eventRows}
Declarative handlers.wrn
${escapeCode(declarativeCode)}
Direct output handlers.js
${escapeCode(listenerCode)}
`; } function detailPage(component, categoryComponents) { // Names are per page; without this they accumulate across the whole run. resetHoistedObjects(); const index = categoryComponents.findIndex((item) => item.name === component.name); const previous = categoryComponents[index - 1]; const next = categoryComponents[index + 1]; const uses = demoUses(component); const demos = uses .map((useCase, variation) => { const configuredDemo = demoConfiguration(component, variation); const isPinLengthDemo = component.name === "PinInput" && variation === 1; const pinLengths = [3, 5, 7]; const mount = isPinLengthDemo ? `
${pinLengths .map((length) => componentMount(component, variation, { ...configuredDemo, label: `${length}-digit code`, name: `pin-length-${length}`, length, }), ) .join("")}
` : componentMount(component, variation, configuredDemo); const snippet = isPinLengthDemo ? pinLengths .map((length) => sourceSnippet(component, variation, { ...configuredDemo, label: `${length}-digit code`, name: `pinLength${length}`, length, }), ) .join("\n\n") : sourceSnippet(component, variation, configuredDemo); const validationSchema = component.name === "AdvancedSelect" && variation === 25 ? "advanced-select-validation" : component.name === "AdvancedSelect" && variation === 26 ? "advanced-select-dynamic-validation" : component.name === "ComboBox" && variation === 14 ? "combobox-validation" : component.name === "PinInput" && variation === 9 ? "pin-input-validation" : component.name === "TogglePassword" && variation === 5 ? "toggle-password-validation" : ""; const preview = validationSchema ? `
${mount}
` : mount; // `before` renders markup NEXT TO the mount rather than inside its slot. // Host-style components (Toaster) have no slot at all: what a reader // needs to see is the controls that drive them, and slot content would // simply be dropped. const beforeMarkup = useCase.before ?? ""; return `
${useCase.eyebrow}

${useCase.title}

${useCase.description}

0${variation + 1}
Live preview
${isInteractiveOverlay(component) ? `

${escapeText(overlayInteractionLabel(component))}

` : ""}
${beforeMarkup}${preview}
Component usage.wrn
${snippet}
`; }) .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)}${hoistedObjectStates()} seo { title = "${escapeAttribute(displayName(component.name))}" description = "${escapeAttribute(component.purpose)}" } view {
${titleCase(component.category)} component

${escapeText(displayName(component.name))}

${escapeText(component.purpose)}

${component.props.length} props${component.slots.length} slots${component.events.length} outputsTheme readyResponsive
${playgroundSection(component)}
Live examples

Designed for real product surfaces

Compare configurations and resize the browser to check responsive behavior.

${demos}
${eventDocumentation(component)}
Component API

Props and configuration

All content and behavior shown above is supplied through these props and slots.

${propsTable(component)}
} } `; } function categoryPage(category, components) { const liveDemoCount = components.reduce( (total, component) => total + demoUses(component).length, 0, ); const cards = components .map( (component) => `
${componentMount(component, 0, demoConfiguration(component, 0))}${isInteractiveOverlay(component) ? `

${escapeText(overlayInteractionLabel(component))}

` : ""}
${titleCase(category)}

${escapeText(displayName(component.name))}

${escapeText(component.purpose)}

`, ) .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 {
Component category

${titleCase(category)}

${components.length} unique, responsive, theme-aware components. Open a component to inspect multiple live configurations and its complete props API.

${components.length} componentsMultiple use cases${liveDemoCount} live configurations
${cards}
} } `; } const categories = Object.groupBy(reference.components, (component) => component.category); const categoryEntries = Object.entries(categories).sort(([left], [right]) => left === "advanced-forms" ? 1 : right === "advanced-forms" ? -1 : left.localeCompare(right), ); const totalDemoCount = reference.components.reduce( (total, component) => total + demoUses(component).length, 0, ); function showcaseLayout() { const directory = categoryEntries .map( ([category, components]) => `
${titleCase(category)}
${components .map( (component) => `${escapeText(displayName(component.name))}`, ) .join("")}
`, ) .join(""); return `// Generated by scripts/generate-showcase.mjs. Do not edit directly. layout Showcase { view {
WRNexus UI
} } `; } function documentLayout() { return `// Generated by scripts/generate-showcase.mjs. Do not edit directly. layout Document { runtime = "server" props { cookies = {} theme = "light" language = "en" url = "" pathname = "/" } view {
} } `; } mkdirSync(pagesDir, { recursive: true }); mkdirSync(detailPagesDir, { recursive: true }); mkdirSync(layoutsDir, { recursive: true }); writeFileSync(join(layoutsDir, "showcase.wrn"), showcaseLayout(), "utf8"); writeFileSync(join(layoutsDir, "document.wrn"), documentLayout(), "utf8"); for (const file of readdirSync(pagesDir).filter((entry) => entry.endsWith(".wrn"))) { unlinkSync(join(pagesDir, file)); } for (const file of readdirSync(detailPagesDir).filter((entry) => entry.endsWith(".wrn"))) { unlinkSync(join(detailPagesDir, file)); } for (const [category, components] of categoryEntries) { writeFileSync(join(pagesDir, `${category}.wrn`), categoryPage(category, components), "utf8"); for (const component of components) { writeFileSync( join(detailPagesDir, `${slugOf(component.name)}.wrn`), detailPage(component, components), "utf8", ); } } const showcaseManifest = { generatedFrom: "packages/ui/component-reference.json", removed: [...excludedNames], blocks: blockComponents.map(component => ({ name: component.name, slug: slugOf(component.name), path: `/blocks/${slugOf(component.name)}` })), componentCount: reference.count, categoryCount: categoryEntries.length, totalDemoCount, components: reference.components.map((component) => ({ name: component.name, mount: component.mount, slug: slugOf(component.name), category: component.category, purpose: component.purpose, demoCount: demoUses(component).length, propCount: component.props.length, slots: component.slots, events: component.events, profiled: Boolean(profileFor(component.name)), })), }; const formattedManifest = await prettier.format(`${JSON.stringify(showcaseManifest, null, 2)}\n`, { parser: "json", printWidth: 100, endOfLine: "lf", }); writeFileSync(join(exampleRoot, "showcase-manifest.json"), formattedManifest, "utf8"); const categoryCards = categoryEntries .map( ([category, components], index) => `0${index + 1}
${titleCase(category)}

${components.length} components · ${components.reduce((total, component) => total + demoUses(component).length, 0)} live demos

`, ) .join(""); writeFileSync( join(pagesDir, "index.wrn"), `// Generated by scripts/generate-showcase.mjs. Do not edit directly. page ComponentShowcase { layout = "showcase" seo { title = "WRNexus component system" description = "Explore every unique @wrnexus/ui component through live, production-quality examples." } view {
The UI foundation for WRNexus

Build modern products
at framework speed.

A complete, accessible component system with ${reference.count} primitives, ready-made blocks, page templates, responsive behavior, and deeply configurable design tokens.

$bun add @wrnexus/ui
${reference.count}Unique components
${totalDemoCount}Live configurations
0Duplicate implementations
${categoryEntries.length}Focused categories
Explore the system

Everything your product needs

${categoryCards}
} } `, "utf8", ); const docsSections = [ [ "installation", "Installation", "Install the package and import the shared stylesheet once in your application.", "bun add @wrnexus/ui", ], [ "framework-guides", "Framework guides", "WRNexus components use native HTML semantics and framework component mounts.", ], [ "accessibility", "Accessibility", "Keyboard interaction, visible focus states, labels, and reduced-motion support ship by default.", ], [ "dark-mode", "Dark mode", "Choose light, dark, or system mode. The selection persists and updates every preview.", ], [ "themes", "Themes", "Theme presets control radius, surface treatment, borders, shadows, and motion.", ], [ "colors", "Colors", "Semantic primary, secondary, success, warning, danger, and info tokens remain available in every palette.", ], [ "fonts", "Fonts", "Select a font family globally while components continue to inherit typography correctly.", ], [ "sizing", "Sizing", "Compact through large scales adjust the complete component system consistently.", ], [ "resources", "Resources", "Use the component API pages, blocks, and templates as production starting points.", ], ]; writeFileSync( join(pagesDir, "docs.wrn"), `page Documentation { layout = "showcase" seo { title = "Documentation" description = "Install, configure, and use WRNexus UI." } view {
Getting started

Build with WRNexus UI

Configure one design system and use it across components, blocks, and complete page templates.

Explore the documentation

Every Getting Started and Customization topic now has its own page. Choose a guide from the sidebar to continue.

} } `, "utf8", ); const v06Guides = [ [ "types", "Types", "TypeScript contracts for props, state, outputs, imported application types, and generated declarations.", [ [ "Application types", "Load ambient app/types/global.d.ts and explicitly import types from app/types/*.ts.", ], [ "Typed contracts", "Validate component props, state assignments, function parameters, returns, and output payloads.", ], [ "Generated declarations", "Generate component, output, store, layout, and server-call declarations under dist/types.", ], ], ], [ "functions", "Functions", "Classify behavior for the browser, server, or both runtimes from one functions block.", [ ["Client", "Compile browser-only functions into CSP-safe client modules."], [ "Server", "Keep server-only functions out of browser artifacts and expose typed RPC calls when referenced.", ], ["Shared", "Emit deterministic shared helpers to both compilation targets."], ], ], [ "outputs", "Outputs", "Use typed callable outputs for child-to-parent communication without application-level $emit or event.detail.", [ ["Declare", "Define zero- or one-payload output signatures in an outputs block."], ["Invoke", "Call output.name(payload) from client functions."], ["Consume", "Receive the typed payload local in parent component listeners."], ], ], [ "stores", "Stores", "Create request-safe global stores and route-scoped page stores with typed actions and computed state.", [ ["Global stores", "Survive CSR navigation while remaining isolated for every SSR request."], [ "Page stores", "Share state with route descendants and dispose it when navigation leaves the route.", ], ["Persistence", "Persist include-only safe fields to memory, session, or local storage."], ], ], [ "imports", "Imports", "Declare component, layout, store, package, and type dependencies explicitly while retaining compatibility mode.", [ ["Application imports", "Resolve @/ aliases to the active application directory."], ["Package imports", "Use named imports for components exported by @wrnexus/ui."], [ "Compatibility", "Run legacy, compatible, or explicit import modes during project migration.", ], ], ], [ "state", "State", "Use individual or grouped typed state declarations with shared, client-only, and server-only scopes.", [ [ "Shared state", "Render during SSR and safely hydrate serializable values into the browser.", ], ["Client state", "Keep browser-only interaction state out of server artifacts."], ["Server state", "Keep sensitive request-only state out of hydration data."], ], ], [ "runtime", "Runtime", "Split server and browser artifacts, hydrate only interactive components, and preserve compatible state during HMR.", [ [ "Partial hydration", "Load client code using load, idle, visible, interaction, media, or none strategies.", ], [ "RPC", "Validate browser-to-server inputs and outputs with same-origin and CSRF-aware defaults.", ], ["HMR", "Replace functions and preserve compatible component and store state."], ], ], [ "migration", "Migration", "Upgrade older projects through idempotent v0.6 migrations with backups, dry runs, and review reports.", [ [ "Legacy outputs", "Convert @event declarations, $emit calls, and event.detail listeners to outputs and payload.", ], [ "Explicit dependencies", "Add component and layout imports only when resolution is unambiguous.", ], [ "Safety", "Parse and type-check changed files and roll back when migration validation fails.", ], ], ], ]; for (const [slug, title, description, sections] of v06Guides) { writeFileSync( join(pagesDir, `v06-${slug}.wrn`), `// Generated by scripts/generate-showcase.mjs. Do not edit directly. page V06${identifier(title)}Guide { layout = "showcase" seo { title = "WRNexusJS v0.6 ${escapeAttribute(title)}" description = "${escapeAttribute(description)}" } view {
WRNexusJS v0.6

${escapeText(title)}

${escapeText(description)}

${sections.map(([heading, copy]) => `

${escapeText(heading)}

${escapeText(copy)}

`).join("")}
} } `, "utf8", ); } const practicalGuides = { installation: {heading:"Render your first component", copy:"Import the shared UI stylesheet in your application entry. In a WRN page, use registered components directly. Keep user-editable values in page state and listen to typed outputs.", code:'import "@wrnexus/ui/ui.css"\n\npage Welcome {\n view {