From da7fb002d57ae90487713f1eff27532d5160e82a Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sun, 21 Jun 2026 10:20:21 -0700 Subject: [PATCH] FN-6862: give engine controls popover an opaque surface Keep the footer engine controls popover opaque by using valid dashboard CSS tokens. - Replace the undefined elevated surface token with the solid card surface. - Align disabled action opacity, range accent, and mobile height CSS with defined tokens/values. - Add a CSS token regression test for the engine control menu popover. Files changed: .../dashboard/app/components/EngineControlMenu.css | 8 +- .../dashboard/app/components/EngineControlMenu.tsx | 3 + .../__tests__/EngineControlMenu.css.test.ts | 99 ++++++++++++++++++++++ 3 files changed, 106 insertions(+), 4 deletions(-) Fusion-Task-Id: FN-6862 Fusion-Task-Lineage: 5e41177a-6fc5-48d0-861d-fefbcfca7060 --- .../app/components/EngineControlMenu.css | 8 +- .../app/components/EngineControlMenu.tsx | 3 + .../__tests__/EngineControlMenu.css.test.ts | 99 +++++++++++++++++++ 3 files changed, 106 insertions(+), 4 deletions(-) create mode 100644 packages/dashboard/app/components/__tests__/EngineControlMenu.css.test.ts diff --git a/packages/dashboard/app/components/EngineControlMenu.css b/packages/dashboard/app/components/EngineControlMenu.css index 41be5c675f..efe0ee9e0f 100644 --- a/packages/dashboard/app/components/EngineControlMenu.css +++ b/packages/dashboard/app/components/EngineControlMenu.css @@ -24,7 +24,7 @@ display: flex; flex-direction: column; gap: var(--space-md); - background: var(--surface-elevated); + background: var(--card); border: 1px solid var(--border); border-radius: var(--radius-lg); box-shadow: var(--shadow-lg); @@ -48,7 +48,7 @@ } .engine-control-menu__action:disabled { - opacity: var(--opacity-disabled); + opacity: 0.5; cursor: not-allowed; } @@ -98,7 +98,7 @@ .engine-control-menu__range { width: 100%; - accent-color: var(--color-primary); + accent-color: var(--accent); } .engine-control-menu__error { @@ -113,6 +113,6 @@ right: var(--space-sm); bottom: calc(var(--mobile-nav-height) + max(env(safe-area-inset-bottom, 0px), var(--space-md)) + var(--space-2xl)); width: auto; - max-height: min(28rem, calc(100vh - var(--mobile-nav-height) - var(--space-3xl))); + max-height: min(28rem, calc(100vh - var(--mobile-nav-height) - var(--space-2xl) - var(--space-lg))); } } diff --git a/packages/dashboard/app/components/EngineControlMenu.tsx b/packages/dashboard/app/components/EngineControlMenu.tsx index 80683b0708..c3cec88255 100644 --- a/packages/dashboard/app/components/EngineControlMenu.tsx +++ b/packages/dashboard/app/components/EngineControlMenu.tsx @@ -55,6 +55,9 @@ function getErrorMessage(error: unknown, fallback: string) { /* FNXC:EngineControls 2026-06-21-00:00: Engine stop/start, triage pause/resume, and live scheduler concurrency/worktree sliders moved from the Header split button into the footer status bar. Operators open this popover from the footer trigger or running-status text, and the sliders reuse the existing /api/settings debounce flow so no backend route is added for live scheduler tuning. + +FNXC:EngineControls 2026-06-21-00:00: +FN-6862 requires the footer popover chrome to stay opaque across themes. Its CSS must use a defined solid surface token (`var(--card)`) because `--surface-elevated` is not in the dashboard token vocabulary and makes the menu transparent when unresolved. */ export const EngineControlMenu = forwardRef(function EngineControlMenu({ projectId }, ref) { const { t } = useTranslation("app"); diff --git a/packages/dashboard/app/components/__tests__/EngineControlMenu.css.test.ts b/packages/dashboard/app/components/__tests__/EngineControlMenu.css.test.ts new file mode 100644 index 0000000000..6f5e9f76c8 --- /dev/null +++ b/packages/dashboard/app/components/__tests__/EngineControlMenu.css.test.ts @@ -0,0 +1,99 @@ +/* +FNXC:EngineControls 2026-06-21-00:00: +FN-6862 guards the footer engine-control popover at raw CSS-text level because jsdom does not resolve undefined custom properties. The popover must keep an opaque dashboard surface (`var(--card)`) and this component stylesheet must not reference custom properties absent from the dashboard CSS vocabulary. +*/ +import { readdirSync, readFileSync, statSync } from "node:fs"; +import { join, relative, resolve } from "node:path"; +import { describe, expect, it } from "vitest"; + +const APP_DIR = resolve(__dirname, "..", ".."); +const COMPONENT_CSS = join(APP_DIR, "components", "EngineControlMenu.css"); +const STYLES_CSS = join(APP_DIR, "styles.css"); +const THEME_DATA_CSS = join(APP_DIR, "public", "theme-data.css"); + +function stripCssComments(source: string): string { + return source.replace(/\/\*[\s\S]*?\*\//g, ""); +} + +function collectCssFiles(dir: string): string[] { + const files: string[] = []; + for (const entry of readdirSync(dir)) { + if (entry === "node_modules" || entry === "dist" || entry.startsWith(".")) continue; + const fullPath = join(dir, entry); + const info = statSync(fullPath); + if (info.isDirectory()) { + files.push(...collectCssFiles(fullPath)); + } else if (info.isFile() && entry.endsWith(".css")) { + files.push(fullPath); + } + } + return files; +} + +function collectDefinedProperties(css: string, into: Set): void { + const uncommented = stripCssComments(css); + for (const match of uncommented.matchAll(/(^|[\s{;])(--[A-Za-z0-9_-]+)\s*:/g)) { + into.add(match[2]); + } +} + +function collectReferencedProperties(css: string): Map { + const refs = new Map(); + stripCssComments(css) + .split("\n") + .forEach((line, index) => { + for (const match of line.matchAll(/var\(\s*(--[A-Za-z0-9_-]+)/g)) { + const name = match[1]; + const lineNumbers = refs.get(name) ?? []; + lineNumbers.push(index + 1); + refs.set(name, lineNumbers); + } + }); + return refs; +} + +function extractRuleBlock(css: string, selector: string): string { + const ruleStart = css.indexOf(`${selector} {`); + expect(ruleStart, `Expected ${selector} to exist in EngineControlMenu.css`).toBeGreaterThanOrEqual(0); + const bodyStart = css.indexOf("{", ruleStart); + const bodyEnd = css.indexOf("\n}", bodyStart); + expect(bodyEnd, `Expected ${selector} rule to have a closing brace`).toBeGreaterThan(bodyStart); + return css.slice(bodyStart + 1, bodyEnd); +} + +describe("EngineControlMenu CSS token validity (FN-6862)", () => { + const componentCss = readFileSync(COMPONENT_CSS, "utf8"); + const stylesCss = readFileSync(STYLES_CSS, "utf8"); + const themeDataCss = readFileSync(THEME_DATA_CSS, "utf8"); + + const defined = new Set(); + collectDefinedProperties(stylesCss, defined); + collectDefinedProperties(themeDataCss, defined); + for (const cssFile of collectCssFiles(APP_DIR)) { + collectDefinedProperties(readFileSync(cssFile, "utf8"), defined); + } + + it("uses the defined solid card token for the footer popover background", () => { + expect(defined.has("--card"), "--card must be part of the dashboard token vocabulary").toBe(true); + expect(stylesCss, "styles.css should define --card for the default and light themes").toMatch(/--card\s*:/); + expect(themeDataCss, "theme-data.css should define --card for theme-generated palettes").toMatch(/--card\s*:/); + + const popoverBlock = extractRuleBlock(componentCss, ".engine-control-menu__popover"); + expect(popoverBlock).toMatch(/(^|\n)\s*background\s*:\s*var\(--card\)\s*;/); + }); + + it("does not reference the undefined elevated surface token", () => { + expect(componentCss).not.toContain("--surface-elevated"); + }); + + it("references only defined dashboard custom properties", () => { + const violations: string[] = []; + for (const [name, lineNumbers] of collectReferencedProperties(componentCss)) { + if (!defined.has(name)) { + violations.push(`${relative(APP_DIR, COMPONENT_CSS)}: var(${name}) at line(s) ${lineNumbers.join(", ")}`); + } + } + + expect(violations, `Undefined CSS custom properties referenced in EngineControlMenu.css:\n${violations.join("\n")}`).toEqual([]); + }); +});