33 lines
2.8 KiB
TypeScript
33 lines
2.8 KiB
TypeScript
import type { ReactNode } from 'react';
|
|
export function Card({ title, children, right, className = '' }: { title?: string; children: ReactNode; right?: ReactNode; className?: string }) {
|
|
return (
|
|
<section className={`rounded-2xl border border-slate-200/80 bg-white/95 p-5 shadow-[0_12px_30px_rgba(15,23,42,0.045)] backdrop-blur-sm sm:p-6 ${className}`}>
|
|
{title && (
|
|
<div className="mb-5 flex items-center justify-between gap-4">
|
|
<h2 className="text-sm font-semibold uppercase tracking-[0.13em] text-slate-500">{title}</h2>
|
|
{right}
|
|
</div>
|
|
)}
|
|
{children}
|
|
</section>
|
|
);
|
|
}
|
|
export function StatTile({ label, value, sub }: { label: string; value: ReactNode; sub?: string }) {
|
|
return (
|
|
<div className="group rounded-2xl border border-slate-200/80 bg-white/90 p-4 shadow-[0_10px_24px_rgba(15,23,42,0.035)] transition duration-200 hover:-translate-y-0.5 hover:shadow-[0_16px_30px_rgba(15,23,42,0.07)]">
|
|
<div className="text-[11px] font-semibold uppercase tracking-[0.12em] text-slate-500">{label}</div>
|
|
<div className="mt-2 text-2xl font-semibold tracking-tight text-slate-900">{value}</div>
|
|
{sub && <div className="mt-1.5 text-xs leading-5 text-slate-400">{sub}</div>}
|
|
</div>
|
|
);
|
|
}
|
|
const TONE: Record<string, string> = {
|
|
ok: 'border-emerald-200 bg-emerald-50 text-emerald-700', pass: 'border-emerald-200 bg-emerald-50 text-emerald-700', success: 'border-emerald-200 bg-emerald-50 text-emerald-700', allow: 'border-emerald-200 bg-emerald-50 text-emerald-700', approved: 'border-emerald-200 bg-emerald-50 text-emerald-700', answered: 'border-emerald-200 bg-emerald-50 text-emerald-700', certified: 'border-emerald-200 bg-emerald-50 text-emerald-700', verified: 'border-emerald-200 bg-emerald-50 text-emerald-700',
|
|
warn: 'border-amber-200 bg-amber-50 text-amber-700', stale: 'border-amber-200 bg-amber-50 text-amber-700', draft: 'border-amber-200 bg-amber-50 text-amber-700', pending: 'border-amber-200 bg-amber-50 text-amber-700', attention: 'border-amber-200 bg-amber-50 text-amber-700',
|
|
fail: 'border-rose-200 bg-rose-50 text-rose-700', failed: 'border-rose-200 bg-rose-50 text-rose-700', denied: 'border-rose-200 bg-rose-50 text-rose-700', blocked: 'border-rose-200 bg-rose-50 text-rose-700', deny: 'border-rose-200 bg-rose-50 text-rose-700', block: 'border-rose-200 bg-rose-50 text-rose-700', crit: 'border-rose-200 bg-rose-50 text-rose-700', breach: 'border-rose-200 bg-rose-50 text-rose-700', halted: 'border-rose-200 bg-rose-50 text-rose-700',
|
|
};
|
|
export function StatusBadge({ value }: { value: string }) {
|
|
const tone = TONE[String(value).toLowerCase()] ?? 'border-slate-200 bg-slate-50 text-slate-600';
|
|
return <span className={`inline-flex items-center rounded-full border px-2.5 py-1 text-[11px] font-semibold leading-none ${tone}`}>{value}</span>;
|
|
}
|