Main navigation shell (#20) #66

Merged
james merged 1 commit from 20-navigation into main 2026-06-15 13:47:04 +00:00
Owner

Closes #20.

Summary

  • New app/(app)/ route group hosts the authenticated app surface with a shared chrome (top nav + content container). The (app) segment is part of the source path, not the URL — app/(app)/profile/page.tsx maps to /profile.
  • app/(app)/layout.tsx renders <TopNav /> then a max-width: 72rem content container. CSS variables from ADR-0008 throughout; the layout looks coherent in both shipped themes.
  • app/(app)/components/top-nav.tsx is a "use client" component with the five nav links (Profile, Skills, Experience, Projects, Network) + the embedded theme switcher. Uses usePathname() for active-link detection.
  • Five placeholder pages render via a shared <Placeholder title ticket blurb /> component so when each feature ticket lands it replaces the call with real content. Each placeholder names the tracking ticket(s) (epic #4 / issue #21, etc.).
  • /notes moved from app/notes/ to app/(app)/notes/ so it inherits the chrome and matches the future shape feature surfaces will follow. Not in the main nav (it's a reference, not a feature) but reachable by URL. Its internal styling was reduced (no more page-owned background / min-height / max-width) so it fits inside the shell.
  • app/page.tsx (the public landing) no longer mounts the theme switcher — it now lives in the nav shell. Unauth visitors get OS-matched themes via prefers-color-scheme per ADR-0008. The landing copy points first-time users at POST /api/auth/register and then /profile.

Keyboard accessibility

  • <nav aria-label="Main"> + <ul> of <a> (via Next.js <Link>), in tab order.
  • aria-current="page" set on the active link.
  • Default browser <a> focus rings preserved — inline styles don't strip outline, so keyboard users see a visible focus indicator without a custom CSS rule.
  • Theme switcher already has role="radiogroup" / role="radio" from #19.

Build / route summary

○  /                  static (public landing)
○  /experience        static (placeholder)
○  /network           static (placeholder)
○  /profile           static (placeholder)
○  /projects          static (placeholder)
○  /skills            static (placeholder)
ƒ  /notes             dynamic (server prefetch from ADR-0012)
○  /offline           static (force-static, ADR-0008 precache intact)
ƒ  Proxy (Middleware)

/offline still ○ (Static) is the load-bearing thing — the nav layout lives only under (app)/, so the root layout and /offline's precachability stay untouched.

What didn't change

  • Root layout. ADR-0008 invariant preserved.
  • The proxy's public-route allowlist. All five new routes are auto-gated by default-deny.
  • No new dependencies. No new tests — placeholders have no behaviour worth asserting (the proxy + auth tests already cover "arbitrary route 401s without a session"; manual probe confirms).
  • The auth/data/theming stack from earlier PRs.

Test plan

  • npm run typecheck / npm run lint / npm test — all green (119 / 32 skipped).
  • npm run build — succeeds. Route summary above.
  • npm run dev — manual probes:
    • Authed: /, /profile, /skills, /experience, /projects, /network, /notes, /offline → all 200.
    • Unauth: /profile, /skills, /notes → 401 (default-deny intact).
    • /profile HTML contains <nav aria-label="Main">, the five links in order, aria-current="page" set on Profile (the active link), the theme switcher embedded in the nav, and the placeholder content with the tracking ticket link.
  • CI: build, dual-engine tests, security scans, gitleaks (now also in CI per ADR-0011).
  • Manual browser: flip OS prefers-color-scheme + click through the theme switcher to confirm coherent rendering in both themes; keyboard-tab through the nav to confirm focus visibility.

🤖 Generated with Claude Code

Closes #20. ## Summary - New `app/(app)/` route group hosts the authenticated app surface with a shared chrome (top nav + content container). The `(app)` segment is part of the source path, not the URL — `app/(app)/profile/page.tsx` maps to `/profile`. - `app/(app)/layout.tsx` renders `<TopNav />` then a `max-width: 72rem` content container. CSS variables from ADR-0008 throughout; the layout looks coherent in both shipped themes. - `app/(app)/components/top-nav.tsx` is a `"use client"` component with the five nav links (Profile, Skills, Experience, Projects, Network) + the embedded theme switcher. Uses `usePathname()` for active-link detection. - Five placeholder pages render via a shared `<Placeholder title ticket blurb />` component so when each feature ticket lands it replaces the call with real content. Each placeholder names the tracking ticket(s) (epic #4 / issue #21, etc.). - `/notes` moved from `app/notes/` to `app/(app)/notes/` so it inherits the chrome and matches the future shape feature surfaces will follow. Not in the main nav (it's a reference, not a feature) but reachable by URL. Its internal styling was reduced (no more page-owned background / min-height / max-width) so it fits inside the shell. - `app/page.tsx` (the public landing) no longer mounts the theme switcher — it now lives in the nav shell. Unauth visitors get OS-matched themes via `prefers-color-scheme` per ADR-0008. The landing copy points first-time users at `POST /api/auth/register` and then `/profile`. ## Keyboard accessibility - `<nav aria-label="Main">` + `<ul>` of `<a>` (via Next.js `<Link>`), in tab order. - `aria-current="page"` set on the active link. - Default browser `<a>` focus rings preserved — inline styles don't strip `outline`, so keyboard users see a visible focus indicator without a custom CSS rule. - Theme switcher already has `role="radiogroup"` / `role="radio"` from #19. ## Build / route summary ``` ○ / static (public landing) ○ /experience static (placeholder) ○ /network static (placeholder) ○ /profile static (placeholder) ○ /projects static (placeholder) ○ /skills static (placeholder) ƒ /notes dynamic (server prefetch from ADR-0012) ○ /offline static (force-static, ADR-0008 precache intact) ƒ Proxy (Middleware) ``` `/offline` still `○ (Static)` is the load-bearing thing — the nav layout lives only under `(app)/`, so the root layout and `/offline`'s precachability stay untouched. ## What didn't change - Root layout. ADR-0008 invariant preserved. - The proxy's public-route allowlist. All five new routes are auto-gated by default-deny. - No new dependencies. No new tests — placeholders have no behaviour worth asserting (the proxy + auth tests already cover "arbitrary route 401s without a session"; manual probe confirms). - The auth/data/theming stack from earlier PRs. ## Test plan - [x] `npm run typecheck` / `npm run lint` / `npm test` — all green (119 / 32 skipped). - [x] `npm run build` — succeeds. Route summary above. - [x] `npm run dev` — manual probes: - Authed: `/`, `/profile`, `/skills`, `/experience`, `/projects`, `/network`, `/notes`, `/offline` → all 200. - Unauth: `/profile`, `/skills`, `/notes` → 401 (default-deny intact). - `/profile` HTML contains `<nav aria-label="Main">`, the five links in order, `aria-current="page"` set on Profile (the active link), the theme switcher embedded in the nav, and the placeholder content with the tracking ticket link. - [ ] CI: build, dual-engine tests, security scans, gitleaks (now also in CI per ADR-0011). - [ ] **Manual browser:** flip OS prefers-color-scheme + click through the theme switcher to confirm coherent rendering in both themes; keyboard-tab through the nav to confirm focus visibility. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Main navigation shell (#20)
All checks were successful
PR / Build (pull_request) Successful in 1m17s
PR / Trivy (image) (pull_request) Successful in 1m20s
Secrets / gitleaks (pull_request) Successful in 25s
PR / OSV-Scanner (pull_request) Successful in 26s
PR / Typecheck (pull_request) Successful in 45s
PR / Static analysis (Semgrep) (pull_request) Successful in 48s
PR / Lint (pull_request) Successful in 47s
PR / npm audit (pull_request) Successful in 49s
PR / Test (postgres) (pull_request) Successful in 1m4s
PR / Test (sqlite) (pull_request) Successful in 1m5s
335872144a
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
james merged commit 5ea58d3032 into main 2026-06-15 13:47:04 +00:00
Sign in to join this conversation.
No description provided.