From 5033348320fc82c6228280902bb466dbd4b602b5 Mon Sep 17 00:00:00 2001 From: Carlos Escalante Date: Sat, 4 Jul 2026 14:00:59 -0600 Subject: [PATCH] Inicio: hover-cards on stat tiles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gasto del ciclo reveals the by-source breakdown, Próximas cuotas the full active-plan list, Pensión last-period rendimientos per fund. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/src/components/stat-tile.tsx | 23 ++++++++- frontend/src/pages/Inicio.tsx | 71 +++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/stat-tile.tsx b/frontend/src/components/stat-tile.tsx index 98f6f93..6c4bb4c 100644 --- a/frontend/src/components/stat-tile.tsx +++ b/frontend/src/components/stat-tile.tsx @@ -2,6 +2,11 @@ import { Link } from "react-router-dom"; import { cn } from "@/lib/utils"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + HoverCard, + HoverCardContent, + HoverCardTrigger, +} from "@/components/ui/hover-card"; import { Skeleton } from "@/components/ui/skeleton"; /** Stat-tile contract (dataviz skill): label · value · optional delta · @@ -21,6 +26,8 @@ export interface StatTileProps { /** Makes the whole tile a link (adds hover border + focus ring). */ to?: string; ariaLabel?: string; + /** Extra detail revealed on hover (HoverCard). */ + hoverContent?: React.ReactNode; /** Set false for non-monetary values that shouldn't blur (counts, dates). */ sensitive?: boolean; className?: string; @@ -35,6 +42,7 @@ export function StatTile({ children, to, ariaLabel, + hoverContent, sensitive = true, className, }: StatTileProps) { @@ -83,8 +91,7 @@ export function StatTile({ ); - if (!to) return card; - return ( + const wrapped = to ? ( {card} + ) : ( + card + ); + + if (!hoverContent) return wrapped; + return ( + + }> + {wrapped} + + {hoverContent} + ); } diff --git a/frontend/src/pages/Inicio.tsx b/frontend/src/pages/Inicio.tsx index 975806f..1015397 100644 --- a/frontend/src/pages/Inicio.tsx +++ b/frontend/src/pages/Inicio.tsx @@ -25,6 +25,36 @@ import { Skeleton } from "@/components/ui/skeleton"; const CYCLE = currentBudgetCycle(); +const SOURCE_LABELS: Record = { + CREDIT_CARD: "Tarjeta", + CASH: "Efectivo", + TRANSFER: "Transferencias", +}; + +function HoverRows({ + title, + rows, +}: { + title: string; + rows: { label: string; value: string }[]; +}) { + return ( +
+

{title}

+
+ {rows.map((r) => ( +
+ {r.label} + + {r.value} + +
+ ))} +
+
+ ); +} + function GastoDelCicloCard() { const q = useQuery({ queryKey: ["budget", "month", CYCLE.year, CYCLE.month], @@ -44,6 +74,25 @@ function GastoDelCicloCard() { loading={q.isPending} to="/budget" ariaLabel="Ver presupuesto" + hoverContent={ + q.data ? ( + s.count > 0) + .map((s) => ({ + label: `${SOURCE_LABELS[s.source] ?? s.source} (${s.count})`, + value: formatAmount(s.net, "CRC"), + })), + { + label: "Gran total egresos", + value: formatAmount(q.data.gran_total_egresos, "CRC"), + }, + ]} + /> + ) : undefined + } > {balance !== null && (

@@ -105,6 +154,17 @@ function ProximasCuotasCard() { loading={q.isPending} to="/financiamientos" ariaLabel="Ver financiamientos" + hoverContent={ + upcoming.length > 0 ? ( + ({ + label: `${p.merchant} (${p.cuotas_billed}/${p.num_installments})`, + value: `${formatShortDate(p.next_cuota_date!)} · ${formatAmount(nextCuotaOf(p), p.currency)}`, + }))} + /> + ) : undefined + } > {topThree.length > 0 && (

    @@ -146,6 +206,17 @@ function PensionCard() { loading={q.isPending} to="/pensions" ariaLabel="Ver pensiones" + hoverContent={ + q.data && q.data.length > 0 ? ( + ({ + label: `${f.fund} · rendimientos`, + value: `+${formatAmount(f.rendimientos, "CRC")}`, + }))} + /> + ) : undefined + } > {q.data && q.data.length > 0 && (