diff --git a/NodeWarden-compat b/NodeWarden-compat new file mode 160000 index 0000000..408a89b --- /dev/null +++ b/NodeWarden-compat @@ -0,0 +1 @@ +Subproject commit 408a89b07d43d82be0841bee19baa11582ffdeb4 diff --git a/webapp/src/components/AppAuthenticatedShell.tsx b/webapp/src/components/AppAuthenticatedShell.tsx index 7afd533..58eea71 100644 --- a/webapp/src/components/AppAuthenticatedShell.tsx +++ b/webapp/src/components/AppAuthenticatedShell.tsx @@ -1,4 +1,6 @@ -import { ArrowUpDown, Cloud, Clock3, Folder as FolderIcon, Globe2, KeyRound, Lock, LogOut, Send as SendIcon, Settings as SettingsIcon, Shield, ShieldUser } from 'lucide-preact'; +import { ChevronDown, Clock3, Cloud, Folder as FolderIcon, KeyRound, Lock, LogOut, Send as SendIcon, Settings as SettingsIcon, ShieldUser } from 'lucide-preact'; +import type { ComponentChildren } from 'preact'; +import { useState } from 'preact/hooks'; import { Link } from 'wouter'; import AppMainRoutes from '@/components/AppMainRoutes'; import ThemeSwitch from '@/components/ThemeSwitch'; @@ -32,6 +34,53 @@ function isAdminProfile(profile: Profile | null): boolean { export default function AppAuthenticatedShell(props: AppAuthenticatedShellProps) { const routeAnimationKey = props.isImportRoute ? props.importRoute : props.location; const isAdmin = isAdminProfile(props.profile); + const vaultActive = props.location === '/vault' || props.location === '/vault/totp'; + const settingsActive = props.location === props.settingsAccountRoute || props.location === '/settings/domain-rules'; + const dataActive = props.location === '/backup' || props.isImportRoute; + const managementActive = props.location === '/admin' || props.location === '/security/devices'; + const [expandedGroups, setExpandedGroups] = useState({ + vault: true, + settings: false, + data: false, + management: false, + }); + + function toggleGroup(group: keyof typeof expandedGroups): void { + setExpandedGroups((current) => ({ ...current, [group]: !current[group] })); + } + + function groupOpen(group: keyof typeof expandedGroups, active: boolean): boolean { + return expandedGroups[group] || active; + } + + function renderNavGroup( + group: keyof typeof expandedGroups, + title: string, + icon: ComponentChildren, + active: boolean, + children: ComponentChildren + ) { + const open = groupOpen(group, active); + return ( +
+ +
+
+ {children} +
+
+
+ ); + } return (
@@ -76,46 +125,70 @@ export default function AppAuthenticatedShell(props: AppAuthenticatedShellProps)
diff --git a/webapp/src/lib/i18n/locales/en.ts b/webapp/src/lib/i18n/locales/en.ts index 97fe7f5..7e50cef 100644 --- a/webapp/src/lib/i18n/locales/en.ts +++ b/webapp/src/lib/i18n/locales/en.ts @@ -4,9 +4,12 @@ const en: Record = { "nav_admin_panel": "Admin Panel", "nav_device_management": "Device Management", "nav_my_vault": "My Vault", + "nav_vault_items": "Vault", "nav_sends": "Sends", "nav_backup_strategy": "Cloud Backup", "nav_import_export": "Import & Export", + "nav_group_data_backup": "Data & Backup", + "nav_group_management": "Management", "txt_page_not_found": "Page Not Found", "txt_page_not_found_hint": "The page may have been removed, expired, or the link is incomplete.", "txt_back_to_home": "Back To Home", diff --git a/webapp/src/lib/i18n/locales/es.ts b/webapp/src/lib/i18n/locales/es.ts index 9b6f7d7..f01a026 100644 --- a/webapp/src/lib/i18n/locales/es.ts +++ b/webapp/src/lib/i18n/locales/es.ts @@ -4,9 +4,12 @@ const es: Record = { "nav_admin_panel": "Panel de administración", "nav_device_management": "Gestión de dispositivos", "nav_my_vault": "Mi bóveda", + "nav_vault_items": "Bóveda", "nav_sends": "Envíos", "nav_backup_strategy": "Copia de seguridad en la nube", "nav_import_export": "Importar y exportar", + "nav_group_data_backup": "Datos y copias", + "nav_group_management": "Gestión", "txt_page_not_found": "Página no encontrada", "txt_page_not_found_hint": "La página pudo haberse eliminado, expirado, o el enlace está incompleto.", "txt_back_to_home": "Volver al inicio", diff --git a/webapp/src/lib/i18n/locales/ru.ts b/webapp/src/lib/i18n/locales/ru.ts index 043fb31..587ca7c 100644 --- a/webapp/src/lib/i18n/locales/ru.ts +++ b/webapp/src/lib/i18n/locales/ru.ts @@ -5,9 +5,12 @@ const ru: Record = { "nav_admin_panel": "Панель администратора", "nav_device_management": "Управление устройствами", "nav_my_vault": "Мое хранилище", + "nav_vault_items": "Хранилище", "nav_sends": "Отправляет", "nav_backup_strategy": "Облачное резервное копирование", "nav_import_export": "Импорт и экспорт", + "nav_group_data_backup": "Данные и резервные копии", + "nav_group_management": "Управление", "txt_page_not_found": "Страница не найдена", "txt_page_not_found_hint": "Страница могла быть удалена, срок ее действия истек, или ссылка неполная.", "txt_back_to_home": "На главную", diff --git a/webapp/src/lib/i18n/locales/zh-CN.ts b/webapp/src/lib/i18n/locales/zh-CN.ts index 6255159..7fd60c6 100644 --- a/webapp/src/lib/i18n/locales/zh-CN.ts +++ b/webapp/src/lib/i18n/locales/zh-CN.ts @@ -4,9 +4,12 @@ const zhCN: Record = { "nav_admin_panel": "用户管理", "nav_device_management": "设备管理", "nav_my_vault": "我的密码库", + "nav_vault_items": "密码库", "nav_sends": "Send", "nav_backup_strategy": "云端备份", "nav_import_export": "导入导出", + "nav_group_data_backup": "数据与备份", + "nav_group_management": "管理", "txt_page_not_found": "页面不存在", "txt_page_not_found_hint": "这个页面可能已经删除、过期,或者链接不完整。", "txt_back_to_home": "回到首页", diff --git a/webapp/src/lib/i18n/locales/zh-TW.ts b/webapp/src/lib/i18n/locales/zh-TW.ts index 5c755f6..d006879 100644 --- a/webapp/src/lib/i18n/locales/zh-TW.ts +++ b/webapp/src/lib/i18n/locales/zh-TW.ts @@ -4,9 +4,12 @@ const zhTW: Record = { "nav_admin_panel": "用戶管理", "nav_device_management": "設備管理", "nav_my_vault": "我的密碼庫", + "nav_vault_items": "密碼庫", "nav_sends": "Send", "nav_backup_strategy": "雲端備份", "nav_import_export": "導入導出", + "nav_group_data_backup": "資料與備份", + "nav_group_management": "管理", "txt_page_not_found": "頁面不存在", "txt_page_not_found_hint": "這個頁面可能已經刪除、過期,或者連結不完整。", "txt_back_to_home": "回到首頁", diff --git a/webapp/src/styles/shell.css b/webapp/src/styles/shell.css index 652a565..3be1c35 100644 --- a/webapp/src/styles/shell.css +++ b/webapp/src/styles/shell.css @@ -167,6 +167,75 @@ @apply flex items-center gap-2.5 rounded-xl border border-transparent px-3 py-2.5 text-sm font-semibold text-muted-strong no-underline transition; } +.side-nav-group { + @apply grid gap-1; +} + +.side-group-trigger { + @apply flex w-full cursor-pointer items-center gap-2.5 rounded-xl border border-transparent px-3 py-2.5 text-left text-sm font-semibold text-muted-strong transition; + background: transparent; +} + +.side-group-trigger:hover { + background: #fff; + border-color: rgba(128, 152, 192, 0.18); + color: var(--text); + box-shadow: 0 10px 18px rgba(15, 23, 42, 0.04); +} + +.side-group-trigger.active { + color: var(--primary-strong); +} + +.side-group-trigger span { + @apply min-w-0 flex-1 truncate; +} + +.side-group-chevron { + @apply shrink-0; + transition: transform 190ms var(--ease-out-soft); +} + +.side-nav-group.open .side-group-chevron { + transform: rotate(180deg); +} + +.side-subnav { + display: grid; + grid-template-rows: 0fr; + opacity: 0; + transform: translateY(-4px); + transition: + grid-template-rows 220ms var(--ease-smooth), + opacity 170ms var(--ease-smooth), + transform 220ms var(--ease-out-soft); +} + +.side-subnav.open { + grid-template-rows: 1fr; + opacity: 1; + transform: translateY(0); +} + +.side-subnav-inner { + @apply grid gap-1 overflow-hidden pl-[38px] pr-1; +} + +.side-sub-link { + @apply block rounded-lg border border-transparent px-3 py-2 text-sm font-semibold text-muted no-underline transition; +} + +.side-sub-link:hover { + background: rgba(255, 255, 255, 0.78); + color: var(--text); +} + +.side-sub-link.active { + background: rgba(37, 99, 235, 0.10); + border-color: rgba(37, 99, 235, 0.18); + color: var(--primary-strong); +} + .side-link:hover { background: #fff; border-color: rgba(128, 152, 192, 0.18);