FN-6130: make compound engineering theme colors dark-mode aware
Align compound engineering dashboard colors with the active theme tokens. - replace hardcoded and legacy compound engineering CSS colors with dashboard theme tokens - switch dimmed text treatments from opacity to explicit theme-aware text tokens for light and dark mode - add regression coverage for token usage in compound engineering styles - keep branch authority checks compatible with the optional base SHA argument Files changed: packages/engine/src/branch-conflicts.ts | 1 + .../src/dashboard/CompoundEngineeringView.css | 106 +++++++++++---------- .../src/dashboard/__tests__/theme-tokens.test.ts | 97 +++++++++++++++++++ 3 files changed, 153 insertions(+), 51 deletions(-) Fusion-Task-Id: FN-6130 Fusion-Task-Lineage: 00adc217-0649-4b4f-9b9b-92c551f02926
This commit is contained in:
@@ -377,6 +377,7 @@ export async function isBranchAuthoritativeForTask(
|
||||
repoDir: string,
|
||||
branch: string,
|
||||
taskId: string,
|
||||
_baseSha?: string,
|
||||
): Promise<{ ok: true } | { ok: false; reason: string }> {
|
||||
try {
|
||||
await revParse(repoDir, `refs/heads/${branch}`);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
.ce-view {
|
||||
display: flex;
|
||||
color: var(--text);
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
@@ -25,7 +26,7 @@
|
||||
|
||||
.ce-view-summary {
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.7;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.ce-loading,
|
||||
@@ -35,7 +36,7 @@
|
||||
}
|
||||
|
||||
.ce-view-error {
|
||||
color: var(--color-danger, #d23);
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
.ce-empty {
|
||||
@@ -52,7 +53,7 @@
|
||||
}
|
||||
|
||||
.ce-empty-hint {
|
||||
opacity: 0.7;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
@@ -63,7 +64,7 @@
|
||||
}
|
||||
|
||||
.ce-group {
|
||||
border: 1px solid var(--color-border, rgba(128, 128, 128, 0.25));
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem 1rem;
|
||||
display: flex;
|
||||
@@ -72,7 +73,7 @@
|
||||
}
|
||||
|
||||
.ce-group[data-empty="true"] {
|
||||
opacity: 0.65;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.ce-group-header {
|
||||
@@ -88,13 +89,13 @@
|
||||
|
||||
.ce-group-count {
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.6;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.ce-group-empty {
|
||||
margin: 0;
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.6;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.ce-artifact-list {
|
||||
@@ -132,14 +133,14 @@
|
||||
|
||||
.ce-artifact-path {
|
||||
font-size: 0.7rem;
|
||||
opacity: 0.55;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.ce-artifact-open {
|
||||
appearance: none;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--color-primary, #2563eb);
|
||||
color: var(--color-info);
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
padding: 0;
|
||||
@@ -148,11 +149,11 @@
|
||||
|
||||
.ce-artifact-open:hover,
|
||||
.ce-artifact-open:focus-visible {
|
||||
color: var(--color-primary-hover, #1d4ed8);
|
||||
color: var(--color-info);
|
||||
}
|
||||
|
||||
.ce-artifact-error .ce-artifact-error-msg {
|
||||
color: var(--color-danger, #d23);
|
||||
color: var(--color-error);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
@@ -224,7 +225,7 @@
|
||||
}
|
||||
.ce-flow-status {
|
||||
font-size: 0.72rem;
|
||||
opacity: 0.7;
|
||||
color: var(--text-muted);
|
||||
text-transform: capitalize;
|
||||
}
|
||||
.ce-flow-close {
|
||||
@@ -248,14 +249,14 @@
|
||||
.ce-flow-turn-role {
|
||||
font-size: 0.65rem;
|
||||
text-transform: uppercase;
|
||||
opacity: 0.55;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
.ce-flow-turn-agent .ce-flow-turn-text {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.ce-flow-thinking {
|
||||
font-style: italic;
|
||||
opacity: 0.7;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.ce-flow-question {
|
||||
display: flex;
|
||||
@@ -268,7 +269,7 @@
|
||||
}
|
||||
.ce-flow-question-desc {
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.75;
|
||||
color: var(--text-muted);
|
||||
margin: 0;
|
||||
}
|
||||
.ce-flow-text {
|
||||
@@ -308,7 +309,7 @@
|
||||
}
|
||||
.ce-flow-option-desc {
|
||||
font-size: 0.72rem;
|
||||
opacity: 0.7;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.ce-flow-checkbox {
|
||||
display: flex;
|
||||
@@ -317,26 +318,26 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
.ce-flow-error {
|
||||
color: var(--color-danger, #d23);
|
||||
color: var(--color-error);
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
/* Degraded chat fallback (R8/AE1) — must read as visibly distinct. */
|
||||
.ce-flow-degraded {
|
||||
border: 1px dashed var(--color-warning, #c80);
|
||||
border: 1px dashed var(--color-warning);
|
||||
border-radius: 6px;
|
||||
padding: 0.6rem;
|
||||
background: color-mix(in srgb, var(--color-warning, #c80) 8%, transparent);
|
||||
background: color-mix(in srgb, var(--color-warning) 8%, transparent);
|
||||
}
|
||||
.ce-flow-degraded-banner {
|
||||
margin: 0 0 0.4rem;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-warning, #a60);
|
||||
color: var(--color-warning);
|
||||
}
|
||||
.ce-flow-degraded-options {
|
||||
font-size: 0.78rem;
|
||||
opacity: 0.8;
|
||||
color: var(--text-muted);
|
||||
margin: 0 0 0.4rem;
|
||||
padding-left: 1.1rem;
|
||||
}
|
||||
@@ -359,8 +360,8 @@
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.ce-session-row.is-active .ce-session-open {
|
||||
border-color: var(--color-accent, #36c);
|
||||
background: color-mix(in srgb, var(--color-accent, #36c) 8%, transparent);
|
||||
border-color: var(--todo);
|
||||
background: color-mix(in srgb, var(--todo) 8%, transparent);
|
||||
}
|
||||
.ce-session-open {
|
||||
flex: 1;
|
||||
@@ -369,7 +370,7 @@
|
||||
gap: 0.6rem;
|
||||
text-align: left;
|
||||
padding: 0.4rem 0.6rem;
|
||||
border: 1px solid var(--color-border, #ddd);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
@@ -384,26 +385,26 @@
|
||||
.ce-session-status {
|
||||
font-size: 0.74rem;
|
||||
text-transform: capitalize;
|
||||
opacity: 0.8;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.ce-session-status-awaiting_input {
|
||||
color: var(--color-warning, #a60);
|
||||
color: var(--color-warning);
|
||||
font-weight: 600;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.ce-session-status-error,
|
||||
.ce-session-status-interrupted {
|
||||
color: var(--color-danger, #d23);
|
||||
opacity: 1;
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
.ce-session-status-completed {
|
||||
color: var(--color-success, #2a7);
|
||||
opacity: 1;
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.ce-session-updated {
|
||||
margin-left: auto;
|
||||
font-size: 0.72rem;
|
||||
opacity: 0.6;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
/* ── Q&A transcript bubbles ────────────────────────────────────────────── */
|
||||
@@ -425,18 +426,18 @@
|
||||
max-width: 85%;
|
||||
padding: 0.45rem 0.65rem;
|
||||
border-radius: 10px;
|
||||
background: color-mix(in srgb, var(--color-border, #ddd) 30%, transparent);
|
||||
background: color-mix(in srgb, var(--border) 30%, transparent);
|
||||
}
|
||||
.ce-flow-turn-user {
|
||||
align-self: flex-end;
|
||||
background: color-mix(in srgb, var(--color-accent, #36c) 12%, transparent);
|
||||
background: color-mix(in srgb, var(--todo) 12%, transparent);
|
||||
}
|
||||
.ce-flow-turn-role {
|
||||
font-size: 0.68rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
opacity: 0.55;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
.ce-flow-turn-text {
|
||||
white-space: pre-wrap;
|
||||
@@ -444,23 +445,26 @@
|
||||
font-size: 0.86rem;
|
||||
}
|
||||
.ce-flow-turn-question {
|
||||
border-left: 3px solid var(--color-accent, #36c);
|
||||
border-left: 3px solid var(--todo);
|
||||
}
|
||||
.ce-flow-turn-answer.is-steering {
|
||||
border-left: 3px solid var(--color-warning, #c80);
|
||||
border-left: 3px solid var(--color-warning);
|
||||
}
|
||||
.ce-flow-turn-comment {
|
||||
font-size: 0.78rem;
|
||||
font-style: italic;
|
||||
opacity: 0.85;
|
||||
border-top: 1px dashed color-mix(in srgb, var(--color-border, #ddd) 60%, transparent);
|
||||
color: var(--text-muted);
|
||||
border-top: 1px dashed color-mix(in srgb, var(--border) 60%, transparent);
|
||||
padding-top: 0.25rem;
|
||||
}
|
||||
.ce-flow-turn-done {
|
||||
align-self: center;
|
||||
background: color-mix(in srgb, var(--color-success, #2a7) 10%, transparent);
|
||||
background: color-mix(in srgb, var(--color-success) 10%, transparent);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.ce-flow-artifact-path {
|
||||
color: var(--text-dim);
|
||||
}
|
||||
.ce-flow-turn-activity {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
@@ -471,7 +475,7 @@
|
||||
.ce-flow-activity-details summary {
|
||||
cursor: pointer;
|
||||
font-size: 0.78rem;
|
||||
opacity: 0.7;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.ce-flow-activity {
|
||||
display: flex;
|
||||
@@ -479,9 +483,9 @@
|
||||
gap: 0.25rem;
|
||||
margin: 0.3rem 0 0;
|
||||
padding: 0.5rem 0.6rem;
|
||||
border: 1px solid color-mix(in srgb, var(--color-border, #ddd) 70%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
|
||||
border-radius: 8px;
|
||||
background: color-mix(in srgb, var(--color-border, #ddd) 12%, transparent);
|
||||
background: color-mix(in srgb, var(--border) 12%, transparent);
|
||||
max-height: 16rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
@@ -493,7 +497,7 @@
|
||||
font-family: var(--font-mono, ui-monospace, monospace);
|
||||
}
|
||||
.ce-activity-thinking {
|
||||
opacity: 0.6;
|
||||
color: var(--text-dim);
|
||||
font-style: italic;
|
||||
}
|
||||
.ce-activity-tool {
|
||||
@@ -501,13 +505,13 @@
|
||||
font-family: var(--font-mono, ui-monospace, monospace);
|
||||
}
|
||||
.ce-activity-tool.is-running .ce-activity-tool-marker {
|
||||
color: var(--color-accent, #36c);
|
||||
color: var(--todo);
|
||||
}
|
||||
.ce-activity-tool.is-done .ce-activity-tool-marker {
|
||||
color: var(--color-success, #2a7);
|
||||
color: var(--color-success);
|
||||
}
|
||||
.ce-activity-tool.is-error .ce-activity-tool-marker {
|
||||
color: var(--color-danger, #d23);
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
/* ── Live working pane ──────────────────────────────────────────────────── */
|
||||
@@ -520,13 +524,13 @@
|
||||
gap: 0.45rem;
|
||||
margin: 0 0 0.3rem;
|
||||
font-size: 0.82rem;
|
||||
opacity: 0.85;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.ce-flow-pulse {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-accent, #36c);
|
||||
background: var(--todo);
|
||||
animation: ce-pulse 1.2s ease-in-out infinite;
|
||||
}
|
||||
@keyframes ce-pulse {
|
||||
@@ -538,12 +542,12 @@
|
||||
.ce-flow-guidance {
|
||||
margin-top: 0.5rem;
|
||||
padding-top: 0.5rem;
|
||||
border-top: 1px dashed color-mix(in srgb, var(--color-border, #ddd) 70%, transparent);
|
||||
border-top: 1px dashed color-mix(in srgb, var(--border) 70%, transparent);
|
||||
}
|
||||
.ce-flow-guidance-label {
|
||||
display: block;
|
||||
font-size: 0.74rem;
|
||||
opacity: 0.65;
|
||||
color: var(--text-dim);
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
.ce-flow-guidance-row {
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const cssPath = path.resolve(__dirname, "../CompoundEngineeringView.css");
|
||||
const css = fs.readFileSync(cssPath, "utf-8");
|
||||
|
||||
function selectorBlocks(selector: string): string[] {
|
||||
const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const pattern = new RegExp(`${escaped}\\s*\\{[^}]*\\}`, "g");
|
||||
return css.match(pattern) ?? [];
|
||||
}
|
||||
|
||||
describe("CompoundEngineeringView theme tokens", () => {
|
||||
it("does not use hardcoded legacy color fallbacks", () => {
|
||||
const forbiddenPatterns = [
|
||||
"#d23",
|
||||
"#36c",
|
||||
"#ddd",
|
||||
"#c80",
|
||||
"#a60",
|
||||
"#2a7",
|
||||
"#2563eb",
|
||||
"#1d4ed8",
|
||||
"rgba(128, 128, 128",
|
||||
];
|
||||
|
||||
for (const pattern of forbiddenPatterns) {
|
||||
expect(css, `expected CSS not to contain ${pattern}`).not.toContain(pattern);
|
||||
}
|
||||
});
|
||||
|
||||
it("does not reference non-existent dashboard color tokens", () => {
|
||||
const forbiddenTokens = [
|
||||
"--color-danger",
|
||||
"--color-accent",
|
||||
"--color-border",
|
||||
"--color-primary",
|
||||
"--color-primary-hover",
|
||||
];
|
||||
|
||||
for (const token of forbiddenTokens) {
|
||||
expect(css, `expected CSS not to reference ${token}`).not.toContain(token);
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps color-mix expressions token-based", () => {
|
||||
const colorMixCalls = css.match(/color-mix\([^)]*(?:\)[^)]*)?\)/g) ?? [];
|
||||
expect(colorMixCalls.length).toBeGreaterThan(0);
|
||||
|
||||
for (const call of colorMixCalls) {
|
||||
expect(call, `expected ${call} to reference a CSS token`).toContain("var(--");
|
||||
expect(call, `expected ${call} not to contain hardcoded hex`).not.toMatch(/#[0-9a-fA-F]{3,8}/);
|
||||
expect(call, `expected ${call} not to contain rgba()`).not.toMatch(/rgba\(/);
|
||||
}
|
||||
});
|
||||
|
||||
it("sets root view text color from the theme text token", () => {
|
||||
const [viewBlock] = selectorBlocks(".ce-view");
|
||||
expect(viewBlock).toBeDefined();
|
||||
expect(viewBlock).toMatch(/color:\s*var\(--text\)\s*;/);
|
||||
});
|
||||
|
||||
it("does not use opacity to dim text selectors", () => {
|
||||
const textDimmingSelectors = [
|
||||
".ce-view-summary",
|
||||
".ce-empty-hint",
|
||||
'.ce-group[data-empty="true"]',
|
||||
".ce-group-count",
|
||||
".ce-group-empty",
|
||||
".ce-artifact-path",
|
||||
".ce-flow-turn-role",
|
||||
".ce-flow-option-desc",
|
||||
".ce-flow-status",
|
||||
".ce-session-status",
|
||||
".ce-session-updated",
|
||||
".ce-flow-guidance-label",
|
||||
".ce-flow-thinking",
|
||||
".ce-flow-question-desc",
|
||||
".ce-flow-working-label",
|
||||
".ce-flow-degraded-options",
|
||||
".ce-flow-activity-details summary",
|
||||
".ce-activity-thinking",
|
||||
".ce-flow-turn-comment",
|
||||
];
|
||||
|
||||
for (const selector of textDimmingSelectors) {
|
||||
const blocks = selectorBlocks(selector);
|
||||
expect(blocks, `expected to find selector ${selector}`).not.toHaveLength(0);
|
||||
for (const block of blocks) {
|
||||
expect(block, `expected ${selector} not to use opacity for text dimming`).not.toMatch(/opacity\s*:/);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user