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;
|
||||
|
||||
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 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-bold text-text-primary truncate pr-4">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<h3 className="text-2xl font-bold text-text-primary leading-snug">
|
||||
{project.name}
|
||||
</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
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 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) => {
|
||||
const pct = (tasks.length / totalTasks) * 100;
|
||||
if (pct < 1) return null;
|
||||
@@ -50,17 +50,17 @@ function ProjectRow({ project }: { project: Project }) {
|
||||
</div>
|
||||
|
||||
{/* Stage chips */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{stages.map(([stageName, tasks], i) => (
|
||||
<span
|
||||
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
|
||||
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="font-mono font-bold text-text-primary">{tasks.length}</span>
|
||||
<span className="leading-tight">{stageName}</span>
|
||||
<span className="font-mono font-bold text-text-primary text-lg">{tasks.length}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
@@ -82,20 +82,20 @@ export function KanbanBoard({ projects }: KanbanBoardProps) {
|
||||
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">
|
||||
<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">
|
||||
<span className="font-bold text-text-primary">{activeProjects.length}</span> proyectos
|
||||
activos
|
||||
</span>
|
||||
<span className="text-text-muted">|</span>
|
||||
<span className="text-lg text-text-secondary">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{/* Projects grid */}
|
||||
<div className="flex-1 overflow-y-auto px-16 py-6">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="flex-1 overflow-y-auto px-16 py-8">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
{activeProjects.map((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));
|
||||
if (remaining.length > 0) {
|
||||
categorized.push({ label: "Otros", nodes: remaining });
|
||||
@@ -45,37 +44,37 @@ export function NetworkGraph({ nodes }: NetworkGraphProps) {
|
||||
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">
|
||||
<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">
|
||||
<span className="w-3 h-3 rounded-full bg-success" />
|
||||
<span className="text-lg text-text-secondary">
|
||||
<span className="w-4 h-4 rounded-full bg-success" />
|
||||
<span className="text-xl 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-3 h-3 rounded-full bg-danger" />
|
||||
<span className="text-lg text-text-secondary">
|
||||
<span className="w-4 h-4 rounded-full bg-danger" />
|
||||
<span className="text-xl text-text-secondary">
|
||||
<span className="font-bold text-text-primary">{total - onlineCount}</span> offline
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-text-muted">|</span>
|
||||
<span className="text-lg text-text-secondary">
|
||||
<span className="text-border-light text-2xl">|</span>
|
||||
<span className="text-xl text-text-secondary">
|
||||
<span className="font-bold text-text-primary">{total}</span> dispositivos
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 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) => (
|
||||
<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}
|
||||
<span className="ml-2 text-text-muted font-normal">({cat.nodes.length})</span>
|
||||
<span className="ml-3 font-normal">({cat.nodes.length})</span>
|
||||
</h3>
|
||||
<div
|
||||
className="grid gap-3"
|
||||
className="grid gap-4"
|
||||
style={{
|
||||
gridTemplateColumns: "repeat(auto-fill, minmax(320px, 1fr))",
|
||||
gridTemplateColumns: "repeat(auto-fill, minmax(520px, 1fr))",
|
||||
}}
|
||||
>
|
||||
{cat.nodes.map((node) => (
|
||||
|
||||
@@ -20,7 +20,7 @@ interface NodeCardProps {
|
||||
}
|
||||
|
||||
export function NodeCard({ node }: NodeCardProps) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
const isUp = node.status === "up";
|
||||
const statusBg = isUp ? "bg-success-dim" : "bg-danger-dim";
|
||||
@@ -29,42 +29,45 @@ export function NodeCard({ node }: NodeCardProps) {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`relative ${statusBg} border ${statusBorder} rounded-lg px-4 py-3 cursor-pointer transition-all duration-200 hover:bg-bg-card-hover`}
|
||||
onClick={() => setExpanded((prev) => !prev)}
|
||||
className={`${statusBg} border ${statusBorder} rounded-2xl px-6 py-5 cursor-pointer transition-all duration-200 hover:bg-bg-card-hover`}
|
||||
onClick={() => setShowPassword((prev) => !prev)}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className={`w-2.5 h-2.5 rounded-full ${statusDot} shrink-0`} />
|
||||
<span className="text-xl shrink-0">{ICON_MAP[node.icon] || "📦"}</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-base font-semibold text-text-primary truncate leading-tight">
|
||||
{/* Row 1: Icon + Name + Status */}
|
||||
<div className="flex items-center gap-4 mb-2">
|
||||
<span className="text-4xl shrink-0">{ICON_MAP[node.icon] || "📦"}</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-xl font-bold text-text-primary leading-snug">
|
||||
{node.name}
|
||||
</p>
|
||||
<p className="text-sm font-mono text-text-secondary leading-tight">{node.ip}</p>
|
||||
</div>
|
||||
{node.public_url && (
|
||||
<span className="text-xs text-accent truncate max-w-[180px] shrink-0 hidden 2xl:block">
|
||||
{node.public_url.replace(/^https?:\/\//, "")}
|
||||
</span>
|
||||
)}
|
||||
<span className={`w-4 h-4 rounded-full ${statusDot} shrink-0`} />
|
||||
</div>
|
||||
|
||||
{expanded && (node.username || node.public_url) && (
|
||||
<div className="mt-2 pt-2 border-t border-border-light text-sm space-y-1">
|
||||
{/* Row 2: IP */}
|
||||
<p className="text-lg font-mono text-text-secondary mb-1">{node.ip}</p>
|
||||
|
||||
{/* Row 3: Credentials */}
|
||||
{node.username && (
|
||||
<p className="text-text-secondary">
|
||||
<span className="text-text-muted">User:</span> {node.username}
|
||||
<p className="text-base text-text-secondary">
|
||||
<span className="text-text-muted">Usuario:</span>{" "}
|
||||
<span className="text-text-primary">{node.username}</span>
|
||||
{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>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Row 4: Public URL */}
|
||||
{node.public_url && (
|
||||
<p className="text-accent truncate">{node.public_url}</p>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-base text-accent mt-1">
|
||||
{node.public_url}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user