diff --git a/apps/desktop/src/components/document/DocumentBrowser.vue b/apps/desktop/src/components/document/DocumentBrowser.vue index 1fc70f2d44..cf97f0392f 100644 --- a/apps/desktop/src/components/document/DocumentBrowser.vue +++ b/apps/desktop/src/components/document/DocumentBrowser.vue @@ -2,7 +2,7 @@ import { computed, ref, nextTick, watch, onMounted, onBeforeUnmount } from "vue"; import { uuid } from "@/lib/common/utils"; import { useI18n } from "vue-i18n"; -import { RefreshCw, Trash2, Plus, Save, ChevronDown, ChevronLeft, ChevronRight, Table2, Braces, X, Search, Wrench, Filter, Columns3Cog, SquareDashed, Minus, Rows3, AlignLeft, AlignRight, EyeOff } from "@lucide/vue"; +import { RefreshCw, Trash2, Plus, Save, ChevronDown, ChevronLeft, ChevronRight, Table2, Braces, X, Search, Wrench, Filter, Columns3Cog, SquareDashed, Minus, Rows3, AlignLeft, AlignRight, EyeOff, Palette } from "@lucide/vue"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Input } from "@/components/ui/input"; @@ -135,6 +135,7 @@ const columnWidthDensity = computed(() => settingsStore.editorSettings.columnWid const dataGridRenderMode = computed(() => settingsStore.editorSettings.dataGridRenderMode); const tableFontSize = computed(() => settingsStore.editorSettings.tableFontSize); const numericColumnRightAlign = computed(() => settingsStore.editorSettings.numericColumnRightAlign ?? true); +const colorizeDataGridCellTypes = computed(() => settingsStore.editorSettings.colorizeDataGridCellTypes); const viewMode = computed({ get: () => settingsStore.editorSettings.mongoViewMode, set: (value) => settingsStore.updateEditorSettings({ mongoViewMode: value }), @@ -177,6 +178,10 @@ function increaseTableFontSize() { function setNumericColumnRightAlign(value: boolean) { settingsStore.updateEditorSettings({ numericColumnRightAlign: value }); } + +function setColorizeDataGridCellTypes(value: boolean) { + settingsStore.updateEditorSettings({ colorizeDataGridCellTypes: value }); +} const tableSearchSplitContainerRef = ref(); const tableFindPaneWidth = ref(null); const isResizingTableSearchSplit = ref(false); @@ -1867,6 +1872,13 @@ defineExpose({ focusSearch }); +
+
+ + {{ t("grid.colorizeDataTypes") }} +
+ +
diff --git a/apps/desktop/src/components/editor/EditorSettingsDialog.vue b/apps/desktop/src/components/editor/EditorSettingsDialog.vue index ee85096de1..06badb6fd7 100644 --- a/apps/desktop/src/components/editor/EditorSettingsDialog.vue +++ b/apps/desktop/src/components/editor/EditorSettingsDialog.vue @@ -309,6 +309,7 @@ const debugLogCopied = ref(false); const debugLogDownloaded = ref(false); const editShowColumnCommentsInHeader = ref(settingsStore.editorSettings.showColumnCommentsInHeader); const editShowColumnTypesInHeader = ref(settingsStore.editorSettings.showColumnTypesInHeader); +const editColorizeDataGridCellTypes = ref(settingsStore.editorSettings.colorizeDataGridCellTypes); const editShowIndexIndicatorsInHeader = ref(settingsStore.editorSettings.showIndexIndicatorsInHeader); const editCompactColumnHeaderActions = ref(settingsStore.editorSettings.compactColumnHeaderActions); const editDataGridQuickEntry = ref(settingsStore.editorSettings.dataGridQuickEntry); @@ -482,6 +483,7 @@ function currentEditorSettingsDraft(): EditorSettingsDraft { tabLayout: editTabLayout.value, showColumnCommentsInHeader: editShowColumnCommentsInHeader.value, showColumnTypesInHeader: editShowColumnTypesInHeader.value, + colorizeDataGridCellTypes: editColorizeDataGridCellTypes.value, showIndexIndicatorsInHeader: editShowIndexIndicatorsInHeader.value, compactColumnHeaderActions: editCompactColumnHeaderActions.value, dataGridQuickEntry: editDataGridQuickEntry.value, @@ -754,6 +756,7 @@ function syncEditorSettingsDraftFromStore() { editTabLayout.value = settingsStore.editorSettings.tabLayout; editShowColumnCommentsInHeader.value = settingsStore.editorSettings.showColumnCommentsInHeader; editShowColumnTypesInHeader.value = settingsStore.editorSettings.showColumnTypesInHeader; + editColorizeDataGridCellTypes.value = settingsStore.editorSettings.colorizeDataGridCellTypes; editShowIndexIndicatorsInHeader.value = settingsStore.editorSettings.showIndexIndicatorsInHeader; editCompactColumnHeaderActions.value = settingsStore.editorSettings.compactColumnHeaderActions; editDataGridQuickEntry.value = settingsStore.editorSettings.dataGridQuickEntry; @@ -998,6 +1001,7 @@ function resetDefaultsForTab(tab: SettingsCategory) { } else if (tab === "data") { editShowColumnCommentsInHeader.value = DEFAULT_EDITOR_SETTINGS.showColumnCommentsInHeader; editShowColumnTypesInHeader.value = DEFAULT_EDITOR_SETTINGS.showColumnTypesInHeader; + editColorizeDataGridCellTypes.value = DEFAULT_EDITOR_SETTINGS.colorizeDataGridCellTypes; editShowIndexIndicatorsInHeader.value = DEFAULT_EDITOR_SETTINGS.showIndexIndicatorsInHeader; editCompactColumnHeaderActions.value = DEFAULT_EDITOR_SETTINGS.compactColumnHeaderActions; editDataGridQuickEntry.value = DEFAULT_EDITOR_SETTINGS.dataGridQuickEntry; @@ -1065,6 +1069,7 @@ function resetAllDefaults() { editSidebarTablePageSize.value = DEFAULT_SIDEBAR_TABLE_PAGE_SIZE; editShowColumnCommentsInHeader.value = DEFAULT_EDITOR_SETTINGS.showColumnCommentsInHeader; editShowColumnTypesInHeader.value = DEFAULT_EDITOR_SETTINGS.showColumnTypesInHeader; + editColorizeDataGridCellTypes.value = DEFAULT_EDITOR_SETTINGS.colorizeDataGridCellTypes; editShowIndexIndicatorsInHeader.value = DEFAULT_EDITOR_SETTINGS.showIndexIndicatorsInHeader; editCompactColumnHeaderActions.value = DEFAULT_EDITOR_SETTINGS.compactColumnHeaderActions; editDataGridQuickEntry.value = DEFAULT_EDITOR_SETTINGS.dataGridQuickEntry; @@ -4817,6 +4822,17 @@ onUnmounted(() => {
+
+
+ +

+ {{ t("settings.colorizeDataGridCellTypesDescription") }} +

+
+ +
+
+
+ + {{ t("grid.colorizeDataTypes") }} +
+ +
@@ -1858,6 +1896,13 @@ defineExpose({ focusSearch, refreshData, refreshQueryEditorCompletionCache, hand
+
+
+ + {{ t("grid.colorizeDataTypes") }} +
+ +
diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 29063a8635..131c5b47ae 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -1675,6 +1675,7 @@ export default { searchModeHint: "Choose whether Ctrl+F filters matching rows or keeps all rows visible and highlights matches.", searchModeFilter: "Filter", searchModeHighlight: "Highlight", + colorizeDataTypes: "Data type colors", numericColumnAlign: "Numeric Column Align", numericColumnAlignLeft: "Left", numericColumnAlignRight: "Right", @@ -5604,6 +5605,8 @@ export default { showColumnCommentsInHeaderDescription: "Display table column comments directly below grid column names.", showColumnTypesInHeader: "Show column types under names", showColumnTypesInHeaderDescription: "Display each column's data type directly below grid column names.", + colorizeDataGridCellTypes: "Color-code data grid values", + colorizeDataGridCellTypesDescription: "Use subtle, theme-aware text colors to distinguish values by their column data type.", showIndexIndicatorsInHeader: "Show index indicators beside column names", showIndexIndicatorsInHeaderDescription: "Use icons to indicate each column's index type in the grid header: primary key (orange), unique index (red), regular index (green).", compactColumnHeaderActions: "Compact column header tools", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index ba97bea958..32e6dab838 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -1839,6 +1839,7 @@ export default withEnglishFallback({ }, cachedResultUnavailable: "Resultado en caché faltante o incompatible.", reexecuteQuery: "Volver a ejecutar consulta", + colorizeDataTypes: "Colores de tipos de datos", numericColumnAlign: "Alineación de columna numérica", numericColumnAlignLeft: "Alineación izquierda", numericColumnAlignRight: "Alineación derecha", @@ -5359,6 +5360,8 @@ export default withEnglishFallback({ showColumnCommentsInHeaderDescription: "Muestra los comentarios de columnas directamente debajo del nombre de la columna.", showColumnTypesInHeader: "Mostrar tipos de columna bajo los nombres", showColumnTypesInHeaderDescription: "Muestra el tipo de dato de cada columna directamente debajo del nombre de la columna.", + colorizeDataGridCellTypes: "Colorear valores por tipo de dato", + colorizeDataGridCellTypesDescription: "Usa colores de texto sutiles adaptados al tema para distinguir los valores según el tipo de dato de su columna.", showIndexIndicatorsInHeader: "Mostrar indicadores de índice junto a los nombres", showIndexIndicatorsInHeaderDescription: "Usa iconos para indicar el tipo de índice de cada columna en el encabezado: clave primaria (naranja), índice único (rojo), índice regular (verde).", compactColumnHeaderActions: "Compactar herramientas del encabezado", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index cd95acd5e0..38596ca8c1 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -1837,6 +1837,7 @@ export default withEnglishFallback({ }, cachedResultUnavailable: "Risultato in cache mancante o non compatibile.", reexecuteQuery: "Esegui di nuovo la query", + colorizeDataTypes: "Colori dei tipi di dato", numericColumnAlign: "Allineamento colonna numerica", numericColumnAlignLeft: "Allineamento a sinistra", numericColumnAlignRight: "Allineamento a destra", @@ -5359,6 +5360,8 @@ export default withEnglishFallback({ showColumnCommentsInHeaderDescription: "Visualizza i commenti delle colonne della tabella direttamente sotto i nomi delle colonne nella griglia.", showColumnTypesInHeader: "Mostra i tipi di colonna sotto i nomi", showColumnTypesInHeaderDescription: "Visualizza il tipo di dato di ciascuna colonna direttamente sotto i nomi delle colonne nella griglia.", + colorizeDataGridCellTypes: "Colora i valori in base al tipo di dato", + colorizeDataGridCellTypesDescription: "Usa colori del testo discreti e adatti al tema per distinguere i valori in base al tipo di dato della colonna.", showIndexIndicatorsInHeader: "Mostra gli indicatori di indice accanto ai nomi", showIndexIndicatorsInHeaderDescription: "Utilizza icone per indicare il tipo di indice di ciascuna colonna nell'intestazione: chiave primaria (arancione), indice univoco (rosso), indice normale (verde).", compactColumnHeaderActions: "Strumenti intestazione colonna compatti", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index 39b51d46bd..02f164fd16 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -1859,6 +1859,7 @@ export default withEnglishFallback({ }, cachedResultUnavailable: "キャッシュ結果が不足しているか互換性がありません。", reexecuteQuery: "クエリを再実行", + colorizeDataTypes: "データ型の色分け", numericColumnAlign: "数値列の配置", numericColumnAlignLeft: "左揃え", numericColumnAlignRight: "右揃え", @@ -5394,6 +5395,8 @@ export default withEnglishFallback({ showColumnCommentsInHeaderDescription: "グリッド列名の直下にテーブル列コメントを表示します。", showColumnTypesInHeader: "列名の下にデータ型を表示", showColumnTypesInHeaderDescription: "グリッド列名の直下に各列のデータ型を表示します。", + colorizeDataGridCellTypes: "データ型に応じて値を色分け", + colorizeDataGridCellTypesDescription: "テーマに合わせた控えめな文字色で、列のデータ型ごとに値を区別します。", showIndexIndicatorsInHeader: "列名の横にインデックス指標を表示", showIndexIndicatorsInHeaderDescription: "グリッド列ヘッダーにインデックスの種類をアイコンで表示します:主キー(オレンジ)、一意インデックス(赤)、通常インデックス(緑)。", compactColumnHeaderActions: "列ヘッダーツールをコンパクトに", diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts index e6381f7bdf..fc65ef33a0 100644 --- a/apps/desktop/src/i18n/locales/ko.ts +++ b/apps/desktop/src/i18n/locales/ko.ts @@ -1559,6 +1559,7 @@ export default withEnglishFallback({ searchModeHint: "Ctrl+F가 일치하는 행을 필터링할지, 모든 행을 표시하고 일치 항목을 강조할지 선택하세요.", searchModeFilter: "필터", searchModeHighlight: "강조", + colorizeDataTypes: "데이터 타입 색상", numericColumnAlign: "숫자 컬럼 정렬", numericColumnAlignLeft: "왼쪽", numericColumnAlignRight: "오른쪽", @@ -5102,6 +5103,8 @@ export default withEnglishFallback({ showColumnCommentsInHeaderDescription: "그리드 컬럼 이름 바로 아래에 테이블 컬럼 주석을 표시합니다.", showColumnTypesInHeader: "이름 아래에 컬럼 타입 표시", showColumnTypesInHeaderDescription: "그리드 컬럼 이름 바로 아래에 각 컬럼의 데이터 타입을 표시합니다.", + colorizeDataGridCellTypes: "데이터 타입별 값 색상 표시", + colorizeDataGridCellTypesDescription: "테마에 맞는 은은한 텍스트 색상으로 컬럼 데이터 타입에 따라 값을 구분합니다.", showIndexIndicatorsInHeader: "컬럼 이름 옆에 인덱스 표시", showIndexIndicatorsInHeaderDescription: "그리드 컬럼 헤더에 인덱스 유형을 아이콘으로 표시합니다: 기본키(주황색), 고유 인덱스(빨간색), 일반 인덱스(녹색).", compactColumnHeaderActions: "컴팩트한 컬럼 머리글 도구", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index fe0d163aa5..0b9b2be474 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -1839,6 +1839,7 @@ export default withEnglishFallback({ }, cachedResultUnavailable: "Resultado em cache ausente ou incompatível.", reexecuteQuery: "Reexecutar consulta", + colorizeDataTypes: "Cores por tipo de dado", numericColumnAlign: "Alinhamento de colunas numéricas", numericColumnAlignLeft: "Alinhamento à esquerda", numericColumnAlignRight: "Alinhamento à direita", @@ -5361,6 +5362,8 @@ export default withEnglishFallback({ showColumnCommentsInHeaderDescription: "Exibir comentários de colunas da tabela diretamente abaixo dos nomes das colunas da grade.", showColumnTypesInHeader: "Mostrar tipos de coluna sob os nomes", showColumnTypesInHeaderDescription: "Exibir o tipo de dado de cada coluna diretamente abaixo dos nomes das colunas da grade.", + colorizeDataGridCellTypes: "Colorir valores por tipo de dado", + colorizeDataGridCellTypesDescription: "Usa cores de texto sutis e adaptadas ao tema para diferenciar valores pelo tipo de dado da coluna.", showIndexIndicatorsInHeader: "Mostrar indicadores de índice ao lado dos nomes", showIndexIndicatorsInHeaderDescription: "Use ícones para indicar o tipo de índice de cada coluna no cabeçalho: chave primária (laranja), índice exclusivo (vermelho), índice regular (verde).", compactColumnHeaderActions: "Ferramentas compactas no cabeçalho da coluna", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 71ee8123ba..3d481334d1 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -1601,6 +1601,7 @@ export default withEnglishFallback({ searchModeHint: "选择 Ctrl+F 搜索时过滤匹配行,或保留全部行并高亮定位命中项。", searchModeFilter: "过滤", searchModeHighlight: "高亮", + colorizeDataTypes: "数据类型着色", numericColumnAlign: "数值列对齐", numericColumnAlignLeft: "左对齐", numericColumnAlignRight: "右对齐", @@ -5526,6 +5527,8 @@ export default withEnglishFallback({ showColumnCommentsInHeaderDescription: "把表字段注释直接显示在结果表头字段名下方。", showColumnTypesInHeader: "在字段名下方显示数据类型", showColumnTypesInHeaderDescription: "把每个字段的数据类型直接显示在结果表头字段名下方。", + colorizeDataGridCellTypes: "按数据类型为表格值着色", + colorizeDataGridCellTypesDescription: "使用适配主题的克制文字颜色,按字段数据类型区分表格中的值。", showIndexIndicatorsInHeader: "在字段名旁显示索引标识", showIndexIndicatorsInHeaderDescription: "在结果表头字段名旁用图标标识字段的索引类型:主键(橙色)、唯一索引(红色)、普通索引(绿色)。", compactColumnHeaderActions: "收起字段表头工具", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index f11653917b..3e80ff2925 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -1843,6 +1843,7 @@ export default withEnglishFallback({ }, cachedResultUnavailable: "快取結果缺失或不相容。", reexecuteQuery: "重新執行查詢", + colorizeDataTypes: "資料類型著色", numericColumnAlign: "數值列對齊", numericColumnAlignLeft: "左對齊", numericColumnAlignRight: "右對齊", @@ -4699,6 +4700,8 @@ export default withEnglishFallback({ showColumnCommentsInHeaderDescription: "直接在資料表格欄位名稱下方顯示資料表欄位註解。", showColumnTypesInHeader: "在欄位名稱下方顯示資料類型", showColumnTypesInHeaderDescription: "直接在資料表格欄位名稱下方顯示每個欄位的資料類型。", + colorizeDataGridCellTypes: "依資料類型為表格值著色", + colorizeDataGridCellTypesDescription: "使用配合主題的柔和文字顏色,依欄位資料類型區分表格中的值。", showIndexIndicatorsInHeader: "在欄位名稱旁顯示索引標識", showIndexIndicatorsInHeaderDescription: "在資料表格欄位名稱旁用圖示標識欄位的索引類型:主鍵(橙色)、唯一索引(紅色)、普通索引(綠色)。", compactColumnHeaderActions: "收起欄位表頭工具", diff --git a/apps/desktop/src/lib/__tests__/dataGrid/dataGridCellTypeVisual.spec.ts b/apps/desktop/src/lib/__tests__/dataGrid/dataGridCellTypeVisual.spec.ts new file mode 100644 index 0000000000..9dc4e0d4ec --- /dev/null +++ b/apps/desktop/src/lib/__tests__/dataGrid/dataGridCellTypeVisual.spec.ts @@ -0,0 +1,105 @@ +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; +import { resolveDataGridCellTextRole } from "@/lib/dataGrid/dataGridCellTextVisual"; +import { resolveDataGridTypeVisualKind, resolveHeaderColumnType, type DataGridTypeVisualKind } from "@/lib/dataGrid/dataGridColumnType"; + +const dataGridSource = readFileSync(new URL("../../../components/grid/DataGrid.vue", import.meta.url), "utf8"); +const globalStylesSource = readFileSync(new URL("../../../styles/globals.css", import.meta.url), "utf8"); + +describe("data grid type visual kind", () => { + it.each<[string, DataGridTypeVisualKind]>([ + ["BIGINT", "integer"], + ["Nullable(UInt64)", "integer"], + ["DECIMAL(18, 4)", "numeric"], + ["DOUBLE PRECISION", "numeric"], + ["character varying(255)", "string"], + ["BOOLEAN", "boolean"], + ["timestamp with time zone", "temporal"], + ["JSONB", "structured"], + ["Array(Nullable(UInt64))", "structured"], + ["text[]", "structured"], + ["_int4", "structured"], + ["_text", "structured"], + ["_jsonb", "structured"], + ["_uuid", "structured"], + ["UUID", "identifier"], + ["BYTEA", "binary"], + ["SDO_GEOMETRY", "spatial"], + ["inet", "unknown"], + ])("maps %s to %s", (dataType, expected) => { + expect(resolveDataGridTypeVisualKind(dataType)).toBe(expected); + }); + + it("keeps an absent type neutral", () => { + expect(resolveDataGridTypeVisualKind(undefined)).toBe("unknown"); + expect(resolveDataGridTypeVisualKind(" ")).toBe("unknown"); + }); + + it.each([ + ["timestamp", "sqlserver", "binary"], + ["rowversion", "sqlserver", "binary"], + ["bit(8)", "postgres", "binary"], + ["bit varying(8)", "postgres", "binary"], + ] as const)("maps %s for %s to %s", (dataType, databaseType, expected) => { + expect(resolveDataGridTypeVisualKind(dataType, databaseType)).toBe(expected); + }); + + it("keeps ambiguous types on their generic defaults without a matching dialect", () => { + expect(resolveDataGridTypeVisualKind("timestamp")).toBe("temporal"); + expect(resolveDataGridTypeVisualKind("timestamp", "postgres")).toBe("temporal"); + expect(resolveDataGridTypeVisualKind("bit")).toBe("boolean"); + expect(resolveDataGridTypeVisualKind("bit", "sqlserver")).toBe("boolean"); + }); +}); + +describe("data grid header type color", () => { + it("uses the displayed metadata type for the standard header color", () => { + const displayedType = resolveHeaderColumnType({ + tableColumnType: "decimal(10,2)", + resultColumnTypes: ["text"], + actualColIdx: 0, + }); + + expect(displayedType).toBe("decimal(10,2)"); + expect(resolveDataGridTypeVisualKind(displayedType)).toBe("numeric"); + expect(dataGridSource).toContain(':type-class="typeColorClass(headerColumnType(col.name, col.actualColIdx))"'); + expect(dataGridSource).not.toContain("typeColorClass(allColumnTypes[col.actualColIdx]"); + }); +}); + +describe("data grid cell text visual priority", () => { + const ordinaryInteger = { + colorizeTypes: true, + typeKind: "integer" as const, + }; + + it("uses a type color only for an ordinary typed value", () => { + expect(resolveDataGridCellTextRole(ordinaryInteger)).toBe("type"); + }); + + it.each([{ colorizeTypes: false }, { typeKind: "unknown" as const }, { isEditing: true }, { isControl: true }, { isSelected: true }, { isCurrentSearchMatch: true }, { isSearchMatch: true }, { isDirty: true }, { isDeleted: true }])("keeps interaction state visually dominant for %o", (override) => { + expect(resolveDataGridCellTextRole({ ...ordinaryInteger, ...override })).toBe("neutral"); + }); + + it.each([{ isNull: true }, { isDraft: true }, { isNull: true, isSelected: true }])("keeps null and draft placeholders muted for %o", (override) => { + expect(resolveDataGridCellTextRole({ ...ordinaryInteger, ...override })).toBe("muted"); + }); + + it("uses a neutral foreground on editable DOM hover surfaces", () => { + expect(dataGridSource).toContain("'hover:bg-gray-200 hover:text-foreground dark:hover:bg-gray-800':"); + expect(dataGridSource).toContain("'cursor-text hover:bg-gray-200 hover:text-foreground dark:hover:bg-gray-800':"); + }); + + it("returns before reading cell state when type colors are disabled", () => { + expect(dataGridSource).toContain('function gridCellTextColorClass(item: RowItem, actualColIdx: number, visibleColIdx: number): string {\n if (!colorizeDataGridCellTypes.value) return "text-foreground";\n const value = item.data[actualColIdx];'); + expect(dataGridSource).toContain('function transposeCellTextColorClass(recordIndex: number, actualColIdx: number): string {\n if (!colorizeDataGridCellTypes.value) return "text-foreground";\n const item = displayItems.value[recordIndex];'); + }); + + it("places data-grid type selectors in the components layer", () => { + const componentsLayerStart = globalStylesSource.indexOf("@layer components {"); + const integerTypeSelector = globalStylesSource.indexOf(".data-grid-type-integer"); + + expect(componentsLayerStart).toBeGreaterThan(-1); + expect(integerTypeSelector).toBeGreaterThan(componentsLayerStart); + }); +}); diff --git a/apps/desktop/src/lib/__tests__/dataGrid/dataGridPaintTheme.spec.ts b/apps/desktop/src/lib/__tests__/dataGrid/dataGridPaintTheme.spec.ts index 1f8f530db2..c0d9dcaef2 100644 --- a/apps/desktop/src/lib/__tests__/dataGrid/dataGridPaintTheme.spec.ts +++ b/apps/desktop/src/lib/__tests__/dataGrid/dataGridPaintTheme.spec.ts @@ -3,6 +3,8 @@ 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 hex = value.match(/^#([\da-f]{2})([\da-f]{2})([\da-f]{2})$/i); + if (hex) return { r: Number.parseInt(hex[1], 16), g: Number.parseInt(hex[2], 16), b: Number.parseInt(hex[3], 16) }; 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]) }; @@ -112,6 +114,39 @@ describe("data grid paint theme", () => { expect(theme.cellDirty).toBe("rgb(94, 56, 57)"); }); + + it("resolves accessible semantic type colors for light and dark grids", () => { + const emptyCssVariable = () => ""; + const light = resolveDataGridPaintTheme({ getVar: emptyCssVariable, isDark: false }); + const dark = resolveDataGridPaintTheme({ getVar: emptyCssVariable, isDark: true }); + + expect(light.typeForegrounds.integer).toBe("#1d4ed8"); + expect(light.typeForegrounds.boolean).toBe("#c2410c"); + expect(dark.typeForegrounds.integer).toBe("#93c5fd"); + expect(dark.typeForegrounds.boolean).toBe("#fdba74"); + expect(light.typeForegrounds.unknown).toBe(light.foreground); + expect(dark.typeForegrounds.unknown).toBe(dark.foreground); + + for (const [kind, color] of Object.entries(light.typeForegrounds)) { + if (kind === "unknown") continue; + expect(contrastRatio(color, "#ffffff")).toBeGreaterThanOrEqual(4.5); + expect(contrastRatio(color, "#f0f0f0")).toBeGreaterThanOrEqual(4.5); + } + for (const [kind, color] of Object.entries(dark.typeForegrounds)) { + if (kind === "unknown") continue; + expect(contrastRatio(color, "#131416")).toBeGreaterThanOrEqual(4.5); + expect(contrastRatio(color, "#28282b")).toBeGreaterThanOrEqual(4.5); + } + }); + + it("honors a custom semantic type color token", () => { + const theme = resolveDataGridPaintTheme({ + getVar: (name) => (name === "--data-grid-type-spatial-fg" ? "rgb(12, 98, 74)" : ""), + isDark: false, + }); + + expect(theme.typeForegrounds.spatial).toBe("rgb(12, 98, 74)"); + }); }); describe("dbx-control-chrome cascade contract", () => { diff --git a/apps/desktop/src/lib/dataGrid/canvasDataGridRenderer.ts b/apps/desktop/src/lib/dataGrid/canvasDataGridRenderer.ts index 61c31bc70b..d310a0ff7c 100644 --- a/apps/desktop/src/lib/dataGrid/canvasDataGridRenderer.ts +++ b/apps/desktop/src/lib/dataGrid/canvasDataGridRenderer.ts @@ -1,9 +1,11 @@ import { firstLineCellDisplayValue, type CellValue } from "@/lib/dataGrid/cellValue"; import { BOOLEAN_CHECKBOX_SIZE, isBooleanCellValue, normalizeBooleanCellValue } from "@/lib/dataGrid/dataGridBooleanColumn"; +import { resolveDataGridCellTextRole } from "@/lib/dataGrid/dataGridCellTextVisual"; +import type { DataGridTypeVisualKind } from "@/lib/dataGrid/dataGridColumnType"; import { dataGridFrameCoversRow, dataGridFrameIsMultiCell, dataGridSelectionFrameKindAtCell, dataGridSelectionUsesOuterFrame } from "@/lib/dataGrid/dataGridSelectionFrames"; import type { CellSelectionRange } from "@/lib/dataGrid/gridSelection"; import type { RowStatus } from "@/lib/dataGrid/gridRowStatus"; -import { DATA_GRID_DARK_SEARCH_COLORS, resolveDataGridPaintTheme, type DataGridPaintTheme } from "@/lib/dataGrid/dataGridPaintTheme"; +import { DATA_GRID_DARK_SEARCH_COLORS, dataGridTypeForeground, resolveDataGridPaintTheme, type DataGridPaintTheme } from "@/lib/dataGrid/dataGridPaintTheme"; export const CANVAS_DATA_GRID_ROW_HEIGHT = 26; export const MAX_CANVAS_DATA_GRID_PIXEL_RATIO = 4; @@ -89,6 +91,8 @@ export interface DrawCanvasDataGridOptions { pageOffset: number; frozenColumnCount?: number; columnAligns?: readonly ("left" | "right")[]; + columnTypeVisualKinds?: readonly DataGridTypeVisualKind[]; + colorizeDataTypes?: boolean; rightAlignedActionCell?: CanvasRightAlignedActionCell | null; booleanDisplayMode?: "checkbox" | "dropdown"; } @@ -344,6 +348,8 @@ export function drawCanvasDataGrid(options: DrawCanvasDataGridOptions) { pageOffset, frozenColumnCount = 0, columnAligns, + columnTypeVisualKinds, + colorizeDataTypes = false, rightAlignedActionCell, columnIsBoolean, booleanDisplayMode = "dropdown", @@ -534,8 +540,23 @@ export function drawCanvasDataGrid(options: DrawCanvasDataGridOptions) { const isEditingThisCell = editingCell?.rowId === item.id && editingCell.col === actualColIdx; const isBooleanNullCell = booleanDisplayMode === "checkbox" && isBooleanCell && value === null && !isEditingThisCell; const shouldRenderBooleanCheckbox = booleanDisplayMode === "checkbox" && isBooleanCell && value !== null && !isEditingThisCell; + const typeKind = columnTypeVisualKinds?.[visibleColIdx] ?? "unknown"; + const textRole = resolveDataGridCellTextRole({ + colorizeTypes: colorizeDataTypes, + typeKind, + isNull: value === null, + isDraft: item.isDraft && value === null, + isEditing: isEditingThisCell, + isControl: shouldRenderBooleanCheckbox, + isSelected: selectedFillVisual, + isCurrentSearchMatch, + isSearchMatch, + isDirty: isDirtyCell, + isDeleted: item.isDeleted, + }); + const cellTextColor = textRole === "muted" ? theme.mutedForeground : textRole === "type" ? dataGridTypeForeground(theme, typeKind) : theme.foreground; ctx.textAlign = isBooleanNullCell ? "center" : isRightAlign ? "right" : "left"; - ctx.fillStyle = value === null ? theme.mutedForeground : theme.foreground; + ctx.fillStyle = cellTextColor; ctx.font = value === null ? italicFont : tabularFont; setCanvasNumericVariant(ctx, value === null ? "normal" : "tabular-nums"); const reservedWidth = rightAlignedActionCell?.rowIndex === item.displayIndex && rightAlignedActionCell.visibleColIdx === visibleColIdx ? rightAlignedActionCell.reservedWidth : 0; @@ -545,7 +566,7 @@ export function drawCanvasDataGrid(options: DrawCanvasDataGridOptions) { if (item.isDeleted) { const boxX = alignCanvasPixel(drawX + (colWidth - BOOLEAN_CHECKBOX_SIZE) / 2, scaleX); const strikeY = alignCanvasPixel(y + CANVAS_DATA_GRID_ROW_HEIGHT / 2, scaleY); - ctx.strokeStyle = theme.foreground; + ctx.strokeStyle = cellTextColor; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(boxX - 1, strikeY); @@ -561,7 +582,7 @@ export function drawCanvasDataGrid(options: DrawCanvasDataGridOptions) { if (item.isDeleted && text) { const textWidth = ctx.measureText(text).width; const lineStartX = isBooleanNullCell ? anchorX - textWidth / 2 : isRightAlign ? textAnchorX - textWidth : textAnchorX; - ctx.strokeStyle = theme.foreground; + ctx.strokeStyle = cellTextColor; ctx.beginPath(); ctx.moveTo(lineStartX, textY); ctx.lineTo(alignCanvasPixel(lineStartX + textWidth, scaleX), textY); diff --git a/apps/desktop/src/lib/dataGrid/dataGridCellTextVisual.ts b/apps/desktop/src/lib/dataGrid/dataGridCellTextVisual.ts new file mode 100644 index 0000000000..00c44e9058 --- /dev/null +++ b/apps/desktop/src/lib/dataGrid/dataGridCellTextVisual.ts @@ -0,0 +1,50 @@ +import type { DataGridTypeVisualKind } from "@/lib/dataGrid/dataGridColumnType"; + +export type DataGridCellTextRole = "muted" | "neutral" | "type"; + +export interface DataGridCellTextVisualState { + colorizeTypes: boolean; + typeKind: DataGridTypeVisualKind; + isNull?: boolean; + isDraft?: boolean; + isEditing?: boolean; + isControl?: boolean; + isSelected?: boolean; + isCurrentSearchMatch?: boolean; + isSearchMatch?: boolean; + isDirty?: boolean; + isDeleted?: boolean; +} + +const DATA_GRID_TYPE_VISUAL_CLASSES: Record = { + integer: "data-grid-type-integer", + numeric: "data-grid-type-numeric", + string: "data-grid-type-string", + boolean: "data-grid-type-boolean", + temporal: "data-grid-type-temporal", + structured: "data-grid-type-structured", + identifier: "data-grid-type-identifier", + binary: "data-grid-type-binary", + spatial: "data-grid-type-spatial", + unknown: "text-muted-foreground", +}; + +export function dataGridTypeVisualClass(kind: DataGridTypeVisualKind): string { + return DATA_GRID_TYPE_VISUAL_CLASSES[kind]; +} + +/** Keep interaction and edit state more prominent than the optional type cue. */ +export function resolveDataGridCellTextRole(state: DataGridCellTextVisualState): DataGridCellTextRole { + if (state.isNull || state.isDraft) return "muted"; + if (!state.colorizeTypes || state.typeKind === "unknown" || state.isEditing || state.isControl || state.isSelected || state.isCurrentSearchMatch || state.isSearchMatch || state.isDirty || state.isDeleted) { + return "neutral"; + } + return "type"; +} + +export function dataGridCellTextClass(state: DataGridCellTextVisualState): string { + const role = resolveDataGridCellTextRole(state); + if (role === "muted") return "text-muted-foreground italic"; + if (role === "neutral") return "text-foreground"; + return dataGridTypeVisualClass(state.typeKind); +} diff --git a/apps/desktop/src/lib/dataGrid/dataGridColumnType.ts b/apps/desktop/src/lib/dataGrid/dataGridColumnType.ts index e754c3afbb..9569d757dc 100644 --- a/apps/desktop/src/lib/dataGrid/dataGridColumnType.ts +++ b/apps/desktop/src/lib/dataGrid/dataGridColumnType.ts @@ -1,3 +1,5 @@ +import type { DatabaseType } from "@/types/database"; + /** * Resolve the data type to display in a data-grid column header. * @@ -138,3 +140,133 @@ export function isNumericColumnType(dataType: string | undefined): boolean { const base = normalized.split(/[\s([]/, 1)[0]; return NUMERIC_COLUMN_TYPE_BASES.has(base); } + +export const DATA_GRID_TYPE_VISUAL_KINDS = ["integer", "numeric", "string", "boolean", "temporal", "structured", "identifier", "binary", "spatial", "unknown"] as const; + +export type DataGridTypeVisualKind = (typeof DATA_GRID_TYPE_VISUAL_KINDS)[number]; + +const INTEGER_COLUMN_TYPE_BASES = new Set([ + "tinyint", + "smallint", + "mediumint", + "int", + "integer", + "bigint", + "serial", + "smallserial", + "bigserial", + "int1", + "int2", + "int4", + "int8", + "int16", + "int32", + "int64", + "int128", + "int256", + "intn", + "uint", + "uint8", + "uint16", + "uint32", + "uint64", + "uint128", + "uint256", + "year", +]); + +const STRING_COLUMN_TYPE_BASES = new Set([ + "varchar", + "varchar2", + "nvarchar", + "nvarchar2", + "text", + "char", + "nchar", + "ntext", + "string", + "fixedstring", + "tinytext", + "mediumtext", + "longtext", + "clob", + "nclob", + "long", + "enum", + "enum8", + "enum16", + "set", + "character", + "character varying", + "national character", + "national character varying", +]); + +const BOOLEAN_COLUMN_TYPE_BASES = new Set(["bool", "boolean", "bit"]); +const TEMPORAL_COLUMN_TYPE_BASES = new Set(["date", "date32", "daten", "time", "time64", "timen", "timetz", "datetime", "datetime2", "datetime4", "datetime64", "datetimen", "datetimeoffset", "datetimeoffsetn", "smalldatetime", "timestamp", "timestampdty", "timestamptz", "interval"]); +const STRUCTURED_COLUMN_TYPE_BASES = new Set(["json", "jsonb", "jsonpath", "xml", "xmltype", "array", "map", "tuple", "struct", "row", "object", "document", "variant"]); +const IDENTIFIER_COLUMN_TYPE_BASES = new Set(["uuid", "uniqueidentifier", "rowid", "urowid"]); +const BINARY_COLUMN_TYPE_BASES = new Set(["bytea", "blob", "tinyblob", "mediumblob", "longblob", "binary", "varbinary", "image", "raw", "long raw", "bfile"]); +const SPATIAL_COLUMN_TYPE_BASES = new Set(["geometry", "geography", "sdo_geometry", "point", "linestring", "polygon", "multipoint", "multilinestring", "multipolygon", "geometrycollection"]); +const TYPE_VISUAL_TRANSPARENT_WRAPPERS = new Set(["nullable", "lowcardinality"]); + +function unwrapDataGridColumnType(dataType: string): { normalized: string; array: boolean } { + let normalized = dataType.trim().toLowerCase().replace(/\s+/g, " "); + let array = false; + while (normalized) { + if (/\[\s*\]$/.test(normalized)) { + array = true; + normalized = normalized.replace(/(?:\[\s*\])+$/, "").trim(); + continue; + } + const wrapper = normalized.match(/^([a-z][a-z0-9_]*)\s*\((.*)\)$/s); + if (!wrapper) break; + const wrapperName = wrapper[1] ?? ""; + if (wrapperName === "array") { + array = true; + normalized = wrapper[2]?.trim() ?? ""; + continue; + } + if (!TYPE_VISUAL_TRANSPARENT_WRAPPERS.has(wrapperName)) break; + normalized = wrapper[2]?.trim() ?? ""; + } + // PostgreSQL exposes array type names through Type.name() using its catalog + // convention (`_int4`, `_text`, `_jsonb`, including custom arrays). + if (normalized.length > 1 && normalized.startsWith("_")) array = true; + return { normalized, array }; +} + +function dataGridColumnTypeBase(dataType: string): { base: string; array: boolean } { + const unwrapped = unwrapDataGridColumnType(dataType); + let base = unwrapped.normalized.replace(/\s+unsigned\b/g, "").trim(); + const parameterStart = base.indexOf("("); + if (parameterStart >= 0) base = base.slice(0, parameterStart).trim(); + if (base.startsWith("timestamp with ")) base = "timestamptz"; + else if (base.startsWith("timestamp without ")) base = "timestamp"; + else if (base.startsWith("time with ")) base = "timetz"; + else if (base.startsWith("time without ")) base = "time"; + else if (base === "double precision") base = "double"; + return { base, array: unwrapped.array }; +} + +/** + * Collapse driver-specific SQL type names into the small semantic palette used + * by grid headers and values. Unknown types deliberately stay neutral. + */ +export function resolveDataGridTypeVisualKind(dataType: string | undefined, databaseType?: DatabaseType): DataGridTypeVisualKind { + if (!dataType?.trim()) return "unknown"; + const { base, array } = dataGridColumnTypeBase(dataType); + if (array) return "structured"; + if (databaseType === "sqlserver" && (base === "timestamp" || base === "rowversion")) return "binary"; + if (databaseType === "postgres" && (base === "bit" || base === "bit varying")) return "binary"; + if (INTEGER_COLUMN_TYPE_BASES.has(base)) return "integer"; + if (isNumericColumnType(dataType)) return "numeric"; + if (BOOLEAN_COLUMN_TYPE_BASES.has(base)) return "boolean"; + if (TEMPORAL_COLUMN_TYPE_BASES.has(base) || base.startsWith("timestamp_")) return "temporal"; + if (STRUCTURED_COLUMN_TYPE_BASES.has(base)) return "structured"; + if (IDENTIFIER_COLUMN_TYPE_BASES.has(base)) return "identifier"; + if (BINARY_COLUMN_TYPE_BASES.has(base)) return "binary"; + if (SPATIAL_COLUMN_TYPE_BASES.has(base)) return "spatial"; + if (STRING_COLUMN_TYPE_BASES.has(base)) return "string"; + return "unknown"; +} diff --git a/apps/desktop/src/lib/dataGrid/dataGridPaintTheme.ts b/apps/desktop/src/lib/dataGrid/dataGridPaintTheme.ts index cded9f8108..06330e82bb 100644 --- a/apps/desktop/src/lib/dataGrid/dataGridPaintTheme.ts +++ b/apps/desktop/src/lib/dataGrid/dataGridPaintTheme.ts @@ -1,3 +1,7 @@ +import type { DataGridTypeVisualKind } from "@/lib/dataGrid/dataGridColumnType"; + +export type DataGridTypeForegrounds = Record; + export interface DataGridPaintTheme { background: string; border: string; @@ -28,6 +32,7 @@ export interface DataGridPaintTheme { rowNumberTextNew: string; rowNumberTextEdited: string; rowNumberTextDeleted: string; + typeForegrounds: DataGridTypeForegrounds; } export const DATA_GRID_DARK_SEARCH_COLORS = { @@ -266,6 +271,18 @@ export function resolveDataGridPaintTheme(options: { getVar: (name: string) => s const rowNumberDeleted = isDark ? DATA_GRID_DARK_ROW_NUMBER_DELETED_BG : DATA_GRID_LIGHT_ROW_NUMBER_DELETED_BG; const rowNumberActive = activeSurface; const rowNumberSelected = isDark ? "rgb(30, 64, 96)" : "rgb(191, 219, 254)"; + const typeForegrounds: DataGridTypeForegrounds = { + integer: paintToken(getVar, "--data-grid-type-integer-fg", isDark ? "#93c5fd" : "#1d4ed8"), + numeric: paintToken(getVar, "--data-grid-type-numeric-fg", isDark ? "#67e8f9" : "#0e7490"), + string: paintToken(getVar, "--data-grid-type-string-fg", isDark ? "#86efac" : "#166534"), + boolean: paintToken(getVar, "--data-grid-type-boolean-fg", isDark ? "#fdba74" : "#c2410c"), + temporal: paintToken(getVar, "--data-grid-type-temporal-fg", isDark ? "#d8b4fe" : "#7e22ce"), + structured: paintToken(getVar, "--data-grid-type-structured-fg", isDark ? "#f9a8d4" : "#be185d"), + identifier: paintToken(getVar, "--data-grid-type-identifier-fg", isDark ? "#fcd34d" : "#92400e"), + binary: paintToken(getVar, "--data-grid-type-binary-fg", isDark ? "#fca5a5" : "#b91c1c"), + spatial: paintToken(getVar, "--data-grid-type-spatial-fg", isDark ? "#6ee7b7" : "#047857"), + unknown: foreground, + }; return { background, @@ -297,5 +314,10 @@ export function resolveDataGridPaintTheme(options: { getVar: (name: string) => s rowNumberTextNew: isDark ? "rgb(94, 233, 181)" : "rgb(0, 122, 85)", rowNumberTextEdited: isDark ? "rgb(255, 210, 48)" : "rgb(187, 77, 0)", rowNumberTextDeleted: destructive, + typeForegrounds, }; } + +export function dataGridTypeForeground(theme: Pick, kind: DataGridTypeVisualKind): string { + return theme.typeForegrounds[kind] ?? theme.foreground; +} diff --git a/apps/desktop/src/lib/settings/__tests__/editorSettingsDraft.spec.ts b/apps/desktop/src/lib/settings/__tests__/editorSettingsDraft.spec.ts index 65b049044d..673ced7003 100644 --- a/apps/desktop/src/lib/settings/__tests__/editorSettingsDraft.spec.ts +++ b/apps/desktop/src/lib/settings/__tests__/editorSettingsDraft.spec.ts @@ -60,6 +60,10 @@ describe("EDITOR_SETTINGS_DRAFT_KEYS", () => { expect(EDITOR_SETTINGS_DRAFT_KEYS).toContain("dataTabReuseMode"); }); + it("includes data grid type colors", () => { + expect(EDITOR_SETTINGS_DRAFT_KEYS).toContain("colorizeDataGridCellTypes"); + }); + it("includes completionTriggerMode", () => { expect(EDITOR_SETTINGS_DRAFT_KEYS).toContain("completionTriggerMode"); }); @@ -86,6 +90,10 @@ describe("editorSettingsDraftFromSettings", () => { expect(draft.continueOnErrorOnBatch).toBe(false); }); + it("maps the data grid type color preference from settings", () => { + expect(editorSettingsDraftFromSettings(makeSettings({ colorizeDataGridCellTypes: false })).colorizeDataGridCellTypes).toBe(false); + }); + it("preserves the table-open default for legacy settings", () => { const settings = makeSettings(); delete (settings as Partial).tableOpenPageSize; diff --git a/apps/desktop/src/lib/settings/__tests__/settingsSearch.spec.ts b/apps/desktop/src/lib/settings/__tests__/settingsSearch.spec.ts index 84fed15bd3..17c7ec11a2 100644 --- a/apps/desktop/src/lib/settings/__tests__/settingsSearch.spec.ts +++ b/apps/desktop/src/lib/settings/__tests__/settingsSearch.spec.ts @@ -137,6 +137,7 @@ describe("settings search", () => { { titleKey: "settings.confirmDangerousSqlExecution", category: "editor", targetId: "editor" }, { titleKey: "settings.continueOnErrorOnBatch", category: "editor", targetId: "editor" }, { titleKey: "settings.dataGridQuickEntry", category: "data", targetId: "data" }, + { titleKey: "settings.colorizeDataGridCellTypes", category: "data", targetId: "data" }, { titleKey: "transfer.dataTransfer", category: "appearance", targetId: "appearance" }, { titleKey: "toolbar.driverManager", category: "appearance", targetId: "appearance" }, { titleKey: "toolbar.theme", category: "appearance", targetId: "appearance" }, diff --git a/apps/desktop/src/lib/settings/editorSettingsDraft.ts b/apps/desktop/src/lib/settings/editorSettingsDraft.ts index 458c7b69a6..315d9d5440 100644 --- a/apps/desktop/src/lib/settings/editorSettingsDraft.ts +++ b/apps/desktop/src/lib/settings/editorSettingsDraft.ts @@ -31,6 +31,7 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [ "tabLayout", "showColumnCommentsInHeader", "showColumnTypesInHeader", + "colorizeDataGridCellTypes", "showIndexIndicatorsInHeader", "compactColumnHeaderActions", "dataGridQuickEntry", diff --git a/apps/desktop/src/lib/settings/settingsSearch.ts b/apps/desktop/src/lib/settings/settingsSearch.ts index 3b4036c9cf..2bcbdda420 100644 --- a/apps/desktop/src/lib/settings/settingsSearch.ts +++ b/apps/desktop/src/lib/settings/settingsSearch.ts @@ -173,6 +173,7 @@ export const SETTINGS_SEARCH_DEFINITIONS: readonly SettingsSearchDefinition[] = { id: "query-result-max-rows", category: "data", titleKey: "settings.queryResultMaxRows", descriptionKey: "settings.queryResultMaxRowsDescription", targetId: "data" }, { id: "data-grid-header-comments", category: "data", titleKey: "settings.showColumnCommentsInHeader", descriptionKey: "settings.showColumnCommentsInHeaderDescription", targetId: "data" }, { id: "data-grid-header-types", category: "data", titleKey: "settings.showColumnTypesInHeader", descriptionKey: "settings.showColumnTypesInHeaderDescription", targetId: "data" }, + { id: "data-grid-cell-type-colors", category: "data", titleKey: "settings.colorizeDataGridCellTypes", descriptionKey: "settings.colorizeDataGridCellTypesDescription", targetId: "data" }, { id: "data-grid-index-indicators", category: "data", titleKey: "settings.showIndexIndicatorsInHeader", descriptionKey: "settings.showIndexIndicatorsInHeaderDescription", targetId: "data" }, { id: "data-grid-compact-header-actions", category: "data", titleKey: "settings.compactColumnHeaderActions", descriptionKey: "settings.compactColumnHeaderActionsDescription", targetId: "data" }, { id: "data-grid-auto-total", category: "data", titleKey: "settings.autoCalculateTotalRows", descriptionKey: "settings.autoCalculateTotalRowsDescription", targetId: "data" }, diff --git a/apps/desktop/src/stores/__tests__/settingsStore.spec.ts b/apps/desktop/src/stores/__tests__/settingsStore.spec.ts index 20cabf1664..4d730854e9 100644 --- a/apps/desktop/src/stores/__tests__/settingsStore.spec.ts +++ b/apps/desktop/src/stores/__tests__/settingsStore.spec.ts @@ -5,6 +5,12 @@ import { DEFAULT_EDITOR_SETTINGS, EXECUTE_MODE_CURRENT_DEFAULT_VERSION, enforceR import type { AiConfigItem } from "@/types/ai"; describe("normalizeEditorSettings", () => { + it("keeps data type colors disabled by default and preserves an explicit opt-in", () => { + expect(normalizeEditorSettings({}).colorizeDataGridCellTypes).toBe(false); + expect(normalizeEditorSettings({ colorizeDataGridCellTypes: true }).colorizeDataGridCellTypes).toBe(true); + expect(normalizeEditorSettings({ colorizeDataGridCellTypes: false }).colorizeDataGridCellTypes).toBe(false); + }); + it("defaults and migrates the data-tab reuse mode", () => { expect(normalizeEditorSettings({}).dataTabReuseMode).toBe("same-table"); expect(normalizeEditorSettings({ dataTabReuseMode: "always-new" }).dataTabReuseMode).toBe("always-new"); diff --git a/apps/desktop/src/stores/settingsStore.ts b/apps/desktop/src/stores/settingsStore.ts index 5d36ce6cb8..29f925e30f 100644 --- a/apps/desktop/src/stores/settingsStore.ts +++ b/apps/desktop/src/stores/settingsStore.ts @@ -538,6 +538,7 @@ export interface EditorSettings { mongoViewMode: "document" | "table"; showColumnCommentsInHeader: boolean; showColumnTypesInHeader: boolean; + colorizeDataGridCellTypes: boolean; showIndexIndicatorsInHeader: boolean; compactColumnHeaderActions: boolean; columnWidthDensity: ColumnWidthDensity; @@ -729,6 +730,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = { mongoViewMode: "document", showColumnCommentsInHeader: true, showColumnTypesInHeader: true, + colorizeDataGridCellTypes: false, showIndexIndicatorsInHeader: true, compactColumnHeaderActions: true, columnWidthDensity: "standard", @@ -1065,6 +1067,7 @@ export function normalizeEditorSettings(settings: Partial, exist mongoViewMode: settings.mongoViewMode === "table" ? "table" : DEFAULT_EDITOR_SETTINGS.mongoViewMode, showColumnCommentsInHeader: settings.showColumnCommentsInHeader ?? DEFAULT_EDITOR_SETTINGS.showColumnCommentsInHeader, showColumnTypesInHeader: settings.showColumnTypesInHeader ?? DEFAULT_EDITOR_SETTINGS.showColumnTypesInHeader, + colorizeDataGridCellTypes: settings.colorizeDataGridCellTypes ?? DEFAULT_EDITOR_SETTINGS.colorizeDataGridCellTypes, showIndexIndicatorsInHeader: settings.showIndexIndicatorsInHeader ?? DEFAULT_EDITOR_SETTINGS.showIndexIndicatorsInHeader, compactColumnHeaderActions: settings.compactColumnHeaderActions ?? DEFAULT_EDITOR_SETTINGS.compactColumnHeaderActions, columnWidthDensity: normalizeColumnWidthDensity(settings.columnWidthDensity), @@ -1640,6 +1643,7 @@ export const useSettingsStore = defineStore("settings", () => { if (partial.mongoViewMode !== undefined) editorSettings.value.mongoViewMode = partial.mongoViewMode; if (partial.showColumnCommentsInHeader !== undefined) editorSettings.value.showColumnCommentsInHeader = partial.showColumnCommentsInHeader; if (partial.showColumnTypesInHeader !== undefined) editorSettings.value.showColumnTypesInHeader = partial.showColumnTypesInHeader; + if (partial.colorizeDataGridCellTypes !== undefined) editorSettings.value.colorizeDataGridCellTypes = partial.colorizeDataGridCellTypes === true; if (partial.showIndexIndicatorsInHeader !== undefined) editorSettings.value.showIndexIndicatorsInHeader = partial.showIndexIndicatorsInHeader; if (partial.compactColumnHeaderActions !== undefined) editorSettings.value.compactColumnHeaderActions = partial.compactColumnHeaderActions; if (partial.columnWidthDensity !== undefined) editorSettings.value.columnWidthDensity = normalizeColumnWidthDensity(partial.columnWidthDensity); diff --git a/apps/desktop/src/styles/globals.css b/apps/desktop/src/styles/globals.css index 827cd61527..832a463e6d 100644 --- a/apps/desktop/src/styles/globals.css +++ b/apps/desktop/src/styles/globals.css @@ -76,6 +76,68 @@ */ @import "./tokens.css"; +:root { + --data-grid-type-integer-fg: #1d4ed8; + --data-grid-type-numeric-fg: #0e7490; + --data-grid-type-string-fg: #166534; + --data-grid-type-boolean-fg: #c2410c; + --data-grid-type-temporal-fg: #7e22ce; + --data-grid-type-structured-fg: #be185d; + --data-grid-type-identifier-fg: #92400e; + --data-grid-type-binary-fg: #b91c1c; + --data-grid-type-spatial-fg: #047857; +} + +:root.dark { + --data-grid-type-integer-fg: #93c5fd; + --data-grid-type-numeric-fg: #67e8f9; + --data-grid-type-string-fg: #86efac; + --data-grid-type-boolean-fg: #fdba74; + --data-grid-type-temporal-fg: #d8b4fe; + --data-grid-type-structured-fg: #f9a8d4; + --data-grid-type-identifier-fg: #fcd34d; + --data-grid-type-binary-fg: #fca5a5; + --data-grid-type-spatial-fg: #6ee7b7; +} + +@layer components { + .data-grid-type-integer { + color: var(--data-grid-type-integer-fg); + } + + .data-grid-type-numeric { + color: var(--data-grid-type-numeric-fg); + } + + .data-grid-type-string { + color: var(--data-grid-type-string-fg); + } + + .data-grid-type-boolean { + color: var(--data-grid-type-boolean-fg); + } + + .data-grid-type-temporal { + color: var(--data-grid-type-temporal-fg); + } + + .data-grid-type-structured { + color: var(--data-grid-type-structured-fg); + } + + .data-grid-type-identifier { + color: var(--data-grid-type-identifier-fg); + } + + .data-grid-type-binary { + color: var(--data-grid-type-binary-fg); + } + + .data-grid-type-spatial { + color: var(--data-grid-type-spatial-fg); + } +} + :root[data-corner-style="large"] { --dbx-radius-default: 6px; --dbx-radius-sm: 6px;