85 lines
2.9 KiB
TypeScript
85 lines
2.9 KiB
TypeScript
import React from 'react';
|
|
import { clsx } from 'clsx';
|
|
import { twMerge } from 'tailwind-merge';
|
|
import { TrendingUp, TrendingDown, Minus } from 'lucide-react';
|
|
|
|
export interface StatCardProps {
|
|
title: string;
|
|
value: string | number;
|
|
change?: {
|
|
value: string;
|
|
trend: 'up' | 'down' | 'neutral';
|
|
label?: string;
|
|
};
|
|
subtitle?: string;
|
|
icon?: React.ReactNode;
|
|
actionText?: string;
|
|
onAction?: () => void;
|
|
className?: string;
|
|
}
|
|
|
|
export const StatCard: React.FC<StatCardProps> = ({
|
|
title,
|
|
value,
|
|
change,
|
|
subtitle,
|
|
icon,
|
|
actionText,
|
|
onAction,
|
|
className,
|
|
}) => {
|
|
return (
|
|
<div
|
|
className={twMerge(
|
|
clsx(
|
|
'bg-krona-surface border border-krona-border rounded-lg p-4 shadow-krona-soft flex flex-col justify-between transition-all duration-150 hover:border-krona-border-strong',
|
|
className
|
|
)
|
|
)}
|
|
>
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-xs font-medium text-krona-secondary truncate">{title}</p>
|
|
<div className="mt-1.5 flex items-baseline gap-2">
|
|
<span className="text-2xl font-semibold text-krona-primary tracking-tight">{value}</span>
|
|
{change && (
|
|
<span
|
|
className={clsx(
|
|
'inline-flex items-center text-xs font-medium px-1.5 py-0.5 rounded',
|
|
change.trend === 'up' && 'text-status-success-text bg-status-success-soft',
|
|
change.trend === 'down' && 'text-status-danger-text bg-status-danger-soft',
|
|
change.trend === 'neutral' && 'text-krona-secondary bg-krona-muted'
|
|
)}
|
|
>
|
|
{change.trend === 'up' && <TrendingUp className="w-3 h-3 mr-0.5" aria-hidden="true" />}
|
|
{change.trend === 'down' && <TrendingDown className="w-3 h-3 mr-0.5" aria-hidden="true" />}
|
|
{change.trend === 'neutral' && <Minus className="w-3 h-3 mr-0.5" aria-hidden="true" />}
|
|
{change.value}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
{icon && (
|
|
<div className="p-2.5 rounded-lg bg-krona-workspace border border-krona-border/60 text-krona-secondary shrink-0">
|
|
{icon}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{(subtitle || actionText) && (
|
|
<div className="mt-3 pt-2.5 border-t border-krona-border/40 flex items-center justify-between text-xs text-krona-secondary">
|
|
{subtitle && <span>{subtitle}</span>}
|
|
{actionText && (
|
|
<button
|
|
type="button"
|
|
onClick={onAction}
|
|
className="text-krona-green hover:text-krona-green-hover font-medium hover:underline focus:outline-none ml-auto cursor-pointer"
|
|
>
|
|
{actionText} →
|
|
</button>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|