ui fixers in 10/16

This commit is contained in:
2026-10-16 14:34:28 +05:30
parent 379ad204bd
commit e289b8430f
47 changed files with 1904 additions and 1046 deletions
@@ -22,7 +22,7 @@ export function KpiRow({ tiles }: { tiles: KpiTileSpec[] | null }) {
? tiles.map((tile) => (
<StatCard key={tile.label} label={tile.label} value={tile.value} icon={tile.icon} />
))
: Array.from({ length: 2 }).map((_, i) => <Skeleton key={i} className="h-[90px] rounded-xl" />)}
: Array.from({ length: 2 }).map((_, i) => <Skeleton key={i} className="h-24 rounded-2xl" />)}
</div>
)
}
+47 -24
View File
@@ -46,63 +46,86 @@ export interface StatCardProps {
label: string
value: string | number
icon?: LucideIcon
/** Small muted line under the value, e.g. "All time" or "3 with balance due". */
hint?: string
delta?: number
deltaLabel?: string
/** Set true for metrics where an increase is bad (e.g. churn, costs). */
invert?: boolean
trend?: number[]
className?: string
/** Extra classes for the value, e.g. a success/destructive colour. */
valueClassName?: string
/** Extra classes for the icon tile, e.g. a warning tint instead of the primary one. */
iconClassName?: string
}
/**
* The app's KPI tile: a tinted icon square on the left, then label / bold value / optional
* hint stacked beside it. Every list page's stat row uses this so they all read the same.
*/
export function StatCard({
label,
value,
icon: Icon,
hint,
delta,
deltaLabel,
invert = false,
trend,
className,
valueClassName,
iconClassName,
}: StatCardProps) {
const hasDelta = typeof delta === "number" && delta !== 0
const isPositive = hasDelta && delta > 0
const isGood = hasDelta && (invert ? !isPositive : isPositive)
return (
// A size container: the card reads its *own* width, so it lays out the same whether a page puts
// it in a 2-up phone grid or a wide desktop row.
<div
className={cn(
"flex flex-col gap-1.5 rounded-xl bg-card p-3.5 shadow-sm ring-1 ring-foreground/10 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg hover:ring-primary/30",
"@container min-w-0 rounded-2xl bg-card px-4 py-5 shadow-sm ring-1 ring-foreground/10 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg hover:ring-primary/30",
className
)}
>
<div className="flex items-start justify-between gap-2">
<p className="text-sm font-medium text-muted-foreground">{label}</p>
{/* Wide enough: icon beside the text. Narrow (e.g. two per row on a phone): icon above, so the
value keeps the full width instead of being cut off. */}
<div className="flex flex-col gap-2.5 @[12.5rem]:flex-row @[12.5rem]:items-center @[12.5rem]:gap-3.5">
{Icon && (
<div className="flex size-7 shrink-0 items-center justify-center rounded-lg bg-primary/10">
<Icon className="size-3.5 text-primary" />
</div>
)}
</div>
<div className="flex items-end justify-between gap-2">
<p className="text-lg font-bold tracking-tight text-foreground">{formatValue(value)}</p>
{trend && trend.length > 1 && <Sparkline points={trend} />}
</div>
{hasDelta && (
<div className="flex items-center gap-1 text-xs">
<span
<div
className={cn(
"font-semibold",
isGood ? "text-success" : "text-destructive"
"flex size-9 shrink-0 items-center justify-center rounded-xl bg-primary/10 text-primary @[12.5rem]:size-11",
iconClassName
)}
>
{isPositive ? "+" : "-"}
{Math.abs(delta).toFixed(1)}%
</span>
{deltaLabel && <span className="text-muted-foreground">{deltaLabel}</span>}
<Icon className="size-4.5 @[12.5rem]:size-5" aria-hidden />
</div>
)}
<div className="flex min-w-0 flex-1 flex-col">
<p className="truncate text-sm text-muted-foreground">{label}</p>
<div className="flex items-end justify-between gap-2">
<p className={cn("text-base leading-tight font-semibold tracking-tight wrap-break-word text-foreground @[12.5rem]:text-lg", valueClassName)}>
{formatValue(value)}
</p>
{trend && trend.length > 1 && <Sparkline points={trend} />}
</div>
{hasDelta ? (
<div className="flex items-center gap-1 text-xs">
<span className={cn("font-semibold", isGood ? "text-success" : "text-destructive")}>
{isPositive ? "+" : "-"}
{Math.abs(delta).toFixed(1)}%
</span>
{deltaLabel && <span className="text-muted-foreground">{deltaLabel}</span>}
</div>
) : (
hint && <p className="truncate text-xs text-muted-foreground">{hint}</p>
)}
</div>
)}
</div>
</div>
)
}