| 1 | import type { ReactNode } from "react"; |
| 2 | |
| 3 | type Tone = "neutral" | "positive" | "negative" | "warning" | "info"; |
| 4 | |
| 5 | export function Card({ |
| 6 | children, |
| 7 | className = "", |
| 8 | as: Component = "section" |
| 9 | }: { |
| 10 | children: ReactNode; |
| 11 | className?: string; |
| 12 | as?: "section" | "article" | "div"; |
| 13 | }) { |
| 14 | return <Component className={`card ${className}`}>{children}</Component>; |
| 15 | } |
| 16 | |
| 17 | export function Badge({ children, tone = "neutral" }: { children: ReactNode; tone?: Tone }) { |
| 18 | return <span className={`badge badge-${tone}`}>{children}</span>; |
| 19 | } |
| 20 | |
| 21 | export function Button({ |
| 22 | children, |
| 23 | variant = "primary", |
| 24 | className = "", |
| 25 | ...props |
| 26 | }: React.ButtonHTMLAttributes<HTMLButtonElement> & { |
| 27 | variant?: "primary" | "secondary" | "ghost" | "danger"; |
| 28 | }) { |
| 29 | return ( |
| 30 | <button className={`button button-${variant} ${className}`} {...props}> |
| 31 | {children} |
| 32 | </button> |
| 33 | ); |
| 34 | } |
| 35 | |
| 36 | export function Field({ |
| 37 | label, |
| 38 | children, |
| 39 | hint |
| 40 | }: { |
| 41 | label: string; |
| 42 | children: ReactNode; |
| 43 | hint?: string; |
| 44 | }) { |
| 45 | return ( |
| 46 | <label className="field"> |
| 47 | <span>{label}</span> |
| 48 | {children} |
| 49 | {hint ? <small>{hint}</small> : null} |
| 50 | </label> |
| 51 | ); |
| 52 | } |
| 53 | |
| 54 | export function MetricCard({ |
| 55 | label, |
| 56 | value, |
| 57 | meta, |
| 58 | tone = "neutral" |
| 59 | }: { |
| 60 | label: string; |
| 61 | value: string; |
| 62 | meta?: string; |
| 63 | tone?: Tone; |
| 64 | }) { |
| 65 | return ( |
| 66 | <Card className="metric-card" as="article"> |
| 67 | <span className="metric-label">{label}</span> |
| 68 | <strong className={`metric-value metric-${tone}`}>{value}</strong> |
| 69 | {meta ? <span className="metric-meta">{meta}</span> : null} |
| 70 | </Card> |
| 71 | ); |
| 72 | } |
| 73 | |
| 74 | export function Skeleton({ rows = 1 }: { rows?: number }) { |
| 75 | return ( |
| 76 | <div className="skeleton-stack" aria-label="Loading"> |
| 77 | {Array.from({ length: rows }, (_, index) => ( |
| 78 | <div className="skeleton-line" key={index} /> |
| 79 | ))} |
| 80 | </div> |
| 81 | ); |
| 82 | } |
| 83 | |
| 84 | export function EmptyState({ |
| 85 | title, |
| 86 | message, |
| 87 | action |
| 88 | }: { |
| 89 | title: string; |
| 90 | message: string; |
| 91 | action?: ReactNode; |
| 92 | }) { |
| 93 | return ( |
| 94 | <div className="empty-state"> |
| 95 | <h3>{title}</h3> |
| 96 | <p>{message}</p> |
| 97 | {action} |
| 98 | </div> |
| 99 | ); |
| 100 | } |
| 101 | |
| 102 | export function ErrorState({ |
| 103 | message, |
| 104 | correlationId, |
| 105 | action |
| 106 | }: { |
| 107 | message: string; |
| 108 | correlationId?: string; |
| 109 | action?: ReactNode; |
| 110 | }) { |
| 111 | return ( |
| 112 | <div className="error-state" role="alert"> |
| 113 | <h3>Unable to load data</h3> |
| 114 | <p>{message}</p> |
| 115 | {correlationId ? <small>Correlation ID: {correlationId}</small> : null} |
| 116 | {action} |
| 117 | </div> |
| 118 | ); |
| 119 | } |