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>
82 lines
2.1 KiB
TypeScript
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>
|
|
);
|
|
}
|