feat: mejoras componentes UI — card variants, kpi_card slots, sparkline colors, bar_chart horizontal radius
- card: variantes default/borderless/ghost con ring condicional - kpi_card: props unit, action, chart y delta con label/suffix personalizable - sparkline: prop colors para colores por barra en variant bar - bar_chart: radius condicional según orientación horizontal/vertical Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,25 +6,35 @@ type KPICardSize = 'sm' | 'default' | 'lg'
|
||||
interface Delta {
|
||||
value: number
|
||||
isPositive: boolean
|
||||
/** Descriptive label before value, e.g. "Increased by" */
|
||||
label?: string
|
||||
/** Suffix after value, e.g. "vs yesterday" */
|
||||
suffix?: string
|
||||
}
|
||||
|
||||
interface KPICardProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
label: string
|
||||
value: string | number
|
||||
/** Unit displayed next to value in smaller font, e.g. "k", "ms", "%" */
|
||||
unit?: string
|
||||
delta?: Delta
|
||||
icon?: React.ReactNode
|
||||
/** Action slot rendered top-right, e.g. a menu button */
|
||||
action?: React.ReactNode
|
||||
/** Inline chart slot rendered to the right of the value */
|
||||
chart?: React.ReactNode
|
||||
subtitle?: string
|
||||
size?: KPICardSize
|
||||
}
|
||||
|
||||
const sizeStyles: Record<KPICardSize, { value: string; label: string }> = {
|
||||
sm: { value: 'text-2xl font-bold', label: 'text-xs' },
|
||||
default: { value: 'text-3xl font-bold', label: 'text-sm' },
|
||||
lg: { value: 'text-4xl font-bold', label: 'text-base' },
|
||||
const sizeStyles: Record<KPICardSize, { value: string; unit: string; label: string }> = {
|
||||
sm: { value: 'text-2xl font-bold', unit: 'text-base font-medium', label: 'text-xs' },
|
||||
default: { value: 'text-3xl font-bold', unit: 'text-lg font-medium', label: 'text-sm' },
|
||||
lg: { value: 'text-4xl font-bold', unit: 'text-xl font-medium', label: 'text-base' },
|
||||
}
|
||||
|
||||
const KPICard = React.forwardRef<HTMLDivElement, KPICardProps>(
|
||||
({ label, value, delta, icon, subtitle, size = 'default', className, ...props }, ref) => {
|
||||
({ label, value, unit, delta, icon, action, chart, subtitle, size = 'default', className, ...props }, ref) => {
|
||||
const styles = sizeStyles[size]
|
||||
const deltaColor = delta
|
||||
? delta.value === 0 ? 'text-muted-foreground'
|
||||
@@ -35,21 +45,32 @@ const KPICard = React.forwardRef<HTMLDivElement, KPICardProps>(
|
||||
return (
|
||||
<div ref={ref} className={cn('rounded-lg border bg-card p-4 text-card-foreground shadow-sm', className)} {...props}>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="space-y-1">
|
||||
<p className={cn('text-muted-foreground', styles.label)}>{label}</p>
|
||||
{subtitle && <p className="text-xs text-muted-foreground/80">{subtitle}</p>}
|
||||
<div className="flex items-center gap-2">
|
||||
{icon && <div className="text-muted-foreground">{icon}</div>}
|
||||
<div className="space-y-1">
|
||||
<p className={cn('text-muted-foreground', styles.label)}>{label}</p>
|
||||
{subtitle && <p className="text-xs text-muted-foreground/80">{subtitle}</p>}
|
||||
</div>
|
||||
</div>
|
||||
{icon && <div className="text-muted-foreground">{icon}</div>}
|
||||
{action && <div className="text-muted-foreground">{action}</div>}
|
||||
</div>
|
||||
<div className="mt-3 flex items-end justify-between gap-4">
|
||||
<div className="space-y-1">
|
||||
<p className={cn('tracking-tight', styles.value)}>{value}</p>
|
||||
<div className="flex items-baseline gap-1">
|
||||
<span className={cn('tracking-tight', styles.value)}>{value}</span>
|
||||
{unit && <span className={cn('text-muted-foreground', styles.unit)}>{unit}</span>}
|
||||
</div>
|
||||
{delta && (
|
||||
<div className={cn('flex items-center gap-1 text-sm font-medium', deltaColor)}>
|
||||
<span>{delta.value > 0 ? '+' : ''}{delta.value}%</span>
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
{delta.label && <span>{delta.label}</span>}
|
||||
<span className={cn('font-medium', deltaColor)}>
|
||||
{delta.isPositive ? '▲' : '▼'} {delta.value > 0 ? '+' : ''}{delta.value}{delta.label ? '' : '%'}
|
||||
</span>
|
||||
{delta.suffix && <span>{delta.suffix}</span>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{chart && <div className="flex-shrink-0">{chart}</div>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -57,4 +78,5 @@ const KPICard = React.forwardRef<HTMLDivElement, KPICardProps>(
|
||||
)
|
||||
KPICard.displayName = 'KPICard'
|
||||
|
||||
export { KPICard, type KPICardProps, type Delta, type KPICardSize }
|
||||
export { KPICard }
|
||||
export type { KPICardProps, Delta, KPICardSize }
|
||||
|
||||
Reference in New Issue
Block a user