refactor: organize sidebar navigation into categorized sections with Overview, Access, and Infrastructure groups

Group navigation items into three sections with section titles. Add nav-sections and nav-section containers with gap spacing and uppercase section title styling.

Remove topbar icon wrapper and associated styling. Center sidebar brand logo with justify-content and add bottom padding. Adjust nav margin-top to 0 and move gap spacing to nav-sections level.

Move page-title-icon dimensions
This commit is contained in:
2026-03-24 17:58:22 +01:00
parent 8282c1fbf4
commit c1f1b6c41f
2 changed files with 82 additions and 44 deletions

View File

@@ -1,26 +1,40 @@
import { ChevronRight } from "lucide-react"; import { ChevronRight } from "lucide-react";
import { NavLink, Outlet } from "react-router-dom"; import { NavLink, Outlet } from "react-router-dom";
import { appIconMap, topbarIcon, type AppIconKey } from "./app-icons"; import { appIconMap, type AppIconKey } from "./app-icons";
const items = [ const sections = [
["Dashboard", "/", "dashboard"], {
["Users", "/users", "users"], title: "Overview",
["Groups", "/groups", "groups"], items: [["Dashboard", "/", "dashboard"]]
["Devices", "/devices", "devices"], },
["Services", "/services", "services"], {
["Policies", "/policies", "policies"], title: "Access",
["Gateways", "/gateways", "gateways"], items: [
["Audit", "/audit", "audit"], ["Users", "/users", "users"],
["Settings", "/settings", "settings"] ["Groups", "/groups", "groups"],
] as const satisfies ReadonlyArray<[string, string, AppIconKey]>; ["Devices", "/devices", "devices"],
["Services", "/services", "services"],
["Policies", "/policies", "policies"]
]
},
{
title: "Infrastructure",
items: [
["Gateways", "/gateways", "gateways"],
["Audit", "/audit", "audit"],
["Settings", "/settings", "settings"]
]
}
] as const satisfies ReadonlyArray<{
title: string;
items: ReadonlyArray<[string, string, AppIconKey]>;
}>;
type LayoutProps = { type LayoutProps = {
onLogout: () => void; onLogout: () => void;
}; };
const TopbarIcon = topbarIcon;
export function Layout({ onLogout }: LayoutProps) { export function Layout({ onLogout }: LayoutProps) {
return ( return (
<div className="shell"> <div className="shell">
@@ -28,31 +42,35 @@ export function Layout({ onLogout }: LayoutProps) {
<div className="sidebar-brand"> <div className="sidebar-brand">
<img className="sidebar-brand-logo" src="/NexaVPN_Logo.png" alt="NexaVPN" /> <img className="sidebar-brand-logo" src="/NexaVPN_Logo.png" alt="NexaVPN" />
</div> </div>
<nav className="nav"> <div className="nav-sections">
{items.map(([label, path, iconKey]) => { {sections.map((section) => (
const Icon = appIconMap[iconKey]; <div className="nav-section" key={section.title}>
return ( <p className="nav-section-title">{section.title}</p>
<NavLink <nav className="nav">
key={path} {section.items.map(([label, path, iconKey]) => {
to={path} const Icon = appIconMap[iconKey];
className={({ isActive }) => (isActive ? "nav-link active" : "nav-link")} return (
> <NavLink
<span className="nav-link-copy"> key={path}
<Icon className="nav-link-icon" size={18} strokeWidth={2} /> to={path}
<span>{label}</span> className={({ isActive }) => (isActive ? "nav-link active" : "nav-link")}
</span> >
<ChevronRight className="nav-link-chevron" size={16} strokeWidth={2} /> <span className="nav-link-copy">
</NavLink> <Icon className="nav-link-icon" size={18} strokeWidth={2} />
); <span>{label}</span>
})} </span>
</nav> <ChevronRight className="nav-link-chevron" size={16} strokeWidth={2} />
</NavLink>
);
})}
</nav>
</div>
))}
</div>
</aside> </aside>
<main className="content"> <main className="content">
<header className="topbar"> <header className="topbar">
<div className="topbar-brand"> <div className="topbar-brand">
<div className="topbar-icon-wrap" aria-hidden="true">
<TopbarIcon size={22} strokeWidth={2} />
</div>
<div className="topbar-copy"> <div className="topbar-copy">
<p className="eyebrow">Enterprise WireGuard</p> <p className="eyebrow">Enterprise WireGuard</p>
<h2>Self-hosted VPN management</h2> <h2>Self-hosted VPN management</h2>

View File

@@ -92,7 +92,6 @@ button {
align-items: flex-start; align-items: flex-start;
} }
.topbar-icon-wrap,
.page-title-icon { .page-title-icon {
display: grid; display: grid;
place-items: center; place-items: center;
@@ -174,6 +173,8 @@ button {
.sidebar-brand { .sidebar-brand {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center;
padding-bottom: 10px;
} }
.sidebar-brand-logo { .sidebar-brand-logo {
@@ -182,10 +183,30 @@ button {
display: block; display: block;
} }
.nav-sections {
display: grid;
gap: 26px;
margin-top: 24px;
}
.nav-section {
display: grid;
gap: 12px;
}
.nav-section-title {
margin: 0;
padding: 0 12px;
color: var(--muted);
font-size: 0.76rem;
text-transform: uppercase;
letter-spacing: 0.16em;
}
.nav { .nav {
display: grid; display: grid;
gap: 10px; gap: 10px;
margin-top: 28px; margin-top: 0;
} }
.nav-link { .nav-link {
@@ -243,13 +264,6 @@ button {
gap: 6px; gap: 6px;
} }
.topbar-icon-wrap,
.page-title-icon {
width: 52px;
height: 52px;
border-radius: 18px;
}
.page { .page {
display: grid; display: grid;
gap: 22px; gap: 22px;
@@ -283,6 +297,12 @@ button {
max-width: 760px; max-width: 760px;
} }
.page-title-icon {
width: 52px;
height: 52px;
border-radius: 18px;
}
.grid { .grid {
display: grid; display: grid;
gap: 18px; gap: 18px;