feat(theme): add custom UI color palettes

This commit is contained in:
Domye authored and GitHub committed 2026-08-16 11:59:28 +08:00
1 parent c81466d0db
commit 8996da7d9a
15 files changed
+618 -37

No files matched your search

@@ -125,7 +125,7 @@ import ChangelogPanel from "@/components/settings/ChangelogPanel.vue";
import McpConnectionScopePicker from "@/components/settings/McpConnectionScopePicker.vue";
import ScheduledDatabaseBackupSettings from "@/components/backup/ScheduledDatabaseBackupSettings.vue";
import SqlFormatterSettingsPanel from "./SqlFormatterSettingsPanel.vue";
import { APP_THEME_PALETTES, type AppCornerStyle, type AppThemeAppearance, type AppThemeMode, type AppThemePalette } from "@/lib/app/appTheme";
import { APP_CUSTOM_UI_COLOR_DEFS, APP_THEME_PALETTES, type AppCornerStyle, type AppCustomUiColors, type AppThemeAppearance, type AppThemeMode, type AppThemePalette } from "@/lib/app/appTheme";
import { editorSettingsDraftChanged, editorSettingsDraftFromSettings, editorSettingsPatchFromDraft, normalizeQueryResultMaxRowsDraft, normalizeTableOpenPageSizeDraft, type EditorSettingsDraft } from "@/lib/settings/editorSettingsDraft";
import { useConnectionStore } from "@/stores/connectionStore";
import { useSavedSqlStore } from "@/stores/savedSqlStore";
@@ -152,7 +152,11 @@ const connectionStore = useConnectionStore();
const savedSqlStore = useSavedSqlStore();
const promptTemplateStore = usePromptTemplateStore();
const tunnelProfileStore = useTunnelProfileStore();
const { isDark, themeMode, themePalette, cornerStyle, setThemeMode, setThemePalette, setCornerStyle } = useTheme();
const { isDark, themeMode, themePalette, activeCustomUiColors, cornerStyle, setThemeMode, setThemePalette, setCustomUiColors, resetCustomUiColors, setCornerStyle } = useTheme();
function updateCustomUiColor(key: keyof AppCustomUiColors, value: string) {
setCustomUiColors({ ...activeCustomUiColors.value, [key]: value });
}
const appThemePaletteOptions = computed(
(): Array<{ value: AppThemePalette; label: string; previewColor: string }> =>
@@ -4157,29 +4161,36 @@ onUnmounted(() => {
<div class="flex h-9 items-end">
<Label class="whitespace-normal leading-tight">{{ t("settings.colorTheme") }}</Label>
</div>
<Select :model-value="themePalette" @update:model-value="(value) => setThemePalette(value as AppThemePalette)">
<SelectTrigger class="h-8 w-full gap-1">
<SelectValue :placeholder="t('settings.selectColorTheme')">
<span v-if="selectedThemePaletteOption" class="flex min-w-0 items-center gap-1">
<span
class="h-3 w-3 shrink-0 rounded-full border border-border shadow-xs"
:style="{
background: selectedThemePaletteOption.previewColor,
}"
/>
<span class="truncate">{{ selectedThemePaletteOption.label }}</span>
</span>
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem v-for="option in appThemePaletteOptions" :key="option.value" :value="option.value">
<div class="flex items-center gap-2">
<span class="h-3 w-3 rounded-full border border-border shadow-xs" :style="{ background: option.previewColor }" />
{{ option.label }}
</div>
</SelectItem>
</SelectContent>
</Select>
<div class="flex items-center gap-2">
<div class="min-w-0 flex-1">
<Select :model-value="themePalette" @update:model-value="(value) => setThemePalette(value as AppThemePalette)">
<SelectTrigger class="h-8 w-full gap-1">
<SelectValue :placeholder="t('settings.selectColorTheme')">
<span v-if="selectedThemePaletteOption" class="flex min-w-0 items-center gap-1">
<span
class="h-3 w-3 shrink-0 rounded-full border border-border shadow-xs"
:style="{
background: selectedThemePaletteOption.previewColor,
}"
/>
<span class="truncate">{{ selectedThemePaletteOption.label }}</span>
</span>
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem v-for="option in appThemePaletteOptions" :key="option.value" :value="option.value">
<div class="flex items-center gap-2">
<span class="h-3 w-3 rounded-full border border-border shadow-xs" :style="{ background: option.previewColor }" />
{{ option.label }}
</div>
</SelectItem>
</SelectContent>
</Select>
</div>
<Button v-if="themePalette === 'custom'" type="button" variant="outline" size="sm" class="h-8 shrink-0" @click="resetCustomUiColors">
{{ t("settings.customUiReset") }}
</Button>
</div>
</div>
<div class="settings-appearance-field min-w-0">
@@ -4208,6 +4219,20 @@ onUnmounted(() => {
</SelectContent>
</Select>
</div>
<template v-if="themePalette === 'custom'">
<div class="settings-appearance-field min-w-0">
<div class="flex h-9 items-center gap-1">
<Label class="truncate text-xs text-muted-foreground">{{ isDark ? t("settings.customUiEditingDark") : t("settings.customUiEditingLight") }}</Label>
</div>
</div>
<div v-for="def in APP_CUSTOM_UI_COLOR_DEFS" :key="def.key" class="settings-appearance-field min-w-0">
<div class="flex h-9 items-center justify-between gap-2">
<Label :for="`custom-ui-${def.key}`" class="truncate text-xs text-muted-foreground">{{ t(def.labelKey) }}</Label>
<input :id="`custom-ui-${def.key}`" type="color" class="h-6 w-10 shrink-0 cursor-pointer rounded border border-border bg-transparent p-0.5" :value="activeCustomUiColors[def.key]" @input="updateCustomUiColor(def.key, ($event.target as HTMLInputElement).value)" />
</div>
</div>
</template>
</div>
<div class="grid gap-3 md:grid-cols-2">
@@ -100,7 +100,7 @@ import {
type QueryEditorTableReferencePayload,
} from "@/lib/editor/queryEditorTableDrop";
import type { SqlHighlighter } from "@/lib/sql/sqlHighlighter";
import { EDITOR_FONT_FAMILY_CSS_VAR, EDITOR_FONT_SIZE_CSS_VAR, loadEditorTheme, editorFontTheme, sqlCompletionTheme, sqlSemanticHighlightTheme } from "@/lib/editor/editorThemes";
import { EDITOR_FONT_FAMILY_CSS_VAR, EDITOR_FONT_SIZE_CSS_VAR, editorDiagnosticColors, editorThemeAppearanceFor, loadEditorTheme, editorFontTheme, sqlCompletionTheme, sqlSemanticHighlightTheme } from "@/lib/editor/editorThemes";
import { createStatementGutterMarkerDom, shouldShowStatementGutter } from "@/lib/editor/codemirrorStatementGutter";
import { createQueryEditorSearchKeymap } from "@/lib/editor/queryEditorSearchKeymap";
import { searchKeymapWithoutModD } from "@/lib/editor/codemirrorSearchKeymap";
@@ -225,7 +225,7 @@ function sqlStatementParameterOptions() {
const toggles = resolveSqlVariableSyntaxToggles(settingsStore.editorSettings.sqlVariableSyntaxOverrides, props.databaseType, settingsStore.editorSettings.sqlVariableSubstitutionEnabled);
return { databaseType: props.databaseType, enabledSyntaxes: enabledSqlParameterSyntaxes(toggles) };
}
const { isDark, themePalette } = useTheme();
const { isDark, themePalette, activeCustomUiColors } = useTheme();
const { t } = useI18n();
const { toast } = useToast();
const snippetDatabaseType = computed(() => {
@@ -595,7 +595,7 @@ const tableNavigationHoverClass = "query-editor--table-navigation-hover";
const DBX_VIM_SAVE_EVENT = "dbx-vim-save";
function editorThemeAppearance() {
return isDark.value ? "dark" : "light";
return editorThemeAppearanceFor(isDark.value ? "dark" : "light", themePalette.value, themePalette.value === "custom" ? activeCustomUiColors.value : undefined);
}
// Completion cache
@@ -681,6 +681,16 @@ function syncEditorFontCssVars(fontSize = liveFontSize.value, fontFamily = setti
editorRef.value.style.setProperty(EDITOR_FONT_FAMILY_CSS_VAR, fontFamily);
}
// Diagnostics render on the editor surface, so their marker colors follow the
// resolved editor appearance (which already adapts to custom backgrounds) via
// editor-scoped variables instead of the app-level warning/destructive tokens.
function syncEditorDiagnosticCssVars() {
if (!editorRef.value) return;
const colors = editorDiagnosticColors(editorThemeAppearance());
editorRef.value.style.setProperty("--dbx-editor-diagnostic-error", colors.error);
editorRef.value.style.setProperty("--dbx-editor-diagnostic-warning", colors.warning);
}
let pendingFontReconfig: { size: number; family: string } | null = null;
let fontReconfigScheduled = false;
@@ -4488,11 +4498,11 @@ onMounted(async () => {
const diagnosticTheme = EditorView.baseTheme({
".cm-sql-error": {
textDecoration: "underline wavy var(--destructive)",
textDecoration: "underline wavy var(--dbx-editor-diagnostic-error, var(--destructive))",
textUnderlineOffset: "3px",
},
".cm-sql-semantic-warning": {
textDecoration: "underline wavy var(--warning)",
textDecoration: "underline wavy var(--dbx-editor-diagnostic-warning, var(--warning))",
textUnderlineOffset: "3px",
},
});
@@ -5309,6 +5319,7 @@ onMounted(async () => {
restoreEditorViewport();
syncContextMenuState(view.value);
syncEditorFontCssVars(liveFontSize.value, initialSettings.fontFamily);
syncEditorDiagnosticCssVars();
registerTableReferenceDropListener();
cachedTables = [];
@@ -5476,7 +5487,7 @@ function getCurrentCustomThemeColors() {
// Reactively apply editor settings changes
watch(
[queryEditorAppearanceSettings, () => isDark.value, () => themePalette.value],
[queryEditorAppearanceSettings, () => isDark.value, () => themePalette.value, editorThemeAppearance],
async ([ss]) => {
if (!view.value || !codeMirrorTheme || !fontThemeComp || !wordWrapComp || !vimModeComp || !closeBracketsComp || !runGutterComp || !runKeymapComp || !editorViewModule) {
return;
@@ -5485,6 +5496,7 @@ watch(
liveFontSize.value = ss.fontSize;
}
syncEditorFontCssVars(liveFontSize.value, ss.fontFamily);
syncEditorDiagnosticCssVars();
const themeColors = getCurrentCustomThemeColors();
const [themeExt] = await Promise.all([loadEditorTheme(ss.theme, editorThemeAppearance(), themeColors, themePalette.value), ss.vimModeEnabled ? ensureCodeMirrorVim() : Promise.resolve(false)]);
if (!view.value || !codeMirrorTheme || !wordWrapComp || !vimModeComp || !closeBracketsComp || !runGutterComp || !runKeymapComp || !editorViewModule) {