953f598b9b
Componentes React reutilizables: card, dialog, tabs, select, alert, badge, button, input, label, skeleton, tooltip, progress_bar, page_header, form_field, settings_page, crud_page, analytics_page, dashboard_layout. Charts: area, bar, line, sparkline, kpi_card, chart_container. Hooks Wails: use_wails_query, use_wails_mutation, use_wails_stream, use_wails_event, use_animated_canvas. Funciones core: cn, format_compact, chart_colors, get_series_color, wails_cache, theme_config_to_colors. Tipos: chart_series, wails_ipc, theme_config, component_variants. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
61 lines
2.0 KiB
TypeScript
61 lines
2.0 KiB
TypeScript
import * as React from 'react'
|
|
import { cn } from '../core/cn'
|
|
|
|
type KPICardSize = 'sm' | 'default' | 'lg'
|
|
|
|
interface Delta {
|
|
value: number
|
|
isPositive: boolean
|
|
}
|
|
|
|
interface KPICardProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
label: string
|
|
value: string | number
|
|
delta?: Delta
|
|
icon?: 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 KPICard = React.forwardRef<HTMLDivElement, KPICardProps>(
|
|
({ label, value, delta, icon, subtitle, size = 'default', className, ...props }, ref) => {
|
|
const styles = sizeStyles[size]
|
|
const deltaColor = delta
|
|
? delta.value === 0 ? 'text-muted-foreground'
|
|
: delta.isPositive ? 'text-green-600 dark:text-green-500'
|
|
: 'text-red-600 dark:text-red-500'
|
|
: ''
|
|
|
|
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>
|
|
{icon && <div className="text-muted-foreground">{icon}</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>
|
|
{delta && (
|
|
<div className={cn('flex items-center gap-1 text-sm font-medium', deltaColor)}>
|
|
<span>{delta.value > 0 ? '+' : ''}{delta.value}%</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
)
|
|
KPICard.displayName = 'KPICard'
|
|
|
|
export { KPICard, type KPICardProps, type Delta, type KPICardSize }
|