@
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:
@@ -0,0 +1,156 @@
|
||||
// DataTable demo. Route: /table
|
||||
//
|
||||
// Two tables: one over a local array, one backed by /api/accounts.
|
||||
//
|
||||
// The remote table does not receive a fetch function. Component props travel
|
||||
// as HTML attributes, so a function passed that way arrives as its own source
|
||||
// text rather than something callable. Instead the table emits a `request`
|
||||
// output and we answer it by dispatching the rows back with the same id.
|
||||
page TablePage {
|
||||
|
||||
state localRows = [
|
||||
{ id: 1, name: "Northwind", plan: "Scale", seats: 42, renewsOn: "2026-02-09" },
|
||||
{ id: 2, name: "Acme Industrial", plan: "Team", seats: 8, renewsOn: "2026-01-10" },
|
||||
{ id: 3, name: "Globex", plan: "Enterprise", seats: 210, renewsOn: "2025-12-24" },
|
||||
{ id: 4, name: "Initech", plan: "Starter", seats: 5, renewsOn: "2026-03-02" },
|
||||
{ id: 5, name: "Umbrella", plan: "Scale", seats: 96, renewsOn: "2026-01-31" }
|
||||
]
|
||||
|
||||
state localColumns = [
|
||||
{ key: "name", label: "Account", sortable: true },
|
||||
{ key: "plan", label: "Plan", sortable: true },
|
||||
{ key: "seats", label: "Seats", align: "end", sortable: true, type: "number" },
|
||||
{ key: "renewsOn", label: "Renews", align: "end", sortable: true, type: "date" }
|
||||
]
|
||||
|
||||
state remoteColumns = [
|
||||
{ key: "name", label: "Account", sortable: true },
|
||||
{ key: "owner", label: "Owner", sortable: true },
|
||||
{ key: "plan", label: "Plan", align: "center" },
|
||||
{ key: "seats", label: "Seats", align: "end", sortable: true, type: "number" },
|
||||
{ key: "renewsOn", label: "Renews", align: "end", sortable: true, type: "date" }
|
||||
]
|
||||
|
||||
state tableActions = []
|
||||
state lastEvent = "none yet"
|
||||
|
||||
functions {
|
||||
// The table asks for rows through its `request` output; we answer by
|
||||
// dispatching the result back with the same instanceId. It cannot simply
|
||||
// call a function we hand it: component props travel as HTML attributes,
|
||||
// so a function would arrive as its own source text.
|
||||
function loadAccounts(payload) {
|
||||
var id = payload.instanceId
|
||||
var search = new URLSearchParams({
|
||||
page: String(payload.page),
|
||||
pageSize: String(payload.pageSize),
|
||||
query: payload.query || "",
|
||||
sortKey: payload.sortKey || "",
|
||||
sortDirection: payload.sortDirection || "asc"
|
||||
})
|
||||
|
||||
fetch("/api/accounts?" + search.toString())
|
||||
.then(function (response) {
|
||||
if (!response.ok) {
|
||||
throw new Error("Request failed with status " + response.status)
|
||||
}
|
||||
return response.json()
|
||||
})
|
||||
.then(function (data) {
|
||||
window.dispatchEvent(new CustomEvent("wrnexus:datatable:rows", {
|
||||
detail: { instanceId: id, rows: data.rows, total: data.total }
|
||||
}))
|
||||
})
|
||||
.catch(function (error) {
|
||||
window.dispatchEvent(new CustomEvent("wrnexus:datatable:error", {
|
||||
detail: { instanceId: id, message: error.message }
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
function onTableChange(payload) {
|
||||
lastEvent =
|
||||
"page " + payload.page + " of " + Math.max(1, Math.ceil(payload.total / payload.pageSize)) +
|
||||
" · " + payload.total + " records" +
|
||||
(payload.sortKey ? " · sorted by " + payload.sortKey + " " + payload.sortDirection : "")
|
||||
}
|
||||
|
||||
function onTableAction(payload) {
|
||||
toast.info(payload.id + " on " + payload.rows.length + " record(s)")
|
||||
}
|
||||
}
|
||||
|
||||
style {
|
||||
.table-demo-section {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.table-demo-note {
|
||||
margin: 0;
|
||||
color: var(--wire-color-text-muted);
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.table-demo-note code {
|
||||
color: var(--wire-color-text);
|
||||
}
|
||||
}
|
||||
|
||||
view {
|
||||
<h1>Data table</h1>
|
||||
<p>
|
||||
Sorting, filtering, paging and selection over a local array, and the same
|
||||
component driven by an API through its <code>request</code> output.
|
||||
</p>
|
||||
|
||||
<section class="table-demo-section">
|
||||
<h2>Local array</h2>
|
||||
<p class="table-demo-note">
|
||||
Everything is derived in the browser. The Renews column declares
|
||||
<code>type: "date"</code>, so it sorts chronologically rather than
|
||||
alphabetically.
|
||||
</p>
|
||||
|
||||
<DataTable
|
||||
columns={localColumns}
|
||||
rows={localRows}
|
||||
actions={tableActions}
|
||||
caption="Accounts"
|
||||
pageSize={5}
|
||||
selectable={true}
|
||||
gridlines="grid"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section class="table-demo-section">
|
||||
<h2>Loaded from an API</h2>
|
||||
<p class="table-demo-note">
|
||||
With <code>remote</code> the table emits a <code>request</code> for each
|
||||
view change and waits to be handed rows back. Sorting and searching are
|
||||
done by <code>/api/accounts</code> — the table never sees the other pages.
|
||||
</p>
|
||||
|
||||
<DataTable
|
||||
columns={remoteColumns}
|
||||
remote={true}
|
||||
@request="loadAccounts(payload)"
|
||||
caption="Accounts (server side)"
|
||||
description="22 records, paged by the API."
|
||||
pageSize={6}
|
||||
pageSizes={[6, 12, 22]}
|
||||
paginationStyle="numbered"
|
||||
searchPlaceholder="Search accounts"
|
||||
stickyFirstColumn={true}
|
||||
selectable={true}
|
||||
@change="onTableChange(payload)"
|
||||
@action="onTableAction(payload)"
|
||||
/>
|
||||
|
||||
<p class="table-demo-note">Last change event: <strong>{lastEvent}</strong></p>
|
||||
</section>
|
||||
|
||||
<Toaster position="bottom-right" />
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user