From 285d35601bcae82bba107506e057c9809653af23 Mon Sep 17 00:00:00 2001 From: Anders Olsson Date: Sun, 7 Jun 2026 18:18:03 +0200 Subject: [PATCH 1/9] =?UTF-8?q?docs(specs):=20app=20header=20wayfinding=20?= =?UTF-8?q?=E2=80=94=20breadcrumb,=20user=20menu,=20search,=20app=5Fname?= =?UTF-8?q?=20(#54)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2026-06-07-header-wayfinding-design.md | 169 ++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-07-header-wayfinding-design.md diff --git a/docs/superpowers/specs/2026-06-07-header-wayfinding-design.md b/docs/superpowers/specs/2026-06-07-header-wayfinding-design.md new file mode 100644 index 0000000..bc357f3 --- /dev/null +++ b/docs/superpowers/specs/2026-06-07-header-wayfinding-design.md @@ -0,0 +1,169 @@ +# 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 `