Wire branding into the app shell (todo §2); render config logo + default theme, fall back to the brand mark

This commit is contained in:
2026-06-16 16:07:24 +02:00
parent 952dd03cc2
commit ff7b55be4c
10 changed files with 43 additions and 10 deletions

View File

@@ -40,6 +40,17 @@ test("serves the home page: the app-shell People dashboard, filterable via the U
assert.doesNotMatch(await empty.text(), /Avery Kline/);
});
test("renders branding from the menu config into the shell: logo + default theme", async (t) => {
const app = createApp({ menu: { branding: { logo: "/public/brand/logo.svg", name: "Acme Ops", theme: "dark" }, override: {} } });
await new Promise<void>((r) => app.listen(0, r));
t.after(() => app.close());
const html = await (await fetch(`http://localhost:${(app.address() as AddressInfo).port}/`)).text();
assert.match(html, /<img class="brand-logo" src="\/public\/brand\/logo\.svg"/);
assert.match(html, /Acme Ops/);
assert.match(html, /id="theme-dark"\s+checked/); // config default theme reaches the switch
});
test("serves a static file: GET sends body + content-type, HEAD sends headers only", async () => {
const get = await fetch(base + "/public/css/styles.css");
assert.equal(get.status, 200);

View File

@@ -59,11 +59,12 @@ test("dashboard sorts by a column, reflects direction, and the header toggles",
test("dashboard applies the central menu config: branding + nav override (rename/hide)", () => {
const m = buildDashboardModel(new URL("http://x/"), [], {
branding: { name: "Acme Ops", sub: "Admin" },
branding: { logo: "/public/logo.svg", name: "Acme Ops", sub: "Admin", theme: "dark" },
override: { hide: ["teams"], rename: { people: "Staff" } },
});
assert.deepEqual(m.shell.brand, { name: "Acme Ops", sub: "Admin" });
assert.deepEqual(m.shell.brand, { logo: "/public/logo.svg", name: "Acme Ops", sub: "Admin" });
assert.equal(m.shell.theme, "dark");
const labels = m.nav.map((n) => n.label);
assert.ok(labels.includes("Staff")); // "People" renamed
assert.ok(!labels.includes("Teams")); // "Teams" hidden

View File

@@ -106,8 +106,13 @@ export function buildDashboardModel(url: URL | URLSearchParams | string, roles:
nav: nav(roles, menu.override),
pagination: pagination(state, page),
shell: {
brand: { name: menu.branding.name, ...(menu.branding.sub != null ? { sub: menu.branding.sub } : {}) },
brand: {
...(menu.branding.logo != null ? { logo: menu.branding.logo } : {}),
name: menu.branding.name,
...(menu.branding.sub != null ? { sub: menu.branding.sub } : {}),
},
breadcrumbs: [{ href: "?", label: "Directory" }, { label: "People" }],
...(menu.branding.theme != null ? { theme: menu.branding.theme } : {}),
title: "People",
user: { email: "sam.rivers@example.com", initials: "SR", name: "Sam Rivers" }, // demo until §4
},

View File

@@ -37,6 +37,17 @@ test("app shell renders sidebar, topbar and the content slot", async () => {
assert.match(html, /<use href="#i-menu"\s*\/?>/); // hamburger references the menu icon
});
test("app shell renders a configured logo + default theme, falls back to the brand mark", async () => {
const branded = await render({ brand: { logo: "/public/brand/logo.svg", name: "Acme" }, theme: "dark" });
assert.match(branded, /<img class="brand-logo" src="\/public\/brand\/logo\.svg"/);
assert.doesNotMatch(branded, /brand-mark/); // a logo replaces the default mark
assert.match(branded, /id="theme-dark"\s+checked/); // default theme applied to the switch
const plain = await render({ brand: { name: "Acme" } }); // no logo, no theme
assert.match(plain, /<span class="brand-mark">/); // default mark
assert.match(plain, /id="theme-auto"\s+checked/); // theme-switch default
});
test("app shell escapes text but passes slot HTML through, and renders with defaults", async () => {
const escaped = await render({ title: "<x>", body: "<p>raw</p>" });
assert.match(escaped, /<title>&lt;x&gt;<\/title>/); // user text is escaped