From cd8503704250953539ca3f3198cf77728aacc902 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Sat, 16 May 2026 02:31:30 +0200 Subject: [PATCH] feat(portal-shell): align theme switcher trigger with locale switcher shape MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- .../theme-switcher/theme-switcher.html | 6 ++- .../theme-switcher/theme-switcher.scss | 41 +++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/apps/portal-shell/src/app/components/theme-switcher/theme-switcher.html b/apps/portal-shell/src/app/components/theme-switcher/theme-switcher.html index c244318..23928cd 100644 --- a/apps/portal-shell/src/app/components/theme-switcher/theme-switcher.html +++ b/apps/portal-shell/src/app/components/theme-switcher/theme-switcher.html @@ -1,10 +1,12 @@ diff --git a/apps/portal-shell/src/app/components/theme-switcher/theme-switcher.scss b/apps/portal-shell/src/app/components/theme-switcher/theme-switcher.scss index 5e7faf2..ac6115b 100644 --- a/apps/portal-shell/src/app/components/theme-switcher/theme-switcher.scss +++ b/apps/portal-shell/src/app/components/theme-switcher/theme-switcher.scss @@ -3,6 +3,47 @@ // when the labels grow under translation. dark-mode surface mirrors // the rest of the shell (gray-900 background, gray-800 border). +// Trigger mirrors `.locale-switcher__trigger` — same chip shape, +// same hover / focus tokens, same AAA-friendly 44×44 tap target +// via vertical-padding overflow inside the thin footer. The two +// switchers sit side-by-side in the footer's device-prefs cluster +// (#162), so a single visual family across the pair reads as one +// component. The leading icon stays driven by `currentIcon()` so a +// sun / moon / monitor glyph still signals which mode is selected. +.theme-switcher__trigger { + display: inline-flex; + align-items: center; + gap: 0.375rem; + min-height: 2.75rem; + margin: -0.5rem 0; + padding: 0 0.5rem; + border-radius: 0.25rem; + background: transparent; + border: 0; + color: inherit; + font-size: inherit; + font-weight: 500; + cursor: pointer; + transition: + background-color 0.12s ease-out, + color 0.12s ease-out; + + &:hover { + background-color: #f3f4f6; + color: var(--color-brand-primary-500); + + :where(.dark) & { + background-color: #1f2937; + color: var(--color-brand-primary-300); + } + } + + &:focus-visible { + outline: 2px solid var(--color-brand-primary-500); + outline-offset: 2px; + } +} + .theme-switcher__menu { min-width: 11rem; margin-top: 0.25rem;