fix(portal-shell): dark mode actually applies to component-scoped surfaces (#87)
## 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
This commit was merged in pull request #87.
This commit is contained in:
@@ -4,6 +4,12 @@
|
||||
// room on each side. The transition is short and animation-friendly —
|
||||
// users with `prefers-reduced-motion: reduce` get the no-animation
|
||||
// branch below.
|
||||
//
|
||||
// Dark mode rules sit OUTSIDE the nested blocks and key off
|
||||
// `:host-context(.dark)` — `.dark` lives on <html>, outside this
|
||||
// component's encapsulation, and a nested `:where(.dark) &` would be
|
||||
// rewritten by Angular's emulated CSS scoping into a selector that
|
||||
// no longer matches <html>.
|
||||
.sidebar {
|
||||
width: 16rem;
|
||||
transition: width 0.18s ease-out;
|
||||
@@ -44,27 +50,14 @@
|
||||
background-color 0.12s ease-out,
|
||||
color 0.12s ease-out;
|
||||
|
||||
:where(.dark) & {
|
||||
color: #e5e7eb;
|
||||
}
|
||||
|
||||
&: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;
|
||||
|
||||
:where(.dark) & {
|
||||
outline-color: var(--color-brand-primary-300);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,15 +69,6 @@
|
||||
background-color: var(--color-brand-primary-600);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
:where(.dark) & {
|
||||
background-color: var(--color-brand-primary-400);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-brand-primary-300);
|
||||
color: #0b1620;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar__label {
|
||||
@@ -101,3 +85,30 @@
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Dark mode overrides --------------------------------------------------
|
||||
|
||||
:host-context(.dark) {
|
||||
.sidebar__link,
|
||||
.sidebar__toggle {
|
||||
color: #e5e7eb;
|
||||
|
||||
&:hover {
|
||||
background-color: #1f2937;
|
||||
color: var(--color-brand-primary-300);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline-color: var(--color-brand-primary-300);
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar__link--active {
|
||||
background-color: var(--color-brand-primary-400);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-brand-primary-300);
|
||||
color: #0b1620;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user