Add recursive nav-tree partial (todo §1); header/leaf × clickable/static, counts + aria-current
This commit is contained in:
@@ -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. the app shell + icon sprite)
|
||||
views/ Core EJS templates (index, 403/404/500, partials/ incl. app shell, nav tree, 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)
|
||||
|
||||
68
src/nav-tree.test.ts
Normal file
68
src/nav-tree.test.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
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 navTree = join(dirname(fileURLToPath(import.meta.url)), "..", "views", "partials", "nav-tree.ejs");
|
||||
const render = (data: Record<string, unknown> = {}): Promise<string> => ejs.renderFile(navTree, data);
|
||||
const flat = (s: string): string => s.replace(/>\s+</g, "><").replace(/\s+/g, " ").trim();
|
||||
|
||||
const nodes = [
|
||||
{ label: "Overview", href: "/overview", icon: "i-grid" }, // leaf · clickable · icon
|
||||
{
|
||||
label: "Workspace",
|
||||
open: true, // header · static · open
|
||||
children: [
|
||||
{
|
||||
label: "Directory",
|
||||
href: "/dir",
|
||||
icon: "i-users",
|
||||
count: 4,
|
||||
open: true, // header · clickable · icon · count
|
||||
children: [
|
||||
{ label: "People", href: "/people", count: "1,284", current: true }, // leaf · clickable · current
|
||||
{ label: "Webhooks (soon)" }, // leaf · static
|
||||
],
|
||||
},
|
||||
{ label: "Roles & Access", children: [{ label: "Roles", href: "/roles" }] }, // header · static · closed
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
test("nav-tree renders the header/leaf × clickable/static matrix with counts, icons and aria-current", async () => {
|
||||
const html = flat(await render({ nodes }));
|
||||
|
||||
// Root list vs. recursive child lists.
|
||||
assert.match(html, /<ul class="nav-tree">/);
|
||||
assert.match(html, /<ul class="nav-children">/);
|
||||
|
||||
// Leaf · clickable · icon — spacer (no toggle), <a>, inlined sprite ref.
|
||||
assert.match(
|
||||
html,
|
||||
/<span class="nav-spacer" aria-hidden="true"><\/span><a class="nav-self" href="\/overview"><svg class="ico"><use href="#i-grid"\s*\/?><\/svg><span class="nav-label">Overview<\/span><\/a>/,
|
||||
);
|
||||
|
||||
// Header · static · open — disclosure with [open] + escaped aria-label, <span> self.
|
||||
assert.match(
|
||||
html,
|
||||
/<details class="nav-disc" open><summary class="nav-tog" aria-label="Toggle Workspace">.*?<\/summary><\/details><span class="nav-self"><span class="nav-label">Workspace<\/span><\/span>/,
|
||||
);
|
||||
|
||||
// Header · clickable · icon · count.
|
||||
assert.match(html, /<a class="nav-self" href="\/dir"><svg class="ico"><use href="#i-users"\s*\/?><\/svg><span class="nav-label">Directory<\/span><span class="nav-count">4<\/span><\/a>/);
|
||||
|
||||
// Leaf · clickable · current · count.
|
||||
assert.match(html, /<a class="nav-self" href="\/people" aria-current="page"><span class="nav-label">People<\/span><span class="nav-count">1,284<\/span><\/a>/);
|
||||
|
||||
// Leaf · static (no href → <span>, no toggle).
|
||||
assert.match(html, /<span class="nav-self"><span class="nav-label">Webhooks \(soon\)<\/span><\/span>/);
|
||||
|
||||
// Header · static · closed (no [open]) + label escaping in both label and aria-label.
|
||||
assert.match(html, /<details class="nav-disc"><summary class="nav-tog" aria-label="Toggle Roles & Access">/);
|
||||
assert.match(html, /<span class="nav-label">Roles & Access<\/span>/);
|
||||
});
|
||||
|
||||
test("nav-tree renders an empty root list with no nodes and never throws", async () => {
|
||||
assert.match(flat(await render()), /<ul class="nav-tree"><\/ul>/);
|
||||
});
|
||||
3
todo.md
3
todo.md
@@ -28,7 +28,7 @@ everything via Docker.
|
||||
- [x] Move `styles.css` + `auth.css` into `public/css/`; remove existing `style.css`. → `git mv` from `html-css-foundation/` into `public/css/`; dropped the placeholder `style.css`; views + tests now reference `styles.css`; foundation mockups repointed to `../public/css/`.
|
||||
- [x] Lucide icon sprite from `lucide-static` (dep added) → `views/partials/icons.ejs`; serve/inline only the icons used. → `src/icons.ts` (id→lucide map + `buildIconSprite`) generates a hidden `<symbol>` sprite of the 31 icons the mockups reference, paths sourced from pinned lucide-static; `icons.test.ts` guards provenance + only-used. Stale image rebuilt (lucide-static was missing). Wiring into the app shell is the next item.
|
||||
- [x] App-shell partial (sidebar + topbar + content slot). → `views/partials/shell.ejs`: full document wrapping `.app` → sidebar (brand + `nav` slot + theme/profile footer) · `.scrim` · `.content` (`.topbar` + `body` slot); reuses the mockup's classes (styled by `styles.css`), inlines the icon sprite. Slots `nav`/`actions`/`body` are HTML locals, `title`/`brand`/`user`/`breadcrumbs` text; defaults render standalone. `shell.test.ts` covers landmarks, slots, escaping, defaults. Not yet routed (that's "replace placeholder index").
|
||||
- [ ] Nav-tree partial — recursive, header/leaf × clickable/static, counts, `aria-current`.
|
||||
- [x] Nav-tree partial — recursive, header/leaf × clickable/static, counts, `aria-current`. → `views/partials/nav-tree.ejs`: data-driven, self-including. Node `{ label, href?, icon?, count?, current?, open?, children? }`; header (children → `.nav-disc` toggle + sibling `.nav-children`) vs leaf (spacer), clickable (`<a>`) vs static (`<span>`), orthogonal. Renders into the shell's `nav` slot. `nav-tree.test.ts` covers the full matrix + counts/icons/aria-current/escaping/empty.
|
||||
- [ ] Filter-bar partial — GET form (search, segmented, selects, chips, daterange, applied pills).
|
||||
- [ ] Data-table partial — sortable headers, row-select, badges, kebab row actions.
|
||||
- [ ] Pagination partial — rows-per-page + page numbers, query-param driven.
|
||||
@@ -38,6 +38,7 @@ everything via Docker.
|
||||
- [ ] Helper `parseListQuery(url)` → `{ q, filters, sort, page, pageSize }`.
|
||||
- [ ] Helper `paginate(total, page, pageSize)` → page model.
|
||||
- [ ] Replace placeholder `index` with the app-shell dashboard.
|
||||
- [ ] Check the full system in Playwright and make screenshots and compare to the static original design in html-css-foundation to make sure we're showing the correct graphics.
|
||||
- [ ] Go over all HTML and CSS and make adjust it to be as sematic as we can, css classes, ids html elements and all, then add semantic DOM as a priority in this project.
|
||||
- [ ] Run the architecture _and_ the stability reviewer agents on the _whole_ project, not just the latest changes, and address their issues.
|
||||
- [ ] Go over all comments in the code and the README and try to make it shorter and more information dense. Remove not strictly needed stuff.
|
||||
|
||||
28
views/partials/nav-tree.ejs
Normal file
28
views/partials/nav-tree.ejs
Normal file
@@ -0,0 +1,28 @@
|
||||
<%#
|
||||
Recursive nav tree. Each node: { label, href?, icon?, count?, current?, open?, children? }.
|
||||
Shape is orthogonal: header (children → disclosure toggle + nested <ul>) vs leaf (spacer),
|
||||
and clickable (href → <a>) vs static (<span>). Mirrors the html-css-foundation markup; the
|
||||
CSS reveals children only when the sibling .nav-disc is [open]. `root` picks the outer class.
|
||||
%><%
|
||||
const nodes = locals.nodes || [];
|
||||
const root = locals.root !== false;
|
||||
-%>
|
||||
<ul class="<%= root ? "nav-tree" : "nav-children" %>">
|
||||
<% nodes.forEach((node) => { const header = node.children && node.children.length; const tag = node.href ? "a" : "span"; -%>
|
||||
<li class="nav-node">
|
||||
<div class="nav-row">
|
||||
<% if (header) { -%>
|
||||
<details class="nav-disc"<%= node.open ? " open" : "" %>>
|
||||
<summary class="nav-tog" aria-label="Toggle <%= node.label %>"><svg class="ico chev"><use href="#i-chev"/></svg></summary>
|
||||
</details>
|
||||
<% } else { -%>
|
||||
<span class="nav-spacer" aria-hidden="true"></span>
|
||||
<% } -%>
|
||||
<<%= tag %> class="nav-self"<% if (node.href) { %> href="<%= node.href %>"<% } %><% if (node.current) { %> aria-current="page"<% } %>><% if (node.icon) { %><svg class="ico"><use href="#<%= node.icon %>"/></svg><% } %><span class="nav-label"><%= node.label %></span><% if (node.count != null) { %><span class="nav-count"><%= node.count %></span><% } %></<%= tag %>>
|
||||
</div>
|
||||
<% if (header) { -%>
|
||||
<%- include("nav-tree", { nodes: node.children, root: false }) %>
|
||||
<% } -%>
|
||||
</li>
|
||||
<% }) -%>
|
||||
</ul>
|
||||
Reference in New Issue
Block a user