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 => (
| {h} |
))}
{children}
)
}
// ── 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()}>
)
}
// ── 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) =>
export const Textarea = (props: React.TextareaHTMLAttributes) =>