fix(portal-shell): dark mode actually applies to component-scoped surfaces #87
@@ -3,6 +3,14 @@
|
|||||||
// owns its own scrolling (long menus don't push the header off-screen);
|
// owns its own scrolling (long menus don't push the header off-screen);
|
||||||
// the main pane scrolls independently so opening a long page never
|
// the main pane scrolls independently so opening a long page never
|
||||||
// scrolls the sidebar out of view.
|
// 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 {
|
:host {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -22,11 +30,11 @@
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background-color: #f9fafb;
|
background-color: #f9fafb;
|
||||||
color: #111827;
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
:where(.dark) & {
|
:host-context(.dark) .shell-main {
|
||||||
background-color: #0b1220;
|
background-color: #0b1220;
|
||||||
color: #e5e7eb;
|
color: #e5e7eb;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip-link (WCAG 2.4.1 "Bypass Blocks"). Hidden by default, fully
|
// Skip-link (WCAG 2.4.1 "Bypass Blocks"). Hidden by default, fully
|
||||||
|
|||||||
@@ -5,6 +5,10 @@
|
|||||||
// change, update both files together — extracting a shared SCSS
|
// change, update both files together — extracting a shared SCSS
|
||||||
// variable would be the right move only if a third surface needs the
|
// variable would be the right move only if a third surface needs the
|
||||||
// same coupling.
|
// same coupling.
|
||||||
|
//
|
||||||
|
// No vertical divider between the logo zone and the rest of the
|
||||||
|
// header on purpose — the alignment carries the visual relationship
|
||||||
|
// to the sidebar rail below; a second hairline would only add noise.
|
||||||
.header__logo-zone {
|
.header__logo-zone {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -12,13 +16,8 @@
|
|||||||
width: 16rem;
|
width: 16rem;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 0 0.75rem;
|
padding: 0 0.75rem;
|
||||||
border-right: 1px solid #e5e7eb;
|
|
||||||
transition: width 0.18s ease-out;
|
transition: width 0.18s ease-out;
|
||||||
|
|
||||||
:where(.dark) & {
|
|
||||||
border-right-color: #1f2937;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--collapsed {
|
&--collapsed {
|
||||||
width: 4rem;
|
width: 4rem;
|
||||||
padding: 0 0.5rem;
|
padding: 0 0.5rem;
|
||||||
|
|||||||
@@ -4,6 +4,12 @@
|
|||||||
// room on each side. The transition is short and animation-friendly —
|
// room on each side. The transition is short and animation-friendly —
|
||||||
// users with `prefers-reduced-motion: reduce` get the no-animation
|
// users with `prefers-reduced-motion: reduce` get the no-animation
|
||||||
// branch below.
|
// 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 {
|
.sidebar {
|
||||||
width: 16rem;
|
width: 16rem;
|
||||||
transition: width 0.18s ease-out;
|
transition: width 0.18s ease-out;
|
||||||
@@ -44,27 +50,14 @@
|
|||||||
background-color 0.12s ease-out,
|
background-color 0.12s ease-out,
|
||||||
color 0.12s ease-out;
|
color 0.12s ease-out;
|
||||||
|
|
||||||
:where(.dark) & {
|
|
||||||
color: #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #f3f4f6;
|
background-color: #f3f4f6;
|
||||||
color: var(--color-brand-primary-500);
|
color: var(--color-brand-primary-500);
|
||||||
|
|
||||||
:where(.dark) & {
|
|
||||||
background-color: #1f2937;
|
|
||||||
color: var(--color-brand-primary-300);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
outline: 2px solid var(--color-brand-primary-500);
|
outline: 2px solid var(--color-brand-primary-500);
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
|
|
||||||
:where(.dark) & {
|
|
||||||
outline-color: var(--color-brand-primary-300);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,15 +69,6 @@
|
|||||||
background-color: var(--color-brand-primary-600);
|
background-color: var(--color-brand-primary-600);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
:where(.dark) & {
|
|
||||||
background-color: var(--color-brand-primary-400);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--color-brand-primary-300);
|
|
||||||
color: #0b1620;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar__label {
|
.sidebar__label {
|
||||||
@@ -101,3 +85,30 @@
|
|||||||
padding-right: 0.5rem;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import { CdkMenu, CdkMenuItem, CdkMenuTrigger } from '@angular/cdk/menu';
|
import { CdkMenu, CdkMenuItem, CdkMenuTrigger } from '@angular/cdk/menu';
|
||||||
import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core';
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
ViewEncapsulation,
|
||||||
|
computed,
|
||||||
|
inject,
|
||||||
|
} from '@angular/core';
|
||||||
import { Icon, type IconName } from '../icon/icon';
|
import { Icon, type IconName } from '../icon/icon';
|
||||||
import { LayoutStateService, type ThemeMode } from '../../state/layout-state.service';
|
import { LayoutStateService, type ThemeMode } from '../../state/layout-state.service';
|
||||||
|
|
||||||
@@ -36,6 +42,14 @@ const MODE_OPTIONS: readonly ModeOption[] = [
|
|||||||
templateUrl: './theme-switcher.html',
|
templateUrl: './theme-switcher.html',
|
||||||
styleUrl: './theme-switcher.scss',
|
styleUrl: './theme-switcher.scss',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
// The CDK menu opens in an overlay portal appended to <body>, so it
|
||||||
|
// is NOT a descendant of this component's host. With the default
|
||||||
|
// emulated encapsulation, the menu DOM would lose the scoping
|
||||||
|
// attribute and our `.theme-switcher__*` rules would not apply.
|
||||||
|
// Disabling encapsulation emits the styles into the global sheet —
|
||||||
|
// the BEM-style class names below are specific enough to avoid
|
||||||
|
// collisions, and `.dark` ancestor matching just works.
|
||||||
|
encapsulation: ViewEncapsulation.None,
|
||||||
})
|
})
|
||||||
export class ThemeSwitcher {
|
export class ThemeSwitcher {
|
||||||
private readonly layout = inject(LayoutStateService);
|
private readonly layout = inject(LayoutStateService);
|
||||||
|
|||||||
Reference in New Issue
Block a user