feat(settings): add global settings search

This commit is contained in:
二丫讲梵 authored and GitHub committed 2026-08-02 16:34:14 +08:00
1 parent 3af3dda9b5
commit 86c53479a1
12 files changed
+742 -47

No files matched your search

@@ -121,6 +121,7 @@ import { useSavedSqlStore } from "@/stores/savedSqlStore";
import { usePromptTemplateStore } from "@/stores/promptTemplateStore";
import { useTunnelProfileStore } from "@/stores/tunnelProfileStore";
import { currentLocale, setLocale, type Locale } from "@/i18n";
import { SETTINGS_SEARCH_DEFINITIONS, TOOLBAR_VISIBILITY_ITEMS, createShortcutSettingsSearchDefinitions, resolveSettingsSearchEntries, searchSettings, toolbarVisibilityItemLabel, type SettingsCategory, type SettingsSearchEntry, type ToolbarVisibilityItem } from "@/lib/settings/settingsSearch";
import { LOCALE_OPTIONS } from "@/lib/app/localeOptions";
import { DEFAULT_WEB_DAV_AUTO_UPLOAD_INTERVAL_MINUTES, DEFAULT_WEB_DAV_REMOTE_PATH, normalizedWebDavAutoUploadInterval, writeWebDavAutoUploadFields } from "@/lib/webdav/webdavAutoUploadConfig";
import { apiUrl } from "@/lib/common/webPath";
@@ -410,6 +411,10 @@ const editExportRowLimit = ref(settingsStore.editorSettings.exportRowLimit);
const editQueryExportKeysetOptimizationEnabled = ref(settingsStore.editorSettings.queryExportKeysetOptimizationEnabled);
const editUpdateDownloadSource = ref<UpdateDownloadSource>(settingsStore.editorSettings.updateDownloadSource);
const editToolbarItems = ref({ ...settingsStore.editorSettings.toolbarItems });
const toolbarVisibilityItems = TOOLBAR_VISIBILITY_ITEMS;
function getToolbarVisibilityItemLabel(item: ToolbarVisibilityItem): string {
return toolbarVisibilityItemLabel(item, t);
}
const systemFonts = ref<string[]>([]);
const systemFontsLoading = ref(false);
const systemFontsLoaded = ref(false);
@@ -1369,7 +1374,6 @@ const appSupportInfoLabels = computed<AppSupportInfoLabels>(() => ({
unknown: t("settings.supportInfoUnknown"),
}));
const appSupportInfoRows = computed(() => (appSupportInfo.value ? buildAppSupportInfoRows(appSupportInfo.value, appSupportInfoLabels.value) : []));
type SettingsCategory = "editor" | "formatter" | "appearance" | "navigation" | "data" | "backups" | "tunnels" | "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") },
@@ -1399,6 +1403,164 @@ function settingsCategoryButton(value: SettingsCategory): string {
].join(" ");
}
const settingsSearchQuery = ref("");
const settingsSearchOpen = ref(false);
const settingsSearchActiveIndex = ref(0);
const settingsSearchInputContainerRef = ref<HTMLElement | null>(null);
const highlightedSettingsSearchTargetId = ref("");
let highlightedSettingsSearchElement: HTMLElement | null = null;
let pendingSettingsSearchResult: SettingsSearchEntry | null = null;
let settingsSearchHighlightTimer: ReturnType<typeof window.setTimeout> | null = null;
let settingsSearchHighlightAnimationHandler: ((event: AnimationEvent) => void) | null = null;
const settingsSearchHighlightClasses = ["rounded-md", "bg-primary/5", "transition-[box-shadow,background-color]", "duration-200", "settings-search-highlight-breathe"];
const settingsSearchCategoryLabels = computed(() => Object.fromEntries(settingsCategoryNav.value.map((category) => [category.value, category.label])) as Record<SettingsCategory, string>);
const settingsSearchEntries = computed(() =>
resolveSettingsSearchEntries(
[...SETTINGS_SEARCH_DEFINITIONS, ...createShortcutSettingsSearchDefinitions(SHORTCUT_DEFINITIONS)],
{
isWeb,
visibleCategories: new Set(settingsCategoryNav.value.map((category) => category.value)),
},
t,
settingsSearchCategoryLabels.value,
),
);
const settingsSearchResults = computed(() => searchSettings(settingsSearchEntries.value, settingsSearchQuery.value, currentLocale()));
const settingsSearchActive = computed(() => Boolean(settingsSearchQuery.value.trim()));
const settingsSearchVisible = computed(() => settingsSearchOpen.value && settingsSearchActive.value);
const settingsSearchResultGroups = computed(() => {
const groups = new Map<SettingsCategory, { categoryLabel: string; results: (typeof settingsSearchResults.value)[number][] }>();
for (const result of settingsSearchResults.value) {
const group = groups.get(result.category) ?? { categoryLabel: result.categoryLabel, results: [] };
group.results.push(result);
groups.set(result.category, group);
}
return Array.from(groups, ([category, group]) => ({ category, ...group }));
});
function clearSettingsSearchHighlight() {
if (settingsSearchHighlightTimer) {
window.clearTimeout(settingsSearchHighlightTimer);
settingsSearchHighlightTimer = null;
}
if (highlightedSettingsSearchElement && settingsSearchHighlightAnimationHandler) {
highlightedSettingsSearchElement.removeEventListener("animationend", settingsSearchHighlightAnimationHandler);
}
settingsSearchHighlightAnimationHandler = null;
highlightedSettingsSearchElement?.classList.remove(...settingsSearchHighlightClasses);
highlightedSettingsSearchElement = null;
highlightedSettingsSearchTargetId.value = "";
}
function resetSettingsSearchState() {
settingsSearchQuery.value = "";
settingsSearchOpen.value = false;
settingsSearchActiveIndex.value = 0;
pendingSettingsSearchResult = null;
shortcutSearchQuery.value = "";
clearSettingsSearchHighlight();
}
function exitSettingsSearch() {
settingsSearchQuery.value = "";
settingsSearchOpen.value = false;
}
async function focusSettingsSearchInput() {
await nextTick();
settingsSearchInputContainerRef.value?.querySelector<HTMLInputElement>("input")?.focus();
}
function settingsSearchTargetClass(targetId: string): string {
return highlightedSettingsSearchTargetId.value === targetId ? "ring-2 ring-primary ring-offset-2 ring-offset-background transition-shadow" : "";
}
function onSettingsCategoryClick(category: SettingsCategory) {
settingsSearchOpen.value = false;
activeSettingsTab.value = category;
}
function applySettingsSearchRoute(result: SettingsSearchEntry) {
if (result.route?.syncMethodTab) syncMethodTab.value = result.route.syncMethodTab;
}
function normalizeSettingsSearchText(value: string | null | undefined): string {
return value?.replace(/\s+/g, " ").trim() ?? "";
}
function findSettingsSearchHighlightTarget(searchRoot: HTMLElement, title: string): HTMLElement {
const titleElement = Array.from(searchRoot.querySelectorAll<HTMLElement>("label, h3, h4")).find((element) => normalizeSettingsSearchText(element.textContent) === title);
if (!titleElement) return searchRoot;
let candidate = titleElement.parentElement;
while (candidate && candidate !== searchRoot) {
if (candidate.classList.contains("rounded-md") && candidate.classList.contains("border")) return candidate;
if (candidate.querySelector("input, button, [role='combobox'], textarea")) return candidate;
candidate = candidate.parentElement;
}
return titleElement;
}
async function revealSettingsSearchTarget(result: SettingsSearchEntry) {
await nextTick();
const searchRoot = settingsContentScrollRef.value?.querySelector<HTMLElement>(`[data-settings-search-id="${result.targetId}"]`);
if (!searchRoot) return;
const target = findSettingsSearchHighlightTarget(searchRoot, result.title);
target.scrollIntoView({ block: "center", behavior: "smooth" });
clearSettingsSearchHighlight();
if (target === searchRoot) {
highlightedSettingsSearchTargetId.value = result.targetId;
}
target.classList.add(...settingsSearchHighlightClasses);
highlightedSettingsSearchElement = target;
if (window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) {
settingsSearchHighlightTimer = window.setTimeout(clearSettingsSearchHighlight, 1500);
return;
}
settingsSearchHighlightAnimationHandler = (event) => {
if (event.animationName === "settings-search-highlight-breathe") clearSettingsSearchHighlight();
};
target.addEventListener("animationend", settingsSearchHighlightAnimationHandler);
}
async function selectSettingsSearchResult(result: SettingsSearchEntry) {
pendingSettingsSearchResult = result;
if (result.shortcutId) shortcutSearchQuery.value = result.title;
applySettingsSearchRoute(result);
settingsSearchQuery.value = "";
settingsSearchOpen.value = false;
settingsSearchActiveIndex.value = 0;
if (activeSettingsTab.value === result.category) {
pendingSettingsSearchResult = null;
await revealSettingsSearchTarget(result);
return;
}
activeSettingsTab.value = result.category;
}
function onSettingsSearchKeydown(event: KeyboardEvent) {
const results = settingsSearchResults.value;
if (event.key === "Escape") {
event.preventDefault();
exitSettingsSearch();
return;
}
if (!results.length) return;
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
event.preventDefault();
settingsSearchOpen.value = true;
const direction = event.key === "ArrowDown" ? 1 : -1;
settingsSearchActiveIndex.value = (settingsSearchActiveIndex.value + direction + results.length) % results.length;
return;
}
if (event.key === "Enter") {
event.preventDefault();
void selectSettingsSearchResult(results[settingsSearchActiveIndex.value] ?? results[0]);
}
}
async function resetSettingsContentScroll() {
await nextTick();
const scroller = settingsContentScrollRef.value;
@@ -2041,6 +2203,8 @@ watch(
() => settingsVisible.value,
async (open) => {
if (open) {
resetSettingsSearchState();
void focusSettingsSearchInput();
snippetSyncSettingsLoading.value = true;
mcpPolicyLoading.value = true;
mcpPolicyLoadError.value = "";
@@ -2095,6 +2259,8 @@ watch(
if (!isWeb && activeSettingsTab.value === "ai" && aiIsCliProvider.value) void ensureCliMcpStatus();
if (activeSettingsTab.value === "about") void refreshAppSupportInfo();
await scrollToInitialSettingsSection();
} else {
resetSettingsSearchState();
}
},
{ immediate: true },
@@ -2155,6 +2321,16 @@ watch(activeSettingsTab, async (tab) => {
checkLayoutDescTruncation();
checkIconThemeDescTruncation();
}
const result = pendingSettingsSearchResult;
if (result) {
pendingSettingsSearchResult = null;
await revealSettingsSearchTarget(result);
}
});
watch(settingsSearchQuery, (query) => {
settingsSearchActiveIndex.value = 0;
settingsSearchOpen.value = Boolean(query.trim());
});
// If the store finishes loading while the AI tab is already open (e.g. a retry
@@ -3225,7 +3401,10 @@ watch(
},
);
onUnmounted(cleanupPreviewEditor);
onUnmounted(() => {
cleanupPreviewEditor();
resetSettingsSearchState();
});
</script>
<template>
@@ -3239,15 +3418,72 @@ onUnmounted(cleanupPreviewEditor);
</DialogHeader>
<div class="settings-layout flex min-h-0 flex-1 flex-col gap-3 overflow-hidden lg:flex-row">
<nav class="settingsCategoryNav settings-category-nav 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">
<nav class="settingsCategoryNav settings-category-nav flex min-h-0 shrink-0 gap-1 overflow-x-auto border-b pb-3 lg:w-52 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="onSettingsCategoryClick(category.value)">
{{ category.label }}
</button>
</nav>
<div class="min-w-0 flex-1 overflow-hidden px-1 flex flex-col">
<div ref="settingsContentScrollRef" class="min-h-0 flex-1 overflow-y-auto overflow-x-hidden px-1 pr-2">
<section v-if="activeSettingsTab === 'editor'" class="flex flex-col gap-5 py-2">
<div class="shrink-0 px-2 pt-1 pb-3">
<div ref="settingsSearchInputContainerRef" class="relative">
<Search class="pointer-events-none absolute top-1/2 left-4 z-10 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
v-model="settingsSearchQuery"
type="text"
autocomplete="off"
role="combobox"
:aria-label="t('settings.searchSettings')"
:aria-expanded="settingsSearchVisible ? 'true' : 'false'"
aria-controls="settings-search-results"
:aria-activedescendant="settingsSearchVisible && settingsSearchResults.length ? `settings-search-result-${settingsSearchResults[settingsSearchActiveIndex]?.id}` : undefined"
:placeholder="t('settings.searchSettings')"
class="h-11 w-full rounded-xl border-border bg-muted/30 pr-10 pl-11 text-sm shadow-none hover:bg-muted/40 focus-visible:ring-2 focus-visible:ring-inset focus-visible:border-primary focus-visible:bg-background"
@focus="settingsSearchOpen = Boolean(settingsSearchQuery.trim())"
@keydown="onSettingsSearchKeydown"
/>
<button v-if="settingsSearchQuery" type="button" class="absolute top-1/2 right-2 flex h-7 w-7 -translate-y-1/2 items-center justify-center rounded-md text-muted-foreground hover:bg-muted hover:text-foreground" :aria-label="t('settings.clearSettingsSearch')" @click="exitSettingsSearch">
<X class="h-4 w-4" />
</button>
</div>
</div>
<div v-if="settingsSearchVisible" id="settings-search-results" role="listbox" :aria-label="t('settings.searchSettingsResults')" class="min-h-0 flex-1 overflow-y-auto px-1 pr-2">
<div class="mx-auto w-full max-w-3xl pb-4">
<button type="button" class="mb-4 inline-flex items-center gap-1.5 rounded-md px-1 py-1 text-sm text-muted-foreground transition-colors hover:text-foreground" @click="exitSettingsSearch">
<ArrowLeft class="h-4 w-4" />
{{ t("settings.exitSettingsSearch") }}
</button>
<div v-if="settingsSearchResults.length === 0" class="rounded-xl border border-dashed px-4 py-12 text-center text-sm text-muted-foreground">
{{ t("settings.searchSettingsNoResults") }}
</div>
<div v-for="group in settingsSearchResultGroups" :key="group.category" class="mb-6 last:mb-0">
<div class="mb-2 flex items-center gap-2 px-1 text-sm font-medium text-muted-foreground">
<span class="flex h-7 w-7 items-center justify-center rounded-md border bg-muted/40">
<Settings class="h-4 w-4" />
</span>
{{ group.categoryLabel }}
</div>
<div class="overflow-hidden rounded-xl border bg-card p-1 shadow-sm">
<button
v-for="result in group.results"
:id="`settings-search-result-${result.id}`"
:key="result.id"
type="button"
role="option"
:aria-selected="result.id === settingsSearchResults[settingsSearchActiveIndex]?.id"
:class="['flex w-full flex-col gap-1 rounded-lg px-3 py-3 text-left outline-none transition-colors sm:px-4', result.id === settingsSearchResults[settingsSearchActiveIndex]?.id ? 'bg-accent text-accent-foreground' : 'hover:bg-muted/70']"
@mousedown.prevent
@click="void selectSettingsSearchResult(result)"
>
<span class="text-sm font-medium">{{ result.title }}</span>
<span v-if="result.description" class="line-clamp-2 text-xs text-muted-foreground">{{ result.description }}</span>
</button>
</div>
</div>
</div>
</div>
<div v-else ref="settingsContentScrollRef" class="min-h-0 flex-1 overflow-y-auto overflow-x-hidden px-1 pr-2">
<section v-if="activeSettingsTab === 'editor'" data-settings-search-id="editor" :class="['flex flex-col gap-5 py-2', settingsSearchTargetClass('editor')]">
<div class="grid gap-4 md:grid-cols-[1fr_auto]">
<!-- Font Family -->
<div class="space-y-2 min-w-0">
@@ -3564,7 +3800,7 @@ onUnmounted(cleanupPreviewEditor);
</div>
</section>
<section v-else-if="activeSettingsTab === 'formatter'" class="flex flex-col gap-5 py-2">
<section v-else-if="activeSettingsTab === 'formatter'" data-settings-search-id="formatter" :class="['flex flex-col gap-5 py-2', settingsSearchTargetClass('formatter')]">
<div class="space-y-3 rounded-md border border-border/70 bg-muted/10 p-3">
<div class="text-sm font-medium">
{{ t("settings.sqlFormatterEditorShortcuts") }}
@@ -3637,7 +3873,7 @@ onUnmounted(cleanupPreviewEditor);
<SqlFormatterSettingsPanel v-model="editSqlFormatter" @validity-change="(value: boolean) => (sqlFormatterConfigValid = value)" />
</section>
<section v-else-if="activeSettingsTab === 'appearance'" class="settings-appearance-section flex flex-col gap-4 py-2">
<section v-else-if="activeSettingsTab === 'appearance'" class="settings-appearance-section flex flex-col gap-4 py-2" data-settings-search-id="appearance" :class="settingsSearchTargetClass('appearance')">
<div class="settings-appearance-top-grid">
<div class="settings-appearance-field min-w-0">
<div class="flex h-9 items-end">
@@ -4142,38 +4378,15 @@ onUnmounted(cleanupPreviewEditor);
<Switch id="exclusive-right-sidebar-panels" v-model="editToolbarItems.exclusiveRightSidebarPanels" />
</div>
<div class="grid grid-cols-3 gap-2 mt-2">
<div
v-for="item in [
{
key: 'dataTransfer',
label: t('transfer.dataTransfer'),
},
{
key: 'driverManager',
label: t('toolbar.driverManager'),
},
{ key: 'sqlFile', label: t('sqlFile.title') },
{ key: 'schemaDiff', label: t('diff.title') },
{ key: 'dataCompare', label: t('dataCompare.title') },
{ key: 'checkUpdates', label: t('updates.check') },
{ key: 'sqlLibrary', label: t('sqlLibrary.title') },
{ key: 'sqlFileTree', label: t('sqlFileTree.title') },
{ key: 'history', label: t('history.title') },
{ key: 'ai', label: 'AI' },
{ key: 'theme', label: t('toolbar.theme') },
{ key: 'github', label: 'GitHub' },
]"
:key="item.key"
class="flex items-center gap-2"
>
<div v-for="item in toolbarVisibilityItems" :key="item.key" class="flex items-center gap-2">
<Switch :id="`toolbar-${item.key}`" :model-value="(editToolbarItems as any)[item.key]" @update:model-value="(v: boolean) => ((editToolbarItems as any)[item.key] = v)" />
<Label :for="`toolbar-${item.key}`" class="text-sm cursor-pointer">{{ item.label }}</Label>
<Label :for="`toolbar-${item.key}`" class="text-sm cursor-pointer">{{ getToolbarVisibilityItemLabel(item) }}</Label>
</div>
</div>
</div>
</section>
<section v-else-if="activeSettingsTab === 'navigation'" class="flex flex-col gap-5 py-2">
<section v-else-if="activeSettingsTab === 'navigation'" data-settings-search-id="navigation" :class="['flex flex-col gap-5 py-2', settingsSearchTargetClass('navigation')]">
<div class="space-y-2">
<Label>{{ t("settings.sidebarActivation") }}</Label>
<div class="grid grid-cols-2 gap-2">
@@ -4410,7 +4623,7 @@ onUnmounted(cleanupPreviewEditor);
</section>
<!-- Data Tab -->
<section v-else-if="activeSettingsTab === 'data'" class="flex flex-col gap-5 py-2">
<section v-else-if="activeSettingsTab === 'data'" data-settings-search-id="data" :class="['flex flex-col gap-5 py-2', settingsSearchTargetClass('data')]">
<div class="space-y-3">
<div class="text-sm font-medium text-muted-foreground">
{{ t("settings.dataGridDisplay") }}
@@ -4599,7 +4812,7 @@ onUnmounted(cleanupPreviewEditor);
<div class="text-sm font-medium text-muted-foreground">
{{ t("settings.tableStructureSection") }}
</div>
<div ref="tableColumnTemplateSectionRef" class="space-y-2 rounded-md border bg-muted/20 px-3 py-2">
<div ref="tableColumnTemplateSectionRef" data-settings-search-id="table-column-templates" :class="['space-y-2 rounded-md border bg-muted/20 px-3 py-2', settingsSearchTargetClass('table-column-templates')]">
<div class="flex items-start justify-between gap-3">
<div class="space-y-1">
<Label>{{ t("settings.tableColumnTemplateFields") }}</Label>
@@ -4702,7 +4915,7 @@ onUnmounted(cleanupPreviewEditor);
</div>
</section>
<section v-else-if="activeSettingsTab === 'shortcuts'" class="flex flex-col gap-2 py-2">
<section v-else-if="activeSettingsTab === 'shortcuts'" data-settings-search-id="shortcuts" :class="['flex flex-col gap-2 py-2', settingsSearchTargetClass('shortcuts')]">
<div class="relative">
<Search class="pointer-events-none absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input v-model="shortcutSearchQuery" autocomplete="off" :placeholder="t('settings.shortcutSearchPlaceholder')" class="h-9 pl-9 text-sm" />
@@ -4782,7 +4995,7 @@ onUnmounted(cleanupPreviewEditor);
</section>
<!-- Snippets Tab -->
<section v-else-if="activeSettingsTab === 'snippets'" class="flex flex-col gap-4 py-2">
<section v-else-if="activeSettingsTab === 'snippets'" data-settings-search-id="snippets" :class="['flex flex-col gap-4 py-2', settingsSearchTargetClass('snippets')]">
<div class="flex items-center justify-between">
<p class="text-sm text-muted-foreground">
{{ t("settings.snippetsDescription") }}
@@ -4847,18 +5060,18 @@ onUnmounted(cleanupPreviewEditor);
</div>
</section>
<section v-else-if="activeSettingsTab === 'backups' && !isWeb" class="py-2">
<section v-else-if="activeSettingsTab === 'backups' && !isWeb" data-settings-search-id="backups" :class="['py-2', settingsSearchTargetClass('backups')]">
<ScheduledDatabaseBackupSettings />
</section>
<section v-else-if="activeSettingsTab === 'sync'" class="py-2">
<section v-else-if="activeSettingsTab === 'sync'" data-settings-search-id="sync" :class="['py-2', settingsSearchTargetClass('sync')]">
<Tabs v-model="syncMethodTab" class="w-full">
<TabsList class="grid w-full grid-cols-2">
<TabsTrigger value="webdav">WebDAV</TabsTrigger>
<TabsTrigger value="snippet">GitHub / Gitee</TabsTrigger>
</TabsList>
<TabsContent value="webdav" class="mt-5 space-y-5">
<TabsContent value="webdav" data-settings-search-id="sync-webdav" :class="['mt-5 space-y-5', settingsSearchTargetClass('sync-webdav')]">
<div class="space-y-1">
<div class="flex items-center gap-2 text-sm font-medium">
<Cloud class="h-4 w-4 text-muted-foreground" />
@@ -4954,7 +5167,7 @@ onUnmounted(cleanupPreviewEditor);
</div>
</TabsContent>
<TabsContent value="snippet" class="mt-5 space-y-5">
<TabsContent value="snippet" data-settings-search-id="sync-snippet" :class="['mt-5 space-y-5', settingsSearchTargetClass('sync-snippet')]">
<div class="space-y-1">
<div class="flex items-center justify-between gap-3">
<div class="flex items-center gap-2 text-sm font-medium">
@@ -5112,7 +5325,7 @@ onUnmounted(cleanupPreviewEditor);
</section>
<!-- AI Settings Tab -->
<section v-else-if="activeSettingsTab === 'ai'" class="flex flex-col gap-5 py-2">
<section v-else-if="activeSettingsTab === 'ai'" data-settings-search-id="ai" :class="['flex flex-col gap-5 py-2', settingsSearchTargetClass('ai')]">
<!-- Config List View -->
<div v-if="aiConfigListMode === 'list'" class="space-y-4">
<div class="flex items-center justify-between">
@@ -5574,7 +5787,7 @@ onUnmounted(cleanupPreviewEditor);
</div>
</section>
<section v-else-if="activeSettingsTab === 'mcp'" class="flex flex-col gap-5 py-2">
<section v-else-if="activeSettingsTab === 'mcp'" data-settings-search-id="mcp" :class="['flex flex-col gap-5 py-2', settingsSearchTargetClass('mcp')]">
<div class="rounded-md border bg-muted/20 p-4">
<div class="flex items-start justify-between gap-4">
<div class="min-w-0 space-y-2">
@@ -5922,7 +6135,7 @@ onUnmounted(cleanupPreviewEditor);
</div>
</section>
<section v-else-if="activeSettingsTab === 'security' && isWeb" class="flex flex-col gap-5 py-2">
<section v-else-if="activeSettingsTab === 'security' && isWeb" data-settings-search-id="security" :class="['flex flex-col gap-5 py-2', settingsSearchTargetClass('security')]">
<div class="space-y-3">
<Label class="text-base">{{ t("auth.changePassword") }}</Label>
<p class="text-sm text-muted-foreground">
@@ -5937,11 +6150,11 @@ onUnmounted(cleanupPreviewEditor);
</div>
</section>
<section v-else-if="activeSettingsTab === 'tunnels'" class="flex flex-col gap-5 py-2">
<section v-else-if="activeSettingsTab === 'tunnels'" data-settings-search-id="tunnels" :class="['flex flex-col gap-5 py-2', settingsSearchTargetClass('tunnels')]">
<TunnelProfileManager />
</section>
<section v-else-if="activeSettingsTab === 'about'" class="flex flex-col gap-5 py-2">
<section v-else-if="activeSettingsTab === 'about'" data-settings-search-id="about" :class="['flex flex-col gap-5 py-2', settingsSearchTargetClass('about')]">
<div class="rounded-lg border p-4">
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div class="min-w-0 space-y-1">