From 637d5cf66dadd5fdb935ee3d067304da001a8b2e Mon Sep 17 00:00:00 2001 From: lilleman Date: Mon, 15 Jun 2026 12:04:25 +0200 Subject: [PATCH] =?UTF-8?q?Add=20data-driven=20filter-bar=20partial=20(tod?= =?UTF-8?q?o=20=C2=A71);=20GET=20form:=20search/segmented/select/chips/dat?= =?UTF-8?q?erange=20+=20applied=20pills?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- src/filter-bar.test.ts | 84 +++++++++++++++++++++++++++++++++++ todo.md | 2 +- views/partials/filter-bar.ejs | 51 +++++++++++++++++++++ 4 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 src/filter-bar.test.ts create mode 100644 views/partials/filter-bar.ejs diff --git a/README.md b/README.md index 94f23d1..28356c5 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, icon sprite) +views/ Core EJS templates (index, 403/404/500, partials/ incl. app shell, nav tree, filter bar, 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/filter-bar.test.ts b/src/filter-bar.test.ts new file mode 100644 index 0000000..71aa1b0 --- /dev/null +++ b/src/filter-bar.test.ts @@ -0,0 +1,84 @@ +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 filterBar = join(dirname(fileURLToPath(import.meta.url)), "..", "views", "partials", "filter-bar.ejs"); +const render = (data: Record = {}): Promise => ejs.renderFile(filterBar, data); +const flat = (s: string): string => s.replace(/>\s+<").replace(/\s+/g, " ").trim(); + +const config = { + label: "Filter people", + rows: [ + [ + { type: "search", name: "q", placeholder: "Search people…", value: "ann", label: "Search people" }, + { + type: "segmented", + name: "status", + legend: "Status", + value: "active", + options: [ + { value: "all", label: "All", count: "1,284" }, + { value: "active", label: "Active" }, + { value: "archived", label: "Archived" }, + ], + }, + { type: "select", name: "team", label: "Team", value: "design", options: [{ value: "", label: "All teams" }, { value: "design", label: "Design" }] }, + { type: "spacer" }, + ], + [ + { + type: "chips", + name: "tag", + legend: "Tags", + value: ["engineering", "oncall"], + options: [{ value: "engineering", label: "Engineering" }, { value: "design", label: "Design" }, { value: "oncall", label: "On-call" }], + }, + { type: "daterange", legend: "Joined", from: { name: "joined_from", value: "2026-01-01", label: "Joined from" }, to: { name: "joined_to", value: "2026-06-14", label: "Joined to" } }, + ], + ], + pills: [{ label: "Team", value: "Engineering", remove: "?tag=oncall" }], + clearHref: "?", +}; + +test("filter-bar renders a GET form with every control type, reflecting current values", async () => { + const html = flat(await render(config)); + + // GET form (server-side filtering, zero-JS). + assert.match(html, /
/); + + // search — icon + value reflected. + assert.match(html, /