import React, { useEffect, useState } from 'react' // ── Empty state ─────────────────────────────────────────────────────────────── export function EmptyState({ icon, title, message, action }: { icon?: string; title: string; message?: string action?: { label: string; onClick: () => void } }) { return (
{icon &&
{icon}
}
{title}
{message &&
{message}
} {action && ( )}
) } // ── Stat card ───────────────────────────────────────────────────────────────── export function StatCard({ label, value, sub, color }: { label: string; value: string | number | null; sub?: string; color?: string }) { return (
{label}
{value === null ? '—' : value}
{sub &&
{sub}
}
) } // ── Tag / badge ─────────────────────────────────────────────────────────────── const TAG_STYLES: Record = { green: { bg: '#EAF3DE', color: '#27500A' }, amber: { bg: '#FAEEDA', color: '#633806' }, red: { bg: '#FCEBEB', color: '#791F1F' }, blue: { bg: '#E6F1FB', color: '#0C447C' }, purple: { bg: '#EEEDFE', color: '#3C3489' }, gray: { bg: '#F1EFE8', color: '#444441' }, teal: { bg: '#E1F5EE', color: '#085041' }, } export function Tag({ children, color = 'gray' }: { children: React.ReactNode; color?: string }) { const s = TAG_STYLES[color] || TAG_STYLES.gray return ( {children} ) } // ── Status dot ──────────────────────────────────────────────────────────────── const STATUS_COLORS: Record = { OPEN: '#378ADD', IN_PROGRESS: '#EF9F27', OVERDUE: '#E24B4A', CLOSED: '#1D9E75', SCHEDULED: '#378ADD', COMPLETED: '#1D9E75', CANCELLED: '#aaa', INVESTIGATING: '#EF9F27', ESCALATED: '#E24B4A', RESOLVED: '#1D9E75', APPROVED: '#1D9E75', UNDER_REVIEW: '#EF9F27', SUSPENDED: '#E24B4A', DRAFT: '#aaa', ACTIVE: '#1D9E75', REVIEW_READY: '#EF9F27', STANDARD_SET: '#1D9E75', ARCHIVED: '#aaa', CURRENT: '#1D9E75', PENDING_REVIEW: '#EF9F27', EXPIRED: '#E24B4A', } export function StatusDot({ status }: { status: string }) { return ( ) } // ── Card ────────────────────────────────────────────────────────────────────── export function Card({ children, style }: { children: React.ReactNode; style?: React.CSSProperties }) { return (
{children}
) } // ── Table ───────────────────────────────────────────────────────────────────── export function Table({ headers, children, empty }: { headers: string[]; children: React.ReactNode; empty?: boolean }) { return ( {headers.map(h => ( ))} {children}
{h}
) } // ── Toast ───────────────────────────────────────────────────────────────────── type ToastType = 'success' | 'error' | 'info' let toastCallback: ((msg: string, type: ToastType) => void) | null = null export function showToast(msg: string, type: ToastType = 'success') { toastCallback?.(msg, type) } export function ToastProvider() { const [toasts, setToasts] = useState<{ id: number; msg: string; type: ToastType }[]>([]) let counter = 0 useEffect(() => { toastCallback = (msg, type) => { const id = ++counter setToasts(t => [...t, { id, msg, type }]) setTimeout(() => setToasts(t => t.filter(x => x.id !== id)), 3500) } return () => { toastCallback = null } }, []) const colors = { success: '#1D9E75', error: '#E24B4A', info: '#534AB7' } return (
{toasts.map(t => (
{t.msg}
))}
) } // ── Button ──────────────────────────────────────────────────────────────────── export function Btn({ children, onClick, variant = 'primary', size = 'md', disabled, type = 'button', style }: { children: React.ReactNode; onClick?: () => void variant?: 'primary' | 'ghost' | 'danger'; size?: 'sm' | 'md' disabled?: boolean; type?: 'button' | 'submit'; style?: React.CSSProperties }) { const base: React.CSSProperties = { display: 'inline-flex', alignItems: 'center', gap: '5px', border: 'none', cursor: disabled ? 'not-allowed' : 'pointer', fontFamily: 'inherit', fontWeight: '500', transition: 'all 0.1s', opacity: disabled ? 0.6 : 1, padding: size === 'sm' ? '5px 10px' : '7px 14px', fontSize: size === 'sm' ? '11px' : '12px', borderRadius: '8px', } const variants = { primary: { background: '#534AB7', color: 'white', border: 'none' }, ghost: { background: 'transparent', color: '#444', border: '0.5px solid #ddd' }, danger: { background: '#FCEBEB', color: '#791F1F', border: '0.5px solid #F09595' }, } return ( ) } // ── Modal ───────────────────────────────────────────────────────────────────── export function Modal({ open, onClose, title, children, width = 400 }: { open: boolean; onClose: () => void; title: string children: React.ReactNode; width?: number }) { if (!open) return null return (
e.target === e.currentTarget && onClose()}>
{title}
{children}
) } // ── Form helpers ────────────────────────────────────────────────────────────── export function Field({ label, children, required }: { label: string; children: React.ReactNode; required?: boolean }) { return (
{children}
) } const inputStyle: React.CSSProperties = { width: '100%', padding: '7px 10px', fontSize: '12px', border: '0.5px solid #ddd', borderRadius: '8px', background: 'transparent', color: '#1a1a1a', fontFamily: 'inherit', outline: 'none' } export const Input = (props: React.InputHTMLAttributes) => export const Select = (props: React.SelectHTMLAttributes) =>