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:
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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,");
|
||||
|
||||
@@ -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: "" } });
|
||||
|
||||
|
||||
@@ -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 ─────────────────────────────────────────────── */
|
||||
|
||||
Reference in New Issue
Block a user