fix: reduce font size and compact all views to fit screen
Base font: clamp(10px, 0.75vw, 16px) — much smaller scaling. At 1920px viewport = ~14px, at 3840px = 16px (capped). All components heavily compacted: - Header: minimal padding, smaller text - Topology: tight InfraCards, small VmPills, 3-col VM grid - Kanban: 3-col project grid, compact cards - Calendar: tighter spacing - Summary bars: single-line, text-xs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,76 +16,50 @@ export function CalendarView({ events }: CalendarViewProps) {
|
||||
(e) => !e.start.startsWith(today) && !e.start.startsWith(tomorrow)
|
||||
);
|
||||
|
||||
const formatDate = (dateStr: string) => {
|
||||
return new Date(dateStr).toLocaleDateString("es-MX", {
|
||||
weekday: "long",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
};
|
||||
|
||||
const hasEvents = events.length > 0;
|
||||
const fmt = (d: string) =>
|
||||
new Date(d).toLocaleDateString("es-MX", { weekday: "long", month: "long", day: "numeric" });
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<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">
|
||||
<div className="flex items-center gap-4 px-6 py-1.5 bg-bg-secondary border-b border-border shrink-0 text-xs">
|
||||
<span className="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-8 py-6">
|
||||
{!hasEvents ? (
|
||||
<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-lg text-text-muted">
|
||||
Los próximos eventos del calendario de Odoo aparecerán aquí
|
||||
</p>
|
||||
<div className="flex-1 overflow-y-auto px-6 py-4">
|
||||
{events.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full gap-3">
|
||||
<span className="text-5xl opacity-30">📅</span>
|
||||
<p className="text-xl font-semibold text-text-secondary">Sin eventos programados</p>
|
||||
<p className="text-base text-text-muted">Los eventos de Odoo aparecerán aquí</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-3 gap-8 h-full">
|
||||
<div className="grid grid-cols-3 gap-6 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-base text-text-secondary">Sin eventos</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{todayEvents.map((e) => <EventCard key={e.id} event={e} />)}
|
||||
</div>
|
||||
)}
|
||||
<h2 className="text-lg font-bold text-text-primary mb-1">Hoy</h2>
|
||||
<p className="text-xs text-text-muted capitalize mb-3">{fmt(today)}</p>
|
||||
{todayEvents.length === 0
|
||||
? <p className="text-sm text-text-secondary">Sin eventos</p>
|
||||
: <div className="space-y-2">{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>
|
||||
)}
|
||||
<h2 className="text-lg font-bold text-text-primary mb-1">Mañana</h2>
|
||||
<p className="text-xs text-text-muted capitalize mb-3">{fmt(tomorrow)}</p>
|
||||
{tomorrowEvents.length === 0
|
||||
? <p className="text-sm text-text-secondary">Sin eventos</p>
|
||||
: <div className="space-y-2">{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-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>
|
||||
<h2 className="text-lg font-bold text-text-primary mb-1">Esta semana</h2>
|
||||
<p className="text-xs text-text-muted mb-3">Próximos días</p>
|
||||
{laterEvents.length === 0
|
||||
? <p className="text-sm text-text-secondary">Sin eventos</p>
|
||||
: <div className="space-y-1.5">{laterEvents.map((e) => (
|
||||
<div key={e.id} className="flex gap-2 text-xs bg-bg-card rounded px-2 py-1.5 border border-border">
|
||||
<span className="font-mono text-text-muted capitalize">{fmt(e.start)}</span>
|
||||
<span className="text-text-primary truncate">{e.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
))}</div>}
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -27,25 +27,18 @@ export function Header({ viewName, connected }: HeaderProps) {
|
||||
});
|
||||
|
||||
return (
|
||||
<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>
|
||||
<header className="flex items-center justify-between px-6 py-2 bg-bg-secondary border-b border-border shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<h1 className="text-xl font-bold tracking-tight text-text-primary">Consultoria AS</h1>
|
||||
<span className="text-text-muted">|</span>
|
||||
<span className="text-lg font-medium text-accent">{viewName}</span>
|
||||
<span className="text-base font-medium text-accent">{viewName}</span>
|
||||
</div>
|
||||
<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">
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-sm text-text-secondary capitalize">{dateStr}</span>
|
||||
<span className="text-xl font-mono font-bold tabular-nums">{timeStr}</span>
|
||||
<span
|
||||
className={`w-3 h-3 rounded-full ${connected ? "bg-success animate-pulse" : "bg-danger"}`}
|
||||
className={`w-2.5 h-2.5 rounded-full ${connected ? "bg-success animate-pulse" : "bg-danger"}`}
|
||||
/>
|
||||
<span className="text-xs text-text-secondary">
|
||||
{connected ? "Conectado" : "Sin conexión"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
|
||||
@@ -5,57 +5,37 @@ interface KanbanBoardProps {
|
||||
}
|
||||
|
||||
const STAGE_COLORS = [
|
||||
"bg-blue-500",
|
||||
"bg-emerald-500",
|
||||
"bg-amber-500",
|
||||
"bg-purple-500",
|
||||
"bg-rose-500",
|
||||
"bg-cyan-500",
|
||||
"bg-orange-500",
|
||||
"bg-teal-500",
|
||||
"bg-blue-500", "bg-emerald-500", "bg-amber-500", "bg-purple-500",
|
||||
"bg-rose-500", "bg-cyan-500", "bg-orange-500", "bg-teal-500",
|
||||
];
|
||||
|
||||
function ProjectRow({ project }: { project: Project }) {
|
||||
const stages = Object.entries(project.stages);
|
||||
const totalTasks = stages.reduce((sum, [, tasks]) => sum + tasks.length, 0);
|
||||
|
||||
if (totalTasks === 0) return null;
|
||||
|
||||
return (
|
||||
<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">
|
||||
<div className="bg-bg-card border border-border rounded-lg p-3 flex flex-col gap-1.5">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<h3 className="text-sm font-bold text-text-primary leading-tight truncate">
|
||||
{project.name}
|
||||
</h3>
|
||||
<span className="text-sm font-mono text-text-secondary shrink-0">
|
||||
{totalTasks}
|
||||
</span>
|
||||
<span className="text-xs font-mono text-text-muted shrink-0">{totalTasks}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex h-3 rounded-full overflow-hidden bg-bg-primary">
|
||||
{stages.map(([stageName, tasks], i) => {
|
||||
<div className="flex h-2 rounded-full overflow-hidden bg-bg-primary">
|
||||
{stages.map(([name, tasks], i) => {
|
||||
const pct = (tasks.length / totalTasks) * 100;
|
||||
if (pct < 1) return null;
|
||||
return (
|
||||
<div
|
||||
key={stageName}
|
||||
className={`${STAGE_COLORS[i % STAGE_COLORS.length]}`}
|
||||
style={{ width: `${pct}%` }}
|
||||
/>
|
||||
<div key={name} className={STAGE_COLORS[i % STAGE_COLORS.length]} style={{ width: `${pct}%` }} />
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{stages.map(([stageName, tasks], i) => (
|
||||
<span
|
||||
key={stageName}
|
||||
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-2 h-2 rounded-full shrink-0 ${STAGE_COLORS[i % STAGE_COLORS.length]}`}
|
||||
/>
|
||||
<span className="truncate max-w-[12rem]">{stageName}</span>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{stages.map(([name, tasks], i) => (
|
||||
<span key={name} className="inline-flex items-center gap-1 text-xs text-text-secondary bg-bg-secondary rounded px-1.5 py-0.5">
|
||||
<span className={`w-1.5 h-1.5 rounded-full ${STAGE_COLORS[i % STAGE_COLORS.length]}`} />
|
||||
<span className="truncate max-w-[10rem]">{name}</span>
|
||||
<span className="font-mono font-bold text-text-primary">{tasks.length}</span>
|
||||
</span>
|
||||
))}
|
||||
@@ -65,33 +45,27 @@ function ProjectRow({ project }: { project: Project }) {
|
||||
}
|
||||
|
||||
export function KanbanBoard({ projects }: KanbanBoardProps) {
|
||||
const activeProjects = projects.filter((p) => {
|
||||
const total = Object.values(p.stages).reduce((s, t) => s + t.length, 0);
|
||||
return total > 0;
|
||||
});
|
||||
|
||||
const totalTasks = activeProjects.reduce(
|
||||
(sum, p) => sum + Object.values(p.stages).reduce((s, t) => s + t.length, 0),
|
||||
0
|
||||
const active = projects.filter((p) =>
|
||||
Object.values(p.stages).reduce((s, t) => s + t.length, 0) > 0
|
||||
);
|
||||
const total = active.reduce(
|
||||
(sum, p) => sum + Object.values(p.stages).reduce((s, t) => s + t.length, 0), 0
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<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
|
||||
<div className="flex items-center gap-4 px-6 py-1.5 bg-bg-secondary border-b border-border shrink-0 text-xs">
|
||||
<span className="text-text-secondary">
|
||||
<span className="font-bold text-text-primary">{active.length}</span> proyectos
|
||||
</span>
|
||||
<span className="text-border-light">|</span>
|
||||
<span className="text-base text-text-secondary">
|
||||
<span className="font-bold text-text-primary">{totalTasks}</span> tareas
|
||||
<span className="text-text-secondary">
|
||||
<span className="font-bold text-text-primary">{total}</span> tareas
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<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} />
|
||||
))}
|
||||
<div className="flex-1 overflow-y-auto px-6 py-3">
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{active.map((p) => <ProjectRow key={p.id} project={p} />)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,171 +11,137 @@ const ICON_MAP: Record<string, string> = {
|
||||
phone: "📞", camera: "📷", device: "📱",
|
||||
};
|
||||
|
||||
/* ── Compact VM pill ─────────────────────────────────────── */
|
||||
function VmPill({ node }: { node: NetworkNode }) {
|
||||
const isUp = node.status === "up";
|
||||
const dotColor = isUp ? "bg-success" : "bg-danger";
|
||||
|
||||
const dot = node.status === "up" ? "bg-success" : "bg-danger";
|
||||
return (
|
||||
<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 className="flex items-center gap-1 bg-bg-card/80 border border-border rounded px-1.5 py-0.5">
|
||||
<span className={`w-1.5 h-1.5 rounded-full ${dot} shrink-0`} />
|
||||
<span className="text-xs text-text-primary truncate">{node.name}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── 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";
|
||||
const bgColor = isUp ? "bg-success-dim" : "bg-danger-dim";
|
||||
const dotColor = isUp ? "bg-success" : "bg-danger";
|
||||
const border = isUp ? "border-success/30" : "border-danger/30";
|
||||
const bg = isUp ? "bg-success-dim" : "bg-danger-dim";
|
||||
const dot = isUp ? "bg-success" : "bg-danger";
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${bgColor} border ${borderColor} rounded-xl px-4 py-3 cursor-pointer transition-all hover:brightness-110`}
|
||||
className={`${bg} border ${border} rounded-lg px-3 py-2 cursor-pointer hover:brightness-110 transition-all`}
|
||||
onClick={() => setShowPass((p) => !p)}
|
||||
>
|
||||
<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-lg font-bold text-text-primary leading-tight">{node.name}</p>
|
||||
<p className="text-sm font-mono text-text-secondary">{node.ip}</p>
|
||||
<span className="text-lg">{ICON_MAP[node.icon] || "📦"}</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-bold text-text-primary leading-none">{node.name}</p>
|
||||
<p className="text-xs font-mono text-text-secondary leading-none mt-0.5">{node.ip}</p>
|
||||
</div>
|
||||
<span className={`w-3 h-3 rounded-full ${dotColor} shrink-0`} />
|
||||
<span className={`w-2.5 h-2.5 rounded-full ${dot} shrink-0`} />
|
||||
</div>
|
||||
{showPass && node.username && (
|
||||
<p className="text-xs text-text-secondary mt-1">
|
||||
<p className="text-xs text-text-muted mt-1">
|
||||
{node.username}
|
||||
{node.password && <span className="font-mono text-warning"> / {node.password}</span>}
|
||||
{node.password && <span className="text-warning font-mono"> / {node.password}</span>}
|
||||
</p>
|
||||
)}
|
||||
{showPass && node.public_url && (
|
||||
<p className="text-xs text-accent mt-0.5">{node.public_url}</p>
|
||||
<p className="text-xs text-accent mt-0.5 truncate">{node.public_url}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Proxmox server column ───────────────────────────────── */
|
||||
function ProxmoxColumn({ server, vms }: { server: NetworkNode; vms: NetworkNode[] }) {
|
||||
const upCount = vms.filter((v) => v.status === "up").length;
|
||||
|
||||
function ProxmoxCol({ server, vms }: { server: NetworkNode; vms: NetworkNode[] }) {
|
||||
const up = vms.filter((v) => v.status === "up").length;
|
||||
return (
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="flex flex-col items-center flex-1">
|
||||
<div className="w-px h-3 bg-border-light" />
|
||||
<InfraCard node={server} />
|
||||
<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} />
|
||||
))}
|
||||
<div className="w-px h-2 bg-border-light" />
|
||||
<span className="text-xs text-text-muted mb-1">{up}/{vms.length}</span>
|
||||
<div className="grid grid-cols-3 gap-1 w-full">
|
||||
{vms.map((vm) => <VmPill key={vm.ip + vm.name} node={vm} />)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Vertical line ───────────────────────────────────────── */
|
||||
function VLine() {
|
||||
return <div className="w-px h-5 bg-border-light mx-auto" />;
|
||||
}
|
||||
|
||||
/* ── Main topology ───────────────────────────────────────── */
|
||||
export function NetworkGraph({ nodes }: NetworkGraphProps) {
|
||||
const findByName = (name: string) => nodes.find((n) => n.name === name);
|
||||
const modem = findByName("Router Telmex");
|
||||
const firewall = findByName("Firewall OPNsense");
|
||||
const switchNode = findByName("Switch Cisco");
|
||||
const find = (name: string) => nodes.find((n) => n.name === name);
|
||||
const modem = find("Router Telmex");
|
||||
const firewall = find("Firewall OPNsense");
|
||||
const sw = find("Switch Cisco");
|
||||
|
||||
const proxmoxServers = nodes.filter((n) => n.type === "proxmox");
|
||||
const otherDevices = nodes.filter(
|
||||
(n) =>
|
||||
!n.type &&
|
||||
n.name !== "Router Telmex" &&
|
||||
n.name !== "Firewall OPNsense" &&
|
||||
n.name !== "Switch Cisco"
|
||||
const pve = nodes.filter((n) => n.type === "proxmox");
|
||||
const vms = new Map<string, NetworkNode[]>();
|
||||
for (const n of nodes) {
|
||||
if ((n.type === "vm" || n.type === "ct") && n.parent) {
|
||||
const list = vms.get(n.parent) || [];
|
||||
list.push(n);
|
||||
vms.set(n.parent, list);
|
||||
}
|
||||
}
|
||||
const other = nodes.filter(
|
||||
(n) => !n.type && !["Router Telmex", "Firewall OPNsense", "Switch Cisco"].includes(n.name)
|
||||
);
|
||||
|
||||
const vmsByParent = new Map<string, NetworkNode[]>();
|
||||
for (const node of nodes) {
|
||||
if ((node.type === "vm" || node.type === "ct") && node.parent) {
|
||||
const list = vmsByParent.get(node.parent) || [];
|
||||
list.push(node);
|
||||
vmsByParent.set(node.parent, list);
|
||||
}
|
||||
}
|
||||
|
||||
const onlineCount = nodes.filter((n) => n.status === "up").length;
|
||||
const total = nodes.length;
|
||||
const online = nodes.filter((n) => n.status === "up").length;
|
||||
const vmTotal = nodes.filter((n) => n.type === "vm" || n.type === "ct").length;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Summary bar */}
|
||||
<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
|
||||
{/* Stats */}
|
||||
<div className="flex items-center gap-4 px-6 py-1.5 bg-bg-secondary border-b border-border shrink-0 text-xs">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="w-2 h-2 rounded-full bg-success" />
|
||||
<span className="font-bold text-text-primary">{online}</span>
|
||||
<span className="text-text-secondary">online</span>
|
||||
</span>
|
||||
</div>
|
||||
<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 className="flex items-center gap-1.5">
|
||||
<span className="w-2 h-2 rounded-full bg-danger" />
|
||||
<span className="font-bold text-text-primary">{nodes.length - online}</span>
|
||||
<span className="text-text-secondary">offline</span>
|
||||
</span>
|
||||
</div>
|
||||
<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>{" "}
|
||||
VMs/CTs
|
||||
<span className="text-text-secondary">
|
||||
<span className="font-bold text-text-primary">{pve.length}</span> Proxmox ·{" "}
|
||||
<span className="font-bold text-text-primary">{vmTotal}</span> VMs/CTs
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 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">
|
||||
{/* Tree */}
|
||||
<div className="flex-1 flex flex-col items-center justify-start px-4 py-3 overflow-hidden">
|
||||
{/* Top chain */}
|
||||
<div className="flex items-center gap-2">
|
||||
{modem && <InfraCard node={modem} />}
|
||||
<span className="text-border-light text-xl">→</span>
|
||||
<span className="text-border-light text-base">→</span>
|
||||
{firewall && <InfraCard node={firewall} />}
|
||||
<span className="text-border-light text-xl">→</span>
|
||||
{switchNode && <InfraCard node={switchNode} />}
|
||||
<span className="text-border-light text-base">→</span>
|
||||
{sw && <InfraCard node={sw} />}
|
||||
</div>
|
||||
|
||||
<VLine />
|
||||
|
||||
{/* Horizontal branch line */}
|
||||
<div className="w-3/4 h-px bg-border-light" />
|
||||
{/* Branch down */}
|
||||
<div className="w-px h-3 bg-border-light" />
|
||||
<div className="h-px bg-border-light" style={{ width: "80%" }} />
|
||||
|
||||
{/* 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 className="flex gap-4 w-full">
|
||||
{pve.map((s) => (
|
||||
<ProxmoxCol key={s.ip} server={s} vms={vms.get(s.name) || []} />
|
||||
))}
|
||||
</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">
|
||||
{/* Others */}
|
||||
{other.length > 0 && (
|
||||
<div className="mt-3 w-full">
|
||||
<p className="text-xs text-text-muted uppercase tracking-wider text-center mb-1.5">
|
||||
Otros dispositivos
|
||||
</p>
|
||||
<div className="flex justify-center gap-3 flex-wrap">
|
||||
{otherDevices.map((node) => (
|
||||
<InfraCard key={node.ip} node={node} />
|
||||
))}
|
||||
<div className="flex justify-center gap-2 flex-wrap">
|
||||
{other.map((n) => <InfraCard key={n.ip} node={n} />)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: clamp(14px, 1.15vw, 24px);
|
||||
/* Scales with viewport: ~14px at 1920w, ~16px at 3840w, min 10px */
|
||||
font-size: clamp(10px, 0.75vw, 16px);
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -35,12 +36,12 @@ body {
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
width: 4px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--color-border);
|
||||
border-radius: 3px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user