diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index 682d93ee15..2ab767bcae 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -138,7 +138,7 @@ import { canFetchNextDataGridSegment, canGoNextDataGridPage, hasCompleteLocalDat import { dataGridCountQueryOptions } from "@/lib/dataGrid/dataGridQueryOptions"; import { dataGridBottomScrollTop, dataGridScrollPosition, isDataGridAtScrollBottom, isDataGridNearScrollBottom, shouldCheckInfiniteScrollAfterScroll, type DataGridScrollPosition } from "@/lib/dataGrid/dataGridInfiniteScroll"; import { CANVAS_DATA_GRID_ROW_HEIGHT, canvasDataGridActionReservedWidth, dataGridSearchMatchKey, drawCanvasDataGrid } from "@/lib/dataGrid/canvasDataGridRenderer"; -import { DATA_GRID_DARK_ROW_NUMBER_BG, DATA_GRID_DARK_STRIPED_ROW_BG, DATA_GRID_LIGHT_STRIPED_ROW_BG, dataGridActiveRowBackground } from "@/lib/dataGrid/dataGridPaintTheme"; +import { DATA_GRID_DARK_STRIPED_ROW_BG, DATA_GRID_LIGHT_STRIPED_ROW_BG, dataGridActiveRowBackground } from "@/lib/dataGrid/dataGridPaintTheme"; import { createRowLowerTextCache } from "@/lib/dataGrid/dataGridRowLowerText"; import { dataGridPreviewLabelKey, dataGridSaveActionMode, dataGridSaveToolbarState } from "@/lib/dataGrid/dataGridSaveUi"; import type { QueryEditabilityReason } from "@/lib/sql/sqlAnalysis"; @@ -4388,16 +4388,16 @@ function isNull(value: unknown): boolean { function rowNumberStatusClass(item: RowItem): string { if (item.status === "draft") { - return "data-grid-row-number--status-draft font-semibold"; + return "border-muted-foreground/20 bg-muted/20 font-semibold text-muted-foreground"; } if (item.status === "new") { - return "data-grid-row-number--status-new font-semibold"; + return "border-emerald-500/40 bg-emerald-500/15 font-semibold text-emerald-700 dark:text-emerald-300"; } if (item.status === "edited") { - return "data-grid-row-number--status-edited font-semibold"; + return "border-amber-500/40 bg-amber-500/15 font-semibold text-amber-700 dark:text-amber-300"; } if (item.status === "deleted") { - return "data-grid-row-number--status-deleted font-semibold line-through"; + return "border-destructive/40 bg-destructive/15 font-semibold text-destructive line-through"; } return "text-muted-foreground"; } @@ -4408,27 +4408,53 @@ function rowCellsUseSelectionVisual(rowId: number): boolean { function dataGridRowStyle(item: RowItem): CSSProperties { const dark = isDark.value || (typeof document !== "undefined" && document.documentElement.classList.contains("dark")); - const deletedBg = "var(--color-error-bg, color-mix(in srgb, var(--destructive) 12%, transparent))"; - const newBg = "var(--success-bg, color-mix(in srgb, var(--success) 12%, transparent))"; - const editedBg = "var(--warning-bg, color-mix(in srgb, var(--warning) 14%, transparent))"; - const activeBg = `var(--data-grid-cell-active-bg, ${dataGridActiveRowBackground(dark)})`; - const resolvedRowBg = item.isDeleted - ? deletedBg + const activeRowBg = dataGridActiveRowBackground(dark); + const rowBg = item.isDeleted + ? dark + ? "rgb(55, 31, 32)" + : "rgb(255, 244, 244)" : isRowActive(item.displayIndex) - ? activeBg - : item.isNew || item.isDraft - ? "color-mix(in srgb, var(--muted) 80%, var(--background))" - : item.displayIndex % 2 === 1 - ? `var(--data-grid-row-muted-bg, ${dark ? DATA_GRID_DARK_STRIPED_ROW_BG : DATA_GRID_LIGHT_STRIPED_ROW_BG})` - : "var(--background)"; - const rowNumberBg = item.status === "new" ? newBg : item.status === "edited" ? editedBg : item.status === "deleted" ? deletedBg : isRowActive(item.displayIndex) && !item.isDeleted ? activeBg : dark ? DATA_GRID_DARK_ROW_NUMBER_BG : "var(--background)"; + ? activeRowBg + : item.isNew + ? dark + ? "rgb(51, 51, 55)" + : "rgb(243, 243, 243)" + : item.isDraft + ? dark + ? "rgb(51, 51, 55)" + : "rgb(243, 243, 243)" + : item.displayIndex % 2 === 1 + ? `var(--data-grid-row-muted-bg, ${dark ? DATA_GRID_DARK_STRIPED_ROW_BG : DATA_GRID_LIGHT_STRIPED_ROW_BG})` + : dark + ? "rgb(19, 20, 22)" + : "rgb(255, 255, 255)"; + const rowNumberBg = + item.status === "new" + ? dark + ? "rgb(33, 45, 40)" + : "rgb(219, 244, 233)" + : item.status === "edited" + ? dark + ? "rgb(48, 41, 28)" + : "rgb(253, 241, 219)" + : item.status === "deleted" + ? dark + ? "rgb(55, 31, 32)" + : "rgb(255, 244, 244)" + : isRowActive(item.displayIndex) && !item.isDeleted + ? activeRowBg + : dark + ? "rgb(35, 37, 42)" + : "rgb(255, 255, 255)"; return { - "--data-grid-cell-bg": resolvedRowBg, + "--data-grid-cell-bg": rowBg, "--data-grid-row-number-bg": rowNumberBg, - // Keep selection fill/border from [data-grid-root] tokens (primary/ring mix). - // Do not flatten them to --accent/--border — that erases outline contrast. - "--data-grid-cell-selected-dirty-bg": editedBg, - "--data-grid-row-number-active-bg": activeBg, + "--data-grid-cell-selected-bg": dark ? "rgb(20, 40, 60)" : "rgb(239, 246, 255)", + "--data-grid-cell-selected-single-bg": dark ? "rgb(30, 64, 96)" : "rgb(191, 219, 254)", + "--data-grid-cell-selected-dirty-bg": dark ? "rgb(76, 66, 38)" : "rgb(235, 224, 184)", + "--data-grid-cell-selected-border": dark ? "rgb(96, 165, 250)" : "rgb(59, 130, 246)", + "--data-grid-row-number-active-bg": activeRowBg, + "--data-grid-row-number-selected-bg": dark ? "rgb(30, 64, 96)" : "rgb(191, 219, 254)", } as CSSProperties; } @@ -7979,8 +8005,8 @@ const gridContextMenuItems = computed(() => {
(() => { 'row-cell-selected': transposeRecordUsesSelectionVisual(cell.recordIndex) && !transposeCellIsSelected(cell.recordIndex, cell.valueIndex) && !displayItems[cell.recordIndex]?.isDirtyCol[cell.valueIndex], 'row-cell-selected-dirty': transposeRecordUsesSelectionVisual(cell.recordIndex) && !transposeCellIsSelected(cell.recordIndex, cell.valueIndex) && displayItems[cell.recordIndex]?.isDirtyCol[cell.valueIndex], 'bg-primary/15': transposeRecordUsesActiveHighlight(cell.recordIndex) && !transposeRecordUsesSelectionVisual(cell.recordIndex) && !displayItems[cell.recordIndex]?.isDirtyCol[cell.valueIndex] && !transposeCellIsSelected(cell.recordIndex, cell.valueIndex), - 'cell-dirty': displayItems[cell.recordIndex]?.isDirtyCol[cell.valueIndex], - 'cell-search-match': cellIsSearchMatch(cell.recordIndex, cell.valueIndex), - 'cell-current-search-match': cellIsCurrentMatch(cell.recordIndex, cell.valueIndex), + 'bg-yellow-500/10 cell-dirty': displayItems[cell.recordIndex]?.isDirtyCol[cell.valueIndex], + 'bg-yellow-200/60 dark:bg-yellow-500/20': cellIsSearchMatch(cell.recordIndex, cell.valueIndex), + 'ring-2 ring-inset ring-yellow-500 bg-yellow-300/60 dark:bg-yellow-500/40': cellIsCurrentMatch(cell.recordIndex, cell.valueIndex), 'cursor-text': !isScrolling && canEditCellItem(displayItems[cell.recordIndex], cell.valueIndex), - 'hover:bg-accent': + 'hover:bg-gray-200 dark:hover:bg-gray-800': !isScrolling && canEditCellItem(displayItems[cell.recordIndex], cell.valueIndex) && !transposeRecordUsesSelectionVisual(cell.recordIndex) && !transposeRecordUsesActiveHighlight(cell.recordIndex) && !transposeCellIsSelected(cell.recordIndex, cell.valueIndex), }" :style="{ width: `${getTransposeRecordWidth(cell.recordIndex)}px` }" @@ -8651,15 +8677,17 @@ const gridContextMenuItems = computed(() => { 'data-grid-cell--frozen-separator': frozenColumnCount > 0 && col.visibleColIdx === frozenColumnCount - 1, 'text-right': columnAligns[col.visibleColIdx] === 'right', 'text-muted-foreground italic': isNull(item.data[col.actualColIdx]), - 'cell-dirty': item.isDirtyCol[col.actualColIdx], + 'bg-yellow-500/10 cell-dirty': item.isDirtyCol[col.actualColIdx], 'cell-selected': cellIsSelected(item.displayIndex, col.visibleColIdx) && !item.isDirtyCol[col.actualColIdx], 'cell-selected-dirty': cellIsSelected(item.displayIndex, col.visibleColIdx) && item.isDirtyCol[col.actualColIdx], 'row-cell-selected': rowCellsUseSelectionVisual(item.id) && !cellIsSelected(item.displayIndex, col.visibleColIdx) && !item.isDirtyCol[col.actualColIdx], 'row-cell-selected-dirty': rowCellsUseSelectionVisual(item.id) && !cellIsSelected(item.displayIndex, col.visibleColIdx) && item.isDirtyCol[col.actualColIdx], 'cell-search-match': cellIsSearchMatch(item.displayIndex, col.actualColIdx), 'cell-current-search-match': cellIsCurrentMatch(item.displayIndex, col.actualColIdx), + 'bg-yellow-200/60 dark:bg-yellow-500/20': cellIsSearchMatch(item.displayIndex, col.actualColIdx), + 'ring-2 ring-inset ring-yellow-500 bg-yellow-300/60 dark:bg-yellow-500/40': cellIsCurrentMatch(item.displayIndex, col.actualColIdx), 'tabular-nums': typeof item.data[col.actualColIdx] === 'number', - 'cursor-text hover:bg-accent': !isScrolling && canEditCellItem(item, col.actualColIdx), + 'cursor-text hover:bg-gray-200 dark:hover:bg-gray-800': !isScrolling && canEditCellItem(item, col.actualColIdx), 'line-through': item.isDeleted, 'overflow-visible z-20 border-r-transparent': editingCell?.rowId === item.id && editingCell?.col === col.actualColIdx, 'overflow-hidden': !(editingCell?.rowId === item.id && editingCell?.col === col.actualColIdx), @@ -8916,8 +8944,8 @@ const gridContextMenuItems = computed(() => {
{{ index.name }}
- PK - UNIQUE + PK + UNIQUE {{ index.index_type }}
@@ -9312,32 +9340,74 @@ const gridContextMenuItems = computed(() => { @reference "../../styles/globals.css"; [data-grid-root] { - /* Status / selection chrome — derived from theme tokens so palettes stay in sync. */ - --data-grid-row-muted-bg: color-mix(in srgb, var(--muted) 92%, var(--foreground)); - --data-grid-row-new-bg: color-mix(in srgb, var(--muted) 80%, var(--background)); - --data-grid-row-deleted-bg: var(--color-error-bg, color-mix(in srgb, var(--destructive) 12%, transparent)); - --data-grid-cell-active-bg: color-mix(in srgb, var(--info) 10%, var(--background)); - --data-grid-cell-dirty-bg: var(--warning-bg, color-mix(in srgb, var(--warning) 14%, transparent)); - --data-grid-cell-selected-bg: color-mix(in srgb, var(--primary) 12%, var(--accent)); - --data-grid-cell-selected-single-bg: color-mix(in srgb, var(--primary) 22%, var(--accent)); - --data-grid-cell-selected-dirty-bg: color-mix(in srgb, var(--warning) 22%, var(--accent)); - /* Prefer ring/primary so the outline stays visible on the selected fill. */ - --data-grid-cell-selected-border: color-mix(in srgb, var(--ring) 55%, var(--primary)); - --data-grid-cell-selected-single-border: var(--primary); - --data-grid-cell-hover-bg: var(--accent); - --data-grid-cell-search-bg: var(--warning-bg, color-mix(in srgb, var(--warning) 18%, transparent)); - --data-grid-cell-current-search-bg: color-mix(in srgb, var(--warning) 35%, transparent); - --data-grid-cell-current-search-border: var(--warning); - --data-grid-row-number-default-bg: var(--background); - --data-grid-row-number-new-bg: var(--success-bg, color-mix(in srgb, var(--success) 12%, transparent)); - --data-grid-row-number-edited-bg: var(--warning-bg, color-mix(in srgb, var(--warning) 14%, transparent)); - --data-grid-row-number-deleted-bg: var(--color-error-bg, color-mix(in srgb, var(--destructive) 12%, transparent)); - --data-grid-row-number-active-bg: var(--data-grid-cell-active-bg); - --data-grid-row-number-selected-bg: var(--data-grid-cell-selected-single-bg); - --data-grid-scrollbar-thumb: color-mix(in srgb, var(--foreground) 30%, transparent); - --data-grid-scrollbar-thumb-hover: color-mix(in srgb, var(--foreground) 48%, transparent); + --data-grid-row-muted-bg: rgb(240, 240, 240); + --data-grid-row-new-bg: rgb(243, 243, 243); + --data-grid-row-deleted-bg: rgb(255, 244, 244); + --data-grid-cell-active-bg: rgb(244, 248, 255); + --data-grid-cell-dirty-bg: rgb(255, 248, 230); + --data-grid-cell-selected-bg: rgb(239, 246, 255); + --data-grid-cell-selected-single-bg: rgb(191, 219, 254); + --data-grid-cell-selected-dirty-bg: rgb(235, 224, 184); + --data-grid-cell-selected-border: rgb(59, 130, 246); + --data-grid-cell-hover-bg: rgb(245, 245, 245); + --data-grid-cell-search-bg: rgb(253, 245, 184); + --data-grid-cell-current-search-bg: rgba(253, 224, 71, 0.52); + --data-grid-cell-current-search-border: rgba(234, 179, 8, 0.82); + --data-grid-row-number-default-bg: rgb(255, 255, 255); + --data-grid-row-number-new-bg: rgb(219, 244, 233); + --data-grid-row-number-edited-bg: rgb(253, 241, 219); + --data-grid-row-number-deleted-bg: rgb(255, 244, 244); + --data-grid-row-number-active-bg: rgb(244, 248, 255); + --data-grid-row-number-selected-bg: rgb(191, 219, 254); + --data-grid-scrollbar-thumb: color-mix(in oklch, var(--foreground) 30%, transparent); + --data-grid-scrollbar-thumb-hover: color-mix(in oklch, var(--foreground) 48%, transparent); --data-grid-scrollbar-track: transparent; - background-color: var(--background); + background-color: rgb(255, 255, 255); +} + +[data-grid-root].data-grid--dark, +:global(.dark) [data-grid-root] { + --data-grid-row-muted-bg: rgb(40, 40, 43); + --data-grid-row-new-bg: rgb(51, 51, 55); + --data-grid-row-deleted-bg: rgb(55, 31, 32); + --data-grid-cell-active-bg: rgb(25, 34, 46); + --data-grid-cell-dirty-bg: rgb(94, 75, 26); + --data-grid-cell-selected-bg: rgb(20, 40, 60); + --data-grid-cell-selected-single-bg: rgb(30, 64, 96); + --data-grid-cell-selected-dirty-bg: rgb(76, 66, 38); + --data-grid-cell-selected-border: rgb(96, 165, 250); + --data-grid-cell-hover-bg: rgb(46, 47, 51); + --data-grid-cell-search-bg: rgb(72, 57, 8); + --data-grid-cell-current-search-bg: rgb(116, 87, 0); + --data-grid-cell-current-search-border: rgb(239, 177, 0); + --data-grid-row-number-default-bg: rgb(35, 37, 42); + --data-grid-row-number-new-bg: rgb(33, 45, 40); + --data-grid-row-number-edited-bg: rgb(48, 41, 28); + --data-grid-row-number-deleted-bg: rgb(55, 31, 32); + --data-grid-row-number-active-bg: rgb(25, 34, 46); + --data-grid-row-number-selected-bg: rgb(30, 64, 96); + --data-grid-scrollbar-thumb: rgb(82, 82, 91); + --data-grid-scrollbar-thumb-hover: rgb(113, 113, 122); + --data-grid-scrollbar-track: rgb(24, 24, 27); + background-color: rgb(19, 20, 22); +} + +@supports (background: color-mix(in oklab, white 50%, transparent)) { + [data-grid-root] { + --data-grid-row-muted-bg: color-mix(in oklab, var(--muted) 99%, var(--foreground)); + --data-grid-row-new-bg: color-mix(in oklab, var(--primary) 5%, transparent); + --data-grid-row-deleted-bg: color-mix(in oklab, var(--destructive) 5%, transparent); + --data-grid-cell-dirty-bg: color-mix(in oklab, rgb(240 177 0) 10%, transparent); + --data-grid-cell-selected-bg: color-mix(in oklab, rgb(59 130 246) 12%, var(--background)); + --data-grid-cell-selected-single-bg: color-mix(in oklab, rgb(59 130 246) 30%, var(--background)); + --data-grid-cell-selected-dirty-bg: color-mix(in oklab, rgb(234 181 50) 30%, color-mix(in oklab, rgb(59 130 246) 18%, var(--background))); + --data-grid-cell-selected-border: color-mix(in oklab, rgb(59 130 246) 75%, transparent); + --data-grid-cell-hover-bg: color-mix(in oklab, var(--accent) 50%, transparent); + --data-grid-row-number-new-bg: color-mix(in oklab, rgb(16 185 129) 15%, var(--background)); + --data-grid-row-number-edited-bg: color-mix(in oklab, rgb(245 158 11) 15%, var(--background)); + --data-grid-row-number-deleted-bg: color-mix(in oklab, var(--destructive) 15%, var(--background)); + --data-grid-row-number-selected-bg: color-mix(in oklab, rgb(59 130 246) 30%, var(--background)); + } } .data-grid-header-shell { @@ -9347,28 +9417,36 @@ const gridContextMenuItems = computed(() => { } .data-grid-header-cell { - background-color: color-mix(in srgb, var(--muted) 88%, var(--background)); + background-color: rgb(239, 239, 239); } [data-grid-root].data-grid--dark .data-grid-header-cell, :global(.dark) [data-grid-root] .data-grid-header-cell { - background-color: color-mix(in srgb, var(--muted) 70%, var(--background)); + background-color: rgb(32, 32, 34) !important; } [data-grid-root].data-grid--dark .data-grid-header-row, :global(.dark) [data-grid-root] .data-grid-header-row { - color: var(--foreground); + color: rgb(215, 215, 219); } [data-grid-root].data-grid--dark .data-grid-header-cell:hover, :global(.dark) [data-grid-root] .data-grid-header-cell:hover { - background-color: var(--accent); + background-color: rgb(46, 47, 51) !important; } .data-grid-header-cell--selected { background-color: var(--data-grid-cell-selected-single-bg) !important; } +:global(.dark) [data-grid-root] { + --data-grid-cell-selected-bg: rgb(20, 40, 60); + --data-grid-cell-selected-single-bg: rgb(30, 64, 96); + --data-grid-cell-selected-dirty-bg: rgb(76, 66, 38); + --data-grid-cell-selected-border: rgb(96, 165, 250); + --data-grid-row-number-selected-bg: rgb(30, 64, 96); +} + [data-grid-root].data-grid--dark .data-grid-header-cell--selected, [data-grid-root].data-grid--dark .transpose-record-header-selected, [data-grid-root].data-grid--dark .transpose-record-header-active, @@ -9376,7 +9454,7 @@ const gridContextMenuItems = computed(() => { :global(.dark) [data-grid-root] .transpose-record-header-selected, :global(.dark) [data-grid-root] .transpose-record-header-active { background-color: var(--data-grid-cell-selected-single-bg) !important; - color: var(--foreground) !important; + color: rgb(244, 244, 245) !important; } .data-grid-row { @@ -9389,12 +9467,12 @@ const gridContextMenuItems = computed(() => { /* 冻结列:不透明背景遮挡滚动的非冻结列;状态 class 的 !important 会覆盖此项 */ .data-grid-cell--frozen { - background-color: var(--data-grid-cell-bg, var(--background)) !important; + background-color: var(--data-grid-cell-bg, rgb(255, 255, 255)) !important; } /* 冻结列分隔线:与 Canvas 模式和列头一致(2px 深色右边框) */ .data-grid-cell--frozen-separator { - border-right: 2px solid color-mix(in srgb, var(--border) 80%, var(--foreground)) !important; + border-right: 2px solid rgb(100, 116, 139) !important; } .data-grid-row-number { @@ -9847,28 +9925,16 @@ const gridContextMenuItems = computed(() => { background-color: var(--data-grid-cell-selected-dirty-bg) !important; } -.data-grid-row-number--status-draft { - border: 1px solid color-mix(in srgb, var(--muted-foreground) 20%, transparent); - background-color: color-mix(in srgb, var(--muted) 20%, transparent); - color: var(--muted-foreground); -} - -.data-grid-row-number--status-new { - border: 1px solid color-mix(in srgb, var(--success) 40%, transparent); +.data-grid-row-number.bg-emerald-500\/15 { background-color: var(--data-grid-row-number-new-bg); - color: var(--success); } -.data-grid-row-number--status-edited { - border: 1px solid color-mix(in srgb, var(--warning) 40%, transparent); +.data-grid-row-number.bg-amber-500\/15 { background-color: var(--data-grid-row-number-edited-bg); - color: var(--warning); } -.data-grid-row-number--status-deleted { - border: 1px solid color-mix(in srgb, var(--destructive) 40%, transparent); +.data-grid-row-number.bg-destructive\/15 { background-color: var(--data-grid-row-number-deleted-bg); - color: var(--destructive); } .active-row > .data-grid-row-number { diff --git a/apps/desktop/src/lib/__tests__/dataGrid/dataGridPaintTheme.spec.ts b/apps/desktop/src/lib/__tests__/dataGrid/dataGridPaintTheme.spec.ts index c59d49f3b2..d16b98008d 100644 --- a/apps/desktop/src/lib/__tests__/dataGrid/dataGridPaintTheme.spec.ts +++ b/apps/desktop/src/lib/__tests__/dataGrid/dataGridPaintTheme.spec.ts @@ -39,36 +39,7 @@ describe("data grid paint theme", () => { 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 = { - "--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", () => { + it("keeps the classic blue selection palette instead of theme accent/ring mixing", () => { const vars: Record = { "--background": "rgb(255, 255, 255)", "--foreground": "rgb(10, 10, 10)", @@ -79,19 +50,32 @@ describe("data grid paint theme", () => { "--border": "rgb(229, 229, 229)", "--ring": "rgb(23, 23, 23)", "--muted": "rgb(245, 245, 245)", + "--success": "rgb(22, 163, 74)", + "--warning": "rgb(217, 119, 6)", }; - const theme = resolveDataGridPaintTheme({ + const light = resolveDataGridPaintTheme({ getVar: (name) => vars[name] ?? "", isDark: false, }); + const dark = resolveDataGridPaintTheme({ + getVar: (name) => vars[name] ?? "", + isDark: true, + }); - expect(theme.cellSelectedBorder).toBe("rgb(23, 23, 23)"); - expect(theme.cellSelectedBorder).not.toBe(vars["--border"]); - expect(contrastRatio(theme.cellSelectedBorder, theme.cellSelected)).toBeGreaterThanOrEqual(3); + expect(light.cellSelected).toBe("rgb(239, 246, 255)"); + expect(light.cellSelectedBorder).toBe("rgb(59, 130, 246)"); + expect(light.cellSelectedSingle).toBe("rgb(191, 219, 254)"); + expect(light.rowNumberTextNew).toBe("rgb(0, 122, 85)"); + expect(light.rowNumberTextEdited).toBe("rgb(187, 77, 0)"); + expect(contrastRatio(light.cellSelectedBorder, light.cellSelected)).toBeGreaterThanOrEqual(3); + + expect(dark.cellSelected).toBe("rgb(20, 40, 60)"); + expect(dark.cellSelectedBorder).toBe("rgb(96, 165, 250)"); + expect(dark.cellSelectedSingle).toBe("rgb(30, 64, 96)"); }); - it("honors an explicit --data-grid-cell-selected-border token when provided", () => { + it("honors an explicit --data-grid-cell-selected-border token when provided in light mode", () => { const vars: Record = { "--background": "rgb(255, 255, 255)", "--foreground": "rgb(10, 10, 10)", @@ -100,7 +84,6 @@ describe("data grid paint theme", () => { "--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)", }; diff --git a/apps/desktop/src/lib/dataGrid/dataGridPaintTheme.ts b/apps/desktop/src/lib/dataGrid/dataGridPaintTheme.ts index 4c5e91b805..73180367de 100644 --- a/apps/desktop/src/lib/dataGrid/dataGridPaintTheme.ts +++ b/apps/desktop/src/lib/dataGrid/dataGridPaintTheme.ts @@ -246,33 +246,26 @@ 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 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 activeSurface = dataGridActiveRowBackground(isDark); const rowMuted = isDark ? DATA_GRID_DARK_STRIPED_ROW_BG : DATA_GRID_LIGHT_STRIPED_ROW_BG; - const rowNew = paintToken(getVar, "--muted", isDark ? "rgb(51, 51, 55)" : "rgb(243, 243, 243)"); - const rowDeleted = destructiveBg; + const rowNew = isDark ? "rgb(51, 51, 55)" : "rgb(243, 243, 243)"; + const rowDeleted = isDark ? "rgb(55, 31, 32)" : "rgb(255, 244, 244)"; const cellActive = activeSurface; - 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 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 cellHover = accent; - 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 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 rowNumberDefault = isDark ? DATA_GRID_DARK_ROW_NUMBER_BG : paintToken(getVar, "--data-grid-row-number-default-bg", "rgb(255, 255, 255)"); - const rowNumberNew = successBg; - const rowNumberEdited = warningBg; - const rowNumberDeleted = destructiveBg; + 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 rowNumberActive = activeSurface; - const rowNumberSelected = cellSelectedSingle; + const rowNumberSelected = isDark ? "rgb(30, 64, 96)" : "rgb(191, 219, 254)"; return { background, @@ -280,29 +273,29 @@ export function resolveDataGridPaintTheme(options: { getVar: (name: string) => s foreground, mutedForeground, primary, - 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), + 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), rowNumberDefault, - 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), + 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), rowNumberTextClean: mutedForeground, - rowNumberTextNew: success, - rowNumberTextEdited: warning, + rowNumberTextNew: isDark ? "rgb(94, 233, 181)" : "rgb(0, 122, 85)", + rowNumberTextEdited: isDark ? "rgb(255, 210, 48)" : "rgb(187, 77, 0)", rowNumberTextDeleted: destructive, }; } diff --git a/packages/app-tests/dataGridHeaderBackground.test.ts b/packages/app-tests/dataGridHeaderBackground.test.ts index 21fae8e926..ef9b553338 100644 --- a/packages/app-tests/dataGridHeaderBackground.test.ts +++ b/packages/app-tests/dataGridHeaderBackground.test.ts @@ -9,7 +9,7 @@ test("unused header width keeps the result background", () => { const style = descriptor.styles.map((block) => block.content).join("\n"); assert.match(style, /\.data-grid-header-shell\s*\{[^}]*background-color:\s*var\(--background\)/s); - assert.match(style, /\.data-grid-header-cell\s*\{[^}]*background-color:\s*color-mix\(in srgb, var\(--muted\) 88%, var\(--background\)\)/s); + assert.match(style, /\.data-grid-header-cell\s*\{[^}]*background-color:\s*rgb\(239,\s*239,\s*239\)/s); assert.doesNotMatch(style, /\.data-grid-header-shell\s*,\s*\.data-grid-header-cell/); assert.doesNotMatch(style, /data-grid--dark\s+\.data-grid-header-shell/); }); diff --git a/packages/app-tests/dataGridSelectionContrast.test.ts b/packages/app-tests/dataGridSelectionContrast.test.ts index 9781c89b59..efc9dd700e 100644 --- a/packages/app-tests/dataGridSelectionContrast.test.ts +++ b/packages/app-tests/dataGridSelectionContrast.test.ts @@ -2,9 +2,11 @@ import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { test } from "vitest"; -test("data grid selection outline uses ring/primary tokens instead of flat border", () => { +test("data grid selection colors stay on the classic blue palette", () => { const source = readFileSync("apps/desktop/src/components/grid/DataGrid.vue", "utf8"); - assert.match(source, /--data-grid-cell-selected-border:\s*color-mix\(in srgb, var\(--ring\) 55%, var\(--primary\)\)/); - assert.doesNotMatch(source, /"--data-grid-cell-selected-border":\s*"var\(--border\)"/); - assert.doesNotMatch(source, /"--data-grid-cell-selected-bg":\s*"var\(--accent\)"/); + assert.match(source, /--data-grid-cell-selected-bg:\s*rgb\(239,\s*246,\s*255\)/); + assert.match(source, /--data-grid-cell-selected-border:\s*rgb\(59,\s*130,\s*246\)/); + assert.match(source, /"--data-grid-cell-selected-bg":\s*dark \? "rgb\(20, 40, 60\)" : "rgb\(239, 246, 255\)"/); + assert.doesNotMatch(source, /--data-grid-cell-selected-border:\s*color-mix\(in srgb, var\(--ring\)/); + assert.doesNotMatch(source, /--data-grid-cell-selected-bg:\s*color-mix\(in srgb, var\(--primary\)/); });