feat: redesign sidebar navigation with grouped sections, improved visual hierarchy, and enhanced active state indicators
Restructure navigation into three logical groups (Operate, Network, Security) with uppercase section labels, move system-related items (Jobs, Audit, Users, Settings) to separate bottom section with border separator, add NF logo badge next to app title in header, extract SidebarLink component with refined styling including left accent bar for active items, update active state to
This commit is contained in:
@@ -18,31 +18,76 @@ import {
|
|||||||
SquareStack,
|
SquareStack,
|
||||||
Sun,
|
Sun,
|
||||||
Users,
|
Users,
|
||||||
|
type LucideIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
import { token } from "../api/client";
|
import { token } from "../api/client";
|
||||||
import { useTheme } from "../stores/theme";
|
import { useTheme } from "../stores/theme";
|
||||||
|
|
||||||
const nav = [
|
const navGroups = [
|
||||||
|
{
|
||||||
|
label: "Operate",
|
||||||
|
items: [
|
||||||
{ to: "/", label: "Dashboard", icon: LayoutDashboard },
|
{ to: "/", label: "Dashboard", icon: LayoutDashboard },
|
||||||
{ to: "/clusters", label: "Clusters", icon: Server },
|
{ to: "/clusters", label: "Clusters", icon: Server },
|
||||||
{ to: "/nodes", label: "Nodes", icon: Activity },
|
{ to: "/nodes", label: "Nodes", icon: Activity },
|
||||||
{ to: "/workloads", label: "VMs/LXCs", icon: Blocks },
|
{ to: "/workloads", label: "VMs/LXCs", icon: Blocks },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Network",
|
||||||
|
items: [
|
||||||
{ to: "/networks", label: "Networks", icon: Network },
|
{ to: "/networks", label: "Networks", icon: Network },
|
||||||
{ to: "/ipam", label: "IPAM", icon: Database },
|
{ to: "/ipam", label: "IPAM", icon: Database },
|
||||||
|
{ to: "/services", label: "Service Catalog", icon: SquareStack },
|
||||||
{ to: "/tenants", label: "Tenants", icon: BriefcaseBusiness },
|
{ to: "/tenants", label: "Tenants", icon: BriefcaseBusiness },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Security",
|
||||||
|
items: [
|
||||||
{ to: "/security-groups", label: "Security Groups", icon: Shield },
|
{ to: "/security-groups", label: "Security Groups", icon: Shield },
|
||||||
{ to: "/policies", label: "Policies", icon: GitBranch },
|
{ to: "/policies", label: "Policies", icon: GitBranch },
|
||||||
{ to: "/services", label: "Service Catalog", icon: SquareStack },
|
|
||||||
{ to: "/designer", label: "Policy Designer", icon: LockKeyhole },
|
{ to: "/designer", label: "Policy Designer", icon: LockKeyhole },
|
||||||
{ to: "/firewall", label: "Firewall Preview", icon: Flame },
|
{ to: "/firewall", label: "Firewall Preview", icon: Flame },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const systemNav = [
|
||||||
{ to: "/jobs", label: "Jobs", icon: ClipboardList },
|
{ to: "/jobs", label: "Jobs", icon: ClipboardList },
|
||||||
{ to: "/audit", label: "Audit Logs", icon: BookOpen },
|
{ to: "/audit", label: "Audit Logs", icon: BookOpen },
|
||||||
{ to: "/users", label: "Users", icon: Users },
|
{ to: "/users", label: "Users", icon: Users },
|
||||||
{ to: "/settings", label: "Settings", icon: Settings },
|
{ to: "/settings", label: "Settings", icon: Settings },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
function SidebarLink({ item }: { item: { to: string; label: string; icon: LucideIcon } }) {
|
||||||
|
const Icon = item.icon;
|
||||||
|
return (
|
||||||
|
<NavLink
|
||||||
|
key={item.to}
|
||||||
|
to={item.to}
|
||||||
|
end={item.to === "/"}
|
||||||
|
className={({ isActive }) =>
|
||||||
|
`group relative flex h-9 items-center gap-3 rounded-md px-3 text-sm transition-colors ${
|
||||||
|
isActive
|
||||||
|
? "bg-accent/10 text-accent dark:bg-accent/15"
|
||||||
|
: "text-slate-600 hover:bg-slate-100 hover:text-slate-950 dark:text-slate-300 dark:hover:bg-slate-800/80 dark:hover:text-white"
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{({ isActive }) => (
|
||||||
|
<>
|
||||||
|
<span className={`absolute left-0 h-5 w-0.5 rounded-r-full ${isActive ? "bg-accent" : "bg-transparent"}`} />
|
||||||
|
<Icon size={17} className={isActive ? "text-accent" : "text-slate-400 group-hover:text-slate-700 dark:group-hover:text-slate-200"} />
|
||||||
|
<span className="truncate">{item.label}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</NavLink>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function Layout() {
|
export function Layout() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { dark, toggle } = useTheme();
|
const { dark, toggle } = useTheme();
|
||||||
@@ -60,29 +105,31 @@ export function Layout() {
|
|||||||
<div className="min-h-screen bg-canvas text-slate-900 dark:text-slate-100">
|
<div className="min-h-screen bg-canvas text-slate-900 dark:text-slate-100">
|
||||||
<aside className="fixed inset-y-0 left-0 hidden w-64 border-r border-border bg-panel md:block">
|
<aside className="fixed inset-y-0 left-0 hidden w-64 border-r border-border bg-panel md:block">
|
||||||
<div className="flex h-16 items-center border-b border-border px-5">
|
<div className="flex h-16 items-center border-b border-border px-5">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="grid h-9 w-9 place-items-center rounded-md bg-accent text-sm font-semibold text-white">NF</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-lg font-semibold">NexaFabric</div>
|
<div className="text-base font-semibold leading-5">NexaFabric</div>
|
||||||
<div className="text-xs text-slate-500 dark:text-slate-400">Control Plane</div>
|
<div className="text-xs text-slate-500 dark:text-slate-400">Control Plane</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<nav className="h-[calc(100vh-4rem)] overflow-y-auto p-3">
|
</div>
|
||||||
{nav.map((item) => {
|
<nav className="flex h-[calc(100vh-4rem)] flex-col overflow-y-auto p-3">
|
||||||
const Icon = item.icon;
|
<div className="space-y-5">
|
||||||
return (
|
{navGroups.map((group) => (
|
||||||
<NavLink
|
<section key={group.label}>
|
||||||
key={item.to}
|
<div className="mb-2 px-3 text-[11px] font-semibold uppercase tracking-wider text-slate-400 dark:text-slate-500">{group.label}</div>
|
||||||
to={item.to}
|
<div className="space-y-1">
|
||||||
className={({ isActive }) =>
|
{group.items.map((item) => <SidebarLink key={item.to} item={item} />)}
|
||||||
`mb-1 flex h-10 items-center gap-3 rounded-md px-3 text-sm ${
|
</div>
|
||||||
isActive ? "bg-accent text-white" : "text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800"
|
</section>
|
||||||
}`
|
))}
|
||||||
}
|
</div>
|
||||||
>
|
<div className="mt-auto border-t border-border pt-3">
|
||||||
<Icon size={18} />
|
<div className="mb-2 px-3 text-[11px] font-semibold uppercase tracking-wider text-slate-400 dark:text-slate-500">System</div>
|
||||||
<span>{item.label}</span>
|
<div className="space-y-1">
|
||||||
</NavLink>
|
{systemNav.map((item) => <SidebarLink key={item.to} item={item} />)}
|
||||||
);
|
</div>
|
||||||
})}
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
<main className="md:pl-64">
|
<main className="md:pl-64">
|
||||||
|
|||||||
Reference in New Issue
Block a user