# App Header Wayfinding — Design **Date:** 2026-06-07 **Status:** Approved (brainstorming) — ready for implementation planning. **Issue:** #54. ## Context The `
` in `web/src/shell/app-shell.tsx` carries only a flex spacer + ThemeSwitch + LangSwitch + a Sign out button — no "where am I", no user identity, no search entry. Three further gaps: deep-linking to nested/create routes gives no header cue; the configured `app_name` (`useConfig()`, default "Collection Management System") is never shown — the sidebar brand (`sidebar.tsx:76`) and login heading (`login-page.tsx:38`) render the hardcoded `t("app.name")` = "Collection"; and global search requires a full route change via the sidebar. Facts established: routes are JSX `` elements (no `useMatches`/`handle`), so a breadcrumb can't use react-router's match data — and the object routes carry a UUID `:id`, not the `object_number` the breadcrumb wants. `useMe()` (`api/queries.ts:30`) returns `UserView | null` = `{ email, id, role }`. `useLogout()` (`queries.ts:129`) POSTs `/api/admin/logout`. Base UI ships a Menu primitive — `import { Menu } from "@base-ui/react/menu"` (namespace export) — no new dependency. `#57` added `useDocumentTitle(page)` per page; this milestone adds a parallel page-driven breadcrumb. The full ⌘K command palette / in-place search modal is **#33** (out of scope here). ### Decisions (from brainstorming) 1. **Page-driven breadcrumb context** (`useBreadcrumb(trail)`), parallel to `useDocumentTitle` — the header stays dumb; detail pages supply exact dynamic crumbs (`object_number`, vocab name). 2. **User-menu dropdown** using a new `ui/menu.tsx` Base UI Menu wrapper; Sign out moves into it. 3. **Compact header search** that navigates to `/search?q=…` (light entry; the palette is #33). 4. **Brand + login use `useConfig().app_name`**; the hardcoded `app.name` i18n key is removed. ## Components ### 1. `app_name` everywhere (kill "Collection") - `web/src/shell/sidebar.tsx:76`: `t("app.name")` → `useConfig().app_name` (still hidden when the sidebar is collapsed). - `web/src/auth/login-page.tsx`: the `

` (line 38) and the `document.title` effect (line 18, added in #57) → `useConfig().app_name`. Login is rendered inside `ConfigProvider` (mounted in `main.tsx` around everything), so `useConfig()` works there; if `/api/config` is auth-gated pre-login, it returns the default "Collection Management System" — still the correct product name, and better than "Collection". - Remove the now-unused `app.name` key from `en.json` and `sv.json` (grep confirms only the three usages above; all are migrated). `useDocumentTitle` already reads `app_name` from `useConfig`, not i18n, so nothing else depends on the key. ### 2. Breadcrumb (page-driven context) - **`web/src/shell/breadcrumb-context.ts`** — `export type BreadcrumbItem = { label: string; to?: string }` and a context `{ trail: BreadcrumbItem[]; setTrail: (t: BreadcrumbItem[]) => void }` (default `{ trail: [], setTrail: () => {} }`). - **`web/src/shell/breadcrumb-provider.tsx`** — holds `useState([])`, provides `{ trail, setTrail }`. Mounted in `AppShell` wrapping the header + main. - **`web/src/shell/use-breadcrumb.ts`** — `useBreadcrumb(trail: BreadcrumbItem[])`: in a `useEffect` calls `setTrail(trail)`, keyed on a **serialized** trail string (`trail.map(i => \`${i.label}${i.to ?? ""}\`).join("")`) to avoid re-running on every render from a fresh array literal. (No clear-on-unmount: every AppShell route sets its own trail, so the next page overwrites — avoids an empty-then-refill flash.) - **Header rendering:** a `Breadcrumb` element on the header's left renders the trail as `crumb / crumb / crumb`; non-terminal crumbs with a `to` are ``s (`text-muted-foreground hover:text-foreground`), the terminal crumb is `text-foreground`. Separator `/` (muted). Uses `text-sm`. Truncates with `truncate`/`min-w-0` so a long `object_number` doesn't push the right side off. (No `aria` nav landmark needed beyond a simple `