File size: 8,635 Bytes
383d246 5f5514e 383d246 5f5514e 383d246 5f5514e 383d246 5f5514e 383d246 5f5514e 383d246 5f5514e 383d246 5f5514e 383d246 5f5514e 383d246 5f5514e 383d246 5f5514e 383d246 5f5514e 383d246 5f5514e 383d246 5f5514e 383d246 5f5514e 383d246 5f5514e 383d246 5f5514e 383d246 5f5514e 383d246 5f5514e 383d246 5f5514e 383d246 5f5514e 9ff7e0c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 | 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<Toast, 'id'>) => void }>({ addToast: () => {} });
export const useToast = () => useContext(ToastContext);
export function ToastProvider({ children }: { children: React.ReactNode }) {
const [toasts, setToasts] = useState<Toast[]>([]);
const addToast = useCallback((t: Omit<Toast, 'id'>) => {
const id = Date.now().toString(36);
setToasts(p => [...p, { ...t, id }]);
setTimeout(() => setToasts(p => p.filter(x => x.id !== id)), 4000);
}, []);
return (
<ToastContext.Provider value={{ addToast }}>
{children}
<div className="fixed top-4 right-4 z-50 flex flex-col gap-2">
{toasts.map(t => (
<div key={t.id} className="bg-canvas rounded-xl p-4 shadow-soft border border-hairline min-w-[300px] page-enter cursor-pointer" onClick={() => setToasts(p => p.filter(x => x.id !== t.id))}>
<div className="flex items-center gap-3">
<div className={`w-2 h-2 rounded-full ${t.type === 'success' ? 'bg-semantic-up' : t.type === 'error' ? 'bg-semantic-down' : 'bg-primary'}`} />
<div>
<div className="text-title-sm text-ink">{t.title}</div>
{t.message && <div className="text-body-sm text-body mt-0.5">{t.message}</div>}
</div>
</div>
</div>
))}
</div>
</ToastContext.Provider>
);
}
/* βββ 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 <span className="number-mono">{prefix}{display.toLocaleString(undefined, { minimumFractionDigits: decimals, maximumFractionDigits: decimals })}{suffix}</span>;
}
/* βββ ASSET ROW ββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
export function AssetRow({ icon, name, ticker, price, change, onClick }: {
icon: string; name: string; ticker: string; price: number; change?: number; onClick?: () => void;
}) {
return (
<div className="asset-row px-4 cursor-pointer" onClick={onClick}>
<div className="asset-icon mr-3 text-sm font-bold text-ink">{icon}</div>
<div className="flex-1">
<div className="text-title-sm text-ink">{name}</div>
<div className="text-caption text-muted">{ticker}</div>
</div>
<div className="text-right">
<div className="number-mono text-number-display text-ink">${price.toLocaleString(undefined, { minimumFractionDigits: 2 })}</div>
{change !== undefined && (
<div className={`number-mono text-caption ${change >= 0 ? 'text-semantic-up' : 'text-semantic-down'}`}>
{change >= 0 ? '+' : ''}{change.toFixed(2)}%
</div>
)}
</div>
</div>
);
}
/* βββ PRODUCT UI CARD (Dark) βββββββββββββββββββββββββββββββββββββββββ */
export function ProductCard({ children, className = '' }: { children: React.ReactNode; className?: string }) {
return <div className={`card-dark ${className}`}>{children}</div>;
}
/* βββ STEP INDICATOR βββββββββββββββββββββββββββββββββββββββββββββββββ */
export function StepIndicator({ steps, current }: { steps: string[]; current: number }) {
return (
<div className="flex items-center justify-center gap-3 mb-12">
{steps.map((label, i) => (
<React.Fragment key={i}>
<div className="flex flex-col items-center gap-1.5">
<div className={`w-8 h-8 rounded-full flex items-center justify-center text-caption-strong transition-colors ${
i < current ? 'bg-primary text-on-primary' :
i === current ? 'bg-primary text-on-primary' :
'bg-surface-strong text-muted'
}`}>
{i < current ? 'β' : i + 1}
</div>
<span className={`text-caption ${i <= current ? 'text-ink' : 'text-muted'}`}>{label}</span>
</div>
{i < steps.length - 1 && (
<div className={`w-10 h-0.5 -mt-5 rounded-full ${i < current ? 'bg-primary' : 'bg-surface-strong'}`} />
)}
</React.Fragment>
))}
</div>
);
}
/* βββ 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 (
<div className="bg-surface-soft rounded-xl p-4 mt-3">
<div className="text-caption-strong text-muted uppercase tracking-wider mb-2">QVAC Pipeline Trace</div>
<div className="space-y-1.5">
{steps.map((s, i) => (
<div key={i} className="flex items-start gap-2 text-caption">
<div className="w-1.5 h-1.5 rounded-full bg-primary mt-1.5 shrink-0" />
<div className="flex-1 min-w-0">
<span className="font-mono text-primary text-caption-strong">{s.module}</span>
<span className="text-muted mx-1">β</span>
<span className="text-body">{s.operation}</span>
<span className="text-muted-soft ml-1 number-mono">{s.durationMs}ms</span>
</div>
</div>
))}
</div>
</div>
);
}
/* βββ RISK BADGE βββββββββββββββββββββββββββββββββββββββββββββββββββββ */
export function RiskBadge({ level, score }: { level: string; score: number }) {
const colors: Record<string, string> = { safe: 'badge-pill-green', caution: 'badge-pill', warning: 'badge-pill-red', danger: 'badge-pill-red' };
return <span className={`badge-pill ${colors[level] || 'badge-pill'}`}>{level.toUpperCase()} Β· {score}</span>;
}
/* βββ 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 <div style={{ width, height }} />;
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 (
<svg width={width} height={height} viewBox={`0 0 ${width} ${height}`} className="overflow-visible">
<polyline fill="none" stroke={c} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" points={points} />
<circle cx={(data.length - 1) / (data.length - 1) * width} cy={height - ((last - min) / range) * (height - 4) - 2} r="2" fill={c} />
</svg>
);
}
/* βββ KEYBOARD SHORTCUT HINT βββββββββββββββββββββββββββββββββββββββββ */
export function Kbd({ children }: { children: string }) {
return <kbd className="inline-flex items-center px-1.5 py-0.5 bg-surface-strong text-muted rounded-xs text-[10px] font-mono border border-hairline">{children}</kbd>;
}
|