Merge pull request 'Redesigned nav bar' (#136) from finishline into main

Reviewed-on: #136
This commit was merged in pull request #136.
This commit is contained in:
2026-10-04 18:27:34 +02:00
@@ -8,8 +8,6 @@ import {
ShieldAlert, ShieldAlert,
Settings, Settings,
LogOut, LogOut,
Menu,
X,
ChevronRight, ChevronRight,
ChevronDown, ChevronDown,
ShieldCheck, ShieldCheck,
@@ -19,6 +17,10 @@ import {
Users, Users,
GitPullRequest, GitPullRequest,
MapPin, MapPin,
PanelLeftClose,
PanelLeftOpen,
ChevronsLeft,
ChevronsRight,
} from 'lucide-react'; } from 'lucide-react';
import { useAuth } from '../../auth/AuthProvider'; import { useAuth } from '../../auth/AuthProvider';
import { BrandLogo } from '../ui/BrandLogo'; import { BrandLogo } from '../ui/BrandLogo';
@@ -181,42 +183,115 @@ export const AppLayout: React.FC = () => {
{/* Sidebar */} {/* Sidebar */}
<aside <aside
className={`${ className={`${
sidebarOpen ? 'w-64' : 'w-20' sidebarOpen ? 'w-64' : 'w-[72px]'
} shrink-0 bg-[#0b101b]/95 text-slate-200 border-r border-slate-800/80 transition-all duration-300 flex flex-col z-30 shadow-2xl backdrop-blur-md`} } shrink-0 bg-gradient-to-b from-[#0b101b] via-[#090d16] to-[#0b101b] text-slate-200 border-r border-slate-800/80 transition-all duration-300 ease-in-out flex flex-col z-30 shadow-2xl backdrop-blur-md relative`}
> >
{/* Brand Header */} {/* Brand Header */}
<div className="h-16 px-4 flex items-center justify-between border-b border-slate-800/80 bg-[#0d121f]/90"> <div className={`h-16 border-b border-slate-800/80 bg-[#0d121f]/90 flex items-center transition-all ${
<Link sidebarOpen ? 'px-4 justify-between' : 'px-2 justify-center'
to="/" }`}>
id="nav-brand-home-link" {sidebarOpen ? (
title="Go to Home page" <>
className="flex items-center gap-3 overflow-hidden group cursor-pointer" <Link
> to="/"
<BrandLogo size={36} showGlow animateOnHover /> id="nav-brand-home-link"
{sidebarOpen && ( title="Go to Home page"
<div className="flex flex-col min-w-0"> className="flex items-center gap-3 overflow-hidden group cursor-pointer"
<span className="font-bold text-sm tracking-tight text-white truncate group-hover:text-blue-400 transition"> >
PFM Console <BrandLogo size={32} showGlow animateOnHover />
</span> <div className="flex flex-col min-w-0">
<span className="text-[11px] text-blue-400 font-medium tracking-tight">Enterprise Management</span> <span className="font-bold text-sm tracking-tight text-white truncate group-hover:text-blue-400 transition">
</div> PFM Console
)} </span>
</Link> <span className="text-[11px] text-blue-400 font-medium tracking-tight">Enterprise Management</span>
<button </div>
onClick={() => setSidebarOpen(!sidebarOpen)} </Link>
className="rounded-lg p-1.5 text-slate-400 hover:text-white hover:bg-slate-800/60 transition cursor-pointer" <button
title={sidebarOpen ? 'Collapse sidebar' : 'Expand sidebar'} onClick={() => setSidebarOpen(false)}
> className="p-1.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800/80 border border-transparent hover:border-slate-700/60 transition cursor-pointer shadow-xs"
{sidebarOpen ? <X className="w-4 h-4" /> : <Menu className="w-4 h-4" />} title="Collapse sidebar shelf"
</button> aria-label="Collapse sidebar shelf"
>
<PanelLeftClose className="w-4 h-4" />
</button>
</>
) : (
<div className="flex items-center justify-center">
<Link
to="/"
id="nav-brand-home-link-collapsed"
title="PFM Console • Go to Home page"
className="cursor-pointer transition transform hover:scale-105"
>
<BrandLogo size={32} showGlow animateOnHover />
</Link>
</div>
)}
</div> </div>
{/* Collapsed Shelf Expand Button */}
{!sidebarOpen && (
<div className="pt-2 pb-1 px-2 flex justify-center border-b border-slate-800/40">
<button
onClick={() => setSidebarOpen(true)}
className="w-10 h-8 flex items-center justify-center rounded-lg text-slate-400 hover:text-blue-400 hover:bg-blue-600/10 border border-slate-800 hover:border-blue-500/30 transition cursor-pointer shadow-xs group relative"
title="Expand sidebar shelf"
aria-label="Expand sidebar shelf"
>
<PanelLeftOpen className="w-4 h-4 transition-transform group-hover:scale-110" />
<div className="absolute left-full ml-3 px-2.5 py-1 bg-slate-900 text-white text-xs font-semibold rounded-lg shadow-2xl border border-slate-700 whitespace-nowrap z-50 pointer-events-none opacity-0 group-hover:opacity-100 transition-all duration-150">
Expand Navigation Shelf
</div>
</button>
</div>
)}
{/* Navigation Items */} {/* Navigation Items */}
<nav className="flex-1 overflow-y-auto px-3 py-4 space-y-1.5"> <nav className={`flex-1 overflow-y-auto space-y-1.5 ${sidebarOpen ? 'px-3 py-4' : 'px-2 py-3'}`}>
{visibleNavItems.map((item) => { {visibleNavItems.map((item) => {
const Icon = item.icon; const Icon = item.icon;
const hasSub = item.subItems && item.subItems.length > 0; const hasSub = item.subItems && item.subItems.length > 0;
if (!sidebarOpen) {
return (
<div key={item.path} className="flex justify-center">
<NavLink
to={item.path}
end={hasSub}
className={({ isActive }) =>
`group relative flex items-center justify-center w-11 h-11 rounded-xl transition-all ${
isActive
? 'bg-gradient-to-r from-blue-600 to-indigo-600 text-white shadow-lg shadow-blue-600/30 font-semibold ring-1 ring-blue-400/50'
: 'text-slate-400 hover:text-white hover:bg-slate-800/70 border border-transparent hover:border-slate-700/60'
}`
}
title={item.name}
>
<Icon className="w-5 h-5 shrink-0" />
{/* Hover Floating Tooltip Shelf */}
<div className="absolute left-full ml-3 px-3 py-2 bg-[#0d1322] text-slate-100 rounded-xl shadow-2xl border border-slate-700/90 whitespace-nowrap z-50 pointer-events-none opacity-0 group-hover:opacity-100 transition-all duration-150 translate-x-1 group-hover:translate-x-0 min-w-[160px]">
<div className="text-xs font-bold text-white flex items-center gap-1.5">
<Icon className="w-3.5 h-3.5 text-blue-400" />
<span>{item.name}</span>
</div>
<div className="text-[10px] text-slate-400 font-normal mt-0.5 max-w-[200px] line-clamp-2">
{item.help}
</div>
{hasSub && (
<div className="mt-1.5 pt-1.5 border-t border-slate-800 flex flex-col gap-1 text-[10px] text-blue-300">
{item.subItems?.map((s) => (
<span key={s.path} className="flex items-center gap-1 text-slate-300">
• {s.name}
</span>
))}
</div>
)}
</div>
</NavLink>
</div>
);
}
return ( return (
<div key={item.path} className="space-y-1"> <div key={item.path} className="space-y-1">
<NavLink <NavLink
@@ -231,12 +306,12 @@ export const AppLayout: React.FC = () => {
} }
title={item.name} title={item.name}
> >
<Icon className="w-4 h-4 shrink-0" /> <Icon className="w-4.5 h-4.5 shrink-0" />
{sidebarOpen && <span className="truncate flex-1">{item.name}</span>} <span className="truncate flex-1">{item.name}</span>
</NavLink> </NavLink>
{/* Sub-items under parent item */} {/* Sub-items under parent item */}
{hasSub && sidebarOpen && ( {hasSub && (
<div className="pl-6 pr-1 py-1 space-y-1 border-l border-slate-800/80 ml-5"> <div className="pl-6 pr-1 py-1 space-y-1 border-l border-slate-800/80 ml-5">
{item.subItems?.map((subItem) => { {item.subItems?.map((subItem) => {
const SubIcon = subItem.icon; const SubIcon = subItem.icon;
@@ -266,12 +341,13 @@ export const AppLayout: React.FC = () => {
</nav> </nav>
{/* User Card */} {/* User Card */}
<div className="p-3 border-t border-slate-800/80 bg-[#0d121f]/90"> <div className={`border-t border-slate-800/80 bg-[#0d121f]/90 ${sidebarOpen ? 'p-3' : 'p-2'}`}>
<div className="flex items-center gap-3"> {sidebarOpen ? (
<div className="h-9 w-9 shrink-0 rounded-full bg-blue-600/20 border border-blue-500/40 flex items-center justify-center font-bold text-xs text-blue-400"> <div className="flex items-center gap-3">
{tokenUserName ? tokenUserName.slice(0, 2).toUpperCase() : 'AU'} <div className="h-9 w-9 shrink-0 rounded-full bg-blue-600/20 border border-blue-500/40 flex items-center justify-center font-bold text-xs text-blue-400 relative shadow-xs">
</div> {tokenUserName ? tokenUserName.slice(0, 2).toUpperCase() : 'AU'}
{sidebarOpen && ( <span className="absolute bottom-0 right-0 w-2.5 h-2.5 rounded-full bg-emerald-500 border-2 border-[#0d121f]" title="Active Online Session" />
</div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-xs font-semibold text-white truncate" title={tokenUserName}> <p className="text-xs font-semibold text-white truncate" title={tokenUserName}>
{tokenUserName} {tokenUserName}
@@ -280,16 +356,38 @@ export const AppLayout: React.FC = () => {
{tokenUserEmail || 'Authenticated User'} {tokenUserEmail || 'Authenticated User'}
</p> </p>
</div> </div>
)} <button
<button onClick={handleLogout}
onClick={handleLogout} id="sidebar-logout-btn"
id="sidebar-logout-btn" className="rounded-lg p-1.5 text-slate-400 hover:text-rose-400 hover:bg-rose-950/30 transition cursor-pointer"
className="rounded-lg p-1.5 text-slate-400 hover:text-rose-400 hover:bg-rose-950/30 transition cursor-pointer" title="Sign Out of Session"
title="Sign Out of Session" >
> <LogOut className="w-4 h-4" />
<LogOut className="w-4 h-4" /> </button>
</button> </div>
</div> ) : (
<div className="flex flex-col items-center gap-2">
<div
className="h-10 w-10 shrink-0 rounded-xl bg-blue-600/20 border border-blue-500/40 flex items-center justify-center font-bold text-xs text-blue-400 relative group cursor-pointer shadow-xs"
title={`${tokenUserName} (${tokenUserEmail})`}
>
{tokenUserName ? tokenUserName.slice(0, 2).toUpperCase() : 'AU'}
<span className="absolute -bottom-0.5 -right-0.5 w-3 h-3 rounded-full bg-emerald-500 border-2 border-[#0d121f]" />
<div className="absolute left-full ml-3 px-3 py-2 bg-[#0d1322] text-white text-xs font-medium rounded-xl shadow-2xl border border-slate-700 whitespace-nowrap z-50 pointer-events-none opacity-0 group-hover:opacity-100 transition-all duration-150">
<div className="font-semibold">{tokenUserName}</div>
<div className="text-[10px] text-slate-400 font-mono mt-0.5">{tokenUserEmail}</div>
</div>
</div>
<button
onClick={handleLogout}
id="sidebar-logout-btn-collapsed"
className="w-10 h-9 rounded-xl flex items-center justify-center text-slate-400 hover:text-rose-400 hover:bg-rose-950/30 border border-transparent hover:border-rose-900/40 transition cursor-pointer"
title="Sign Out of Session"
>
<LogOut className="w-4 h-4" />
</button>
</div>
)}
</div> </div>
</aside> </aside>