mirror of
https://github.com/escalante29/WealthySmart.git
synced 2026-07-17 11:28:47 +02:00
UI foundations: PageHeader, StatTile, validated chart palette, registry primitives
- PageHeader + StatTile standardize the four header and three stat-tile variants found in the audit; Inicio and Salarios migrated as proof. - chart tokens replaced with a teal-anchored categorical scale, CVD- ordered and validated (six checks, light + dark surfaces). - Registry adds: empty, field, item, spinner, progress, toggle-group, input-group, popover, kbd (+ toggle dep). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
96
frontend/src/components/stat-tile.tsx
Normal file
96
frontend/src/components/stat-tile.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
/** Stat-tile contract (dataviz skill): label · value · optional delta ·
|
||||
* optional context/detail rows. Values are pre-formatted by the caller
|
||||
* (formatAmount etc.) and blur under privacy mode via data-sensitive. */
|
||||
export interface StatTileProps {
|
||||
label: string;
|
||||
/** Small line under the label, e.g. "Ciclo al 18 jul". */
|
||||
description?: string;
|
||||
/** Pre-formatted headline value. null renders an em dash. */
|
||||
value: string | null;
|
||||
loading?: boolean;
|
||||
/** Signed, pre-formatted delta plus whether it's good news (colors it). */
|
||||
delta?: { text: string; positive: boolean };
|
||||
/** Muted line(s) or detail rows under the value. */
|
||||
children?: React.ReactNode;
|
||||
/** Makes the whole tile a link (adds hover border + focus ring). */
|
||||
to?: string;
|
||||
ariaLabel?: string;
|
||||
/** Set false for non-monetary values that shouldn't blur (counts, dates). */
|
||||
sensitive?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function StatTile({
|
||||
label,
|
||||
description,
|
||||
value,
|
||||
loading = false,
|
||||
delta,
|
||||
children,
|
||||
to,
|
||||
ariaLabel,
|
||||
sensitive = true,
|
||||
className,
|
||||
}: StatTileProps) {
|
||||
const card = (
|
||||
<Card
|
||||
className={cn(
|
||||
"h-full",
|
||||
to && "transition-colors hover:border-primary/40 cursor-pointer",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||
{label}
|
||||
</CardTitle>
|
||||
{description && (
|
||||
<p className="text-xs text-muted-foreground/80">{description}</p>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
<div className="flex items-baseline gap-2">
|
||||
{loading ? (
|
||||
<Skeleton className="h-8 w-32" />
|
||||
) : (
|
||||
<span
|
||||
{...(sensitive ? { "data-sensitive": true } : {})}
|
||||
className="text-2xl font-bold font-mono"
|
||||
>
|
||||
{value ?? "—"}
|
||||
</span>
|
||||
)}
|
||||
{!loading && delta && (
|
||||
<span
|
||||
data-sensitive
|
||||
className={cn(
|
||||
"text-xs font-mono font-medium",
|
||||
delta.positive ? "text-primary" : "text-destructive",
|
||||
)}
|
||||
>
|
||||
{delta.text}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{children}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
if (!to) return card;
|
||||
return (
|
||||
<Link
|
||||
to={to}
|
||||
aria-label={ariaLabel ?? label}
|
||||
className="block h-full rounded-xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
{card}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user