diff --git a/webapp/src/components/AppAuthenticatedShell.tsx b/webapp/src/components/AppAuthenticatedShell.tsx index b7b9613..220bcf2 100644 --- a/webapp/src/components/AppAuthenticatedShell.tsx +++ b/webapp/src/components/AppAuthenticatedShell.tsx @@ -1,6 +1,6 @@ -import { ArrowUpDown, Check, ChevronDown, Clock3, Cloud, FileClock, Folder as FolderIcon, KeyRound, Lock, LogOut, MonitorSmartphone, Send as SendIcon, Settings as SettingsIcon, ShieldCheck, ShieldUser, SlidersHorizontal, Sparkles, Users } from 'lucide-preact'; +import { ArrowUpDown, ChevronDown, Clock3, Cloud, FileClock, Folder as FolderIcon, KeyRound, Lock, LogOut, MonitorSmartphone, Send as SendIcon, Settings as SettingsIcon, ShieldCheck, ShieldUser, Sparkles, Users } from 'lucide-preact'; import type { ComponentChildren } from 'preact'; -import { useEffect, useRef, useState } from 'preact/hooks'; +import { useState } from 'preact/hooks'; import { Link } from 'wouter'; import AppMainRoutes from '@/components/AppMainRoutes'; import NetworkStatusBadge from '@/components/NetworkStatusBadge'; @@ -28,19 +28,32 @@ interface AppAuthenticatedShellProps { mainRoutesProps: AppMainRoutesProps; } -type NavLayoutMode = 'flat' | 'grouped-expanded' | 'grouped-smart'; +const NAV_GROUPS_STORAGE_KEY = 'nodewarden.navGroups'; -const NAV_LAYOUT_STORAGE_KEY = 'nodewarden.navLayoutMode'; +const DEFAULT_EXPANDED_GROUPS = { + tools: true, + settings: true, + management: true, +}; -function readNavLayoutMode(): NavLayoutMode { - if (typeof window === 'undefined') return 'flat'; +type NavGroup = keyof typeof DEFAULT_EXPANDED_GROUPS; +type ExpandedGroups = Record; + +function readExpandedGroups(): ExpandedGroups { + if (typeof window === 'undefined') return DEFAULT_EXPANDED_GROUPS; try { - const saved = window.localStorage.getItem(NAV_LAYOUT_STORAGE_KEY); - if (saved === 'flat' || saved === 'grouped-expanded' || saved === 'grouped-smart') return saved; + const saved = window.localStorage.getItem(NAV_GROUPS_STORAGE_KEY); + if (!saved) return DEFAULT_EXPANDED_GROUPS; + const parsed = JSON.parse(saved) as Partial; + return { + tools: typeof parsed.tools === 'boolean' ? parsed.tools : DEFAULT_EXPANDED_GROUPS.tools, + settings: typeof parsed.settings === 'boolean' ? parsed.settings : DEFAULT_EXPANDED_GROUPS.settings, + management: typeof parsed.management === 'boolean' ? parsed.management : DEFAULT_EXPANDED_GROUPS.management, + }; } catch { // Ignore local preference read failures. } - return 'flat'; + return DEFAULT_EXPANDED_GROUPS; } function isAdminProfile(profile: Profile | null): boolean { @@ -55,58 +68,19 @@ export default function AppAuthenticatedShell(props: AppAuthenticatedShellProps) const isDomainRulesRoute = props.location === '/settings/domain-rules'; const isLogRoute = props.location === '/logs'; const isAdmin = isAdminProfile(props.profile); - const vaultActive = props.location === '/vault' || props.location === '/vault/totp' || props.location === '/security/password-health'; const deviceManagementActive = props.location === DEVICE_MANAGEMENT_ROUTE || props.location === LEGACY_DEVICE_MANAGEMENT_ROUTE; - const settingsActive = props.location === '/settings' || props.location === props.settingsAccountRoute || props.location === '/settings/domain-rules' || deviceManagementActive; - const flatSettingsActive = settingsActive && !deviceManagementActive; - const dataActive = props.location === '/backup' || props.isImportRoute; - const managementActive = props.location === '/admin' || props.location === '/logs'; - const [navLayoutMode, setNavLayoutMode] = useState(readNavLayoutMode); - const [navLayoutPickerOpen, setNavLayoutPickerOpen] = useState(false); - const navLayoutPickerRef = useRef(null); - const [expandedGroups, setExpandedGroups] = useState({ - vault: true, - settings: false, - data: false, - management: false, - }); + const [expandedGroups, setExpandedGroups] = useState(readExpandedGroups); - useEffect(() => { - const onPointerDown = (event: Event) => { - if (!navLayoutPickerOpen) return; - const target = event.target as Node | null; - if (navLayoutPickerRef.current && target && !navLayoutPickerRef.current.contains(target)) { - setNavLayoutPickerOpen(false); + function toggleGroup(group: NavGroup): void { + setExpandedGroups((current) => { + const next = { ...current, [group]: !current[group] }; + try { + window.localStorage.setItem(NAV_GROUPS_STORAGE_KEY, JSON.stringify(next)); + } catch { + // Ignore local preference write failures. } - }; - const onKeyDown = (event: KeyboardEvent) => { - if (event.key === 'Escape') setNavLayoutPickerOpen(false); - }; - document.addEventListener('pointerdown', onPointerDown); - document.addEventListener('keydown', onKeyDown); - return () => { - document.removeEventListener('pointerdown', onPointerDown); - document.removeEventListener('keydown', onKeyDown); - }; - }, [navLayoutPickerOpen]); - - function setNavMode(mode: NavLayoutMode): void { - setNavLayoutMode(mode); - setNavLayoutPickerOpen(false); - try { - window.localStorage.setItem(NAV_LAYOUT_STORAGE_KEY, mode); - } catch { - // Ignore local preference write failures. - } - } - - function toggleGroup(group: keyof typeof expandedGroups): void { - setExpandedGroups((current) => ({ ...current, [group]: !current[group] })); - } - - function groupOpen(group: keyof typeof expandedGroups, active: boolean): boolean { - if (navLayoutMode === 'grouped-expanded') return true; - return expandedGroups[group] || active; + return next; + }); } function renderSideLink(href: string, active: boolean, icon: ComponentChildren, label: string) { @@ -127,18 +101,17 @@ export default function AppAuthenticatedShell(props: AppAuthenticatedShellProps) } function renderNavGroup( - group: keyof typeof expandedGroups, + group: NavGroup, title: string, icon: ComponentChildren, - active: boolean, children: ComponentChildren ) { - const open = groupOpen(group, active); + const open = expandedGroups[group]; return (
- ))} -
- )} - + {groupedNav}
diff --git a/webapp/src/components/AppMainRoutes.tsx b/webapp/src/components/AppMainRoutes.tsx index bb2ff1e..d8702b0 100644 --- a/webapp/src/components/AppMainRoutes.tsx +++ b/webapp/src/components/AppMainRoutes.tsx @@ -210,9 +210,19 @@ export default function AppMainRoutes(props: AppMainRoutesProps) { return ( - }> - - +
+ {props.mobileLayout && ( +
+ +
+ )} + }> + + +
}> @@ -333,6 +343,19 @@ export default function AppMainRoutes(props: AppMainRoutesProps) { {props.profile ? (
+
+

{t('nav_group_tools')}

+
+ + + {t('nav_password_security')} + + + + {t('nav_import_export')} + +
+

{t('txt_settings')}

@@ -340,10 +363,6 @@ export default function AppMainRoutes(props: AppMainRoutesProps) { {t('nav_account_settings')} - - - {t('nav_password_security')} - {t('nav_device_management')} @@ -354,25 +373,14 @@ export default function AppMainRoutes(props: AppMainRoutesProps) {
-
-

{t('nav_group_data_backup')}

-
- - - {t('nav_import_export')} - - {isAdmin && ( + {isAdmin && ( +
+

{t('nav_group_system_management')}

+
{t('nav_backup_strategy')} - )} -
-
- {isAdmin && ( -
-

{t('nav_group_management')}

-
{t('nav_admin_panel')} diff --git a/webapp/src/lib/i18n/locales/de.ts b/webapp/src/lib/i18n/locales/de.ts index 99fbcec..cdd2df3 100644 --- a/webapp/src/lib/i18n/locales/de.ts +++ b/webapp/src/lib/i18n/locales/de.ts @@ -12,6 +12,8 @@ const de: Record = { "nav_import_export": "Import und Export", "nav_group_data_backup": "Daten & Backup", "nav_group_management": "Verwaltung", + "nav_group_tools": "Werkzeuge", + "nav_group_system_management": "Systemverwaltung", "txt_settings_appearance": "Erscheinungsbild", "txt_theme": "Design", "txt_use_system_theme": "Systemdesign verwenden", diff --git a/webapp/src/lib/i18n/locales/en.ts b/webapp/src/lib/i18n/locales/en.ts index 8a927c6..ff58dce 100644 --- a/webapp/src/lib/i18n/locales/en.ts +++ b/webapp/src/lib/i18n/locales/en.ts @@ -35,6 +35,8 @@ const en: Record = { "nav_import_export": "Import & Export", "nav_group_data_backup": "Data & Backup", "nav_group_management": "Management", + "nav_group_tools": "Tools", + "nav_group_system_management": "System Management", "txt_settings_appearance": "Appearance", "txt_theme": "Theme", "txt_use_system_theme": "Use system theme", diff --git a/webapp/src/lib/i18n/locales/es.ts b/webapp/src/lib/i18n/locales/es.ts index fa8f122..1e31887 100644 --- a/webapp/src/lib/i18n/locales/es.ts +++ b/webapp/src/lib/i18n/locales/es.ts @@ -12,6 +12,8 @@ const es: Record = { "nav_import_export": "Importar y exportar", "nav_group_data_backup": "Datos y copias", "nav_group_management": "Gestión", + "nav_group_tools": "Herramientas", + "nav_group_system_management": "Administración del sistema", "txt_settings_appearance": "Apariencia", "txt_theme": "Tema", "txt_use_system_theme": "Usar tema del sistema", diff --git a/webapp/src/lib/i18n/locales/fi.ts b/webapp/src/lib/i18n/locales/fi.ts index f561ed2..e96cdb2 100644 --- a/webapp/src/lib/i18n/locales/fi.ts +++ b/webapp/src/lib/i18n/locales/fi.ts @@ -12,6 +12,8 @@ const fi: Record = { "nav_import_export": "Tuonti ja Vienti", "nav_group_data_backup": "Data & Varmuuskopiointi", "nav_group_management": "Hallinta", + "nav_group_tools": "Työkalut", + "nav_group_system_management": "Järjestelmän hallinta", "txt_settings_appearance": "Ulkoasu", "txt_theme": "Teema", "txt_use_system_theme": "Käytä järjestelmän teemaa", diff --git a/webapp/src/lib/i18n/locales/fr.ts b/webapp/src/lib/i18n/locales/fr.ts index 0ced531..eaf6214 100644 --- a/webapp/src/lib/i18n/locales/fr.ts +++ b/webapp/src/lib/i18n/locales/fr.ts @@ -12,6 +12,8 @@ const fr: Record = { "nav_import_export": "Importer & Exporter", "nav_group_data_backup": "Données & Sauvegarde", "nav_group_management": "Gestion", + "nav_group_tools": "Outils", + "nav_group_system_management": "Administration système", "txt_settings_appearance": "Apparence", "txt_theme": "Thème", "txt_use_system_theme": "Utiliser le thème du système", diff --git a/webapp/src/lib/i18n/locales/it.ts b/webapp/src/lib/i18n/locales/it.ts index 1f882c1..eab8758 100644 --- a/webapp/src/lib/i18n/locales/it.ts +++ b/webapp/src/lib/i18n/locales/it.ts @@ -12,6 +12,8 @@ const it: Record = { "nav_import_export": "Importa ed Esporta", "nav_group_data_backup": "Dati e Backup", "nav_group_management": "Gestione", + "nav_group_tools": "Strumenti", + "nav_group_system_management": "Gestione del sistema", "txt_settings_appearance": "Aspetto", "txt_theme": "Tema", "txt_use_system_theme": "Usa il tema del sistema", diff --git a/webapp/src/lib/i18n/locales/ru.ts b/webapp/src/lib/i18n/locales/ru.ts index 8a97d4b..33ff27b 100644 --- a/webapp/src/lib/i18n/locales/ru.ts +++ b/webapp/src/lib/i18n/locales/ru.ts @@ -13,6 +13,8 @@ const ru: Record = { "nav_import_export": "Импорт и экспорт", "nav_group_data_backup": "Данные и резервные копии", "nav_group_management": "Управление", + "nav_group_tools": "Инструменты", + "nav_group_system_management": "Управление системой", "txt_settings_appearance": "Внешний вид", "txt_theme": "Тема", "txt_use_system_theme": "Использовать системную тему", diff --git a/webapp/src/lib/i18n/locales/sv.ts b/webapp/src/lib/i18n/locales/sv.ts index 9baf63d..94d9aa2 100644 --- a/webapp/src/lib/i18n/locales/sv.ts +++ b/webapp/src/lib/i18n/locales/sv.ts @@ -12,6 +12,8 @@ const sv: Record = { "nav_import_export": "Importera och Exportera", "nav_group_data_backup": "Data och Säkerhetskopiering", "nav_group_management": "Hantering", + "nav_group_tools": "Verktyg", + "nav_group_system_management": "Systemadministration", "txt_settings_appearance": "Utseende", "txt_theme": "Tema", "txt_use_system_theme": "Använd systemtema", diff --git a/webapp/src/lib/i18n/locales/zh-CN.ts b/webapp/src/lib/i18n/locales/zh-CN.ts index 90ed5ed..fa4bf22 100644 --- a/webapp/src/lib/i18n/locales/zh-CN.ts +++ b/webapp/src/lib/i18n/locales/zh-CN.ts @@ -15,6 +15,8 @@ const zhCN: Record = { "nav_import_export": "导入导出", "nav_group_data_backup": "数据与备份", "nav_group_management": "管理", + "nav_group_tools": "工具", + "nav_group_system_management": "系统管理", "txt_settings_appearance": "外观", "txt_theme": "主题", "txt_use_system_theme": "使用系统主题", diff --git a/webapp/src/lib/i18n/locales/zh-TW.ts b/webapp/src/lib/i18n/locales/zh-TW.ts index 7a689fc..8d190c2 100644 --- a/webapp/src/lib/i18n/locales/zh-TW.ts +++ b/webapp/src/lib/i18n/locales/zh-TW.ts @@ -15,6 +15,8 @@ const zhTW: Record = { "nav_import_export": "導入導出", "nav_group_data_backup": "資料與備份", "nav_group_management": "管理", + "nav_group_tools": "工具", + "nav_group_system_management": "系統管理", "txt_settings_appearance": "外觀", "txt_theme": "主題", "txt_use_system_theme": "使用系統主題", diff --git a/webapp/src/styles.css b/webapp/src/styles.css index b7cdcfb..ab38e59 100644 --- a/webapp/src/styles.css +++ b/webapp/src/styles.css @@ -677,7 +677,7 @@ h4 { } .card { - margin-bottom: 0px; + margin-bottom: 10; padding: 14px; }