fix(FN-2365): tokenize Git Manager light-theme surface styles

- Add semantic neutral surface tier tokens in root and light-theme blocks for subtle, muted, emphasis, and stronger hover states
- Replace Git Manager light-theme hardcoded neutral rgba backgrounds with surface tokens and token-based color-mix accent states
- Add dedicated Git Manager theme styling tests to guard token usage and prevent regressions to raw rgba values
- Expand status color/theme token tests and stabilize SettingsModal navigation selectors used by related test coverage
This commit is contained in:
Fusion
2026-04-24 12:57:24 -07:00
committed by gsxdsm
parent 59e270980e
commit ba9c0d7b2e
4 changed files with 118 additions and 27 deletions

View File

@@ -0,0 +1,54 @@
import { describe, it, expect, beforeAll } from "vitest";
import fs from "fs";
import path from "path";
describe("Git Manager light-theme tokenization", () => {
const stylesPath = path.resolve(__dirname, "../styles.css");
let css: string;
beforeAll(() => {
css = fs.readFileSync(stylesPath, "utf-8");
});
function getGitManagerLightBlock(): string {
const startMarker = "/* ── Light Theme Overrides ── */";
const endMarker = "/* ── Task Changes Tab Styles";
const startIdx = css.indexOf(startMarker);
const endIdx = css.indexOf(endMarker);
if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx) {
throw new Error("Could not locate Git Manager light-theme override block boundaries");
}
return css.slice(startIdx, endIdx);
}
it("uses semantic neutral surface tokens for scoped Git Manager light-theme backgrounds", () => {
const block = getGitManagerLightBlock();
expect(block).toContain('[data-theme="light"] .gm-sidebar {\n background: var(--surface-subtle);');
expect(block).toContain('[data-theme="light"] .gm-nav-item:hover {\n background: var(--surface-hover);');
expect(block).toContain('[data-theme="light"] .gm-commit-header:hover {\n background: var(--surface-muted);');
expect(block).toContain('[data-theme="light"] .gm-hash {\n background: var(--surface-emphasis);');
expect(block).toContain('[data-theme="light"] .gm-icon-btn:hover {\n background: var(--surface-hover-strong);');
});
it("does not reintroduce direct neutral rgba(0,0,0,0.02-0.06) backgrounds in scoped Git Manager light-theme rules", () => {
const block = getGitManagerLightBlock();
expect(block).not.toMatch(/background:\s*rgba\(0,\s*0,\s*0,\s*0\.0[2-6]\)/);
});
it("uses token-based accent states for selected/default Git Manager light-theme rules", () => {
const block = getGitManagerLightBlock();
expect(block).toContain("color-mix(in srgb, var(--todo) 8%, transparent)");
expect(block).toContain("color-mix(in srgb, var(--todo) 12%, transparent)");
expect(block).toContain("color-mix(in srgb, var(--todo) 10%, transparent)");
expect(block).toContain("color: var(--todo);");
expect(block).not.toContain("rgba(9, 105, 218");
expect(block).not.toContain("#0969da");
});
});

View File

