From bddc1f891d80a69bd3d1deb5f19a6aa1407a0fd8 Mon Sep 17 00:00:00 2001 From: lilleman Date: Mon, 15 Jun 2026 13:27:44 +0200 Subject: [PATCH] =?UTF-8?q?Add=20menu/popover=20+=20theme-switch=20partial?= =?UTF-8?q?s=20(todo=20=C2=A71);=20data-driven=20.menu=20(items/check-grou?= =?UTF-8?q?ps/positioning),=20Light/Auto/Dark=20switch,=20shell=20reuses?= =?UTF-8?q?=20both?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- public/css/styles.css | 1 + src/menu.test.ts | 59 +++++++++++++++++++++++++++++++++ src/theme-switch.test.ts | 25 ++++++++++++++ todo.md | 2 +- views/partials/menu.ejs | 37 +++++++++++++++++++++ views/partials/shell.ejs | 25 ++++++-------- views/partials/theme-switch.ejs | 14 ++++++++ 8 files changed, 148 insertions(+), 17 deletions(-) create mode 100644 src/menu.test.ts create mode 100644 src/theme-switch.test.ts create mode 100644 views/partials/menu.ejs create mode 100644 views/partials/theme-switch.ejs diff --git a/README.md b/README.md index ddb8bb1..44055a5 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, data table, pagination, form field, auth card, icon sprite) +views/ Core EJS templates (index, 403/404/500, partials/ incl. app shell, nav tree, filter bar, data table, pagination, form field, auth card, menu/popover, theme switch, 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/public/css/styles.css b/public/css/styles.css index ebe33e4..abc6bf4 100644 --- a/public/css/styles.css +++ b/public/css/styles.css @@ -479,6 +479,7 @@ span.nav-self { cursor: default; } /* static / non-clickable */ box-shadow: 0 8px 28px rgba(0,0,0,.16); } .menu-pop.left { right: auto; left: 0; } +.menu-pop.up { top: auto; bottom: calc(100% + 6px); } .menu-head { font-size: var(--fz-xs); text-transform: uppercase; letter-spacing: .05em; color: var(--text-faint); font-weight: 600; padding: 5px 8px; } diff --git a/src/menu.test.ts b/src/menu.test.ts new file mode 100644 index 0000000..9b39789 --- /dev/null +++ b/src/menu.test.ts @@ -0,0 +1,59 @@ +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 menu = join(dirname(fileURLToPath(import.meta.url)), "..", "views", "partials", "menu.ejs"); +const render = (data: Record = {}): Promise => ejs.renderFile(menu, data); +const flat = (s: string): string => s.replace(/>\s+<").replace(/\s+/g, " ").trim(); + +test("menu renders trigger, positioning, the item matrix and check groups", async () => { + const html = flat(await render({ + trigger: { icon: "i-cols", text: "Columns", label: "Column settings" }, + align: "left", up: true, width: 240, + items: [ + { head: "Actions" }, + { label: "Profile", icon: "i-user" }, // button (default), with icon + { label: "Docs", href: "/docs" }, // link + { sep: true }, + { label: "Sign out", icon: "i-logout", danger: true }, + { group: { legend: "Role", name: "role", control: "radio", options: [ + { value: "", label: "Any role", checked: true }, + { value: "admin", label: "Admin" }, + ] } }, + { group: { name: "col", options: [{ value: "name", label: "Name", checked: true }] } }, // checkbox default, no legend + ], + })); + + // Trigger: icon + text + aria-label; popover carries align/up classes + width. + assert.match(html, /'); +}); diff --git a/src/theme-switch.test.ts b/src/theme-switch.test.ts new file mode 100644 index 0000000..a283680 --- /dev/null +++ b/src/theme-switch.test.ts @@ -0,0 +1,25 @@ +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 themeSwitch = join(dirname(fileURLToPath(import.meta.url)), "..", "views", "partials", "theme-switch.ejs"); +const render = (data: Record = {}): Promise => ejs.renderFile(themeSwitch, data); +const flat = (s: string): string => s.replace(/>\s+<").replace(/\s+/g, " ").trim(); + +test("theme switch renders the Light/Auto/Dark radiogroup with CSS-coupled ids", async () => { + // ids must be theme-light/auto/dark — styles.css keys html:has(#theme-…:checked) off them. + const html = flat(await render({ value: "dark", label: "Appearance" })); + assert.match(html, /
/); + assert.match(html, /