feat(ui): replace sidebar with compact top header navigation

- Rewrite MainLayout from vertical sidebar to horizontal header bar
- Brand left, nav tabs center, status indicator right
- Simplify StatusBar to single inline layout (remove collapsed variant)
- Widen content area from max-w-4xl to max-w-6xl
- Remove sidebar-collapsed localStorage persistence
This commit is contained in:
Gemini Nexus Bot committed 2026-05-02 00:27:36 -03:00
1 parent 0753bbdce6
commit 7d2977b665
3 files changed
+94 -253

No files matched your search

+26 -96
View File
@@ -3,15 +3,9 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { isProcessRunning, startGeminiNexus, closeGeminiNexus } from '@/actions/process'; import { isProcessRunning, startGeminiNexus, closeGeminiNexus } from '@/actions/process';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { Play, Square, Loader2, Power } from 'lucide-react'; import { Play, Square, Loader2 } from 'lucide-react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; export const StatusBar: React.FC = () => {
interface StatusBarProps {
isCollapsed?: boolean;
}
export const StatusBar: React.FC<StatusBarProps> = ({ isCollapsed = false }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -44,112 +38,48 @@ export const StatusBar: React.FC<StatusBarProps> = ({ isCollapsed = false }) =>
}; };
const isPending = startMutation.isPending || stopMutation.isPending; const isPending = startMutation.isPending || stopMutation.isPending;
// Collapsed View
if (isCollapsed) {
return (
<div className="flex justify-center py-2">
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger asChild>
<button
onClick={handleToggle}
disabled={isLoading || isPending}
className={cn(
'flex h-8 w-8 items-center justify-center rounded-lg transition-all duration-200',
'disabled:opacity-50 disabled:cursor-not-allowed',
isRunning
? 'bg-emerald-500/10 text-emerald-400 hover:bg-emerald-500/20'
: 'bg-white/[0.04] text-muted-foreground hover:bg-white/[0.08] hover:text-foreground',
)}
>
{isPending ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Power className="h-4 w-4" />
)}
</button>
</TooltipTrigger>
<TooltipContent side="right" className="flex items-center gap-2 text-xs">
<div
className={cn(
'h-1.5 w-1.5 rounded-full',
isRunning ? 'bg-emerald-400' : 'bg-muted-foreground',
)}
/>
<span>
{isLoading
? t('status.checking')
: isRunning
? t('status.running')
: t('status.stopped')}
</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
);
}
// Expanded View
return ( return (
<div <div className="flex items-center gap-2 rounded-md border border-white/[0.06] bg-white/[0.02] px-3 py-2">
className={cn( {isLoading ? (
'flex items-center justify-between rounded-lg px-3 py-2 transition-all duration-200', <Loader2 className="text-muted-foreground h-3 w-3 animate-spin" />
isRunning ) : (
? 'bg-emerald-500/[0.08]' <div
: 'bg-white/[0.02]', className={cn(
'h-2 w-2 rounded-full',
isRunning
? 'bg-emerald-400 shadow-[0_0_6px_rgba(16,185,129,0.4)]'
: 'bg-muted-foreground/50',
)}
/>
)} )}
> <span
<div className="flex items-center gap-2.5"> className={cn(
{isLoading ? ( 'text-xs font-medium',
<Loader2 className="h-3 w-3 shrink-0 animate-spin text-muted-foreground" /> isRunning ? 'text-emerald-400' : 'text-muted-foreground',
) : (
<div
className={cn(
'h-2 w-2 shrink-0 rounded-full transition-colors duration-300',
isRunning
? 'bg-emerald-400 shadow-[0_0_6px_rgba(16,185,129,0.4)]'
: 'bg-muted-foreground/50',
)}
/>
)} )}
<div className="flex flex-col"> >
<span className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground"> {isLoading ? '...' : isRunning ? 'Running' : 'Stopped'}
Status </span>
</span>
<span className={cn(
'text-xs font-medium leading-tight',
isRunning ? 'text-emerald-400' : 'text-muted-foreground',
)}>
{isLoading
? t('status.checking')
: isRunning
? t('status.running')
: t('status.stopped')}
</span>
</div>
</div>
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={handleToggle} onClick={handleToggle}
disabled={isLoading || isPending} disabled={isLoading || isPending}
className={cn( className={cn(
'h-7 gap-1.5 rounded-md px-2.5 text-xs', 'h-7 w-7 rounded-md p-0',
isRunning isRunning
? 'text-emerald-400 hover:bg-emerald-500/10' ? 'text-emerald-400 hover:bg-emerald-500/10'
: 'text-muted-foreground hover:bg-white/[0.06]', : 'text-muted-foreground hover:bg-white/[0.06]',
)} )}
> >
{isPending ? ( {isPending ? (
<Loader2 className="h-3 w-3 animate-spin" /> <Loader2 className="h-3.5 w-3.5 animate-spin" />
) : isRunning ? ( ) : isRunning ? (
<Square className="h-3 w-3 fill-current" /> <Square className="h-3.5 w-3.5 fill-current" />
) : ( ) : (
<Play className="h-3 w-3 fill-current" /> <Play className="h-3.5 w-3.5 fill-current" />
)} )}
<span>{isRunning ? t('action.stop') : t('action.start')}</span> <span className="sr-only">{isRunning ? t('action.stop') : t('action.start')}</span>
</Button> </Button>
</div> </div>
); );
+67 -156
View File
@@ -1,19 +1,10 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useRef } from 'react';
import { Link, Outlet, useLocation } from '@tanstack/react-router'; import { Link, Outlet, useLocation } from '@tanstack/react-router';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { StatusBar } from '@/components/StatusBar'; import { StatusBar } from '@/components/StatusBar';
import { import { LayoutDashboard, Settings, Network, RefreshCw, BarChart3 } from 'lucide-react';
LayoutDashboard,
Settings,
Network,
ChevronLeft,
ChevronRight,
RefreshCw,
BarChart3,
} from 'lucide-react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { ErrorBoundary } from 'react-error-boundary'; import { ErrorBoundary } from 'react-error-boundary';
import { useToast } from '@/components/ui/use-toast'; import { useToast } from '@/components/ui/use-toast';
import { getLocalizedErrorMessage } from '@/utils/errorMessages'; import { getLocalizedErrorMessage } from '@/utils/errorMessages';
@@ -25,17 +16,6 @@ export const MainLayout: React.FC = () => {
const { toast } = useToast(); const { toast } = useToast();
const hasShownRouteErrorToastRef = useRef(false); const hasShownRouteErrorToastRef = useRef(false);
// Initialize state from localStorage if available, default to false (expanded)
const [isCollapsed, setIsCollapsed] = useState(() => {
const saved = localStorage.getItem('sidebar-collapsed');
return saved ? JSON.parse(saved) : false;
});
// Persist state changes
useEffect(() => {
localStorage.setItem('sidebar-collapsed', JSON.stringify(isCollapsed));
}, [isCollapsed]);
const navItems = [ const navItems = [
{ {
to: '/', to: '/',
@@ -61,146 +41,77 @@ export const MainLayout: React.FC = () => {
return ( return (
<div className="bg-background text-foreground flex h-screen flex-col overflow-hidden"> <div className="bg-background text-foreground flex h-screen flex-col overflow-hidden">
<div className="flex flex-1 overflow-hidden"> <header className="flex h-12 items-center border-b border-white/[0.06] px-4">
{/* Sidebar */} <div className="flex shrink-0 items-center gap-2.5">
<aside <div className="flex h-7 w-7 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-transparent">
className={cn( <img src={iconPng} alt="Gemini Nexus Logo" className="h-full w-full object-contain" />
'relative flex flex-col border-r border-white/[0.06] transition-all duration-300 ease-[cubic-bezier(0.4,0,0.2,1)]', </div>
isCollapsed ? 'w-[60px]' : 'w-56', <h1 className="text-[15px] font-semibold tracking-tight whitespace-nowrap">
)} Gemini Nexus
> </h1>
{/* Collapse toggle */} </div>
<button
className={cn(
'absolute -right-3 top-7 z-10 flex h-6 w-6 items-center justify-center rounded-full',
'bg-muted border border-white/[0.1] text-muted-foreground',
'opacity-0 transition-all duration-200 hover:bg-white/[0.1] hover:text-foreground',
'group-hover:opacity-100',
)}
style={{ opacity: 0 }}
onMouseEnter={(e) => (e.currentTarget.style.opacity = '1')}
onMouseLeave={(e) => (e.currentTarget.style.opacity = '0')}
onClick={() => setIsCollapsed(!isCollapsed)}
>
{isCollapsed ? (
<ChevronRight className="h-3 w-3" />
) : (
<ChevronLeft className="h-3 w-3" />
)}
</button>
{/* Brand */} <nav className="mx-6 flex flex-1 items-center justify-center gap-1 overflow-x-auto">
<div className={cn('flex flex-col', isCollapsed ? 'items-center p-3' : 'px-4 py-5')}> {navItems.map((item) => {
<div className="flex items-center gap-2.5 overflow-hidden whitespace-nowrap"> const isActive = location.pathname === item.to;
<div className="flex h-7 w-7 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-transparent">
<img return (
src={iconPng} <Link
alt="Gemini Nexus Logo" key={item.to}
className="h-full w-full object-contain" to={item.to}
/>
</div>
<div
className={cn( className={cn(
'overflow-hidden transition-all duration-300', 'flex items-center gap-2 rounded-md px-3 py-2 text-[13px] font-medium whitespace-nowrap transition-colors',
isCollapsed ? 'w-0 opacity-0' : 'w-auto opacity-100', isActive
? 'text-foreground bg-white/[0.08]'
: 'text-muted-foreground hover:text-foreground hover:bg-white/[0.04]',
)} )}
> >
<h1 className="text-[15px] font-semibold tracking-tight">Gemini Nexus</h1> <item.icon className="h-[16px] w-[16px]" />
{item.label}
</Link>
);
})}
</nav>
<div className="shrink-0">
<StatusBar />
</div>
</header>
<main className="flex-1 overflow-auto">
<ErrorBoundary
resetKeys={[location.pathname]}
onReset={() => {
hasShownRouteErrorToastRef.current = false;
}}
onError={(error) => {
if (hasShownRouteErrorToastRef.current) {
return;
}
toast({
title: t('error.generic'),
description: getLocalizedErrorMessage(error, t),
variant: 'destructive',
});
hasShownRouteErrorToastRef.current = true;
}}
fallbackRender={({ resetErrorBoundary }) => (
<div className="mx-auto max-w-3xl p-6">
<div className="rounded-xl border border-dashed border-white/[0.06] p-8 text-center">
<div className="text-sm font-semibold">{t('error.generic')}</div>
<div className="text-muted-foreground mt-2 text-xs">{t('action.retry')}</div>
<Button className="mt-4" variant="outline" onClick={resetErrorBoundary}>
<RefreshCw className="mr-2 h-4 w-4" />
{t('action.retry')}
</Button>
</div> </div>
</div> </div>
</div> )}
>
{/* Navigation */} <Outlet />
<nav className="flex-1 space-y-1 px-2 pt-2"> </ErrorBoundary>
<TooltipProvider> </main>
{navItems.map((item) => {
const isActive = location.pathname === item.to;
if (isCollapsed) {
return (
<Tooltip key={item.to} delayDuration={0}>
<TooltipTrigger asChild>
<Link
to={item.to}
className={cn(
'mx-auto flex h-9 w-9 items-center justify-center rounded-lg transition-all duration-200',
isActive
? 'bg-white/[0.08] text-foreground'
: 'text-muted-foreground hover:bg-white/[0.04] hover:text-foreground',
)}
>
<item.icon className="h-[18px] w-[18px]" />
<span className="sr-only">{item.label}</span>
</Link>
</TooltipTrigger>
<TooltipContent side="right" className="text-xs">
{item.label}
</TooltipContent>
</Tooltip>
);
}
return (
<Link
key={item.to}
to={item.to}
className={cn(
'flex items-center gap-3 rounded-lg px-3 py-2 text-[13px] font-medium transition-all duration-200',
isActive
? 'bg-white/[0.08] text-foreground'
: 'text-muted-foreground hover:bg-white/[0.04] hover:text-foreground',
)}
>
<item.icon className="h-[18px] w-[18px]" />
{item.label}
</Link>
);
})}
</TooltipProvider>
</nav>
{/* Status bar */}
<div className="border-t border-white/[0.06] p-2">
<StatusBar isCollapsed={isCollapsed} />
</div>
</aside>
{/* Content Area */}
<main className="flex-1 overflow-auto transition-all duration-300">
<ErrorBoundary
resetKeys={[location.pathname]}
onReset={() => {
hasShownRouteErrorToastRef.current = false;
}}
onError={(error) => {
if (hasShownRouteErrorToastRef.current) {
return;
}
toast({
title: t('error.generic'),
description: getLocalizedErrorMessage(error, t),
variant: 'destructive',
});
hasShownRouteErrorToastRef.current = true;
}}
fallbackRender={({ resetErrorBoundary }) => (
<div className="mx-auto max-w-3xl p-6">
<div className="rounded-xl border border-white/[0.06] border-dashed p-8 text-center">
<div className="text-sm font-semibold">{t('error.generic')}</div>
<div className="text-muted-foreground mt-2 text-xs">{t('action.retry')}</div>
<Button className="mt-4" variant="outline" onClick={resetErrorBoundary}>
<RefreshCw className="mr-2 h-4 w-4" />
{t('action.retry')}
</Button>
</div>
</div>
)}
>
<Outlet />
</ErrorBoundary>
</main>
</div>
</div> </div>
); );
}; };
+1 -1
View File
@@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next';
function HomePage() { function HomePage() {
return ( return (
<div className="container mx-auto max-w-4xl p-6"> <div className="container mx-auto max-w-6xl p-6">
<CloudAccountList /> <CloudAccountList />
</div> </div>
); );