feat: deployable design studio and updated component showcase

This commit is contained in:
Clintchiz committed 2026-10-19 13:18:06 +05:30
1 parent 3dddcaac1a
commit 3987ddbcfe
113 files changed
+4544 -1039

No files matched your search

+184 -50
View File
@@ -1,67 +1,201 @@
import { existsSync, unlinkSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
import {
existsSync,
unlinkSync,
mkdirSync,
readFileSync,
readdirSync,
writeFileSync,
} from "node:fs";
import { join } from "node:path";
export async function generateCatalogPages({ exampleRoot, pagesDir, blockComponents, componentMount, sourceSnippet, demoConfiguration, slugOf, displayName, escapeCode, escapeText, resetHoistedObjects, hoistedObjectStates }) {
const write = (path, text) => { mkdirSync(join(pagesDir, path.substring(0,path.lastIndexOf('/')) || '.'), {recursive:true}); writeFileSync(join(pagesDir,path), text); };
const codePanel = (source, id="example-source") => `<section class="example-code-panel"><header><h2>Copy this example</h2><button type="button" data-copy-target="#${id}">Copy code</button></header><pre><code id="${id}">${escapeCode(source)}</code></pre></section>`;
const page = (name,title,body) => `page ${name} {\n layout = "gallery"\n seo { title = "${title}" }\n view { ${body} }\n}\n`;
const cards = entries => `<div class="example-catalog">${entries.map(e=>`<a class="example-card" href="${e.href}"><div class="example-card__art"><span class="icon-[lucide--${e.icon}]" aria-hidden="true"></span></div><div><h2>${e.title}</h2><p>${e.description}</p><strong>Open example →</strong></div></a>`).join('')}</div>`;
export async function generateCatalogPages({
exampleRoot,
pagesDir,
blockComponents,
componentMount,
demoConfiguration,
slugOf,
displayName,
escapeCode,
resetHoistedObjects,
hoistedObjectStates,
}) {
const write = (path, text) => {
mkdirSync(join(pagesDir, path.substring(0, path.lastIndexOf("/")) || "."), { recursive: true });
writeFileSync(join(pagesDir, path), text);
};
const codePanel = (source, id = "example-source") =>
`<section class="example-code-panel"><header><h2>Copy this example</h2><button type="button" data-copy-target="#${id}">Copy code</button></header><pre><code id="${id}">${escapeCode(source)}</code></pre></section>`;
const page = (name, title, body) =>
`page ${name} {\n layout = "gallery"\n seo { title = "${title}" }\n view { ${body} }\n}\n`;
const cards = (entries) =>
`<div class="example-catalog">${entries.map((e) => `<a class="example-card" href="${e.href}"><div class="example-card__art"><span class="icon-[lucide--${e.icon}]" aria-hidden="true"></span></div><div><h2>${e.title}</h2><p>${e.description}</p><strong>Open example →</strong></div></a>`).join("")}</div>`;
const examples = [
{slug:'workspace',title:'Workspace overview',icon:'layout-dashboard',description:'A complete dashboard with navigation, metrics, and service cards.'},
{slug:'profile',title:'Profile & preferences',icon:'user-round',description:'A responsive settings form with grouped fields and save feedback.'},
{slug:'sign-in',title:'Sign in',icon:'log-in',description:'A focused authentication page with a compact form.'},
{slug:'scheduling',title:'Scheduling',icon:'calendar-days',description:'Date, date-and-time, and time controls in a practical form.'},
{slug:'custom-modal',title:'Custom modal',icon:'panels-top-left',description:'Open and close a fully controlled dialog from your own buttons.'},
{slug:'project-board',title:'Project workspace',icon:'list-checks',description:'Reorder priorities and write rich-text project updates.'},
{
slug: "workspace",
title: "Workspace overview",
icon: "layout-dashboard",
description: "A complete dashboard with navigation, metrics, and service cards.",
},
{
slug: "profile",
title: "Profile & preferences",
icon: "user-round",
description: "A responsive settings form with grouped fields and save feedback.",
},
{
slug: "sign-in",
title: "Sign in",
icon: "log-in",
description: "A focused authentication page with a compact form.",
},
{
slug: "scheduling",
title: "Scheduling",
icon: "calendar-days",
description: "Date, date-and-time, and time controls in a practical form.",
},
{
slug: "custom-modal",
title: "Custom modal",
icon: "panels-top-left",
description: "Open and close a fully controlled dialog from your own buttons.",
},
{
slug: "project-board",
title: "Project workspace",
icon: "list-checks",
description: "Reorder priorities and write rich-text project updates.",
},
];
const extra = {
scheduling:`page SchedulingExample {\n view { <main class="example-form-page" style="max-width:42rem;margin:3rem auto;padding:1.5rem;display:grid;gap:1.5rem"><h1>Schedule a meeting</h1><p>Choose the date and time that work for your team.</p><DatePicker label="Meeting date" name="date" /><DatePicker type="datetime-local" label="Send reminder" name="reminder" value="2026-09-20T09:30" /><TimePicker label="Start time" name="time" value="09:30" /></main> }\n}`,
'custom-modal':`page CustomModalExample {\n state dialogOpen: boolean = false\n view { <main class="example-form-page" style="max-width:42rem;margin:3rem auto;padding:1.5rem;display:grid;gap:1.5rem"><h1>Your own modal controls</h1><Button label="Invite a teammate" @click="dialogOpen = true" /><Modal open="{dialogOpen}" @close="dialogOpen = false" title="" description="" showFooter="true" cancelLabel="" confirmLabel=""><div data-slot="header"><h2>Invite a teammate</h2></div><Input label="Email address" type="email" name="invite-email" placeholder="name@company.com" /><div data-slot="footer"><Button label="Cancel" variant="secondary" @click="dialogOpen = false" /><Button label="Done" @click="dialogOpen = false" /></div></Modal></main> }\n}`,
'project-board':`page ProjectBoardExample {\n state tasks = [{label:"Review designs"},{label:"Build the dashboard"},{label:"Test accessibility"}]\n view { <main class="example-form-page" style="max-width:42rem;margin:3rem auto;padding:1.5rem;display:grid;gap:1.5rem"><h1>Project workspace</h1><DragAndDrop title="Priorities" description="Drag a row or use its arrow buttons." items="{tasks}" /><WysiwygEditor title="Team update" value="Share what you are working on this week." /></main> }\n}`,
scheduling: `page SchedulingExample {\n view { <main class="example-form-page" style="max-width:42rem;margin:3rem auto;padding:1.5rem;display:grid;gap:1.5rem"><h1>Schedule a meeting</h1><p>Choose the date and time that work for your team.</p><DatePicker label="Meeting date" name="date" /><DatePicker type="datetime-local" label="Send reminder" name="reminder" value="2026-09-20T09:30" /><TimePicker label="Start time" name="time" value="09:30" /></main> }\n}`,
"custom-modal": `page CustomModalExample {\n state dialogOpen: boolean = false\n view { <main class="example-form-page" style="max-width:42rem;margin:3rem auto;padding:1.5rem;display:grid;gap:1.5rem"><h1>Your own modal controls</h1><Button label="Invite a teammate" @click="dialogOpen = true" /><Modal open="{dialogOpen}" @close="dialogOpen = false" title="" description="" showFooter="true" cancelLabel="" confirmLabel=""><div data-slot="header"><h2>Invite a teammate</h2></div><Input label="Email address" type="email" name="invite-email" placeholder="name@company.com" /><div data-slot="footer"><Button label="Cancel" variant="secondary" @click="dialogOpen = false" /><Button label="Done" @click="dialogOpen = false" /></div></Modal></main> }\n}`,
"project-board": `page ProjectBoardExample {\n state tasks = [{label:"Review designs"},{label:"Build the dashboard"},{label:"Test accessibility"}]\n view { <main class="example-form-page" style="max-width:42rem;margin:3rem auto;padding:1.5rem;display:grid;gap:1.5rem"><h1>Project workspace</h1><DragAndDrop title="Priorities" description="Drag a row or use its arrow buttons." items="{tasks}" /><WysiwygEditor title="Team update" value="Share what you are working on this week." /></main> }\n}`,
};
for (const e of examples) { const old = join(pagesDir,'templates',e.slug+'.wrn'); if(existsSync(old)) unlinkSync(old); }
const css = readFileSync(join(exampleRoot,'app/styles/workspace-preview.css'),'utf8');
for(const e of examples) {
const source = extra[e.slug] || readFileSync(join(exampleRoot,'examples',e.slug+'.wrn'),'utf8');
const at = source.lastIndexOf('\n }');
let rendered;
if(at>=0) rendered = source.slice(0,at)+codePanel(source)+`<details class="example-code-panel"><summary>Page styles · copy with the example</summary><button type="button" data-copy-target="#example-styles">Copy CSS</button><pre><code id="example-styles">${escapeCode(css)}</code></pre></details>`+source.slice(at);
else rendered = source.replace(/}\s*}\s*$/,codePanel(source)+' } }');
rendered = rendered.replace(/\bview\s*\{/, 'view {\n<nav class="example-toolbar"><a href="/preview">← All examples</a><a href="#example-source">View code</a></nav>');
write(`preview/${e.slug}.wrn`, rendered);
for (const e of examples) {
const old = join(pagesDir, "templates", e.slug + ".wrn");
if (existsSync(old)) unlinkSync(old);
}
write('preview/index.wrn',page('PreviewIndex','Examples',`<header class="docs-intro"><span class="showcase-eyebrow">Ready to use</span><h1>Page examples</h1><p>Explore complete layouts, interact with the controls, and copy the source.</p></header>${cards(examples.map(e=>({...e,href:'/preview/'+e.slug})))}`));
const css = readFileSync(join(exampleRoot, "app/styles/workspace-preview.css"), "utf8");
for (const e of examples) {
const source =
extra[e.slug] || readFileSync(join(exampleRoot, "examples", e.slug + ".wrn"), "utf8");
const at = source.lastIndexOf("\n }");
let rendered;
if (at >= 0)
rendered =
source.slice(0, at) +
codePanel(source) +
`<details class="example-code-panel"><summary>Page styles · copy with the example</summary><button type="button" data-copy-target="#example-styles">Copy CSS</button><pre><code id="example-styles">${escapeCode(css)}</code></pre></details>` +
source.slice(at);
else rendered = source.replace(/}\s*}\s*$/, codePanel(source) + " } }");
rendered = rendered.replace(
/\bview\s*\{/,
'view {\n<nav class="example-toolbar"><a href="/preview">← All examples</a><a href="#example-source">View code</a></nav>',
);
write(`preview/${e.slug}.wrn`, rendered);
}
write(
"preview/index.wrn",
page(
"PreviewIndex",
"Examples",
`<header class="docs-intro"><span class="showcase-eyebrow">Ready to use</span><h1>Page examples</h1><p>Explore complete layouts, interact with the controls, and copy the source.</p></header>${cards(examples.map((e) => ({ ...e, href: "/preview/" + e.slug })))}`,
),
);
const templateGroups = [
{slug:'dashboards',title:'Dashboards & admin',icon:'layout-dashboard',description:'Analytics and account operations with meaningful metrics.',heading:'Workspace performance',body:`<MetricCard title="Active accounts" value="1,284" /><Chart title="Weekly activity" type="bar" data='[{"label":"Mon","value":24},{"label":"Tue","value":38},{"label":"Wed","value":52}]' showLegend="false" /><Card title="Team access" description="Manage roles and permissions for your workspace." actionLabel="Manage team" actionHref="/preview/profile" />`},
{slug:'commerce',title:'Commerce',icon:'shopping-bag',description:'A product collection with a working shopping bag.',heading:'Everyday essentials',body:`<p>Thoughtfully made tools for your desk.</p><div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,240px),1fr));gap:1.25rem"><Card title="Studio notebook" description="Dot grid · recycled paper" footer="$18"><Button label="Add notebook" @click="bag = bag + 1" /></Card><Card title="Desk organizer" description="Solid oak · natural finish" footer="$42"><Button label="Add organizer" @click="bag = bag + 1" /></Card><Card title="Travel tumbler" description="Insulated · 350 ml" footer="$24"><Button label="Add tumbler" @click="bag = bag + 1" /></Card></div><p role="status">Shopping bag: {bag} items</p><Button label="Clear bag" variant="secondary" @click="bag = 0" />`},
{slug:'marketing',title:'Marketing websites',icon:'sparkles',description:'A SaaS landing page with benefits, pricing, and a clear next step.',heading:'Make room for your best work.',body:`<p>One calm workspace for projects, notes, and the people behind them.</p><Button label="Explore the workspace" href="/preview/workspace" /><div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,240px),1fr));gap:1.25rem"><Card title="Plan together" description="Turn your next idea into clear, shared priorities." /><Card title="Stay focused" description="Keep tasks, notes, and decisions in one place." /><Card title="Grow naturally" description="Start with your team. Add more space when you need it." /></div><Card title="Team plan" description="Everything your team needs to get started." footer="$19 / member / month" actionLabel="Get started" actionHref="/preview/sign-in" />`},
{slug:'productivity',title:'Productivity',icon:'list-checks',description:'A planning workspace for priorities, deadlines, and notes.',heading:'Your week, in focus',body:`<DragAndDrop title="Priorities" items='[{"label":"Review the launch plan"},{"label":"Prepare customer interviews"},{"label":"Share the weekly update"}]' /><DatePicker label="Next milestone" name="milestone" /><WysiwygEditor title="Meeting notes" value="Capture decisions and next steps here." />`},
{
slug: "dashboards",
title: "Dashboards & admin",
icon: "layout-dashboard",
description: "Analytics and account operations with meaningful metrics.",
heading: "Workspace performance",
body: `<MetricCard title="Active accounts" value="1,284" /><Chart title="Weekly activity" type="bar" data='[{"label":"Mon","value":24},{"label":"Tue","value":38},{"label":"Wed","value":52}]' showLegend="false" /><Card title="Team access" description="Manage roles and permissions for your workspace." actionLabel="Manage team" actionHref="/preview/profile" />`,
},
{
slug: "commerce",
title: "Commerce",
icon: "shopping-bag",
description: "A product collection with a working shopping bag.",
heading: "Everyday essentials",
body: `<p>Thoughtfully made tools for your desk.</p><div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,240px),1fr));gap:1.25rem"><Card title="Studio notebook" description="Dot grid · recycled paper" footer="$18"><Button label="Add notebook" @click="bag = bag + 1" /></Card><Card title="Desk organizer" description="Solid oak · natural finish" footer="$42"><Button label="Add organizer" @click="bag = bag + 1" /></Card><Card title="Travel tumbler" description="Insulated · 350 ml" footer="$24"><Button label="Add tumbler" @click="bag = bag + 1" /></Card></div><p role="status">Shopping bag: {bag} items</p><Button label="Clear bag" variant="secondary" @click="bag = 0" />`,
},
{
slug: "marketing",
title: "Marketing websites",
icon: "sparkles",
description: "A SaaS landing page with benefits, pricing, and a clear next step.",
heading: "Make room for your best work.",
body: `<p>One calm workspace for projects, notes, and the people behind them.</p><Button label="Explore the workspace" href="/preview/workspace" /><div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,240px),1fr));gap:1.25rem"><Card title="Plan together" description="Turn your next idea into clear, shared priorities." /><Card title="Stay focused" description="Keep tasks, notes, and decisions in one place." /><Card title="Grow naturally" description="Start with your team. Add more space when you need it." /></div><Card title="Team plan" description="Everything your team needs to get started." footer="$19 / member / month" actionLabel="Get started" actionHref="/preview/sign-in" />`,
},
{
slug: "productivity",
title: "Productivity",
icon: "list-checks",
description: "A planning workspace for priorities, deadlines, and notes.",
heading: "Your week, in focus",
body: `<DragAndDrop title="Priorities" items='[{"label":"Review the launch plan"},{"label":"Prepare customer interviews"},{"label":"Share the weekly update"}]' /><DatePicker label="Next milestone" name="milestone" /><WysiwygEditor title="Meeting notes" value="Capture decisions and next steps here." />`,
},
];
write('templates.wrn',page('Templates','Templates',`<header class="docs-intro"><span class="showcase-eyebrow">Start a product</span><h1>Templates for your next idea</h1><p>Four starting points with complete layouts and copyable source. Examples demonstrate controls; templates combine them into a product page.</p></header>${cards(templateGroups.map(e=>({...e,href:'/templates/'+e.slug})))}`));
write(
"templates.wrn",
page(
"Templates",
"Templates",
`<header class="docs-intro"><span class="showcase-eyebrow">Start a product</span><h1>Templates for your next idea</h1><p>Four starting points with complete layouts and copyable source. Examples demonstrate controls; templates combine them into a product page.</p></header>${cards(templateGroups.map((e) => ({ ...e, href: "/templates/" + e.slug })))}`,
),
);
for (const t of templateGroups) {
const source = `page ${t.slug.replace(/-/g,'')}Template {
const source = `page ${t.slug.replace(/-/g, "")}Template {
state bag: number = 0
view { <main style="max-width:72rem;margin:3rem auto;padding:2rem;display:grid;gap:1.5rem"><h1>${t.heading}</h1>${t.body}</main> }
}`;
write(`templates/${t.slug}.wrn`, source.replace('view {', 'view {<nav class="example-toolbar"><a href="/templates">← All templates</a><a href="#example-source">View code</a></nav>').replace(/}\s*}\s*$/,codePanel(source)+' } }'));
write(
`templates/${t.slug}.wrn`,
source
.replace(
"view {",
'view {<nav class="example-toolbar"><a href="/templates">← All templates</a><a href="#example-source">View code</a></nav>',
)
.replace(/}\s*}\s*$/, codePanel(source) + " } }"),
);
}
const blockCards=[];
for(const c of blockComponents) {
const slug=slugOf(c.name); resetHoistedObjects();
const config=demoConfiguration(c,0);
const mount=componentMount(c,0,config);
const states=hoistedObjectStates();
const source=`page ${c.name}Example {${states}\n view {\n ${mount}\n }\n}`;
write(`blocks/${slug}.wrn`, `page ${c.name}Block {\n layout = "gallery"\n ${states}\n seo { title = "${displayName(c.name)} block" }\n view { <header class="docs-intro"><a href="/block-library">← All blocks</a><h1>${displayName(c.name)}</h1><p>A ready-to-use section. Copy the complete example below.</p></header><section class="block-live-preview">${mount}</section>${codePanel(source)} }\n}`);
blockCards.push({title:displayName(c.name),description:c.purpose || 'A complete reusable page section.',icon:'layout-template',href:'/blocks/'+slug});
const blockCards = [];
for (const c of blockComponents) {
const slug = slugOf(c.name);
resetHoistedObjects();
const config = demoConfiguration(c, 0);
const mount = componentMount(c, 0, config);
const states = hoistedObjectStates();
const source = `page ${c.name}Example {${states}\n view {\n ${mount}\n }\n}`;
write(
`blocks/${slug}.wrn`,
`page ${c.name}Block {\n layout = "gallery"\n ${states}\n seo { title = "${displayName(c.name)} block" }\n view { <header class="docs-intro"><a href="/block-library">← All blocks</a><h1>${displayName(c.name)}</h1><p>A ready-to-use section. Copy the complete example below.</p></header><section class="block-live-preview">${mount}</section>${codePanel(source)} }\n}`,
);
blockCards.push({
title: displayName(c.name),
description: c.purpose || "A complete reusable page section.",
icon: "layout-template",
href: "/blocks/" + slug,
});
}
write('block-library.wrn',page('BlockLibrary','UI blocks',`<header class="docs-intro"><span class="showcase-eyebrow">Composed sections</span><h1>Ready-to-use blocks</h1><p>Real sections with live previews and complete source code.</p></header>${cards(blockCards)}`));
write('blocks/index.wrn',page('BlocksIndex','UI blocks',cards(blockCards)));
write(
"block-library.wrn",
page(
"BlockLibrary",
"UI blocks",
`<header class="docs-intro"><span class="showcase-eyebrow">Composed sections</span><h1>Ready-to-use blocks</h1><p>Real sections with live previews and complete source code.</p></header>${cards(blockCards)}`,
),
);
write("blocks/index.wrn", page("BlocksIndex", "UI blocks", cards(blockCards)));
// Update internal guides to point to the moved block documentation.
for(const directory of [pagesDir,join(pagesDir,'components')]) for(const file of readdirSync(directory).filter(f=>f.endsWith('.wrn'))) {
const path=join(directory,file);let text=readFileSync(path,'utf8');
for(const c of blockComponents) text=text.replaceAll('/components/'+slugOf(c.name),'/blocks/'+slugOf(c.name));
if (text !== readFileSync(path,"utf8")) writeFileSync(path,text);
}
for (const directory of [pagesDir, join(pagesDir, "components")])
for (const file of readdirSync(directory).filter((f) => f.endsWith(".wrn"))) {
const path = join(directory, file);
let text = readFileSync(path, "utf8");
for (const c of blockComponents)
text = text.replaceAll("/components/" + slugOf(c.name), "/blocks/" + slugOf(c.name));
if (text !== readFileSync(path, "utf8")) writeFileSync(path, text);
}
}