Feature:新增多套应用配色主题 (#2559)
Co-authored-by: staff <staff@qimaos-MacBook-Pro.local>
This commit is contained in:
parent
896580f1c5
commit
5c35755219
|
|
@ -122,7 +122,7 @@ const connectionDialogPrefill = ref<ConnectionDeepLinkDraft | null>(null);
|
|||
const connectionDialogInitialTab = ref<ConfigTab | undefined>(undefined);
|
||||
const settingsPageTabOpen = ref(false);
|
||||
const settingsPageActive = ref(false);
|
||||
const settingsInitialTab = ref("editor");
|
||||
const settingsInitialTab = ref("appearance");
|
||||
const settingsInitialSection = ref<string | undefined>(undefined);
|
||||
const showQueryEditorDdlDialog = ref(false);
|
||||
const driverStoreTabOpen = ref(false);
|
||||
|
|
@ -265,7 +265,7 @@ const appVersion = ref("");
|
|||
const isClassicLayout = computed(() => settingsStore.editorSettings.appLayout === "classic");
|
||||
const updateNotificationsEnabled = computed(() => settingsStore.editorSettings.updateNotificationsEnabled);
|
||||
|
||||
function openSettings(initialTab = "editor", initialSection?: string) {
|
||||
function openSettings(initialTab = "appearance", initialSection?: string) {
|
||||
settingsInitialTab.value = initialTab;
|
||||
settingsInitialSection.value = initialSection;
|
||||
if (!settingsPageActive.value) {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import { ArrowLeft, Copy, Download, Play, Loader2, PlusCircle, XCircle, ArrowRig
|
|||
const { t } = useI18n();
|
||||
const { toast } = useToast();
|
||||
const settingsStore = useSettingsStore();
|
||||
const { isDark } = useTheme();
|
||||
const { isDark, themePalette } = useTheme();
|
||||
|
||||
const props = defineProps<{
|
||||
deploySql: string;
|
||||
|
|
@ -120,7 +120,7 @@ async function initEditor() {
|
|||
const fontSize = settingsStore.editorSettings.fontSize;
|
||||
const fontFamily = settingsStore.editorSettings.fontFamily;
|
||||
|
||||
const themeExt = await loadEditorTheme(editorTheme, appAppearance);
|
||||
const themeExt = await loadEditorTheme(editorTheme, appAppearance, undefined, themePalette.value);
|
||||
const fontExt = editorFontTheme(EditorView, fontSize, fontFamily, { fixedHeight: true, scrollable: true });
|
||||
|
||||
const dialect = createDbxCodeMirrorSqlDialect(langSql, "postgres");
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ import { DEFAULT_SQL_SNIPPETS } from "@/lib/sql/sqlCompletion";
|
|||
import AiProviderLogo from "@/components/icons/AiProviderLogo.vue";
|
||||
import AppLogo from "@/components/icons/AppLogo.vue";
|
||||
import SqlFormatterSettingsPanel from "./SqlFormatterSettingsPanel.vue";
|
||||
import type { AppThemeAppearance } from "@/lib/app/appTheme";
|
||||
import { APP_THEME_PALETTES, type AppThemeAppearance, type AppThemeMode, type AppThemePalette } from "@/lib/app/appTheme";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useSavedSqlStore } from "@/stores/savedSqlStore";
|
||||
import { currentLocale, setLocale, type Locale } from "@/i18n";
|
||||
|
|
@ -95,7 +95,23 @@ const { toast } = useToast();
|
|||
const settingsStore = useSettingsStore();
|
||||
const connectionStore = useConnectionStore();
|
||||
const savedSqlStore = useSavedSqlStore();
|
||||
const { isDark, themeMode, setThemeMode } = useTheme();
|
||||
const { isDark, themeMode, themePalette, setThemeMode, setThemePalette } = useTheme();
|
||||
|
||||
const appThemePaletteOptions = computed(
|
||||
(): Array<{ value: AppThemePalette; label: string; previewColor: string }> =>
|
||||
APP_THEME_PALETTES.map((palette) => ({
|
||||
value: palette.value,
|
||||
label: t(palette.labelKey),
|
||||
previewColor: palette.previewColor,
|
||||
})),
|
||||
);
|
||||
const selectedThemePaletteOption = computed(() => appThemePaletteOptions.value.find((option) => option.value === themePalette.value) ?? appThemePaletteOptions.value[0]);
|
||||
const selectedLocaleOption = computed(() => LOCALE_OPTIONS.find((locale) => locale.value === currentLocale()) ?? LOCALE_OPTIONS[0]);
|
||||
const appThemeModeOptions = computed(() => [
|
||||
{ value: "light" as AppThemeMode, label: t("toolbar.themeLight"), icon: Sun },
|
||||
{ value: "dark" as AppThemeMode, label: t("toolbar.themeDark"), icon: Moon },
|
||||
{ value: "system" as AppThemeMode, label: t("toolbar.themeSystem"), icon: SunMoon },
|
||||
]);
|
||||
|
||||
let cachedSystemFonts: string[] | null = null;
|
||||
let pendingSystemFonts: Promise<string[]> | null = null;
|
||||
|
|
@ -274,7 +290,9 @@ const systemFontsLoaded = ref(false);
|
|||
const uiScaleOptions = [0.75, 0.9, 1, 1.1, 1.25, 1.5, 1.75, 2];
|
||||
const fontSearchTriggerClass =
|
||||
"h-8 w-full max-w-none justify-between gap-1.5 rounded-[6px] border border-input bg-transparent py-2 pl-2.5 pr-2 text-sm font-normal shadow-none hover:bg-transparent focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 aria-expanded:bg-transparent dark:bg-input/30 dark:hover:bg-input/50";
|
||||
const appearanceFontSearchTriggerClass = `${fontSearchTriggerClass} gap-0 pl-2 pr-1.5`;
|
||||
const fontSearchTriggerIconClass = "size-4 text-muted-foreground";
|
||||
const appearanceFontSearchTriggerIconClass = "size-2.5 text-muted-foreground";
|
||||
const disconnectTabHandlingModeDescriptionKey = computed(() => {
|
||||
switch (editDisconnectTabHandlingMode.value) {
|
||||
case "close-tabs":
|
||||
|
|
@ -1123,14 +1141,14 @@ function setSidebarActivation(value: "single" | "double") {
|
|||
editSidebarActivation.value = value;
|
||||
}
|
||||
|
||||
const activeSettingsTab = ref("editor");
|
||||
const activeSettingsTab = ref("appearance");
|
||||
const isWeb = !isTauriRuntime();
|
||||
const displayedAppVersion = computed(() => (props.appVersion ? `v${props.appVersion}` : ""));
|
||||
type SettingsCategory = "editor" | "formatter" | "appearance" | "navigation" | "data" | "shortcuts" | "snippets" | "sync" | "ai" | "mcp" | "security" | "about";
|
||||
const settingsCategoryNav = computed<{ value: SettingsCategory; label: string }[]>(() => [
|
||||
{ value: "appearance", label: t("settings.appearanceTab") },
|
||||
{ value: "editor", label: t("settings.editorTab") },
|
||||
{ value: "formatter", label: t("settings.sqlFormatterTab") },
|
||||
{ value: "appearance", label: t("settings.appearanceTab") },
|
||||
{ value: "navigation", label: t("settings.navigationTab") },
|
||||
{ value: "data", label: t("settings.dataTab") },
|
||||
{ value: "shortcuts", label: t("settings.shortcutsTab") },
|
||||
|
|
@ -1148,7 +1166,7 @@ function hasSettingsApplyFooter(value: SettingsCategory): boolean {
|
|||
}
|
||||
|
||||
function settingsCategoryButton(value: SettingsCategory): string {
|
||||
return ["w-full rounded-md px-3 py-2 text-left text-sm transition-colors", value === activeSettingsTab.value ? "bg-primary text-primary-foreground shadow-sm" : "text-muted-foreground hover:bg-muted hover:text-foreground"].join(" ");
|
||||
return ["w-auto shrink-0 whitespace-nowrap rounded-md px-3 py-2 text-left text-sm transition-colors lg:w-full", value === activeSettingsTab.value ? "bg-primary text-primary-foreground shadow-sm" : "text-muted-foreground hover:bg-muted hover:text-foreground"].join(" ");
|
||||
}
|
||||
|
||||
function openExternalUrl(url: string) {
|
||||
|
|
@ -1477,7 +1495,7 @@ watch(
|
|||
() => settingsVisible.value,
|
||||
async (open) => {
|
||||
if (open) {
|
||||
activeSettingsTab.value = props.initialTab || "editor";
|
||||
activeSettingsTab.value = props.initialTab || "appearance";
|
||||
passwordMessage.value = "";
|
||||
oldPassword.value = "";
|
||||
newPassword.value = "";
|
||||
|
|
@ -1962,12 +1980,14 @@ const previewSettings = computed<{
|
|||
fontSize: number;
|
||||
theme: EditorTheme;
|
||||
appAppearance: AppThemeAppearance;
|
||||
appPalette: AppThemePalette;
|
||||
customColors?: CustomThemeColors;
|
||||
}>(() => ({
|
||||
fontFamily: editFontFamily.value,
|
||||
fontSize: editFontSize.value,
|
||||
theme: editTheme.value,
|
||||
appAppearance: isDark.value ? "dark" : "light",
|
||||
appPalette: themePalette.value,
|
||||
customColors: getPreviewCustomThemeColors(),
|
||||
}));
|
||||
|
||||
|
|
@ -2017,7 +2037,7 @@ watch(
|
|||
async ([ss]) => {
|
||||
if (!previewView.value || !fontThemeComp || !themeComp || !editorViewModule) return;
|
||||
|
||||
const themeExt = await loadEditorTheme(ss.theme, ss.appAppearance, ss.customColors);
|
||||
const themeExt = await loadEditorTheme(ss.theme, ss.appAppearance, ss.customColors, ss.appPalette);
|
||||
previewView.value.dispatch({
|
||||
effects: [themeComp.reconfigure(themeExt), fontThemeComp.reconfigure(editorFontTheme(editorViewModule.EditorView, ss.fontSize, ss.fontFamily))],
|
||||
});
|
||||
|
|
@ -2065,7 +2085,7 @@ watch(previewRef, async (el) => {
|
|||
previewSqlDiagnostics = previewDiagnosticsForSql(currentPreviewSql());
|
||||
|
||||
const ss = previewSettings.value;
|
||||
const themeExt = await loadEditorTheme(ss.theme, ss.appAppearance, ss.customColors);
|
||||
const themeExt = await loadEditorTheme(ss.theme, ss.appAppearance, ss.customColors, ss.appPalette);
|
||||
const diagnosticTheme = EditorView.baseTheme({
|
||||
".cm-settings-preview-sql-error": {
|
||||
textDecoration: "underline wavy var(--destructive)",
|
||||
|
|
@ -2125,8 +2145,8 @@ onUnmounted(cleanupPreviewEditor);
|
|||
</component>
|
||||
</DialogHeader>
|
||||
|
||||
<div class="flex min-h-0 flex-1 flex-col gap-3 overflow-hidden sm:flex-row">
|
||||
<nav class="settingsCategoryNav flex min-h-0 shrink-0 gap-1 overflow-x-auto border-b pb-3 sm:w-40 sm:flex-col sm:overflow-x-hidden sm:overflow-y-auto sm:border-b-0 sm:border-r sm:pb-0 sm:pr-3">
|
||||
<div class="flex min-h-0 flex-1 flex-col gap-3 overflow-hidden lg:flex-row">
|
||||
<nav class="settingsCategoryNav flex min-h-0 shrink-0 gap-1 overflow-x-auto border-b pb-3 lg:w-40 lg:flex-col lg:overflow-x-hidden lg:overflow-y-auto lg:border-b-0 lg:border-r lg:pb-0 lg:pr-3">
|
||||
<button v-for="category in settingsCategoryNav" :key="category.value" type="button" :class="settingsCategoryButton(category.value)" @click="activeSettingsTab = category.value">
|
||||
{{ category.label }}
|
||||
</button>
|
||||
|
|
@ -2374,16 +2394,25 @@ onUnmounted(cleanupPreviewEditor);
|
|||
</section>
|
||||
|
||||
<section v-else-if="activeSettingsTab === 'appearance'" class="flex flex-col gap-5 py-2">
|
||||
<div class="grid gap-4 md:grid-cols-[minmax(220px,280px)_minmax(260px,1fr)]">
|
||||
<div class="grid gap-x-1.5 gap-y-4 sm:grid-cols-[minmax(0,127fr)_minmax(0,127fr)_minmax(0,191fr)_minmax(0,130fr)]">
|
||||
<div class="space-y-2 min-w-0">
|
||||
<Label>{{ t("settings.languageTitle") }}</Label>
|
||||
<div class="flex h-9 items-end">
|
||||
<Label class="whitespace-normal leading-tight">{{ t("settings.languageTitle") }}</Label>
|
||||
</div>
|
||||
<Select :model-value="currentLocale()" @update:model-value="onLocaleChange">
|
||||
<SelectTrigger class="h-8 w-full">
|
||||
<SelectValue />
|
||||
<SelectTrigger class="h-8 w-full gap-0.5 px-0.5">
|
||||
<SelectValue>
|
||||
<span v-if="selectedLocaleOption" class="flex min-w-0 items-center gap-0.5">
|
||||
<span class="inline-flex h-5 shrink-0 items-center justify-center text-sm font-medium leading-none">
|
||||
{{ selectedLocaleOption.flag }}
|
||||
</span>
|
||||
<span class="truncate">{{ selectedLocaleOption.label }}</span>
|
||||
</span>
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectContent class="w-[150px]">
|
||||
<SelectItem v-for="locale in LOCALE_OPTIONS" :key="locale.value" :value="locale.value">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="inline-flex h-5 w-6 shrink-0 items-center justify-center text-sm font-medium leading-none">
|
||||
{{ locale.flag }}
|
||||
</span>
|
||||
|
|
@ -2395,7 +2424,36 @@ onUnmounted(cleanupPreviewEditor);
|
|||
</div>
|
||||
|
||||
<div class="space-y-2 min-w-0">
|
||||
<Label>{{ t("settings.uiFontFamily") }}</Label>
|
||||
<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>
|
||||
|
||||
<div class="space-y-2 min-w-0">
|
||||
<div class="flex h-9 items-end gap-1">
|
||||
<Label class="min-w-0 whitespace-normal leading-tight">{{ t("settings.uiFontFamily") }}</Label>
|
||||
<HelpTooltip :label="t('settings.uiFontFamily')" trigger-class="[&_svg]:h-3 [&_svg]:w-3" content-class="max-w-64">
|
||||
<p>{{ t("settings.uiFontFamilyDescription") }}</p>
|
||||
</HelpTooltip>
|
||||
</div>
|
||||
<SearchableSelect
|
||||
:model-value="editUiFontFamily"
|
||||
:options="uiFontOptions"
|
||||
|
|
@ -2406,8 +2464,8 @@ onUnmounted(cleanupPreviewEditor);
|
|||
allow-custom
|
||||
:display-name="displayUiFontFamily"
|
||||
:normalize-custom="normalizeCustomFontFamilyInput"
|
||||
:trigger-class="fontSearchTriggerClass"
|
||||
:trigger-icon-class="fontSearchTriggerIconClass"
|
||||
:trigger-class="appearanceFontSearchTriggerClass"
|
||||
:trigger-icon-class="appearanceFontSearchTriggerIconClass"
|
||||
content-class="w-[var(--reka-popover-trigger-width)] min-w-[260px]"
|
||||
@update:model-value="onUiFontFamilyChange"
|
||||
@update:open="(open: boolean) => open && loadSystemFontOptions()"
|
||||
|
|
@ -2426,26 +2484,46 @@ onUnmounted(cleanupPreviewEditor);
|
|||
</span>
|
||||
</template>
|
||||
</SearchableSelect>
|
||||
<p class="text-xs text-muted-foreground">{{ t("settings.uiFontFamilyDescription") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2 min-w-0">
|
||||
<div class="flex h-9 items-end gap-1">
|
||||
<Label class="min-w-0 whitespace-normal leading-tight">{{ t("settings.uiScale") }}</Label>
|
||||
<HelpTooltip :label="t('settings.uiScale')" trigger-class="[&_svg]:h-3 [&_svg]:w-3" content-class="max-w-64">
|
||||
<p>{{ t("settings.uiScaleDescription") }}</p>
|
||||
</HelpTooltip>
|
||||
</div>
|
||||
<Select
|
||||
:model-value="String(editUiScale)"
|
||||
@update:model-value="
|
||||
(value: any) => {
|
||||
const next = Number(value);
|
||||
if (Number.isFinite(next)) editUiScale = next;
|
||||
}
|
||||
"
|
||||
>
|
||||
<SelectTrigger class="h-8 w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem v-for="scale in uiScaleOptions" :key="scale" :value="String(scale)" class="pl-2.5"> {{ Math.round(scale * 100) }}% </SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label>{{ t("settings.theme") }}</Label>
|
||||
<div class="flex gap-2">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Button
|
||||
v-for="option in [
|
||||
{ value: 'light', label: t('toolbar.themeLight'), icon: Sun },
|
||||
{ value: 'dark', label: t('toolbar.themeDark'), icon: Moon },
|
||||
{ value: 'system', label: t('toolbar.themeSystem'), icon: SunMoon },
|
||||
]"
|
||||
v-for="option in appThemeModeOptions"
|
||||
:key="option.value"
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="h-auto gap-1.5 px-3 py-1.5"
|
||||
:class="themeMode === option.value ? 'border-blue-300 ring-2 ring-blue-300/50' : ''"
|
||||
@click="setThemeMode(option.value as 'light' | 'dark' | 'system')"
|
||||
class="h-8 gap-1.5 rounded-[6px] px-3"
|
||||
:class="themeMode === option.value ? 'border-primary/40 bg-primary/10 text-primary ring-1 ring-primary/30' : 'text-foreground'"
|
||||
@click="setThemeMode(option.value)"
|
||||
>
|
||||
<component :is="option.icon" class="h-3.5 w-3.5" />
|
||||
{{ option.label }}
|
||||
|
|
@ -2455,29 +2533,6 @@ onUnmounted(cleanupPreviewEditor);
|
|||
|
||||
<Separator />
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label>{{ t("settings.uiScale") }}</Label>
|
||||
<Select
|
||||
:model-value="String(editUiScale)"
|
||||
@update:model-value="
|
||||
(value: any) => {
|
||||
const next = Number(value);
|
||||
if (Number.isFinite(next)) editUiScale = next;
|
||||
}
|
||||
"
|
||||
>
|
||||
<SelectTrigger class="min-w-36">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem v-for="scale in uiScaleOptions" :key="scale" :value="String(scale)" class="pl-2.5"> {{ Math.round(scale * 100) }}% </SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p class="text-xs text-muted-foreground">{{ t("settings.uiScaleDescription") }}</p>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label>{{ t("settings.appLayout") }}</Label>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ let latestViewport: { scrollTop: number; scrollLeft: number } | undefined = prop
|
|||
let latestSelection: { anchor: number; head: number } | undefined = props.initialSelection;
|
||||
const connectionStore = useConnectionStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
const { isDark } = useTheme();
|
||||
const { isDark, themePalette } = useTheme();
|
||||
const { t } = useI18n();
|
||||
const { toast } = useToast();
|
||||
|
||||
|
|
@ -2295,7 +2295,7 @@ onMounted(async () => {
|
|||
const dialect = createDbxCodeMirrorSqlDialect(langSql, props.dialect);
|
||||
|
||||
const initialSettings = settingsStore.editorSettings;
|
||||
const theme = await loadEditorTheme(initialSettings.theme, editorThemeAppearance(), getCurrentCustomThemeColors());
|
||||
const theme = await loadEditorTheme(initialSettings.theme, editorThemeAppearance(), getCurrentCustomThemeColors(), themePalette.value);
|
||||
if (initialSettings.vimModeEnabled) {
|
||||
await ensureCodeMirrorVim();
|
||||
}
|
||||
|
|
@ -2643,7 +2643,7 @@ onMounted(async () => {
|
|||
if (!view.value || !codeMirrorTheme) return;
|
||||
const settings = settingsStore.editorSettings;
|
||||
const themeColors = settings.theme === "custom" ? getCurrentCustomThemeColors() : settings.customThemeColors;
|
||||
const themeExt = await loadEditorTheme(settings.theme, editorThemeAppearance(), themeColors);
|
||||
const themeExt = await loadEditorTheme(settings.theme, editorThemeAppearance(), themeColors, themePalette.value);
|
||||
view.value.dispatch({
|
||||
effects: [codeMirrorTheme.reconfigure(themeExt)],
|
||||
});
|
||||
|
|
@ -2724,7 +2724,7 @@ function getCurrentCustomThemeColors() {
|
|||
|
||||
// Reactively apply editor settings changes
|
||||
watch(
|
||||
[queryEditorAppearanceSettings, () => isDark.value],
|
||||
[queryEditorAppearanceSettings, () => isDark.value, () => themePalette.value],
|
||||
async ([ss]) => {
|
||||
if (!view.value || !codeMirrorTheme || !fontThemeComp || !wordWrapComp || !vimModeComp || !runKeymapComp || !editorViewModule) {
|
||||
return;
|
||||
|
|
@ -2734,7 +2734,7 @@ watch(
|
|||
}
|
||||
syncEditorFontCssVars(liveFontSize.value, ss.fontFamily);
|
||||
const themeColors = getCurrentCustomThemeColors();
|
||||
const [themeExt] = await Promise.all([loadEditorTheme(ss.theme, editorThemeAppearance(), themeColors), ss.vimModeEnabled ? ensureCodeMirrorVim() : Promise.resolve(false)]);
|
||||
const [themeExt] = await Promise.all([loadEditorTheme(ss.theme, editorThemeAppearance(), themeColors, themePalette.value), ss.vimModeEnabled ? ensureCodeMirrorVim() : Promise.resolve(false)]);
|
||||
if (!view.value || !codeMirrorTheme || !wordWrapComp || !vimModeComp || !runKeymapComp || !editorViewModule) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -169,7 +169,7 @@ const connectionStore = useConnectionStore();
|
|||
const queryStore = useQueryStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
const tableFontSize = computed(() => settingsStore.editorSettings.tableFontSize);
|
||||
const { isDark } = useTheme();
|
||||
const { isDark, themePalette } = useTheme();
|
||||
const { toast } = useToast();
|
||||
const { highlight } = useSqlHighlighter();
|
||||
const binaryCellDownloadMenuItems = computed(() =>
|
||||
|
|
@ -4655,6 +4655,7 @@ let dialogJsonPreviewEditor: UseCellDetailEditorReturn | null = null;
|
|||
|
||||
const editorThemeAccessor = () => settingsStore.editorSettings.theme;
|
||||
const editorAppAppearance = () => (isDark.value ? "dark" : "light") as import("@/lib/app/appTheme").AppThemeAppearance;
|
||||
const editorAppPalette = () => themePalette.value;
|
||||
const editorFontSize = () => settingsStore.editorSettings.fontSize;
|
||||
const editorFontFamily = () => settingsStore.editorSettings.fontFamily;
|
||||
const SIDE_DETAIL_EDITOR_MIN_HEIGHT = 160;
|
||||
|
|
@ -4681,6 +4682,7 @@ watch(detailsEditorContainer, async (el) => {
|
|||
onEscape: () => cancelDetailEdit(),
|
||||
editorTheme: editorThemeAccessor,
|
||||
appAppearance: editorAppAppearance,
|
||||
appPalette: editorAppPalette,
|
||||
fontSize: editorFontSize,
|
||||
fontFamily: editorFontFamily,
|
||||
});
|
||||
|
|
@ -4701,6 +4703,7 @@ watch(valueEditorContainer, async (el) => {
|
|||
onBlur: () => commitValueEditorEdit(),
|
||||
editorTheme: editorThemeAccessor,
|
||||
appAppearance: editorAppAppearance,
|
||||
appPalette: editorAppPalette,
|
||||
fontSize: editorFontSize,
|
||||
fontFamily: editorFontFamily,
|
||||
});
|
||||
|
|
@ -4718,6 +4721,7 @@ watch(sideJsonPreviewContainer, async (el) => {
|
|||
readOnly: true,
|
||||
editorTheme: editorThemeAccessor,
|
||||
appAppearance: editorAppAppearance,
|
||||
appPalette: editorAppPalette,
|
||||
fontSize: editorFontSize,
|
||||
fontFamily: editorFontFamily,
|
||||
});
|
||||
|
|
@ -4735,6 +4739,7 @@ watch(dialogJsonPreviewContainer, async (el) => {
|
|||
readOnly: true,
|
||||
editorTheme: editorThemeAccessor,
|
||||
appAppearance: editorAppAppearance,
|
||||
appPalette: editorAppPalette,
|
||||
fontSize: editorFontSize,
|
||||
fontFamily: editorFontFamily,
|
||||
});
|
||||
|
|
@ -5180,7 +5185,7 @@ 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}:${settingsStore.editorSettings.fontFamily}:${tableFontSize.value}`);
|
||||
const canvasRenderStyleKey = computed(() => `${settingsStore.editorSettings.theme}:${settingsStore.editorSettings.uiScale}:${canvasBackingPixelRatio.value}:${isDark.value}:${themePalette.value}:${settingsStore.editorSettings.fontFamily}:${tableFontSize.value}`);
|
||||
const CANVAS_MOUSE_WHEEL_SCROLL_MULTIPLIER = 1.5;
|
||||
const CANVAS_TRACKPAD_DELTA_THRESHOLD = 40;
|
||||
let canvasResizeObserver: ResizeObserver | null = null;
|
||||
|
|
|
|||
|
|
@ -148,9 +148,9 @@ defineExpose({ focusSearch });
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full shrink-0 relative select-none" :class="classicLayout ? '' : 'rounded-md border border-border/80 bg-background'" :style="{ width: sidebarWidth + 'px' }">
|
||||
<div class="app-sidebar-panel h-full shrink-0 relative select-none" :class="classicLayout ? '' : 'rounded-md border border-border/80 bg-background'" :style="{ width: sidebarWidth + 'px' }">
|
||||
<div class="h-full flex flex-col overflow-hidden">
|
||||
<div class="flex items-center gap-px px-3 text-xs font-medium text-muted-foreground border-b bg-muted/20" :class="classicLayout ? 'h-9' : 'h-10'">
|
||||
<div class="app-sidebar-toolbar flex items-center gap-px px-3 text-xs font-medium text-muted-foreground border-b bg-muted/20" :class="classicLayout ? 'h-9' : 'h-10'">
|
||||
<span class="flex self-stretch items-center truncate" data-tauri-drag-region>{{ t("sidebar.connections") }}</span>
|
||||
<span class="flex-1 self-stretch" data-tauri-drag-region />
|
||||
<template v-if="showConnectionMultiSelectToolbar">
|
||||
|
|
|
|||
|
|
@ -524,7 +524,7 @@ function onOverflowItemKeydown(event: KeyboardEvent, tabId: string, kind: "regul
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="queryStore.tabs.length > 0 || driverStoreOpen || settingsPageOpen" class="relative flex w-full min-w-0 shrink-0 overflow-hidden border-b" :class="[isClassicLayout ? 'bg-muted' : 'bg-background', hasFixedTabs ? 'flex-col' : '']">
|
||||
<div v-if="queryStore.tabs.length > 0 || driverStoreOpen || settingsPageOpen" class="app-tab-bar relative flex w-full min-w-0 shrink-0 overflow-hidden border-b" :class="[isClassicLayout ? 'bg-muted' : 'bg-background', hasFixedTabs ? 'flex-col' : '']">
|
||||
<div class="flex w-full min-w-0 shrink-0 overflow-hidden" :class="isClassicLayout ? 'h-9 items-stretch' : 'h-10 items-center px-2'">
|
||||
<div class="app-tab-strip relative h-full min-w-0 flex-1 overflow-hidden">
|
||||
<div v-if="showRegularTabScrollbar" class="app-tab-scrollbar" :class="{ 'app-tab-scrollbar--dragging': isScrollbarDragging }" @pointerdown="startScrollbarDrag">
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import ExportProgressPopover from "@/components/export/ExportProgressPopover.vue
|
|||
import { shouldReserveMacTrafficLightInset, useWindowControls } from "@/composables/useWindowControls";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import type { AppThemeMode } from "@/lib/app/appTheme";
|
||||
import { isSystemAppThemeMode, type AppThemeMode } from "@/lib/app/appTheme";
|
||||
|
||||
const GithubIcon = {
|
||||
render() {
|
||||
|
|
@ -61,14 +61,15 @@ const toolbarItems = computed(() => settingsStore.editorSettings.toolbarItems);
|
|||
const { isMac, isDesktop, showControls, isMaximized, isFullscreen, minimize, toggleMaximize, close } = useWindowControls();
|
||||
|
||||
const themeTriggerIcon = computed(() => {
|
||||
if (props.themeMode === "system") return SunMoon;
|
||||
if (isSystemAppThemeMode(props.themeMode)) return SunMoon;
|
||||
return props.isDark ? Moon : Sun;
|
||||
});
|
||||
|
||||
const themeCycle: AppThemeMode[] = ["light", "dark", "system"];
|
||||
|
||||
function nextThemeMode(mode: AppThemeMode): AppThemeMode {
|
||||
if (mode === "light") return "dark";
|
||||
if (mode === "dark") return "system";
|
||||
return "light";
|
||||
const index = themeCycle.indexOf(mode);
|
||||
return themeCycle[(index + 1) % themeCycle.length] ?? themeCycle[0];
|
||||
}
|
||||
|
||||
function themeModeLabel(mode: AppThemeMode): string {
|
||||
|
|
@ -362,7 +363,7 @@ const toolbarDropdownTriggerClass = `inline-flex h-8 items-center gap-1 rounded-
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="toolbarEl" class="h-10 flex items-center gap-1 px-2 border-b bg-muted/30 shrink-0 overflow-hidden" :class="{ 'pl-17.5': shouldReserveMacTrafficLightInset(isMac, isFullscreen, isDesktop) }" data-tauri-drag-region @dblclick="onToolbarDblClick">
|
||||
<div ref="toolbarEl" class="app-toolbar h-10 flex items-center gap-1 px-2 border-b bg-muted/30 shrink-0 overflow-hidden" :class="{ 'pl-17.5': shouldReserveMacTrafficLightInset(isMac, isFullscreen, isDesktop) }" data-tauri-drag-region @dblclick="onToolbarDblClick">
|
||||
<Button variant="ghost" size="sm" :class="toolbarTextButtonClass" @click="emit('new-connection')">
|
||||
<DatabaseZap class="h-3.5 w-3.5" />
|
||||
<span :class="toolbarTextLabelClass">{{ t("toolbar.newConnection") }}</span>
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ function connectionById(connectionId: string): ConnectionConfig | undefined {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-9 shrink-0 border-b bg-background/80 px-3 flex items-center gap-1 text-xs text-muted-foreground relative z-10" :style="toolbarStyle">
|
||||
<div class="app-editor-toolbar h-9 shrink-0 border-b bg-background/80 px-3 flex items-center gap-1 text-xs text-muted-foreground relative z-10" :style="toolbarStyle">
|
||||
<div class="flex items-center gap-0.5">
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ const { toast } = useToast();
|
|||
const { t } = useI18n();
|
||||
const settingsStore = useSettingsStore();
|
||||
const connectionStore = useConnectionStore();
|
||||
const { isDark } = useTheme();
|
||||
const { isDark, themePalette } = useTheme();
|
||||
const activeTab = ref<AdminTab>("configs");
|
||||
const connectionInfo = ref<NacosConnectionInfo | null>(null);
|
||||
const connectionError = ref("");
|
||||
|
|
@ -191,7 +191,7 @@ async function mountConfigEditor() {
|
|||
configLanguageExtension(configType.value),
|
||||
]);
|
||||
const editorSettings = settingsStore.editorSettings;
|
||||
const theme = await loadEditorTheme(editorSettings.theme, editorThemeAppearance(), currentCustomThemeColors());
|
||||
const theme = await loadEditorTheme(editorSettings.theme, editorThemeAppearance(), currentCustomThemeColors(), themePalette.value);
|
||||
const view = new EditorView({
|
||||
parent: configEditorHost.value,
|
||||
state: EditorState.create({
|
||||
|
|
@ -819,11 +819,11 @@ watch(historyCompareOpen, (value) => {
|
|||
});
|
||||
|
||||
watch(
|
||||
[() => settingsStore.editorSettings, () => isDark.value],
|
||||
[() => settingsStore.editorSettings, () => isDark.value, () => themePalette.value],
|
||||
async ([settings]) => {
|
||||
const view = configEditorView.value;
|
||||
if (!view) return;
|
||||
const [{ EditorView }, theme] = await Promise.all([import("@codemirror/view"), loadEditorTheme(settings.theme, editorThemeAppearance(), currentCustomThemeColors())]);
|
||||
const [{ EditorView }, theme] = await Promise.all([import("@codemirror/view"), loadEditorTheme(settings.theme, editorThemeAppearance(), currentCustomThemeColors(), themePalette.value)]);
|
||||
if (configEditorView.value !== view) return;
|
||||
view.dispatch({
|
||||
effects: [configEditorTheme.reconfigure(theme), configEditorFontTheme.reconfigure(editorFontTheme(EditorView, settings.fontSize, settings.fontFamily, { fixedHeight: true, scrollable: true }))],
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ const emit = defineEmits<{
|
|||
|
||||
const { t } = useI18n();
|
||||
const { toast } = useToast();
|
||||
const { isDark } = useTheme();
|
||||
const { isDark, themePalette } = useTheme();
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
const ddlContent = ref("");
|
||||
|
|
@ -84,7 +84,7 @@ async function initDdlEditor(content: string) {
|
|||
const appAppearance = isDark.value ? "dark" : "light";
|
||||
const fontSize = settingsStore.editorSettings.fontSize;
|
||||
const fontFamily = settingsStore.editorSettings.fontFamily;
|
||||
const themeExt = await loadEditorTheme(editorTheme, appAppearance);
|
||||
const themeExt = await loadEditorTheme(editorTheme, appAppearance, undefined, themePalette.value);
|
||||
const fontExt = editorFontTheme(EditorView, fontSize, fontFamily, { fixedHeight: true, scrollable: true });
|
||||
const dialect = createDbxCodeMirrorSqlDialect(langSql, props.dialect);
|
||||
const state = EditorState.create({
|
||||
|
|
|
|||
|
|
@ -991,7 +991,7 @@ defineExpose({ focusSearch, createNewGroup, collapseAllTreeNodes });
|
|||
|
||||
<template>
|
||||
<div ref="rootRef" class="h-full min-h-0 flex flex-col text-sm select-none" @pointerenter="pointerInsideTree = true" @pointerleave="pointerInsideTree = false">
|
||||
<div class="sticky top-0 z-10 bg-background px-2 py-1">
|
||||
<div class="connection-tree-search sticky top-0 z-10 bg-background px-2 py-1">
|
||||
<div class="relative flex items-center gap-1">
|
||||
<div class="relative flex-1">
|
||||
<Search class="absolute left-2 top-1/2 -translate-y-1/2 h-3 w-3 text-muted-foreground" />
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import { clampEditorFontSize, createEditorZoomCommitScheduler, fontSizeFromGestu
|
|||
import i18n from "@/i18n";
|
||||
import EditorSearchPanel from "@/components/editor/EditorSearchPanel.vue";
|
||||
import type { EditorTheme } from "@/stores/settingsStore";
|
||||
import type { AppThemeAppearance } from "@/lib/app/appTheme";
|
||||
import type { AppThemeAppearance, AppThemePalette } from "@/lib/app/appTheme";
|
||||
|
||||
export interface UseCellDetailEditorOptions {
|
||||
onChange?: (value: string) => void;
|
||||
|
|
@ -24,6 +24,7 @@ export interface UseCellDetailEditorOptions {
|
|||
readOnly?: boolean;
|
||||
editorTheme: () => EditorTheme;
|
||||
appAppearance: () => AppThemeAppearance;
|
||||
appPalette: () => AppThemePalette;
|
||||
fontSize: () => number;
|
||||
fontFamily: () => string;
|
||||
}
|
||||
|
|
@ -130,14 +131,14 @@ export function useCellDetailEditor(options: UseCellDetailEditorOptions): UseCel
|
|||
zoomCommitScheduler.flush(liveFontSize);
|
||||
}
|
||||
|
||||
watch([() => options.fontSize(), () => options.fontFamily(), () => options.editorTheme(), () => options.appAppearance()], async ([fontSize, fontFamily, editorTheme, appearance]) => {
|
||||
watch([() => options.fontSize(), () => options.fontFamily(), () => options.editorTheme(), () => options.appAppearance(), () => options.appPalette()], async ([fontSize, fontFamily, editorTheme, appearance, palette]) => {
|
||||
const editor = view.value;
|
||||
if (!editor || destroyed) return;
|
||||
if (!isGestureZooming && !zoomCommitScheduler.hasPendingCommit()) {
|
||||
liveFontSize = clampEditorFontSize(fontSize);
|
||||
}
|
||||
syncEditorFontCssVars(liveFontSize, fontFamily);
|
||||
const theme = await loadEditorTheme(editorTheme, appearance);
|
||||
const theme = await loadEditorTheme(editorTheme, appearance, undefined, palette);
|
||||
if (!view.value || destroyed) return;
|
||||
view.value.dispatch({
|
||||
effects: [themeComp.reconfigure(theme), fontThemeComp.reconfigure(editorFontTheme(EditorView, liveFontSize, fontFamily, { fixedHeight: true, scrollable: true }))],
|
||||
|
|
@ -150,7 +151,7 @@ export function useCellDetailEditor(options: UseCellDetailEditorOptions): UseCel
|
|||
const doc = initialValue ?? "";
|
||||
currentIsJson = options.language === "json" || shouldUseJsonMode(columnType, doc);
|
||||
|
||||
const theme = await loadEditorTheme(options.editorTheme(), options.appAppearance());
|
||||
const theme = await loadEditorTheme(options.editorTheme(), options.appAppearance(), undefined, options.appPalette());
|
||||
liveFontSize = clampEditorFontSize(options.fontSize());
|
||||
const fontTheme = editorFontTheme(EditorView, liveFontSize, options.fontFamily(), { fixedHeight: true, scrollable: true });
|
||||
const shortcuts = settingsStore.editorSettings.shortcuts;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,25 @@
|
|||
import { computed, ref } from "vue";
|
||||
import { APP_THEME_STORAGE_KEY, getTauriThemeForMode, normalizeAppThemeMode, resolveAppThemeAppearance, type AppThemeMode } from "@/lib/app/appTheme";
|
||||
import {
|
||||
APP_THEME_PALETTE_CLASS_NAMES,
|
||||
APP_THEME_PALETTE_STORAGE_KEY,
|
||||
APP_THEME_STORAGE_KEY,
|
||||
getAppThemePaletteClass,
|
||||
getTauriThemeForMode,
|
||||
isSystemAppThemeMode,
|
||||
normalizeAppThemeMode,
|
||||
normalizeAppThemePalette,
|
||||
resolveAppThemeAppearance,
|
||||
type AppThemeMode,
|
||||
type AppThemePalette,
|
||||
} from "@/lib/app/appTheme";
|
||||
import { safeLocalStorageGet, safeLocalStorageSet } from "@/lib/backend/safeStorage";
|
||||
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
|
||||
|
||||
const themeMode = ref<AppThemeMode>(normalizeAppThemeMode(safeLocalStorageGet(APP_THEME_STORAGE_KEY)));
|
||||
const savedThemeMode = safeLocalStorageGet(APP_THEME_STORAGE_KEY);
|
||||
const themeMode = ref<AppThemeMode>(normalizeAppThemeMode(savedThemeMode));
|
||||
const savedThemePalette = safeLocalStorageGet(APP_THEME_PALETTE_STORAGE_KEY);
|
||||
const themePalette = ref<AppThemePalette>(normalizeAppThemePalette(savedThemePalette));
|
||||
if (savedThemeMode && savedThemeMode !== themeMode.value) safeLocalStorageSet(APP_THEME_STORAGE_KEY, themeMode.value);
|
||||
const systemPrefersDark = ref(readSystemPrefersDark());
|
||||
const isDark = computed(() => resolveAppThemeAppearance(themeMode.value, systemPrefersDark.value) === "dark");
|
||||
|
||||
|
|
@ -22,7 +38,7 @@ function setupSystemThemeListener() {
|
|||
systemPrefersDark.value = mediaQuery.matches;
|
||||
const onChange = (event: MediaQueryListEvent) => {
|
||||
systemPrefersDark.value = event.matches;
|
||||
if (themeMode.value === "system") applyTheme();
|
||||
if (isSystemAppThemeMode(themeMode.value)) applyTheme();
|
||||
};
|
||||
mediaQuery.addEventListener("change", onChange);
|
||||
isListeningForSystemTheme = true;
|
||||
|
|
@ -36,6 +52,9 @@ function applyTheme() {
|
|||
|
||||
doc.classList.add("disable-transitions");
|
||||
doc.classList.toggle("dark", dark);
|
||||
for (const className of APP_THEME_PALETTE_CLASS_NAMES) doc.classList.remove(className);
|
||||
const paletteClass = getAppThemePaletteClass(themePalette.value);
|
||||
if (paletteClass) doc.classList.add(paletteClass);
|
||||
doc.style.colorScheme = dark ? "dark" : "light";
|
||||
|
||||
// force reflow so the class toggle takes effect before re-enabling transitions
|
||||
|
|
@ -65,6 +84,12 @@ function setThemeMode(mode: AppThemeMode) {
|
|||
applyTheme();
|
||||
}
|
||||
|
||||
function setThemePalette(palette: AppThemePalette) {
|
||||
themePalette.value = palette;
|
||||
safeLocalStorageSet(APP_THEME_PALETTE_STORAGE_KEY, palette);
|
||||
applyTheme();
|
||||
}
|
||||
|
||||
export function useTheme() {
|
||||
setupSystemThemeListener();
|
||||
|
||||
|
|
@ -72,5 +97,5 @@ export function useTheme() {
|
|||
setThemeMode(isDark.value ? "light" : "dark");
|
||||
}
|
||||
|
||||
return { isDark, themeMode, applyTheme, setThemeMode, toggleTheme };
|
||||
return { isDark, themeMode, themePalette, applyTheme, setThemeMode, setThemePalette, toggleTheme };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2681,6 +2681,21 @@ export default {
|
|||
uiScaleDescription: "Scale the entire desktop UI for high-DPI displays. Changes apply immediately and are restored on next launch.",
|
||||
theme: "Theme",
|
||||
selectTheme: "Select theme...",
|
||||
colorTheme: "Color Theme",
|
||||
selectColorTheme: "Select color theme...",
|
||||
themePalettePearl: "White",
|
||||
themePaletteMist: "Mist Gray",
|
||||
themePaletteGraphite: "Titanium Gray",
|
||||
themePaletteCobalt: "Cobalt Blue",
|
||||
themePaletteSage: "Sage Green",
|
||||
themePaletteAmber: "Warm Yellow",
|
||||
themePaletteBlush: "Sakura Pink",
|
||||
themePaletteVscode: "VS Code",
|
||||
themePaletteIdea: "IDEA",
|
||||
themePaletteXcode: "Xcode",
|
||||
themePaletteJetbrains: "JetBrains",
|
||||
themePaletteCursor: "Cursor",
|
||||
themePaletteClaude: "Claude Code",
|
||||
followAppTheme: "Follow app theme",
|
||||
appLayout: "Interface Layout",
|
||||
appLayoutSeparated: "Modern separated",
|
||||
|
|
|
|||
|
|
@ -2637,6 +2637,21 @@ export default withEnglishFallback({
|
|||
uiScaleDescription: "Escala toda la interfaz de escritorio para pantallas de alta densidad. Los cambios se aplican al instante y se restauran al volver a abrir.",
|
||||
theme: "Tema",
|
||||
selectTheme: "Seleccionar tema...",
|
||||
colorTheme: "Tema de color",
|
||||
selectColorTheme: "Seleccionar tema de color...",
|
||||
themePalettePearl: "Blanco",
|
||||
themePaletteMist: "Gris niebla",
|
||||
themePaletteGraphite: "Gris titanio",
|
||||
themePaletteCobalt: "Azul cobalto",
|
||||
themePaletteSage: "Verde salvia",
|
||||
themePaletteAmber: "Amarillo cálido",
|
||||
themePaletteBlush: "Rosa sakura",
|
||||
themePaletteVscode: "VS Code",
|
||||
themePaletteIdea: "IDEA",
|
||||
themePaletteXcode: "Xcode",
|
||||
themePaletteJetbrains: "JetBrains",
|
||||
themePaletteCursor: "Cursor",
|
||||
themePaletteClaude: "Claude Code",
|
||||
followAppTheme: "Seguir el tema de la aplicación",
|
||||
appLayout: "Diseño de la interfaz",
|
||||
appLayoutSeparated: "Moderno separado",
|
||||
|
|
|
|||
|
|
@ -2635,6 +2635,21 @@ export default withEnglishFallback({
|
|||
uiScaleDescription: "Scala l'intera interfaccia utente desktop per display ad alta densità (High-DPI). Le modifiche si applicano immediatamente e vengono ripristinate al prossimo avvio.",
|
||||
theme: "Tema",
|
||||
selectTheme: "Seleziona tema...",
|
||||
colorTheme: "Tema colore",
|
||||
selectColorTheme: "Seleziona tema colore...",
|
||||
themePalettePearl: "Bianco",
|
||||
themePaletteMist: "Grigio nebbia",
|
||||
themePaletteGraphite: "Grigio titanio",
|
||||
themePaletteCobalt: "Blu cobalto",
|
||||
themePaletteSage: "Verde salvia",
|
||||
themePaletteAmber: "Giallo caldo",
|
||||
themePaletteBlush: "Rosa sakura",
|
||||
themePaletteVscode: "VS Code",
|
||||
themePaletteIdea: "IDEA",
|
||||
themePaletteXcode: "Xcode",
|
||||
themePaletteJetbrains: "JetBrains",
|
||||
themePaletteCursor: "Cursor",
|
||||
themePaletteClaude: "Claude Code",
|
||||
followAppTheme: "Segui il tema dell'applicazione",
|
||||
appLayout: "Layout dell'Interfaccia",
|
||||
appLayoutSeparated: "Separato moderno",
|
||||
|
|
|
|||
|
|
@ -2635,6 +2635,21 @@ export default withEnglishFallback({
|
|||
uiScaleDescription: "高DPIディスプレイ向けにデスクトップUI全体をスケーリングします。変更は即座に適用され、次回起動時に復元されます。",
|
||||
theme: "テーマ",
|
||||
selectTheme: "テーマを選択...",
|
||||
colorTheme: "配色テーマ",
|
||||
selectColorTheme: "配色テーマを選択...",
|
||||
themePalettePearl: "白",
|
||||
themePaletteMist: "霧灰",
|
||||
themePaletteGraphite: "チタングレー",
|
||||
themePaletteCobalt: "コバルトブルー",
|
||||
themePaletteSage: "セージグリーン",
|
||||
themePaletteAmber: "暖黄",
|
||||
themePaletteBlush: "桜ピンク",
|
||||
themePaletteVscode: "VS Code",
|
||||
themePaletteIdea: "IDEA",
|
||||
themePaletteXcode: "Xcode",
|
||||
themePaletteJetbrains: "JetBrains",
|
||||
themePaletteCursor: "Cursor",
|
||||
themePaletteClaude: "Claude Code",
|
||||
followAppTheme: "アプリテーマに従う",
|
||||
appLayout: "インターフェースレイアウト",
|
||||
appLayoutSeparated: "モダン分離型",
|
||||
|
|
|
|||
|
|
@ -2636,6 +2636,21 @@ export default withEnglishFallback({
|
|||
uiScaleDescription: "Dimensione toda a UI do desktop para telas de alta resolução (high-DPI). As alterações são aplicadas imediatamente e restauradas na próxima inicialização.",
|
||||
theme: "Tema",
|
||||
selectTheme: "Selecionar tema...",
|
||||
colorTheme: "Tema de cores",
|
||||
selectColorTheme: "Selecionar tema de cores...",
|
||||
themePalettePearl: "Branco",
|
||||
themePaletteMist: "Cinza névoa",
|
||||
themePaletteGraphite: "Cinza titânio",
|
||||
themePaletteCobalt: "Azul cobalto",
|
||||
themePaletteSage: "Verde sálvia",
|
||||
themePaletteAmber: "Amarelo quente",
|
||||
themePaletteBlush: "Rosa sakura",
|
||||
themePaletteVscode: "VS Code",
|
||||
themePaletteIdea: "IDEA",
|
||||
themePaletteXcode: "Xcode",
|
||||
themePaletteJetbrains: "JetBrains",
|
||||
themePaletteCursor: "Cursor",
|
||||
themePaletteClaude: "Claude Code",
|
||||
followAppTheme: "Seguir o tema do app",
|
||||
appLayout: "Layout da interface",
|
||||
appLayoutSeparated: "Separado moderno",
|
||||
|
|
|
|||
|
|
@ -2681,6 +2681,21 @@ export default withEnglishFallback({
|
|||
uiScaleDescription: "按比例缩放整个桌面端界面,适合高清屏;修改后立即生效,并在下次启动时恢复。",
|
||||
theme: "主题",
|
||||
selectTheme: "选择主题...",
|
||||
colorTheme: "配色主题",
|
||||
selectColorTheme: "选择配色主题...",
|
||||
themePalettePearl: "洁白",
|
||||
themePaletteMist: "雾灰",
|
||||
themePaletteGraphite: "钛灰",
|
||||
themePaletteCobalt: "钴蓝",
|
||||
themePaletteSage: "青绿",
|
||||
themePaletteAmber: "暖黄",
|
||||
themePaletteBlush: "樱粉",
|
||||
themePaletteVscode: "VS Code",
|
||||
themePaletteIdea: "IDEA",
|
||||
themePaletteXcode: "Xcode",
|
||||
themePaletteJetbrains: "JetBrains",
|
||||
themePaletteCursor: "Cursor",
|
||||
themePaletteClaude: "Claude Code",
|
||||
followAppTheme: "跟随应用主题",
|
||||
appLayout: "界面布局",
|
||||
appLayoutSeparated: "现代分隔",
|
||||
|
|
|
|||
|
|
@ -2538,6 +2538,21 @@ export default withEnglishFallback({
|
|||
uiScaleDescription: "按比例縮放整個桌面端介面,適合高 DPI 螢幕;修改後立即生效,並在下次啟動時復原。",
|
||||
theme: "主題",
|
||||
selectTheme: "選擇主題……",
|
||||
colorTheme: "配色主題",
|
||||
selectColorTheme: "選擇配色主題……",
|
||||
themePalettePearl: "潔白",
|
||||
themePaletteMist: "霧灰",
|
||||
themePaletteGraphite: "鈦灰",
|
||||
themePaletteCobalt: "鈷藍",
|
||||
themePaletteSage: "青綠",
|
||||
themePaletteAmber: "暖黃",
|
||||
themePaletteBlush: "櫻粉",
|
||||
themePaletteVscode: "VS Code",
|
||||
themePaletteIdea: "IDEA",
|
||||
themePaletteXcode: "Xcode",
|
||||
themePaletteJetbrains: "JetBrains",
|
||||
themePaletteCursor: "Cursor",
|
||||
themePaletteClaude: "Claude Code",
|
||||
followAppTheme: "跟隨應用程式主題",
|
||||
appLayout: "介面配置",
|
||||
appLayoutSeparated: "現代分隔",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { resolveEditorTheme } from "@/lib/editor/editorThemes";
|
||||
import type { AppThemePalette } from "@/lib/app/appTheme";
|
||||
import type { EditorTheme } from "@/stores/settingsStore";
|
||||
|
||||
describe("resolveEditorTheme", () => {
|
||||
it("maps only the follow-app editor theme to application IDE palettes", () => {
|
||||
expect(resolveEditorTheme("app", "light", "xcode")).toBe("xcode");
|
||||
expect(resolveEditorTheme("app", "dark", "xcode")).toBe("xcode-dark");
|
||||
expect(resolveEditorTheme("app", "light", "cursor")).toBe("cursor-light");
|
||||
expect(resolveEditorTheme("app", "dark", "cursor")).toBe("cursor-dark");
|
||||
});
|
||||
|
||||
it("keeps explicit editor themes unchanged across application palettes", () => {
|
||||
const explicitThemes: Array<Exclude<EditorTheme, "app">> = [
|
||||
"one-dark",
|
||||
"vscode-dark",
|
||||
"vscode-light",
|
||||
"nord",
|
||||
"okaidia",
|
||||
"material",
|
||||
"duotone-light",
|
||||
"duotone-dark",
|
||||
"xcode",
|
||||
"xcode-dark",
|
||||
"idea-light",
|
||||
"idea-dark",
|
||||
"jetbrains-light",
|
||||
"jetbrains-dark",
|
||||
"cursor-light",
|
||||
"cursor-dark",
|
||||
"claude-light",
|
||||
"claude-dark",
|
||||
"custom",
|
||||
];
|
||||
const appPalettes: AppThemePalette[] = ["pearl", "vscode", "idea", "xcode", "jetbrains", "cursor", "claude"];
|
||||
|
||||
for (const theme of explicitThemes) {
|
||||
for (const palette of appPalettes) {
|
||||
expect(resolveEditorTheme(theme, "dark", palette)).toBe(theme);
|
||||
expect(resolveEditorTheme(theme, "light", palette)).toBe(theme);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -1,20 +1,64 @@
|
|||
import type { Theme } from "@tauri-apps/api/window";
|
||||
|
||||
export const APP_THEME_STORAGE_KEY = "dbx-theme";
|
||||
export const APP_THEME_PALETTE_STORAGE_KEY = "dbx-theme-palette";
|
||||
|
||||
export type AppThemeMode = "light" | "dark" | "system";
|
||||
export type AppThemeAppearance = "light" | "dark";
|
||||
export type AppThemePalette = "pearl" | "mist" | "graphite" | "cobalt" | "sage" | "amber" | "blush" | "vscode" | "idea" | "xcode" | "jetbrains" | "cursor" | "claude";
|
||||
|
||||
export type AppThemePaletteOption = {
|
||||
value: AppThemePalette;
|
||||
labelKey: string;
|
||||
className: string | null;
|
||||
previewColor: string;
|
||||
};
|
||||
|
||||
export const APP_THEME_PALETTES: AppThemePaletteOption[] = [
|
||||
{ value: "pearl", labelKey: "settings.themePalettePearl", className: null, previewColor: "#ffffff" },
|
||||
{ value: "mist", labelKey: "settings.themePaletteMist", className: "theme-soft", previewColor: "#e4eaf2" },
|
||||
{ value: "graphite", labelKey: "settings.themePaletteGraphite", className: "theme-graphite", previewColor: "#d8dce4" },
|
||||
{ value: "cobalt", labelKey: "settings.themePaletteCobalt", className: "theme-cobalt", previewColor: "#d8e6f7" },
|
||||
{ value: "sage", labelKey: "settings.themePaletteSage", className: "theme-sage", previewColor: "#dbe9e2" },
|
||||
{ value: "amber", labelKey: "settings.themePaletteAmber", className: "theme-amber", previewColor: "#f4e4b8" },
|
||||
{ value: "blush", labelKey: "settings.themePaletteBlush", className: "theme-blush", previewColor: "#f4d9e6" },
|
||||
{ value: "vscode", labelKey: "settings.themePaletteVscode", className: "theme-vscode", previewColor: "#007acc" },
|
||||
{ value: "idea", labelKey: "settings.themePaletteIdea", className: "theme-idea", previewColor: "#4b6eaf" },
|
||||
{ value: "xcode", labelKey: "settings.themePaletteXcode", className: "theme-xcode", previewColor: "#0a84ff" },
|
||||
{ value: "jetbrains", labelKey: "settings.themePaletteJetbrains", className: "theme-jetbrains", previewColor: "#7b61ff" },
|
||||
{ value: "cursor", labelKey: "settings.themePaletteCursor", className: "theme-cursor", previewColor: "#6ba4ff" },
|
||||
{ value: "claude", labelKey: "settings.themePaletteClaude", className: "theme-claude", previewColor: "#c47a50" },
|
||||
];
|
||||
|
||||
export const APP_THEME_PALETTE_CLASS_NAMES = APP_THEME_PALETTES.map((palette) => palette.className).filter((className): className is string => Boolean(className));
|
||||
|
||||
export function normalizeAppThemeMode(value: string | null): AppThemeMode {
|
||||
if (value === "soft-light") return "light";
|
||||
if (value === "soft-dark") return "dark";
|
||||
if (value === "soft-system") return "system";
|
||||
if (value === "dark" || value === "light" || value === "system") return value;
|
||||
return "light";
|
||||
}
|
||||
|
||||
export function normalizeAppThemePalette(value: string | null): AppThemePalette {
|
||||
if (value === "mist" || value === "graphite" || value === "cobalt" || value === "sage" || value === "amber" || value === "blush" || value === "vscode" || value === "idea" || value === "xcode" || value === "jetbrains" || value === "cursor" || value === "claude" || value === "pearl") return value;
|
||||
return "pearl";
|
||||
}
|
||||
|
||||
export function getAppThemePaletteClass(palette: AppThemePalette): string | null {
|
||||
return APP_THEME_PALETTES.find((option) => option.value === palette)?.className ?? null;
|
||||
}
|
||||
|
||||
export function isSystemAppThemeMode(mode: AppThemeMode): boolean {
|
||||
return mode === "system";
|
||||
}
|
||||
|
||||
export function resolveAppThemeAppearance(mode: AppThemeMode, systemPrefersDark: boolean): AppThemeAppearance {
|
||||
if (mode === "system") return systemPrefersDark ? "dark" : "light";
|
||||
return mode;
|
||||
if (isSystemAppThemeMode(mode)) return systemPrefersDark ? "dark" : "light";
|
||||
return mode === "dark" ? "dark" : "light";
|
||||
}
|
||||
|
||||
export function getTauriThemeForMode(mode: AppThemeMode): Theme | null {
|
||||
return mode === "system" ? null : mode;
|
||||
if (isSystemAppThemeMode(mode)) return null;
|
||||
return resolveAppThemeAppearance(mode, false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { Extension } from "@codemirror/state";
|
||||
import type { EditorTheme, CustomThemeColors } from "@/stores/settingsStore";
|
||||
import type { AppThemeAppearance } from "@/lib/app/appTheme";
|
||||
import type { AppThemeAppearance, AppThemePalette } from "@/lib/app/appTheme";
|
||||
import { HighlightStyle, syntaxHighlighting } from "@codemirror/language";
|
||||
import { tags } from "@lezer/highlight";
|
||||
|
||||
|
|
@ -54,7 +54,7 @@ function createCustomTheme(EditorView: typeof import("@codemirror/view").EditorV
|
|||
// 根据系统主题设置默认背景色和前景色
|
||||
const defaultColors = isDark ? { background: "#1e1e2e", foreground: "#cdd6f4" } : { background: "#fafafa", foreground: "#242424" };
|
||||
|
||||
const c = { ...defaultColors, ...customThemeColors, ...(colors || {}) };
|
||||
const c = { ...defaultColors, ...customThemeColors, ...colors };
|
||||
|
||||
// 映射用户自定义属性名到 CodeMirror 内部属性名
|
||||
if (colors) {
|
||||
|
|
@ -179,6 +179,371 @@ function createCustomTheme(EditorView: typeof import("@codemirror/view").EditorV
|
|||
return [theme, syntaxHighlighting(highlightStyle)];
|
||||
}
|
||||
|
||||
type IdeEditorThemeColors = {
|
||||
dark: boolean;
|
||||
background: string;
|
||||
foreground: string;
|
||||
selection: string;
|
||||
selectionMatch: string;
|
||||
cursor: string;
|
||||
gutterBackground: string;
|
||||
gutterForeground: string;
|
||||
gutterActiveForeground: string;
|
||||
activeLine: string;
|
||||
matchingBracket: string;
|
||||
gutterBorder: string;
|
||||
keyword: string;
|
||||
string: string;
|
||||
number: string;
|
||||
comment: string;
|
||||
type: string;
|
||||
variable: string;
|
||||
function: string;
|
||||
operator: string;
|
||||
punctuation: string;
|
||||
property: string;
|
||||
builtin: string;
|
||||
meta: string;
|
||||
invalid: string;
|
||||
tag: string;
|
||||
attribute: string;
|
||||
className: string;
|
||||
keywordBold?: boolean;
|
||||
stringBold?: boolean;
|
||||
numberBold?: boolean;
|
||||
};
|
||||
|
||||
const IDE_EDITOR_THEMES = {
|
||||
ideaLight: {
|
||||
dark: false,
|
||||
background: "#ffffff",
|
||||
foreground: "#080808",
|
||||
selection: "#a6d2ff",
|
||||
selectionMatch: "#93d9d9",
|
||||
cursor: "#000000",
|
||||
gutterBackground: "#f2f2f2",
|
||||
gutterForeground: "#adadad",
|
||||
gutterActiveForeground: "#767a8a",
|
||||
activeLine: "#fcfaed",
|
||||
matchingBracket: "#93d9d9",
|
||||
gutterBorder: "#d4d4d4",
|
||||
keyword: "#0033b3",
|
||||
string: "#067d17",
|
||||
number: "#1750eb",
|
||||
comment: "#8c8c8c",
|
||||
type: "#0033b3",
|
||||
variable: "#174ad4",
|
||||
function: "#00627a",
|
||||
operator: "#080808",
|
||||
punctuation: "#080808",
|
||||
property: "#871094",
|
||||
builtin: "#0033b3",
|
||||
meta: "#9e880d",
|
||||
invalid: "#f50000",
|
||||
tag: "#0033b3",
|
||||
attribute: "#871094",
|
||||
className: "#174be6",
|
||||
keywordBold: true,
|
||||
stringBold: true,
|
||||
numberBold: true,
|
||||
},
|
||||
ideaDark: {
|
||||
dark: true,
|
||||
background: "#2b2b2b",
|
||||
foreground: "#a9b7c6",
|
||||
selection: "#214283",
|
||||
selectionMatch: "#3b514d",
|
||||
cursor: "#bbbbbb",
|
||||
gutterBackground: "#313335",
|
||||
gutterForeground: "#606366",
|
||||
gutterActiveForeground: "#a4a3a3",
|
||||
activeLine: "#323232",
|
||||
matchingBracket: "#3b514d",
|
||||
gutterBorder: "#4d4d4d",
|
||||
keyword: "#cc7832",
|
||||
string: "#6a8759",
|
||||
number: "#6897bb",
|
||||
comment: "#808080",
|
||||
type: "#a9b7c6",
|
||||
variable: "#a9b7c6",
|
||||
function: "#ffc66d",
|
||||
operator: "#cc7832",
|
||||
punctuation: "#a9b7c6",
|
||||
property: "#9876aa",
|
||||
builtin: "#cc7832",
|
||||
meta: "#bbb529",
|
||||
invalid: "#ff0000",
|
||||
tag: "#e8bf6a",
|
||||
attribute: "#bababa",
|
||||
className: "#a9b7c6",
|
||||
},
|
||||
jetbrainsLight: {
|
||||
dark: false,
|
||||
background: "#ffffff",
|
||||
foreground: "#080808",
|
||||
selection: "#a6d2ff",
|
||||
selectionMatch: "#c9ecec",
|
||||
cursor: "#000000",
|
||||
gutterBackground: "#ffffff",
|
||||
gutterForeground: "#aeb3c2",
|
||||
gutterActiveForeground: "#767a8a",
|
||||
activeLine: "#f5f8fe",
|
||||
matchingBracket: "#93d9d9",
|
||||
gutterBorder: "#ebecf0",
|
||||
keyword: "#0033b3",
|
||||
string: "#067d17",
|
||||
number: "#1750eb",
|
||||
comment: "#8c8c8c",
|
||||
type: "#0033b3",
|
||||
variable: "#174ad4",
|
||||
function: "#00627a",
|
||||
operator: "#080808",
|
||||
punctuation: "#080808",
|
||||
property: "#871094",
|
||||
builtin: "#0033b3",
|
||||
meta: "#9e880d",
|
||||
invalid: "#f50000",
|
||||
tag: "#0033b3",
|
||||
attribute: "#871094",
|
||||
className: "#174be6",
|
||||
keywordBold: true,
|
||||
stringBold: true,
|
||||
numberBold: true,
|
||||
},
|
||||
jetbrainsDark: {
|
||||
dark: true,
|
||||
background: "#1e1f22",
|
||||
foreground: "#bcbec4",
|
||||
selection: "#2e436e",
|
||||
selectionMatch: "#114957",
|
||||
cursor: "#ced0d6",
|
||||
gutterBackground: "#1e1f22",
|
||||
gutterForeground: "#4b5059",
|
||||
gutterActiveForeground: "#a1a3ab",
|
||||
activeLine: "#26282e",
|
||||
matchingBracket: "#43454a",
|
||||
gutterBorder: "#313438",
|
||||
keyword: "#cf8e6d",
|
||||
string: "#6aab73",
|
||||
number: "#2aacb8",
|
||||
comment: "#7a7e85",
|
||||
type: "#cf8e6d",
|
||||
variable: "#bcbec4",
|
||||
function: "#56a8f5",
|
||||
operator: "#bcbec4",
|
||||
punctuation: "#bcbec4",
|
||||
property: "#c77dbb",
|
||||
builtin: "#cf8e6d",
|
||||
meta: "#b3ae60",
|
||||
invalid: "#f75464",
|
||||
tag: "#2fbaa3",
|
||||
attribute: "#b3ae60",
|
||||
className: "#56a8f5",
|
||||
},
|
||||
cursorLight: {
|
||||
dark: false,
|
||||
background: "#fcfcfc",
|
||||
foreground: "#141414eb",
|
||||
selection: "#1414141e",
|
||||
selectionMatch: "#14141411",
|
||||
cursor: "#141414eb",
|
||||
gutterBackground: "#fcfcfc",
|
||||
gutterForeground: "#1414147a",
|
||||
gutterActiveForeground: "#141414ad",
|
||||
activeLine: "#ededed",
|
||||
matchingBracket: "#1414141e",
|
||||
gutterBorder: "#14141413",
|
||||
keyword: "#b3003f",
|
||||
string: "#9e94d5",
|
||||
number: "#b8448b",
|
||||
comment: "#141414ad",
|
||||
type: "#206595",
|
||||
variable: "#206595",
|
||||
function: "#db704b",
|
||||
operator: "#b3003f",
|
||||
punctuation: "#141414eb",
|
||||
property: "#1f8a65",
|
||||
builtin: "#206595",
|
||||
meta: "#1f8a65",
|
||||
invalid: "#cf2d56",
|
||||
tag: "#206595",
|
||||
attribute: "#6049b3",
|
||||
className: "#206595",
|
||||
},
|
||||
cursorDark: {
|
||||
dark: true,
|
||||
background: "#181818",
|
||||
foreground: "#e4e4e4eb",
|
||||
selection: "#40404099",
|
||||
selectionMatch: "#404040cc",
|
||||
cursor: "#e4e4e4eb",
|
||||
gutterBackground: "#181818",
|
||||
gutterForeground: "#e4e4e442",
|
||||
gutterActiveForeground: "#e4e4e4eb",
|
||||
activeLine: "#262626",
|
||||
matchingBracket: "#e4e4e41e",
|
||||
gutterBorder: "#e4e4e413",
|
||||
keyword: "#82d2ce",
|
||||
string: "#e394dc",
|
||||
number: "#ebc88d",
|
||||
comment: "#e4e4e45e",
|
||||
type: "#efb080",
|
||||
variable: "#87c3ff",
|
||||
function: "#efb080",
|
||||
operator: "#d6d6dd",
|
||||
punctuation: "#d6d6dd",
|
||||
property: "#82d2ce",
|
||||
builtin: "#a8cc7c",
|
||||
meta: "#a8cc7c",
|
||||
invalid: "#e34671",
|
||||
tag: "#82d2ce",
|
||||
attribute: "#aaa0fa",
|
||||
className: "#efb080",
|
||||
},
|
||||
claudeLight: {
|
||||
dark: false,
|
||||
background: "#fffdf8",
|
||||
foreground: "#302820",
|
||||
selection: "#ead8c6",
|
||||
selectionMatch: "#e7d8c8",
|
||||
cursor: "#b86b3c",
|
||||
gutterBackground: "#f8f1e9",
|
||||
gutterForeground: "#9a7f66",
|
||||
gutterActiveForeground: "#5f4b3a",
|
||||
activeLine: "#f4e9de",
|
||||
matchingBracket: "#e2c9b1",
|
||||
gutterBorder: "#ddcdbb",
|
||||
keyword: "#9a4f2e",
|
||||
string: "#4f7d5d",
|
||||
number: "#2d6f91",
|
||||
comment: "#8c745f",
|
||||
type: "#7a5aa8",
|
||||
variable: "#3e5f75",
|
||||
function: "#b86b3c",
|
||||
operator: "#6b5544",
|
||||
punctuation: "#6b5544",
|
||||
property: "#9a4f2e",
|
||||
builtin: "#4f7d5d",
|
||||
meta: "#8f6b2e",
|
||||
invalid: "#c3493d",
|
||||
tag: "#7a5aa8",
|
||||
attribute: "#b86b3c",
|
||||
className: "#7a5aa8",
|
||||
},
|
||||
claudeDark: {
|
||||
dark: true,
|
||||
background: "#211f1c",
|
||||
foreground: "#d8d0c4",
|
||||
selection: "#44382f",
|
||||
selectionMatch: "#564539",
|
||||
cursor: "#d28a5f",
|
||||
gutterBackground: "#1b1917",
|
||||
gutterForeground: "#8f7f70",
|
||||
gutterActiveForeground: "#d8d0c4",
|
||||
activeLine: "#2a2723",
|
||||
matchingBracket: "#564539",
|
||||
gutterBorder: "#7f6c5b59",
|
||||
keyword: "#d28a5f",
|
||||
string: "#74b195",
|
||||
number: "#65a1c6",
|
||||
comment: "#a39686",
|
||||
type: "#a08fcd",
|
||||
variable: "#d8d0c4",
|
||||
function: "#d69a6b",
|
||||
operator: "#e0d0c0",
|
||||
punctuation: "#c9b9a7",
|
||||
property: "#d28a5f",
|
||||
builtin: "#74b195",
|
||||
meta: "#d4ae63",
|
||||
invalid: "#e2675f",
|
||||
tag: "#a08fcd",
|
||||
attribute: "#d69a6b",
|
||||
className: "#a08fcd",
|
||||
},
|
||||
} satisfies Record<string, IdeEditorThemeColors>;
|
||||
|
||||
function createIdeEditorTheme(EditorView: typeof import("@codemirror/view").EditorView, c: IdeEditorThemeColors): Extension {
|
||||
const theme = EditorView.theme(
|
||||
{
|
||||
"&": {
|
||||
backgroundColor: c.background,
|
||||
color: c.foreground,
|
||||
[EDITOR_SELECTION_BACKGROUND_CSS_VAR]: c.selection,
|
||||
},
|
||||
".cm-scroller": {
|
||||
backgroundColor: c.background,
|
||||
},
|
||||
".cm-content": {
|
||||
caretColor: c.cursor,
|
||||
},
|
||||
".cm-cursor": {
|
||||
borderLeftColor: c.cursor,
|
||||
},
|
||||
"&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": {
|
||||
backgroundColor: c.selection,
|
||||
},
|
||||
".cm-selectionMatch": {
|
||||
backgroundColor: c.selectionMatch,
|
||||
},
|
||||
".cm-activeLine": {
|
||||
backgroundColor: c.activeLine,
|
||||
},
|
||||
".cm-gutters": {
|
||||
backgroundColor: c.gutterBackground,
|
||||
borderRight: `1px solid ${c.gutterBorder}`,
|
||||
color: c.gutterForeground,
|
||||
},
|
||||
".cm-activeLineGutter": {
|
||||
backgroundColor: c.activeLine,
|
||||
color: c.gutterActiveForeground,
|
||||
},
|
||||
".cm-matchingBracket": {
|
||||
backgroundColor: c.matchingBracket,
|
||||
outline: "none",
|
||||
},
|
||||
},
|
||||
{ dark: c.dark },
|
||||
);
|
||||
|
||||
const highlightStyle = HighlightStyle.define([
|
||||
{ tag: [tags.keyword, tags.controlKeyword, tags.definitionKeyword, tags.moduleKeyword, tags.operatorKeyword, tags.modifier, tags.bool, tags.null], color: c.keyword, ...(c.keywordBold ? { fontWeight: "bold" } : {}) },
|
||||
{ tag: [tags.string, tags.special(tags.string), tags.regexp, tags.escape, tags.inserted], color: c.string, ...(c.stringBold ? { fontWeight: "bold" } : {}) },
|
||||
{ tag: [tags.number, tags.integer, tags.float], color: c.number, ...(c.numberBold ? { fontWeight: "bold" } : {}) },
|
||||
{ tag: [tags.comment, tags.lineComment, tags.blockComment, tags.quote], color: c.comment, fontStyle: "italic" },
|
||||
{ tag: [tags.typeName, tags.typeOperator, tags.unit], color: c.type },
|
||||
{ tag: [tags.name, tags.variableName, tags.definition(tags.variableName)], color: c.variable },
|
||||
{ tag: [tags.function(tags.variableName), tags.function(tags.propertyName), tags.function(tags.name), tags.macroName], color: c.function },
|
||||
{ tag: [tags.standard(tags.variableName), tags.special(tags.name)], color: c.builtin },
|
||||
{ tag: [tags.propertyName, tags.labelName, tags.annotation], color: c.property },
|
||||
{ tag: [tags.operator, tags.compareOperator, tags.logicOperator, tags.arithmeticOperator, tags.derefOperator], color: c.operator },
|
||||
{ tag: [tags.punctuation, tags.separator, tags.paren, tags.brace, tags.bracket, tags.angleBracket], color: c.punctuation },
|
||||
{ tag: tags.tagName, color: c.tag },
|
||||
{ tag: tags.attributeName, color: c.attribute },
|
||||
{ tag: tags.attributeValue, color: c.string },
|
||||
{ tag: [tags.className, tags.namespace], color: c.className },
|
||||
{ tag: [tags.meta, tags.processingInstruction], color: c.meta },
|
||||
{ tag: tags.invalid, color: c.invalid },
|
||||
{ tag: [tags.heading, tags.heading1, tags.heading2, tags.heading3], color: c.keyword, fontWeight: "bold" },
|
||||
{ tag: tags.strong, color: c.foreground, fontWeight: "bold" },
|
||||
{ tag: tags.emphasis, color: c.foreground, fontStyle: "italic" },
|
||||
{ tag: [tags.link, tags.url], color: c.type, textDecoration: "underline" },
|
||||
{ tag: tags.literal, color: c.string },
|
||||
{ tag: tags.deleted, color: c.invalid },
|
||||
{ tag: tags.changed, color: c.property },
|
||||
{ tag: tags.self, color: c.keyword },
|
||||
{ tag: tags.list, color: c.foreground },
|
||||
{ tag: tags.monospace, color: c.foreground },
|
||||
{ tag: tags.strikethrough, color: c.invalid, textDecoration: "line-through" },
|
||||
{ tag: tags.contentSeparator, color: c.operator },
|
||||
]);
|
||||
|
||||
return [theme, syntaxHighlighting(highlightStyle)];
|
||||
}
|
||||
|
||||
async function loadIdeEditorTheme(colors: IdeEditorThemeColors): Promise<Extension> {
|
||||
return createIdeEditorTheme((await import("@codemirror/view")).EditorView, colors);
|
||||
}
|
||||
|
||||
// ======================================================
|
||||
|
||||
const TABLE_ICON: LucideIconNode = [
|
||||
|
|
@ -242,15 +607,32 @@ export function cellDetailActiveLineColor(): string {
|
|||
return colorMixValue("var(--accent)", "color-mix(in oklch, var(--foreground) 4%, transparent)");
|
||||
}
|
||||
|
||||
/** Load a CodeMirror theme extension by theme name. */
|
||||
export function resolveEditorTheme(theme: EditorTheme, appAppearance: AppThemeAppearance): Exclude<EditorTheme, "app"> {
|
||||
if (theme === "app") return appAppearance === "dark" ? "one-dark" : "vscode-light";
|
||||
/** Resolve the concrete CodeMirror theme used by the "Follow app theme" setting. */
|
||||
export function resolveEditorTheme(theme: EditorTheme, appAppearance: AppThemeAppearance, appPalette: AppThemePalette = "pearl"): Exclude<EditorTheme, "app"> {
|
||||
if (theme === "app") {
|
||||
switch (appPalette) {
|
||||
case "vscode":
|
||||
return appAppearance === "dark" ? "vscode-dark" : "vscode-light";
|
||||
case "idea":
|
||||
return appAppearance === "dark" ? "idea-dark" : "idea-light";
|
||||
case "xcode":
|
||||
return appAppearance === "dark" ? "xcode-dark" : "xcode";
|
||||
case "jetbrains":
|
||||
return appAppearance === "dark" ? "jetbrains-dark" : "jetbrains-light";
|
||||
case "cursor":
|
||||
return appAppearance === "dark" ? "cursor-dark" : "cursor-light";
|
||||
case "claude":
|
||||
return appAppearance === "dark" ? "claude-dark" : "claude-light";
|
||||
default:
|
||||
return appAppearance === "dark" ? "one-dark" : "vscode-light";
|
||||
}
|
||||
}
|
||||
return theme;
|
||||
}
|
||||
|
||||
/** Load a CodeMirror theme extension by theme name. */
|
||||
export async function loadEditorTheme(theme: EditorTheme, appAppearance: AppThemeAppearance = "dark", customColors?: CustomThemeColors): Promise<Extension> {
|
||||
const resolvedTheme = resolveEditorTheme(theme, appAppearance);
|
||||
export async function loadEditorTheme(theme: EditorTheme, appAppearance: AppThemeAppearance = "dark", customColors?: CustomThemeColors, appPalette: AppThemePalette = "pearl"): Promise<Extension> {
|
||||
const resolvedTheme = resolveEditorTheme(theme, appAppearance, appPalette);
|
||||
switch (resolvedTheme) {
|
||||
case "one-dark":
|
||||
return (await import("@codemirror/theme-one-dark")).oneDark;
|
||||
|
|
@ -270,6 +652,24 @@ export async function loadEditorTheme(theme: EditorTheme, appAppearance: AppThem
|
|||
return (await import("@uiw/codemirror-theme-duotone")).duotoneDark;
|
||||
case "xcode":
|
||||
return (await import("@uiw/codemirror-theme-xcode")).xcodeLight;
|
||||
case "xcode-dark":
|
||||
return (await import("@uiw/codemirror-theme-xcode")).xcodeDark;
|
||||
case "idea-light":
|
||||
return loadIdeEditorTheme(IDE_EDITOR_THEMES.ideaLight);
|
||||
case "idea-dark":
|
||||
return loadIdeEditorTheme(IDE_EDITOR_THEMES.ideaDark);
|
||||
case "jetbrains-light":
|
||||
return loadIdeEditorTheme(IDE_EDITOR_THEMES.jetbrainsLight);
|
||||
case "jetbrains-dark":
|
||||
return loadIdeEditorTheme(IDE_EDITOR_THEMES.jetbrainsDark);
|
||||
case "cursor-light":
|
||||
return loadIdeEditorTheme(IDE_EDITOR_THEMES.cursorLight);
|
||||
case "cursor-dark":
|
||||
return loadIdeEditorTheme(IDE_EDITOR_THEMES.cursorDark);
|
||||
case "claude-light":
|
||||
return loadIdeEditorTheme(IDE_EDITOR_THEMES.claudeLight);
|
||||
case "claude-dark":
|
||||
return loadIdeEditorTheme(IDE_EDITOR_THEMES.claudeDark);
|
||||
case "custom":
|
||||
return createCustomTheme((await import("@codemirror/view")).EditorView, customColors, appAppearance === "dark");
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -30,6 +30,12 @@ describe("normalizeEditorSettings", () => {
|
|||
expect(normalizeEditorSettings({}).updateDownloadSource).toBe("official");
|
||||
});
|
||||
|
||||
it("preserves explicit editor themes from saved settings", () => {
|
||||
expect(normalizeEditorSettings({ theme: "xcode" }).theme).toBe("xcode");
|
||||
expect(normalizeEditorSettings({ theme: "one-dark" }).theme).toBe("one-dark");
|
||||
expect(normalizeEditorSettings({ theme: "custom" }).theme).toBe("custom");
|
||||
});
|
||||
|
||||
it("restores all open tabs on launch by default", () => {
|
||||
expect(normalizeEditorSettings({}).openTabsRestoreMode).toBe("all");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -252,7 +252,27 @@ function inferAiProviderFromConfig(config: Partial<AiConfig> | null | undefined)
|
|||
return "claude";
|
||||
}
|
||||
|
||||
export type EditorTheme = "app" | "one-dark" | "vscode-dark" | "vscode-light" | "nord" | "okaidia" | "material" | "duotone-light" | "duotone-dark" | "xcode" | "custom";
|
||||
export type EditorTheme =
|
||||
| "app"
|
||||
| "one-dark"
|
||||
| "vscode-dark"
|
||||
| "vscode-light"
|
||||
| "nord"
|
||||
| "okaidia"
|
||||
| "material"
|
||||
| "duotone-light"
|
||||
| "duotone-dark"
|
||||
| "xcode"
|
||||
| "xcode-dark"
|
||||
| "idea-light"
|
||||
| "idea-dark"
|
||||
| "jetbrains-light"
|
||||
| "jetbrains-dark"
|
||||
| "cursor-light"
|
||||
| "cursor-dark"
|
||||
| "claude-light"
|
||||
| "claude-dark"
|
||||
| "custom";
|
||||
|
||||
const STRUCTURE_EDITOR_DENSITIES = ["compact", "standard", "comfortable"] as const;
|
||||
export type StructureEditorDensity = (typeof STRUCTURE_EDITOR_DENSITIES)[number];
|
||||
|
|
@ -426,6 +446,15 @@ export const EDITOR_THEMES: { value: EditorTheme; label: string; dark: boolean }
|
|||
{ value: "duotone-light", label: "Duotone Light", dark: false },
|
||||
{ value: "duotone-dark", label: "Duotone Dark", dark: true },
|
||||
{ value: "xcode", label: "Xcode", dark: false },
|
||||
{ value: "xcode-dark", label: "Xcode Dark", dark: true },
|
||||
{ value: "idea-light", label: "IDEA Light", dark: false },
|
||||
{ value: "idea-dark", label: "IDEA Darcula", dark: true },
|
||||
{ value: "jetbrains-light", label: "JetBrains Light", dark: false },
|
||||
{ value: "jetbrains-dark", label: "JetBrains Dark", dark: true },
|
||||
{ value: "cursor-light", label: "Cursor Light", dark: false },
|
||||
{ value: "cursor-dark", label: "Cursor Dark", dark: true },
|
||||
{ value: "claude-light", label: "Claude Code Light", dark: false },
|
||||
{ value: "claude-dark", label: "Claude Code Dark", dark: true },
|
||||
{ value: "custom", label: "Custom", dark: true },
|
||||
];
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue