feat(portal-shell): align theme switcher trigger with locale switcher shape #164

Merged
julien merged 1 commits from feat/portal-shell-theme-switcher-chip-shape into main 2026-05-16 02:52:53 +02:00
Owner

Summary

Aligns the theme-switcher trigger on the same chip shape as the locale-switcher. Both controls live side-by-side in the footer's device-prefs cluster (#162); the visual mismatch (round icon button vs. chip) made them read as two unrelated widgets rather than one family.

Before:  [☀]                       (round icon button, 44×44)
After:   [☀ System ▾]              (chip — icon + label + chevron)

The leading icon stays driven by currentIcon() so a sun / moon / monitor glyph still flips with the selected mode — that's the visual feedback the original icon-only design existed for, and it's preserved.

What lands

theme-switcher.html

The round Tailwind-utility icon button becomes a .theme-switcher__trigger chip with three children — current-mode icon (size 14), localised mode label, chevron-down (size 12). Same children layout as .locale-switcher__trigger.

theme-switcher.scss

Adds .theme-switcher__trigger mirroring .locale-switcher__trigger rule-for-rule: same chip metrics (min-height 2.75rem for the AAA 44×44 tap target via vertical-padding overflow inside the thin footer), same hover/focus tokens, same dark-mode swap.

Two copies rather than a shared _chip-trigger.scss partial — two switchers in one app is below the "three similar things" threshold CLAUDE.md sets for extraction. Promotes when a third switcher lands.

