fix(ui): scope the dark app surface to body.dark-theme

The global `.dark-theme { background: var(--mat-sys-surface) !important }`
rule was inert while --mat-sys-surface did not exist. Now that it resolves,
it also painted every component that carries `dark-theme` only for its
tokens: the playback diagnostic's alternative sources lost their #161b24
surface to #121316, in the light app theme too. Move the background to
body.dark-theme, the element SettingsService toggles, and drop the fullscreen
channel panel's !important override that only existed to outrank the old
rule.

theme-tokens.e2e.ts checks in both themes that a nested `dark-theme`
element keeps its own background and that the dark body takes the surface.
Without this change, the nested element reads rgb(18, 19, 22).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 5.5 committed 2026-10-01 08:02:10 +02:00
1 parent 14fd2897e2
commit d08a9f90d0
3 files changed
+46 -7

No files matched your search

@@ -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<string> {
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<string> {
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.
+7 -1
View File
@@ -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
@@ -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 {