release: complete UI component catalog 0.2.58
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
const root = join(import.meta.dirname, "..", "packages", "ui");
|
||||
const componentsDir = join(root, "components");
|
||||
const catalog = JSON.parse(readFileSync(join(root, "component-catalog.json"), "utf8"));
|
||||
|
||||
function add(name, source) {
|
||||
const file = join(componentsDir, `${name}.wrn`);
|
||||
const generated = existsSync(file) && readFileSync(file, "utf8").includes("props { ");
|
||||
if (!existsSync(file) || generated) writeFileSync(file, `${source.trim()}\n`, "utf8");
|
||||
}
|
||||
|
||||
function action(name) {
|
||||
return `component ${name} {
|
||||
props {
|
||||
label = "Action"
|
||||
href = ""
|
||||
type = "button"
|
||||
variant = "primary"
|
||||
disabled = false
|
||||
class = ""
|
||||
}
|
||||
view { <span class="wire-catalog-action {class}"><a data-show="href" href="{href}" class="wire-btn wire-btn--{variant}">{label}<slot /></a><button data-show="!href" type="{type}" disabled="{disabled}" class="wire-btn wire-btn--{variant}">{label}<slot /></button></span> }
|
||||
}`;
|
||||
}
|
||||
|
||||
function form(name) {
|
||||
if (name.endsWith("Form")) {
|
||||
return `component ${name} {
|
||||
props {
|
||||
title = ""
|
||||
description = ""
|
||||
action = ""
|
||||
method = "post"
|
||||
submitLabel = "Submit"
|
||||
loading = false
|
||||
disabled = false
|
||||
class = ""
|
||||
}
|
||||
view { <form action="{action}" method="{method}" class="wire-form wire-catalog-form {class}"><header data-show="title || description"><h2 data-show="title">{title}</h2><p data-show="description">{description}</p></header><slot /><footer class="wire-dialog__actions"><button type="submit" disabled="{disabled || loading}" class="wire-btn wire-btn--primary">{loading ? 'Submitting…' : submitLabel}</button></footer></form> }
|
||||
}`;
|
||||
}
|
||||
return `component ${name} {
|
||||
props {
|
||||
label = "${name.replace(/(Input|Field|Selector|Picker|Toggle|Group|Editor)$/, "")}"
|
||||
name = ""
|
||||
value = ""
|
||||
placeholder = ""
|
||||
type = "text"
|
||||
required = false
|
||||
disabled = false
|
||||
readonly = false
|
||||
help = ""
|
||||
error = ""
|
||||
class = ""
|
||||
}
|
||||
view { <label class="wire-field wire-catalog-field {class}"><span class="wire-label">{label}<span data-show="required" class="wire-required"> *</span></span><input name="{name}" value="{value}" placeholder="{placeholder}" type="{type}" required="{required}" disabled="{disabled}" readonly="{readonly}" aria-invalid="{error ? 'true' : 'false'}" class="wire-input" /><span data-show="help && !error" class="wire-field-message wire-field-message--help">{help}</span><span data-show="error" role="alert" class="wire-field-message wire-field-message--error">{error}</span><slot /></label> }
|
||||
}`;
|
||||
}
|
||||
|
||||
function overlay(name) {
|
||||
return `component ${name} {
|
||||
props {
|
||||
title = ""
|
||||
description = ""
|
||||
open = false
|
||||
closeLabel = "Close"
|
||||
class = ""
|
||||
}
|
||||
view { <div data-show="open" class="wire-overlay wire-catalog-overlay {class}"><section role="dialog" aria-modal="true" aria-label="{title}" class="wire-dialog"><header><h2 data-show="title" class="wire-dialog__title">{title}</h2><p data-show="description" class="wire-dialog__description">{description}</p></header><slot /><button type="button" class="wire-btn wire-btn--ghost">{closeLabel}</button></section></div> }
|
||||
}`;
|
||||
}
|
||||
|
||||
function collection(name) {
|
||||
return `component ${name} {
|
||||
props {
|
||||
title = ""
|
||||
description = ""
|
||||
empty = false
|
||||
emptyText = "No items available."
|
||||
class = ""
|
||||
}
|
||||
view { <section class="wire-catalog-collection {class}"><header data-show="title || description"><h2 data-show="title">{title}</h2><p data-show="description">{description}</p></header><div data-show="!empty" role="list" class="wire-catalog-collection__items"><slot /></div><p data-show="empty" role="status" class="wire-muted">{emptyText}</p></section> }
|
||||
}`;
|
||||
}
|
||||
|
||||
function visualization(name) {
|
||||
return `component ${name} {
|
||||
props {
|
||||
title = ""
|
||||
value = ""
|
||||
description = ""
|
||||
summary = ""
|
||||
loading = false
|
||||
class = ""
|
||||
}
|
||||
view { <figure aria-label="{summary || title}" class="wire-card wire-catalog-visualization {class}"><figcaption><strong data-show="title">{title}</strong><span data-show="value" class="wire-metric__value">{value}</span><span data-show="description" class="wire-muted">{description}</span></figcaption><div data-show="loading" class="wire-skeleton wire-catalog-visualization__skeleton"></div><div data-show="!loading" class="wire-catalog-visualization__content"><slot /></div><p data-show="summary" class="wire-visually-hidden">{summary}</p></figure> }
|
||||
}`;
|
||||
}
|
||||
|
||||
function feedback(name) {
|
||||
return `component ${name} {
|
||||
props {
|
||||
label = "${name.replace(/(Badge|Indicator|Counter|State|Alert|Banner|Notice|Skeleton)$/, "")}"
|
||||
title = ""
|
||||
description = ""
|
||||
value = ""
|
||||
variant = "default"
|
||||
class = ""
|
||||
}
|
||||
view { <aside role="status" class="wire-alert wire-alert--{variant} wire-catalog-feedback {class}"><strong data-show="title || label">{title || label}</strong><span data-show="value" class="wire-badge wire-badge--{variant}">{value}</span><p data-show="description">{description}</p><slot /></aside> }
|
||||
}`;
|
||||
}
|
||||
|
||||
function layout(name) {
|
||||
return `component ${name} {
|
||||
props {
|
||||
eyebrow = ""
|
||||
title = ""
|
||||
description = ""
|
||||
align = "start"
|
||||
class = ""
|
||||
}
|
||||
view { <section class="wire-catalog-section wire-text--{align} {class}"><header data-show="eyebrow || title || description"><span data-show="eyebrow" class="wire-eyebrow">{eyebrow}</span><h2 data-show="title">{title}</h2><p data-show="description" class="wire-muted">{description}</p></header><slot /></section> }
|
||||
}`;
|
||||
}
|
||||
|
||||
function content(name) {
|
||||
return `component ${name} {
|
||||
props {
|
||||
eyebrow = ""
|
||||
title = ""
|
||||
description = ""
|
||||
href = ""
|
||||
actionLabel = "Learn more"
|
||||
image = ""
|
||||
alt = ""
|
||||
class = ""
|
||||
}
|
||||
view { <article class="wire-card wire-catalog-card {class}"><img data-show="image" src="{image}" alt="{alt}" class="wire-catalog-card__image" /><span data-show="eyebrow" class="wire-eyebrow">{eyebrow}</span><h3 data-show="title">{title}</h3><p data-show="description">{description}</p><slot /><a data-show="href" href="{href}" class="wire-btn wire-btn--ghost">{actionLabel}</a></article> }
|
||||
}`;
|
||||
}
|
||||
|
||||
const factories = {
|
||||
actions: action,
|
||||
forms: form,
|
||||
overlays: overlay,
|
||||
data: collection,
|
||||
visualization,
|
||||
feedback,
|
||||
layout,
|
||||
content,
|
||||
};
|
||||
for (const entry of catalog.components) {
|
||||
add(entry.name, factories[entry.category](entry.name));
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { readdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { basename, join } from "node:path";
|
||||
|
||||
const uiRoot = join(import.meta.dirname, "..", "packages", "ui");
|
||||
const componentsDir = join(uiRoot, "components");
|
||||
const catalog = JSON.parse(readFileSync(join(uiRoot, "component-catalog.json"), "utf8"));
|
||||
const metadata = new Map(catalog.components.map((entry) => [entry.name, entry]));
|
||||
|
||||
function block(source, keyword) {
|
||||
const start = source.indexOf(`${keyword} {`);
|
||||
if (start < 0) return "";
|
||||
const open = source.indexOf("{", start);
|
||||
let depth = 0;
|
||||
for (let index = open; index < source.length; index++) {
|
||||
if (source[index] === "{") depth++;
|
||||
if (source[index] === "}" && --depth === 0) return source.slice(open + 1, index);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function propsOf(source) {
|
||||
const props = [];
|
||||
for (const line of block(source, "props").split(/\r?\n/)) {
|
||||
const match = line.trim().match(/^([A-Za-z][A-Za-z0-9_]*)\s*(?:=\s*(.+))?$/);
|
||||
if (!match) continue;
|
||||
const [, name, rawDefault] = match;
|
||||
const value = rawDefault?.trim();
|
||||
const type =
|
||||
value === "true" || value === "false"
|
||||
? "boolean"
|
||||
: /^-?\d+(?:\.\d+)?$/.test(value ?? "")
|
||||
? "number"
|
||||
: "string";
|
||||
props.push({ name, type, required: value === undefined, default: value ?? null });
|
||||
}
|
||||
return props;
|
||||
}
|
||||
|
||||
function words(name) {
|
||||
return name.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase();
|
||||
}
|
||||
|
||||
const components = readdirSync(componentsDir)
|
||||
.filter((file) => file.endsWith(".wrn"))
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.map((file) => {
|
||||
const name = basename(file, ".wrn");
|
||||
const source = readFileSync(join(componentsDir, file), "utf8");
|
||||
const entry = metadata.get(name);
|
||||
const slots = [...source.matchAll(/<slot(?:\s+name="([^"]+)")?/g)].map(
|
||||
(match) => match[1] ?? "default",
|
||||
);
|
||||
const events = [
|
||||
...new Set([...source.matchAll(/@([A-Za-z][A-Za-z0-9_-]*)=/g)].map((match) => match[1])),
|
||||
];
|
||||
return {
|
||||
name,
|
||||
mount: name,
|
||||
category: entry?.category ?? "core",
|
||||
purpose: entry?.purpose ?? `Reusable ${words(name)} component.`,
|
||||
props: propsOf(source),
|
||||
slots: [...new Set(slots)],
|
||||
events,
|
||||
source: `components/${file}`,
|
||||
};
|
||||
});
|
||||
|
||||
writeFileSync(
|
||||
join(uiRoot, "component-reference.json"),
|
||||
JSON.stringify(
|
||||
{ generatedFrom: "packages/ui/components/*.wrn", count: components.length, components },
|
||||
null,
|
||||
2,
|
||||
) + "\n",
|
||||
);
|
||||
|
||||
const lines = [
|
||||
"# WRNexus UI component reference",
|
||||
"",
|
||||
`This reference is generated from the ${components.length} packaged \`.wrn\` component sources. Props marked required have no default; all others show their runtime default.`,
|
||||
"",
|
||||
];
|
||||
for (const category of [...new Set(components.map((component) => component.category))].sort()) {
|
||||
lines.push(`## ${category[0].toUpperCase()}${category.slice(1)}`, "");
|
||||
for (const component of components.filter((item) => item.category === category)) {
|
||||
const props = component.props.length
|
||||
? component.props
|
||||
.map(
|
||||
(prop) =>
|
||||
`\`${prop.name}: ${prop.type}${prop.required ? " (required)" : ` = ${prop.default}`}\``,
|
||||
)
|
||||
.join(", ")
|
||||
: "None";
|
||||
lines.push(
|
||||
`### ${component.name}`,
|
||||
"",
|
||||
component.purpose,
|
||||
"",
|
||||
`- Mount: \`data-component="${component.mount}"\``,
|
||||
`- Props: ${props}`,
|
||||
`- Slots: ${component.slots.length ? component.slots.map((slot) => `\`${slot}\``).join(", ") : "None"}`,
|
||||
`- Events: ${component.events.length ? component.events.map((event) => `\`${event}\``).join(", ") : "None"}`,
|
||||
"",
|
||||
);
|
||||
}
|
||||
}
|
||||
writeFileSync(join(uiRoot, "COMPONENTS.md"), `${lines.join("\n")}\n`);
|
||||
@@ -40,7 +40,13 @@ const LICENSE = "MIT";
|
||||
|
||||
/** Extra (non-code) files each package must ship, relative to the package root. */
|
||||
const ASSETS: Record<string, string[]> = {
|
||||
"@wrnexus/ui": ["components", "ui.css"],
|
||||
"@wrnexus/ui": [
|
||||
"components",
|
||||
"ui.css",
|
||||
"component-catalog.json",
|
||||
"component-reference.json",
|
||||
"COMPONENTS.md",
|
||||
],
|
||||
};
|
||||
|
||||
interface Pkg {
|
||||
|
||||
Reference in New Issue
Block a user