Notes for the reviewer

  • No spec changes. The existing assertions check button[aria-haspopup="menu"] + the aria-label content ("Theme: <current> (open menu)"). Both preserved — the trigger button still has aria-haspopup="menu" from cdkMenuTriggerFor, and triggerAriaLabel() is unchanged.
  • No new i18n strings. The chip text reuses currentLabel(), which already returns Light / Dark / System from the existing theme.mode.{light,dark,system} translation units (they were used in the dropdown menu items before this PR; now they also drive the trigger label). Prod i18n-strict build passes.
  • Why mirror, not refactor into a shared partial? Two switchers, identical chip shape — extracting now would be premature abstraction per CLAUDE.md. When a third switcher lands (an accessibility-panel toggle is the most likely candidate per ADR-0016's preferences-panel plan), _chip-trigger.scss or a <lib-chip-trigger> component starts to pay off. The two copies today are mechanical and live next door — easy to keep in sync.
  • No portal-admin impact. No theme switcher there (admin chrome is brand-primary-600 hardcoded for now); no change needed.

Test plan

  • pnpm nx run-many -t lint test build --projects=portal-shell — green; 43 specs pass (unchanged from main).
  • pnpm nx build portal-shell --configuration=production — i18n-strict prod build passes.
  • Manual visual smokepnpm nx serve portal-shell, open the home page in a browser, confirm:
    • The theme switcher in the bottom-right of the footer is now a chip with the current mode icon + label + chevron, matching the locale chip beside it.
    • Hovering both switchers shows the same brand-primary hover color (light + dark mode).
    • Clicking the theme chip still opens the menu with three options (Light / Dark / System), the current option still carries a check.
    • Selecting Dark → trigger icon flips to moon + label flips to "Dark" + page goes dark. Switching to System: trigger icon flips to monitor + label flips to "System".
    • Tabbing across the footer hits accessibility link → locale → theme in that order, focus rings are identical between the two switchers.
## Summary Aligns the theme-switcher trigger on the same chip shape as the locale-switcher. Both controls live side-by-side in the footer's device-prefs cluster (#162); the visual mismatch (round icon button vs. chip) made them read as two unrelated widgets rather than one family. ``` Before: [☀] (round icon button, 44×44) After: [☀ System ▾] (chip — icon + label + chevron) ``` The leading icon stays driven by `currentIcon()` so a **sun / moon / monitor** glyph still flips with the selected mode — that's the visual feedback the original icon-only design existed for, and it's preserved. ## What lands ### [`theme-switcher.html`](apps/portal-shell/src/app/components/theme-switcher/theme-switcher.html) The round Tailwind-utility icon button becomes a `.theme-switcher__trigger` chip with three children — current-mode icon (size 14), localised mode label, `chevron-down` (size 12). Same children layout as `.locale-switcher__trigger`. ### [`theme-switcher.scss`](apps/portal-shell/src/app/components/theme-switcher/theme-switcher.scss) Adds `.theme-switcher__trigger` mirroring `.locale-switcher__trigger` rule-for-rule: same chip metrics (min-height 2.75rem for the AAA 44×44 tap target via vertical-padding overflow inside the thin footer), same hover/focus tokens, same dark-mode swap. Two copies rather than a shared `_chip-trigger.scss` partial — two switchers in one app is below the "three similar things" threshold CLAUDE.md sets for extraction. Promotes when a third switcher lands. ## Notes for the reviewer - **No spec changes**. The existing assertions check `button[aria-haspopup="menu"]` + the aria-label content (`"Theme: <current> (open menu)"`). Both preserved — the trigger button still has `aria-haspopup="menu"` from `cdkMenuTriggerFor`, and `triggerAriaLabel()` is unchanged. - **No new i18n strings**. The chip text reuses `currentLabel()`, which already returns `Light` / `Dark` / `System` from the existing `theme.mode.{light,dark,system}` translation units (they were used in the dropdown menu items before this PR; now they also drive the trigger label). Prod i18n-strict build passes. - **Why mirror, not refactor into a shared partial?** Two switchers, identical chip shape — extracting now would be premature abstraction per [CLAUDE.md](CLAUDE.md). When a third switcher lands (an accessibility-panel toggle is the most likely candidate per [ADR-0016](docs/decisions/0016-accessibility-baseline-wcag-aa-targeted-aaa.md)'s preferences-panel plan), `_chip-trigger.scss` or a `<lib-chip-trigger>` component starts to pay off. The two copies today are mechanical and live next door — easy to keep in sync. - **No `portal-admin` impact.** No theme switcher there (admin chrome is brand-primary-600 hardcoded for now); no change needed. ## Test plan - [x] `pnpm nx run-many -t lint test build --projects=portal-shell` — green; **43 specs pass** (unchanged from main). - [x] `pnpm nx build portal-shell --configuration=production` — i18n-strict prod build passes. - [ ] **Manual visual smoke** — `pnpm nx serve portal-shell`, open the home page in a browser, confirm: - The theme switcher in the bottom-right of the footer is now a chip with the current mode icon + label + chevron, matching the locale chip beside it. - Hovering both switchers shows the same brand-primary hover color (light + dark mode). - Clicking the theme chip still opens the menu with three options (Light / Dark / System), the current option still carries a `✓` check. - Selecting Dark → trigger icon flips to moon + label flips to "Dark" + page goes dark. Switching to System: trigger icon flips to monitor + label flips to "System". - Tabbing across the footer hits accessibility link → locale → theme in that order, focus rings are identical between the two switchers.
julien added 1 commit 2026-05-16 02:40:43 +02:00
feat(portal-shell): align theme switcher trigger with locale switcher shape
CI / scan (pull_request) Successful in 3m5s
CI / commits (pull_request) Successful in 3m7s
CI / check (pull_request) Successful in 3m18s
CI / a11y (pull_request) Successful in 1m51s
CI / perf (pull_request) Successful in 3m24s
cd85037042
The theme switcher used to render as a round 44×44 icon button while
the locale switcher next to it (in the footer's device-prefs cluster
since #162) renders as a small chip: icon + label + chevron. Two
controls sitting side-by-side with two different visual shapes read
as two unrelated widgets instead of one family of preferences.

This PR aligns the theme trigger on the locale chip shape:

  Before:  [☀]                       (round icon button, 44×44)
  After:   [☀ System ▾]              (chip — icon + label + chevron)

The leading icon stays driven by `currentIcon()` so it still flips
between sun / moon / monitor as the user selects light / dark /
system — that visual feedback was the one reason the original icon-
only design existed and it's preserved.

Implementation:

* `theme-switcher.html` swaps the Tailwind utility-class icon button
  for a `.theme-switcher__trigger` chip with three children — the
  current-mode icon (size 14), the localised mode label, and a
  `chevron-down` (size 12). Same children layout as
  `.locale-switcher__trigger`.

* `theme-switcher.scss` adds `.theme-switcher__trigger` mirroring
  `.locale-switcher__trigger` rule-for-rule: same chip metrics
  (min-height 2.75rem for the AAA 44×44 tap target via vertical-
  padding overflow inside the thin footer), same hover/focus tokens,
  same dark-mode swap. Two copies rather than a shared `_chip-
  trigger.scss` partial — two switchers in one app is below the
  "three similar things" threshold from CLAUDE.md where extraction
  starts paying off. Promotes when a third switcher lands.

No spec changes: the existing assertions check
`button[aria-haspopup="menu"]` + the aria-label content, both of
which are preserved. `triggerAriaLabel()` is unchanged.

Verification:
  * 43 portal-shell specs pass.
  * `pnpm nx run portal-shell:build:production` (i18n-strict) green
    — confirms the `theme.mode.{light,dark,system}` labels are
    already translated (they were used in the menu items before;
    the trigger just reuses the same `currentLabel()` computed).
julien merged commit 10c80f189d into main 2026-05-16 02:52:53 +02:00
julien deleted branch feat/portal-shell-theme-switcher-chip-shape 2026-05-16 02:52:54 +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#164