feat: deployable design studio and updated component showcase
This commit is contained in:
1 parent
3dddcaac1a
commit
3987ddbcfe
113 files changed
+4544
-1039
No files matched your search
@@ -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);
|
||||
}
|
||||
}
|
||||
+785
-177
File diff suppressed because it is too large.
Load diff
+330
-24
@@ -2873,7 +2873,10 @@ export const componentProfiles = {
|
||||
},
|
||||
),
|
||||
],
|
||||
options: { size: ["sm", "default", "lg"], variant: ["default", "soft", "raised", "outline", "minimal", "compact", "alternate"] },
|
||||
options: {
|
||||
size: ["sm", "default", "lg"],
|
||||
variant: ["default", "soft", "raised", "outline", "minimal", "compact", "alternate"],
|
||||
},
|
||||
},
|
||||
|
||||
Typography: {
|
||||
@@ -2958,28 +2961,280 @@ const genericEventDescriptions = {
|
||||
toggle: "Fires when the component changes between open and closed or on and off states.",
|
||||
};
|
||||
|
||||
|
||||
componentProfiles.Card.demos.push(
|
||||
standard("Image with footer", "A cover image, concise body, and separate footer.", {title:"Design your next chapter",description:"A practical guide to building thoughtful products.",imageSrc:"/showcase-images/green-hero.svg",imageAlt:"Green abstract cover",footer:"6 min read",actionLabel:"Read guide",actionHref:"#guide"}),
|
||||
compact("Without a header", "Body content with an optional footer; no reserved heading space.", {title:"",description:"Your workspace is ready. Invite your teammates to get started.",footer:"Updated just now"}, {default:""}),
|
||||
advanced("Image-only card", "A simple image surface without header or footer chrome.", {title:"",description:"",imageSrc:"/showcase-images/blue-mountain.svg",imageAlt:"Blue geometric landscape"}, {default:""}),
|
||||
standard("Image with footer", "A cover image, concise body, and separate footer.", {
|
||||
title: "Design your next chapter",
|
||||
description: "A practical guide to building thoughtful products.",
|
||||
imageSrc: "/showcase-images/green-hero.svg",
|
||||
imageAlt: "Green abstract cover",
|
||||
footer: "6 min read",
|
||||
actionLabel: "Read guide",
|
||||
actionHref: "#guide",
|
||||
}),
|
||||
compact(
|
||||
"Without a header",
|
||||
"Body content with an optional footer; no reserved heading space.",
|
||||
{
|
||||
title: "",
|
||||
description: "Your workspace is ready. Invite your teammates to get started.",
|
||||
footer: "Updated just now",
|
||||
},
|
||||
{ default: "" },
|
||||
),
|
||||
advanced(
|
||||
"Image-only card",
|
||||
"A simple image surface without header or footer chrome.",
|
||||
{
|
||||
title: "",
|
||||
description: "",
|
||||
imageSrc: "/showcase-images/blue-mountain.svg",
|
||||
imageAlt: "Blue geometric landscape",
|
||||
},
|
||||
{ default: "" },
|
||||
),
|
||||
);
|
||||
componentProfiles.Map = { demos:[
|
||||
standard("OpenStreetMap", "A real interactive geographic map. Set latitude, longitude and zoomLevel; no API key is needed.", {title:"Bengaluru",description:"Pan and zoom to explore the city",provider:"openstreetmap",latitude:12.9716,longitude:77.5946,zoomLevel:13,imageSrc:"",items:[]}),
|
||||
compact("Google Maps", "Pass your restricted Maps Embed API key and a place query to show Google Maps.", {title:"Google Maps",provider:"google",apiKey:"",query:"Bengaluru",imageSrc:"",items:[]}),
|
||||
], playground:{title:"Bengaluru",description:"Interactive geographic map",provider:"openstreetmap",latitude:12.9716,longitude:77.5946,zoomLevel:13,imageSrc:"",items:[]}, options:{provider:["openstreetmap","google"]} };
|
||||
componentProfiles.Rating = {demos:[standard("Whole stars", "Choose a rating from zero to five with step 1.",{title:"Rate your experience",value:3,step:1}),compact("Half stars", "Click a half star or use the precise slider and arrow keys.",{title:"Half-star rating",value:3.5,step:0.5}),advanced("Fine increments", "Use increments of 0.2 for a more precise rating.",{title:"Detailed score",value:4.2,step:0.2})]};
|
||||
componentProfiles.LegendIndicator = {demos:[standard("Chart legend", "Toggle a series and handle @toggle to update your chart.",{title:"Traffic sources",description:"Select a source to toggle its visibility.",items:[{label:"Direct",color:"#8b5cf6",value:"42%"},{label:"Search",color:"#14b8a6",value:"35%"},{label:"Referral",color:"#f59e0b",value:"23%"}]})]};
|
||||
componentProfiles.Chart = {demos:[standard("Bar chart", "Pass an array of label/value objects through data.",{title:"Weekly signups",type:"bar",data:[{label:"Mon",value:24},{label:"Tue",value:38},{label:"Wed",value:29},{label:"Thu",value:52},{label:"Fri",value:45}],items:[],showLegend:false}),compact("Line chart", "Use type=line to compare a trend over time.",{title:"Monthly revenue",type:"line",data:[{label:"Jan",value:20},{label:"Feb",value:35},{label:"Mar",value:28},{label:"Apr",value:55}],items:[],showLegend:false})],options:{type:["bar","line"]}};
|
||||
componentProfiles.DatePicker = { demos:[
|
||||
standard("Date picker", "An anchored calendar that does not move surrounding content.", {label:"Start date",name:"start-date",type:"date"}),
|
||||
compact("Date and time", "Choose a calendar date and a time in one field.", {label:"Schedule for",name:"schedule",type:"datetime-local",value:"2026-09-20T09:30"}),
|
||||
advanced("Disabled date", "A read-only scheduled date.", {label:"Scheduled date",readonly:true}),
|
||||
], options:{type:["date","datetime-local"]} };
|
||||
componentProfiles.TimePicker = { demos:[standard("Time picker", "Choose an hour and minute in an anchored panel.", {label:"Meeting time",name:"meeting-time",value:"09:30"})] };
|
||||
componentProfiles.Map = {
|
||||
demos: [
|
||||
standard(
|
||||
"OpenStreetMap",
|
||||
"A real interactive geographic map. Set latitude, longitude and zoomLevel; no API key is needed.",
|
||||
{
|
||||
title: "Bengaluru",
|
||||
description: "Pan and zoom to explore the city",
|
||||
provider: "openstreetmap",
|
||||
latitude: 12.9716,
|
||||
longitude: 77.5946,
|
||||
zoomLevel: 13,
|
||||
imageSrc: "",
|
||||
items: [],
|
||||
},
|
||||
),
|
||||
compact(
|
||||
"Google Maps",
|
||||
"Pass your restricted Maps Embed API key and a place query to show Google Maps.",
|
||||
{
|
||||
title: "Google Maps",
|
||||
provider: "google",
|
||||
apiKey: "",
|
||||
query: "Bengaluru",
|
||||
imageSrc: "",
|
||||
items: [],
|
||||
},
|
||||
),
|
||||
],
|
||||
playground: {
|
||||
title: "Bengaluru",
|
||||
description: "Interactive geographic map",
|
||||
provider: "openstreetmap",
|
||||
latitude: 12.9716,
|
||||
longitude: 77.5946,
|
||||
zoomLevel: 13,
|
||||
imageSrc: "",
|
||||
items: [],
|
||||
},
|
||||
options: { provider: ["openstreetmap", "google"] },
|
||||
};
|
||||
componentProfiles.Rating = {
|
||||
demos: [
|
||||
standard("Whole stars", "Choose a rating from zero to five with step 1.", {
|
||||
title: "Rate your experience",
|
||||
value: 3,
|
||||
step: 1,
|
||||
}),
|
||||
compact("Half stars", "Click a half star or use the precise slider and arrow keys.", {
|
||||
title: "Half-star rating",
|
||||
value: 3.5,
|
||||
step: 0.5,
|
||||
}),
|
||||
advanced("Fine increments", "Use increments of 0.2 for a more precise rating.", {
|
||||
title: "Detailed score",
|
||||
value: 4.2,
|
||||
step: 0.2,
|
||||
}),
|
||||
],
|
||||
};
|
||||
componentProfiles.LegendIndicator = {
|
||||
demos: [
|
||||
standard("Chart legend", "Toggle a series and handle @toggle to update your chart.", {
|
||||
title: "Traffic sources",
|
||||
description: "Select a source to toggle its visibility.",
|
||||
items: [
|
||||
{ label: "Direct", color: "#8b5cf6", value: "42%" },
|
||||
{ label: "Search", color: "#14b8a6", value: "35%" },
|
||||
{ label: "Referral", color: "#f59e0b", value: "23%" },
|
||||
],
|
||||
}),
|
||||
],
|
||||
};
|
||||
componentProfiles.Chart = {
|
||||
demos: [
|
||||
standard("Bar chart", "Chart.js bars with positive and negative values.", {
|
||||
title: "Sample weekly changes",
|
||||
description: "Sample data",
|
||||
type: "bar",
|
||||
data: [
|
||||
{ label: "Mon", value: 24 },
|
||||
{ label: "Tue", value: -12 },
|
||||
{ label: "Wed", value: 29 },
|
||||
{ label: "Thu", value: 52 },
|
||||
{ label: "Fri", value: 45 },
|
||||
],
|
||||
items: [],
|
||||
showLegend: true,
|
||||
showValues: false,
|
||||
}),
|
||||
...["line", "area", "pie", "doughnut", "radar", "polarArea"].map((type) =>
|
||||
compact(
|
||||
type + " chart",
|
||||
"Rendered by Chart.js with native tooltips and legend interactions.",
|
||||
{
|
||||
title: "Sample " + type,
|
||||
description: "Sample data",
|
||||
type,
|
||||
data: {
|
||||
labels: ["Mon", "Tue", "Wed", "Thu", "Fri"],
|
||||
datasets: [{ label: "Series A", data: [24, 38, 29, 52, 45] }],
|
||||
},
|
||||
items: [],
|
||||
showLegend: true,
|
||||
showValues: false,
|
||||
},
|
||||
),
|
||||
),
|
||||
advanced("Scatter chart", "Pass native x/y coordinates without flattening them.", {
|
||||
title: "Sample coordinates",
|
||||
description: "Sample data",
|
||||
type: "scatter",
|
||||
items: [],
|
||||
data: {
|
||||
datasets: [
|
||||
{
|
||||
label: "Observations",
|
||||
data: [
|
||||
{ x: 1, y: 4 },
|
||||
{ x: 3, y: 8 },
|
||||
{ x: 5, y: 3 },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
showValues: false,
|
||||
}),
|
||||
advanced("Bubble chart", "Native x/y/r points preserve bubble radii.", {
|
||||
title: "Sample bubbles",
|
||||
description: "Sample data",
|
||||
type: "bubble",
|
||||
items: [],
|
||||
data: {
|
||||
datasets: [
|
||||
{
|
||||
label: "Observations",
|
||||
data: [
|
||||
{ x: 1, y: 4, r: 8 },
|
||||
{ x: 3, y: 8, r: 16 },
|
||||
{ x: 5, y: 3, r: 12 },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
showValues: false,
|
||||
}),
|
||||
advanced("Mixed chart", "Set each dataset's type and pass native Chart.js options.", {
|
||||
title: "Sample mixed chart",
|
||||
description: "Sample data",
|
||||
type: "bar",
|
||||
items: [],
|
||||
data: {
|
||||
labels: ["Jan", "Feb", "Mar"],
|
||||
datasets: [
|
||||
{ type: "bar", label: "Actual", data: [20, 35, 28] },
|
||||
{ type: "line", label: "Target", data: [25, 30, 40] },
|
||||
],
|
||||
},
|
||||
showValues: false,
|
||||
}),
|
||||
advanced("Horizontal stacked bars", "Configure indexAxis and stacked scales through options.", {
|
||||
title: "Sample stacked chart",
|
||||
description: "Sample data",
|
||||
type: "bar",
|
||||
items: [],
|
||||
data: {
|
||||
labels: ["Jan", "Feb", "Mar"],
|
||||
datasets: [
|
||||
{ label: "A", data: [20, 35, 28] },
|
||||
{ label: "B", data: [25, 30, 40] },
|
||||
],
|
||||
},
|
||||
options: { indexAxis: "y", scales: { x: { stacked: true }, y: { stacked: true } } },
|
||||
showValues: false,
|
||||
}),
|
||||
],
|
||||
options: {
|
||||
type: ["bar", "line", "area", "pie", "doughnut", "radar", "polarArea", "scatter", "bubble"],
|
||||
},
|
||||
};
|
||||
componentProfiles.DatePicker = {
|
||||
demos: [
|
||||
standard("Date picker", "An anchored calendar that does not move surrounding content.", {
|
||||
label: "Start date",
|
||||
name: "start-date",
|
||||
type: "date",
|
||||
}),
|
||||
compact("Date and time", "Choose a calendar date and a time in one field.", {
|
||||
label: "Schedule for",
|
||||
name: "schedule",
|
||||
type: "datetime-local",
|
||||
value: "2026-09-20T09:30",
|
||||
}),
|
||||
advanced("Disabled date", "A read-only scheduled date.", {
|
||||
label: "Scheduled date",
|
||||
readonly: true,
|
||||
}),
|
||||
],
|
||||
options: { type: ["date", "datetime-local"] },
|
||||
};
|
||||
componentProfiles.TimePicker = {
|
||||
demos: [
|
||||
standard("Time picker", "Choose an hour and minute in an anchored panel.", {
|
||||
label: "Meeting time",
|
||||
name: "meeting-time",
|
||||
value: "09:30",
|
||||
}),
|
||||
],
|
||||
};
|
||||
componentProfiles.Modal.demos.push(
|
||||
advanced("Custom header and footer", "Supply your own header, body, and footer actions.", {title:"",description:"",cancelLabel:"",confirmLabel:"",triggerLabel:"Open custom dialog"}, {header:'<h2>Invite a teammate</h2>',default:'<p>Give your teammate access to your workspace.</p>',footer:`<button type="button" class="showcase-action" data-modal-close="true">Done</button>`}),
|
||||
compact("Centered content", "Omit the standard header and footer for a fully custom composition.", {showHeader:false,showFooter:false,title:"",description:"",icon:"",size:"sm",triggerLabel:"Open centered dialog"}, {default:`<div style="text-align:center;padding:2rem"><h2>You are all set</h2><p>Your preferences have been saved.</p><button type="button" class="showcase-action" data-modal-close="true">Continue</button></div>`}),
|
||||
advanced(
|
||||
"Custom header and footer",
|
||||
"Supply your own header, body, and footer actions.",
|
||||
{
|
||||
title: "",
|
||||
description: "",
|
||||
cancelLabel: "",
|
||||
confirmLabel: "",
|
||||
triggerLabel: "Open custom dialog",
|
||||
},
|
||||
{
|
||||
header: "<h2>Invite a teammate</h2>",
|
||||
default: "<p>Give your teammate access to your workspace.</p>",
|
||||
footer: `<button type="button" class="showcase-action" data-modal-close="true">Done</button>`,
|
||||
},
|
||||
),
|
||||
compact(
|
||||
"Centered content",
|
||||
"Omit the standard header and footer for a fully custom composition.",
|
||||
{
|
||||
showHeader: false,
|
||||
showFooter: false,
|
||||
title: "",
|
||||
description: "",
|
||||
icon: "",
|
||||
size: "sm",
|
||||
triggerLabel: "Open centered dialog",
|
||||
},
|
||||
{
|
||||
default: `<div style="text-align:center;padding:2rem"><h2>You are all set</h2><p>Your preferences have been saved.</p><button type="button" class="showcase-action" data-modal-close="true">Continue</button></div>`,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
export function profileFor(name) {
|
||||
@@ -2992,8 +3247,59 @@ export function eventDescriptionFor(_componentName, eventName) {
|
||||
);
|
||||
}
|
||||
|
||||
componentProfiles.Card.demos.push(advanced("Custom slots", "Supply arbitrary header, body, and footer content. Use showHeader and showFooter to hide either region.", {title:"",description:"",showHeader:true,showFooter:true,footer:"",actionLabel:""}, {header:"<strong>Team invitation</strong>",default:'<Input label="Email" placeholder="name@company.com" />',footer:'<Button label="Send invitation" />'}));
|
||||
componentProfiles.RangeSlider = { demos: [compact("Fractional steps", "Use step=0.2 for precise increments with buttons, dragging, or arrow keys.", {label:"Scale",min:0,max:5,value:2.4,step:0.2})] };
|
||||
componentProfiles.Card.demos.push(
|
||||
advanced(
|
||||
"Custom slots",
|
||||
"Supply arbitrary header, body, and footer content. Use showHeader and showFooter to hide either region.",
|
||||
{ title: "", description: "", showHeader: true, showFooter: true, footer: "", actionLabel: "" },
|
||||
{
|
||||
header: "<strong>Team invitation</strong>",
|
||||
default: '<Input label="Email" placeholder="name@company.com" />',
|
||||
footer: '<Button label="Send invitation" />',
|
||||
},
|
||||
),
|
||||
);
|
||||
componentProfiles.RangeSlider = {
|
||||
demos: [
|
||||
compact(
|
||||
"Fractional steps",
|
||||
"Use step=0.2 for precise increments with buttons, dragging, or arrow keys.",
|
||||
{ label: "Scale", min: 0, max: 5, value: 2.4, step: 0.2 },
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
componentProfiles.FileInput = {demos:[standard("Choose a document", "Select a PDF or image. The selected filename replaces the placeholder.", {label:"Attachment",name:"attachment",value:"",accept:".pdf,image/*",helperText:"PDF or image",placeholder:"Choose a file"}),compact("Multiple attachments", "Select several files; each filename is shown in the control.",{label:"Attachments",name:"attachments",value:"",multiple:true,placeholder:"Choose files"}),advanced("Disabled upload", "A disabled control cannot open the file chooser.",{label:"Attachment",name:"disabled-file",value:"",disabled:true,placeholder:"Uploads unavailable"})]};
|
||||
componentProfiles.FileInput = {
|
||||
demos: [
|
||||
standard(
|
||||
"Choose a document",
|
||||
"Select a PDF or image. The selected filename replaces the placeholder.",
|
||||
{
|
||||
label: "Attachment",
|
||||
name: "attachment",
|
||||
value: "",
|
||||
accept: ".pdf,image/*",
|
||||
helperText: "PDF or image",
|
||||
placeholder: "Choose a file",
|
||||
},
|
||||
),
|
||||
compact(
|
||||
"Multiple attachments",
|
||||
"Select several files; each filename is shown in the control.",
|
||||
{
|
||||
label: "Attachments",
|
||||
name: "attachments",
|
||||
value: "",
|
||||
multiple: true,
|
||||
placeholder: "Choose files",
|
||||
},
|
||||
),
|
||||
advanced("Disabled upload", "A disabled control cannot open the file chooser.", {
|
||||
label: "Attachment",
|
||||
name: "disabled-file",
|
||||
value: "",
|
||||
disabled: true,
|
||||
placeholder: "Uploads unavailable",
|
||||
}),
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import { startStudio } from "./studio.ts";
|
||||
|
||||
const studio = await startStudio();
|
||||
const site = Bun.spawn([process.execPath, "dist/server.js"], {
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
stdin: "inherit",
|
||||
env: process.env,
|
||||
});
|
||||
const stop = () => {
|
||||
studio.stop(true);
|
||||
site.kill("SIGTERM");
|
||||
};
|
||||
process.on("SIGINT", stop);
|
||||
process.on("SIGTERM", stop);
|
||||
const code = await site.exited;
|
||||
studio.stop(true);
|
||||
process.exit(code);
|
||||
@@ -0,0 +1,18 @@
|
||||
import { createStudioHandler } from "@wrnexus/design-studio/server";
|
||||
|
||||
export async function startStudio() {
|
||||
const fetch = await createStudioHandler({
|
||||
hosted: { basePath: "/design-studio" },
|
||||
});
|
||||
const server = Bun.serve({
|
||||
hostname: "127.0.0.1",
|
||||
port: Number(process.env.DESIGN_STUDIO_PORT ?? 4320),
|
||||
fetch,
|
||||
});
|
||||
console.log(
|
||||
`Export-only Design Studio listening on ${server.url}design-studio/`,
|
||||
);
|
||||
return server;
|
||||
}
|
||||
|
||||
if (import.meta.main) await startStudio();
|
||||
Reference in new issue
Block a user