feat: redesign sidebar with icons, collapse toggle, and improved theme

- Add lucide icons to all navigation items
- Collapsible sidebar with icon-only mode and tooltips
- Narrower sidebar (w-60 vs w-64), compact top bar (h-12 vs h-14)
- Better search bar UX in top bar with keyboard shortcut hint
- Refined dark theme with better contrast and separation
- Custom thin scrollbar styling
- Backdrop blur for sidebar and top bar

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-02-22 02:09:28 -08:00
parent 7dbd2c1414
commit 9a33cef951
5 changed files with 286 additions and 112 deletions

View File

@ -21,11 +21,11 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
if (!ready) return null;
return (
<div className="flex h-screen">
<div className="flex h-screen bg-background">
<Sidebar />
<div className="flex-1 flex flex-col overflow-hidden">
<div className="flex-1 flex flex-col overflow-hidden min-w-0">
<TopBar />
<main className="flex-1 overflow-y-auto p-6">
<main className="flex-1 overflow-y-auto scrollbar-thin p-5">
{children}
</main>
</div>

View File

@ -1,21 +1,58 @@
'use client';
import { useState } from 'react';
import { useState, createContext, useContext } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { cn } from '@/lib/utils';
import {
LayoutDashboard,
Bot,
BookOpen,
ClipboardList,
Server,
Activity,
Terminal,
Shield,
FileSearch,
MessageSquare,
Building2,
Settings,
ChevronRight,
PanelLeftClose,
PanelLeft,
} from 'lucide-react';
/* ---------- Sidebar context for collapse state ---------- */
interface SidebarContextValue {
collapsed: boolean;
setCollapsed: (v: boolean) => void;
}
const SidebarContext = createContext<SidebarContextValue>({
collapsed: false,
setCollapsed: () => {},
});
export const useSidebar = () => useContext(SidebarContext);
/* ---------- Nav config ---------- */
interface NavItem {
label: string;
href: string;
icon: React.ReactNode;
children?: { label: string; href: string }[];
}
const iconClass = 'h-4 w-4 shrink-0';
const navItems: NavItem[] = [
{ label: 'Dashboard', href: '/dashboard' },
{ label: 'Dashboard', href: '/dashboard', icon: <LayoutDashboard className={iconClass} /> },
{
label: 'Agent Config',
href: '/agent-config',
icon: <Bot className={iconClass} />,
children: [
{ label: 'Engine & Prompt', href: '/agent-config' },
{ label: 'SDK Config', href: '/agent-config/sdk' },
@ -23,11 +60,12 @@ const navItems: NavItem[] = [
{ label: 'Hooks', href: '/agent-config/hooks' },
],
},
{ label: 'Runbooks', href: '/runbooks' },
{ label: 'Standing Orders', href: '/standing-orders' },
{ label: 'Runbooks', href: '/runbooks', icon: <BookOpen className={iconClass} /> },
{ label: 'Standing Orders', href: '/standing-orders', icon: <ClipboardList className={iconClass} /> },
{
label: 'Servers',
href: '/servers',
icon: <Server className={iconClass} />,
children: [
{ label: 'All Servers', href: '/servers' },
{ label: 'Clusters', href: '/servers/clusters' },
@ -36,15 +74,17 @@ const navItems: NavItem[] = [
{
label: 'Monitoring',
href: '/monitoring/alert-rules',
icon: <Activity className={iconClass} />,
children: [
{ label: 'Alert Rules', href: '/monitoring/alert-rules' },
{ label: 'Health Checks', href: '/monitoring/health-checks' },
],
},
{ label: 'Terminal', href: '/terminal' },
{ label: 'Terminal', href: '/terminal', icon: <Terminal className={iconClass} /> },
{
label: 'Security',
href: '/security/risk-rules',
icon: <Shield className={iconClass} />,
children: [
{ label: 'Risk Rules', href: '/security/risk-rules' },
{ label: 'Credentials', href: '/security/credentials' },
@ -53,16 +93,19 @@ const navItems: NavItem[] = [
{
label: 'Audit',
href: '/audit/logs',
icon: <FileSearch className={iconClass} />,
children: [
{ label: 'Logs', href: '/audit/logs' },
{ label: 'Session Replay', href: '/audit/replay' },
],
},
{ label: 'Communication', href: '/communication' },
{ label: 'Tenants', href: '/tenants' },
{ label: 'Settings', href: '/settings' },
{ label: 'Communication', href: '/communication', icon: <MessageSquare className={iconClass} /> },
{ label: 'Tenants', href: '/tenants', icon: <Building2 className={iconClass} /> },
{ label: 'Settings', href: '/settings', icon: <Settings className={iconClass} /> },
];
/* ---------- Helpers ---------- */
function isActive(pathname: string, href: string) {
return pathname === href || pathname.startsWith(href + '/');
}
@ -72,81 +115,140 @@ function isGroupActive(pathname: string, item: NavItem) {
return item.children?.some((c) => isActive(pathname, c.href)) ?? false;
}
/* ---------- Tooltip for collapsed mode ---------- */
function Tooltip({ children, label }: { children: React.ReactNode; label: string }) {
return (
<div className="relative group/tip">
{children}
<div
className="absolute left-full top-1/2 -translate-y-1/2 ml-2 px-2.5 py-1 rounded-md
bg-popover text-popover-foreground text-xs font-medium whitespace-nowrap
shadow-md border opacity-0 pointer-events-none
group-hover/tip:opacity-100 transition-opacity duration-150 z-50"
>
{label}
</div>
</div>
);
}
/* ---------- Main component ---------- */
export function Sidebar() {
const pathname = usePathname();
const [expanded, setExpanded] = useState<Record<string, boolean>>({});
const [collapsed, setCollapsed] = useState(false);
const toggle = (label: string) =>
setExpanded((prev) => ({ ...prev, [label]: !prev[label] }));
return (
<aside className="w-64 bg-card border-r flex flex-col">
<div className="p-4 border-b">
<h1 className="text-xl font-bold">IT0 Admin</h1>
<p className="text-xs text-muted-foreground">Operations Console</p>
<SidebarContext.Provider value={{ collapsed, setCollapsed }}>
<aside
className={cn(
'bg-card/50 backdrop-blur-sm border-r flex flex-col transition-all duration-200 ease-in-out',
collapsed ? 'w-16' : 'w-60',
)}
>
{/* Logo area */}
<div className={cn('border-b flex items-center', collapsed ? 'px-3 py-4 justify-center' : 'px-4 py-4')}>
<div className="flex items-center gap-2.5 min-w-0">
<div className="w-8 h-8 rounded-lg bg-primary/15 flex items-center justify-center shrink-0">
<span className="text-primary font-bold text-sm">IT</span>
</div>
<nav className="flex-1 p-2 space-y-0.5 overflow-y-auto">
{!collapsed && (
<div className="min-w-0">
<h1 className="text-sm font-semibold tracking-tight truncate">IT0 Admin</h1>
<p className="text-[10px] text-muted-foreground leading-none mt-0.5">Operations Console</p>
</div>
)}
</div>
</div>
{/* Navigation */}
<nav className="flex-1 py-2 px-2 space-y-0.5 overflow-y-auto overflow-x-hidden">
{navItems.map((item) => {
const hasChildren = item.children && item.children.length > 0;
const groupActive = isGroupActive(pathname, item);
const isOpen = expanded[item.label] ?? groupActive;
/* ---- Collapsed: icon-only with tooltip ---- */
if (collapsed) {
const linkTarget = hasChildren
? item.children![0].href
: item.href;
return (
<Tooltip key={item.label} label={item.label}>
<Link
href={linkTarget}
className={cn(
'flex items-center justify-center h-9 w-full rounded-md transition-colors',
groupActive
? 'bg-primary/10 text-primary'
: 'text-muted-foreground hover:bg-accent hover:text-foreground',
)}
>
{item.icon}
</Link>
</Tooltip>
);
}
/* ---- Expanded: no children ---- */
if (!hasChildren) {
return (
<Link
key={item.href}
href={item.href}
className={cn(
'flex items-center gap-3 px-3 py-2 rounded-md text-sm transition-colors',
'flex items-center gap-2.5 px-2.5 py-2 rounded-md text-[13px] transition-colors',
isActive(pathname, item.href)
? 'bg-primary/10 text-primary'
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground',
? 'bg-primary/10 text-primary font-medium'
: 'text-muted-foreground hover:bg-accent hover:text-foreground',
)}
>
<span>{item.label}</span>
{item.icon}
<span className="truncate">{item.label}</span>
</Link>
);
}
/* ---- Expanded: with children ---- */
return (
<div key={item.label}>
<button
onClick={() => toggle(item.label)}
className={cn(
'flex items-center justify-between w-full px-3 py-2 rounded-md text-sm transition-colors',
'flex items-center gap-2.5 w-full px-2.5 py-2 rounded-md text-[13px] transition-colors',
groupActive
? 'text-primary'
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground',
: 'text-muted-foreground hover:bg-accent hover:text-foreground',
)}
>
<span>{item.label}</span>
<svg
{item.icon}
<span className="flex-1 text-left truncate">{item.label}</span>
<ChevronRight
className={cn(
'w-4 h-4 transition-transform',
'h-3.5 w-3.5 shrink-0 transition-transform duration-200',
isOpen && 'rotate-90',
)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
</svg>
/>
</button>
{isOpen && (
<div className="ml-4 mt-0.5 space-y-0.5">
<div className="ml-[18px] mt-0.5 space-y-0.5 border-l border-border/50 pl-2.5">
{item.children!.map((child) => (
<Link
key={child.href}
href={child.href}
className={cn(
'flex items-center gap-2 px-3 py-1.5 rounded-md text-xs transition-colors',
'flex items-center px-2.5 py-1.5 rounded-md text-xs transition-colors',
isActive(pathname, child.href)
? 'bg-primary/10 text-primary font-medium'
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground',
: 'text-muted-foreground hover:bg-accent hover:text-foreground',
)}
>
<span>{child.label}</span>
<span className="truncate">{child.label}</span>
</Link>
))}
</div>
@ -155,6 +257,28 @@ export function Sidebar() {
);
})}
</nav>
{/* Collapse toggle at bottom */}
<div className="border-t p-2">
<button
onClick={() => setCollapsed(!collapsed)}
className={cn(
'flex items-center gap-2.5 w-full rounded-md text-xs text-muted-foreground',
'hover:bg-accent hover:text-foreground transition-colors',
collapsed ? 'justify-center h-9' : 'px-2.5 py-2',
)}
>
{collapsed ? (
<PanelLeft className="h-4 w-4 shrink-0" />
) : (
<>
<PanelLeftClose className="h-4 w-4 shrink-0" />
<span>Collapse</span>
</>
)}
</button>
</div>
</aside>
</SidebarContext.Provider>
);
}

View File

@ -1,26 +1,49 @@
'use client';
import { useTenantStore } from '@/stores/zustand/tenant-store';
import { Search, Bell } from 'lucide-react';
export function TopBar() {
const { currentTenant } = useTenantStore();
return (
<header className="h-14 border-b bg-card flex items-center justify-between px-6">
<div className="flex items-center gap-4">
<kbd className="px-2 py-1 text-xs bg-muted rounded border text-muted-foreground">
Cmd+K
<header className="h-12 border-b bg-card/50 backdrop-blur-sm flex items-center justify-between px-4 shrink-0">
{/* Left: search / command palette */}
<button
className="flex items-center gap-2 px-3 py-1.5 rounded-md border border-border/60
text-muted-foreground hover:text-foreground hover:border-border
transition-colors text-xs w-56"
onClick={() => {
/* TODO: open command palette */
}}
>
<Search className="h-3.5 w-3.5 shrink-0" />
<span className="flex-1 text-left">Search...</span>
<kbd className="text-[10px] bg-muted/60 px-1.5 py-0.5 rounded border border-border/40 font-mono">
K
</kbd>
<span className="text-sm text-muted-foreground">Command Palette</span>
</div>
<div className="flex items-center gap-4">
<div className="text-sm">
</button>
{/* Right: tenant + user */}
<div className="flex items-center gap-3">
{/* Notification bell */}
<button className="relative p-1.5 rounded-md text-muted-foreground hover:text-foreground hover:bg-accent transition-colors">
<Bell className="h-4 w-4" />
</button>
{/* Divider */}
<div className="h-5 w-px bg-border" />
{/* Tenant indicator */}
<div className="text-xs">
<span className="text-muted-foreground">Tenant: </span>
<span className="font-medium">{currentTenant?.name || 'Not selected'}</span>
</div>
<div className="w-8 h-8 rounded-full bg-primary/20 flex items-center justify-center text-sm font-medium">
{/* User avatar */}
<button className="w-7 h-7 rounded-full bg-primary/15 border border-primary/20 flex items-center justify-center text-xs font-semibold text-primary hover:bg-primary/25 transition-colors">
A
</div>
</button>
</div>
</header>
);

View File

@ -4,23 +4,25 @@
@layer base {
:root {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--primary: 217.2 91.2% 59.8%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--background: 224 71% 4%;
--foreground: 213 31% 91%;
--card: 224 71% 4%;
--card-foreground: 213 31% 91%;
--popover: 224 71% 4%;
--popover-foreground: 213 31% 91%;
--primary: 210 100% 52%;
--primary-foreground: 0 0% 100%;
--secondary: 222 47% 11%;
--secondary-foreground: 213 31% 91%;
--muted: 223 47% 11%;
--muted-foreground: 215.4 16.3% 56.9%;
--accent: 216 34% 17%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive: 0 63% 31%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 224.3 76.3% 48%;
--border: 216 34% 17%;
--input: 216 34% 17%;
--ring: 210 100% 52%;
--radius: 0.5rem;
}
}
@ -30,6 +32,27 @@
@apply border-border;
}
body {
@apply bg-background text-foreground;
@apply bg-background text-foreground antialiased;
}
}
/* Custom scrollbar */
@layer utilities {
.scrollbar-thin {
scrollbar-width: thin;
scrollbar-color: hsl(var(--border)) transparent;
}
.scrollbar-thin::-webkit-scrollbar {
width: 6px;
}
.scrollbar-thin::-webkit-scrollbar-track {
background: transparent;
}
.scrollbar-thin::-webkit-scrollbar-thumb {
background-color: hsl(var(--border));
border-radius: 3px;
}
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
background-color: hsl(var(--muted-foreground) / 0.5);
}
}

View File

@ -34,6 +34,10 @@ const config: Config = {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))',
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))',
},
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',