diff --git a/README.md b/README.md index 28356c5..8417b19 100644 --- a/README.md +++ b/README.md @@ -362,7 +362,7 @@ src/context.ts RequestContext handed to handlers + buildContext() src/config.ts Env loader — Ory endpoints, cookie/CSRF secrets, JWKS, port; validated at boot src/icons.ts Used-icon registry + sprite builder from lucide-static (regenerates partials/icons.ejs) src/plugin.ts definePlugin() + the host's plugin discovery/router (planned) -views/ Core EJS templates (index, 403/404/500, partials/ incl. app shell, nav tree, filter bar, icon sprite) +views/ Core EJS templates (index, 403/404/500, partials/ incl. app shell, nav tree, filter bar, data table, icon sprite) public/ Static assets under /public/ (css/styles.css + auth.css, favicon, robots.txt) config/menu.ts Central menu override + branding (planned) plugins/ Drop-in plugin folders, auto-discovered (planned) diff --git a/src/data-table.test.ts b/src/data-table.test.ts new file mode 100644 index 0000000..1bb7ef5 --- /dev/null +++ b/src/data-table.test.ts @@ -0,0 +1,77 @@ +import assert from "node:assert/strict"; +import { dirname, join } from "node:path"; +import { test } from "node:test"; +import { fileURLToPath } from "node:url"; +import * as ejs from "ejs"; + +const dataTable = join(dirname(fileURLToPath(import.meta.url)), "..", "views", "partials", "data-table.ejs"); +const render = (data: Record = {}): Promise => ejs.renderFile(dataTable, data); +const flat = (s: string): string => s.replace(/>\s+<").replace(/\s+/g, " ").trim(); + +const config = { + caption: "People in the directory", + selectable: true, + actions: true, + columns: [ + { label: "Name", sortable: true, sort: "asc", href: "?sort=name&dir=desc" }, // active ascending + { label: "Email", sortable: true, href: "?sort=email&dir=asc" }, // sortable, inactive + { label: "Team" }, // not sortable + { label: "Status" }, + { label: "Detail" }, + ], + rows: [ + { + name: "Mara Delgado", + cells: [ + { user: { name: "Mara Delgado", initials: "MD" } }, + { text: "mara@x.io", className: "cell-muted cell-mono" }, + { text: "Engineering", className: "cell-muted" }, + { badge: { tone: "pos", label: "Active" } }, + { html: 'open' }, + ], + actions: [ + { label: "Edit", icon: "i-edit", href: "/people/1/edit" }, + { label: "Delete", icon: "i-trash", danger: true, separatorBefore: true }, + ], + }, + ], +}; + +test("data-table renders sortable headers, row-select, typed cells, badges and kebab actions", async () => { + const html = flat(await render(config)); + + assert.match(html, /
People in the directory<\/caption>/); + + // Row-select: header select-all + per-row checkbox with a descriptive label. + assert.match(html, /
<\/th>/); + assert.match(html, /<\/td>/); + + // Sortable header — active ascending: aria-sort + link + up icon (& escaped in href). + assert.match(html, /Name <\/svg><\/a><\/th>/); + // Sortable header — inactive: no aria-sort, neutral sort icon. + assert.match(html, /Email <\/svg><\/a><\/th>/); + // Non-sortable header — plain text, no button/link. + assert.match(html, /Team<\/th>/); + // Actions header. + assert.match(html, /Actions<\/span><\/th>/); + + // Typed cells: user (avatar + strong), classed text, badge tone, raw html. + assert.match(html, /mara@x.io<\/td>/); + assert.match(html, /Engineering<\/td>/); + assert.match(html, /<\/span>Active<\/span><\/td>/); + assert.match(html, /open<\/a><\/td>/); + + // Kebab row actions: link item, danger button, separator. + assert.match(html, /