feat: release design system engine, studio and component contracts

This commit is contained in:
Clintchiz committed 2026-10-19 13:09:03 +05:30
1 parent ff1065fb27
commit 3943b946d6
693 files changed
+175546 -3894

No files matched your search

@@ -6,6 +6,13 @@ import prettier from "prettier";
import { generateCatalogPages } from "./generate-catalog-pages.mjs";
import { eventDescriptionFor, profileFor } from "./showcase-profiles.mjs";
// Keep generated WRN blank lines free of trailing whitespace.
function writeGeneratedFile(path, source, encoding) {
const content =
typeof source === "string" && path.endsWith(".wrn") ? source.replace(/[ \t]+$/gm, "") : source;
writeFileSync(path, content, encoding);
}
const exampleRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
const workspaceRoot = join(exampleRoot, "..", "..");
const referencePath = join(workspaceRoot, "packages", "ui", "component-reference.json");
@@ -13,13 +20,34 @@ 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));
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",
@@ -283,21 +311,186 @@ 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",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"},
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",
@@ -780,7 +973,13 @@ function sourceSnippet(component, variation, overrides = {}) {
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 (typeof data === "string") {
try {
data = JSON.parse(data);
} catch {
/* Keep non-JSON examples as their original strings. */
}
}
if (Array.isArray(data)) {
declarations.push(` state ${prop.name} = ${JSON.stringify(data, null, 2)}`);
return [`${prop.name}="{${prop.name}}"`];
@@ -824,7 +1023,14 @@ function sourceSnippet(component, variation, overrides = {}) {
: 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);
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}"`;
@@ -863,6 +1069,7 @@ function playgroundStateName(prop) {
function isStructuredProp(prop) {
return (
(prop.name === "data" && prop.type === "unknown") ||
(prop.default ?? "").startsWith("[") ||
prop.default === "[]" ||
prop.default === "{}" ||
@@ -1930,7 +2137,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="/preview">Examples</a><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><a href="/design-studio/" data-no-nav>Design Studio</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>
@@ -1991,8 +2198,8 @@ layout Document {
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");
writeGeneratedFile(join(layoutsDir, "showcase.wrn"), showcaseLayout(), "utf8");
writeGeneratedFile(join(layoutsDir, "document.wrn"), documentLayout(), "utf8");
for (const file of readdirSync(pagesDir).filter((entry) => entry.endsWith(".wrn"))) {
unlinkSync(join(pagesDir, file));
}
@@ -2001,9 +2208,9 @@ for (const file of readdirSync(detailPagesDir).filter((entry) => entry.endsWith(
}
for (const [category, components] of categoryEntries) {
writeFileSync(join(pagesDir, `${category}.wrn`), categoryPage(category, components), "utf8");
writeGeneratedFile(join(pagesDir, `${category}.wrn`), categoryPage(category, components), "utf8");
for (const component of components) {
writeFileSync(
writeGeneratedFile(
join(detailPagesDir, `${slugOf(component.name)}.wrn`),
detailPage(component, components),
"utf8",
@@ -2014,7 +2221,11 @@ 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)}` })),
blocks: blockComponents.map((component) => ({
name: component.name,
slug: slugOf(component.name),
path: `/blocks/${slugOf(component.name)}`,
})),
componentCount: reference.count,
categoryCount: categoryEntries.length,
totalDemoCount,
@@ -2036,7 +2247,7 @@ const formattedManifest = await prettier.format(`${JSON.stringify(showcaseManife
printWidth: 100,
endOfLine: "lf",
});
writeFileSync(join(exampleRoot, "showcase-manifest.json"), formattedManifest, "utf8");
writeGeneratedFile(join(exampleRoot, "showcase-manifest.json"), formattedManifest, "utf8");
const categoryCards = categoryEntries
.map(
@@ -2045,7 +2256,7 @@ const categoryCards = categoryEntries
)
.join("");
writeFileSync(
writeGeneratedFile(
join(pagesDir, "index.wrn"),
`// Generated by scripts/generate-showcase.mjs. Do not edit directly.
page ComponentShowcase {
@@ -2126,7 +2337,7 @@ const docsSections = [
"Use the component API pages, blocks, and templates as production starting points.",
],
];
writeFileSync(
writeGeneratedFile(
join(pagesDir, "docs.wrn"),
`page Documentation {
layout = "showcase"
@@ -2261,7 +2472,7 @@ const v06Guides = [
],
];
for (const [slug, title, description, sections] of v06Guides) {
writeFileSync(
writeGeneratedFile(
join(pagesDir, `v06-${slug}.wrn`),
`// Generated by scripts/generate-showcase.mjs. Do not edit directly.
page V06${identifier(title)}Guide {
@@ -2285,15 +2496,51 @@ 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}" />'}
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) {
@@ -2334,7 +2581,7 @@ for (const [slug, title, description, code] of docsSections) {
"The setting is inherited by components, blocks, templates, and playground previews.",
],
];
writeFileSync(
writeGeneratedFile(
join(pagesDir, `${slug}.wrn`),
`page ${identifier(slug)}Guide {
layout = "showcase"
@@ -2399,7 +2646,7 @@ const blockCards = [
],
];
// Keep /blocks for the generated Blocks component category.
writeFileSync(
writeGeneratedFile(
join(pagesDir, "block-library.wrn"),
`page BlockLibrary {
layout = "showcase"
@@ -2448,7 +2695,7 @@ const templates = [
"Accounts, transactions, reporting, transfers, and controls.",
],
];
writeFileSync(
writeGeneratedFile(
join(pagesDir, "templates.wrn"),
`page Templates {
layout = "showcase"
@@ -2469,4 +2716,17 @@ 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 });
await generateCatalogPages({
exampleRoot,
pagesDir,
blockComponents,
componentMount,
sourceSnippet,
demoConfiguration,
slugOf,
displayName,
escapeCode,
escapeText,
resetHoistedObjects,
hoistedObjectStates,
});