fix: make layout fully responsive for any screen size
Root cause: viewport was hardcoded to width=3840 and body to 3840x2160px. If TV browser has different viewport, content overflows and can't be zoomed to fit. Changes: - viewport meta: width=device-width instead of width=3840 - body: 100vw/100vh instead of fixed pixels - App container: w-screen h-screen - font-size: clamp(14px, 1.15vw, 24px) scales with viewport - Topology: horizontal chain (Modem → FW → Switch) saves vertical space, VM pills in 3-col grid, all sizes relative - Kanban: 3-col grid, compact project cards - All padding/gaps use rem (scale with base font) - Removed all hardcoded pixel max-widths Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=3840, initial-scale=1.0" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>TV Dashboard</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -53,7 +53,7 @@ function App() {
|
||||
const connected = !topology.error && !tasks.error && !calendar.error;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-[3840px] h-[2160px]">
|
||||
<div className="flex flex-col w-screen h-screen">
|
||||
<Header viewName={VIEW_NAMES[activeView]} connected={connected} />
|
||||
<ViewRotator activeView={activeView}>
|
||||
<div className="h-full">
|
||||
|
||||
@@ -28,73 +28,57 @@ export function CalendarView({ events }: CalendarViewProps) {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Summary bar */}
|
||||
<div className="flex items-center gap-8 px-16 py-4 bg-bg-secondary border-b border-border">
|
||||
<span className="text-lg text-text-secondary">
|
||||
<span className="font-bold text-text-primary">{events.length}</span> eventos esta
|
||||
semana
|
||||
<div className="flex items-center gap-6 px-8 py-2 bg-bg-secondary border-b border-border shrink-0">
|
||||
<span className="text-base text-text-secondary">
|
||||
<span className="font-bold text-text-primary">{events.length}</span> eventos esta semana
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto px-16 py-8">
|
||||
<div className="flex-1 overflow-y-auto px-8 py-6">
|
||||
{!hasEvents ? (
|
||||
<div className="flex flex-col items-center justify-center h-full gap-6 -mt-16">
|
||||
<span className="text-8xl opacity-30">📅</span>
|
||||
<p className="text-3xl font-semibold text-text-secondary">
|
||||
<div className="flex flex-col items-center justify-center h-full gap-4">
|
||||
<span className="text-6xl opacity-30">📅</span>
|
||||
<p className="text-2xl font-semibold text-text-secondary">
|
||||
Sin eventos programados
|
||||
</p>
|
||||
<p className="text-xl text-text-muted">
|
||||
<p className="text-lg text-text-muted">
|
||||
Los próximos eventos del calendario de Odoo aparecerán aquí
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-3 gap-10 h-full">
|
||||
{/* Today */}
|
||||
<section className="flex flex-col">
|
||||
<h2 className="text-2xl font-bold text-text-primary mb-2">Hoy</h2>
|
||||
<p className="text-base text-text-muted capitalize mb-6">{formatDate(today)}</p>
|
||||
<div className="grid grid-cols-3 gap-8 h-full">
|
||||
<section>
|
||||
<h2 className="text-xl font-bold text-text-primary mb-1">Hoy</h2>
|
||||
<p className="text-sm text-text-muted capitalize mb-4">{formatDate(today)}</p>
|
||||
{todayEvents.length === 0 ? (
|
||||
<p className="text-lg text-text-secondary">Sin eventos</p>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{todayEvents.map((e) => (
|
||||
<EventCard key={e.id} event={e} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Tomorrow */}
|
||||
<section className="flex flex-col">
|
||||
<h2 className="text-2xl font-bold text-text-primary mb-2">Mañana</h2>
|
||||
<p className="text-base text-text-muted capitalize mb-6">
|
||||
{formatDate(tomorrow)}
|
||||
</p>
|
||||
{tomorrowEvents.length === 0 ? (
|
||||
<p className="text-lg text-text-secondary">Sin eventos</p>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{tomorrowEvents.map((e) => (
|
||||
<EventCard key={e.id} event={e} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* This week */}
|
||||
<section className="flex flex-col">
|
||||
<h2 className="text-2xl font-bold text-text-primary mb-2">Esta semana</h2>
|
||||
<p className="text-base text-text-muted mb-6">Próximos días</p>
|
||||
{laterEvents.length === 0 ? (
|
||||
<p className="text-lg text-text-secondary">Sin eventos</p>
|
||||
<p className="text-base text-text-secondary">Sin eventos</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{todayEvents.map((e) => <EventCard key={e.id} event={e} />)}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
<section>
|
||||
<h2 className="text-xl font-bold text-text-primary mb-1">Mañana</h2>
|
||||
<p className="text-sm text-text-muted capitalize mb-4">{formatDate(tomorrow)}</p>
|
||||
{tomorrowEvents.length === 0 ? (
|
||||
<p className="text-base text-text-secondary">Sin eventos</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{tomorrowEvents.map((e) => <EventCard key={e.id} event={e} />)}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
<section>
|
||||
<h2 className="text-xl font-bold text-text-primary mb-1">Esta semana</h2>
|
||||
<p className="text-sm text-text-muted mb-4">Próximos días</p>
|
||||
{laterEvents.length === 0 ? (
|
||||
<p className="text-base text-text-secondary">Sin eventos</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{laterEvents.map((e) => (
|
||||
<div
|
||||
key={e.id}
|
||||
className="flex gap-4 text-base bg-bg-card rounded-lg px-4 py-3 border border-border"
|
||||
>
|
||||
<span className="font-mono text-text-muted min-w-[160px] capitalize">
|
||||
<div key={e.id} className="flex gap-3 text-sm bg-bg-card rounded-lg px-3 py-2 border border-border">
|
||||
<span className="font-mono text-text-muted min-w-[10rem] capitalize">
|
||||
{formatDate(e.start)}
|
||||
</span>
|
||||
<span className="text-text-primary truncate">{e.name}</span>
|
||||
|
||||
@@ -27,22 +27,22 @@ export function Header({ viewName, connected }: HeaderProps) {
|
||||
});
|
||||
|
||||
return (
|
||||
<header className="flex items-center justify-between px-16 py-5 bg-bg-secondary border-b border-border">
|
||||
<div className="flex items-center gap-6">
|
||||
<h1 className="text-3xl font-bold tracking-tight text-text-primary">
|
||||
<header className="flex items-center justify-between px-8 py-3 bg-bg-secondary border-b border-border shrink-0">
|
||||
<div className="flex items-center gap-4">
|
||||
<h1 className="text-2xl font-bold tracking-tight text-text-primary">
|
||||
Consultoria AS
|
||||
</h1>
|
||||
<span className="text-xl text-text-muted">|</span>
|
||||
<span className="text-xl font-medium text-accent">{viewName}</span>
|
||||
<span className="text-text-muted">|</span>
|
||||
<span className="text-lg font-medium text-accent">{viewName}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-10">
|
||||
<span className="text-lg text-text-secondary capitalize">{dateStr}</span>
|
||||
<span className="text-3xl font-mono font-bold tabular-nums">{timeStr}</span>
|
||||
<div className="flex items-center gap-6">
|
||||
<span className="text-base text-text-secondary capitalize">{dateStr}</span>
|
||||
<span className="text-2xl font-mono font-bold tabular-nums">{timeStr}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={`w-3 h-3 rounded-full ${connected ? "bg-success animate-pulse" : "bg-danger"}`}
|
||||
/>
|
||||
<span className="text-sm text-text-secondary">
|
||||
<span className="text-xs text-text-secondary">
|
||||
{connected ? "Conectado" : "Sin conexión"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -22,45 +22,41 @@ function ProjectRow({ project }: { project: Project }) {
|
||||
if (totalTasks === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="bg-bg-card border border-border rounded-2xl p-7 flex flex-col gap-4">
|
||||
{/* Project header */}
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<h3 className="text-2xl font-bold text-text-primary leading-snug">
|
||||
<div className="bg-bg-card border border-border rounded-xl p-4 flex flex-col gap-2.5">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<h3 className="text-lg font-bold text-text-primary leading-snug">
|
||||
{project.name}
|
||||
</h3>
|
||||
<span className="text-lg font-mono text-text-secondary shrink-0">
|
||||
{totalTasks} tareas
|
||||
<span className="text-sm font-mono text-text-secondary shrink-0">
|
||||
{totalTasks}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Progress bar */}
|
||||
<div className="flex h-5 rounded-full overflow-hidden bg-bg-primary">
|
||||
<div className="flex h-3 rounded-full overflow-hidden bg-bg-primary">
|
||||
{stages.map(([stageName, tasks], i) => {
|
||||
const pct = (tasks.length / totalTasks) * 100;
|
||||
if (pct < 1) return null;
|
||||
return (
|
||||
<div
|
||||
key={stageName}
|
||||
className={`${STAGE_COLORS[i % STAGE_COLORS.length]} transition-all`}
|
||||
className={`${STAGE_COLORS[i % STAGE_COLORS.length]}`}
|
||||
style={{ width: `${pct}%` }}
|
||||
title={`${stageName}: ${tasks.length}`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Stage chips */}
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{stages.map(([stageName, tasks], i) => (
|
||||
<span
|
||||
key={stageName}
|
||||
className="inline-flex items-center gap-2 text-base text-text-secondary bg-bg-secondary rounded-lg px-4 py-2"
|
||||
className="inline-flex items-center gap-1.5 text-sm text-text-secondary bg-bg-secondary rounded-md px-2.5 py-1"
|
||||
>
|
||||
<span
|
||||
className={`w-3 h-3 rounded-full shrink-0 ${STAGE_COLORS[i % STAGE_COLORS.length]}`}
|
||||
className={`w-2 h-2 rounded-full shrink-0 ${STAGE_COLORS[i % STAGE_COLORS.length]}`}
|
||||
/>
|
||||
<span className="leading-tight">{stageName}</span>
|
||||
<span className="font-mono font-bold text-text-primary text-lg">{tasks.length}</span>
|
||||
<span className="truncate max-w-[12rem]">{stageName}</span>
|
||||
<span className="font-mono font-bold text-text-primary">{tasks.length}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
@@ -81,21 +77,18 @@ export function KanbanBoard({ projects }: KanbanBoardProps) {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Summary bar */}
|
||||
<div className="flex items-center gap-10 px-16 py-5 bg-bg-secondary border-b border-border">
|
||||
<span className="text-xl text-text-secondary">
|
||||
<div className="flex items-center gap-6 px-8 py-2 bg-bg-secondary border-b border-border shrink-0">
|
||||
<span className="text-base text-text-secondary">
|
||||
<span className="font-bold text-text-primary">{activeProjects.length}</span> proyectos
|
||||
activos
|
||||
</span>
|
||||
<span className="text-border-light text-2xl">|</span>
|
||||
<span className="text-xl text-text-secondary">
|
||||
<span className="font-bold text-text-primary">{totalTasks}</span> tareas totales
|
||||
<span className="text-border-light">|</span>
|
||||
<span className="text-base text-text-secondary">
|
||||
<span className="font-bold text-text-primary">{totalTasks}</span> tareas
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Projects grid */}
|
||||
<div className="flex-1 overflow-y-auto px-16 py-8">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div className="flex-1 overflow-y-auto px-8 py-4">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{activeProjects.map((project) => (
|
||||
<ProjectRow key={project.id} project={project} />
|
||||
))}
|
||||
|
||||
@@ -17,16 +17,15 @@ function VmPill({ node }: { node: NetworkNode }) {
|
||||
const dotColor = isUp ? "bg-success" : "bg-danger";
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 bg-bg-card border border-border rounded-lg px-3 py-2">
|
||||
<span className={`w-2.5 h-2.5 rounded-full ${dotColor} shrink-0`} />
|
||||
<span className="text-base font-medium text-text-primary truncate">{node.name}</span>
|
||||
<span className="text-sm font-mono text-text-muted">{node.ip.split(".").pop()}</span>
|
||||
<div className="flex items-center gap-1.5 bg-bg-card border border-border rounded-md px-2 py-1">
|
||||
<span className={`w-2 h-2 rounded-full ${dotColor} shrink-0`} />
|
||||
<span className="text-sm font-medium text-text-primary truncate">{node.name}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Full node card (infrastructure + proxmox) ───────────── */
|
||||
function InfraCard({ node, isCenter }: { node: NetworkNode; isCenter?: boolean }) {
|
||||
/* ── Infrastructure card ─────────────────────────────────── */
|
||||
function InfraCard({ node }: { node: NetworkNode }) {
|
||||
const [showPass, setShowPass] = useState(false);
|
||||
const isUp = node.status === "up";
|
||||
const borderColor = isUp ? "border-success/40" : "border-danger/40";
|
||||
@@ -35,56 +34,42 @@ function InfraCard({ node, isCenter }: { node: NetworkNode; isCenter?: boolean }
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${bgColor} border-2 ${borderColor} rounded-2xl px-6 py-5 cursor-pointer transition-all hover:brightness-110 ${isCenter ? "min-w-[280px]" : ""}`}
|
||||
className={`${bgColor} border ${borderColor} rounded-xl px-4 py-3 cursor-pointer transition-all hover:brightness-110`}
|
||||
onClick={() => setShowPass((p) => !p)}
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-1">
|
||||
<span className="text-3xl">{ICON_MAP[node.icon] || "📦"}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-2xl">{ICON_MAP[node.icon] || "📦"}</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-xl font-bold text-text-primary leading-tight">{node.name}</p>
|
||||
<p className="text-base font-mono text-text-secondary">{node.ip}</p>
|
||||
<p className="text-lg font-bold text-text-primary leading-tight">{node.name}</p>
|
||||
<p className="text-sm font-mono text-text-secondary">{node.ip}</p>
|
||||
</div>
|
||||
<span className={`w-4 h-4 rounded-full ${dotColor} shrink-0`} />
|
||||
<span className={`w-3 h-3 rounded-full ${dotColor} shrink-0`} />
|
||||
</div>
|
||||
{node.username && (
|
||||
<p className="text-sm text-text-secondary mt-1">
|
||||
{showPass && node.username && (
|
||||
<p className="text-xs text-text-secondary mt-1">
|
||||
{node.username}
|
||||
{node.password && (
|
||||
<span className="ml-1 font-mono text-warning">
|
||||
{showPass ? ` / ${node.password}` : " / ••••••"}
|
||||
</span>
|
||||
)}
|
||||
{node.password && <span className="font-mono text-warning"> / {node.password}</span>}
|
||||
</p>
|
||||
)}
|
||||
{node.public_url && (
|
||||
<p className="text-sm text-accent mt-0.5">{node.public_url}</p>
|
||||
{showPass && node.public_url && (
|
||||
<p className="text-xs text-accent mt-0.5">{node.public_url}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Proxmox server with its VMs ─────────────────────────── */
|
||||
/* ── Proxmox server column ───────────────────────────────── */
|
||||
function ProxmoxColumn({ server, vms }: { server: NetworkNode; vms: NetworkNode[] }) {
|
||||
const vmCount = vms.length;
|
||||
const upCount = vms.filter((v) => v.status === "up").length;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-0">
|
||||
{/* Server card */}
|
||||
<div className="flex flex-col items-center">
|
||||
<InfraCard node={server} />
|
||||
|
||||
{/* Vertical connector */}
|
||||
<div className="w-0.5 h-6 bg-border-light" />
|
||||
|
||||
{/* VM count badge */}
|
||||
<div className="flex items-center gap-2 bg-bg-secondary border border-border rounded-full px-4 py-1.5 mb-2">
|
||||
<span className="text-sm text-text-muted">
|
||||
{upCount}/{vmCount} VMs
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* VM grid */}
|
||||
<div className="grid grid-cols-2 gap-2 max-w-[520px]">
|
||||
<div className="w-px h-4 bg-border-light" />
|
||||
<span className="text-xs text-text-muted mb-2">
|
||||
{upCount}/{vms.length} activos
|
||||
</span>
|
||||
<div className="grid grid-cols-3 gap-1.5 w-full">
|
||||
{vms.map((vm) => (
|
||||
<VmPill key={vm.ip + vm.name} node={vm} />
|
||||
))}
|
||||
@@ -93,14 +78,13 @@ function ProxmoxColumn({ server, vms }: { server: NetworkNode; vms: NetworkNode[
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Vertical connector line ─────────────────────────────── */
|
||||
/* ── Vertical line ───────────────────────────────────────── */
|
||||
function VLine() {
|
||||
return <div className="w-0.5 h-8 bg-border-light mx-auto" />;
|
||||
return <div className="w-px h-5 bg-border-light mx-auto" />;
|
||||
}
|
||||
|
||||
/* ── Main topology graph ─────────────────────────────────── */
|
||||
/* ── Main topology ───────────────────────────────────────── */
|
||||
export function NetworkGraph({ nodes }: NetworkGraphProps) {
|
||||
// Find nodes by role
|
||||
const findByName = (name: string) => nodes.find((n) => n.name === name);
|
||||
const modem = findByName("Router Telmex");
|
||||
const firewall = findByName("Firewall OPNsense");
|
||||
@@ -115,7 +99,6 @@ export function NetworkGraph({ nodes }: NetworkGraphProps) {
|
||||
n.name !== "Switch Cisco"
|
||||
);
|
||||
|
||||
// Group VMs by parent
|
||||
const vmsByParent = new Map<string, NetworkNode[]>();
|
||||
for (const node of nodes) {
|
||||
if ((node.type === "vm" || node.type === "ct") && node.parent) {
|
||||
@@ -131,23 +114,22 @@ export function NetworkGraph({ nodes }: NetworkGraphProps) {
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Summary bar */}
|
||||
<div className="flex items-center gap-10 px-16 py-4 bg-bg-secondary border-b border-border">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="w-4 h-4 rounded-full bg-success" />
|
||||
<span className="text-xl text-text-secondary">
|
||||
<div className="flex items-center gap-6 px-8 py-2 bg-bg-secondary border-b border-border">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-3 h-3 rounded-full bg-success" />
|
||||
<span className="text-base text-text-secondary">
|
||||
<span className="font-bold text-text-primary">{onlineCount}</span> online
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="w-4 h-4 rounded-full bg-danger" />
|
||||
<span className="text-xl text-text-secondary">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-3 h-3 rounded-full bg-danger" />
|
||||
<span className="text-base text-text-secondary">
|
||||
<span className="font-bold text-text-primary">{total - onlineCount}</span> offline
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-border-light text-2xl">|</span>
|
||||
<span className="text-xl text-text-secondary">
|
||||
<span className="font-bold text-text-primary">{proxmoxServers.length}</span> Proxmox
|
||||
<span className="mx-2">·</span>
|
||||
<span className="text-border-light">|</span>
|
||||
<span className="text-base text-text-secondary">
|
||||
<span className="font-bold text-text-primary">{proxmoxServers.length}</span> Proxmox ·{" "}
|
||||
<span className="font-bold text-text-primary">
|
||||
{nodes.filter((n) => n.type === "vm" || n.type === "ct").length}
|
||||
</span>{" "}
|
||||
@@ -155,65 +137,48 @@ export function NetworkGraph({ nodes }: NetworkGraphProps) {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Diagram area */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div className="flex flex-col items-center py-8 px-8">
|
||||
{/* Diagram */}
|
||||
<div className="flex-1 overflow-y-auto flex flex-col items-center px-6 py-4">
|
||||
{/* Top chain: Modem → Firewall → Switch */}
|
||||
<div className="flex items-center gap-3">
|
||||
{modem && <InfraCard node={modem} />}
|
||||
<span className="text-border-light text-xl">→</span>
|
||||
{firewall && <InfraCard node={firewall} />}
|
||||
<span className="text-border-light text-xl">→</span>
|
||||
{switchNode && <InfraCard node={switchNode} />}
|
||||
</div>
|
||||
|
||||
{/* ── Level 1: Modem ── */}
|
||||
{modem && <InfraCard node={modem} isCenter />}
|
||||
<VLine />
|
||||
<VLine />
|
||||
|
||||
{/* ── Level 2: Firewall ── */}
|
||||
{firewall && <InfraCard node={firewall} isCenter />}
|
||||
<VLine />
|
||||
{/* Horizontal branch line */}
|
||||
<div className="w-3/4 h-px bg-border-light" />
|
||||
|
||||
{/* ── Level 3: Switch ── */}
|
||||
{switchNode && <InfraCard node={switchNode} isCenter />}
|
||||
<VLine />
|
||||
|
||||
{/* ── Branch lines to Proxmox servers ── */}
|
||||
<div className="flex items-start justify-center w-full">
|
||||
{/* Horizontal bar spanning all columns */}
|
||||
<div className="relative flex justify-center" style={{ width: "100%", maxWidth: "3600px" }}>
|
||||
{/* Horizontal line */}
|
||||
<div
|
||||
className="absolute top-0 h-0.5 bg-border-light"
|
||||
style={{
|
||||
left: `${100 / (proxmoxServers.length * 2)}%`,
|
||||
right: `${100 / (proxmoxServers.length * 2)}%`,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Proxmox columns */}
|
||||
<div className="flex justify-center gap-8 w-full pt-0">
|
||||
{proxmoxServers.map((server) => {
|
||||
const vms = vmsByParent.get(server.name) || [];
|
||||
return (
|
||||
<div key={server.ip} className="flex-1 flex flex-col items-center max-w-[600px]">
|
||||
{/* Vertical connector from horizontal bar */}
|
||||
<div className="w-0.5 h-8 bg-border-light" />
|
||||
<ProxmoxColumn server={server} vms={vms} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{/* Proxmox columns */}
|
||||
<div className="flex gap-6 w-full px-4 mt-0">
|
||||
{proxmoxServers.map((server) => {
|
||||
const vms = vmsByParent.get(server.name) || [];
|
||||
return (
|
||||
<div key={server.ip} className="flex-1 flex flex-col items-center">
|
||||
<div className="w-px h-4 bg-border-light" />
|
||||
<ProxmoxColumn server={server} vms={vms} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Other devices */}
|
||||
{otherDevices.length > 0 && (
|
||||
<div className="mt-4 w-full">
|
||||
<p className="text-xs font-bold text-text-muted uppercase tracking-widest mb-2 text-center">
|
||||
Otros dispositivos
|
||||
</p>
|
||||
<div className="flex justify-center gap-3 flex-wrap">
|
||||
{otherDevices.map((node) => (
|
||||
<InfraCard key={node.ip} node={node} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Other devices ── */}
|
||||
{otherDevices.length > 0 && (
|
||||
<div className="mt-8 w-full max-w-[3600px]">
|
||||
<h3 className="text-base font-bold text-text-muted uppercase tracking-widest mb-4 text-center">
|
||||
Otros dispositivos conectados al switch
|
||||
</h3>
|
||||
<div className="flex justify-center gap-4 flex-wrap">
|
||||
{otherDevices.map((node) => (
|
||||
<InfraCard key={node.ip} node={node} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 24px;
|
||||
font-size: clamp(14px, 1.15vw, 24px);
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -30,11 +30,10 @@ body {
|
||||
font-family: "Inter", system-ui, -apple-system, sans-serif;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
width: 3840px;
|
||||
height: 2160px;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* Smooth scrollbar for overflow areas */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user