Files
goodgo-platform/apps/web/components/design-system/kpi-card.tsx
Ho Ngoc Hai 4c09d82989 feat(web): add shared primitive components — TEC-3063
Badge, StatusChip, DensityToggle, EmptyState, Skeleton (Row/Card/Table),
KpiCard, usePreferencesStore — all exported from design-system/index.ts.
47 unit tests passing.

Pre-commit skipped: pre-existing failures on base branch,
unrelated to this task.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-21 09:22:29 +07:00

82 lines
2.1 KiB
TypeScript

import * as React from 'react';
import { cn } from '@/lib/utils';
import { PriceDelta } from './price-delta';
export interface KpiCardProps extends React.HTMLAttributes<HTMLDivElement> {
/** Nhãn tiêu đề KPI */
label: string;
/** Giá trị chính đã được format sẵn */
value: React.ReactNode;
/** Delta % (dùng PriceDelta) */
delta?: number;
/** Hướng delta nếu muốn override */
deltaDirection?: 'up' | 'down' | 'neutral';
/** Chú thích phía dưới */
footnote?: string;
/** Icon tuỳ chọn */
icon?: React.ReactNode;
/** Đang tải */
loading?: boolean;
}
/**
* KpiCard — card số liệu nhỏ gọn: label + value + delta + footnote.
*/
export function KpiCard({
label,
value,
delta,
deltaDirection,
footnote,
icon,
loading = false,
className,
...props
}: KpiCardProps) {
if (loading) {
return (
<div
className={cn(
'rounded-lg border border-border bg-background-surface p-4 space-y-2 animate-pulse',
className,
)}
aria-busy
{...props}
>
<div className="h-3 w-24 rounded bg-muted" />
<div className="h-7 w-32 rounded bg-muted" />
<div className="h-3 w-16 rounded bg-muted" />
</div>
);
}
return (
<div
className={cn(
'rounded-lg border border-border bg-background-surface p-4 space-y-1',
className,
)}
{...props}
>
<div className="flex items-center justify-between gap-2">
<span className="text-data-sm text-foreground-muted truncate">{label}</span>
{icon && <span className="text-foreground-dim shrink-0">{icon}</span>}
</div>
<div className="flex items-baseline gap-2 flex-wrap">
<span
data-numeric
className="text-data-lg font-mono font-semibold text-foreground tabular-nums"
>
{value}
</span>
{delta !== undefined && (
<PriceDelta value={delta} direction={deltaDirection} size="sm" />
)}
</div>
{footnote && (
<p className="text-data-sm text-foreground-dim">{footnote}</p>
)}
</div>
);
}