Refresh UI components and showcase; release updated framework packages

This commit is contained in:
Clintchiz committed 2026-09-20 18:13:24 +05:30
1 parent d800228585
commit c77f5e1cec
293 files changed
+22544 -35778

No files matched your search

@@ -3,6 +3,7 @@ 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)), "..");
@@ -12,6 +13,12 @@ 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",
@@ -56,7 +63,7 @@ for (const component of reference.components) {
}
const slugOf = (name) => {
const slug = baseSlugOf(name);
if (slugCounts.get(slug) === 1) return slug;
if (!slugCounts.has(slug) || slugCounts.get(slug) === 1) return slug;
const component = reference.components.find((entry) => entry.name === name);
return `${slug}-${component?.category ?? "component"}`;
};
@@ -263,7 +270,7 @@ function defaultValue(prop) {
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 ?? "")) {
if (/^".*"$/.test(prop.default ?? "") || (prop.default ?? "").startsWith("[")) {
try {
return JSON.parse(prop.default);
} catch {
@@ -275,6 +282,216 @@ function defaultValue(prop) {
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",status:"Delivered"},{text:"Thanks, I will take a look this afternoon.",direction:"incoming",timestamp:"10:26"}] },
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];
@@ -417,6 +634,7 @@ function sampleValue(prop, component, variation) {
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) {
@@ -479,6 +697,8 @@ function componentAttributes(component, variation, overrides = {}) {
function configuredSlotMarkup(component, variation, overrides = {}) {
const configured = overrides.__slots ?? {};
if (component.name === "LayoutSplitter" && !Object.keys(configured).length)
return '<div data-slot="start"><h3>Project files</h3><p>Drag the divider to resize these panels.</p></div><div data-slot="end"><h3>Editor</h3><p>Your selected file appears here.</p></div>';
return component.slots
.map((slot) => {
const explicitlyConfigured = Object.hasOwn(configured, slot);
@@ -552,11 +772,21 @@ function indentSource(value, spaces = 2) {
}
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 &&
@@ -594,7 +824,7 @@ function sourceSnippet(component, variation, overrides = {}) {
: formattedAttributes
? `<${component.mount}${formattedAttributes}\n/>`
: `<${component.mount} />`;
return escapeCode(source);
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}"`;
@@ -633,6 +863,7 @@ function playgroundStateName(prop) {
function isStructuredProp(prop) {
return (
(prop.default ?? "").startsWith("[") ||
prop.default === "[]" ||
prop.default === "{}" ||
prop.type === "array" ||
@@ -655,7 +886,10 @@ function playgroundStates(component) {
return ` state ${playgroundStateName(prop)} = Number(${query} ?? ${JSON.stringify(String(initial))})`;
}
if (isStructuredProp(prop)) {
const initial = JSON.stringify(initialValue ?? (prop.default === "{}" ? {} : []));
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 ?? ""))}`;
@@ -689,6 +923,7 @@ function playgroundControl(prop, component) {
}
const structured =
(prop.default ?? "").startsWith("[") ||
prop.default === "[]" ||
prop.default === "{}" ||
prop.type === "array" ||
@@ -1661,7 +1896,7 @@ page ${identifier(category)}Showcase {
const categories = Object.groupBy(reference.components, (component) => component.category);
const categoryEntries = Object.entries(categories).sort(([left], [right]) =>
left.localeCompare(right),
left === "advanced-forms" ? 1 : right === "advanced-forms" ? -1 : left.localeCompare(right),
);
const totalDemoCount = reference.components.reduce(
(total, component) => total + demoUses(component).length,
@@ -1695,7 +1930,7 @@ layout Showcase {
<label class="docs-search"><span class="icon-[lucide--search] size-4" aria-hidden="true"></span><input type="search" placeholder="Search documentation..." data-docs-search /><kbd>⌘ K</kbd></label>
</div>
<div class="showcase-header-end">
<nav aria-label="Primary navigation" class="showcase-nav"><a href="/docs">Docs</a><a href="/block-library">Blocks</a><a href="/templates">Templates</a></nav>
<nav aria-label="Primary navigation" class="showcase-nav"><a href="/preview">Examples</a><a href="/docs">Docs</a><a href="/block-library">Blocks</a><a href="/templates">Templates</a></nav>
<button type="button" class="showcase-theme" data-design-panel-toggle aria-label="Customize design"><span class="icon-[lucide--palette] size-5" aria-hidden="true"></span></button>
</div>
</header>
@@ -1712,11 +1947,10 @@ layout Showcase {
<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>WRNexusJS v0.6</strong><div><a href="/v06-types">Types</a><a href="/v06-functions">Functions</a><a href="/v06-outputs">Outputs</a><a href="/v06-stores">Stores</a><a href="/v06-imports">Imports</a><a href="/v06-state">State</a><a href="/v06-runtime">Runtime</a><a href="/v06-migration">Migration</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>
<p class="docs-directory-empty" data-docs-empty hidden>No components found.</p>
</aside>
<main class="showcase-main"><slot /></main>
</div>
@@ -1779,6 +2013,8 @@ for (const [category, components] of categoryEntries) {
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,
@@ -2048,6 +2284,17 @@ page V06${identifier(title)}Guide {
);
}
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 { <Button label="Get started" href="/preview" /> }\n}'},
"framework-guides": {heading:"Connect a field to page state",copy:"Props supply initial configuration. Output handlers receive payload directly. Use payload.value to update your page state and display the committed result.",code:'page NameForm {\n state name: string = ""\n view { <Input label="Your name" value="{name}" @input="name = payload.value" /><p>Hello {name}</p> }\n}'},
accessibility: {heading:"Label controls and explain errors",copy:"Give every control a visible label. Use helperText for instructions and error for actionable feedback. Test Tab, Shift+Tab, Enter and Escape; focus must remain visible and follow the reading order.",code:'<Input label="Email address" type="email" name="email" required="true" helperText="Use your work email." error="Enter a valid email address." />\n<Button label="Save changes" disabled="true" />'},
"dark-mode": {heading:"Use semantic colors in both modes",copy:"Use shared surface, text, border and focus tokens instead of hard-coded white and black. Check disabled, hover, selected and error states in both themes using the appearance control.",code:'.panel { background: var(--wrn-color-surface); color: var(--wrn-color-text); border: 1px solid var(--wrn-color-border); }'},
themes: {heading:"Keep theme overrides in one place",copy:"Override design tokens at your application root so all components inherit the same appearance. Start with radius and brand color, then review overlays, forms and navigation together.",code:':root { --wrn-radius: 8px; --wrn-color-primary: #7c3aed; }\n<Button label="Primary action" color="primary" />'},
colors: {heading:"Choose color by meaning",copy:"Use primary for the main action, danger for destructive actions and success for completion. Always include text or an icon so color is not the only signal.",code:'<Button label="Save" color="primary" />\n<Button label="Delete record" color="danger" />\n<Badge label="Active" color="success" />'},
fonts: {heading:"Set one application font",copy:"Components inherit the shared font family. Supply a font stack once; use font weights and spacing to establish hierarchy instead of enlarging every label.",code:':root { --wrn-font-sans: "Inter", system-ui, sans-serif; }\nbody { font-family: var(--wrn-font-sans); }'},
sizing: {heading:"Match density to the task",copy:"Use compact controls for dense toolbars and tables, and the default size for forms. Keep related controls at the same size and retain usable touch targets on small screens.",code:'<Button label="Filter" size="sm" />\n<Input label="Project name" size="default" />\n<RangeSlider label="Scale" min="0" max="5" step="0.2" value="2.4" />'},
resources: {heading:"Choose the right starting point",copy:"Component pages document individual controls. Blocks combine controls into sections. Examples demonstrate complete workflows. Templates provide product-oriented starting pages with source you can adapt.",code:'// DataTable accepts arrays directly through bindings.\nstate rows = [{ id: 1, name: "Northwind" }]\nstate columns = [{ key: "name", label: "Account" }]\n<DataTable rows="{rows}" columns="{columns}" />'}
};
const customizationGuides = new Set(["dark-mode", "themes", "colors", "fonts", "sizing"]);
for (const [slug, title, description, code] of docsSections) {
const section = customizationGuides.has(slug) ? "Customization" : "Getting Started";
@@ -2100,6 +2347,7 @@ for (const [slug, title, description, code] of docsSections) {
<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("")}
${practicalGuides[slug] ? `<section class="guide-section"><h2>${escapeText(practicalGuides[slug].heading)}</h2><p>${escapeText(practicalGuides[slug].copy)}</p><pre><code>${escapeCode(practicalGuides[slug].code)}</code></pre><h3>Verify the result</h3><p>Open the matching component page, change its options, and try the keyboard controls. Copy the demonstrated configuration into your page, then replace sample content with your data.</p></section>` : ""}
${code ? `<section class="guide-section"><h2>Quick start</h2><pre><code>${escapeCode(code)}</code></pre></section>` : ""}
<nav class="guide-next"><a href="/components/button">Explore components <span aria-hidden="true">→</span></a><a href="/block-library">Browse blocks <span aria-hidden="true">→</span></a></nav>
</article>
@@ -2220,3 +2468,5 @@ writeFileSync(
process.stdout.write(
`Generated ${reference.count} component detail pages, ${totalDemoCount} live demos, and ${categoryEntries.length} category pages.\n`,
);
await generateCatalogPages({ exampleRoot, pagesDir, blockComponents, componentMount, sourceSnippet, demoConfiguration, slugOf, displayName, escapeCode, escapeText, resetHoistedObjects, hoistedObjectStates });