mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-20 00:17:44 +00:00
feat(editor): allow customizing active line and editor UI colors
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -7793,6 +7793,12 @@ export default withEnglishFallback({
|
||||
customThemeBuiltin: "組み込み",
|
||||
customThemeBackground: "背景",
|
||||
customThemeForeground: "前景",
|
||||
customThemeActiveLine: "アクティブ行",
|
||||
customThemeSelection: "選択範囲",
|
||||
customThemeCursor: "カーソル",
|
||||
customThemeGutterBackground: "ガター背景",
|
||||
customThemeLineNumber: "行番号",
|
||||
customThemeMatchingBracket: "対応する括弧",
|
||||
customThemeDefaultName: "カスタムテーマ",
|
||||
customThemeCopySuffix: "コピー",
|
||||
customThemeApplied: "配色テーマ「{name}」を適用しました",
|
||||
|
||||
@@ -7606,6 +7606,12 @@ export default withEnglishFallback({
|
||||
customThemeBuiltin: "내장",
|
||||
customThemeBackground: "배경",
|
||||
customThemeForeground: "전경",
|
||||
customThemeActiveLine: "현재 줄",
|
||||
customThemeSelection: "선택 영역",
|
||||
customThemeCursor: "커서",
|
||||
customThemeGutterBackground: "여백 배경",
|
||||
customThemeLineNumber: "줄 번호",
|
||||
customThemeMatchingBracket: "일치하는 괄호",
|
||||
customThemeDefaultName: "사용자 지정 테마",
|
||||
customThemeCopySuffix: " 사본",
|
||||
customThemeApplied: '"{name}" 색 구성표를 적용했습니다',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -9050,6 +9050,12 @@ export default withEnglishFallback({
|
||||
customThemeBuiltin: "Встроенный",
|
||||
customThemeBackground: "Фон",
|
||||
customThemeForeground: "Передний план",
|
||||
customThemeActiveLine: "Текущая строка",
|
||||
customThemeSelection: "Выделение",
|
||||
customThemeCursor: "Курсор",
|
||||
customThemeGutterBackground: "Фон полосы номеров строк",
|
||||
customThemeLineNumber: "Номер строки",
|
||||
customThemeMatchingBracket: "Парная скобка",
|
||||
customThemeDefaultName: "Своя тема",
|
||||
customThemeCopySuffix: " (копия)",
|
||||
customThemeApplied: "Применена цветовая схема «{name}»",
|
||||
|
||||
@@ -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ı',
|
||||
|
||||
@@ -8722,6 +8722,12 @@ export default withEnglishFallback({
|
||||
customThemeBuiltin: "内置变量",
|
||||
customThemeBackground: "背景色",
|
||||
customThemeForeground: "前景色",
|
||||
customThemeActiveLine: "当前行高亮",
|
||||
customThemeSelection: "选中文本",
|
||||
customThemeCursor: "光标颜色",
|
||||
customThemeGutterBackground: "行号栏背景",
|
||||
customThemeLineNumber: "行号颜色",
|
||||
customThemeMatchingBracket: "匹配括号",
|
||||
customThemeDefaultName: "自定义主题",
|
||||
customThemeCopySuffix: " 副本",
|
||||
customThemeApplied: "已应用「{name}」配色方案",
|
||||
|
||||
@@ -7124,6 +7124,12 @@ export default withEnglishFallback({
|
||||
customThemeBuiltin: "內置變量",
|
||||
customThemeBackground: "背景色",
|
||||
customThemeForeground: "前景色",
|
||||
customThemeActiveLine: "當前行高亮",
|
||||
customThemeSelection: "選中文本",
|
||||
customThemeCursor: "光標顏色",
|
||||
customThemeGutterBackground: "行號欄背景",
|
||||
customThemeLineNumber: "行號顏色",
|
||||
customThemeMatchingBracket: "匹配括號",
|
||||
customThemeDefaultName: "自定義主題",
|
||||
customThemeCopySuffix: " 副本",
|
||||
customThemeApplied: "已應用「{name}」配色方案",
|
||||
|
||||
+27
-2
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: [] });
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user