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.
This commit is contained in:
Julien Gautier
2026-05-11 02:03:20 +02:00
parent b6aa17f6a0
commit f6ba6cde2c
20 changed files with 505 additions and 76 deletions
+20
View File
@@ -17,6 +17,9 @@ importers:
.:
dependencies:
'@angular/cdk':
specifier: ~21.2.10
version: 21.2.10(@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))(@angular/platform-browser@21.2.12(@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)))(rxjs@7.8.2)
'@angular/common':
specifier: ~21.2.0
version: 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)
@@ -575,6 +578,14 @@ packages:
vitest:
optional: true
'@angular/cdk@21.2.10':
resolution: {integrity: sha512-yfCzUFNfeSMNnCkc0P5Pozqz1EViDe9KLPQyHVY/hApsNgBWvckpA+ZEWgKNfAf72f8bUJvZoHejaSMGYrpvuw==}
peerDependencies:
'@angular/common': ^21.0.0 || ^22.0.0
'@angular/core': ^21.0.0 || ^22.0.0
'@angular/platform-browser': ^21.0.0 || ^22.0.0
rxjs: ^6.5.3 || ^7.4.0
'@angular/cli@21.2.10':
resolution: {integrity: sha512-ezf9LM0GgexG2l8ae/uN4fUyxGqeFEH9iu30mUMU5dwow76aK+b4Abuf5eSuR0F8zTLEA3ZUEYywI+gajbAUuA==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'}
@@ -10383,6 +10394,15 @@ snapshots:
- tsx
- yaml
'@angular/cdk@21.2.10(@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))(@angular/platform-browser@21.2.12(@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)))(rxjs@7.8.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)
'@angular/platform-browser': 21.2.12(@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))
parse5: 8.0.1
rxjs: 7.8.2
tslib: 2.8.1
'@angular/cli@21.2.10(@types/node@24.12.3)(chokidar@5.0.0)':
dependencies:
'@angular-devkit/architect': 0.2102.10(chokidar@5.0.0)