fix(desktop): unify UI colors onto theme tokens

This commit is contained in:
Freedom authored and GitHub committed 2026-07-28 11:24:10 +08:00
1 parent af479c7770
commit 1cdbf4fa3d
31 files changed
+558 -582

No files matched your search

@@ -1,8 +1,34 @@
import { describe, expect, it } from "vitest";
import { readFileSync } from "node:fs";
import { DATA_GRID_DARK_ACTIVE_ROW_BG, DATA_GRID_LIGHT_ACTIVE_ROW_BG, dataGridActiveRowBackground, resolveDataGridPaintTheme } from "@/lib/dataGrid/dataGridPaintTheme";
function parseRgb(value: string): { r: number; g: number; b: number } | null {
const match = value.match(/rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)/i);
if (!match) return null;
return { r: Number(match[1]), g: Number(match[2]), b: Number(match[3]) };
}
function relativeLuminance(rgb: { r: number; g: number; b: number }): number {
const channel = (value: number) => {
const s = value / 255;
return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
};
return 0.2126 * channel(rgb.r) + 0.7152 * channel(rgb.g) + 0.0722 * channel(rgb.b);
}
function contrastRatio(a: string, b: string): number {
const left = parseRgb(a);
const right = parseRgb(b);
if (!left || !right) return 0;
const l1 = relativeLuminance(left);
const l2 = relativeLuminance(right);
const lighter = Math.max(l1, l2);
const darker = Math.min(l1, l2);
return (lighter + 0.05) / (darker + 0.05);
}
describe("data grid paint theme", () => {
it("uses a subtle blue active-row surface in both color schemes", () => {
it("uses a subtle active-row surface fallback in both color schemes", () => {
expect(dataGridActiveRowBackground(false)).toBe(DATA_GRID_LIGHT_ACTIVE_ROW_BG);
expect(dataGridActiveRowBackground(true)).toBe(DATA_GRID_DARK_ACTIVE_ROW_BG);
@@ -12,4 +38,91 @@ describe("data grid paint theme", () => {
expect(resolveDataGridPaintTheme({ getVar: emptyCssVariable, isDark: true }).cellActive).toBe(DATA_GRID_DARK_ACTIVE_ROW_BG);
expect(resolveDataGridPaintTheme({ getVar: emptyCssVariable, isDark: true }).rowNumberActive).toBe(DATA_GRID_DARK_ACTIVE_ROW_BG);
});
it("reads semantic success/warning tokens when present", () => {
const vars: Record<string, string> = {
"--background": "rgb(255, 255, 255)",
"--foreground": "rgb(10, 10, 10)",
"--muted-foreground": "rgb(115, 115, 115)",
"--primary": "rgb(23, 23, 23)",
"--destructive": "rgb(231, 0, 11)",
"--accent": "rgb(245, 245, 245)",
"--border": "rgb(229, 229, 229)",
"--muted": "rgb(245, 245, 245)",
"--success": "rgb(22, 163, 74)",
"--warning": "rgb(217, 119, 6)",
"--success-bg": "rgb(220, 252, 231)",
"--warning-bg": "rgb(254, 243, 199)",
"--color-error-bg": "rgb(254, 226, 226)",
};
const theme = resolveDataGridPaintTheme({
getVar: (name) => vars[name] ?? "",
isDark: false,
});
expect(theme.rowNumberTextNew).toBe("rgb(22, 163, 74)");
expect(theme.rowNumberTextEdited).toBe("rgb(217, 119, 6)");
expect(theme.rowNumberNew).toBe("rgb(220, 252, 231)");
expect(theme.cellDirty).toBe("rgb(254, 243, 199)");
expect(theme.rowDeleted).toBe("rgb(254, 226, 226)");
});
it("derives selection border from ring/primary instead of the low-contrast border token", () => {
const vars: Record<string, string> = {
"--background": "rgb(255, 255, 255)",
"--foreground": "rgb(10, 10, 10)",
"--muted-foreground": "rgb(115, 115, 115)",
"--primary": "rgb(23, 23, 23)",
"--destructive": "rgb(231, 0, 11)",
"--accent": "rgb(226, 226, 226)",
"--border": "rgb(229, 229, 229)",
"--ring": "rgb(23, 23, 23)",
"--muted": "rgb(245, 245, 245)",
};
const theme = resolveDataGridPaintTheme({
getVar: (name) => vars[name] ?? "",
isDark: false,
});
expect(theme.cellSelectedBorder).toBe("rgb(23, 23, 23)");
expect(theme.cellSelectedBorder).not.toBe(vars["--border"]);
expect(contrastRatio(theme.cellSelectedBorder, theme.cellSelected)).toBeGreaterThanOrEqual(3);
});
it("honors an explicit --data-grid-cell-selected-border token when provided", () => {
const vars: Record<string, string> = {
"--background": "rgb(255, 255, 255)",
"--foreground": "rgb(10, 10, 10)",
"--muted-foreground": "rgb(115, 115, 115)",
"--primary": "rgb(23, 23, 23)",
"--destructive": "rgb(231, 0, 11)",
"--accent": "rgb(226, 226, 226)",
"--border": "rgb(229, 229, 229)",
"--ring": "rgb(161, 161, 161)",
"--muted": "rgb(245, 245, 245)",
"--data-grid-cell-selected-border": "rgb(37, 99, 235)",
};
const theme = resolveDataGridPaintTheme({
getVar: (name) => vars[name] ?? "",
isDark: false,
});
expect(theme.cellSelectedBorder).toBe("rgb(37, 99, 235)");
});
});
describe("dbx-control-chrome cascade contract", () => {
it("keeps chrome defaults layered and low-specificity so invalid/focus utilities can win", () => {
const css = readFileSync(new URL("../../../styles/globals.css", import.meta.url), "utf8");
const chromeBlockStart = css.indexOf("Shared control chrome defaults");
expect(chromeBlockStart).toBeGreaterThanOrEqual(0);
const chromeSlice = css.slice(chromeBlockStart - 40, chromeBlockStart + 1600);
expect(chromeSlice).toMatch(/@layer components/);
expect(chromeSlice).toMatch(/:where\(\.dbx-control-chrome\)/);
expect(chromeSlice).toMatch(/\[aria-invalid="true"\]/);
expect(chromeSlice).not.toMatch(/box-shadow:\s*none/);
});
});
@@ -30,13 +30,14 @@ describe("dataGridRuntime", () => {
});
it("keeps runtime modules independent from the DataGrid component", () => {
const sourceRoots = [join(process.cwd(), "apps/desktop/src/lib/dataGrid"), join(process.cwd(), "apps/desktop/src/composables")];
const repoRoot = process.cwd().replace(/[\\/]apps[\\/]desktop$/, "");
const sourceRoots = [join(repoRoot, "apps/desktop/src/lib/dataGrid"), join(repoRoot, "apps/desktop/src/composables")];
const sourceFiles: string[] = [];
const visit = (directory: string) => {
for (const entry of readdirSync(directory, { withFileTypes: true })) {
const path = join(directory, entry.name);
if (entry.isDirectory()) visit(path);
else if (entry.name.endsWith(".ts") && (!directory.endsWith("/composables") || entry.name.startsWith("useDataGrid"))) sourceFiles.push(path);
else if (entry.name.endsWith(".ts") && (!directory.endsWith("composables") || entry.name.startsWith("useDataGrid"))) sourceFiles.push(path);
}
};
sourceRoots.forEach(visit);
@@ -246,26 +246,33 @@ export function resolveDataGridPaintTheme(options: { getVar: (name: string) => s
const primary = cssVarColor(getVar, "--primary", isDark ? "rgb(208, 208, 214)" : "rgb(23, 23, 23)");
const destructive = cssVarColor(getVar, "--destructive", isDark ? "rgb(243, 98, 95)" : "rgb(231, 0, 11)");
const accent = cssVarColor(getVar, "--accent", isDark ? "rgb(46, 47, 51)" : "rgb(245, 245, 245)");
const activeSurface = dataGridActiveRowBackground(isDark);
const success = cssVarColor(getVar, "--success", isDark ? "rgb(74, 222, 128)" : "rgb(22, 163, 74)");
const warning = cssVarColor(getVar, "--warning", isDark ? "rgb(251, 191, 36)" : "rgb(217, 119, 6)");
const successBg = paintToken(getVar, "--success-bg", isDark ? DATA_GRID_DARK_ROW_NUMBER_NEW_BG : DATA_GRID_LIGHT_ROW_NUMBER_NEW_BG);
const warningBg = paintToken(getVar, "--warning-bg", isDark ? DATA_GRID_DARK_ROW_NUMBER_EDITED_BG : DATA_GRID_LIGHT_ROW_NUMBER_EDITED_BG);
const destructiveBg = paintToken(getVar, "--color-error-bg", isDark ? DATA_GRID_DARK_ROW_NUMBER_DELETED_BG : DATA_GRID_LIGHT_ROW_NUMBER_DELETED_BG);
const activeSurface = paintToken(getVar, "--data-grid-cell-active-bg", dataGridActiveRowBackground(isDark));
const rowMuted = isDark ? DATA_GRID_DARK_STRIPED_ROW_BG : DATA_GRID_LIGHT_STRIPED_ROW_BG;
const rowNew = isDark ? "rgb(51, 51, 55)" : "rgb(243, 243, 243)";
const rowDeleted = isDark ? "rgb(55, 31, 32)" : "rgb(255, 244, 244)";
const rowNew = paintToken(getVar, "--muted", isDark ? "rgb(51, 51, 55)" : "rgb(243, 243, 243)");
const rowDeleted = destructiveBg;
const cellActive = activeSurface;
const cellDirty = isDark ? "rgb(94, 75, 26)" : "rgb(255, 248, 230)";
const cellSelected = isDark ? "rgb(20, 40, 60)" : "rgb(239, 246, 255)";
const cellSelectedDirty = isDark ? "rgb(76, 66, 38)" : "rgb(235, 224, 184)";
const cellSelectedBorder = isDark ? "rgb(96, 165, 250)" : "rgb(59, 130, 246)";
const cellSelectedSingle = isDark ? "rgb(30, 64, 96)" : "rgb(191, 219, 254)";
const cellDirty = warningBg;
const cellSelected = paintToken(getVar, "--accent", isDark ? "rgb(20, 40, 60)" : "rgb(239, 246, 255)");
const cellSelectedDirty = warningBg;
// Selection outline must stay readable on the selected fill (WCAG non-text ~3:1).
// Prefer ring/primary over the generic border token, which is often near the fill.
const cellSelectedBorder = paintToken(getVar, "--ring", isDark ? "rgb(96, 165, 250)" : primary);
const cellSelectedSingle = paintToken(getVar, "--muted", isDark ? "rgb(30, 64, 96)" : "rgb(191, 219, 254)");
const cellHover = accent;
const cellSearch = isDark ? DATA_GRID_DARK_SEARCH_COLORS.match : "rgb(253, 245, 184)";
const cellCurrentSearch = isDark ? DATA_GRID_DARK_SEARCH_COLORS.current : "rgba(253, 224, 71, 0.52)";
const cellCurrentSearchBorder = isDark ? DATA_GRID_DARK_SEARCH_COLORS.currentBorder : "rgba(234, 179, 8, 0.82)";
const cellSearch = paintToken(getVar, "--warning-bg", isDark ? DATA_GRID_DARK_SEARCH_COLORS.match : "rgb(253, 245, 184)");
const cellCurrentSearch = paintToken(getVar, "--warning-bg", isDark ? DATA_GRID_DARK_SEARCH_COLORS.current : "rgba(253, 224, 71, 0.52)");
const cellCurrentSearchBorder = paintToken(getVar, "--warning", isDark ? DATA_GRID_DARK_SEARCH_COLORS.currentBorder : "rgba(234, 179, 8, 0.82)");
const rowNumberDefault = isDark ? DATA_GRID_DARK_ROW_NUMBER_BG : paintToken(getVar, "--data-grid-row-number-default-bg", "rgb(255, 255, 255)");
const rowNumberNew = isDark ? DATA_GRID_DARK_ROW_NUMBER_NEW_BG : DATA_GRID_LIGHT_ROW_NUMBER_NEW_BG;
const rowNumberEdited = isDark ? DATA_GRID_DARK_ROW_NUMBER_EDITED_BG : DATA_GRID_LIGHT_ROW_NUMBER_EDITED_BG;
const rowNumberDeleted = isDark ? DATA_GRID_DARK_ROW_NUMBER_DELETED_BG : DATA_GRID_LIGHT_ROW_NUMBER_DELETED_BG;
const rowNumberNew = successBg;
const rowNumberEdited = warningBg;
const rowNumberDeleted = destructiveBg;
const rowNumberActive = activeSurface;
const rowNumberSelected = isDark ? "rgb(30, 64, 96)" : "rgb(191, 219, 254)";
const rowNumberSelected = cellSelectedSingle;
return {
background,
@@ -273,29 +280,29 @@ export function resolveDataGridPaintTheme(options: { getVar: (name: string) => s
foreground,
mutedForeground,
primary,
rowMuted: isDark ? rowMuted : paintToken(getVar, "--data-grid-row-muted-bg", rowMuted),
rowNew: isDark ? rowNew : paintToken(getVar, "--data-grid-row-new-bg", rowNew),
rowDeleted: isDark ? rowDeleted : paintToken(getVar, "--data-grid-row-deleted-bg", rowDeleted),
cellActive: isDark ? cellActive : paintToken(getVar, "--data-grid-cell-active-bg", cellActive),
cellDirty: isDark ? cellDirty : paintToken(getVar, "--data-grid-cell-dirty-bg", cellDirty),
cellSelected: isDark ? cellSelected : paintToken(getVar, "--data-grid-cell-selected-bg", cellSelected),
cellSelectedDirty: isDark ? cellSelectedDirty : paintToken(getVar, "--data-grid-cell-selected-dirty-bg", cellSelectedDirty),
cellSelectedBorder: isDark ? cellSelectedBorder : paintToken(getVar, "--data-grid-cell-selected-border", cellSelectedBorder),
cellSelectedSingle: isDark ? cellSelectedSingle : paintToken(getVar, "--data-grid-cell-selected-single-bg", cellSelectedSingle),
cellSelectedSingleBorder: isDark ? primary : paintToken(getVar, "--data-grid-cell-selected-single-border", primary),
cellHover: isDark ? cellHover : paintToken(getVar, "--data-grid-cell-hover-bg", cellHover),
cellSearch: isDark ? cellSearch : paintToken(getVar, "--data-grid-cell-search-bg", cellSearch),
cellCurrentSearch: isDark ? cellCurrentSearch : paintToken(getVar, "--data-grid-cell-current-search-bg", cellCurrentSearch),
cellCurrentSearchBorder: isDark ? cellCurrentSearchBorder : paintToken(getVar, "--data-grid-cell-current-search-border", cellCurrentSearchBorder),
rowMuted: paintToken(getVar, "--data-grid-row-muted-bg", rowMuted),
rowNew: paintToken(getVar, "--data-grid-row-new-bg", rowNew),
rowDeleted: paintToken(getVar, "--data-grid-row-deleted-bg", rowDeleted),
cellActive: paintToken(getVar, "--data-grid-cell-active-bg", cellActive),
cellDirty: paintToken(getVar, "--data-grid-cell-dirty-bg", cellDirty),
cellSelected: paintToken(getVar, "--data-grid-cell-selected-bg", cellSelected),
cellSelectedDirty: paintToken(getVar, "--data-grid-cell-selected-dirty-bg", cellSelectedDirty),
cellSelectedBorder: paintToken(getVar, "--data-grid-cell-selected-border", cellSelectedBorder),
cellSelectedSingle: paintToken(getVar, "--data-grid-cell-selected-single-bg", cellSelectedSingle),
cellSelectedSingleBorder: paintToken(getVar, "--data-grid-cell-selected-single-border", primary),
cellHover: paintToken(getVar, "--data-grid-cell-hover-bg", cellHover),
cellSearch: paintToken(getVar, "--data-grid-cell-search-bg", cellSearch),
cellCurrentSearch: paintToken(getVar, "--data-grid-cell-current-search-bg", cellCurrentSearch),
cellCurrentSearchBorder: paintToken(getVar, "--data-grid-cell-current-search-border", cellCurrentSearchBorder),
rowNumberDefault,
rowNumberNew: isDark ? rowNumberNew : paintToken(getVar, "--data-grid-row-number-new-bg", rowNumberNew),
rowNumberEdited: isDark ? rowNumberEdited : paintToken(getVar, "--data-grid-row-number-edited-bg", rowNumberEdited),
rowNumberDeleted: isDark ? rowNumberDeleted : paintToken(getVar, "--data-grid-row-number-deleted-bg", rowNumberDeleted),
rowNumberActive: isDark ? rowNumberActive : paintToken(getVar, "--data-grid-row-number-active-bg", rowNumberActive),
rowNumberSelected: isDark ? rowNumberSelected : paintToken(getVar, "--data-grid-row-number-selected-bg", rowNumberSelected),
rowNumberNew: paintToken(getVar, "--data-grid-row-number-new-bg", rowNumberNew),
rowNumberEdited: paintToken(getVar, "--data-grid-row-number-edited-bg", rowNumberEdited),
rowNumberDeleted: paintToken(getVar, "--data-grid-row-number-deleted-bg", rowNumberDeleted),
rowNumberActive: paintToken(getVar, "--data-grid-row-number-active-bg", rowNumberActive),
rowNumberSelected: paintToken(getVar, "--data-grid-row-number-selected-bg", rowNumberSelected),
rowNumberTextClean: mutedForeground,
rowNumberTextNew: isDark ? "rgb(94, 233, 181)" : "rgb(0, 122, 85)",
rowNumberTextEdited: isDark ? "rgb(255, 210, 48)" : "rgb(187, 77, 0)",
rowNumberTextNew: success,
rowNumberTextEdited: warning,
rowNumberTextDeleted: destructive,
};
}