feat(portal-shell): app-shell layout with collapsable sidebar (#83)
## 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). --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #83
This commit was merged in pull request #83.
This commit is contained in:
Generated
+15
@@ -107,6 +107,9 @@ importers:
|
||||
class-validator:
|
||||
specifier: ^0.15.1
|
||||
version: 0.15.1
|
||||
lucide-angular:
|
||||
specifier: ^1.0.0
|
||||
version: 1.0.0(@angular/common@21.2.12(@angular/core@21.2.12(@angular/compiler@21.2.12)(rxjs@7.8.2)(zone.js@0.16.2))(rxjs@7.8.2))(@angular/core@21.2.12(@angular/compiler@21.2.12)(rxjs@7.8.2)(zone.js@0.16.2))
|
||||
nestjs-cls:
|
||||
specifier: ^6.2.0
|
||||
version: 6.2.0(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||
@@ -7360,6 +7363,12 @@ packages:
|
||||
resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
lucide-angular@1.0.0:
|
||||
resolution: {integrity: sha512-YxCNEXHUz2IzAZIlxU4CkD55ljMjOlm3/am4eqadX/qkFszyGDzZwtbWOP1wj6vlbn/BNL4RhJeXbusLz96ajg==}
|
||||
peerDependencies:
|
||||
'@angular/common': 13.x - 21.x
|
||||
'@angular/core': 13.x - 21.x
|
||||
|
||||
luxon@3.7.2:
|
||||
resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -18408,6 +18417,12 @@ snapshots:
|
||||
|
||||
lru-cache@7.18.3: {}
|
||||
|
||||
lucide-angular@1.0.0(@angular/common@21.2.12(@angular/core@21.2.12(@angular/compiler@21.2.12)(rxjs@7.8.2)(zone.js@0.16.2))(rxjs@7.8.2))(@angular/core@21.2.12(@angular/compiler@21.2.12)(rxjs@7.8.2)(zone.js@0.16.2)):
|
||||
dependencies:
|
||||
'@angular/common': 21.2.12(@angular/core@21.2.12(@angular/compiler@21.2.12)(rxjs@7.8.2)(zone.js@0.16.2))(rxjs@7.8.2)
|
||||
'@angular/core': 21.2.12(@angular/compiler@21.2.12)(rxjs@7.8.2)(zone.js@0.16.2)
|
||||
tslib: 2.8.1
|
||||
|
||||
luxon@3.7.2: {}
|
||||
|
||||
magic-string@0.25.7:
|
||||
|
||||
Reference in New Issue
Block a user