Files
WRNexusJS/scripts/reset-ui-components.mjs
T
2026-07-24 12:46:44 +05:30

449 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import process from "node:process";
const root = join(import.meta.dirname, "..");
const uiRoot = join(root, "packages", "ui");
const componentsDir = join(uiRoot, "components");
const preservedButton = readFileSync(join(componentsDir, "Button.wrn"), "utf8");
const preservedAdvancedSelect = readFileSync(join(componentsDir, "AdvancedSelect.wrn"), "utf8");
const groups = {
layout: [
"Container",
"Columns",
"Grid",
"LayoutSplitter",
"Typography",
"Image",
"Link",
"Divider",
"Kbd",
"CustomScrollbar",
],
base: [
"Accordion",
"Alert",
"Avatar",
"AvatarGroup",
"Badge",
"Blockquote",
"Button",
"ButtonGroup",
"Card",
"ChatBubble",
"Carousel",
"Collapse",
"DatePicker",
"DeviceFrame",
"List",
"ListGroup",
"LegendIndicator",
"Progress",
"FileUploadProgress",
"Rating",
"Skeleton",
"Spinner",
"StyledIcon",
"Toast",
"Timeline",
"TreeView",
"Marquee",
],
navigation: [
"Navbar",
"MegaMenu",
"Nav",
"Tabs",
"Sidebar",
"Scrollspy",
"Breadcrumb",
"Pagination",
"Stepper",
],
forms: [
"Input",
"InputGroup",
"Textarea",
"FileInput",
"Checkbox",
"Radio",
"Switch",
"Select",
"RangeSlider",
"ColorPicker",
"TimePicker",
],
"advanced-forms": [
"AdvancedSelect",
"Combobox",
"SearchBox",
"InputNumber",
"StrongPassword",
"TogglePassword",
"ToggleCount",
"CopyMarkup",
"PinInput",
],
overlays: ["Dropdown", "ContextMenu", "Modal", "Drawer", "Popover", "Tooltip"],
tables: ["Table"],
integrations: [
"AdvancedRangeSlider",
"AdvancedDatePicker",
"Chart",
"Clipboard",
"Confetti",
"DataMap",
"DataTable",
"DragAndDrop",
"FileUpload",
"Map",
"ToastNotifications",
"WysiwygEditor",
],
};
const displayName = (name) => name.replace(/([a-z0-9])([A-Z])/g, "$1 $2");
const kebab = (name) => displayName(name).replace(/\s+/g, "-").toLowerCase();
function layoutComponent(name) {
if (name === "Image")
return `component Image {
props {
src = ""
alt = ""
width = ""
height = ""
loading = "lazy"
rounded = false
class = ""
}
view {
<figure class="wire-next wire-next--image {rounded ? 'wire-next--rounded' : ''} {class}">
<img src="{src}" alt="{alt}" width="{width}" height="{height}" loading="{loading}" />
<slot />
</figure>
}
}
`;
if (name === "Link")
return `component Link {
props {
label = "Link"
href = "#"
target = ""
rel = ""
external = false
class = ""
}
view {
<a {...attrs} href="{href}" target="{target}" rel="{rel}" class="wire-next wire-next--link {class}"><slot>{label}</slot>{#if external}<span aria-hidden="true">↗</span>{/if}</a>
}
}
`;
if (name === "Divider")
return `component Divider {
props {
label = ""
orientation = "horizontal"
class = ""
}
view {
<div class="wire-next wire-next--divider wire-next--divider-{orientation} {class}" role="separator" aria-orientation="{orientation}">{#if label}<span>{label}</span>{/if}</div>
}
}
`;
if (name === "Kbd")
return `component Kbd {
props {
label = "⌘ K"
class = ""
}
view { <kbd class="wire-next wire-next--kbd {class}"><slot>{label}</slot></kbd> }
}
`;
const tag = name === "Typography" ? "article" : "div";
return `component ${name} {
props {
columns = 2
gap = "md"
maxWidth = "xl"
class = ""
}
view {
<${tag} class="wire-next wire-next--${kebab(name)} wire-next--gap-{gap} wire-next--columns-{columns} wire-next--max-{maxWidth} {class}"><slot /></${tag}>
}
}
`;
}
function collectionComponent(name) {
if (name === "Accordion" || name === "Collapse")
return `component ${name} {
props {
items = []
multiple = false
class = ""
}
view {
<div class="wire-next wire-next--${kebab(name)} {class}">
{#each items as item}<details open="{item.open}"><summary>{item.label}</summary><div>{item.content}</div></details>{/each}
<slot />
</div>
}
}
`;
if (name === "Progress" || name === "FileUploadProgress")
return `component ${name} {
props {
label = "Progress"
value = 50
max = 100
showValue = true
class = ""
}
view {
<div class="wire-next wire-next--${kebab(name)} {class}">
<div class="wire-next__row"><span>{label}</span>{#if showValue}<strong>{value}%</strong>{/if}</div>
<progress value="{value}" max="{max}"></progress>
</div>
}
}
`;
if (name === "Spinner" || name === "Skeleton")
return `component ${name} {
props {
label = "Loading"
size = "md"
lines = 3
class = ""
}
view {
<div class="wire-next wire-next--${kebab(name)} wire-next--size-{size} {class}" role="status" aria-label="{label}">
{#if "${name}" === "Skeleton"}{#each Array(lines).fill(0) as line}<span></span>{/each}{:else}<i aria-hidden="true"></i>{/if}
</div>
}
}
`;
return `component ${name} {
props {
title = "${displayName(name)}"
description = ""
items = []
variant = "default"
class = ""
}
view {
<section class="wire-next wire-next--${kebab(name)} wire-next--variant-{variant} {class}">
{#if title}<strong>{title}</strong>{/if}
{#if description}<p>{description}</p>{/if}
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
<slot />
</section>
}
}
`;
}
function navigationComponent(name) {
return `component ${name} {
props {
label = "${displayName(name)}"
items = []
active = ""
orientation = "horizontal"
class = ""
}
view {
<nav class="wire-next wire-next--${kebab(name)} wire-next--{orientation} {class}" aria-label="{label}">
{#each items as item}<a href="{item.href}" aria-current="{item.value === active ? 'page' : ''}">{item.label}</a>{/each}
<slot />
</nav>
}
}
`;
}
function formComponent(name) {
const typeByName = {
Input: "text",
SearchBox: "search",
InputNumber: "number",
StrongPassword: "password",
TogglePassword: "password",
PinInput: "text",
DatePicker: "date",
AdvancedDatePicker: "date",
TimePicker: "time",
ColorPicker: "color",
RangeSlider: "range",
AdvancedRangeSlider: "range",
FileInput: "file",
};
if (name === "Textarea" || name === "WysiwygEditor")
return `component ${name} {
props {
label = "${displayName(name)}"
name = ""
value = ""
placeholder = ""
rows = 5
disabled = false
required = false
class = ""
}
view {
<label class="wire-next wire-next--field wire-next--${kebab(name)} {class}"><span>{label}</span><textarea {...attrs} name="{name}" placeholder="{placeholder}" rows="{rows}" disabled="{disabled}" required="{required}">{value}</textarea></label>
}
}
`;
if (name === "Checkbox" || name === "Radio" || name === "Switch")
return `component ${name} {
props {
label = "${displayName(name)}"
name = ""
value = "on"
checked = false
disabled = false
class = ""
}
view {
<label class="wire-next wire-next--choice wire-next--${kebab(name)} {class}"><input {...attrs} type="${name === "Radio" ? "radio" : "checkbox"}" name="{name}" value="{value}" checked="{checked}" disabled="{disabled}" /><span>{label}</span></label>
}
}
`;
if (name.includes("Select") || name === "Combobox")
return `component ${name} {
props {
label = "${displayName(name)}"
name = ""
value = ""
options = []
placeholder = "Select an option"
multiple = false
disabled = false
class = ""
}
view {
<label class="wire-next wire-next--field wire-next--${kebab(name)} {class}"><span>{label}</span><select {...attrs} name="{name}" multiple="{multiple}" disabled="{disabled}"><option value="">{placeholder}</option>{#each options as option}<option value="{option.value}" selected="{option.value === value}">{option.label}</option>{/each}</select></label>
}
}
`;
const inputType = typeByName[name] ?? "text";
return `component ${name} {
props {
label = "${displayName(name)}"
name = ""
value = ""
placeholder = ""
type = "${inputType}"
min = ""
max = ""
step = ""
disabled = false
required = false
class = ""
}
view {
<label class="wire-next wire-next--field wire-next--${kebab(name)} {class}"><span>{label}</span><input {...attrs} type="{type}" name="{name}" value="{value}" placeholder="{placeholder}" min="{min}" max="{max}" step="{step}" disabled="{disabled}" required="{required}" /></label>
}
}
`;
}
function overlayComponent(name) {
const modal = name === "Modal" || name === "Drawer";
return `component ${name} {
props {
title = "${displayName(name)}"
description = ""
open = false
placement = "bottom"
closeLabel = "Close"
class = ""
}
view {
<div data-show="{open}" class="wire-next wire-next--${kebab(name)} wire-next--placement-{placement} {class}" role="${modal ? "dialog" : "region"}" aria-modal="${modal}" aria-label="{title}">
<header><strong>{title}</strong><button type="button" aria-label="{closeLabel}">×</button></header>
{#if description}<p>{description}</p>{/if}
<slot />
</div>
}
}
`;
}
function dataComponent(name) {
if (name === "Table" || name === "DataTable")
return `component ${name} {
props {
caption = "${displayName(name)}"
columns = []
rows = []
striped = true
class = ""
}
view {
<div class="wire-next wire-next--table {class}"><table><caption>{caption}</caption><thead><tr>{#each columns as column}<th>{column.label}</th>{/each}</tr></thead><tbody>{#each rows as row}<tr>{#each columns as column}<td>{row[column.key]}</td>{/each}</tr>{/each}</tbody></table><slot /></div>
}
}
`;
return collectionComponent(name);
}
function sourceFor(name, category) {
if (name === "Button") return preservedButton;
if (name === "AdvancedSelect") return preservedAdvancedSelect;
if (category === "layout") return layoutComponent(name);
if (category === "navigation") return navigationComponent(name);
if (category === "forms" || category === "advanced-forms") return formComponent(name);
if (category === "overlays") return overlayComponent(name);
if (category === "tables" || category === "integrations") return dataComponent(name);
if (name === "DatePicker") return formComponent(name);
return collectionComponent(name);
}
function withUniversalDesignProps(source) {
let next = source;
if (!/^\s+color\s*=/m.test(next)) {
next = next.replace(/(\s+props\s*\{\r?\n)/, '$1 color = "primary"\n');
}
if (!/^\s+size\s*=/m.test(next)) {
next = next.replace(/(\s+props\s*\{\r?\n)/, '$1 size = "default"\n');
}
return next.replace(
/class="wire-next(?![^"]*wire-next--color-)/,
'class="wire-next wire-next--color-{color} wire-next--size-{size}',
);
}
const entries = Object.entries(groups).flatMap(([category, names]) =>
names.map((name) => ({
name,
category,
purpose: `Theme-aware, responsive ${displayName(name).toLowerCase()} component.`,
})),
);
for (const file of readdirSync(componentsDir)) {
if (file.endsWith(".wrn")) unlinkSync(join(componentsDir, file));
}
for (const entry of entries) {
writeFileSync(
join(componentsDir, `${entry.name}.wrn`),
withUniversalDesignProps(sourceFor(entry.name, entry.category)),
"utf8",
);
}
writeFileSync(
join(uiRoot, "component-catalog.json"),
`${JSON.stringify({ sourceDocuments: ["Screenshot-directed WRNexus UI catalog reset"], components: entries }, null, 2)}\n`,
);
writeFileSync(
join(uiRoot, "component-migrations.json"),
`${JSON.stringify({ generatedFrom: "full component catalog reset", removedCount: 0, replacements: {} }, null, 2)}\n`,
);
process.stdout.write(`Reset @wrnexus/ui to ${entries.length} canonical components.\n`);