feat: release design system engine, studio and component contracts
This commit is contained in:
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,
|
||||
});
|
||||
Reference in new issue
Block a user