"use client"; import { useState, useEffect, useRef, useMemo } from 'react'; import { useRouter } from '@/i18n/navigation'; import { useTranslations, useMessages } from 'next-intl'; import { ArrowLeft, ChevronRight, LogOut, Settings as SettingsIcon, Palette, Search, User, Shield, UserPen, PalmtreeIcon, Calendar, Filter, FileText, FolderOpen, Tags, HardDrive, BookUser, KeyRound, PanelLeftClose, Bell, Puzzle, LayoutGrid, BookOpen, PenLine, EyeOff, Languages, Info, Bug, X, type LucideIcon, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { AppearanceSettings } from '@/components/settings/appearance-settings'; import { LayoutSettings } from '@/components/settings/layout-settings'; import { LanguageSettings } from '@/components/settings/language-settings'; import { ReadingSettings } from '@/components/settings/reading-settings'; import { ComposingSettings } from '@/components/settings/composing-settings'; import { ContentSendersSettings } from '@/components/settings/content-senders-settings'; import { AccountSettings } from '@/components/settings/account-settings'; import { IdentitySettings } from '@/components/settings/identity-settings'; import { VacationSettings } from '@/components/settings/vacation-settings'; import { CalendarSettings } from '@/components/settings/calendar-settings'; import { CalendarManagementSettings } from '@/components/settings/calendar-management-settings'; import { AddressBookManagementSettings } from '@/components/settings/address-book-management-settings'; import { FilterSettings } from '@/components/settings/filter-settings'; import { TemplateSettings } from '@/components/settings/template-settings'; import { AboutDataSettings } from '@/components/settings/about-data-settings'; import { DebugSettings } from '@/components/settings/debug-settings'; import { FolderSettings } from '@/components/settings/folder-settings'; import { KeywordSettings } from '@/components/settings/keyword-settings'; import { AccountSecuritySettings } from '@/components/settings/account-security-settings'; import { FilesSettingsComponent } from '@/components/settings/files-settings'; import { ContactsSettings } from '@/components/settings/contacts-settings'; import { SmimeSettings } from '@/components/settings/smime-settings'; import { SidebarAppsSettings } from '@/components/settings/sidebar-apps-settings'; import { NotificationSettings } from '@/components/settings/notification-settings'; import { ThemesSettings } from '@/components/settings/themes-settings'; import { PluginsSettings } from '@/components/settings/plugins-settings'; import { useAuthStore, redirectToLogin } from '@/stores/auth-store'; import { useEmailStore } from '@/stores/email-store'; import { usePluginStore } from '@/stores/plugin-store'; import { useThemeStore } from '@/stores/theme-store'; import { useSettingsStore } from '@/stores/settings-store'; import { useIsDesktop } from '@/hooks/use-media-query'; import { NavigationRail } from '@/components/layout/navigation-rail'; import { SidebarAppsModal } from '@/components/layout/sidebar-apps-modal'; import { InlineAppView } from '@/components/layout/inline-app-view'; import { useSidebarApps } from '@/hooks/use-sidebar-apps'; import { ResizeHandle } from '@/components/layout/resize-handle'; import { useConfig } from '@/hooks/use-config'; import { usePolicyStore } from '@/stores/policy-store'; import { cn } from '@/lib/utils'; type Tab = | 'account' | 'language' | 'notifications' | 'appearance' | 'layout' | 'reading' | 'composing' | 'identities' | 'vacation' | 'filters' | 'templates' | 'folders' | 'keywords' | 'security' | 'encryption' | 'content_senders' | 'calendar' | 'contacts' | 'files' | 'sidebar_apps' | 'about_data' | 'themes' | 'plugins' | 'debug'; type TabGroup = 'general' | 'appearance' | 'mail' | 'privacy' | 'apps' | 'advanced'; interface TabDef { id: Tab; label: string; icon: LucideIcon; group: TabGroup; } const tabIcons: Record = { account: User, language: Languages, notifications: Bell, appearance: Palette, layout: LayoutGrid, reading: BookOpen, composing: PenLine, identities: UserPen, vacation: PalmtreeIcon, filters: Filter, templates: FileText, folders: FolderOpen, keywords: Tags, security: Shield, encryption: KeyRound, content_senders: EyeOff, calendar: Calendar, contacts: BookUser, files: HardDrive, sidebar_apps: PanelLeftClose, about_data: Info, themes: Palette, plugins: Puzzle, debug: Bug, }; const tabGroupOrder: TabGroup[] = ['general', 'appearance', 'mail', 'privacy', 'apps', 'advanced']; // Translation paths per tab. Tabs that share a namespace (email_behavior, // appearance) explicitly list the subkeys they actually render so sub-results // are attributed to the correct tab. Tabs with their own namespace just point // at the namespace root. const tabSearchPaths: Record = { account: [ 'settings.account.name_label', 'settings.account.username_label', 'settings.account.account_type_label', 'settings.account.auth_method_label', 'settings.account.email', 'settings.account.server', 'settings.account.storage', ], language: ['settings.appearance.language'], notifications: ['settings.notifications'], appearance: [ 'settings.appearance.theme', 'settings.appearance.font_size', 'settings.appearance.list_density', 'settings.appearance.animations', ], layout: [ 'settings.appearance.toolbar_position', 'settings.appearance.toolbar_labels', 'settings.appearance.hide_account_switcher', 'settings.appearance.show_rail_account_list', 'settings.appearance.unified_mailbox', 'settings.appearance.colorful_sidebar_icons', 'settings.email_behavior.mail_layout', ], reading: [ 'settings.email_behavior.mark_read', 'settings.email_behavior.archive_mode', 'settings.email_behavior.delete_action', 'settings.email_behavior.attachment_click_action', 'settings.email_behavior.attachment_image_previews', 'settings.email_behavior.attachment_position', 'settings.email_behavior.disable_threading', 'settings.email_behavior.emails_per_page', 'settings.email_behavior.hide_inline_image_attachments', 'settings.email_behavior.hover_actions', 'settings.email_behavior.permanently_delete_junk', 'settings.email_behavior.show_preview', 'settings.email_behavior.plain_text_mode', ], composing: [ 'settings.email_behavior.attachment_reminder', 'settings.email_behavior.auto_select_reply_identity', 'settings.email_behavior.default_mail_program', 'settings.email_behavior.signature_position', 'settings.email_behavior.sub_address_delimiter', ], identities: ['settings.identities'], vacation: ['settings.vacation'], filters: ['settings.filters'], templates: ['settings.templates'], folders: ['settings.folders'], keywords: ['settings.keywords'], security: ['settings.security'], encryption: ['smime'], content_senders: [ 'settings.email_behavior.always_light_mode', 'settings.email_behavior.external_content', 'settings.email_behavior.trusted_senders', ], calendar: ['calendar.settings', 'calendar.management'], contacts: ['settings.contacts', 'contacts'], files: ['settings.files'], sidebar_apps: ['settings.sidebar_apps', 'sidebar_apps'], about_data: ['settings.advanced'], themes: [], plugins: [], debug: ['settings.advanced'], }; // Extra English keywords per tab so common search terms hit even when the // translation doesn't contain the literal word. const tabKeywords: Record = { account: 'profile email password user signin signout', language: 'locale region timezone date time format', notifications: 'sound alert push badge', appearance: 'theme dark light font size accent color animation density', layout: 'toolbar sidebar account switcher unified mailbox icons rail', reading: 'mark read preview thread conversation archive delete attachment open', composing: 'editor signature plain text reply forward draft compose', identities: 'from address signature email', vacation: 'auto reply away out of office holiday responder', filters: 'sieve rules block junk forward', templates: 'snippet quick reply', folders: 'mailbox subscribe', keywords: 'tags labels colors', security: 'password 2fa two-factor passkey app password mfa', encryption: 's/mime smime certificate pgp gpg', content_senders: 'block sender remote images privacy tracking', calendar: 'event schedule appointment meeting timezone', contacts: 'address book contact', files: 'attachments cloud drive storage upload', sidebar_apps: 'apps webview iframe', about_data: 'export import storage quota privacy backup', themes: 'custom theme css skin appearance', plugins: 'extensions addons', debug: 'logs developer console diagnostic', }; function flattenStrings(node: unknown, sink: string[]): void { if (typeof node === 'string') { sink.push(node); return; } if (Array.isArray(node)) { for (const item of node) flattenStrings(item, sink); return; } if (node && typeof node === 'object') { for (const value of Object.values(node)) flattenStrings(value, sink); } } interface SubResult { label: string; description?: string; // For plugin setting fields: the id of the plugin whose card needs to be // expanded before the field becomes visible in the DOM. pluginId?: string; } // Walk a translation subtree and emit sub-results for renderable settings. // Picks up: // - bare string leaves (when a tab path points directly at a flat label) // - objects with a `label` or `title` field (the standard pattern) // - flat `*_label` string keys at any object level (e.g. `name_label`) function collectSubResults(node: unknown, sink: SubResult[]): void { if (typeof node === 'string') { sink.push({ label: node }); return; } if (!node || typeof node !== 'object' || Array.isArray(node)) return; const obj = node as Record; const label = typeof obj.label === 'string' ? obj.label : (typeof obj.title === 'string' ? obj.title : undefined); if (label) { sink.push({ label, description: typeof obj.description === 'string' ? obj.description : undefined, }); } for (const [key, value] of Object.entries(obj)) { if (typeof value === 'string' && key !== 'label' && key !== 'title' && key.endsWith('_label')) { sink.push({ label: value }); } } for (const value of Object.values(obj)) { if (value && typeof value === 'object' && !Array.isArray(value)) { collectSubResults(value, sink); } } } function getByPath(obj: unknown, path: string): unknown { let cur: unknown = obj; for (const key of path.split('.')) { if (cur && typeof cur === 'object' && key in (cur as Record)) { cur = (cur as Record)[key]; } else { return undefined; } } return cur; } // Map legacy tab IDs to current ones; runs once on read of localStorage. const LEGACY_TAB_MAP: Record = { email: 'reading', advanced: 'about_data', }; function readPersistedTab(): Tab { try { const saved = localStorage.getItem('settings-active-tab'); if (!saved) return 'appearance'; if (saved in LEGACY_TAB_MAP) { const migrated = LEGACY_TAB_MAP[saved]; try { localStorage.setItem('settings-active-tab', migrated); } catch { /* ignore */ } return migrated; } return saved as Tab; } catch { return 'appearance'; } } export default function SettingsPage() { const router = useRouter(); const t = useTranslations('settings'); const tSidebar = useTranslations('sidebar'); const { client, isAuthenticated, logout, checkAuth, isLoading: authLoading } = useAuthStore(); const { showAppsModal, inlineApp, loadedApps, handleManageApps, handleInlineApp, closeInlineApp, closeAppsModal } = useSidebarApps(); const [initialCheckDone, setInitialCheckDone] = useState(() => useAuthStore.getState().isAuthenticated && !!useAuthStore.getState().client); const { quota, isPushConnected } = useEmailStore(); const { stalwartFeaturesEnabled } = useConfig(); const { isFeatureEnabled } = usePolicyStore(); const [activeTab, setActiveTab] = useState(readPersistedTab); const [mobileShowContent, setMobileShowContent] = useState(false); const [searchQuery, setSearchQuery] = useState(''); const [pendingHighlight, setPendingHighlight] = useState<{ tab: Tab; label: string; pluginId?: string } | null>(null); const isDesktop = useIsDesktop(); const messages = useMessages() as Record; const installedPlugins = usePluginStore((s) => s.plugins); const installedThemes = useThemeStore((s) => s.installedThemes); const sidebarAppsList = useSettingsStore((s) => s.sidebarApps); // Build a per-tab haystack for fulltext search and a list of sub-results // (individual settings) per tab. Sub-results come from translation entries // that have a `label`/`title` field, plus dynamic content (installed // plugins/themes/sidebar apps). const { tabSearchHaystacks, tabSubResults } = useMemo(() => { const haystacks: Partial> = {}; const subs: Partial> = {}; const tabIds = Object.keys(tabSearchPaths) as Tab[]; for (const tabId of tabIds) { const strings: string[] = [tabId.replace(/_/g, ' '), tabKeywords[tabId] ?? '']; const list: SubResult[] = []; for (const path of tabSearchPaths[tabId]) { const node = getByPath(messages, path); flattenStrings(node, strings); collectSubResults(node, list); } // Dedupe sub-results by label const seen = new Set(); subs[tabId] = list.filter((r) => { if (seen.has(r.label)) return false; seen.add(r.label); return true; }); haystacks[tabId] = strings.join(' ').toLowerCase(); } if (installedPlugins.length) { const haystackText = installedPlugins.map((p) => { const fieldText = p.settingsSchema ? Object.values(p.settingsSchema) .map((s) => `${s.label} ${s.description ?? ''}`) .join(' ') : ''; return `${p.name} ${p.description} ${p.author} ${fieldText}`; }).join(' '); haystacks.plugins = `${haystacks.plugins ?? ''} ${haystackText}`.toLowerCase(); const pluginSubs: SubResult[] = installedPlugins.flatMap((p) => { const items: SubResult[] = [{ label: p.name, description: p.description }]; if (p.settingsSchema) { for (const schema of Object.values(p.settingsSchema)) { items.push({ label: schema.label, description: schema.description, pluginId: p.id, }); } } return items; }); subs.plugins = [...(subs.plugins ?? []), ...pluginSubs]; } if (installedThemes.length) { const text = installedThemes.map((th) => `${th.name} ${th.description} ${th.author}`).join(' '); haystacks.themes = `${haystacks.themes ?? ''} ${text}`.toLowerCase(); subs.themes = [ ...(subs.themes ?? []), ...installedThemes.map((th) => ({ label: th.name, description: th.description })), ]; } if (sidebarAppsList.length) { const text = sidebarAppsList.map((a) => `${a.name} ${a.url}`).join(' '); haystacks.sidebar_apps = `${haystacks.sidebar_apps ?? ''} ${text}`.toLowerCase(); subs.sidebar_apps = [ ...(subs.sidebar_apps ?? []), ...sidebarAppsList.map((a) => ({ label: a.name, description: a.url })), ]; } return { tabSearchHaystacks: haystacks, tabSubResults: subs }; }, [messages, installedPlugins, installedThemes, sidebarAppsList]); // Sidebar resize state const [settingsSidebarWidth, setSettingsSidebarWidth] = useState(() => { try { const v = localStorage.getItem('settings-sidebar-width'); return v ? Number(v) : 256; } catch { return 256; } }); const [isResizing, setIsResizing] = useState(false); const dragStartWidth = useRef(256); // Check auth on mount – skip when already authenticated so that navigating // between routes doesn't retrigger checkAuth's transient `{ client: null, // isLoading: true }` reset, which was flashing the spinner on every nav. useEffect(() => { const state = useAuthStore.getState(); if (state.isAuthenticated && state.client) { setInitialCheckDone(true); return; } checkAuth().finally(() => { setInitialCheckDone(true); }); }, [checkAuth]); // Listen for tab change events from child components (with legacy migration) useEffect(() => { const handler = (e: Event) => { const raw = (e as CustomEvent).detail as string; if (!raw) return; const tab = (LEGACY_TAB_MAP[raw] ?? raw) as Tab; setActiveTab(tab); try { localStorage.setItem('settings-active-tab', tab); } catch { /* ignore */ } }; window.addEventListener('settings-tab-change', handler); return () => window.removeEventListener('settings-tab-change', handler); }, []); useEffect(() => { if (initialCheckDone && !isAuthenticated && !authLoading) { try { sessionStorage.setItem('redirect_after_login', window.location.pathname); } catch { /* ignore */ } redirectToLogin(); } }, [initialCheckDone, isAuthenticated, authLoading]); // Sync the mobile submenu view with browser history so the system back // button (or gesture) returns to the settings list before exiting /settings. useEffect(() => { if (isDesktop) return; if (typeof window === 'undefined') return; if (!mobileShowContent) return; window.history.pushState({ __settingsSubmenu: true }, ''); const handlePop = () => { setMobileShowContent(false); }; window.addEventListener('popstate', handlePop); return () => window.removeEventListener('popstate', handlePop); }, [isDesktop, mobileShowContent]); // After clicking a search sub-result, scroll the matching setting into view // and add a temporary highlight class. Some tabs fetch data and render // their SettingItems only after a loading state, so retry until the element // shows up (or we give up after ~2s). useEffect(() => { if (!pendingHighlight) return; if (pendingHighlight.tab !== activeTab) return; if (typeof window === 'undefined') return; // For plugin-setting sub-results, ask the plugins tab to expand the // matching card so the field becomes part of the DOM. Dispatched here // (not in the click handler) because PluginsSettings only mounts after // the tab switches, and its listener registers in its own useEffect - // child effects run before parent effects, so by the time we get here // the listener is guaranteed to be in place. if (pendingHighlight.pluginId) { window.dispatchEvent( new CustomEvent('settings-plugin-expand', { detail: { pluginId: pendingHighlight.pluginId } }) ); } let cancelled = false; let retryTimer: ReturnType | undefined; let cleanupTimer: ReturnType | undefined; let highlightedEl: HTMLElement | null = null; const escaped = pendingHighlight.label.replace(/"/g, '\\"'); const selector = `[data-search-label="${escaped}"]`; const deadline = Date.now() + 2000; const tryHighlight = () => { if (cancelled) return; const el = document.querySelector(selector); if (!el) { if (Date.now() < deadline) { retryTimer = setTimeout(tryHighlight, 80); } return; } el.scrollIntoView({ behavior: 'smooth', block: 'center' }); // Remove + reflow + add restarts the CSS animation if the class was // already present (re-clicking the same sub-result). el.classList.remove('settings-search-highlight'); void el.offsetWidth; el.classList.add('settings-search-highlight'); highlightedEl = el; cleanupTimer = setTimeout(() => { el.classList.remove('settings-search-highlight'); highlightedEl = null; }, 1800); }; // First attempt next frame so the freshly-mounted tab content is in DOM. const raf = window.requestAnimationFrame(tryHighlight); // Do NOT reset pendingHighlight here - that would retrigger this effect // and the cleanup below would strip the class right after we added it. return () => { cancelled = true; window.cancelAnimationFrame(raf); if (retryTimer) clearTimeout(retryTimer); if (cleanupTimer) clearTimeout(cleanupTimer); if (highlightedEl) highlightedEl.classList.remove('settings-search-highlight'); }; }, [pendingHighlight, activeTab]); if (!isAuthenticated) { return null; } const supportsVacation = client?.supportsVacationResponse() ?? false; const supportsCalendar = client?.supportsCalendars() ?? false; const supportsSieve = client?.supportsSieve() ?? false; const supportsFiles = client?.supportsFiles() ?? false; const tabs: TabDef[] = [ // General { id: 'account', label: t('tabs.account'), icon: tabIcons.account, group: 'general' }, { id: 'language', label: t('tabs.language'), icon: tabIcons.language, group: 'general' }, { id: 'notifications', label: t('tabs.notifications'), icon: tabIcons.notifications, group: 'general' }, // Appearance { id: 'appearance', label: t('tabs.appearance'), icon: tabIcons.appearance, group: 'appearance' }, { id: 'layout', label: t('tabs.layout'), icon: tabIcons.layout, group: 'appearance' }, // Mail { id: 'reading', label: t('tabs.reading'), icon: tabIcons.reading, group: 'mail' }, { id: 'composing', label: t('tabs.composing'), icon: tabIcons.composing, group: 'mail' }, { id: 'identities', label: t('tabs.identities'), icon: tabIcons.identities, group: 'mail' }, ...(supportsVacation ? [{ id: 'vacation' as Tab, label: t('tabs.vacation'), icon: tabIcons.vacation, group: 'mail' as TabGroup }] : []), ...(supportsSieve ? [{ id: 'filters' as Tab, label: t('tabs.filters'), icon: tabIcons.filters, group: 'mail' as TabGroup }] : []), ...(isFeatureEnabled('templatesEnabled') ? [{ id: 'templates' as Tab, label: t('tabs.templates'), icon: tabIcons.templates, group: 'mail' as TabGroup }] : []), { id: 'folders', label: t('tabs.folders'), icon: tabIcons.folders, group: 'mail' }, ...(isFeatureEnabled('customKeywordsEnabled') ? [{ id: 'keywords' as Tab, label: t('tabs.keywords'), icon: tabIcons.keywords, group: 'mail' as TabGroup }] : []), // Privacy & Security ...(stalwartFeaturesEnabled ? [{ id: 'security' as Tab, label: t('tabs.security'), icon: tabIcons.security, group: 'privacy' as TabGroup }] : []), ...(isFeatureEnabled('smimeEnabled') ? [{ id: 'encryption' as Tab, label: t('tabs.encryption'), icon: tabIcons.encryption, group: 'privacy' as TabGroup }] : []), { id: 'content_senders', label: t('tabs.content_senders'), icon: tabIcons.content_senders, group: 'privacy' }, // Apps ...(supportsCalendar ? [{ id: 'calendar' as Tab, label: t('tabs.calendar'), icon: tabIcons.calendar, group: 'apps' as TabGroup }] : []), { id: 'contacts', label: t('tabs.contacts'), icon: tabIcons.contacts, group: 'apps' }, ...(supportsFiles ? [{ id: 'files' as Tab, label: t('tabs.files'), icon: tabIcons.files, group: 'apps' as TabGroup }] : []), ...(isFeatureEnabled('sidebarAppsEnabled') ? [{ id: 'sidebar_apps' as Tab, label: t('tabs.sidebar_apps'), icon: tabIcons.sidebar_apps, group: 'apps' as TabGroup }] : []), // Advanced { id: 'about_data', label: t('tabs.about_data'), icon: tabIcons.about_data, group: 'advanced' }, ...(isFeatureEnabled('themesEnabled') ? [{ id: 'themes' as Tab, label: 'Themes', icon: tabIcons.themes, group: 'advanced' as TabGroup }] : []), ...(isFeatureEnabled('pluginsEnabled') ? [{ id: 'plugins' as Tab, label: 'Plugins', icon: tabIcons.plugins, group: 'advanced' as TabGroup }] : []), ...(isFeatureEnabled('debugModeEnabled') ? [{ id: 'debug' as Tab, label: t('tabs.debug'), icon: tabIcons.debug, group: 'advanced' as TabGroup }] : []), ]; // Group tabs by category const groupedTabs = tabGroupOrder .map((group) => ({ group, label: t(`tab_groups.${group}`), items: tabs.filter((tab) => tab.group === group), })) .filter((g) => g.items.length > 0); const trimmedQuery = searchQuery.trim().toLowerCase(); const matchesQuery = (tab: TabDef) => { if (!trimmedQuery) return true; if (tab.label.toLowerCase().includes(trimmedQuery)) return true; return tabSearchHaystacks[tab.id]?.includes(trimmedQuery) ?? false; }; const subResultsForTab = (tabId: Tab): SubResult[] => { if (!trimmedQuery) return []; const list = tabSubResults[tabId] ?? []; return list .filter((r) => r.label.toLowerCase().includes(trimmedQuery) || (r.description?.toLowerCase().includes(trimmedQuery) ?? false) ) .slice(0, 6); }; const filteredGroupedTabs = trimmedQuery ? groupedTabs .map((g) => ({ ...g, items: g.items.filter(matchesQuery) })) .filter((g) => g.items.length > 0) : groupedTabs; // If active tab is not in the visible list (e.g., feature disabled), fall back. const isActiveVisible = tabs.some((tab) => tab.id === activeTab); const effectiveActiveTab: Tab = isActiveVisible ? activeTab : 'appearance'; const handleTabSelect = (tabId: Tab) => { setActiveTab(tabId); try { localStorage.setItem('settings-active-tab', tabId); } catch { /* ignore */ } if (!isDesktop) { setMobileShowContent(true); } }; const handleSubResultSelect = (tabId: Tab, sub: SubResult) => { handleTabSelect(tabId); setPendingHighlight({ tab: tabId, label: sub.label, pluginId: sub.pluginId }); }; const activeTabLabel = tabs.find((tab) => tab.id === effectiveActiveTab)?.label ?? ''; const renderTabContent = () => ( <> {effectiveActiveTab === 'account' && } {effectiveActiveTab === 'language' && } {effectiveActiveTab === 'notifications' && } {effectiveActiveTab === 'appearance' && } {effectiveActiveTab === 'layout' && } {effectiveActiveTab === 'reading' && } {effectiveActiveTab === 'composing' && } {effectiveActiveTab === 'identities' && } {effectiveActiveTab === 'vacation' && } {effectiveActiveTab === 'filters' && } {effectiveActiveTab === 'templates' && } {effectiveActiveTab === 'folders' && } {effectiveActiveTab === 'keywords' && } {effectiveActiveTab === 'security' && } {effectiveActiveTab === 'encryption' && } {effectiveActiveTab === 'content_senders' && } {effectiveActiveTab === 'calendar' && <>
} {effectiveActiveTab === 'contacts' && <>
} {effectiveActiveTab === 'files' && } {effectiveActiveTab === 'sidebar_apps' && } {effectiveActiveTab === 'about_data' && } {effectiveActiveTab === 'themes' && } {effectiveActiveTab === 'plugins' && } {effectiveActiveTab === 'debug' && } ); // Mobile layout if (!isDesktop) { if (mobileShowContent) { return (

