diff --git a/apps/electron-backend-e2e/src/theme-tokens.e2e.ts b/apps/electron-backend-e2e/src/theme-tokens.e2e.ts index 9c18a3147..bcadf0f9d 100644 --- a/apps/electron-backend-e2e/src/theme-tokens.e2e.ts +++ b/apps/electron-backend-e2e/src/theme-tokens.e2e.ts @@ -46,6 +46,18 @@ async function outlineContrast( }, fillSelector); } +/** Resolves a CSS color (including var() and color-mix()) to rgb()/rgba(). */ +async function resolveColor(page: Page, value: string): Promise { + return page.evaluate((css) => { + const probe = document.createElement('div'); + probe.style.color = css; + document.body.appendChild(probe); + const color = getComputedStyle(probe).color; + probe.remove(); + return color; + }, value); +} + async function systemVariable(page: Page, name: string): Promise { return page.evaluate( (variable) => @@ -87,6 +99,30 @@ test.describe('Theme tokens', () => { '--mat-sys-surface' ); + // The dark surface belongs to the app host. Components that + // carry `dark-theme` for its tokens (the fullscreen channel + // panel, the diagnostic's alternative sources) keep their own. + const backgrounds = await page.evaluate(() => { + const nested = document.createElement('div'); + nested.className = 'dark-theme'; + nested.style.background = 'rgb(22, 27, 36)'; + document.body.appendChild(nested); + const result = { + nested: getComputedStyle(nested).backgroundColor, + body: getComputedStyle(document.body).backgroundColor, + }; + nested.remove(); + return result; + }); + expect(backgrounds.nested, `nested in ${theme} theme`).toBe( + 'rgb(22, 27, 36)' + ); + if (theme === 'dark') { + expect(backgrounds.body).toBe( + await resolveColor(page, 'var(--mat-sys-surface)') + ); + } + // Form-field overrides must reach Material's --mat-* tokens // (the retired --mdc-* names were ignored): the outline takes // the app's 10px shape instead of the 4px M3 default. diff --git a/apps/web/src/styles.scss b/apps/web/src/styles.scss index 03382e810..c0c09dafb 100644 --- a/apps/web/src/styles.scss +++ b/apps/web/src/styles.scss @@ -164,8 +164,14 @@ textarea, } // ─── Dark Theme ─────────────────────────────────────────────────────────────── -.dark-theme { +// Only the app host takes the dark surface: components such as the fullscreen +// channel panel and the playback diagnostic's alternative sources carry +// `dark-theme` for its tokens while painting their own surfaces. +body.dark-theme { background: var(--mat-sys-surface) !important; +} + +.dark-theme { color-scheme: dark; // Refined scrollbar for dark mode diff --git a/libs/ui/playback/src/lib/fullscreen-channel-panel/fullscreen-channel-panel.component.scss b/libs/ui/playback/src/lib/fullscreen-channel-panel/fullscreen-channel-panel.component.scss index 71803f4d1..6b75089d2 100644 --- a/libs/ui/playback/src/lib/fullscreen-channel-panel/fullscreen-channel-panel.component.scss +++ b/libs/ui/playback/src/lib/fullscreen-channel-panel/fullscreen-channel-panel.component.scss @@ -117,13 +117,10 @@ visibility 0s linear 220ms; } -// The controls' glass fill over the video. The panel carries `dark-theme` for its tokens, and the app's global -// `.dark-theme { background: var(--mat-sys-surface) !important }` -// (apps/web/src/styles.scss) would replace this gradient with the flat -// surface — or with no background at all where that token is not defined — -// so the compound selector and `!important` are required to out-rank it. +// The controls' glass fill over the video. The panel carries `dark-theme` for +// its tokens only; the app's dark surface is scoped to body.dark-theme. .fullscreen-channel-panel.dark-theme { - background: rgba(12, 16, 23, 0.72) !important; + background: rgba(12, 16, 23, 0.72); } .fullscreen-channel-panel--open {