Files
boc/web-v2/src/components/KPICard.tsx
T

59 lines
1.8 KiB
TypeScript

import { motion } from 'framer-motion'
import { TrendingUp, TrendingDown, Minus } from 'lucide-react'
import { cn } from '@/lib/utils'
interface KPICardProps {
label: string
value: string
change: number
changeLabel: string
icon: React.ReactNode
index?: number
}
export function KPICard({ label, value, change, changeLabel, icon, index = 0 }: KPICardProps) {
const isPositive = change > 0
const isNeutral = change === 0
return (
<motion.div
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.18, delay: index * 0.04, ease: 'easeOut' }}
className="bg-surface rounded-[20px] card-shadow p-6 md:p-7"
>
<div className="flex items-start justify-between">
<div className="space-y-4">
<p className="text-sm text-text-secondary">{label}</p>
<p className="text-2xl md:text-3xl font-semibold text-text-primary tracking-tight">
{value}
</p>
</div>
<div className="w-10 h-10 rounded-xl bg-bg flex items-center justify-center text-text-secondary">
{icon}
</div>
</div>
<div className="mt-5 flex items-center gap-2">
<span
className={cn(
'inline-flex items-center gap-1 text-xs font-medium px-2 py-0.5 rounded-full',
isPositive && 'bg-success-light text-success',
isNeutral && 'bg-bg text-text-secondary',
!isPositive && !isNeutral && 'bg-danger-light text-danger'
)}
>
{isPositive ? (
<TrendingUp size={12} />
) : isNeutral ? (
<Minus size={12} />
) : (
<TrendingDown size={12} />
)}
{Math.abs(change)}%
</span>
<span className="text-xs text-text-secondary">{changeLabel}</span>
</div>
</motion.div>
)
}