feat: implementar sidebar colapsavel profissional

Substitui navegacao por header/menu mobile por sidebar lateral colapsavel
com toggle, persistencia localStorage e responsividade automatica.

- Novo componente Layout.tsx com sidebar, tooltips e overlay mobile
- Estado colapsado persistido em localStorage (desktop)
- Colapsada por defeito em mobile com drawer animado
- Animacoes suaves via framer-motion (spring)
- Removida navegacao duplicada de App.tsx, Monitor.tsx e Financial.tsx
- Rotas envolvidas pelo Layout via React Router Outlet

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 14:58:06 +00:00
parent 8148eb47fe
commit a4271fd06a
5 changed files with 340 additions and 226 deletions

View File

@@ -22,11 +22,8 @@ import {
CheckCircle2,
Timer,
Sparkles,
LayoutDashboard,
Activity,
Target,
Menu,
X,
} from 'lucide-react'
// Types
@@ -422,7 +419,6 @@ function App() {
const [loading, setLoading] = useState(true)
const [refreshing, setRefreshing] = useState(false)
const [error, setError] = useState<string | null>(null)
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
const fetchData = useCallback(async () => {
setRefreshing(true)
@@ -459,7 +455,7 @@ function App() {
if (loading) {
return (
<div className="min-h-screen bg-mesh flex items-center justify-center">
<div className="flex items-center justify-center py-20">
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
@@ -481,7 +477,7 @@ function App() {
// Error state (Vulnerabilidade 3.2)
if (error) {
return (
<div className="min-h-screen bg-mesh flex items-center justify-center">
<div className="flex items-center justify-center py-20">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
@@ -510,93 +506,20 @@ function App() {
if (!data) return null
return (
<div className="min-h-screen bg-mesh">
<div className="bg-grid min-h-screen">
{/* Header */}
<header className="sticky top-0 z-50 border-b border-white/5 bg-[#0a0a0f]/90 backdrop-blur-2xl">
<div className="max-w-[1800px] mx-auto px-6 lg:px-8 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<motion.div
whileHover={{ scale: 1.05, rotate: 5 }}
className="w-11 h-11 rounded-xl bg-gradient-to-br from-brand-500 to-violet-600 flex items-center justify-center shadow-lg shadow-brand-500/30"
>
<Zap className="w-6 h-6 text-white" />
</motion.div>
<div>
<h1 className="text-xl font-bold text-white tracking-tight">Dashboard Descomplicar</h1>
<p className="text-xs text-zinc-500">Painel de Gestão</p>
</div>
</div>
<nav className="hidden md:flex items-center gap-1 bg-white/5 rounded-xl p-1">
<a href="#" className="px-4 py-2 rounded-lg bg-brand-500 text-white text-sm font-medium flex items-center gap-2">
<LayoutDashboard className="w-4 h-4" />
Dashboard
</a>
<a href="/monitor" className="px-4 py-2 rounded-lg text-zinc-400 hover:text-white hover:bg-white/10 text-sm font-medium transition-all flex items-center gap-2">
<Activity className="w-4 h-4" />
Monitor
</a>
<a href="/financial" className="px-4 py-2 rounded-lg text-zinc-400 hover:text-white hover:bg-white/10 text-sm font-medium transition-all flex items-center gap-2">
<CreditCard className="w-4 h-4" />
Financeiro
</a>
</nav>
<div className="flex items-center gap-3">
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
className="md:hidden p-2.5 rounded-xl bg-white/5 hover:bg-white/10 border border-white/10 transition-all"
>
{mobileMenuOpen ? <X className="w-5 h-5 text-zinc-400" /> : <Menu className="w-5 h-5 text-zinc-400" />}
</motion.button>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={fetchData}
disabled={refreshing}
className="p-2.5 rounded-xl bg-white/5 hover:bg-white/10 border border-white/10 transition-all"
>
<RefreshCw className={`w-5 h-5 text-zinc-400 ${refreshing ? 'animate-spin' : ''}`} />
</motion.button>
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-brand-500 to-violet-600 ring-2 ring-white/10" />
</div>
</div>
</div>
</header>
{/* Mobile Navigation */}
<AnimatePresence>
{mobileMenuOpen && (
<motion.nav
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="md:hidden border-b border-white/5 bg-[#0a0a0f]/95 backdrop-blur-2xl overflow-hidden"
<div className="max-w-[1800px] mx-auto px-6 lg:px-8 py-8">
{/* Header com refresh */}
<div className="flex items-center justify-between mb-2">
<div />
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={fetchData}
disabled={refreshing}
className="p-2.5 rounded-xl bg-white/5 hover:bg-white/10 border border-white/10 transition-all"
>
<div className="px-6 py-3 flex flex-col gap-1">
<a href="#" onClick={() => setMobileMenuOpen(false)} className="px-4 py-3 rounded-lg bg-brand-500 text-white text-sm font-medium flex items-center gap-3">
<LayoutDashboard className="w-4 h-4" />
Dashboard
</a>
<a href="/monitor" onClick={() => setMobileMenuOpen(false)} className="px-4 py-3 rounded-lg text-zinc-400 hover:text-white hover:bg-white/10 text-sm font-medium transition-all flex items-center gap-3">
<Activity className="w-4 h-4" />
Monitor
</a>
<a href="/financial" onClick={() => setMobileMenuOpen(false)} className="px-4 py-3 rounded-lg text-zinc-400 hover:text-white hover:bg-white/10 text-sm font-medium transition-all flex items-center gap-3">
<CreditCard className="w-4 h-4" />
Financeiro
</a>
</div>
</motion.nav>
)}
</AnimatePresence>
{/* Main Content */}
<main className="max-w-[1800px] mx-auto px-6 lg:px-8 py-8">
<RefreshCw className={`w-5 h-5 text-zinc-400 ${refreshing ? 'animate-spin' : ''}`} />
</motion.button>
</div>
<AnimatePresence mode="wait">
<motion.div
key="dashboard"
@@ -880,24 +803,18 @@ function App() {
</div>
</motion.div>
</AnimatePresence>
</main>
{/* Footer */}
<footer className="border-t border-white/5 mt-12">
<div className="max-w-[1800px] mx-auto px-6 lg:px-8 py-6">
<div className="flex items-center justify-between text-sm text-zinc-500">
{/* Footer */}
<footer className="border-t border-white/5 mt-12">
<div className="flex items-center justify-between text-sm text-zinc-500 py-6">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" />
<span>Dashboard Descomplicar v3.0</span>
<span className="text-zinc-700">·</span>
<span>Painel de Gestão</span>
</div>
<span>Actualizado: {new Date().toLocaleString('pt-PT')}</span>
</div>
</div>
</footer>
</div>
</div>
</footer>
</div>
)
}

269
src/components/Layout.tsx Normal file
View File

@@ -0,0 +1,269 @@
import { useState, useEffect } from 'react'
import { NavLink, Outlet } from 'react-router-dom'
import { motion, AnimatePresence } from 'framer-motion'
import {
LayoutDashboard,
Activity,
CreditCard,
Zap,
ChevronLeft,
ChevronRight,
Menu,
X,
} from 'lucide-react'
const SIDEBAR_KEY = 'dash-sidebar-collapsed'
const MOBILE_BREAKPOINT = 768
interface NavItem {
to: string
label: string
icon: React.ElementType
}
const NAV_ITEMS: NavItem[] = [
{ to: '/', label: 'Dashboard', icon: LayoutDashboard },
{ to: '/monitor', label: 'Monitor', icon: Activity },
{ to: '/financial', label: 'Financeiro', icon: CreditCard },
]
function useIsMobile() {
const [isMobile, setIsMobile] = useState(
typeof window !== 'undefined' ? window.innerWidth < MOBILE_BREAKPOINT : false
)
useEffect(() => {
const onResize = () => setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
window.addEventListener('resize', onResize)
return () => window.removeEventListener('resize', onResize)
}, [])
return isMobile
}
function getInitialCollapsed(isMobile: boolean): boolean {
if (isMobile) return true
try {
const stored = localStorage.getItem(SIDEBAR_KEY)
if (stored !== null) return stored === 'true'
} catch {
// localStorage indisponivel
}
return false
}
export default function Layout() {
const isMobile = useIsMobile()
const [collapsed, setCollapsed] = useState(() => getInitialCollapsed(isMobile))
const [mobileOpen, setMobileOpen] = useState(false)
// Colapsar automaticamente em mobile
useEffect(() => {
if (isMobile) {
setCollapsed(true)
setMobileOpen(false)
}
}, [isMobile])
// Persistir estado em localStorage (apenas desktop)
useEffect(() => {
if (!isMobile) {
try {
localStorage.setItem(SIDEBAR_KEY, String(collapsed))
} catch {
// localStorage indisponivel
}
}
}, [collapsed, isMobile])
const toggleSidebar = () => {
if (isMobile) {
setMobileOpen(prev => !prev)
} else {
setCollapsed(prev => !prev)
}
}
const closeMobileMenu = () => setMobileOpen(false)
const sidebarWidth = collapsed ? 72 : 240
return (
<div className="min-h-screen bg-mesh">
<div className="bg-grid min-h-screen flex">
{/* Overlay mobile */}
<AnimatePresence>
{isMobile && mobileOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="fixed inset-0 z-40 bg-black/60 backdrop-blur-sm"
onClick={closeMobileMenu}
/>
)}
</AnimatePresence>
{/* Sidebar */}
<AnimatePresence mode="wait">
{(!isMobile || mobileOpen) && (
<motion.aside
initial={isMobile ? { x: -240 } : false}
animate={{ x: 0 }}
exit={isMobile ? { x: -240 } : undefined}
transition={{ type: 'spring', stiffness: 400, damping: 35 }}
style={!isMobile ? { width: sidebarWidth } : undefined}
className={`
${isMobile
? 'fixed top-0 left-0 bottom-0 z-50 w-60'
: 'relative flex-shrink-0 z-30'
}
bg-[#0a0a0f]/95 backdrop-blur-2xl border-r border-white/5
flex flex-col transition-[width] duration-300 ease-in-out
`}
>
{/* Logo */}
<div className="flex items-center gap-3 px-4 py-5 border-b border-white/5 min-h-[72px]">
<motion.div
whileHover={{ scale: 1.05, rotate: 5 }}
className="w-10 h-10 rounded-xl bg-gradient-to-br from-brand-500 to-violet-600 flex items-center justify-center shadow-lg shadow-brand-500/30 flex-shrink-0"
>
<Zap className="w-5 h-5 text-white" />
</motion.div>
<AnimatePresence>
{(!collapsed || isMobile) && (
<motion.div
initial={{ opacity: 0, width: 0 }}
animate={{ opacity: 1, width: 'auto' }}
exit={{ opacity: 0, width: 0 }}
transition={{ duration: 0.2 }}
className="overflow-hidden whitespace-nowrap"
>
<h1 className="text-base font-bold text-white tracking-tight leading-tight">Descomplicar</h1>
<p className="text-[10px] text-zinc-500">Painel de Gestao</p>
</motion.div>
)}
</AnimatePresence>
{/* Fechar em mobile */}
{isMobile && (
<button
onClick={closeMobileMenu}
className="ml-auto p-1.5 rounded-lg hover:bg-white/10 transition-colors"
>
<X className="w-5 h-5 text-zinc-400" />
</button>
)}
</div>
{/* Navegacao */}
<nav className="flex-1 px-3 py-4 space-y-1">
{NAV_ITEMS.map((item) => (
<NavLink
key={item.to}
to={item.to}
end={item.to === '/'}
onClick={isMobile ? closeMobileMenu : undefined}
className={({ isActive }) => `
flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium
transition-all duration-200 group relative
${isActive
? 'bg-brand-500/15 text-white border border-brand-500/30'
: 'text-zinc-400 hover:text-white hover:bg-white/5 border border-transparent'
}
`}
>
{({ isActive }) => (
<>
<item.icon className={`w-5 h-5 flex-shrink-0 ${isActive ? 'text-brand-400' : 'text-zinc-500 group-hover:text-zinc-300'}`} />
<AnimatePresence>
{(!collapsed || isMobile) && (
<motion.span
initial={{ opacity: 0, width: 0 }}
animate={{ opacity: 1, width: 'auto' }}
exit={{ opacity: 0, width: 0 }}
transition={{ duration: 0.2 }}
className="overflow-hidden whitespace-nowrap"
>
{item.label}
</motion.span>
)}
</AnimatePresence>
{/* Tooltip quando colapsado (desktop) */}
{collapsed && !isMobile && (
<div className="absolute left-full ml-3 px-2.5 py-1.5 rounded-lg bg-zinc-800 text-white text-xs font-medium whitespace-nowrap opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 pointer-events-none shadow-xl border border-white/10 z-50">
{item.label}
</div>
)}
</>
)}
</NavLink>
))}
</nav>
{/* Toggle (apenas desktop) */}
{!isMobile && (
<div className="px-3 py-4 border-t border-white/5">
<button
onClick={toggleSidebar}
className="w-full flex items-center justify-center gap-2 px-3 py-2 rounded-xl bg-white/5 hover:bg-white/10 border border-white/10 transition-all text-zinc-400 hover:text-white"
title={collapsed ? 'Expandir sidebar' : 'Colapsar sidebar'}
>
{collapsed
? <ChevronRight className="w-4 h-4" />
: <ChevronLeft className="w-4 h-4" />
}
<AnimatePresence>
{!collapsed && (
<motion.span
initial={{ opacity: 0, width: 0 }}
animate={{ opacity: 1, width: 'auto' }}
exit={{ opacity: 0, width: 0 }}
transition={{ duration: 0.2 }}
className="text-xs font-medium overflow-hidden whitespace-nowrap"
>
Colapsar
</motion.span>
)}
</AnimatePresence>
</button>
</div>
)}
</motion.aside>
)}
</AnimatePresence>
{/* Conteudo principal */}
<div className="flex-1 min-w-0 flex flex-col">
{/* Header mobile com botao de menu */}
{isMobile && (
<header className="sticky top-0 z-30 border-b border-white/5 bg-[#0a0a0f]/90 backdrop-blur-2xl">
<div className="flex items-center gap-3 px-4 py-3">
<button
onClick={toggleSidebar}
className="p-2 rounded-xl bg-white/5 hover:bg-white/10 border border-white/10 transition-all"
>
<Menu className="w-5 h-5 text-zinc-400" />
</button>
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-brand-500 to-violet-600 flex items-center justify-center shadow-lg shadow-brand-500/20">
<Zap className="w-4 h-4 text-white" />
</div>
<span className="text-sm font-bold text-white">Descomplicar</span>
</div>
</div>
</header>
)}
{/* Outlet para as paginas */}
<main className="flex-1">
<Outlet />
</main>
</div>
</div>
</div>
)
}

View File

@@ -6,6 +6,7 @@ import './index.css'
import App from './App.tsx'
import Monitor from './pages/Monitor.tsx'
import Financial from './pages/Financial.tsx'
import Layout from './components/Layout.tsx'
import { oidcConfig } from './auth/config.ts'
import { AuthWrapper } from './auth/AuthWrapper.tsx'
@@ -15,10 +16,12 @@ createRoot(document.getElementById('root')!).render(
<BrowserRouter>
<AuthWrapper>
<Routes>
<Route path="/" element={<App />} />
<Route path="/monitor" element={<Monitor />} />
<Route path="/financial" element={<Financial />} />
<Route path="/callback" element={<App />} />
<Route element={<Layout />}>
<Route path="/" element={<App />} />
<Route path="/monitor" element={<Monitor />} />
<Route path="/financial" element={<Financial />} />
<Route path="/callback" element={<App />} />
</Route>
</Routes>
</AuthWrapper>
</BrowserRouter>

View File

@@ -1,20 +1,15 @@
import { useState, useEffect, useCallback } from 'react'
import { motion } from 'framer-motion'
import { Link } from 'react-router-dom'
import {
BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend,
} from 'recharts'
import {
Zap,
RefreshCw,
ArrowLeft,
TrendingUp,
TrendingDown,
DollarSign,
Receipt,
PiggyBank,
Activity,
LayoutDashboard,
} from 'lucide-react'
interface FinancialData {
@@ -105,7 +100,7 @@ export default function Financial() {
if (loading) {
return (
<div className="min-h-screen bg-mesh flex items-center justify-center">
<div className="flex items-center justify-center py-20">
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="text-center">
<DollarSign className="w-12 h-12 text-brand-400 mx-auto mb-4 animate-pulse" />
<p className="text-zinc-400">A carregar dados financeiros...</p>
@@ -127,59 +122,28 @@ export default function Financial() {
}))
return (
<div className="min-h-screen bg-mesh">
<div className="bg-grid min-h-screen">
{/* Header */}
<header className="sticky top-0 z-50 border-b border-white/5 bg-[#0a0a0f]/90 backdrop-blur-2xl">
<div className="max-w-[1600px] mx-auto px-6 lg:px-8 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<motion.div
whileHover={{ scale: 1.05 }}
className="w-11 h-11 rounded-xl bg-gradient-to-br from-brand-500 to-violet-600 flex items-center justify-center shadow-lg shadow-brand-500/30"
>
<Zap className="w-6 h-6 text-white" />
</motion.div>
<div>
<h1 className="text-xl font-bold text-white tracking-tight">Financeiro</h1>
<p className="text-xs text-zinc-500">Vendas e Despesas {new Date().getFullYear()}</p>
</div>
</div>
<div className="flex items-center gap-4">
<span className={`px-4 py-2 rounded-full text-sm font-semibold ${lucroColor}`}>
{lucroLabel}: {formatEUR(Math.abs(data.lucro_ano))}
</span>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={fetchData}
disabled={refreshing}
className="p-2.5 rounded-xl bg-white/5 hover:bg-white/10 border border-white/10 transition-all"
>
<RefreshCw className={`w-5 h-5 text-zinc-400 ${refreshing ? 'animate-spin' : ''}`} />
</motion.button>
<Link
to="/monitor"
className="hidden md:flex items-center gap-2 px-4 py-2 rounded-xl bg-white/5 hover:bg-white/10 text-zinc-400 hover:text-white transition-all text-sm"
>
<Activity className="w-4 h-4" />
Monitor
</Link>
<Link
to="/"
className="flex items-center gap-2 px-4 py-2 rounded-xl bg-white/5 hover:bg-white/10 text-zinc-400 hover:text-white transition-all text-sm"
>
<ArrowLeft className="w-4 h-4" />
<span className="hidden md:inline">Dashboard</span>
</Link>
</div>
<div className="max-w-[1600px] mx-auto px-6 lg:px-8 py-8">
{/* Header com resumo e refresh */}
<div className="flex items-center justify-between mb-6">
<div>
<h2 className="text-xl font-bold text-white tracking-tight">Financeiro</h2>
<p className="text-xs text-zinc-500">Vendas e Despesas {new Date().getFullYear()}</p>
</div>
<div className="flex items-center gap-3">
<span className={`px-4 py-2 rounded-full text-sm font-semibold ${lucroColor}`}>
{lucroLabel}: {formatEUR(Math.abs(data.lucro_ano))}
</span>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={fetchData}
disabled={refreshing}
className="p-2.5 rounded-xl bg-white/5 hover:bg-white/10 border border-white/10 transition-all"
>
<RefreshCw className={`w-5 h-5 text-zinc-400 ${refreshing ? 'animate-spin' : ''}`} />
</motion.button>
</div>
</div>
</header>
{/* Main Content */}
<main className="max-w-[1600px] mx-auto px-6 lg:px-8 py-8">
<motion.div variants={containerVariants} initial="hidden" animate="show">
{/* Summary Cards */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-5 mb-8">
@@ -280,8 +244,6 @@ export default function Financial() {
</motion.div>
</div>
</motion.div>
</main>
</div>
</div>
</div>
)
}

View File

@@ -1,6 +1,5 @@
import { useState, useEffect, useCallback } from 'react'
import { motion } from 'framer-motion'
import { Link } from 'react-router-dom'
import {
Server,
Wifi,
@@ -11,11 +10,8 @@ import {
RefreshCw,
CheckCircle2,
AlertTriangle,
Zap,
ArrowLeft,
Activity,
Clock,
TrendingUp,
Wrench,
Code,
Network,
@@ -363,7 +359,7 @@ export default function Monitor() {
if (loading) {
return (
<div className="min-h-screen bg-mesh flex items-center justify-center">
<div className="flex items-center justify-center py-20">
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="text-center">
<Activity className="w-12 h-12 text-brand-400 mx-auto mb-4 animate-pulse" />
<p className="text-zinc-400">A carregar monitorizacao...</p>
@@ -392,59 +388,28 @@ export default function Monitor() {
}[data.overall]
return (
<div className="min-h-screen bg-mesh">
<div className="bg-grid min-h-screen">
{/* Header */}
<header className="sticky top-0 z-50 border-b border-white/5 bg-[#0a0a0f]/90 backdrop-blur-2xl">
<div className="max-w-[1600px] mx-auto px-6 lg:px-8 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<motion.div
whileHover={{ scale: 1.05 }}
className="w-11 h-11 rounded-xl bg-gradient-to-br from-brand-500 to-violet-600 flex items-center justify-center shadow-lg shadow-brand-500/30"
>
<Zap className="w-6 h-6 text-white" />
</motion.div>
<div>
<h1 className="text-xl font-bold text-white tracking-tight">Monitorizacao</h1>
<p className="text-xs text-zinc-500">Cluster Proxmox Descomplicar</p>
</div>
</div>
<div className="flex items-center gap-4">
<span className={`px-4 py-2 rounded-full text-sm font-semibold ${overallColor}`}>
{overallLabel}
</span>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={fetchData}
disabled={refreshing}
className="p-2.5 rounded-xl bg-white/5 hover:bg-white/10 border border-white/10 transition-all"
>
<RefreshCw className={`w-5 h-5 text-zinc-400 ${refreshing ? 'animate-spin' : ''}`} />
</motion.button>
<Link
to="/financial"
className="hidden md:flex items-center gap-2 px-4 py-2 rounded-xl bg-white/5 hover:bg-white/10 text-zinc-400 hover:text-white transition-all text-sm"
>
<TrendingUp className="w-4 h-4" />
Financeiro
</Link>
<Link
to="/"
className="flex items-center gap-2 px-4 py-2 rounded-xl bg-white/5 hover:bg-white/10 text-zinc-400 hover:text-white transition-all text-sm"
>
<ArrowLeft className="w-4 h-4" />
Dashboard
</Link>
</div>
<div className="max-w-[1600px] mx-auto px-6 lg:px-8 py-8">
{/* Header com status e refresh */}
<div className="flex items-center justify-between mb-6">
<div>
<h2 className="text-xl font-bold text-white tracking-tight">Monitorizacao</h2>
<p className="text-xs text-zinc-500">Cluster Proxmox Descomplicar</p>
</div>
<div className="flex items-center gap-3">
<span className={`px-4 py-2 rounded-full text-sm font-semibold ${overallColor}`}>
{overallLabel}
</span>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={fetchData}
disabled={refreshing}
className="p-2.5 rounded-xl bg-white/5 hover:bg-white/10 border border-white/10 transition-all"
>
<RefreshCw className={`w-5 h-5 text-zinc-400 ${refreshing ? 'animate-spin' : ''}`} />
</motion.button>
</div>
</div>
</header>
{/* Main Content */}
<main className="max-w-[1600px] mx-auto px-6 lg:px-8 py-8">
<motion.div variants={containerVariants} initial="hidden" animate="show">
{/* Section 1: Cluster Overview */}
@@ -661,9 +626,7 @@ export default function Monitor() {
<span>Auto-refresh: 60s</span>
</div>
</motion.div>
</main>
</div>
</div>
</div>
)
}