From a8ddb0f63dff00cc2ac8d83073cc63914e7c61fc Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Sat, 16 May 2026 01:06:34 +0200 Subject: [PATCH] feat(portal-shell): move theme switcher to footer, drop redundant settings icon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three coupled chrome adjustments — all in `portal-shell`'s shell layout, all motivated by the same realisation that the user-menu introduced in #149 made the header's standalone Settings icon redundant and surfaced an inconsistency in the theme switcher's placement. * **Drop the standalone Settings button** from the header. The UserMenu already carries a "Settings (Soon)" row for authenticated users, and Settings has no meaning for anonymous traffic. The icon pointed nowhere; carrying it doubled the surface and forced readers to guess which control to use. The `header.action.settings` i18n unit is removed from `messages.fr.xlf` in the same step so the prod build's `i18nMissingTranslation=error` policy stays consistent. * **Move `` from the header to the footer.** It now sits beside `` in a single "device-prefs" cluster on the right side of the footer. The alternative considered — keeping it in the header for anonymous traffic and moving it into the user menu once authenticated — was rejected: the trigger location for an identical control (changing read conditions) must not depend on auth state, especially for an a11y-first platform per ADR-0016. A reader who relies on dark mode shouldn't have to expand a menu they don't recognise on first visit to escape a flashing white background. The footer matches what ADR-0019 already established for the locale switcher (ambient device preferences live there). * **Reshape the footer into two semantic clusters.** The Accessibility statement link moves from the right (preferences) cluster to the left, joining the copyright with a typographic separator (`·`). Left = static info / legal anchor ("who owns this, where to find legal info"); right = interactive prefs ("what I control about this page"). Keyboard order naturally follows: a Tab-traversal from main content hits info before interactive controls. Specs updated: - Header spec swaps "renders Settings button" / "embeds theme switcher" assertions for the absence guards that lock the new layout. - Footer spec adds a theme-switcher assertion and a cluster-grouping assertion that pins copyright + accessibility to the first direct-child div, locale + theme to the second. Verification: `pnpm nx run-many -t lint test build --projects=portal-shell` green, 43 portal-shell specs (was 40); the i18n-strict production build (`portal-shell:build:production`) passes too — confirms the xlf cleanup didn't strand any source-locale reference. --- .../src/app/components/footer/footer.html | 12 ++++++---- .../src/app/components/footer/footer.spec.ts | 20 ++++++++++++++++ .../src/app/components/footer/footer.ts | 24 ++++++++++++------- .../src/app/components/header/header.html | 10 -------- .../src/app/components/header/header.spec.ts | 14 +++++++---- .../src/app/components/header/header.ts | 14 +++++------ apps/portal-shell/src/locale/messages.fr.xlf | 4 ---- 7 files changed, 60 insertions(+), 38 deletions(-) diff --git a/apps/portal-shell/src/app/components/footer/footer.html b/apps/portal-shell/src/app/components/footer/footer.html index 233d27e..78fd94e 100644 --- a/apps/portal-shell/src/app/components/footer/footer.html +++ b/apps/portal-shell/src/app/components/footer/footer.html @@ -3,10 +3,9 @@ aria-label="Page footer" class="flex h-10 shrink-0 items-center justify-between border-t border-gray-200 bg-white px-4 text-xs text-gray-500 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-400 sm:px-6" > -

© {{ year }} APF France handicap

- -
- +
+

© {{ year }} APF France handicap

+
+ +
+ + +
diff --git a/apps/portal-shell/src/app/components/footer/footer.spec.ts b/apps/portal-shell/src/app/components/footer/footer.spec.ts index 768d06c..e8aa9dc 100644 --- a/apps/portal-shell/src/app/components/footer/footer.spec.ts +++ b/apps/portal-shell/src/app/components/footer/footer.spec.ts @@ -42,4 +42,24 @@ describe('Footer', () => { await fixture.whenStable(); expect(fixture.nativeElement.querySelector('app-locale-switcher')).not.toBeNull(); }); + + it('embeds the theme switcher (moved here from the header)', async () => { + const fixture = TestBed.createComponent(Footer); + await fixture.whenStable(); + expect(fixture.nativeElement.querySelector('app-theme-switcher')).not.toBeNull(); + }); + + it('groups copyright + accessibility on the left, switchers on the right', async () => { + const fixture = TestBed.createComponent(Footer); + await fixture.whenStable(); + const root = fixture.nativeElement as HTMLElement; + const clusters = Array.from(root.querySelectorAll('footer > div')); + expect(clusters).toHaveLength(2); + // Left cluster — info / legal. + expect(clusters[0]?.querySelector('p')?.textContent).toContain('APF France handicap'); + expect(clusters[0]?.querySelector('a[href="/accessibility"]')).not.toBeNull(); + // Right cluster — device preferences. + expect(clusters[1]?.querySelector('app-locale-switcher')).not.toBeNull(); + expect(clusters[1]?.querySelector('app-theme-switcher')).not.toBeNull(); + }); }); diff --git a/apps/portal-shell/src/app/components/footer/footer.ts b/apps/portal-shell/src/app/components/footer/footer.ts index ea2d6c6..0479089 100644 --- a/apps/portal-shell/src/app/components/footer/footer.ts +++ b/apps/portal-shell/src/app/components/footer/footer.ts @@ -1,24 +1,30 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; import { RouterLink } from '@angular/router'; import { LocaleSwitcher } from '../locale-switcher/locale-switcher'; +import { ThemeSwitcher } from '../theme-switcher/theme-switcher'; /** * Thin full-width footer pinned to the bottom of the shell. * - * v1 ships: - * - APF France handicap copyright with the current year. - * - Locale switcher (FR / EN) — per ADR-0019, hard-refreshes to - * the matching `/fr/...` or `/en/...` prefix. - * - Accessibility statement link — single i18n-marked link - * pointing to the canonical `/accessibility` path. + * Layout — two clusters separated by `justify-between`: + * - **Left cluster** — APF France handicap copyright + accessibility + * statement link, joined by a typographic separator. Static + * informational content that anchors the page (who owns it, where + * to find legal info). + * - **Right cluster** — device preferences the reader controls: + * locale switcher (per ADR-0019) and theme switcher (light / dark + * / system). The theme switcher is in the footer rather than the + * header so the same trigger location works for anonymous and + * authenticated readers; moving it between auth states would + * break consistency for a control whose purpose (changing read + * conditions) is identical across both. * - * The footer is reserved for cross-cutting page furniture. A - * dev-only version badge lands here once `SERVICE_VERSION` is wired + * A dev-only version badge lands here once `SERVICE_VERSION` is wired * to a build-time injection. */ @Component({ selector: 'app-footer', - imports: [RouterLink, LocaleSwitcher], + imports: [RouterLink, LocaleSwitcher, ThemeSwitcher], templateUrl: './footer.html', changeDetection: ChangeDetectionStrategy.OnPush, }) diff --git a/apps/portal-shell/src/app/components/header/header.html b/apps/portal-shell/src/app/components/header/header.html index 2c8631c..d8bc409 100644 --- a/apps/portal-shell/src/app/components/header/header.html +++ b/apps/portal-shell/src/app/components/header/header.html @@ -61,16 +61,6 @@ > - - - @switch (authState().kind) { @case ('loading') {