@
Quality / quality (ubuntu-latest) (push) Failing after 12m30s
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>
@
This commit is contained in:
2026-08-07 14:57:17 +05:30
parent 296728d51d
commit cc98bccd6c
151 changed files with 14350 additions and 9189 deletions
@@ -70,7 +70,19 @@ const escapeText = (value) =>
String(value).replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
// WRN uses braces for reactive expressions. Encode braces inside documentation
// code so hydration keeps them as literal, copyable source text.
const escapeCode = (value) => escapeText(value).replaceAll("{", "&#123;").replaceAll("}", "&#125;");
/*
* Code samples: each brace goes in its own element.
*
* The entity alone is not enough. It survives the compiler, but the BROWSER
* decodes it back to a brace, and the client runtime then walks text nodes
* looking for {mustache} and evaluates whatever it finds -- so a JSON sample
* was silently eaten, leaving "columns=[, , , ]" on the page. A text node can
* only be mistaken for a template when it holds a complete pair, so splitting
* the braces into their own nodes makes samples immune. A reader sees no
* difference.
*/
const escapeCode = (value) =>
escapeText(value).replaceAll("{", "<span>&#123;</span>").replaceAll("}", "<span>&#125;</span>");
const escapeAttribute = (value) =>
escapeText(value).replaceAll('"', "&quot;").replaceAll("'", "&#39;");
@@ -1377,10 +1389,13 @@ function inputNumberConfiguration(variation) {
function demoUses(component) {
const profile = profileFor(component.name);
if (profile?.demos?.length) {
return profile.demos.map(({ eyebrow, title, description }) => ({
// `before` is carried through: it is markup rendered beside the mount for
// host components that have no slot to put controls in.
return profile.demos.map(({ eyebrow, title, description, before }) => ({
eyebrow,
title,
description,
before,
}));
}
@@ -1493,6 +1508,11 @@ function detailPage(component, categoryComponents) {
const preview = validationSchema
? `<form data-schema="${validationSchema}" class="demo-validation-form">${mount}<button type="submit" class="wire-btn wire-btn--variant-default">Validate selection</button></form>`
: mount;
// `before` renders markup NEXT TO the mount rather than inside its slot.
// Host-style components (Toaster) have no slot at all: what a reader
// needs to see is the controls that drive them, and slot content would
// simply be dropped.
const beforeMarkup = useCase.before ?? "";
return `
<article class="demo-case">
<header class="demo-case-header">
@@ -1503,7 +1523,7 @@ function detailPage(component, categoryComponents) {
<div id="${slugOf(component.name)}-demo-${variation + 1}" class="demo-canvas demo-canvas--${variation + 1}">
<div class="demo-browser-chrome"><span></span><span></span><span></span><small>Live preview</small></div>
${isInteractiveOverlay(component) ? `<p class="demo-interaction-hint"><span class="icon-[lucide--mouse-pointer-click] size-4" aria-hidden="true"></span>${escapeText(overlayInteractionLabel(component))}</p>` : ""}
<div class="demo-render">${preview}</div>
<div class="demo-render">${beforeMarkup}${preview}</div>
</div>
<section class="demo-code" aria-label="${escapeAttribute(useCase.title)} usage">
<header><span><span class="icon-[lucide--code-2] size-4" aria-hidden="true"></span>Component usage</span><small>.wrn</small></header>