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>;
}