@@ -77,6 +77,30 @@ describe("Status color CSS custom properties", () => {
);
});
it("defines semantic neutral surface tiers in :root", () => {
const rootBlock = extractRootBlock(css);
expect(rootBlock).toContain("--surface-subtle");
expect(rootBlock).toContain("--surface-muted");
expect(rootBlock).toContain("--surface-emphasis");
expect(rootBlock).toContain("--surface-hover-strong");
});
it("defines semantic neutral surface tier overrides in light theme", () => {
const lightBlock = extractLightThemeBlock(css);
expect(lightBlock).toContain("--surface-subtle");
expect(lightBlock).toContain("--surface-muted");
expect(lightBlock).toContain("--surface-emphasis");
expect(lightBlock).toContain("--surface-hover-strong");
});
it("defines semantic neutral tiers with tokenized color-mix expressions", () => {
expect(css).toMatch(/--surface-subtle:\s*color-mix\(in\s+srgb,\s*var\(--surface\)\s+96%,\s*var\(--text\)\s+4%\)/);
expect(css).toMatch(/--surface-muted:\s*color-mix\(in\s+srgb,\s*var\(--surface\)\s+94%,\s*var\(--text\)\s+6%\)/);
expect(css).toMatch(/--surface-emphasis:\s*color-mix\(in\s+srgb,\s*var\(--surface\)\s+92%,\s*var\(--text\)\s+8%\)/);
expect(css).toMatch(/--surface-hover-strong:\s*color-mix\(in\s+srgb,\s*var\(--surface\)\s+88%,\s*var\(--text\)\s+12%\)/);
expect(css).not.toMatch(/--surface-(subtle|muted|emphasis|hover-strong):\s*rgba\(/);
});
it("uses --surface-hover without per-rule fallback overrides", () => {
expect(css).toContain("var(--surface-hover)");
expect(css).not.toContain("var(--surface-hover,");

View File

@@ -1543,7 +1543,8 @@ describe("SettingsModal", () => {
render(<SettingsModal onClose={onClose} addToast={addToast} />);
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
fireEvent.click(screen.getAllByText("Authentication")[0]);
const authNav = await screen.findByRole("button", { name: /Authentication/ });
fireEvent.click(authNav);
await waitFor(() => expect(fetchAuthStatus).toHaveBeenCalled());
expect(screen.getByText("Anthropic")).toBeTruthy();
@@ -2024,7 +2025,8 @@ describe("SettingsModal", () => {
const { container } = render(<SettingsModal onClose={onClose} addToast={addToast} />);
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
fireEvent.click(screen.getAllByText("Authentication")[0]);
const authNav = await screen.findByRole("button", { name: /Authentication/ });
fireEvent.click(authNav);
await waitFor(() => expect(fetchAuthStatus).toHaveBeenCalled());
const input = screen.getByPlaceholderText("Enter API key");
@@ -2202,6 +2204,7 @@ describe("SettingsModal", () => {
it("has .settings-content as sibling of .settings-sidebar", async () => {
const { container } = render(<SettingsModal onClose={onClose} addToast={addToast} />);
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
await waitFor(() => expect(container.querySelector(".settings-layout")).toBeTruthy());
const layout = container.querySelector(".settings-layout");
const children = Array.from(layout!.children);
@@ -2355,7 +2358,8 @@ describe("SettingsModal", () => {
render(<SettingsModal onClose={onClose} addToast={addToast} />);
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
fireEvent.click(screen.getByText("Notifications"));
const notificationsNav = await screen.findByRole("button", { name: /Notifications/ });
fireEvent.click(notificationsNav);
const input = screen.getByLabelText("ntfy Topic") as HTMLInputElement;
fireEvent.change(input, { target: { value: "" } });

View File

@@ -105,6 +105,11 @@
--card-hover: #282e36;
/* Neutral transient hover/press surface. Theme contract: defined in :root, light mode, and every color-theme block. */
--surface-hover: color-mix(in srgb, var(--surface) 90%, var(--text) 10%);
/* Semantic neutral surface tiers for subtle fills, section headers, compact chips, and emphasized hover states. */
--surface-subtle: color-mix(in srgb, var(--surface) 96%, var(--text) 4%);
--surface-muted: color-mix(in srgb, var(--surface) 94%, var(--text) 6%);
--surface-emphasis: color-mix(in srgb, var(--surface) 92%, var(--text) 8%);
--surface-hover-strong: color-mix(in srgb, var(--surface) 88%, var(--text) 12%);
--bg-secondary: color-mix(in srgb, var(--surface) 70%, var(--card));
--bg-tertiary: color-mix(in srgb, var(--surface) 40%, var(--card));
--border: #30363d;
@@ -11208,6 +11213,10 @@ html .column.drag-over * {
--card-hover: #f3f4f6;
/* Light mode keeps the same semantic role with slightly softer contrast. */
--surface-hover: color-mix(in srgb, var(--surface) 92%, var(--text) 8%);
--surface-subtle: color-mix(in srgb, var(--surface) 98%, var(--text) 2%);
--surface-muted: color-mix(in srgb, var(--surface) 97%, var(--text) 3%);
--surface-emphasis: color-mix(in srgb, var(--surface) 95%, var(--text) 5%);
--surface-hover-strong: color-mix(in srgb, var(--surface) 94%, var(--text) 6%);
--bg-secondary: #f6f8fa;
--bg-tertiary: #eaeef2;
--border: #d0d7de;
@@ -20374,80 +20383,80 @@ html .column.drag-over * {
/* ── Light Theme Overrides ── */
[data-theme="light"] .gm-sidebar {
background: rgba(0, 0, 0, 0.02);
background: var(--surface-subtle);
}
[data-theme="light"] .gm-nav-item:hover {
background: rgba(0, 0, 0, 0.04);
background: var(--surface-hover);
}
[data-theme="light"] .gm-nav-item.active {
background: rgba(9, 105, 218, 0.06);
background: color-mix(in srgb, var(--todo) 6%, transparent);
}
[data-theme="light"] .gm-file-item:hover {
background: rgba(0, 0, 0, 0.02);
background: var(--surface-subtle);
}
[data-theme="light"] .gm-file-item.staged {
background: rgba(26, 127, 55, 0.04);
background: color-mix(in srgb, var(--color-success) 4%, transparent);
}
[data-theme="light"] .gm-commit-header:hover {
background: rgba(0, 0, 0, 0.03);
background: var(--surface-muted);
}
[data-theme="light"] .gm-branch-item:hover {
background: rgba(0, 0, 0, 0.02);
background: var(--surface-subtle);
}
[data-theme="light"] .gm-branch-item.current {
background: rgba(9, 105, 218, 0.04);
background: color-mix(in srgb, var(--todo) 4%, transparent);
}
[data-theme="light"] .gm-branch-item.selected {
background: rgba(9, 105, 218, 0.08);
background: color-mix(in srgb, var(--todo) 8%, transparent);
}
[data-theme="light"] .gm-branch-item.selected:hover {
background: rgba(9, 105, 218, 0.12);
background: color-mix(in srgb, var(--todo) 12%, transparent);
}
[data-theme="light"] .gm-branch-details {
background: rgba(0, 0, 0, 0.02);
background: var(--surface-subtle);
}
/* ── Remotes Two-Column Layout Light Theme ── */
[data-theme="light"] .gm-remote-selector {
background: rgba(0, 0, 0, 0.02);
background: var(--surface-subtle);
border-right-color: rgba(0, 0, 0, 0.08);
}
[data-theme="light"] .gm-remote-selector-item:hover {
background: rgba(0, 0, 0, 0.04);
background: var(--surface-hover);
}
[data-theme="light"] .gm-remote-selector-item.selected {
background: rgba(9, 105, 218, 0.08);
background: color-mix(in srgb, var(--todo) 8%, transparent);
}
[data-theme="light"] .gm-remote-selector-item.selected:hover {
background: rgba(9, 105, 218, 0.12);
background: color-mix(in srgb, var(--todo) 12%, transparent);
}
[data-theme="light"] .gm-remote-sync-card {
background: rgba(0, 0, 0, 0.02);
background: var(--surface-subtle);
border-color: rgba(0, 0, 0, 0.08);
}
[data-theme="light"] .gm-remote-detail-card {
background: rgba(0, 0, 0, 0.02);
background: var(--surface-subtle);
border-color: rgba(0, 0, 0, 0.08);
}
[data-theme="light"] .gm-remote-form {
background: rgba(0, 0, 0, 0.02);
background: var(--surface-subtle);
border-color: rgba(0, 0, 0, 0.08);
}
@@ -20456,24 +20465,24 @@ html .column.drag-over * {
}
[data-theme="light"] .gm-remote-default-badge {
background: rgba(9, 105, 218, 0.1);
color: #0969da;
background: color-mix(in srgb, var(--todo) 10%, transparent);
color: var(--todo);
}
[data-theme="light"] .gm-hash {
background: rgba(0, 0, 0, 0.05);
background: var(--surface-emphasis);
}
[data-theme="light"] .gm-icon-btn:hover {
background: rgba(0, 0, 0, 0.06);
background: var(--surface-hover-strong);
}
[data-theme="light"] .gm-file-section-header {
background: rgba(0, 0, 0, 0.03);
background: var(--surface-muted);
}
[data-theme="light"] .gm-load-more:hover {
background: rgba(0, 0, 0, 0.03);
background: var(--surface-muted);
}
/* ── Task Changes Tab Styles ─────────────────────────────────────────────── */