{activeTabLabel}

{renderTabContent()}
); } return (

{t('title')}

setSearchQuery(e.target.value)} placeholder={t('search_placeholder')} className="pl-9 pr-9 h-10" aria-label={t('search_placeholder')} /> {searchQuery && ( )}
{filteredGroupedTabs.length === 0 && (
{t('search_no_results')}
)} {filteredGroupedTabs.map((group, groupIndex) => (
{groupIndex > 0 &&
}
{group.label}
{group.items.map((tab) => { const Icon = tab.icon; const subs = subResultsForTab(tab.id); return (
{subs.map((sub) => ( ))}
); })}
))}
); } // Desktop layout return (
{inlineApp && ( )} {!inlineApp && ( <>
setSearchQuery(e.target.value)} placeholder={t('search_placeholder')} className="pl-8 pr-8 h-9 text-sm" aria-label={t('search_placeholder')} /> {searchQuery && ( )}
{filteredGroupedTabs.length === 0 && (
{t('search_no_results')}
)} {filteredGroupedTabs.map((group, groupIndex) => (
{groupIndex > 0 &&
}
{group.label}
{group.items.map((tab) => { const Icon = tab.icon; const subs = subResultsForTab(tab.id); return (
{subs.map((sub) => ( ))}
); })}
))}
{ dragStartWidth.current = settingsSidebarWidth; setIsResizing(true); }} onResize={(delta) => setSettingsSidebarWidth(Math.max(180, Math.min(400, dragStartWidth.current + delta)))} onResizeEnd={() => { setIsResizing(false); localStorage.setItem('settings-sidebar-width', String(settingsSidebarWidth)); }} onDoubleClick={() => { setSettingsSidebarWidth(256); localStorage.setItem('settings-sidebar-width', '256'); }} />
{renderTabContent()}
)}
); }