fix(ui): restore click-through navigation on home KPI cards

PR-Agent caught a regression: pre-migration the four home stats had
onClick handlers navigating to /providers, /cliproxy, /accounts, and
/health. The Phase 2 migration dropped those handlers, removing the
shortcuts for all users.

Add an optional onClick prop to KpiCard. When supplied, the tile
promotes from a static <div> to a <button> with hover lift, keyboard
focus ring, and an accessible aria-label derived from the label.
Wire all four home KPIs back to their original navigation targets.
This commit is contained in:
Tam Nhu Tran
2026-04-25 13:18:44 -04:00
parent 339fcd81e6
commit c59622fabd
2 changed files with 49 additions and 4 deletions
+45 -4
View File
@@ -27,6 +27,14 @@ interface KpiCardProps {
/** Tone for the hint line (e.g. positive delta, warning, etc.). */
tone?: 'default' | 'positive' | 'warning' | 'negative';
icon?: ReactNode;
/**
* Optional click handler. When set, the tile renders as a button with
* keyboard + hover affordances — used by pages where each KPI is also a
* navigation entry point (e.g. home dashboard).
*/
onClick?: () => void;
/** Accessible label override when onClick is set. Defaults to `label`. */
ariaLabel?: string;
className?: string;
}
@@ -39,10 +47,22 @@ const TONE_CLASSES: Record<NonNullable<KpiCardProps['tone']>, string> = {
/**
* KpiCard - Single hero stat tile inside a KpiRow.
*
* Renders as a static <div> by default; promotes to a clickable <button>
* when `onClick` is supplied so the tile gains keyboard focus + hover state.
*/
export function KpiCard({ label, value, hint, tone = 'default', icon, className }: KpiCardProps) {
return (
<div className={cn('rounded-xl border bg-card p-4 shadow-sm', className)}>
export function KpiCard({
label,
value,
hint,
tone = 'default',
icon,
onClick,
ariaLabel,
className,
}: KpiCardProps) {
const content = (
<>
<div className="flex items-center justify-between">
<p className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
{label}
@@ -51,6 +71,27 @@ export function KpiCard({ label, value, hint, tone = 'default', icon, className
</div>
<p className="mt-2 text-2xl font-semibold tracking-tight tabular-nums">{value}</p>
{hint && <p className={cn('mt-1 text-xs', TONE_CLASSES[tone])}>{hint}</p>}
</div>
</>
);
const baseClasses = 'rounded-xl border bg-card p-4 shadow-sm';
if (onClick) {
return (
<button
type="button"
onClick={onClick}
aria-label={ariaLabel ?? (typeof label === 'string' ? label : undefined)}
className={cn(
baseClasses,
'text-left transition-all hover:bg-card/80 hover:shadow-md hover:-translate-y-0.5 active:scale-[0.99] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
className
)}
>
{content}
</button>
);
}
return <div className={cn(baseClasses, className)}>{content}</div>;
}
+4
View File
@@ -67,22 +67,26 @@ export function HomePage() {
label={t('home.profiles')}
value={overview?.profiles ?? 0}
icon={<KeyIcon className="size-4" />}
onClick={() => navigate('/providers')}
/>
<KpiCard
label={t('home.cliproxy')}
value={overview?.cliproxy ?? 0}
icon={<Zap className="size-4" />}
onClick={() => navigate('/cliproxy')}
/>
<KpiCard
label={t('home.accounts')}
value={overview?.accounts ?? 0}
icon={<UsersIcon className="size-4" />}
onClick={() => navigate('/accounts')}
/>
<KpiCard
label={t('home.health')}
value={overview?.health ? `${overview.health.passed}/${overview.health.total}` : '—'}
tone={healthTone}
icon={<ActivityIcon className="size-4" />}
onClick={() => navigate('/health')}
/>
</KpiRow>
}