fix: enlarge cards and show full info for TV readability
NodeCard: - Icon 4xl, name text-xl bold, IP text-lg mono - Show credentials and URL always (no hidden sections) - Password hidden with click-to-reveal (dots → text) - Status dot 4x4, rounded-2xl cards, more padding - Grid columns min 520px (was 320px) KanbanBoard: - Grid 2 columns (was 3) for bigger project cards - Name text-2xl, progress bar h-5 (was h-3) - Stage chips text-base with larger count (was text-xs) - More padding and gaps throughout Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -22,19 +22,19 @@ function ProjectRow({ project }: { project: Project }) {
|
|||||||
if (totalTasks === 0) return null;
|
if (totalTasks === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-bg-card border border-border rounded-xl p-5 flex flex-col gap-3">
|
<div className="bg-bg-card border border-border rounded-2xl p-7 flex flex-col gap-4">
|
||||||
{/* Project header */}
|
{/* Project header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between gap-4">
|
||||||
<h3 className="text-lg font-bold text-text-primary truncate pr-4">
|
<h3 className="text-2xl font-bold text-text-primary leading-snug">
|
||||||
{project.name}
|
{project.name}
|
||||||
</h3>
|
</h3>
|
||||||
<span className="text-sm font-mono text-text-muted shrink-0">
|
<span className="text-lg font-mono text-text-secondary shrink-0">
|
||||||
{totalTasks} tareas
|
{totalTasks} tareas
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Progress bar */}
|
{/* Progress bar */}
|
||||||
<div className="flex h-3 rounded-full overflow-hidden bg-bg-primary">
|
<div className="flex h-5 rounded-full overflow-hidden bg-bg-primary">
|
||||||
{stages.map(([stageName, tasks], i) => {
|
{stages.map(([stageName, tasks], i) => {
|
||||||
const pct = (tasks.length / totalTasks) * 100;
|
const pct = (tasks.length / totalTasks) * 100;
|
||||||
if (pct < 1) return null;
|
if (pct < 1) return null;
|
||||||
@@ -50,17 +50,17 @@ function ProjectRow({ project }: { project: Project }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stage chips */}
|
{/* Stage chips */}
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-3">
|
||||||
{stages.map(([stageName, tasks], i) => (
|
{stages.map(([stageName, tasks], i) => (
|
||||||
<span
|
<span
|
||||||
key={stageName}
|
key={stageName}
|
||||||
className="inline-flex items-center gap-1.5 text-xs text-text-secondary bg-bg-secondary rounded-md px-2.5 py-1"
|
className="inline-flex items-center gap-2 text-base text-text-secondary bg-bg-secondary rounded-lg px-4 py-2"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={`w-2 h-2 rounded-full ${STAGE_COLORS[i % STAGE_COLORS.length]}`}
|
className={`w-3 h-3 rounded-full shrink-0 ${STAGE_COLORS[i % STAGE_COLORS.length]}`}
|
||||||
/>
|
/>
|
||||||
<span className="truncate max-w-[180px]">{stageName}</span>
|
<span className="leading-tight">{stageName}</span>
|
||||||
<span className="font-mono font-bold text-text-primary">{tasks.length}</span>
|
<span className="font-mono font-bold text-text-primary text-lg">{tasks.length}</span>
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -82,20 +82,20 @@ export function KanbanBoard({ projects }: KanbanBoardProps) {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
{/* Summary bar */}
|
{/* Summary bar */}
|
||||||
<div className="flex items-center gap-8 px-16 py-4 bg-bg-secondary border-b border-border">
|
<div className="flex items-center gap-10 px-16 py-5 bg-bg-secondary border-b border-border">
|
||||||
<span className="text-lg text-text-secondary">
|
<span className="text-xl text-text-secondary">
|
||||||
<span className="font-bold text-text-primary">{activeProjects.length}</span> proyectos
|
<span className="font-bold text-text-primary">{activeProjects.length}</span> proyectos
|
||||||
activos
|
activos
|
||||||
</span>
|
</span>
|
||||||
<span className="text-text-muted">|</span>
|
<span className="text-border-light text-2xl">|</span>
|
||||||
<span className="text-lg text-text-secondary">
|
<span className="text-xl text-text-secondary">
|
||||||
<span className="font-bold text-text-primary">{totalTasks}</span> tareas totales
|
<span className="font-bold text-text-primary">{totalTasks}</span> tareas totales
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Projects grid */}
|
{/* Projects grid */}
|
||||||
<div className="flex-1 overflow-y-auto px-16 py-6">
|
<div className="flex-1 overflow-y-auto px-16 py-8">
|
||||||
<div className="grid grid-cols-3 gap-4">
|
<div className="grid grid-cols-2 gap-6">
|
||||||
{activeProjects.map((project) => (
|
{activeProjects.map((project) => (
|
||||||
<ProjectRow key={project.id} project={project} />
|
<ProjectRow key={project.id} project={project} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ function categorizeNodes(nodes: NetworkNode[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Any remaining nodes
|
|
||||||
const remaining = nodes.filter((n) => !assigned.has(n.ip));
|
const remaining = nodes.filter((n) => !assigned.has(n.ip));
|
||||||
if (remaining.length > 0) {
|
if (remaining.length > 0) {
|
||||||
categorized.push({ label: "Otros", nodes: remaining });
|
categorized.push({ label: "Otros", nodes: remaining });
|
||||||
@@ -45,37 +44,37 @@ export function NetworkGraph({ nodes }: NetworkGraphProps) {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
{/* Summary bar */}
|
{/* Summary bar */}
|
||||||
<div className="flex items-center gap-8 px-16 py-4 bg-bg-secondary border-b border-border">
|
<div className="flex items-center gap-10 px-16 py-5 bg-bg-secondary border-b border-border">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span className="w-3 h-3 rounded-full bg-success" />
|
<span className="w-4 h-4 rounded-full bg-success" />
|
||||||
<span className="text-lg text-text-secondary">
|
<span className="text-xl text-text-secondary">
|
||||||
<span className="font-bold text-text-primary">{onlineCount}</span> online
|
<span className="font-bold text-text-primary">{onlineCount}</span> online
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span className="w-3 h-3 rounded-full bg-danger" />
|
<span className="w-4 h-4 rounded-full bg-danger" />
|
||||||
<span className="text-lg text-text-secondary">
|
<span className="text-xl text-text-secondary">
|
||||||
<span className="font-bold text-text-primary">{total - onlineCount}</span> offline
|
<span className="font-bold text-text-primary">{total - onlineCount}</span> offline
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-text-muted">|</span>
|
<span className="text-border-light text-2xl">|</span>
|
||||||
<span className="text-lg text-text-secondary">
|
<span className="text-xl text-text-secondary">
|
||||||
<span className="font-bold text-text-primary">{total}</span> dispositivos
|
<span className="font-bold text-text-primary">{total}</span> dispositivos
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Scrollable grid */}
|
{/* Scrollable grid */}
|
||||||
<div className="flex-1 overflow-y-auto px-16 py-6 space-y-6">
|
<div className="flex-1 overflow-y-auto px-16 py-8 space-y-8">
|
||||||
{categories.map((cat) => (
|
{categories.map((cat) => (
|
||||||
<section key={cat.label}>
|
<section key={cat.label}>
|
||||||
<h3 className="text-sm font-bold text-text-muted uppercase tracking-widest mb-3">
|
<h3 className="text-lg font-bold text-text-muted uppercase tracking-widest mb-4">
|
||||||
{cat.label}
|
{cat.label}
|
||||||
<span className="ml-2 text-text-muted font-normal">({cat.nodes.length})</span>
|
<span className="ml-3 font-normal">({cat.nodes.length})</span>
|
||||||
</h3>
|
</h3>
|
||||||
<div
|
<div
|
||||||
className="grid gap-3"
|
className="grid gap-4"
|
||||||
style={{
|
style={{
|
||||||
gridTemplateColumns: "repeat(auto-fill, minmax(320px, 1fr))",
|
gridTemplateColumns: "repeat(auto-fill, minmax(520px, 1fr))",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{cat.nodes.map((node) => (
|
{cat.nodes.map((node) => (
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ interface NodeCardProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function NodeCard({ node }: NodeCardProps) {
|
export function NodeCard({ node }: NodeCardProps) {
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
|
||||||
const isUp = node.status === "up";
|
const isUp = node.status === "up";
|
||||||
const statusBg = isUp ? "bg-success-dim" : "bg-danger-dim";
|
const statusBg = isUp ? "bg-success-dim" : "bg-danger-dim";
|
||||||
@@ -29,42 +29,45 @@ export function NodeCard({ node }: NodeCardProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`relative ${statusBg} border ${statusBorder} rounded-lg px-4 py-3 cursor-pointer transition-all duration-200 hover:bg-bg-card-hover`}
|
className={`${statusBg} border ${statusBorder} rounded-2xl px-6 py-5 cursor-pointer transition-all duration-200 hover:bg-bg-card-hover`}
|
||||||
onClick={() => setExpanded((prev) => !prev)}
|
onClick={() => setShowPassword((prev) => !prev)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
{/* Row 1: Icon + Name + Status */}
|
||||||
<span className={`w-2.5 h-2.5 rounded-full ${statusDot} shrink-0`} />
|
<div className="flex items-center gap-4 mb-2">
|
||||||
<span className="text-xl shrink-0">{ICON_MAP[node.icon] || "📦"}</span>
|
<span className="text-4xl shrink-0">{ICON_MAP[node.icon] || "📦"}</span>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-base font-semibold text-text-primary truncate leading-tight">
|
<p className="text-xl font-bold text-text-primary leading-snug">
|
||||||
{node.name}
|
{node.name}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm font-mono text-text-secondary leading-tight">{node.ip}</p>
|
|
||||||
</div>
|
</div>
|
||||||
{node.public_url && (
|
<span className={`w-4 h-4 rounded-full ${statusDot} shrink-0`} />
|
||||||
<span className="text-xs text-accent truncate max-w-[180px] shrink-0 hidden 2xl:block">
|
|
||||||
{node.public_url.replace(/^https?:\/\//, "")}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{expanded && (node.username || node.public_url) && (
|
{/* Row 2: IP */}
|
||||||
<div className="mt-2 pt-2 border-t border-border-light text-sm space-y-1">
|
<p className="text-lg font-mono text-text-secondary mb-1">{node.ip}</p>
|
||||||
|
|
||||||
|
{/* Row 3: Credentials */}
|
||||||
{node.username && (
|
{node.username && (
|
||||||
<p className="text-text-secondary">
|
<p className="text-base text-text-secondary">
|
||||||
<span className="text-text-muted">User:</span> {node.username}
|
<span className="text-text-muted">Usuario:</span>{" "}
|
||||||
|
<span className="text-text-primary">{node.username}</span>
|
||||||
{node.password && (
|
{node.password && (
|
||||||
<span className="ml-2">
|
<>
|
||||||
<span className="text-text-muted">Pass:</span>{" "}
|
{" · "}
|
||||||
<span className="font-mono text-warning">{node.password}</span>
|
<span className="text-text-muted">Clave:</span>{" "}
|
||||||
|
<span className="font-mono text-warning">
|
||||||
|
{showPassword ? node.password : "••••••"}
|
||||||
</span>
|
</span>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Row 4: Public URL */}
|
||||||
{node.public_url && (
|
{node.public_url && (
|
||||||
<p className="text-accent truncate">{node.public_url}</p>
|
<p className="text-base text-accent mt-1">
|
||||||
)}
|
{node.public_url}
|
||||||
</div>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user