mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-08-04 22:40:11 +00:00
feat: enhance navigation layout and improve mobile settings UI
This commit is contained in:
@@ -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 type { ComponentChildren } from 'preact';
|
||||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
import { useState } from 'preact/hooks';
|
||||||
import { Link } from 'wouter';
|
import { Link } from 'wouter';
|
||||||
import AppMainRoutes from '@/components/AppMainRoutes';
|
import AppMainRoutes from '@/components/AppMainRoutes';
|
||||||
import NetworkStatusBadge from '@/components/NetworkStatusBadge';
|
import NetworkStatusBadge from '@/components/NetworkStatusBadge';
|
||||||
@@ -28,19 +28,32 @@ interface AppAuthenticatedShellProps {
|
|||||||
mainRoutesProps: AppMainRoutesProps;
|
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 {
|
type NavGroup = keyof typeof DEFAULT_EXPANDED_GROUPS;
|
||||||
if (typeof window === 'undefined') return 'flat';
|
type ExpandedGroups = Record<NavGroup, boolean>;
|
||||||
|
|
||||||
|
function readExpandedGroups(): ExpandedGroups {
|
||||||
|
if (typeof window === 'undefined') return DEFAULT_EXPANDED_GROUPS;
|
||||||
try {
|
try {
|
||||||
const saved = window.localStorage.getItem(NAV_LAYOUT_STORAGE_KEY);
|
const saved = window.localStorage.getItem(NAV_GROUPS_STORAGE_KEY);
|
||||||
if (saved === 'flat' || saved === 'grouped-expanded' || saved === 'grouped-smart') return saved;
|
if (!saved) return DEFAULT_EXPANDED_GROUPS;
|
||||||
|
const parsed = JSON.parse(saved) as Partial<ExpandedGroups>;
|
||||||
|
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 {
|
} catch {
|
||||||
// Ignore local preference read failures.
|
// Ignore local preference read failures.
|
||||||
}
|
}
|
||||||
return 'flat';
|
return DEFAULT_EXPANDED_GROUPS;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isAdminProfile(profile: Profile | null): boolean {
|
function isAdminProfile(profile: Profile | null): boolean {
|
||||||
@@ -55,58 +68,19 @@ export default function AppAuthenticatedShell(props: AppAuthenticatedShellProps)
|
|||||||
const isDomainRulesRoute = props.location === '/settings/domain-rules';
|
const isDomainRulesRoute = props.location === '/settings/domain-rules';
|
||||||
const isLogRoute = props.location === '/logs';
|
const isLogRoute = props.location === '/logs';
|
||||||
const isAdmin = isAdminProfile(props.profile);
|
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 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 [expandedGroups, setExpandedGroups] = useState<ExpandedGroups>(readExpandedGroups);
|
||||||
const flatSettingsActive = settingsActive && !deviceManagementActive;
|
|
||||||
const dataActive = props.location === '/backup' || props.isImportRoute;
|
|
||||||
const managementActive = props.location === '/admin' || props.location === '/logs';
|
|
||||||
const [navLayoutMode, setNavLayoutMode] = useState<NavLayoutMode>(readNavLayoutMode);
|
|
||||||
const [navLayoutPickerOpen, setNavLayoutPickerOpen] = useState(false);
|
|
||||||
const navLayoutPickerRef = useRef<HTMLDivElement | null>(null);
|
|
||||||
const [expandedGroups, setExpandedGroups] = useState({
|
|
||||||
vault: true,
|
|
||||||
settings: false,
|
|
||||||
data: false,
|
|
||||||
management: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
function toggleGroup(group: NavGroup): void {
|
||||||
const onPointerDown = (event: Event) => {
|
setExpandedGroups((current) => {
|
||||||
if (!navLayoutPickerOpen) return;
|
const next = { ...current, [group]: !current[group] };
|
||||||
const target = event.target as Node | null;
|
|
||||||
if (navLayoutPickerRef.current && target && !navLayoutPickerRef.current.contains(target)) {
|
|
||||||
setNavLayoutPickerOpen(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
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 {
|
try {
|
||||||
window.localStorage.setItem(NAV_LAYOUT_STORAGE_KEY, mode);
|
window.localStorage.setItem(NAV_GROUPS_STORAGE_KEY, JSON.stringify(next));
|
||||||
} catch {
|
} catch {
|
||||||
// Ignore local preference write failures.
|
// Ignore local preference write failures.
|
||||||
}
|
}
|
||||||
}
|
return next;
|
||||||
|
});
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderSideLink(href: string, active: boolean, icon: ComponentChildren, label: string) {
|
function renderSideLink(href: string, active: boolean, icon: ComponentChildren, label: string) {
|
||||||
@@ -127,18 +101,17 @@ export default function AppAuthenticatedShell(props: AppAuthenticatedShellProps)
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderNavGroup(
|
function renderNavGroup(
|
||||||
group: keyof typeof expandedGroups,
|
group: NavGroup,
|
||||||
title: string,
|
title: string,
|
||||||
icon: ComponentChildren,
|
icon: ComponentChildren,
|
||||||
active: boolean,
|
|
||||||
children: ComponentChildren
|
children: ComponentChildren
|
||||||
) {
|
) {
|
||||||
const open = groupOpen(group, active);
|
const open = expandedGroups[group];
|
||||||
return (
|
return (
|
||||||
<div className={`side-nav-group ${open ? 'open' : ''}`}>
|
<div className={`side-nav-group ${open ? 'open' : ''}`}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`side-group-trigger ${active ? 'active' : ''}`}
|
className="side-group-trigger"
|
||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
onClick={() => toggleGroup(group)}
|
onClick={() => toggleGroup(group)}
|
||||||
>
|
>
|
||||||
@@ -155,82 +128,40 @@ export default function AppAuthenticatedShell(props: AppAuthenticatedShellProps)
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const navLayoutOptions: Array<{ mode: NavLayoutMode; label: string }> = [
|
|
||||||
{
|
|
||||||
mode: 'flat',
|
|
||||||
label: t('txt_nav_layout_flat'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
mode: 'grouped-expanded',
|
|
||||||
label: t('txt_nav_layout_grouped_expanded'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
mode: 'grouped-smart',
|
|
||||||
label: t('txt_nav_layout_grouped_smart'),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const navLayoutLabel = navLayoutOptions.find((option) => option.mode === navLayoutMode)?.label || t('txt_nav_layout_flat');
|
|
||||||
const flatNav = (
|
|
||||||
<>
|
|
||||||
{renderSideLink('/vault', props.location === '/vault', <KeyRound size={16} />, t('nav_vault_items'))}
|
|
||||||
{renderSideLink('/vault/totp', props.location === '/vault/totp', <Clock3 size={16} />, t('txt_verification_code'))}
|
|
||||||
{renderSideLink('/security/password-health', props.location === '/security/password-health', <ShieldCheck size={16} />, t('nav_password_security'))}
|
|
||||||
{renderSideLink('/generator', props.location === '/generator', <Sparkles size={16} />, t('nav_generator'))}
|
|
||||||
{renderSideLink('/sends', props.location === '/sends', <SendIcon size={16} />, t('nav_sends'))}
|
|
||||||
{renderSideLink('/settings', flatSettingsActive, <SettingsIcon size={16} />, t('txt_settings'))}
|
|
||||||
{renderSideLink(DEVICE_MANAGEMENT_ROUTE, deviceManagementActive, <MonitorSmartphone size={16} />, t('nav_device_management'))}
|
|
||||||
{isAdmin && renderSideLink('/backup', props.location === '/backup', <Cloud size={16} />, t('nav_backup_strategy'))}
|
|
||||||
{renderSideLink(props.importRoute, props.isImportRoute, <ArrowUpDown size={16} />, t('nav_import_export'))}
|
|
||||||
{isAdmin && renderSideLink('/admin', props.location === '/admin', <Users size={16} />, t('nav_admin_panel'))}
|
|
||||||
{isAdmin && renderSideLink('/logs', props.location === '/logs', <FileClock size={16} />, t('nav_log_center'))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
const groupedNav = (
|
const groupedNav = (
|
||||||
<>
|
<>
|
||||||
{renderNavGroup(
|
{renderSideLink('/vault', props.location === '/vault', <KeyRound size={16} />, t('nav_vault_items'))}
|
||||||
'vault',
|
|
||||||
t('nav_my_vault'),
|
|
||||||
<KeyRound size={16} />,
|
|
||||||
vaultActive,
|
|
||||||
<>
|
|
||||||
{renderSubLink('/vault', props.location === '/vault', t('nav_vault_items'))}
|
|
||||||
{renderSubLink('/vault/totp', props.location === '/vault/totp', t('txt_verification_code'))}
|
|
||||||
{renderSubLink('/security/password-health', props.location === '/security/password-health', t('nav_password_security'))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{renderSideLink('/generator', props.location === '/generator', <Sparkles size={16} />, t('nav_generator'))}
|
|
||||||
{renderSideLink('/sends', props.location === '/sends', <SendIcon size={16} />, t('nav_sends'))}
|
{renderSideLink('/sends', props.location === '/sends', <SendIcon size={16} />, t('nav_sends'))}
|
||||||
{renderNavGroup(
|
{renderNavGroup(
|
||||||
'settings',
|
'tools',
|
||||||
t('txt_settings'),
|
t('nav_group_tools'),
|
||||||
<SettingsIcon size={16} />,
|
<Sparkles size={16} />,
|
||||||
settingsActive,
|
|
||||||
<>
|
<>
|
||||||
{renderSubLink(props.settingsAccountRoute, props.location === props.settingsAccountRoute, t('nav_account_settings'))}
|
{renderSubLink('/vault/totp', props.location === '/vault/totp', t('txt_verification_code'))}
|
||||||
{renderSubLink('/settings/domain-rules', props.location === '/settings/domain-rules', t('nav_domain_rules'))}
|
{renderSubLink('/generator', props.location === '/generator', t('nav_generator'))}
|
||||||
{renderSubLink(DEVICE_MANAGEMENT_ROUTE, deviceManagementActive, t('nav_device_management'))}
|
{renderSubLink('/security/password-health', props.location === '/security/password-health', t('nav_password_security'))}
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{renderNavGroup(
|
|
||||||
'data',
|
|
||||||
t('nav_group_data_backup'),
|
|
||||||
<Cloud size={16} />,
|
|
||||||
dataActive,
|
|
||||||
<>
|
|
||||||
{isAdmin && renderSubLink('/backup', props.location === '/backup', t('nav_backup_strategy'))}
|
|
||||||
{renderSubLink(props.importRoute, props.isImportRoute, t('nav_import_export'))}
|
{renderSubLink(props.importRoute, props.isImportRoute, t('nav_import_export'))}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{renderNavGroup(
|
{renderNavGroup(
|
||||||
'management',
|
'settings',
|
||||||
t('nav_group_management'),
|
t('txt_settings'),
|
||||||
<ShieldUser size={16} />,
|
<SettingsIcon size={16} />,
|
||||||
managementActive,
|
|
||||||
<>
|
<>
|
||||||
{isAdmin && renderSubLink('/admin', props.location === '/admin', t('nav_admin_panel'))}
|
{renderSubLink(props.settingsAccountRoute, props.location === props.settingsAccountRoute, t('nav_account_settings'))}
|
||||||
{isAdmin && renderSubLink('/logs', props.location === '/logs', t('nav_log_center'))}
|
{renderSubLink(DEVICE_MANAGEMENT_ROUTE, deviceManagementActive, t('nav_device_management'))}
|
||||||
|
{renderSubLink('/settings/domain-rules', props.location === '/settings/domain-rules', t('nav_domain_rules'))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{isAdmin &&
|
||||||
|
renderNavGroup(
|
||||||
|
'management',
|
||||||
|
t('nav_group_system_management'),
|
||||||
|
<ShieldUser size={16} />,
|
||||||
|
<>
|
||||||
|
{renderSubLink('/backup', props.location === '/backup', t('nav_backup_strategy'))}
|
||||||
|
{renderSubLink('/admin', props.location === '/admin', t('nav_admin_panel'))}
|
||||||
|
{renderSubLink('/logs', props.location === '/logs', t('nav_log_center'))}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@@ -281,38 +212,7 @@ export default function AppAuthenticatedShell(props: AppAuthenticatedShellProps)
|
|||||||
<div className="app-main">
|
<div className="app-main">
|
||||||
<aside className="app-side">
|
<aside className="app-side">
|
||||||
<div className="side-nav-main">
|
<div className="side-nav-main">
|
||||||
{navLayoutMode === 'flat' ? flatNav : groupedNav}
|
{groupedNav}
|
||||||
</div>
|
|
||||||
<div className="nav-layout-control" ref={navLayoutPickerRef}>
|
|
||||||
{navLayoutPickerOpen && (
|
|
||||||
<div className="nav-layout-menu" role="menu">
|
|
||||||
{navLayoutOptions.map((option) => (
|
|
||||||
<button
|
|
||||||
key={option.mode}
|
|
||||||
type="button"
|
|
||||||
className={`nav-layout-option ${navLayoutMode === option.mode ? 'active' : ''}`}
|
|
||||||
onClick={() => setNavMode(option.mode)}
|
|
||||||
role="menuitemradio"
|
|
||||||
aria-checked={navLayoutMode === option.mode}
|
|
||||||
>
|
|
||||||
<span className="nav-layout-option-text">
|
|
||||||
<strong>{option.label}</strong>
|
|
||||||
</span>
|
|
||||||
{navLayoutMode === option.mode && <Check size={15} className="nav-layout-check" />}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`nav-layout-trigger ${navLayoutPickerOpen ? 'active' : ''}`}
|
|
||||||
aria-haspopup="menu"
|
|
||||||
aria-expanded={navLayoutPickerOpen}
|
|
||||||
onClick={() => setNavLayoutPickerOpen((open) => !open)}
|
|
||||||
title={t('txt_nav_layout')}
|
|
||||||
>
|
|
||||||
<SlidersHorizontal size={15} />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
<main className="content">
|
<main className="content">
|
||||||
|
|||||||
@@ -210,9 +210,19 @@ export default function AppMainRoutes(props: AppMainRoutesProps) {
|
|||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/security/password-health">
|
<Route path="/security/password-health">
|
||||||
|
<div className="stack">
|
||||||
|
{props.mobileLayout && (
|
||||||
|
<div className="mobile-settings-subhead">
|
||||||
|
<button type="button" className="btn btn-secondary small mobile-settings-back" onClick={() => props.onNavigate(props.settingsHomeRoute)}>
|
||||||
|
<span className="btn-icon" aria-hidden="true">{"<"}</span>
|
||||||
|
{t('txt_back')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<Suspense fallback={<RouteContentFallback />}>
|
<Suspense fallback={<RouteContentFallback />}>
|
||||||
<PasswordSecurityPage ciphers={props.decryptedCiphers} loading={props.ciphersLoading} />
|
<PasswordSecurityPage ciphers={props.decryptedCiphers} loading={props.ciphersLoading} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
</div>
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/generator">
|
<Route path="/generator">
|
||||||
<Suspense fallback={<RouteContentFallback />}>
|
<Suspense fallback={<RouteContentFallback />}>
|
||||||
@@ -333,6 +343,19 @@ export default function AppMainRoutes(props: AppMainRoutesProps) {
|
|||||||
<Route path="/settings">
|
<Route path="/settings">
|
||||||
{props.profile ? (
|
{props.profile ? (
|
||||||
<section className="card mobile-settings-card settings-home-card">
|
<section className="card mobile-settings-card settings-home-card">
|
||||||
|
<div className="settings-home-section">
|
||||||
|
<h3>{t('nav_group_tools')}</h3>
|
||||||
|
<div className="mobile-settings-links">
|
||||||
|
<Link href="/security/password-health" className="mobile-settings-link">
|
||||||
|
<ShieldCheck size={18} />
|
||||||
|
<span>{t('nav_password_security')}</span>
|
||||||
|
</Link>
|
||||||
|
<Link href={props.importRoute} className="mobile-settings-link">
|
||||||
|
<ArrowUpDown size={18} />
|
||||||
|
<span>{t('nav_import_export')}</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="settings-home-section">
|
<div className="settings-home-section">
|
||||||
<h3>{t('txt_settings')}</h3>
|
<h3>{t('txt_settings')}</h3>
|
||||||
<div className="mobile-settings-links">
|
<div className="mobile-settings-links">
|
||||||
@@ -340,10 +363,6 @@ export default function AppMainRoutes(props: AppMainRoutesProps) {
|
|||||||
<SettingsIcon size={18} />
|
<SettingsIcon size={18} />
|
||||||
<span>{t('nav_account_settings')}</span>
|
<span>{t('nav_account_settings')}</span>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/security/password-health" className="mobile-settings-link">
|
|
||||||
<ShieldCheck size={18} />
|
|
||||||
<span>{t('nav_password_security')}</span>
|
|
||||||
</Link>
|
|
||||||
<Link href="/settings/security/device-management" className="mobile-settings-link">
|
<Link href="/settings/security/device-management" className="mobile-settings-link">
|
||||||
<Shield size={18} />
|
<Shield size={18} />
|
||||||
<span>{t('nav_device_management')}</span>
|
<span>{t('nav_device_management')}</span>
|
||||||
@@ -354,25 +373,14 @@ export default function AppMainRoutes(props: AppMainRoutesProps) {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="settings-home-section">
|
|
||||||
<h3>{t('nav_group_data_backup')}</h3>
|
|
||||||
<div className="mobile-settings-links">
|
|
||||||
<Link href={props.importRoute} className="mobile-settings-link">
|
|
||||||
<ArrowUpDown size={18} />
|
|
||||||
<span>{t('nav_import_export')}</span>
|
|
||||||
</Link>
|
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
|
<div className="settings-home-section">
|
||||||
|
<h3>{t('nav_group_system_management')}</h3>
|
||||||
|
<div className="mobile-settings-links">
|
||||||
<Link href="/backup" className="mobile-settings-link">
|
<Link href="/backup" className="mobile-settings-link">
|
||||||
<Cloud size={18} />
|
<Cloud size={18} />
|
||||||
<span>{t('nav_backup_strategy')}</span>
|
<span>{t('nav_backup_strategy')}</span>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{isAdmin && (
|
|
||||||
<div className="settings-home-section">
|
|
||||||
<h3>{t('nav_group_management')}</h3>
|
|
||||||
<div className="mobile-settings-links">
|
|
||||||
<Link href="/admin" className="mobile-settings-link">
|
<Link href="/admin" className="mobile-settings-link">
|
||||||
<ShieldUser size={18} />
|
<ShieldUser size={18} />
|
||||||
<span>{t('nav_admin_panel')}</span>
|
<span>{t('nav_admin_panel')}</span>
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ const de: Record<string, string> = {
|
|||||||
"nav_import_export": "Import und Export",
|
"nav_import_export": "Import und Export",
|
||||||
"nav_group_data_backup": "Daten & Backup",
|
"nav_group_data_backup": "Daten & Backup",
|
||||||
"nav_group_management": "Verwaltung",
|
"nav_group_management": "Verwaltung",
|
||||||
|
"nav_group_tools": "Werkzeuge",
|
||||||
|
"nav_group_system_management": "Systemverwaltung",
|
||||||
"txt_settings_appearance": "Erscheinungsbild",
|
"txt_settings_appearance": "Erscheinungsbild",
|
||||||
"txt_theme": "Design",
|
"txt_theme": "Design",
|
||||||
"txt_use_system_theme": "Systemdesign verwenden",
|
"txt_use_system_theme": "Systemdesign verwenden",
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ const en: Record<string, string> = {
|
|||||||
"nav_import_export": "Import & Export",
|
"nav_import_export": "Import & Export",
|
||||||
"nav_group_data_backup": "Data & Backup",
|
"nav_group_data_backup": "Data & Backup",
|
||||||
"nav_group_management": "Management",
|
"nav_group_management": "Management",
|
||||||
|
"nav_group_tools": "Tools",
|
||||||
|
"nav_group_system_management": "System Management",
|
||||||
"txt_settings_appearance": "Appearance",
|
"txt_settings_appearance": "Appearance",
|
||||||
"txt_theme": "Theme",
|
"txt_theme": "Theme",
|
||||||
"txt_use_system_theme": "Use system theme",
|
"txt_use_system_theme": "Use system theme",
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ const es: Record<string, string> = {
|
|||||||
"nav_import_export": "Importar y exportar",
|
"nav_import_export": "Importar y exportar",
|
||||||
"nav_group_data_backup": "Datos y copias",
|
"nav_group_data_backup": "Datos y copias",
|
||||||
"nav_group_management": "Gestión",
|
"nav_group_management": "Gestión",
|
||||||
|
"nav_group_tools": "Herramientas",
|
||||||
|
"nav_group_system_management": "Administración del sistema",
|
||||||
"txt_settings_appearance": "Apariencia",
|
"txt_settings_appearance": "Apariencia",
|
||||||
"txt_theme": "Tema",
|
"txt_theme": "Tema",
|
||||||
"txt_use_system_theme": "Usar tema del sistema",
|
"txt_use_system_theme": "Usar tema del sistema",
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ const fi: Record<string, string> = {
|
|||||||
"nav_import_export": "Tuonti ja Vienti",
|
"nav_import_export": "Tuonti ja Vienti",
|
||||||
"nav_group_data_backup": "Data & Varmuuskopiointi",
|
"nav_group_data_backup": "Data & Varmuuskopiointi",
|
||||||
"nav_group_management": "Hallinta",
|
"nav_group_management": "Hallinta",
|
||||||
|
"nav_group_tools": "Työkalut",
|
||||||
|
"nav_group_system_management": "Järjestelmän hallinta",
|
||||||
"txt_settings_appearance": "Ulkoasu",
|
"txt_settings_appearance": "Ulkoasu",
|
||||||
"txt_theme": "Teema",
|
"txt_theme": "Teema",
|
||||||
"txt_use_system_theme": "Käytä järjestelmän teemaa",
|
"txt_use_system_theme": "Käytä järjestelmän teemaa",
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ const fr: Record<string, string> = {
|
|||||||
"nav_import_export": "Importer & Exporter",
|
"nav_import_export": "Importer & Exporter",
|
||||||
"nav_group_data_backup": "Données & Sauvegarde",
|
"nav_group_data_backup": "Données & Sauvegarde",
|
||||||
"nav_group_management": "Gestion",
|
"nav_group_management": "Gestion",
|
||||||
|
"nav_group_tools": "Outils",
|
||||||
|
"nav_group_system_management": "Administration système",
|
||||||
"txt_settings_appearance": "Apparence",
|
"txt_settings_appearance": "Apparence",
|
||||||
"txt_theme": "Thème",
|
"txt_theme": "Thème",
|
||||||
"txt_use_system_theme": "Utiliser le thème du système",
|
"txt_use_system_theme": "Utiliser le thème du système",
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ const it: Record<string, string> = {
|
|||||||
"nav_import_export": "Importa ed Esporta",
|
"nav_import_export": "Importa ed Esporta",
|
||||||
"nav_group_data_backup": "Dati e Backup",
|
"nav_group_data_backup": "Dati e Backup",
|
||||||
"nav_group_management": "Gestione",
|
"nav_group_management": "Gestione",
|
||||||
|
"nav_group_tools": "Strumenti",
|
||||||
|
"nav_group_system_management": "Gestione del sistema",
|
||||||
"txt_settings_appearance": "Aspetto",
|
"txt_settings_appearance": "Aspetto",
|
||||||
"txt_theme": "Tema",
|
"txt_theme": "Tema",
|
||||||
"txt_use_system_theme": "Usa il tema del sistema",
|
"txt_use_system_theme": "Usa il tema del sistema",
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ const ru: Record<string, string> = {
|
|||||||
"nav_import_export": "Импорт и экспорт",
|
"nav_import_export": "Импорт и экспорт",
|
||||||
"nav_group_data_backup": "Данные и резервные копии",
|
"nav_group_data_backup": "Данные и резервные копии",
|
||||||
"nav_group_management": "Управление",
|
"nav_group_management": "Управление",
|
||||||
|
"nav_group_tools": "Инструменты",
|
||||||
|
"nav_group_system_management": "Управление системой",
|
||||||
"txt_settings_appearance": "Внешний вид",
|
"txt_settings_appearance": "Внешний вид",
|
||||||
"txt_theme": "Тема",
|
"txt_theme": "Тема",
|
||||||
"txt_use_system_theme": "Использовать системную тему",
|
"txt_use_system_theme": "Использовать системную тему",
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ const sv: Record<string, string> = {
|
|||||||
"nav_import_export": "Importera och Exportera",
|
"nav_import_export": "Importera och Exportera",
|
||||||
"nav_group_data_backup": "Data och Säkerhetskopiering",
|
"nav_group_data_backup": "Data och Säkerhetskopiering",
|
||||||
"nav_group_management": "Hantering",
|
"nav_group_management": "Hantering",
|
||||||
|
"nav_group_tools": "Verktyg",
|
||||||
|
"nav_group_system_management": "Systemadministration",
|
||||||
"txt_settings_appearance": "Utseende",
|
"txt_settings_appearance": "Utseende",
|
||||||
"txt_theme": "Tema",
|
"txt_theme": "Tema",
|
||||||
"txt_use_system_theme": "Använd systemtema",
|
"txt_use_system_theme": "Använd systemtema",
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ const zhCN: Record<string, string> = {
|
|||||||
"nav_import_export": "导入导出",
|
"nav_import_export": "导入导出",
|
||||||
"nav_group_data_backup": "数据与备份",
|
"nav_group_data_backup": "数据与备份",
|
||||||
"nav_group_management": "管理",
|
"nav_group_management": "管理",
|
||||||
|
"nav_group_tools": "工具",
|
||||||
|
"nav_group_system_management": "系统管理",
|
||||||
"txt_settings_appearance": "外观",
|
"txt_settings_appearance": "外观",
|
||||||
"txt_theme": "主题",
|
"txt_theme": "主题",
|
||||||
"txt_use_system_theme": "使用系统主题",
|
"txt_use_system_theme": "使用系统主题",
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ const zhTW: Record<string, string> = {
|
|||||||
"nav_import_export": "導入導出",
|
"nav_import_export": "導入導出",
|
||||||
"nav_group_data_backup": "資料與備份",
|
"nav_group_data_backup": "資料與備份",
|
||||||
"nav_group_management": "管理",
|
"nav_group_management": "管理",
|
||||||
|
"nav_group_tools": "工具",
|
||||||
|
"nav_group_system_management": "系統管理",
|
||||||
"txt_settings_appearance": "外觀",
|
"txt_settings_appearance": "外觀",
|
||||||
"txt_theme": "主題",
|
"txt_theme": "主題",
|
||||||
"txt_use_system_theme": "使用系統主題",
|
"txt_use_system_theme": "使用系統主題",
|
||||||
|
|||||||
@@ -677,7 +677,7 @@ h4 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
margin-bottom: 0px;
|
margin-bottom: 10;
|
||||||
padding: 14px;
|
padding: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user