docs(ui): publish complete component catalog
This commit is contained in:
+86
-22
@@ -1,4 +1,4 @@
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
||||
import { join, resolve } from "node:path";
|
||||
|
||||
const root = resolve(import.meta.dir, "..");
|
||||
@@ -9,7 +9,9 @@ const frameworkVersion = projectPackage.wrnexus?.version;
|
||||
if (!frameworkVersion) throw new Error("package.json is missing wrnexus.version");
|
||||
const pages = join(root, "app", "pages");
|
||||
const packagePages = join(pages, "packages");
|
||||
const componentPages = join(pages, "components");
|
||||
mkdirSync(packagePages, { recursive: true });
|
||||
mkdirSync(componentPages, { recursive: true });
|
||||
|
||||
interface UiComponentReference {
|
||||
count: number;
|
||||
@@ -21,6 +23,7 @@ interface UiComponentReference {
|
||||
props: Array<{ name: string; type: string; required: boolean; default: string | null }>;
|
||||
slots: string[];
|
||||
events: string[];
|
||||
source: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
@@ -259,10 +262,10 @@ ${state}
|
||||
<SkipLink label="Skip to content" href="#main" class="docs-skip-link" />
|
||||
<header class="topbar">
|
||||
<a class="brand" href="/"><span>W</span> WRNexusJS</a>
|
||||
<nav aria-label="Primary"><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="/language">Language</a><a href="/architecture">Architecture</a></nav>
|
||||
<nav aria-label="Primary"><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="/components">Components</a><a href="/language">Language</a><a href="/architecture">Architecture</a></nav>
|
||||
<div class="topbar-actions"><a class="preview-pill" href="/access">Private preview · v${frameworkVersion}</a><button data-wire-theme-toggle class="theme-button" aria-label="Toggle color theme" title="Toggle color theme">◐</button></div>
|
||||
</header>
|
||||
<div class="mobile-doc-nav"><details><summary>Browse documentation</summary><nav><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="/language">Language</a><a href="/architecture">Architecture</a><a href="/tutorial">Tutorial</a><a href="/guides/project-structure">Guides</a><a href="/examples">Examples</a><a href="/search">Search</a></nav></details></div>
|
||||
<div class="mobile-doc-nav"><details><summary>Browse documentation</summary><nav><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="/components">Components</a><a href="/language">Language</a><a href="/architecture">Architecture</a><a href="/tutorial">Tutorial</a><a href="/guides/project-structure">Guides</a><a href="/examples">Examples</a><a href="/search">Search</a></nav></details></div>
|
||||
${content}
|
||||
<footer><div class="footer-brand"><span class="footer-mark" aria-hidden="true">W</span><p><strong>WRNexusJS ${frameworkVersion}</strong><span>Complete API documentation generated from installed package declarations.</span></p></div><nav aria-label="Footer"><a href="/packages">All packages</a><a href="/getting-started">Get started</a><a href="/security">Security</a><a href="/support">Support</a><a href="/llms.txt">AI guide</a></nav><p class="footer-meta">Private Developer Preview · Bun-native</p></footer>
|
||||
<BackToTop />
|
||||
@@ -320,42 +323,103 @@ for (const [name, category, summary] of catalog) {
|
||||
const guide = markdown(readme);
|
||||
const componentReference =
|
||||
name === "ui" && uiReference
|
||||
? `<section id="components" class="prose ui-component-reference"><h2>All ${uiReference.count} UI components and props</h2><p>This reference is generated from the exact installed <code>.wrn</code> sources. Required props have no default; optional props show their default value.</p>${[
|
||||
...new Set(uiReference.components.map((component) => component.category)),
|
||||
]
|
||||
.sort()
|
||||
.map(
|
||||
(componentCategory) =>
|
||||
`<h3>${escape(componentCategory[0]!.toUpperCase() + componentCategory.slice(1))}</h3><div class="table-wrap"><table><thead><tr><th>Component</th><th>Props</th><th>Slots</th><th>Events</th></tr></thead><tbody>${uiReference.components
|
||||
.filter((component) => component.category === componentCategory)
|
||||
.map(
|
||||
(component) =>
|
||||
`<tr><td><strong>${escape(component.name)}</strong><br><code>data-component="${escape(component.mount)}"</code><br><small>${escape(component.purpose)}</small></td><td>${component.props.length ? component.props.map((prop) => `<code>${escape(prop.name)}: ${escape(prop.type)}${prop.required ? " (required)" : ` = ${escape(prop.default ?? "")}`}</code>`).join("<br>") : "None"}</td><td>${component.slots.length ? component.slots.map((slot) => `<code>${escape(slot)}</code>`).join("<br>") : "None"}</td><td>${component.events.length ? component.events.map((event) => `<code>${escape(event)}</code>`).join("<br>") : "None"}</td></tr>`,
|
||||
)
|
||||
.join("")}</tbody></table></div>`,
|
||||
)
|
||||
.join("")}</section>`
|
||||
? `<section class="prose component-catalog-callout"><h2>Explore the component library</h2><p>The component reference now has a dedicated catalog with searchable cards and one complete page per component.</p><p><a class="primary" href="/components">Browse all ${uiReference.count} components →</a></p></section>`
|
||||
: "";
|
||||
const toc = [
|
||||
{ id: "guide", title: "Guide", level: 2 },
|
||||
...guide.headings,
|
||||
{ id: "api", title: "Complete API", level: 2 },
|
||||
{ id: "examples", title: "Examples", level: 2 },
|
||||
...(componentReference ? [{ id: "components", title: "All UI components", level: 2 }] : []),
|
||||
]
|
||||
.map(
|
||||
(heading) =>
|
||||
`<a class="toc-level-${heading.level}" href="#${heading.id}">${escape(heading.title)}</a>`,
|
||||
)
|
||||
.join("");
|
||||
const content = `<main class="page package-page">
|
||||
<aside class="sidebar"><a href="/packages">← All packages</a><span class="category">${category}</span><h2>@wrnexus/${name}</h2><p>${summary}</p><span class="status status-beta">Private preview · ${frameworkVersion}</span><nav><a href="#access">Access</a><a href="#guide">Guide</a><a href="#api">Complete API</a></nav></aside>
|
||||
<article id="main" class="documentation"><section class="doc-intro"><span class="eyebrow">${category} · Preview</span><h1>@wrnexus/${name}</h1><p>${summary}</p><section id="access" class="access-callout"><h2>Private registry access required</h2><p>This package is not available from the public npm registry. After WorkRoot approves access and supplies private registry instructions, install the release-aligned package:</p><pre><code>bun add @wrnexus/${name}@${frameworkVersion}</code><button type="button" class="copy-button" aria-label="Copy installation command">Copy</button></pre><p><a href="/access">Request preview access</a>. Never put registry tokens in source control.</p></section></section><section id="guide" class="prose">${guide.html}</section><section id="api" class="prose api"><h2>Complete TypeScript API</h2><p>This declaration comes from the exact installed package and lists its exported functions, classes, interfaces, and types.</p><pre data-language="typescript"><code>${escape(types)}</code></pre></section><section id="examples" class="prose examples"><h2>Examples</h2><p>Examples are taken from this package's installed documentation and must be evaluated with its requirements and stability notes.</p><div class="example-grid">${examplesFrom(readme, name)}</div></section>${componentReference}</article>
|
||||
const content = `<main class="portal-main docs-layout">
|
||||
<article id="main" class="documentation prose standalone package-document"><nav class="breadcrumbs" aria-label="Breadcrumb"><a href="/">Home</a><span>/</span><a href="/packages">Packages</a><span>/</span><span aria-current="page">@wrnexus/${name}</span></nav><section class="doc-intro"><span class="eyebrow">${category} · Package reference</span><h1>@wrnexus/${name}</h1><p>${summary}</p><div class="doc-meta"><span>v${frameworkVersion}</span><span>Private registry</span><span>${category}</span></div><section id="access" class="access-callout"><h2>Install the package</h2><p>After WorkRoot approves private registry access, install the release-aligned package:</p><pre><code>bun add @wrnexus/${name}@${frameworkVersion}</code><button type="button" class="copy-button" aria-label="Copy installation command">Copy</button></pre><p><a href="/access">Request preview access</a>. Never put registry tokens in source control.</p></section></section><section id="guide">${guide.html}</section><section id="api" class="api"><h2>Complete TypeScript API</h2><p>Generated from the exact installed package declarations.</p><pre data-language="typescript"><code>${escape(types)}</code></pre></section><section id="examples" class="examples"><h2>Examples</h2><p>Copy-ready examples from the installed package documentation.</p><div class="example-grid">${examplesFrom(readme, name)}</div></section>${componentReference}</article>
|
||||
<aside class="on-this-page"><h2>On this page</h2><nav>${toc}</nav></aside>
|
||||
</main>`;
|
||||
writeFileSync(join(packagePages, `${name}.wrn`), shell(`@wrnexus/${name}`, summary, content));
|
||||
}
|
||||
|
||||
if (!uiReference) throw new Error("Install @wrnexus/ui before generating component documentation");
|
||||
|
||||
const componentSlug = (name: string) =>
|
||||
(name === "file-upload" ? "file-upload-primitive" : name)
|
||||
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
||||
.replace(/[^a-zA-Z0-9]+/g, "-")
|
||||
.replace(/^-|-$/g, "")
|
||||
.toLowerCase();
|
||||
const componentCategories = [
|
||||
...new Set(uiReference.components.map((item) => item.category)),
|
||||
].sort();
|
||||
const componentCards = uiReference.components
|
||||
.map(
|
||||
(item) =>
|
||||
`<a class="component-card" href="/components/${componentSlug(item.name)}" data-show="(category === 'All' ? true : category === '${item.category}') ? (query === '' ? true : '${escape(`${item.name} ${item.mount} ${item.category} ${item.purpose}`.toLowerCase())}'.includes(query.toLowerCase())) : false"><span class="category">${escape(item.category)}</span><h2>${escape(item.name)}</h2><p>${escape(item.purpose)}</p><div class="component-card-meta"><span>${item.props.length} props</span><span>${item.slots.length} slots</span><span>${item.events.length} events</span></div><span class="card-link">View component →</span></a>`,
|
||||
)
|
||||
.join("\n");
|
||||
const componentFilters = ["All", ...componentCategories]
|
||||
.map(
|
||||
(category) =>
|
||||
`<button type="button" @click="category = '${category}'">${escape(category[0]!.toUpperCase() + category.slice(1))}<span>${category === "All" ? uiReference.count : uiReference.components.filter((item) => item.category === category).length}</span></button>`,
|
||||
)
|
||||
.join("");
|
||||
|
||||
writeFileSync(
|
||||
join(pages, "components.wrn"),
|
||||
shell(
|
||||
"Components",
|
||||
`Browse all ${uiReference.count} themeable WRNexusJS UI components, props, slots, events, and examples.`,
|
||||
`<main id="main" class="page component-catalog-page"><nav class="breadcrumbs" aria-label="Breadcrumb"><a href="/">Home</a><span>/</span><span aria-current="page">Components</span></nav><section class="hero compact"><span class="eyebrow">@wrnexus/ui · ${uiReference.count} components</span><h1>Component reference</h1><p>Production-ready, server-rendered components documented directly from the installed <code>.wrn</code> sources.</p><input class="search" type="search" placeholder="Search components, props, or categories…" @input="query = event.target.value" /></section><section class="category-filter" aria-label="Filter components by category"><div class="category-row">${componentFilters}</div><p>Showing <strong>{category}</strong> components</p></section><section class="component-grid">${componentCards}</section></main>`,
|
||||
' state query = ""\n state category = "All"\n',
|
||||
),
|
||||
);
|
||||
|
||||
const currentComponentFiles = new Set<string>();
|
||||
for (const item of uiReference.components) {
|
||||
const slug = componentSlug(item.name);
|
||||
currentComponentFiles.add(`${slug}.wrn`);
|
||||
const sourcePath = join(root, "node_modules", "@wrnexus", "ui", item.source);
|
||||
const source = existsSync(sourcePath) ? readFileSync(sourcePath, "utf8") : "";
|
||||
const exampleAttributes = item.props
|
||||
.filter((prop) => prop.required || !["class", "className"].includes(prop.name))
|
||||
.slice(0, 5)
|
||||
.map((prop) => {
|
||||
const value = prop.required
|
||||
? prop.type === "boolean"
|
||||
? "true"
|
||||
: prop.type === "number"
|
||||
? "1"
|
||||
: prop.type.includes("[]")
|
||||
? "[]"
|
||||
: prop.name
|
||||
: (prop.default ?? "").replace(/^"|"$/g, "");
|
||||
return `${prop.name}="${value || prop.name}"`;
|
||||
})
|
||||
.join(" ");
|
||||
const example = `<${item.mount}${exampleAttributes ? ` ${exampleAttributes}` : ""} />`;
|
||||
const propsRows = item.props.length
|
||||
? item.props
|
||||
.map(
|
||||
(prop) =>
|
||||
`<tr><td><code>${escape(prop.name)}</code></td><td><code>${escape(prop.type)}</code></td><td>${prop.required ? '<span class="required">Required</span>' : "Optional"}</td><td><code>${prop.default === null ? "—" : escape(prop.default)}</code></td></tr>`,
|
||||
)
|
||||
.join("")
|
||||
: '<tr><td colspan="4">This component has no props.</td></tr>';
|
||||
const content = `<main class="portal-main docs-layout component-detail-page"><article id="main" class="documentation prose standalone"><nav class="breadcrumbs" aria-label="Breadcrumb"><a href="/">Home</a><span>/</span><a href="/components">Components</a><span>/</span><span aria-current="page">${escape(item.name)}</span></nav><section class="doc-intro"><span class="eyebrow">${escape(item.category)} component</span><h1>${escape(item.name)}</h1><p>${escape(item.purpose)}</p><div class="doc-meta"><span>@wrnexus/ui ${frameworkVersion}</span><span>${item.props.length} props</span><span>${item.slots.length} slots</span><span>${item.events.length} events</span></div></section><section id="usage"><h2>Usage</h2><p>Components are auto-discovered. Use the component directly in any <code>.wrn</code> view:</p><pre data-language="wrn"><code>${escape(example)}</code></pre><p>Legacy mount name: <code>data-component="${escape(item.mount)}"</code>.</p></section><section id="props"><h2>Props and attributes</h2><div class="table-wrap"><table><thead><tr><th>Prop</th><th>Type</th><th>Requirement</th><th>Default</th></tr></thead><tbody>${propsRows}</tbody></table></div></section><section id="slots"><h2>Slots</h2>${item.slots.length ? `<ul>${item.slots.map((slot) => `<li><code>${escape(slot)}</code></li>`).join("")}</ul>` : "<p>This component does not declare a slot.</p>"}</section><section id="events"><h2>Events</h2>${item.events.length ? `<ul>${item.events.map((event) => `<li><code>${escape(event)}</code></li>`).join("")}</ul>` : "<p>This component does not declare a custom event.</p>"}</section><section id="source"><h2>Source contract</h2><p>Installed source: <code>${escape(item.source)}</code></p><pre data-language="wrn"><code>${escape(source)}</code></pre></section></article><aside class="on-this-page"><h2>On this page</h2><nav><a href="#usage">Usage</a><a href="#props">Props and attributes</a><a href="#slots">Slots</a><a href="#events">Events</a><a href="#source">Source contract</a></nav></aside></main>`;
|
||||
writeFileSync(
|
||||
join(componentPages, `${slug}.wrn`),
|
||||
shell(`${item.name} component`, item.purpose, content),
|
||||
);
|
||||
}
|
||||
for (const file of readdirSync(componentPages)) {
|
||||
if (file.endsWith(".wrn") && !currentComponentFiles.has(file)) {
|
||||
throw new Error(`Stale generated component page: ${file}`);
|
||||
}
|
||||
}
|
||||
|
||||
writeFileSync(
|
||||
join(pages, "index.wrn"),
|
||||
shell(
|
||||
|
||||
@@ -29,6 +29,12 @@ const uiReference = existsSync(uiReferencePath)
|
||||
}>;
|
||||
})
|
||||
: undefined;
|
||||
const componentSlug = (name: string) =>
|
||||
(name === "file-upload" ? "file-upload-primitive" : name)
|
||||
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
||||
.replace(/[^a-zA-Z0-9]+/g, "-")
|
||||
.replace(/^-|-$/g, "")
|
||||
.toLowerCase();
|
||||
const uiReferenceText = uiReference
|
||||
? uiReference.components
|
||||
.map(
|
||||
@@ -49,6 +55,7 @@ const code = (s: string) =>
|
||||
const navigation = [
|
||||
["/getting-started", "Get started"],
|
||||
["/packages", "Packages"],
|
||||
["/components", "Components"],
|
||||
["/language", "Language"],
|
||||
["/architecture", "Architecture"],
|
||||
] as const;
|
||||
@@ -119,8 +126,8 @@ function marketingShell(description: string, body: string) {
|
||||
view {
|
||||
<SkipLink label="Skip to content" href="#main" class="docs-skip-link" />
|
||||
<div class="docs-shell marketing-shell">
|
||||
<header class="topbar marketing-header"><a class="brand" href="/"><span>W</span> WRNexusJS</a><nav aria-label="Primary"><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="/language">Language</a><a href="/architecture">Architecture</a></nav><div class="topbar-actions"><a class="preview-pill" href="/access">Private preview · v${version}</a><button data-wire-theme-toggle class="theme-button" aria-label="Toggle color theme">◐</button></div></header>
|
||||
<div class="marketing-mobile-nav"><details><summary>Menu</summary><nav><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="/language">Language</a><a href="/architecture">Architecture</a><a href="/tutorial">Tutorial</a><a href="/examples">Examples</a></nav></details></div>
|
||||
<header class="topbar marketing-header"><a class="brand" href="/"><span>W</span> WRNexusJS</a><nav aria-label="Primary"><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="/components">Components</a><a href="/language">Language</a><a href="/architecture">Architecture</a></nav><div class="topbar-actions"><a class="preview-pill" href="/access">Private preview · v${version}</a><button data-wire-theme-toggle class="theme-button" aria-label="Toggle color theme">◐</button></div></header>
|
||||
<div class="marketing-mobile-nav"><details><summary>Menu</summary><nav><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="/components">Components</a><a href="/language">Language</a><a href="/architecture">Architecture</a><a href="/tutorial">Tutorial</a><a href="/examples">Examples</a></nav></details></div>
|
||||
<main id="main" class="marketing-main">${marketingBody}</main>
|
||||
<footer><div class="footer-brand"><span class="footer-mark" aria-hidden="true">W</span><p><strong>WRNexusJS ${version}</strong><span>Build from the server. Ship only what matters.</span></p></div><nav aria-label="Footer"><a href="/getting-started">Documentation</a><a href="/roadmap">Roadmap</a><a href="/security">Security</a><a href="/support">Support</a><a href="/llms.txt">AI guide</a></nav><p class="footer-meta">Bun-native · Created by <a href="https://workroot.in/">WorkRoot</a></p></footer>
|
||||
<BackToTop />
|
||||
@@ -464,6 +471,7 @@ const routes = [
|
||||
"/architecture",
|
||||
"/language",
|
||||
"/packages",
|
||||
"/components",
|
||||
...Object.keys(guides).map((x) => `/guides/${x}`),
|
||||
"/examples",
|
||||
"/showcase",
|
||||
@@ -477,7 +485,13 @@ const routes = [
|
||||
"/search",
|
||||
"/404",
|
||||
].filter(Boolean);
|
||||
const urls = [...new Set([...routes, ...packageNames.map((x) => `/packages/${x}`)])];
|
||||
const urls = [
|
||||
...new Set([
|
||||
...routes,
|
||||
...packageNames.map((x) => `/packages/${x}`),
|
||||
...(uiReference?.components.map((item) => `/components/${componentSlug(item.name)}`) ?? []),
|
||||
]),
|
||||
];
|
||||
writeFileSync(
|
||||
join(pub, "robots.txt"),
|
||||
`User-agent: *\nAllow: /\nSitemap: https://wrnexusjs.dev/sitemap.xml\n`,
|
||||
|
||||
Reference in New Issue
Block a user