Files
WRNexusJS/scripts/reset-ui-components.mjs
T
ClintchizandClaude Opus 5 949cf78636
Quality / quality (ubuntu-latest) (push) Failing after 13m40s
Quality / quality (windows-latest) (push) Canceled after 0s
feat(ui): add DataTable and Toaster, drop the legacy Table, fix overlay dialogs
DataTable replaces the 20-line Table scaffold entirely: columns, sorting,
filtering, pagination, selection, bulk actions, comparison layout, sticky
first column, custom HTML cells, and a remote source driven by a `request`
output rather than a function prop (props travel as HTML attributes, so a
function arrives as its own source text).

Toaster replaces the hand-rolled status div: tone icons, actions, hover
pause/resume and a progress bar.

Overlays audit -- Modal and Drawer declared aria-modal="true" but nothing
ever moved focus into the panel, so the @keydown handler on their root
never ran and closeOnEscape did nothing. Focus, focus restore, a Tab trap
and a body scroll lock now live in the reactive runtime, shared by both.

ContextMenu placed pointer menus by subtracting a guessed 340x420 from the
viewport, which pushed every menu that was not that size away from the
pointer; it now positions at the pointer and lets the anchored clamp pull
it back once it can be measured.

The reactive runtime size budget moves 150k -> 175k to cover anchored
overlays, dialog behaviour, the toaster and the DataTable client half.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 14:59:58 +05:30

448 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: ["DataTable"],
integrations: [
"AdvancedRangeSlider",
"AdvancedDatePicker",
"Chart",
"Clipboard",
"Confetti",
"DataMap",
"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 === "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`);