fix(desktop): unify UI colors onto theme tokens
This commit is contained in:
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,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user