Commit Graph

2 Commits

Author SHA1 Message Date
Julien Gautier f6ba6cde2c feat(portal-shell): light / dark / auto theme switcher
CI / commits (pull_request) Successful in 3m11s
CI / scan (pull_request) Successful in 3m11s
CI / check (pull_request) Successful in 3m27s
CI / a11y (pull_request) Successful in 2m8s
CI / perf (pull_request) Successful in 4m14s
Add a header dropdown that lets users pick light, dark, or auto (follow
the OS) color schemes. The choice persists in localStorage and applies
across reloads. In `auto` mode, the service listens to the
`prefers-color-scheme` media query so the UI flips when the OS theme
changes without a page refresh.

Architecture:

- `LayoutStateService` grows a `themeMode` signal alongside the
  existing `sidebarCollapsed`, plus an `effectiveTheme` computed that
  resolves `auto` against the system preference. A side-effect toggles
  the `.dark` class on `<html>` so every `dark:` Tailwind utility
  flips at once.
- Tailwind v4 dark mode rewired to class-based via
  `@custom-variant dark (&:where(.dark, .dark *));` in `styles.css`,
  overriding the v4 default (media-query-driven).
- Theme switcher built on `@angular/cdk/menu` for accessible roving
  focus, escape/click-outside dismissal, and `menuitemradio` semantics
  on the three options. Trigger glyph reflects the SELECTED mode (sun
  / moon / monitor) so users can tell which mode they're in even when
  auto resolves to the same scheme as a manual pick.
- Dark variants applied across the shell (header, sidebar, main bg)
  and the existing two pages. No semantic-token refactor yet — that
  belongs in a future ADR (`--color-surface-1`, ...).

Side-edits:

- Replace `apf-small.svg` (94 kB, a base64-PNG-wrapped-in-SVG) with
  a 144x144 PNG optimised by sharp at compression level 9 (7.6 kB).
- Header logo `src` swapped to `apf-small.png`. The wide vector
  `apf-portal.svg` stays in place for future surfaces that want the
  horizontal lockup.
2026-05-11 02:03:20 +02:00
julien 3371fbd613 feat(portal-shell): app-shell layout with collapsable sidebar (#83)
CI / commits (push) Has been skipped
CI / scan (push) Successful in 1m37s
CI / check (push) Successful in 2m25s
CI / a11y (push) Successful in 57s
CI / perf (push) Successful in 3m5s
## 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
2026-05-11 00:36:41 +02:00