feat(portal-shell): light / dark / auto theme switcher #86
Reference in New Issue
Block a user
Delete Branch "feat/portal-shell/theme-toggle"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
localStorage, applies on next reload, and reacts live to OS theme changes when inauto.@angular/cdk/menufor accessible roving focus, escape-to-close, and propermenuitemradiosemantics on the three options.dark:variants 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,--color-text-1, …).Architecture
LayoutStateServicegrows athemeMode: signal<'light'|'dark'|'auto'>alongside the existingsidebarCollapsed, plus aneffectiveThemecomputed that resolvesautoagainst the system preference. A side-effect toggles the.darkclass on<html>, so everydark:Tailwind utility flips at once.@custom-variant dark (&:where(.dark, .dark *));instyles.css. This overrides the v4 default (prefers-color-schemeonly) so the user's explicit override wins over the OS preference.autohappens to resolve to the same scheme as a manual pick.Side-edits in the same PR (already validated in the chat)
apf-small.svg(94 kB — a base64-PNG wrapped in SVG markup, not actually vector) withapf-small.png(144×144, 7.6 kB aftersharp --kernel lanczos3 --compressionLevel 9). Header swaps to the PNG. The wide vectorapf-portal.svgstays around for future surfaces that want the horizontal lockup.@angular/localize(separate ADR).Decisions worth flagging
aria-haspopup, focus management, ESC handling, click-outside dismissal), reusable for future header menus (user, language, notifications), and one tidy primitive rather than three competing buttons.autois the default, notlight. Most users have an OS-level preference already; respecting it is the least surprising baseline.<html>.darkclass lives at the root, not on<app-root>. That's the Tailwind convention and it means CDK overlay popups (the menu itself, future dialogs) inherit the right theme without extra wiring.@angular/cdk/{menu,overlay,a11y}and the dark CSS rules. We stay well under the 300 kB budget. The CDK is already on the architecture menu (ADR-0016 — UI stack: Angular CDK + TailwindCSS) so this is on-strategy spend.dark:bg-gray-900, etc.) instead of a--color-surface-1/--color-text-1token layer. That keeps the change tractable for now; promotion to semantic tokens deserves its own ADR with the design team in the loop.Accessibility (ADR-0016)
aria-haspopup="menu",aria-labelannouncing the current mode + "(open menu)".role="menu", items userole="menuitemradio"witharia-checked— assistive tech announces the selection state correctly.prefers-reduced-motion: reducealready covered by the sidebar transitions; theme switcher has no animations of its own.300step in dark mode so the active states keep contrast against gray-900.What this PR explicitly does NOT do
@angular/localizeADR + PR).color-schemeCSS transition if it bothers users).Test plan
pnpm exec nx run-many -t lint test build --projects=portal-shell— green (33 / 33 specs, +8 for the theme work).auto, change OS theme → UI follows live (Chrome DevTools → Rendering → "Emulate CSS media feature prefers-color-scheme")./in dark mode — score unchanged from light mode.