feat(portal-shell): align header logo zone with the sidebar rail #85

Merged
julien merged 1 commits from feat/portal-shell/header-logo-aligned-to-sidebar into main 2026-05-11 01:14:43 +02:00
Owner

Summary

  • Split the header into two zones: a logo zone whose width tracks the sidebar (16 rem expanded / 4 rem collapsed), and the existing search + actions cluster on the right.
  • The logo glyph stays at both widths; the "APF Portal" wordmark hides when the rail collapses, mirroring the sidebar's icon-only mode.
  • Promote the sidebar's collapsed state to a new LayoutStateService (Signals + localStorage) so header and sidebar share a single source of truth.

Why a service rather than prop-drilling

Two reasons:

  1. The state is shell-level, not sidebar-internal — the header now reads it, and future surfaces (right rail, breadcrumbs, command palette) will too. Passing it down via inputs would force <app-root> to act as the owner and turn every intermediate component into a relay.
  2. As we add other cross-cutting shell state (density, theme, panel pinning, RTL), they all belong in the same place. LayoutStateService is the natural collector and stays trivial as long as we don't over-broaden it. v1 ships with one signal — keeping it narrow.

The service is providedIn: 'root' (singleton), Signals-only, and owns localStorage persistence — same UX as before, just relocated.

Decisions worth flagging

  • Widths stay duplicated between header.scss and sidebar.scss (16 rem / 4 rem). Extracting a shared SCSS variable would be premature — only two callers, and the coupling is loud (cross-file comment in header.scss). If a third surface needs the same widths, we promote to a shared token.
  • Border continuity. The logo zone's right border and the sidebar's right border share the same x coordinate, so they read as one continuous vertical separator running the full shell height. Same #e5e7eb so the seam is invisible.
  • Width transition matches the sidebar's (0.18s ease-out) and is skipped under prefers-reduced-motion: reduce.
  • Sidebar component lost its private state. Its own signal + effect + storage glue is gone; it delegates reads to the service and the toggle click to layout.toggleSidebar(). Net sidebar.ts shrunk by ~15 lines.

Test plan

  • pnpm exec nx run-many -t lint test build --projects=portal-shell — green (25 / 25 specs, +6 for the new service spec + header zone tests).
  • Production build under bundle budgets (100 kB gzip initial — +0.2 kB vs main).
  • Manual: load /, confirm the logo zone's right edge aligns exactly with the sidebar's right edge at both widths.
  • Manual: toggle the sidebar → both columns animate in sync; wordmark hides; logo glyph stays centered in the 4 rem zone.
  • Manual: reload after toggling → both header and sidebar restore to the persisted state.
  • Manual: prefers-reduced-motion: reduce → no width animation in either zone.
## Summary - Split the header into two zones: a **logo zone** whose width tracks the sidebar (16 rem expanded / 4 rem collapsed), and the existing search + actions cluster on the right. - The logo glyph stays at both widths; the "APF Portal" wordmark hides when the rail collapses, mirroring the sidebar's icon-only mode. - Promote the sidebar's `collapsed` state to a new [`LayoutStateService`](apps/portal-shell/src/app/state/layout-state.service.ts) (Signals + `localStorage`) so header and sidebar share a single source of truth. ## Why a service rather than prop-drilling Two reasons: 1. The state is **shell-level**, not sidebar-internal — the header now reads it, and future surfaces (right rail, breadcrumbs, command palette) will too. Passing it down via inputs would force `<app-root>` to act as the owner and turn every intermediate component into a relay. 2. As we add other cross-cutting shell state (density, theme, panel pinning, RTL), they all belong in the same place. `LayoutStateService` is the natural collector and stays trivial as long as we don't over-broaden it. v1 ships with one signal — keeping it narrow. The service is `providedIn: 'root'` (singleton), Signals-only, and owns localStorage persistence — same UX as before, just relocated. ## Decisions worth flagging - **Widths stay duplicated between `header.scss` and `sidebar.scss`** (16 rem / 4 rem). Extracting a shared SCSS variable would be premature — only two callers, and the coupling is loud (cross-file comment in `header.scss`). If a third surface needs the same widths, we promote to a shared token. - **Border continuity.** The logo zone's right border and the sidebar's right border share the same x coordinate, so they read as one continuous vertical separator running the full shell height. Same `#e5e7eb` so the seam is invisible. - **Width transition** matches the sidebar's (`0.18s ease-out`) and is skipped under `prefers-reduced-motion: reduce`. - **Sidebar component lost its private state.** Its own signal + effect + storage glue is gone; it delegates reads to the service and the toggle click to `layout.toggleSidebar()`. Net `sidebar.ts` shrunk by ~15 lines. ## Test plan - [x] `pnpm exec nx run-many -t lint test build --projects=portal-shell` — green (25 / 25 specs, +6 for the new service spec + header zone tests). - [x] Production build under bundle budgets (100 kB gzip initial — +0.2 kB vs main). - [ ] Manual: load `/`, confirm the logo zone's right edge aligns exactly with the sidebar's right edge at both widths. - [ ] Manual: toggle the sidebar → both columns animate in sync; wordmark hides; logo glyph stays centered in the 4 rem zone. - [ ] Manual: reload after toggling → both header and sidebar restore to the persisted state. - [ ] Manual: `prefers-reduced-motion: reduce` → no width animation in either zone.
julien added 1 commit 2026-05-11 01:13:59 +02:00
feat(portal-shell): align header logo zone with the sidebar rail
CI / commits (pull_request) Successful in 1m54s
CI / scan (pull_request) Successful in 2m3s
CI / check (pull_request) Successful in 2m10s
CI / a11y (pull_request) Successful in 2m31s
CI / perf (pull_request) Successful in 4m43s
5795e3c19a
Promote the sidebar's collapsed state to a shell-level
`LayoutStateService` (signal + localStorage persistence) so the header
can read it. Split the header into two zones: a logo zone whose width
matches the sidebar (16rem expanded / 4rem collapsed) and the
existing search + actions cluster on the right. The logo glyph stays
visible at both widths; the "APF Portal" wordmark hides when the
rail is collapsed.

The logo-zone right border and the sidebar right border share the
same x coordinate, so they read as one continuous column separator
running the full height of the shell. The width transition matches
the sidebar's (0.18s ease-out) and is skipped under
`prefers-reduced-motion: reduce`.

Side-effect: cleans up `<app-sidebar>` — its own signal + effect +
storage glue is gone; it now just delegates reads and the toggle
click to the service. Same UX, single source of truth, ready to
absorb future shared shell state (density, theme, ...) without
prop-drilling.
julien merged commit b6aa17f6a0 into main 2026-05-11 01:14:43 +02:00
julien deleted branch feat/portal-shell/header-logo-aligned-to-sidebar 2026-05-11 01:14:44 +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#85