feat(editor): allow customizing active line and editor UI colors

This commit is contained in:
lxk955
2026-10-04 02:30:51 +08:00
committed by GitHub
parent 951f0a21a0
commit 252f8a0d1f
19 changed files with 204 additions and 9 deletions
@@ -5,7 +5,7 @@ import { Button } from "@/components/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Input } from "@/components/ui/input";
import type { CustomTheme, CustomThemeColors } from "@/stores/settingsStore";
import { DEFAULT_CUSTOM_THEMES, DEFAULT_CUSTOM_THEME_COLORS, DEFAULT_CUSTOM_THEME_DDL_COLORS } from "@/stores/settingsStore";
import { DEFAULT_CUSTOM_THEMES, DEFAULT_CUSTOM_THEME_COLORS, DEFAULT_CUSTOM_THEME_DDL_COLORS, DEFAULT_CUSTOM_THEME_OPTIONAL_COLORS } from "@/stores/settingsStore";
import { Plus, Trash2, Copy, Pencil, ChevronDown, Palette } from "@lucide/vue";
import { useToast } from "@/composables/useToast";
import { useI18n } from "vue-i18n";
@@ -70,6 +70,10 @@ const localColors = computed({
},
});
function getDisplayColor(key: keyof CustomThemeColors): string {
return localColors.value[key] ?? DEFAULT_CUSTOM_THEME_OPTIONAL_COLORS[key] ?? "#000000";
}
watch(
localThemes,
() => {
@@ -91,6 +95,12 @@ const colorItems = [
{ key: "builtin" as const, label: t("settings.customThemeBuiltin"), example: "FOUND, SQLERRM", num: "⑩" },
{ key: "background" as const, label: t("settings.customThemeBackground"), example: "Editor background", num: "⑪" },
{ key: "foreground" as const, label: t("settings.customThemeForeground"), example: "Default text color", num: "⑫" },
{ key: "activeLine" as const, label: t("settings.customThemeActiveLine"), example: "Current line highlight", num: "⑬" },
{ key: "selection" as const, label: t("settings.customThemeSelection"), example: "Selection background", num: "⑭" },
{ key: "cursor" as const, label: t("settings.customThemeCursor"), example: "Cursor line/caret", num: "⑮" },
{ key: "gutterBackground" as const, label: t("settings.customThemeGutterBackground"), example: "Gutter background", num: "⑯" },
{ key: "lineNumber" as const, label: t("settings.customThemeLineNumber"), example: "Line numbers", num: "⑰" },
{ key: "matchingBracket" as const, label: t("settings.customThemeMatchingBracket"), example: "Matching bracket highlight", num: "⑱" },
];
// Preset color themes (including all built-in themes)
@@ -537,7 +547,7 @@ function handleImport() {
<!-- Color square + dropdown arrow -->
<div class="relative">
<button type="button" class="flex items-center gap-0.5 rounded border p-0.5 hover:bg-muted transition-colors" @click.stop="togglePalette(item.key)">
<div class="h-6 w-6 rounded-sm" :style="{ backgroundColor: localColors[item.key] }" />
<div class="h-6 w-6 rounded-sm" :style="{ backgroundColor: getDisplayColor(item.key) }" />
<ChevronDown class="h-3 w-3 text-muted-foreground pointer-events-none" />
</button>
<!-- Palette popup -->
@@ -548,8 +558,8 @@ function handleImport() {
</div>
</div>
<div class="mt-2 pt-2 border-t flex items-center gap-2">
<input type="color" :value="localColors[item.key]" @input="handleColorChange(item.key, ($event.target as HTMLInputElement).value)" class="h-6 w-6 cursor-pointer rounded border-0 p-0" />
<input type="text" :value="localColors[item.key]" @input="handleColorChange(item.key, ($event.target as HTMLInputElement).value)" class="w-20 rounded border px-2 py-0.5 text-xs font-mono" />
<input type="color" :value="getDisplayColor(item.key)" @input="handleColorChange(item.key, ($event.target as HTMLInputElement).value)" class="h-6 w-6 cursor-pointer rounded border-0 p-0" />
<input type="text" :value="getDisplayColor(item.key)" @input="handleColorChange(item.key, ($event.target as HTMLInputElement).value)" class="w-20 rounded border px-2 py-0.5 text-xs font-mono" />
</div>
</div>
</div>
+6
View File
@@ -7806,6 +7806,12 @@ export default withEnglishFallback({
customThemeBuiltin: "Daxili",
customThemeBackground: "Arxa plan",
customThemeForeground: "Ön plan",
customThemeActiveLine: "Aktiv sətir",
customThemeSelection: "Seçim",
customThemeCursor: "Kursor",
customThemeGutterBackground: "Sətir kənarı fonu",
customThemeLineNumber: "Sətir nömrəsi",
customThemeMatchingBracket: "Uyğun mötərizə",
customThemeDefaultName: "Fərdi tema",
customThemeCopySuffix: " surəti",
customThemeApplied: '"{name}" rəng sxemi tətbiq edildi',
+6
View File
@@ -8736,6 +8736,12 @@ export default {
customThemeBuiltin: "Builtin",
customThemeBackground: "Background",
customThemeForeground: "Foreground",
customThemeActiveLine: "Active Line",
customThemeSelection: "Selection",
customThemeCursor: "Cursor",
customThemeGutterBackground: "Gutter Background",
customThemeLineNumber: "Line Number",
customThemeMatchingBracket: "Matching Bracket",
customThemeDefaultName: "Custom Theme",
customThemeCopySuffix: " Copy",
customThemeApplied: 'Applied "{name}" color scheme',
+6
View File
@@ -8036,6 +8036,12 @@ export default withEnglishFallback({
customThemeBuiltin: "Variable integrada",
customThemeBackground: "Color de fondo",
customThemeForeground: "Color de primer plano",
customThemeActiveLine: "Línea activa",
customThemeSelection: "Selección",
customThemeCursor: "Cursor",
customThemeGutterBackground: "Fondo del margen de líneas",
customThemeLineNumber: "Número de línea",
customThemeMatchingBracket: "Paréntesis coincidente",
customThemeDefaultName: "Tema personalizado",
customThemeCopySuffix: " Copia",
customThemeApplied: 'Esquema de color "{name}" aplicado',
+6
View File
@@ -8529,6 +8529,12 @@ export default withEnglishFallback({
customThemeBuiltin: "Bawaan",
customThemeBackground: "Latar Belakang",
customThemeForeground: "Latar Depan",
customThemeActiveLine: "Baris Aktif",
customThemeSelection: "Pilihan",
customThemeCursor: "Kursor",
customThemeGutterBackground: "Latar Belakang Margin",
customThemeLineNumber: "Nomor Baris",
customThemeMatchingBracket: "Tanda Kurung Cocok",
customThemeDefaultName: "Tema Kustom",
customThemeCopySuffix: " Salinan",
customThemeApplied: 'Skema warna "{name}" diterapkan',
+6
View File
@@ -7798,6 +7798,12 @@ export default withEnglishFallback({
customThemeBuiltin: "Integrato",
customThemeBackground: "Sfondo",
customThemeForeground: "Primo piano",
customThemeActiveLine: "Riga attiva",
customThemeSelection: "Selezione",
customThemeCursor: "Cursore",
customThemeGutterBackground: "Sfondo del margine",
customThemeLineNumber: "Numero di riga",
customThemeMatchingBracket: "Parentesi corrispondente",
customThemeDefaultName: "Tema personalizzato",
customThemeCopySuffix: " Copia",
customThemeApplied: 'Schema colori "{name}" applicato',
+6
View File
@@ -7793,6 +7793,12 @@ export default withEnglishFallback({
customThemeBuiltin: "組み込み",
customThemeBackground: "背景",
customThemeForeground: "前景",
customThemeActiveLine: "アクティブ行",
customThemeSelection: "選択範囲",
customThemeCursor: "カーソル",
customThemeGutterBackground: "ガター背景",
customThemeLineNumber: "行番号",
customThemeMatchingBracket: "対応する括弧",
customThemeDefaultName: "カスタムテーマ",
customThemeCopySuffix: "コピー",
customThemeApplied: "配色テーマ「{name}」を適用しました",
+6
View File
@@ -7606,6 +7606,12 @@ export default withEnglishFallback({
customThemeBuiltin: "내장",
customThemeBackground: "배경",
customThemeForeground: "전경",
customThemeActiveLine: "현재 줄",
customThemeSelection: "선택 영역",
customThemeCursor: "커서",
customThemeGutterBackground: "여백 배경",
customThemeLineNumber: "줄 번호",
customThemeMatchingBracket: "일치하는 괄호",
customThemeDefaultName: "사용자 지정 테마",
customThemeCopySuffix: " 사본",
customThemeApplied: '"{name}" 색 구성표를 적용했습니다',
+6
View File
@@ -7799,6 +7799,12 @@ export default withEnglishFallback({
customThemeBuiltin: "Integrado",
customThemeBackground: "Plano de fundo",
customThemeForeground: "Primeiro plano",
customThemeActiveLine: "Linha ativa",
customThemeSelection: "Seleção",
customThemeCursor: "Cursor",
customThemeGutterBackground: "Fundo da margem",
customThemeLineNumber: "Número da linha",
customThemeMatchingBracket: "Parêntese correspondente",
customThemeDefaultName: "Tema personalizado",
customThemeCopySuffix: " Cópia",
customThemeApplied: 'Esquema de cores "{name}" aplicado',
+6
View File
@@ -9050,6 +9050,12 @@ export default withEnglishFallback({
customThemeBuiltin: "Встроенный",
customThemeBackground: "Фон",
customThemeForeground: "Передний план",
customThemeActiveLine: "Текущая строка",
customThemeSelection: "Выделение",
customThemeCursor: "Курсор",
customThemeGutterBackground: "Фон полосы номеров строк",
customThemeLineNumber: "Номер строки",
customThemeMatchingBracket: "Парная скобка",
customThemeDefaultName: "Своя тема",
customThemeCopySuffix: " (копия)",
customThemeApplied: "Применена цветовая схема «{name}»",
+6
View File
@@ -7707,6 +7707,12 @@ export default withEnglishFallback({
customThemeBuiltin: "Yerleşik",
customThemeBackground: "Arka plan",
customThemeForeground: "Ön plan",
customThemeActiveLine: "Etkin satır",
customThemeSelection: "Seçim",
customThemeCursor: "İmleç",
customThemeGutterBackground: "Kenar boşluğu arka planı",
customThemeLineNumber: "Satır numarası",
customThemeMatchingBracket: "Eşleşen parantez",
customThemeDefaultName: "Özel Tema",
customThemeCopySuffix: " Kopya",
customThemeApplied: '"{name}" renk şeması uygulandı',
+6
View File
@@ -8722,6 +8722,12 @@ export default withEnglishFallback({
customThemeBuiltin: "内置变量",
customThemeBackground: "背景色",
customThemeForeground: "前景色",
customThemeActiveLine: "当前行高亮",
customThemeSelection: "选中文本",
customThemeCursor: "光标颜色",
customThemeGutterBackground: "行号栏背景",
customThemeLineNumber: "行号颜色",
customThemeMatchingBracket: "匹配括号",
customThemeDefaultName: "自定义主题",
customThemeCopySuffix: " 副本",
customThemeApplied: "已应用「{name}」配色方案",
+6
View File
@@ -7124,6 +7124,12 @@ export default withEnglishFallback({
customThemeBuiltin: "內置變量",
customThemeBackground: "背景色",
customThemeForeground: "前景色",
customThemeActiveLine: "當前行高亮",
customThemeSelection: "選中文本",
customThemeCursor: "光標顏色",
customThemeGutterBackground: "行號欄背景",
customThemeLineNumber: "行號顏色",
customThemeMatchingBracket: "匹配括號",
customThemeDefaultName: "自定義主題",
customThemeCopySuffix: " 副本",
customThemeApplied: "已應用「{name}」配色方案",
+27 -2
View File
@@ -1,7 +1,7 @@
import { describe, expect, it } from "vitest";
import { buildEditorFontThemeRules, buildSqlCompletionThemeRules, editorDiagnosticColors, editorThemeAppearanceFor, IDE_EDITOR_THEMES, resolveCustomThemeBackgrounds, resolveEditorTheme, SQL_BUILTIN_HIGHLIGHT_TAG } from "@/lib/editor/editorThemes";
import { buildEditorFontThemeRules, buildSqlCompletionThemeRules, editorDiagnosticColors, editorThemeAppearanceFor, IDE_EDITOR_THEMES, loadEditorTheme, resolveCustomThemeBackgrounds, resolveEditorTheme, SQL_BUILTIN_HIGHLIGHT_TAG } from "@/lib/editor/editorThemes";
import { DEFAULT_APP_CUSTOM_UI_COLORS, wcagContrastRatio, type AppThemePalette } from "@/lib/app/appTheme";
import type { EditorTheme } from "@/stores/settingsStore";
import { DEFAULT_CUSTOM_THEME_COLORS, type CustomThemeColors, type EditorTheme } from "@/stores/settingsStore";
import { createDbxCodeMirrorSqlDialect } from "@/lib/editor/codemirrorSqlDialect";
import * as langSql from "@codemirror/lang-sql";
@@ -61,6 +61,31 @@ describe("custom editor theme backgrounds", () => {
});
});
it("uses an explicit gutter background when provided", () => {
expect(resolveCustomThemeBackgrounds({ background: "#10131a", gutterBackground: "#0b0c10" }, true)).toEqual({
background: "#10131a",
gutterBackground: "#0b0c10",
});
expect(resolveCustomThemeBackgrounds({ gutterBackground: "#0b0c10" }, false)).toEqual({
background: "#fafafa",
gutterBackground: "#0b0c10",
});
});
it("loads custom editor theme with custom UI colors", async () => {
const customColors: CustomThemeColors = {
...DEFAULT_CUSTOM_THEME_COLORS,
activeLine: "#2a2a3a",
selection: "#3a3a4a",
cursor: "#ff00ff",
gutterBackground: "#111122",
lineNumber: "#777788",
matchingBracket: "#444455",
};
const extension = await loadEditorTheme("custom", "dark", customColors);
expect(extension).toBeDefined();
});
it("keeps the existing custom defaults when background is omitted", () => {
expect(resolveCustomThemeBackgrounds(undefined, true)).toEqual({
background: "#1e1e2e",
+2 -2
View File
@@ -92,10 +92,10 @@ const customThemeColors = {
invalid: "#f38ba8", // 无效字符
};
export function resolveCustomThemeBackgrounds(colors?: Pick<CustomThemeColors, "background">, isDark: boolean = true): { background: string; gutterBackground: string } {
export function resolveCustomThemeBackgrounds(colors?: Pick<CustomThemeColors, "background" | "gutterBackground">, isDark: boolean = true): { background: string; gutterBackground: string } {
return {
background: colors?.background ?? (isDark ? "#1e1e2e" : "#fafafa"),
gutterBackground: colors?.background ?? customThemeColors.gutterBackground,
gutterBackground: colors?.gutterBackground ?? colors?.background ?? customThemeColors.gutterBackground,
};
}
@@ -350,6 +350,42 @@ describe("settingsTransfer", () => {
expect(result.value.editorSettings.customThemes).toEqual([validTheme]);
});
it("accepts custom theme items with optional UI colors", () => {
const base = DEFAULT_EDITOR_SETTINGS.customThemes[0];
const validTheme = {
...base,
id: "t1",
name: "T1",
colors: {
...base.colors,
background: "#282c34",
foreground: "#abb2bf",
activeLine: "#2c313a",
selection: "#3e4451",
cursor: "#528bff",
gutterBackground: "#21252b",
lineNumber: "#4b5263",
matchingBracket: "#515a6b",
},
};
const result = parseSettingsTransferFile(fileWith({ customThemes: [validTheme] }));
expect(result.ok).toBe(true);
if (!result.ok) return;
expect(result.value.editorSettings.customThemes).toEqual([validTheme]);
});
it("rejects custom theme items with wrong-typed optional UI colors", () => {
const base = DEFAULT_EDITOR_SETTINGS.customThemes[0];
const result = parseSettingsTransferFile(
fileWith({
customThemes: [{ ...base, id: "t1", name: "T1", colors: { ...base.colors, activeLine: 123 } }],
}),
);
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error.detail).toContain("customThemes");
});
it("rejects null custom theme items instead of crashing", () => {
const result = parseSettingsTransferFile(fileWith({ customThemes: [null] }));
expect(result.ok).toBe(false);
@@ -349,7 +349,7 @@ const CUSTOM_THEME_DDL_COLORS_SHAPE = valueKindShape(DEFAULT_CUSTOM_THEME_DDL_CO
* cover them and a wrong-typed value (e.g. `background: {}`) would otherwise
* pass validation and reach the persisted settings.
*/
const CUSTOM_THEME_OPTIONAL_COLOR_KEYS = ["background", "foreground"] as const;
const CUSTOM_THEME_OPTIONAL_COLOR_KEYS = ["background", "foreground", "activeLine", "selection", "cursor", "gutterBackground", "lineNumber", "matchingBracket"] as const;
function hasValidOptionalColorKinds(value: unknown): boolean {
if (!isPlainObject(value)) return false;
+35
View File
@@ -5,7 +5,9 @@ import {
AI_PROVIDER_PARTNER_PRESETS,
AI_PROVIDER_PRESETS,
DEFAULT_CUSTOM_THEMES,
DEFAULT_CUSTOM_THEME_COLORS,
DEFAULT_EDITOR_SETTINGS,
type CustomThemeColors,
EXECUTE_MODE_CURRENT_DEFAULT_VERSION,
SIDEBAR_BROWSE_OBJECTS_MIGRATION_VERSION,
enforceRightSidebarPanelExclusivity,
@@ -131,6 +133,39 @@ describe("normalizeEditorSettings", () => {
});
});
it("preserves optional UI colors in customThemes and customThemeColors", () => {
const customColors: CustomThemeColors = {
...DEFAULT_CUSTOM_THEME_COLORS,
background: "#101010",
foreground: "#f0f0f0",
activeLine: "#202020",
selection: "#303030",
cursor: "#ff00ff",
gutterBackground: "#151515",
lineNumber: "#666666",
matchingBracket: "#444444",
};
const normalized = normalizeEditorSettings({
customThemeColors: customColors,
customThemes: [
{
id: "custom-1",
name: "My Custom",
colors: customColors,
ddlColors: DEFAULT_CUSTOM_THEMES[0].ddlColors,
},
],
});
expect(normalized.customThemeColors.activeLine).toBe("#202020");
expect(normalized.customThemeColors.selection).toBe("#303030");
expect(normalized.customThemeColors.cursor).toBe("#ff00ff");
expect(normalized.customThemeColors.gutterBackground).toBe("#151515");
expect(normalized.customThemeColors.lineNumber).toBe("#666666");
expect(normalized.customThemeColors.matchingBracket).toBe("#444444");
expect(normalized.customThemes[0].colors.activeLine).toBe("#202020");
expect(normalized.customThemes[0].colors.gutterBackground).toBe("#151515");
});
it("normalizes customThemes when empty or absent by seeding DEFAULT_CUSTOM_THEMES", () => {
// When customThemes is an empty array
const normalizedEmpty = normalizeEditorSettings({ customThemes: [] });
+17
View File
@@ -769,8 +769,25 @@ export interface CustomThemeColors {
builtin: string;
background?: string;
foreground?: string;
activeLine?: string;
selection?: string;
cursor?: string;
gutterBackground?: string;
lineNumber?: string;
matchingBracket?: string;
}
export const DEFAULT_CUSTOM_THEME_OPTIONAL_COLORS: Partial<Record<keyof CustomThemeColors, string>> = {
background: "#1e1e2e",
foreground: "#cdd6f4",
activeLine: "#313244",
selection: "#313244",
cursor: "#f5e0dc",
gutterBackground: "#181825",
lineNumber: "#6c7086",
matchingBracket: "#45475a",
};
export const DEFAULT_CUSTOM_THEME_COLORS: CustomThemeColors = {
keyword: "#cba6f7",
field: "#f9e2af",