3a0a9c700d
## Bug
Three component stylesheets (`app.scss`, `sidebar.scss`, `theme-switcher.scss`) carried `:where(.dark) &` rules to react to the `<html>.dark` class — the same pattern Tailwind uses internally. **Angular's emulated CSS encapsulation descends into `:where()` and rewrites its contents with the `_ngcontent-XXX` scoping attribute**, so the produced selector looked like:
```css
:where(.dark[_ngcontent-c0]) .shell-main[_ngcontent-c0] { ... }
```
`<html>` carries `.dark` but no `_ngcontent` attribute — the rule never matched.
Visible symptom: the main page background stayed light even when `.dark` was on `<html>` and every Tailwind `dark:` utility in the templates was working correctly. Sidebar hover/focus/active states and the theme-switcher menu surface had the same bug.
## Fix
- **`app.scss` and `sidebar.scss`** switch to `:host-context(.dark)`. The Angular compiler recognises this directive and expands it without forcing the ancestor (`<html>`) to carry the scoping attribute. Compiled output:
```css
.dark[_nghost-c0] .shell-main[_ngcontent-c0],
.dark [_nghost-c0] .shell-main[_ngcontent-c0] { ... }
```
The second selector matches `<html>.dark` as an ancestor of `<app-root>` (the host) — exactly what we want.
- **`theme-switcher` switches to `ViewEncapsulation.None`**. Its CDK menu opens in an overlay portal appended to `<body>` — outside the component's host subtree — so even `:host-context()` would miss it. With encapsulation disabled, the styles emit globally; the BEM-style class names (`.theme-switcher__menu`, `.theme-switcher__item`, ...) keep the rules contained without leaking.
## Drive-by
- Remove the right border on the `.header__logo-zone`. The visible hairline above the sidebar was extra noise — the alignment of widths already carries the visual relationship to the rail below.
## Why not also rip out the `dark:` Tailwind utilities used in the templates?
They work. They're emitted into the global Tailwind sheet, sit outside view encapsulation, and already handle the `.dark` ancestor lookup via their own selector (`:where(.dark, .dark *)`). The bug was specifically about component-authored CSS *inside* an SCSS file under emulated encapsulation.
## Test plan
- [x] `pnpm exec nx run-many -t lint test build --projects=portal-shell` — green (33 / 33 specs).
- [x] Inspect the produced CSS: `:host-context(.dark)` expands to both `.dark[_nghost-XXX]` and `.dark [_nghost-XXX]` selectors, no `_ngcontent` attribute leaked into the `.dark` portion.
- [ ] Manual: pick dark mode → `/` shows the dark page background; sidebar hover / active / focus visibly switches; theme-switcher menu opens with the dark surface.
- [ ] Manual: header no longer shows a vertical hairline between the logo zone and the search/actions cluster.
---------
Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #87
63 lines
1.8 KiB
SCSS
63 lines
1.8 KiB
SCSS
// App shell: fixed-height header on top, sidebar + main beside each
|
|
// other below, both filling the remaining viewport height. The sidebar
|
|
// owns its own scrolling (long menus don't push the header off-screen);
|
|
// the main pane scrolls independently so opening a long page never
|
|
// scrolls the sidebar out of view.
|
|
//
|
|
// Dark mode goes through `:host-context(.dark)` — the `.dark` class
|
|
// sits on <html>, outside this component's view encapsulation, so a
|
|
// nested `:where(.dark) &` would compile to a selector with the
|
|
// `_ngcontent` attribute appended to `.dark` itself, which <html>
|
|
// doesn't carry. `:host-context()` is the Angular-canonical escape
|
|
// hatch that walks the DOM up from the host and matches without
|
|
// applying the scoping attribute to the ancestor selector.
|
|
:host {
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-height: 100vh;
|
|
height: 100vh;
|
|
}
|
|
|
|
.shell-body {
|
|
display: flex;
|
|
flex: 1 1 auto;
|
|
min-height: 0;
|
|
}
|
|
|
|
.shell-main {
|
|
flex: 1 1 auto;
|
|
min-width: 0;
|
|
overflow-y: auto;
|
|
background-color: #f9fafb;
|
|
color: #111827;
|
|
}
|
|
|
|
:host-context(.dark) .shell-main {
|
|
background-color: #0b1220;
|
|
color: #e5e7eb;
|
|
}
|
|
|
|
// Skip-link (WCAG 2.4.1 "Bypass Blocks"). Hidden by default, fully
|
|
// visible and focusable when reached via Tab from the address bar.
|
|
// Plain CSS rather than the `sr-only` Tailwind utilities so the
|
|
// visual state on focus is as predictable as possible across themes.
|
|
.skip-link {
|
|
position: absolute;
|
|
top: -40px;
|
|
left: 0.75rem;
|
|
z-index: 50;
|
|
padding: 0.5rem 0.75rem;
|
|
background: var(--color-brand-primary-500);
|
|
color: #fff;
|
|
border-radius: 0.25rem;
|
|
text-decoration: none;
|
|
transition: top 0.15s ease-out;
|
|
|
|
&:focus,
|
|
&:focus-visible {
|
|
top: 0.5rem;
|
|
outline: 2px solid var(--color-brand-accent-300);
|
|
outline-offset: 2px;
|
|
}
|
|
}
|