feat(portal-shell): app-shell layout with collapsable sidebar #83

Merged
julien merged 1 commits from feat/portal-shell/app-shell-layout into main 2026-05-11 00:36:42 +02:00
Owner

Summary

  • Replace the flat header+main+footer layout with a real app-shell: fixed header on top, collapsable sidebar + scrollable main below. Sidebar state (collapsed / expanded) persists across reloads via localStorage.
  • Introduce the APF brand palette as Tailwind v4 @theme tokens (primary teal #12546c, accent orange #f7a919) so every utility (bg-brand-primary-500, text-brand-accent-400, ring-brand-primary-200, …) is available from now on.
  • Add an <app-icon> façade backed by lucide-angular for v1. Logical kebab-case names already match the icomoon-sprite convention, so the future migration is a single-file change in icon.ts and consumers stay untouched.
  • Migrate the accessibility-statement links from the (now-deleted) footer to the bottom of the sidebar.

Decisions worth flagging

  • Static menu, permission-shaped data. Items point to # placeholders in v1, except Dashboard which is routerLink="/" so the active-state styling is visible on the home page. The MenuItem shape already carries an optional requiredPermissions: string[] so the permission-aware filter (PR 2, alongside ADR-0009 auth) plugs in without restructuring.
  • <app-icon> over direct lucide imports. Consumers write <app-icon name="bell"> rather than importing the lucide pascal-case symbol. When the icomoon sprite lands, only the registry in icon.ts changes — templates do not.
  • Sidebar persistence via localStorage, not backend. Zero round-trip, survives reloads, falls back gracefully when storage is blocked (private mode). Eventually mirrored server-side if the user-preferences feature lands.
  • Footer removed entirely. With the sidebar carrying the FR + EN accessibility-statement links and the role badge, the bottom rail no longer earned its vertical real estate. The version badge moved out for now; it will return as part of a debug/help menu when there's a real release to surface.

Accessibility (ADR-0016)

  • Skip-link preserved (WCAG 2.4.1 Bypass Blocks) and restyled in the brand palette.
  • Sidebar exposes named landmarks (<nav aria-label="Sections">, <nav aria-label="Accessibility">) and the collapse button uses aria-expanded + a descriptive aria-label.
  • Active links carry ariaCurrentWhenActive="page".
  • All interactive controls (header action buttons, sidebar links/toggle) meet the 44×44 px minimum hit-target.
  • Sidebar width transition is skipped under prefers-reduced-motion: reduce.
  • Lucide SVGs are marked aria-hidden (decorative); accessible names live on the parent control.

Perf (ADR-0017)

  • Production build: 100 kB gzip initial transfer (budget: 300 kB). Lucide imports are tree-shaken — only the ~18 icons actually used ship.

What this PR explicitly does NOT do

  • Wire icon-set migration to icomoon (kept as a deferred swap behind <app-icon>).
  • Filter the menu by permission (deferred to PR 2 once the auth flow lands).
  • Replace the avatar placeholder with a real user menu (waits on ADR-0009).
  • Implement the search input behavior (placeholder only; needs a search backend).

Test plan

  • pnpm exec nx run-many -t lint test build --projects=portal-shell — green (19 / 19 specs).
  • Production build under bundle budgets (100 kB gzip initial).
  • Manual: load /, confirm Dashboard appears active in the sidebar, collapse → reload → still collapsed, focus the address bar then Tab → skip-link visible, keyboard-traverse the sidebar.
  • Manual: prefers-reduced-motion: reduce → no width animation when toggling.
  • Manual: zoom to 200 % → no horizontal scroll, header search hides at narrow widths (md: breakpoint).
## Summary - Replace the flat header+main+footer layout with a real app-shell: fixed header on top, collapsable sidebar + scrollable main below. Sidebar state (collapsed / expanded) persists across reloads via `localStorage`. - Introduce the APF brand palette as Tailwind v4 `@theme` tokens (primary teal `#12546c`, accent orange `#f7a919`) so every utility (`bg-brand-primary-500`, `text-brand-accent-400`, `ring-brand-primary-200`, …) is available from now on. - Add an `<app-icon>` façade backed by `lucide-angular` for v1. Logical kebab-case names already match the icomoon-sprite convention, so the future migration is a single-file change in `icon.ts` and consumers stay untouched. - Migrate the accessibility-statement links from the (now-deleted) footer to the bottom of the sidebar. ## Decisions worth flagging - **Static menu, permission-shaped data.** Items point to `#` placeholders in v1, except *Dashboard* which is `routerLink="/"` so the active-state styling is visible on the home page. The `MenuItem` shape already carries an optional `requiredPermissions: string[]` so the permission-aware filter (PR 2, alongside ADR-0009 auth) plugs in without restructuring. - **`<app-icon>` over direct lucide imports.** Consumers write `<app-icon name="bell">` rather than importing the lucide pascal-case symbol. When the icomoon sprite lands, only the registry in `icon.ts` changes — templates do not. - **Sidebar persistence via `localStorage`, not backend.** Zero round-trip, survives reloads, falls back gracefully when storage is blocked (private mode). Eventually mirrored server-side if the user-preferences feature lands. - **Footer removed entirely.** With the sidebar carrying the FR + EN accessibility-statement links and the role badge, the bottom rail no longer earned its vertical real estate. The version badge moved out for now; it will return as part of a debug/help menu when there's a real release to surface. ## Accessibility (ADR-0016) - Skip-link preserved (WCAG 2.4.1 *Bypass Blocks*) and restyled in the brand palette. - Sidebar exposes named landmarks (`<nav aria-label="Sections">`, `<nav aria-label="Accessibility">`) and the collapse button uses `aria-expanded` + a descriptive `aria-label`. - Active links carry `ariaCurrentWhenActive="page"`. - All interactive controls (header action buttons, sidebar links/toggle) meet the 44×44 px minimum hit-target. - Sidebar width transition is skipped under `prefers-reduced-motion: reduce`. - Lucide SVGs are marked `aria-hidden` (decorative); accessible names live on the parent control. ## Perf (ADR-0017) - Production build: **100 kB gzip** initial transfer (budget: 300 kB). Lucide imports are tree-shaken — only the ~18 icons actually used ship. ## What this PR explicitly does NOT do - Wire icon-set migration to icomoon (kept as a deferred swap behind `<app-icon>`). - Filter the menu by permission (deferred to PR 2 once the auth flow lands). - Replace the avatar placeholder with a real user menu (waits on ADR-0009). - Implement the search input behavior (placeholder only; needs a search backend). ## Test plan - [x] `pnpm exec nx run-many -t lint test build --projects=portal-shell` — green (19 / 19 specs). - [x] Production build under bundle budgets (100 kB gzip initial). - [ ] Manual: load `/`, confirm Dashboard appears active in the sidebar, collapse → reload → still collapsed, focus the address bar then Tab → skip-link visible, keyboard-traverse the sidebar. - [ ] Manual: `prefers-reduced-motion: reduce` → no width animation when toggling. - [ ] Manual: zoom to 200 % → no horizontal scroll, header search hides at narrow widths (`md:` breakpoint).
julien added 1 commit 2026-05-11 00:31:13 +02:00
feat(portal-shell): app-shell layout with collapsable sidebar
CI / commits (pull_request) Successful in 1m39s
CI / scan (pull_request) Successful in 1m51s
CI / check (pull_request) Successful in 2m10s
CI / a11y (pull_request) Successful in 1m9s
CI / perf (pull_request) Successful in 2m7s
c144cdcbd5
Switch the SPA from header+main+footer to a fixed header above a
sidebar/main split. The sidebar carries grouped navigation, can be
collapsed to an icon-only rail (state persisted in localStorage), and
hosts the accessibility-statement links the footer used to own. Adds
brand palette tokens (primary teal #12546c, accent orange #f7a919) as
Tailwind v4 @theme entries, an <app-icon> façade over lucide-angular
so the future icomoon sprite migration is a single-file change, and
the APF logo asset under public/logos/.

Menu data is static for v1 — items point to '#' placeholders, with the
Dashboard entry routerLink-bound so the active-state styling is
visible on the home page. The MenuItem shape already carries an
optional requiredPermissions field so the permission-aware filter (to
land alongside the auth flow per ADR-0009) plugs in without
restructuring. WCAG-relevant choices preserved: skip-link, role/aria
landmarks, aria-current="page" on active links, aria-expanded on the
toggle, prefers-reduced-motion respected on width transitions, and a
44px minimum hit-target on every interactive element (ADR-0016).

Footer component removed — its links migrated to the sidebar bottom.
julien merged commit 3371fbd613 into main 2026-05-11 00:36:42 +02:00
julien deleted branch feat/portal-shell/app-shell-layout 2026-05-11 00:36:43 +02:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: julien/apf_portal#83