From 265704a7eb68cbac934cec87e7a4da5f3d47ab84 Mon Sep 17 00:00:00 2001 From: lilleman Date: Mon, 15 Jun 2026 11:44:40 +0200 Subject: [PATCH] =?UTF-8?q?Add=20lucide=20icon=20sprite=20partial=20(todo?= =?UTF-8?q?=20=C2=A71);=20src/icons.ts=20generates=20only-used=20symbols?= =?UTF-8?q?=20from=20pinned=20lucide-static?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 +- src/icons.test.ts | 33 ++++++++++++++++++++++ src/icons.ts | 59 ++++++++++++++++++++++++++++++++++++++++ todo.md | 5 +++- views/partials/icons.ejs | 34 +++++++++++++++++++++++ 5 files changed, 132 insertions(+), 2 deletions(-) create mode 100644 src/icons.test.ts create mode 100644 src/icons.ts create mode 100644 views/partials/icons.ejs diff --git a/README.md b/README.md index 28e7e64..4830f78 100644 --- a/README.md +++ b/README.md @@ -360,8 +360,9 @@ src/jwt.ts JWS signature verify via node:crypto, no jose; claims+JWKS src/cookie.ts Cookie parse + secure Set-Cookie build (session/CSRF cookies, §4) 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/) +views/ Core EJS templates (index, 403/404/500, partials/ incl. the 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/icons.test.ts b/src/icons.test.ts new file mode 100644 index 0000000..30a30af --- /dev/null +++ b/src/icons.test.ts @@ -0,0 +1,33 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { test } from "node:test"; +import { fileURLToPath } from "node:url"; +import * as ejs from "ejs"; +import { ICON_NAMES, buildIconSprite } from "./icons.ts"; + +const rootDir = join(dirname(fileURLToPath(import.meta.url)), ".."); +const lucideDir = join(rootDir, "node_modules", "lucide-static", "icons"); +const partial = join(rootDir, "views", "partials", "icons.ejs"); + +const symbolInner = (sprite: string, id: string): string => + sprite.match(new RegExp(`]*>(.*?)`))?.[1] ?? ""; + +test("icons partial inlines exactly the used lucide-static icons", async () => { + const built = buildIconSprite(lucideDir); + + // The committed partial must be exactly the generator output — proves provenance + // and flags drift when the pinned lucide-static is bumped without regenerating. + assert.equal(readFileSync(partial, "utf8"), built); + + const html = await ejs.renderFile(partial, {}); + const ids = [...html.matchAll(/ m[1]); + assert.deepEqual(ids, Object.keys(ICON_NAMES)); // only the used icons, complete + ordered + assert.match(html.trimStart(), /^]*aria-hidden="true"/); + + // Independent spot-checks: a wrong id→icon mapping is caught regardless of the builder. + assert.match(symbolInner(built, "i-x"), /M18 6 6 18/); + assert.match(symbolInner(built, "i-search"), /circle cx="11" cy="11" r="8"/); + assert.match(symbolInner(built, "i-kebab"), /circle cx="12" cy="12" r="1"/); + assert.match(symbolInner(built, "i-bell"), /M10\.268 21/); // lucide v1.18 path, not the mockup's older one +}); diff --git a/src/icons.ts b/src/icons.ts new file mode 100644 index 0000000..14b8933 --- /dev/null +++ b/src/icons.ts @@ -0,0 +1,59 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +// Sprite id → lucide-static icon, the icons the UI actually references (alphabetical by id). +// Inlined as so pages stay zero-JS: . +export const ICON_NAMES: Record = { + "i-alert": "circle-alert", + "i-arrow-left": "arrow-left", + "i-bell": "bell", + "i-box": "box", + "i-cal": "calendar", + "i-chart": "chart-no-axes-column", + "i-check-circle": "circle-check", + "i-chev": "chevron-right", + "i-cols": "columns-3", + "i-copy": "copy", + "i-download": "download", + "i-edit": "pencil", + "i-gear": "settings", + "i-globe": "globe", + "i-grid": "layout-grid", + "i-kebab": "ellipsis-vertical", + "i-layers": "layers", + "i-lock": "lock", + "i-logout": "log-out", + "i-mail": "mail", + "i-menu": "menu", + "i-plus": "plus", + "i-search": "search", + "i-shield": "shield", + "i-sliders": "sliders-horizontal", + "i-sort": "chevrons-up-down", + "i-trash": "trash-2", + "i-up": "chevron-up", + "i-user": "user", + "i-users": "users", + "i-x": "x", +}; + +// Drop lucide's license comment + wrapper, keep the drawing children (compacted). +function inner(svg: string): string { + const open = svg.indexOf(">", svg.indexOf("")).replace(/\s*\n\s*/g, "").trim(); +} + +// Hidden sprite for the used icons, sourced from the pinned lucide-static. +// Regenerates views/partials/icons.ejs; icons.test.ts asserts the committed file matches. +export function buildIconSprite(iconsDir: string): string { + const symbols = Object.entries(ICON_NAMES).map( + ([id, name]) => ` ${inner(readFileSync(join(iconsDir, `${name}.svg`), "utf8"))}`, + ); + return [ + "<%# Generated from lucide-static by src/icons.ts — regenerate on dep bump (guarded by icons.test.ts). %>", + '", + "", + ].join("\n"); +} diff --git a/todo.md b/todo.md index 0f64033..a52d3c7 100644 --- a/todo.md +++ b/todo.md @@ -26,7 +26,7 @@ everything via Docker. ## 1. Building blocks — extract from `html-css-foundation/` (no Ory needed; render mock data) - [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/`. -- [ ] Lucide icon sprite from `lucide-static` (dep added) → `views/partials/icons.ejs`; serve/inline only the icons used. +- [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 `` 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. - [ ] App-shell partial (sidebar + topbar + content slot). - [ ] Nav-tree partial — recursive, header/leaf × clickable/static, counts, `aria-current`. - [ ] Filter-bar partial — GET form (search, segmented, selects, chips, daterange, applied pills). @@ -43,6 +43,9 @@ everything via Docker. - [ ] 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. - [ ] Go over all tests and combine/unify ones that cover the same stuff or are very related and could be combined in a good way. Remove tests that aren't helping, we only want tests that are actually helpful to us. +### 1.1 Extra input from human +- [ ] Add to principles that we should have full E2E coverage in the Playwright tests - make sure they can run in parallel to get up some speed. + ## 2. Plugin host - [ ] **Specify the plugin contract** (big job, do first — it's the product's main API surface). Write it down as the authoritative reference: the full manifest shape; the `RequestContext` handed to handlers and what's guaranteed stable; **contract versioning** (a `apiVersion`/`engines`-style field so a plugin declares the host it targets, and the host refuses or warns on mismatch); **conflict rules** (two plugins claiming the same `basePath`, nav slot, or `permission` name → defined, loud resolution, not last-write-wins); the **local dev/test story** (how an author runs + tests one plugin in isolation against the host). Audience is experienced devs: optimise for a powerful, predictable, clearly-documented API. Crash-isolation (a bad plugin can't take down the host) is a *nice-to-have*, not a blocker — fail loud at boot/discovery over sandboxing at runtime. - [ ] Discovery: scan `plugins/`, import each `plugin.ts` default export, validate. diff --git a/views/partials/icons.ejs b/views/partials/icons.ejs new file mode 100644 index 0000000..d50fb28 --- /dev/null +++ b/views/partials/icons.ejs @@ -0,0 +1,34 @@ +<%# Generated from lucide-static by src/icons.ts — regenerate on dep bump (guarded by icons.test.ts). %> +