feat(data-grid): add customizable data type colors

This commit is contained in:
kirito authored and GitHub committed 2026-08-18 23:05:17 +08:00
1 parent 95760e320a
commit e0448ac87e
25 files changed
+1209 -15

No files matched your search

+23
View File
@@ -101,6 +101,7 @@ import { safeLocalStorageGet, safeLocalStorageSet } from "@/lib/backend/safeStor
import { apiUrl, webPath } from "@/lib/common/webPath";
import { shouldBlockAppNativeSelectAll } from "@/lib/common/clipboard";
import { APP_FONT_SANS_CSS_VAR, DATA_GRID_FONT_FAMILY_CSS_VAR, DEFAULT_DATA_GRID_FONT_FAMILY, DEFAULT_UI_FONT_FAMILY } from "@/lib/app/appFonts";
import { DATA_GRID_TYPE_COLOR_KEYS, dataGridTypeColorCssVar, resolveActiveDataGridTypeColors } from "@/lib/dataGrid/dataGridTypeColorScheme";
import { rankSavedSqlHistory } from "@/lib/savedSql/savedSqlHistory";
import { savedSqlErrorMessage } from "@/lib/savedSql/savedSqlErrors";
import { savedSqlDefaultTargetForWrite } from "@/lib/savedSql/savedSqlExecutionTarget";
@@ -681,6 +682,20 @@ function applyDataGridFontFamily(fontFamily: string) {
document.documentElement.style.setProperty(DATA_GRID_FONT_FAMILY_CSS_VAR, fontFamily || DEFAULT_DATA_GRID_FONT_FAMILY);
}
// Both grid renderers read these variables, so overriding them here recolors the
// DOM classes and the canvas paint theme at once. Clearing them hands control
// back to the light/dark blocks in globals.css.
function applyDataGridTypeColors() {
if (typeof document === "undefined") return;
const style = document.documentElement.style;
const colors = resolveActiveDataGridTypeColors(settingsStore.editorSettings.dataGridTypeColorSchemes, settingsStore.editorSettings.activeDataGridTypeColorSchemeId);
for (const key of DATA_GRID_TYPE_COLOR_KEYS) {
const varName = dataGridTypeColorCssVar(key);
if (colors) style.setProperty(varName, colors[key]);
else style.removeProperty(varName);
}
}
const appUiFontFamilyStyle = computed<Record<string, string>>(() => {
const fontFamily = settingsStore.editorSettings.uiFontFamily || DEFAULT_UI_FONT_FAMILY;
return {
@@ -750,6 +765,14 @@ watch(
{ immediate: true },
);
watch(
[() => settingsStore.editorSettings.activeDataGridTypeColorSchemeId, () => settingsStore.editorSettings.dataGridTypeColorSchemes],
() => {
applyDataGridTypeColors();
},
{ immediate: true, deep: true },
);
watch(
[() => settingsStore.isEditorSettingsLoaded, () => settingsStore.editorSettings.toolbarItems.exclusiveRightSidebarPanels],
([loaded, exclusive]) => {
@@ -4,7 +4,7 @@ import type { Ref } from "vue";
import type { EditorView as EditorViewType } from "@codemirror/view";
import { useI18n } from "vue-i18n";
import { translateBackendError } from "@/i18n/backend-errors";
import { AlertTriangle, ArrowLeft, Check, CheckCircle2, CircleHelp, Cloud, Copy, Download, ExternalLink, GripVertical, Loader2, Moon, PackageSearch, Pencil, Plus, RefreshCw, RotateCcw, Search, Settings, Sun, SunMoon, Terminal, Trash2, Upload, X } from "@lucide/vue";
import { AlertTriangle, ArrowLeft, Check, CheckCircle2, CircleHelp, Cloud, Copy, Download, ExternalLink, GripVertical, Loader2, Moon, PackageSearch, Palette, Pencil, Plus, RefreshCw, RotateCcw, Search, Settings, Sun, SunMoon, Terminal, Trash2, Upload, X } from "@lucide/vue";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
@@ -58,6 +58,8 @@ import { orderAiConfigsForDisplay } from "@/lib/ai/aiConfigOrdering";
import { isAiConnectionTestConfigCurrent } from "@/lib/ai/aiConnectionTest";
import { MAX_AGENT_TURNS_DEFAULT, MAX_AGENT_TURNS_MAX, MAX_AGENT_TURNS_MIN, maxAgentTurnsOutOfRange, normalizeMaxAgentTurns } from "@/lib/ai/maxAgentTurns";
import ThemeCustomizerDialog from "./ThemeCustomizerDialog.vue";
import DataGridTypeColorSchemeDialog from "@/components/grid/DataGridTypeColorSchemeDialog.vue";
import { DATA_GRID_TYPE_COLOR_SCHEME_AUTO_ID, type DataGridTypeColorScheme } from "@/lib/dataGrid/dataGridTypeColorScheme";
import TunnelProfileManager from "@/components/connection/TunnelProfileManager.vue";
import DangerConfirmDialog from "./DangerConfirmDialog.vue";
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
@@ -305,7 +307,10 @@ const editUiScale = ref(settingsStore.editorSettings.uiScale);
const editTheme = ref(settingsStore.editorSettings.theme);
const editCustomThemes = ref<CustomTheme[]>([...settingsStore.editorSettings.customThemes]);
const editActiveCustomThemeId = ref(settingsStore.editorSettings.activeCustomThemeId);
const editDataGridTypeColorSchemes = ref<DataGridTypeColorScheme[]>(structuredClone(settingsStore.editorSettings.dataGridTypeColorSchemes));
const editActiveDataGridTypeColorSchemeId = ref(settingsStore.editorSettings.activeDataGridTypeColorSchemeId);
const showThemeCustomizer = ref(false);
const showDataGridTypeColorScheme = ref(false);
const editExecuteMode = ref(settingsStore.editorSettings.executeMode);
const editShortcuts = ref(normalizeShortcutSettings(settingsStore.editorSettings.shortcuts));
function translateWithExecuteShortcut(key: string): string {
@@ -533,6 +538,8 @@ function currentEditorSettingsDraft(): EditorSettingsDraft {
showColumnTypesInHeader: editShowColumnTypesInHeader.value,
dataGridShowTransposeFieldMetadata: editDataGridShowTransposeFieldMetadata.value,
colorizeDataGridCellTypes: editColorizeDataGridCellTypes.value,
dataGridTypeColorSchemes: editDataGridTypeColorSchemes.value,
activeDataGridTypeColorSchemeId: editActiveDataGridTypeColorSchemeId.value,
showIndexIndicatorsInHeader: editShowIndexIndicatorsInHeader.value,
compactColumnHeaderActions: editCompactColumnHeaderActions.value,
dataGridQuickEntry: editDataGridQuickEntry.value,
@@ -816,6 +823,8 @@ function syncEditorSettingsDraftFromStore() {
editShowColumnTypesInHeader.value = settingsStore.editorSettings.showColumnTypesInHeader;
editDataGridShowTransposeFieldMetadata.value = settingsStore.editorSettings.dataGridShowTransposeFieldMetadata;
editColorizeDataGridCellTypes.value = settingsStore.editorSettings.colorizeDataGridCellTypes;
editDataGridTypeColorSchemes.value = structuredClone(settingsStore.editorSettings.dataGridTypeColorSchemes);
editActiveDataGridTypeColorSchemeId.value = settingsStore.editorSettings.activeDataGridTypeColorSchemeId;
editShowIndexIndicatorsInHeader.value = settingsStore.editorSettings.showIndexIndicatorsInHeader;
editCompactColumnHeaderActions.value = settingsStore.editorSettings.compactColumnHeaderActions;
editDataGridQuickEntry.value = settingsStore.editorSettings.dataGridQuickEntry;
@@ -1098,6 +1107,8 @@ function resetDefaultsForTab(tab: SettingsCategory) {
editShowColumnTypesInHeader.value = DEFAULT_EDITOR_SETTINGS.showColumnTypesInHeader;
editDataGridShowTransposeFieldMetadata.value = DEFAULT_EDITOR_SETTINGS.dataGridShowTransposeFieldMetadata;
editColorizeDataGridCellTypes.value = DEFAULT_EDITOR_SETTINGS.colorizeDataGridCellTypes;
// Back to the built-in palette, but keep the user's saved schemes available.
editActiveDataGridTypeColorSchemeId.value = DEFAULT_EDITOR_SETTINGS.activeDataGridTypeColorSchemeId;
editShowIndexIndicatorsInHeader.value = DEFAULT_EDITOR_SETTINGS.showIndexIndicatorsInHeader;
editCompactColumnHeaderActions.value = DEFAULT_EDITOR_SETTINGS.compactColumnHeaderActions;
editDataGridQuickEntry.value = DEFAULT_EDITOR_SETTINGS.dataGridQuickEntry;
@@ -1172,6 +1183,8 @@ function resetAllDefaults() {
editShowColumnTypesInHeader.value = DEFAULT_EDITOR_SETTINGS.showColumnTypesInHeader;
editDataGridShowTransposeFieldMetadata.value = DEFAULT_EDITOR_SETTINGS.dataGridShowTransposeFieldMetadata;
editColorizeDataGridCellTypes.value = DEFAULT_EDITOR_SETTINGS.colorizeDataGridCellTypes;
// Reset the selection only; saved schemes survive a full settings reset.
editActiveDataGridTypeColorSchemeId.value = DEFAULT_EDITOR_SETTINGS.activeDataGridTypeColorSchemeId;
editShowIndexIndicatorsInHeader.value = DEFAULT_EDITOR_SETTINGS.showIndexIndicatorsInHeader;
editCompactColumnHeaderActions.value = DEFAULT_EDITOR_SETTINGS.compactColumnHeaderActions;
editDataGridQuickEntry.value = DEFAULT_EDITOR_SETTINGS.dataGridQuickEntry;
@@ -1409,6 +1422,16 @@ function handleThemeSave(updatedThemes: CustomTheme[], activeId: string) {
showThemeCustomizer.value = false;
}
function handleDataGridTypeColorSchemeChange(schemes: DataGridTypeColorScheme[], activeId: string) {
editDataGridTypeColorSchemes.value = structuredClone(schemes);
editActiveDataGridTypeColorSchemeId.value = activeId;
}
const activeDataGridTypeColorSchemeName = computed(() => {
if (editActiveDataGridTypeColorSchemeId.value === DATA_GRID_TYPE_COLOR_SCHEME_AUTO_ID) return t("settings.dataGridTypeColorSchemeAuto");
return editDataGridTypeColorSchemes.value.find((scheme) => scheme.id === editActiveDataGridTypeColorSchemeId.value)?.name ?? t("settings.dataGridTypeColorSchemeAuto");
});
function onDisconnectTabHandlingModeChange(v: any) {
if (v === "close-tabs" || v === "keep-tabs-clear-results" || v === "keep-tabs-keep-results") {
editDisconnectTabHandlingMode.value = v;
@@ -4960,6 +4983,21 @@ onUnmounted(() => {
<div class="text-sm font-medium text-muted-foreground">
{{ t("settings.dataGridDisplay") }}
</div>
<div data-settings-search-id="data-grid-type-colors" :class="['flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2', settingsSearchTargetClass('data-grid-type-colors')]">
<div class="space-y-1 min-w-0">
<Label>{{ t("settings.dataGridTypeColorScheme") }}</Label>
<p class="text-xs text-muted-foreground">
{{ t("settings.dataGridTypeColorSchemeDescription") }}
</p>
</div>
<div class="flex items-center gap-2 shrink-0">
<span class="text-xs text-muted-foreground">{{ activeDataGridTypeColorSchemeName }}</span>
<Button variant="outline" class="h-9 w-auto px-4" @click="showDataGridTypeColorScheme = true">
<Palette class="mr-2 h-4 w-4" />
{{ t("settings.dataGridTypeColorSchemeConfigure") }}
</Button>
</div>
</div>
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="data-grid-filter-view">
@@ -6990,6 +7028,7 @@ onUnmounted(() => {
<!-- Theme Customizer Dialog -->
<ThemeCustomizerDialog v-model:open="showThemeCustomizer" :themes="editCustomThemes" :active-theme-id="editActiveCustomThemeId" @save="handleThemeSave" />
<DataGridTypeColorSchemeDialog v-model:open="showDataGridTypeColorScheme" :schemes="editDataGridTypeColorSchemes" :active-scheme-id="editActiveDataGridTypeColorSchemeId" @change="handleDataGridTypeColorSchemeChange" />
<!-- Snippet Add/Edit Dialog -->
<Dialog :open="snippetDialogOpen" @update:open="snippetDialogOpen = $event">
@@ -88,6 +88,7 @@ import { uuid } from "@/lib/common/utils";
import { generateCellValues, type CellValueGenerationKind } from "@/lib/dataGrid/cellValueGeneration";
import { compactHeaderColumnType, isNumericColumnType, resolveDataGridTypeVisualKind, resolveHeaderColumnType, resolveResultColumnType } from "@/lib/dataGrid/dataGridColumnType";
import { dataGridCellTextClass, dataGridTypeVisualClass } from "@/lib/dataGrid/dataGridCellTextVisual";
import { DATA_GRID_TYPE_COLOR_KEYS, resolveActiveDataGridTypeColors } from "@/lib/dataGrid/dataGridTypeColorScheme";
import {
canDeleteExistingTdengineRows,
canEditExistingTableRows,
@@ -5899,7 +5900,14 @@ const canvasSurfaceWidth = computed(() => {
if (vw <= 0) return total;
return Math.min(vw, total);
});
const canvasRenderStyleKey = computed(() => `${settingsStore.editorSettings.theme}:${settingsStore.editorSettings.uiScale}:${canvasBackingPixelRatio.value}:${isDark.value}:${themePalette.value}:${tableFontFamily.value}:${tableFontSize.value}:${!!saveError.value}`);
// The canvas render state caches the resolved paint theme by this key, so the
// active type palette has to take part or a recolor never repaints.
const dataGridTypeColorKey = computed(() => {
const settings = settingsStore.editorSettings;
const colors = resolveActiveDataGridTypeColors(settings.dataGridTypeColorSchemes, settings.activeDataGridTypeColorSchemeId);
return colors ? DATA_GRID_TYPE_COLOR_KEYS.map((key) => colors[key]).join(",") : "auto";
});
const canvasRenderStyleKey = computed(() => `${settingsStore.editorSettings.theme}:${settingsStore.editorSettings.uiScale}:${canvasBackingPixelRatio.value}:${isDark.value}:${themePalette.value}:${tableFontFamily.value}:${tableFontSize.value}:${!!saveError.value}:${dataGridTypeColorKey.value}`);
const CANVAS_MOUSE_WHEEL_SCROLL_MULTIPLIER = 1.5;
const CANVAS_TRACKPAD_DELTA_THRESHOLD = 40;
let canvasPixelRatioMediaQuery: MediaQueryList | null = null;
@@ -0,0 +1,230 @@
<script setup lang="ts">
import { computed, ref, watch } from "vue";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import ColorSpectrumPicker from "@/components/ui/ColorSpectrumPicker.vue";
import { Copy, Pencil, Plus, RotateCcw, Trash2 } from "@lucide/vue";
import { useI18n } from "vue-i18n";
import { useTheme } from "@/composables/useTheme";
import { DATA_GRID_TYPE_COLOR_SCHEME_AUTO_ID, type DataGridTypeColorKey, type DataGridTypeColorScheme, defaultDataGridTypeColors, normalizeDataGridTypeColors } from "@/lib/dataGrid/dataGridTypeColorScheme";
interface Props {
open: boolean;
schemes: DataGridTypeColorScheme[];
activeSchemeId: string;
}
const props = defineProps<Props>();
const emit = defineEmits<{
(e: "update:open", value: boolean): void;
(e: "change", schemes: DataGridTypeColorScheme[], activeId: string): void;
}>();
const { t } = useI18n();
const { isDark } = useTheme();
const localSchemes = ref<DataGridTypeColorScheme[]>([]);
const activeEditId = ref(DATA_GRID_TYPE_COLOR_SCHEME_AUTO_ID);
const renamingId = ref<string | null>(null);
const renamingName = ref("");
function cloneSchemes(schemes: DataGridTypeColorScheme[]): DataGridTypeColorScheme[] {
return schemes.map((scheme) => ({ ...scheme, colors: { ...scheme.colors } }));
}
// Everything below edits this buffer only. Nothing reaches the settings store
// until Done, so Cancel just drops it.
watch(
() => props.open,
(isOpen) => {
if (!isOpen) return;
localSchemes.value = cloneSchemes(props.schemes);
activeEditId.value = props.activeSchemeId;
renamingId.value = null;
},
);
const typeColorItems: { key: DataGridTypeColorKey; labelKey: string; sample: string }[] = [
{ key: "integer", labelKey: "settings.dataGridTypeColorInteger", sample: "42" },
{ key: "numeric", labelKey: "settings.dataGridTypeColorNumeric", sample: "3.14" },
{ key: "string", labelKey: "settings.dataGridTypeColorString", sample: "'text'" },
{ key: "boolean", labelKey: "settings.dataGridTypeColorBoolean", sample: "true" },
{ key: "temporal", labelKey: "settings.dataGridTypeColorTemporal", sample: "2026-08-17" },
{ key: "structured", labelKey: "settings.dataGridTypeColorStructured", sample: '{"a": 1}' },
{ key: "identifier", labelKey: "settings.dataGridTypeColorIdentifier", sample: "9f8e-4c1a" },
{ key: "binary", labelKey: "settings.dataGridTypeColorBinary", sample: "0x1F8B" },
{ key: "spatial", labelKey: "settings.dataGridTypeColorSpatial", sample: "POINT(0 0)" },
];
/** The auto entry is a sentinel rather than a stored scheme, so it has no editable colors. */
const isFollowingTheme = computed(() => activeEditId.value === DATA_GRID_TYPE_COLOR_SCHEME_AUTO_ID);
const activeScheme = computed(() => localSchemes.value.find((scheme) => scheme.id === activeEditId.value));
// Falls back to the built-in palette so the auto entry still previews real colors.
const displayedColors = computed(() => activeScheme.value?.colors ?? defaultDataGridTypeColors(isDark.value));
function updateColor(key: DataGridTypeColorKey, value: string) {
const scheme = activeScheme.value;
if (!scheme) return;
const trimmed = value.trim();
if (!/^#[0-9a-fA-F]{6}$/.test(trimmed)) return;
scheme.colors = { ...scheme.colors, [key]: trimmed.toLowerCase() };
}
function nextSchemeName(): string {
return `${t("settings.dataGridTypeColorSchemeDefaultName")} ${localSchemes.value.length + 1}`;
}
function selectScheme(id: string) {
activeEditId.value = id;
}
function addScheme() {
const id = `type-colors-${Date.now()}`;
// Seed from whatever is on screen so a new scheme starts as a visible tweak.
localSchemes.value.push({ id, name: nextSchemeName(), colors: { ...displayedColors.value } });
activeEditId.value = id;
}
function duplicateScheme() {
const scheme = activeScheme.value;
if (!scheme) return;
const id = `type-colors-${Date.now()}`;
localSchemes.value.push({ id, name: `${scheme.name}${t("settings.dataGridTypeColorSchemeCopySuffix")}`, colors: { ...scheme.colors } });
activeEditId.value = id;
}
function deleteScheme() {
const scheme = activeScheme.value;
if (!scheme) return;
localSchemes.value = localSchemes.value.filter((entry) => entry.id !== scheme.id);
activeEditId.value = DATA_GRID_TYPE_COLOR_SCHEME_AUTO_ID;
}
function resetScheme() {
const scheme = activeScheme.value;
if (!scheme) return;
scheme.colors = { ...defaultDataGridTypeColors(isDark.value) };
}
function startRename() {
const scheme = activeScheme.value;
if (!scheme) return;
renamingId.value = scheme.id;
renamingName.value = scheme.name;
}
function confirmRename() {
const scheme = localSchemes.value.find((entry) => entry.id === renamingId.value);
const name = renamingName.value.trim();
if (scheme && name) scheme.name = name;
renamingId.value = null;
}
/** The single commit point: the edit buffer becomes the applied settings. */
function handleDone() {
emit(
"change",
cloneSchemes(localSchemes.value).map((scheme) => ({ ...scheme, colors: normalizeDataGridTypeColors(scheme.colors) })),
activeEditId.value,
);
emit("update:open", false);
}
/** Drops the edit buffer; the store never saw any of it. */
function handleCancel() {
emit("update:open", false);
}
</script>
<template>
<Dialog :open="props.open" @update:open="emit('update:open', $event)">
<DialogContent class="max-w-2xl">
<DialogHeader>
<DialogTitle>{{ t("settings.dataGridTypeColorSchemeTitle") }}</DialogTitle>
</DialogHeader>
<div class="flex flex-col gap-4">
<p class="text-xs text-muted-foreground">
{{ t("settings.dataGridTypeColorSchemeDescription") }}
</p>
<!-- Scheme selector + management -->
<div class="flex items-end gap-2">
<div class="flex-1 min-w-0 space-y-2">
<label class="text-sm font-medium">{{ t("settings.dataGridTypeColorScheme") }}</label>
<Select :model-value="activeEditId" @update:model-value="selectScheme(String($event))">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem :value="DATA_GRID_TYPE_COLOR_SCHEME_AUTO_ID">
{{ t("settings.dataGridTypeColorSchemeAuto") }}
</SelectItem>
<SelectItem v-for="scheme in localSchemes" :key="scheme.id" :value="scheme.id">
{{ scheme.name }}
</SelectItem>
</SelectContent>
</Select>
</div>
<Button variant="outline" size="sm" class="h-9" :title="t('settings.dataGridTypeColorSchemeAdd')" @click="addScheme">
<Plus class="h-4 w-4" />
</Button>
<Button variant="outline" size="sm" class="h-9" :disabled="isFollowingTheme" :title="t('settings.dataGridTypeColorSchemeDuplicate')" @click="duplicateScheme">
<Copy class="h-4 w-4" />
</Button>
<Button variant="outline" size="sm" class="h-9" :disabled="isFollowingTheme" :title="t('settings.dataGridTypeColorSchemeRename')" @click="startRename">
<Pencil class="h-4 w-4" />
</Button>
<Button variant="outline" size="sm" class="h-9" :disabled="isFollowingTheme" :title="t('settings.dataGridTypeColorSchemeDelete')" @click="deleteScheme">
<Trash2 class="h-4 w-4" />
</Button>
</div>
<div v-if="renamingId" class="flex items-center gap-2">
<Input v-model="renamingName" class="h-8" @keydown.enter="confirmRename" />
<Button size="sm" @click="confirmRename">{{ t("settings.dataGridTypeColorSchemeRenameConfirm") }}</Button>
</div>
<div v-if="isFollowingTheme" class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<p class="text-xs text-muted-foreground">
{{ t("settings.dataGridTypeColorSchemeAutoHint") }}
</p>
<Button variant="outline" size="sm" class="shrink-0" @click="addScheme">
{{ t("settings.dataGridTypeColorSchemeCreateFromCurrent") }}
</Button>
</div>
<!-- Color rows -->
<div class="grid gap-2 sm:grid-cols-2" :class="{ 'opacity-60': isFollowingTheme }">
<div v-for="item in typeColorItems" :key="item.key" class="flex items-center gap-2 rounded-md border px-2 py-1.5">
<div class="min-w-0 flex-1">
<div class="truncate text-xs font-medium">{{ t(item.labelKey) }}</div>
<div class="truncate font-mono text-xs" :style="{ color: displayedColors[item.key] }">{{ item.sample }}</div>
</div>
<input
type="text"
class="w-[74px] shrink-0 rounded border bg-transparent px-1.5 py-0.5 font-mono text-xs disabled:cursor-not-allowed"
:value="displayedColors[item.key]"
:disabled="isFollowingTheme"
spellcheck="false"
@change="updateColor(item.key, ($event.target as HTMLInputElement).value)"
/>
<ColorSpectrumPicker :model-value="displayedColors[item.key]" :disabled="isFollowingTheme" :label="t(item.labelKey)" @update:model-value="updateColor(item.key, $event)" />
</div>
</div>
</div>
<DialogFooter class="gap-2">
<Button variant="outline" :disabled="isFollowingTheme" class="mr-auto" @click="resetScheme">
<RotateCcw class="mr-2 h-4 w-4" />
{{ t("settings.dataGridTypeColorSchemeReset") }}
</Button>
<Button variant="outline" @click="handleCancel">{{ t("settings.cancel") }}</Button>
<Button @click="handleDone">{{ t("settings.dataGridTypeColorSchemeDone") }}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>
@@ -0,0 +1,168 @@
<script setup lang="ts">
import { computed, onBeforeUnmount, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { Button } from "@/components/ui/button";
import { hexToHsv, hsvToHex } from "@/lib/common/color";
interface Props {
modelValue: string;
disabled?: boolean;
/** Shown next to the live hex readout so a row can label what is being picked. */
label?: string;
}
const props = defineProps<Props>();
const emit = defineEmits<{ (e: "update:modelValue", value: string): void }>();
const { t } = useI18n();
const open = ref(false);
const rootRef = ref<HTMLElement | null>(null);
const spectrumRef = ref<HTMLElement | null>(null);
const hueRef = ref<HTMLElement | null>(null);
const hue = ref(0);
const saturation = ref(0);
const value = ref(0);
// Non-null only while the cursor is over a gradient, so the readout can track it
// without disturbing the committed color.
const previewHex = ref<string | null>(null);
const currentHex = computed(() => hsvToHex({ h: hue.value, s: saturation.value, v: value.value }));
// Hovering wins so the cursor can survey colors, otherwise show the pending pick.
const readoutHex = computed(() => previewHex.value ?? currentHex.value);
function syncFromModel() {
const hsv = hexToHsv(props.modelValue);
if (!hsv) return;
// A greyscale color carries no usable hue, so keep the slider where the user left it.
if (hsv.s > 0) hue.value = hsv.h;
saturation.value = hsv.s;
value.value = hsv.v;
}
watch(() => props.modelValue, syncFromModel, { immediate: true });
function ratioWithin(element: HTMLElement, event: PointerEvent | MouseEvent): { x: number; y: number } {
const rect = element.getBoundingClientRect();
return {
x: Math.min(1, Math.max(0, (event.clientX - rect.left) / rect.width)),
y: Math.min(1, Math.max(0, (event.clientY - rect.top) / rect.height)),
};
}
function spectrumHexAt(event: PointerEvent | MouseEvent): string | null {
if (!spectrumRef.value) return null;
const { x, y } = ratioWithin(spectrumRef.value, event);
return hsvToHex({ h: hue.value, s: x, v: 1 - y });
}
function hueHexAt(event: PointerEvent | MouseEvent): { hue: number; hex: string } | null {
if (!hueRef.value) return null;
const { x } = ratioWithin(hueRef.value, event);
const nextHue = x * 360;
return { hue: nextHue, hex: hsvToHex({ h: nextHue, s: saturation.value || 1, v: value.value || 1 }) };
}
function onSpectrumMove(event: PointerEvent) {
previewHex.value = spectrumHexAt(event);
// Dragging with the button held keeps moving the pending marker; nothing is
// handed to the parent until the confirm button is pressed.
if (event.buttons === 1) onSpectrumPick(event);
}
function onSpectrumPick(event: PointerEvent | MouseEvent) {
if (!spectrumRef.value) return;
const { x, y } = ratioWithin(spectrumRef.value, event);
saturation.value = x;
value.value = 1 - y;
}
function onHueMove(event: PointerEvent) {
previewHex.value = hueHexAt(event)?.hex ?? null;
if (event.buttons === 1) onHuePick(event);
}
function onHuePick(event: PointerEvent | MouseEvent) {
const picked = hueHexAt(event);
if (!picked) return;
hue.value = picked.hue;
if (saturation.value === 0) saturation.value = 1;
if (value.value === 0) value.value = 1;
}
function clearPreview() {
previewHex.value = null;
}
/** The only path that hands a value to the parent. */
function confirmPick() {
emit("update:modelValue", currentHex.value);
open.value = false;
}
function cancelPick() {
open.value = false;
}
function onHexInput(raw: string) {
const trimmed = raw.trim();
if (!/^#?[0-9a-fA-F]{6}$/.test(trimmed)) return;
const hex = trimmed.startsWith("#") ? trimmed.toLowerCase() : `#${trimmed.toLowerCase()}`;
const hsv = hexToHsv(hex);
if (!hsv) return;
if (hsv.s > 0) hue.value = hsv.h;
saturation.value = hsv.s;
value.value = hsv.v;
}
function onDocumentPointerDown(event: PointerEvent) {
if (!open.value || !rootRef.value) return;
if (!rootRef.value.contains(event.target as Node)) open.value = false;
}
watch(open, (isOpen) => {
if (typeof document === "undefined") return;
if (isOpen) {
syncFromModel();
document.addEventListener("pointerdown", onDocumentPointerDown, true);
} else {
clearPreview();
document.removeEventListener("pointerdown", onDocumentPointerDown, true);
}
});
onBeforeUnmount(() => {
if (typeof document !== "undefined") document.removeEventListener("pointerdown", onDocumentPointerDown, true);
});
</script>
<template>
<div ref="rootRef" class="relative">
<button type="button" class="h-6 w-6 shrink-0 rounded border border-border/60 disabled:cursor-not-allowed disabled:opacity-60" :style="{ backgroundColor: props.modelValue }" :disabled="props.disabled" :aria-label="props.label" @click="open = !open" />
<div v-if="open" class="absolute right-0 top-full z-50 mt-1 w-[224px] rounded-lg border bg-popover p-2 shadow-lg" @pointerleave="clearPreview">
<!-- Saturation / value gradient for the active hue -->
<div ref="spectrumRef" class="relative h-[132px] w-full cursor-crosshair rounded" :style="{ backgroundColor: `hsl(${hue}, 100%, 50%)` }" @pointermove="onSpectrumMove" @pointerdown.prevent="onSpectrumPick" @pointerleave="clearPreview">
<div class="pointer-events-none absolute inset-0 rounded" style="background: linear-gradient(to right, #fff, rgba(255, 255, 255, 0))" />
<div class="pointer-events-none absolute inset-0 rounded" style="background: linear-gradient(to top, #000, rgba(0, 0, 0, 0))" />
<div class="pointer-events-none absolute h-3 w-3 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-white shadow" :style="{ left: `${saturation * 100}%`, top: `${(1 - value) * 100}%` }" />
</div>
<!-- Hue strip -->
<div ref="hueRef" class="relative mt-2 h-3 w-full cursor-crosshair rounded" style="background: linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%)" @pointermove="onHueMove" @pointerdown.prevent="onHuePick" @pointerleave="clearPreview">
<div class="pointer-events-none absolute top-1/2 h-4 w-1.5 -translate-x-1/2 -translate-y-1/2 rounded-sm border border-white shadow" :style="{ left: `${(hue / 360) * 100}%` }" />
</div>
<!-- Live readout: follows the cursor, falls back to the pending pick -->
<div class="mt-2 flex items-center gap-2">
<div class="h-6 w-6 shrink-0 rounded border border-border/60" :style="{ backgroundColor: readoutHex }" />
<input type="text" class="w-full min-w-0 rounded border bg-transparent px-1.5 py-0.5 font-mono text-xs" :value="readoutHex" spellcheck="false" @change="onHexInput(($event.target as HTMLInputElement).value)" />
</div>
<div class="mt-2 flex items-center justify-end gap-1.5">
<Button variant="outline" size="sm" class="h-7 px-2 text-xs" @click="cancelPick">{{ t("settings.cancel") }}</Button>
<Button size="sm" class="h-7 px-2 text-xs" @click="confirmPick">{{ t("common.confirm") }}</Button>
</div>
</div>
</div>
</template>
+25
View File
@@ -5889,6 +5889,31 @@ export default {
dataGridShowTransposeFieldMetadataDescription: "Display enabled column types and comments on separate lines in transpose view. This increases row height.",
colorizeDataGridCellTypes: "Color-code data grid values",
colorizeDataGridCellTypesDescription: "Use subtle, theme-aware text colors to distinguish values by their column data type.",
dataGridTypeColorScheme: "Data type color scheme",
dataGridTypeColorSchemeDescription: "Choose which palette the grid uses to color-code values by data type.",
dataGridTypeColorSchemeConfigure: "Customize",
dataGridTypeColorSchemeTitle: "Data type colors",
dataGridTypeColorSchemeAuto: "Follow theme",
dataGridTypeColorSchemeAutoHint: "Built-in colors are used, switching automatically between the light and dark palettes.",
dataGridTypeColorSchemeCreateFromCurrent: "Create scheme from these colors",
dataGridTypeColorSchemeAdd: "New scheme",
dataGridTypeColorSchemeDuplicate: "Duplicate scheme",
dataGridTypeColorSchemeRename: "Rename scheme",
dataGridTypeColorSchemeRenameConfirm: "Rename",
dataGridTypeColorSchemeDelete: "Delete scheme",
dataGridTypeColorSchemeReset: "Reset colors",
dataGridTypeColorSchemeDone: "Done",
dataGridTypeColorSchemeDefaultName: "Color scheme",
dataGridTypeColorSchemeCopySuffix: " Copy",
dataGridTypeColorInteger: "Integer",
dataGridTypeColorNumeric: "Decimal",
dataGridTypeColorString: "Text",
dataGridTypeColorBoolean: "Boolean",
dataGridTypeColorTemporal: "Date & time",
dataGridTypeColorStructured: "Structured",
dataGridTypeColorIdentifier: "Identifier",
dataGridTypeColorBinary: "Binary",
dataGridTypeColorSpatial: "Spatial",
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",
+25
View File
@@ -5630,6 +5630,31 @@ export default withEnglishFallback({
dataGridShowTransposeFieldMetadataDescription: "Muestra los tipos de columna y comentarios habilitados en líneas separadas en la vista transpuesta. Esto aumenta la altura de las filas.",
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.",
dataGridTypeColorScheme: "Esquema de color por tipo de dato",
dataGridTypeColorSchemeDescription: "Elige la paleta que usa la tabla para colorear los valores según su tipo de dato.",
dataGridTypeColorSchemeConfigure: "Personalizar",
dataGridTypeColorSchemeTitle: "Colores por tipo de dato",
dataGridTypeColorSchemeAuto: "Seguir el tema",
dataGridTypeColorSchemeAutoHint: "Se usan los colores integrados, alternando automáticamente entre las paletas clara y oscura.",
dataGridTypeColorSchemeCreateFromCurrent: "Crear esquema con estos colores",
dataGridTypeColorSchemeAdd: "Nuevo esquema",
dataGridTypeColorSchemeDuplicate: "Duplicar esquema",
dataGridTypeColorSchemeRename: "Cambiar nombre del esquema",
dataGridTypeColorSchemeRenameConfirm: "Cambiar nombre",
dataGridTypeColorSchemeDelete: "Eliminar esquema",
dataGridTypeColorSchemeReset: "Restablecer colores",
dataGridTypeColorSchemeDone: "Listo",
dataGridTypeColorSchemeDefaultName: "Esquema de color",
dataGridTypeColorSchemeCopySuffix: " Copia",
dataGridTypeColorInteger: "Entero",
dataGridTypeColorNumeric: "Decimal",
dataGridTypeColorString: "Texto",
dataGridTypeColorBoolean: "Booleano",
dataGridTypeColorTemporal: "Fecha y hora",
dataGridTypeColorStructured: "Estructurado",
dataGridTypeColorIdentifier: "Identificador",
dataGridTypeColorBinary: "Binario",
dataGridTypeColorSpatial: "Espacial",
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",
+25
View File
@@ -5630,6 +5630,31 @@ export default withEnglishFallback({
dataGridShowTransposeFieldMetadataDescription: "Visualizza i tipi di colonna e i commenti abilitati su righe separate nella vista trasposta. Questo aumenta l'altezza delle righe.",
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.",
dataGridTypeColorScheme: "Schema colori per tipo di dato",
dataGridTypeColorSchemeDescription: "Scegli la palette usata dalla griglia per colorare i valori in base al tipo di dato.",
dataGridTypeColorSchemeConfigure: "Personalizza",
dataGridTypeColorSchemeTitle: "Colori per tipo di dato",
dataGridTypeColorSchemeAuto: "Segui il tema",
dataGridTypeColorSchemeAutoHint: "Vengono usati i colori integrati, alternando automaticamente le palette chiara e scura.",
dataGridTypeColorSchemeCreateFromCurrent: "Crea schema da questi colori",
dataGridTypeColorSchemeAdd: "Nuovo schema",
dataGridTypeColorSchemeDuplicate: "Duplica schema",
dataGridTypeColorSchemeRename: "Rinomina schema",
dataGridTypeColorSchemeRenameConfirm: "Rinomina",
dataGridTypeColorSchemeDelete: "Elimina schema",
dataGridTypeColorSchemeReset: "Ripristina colori",
dataGridTypeColorSchemeDone: "Fatto",
dataGridTypeColorSchemeDefaultName: "Schema colori",
dataGridTypeColorSchemeCopySuffix: " Copia",
dataGridTypeColorInteger: "Intero",
dataGridTypeColorNumeric: "Decimale",
dataGridTypeColorString: "Testo",
dataGridTypeColorBoolean: "Booleano",
dataGridTypeColorTemporal: "Data e ora",
dataGridTypeColorStructured: "Strutturato",
dataGridTypeColorIdentifier: "Identificatore",
dataGridTypeColorBinary: "Binario",
dataGridTypeColorSpatial: "Spaziale",
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",
+25
View File
@@ -5665,6 +5665,31 @@ export default withEnglishFallback({
dataGridShowTransposeFieldMetadataDescription: "有効なデータ型とコメントを転置ビューの別行に表示します。有効にすると行の高さが増えます。",
colorizeDataGridCellTypes: "データ型に応じて値を色分け",
colorizeDataGridCellTypesDescription: "テーマに合わせた控えめな文字色で、列のデータ型ごとに値を区別します。",
dataGridTypeColorScheme: "データ型カラースキーム",
dataGridTypeColorSchemeDescription: "データ型に応じて値を色分けする際に使用するパレットを選択します。",
dataGridTypeColorSchemeConfigure: "カスタマイズ",
dataGridTypeColorSchemeTitle: "データ型の配色",
dataGridTypeColorSchemeAuto: "テーマに従う",
dataGridTypeColorSchemeAutoHint: "組み込みの配色を使用し、ライトとダークのパレットを自動的に切り替えます。",
dataGridTypeColorSchemeCreateFromCurrent: "現在の配色からスキームを作成",
dataGridTypeColorSchemeAdd: "スキームを追加",
dataGridTypeColorSchemeDuplicate: "スキームを複製",
dataGridTypeColorSchemeRename: "スキーム名を変更",
dataGridTypeColorSchemeRenameConfirm: "名前を変更",
dataGridTypeColorSchemeDelete: "スキームを削除",
dataGridTypeColorSchemeReset: "配色を既定に戻す",
dataGridTypeColorSchemeDone: "完了",
dataGridTypeColorSchemeDefaultName: "カラースキーム",
dataGridTypeColorSchemeCopySuffix: " のコピー",
dataGridTypeColorInteger: "整数",
dataGridTypeColorNumeric: "小数",
dataGridTypeColorString: "テキスト",
dataGridTypeColorBoolean: "真偽値",
dataGridTypeColorTemporal: "日時",
dataGridTypeColorStructured: "構造化",
dataGridTypeColorIdentifier: "識別子",
dataGridTypeColorBinary: "バイナリ",
dataGridTypeColorSpatial: "空間",
showIndexIndicatorsInHeader: "列名の横にインデックス指標を表示",
showIndexIndicatorsInHeaderDescription: "グリッド列ヘッダーにインデックスの種類をアイコンで表示します:主キー(オレンジ)、一意インデックス(赤)、通常インデックス(緑)。",
compactColumnHeaderActions: "列ヘッダーツールをコンパクトに",
+25
View File
@@ -5342,6 +5342,31 @@ export default withEnglishFallback({
dataGridShowTransposeFieldMetadataDescription: "활성화된 데이터 타입과 주석을 전치 보기의 별도 줄에 표시합니다. 활성화하면 행 높이가 증가합니다.",
colorizeDataGridCellTypes: "데이터 타입별 값 색상 표시",
colorizeDataGridCellTypesDescription: "테마에 맞는 은은한 텍스트 색상으로 컬럼 데이터 타입에 따라 값을 구분합니다.",
dataGridTypeColorScheme: "데이터 타입 색상 구성",
dataGridTypeColorSchemeDescription: "데이터 타입에 따라 값을 색으로 구분할 때 사용할 팔레트를 선택합니다.",
dataGridTypeColorSchemeConfigure: "사용자 지정",
dataGridTypeColorSchemeTitle: "데이터 타입 색상",
dataGridTypeColorSchemeAuto: "테마 따르기",
dataGridTypeColorSchemeAutoHint: "기본 색상을 사용하며 라이트와 다크 팔레트를 자동으로 전환합니다.",
dataGridTypeColorSchemeCreateFromCurrent: "현재 색상으로 구성 만들기",
dataGridTypeColorSchemeAdd: "새 구성",
dataGridTypeColorSchemeDuplicate: "구성 복제",
dataGridTypeColorSchemeRename: "구성 이름 바꾸기",
dataGridTypeColorSchemeRenameConfirm: "이름 바꾸기",
dataGridTypeColorSchemeDelete: "구성 삭제",
dataGridTypeColorSchemeReset: "색상 초기화",
dataGridTypeColorSchemeDone: "완료",
dataGridTypeColorSchemeDefaultName: "색상 구성",
dataGridTypeColorSchemeCopySuffix: " 복사본",
dataGridTypeColorInteger: "정수",
dataGridTypeColorNumeric: "소수",
dataGridTypeColorString: "텍스트",
dataGridTypeColorBoolean: "불리언",
dataGridTypeColorTemporal: "날짜/시간",
dataGridTypeColorStructured: "구조화",
dataGridTypeColorIdentifier: "식별자",
dataGridTypeColorBinary: "바이너리",
dataGridTypeColorSpatial: "공간",
showIndexIndicatorsInHeader: "컬럼 이름 옆에 인덱스 표시",
showIndexIndicatorsInHeaderDescription: "그리드 컬럼 헤더에 인덱스 유형을 아이콘으로 표시합니다: 기본키(주황색), 고유 인덱스(빨간색), 일반 인덱스(녹색).",
compactColumnHeaderActions: "컴팩트한 컬럼 머리글 도구",
+25
View File
@@ -5632,6 +5632,31 @@ export default withEnglishFallback({
dataGridShowTransposeFieldMetadataDescription: "Exibir tipos de coluna e comentários habilitados em linhas separadas na visualização transposta. Isso aumenta a altura das linhas.",
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.",
dataGridTypeColorScheme: "Esquema de cores por tipo de dado",
dataGridTypeColorSchemeDescription: "Escolha a paleta que a grade usa para colorir os valores conforme o tipo de dado.",
dataGridTypeColorSchemeConfigure: "Personalizar",
dataGridTypeColorSchemeTitle: "Cores por tipo de dado",
dataGridTypeColorSchemeAuto: "Seguir o tema",
dataGridTypeColorSchemeAutoHint: "As cores internas são usadas, alternando automaticamente entre as paletas clara e escura.",
dataGridTypeColorSchemeCreateFromCurrent: "Criar esquema com estas cores",
dataGridTypeColorSchemeAdd: "Novo esquema",
dataGridTypeColorSchemeDuplicate: "Duplicar esquema",
dataGridTypeColorSchemeRename: "Renomear esquema",
dataGridTypeColorSchemeRenameConfirm: "Renomear",
dataGridTypeColorSchemeDelete: "Excluir esquema",
dataGridTypeColorSchemeReset: "Redefinir cores",
dataGridTypeColorSchemeDone: "Concluído",
dataGridTypeColorSchemeDefaultName: "Esquema de cores",
dataGridTypeColorSchemeCopySuffix: " Cópia",
dataGridTypeColorInteger: "Inteiro",
dataGridTypeColorNumeric: "Decimal",
dataGridTypeColorString: "Texto",
dataGridTypeColorBoolean: "Booleano",
dataGridTypeColorTemporal: "Data e hora",
dataGridTypeColorStructured: "Estruturado",
dataGridTypeColorIdentifier: "Identificador",
dataGridTypeColorBinary: "Binário",
dataGridTypeColorSpatial: "Espacial",
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",
+25
View File
@@ -5875,6 +5875,31 @@ export default withEnglishFallback({
dataGridShowTransposeFieldMetadataDescription: "在转置视图中分行显示已启用的数据类型和注释。开启后会增加行高。",
colorizeDataGridCellTypes: "按数据类型为表格值着色",
colorizeDataGridCellTypesDescription: "使用适配主题的克制文字颜色,按字段数据类型区分表格中的值。",
dataGridTypeColorScheme: "数据类型配色方案",
dataGridTypeColorSchemeDescription: "选择表格按数据类型为值着色时使用的配色。",
dataGridTypeColorSchemeConfigure: "自定义",
dataGridTypeColorSchemeTitle: "数据类型着色",
dataGridTypeColorSchemeAuto: "跟随主题",
dataGridTypeColorSchemeAutoHint: "使用内置配色,在浅色与深色调色板之间自动切换。",
dataGridTypeColorSchemeCreateFromCurrent: "以当前配色新建方案",
dataGridTypeColorSchemeAdd: "新建方案",
dataGridTypeColorSchemeDuplicate: "复制方案",
dataGridTypeColorSchemeRename: "重命名方案",
dataGridTypeColorSchemeRenameConfirm: "重命名",
dataGridTypeColorSchemeDelete: "删除方案",
dataGridTypeColorSchemeReset: "恢复默认颜色",
dataGridTypeColorSchemeDone: "完成",
dataGridTypeColorSchemeDefaultName: "配色方案",
dataGridTypeColorSchemeCopySuffix: " 副本",
dataGridTypeColorInteger: "整数",
dataGridTypeColorNumeric: "小数",
dataGridTypeColorString: "文本",
dataGridTypeColorBoolean: "布尔值",
dataGridTypeColorTemporal: "日期时间",
dataGridTypeColorStructured: "结构化",
dataGridTypeColorIdentifier: "标识符",
dataGridTypeColorBinary: "二进制",
dataGridTypeColorSpatial: "空间数据",
showIndexIndicatorsInHeader: "在字段名旁显示索引标识",
showIndexIndicatorsInHeaderDescription: "在结果表头字段名旁用图标标识字段的索引类型:主键(橙色)、唯一索引(红色)、普通索引(绿色)。",
compactColumnHeaderActions: "收起字段表头工具",
+25
View File
@@ -4952,6 +4952,31 @@ export default withEnglishFallback({
dataGridShowTransposeFieldMetadataDescription: "在轉置檢視中分行顯示已啟用的資料類型和註解。開啟後會增加列高。",
colorizeDataGridCellTypes: "依資料類型為表格值著色",
colorizeDataGridCellTypesDescription: "使用配合主題的柔和文字顏色,依欄位資料類型區分表格中的值。",
dataGridTypeColorScheme: "資料型別配色方案",
dataGridTypeColorSchemeDescription: "選擇表格依資料型別為值著色時使用的配色。",
dataGridTypeColorSchemeConfigure: "自訂",
dataGridTypeColorSchemeTitle: "資料型別著色",
dataGridTypeColorSchemeAuto: "跟隨主題",
dataGridTypeColorSchemeAutoHint: "使用內建配色,在淺色與深色調色盤之間自動切換。",
dataGridTypeColorSchemeCreateFromCurrent: "以目前配色新增方案",
dataGridTypeColorSchemeAdd: "新增方案",
dataGridTypeColorSchemeDuplicate: "複製方案",
dataGridTypeColorSchemeRename: "重新命名方案",
dataGridTypeColorSchemeRenameConfirm: "重新命名",
dataGridTypeColorSchemeDelete: "刪除方案",
dataGridTypeColorSchemeReset: "還原預設顏色",
dataGridTypeColorSchemeDone: "完成",
dataGridTypeColorSchemeDefaultName: "配色方案",
dataGridTypeColorSchemeCopySuffix: " 副本",
dataGridTypeColorInteger: "整數",
dataGridTypeColorNumeric: "小數",
dataGridTypeColorString: "文字",
dataGridTypeColorBoolean: "布林值",
dataGridTypeColorTemporal: "日期時間",
dataGridTypeColorStructured: "結構化",
dataGridTypeColorIdentifier: "識別碼",
dataGridTypeColorBinary: "二進位",
dataGridTypeColorSpatial: "空間資料",
showIndexIndicatorsInHeader: "在欄位名稱旁顯示索引標識",
showIndexIndicatorsInHeaderDescription: "在資料表格欄位名稱旁用圖示標識欄位的索引類型:主鍵(橙色)、唯一索引(紅色)、普通索引(綠色)。",
compactColumnHeaderActions: "收起欄位表頭工具",
+68
View File
@@ -0,0 +1,68 @@
import { describe, expect, it } from "vitest";
import { hexToHsv, hexToRgba, hsvToHex } from "@/lib/common/color";
describe("hexToHsv", () => {
it.each([
["#ff0000", 0, 1, 1],
["#00ff00", 120, 1, 1],
["#0000ff", 240, 1, 1],
["#ffffff", 0, 0, 1],
["#000000", 0, 0, 0],
["#808080", 0, 0, 0.502],
])("maps %s to its hue, saturation and value", (color, h, s, v) => {
const hsv = hexToHsv(color);
expect(hsv?.h).toBeCloseTo(h, 1);
expect(hsv?.s).toBeCloseTo(s, 2);
expect(hsv?.v).toBeCloseTo(v, 2);
});
it("accepts shorthand and hash-less input", () => {
expect(hexToHsv("#f00")).toEqual(hexToHsv("#ff0000"));
expect(hexToHsv("ff0000")).toEqual(hexToHsv("#ff0000"));
});
it.each([["#12345g"], ["rgb(0,0,0)"], ["red"], [""], ["#ff00"]])("rejects the non-hex input %p", (color) => {
expect(hexToHsv(color)).toBeNull();
});
});
describe("hsvToHex", () => {
it.each([
[{ h: 0, s: 1, v: 1 }, "#ff0000"],
[{ h: 120, s: 1, v: 1 }, "#00ff00"],
[{ h: 240, s: 1, v: 1 }, "#0000ff"],
[{ h: 0, s: 0, v: 1 }, "#ffffff"],
[{ h: 0, s: 0, v: 0 }, "#000000"],
[{ h: 60, s: 1, v: 1 }, "#ffff00"],
])("renders %o as %s", (hsv, expected) => {
expect(hsvToHex(hsv)).toBe(expected);
});
it("wraps the hue and clamps the fractions", () => {
expect(hsvToHex({ h: 360, s: 1, v: 1 })).toBe("#ff0000");
expect(hsvToHex({ h: -120, s: 1, v: 1 })).toBe("#0000ff");
expect(hsvToHex({ h: 0, s: 4, v: 4 })).toBe("#ff0000");
expect(hsvToHex({ h: 0, s: -1, v: -1 })).toBe("#000000");
});
it("always emits a six-digit lowercase hex", () => {
for (let hue = 0; hue < 360; hue += 7) {
expect(hsvToHex({ h: hue, s: 0.42, v: 0.73 })).toMatch(/^#[0-9a-f]{6}$/);
}
});
it("round-trips a color through HSV without drift", () => {
// The spectrum picker converts on every pointer move, so drift would creep.
for (const color of ["#1d4ed8", "#0e7490", "#166534", "#c2410c", "#7e22ce", "#93c5fd", "#6ee7b7"]) {
expect(hsvToHex(hexToHsv(color)!)).toBe(color);
}
});
});
describe("hexToRgba", () => {
it("keeps working alongside the HSV helpers", () => {
expect(hexToRgba("#ff0000", 0.5)).toBe("rgba(255, 0, 0, 0.5)");
expect(hexToRgba("nonsense", 1)).toBeUndefined();
});
});
@@ -1,6 +1,7 @@
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";
import { DEFAULT_DATA_GRID_TYPE_COLORS_DARK, DEFAULT_DATA_GRID_TYPE_COLORS_LIGHT, dataGridTypeColorCssVar } from "@/lib/dataGrid/dataGridTypeColorScheme";
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);
@@ -41,6 +42,30 @@ describe("data grid paint theme", () => {
expect(resolveDataGridPaintTheme({ getVar: emptyCssVariable, isDark: true }).rowNumberActive).toBe(DATA_GRID_DARK_ACTIVE_ROW_BG);
});
it("falls back to the built-in type palette for the active appearance", () => {
const emptyCssVariable = () => "";
expect(resolveDataGridPaintTheme({ getVar: emptyCssVariable, isDark: false }).typeForegrounds.integer).toBe(DEFAULT_DATA_GRID_TYPE_COLORS_LIGHT.integer);
expect(resolveDataGridPaintTheme({ getVar: emptyCssVariable, isDark: true }).typeForegrounds.integer).toBe(DEFAULT_DATA_GRID_TYPE_COLORS_DARK.integer);
});
it("lets an overridden type variable drive the canvas foreground", () => {
// A custom scheme is applied by writing these variables onto the document,
// so the canvas paint theme has to read them rather than its own defaults.
const overrides: Record<string, string> = { [dataGridTypeColorCssVar("integer")]: "#123456" };
const theme = resolveDataGridPaintTheme({ getVar: (name) => overrides[name] ?? "", isDark: false });
// A resolved variable is normalized to a canvas-safe rgb(); an absent one keeps the raw fallback.
expect(theme.typeForegrounds.integer).toBe("rgb(18, 52, 86)");
expect(theme.typeForegrounds.string).toBe(DEFAULT_DATA_GRID_TYPE_COLORS_LIGHT.string);
});
it("keeps unknown-typed values on the neutral foreground", () => {
const theme = resolveDataGridPaintTheme({ getVar: () => "", isDark: false });
expect(theme.typeForegrounds.unknown).toBe(theme.foreground);
});
it("keeps the classic blue selection palette instead of theme accent/ring mixing", () => {
const vars: Record<string, string> = {
"--background": "rgb(255, 255, 255)",
@@ -0,0 +1,111 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
import {
DATA_GRID_TYPE_COLOR_KEYS,
DATA_GRID_TYPE_COLOR_SCHEME_AUTO_ID,
DEFAULT_DATA_GRID_TYPE_COLORS_DARK,
DEFAULT_DATA_GRID_TYPE_COLORS_LIGHT,
dataGridTypeColorCssVar,
defaultDataGridTypeColors,
normalizeActiveDataGridTypeColorSchemeId,
normalizeDataGridTypeColorSchemes,
normalizeDataGridTypeColors,
resolveActiveDataGridTypeColors,
type DataGridTypeColorScheme,
} from "@/lib/dataGrid/dataGridTypeColorScheme";
const globalStylesSource = readFileSync(new URL("../../../styles/globals.css", import.meta.url), "utf8");
function cssBlockVariables(selector: string): Record<string, string> {
const start = globalStylesSource.indexOf(`${selector} {`);
expect(start).toBeGreaterThanOrEqual(0);
const block = globalStylesSource.slice(start, globalStylesSource.indexOf("}", start));
const found: Record<string, string> = {};
for (const [, name, value] of block.matchAll(/(--data-grid-type-[\w-]+):\s*([^;]+);/g)) {
found[name] = value.trim();
}
return found;
}
function scheme(id: string, overrides: Partial<Record<string, string>> = {}): DataGridTypeColorScheme {
return { id, name: id, colors: { ...DEFAULT_DATA_GRID_TYPE_COLORS_LIGHT, ...overrides } };
}
describe("data grid type color keys", () => {
it("covers every visual kind except the neutral unknown", () => {
expect(DATA_GRID_TYPE_COLOR_KEYS).toEqual(["integer", "numeric", "string", "boolean", "temporal", "structured", "identifier", "binary", "spatial"]);
expect(DATA_GRID_TYPE_COLOR_KEYS).not.toContain("unknown");
});
it("keeps the built-in palettes in sync with globals.css", () => {
// The stylesheet cannot import the TS defaults, so drift has to be caught here.
const light = cssBlockVariables(":root");
const dark = cssBlockVariables(":root.dark");
for (const key of DATA_GRID_TYPE_COLOR_KEYS) {
expect(light[dataGridTypeColorCssVar(key)]).toBe(DEFAULT_DATA_GRID_TYPE_COLORS_LIGHT[key]);
expect(dark[dataGridTypeColorCssVar(key)]).toBe(DEFAULT_DATA_GRID_TYPE_COLORS_DARK[key]);
}
});
it("selects the palette matching the appearance", () => {
expect(defaultDataGridTypeColors(false)).toEqual(DEFAULT_DATA_GRID_TYPE_COLORS_LIGHT);
expect(defaultDataGridTypeColors(true)).toEqual(DEFAULT_DATA_GRID_TYPE_COLORS_DARK);
});
});
describe("data grid type color normalization", () => {
it("keeps valid six-digit hex values", () => {
expect(normalizeDataGridTypeColors({ ...DEFAULT_DATA_GRID_TYPE_COLORS_LIGHT, integer: "#ABCDEF" }).integer).toBe("#ABCDEF");
});
it.each([["#fff"], ["red"], ["rgb(0,0,0)"], ["#12345g"], [""], [42], [null]])("falls back for the unusable value %p", (value) => {
expect(normalizeDataGridTypeColors({ integer: value }).integer).toBe(DEFAULT_DATA_GRID_TYPE_COLORS_LIGHT.integer);
});
it("fills every missing key from the supplied fallback palette", () => {
expect(normalizeDataGridTypeColors({}, DEFAULT_DATA_GRID_TYPE_COLORS_DARK)).toEqual(DEFAULT_DATA_GRID_TYPE_COLORS_DARK);
});
it.each([[undefined], [null], ["nope"], [{}]])("treats the non-array scheme list %p as empty", (value) => {
expect(normalizeDataGridTypeColorSchemes(value)).toEqual([]);
});
it("drops entries without a usable id and de-duplicates the rest", () => {
const result = normalizeDataGridTypeColorSchemes([scheme("keep"), { id: " " }, { name: "no id" }, scheme("keep"), null]);
expect(result.map((entry) => entry.id)).toEqual(["keep"]);
});
it("refuses a persisted scheme claiming the reserved auto id", () => {
expect(normalizeDataGridTypeColorSchemes([scheme(DATA_GRID_TYPE_COLOR_SCHEME_AUTO_ID)])).toEqual([]);
});
it("falls back to the id when a scheme has no usable name", () => {
expect(normalizeDataGridTypeColorSchemes([{ id: "abc", colors: DEFAULT_DATA_GRID_TYPE_COLORS_LIGHT }])[0].name).toBe("abc");
});
});
describe("active data grid type color scheme", () => {
const schemes = [scheme("mine", { integer: "#123456" })];
it("keeps an id that resolves to a scheme", () => {
expect(normalizeActiveDataGridTypeColorSchemeId(schemes, "mine")).toBe("mine");
});
it.each([["deleted"], [undefined], [null], [7]])("falls back to auto for the unresolvable id %p", (value) => {
expect(normalizeActiveDataGridTypeColorSchemeId(schemes, value)).toBe(DATA_GRID_TYPE_COLOR_SCHEME_AUTO_ID);
});
it("returns no override under auto so the stylesheet keeps following the theme", () => {
expect(resolveActiveDataGridTypeColors(schemes, DATA_GRID_TYPE_COLOR_SCHEME_AUTO_ID)).toBeNull();
});
it("returns the selected palette", () => {
expect(resolveActiveDataGridTypeColors(schemes, "mine")?.integer).toBe("#123456");
});
it("returns no override when the active scheme is missing", () => {
expect(resolveActiveDataGridTypeColors(schemes, "gone")).toBeNull();
});
});
+75
View File
@@ -28,3 +28,78 @@ export function hexToRgba(color: string | undefined, alpha: number): string | un
const b = value & 255;
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
}
/** Hue in degrees (0-360); saturation and value as 0-1 fractions. */
export interface HsvColor {
h: number;
s: number;
v: number;
}
function clamp01(value: number): number {
if (Number.isNaN(value)) return 0;
return Math.min(1, Math.max(0, value));
}
function parseHexChannels(color: string): [number, number, number] | null {
const hex = color.trim().replace(/^#/, "");
const normalized =
hex.length === 3
? hex
.split("")
.map((char) => char + char)
.join("")
: hex;
if (!/^[0-9a-fA-F]{6}$/.test(normalized)) return null;
const value = Number.parseInt(normalized, 16);
return [(value >> 16) & 255, (value >> 8) & 255, value & 255];
}
/**
* @param color Hex color, with or without the leading hash
* @return Matching HSV coordinates, or null when the input is not a hex color
*/
export function hexToHsv(color: string): HsvColor | null {
const channels = parseHexChannels(color);
if (!channels) return null;
const [r, g, b] = channels.map((channel) => channel / 255);
const max = Math.max(r, g, b);
const delta = max - Math.min(r, g, b);
let h = 0;
if (delta !== 0) {
if (max === r) h = 60 * (((g - b) / delta) % 6);
else if (max === g) h = 60 * ((b - r) / delta + 2);
else h = 60 * ((r - g) / delta + 4);
}
return { h: (h + 360) % 360, s: max === 0 ? 0 : delta / max, v: max };
}
/**
* @param hsv HSV coordinates; hue wraps and the fractions are clamped
* @return Lowercase `#rrggbb` string
*/
export function hsvToHex(hsv: HsvColor): string {
const hue = (((Number.isNaN(hsv.h) ? 0 : hsv.h) % 360) + 360) % 360;
const chroma = clamp01(hsv.v) * clamp01(hsv.s);
const second = chroma * (1 - Math.abs(((hue / 60) % 2) - 1));
const offset = clamp01(hsv.v) - chroma;
const sector = Math.floor(hue / 60) % 6;
const rgb = [
[chroma, second, 0],
[second, chroma, 0],
[0, chroma, second],
[0, second, chroma],
[second, 0, chroma],
[chroma, 0, second],
][sector];
return `#${rgb
.map((channel) =>
Math.round((channel + offset) * 255)
.toString(16)
.padStart(2, "0"),
)
.join("")}`;
}
@@ -1,4 +1,5 @@
import type { DataGridTypeVisualKind } from "@/lib/dataGrid/dataGridColumnType";
import { DATA_GRID_TYPE_COLOR_KEYS, dataGridTypeColorCssVar, defaultDataGridTypeColors } from "@/lib/dataGrid/dataGridTypeColorScheme";
export type DataGridTypeForegrounds = Record<DataGridTypeVisualKind, string>;
@@ -271,18 +272,11 @@ 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,
};
const typeDefaults = defaultDataGridTypeColors(isDark);
const typeForegrounds = { unknown: foreground } as DataGridTypeForegrounds;
for (const key of DATA_GRID_TYPE_COLOR_KEYS) {
typeForegrounds[key] = paintToken(getVar, dataGridTypeColorCssVar(key), typeDefaults[key]);
}
return {
background,
@@ -0,0 +1,120 @@
import { DATA_GRID_TYPE_VISUAL_KINDS, type DataGridTypeVisualKind } from "@/lib/dataGrid/dataGridColumnType";
/** Every visual kind except `unknown`, which deliberately stays on the neutral foreground. */
export type DataGridTypeColorKey = Exclude<DataGridTypeVisualKind, "unknown">;
export type DataGridTypeColors = Record<DataGridTypeColorKey, string>;
export interface DataGridTypeColorScheme {
id: string;
name: string;
colors: DataGridTypeColors;
}
export const DATA_GRID_TYPE_COLOR_KEYS = DATA_GRID_TYPE_VISUAL_KINDS.filter((kind): kind is DataGridTypeColorKey => kind !== "unknown");
/** Sentinel scheme id meaning "leave the stylesheet defaults alone and follow light/dark". */
export const DATA_GRID_TYPE_COLOR_SCHEME_AUTO_ID = "auto";
// Keep these in sync with the :root / :root.dark blocks in styles/globals.css.
// dataGridTypeColorScheme.spec.ts asserts the two stay identical.
export const DEFAULT_DATA_GRID_TYPE_COLORS_LIGHT: DataGridTypeColors = {
integer: "#1d4ed8",
numeric: "#0e7490",
string: "#166534",
boolean: "#c2410c",
temporal: "#7e22ce",
structured: "#be185d",
identifier: "#92400e",
binary: "#b91c1c",
spatial: "#047857",
};
export const DEFAULT_DATA_GRID_TYPE_COLORS_DARK: DataGridTypeColors = {
integer: "#93c5fd",
numeric: "#67e8f9",
string: "#86efac",
boolean: "#fdba74",
temporal: "#d8b4fe",
structured: "#f9a8d4",
identifier: "#fcd34d",
binary: "#fca5a5",
spatial: "#6ee7b7",
};
const HEX_COLOR_RE = /^#[0-9a-fA-F]{6}$/;
/**
* @param key Type visual kind driving the cue
* @return CSS custom property backing both the DOM class and the canvas paint theme
*/
export function dataGridTypeColorCssVar(key: DataGridTypeColorKey): string {
return `--data-grid-type-${key}-fg`;
}
/**
* @param isDark Whether the dark appearance is active
* @return Built-in palette for that appearance
*/
export function defaultDataGridTypeColors(isDark: boolean): DataGridTypeColors {
return isDark ? DEFAULT_DATA_GRID_TYPE_COLORS_DARK : DEFAULT_DATA_GRID_TYPE_COLORS_LIGHT;
}
/**
* @param value Untrusted persisted palette
* @param fallback Palette supplying every key the input fails to provide
* @return Palette with all nine keys set to a valid `#rrggbb` value
*/
export function normalizeDataGridTypeColors(value: unknown, fallback: DataGridTypeColors = DEFAULT_DATA_GRID_TYPE_COLORS_LIGHT): DataGridTypeColors {
const source = (value && typeof value === "object" ? value : {}) as Record<string, unknown>;
const out = {} as DataGridTypeColors;
for (const key of DATA_GRID_TYPE_COLOR_KEYS) {
const candidate = source[key];
out[key] = typeof candidate === "string" && HEX_COLOR_RE.test(candidate) ? candidate : fallback[key];
}
return out;
}
/**
* @param value Untrusted persisted scheme list
* @return Schemes with usable ids, names, and palettes; unusable entries are dropped
*/
export function normalizeDataGridTypeColorSchemes(value: unknown): DataGridTypeColorScheme[] {
if (!Array.isArray(value)) return [];
const seen = new Set<string>();
const out: DataGridTypeColorScheme[] = [];
for (const entry of value) {
if (!entry || typeof entry !== "object") continue;
const { id, name, colors } = entry as Record<string, unknown>;
// The auto id is reserved, so a persisted scheme may never claim it.
if (typeof id !== "string" || !id.trim() || id === DATA_GRID_TYPE_COLOR_SCHEME_AUTO_ID || seen.has(id)) continue;
seen.add(id);
out.push({
id,
name: typeof name === "string" && name.trim() ? name : id,
colors: normalizeDataGridTypeColors(colors),
});
}
return out;
}
/**
* @param schemes Available custom schemes
* @param activeId Selected scheme id
* @return Selected id, or the auto sentinel when it does not resolve to a scheme
*/
export function normalizeActiveDataGridTypeColorSchemeId(schemes: DataGridTypeColorScheme[], activeId: unknown): string {
if (typeof activeId !== "string" || !schemes.some((scheme) => scheme.id === activeId)) return DATA_GRID_TYPE_COLOR_SCHEME_AUTO_ID;
return activeId;
}
/**
* @param schemes Available custom schemes
* @param activeId Selected scheme id
* @return Palette to force onto the CSS variables, or null to keep following the theme
*/
export function resolveActiveDataGridTypeColors(schemes: DataGridTypeColorScheme[], activeId: string): DataGridTypeColors | null {
if (activeId === DATA_GRID_TYPE_COLOR_SCHEME_AUTO_ID) return null;
const scheme = schemes.find((entry) => entry.id === activeId);
return scheme ? scheme.colors : null;
}
@@ -69,6 +69,25 @@ describe("EDITOR_SETTINGS_DRAFT_KEYS", () => {
expect(EDITOR_SETTINGS_DRAFT_KEYS).toContain("colorizeDataGridCellTypes");
});
it("includes the type color scheme in the apply-footer draft", () => {
expect(EDITOR_SETTINGS_DRAFT_KEYS).toContain("dataGridTypeColorSchemes");
expect(EDITOR_SETTINGS_DRAFT_KEYS).toContain("activeDataGridTypeColorSchemeId");
const base = editorSettingsDraftFromSettings(makeSettings({ dataGridTypeColorSchemes: [], activeDataGridTypeColorSchemeId: "auto" }));
const withScheme = editorSettingsDraftFromSettings(
makeSettings({
dataGridTypeColorSchemes: [{ id: "type-colors-1", name: "配色方案 1", colors: { integer: "#254fce", numeric: "#0e7490", string: "#fdc9c9", boolean: "#100cc2", temporal: "#7e22ce", structured: "#be185d", identifier: "#92400e", binary: "#b91c1c", spatial: "#047857" } }],
activeDataGridTypeColorSchemeId: "type-colors-1",
}),
);
expect(editorSettingsDraftChanged(withScheme, base)).toBe(true);
expect(editorSettingsPatchFromDraft(withScheme, base)).toEqual({
dataGridTypeColorSchemes: [{ id: "type-colors-1", name: "配色方案 1", colors: { integer: "#254fce", numeric: "#0e7490", string: "#fdc9c9", boolean: "#100cc2", temporal: "#7e22ce", structured: "#be185d", identifier: "#92400e", binary: "#b91c1c", spatial: "#047857" } }],
activeDataGridTypeColorSchemeId: "type-colors-1",
});
});
it("includes the data grid filter view", () => {
expect(EDITOR_SETTINGS_DRAFT_KEYS).toContain("dataGridFilterEditorView");
expect(EDITOR_SETTINGS_DRAFT_KEYS).toContain("dataGridTextFilterPanelHeight");
@@ -34,6 +34,8 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [
"showColumnTypesInHeader",
"dataGridShowTransposeFieldMetadata",
"colorizeDataGridCellTypes",
"dataGridTypeColorSchemes",
"activeDataGridTypeColorSchemeId",
"showIndexIndicatorsInHeader",
"compactColumnHeaderActions",
"dataGridQuickEntry",
@@ -179,6 +179,7 @@ export const SETTINGS_SEARCH_DEFINITIONS: readonly SettingsSearchDefinition[] =
{ id: "data-grid-header-types", category: "data", titleKey: "settings.showColumnTypesInHeader", descriptionKey: "settings.showColumnTypesInHeaderDescription", targetId: "data" },
{ id: "data-grid-transpose-field-metadata", category: "data", titleKey: "settings.dataGridShowTransposeFieldMetadata", descriptionKey: "settings.dataGridShowTransposeFieldMetadataDescription", targetId: "data" },
{ id: "data-grid-cell-type-colors", category: "data", titleKey: "settings.colorizeDataGridCellTypes", descriptionKey: "settings.colorizeDataGridCellTypesDescription", targetId: "data" },
{ id: "data-grid-type-colors", category: "data", titleKey: "settings.dataGridTypeColorScheme", descriptionKey: "settings.dataGridTypeColorSchemeDescription", targetId: "data-grid-type-colors" },
{ 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" },
@@ -0,0 +1,87 @@
import { createPinia, setActivePinia } from "pinia";
import { readFileSync } from "node:fs";
import { beforeEach, describe, expect, it, vi } from "vitest";
const saved: unknown[] = [];
vi.mock("@/lib/backend/api", () => ({
saveEditorSettings: vi.fn(async (settings: unknown) => {
saved.push(JSON.parse(JSON.stringify(settings)));
}),
loadEditorSettings: vi.fn(async () => null),
loadDesktopSettings: vi.fn(async () => null),
saveDesktopSettings: vi.fn(async () => {}),
}));
const SCHEME = {
id: "type-colors-1",
name: "配色方案 1",
colors: { integer: "#254fce", numeric: "#0e7490", string: "#fdc9c9", boolean: "#100cc2", temporal: "#7e22ce", structured: "#be185d", identifier: "#92400e", binary: "#b91c1c", spatial: "#047857" },
};
describe("settings store type color scheme persistence", () => {
beforeEach(() => {
saved.length = 0;
vi.stubGlobal("localStorage", {
getItem: vi.fn(() => null),
setItem: vi.fn(),
removeItem: vi.fn(),
});
setActivePinia(createPinia());
});
it("keeps a committed scheme in memory", async () => {
const { useSettingsStore } = await import("@/stores/settingsStore");
const store = useSettingsStore();
store.updateEditorSettings({ dataGridTypeColorSchemes: [SCHEME], activeDataGridTypeColorSchemeId: SCHEME.id });
expect(store.editorSettings.dataGridTypeColorSchemes).toHaveLength(1);
expect(store.editorSettings.activeDataGridTypeColorSchemeId).toBe(SCHEME.id);
});
it("writes the scheme through to the persistence layer", async () => {
const { useSettingsStore } = await import("@/stores/settingsStore");
const store = useSettingsStore();
await store.persistEditorSettings();
saved.length = 0;
store.updateEditorSettings({ dataGridTypeColorSchemes: [SCHEME], activeDataGridTypeColorSchemeId: SCHEME.id });
await store.persistEditorSettings();
const last = saved.at(-1) as Record<string, unknown> | undefined;
expect(last?.dataGridTypeColorSchemes).toEqual([SCHEME]);
expect(last?.activeDataGridTypeColorSchemeId).toBe(SCHEME.id);
});
it("persists a scheme committed before the store finished loading", async () => {
// The scheme dialog can commit before anything has awaited initEditorSettings.
// A plain updateEditorSettings only lands in memory there, so the colors show
// up and then vanish on the next reload; the persisting variant must be used.
const { useSettingsStore } = await import("@/stores/settingsStore");
const store = useSettingsStore();
await store.updateEditorSettingsAndPersist({ dataGridTypeColorSchemes: [SCHEME], activeDataGridTypeColorSchemeId: SCHEME.id });
const last = saved.at(-1) as Record<string, unknown> | undefined;
expect(last?.dataGridTypeColorSchemes).toEqual([SCHEME]);
expect(last?.activeDataGridTypeColorSchemeId).toBe(SCHEME.id);
});
it("stages scheme edits in the settings draft", () => {
const dialogSource = readFileSync(new URL("../../components/editor/EditorSettingsDialog.vue", import.meta.url), "utf8");
expect(dialogSource).toContain("editDataGridTypeColorSchemes.value = structuredClone(schemes)");
expect(dialogSource).toContain("editActiveDataGridTypeColorSchemeId.value = activeId");
expect(dialogSource).not.toMatch(/settingsStore\.updateEditorSettings\w*\(\{[^}]*DataGridTypeColorScheme/);
});
it("restores a persisted scheme through normalization", async () => {
const { normalizeEditorSettings } = await import("@/stores/settingsStore");
const restored = normalizeEditorSettings({ dataGridTypeColorSchemes: [SCHEME], activeDataGridTypeColorSchemeId: SCHEME.id });
expect(restored.dataGridTypeColorSchemes).toEqual([SCHEME]);
expect(restored.activeDataGridTypeColorSchemeId).toBe(SCHEME.id);
});
});
+17
View File
@@ -8,6 +8,7 @@ import { safeLocalStorageGet, safeLocalStorageRemove } from "@/lib/backend/safeS
import { type ColumnFormatterConfig, type CustomColumnFormatterConfig, normalizeColumnFormatter, normalizeCustomColumnFormatter, normalizeGlobalDateTimePattern } from "@/lib/dataGrid/columnFormatter";
import { type DataGridCopyPreference, type DataGridExtractorOptions, DEFAULT_DATA_GRID_EXTRACTOR_OPTIONS, normalizeDataGridCopyPreference, normalizeDataGridExtractorOptions } from "@/lib/dataGrid/dataGridCopyExtractor";
import { DATA_GRID_TEXT_FILTER_PANEL_HEIGHT_DEFAULT, normalizeDataGridTextFilterPanelHeight } from "@/lib/dataGrid/dataGridTextFilterPanel";
import { DATA_GRID_TYPE_COLOR_SCHEME_AUTO_ID, type DataGridTypeColorScheme, normalizeActiveDataGridTypeColorSchemeId, normalizeDataGridTypeColorSchemes } from "@/lib/dataGrid/dataGridTypeColorScheme";
import { normalizeResultPageSize } from "@/lib/dataGrid/paginationPageSize";
import { DEFAULT_QUERY_RESULT_MAX_ROWS, normalizeQueryResultMaxRows } from "@/lib/dataGrid/queryResultRowLimit";
import { normalizeConnectTimeoutSecs, normalizeQueryTimeoutSecs } from "@/lib/connection/timeoutLimits";
@@ -551,6 +552,8 @@ export interface EditorSettings {
showColumnTypesInHeader: boolean;
dataGridShowTransposeFieldMetadata: boolean;
colorizeDataGridCellTypes: boolean;
dataGridTypeColorSchemes: DataGridTypeColorScheme[];
activeDataGridTypeColorSchemeId: string;
showIndexIndicatorsInHeader: boolean;
compactColumnHeaderActions: boolean;
columnWidthDensity: ColumnWidthDensity;
@@ -754,6 +757,8 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
showColumnTypesInHeader: true,
dataGridShowTransposeFieldMetadata: false,
colorizeDataGridCellTypes: false,
dataGridTypeColorSchemes: [],
activeDataGridTypeColorSchemeId: DATA_GRID_TYPE_COLOR_SCHEME_AUTO_ID,
showIndexIndicatorsInHeader: true,
compactColumnHeaderActions: true,
columnWidthDensity: "standard",
@@ -1031,6 +1036,8 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
const savedExecuteModeDefaultVersion = settings.executeModeDefaultVersion;
const executeModeDefaultVersion = typeof savedExecuteModeDefaultVersion === "number" && savedExecuteModeDefaultVersion >= EXECUTE_MODE_CURRENT_DEFAULT_VERSION ? savedExecuteModeDefaultVersion : EXECUTE_MODE_CURRENT_DEFAULT_VERSION;
const hasCurrentExecuteModeDefault = executeModeDefaultVersion === savedExecuteModeDefaultVersion;
// The active id can only be validated once the scheme list it points into is known.
const dataGridTypeColorSchemes = normalizeDataGridTypeColorSchemes(settings.dataGridTypeColorSchemes);
return {
fontFamily: normalizeFontFamily(settings.fontFamily, DEFAULT_EDITOR_SETTINGS.fontFamily),
fontSize: settings.fontSize ?? DEFAULT_EDITOR_SETTINGS.fontSize,
@@ -1116,6 +1123,8 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
showColumnTypesInHeader: settings.showColumnTypesInHeader ?? DEFAULT_EDITOR_SETTINGS.showColumnTypesInHeader,
dataGridShowTransposeFieldMetadata: settings.dataGridShowTransposeFieldMetadata === true,
colorizeDataGridCellTypes: settings.colorizeDataGridCellTypes ?? DEFAULT_EDITOR_SETTINGS.colorizeDataGridCellTypes,
dataGridTypeColorSchemes,
activeDataGridTypeColorSchemeId: normalizeActiveDataGridTypeColorSchemeId(dataGridTypeColorSchemes, settings.activeDataGridTypeColorSchemeId),
showIndexIndicatorsInHeader: settings.showIndexIndicatorsInHeader ?? DEFAULT_EDITOR_SETTINGS.showIndexIndicatorsInHeader,
compactColumnHeaderActions: settings.compactColumnHeaderActions ?? DEFAULT_EDITOR_SETTINGS.compactColumnHeaderActions,
columnWidthDensity: normalizeColumnWidthDensity(settings.columnWidthDensity),
@@ -1702,6 +1711,14 @@ export const useSettingsStore = defineStore("settings", () => {
if (partial.showColumnTypesInHeader !== undefined) editorSettings.value.showColumnTypesInHeader = partial.showColumnTypesInHeader;
if (partial.dataGridShowTransposeFieldMetadata !== undefined) editorSettings.value.dataGridShowTransposeFieldMetadata = partial.dataGridShowTransposeFieldMetadata === true;
if (partial.colorizeDataGridCellTypes !== undefined) editorSettings.value.colorizeDataGridCellTypes = partial.colorizeDataGridCellTypes === true;
if (partial.dataGridTypeColorSchemes !== undefined) {
editorSettings.value.dataGridTypeColorSchemes = normalizeDataGridTypeColorSchemes(partial.dataGridTypeColorSchemes);
// Dropping the scheme the grid is painting with has to fall back to the theme defaults.
editorSettings.value.activeDataGridTypeColorSchemeId = normalizeActiveDataGridTypeColorSchemeId(editorSettings.value.dataGridTypeColorSchemes, editorSettings.value.activeDataGridTypeColorSchemeId);
}
if (partial.activeDataGridTypeColorSchemeId !== undefined) {
editorSettings.value.activeDataGridTypeColorSchemeId = normalizeActiveDataGridTypeColorSchemeId(editorSettings.value.dataGridTypeColorSchemes, partial.activeDataGridTypeColorSchemeId);
}
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);