import React, { useState, useEffect, useCallback, createContext, useContext } from 'react'; /* ═══ TOAST SYSTEM ═══════════════════════════════════════════════════ */ type ToastType = 'success' | 'error' | 'info'; interface Toast { id: string; type: ToastType; title: string; message?: string; } const ToastContext = createContext<{ addToast: (t: Omit) => void }>({ addToast: () => {} }); export const useToast = () => useContext(ToastContext); export function ToastProvider({ children }: { children: React.ReactNode }) { const [toasts, setToasts] = useState([]); const addToast = useCallback((t: Omit) => { const id = Date.now().toString(36); setToasts(p => [...p, { ...t, id }]); setTimeout(() => setToasts(p => p.filter(x => x.id !== id)), 4000); }, []); return ( {children}
{toasts.map(t => (
setToasts(p => p.filter(x => x.id !== t.id))}>
{t.title}
{t.message &&
{t.message}
}
))}
); } /* ═══ ANIMATED NUMBER (CoinbaseMono) ═════════════════════════════════ */ export function Num({ value, decimals = 2, prefix = '', suffix = '' }: { value: number; decimals?: number; prefix?: string; suffix?: string }) { const [display, setDisplay] = useState(value); useEffect(() => { const diff = value - display; if (Math.abs(diff) < 0.0001) { setDisplay(value); return; } let step = 0; const interval = setInterval(() => { step++; setDisplay(display + diff * (1 - Math.pow(1 - step / 16, 3))); if (step >= 16) { setDisplay(value); clearInterval(interval); } }, 30); return () => clearInterval(interval); }, [value]); return {prefix}{display.toLocaleString(undefined, { minimumFractionDigits: decimals, maximumFractionDigits: decimals })}{suffix}; } /* ═══ ASSET ROW ══════════════════════════════════════════════════════ */ export function AssetRow({ icon, name, ticker, price, change, onClick }: { icon: string; name: string; ticker: string; price: number; change?: number; onClick?: () => void; }) { return (
{icon}
{name}
{ticker}
${price.toLocaleString(undefined, { minimumFractionDigits: 2 })}
{change !== undefined && (
= 0 ? 'text-semantic-up' : 'text-semantic-down'}`}> {change >= 0 ? '+' : ''}{change.toFixed(2)}%
)}
); } /* ═══ PRODUCT UI CARD (Dark) ═════════════════════════════════════════ */ export function ProductCard({ children, className = '' }: { children: React.ReactNode; className?: string }) { return
{children}
; } /* ═══ STEP INDICATOR ═════════════════════════════════════════════════ */ export function StepIndicator({ steps, current }: { steps: string[]; current: number }) { return (
{steps.map((label, i) => (
{i < current ? '✓' : i + 1}
{label}
{i < steps.length - 1 && (
)} ))}
); } /* ═══ PIPELINE TRACE (shows which QVAC module did what) ══════════════ */ export function PipelineTrace({ steps }: { steps: Array<{ module: string; operation: string; input: string; output: string; durationMs: number }> }) { if (!steps || steps.length === 0) return null; return (
QVAC Pipeline Trace
{steps.map((s, i) => (
{s.module} {s.operation} {s.durationMs}ms
))}
); } /* ═══ RISK BADGE ═════════════════════════════════════════════════════ */ export function RiskBadge({ level, score }: { level: string; score: number }) { const colors: Record = { safe: 'badge-pill-green', caution: 'badge-pill', warning: 'badge-pill-red', danger: 'badge-pill-red' }; return {level.toUpperCase()} · {score}; } /* ═══ SPARKLINE CHART (pure SVG — no library) ════════════════════════ */ export function Sparkline({ data, width = 120, height = 32, color = '#05b169' }: { data: number[]; width?: number; height?: number; color?: string; }) { if (data.length < 2) return
; const min = Math.min(...data); const max = Math.max(...data); const range = max - min || 1; const points = data.map((v, i) => { const x = (i / (data.length - 1)) * width; const y = height - ((v - min) / range) * (height - 4) - 2; return `${x},${y}`; }).join(' '); const last = data[data.length - 1]; const prev = data[data.length - 2]; const trending = last >= prev; const c = trending ? '#05b169' : '#cf202f'; return ( ); } /* ═══ KEYBOARD SHORTCUT HINT ═════════════════════════════════════════ */ export function Kbd({ children }: { children: string }) { return {children}; }