bloby-bot 0.37.1 → 0.39.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/supervisor/harnesses/codex.ts +34 -0
- package/worker/prompts/bloby-system-prompt.txt +2 -2
- package/workspace/client/index.html +3 -0
- package/workspace/client/public/.well-known/assetlinks.json +8 -0
- package/workspace/client/public/bloby-cyberpunk.png +0 -0
- package/workspace/client/public/brand/blackrock.svg +8 -0
- package/workspace/client/public/kid-breakfast.png +0 -0
- package/workspace/client/public/wallpapers/bg.jpg +0 -0
- package/workspace/client/public/wallpapers/crypto_bg.png +0 -0
- package/workspace/client/public/wallpapers/wp-dusk.jpg +0 -0
- package/workspace/client/public/wallpapers/wp-mountain.jpg +0 -0
- package/workspace/client/public/wallpapers/wp-ocean.jpg +0 -0
- package/workspace/client/src/App.tsx +20 -9
- package/workspace/client/src/components/Dashboard/AiChatPage.tsx +145 -0
- package/workspace/client/src/components/Dashboard/CryptoPage.tsx +470 -0
- package/workspace/client/src/components/Dashboard/DashboardPage.tsx +117 -113
- package/workspace/client/src/components/Dashboard/WishlistPage.tsx +464 -0
- package/workspace/client/src/components/Layout/DashboardLayout.tsx +32 -34
- package/workspace/client/src/components/Layout/MiniSidebar.tsx +64 -0
- package/workspace/client/src/components/Layout/MobileNav.tsx +103 -6
- package/workspace/client/src/components/Layout/Sidebar.tsx +11 -10
- package/workspace/client/src/components/Lock/PinInput.tsx +107 -0
- package/workspace/client/src/components/Lock/WorkspaceLock.tsx +484 -0
- package/workspace/client/src/components/StickyNotes/StickyNotesOverlay.tsx +396 -0
- package/workspace/client/src/components/StickyNotes/StickyNotesSettingsPage.tsx +427 -0
- package/workspace/client/src/components/Wallpaper/WallpaperBackground.tsx +12 -0
- package/workspace/client/src/components/Wallpaper/WallpaperContext.tsx +160 -0
- package/workspace/client/src/components/Wallpaper/WallpaperPicker.tsx +67 -0
- package/workspace/client/src/styles/globals.css +89 -4
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
import { forwardRef, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
QrCode,
|
|
4
|
+
Send,
|
|
5
|
+
RefreshCw,
|
|
6
|
+
Wallet,
|
|
7
|
+
History,
|
|
8
|
+
LineChart,
|
|
9
|
+
Settings,
|
|
10
|
+
TrendingUp,
|
|
11
|
+
TrendingDown,
|
|
12
|
+
} from 'lucide-react';
|
|
13
|
+
|
|
14
|
+
type TabId = 'wallet' | 'history' | 'market' | 'settings';
|
|
15
|
+
|
|
16
|
+
interface Asset {
|
|
17
|
+
symbol: string;
|
|
18
|
+
name: string;
|
|
19
|
+
amount: string;
|
|
20
|
+
usd: string;
|
|
21
|
+
change: number;
|
|
22
|
+
color: string;
|
|
23
|
+
initial: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const ASSETS: Asset[] = [
|
|
27
|
+
{ symbol: 'ACH', name: 'Achain', amount: '35478.2', usd: '49.37', change: 67.9, color: '#7C5CFF', initial: 'A' },
|
|
28
|
+
{ symbol: 'LINK', name: 'Chainlink', amount: '152.76', usd: '1121.95', change: -7, color: '#3F7AFF', initial: 'L' },
|
|
29
|
+
{ symbol: 'ZAP', name: 'Zapper', amount: '271032.13', usd: '772', change: 16.77, color: '#9B6BFF', initial: 'Z' },
|
|
30
|
+
{ symbol: 'SNT', name: 'Status', amount: '6234.10', usd: '184.20', change: 4.2, color: '#5DA9FF', initial: 'S' },
|
|
31
|
+
{ symbol: 'ETH', name: 'Ethereum', amount: '2.184', usd: '7321.40', change: 2.8, color: '#8B8DFF', initial: 'E' },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
export default function CryptoPage() {
|
|
35
|
+
const [tab, setTab] = useState<TabId>('wallet');
|
|
36
|
+
const [isDesktop, setIsDesktop] = useState(() =>
|
|
37
|
+
typeof window !== 'undefined' ? window.matchMedia('(min-width: 768px)').matches : false,
|
|
38
|
+
);
|
|
39
|
+
const dockRef = useRef<HTMLDivElement>(null);
|
|
40
|
+
const [dockHeight, setDockHeight] = useState(80);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
const mql = window.matchMedia('(min-width: 768px)');
|
|
44
|
+
const handler = (e: MediaQueryListEvent) => setIsDesktop(e.matches);
|
|
45
|
+
mql.addEventListener('change', handler);
|
|
46
|
+
return () => mql.removeEventListener('change', handler);
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
49
|
+
useLayoutEffect(() => {
|
|
50
|
+
if (!dockRef.current) return;
|
|
51
|
+
const measure = () => {
|
|
52
|
+
if (dockRef.current) setDockHeight(dockRef.current.offsetHeight);
|
|
53
|
+
};
|
|
54
|
+
measure();
|
|
55
|
+
const ro = new ResizeObserver(measure);
|
|
56
|
+
ro.observe(dockRef.current);
|
|
57
|
+
return () => ro.disconnect();
|
|
58
|
+
}, [isDesktop]);
|
|
59
|
+
|
|
60
|
+
// The sheet itself runs to bottom: 0 so the wallpaper never peeks
|
|
61
|
+
// through behind the dock. The inner content reserves dockHeight (+
|
|
62
|
+
// a small margin on desktop so list rows don't slip behind the
|
|
63
|
+
// floating glass pill).
|
|
64
|
+
const contentBottomPadding = isDesktop ? dockHeight + 16 : dockHeight;
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div className="relative h-full w-full overflow-hidden">
|
|
68
|
+
{/* Hero content (uses the global wallpaper) */}
|
|
69
|
+
<div className="relative z-10 flex flex-col items-center pt-12 sm:pt-16 px-6 text-white">
|
|
70
|
+
<p className="text-xs uppercase tracking-[0.2em] text-white/65">Balance</p>
|
|
71
|
+
|
|
72
|
+
<h1
|
|
73
|
+
className="mt-3 text-5xl sm:text-6xl tracking-tight tabular-nums text-white"
|
|
74
|
+
style={{ fontWeight: 900, letterSpacing: '-0.04em' }}
|
|
75
|
+
>
|
|
76
|
+
$ 14,752<span>.19</span>
|
|
77
|
+
</h1>
|
|
78
|
+
|
|
79
|
+
<div className="mt-4 inline-flex items-center gap-1 rounded-full bg-emerald-400/20 border border-emerald-300/30 px-2.5 py-1 text-emerald-200 text-xs font-semibold">
|
|
80
|
+
<TrendingUp className="h-3 w-3" />
|
|
81
|
+
+85.2%
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<div className="mt-10 flex items-center gap-10">
|
|
85
|
+
<ActionButton icon={QrCode} label="Receive" />
|
|
86
|
+
<ActionButton icon={Send} label="Send" />
|
|
87
|
+
<ActionButton icon={RefreshCw} label="Swap" />
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
{/* Draggable bottom sheet — runs to bottom: 0; content clears the dock */}
|
|
92
|
+
<DraggableSheet>
|
|
93
|
+
<div className="px-5 pt-4 pb-2 flex items-center justify-between">
|
|
94
|
+
<h2 className="text-xl font-bold text-zinc-900 tracking-tight">
|
|
95
|
+
{tab === 'wallet' && 'Assets'}
|
|
96
|
+
{tab === 'history' && 'History'}
|
|
97
|
+
{tab === 'market' && 'Market'}
|
|
98
|
+
{tab === 'settings' && 'Settings'}
|
|
99
|
+
</h2>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<div
|
|
103
|
+
className="flex-1 overflow-y-auto px-2"
|
|
104
|
+
style={{ paddingBottom: contentBottomPadding }}
|
|
105
|
+
>
|
|
106
|
+
{tab === 'wallet' && <AssetsList />}
|
|
107
|
+
{tab === 'history' && <HistoryList />}
|
|
108
|
+
{tab === 'market' && <MarketList />}
|
|
109
|
+
{tab === 'settings' && <SettingsList />}
|
|
110
|
+
</div>
|
|
111
|
+
</DraggableSheet>
|
|
112
|
+
|
|
113
|
+
{/* Bottom tab bar — full white dock on mobile, centered glass pill on desktop */}
|
|
114
|
+
<SheetTabBar ref={dockRef} tab={tab} onChange={setTab} isDesktop={isDesktop} />
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* ────────────────────────────────────────────────────────────── */
|
|
120
|
+
/* Action button */
|
|
121
|
+
/* ────────────────────────────────────────────────────────────── */
|
|
122
|
+
function ActionButton({
|
|
123
|
+
icon: Icon,
|
|
124
|
+
label,
|
|
125
|
+
}: {
|
|
126
|
+
icon: React.ComponentType<{ className?: string }>;
|
|
127
|
+
label: string;
|
|
128
|
+
}) {
|
|
129
|
+
return (
|
|
130
|
+
<button type="button" className="flex flex-col items-center gap-2 group">
|
|
131
|
+
<span className="h-12 w-12 rounded-full bg-white/10 hover:bg-white/15 border border-white/15 flex items-center justify-center backdrop-blur-md transition group-active:scale-95">
|
|
132
|
+
<Icon className="h-5 w-5 text-white" />
|
|
133
|
+
</span>
|
|
134
|
+
<span className="text-[11px] text-white/85">{label}</span>
|
|
135
|
+
</button>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/* ────────────────────────────────────────────────────────────── */
|
|
140
|
+
/* Draggable sheet */
|
|
141
|
+
/* ────────────────────────────────────────────────────────────── */
|
|
142
|
+
|
|
143
|
+
// Snap points are vertical offsets from the TOP of the page area, in px.
|
|
144
|
+
// The sheet stretches from `top: snap` all the way to `bottom: 0` — the
|
|
145
|
+
// dock floats above it. This guarantees the wallpaper never shows through
|
|
146
|
+
// the gap between the sheet and the dock at any snap position.
|
|
147
|
+
function DraggableSheet({ children }: { children: React.ReactNode }) {
|
|
148
|
+
const sheetRef = useRef<HTMLDivElement>(null);
|
|
149
|
+
const [snap, setSnap] = useState<'mid' | 'expanded' | 'collapsed'>('mid');
|
|
150
|
+
const dragRef = useRef<{
|
|
151
|
+
startY: number;
|
|
152
|
+
startOffset: number;
|
|
153
|
+
pointerId: number | null;
|
|
154
|
+
lastY: number;
|
|
155
|
+
lastT: number;
|
|
156
|
+
velocity: number;
|
|
157
|
+
}>({ startY: 0, startOffset: 0, pointerId: null, lastY: 0, lastT: 0, velocity: 0 });
|
|
158
|
+
const [dragOffset, setDragOffset] = useState<number | null>(null);
|
|
159
|
+
const [vh, setVh] = useState(() => (typeof window !== 'undefined' ? window.innerHeight : 800));
|
|
160
|
+
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
const onResize = () => setVh(window.innerHeight);
|
|
163
|
+
window.addEventListener('resize', onResize);
|
|
164
|
+
return () => window.removeEventListener('resize', onResize);
|
|
165
|
+
}, []);
|
|
166
|
+
|
|
167
|
+
const SNAP_TOP = {
|
|
168
|
+
expanded: Math.round(vh * 0.10),
|
|
169
|
+
mid: Math.round(vh * 0.42),
|
|
170
|
+
collapsed: Math.round(vh * 0.66),
|
|
171
|
+
} as const;
|
|
172
|
+
|
|
173
|
+
const targetTop = SNAP_TOP[snap];
|
|
174
|
+
const currentTop = dragOffset ?? targetTop;
|
|
175
|
+
|
|
176
|
+
function onPointerDown(e: React.PointerEvent) {
|
|
177
|
+
const target = e.target as HTMLElement;
|
|
178
|
+
if (!target.closest('[data-sheet-drag]')) return;
|
|
179
|
+
e.preventDefault();
|
|
180
|
+
const el = sheetRef.current;
|
|
181
|
+
if (!el) return;
|
|
182
|
+
el.setPointerCapture(e.pointerId);
|
|
183
|
+
dragRef.current = {
|
|
184
|
+
startY: e.clientY,
|
|
185
|
+
startOffset: targetTop,
|
|
186
|
+
pointerId: e.pointerId,
|
|
187
|
+
lastY: e.clientY,
|
|
188
|
+
lastT: performance.now(),
|
|
189
|
+
velocity: 0,
|
|
190
|
+
};
|
|
191
|
+
setDragOffset(targetTop);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function onPointerMove(e: React.PointerEvent) {
|
|
195
|
+
const d = dragRef.current;
|
|
196
|
+
if (d.pointerId !== e.pointerId) return;
|
|
197
|
+
const now = performance.now();
|
|
198
|
+
const dt = Math.max(1, now - d.lastT);
|
|
199
|
+
d.velocity = (e.clientY - d.lastY) / dt; // px/ms
|
|
200
|
+
d.lastY = e.clientY;
|
|
201
|
+
d.lastT = now;
|
|
202
|
+
const next = Math.max(SNAP_TOP.expanded, Math.min(SNAP_TOP.collapsed, d.startOffset + (e.clientY - d.startY)));
|
|
203
|
+
setDragOffset(next);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function onPointerUp(e: React.PointerEvent) {
|
|
207
|
+
const d = dragRef.current;
|
|
208
|
+
if (d.pointerId !== e.pointerId) return;
|
|
209
|
+
const el = sheetRef.current;
|
|
210
|
+
el?.releasePointerCapture(e.pointerId);
|
|
211
|
+
const finalTop = dragOffset ?? targetTop;
|
|
212
|
+
const v = d.velocity; // px/ms; positive = moving down
|
|
213
|
+
|
|
214
|
+
let next: 'expanded' | 'mid' | 'collapsed';
|
|
215
|
+
if (v > 0.6) {
|
|
216
|
+
next = snap === 'expanded' ? 'mid' : 'collapsed';
|
|
217
|
+
} else if (v < -0.6) {
|
|
218
|
+
next = snap === 'collapsed' ? 'mid' : 'expanded';
|
|
219
|
+
} else {
|
|
220
|
+
const distances = (Object.keys(SNAP_TOP) as Array<keyof typeof SNAP_TOP>).map((k) => ({
|
|
221
|
+
k,
|
|
222
|
+
d: Math.abs(SNAP_TOP[k] - finalTop),
|
|
223
|
+
}));
|
|
224
|
+
distances.sort((a, b) => a.d - b.d);
|
|
225
|
+
next = distances[0].k;
|
|
226
|
+
}
|
|
227
|
+
setSnap(next);
|
|
228
|
+
setDragOffset(null);
|
|
229
|
+
dragRef.current.pointerId = null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return (
|
|
233
|
+
<div
|
|
234
|
+
ref={sheetRef}
|
|
235
|
+
onPointerDown={onPointerDown}
|
|
236
|
+
onPointerMove={onPointerMove}
|
|
237
|
+
onPointerUp={onPointerUp}
|
|
238
|
+
onPointerCancel={onPointerUp}
|
|
239
|
+
className="absolute left-0 right-0 bottom-0 z-20 mx-auto flex flex-col bg-white shadow-[0_-20px_60px_-20px_rgba(0,0,0,0.45)]"
|
|
240
|
+
style={{
|
|
241
|
+
top: currentTop,
|
|
242
|
+
borderTopLeftRadius: 28,
|
|
243
|
+
borderTopRightRadius: 28,
|
|
244
|
+
transition: dragOffset == null ? 'top 320ms cubic-bezier(0.32, 0.72, 0, 1)' : 'none',
|
|
245
|
+
touchAction: 'none',
|
|
246
|
+
maxWidth: 720,
|
|
247
|
+
width: '100%',
|
|
248
|
+
}}
|
|
249
|
+
>
|
|
250
|
+
{/* Drag handle */}
|
|
251
|
+
<div
|
|
252
|
+
data-sheet-drag
|
|
253
|
+
className="flex justify-center pt-3 pb-2 cursor-grab active:cursor-grabbing select-none"
|
|
254
|
+
>
|
|
255
|
+
<span className="block h-1.5 w-10 rounded-full bg-zinc-300" />
|
|
256
|
+
</div>
|
|
257
|
+
|
|
258
|
+
{children}
|
|
259
|
+
</div>
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/* ────────────────────────────────────────────────────────────── */
|
|
264
|
+
/* Sheet content */
|
|
265
|
+
/* ────────────────────────────────────────────────────────────── */
|
|
266
|
+
|
|
267
|
+
function AssetsList() {
|
|
268
|
+
return (
|
|
269
|
+
<ul className="px-3 divide-y divide-zinc-100">
|
|
270
|
+
{ASSETS.map((a) => (
|
|
271
|
+
<li key={a.symbol} className="flex items-center gap-3 py-3 px-2">
|
|
272
|
+
<span
|
|
273
|
+
className="h-10 w-10 rounded-full flex items-center justify-center text-white text-sm font-bold shrink-0"
|
|
274
|
+
style={{ backgroundColor: a.color }}
|
|
275
|
+
>
|
|
276
|
+
{a.initial}
|
|
277
|
+
</span>
|
|
278
|
+
<div className="flex-1 min-w-0">
|
|
279
|
+
<p className="text-sm font-semibold text-zinc-900 truncate">{a.name}</p>
|
|
280
|
+
<p className="text-[11px] text-zinc-500">=~${a.usd}</p>
|
|
281
|
+
</div>
|
|
282
|
+
<div className="text-right">
|
|
283
|
+
<p className="text-sm font-semibold text-zinc-900 tabular-nums">{a.amount}</p>
|
|
284
|
+
<ChangeBadge value={a.change} />
|
|
285
|
+
</div>
|
|
286
|
+
</li>
|
|
287
|
+
))}
|
|
288
|
+
</ul>
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function ChangeBadge({ value }: { value: number }) {
|
|
293
|
+
const positive = value >= 0;
|
|
294
|
+
return (
|
|
295
|
+
<span
|
|
296
|
+
className={`inline-flex items-center gap-0.5 rounded-full px-1.5 py-0.5 text-[10px] font-semibold mt-0.5 ${
|
|
297
|
+
positive ? 'bg-emerald-100 text-emerald-700' : 'bg-rose-100 text-rose-700'
|
|
298
|
+
}`}
|
|
299
|
+
>
|
|
300
|
+
{positive ? <TrendingUp className="h-2.5 w-2.5" /> : <TrendingDown className="h-2.5 w-2.5" />}
|
|
301
|
+
{positive ? '+' : ''}
|
|
302
|
+
{value}%
|
|
303
|
+
</span>
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function HistoryList() {
|
|
308
|
+
const items = [
|
|
309
|
+
{ kind: 'Received', who: 'From 0x9a3...e21f', amount: '+ 0.42 ETH', when: 'Today, 09:14', positive: true },
|
|
310
|
+
{ kind: 'Sent', who: 'To 0x12b...88aa', amount: '- 120.00 LINK', when: 'Yesterday', positive: false },
|
|
311
|
+
{ kind: 'Swap', who: 'ACT → LINK', amount: '6.72 LINK', when: 'Mon', positive: true },
|
|
312
|
+
{ kind: 'Received', who: 'From 0x7fa...1bd2', amount: '+ 250 ZAP', when: 'May 1', positive: true },
|
|
313
|
+
];
|
|
314
|
+
return (
|
|
315
|
+
<ul className="px-3 divide-y divide-zinc-100">
|
|
316
|
+
{items.map((it, i) => (
|
|
317
|
+
<li key={i} className="flex items-center gap-3 py-3 px-2">
|
|
318
|
+
<span
|
|
319
|
+
className={`h-10 w-10 rounded-full flex items-center justify-center text-sm font-bold shrink-0 ${
|
|
320
|
+
it.positive ? 'bg-emerald-100 text-emerald-700' : 'bg-rose-100 text-rose-700'
|
|
321
|
+
}`}
|
|
322
|
+
>
|
|
323
|
+
{it.kind[0]}
|
|
324
|
+
</span>
|
|
325
|
+
<div className="flex-1 min-w-0">
|
|
326
|
+
<p className="text-sm font-semibold text-zinc-900">{it.kind}</p>
|
|
327
|
+
<p className="text-[11px] text-zinc-500 truncate">{it.who}</p>
|
|
328
|
+
</div>
|
|
329
|
+
<div className="text-right">
|
|
330
|
+
<p className={`text-sm font-semibold tabular-nums ${it.positive ? 'text-emerald-700' : 'text-rose-700'}`}>{it.amount}</p>
|
|
331
|
+
<p className="text-[11px] text-zinc-400">{it.when}</p>
|
|
332
|
+
</div>
|
|
333
|
+
</li>
|
|
334
|
+
))}
|
|
335
|
+
</ul>
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function MarketList() {
|
|
340
|
+
const items = [
|
|
341
|
+
{ name: 'Bitcoin', symbol: 'BTC', price: '67,420.18', change: 1.4, initial: 'B', color: '#F7931A' },
|
|
342
|
+
{ name: 'Ethereum', symbol: 'ETH', price: '3,352.91', change: 2.8, initial: 'E', color: '#627EEA' },
|
|
343
|
+
{ name: 'Chainlink', symbol: 'LINK', price: '14.62', change: -3.1, initial: 'L', color: '#3F7AFF' },
|
|
344
|
+
{ name: 'Solana', symbol: 'SOL', price: '162.04', change: 4.6, initial: 'S', color: '#9945FF' },
|
|
345
|
+
{ name: 'Achain', symbol: 'ACH', price: '0.00139', change: 67.9, initial: 'A', color: '#7C5CFF' },
|
|
346
|
+
];
|
|
347
|
+
return (
|
|
348
|
+
<ul className="px-3 divide-y divide-zinc-100">
|
|
349
|
+
{items.map((it) => (
|
|
350
|
+
<li key={it.symbol} className="flex items-center gap-3 py-3 px-2">
|
|
351
|
+
<span
|
|
352
|
+
className="h-10 w-10 rounded-full flex items-center justify-center text-white text-sm font-bold shrink-0"
|
|
353
|
+
style={{ backgroundColor: it.color }}
|
|
354
|
+
>
|
|
355
|
+
{it.initial}
|
|
356
|
+
</span>
|
|
357
|
+
<div className="flex-1 min-w-0">
|
|
358
|
+
<p className="text-sm font-semibold text-zinc-900">{it.name}</p>
|
|
359
|
+
<p className="text-[11px] text-zinc-500">{it.symbol}</p>
|
|
360
|
+
</div>
|
|
361
|
+
<div className="text-right">
|
|
362
|
+
<p className="text-sm font-semibold text-zinc-900 tabular-nums">${it.price}</p>
|
|
363
|
+
<ChangeBadge value={it.change} />
|
|
364
|
+
</div>
|
|
365
|
+
</li>
|
|
366
|
+
))}
|
|
367
|
+
</ul>
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function SettingsList() {
|
|
372
|
+
const rows = [
|
|
373
|
+
{ label: 'Notifications', value: 'On' },
|
|
374
|
+
{ label: 'Currency', value: 'USD' },
|
|
375
|
+
{ label: 'Network', value: 'Ethereum' },
|
|
376
|
+
{ label: 'Backup phrase', value: 'View' },
|
|
377
|
+
{ label: 'Connected sites', value: '3' },
|
|
378
|
+
{ label: 'About', value: 'v1.0' },
|
|
379
|
+
];
|
|
380
|
+
return (
|
|
381
|
+
<ul className="px-3 divide-y divide-zinc-100">
|
|
382
|
+
{rows.map((r) => (
|
|
383
|
+
<li key={r.label} className="flex items-center justify-between py-3 px-2">
|
|
384
|
+
<span className="text-sm text-zinc-900">{r.label}</span>
|
|
385
|
+
<span className="text-sm text-zinc-500">{r.value}</span>
|
|
386
|
+
</li>
|
|
387
|
+
))}
|
|
388
|
+
</ul>
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/* ────────────────────────────────────────────────────────────── */
|
|
393
|
+
/* Tab bar — mobile = full-width white dock, desktop = glass pill */
|
|
394
|
+
/* ────────────────────────────────────────────────────────────── */
|
|
395
|
+
const SheetTabBar = forwardRef<
|
|
396
|
+
HTMLDivElement,
|
|
397
|
+
{ tab: TabId; onChange: (t: TabId) => void; isDesktop: boolean }
|
|
398
|
+
>(function SheetTabBar({ tab, onChange, isDesktop }, ref) {
|
|
399
|
+
const tabs: Array<{ id: TabId; icon: React.ComponentType<{ className?: string }>; label: string }> = [
|
|
400
|
+
{ id: 'wallet', icon: Wallet, label: 'Wallet' },
|
|
401
|
+
{ id: 'history', icon: History, label: 'History' },
|
|
402
|
+
{ id: 'market', icon: LineChart, label: 'Market' },
|
|
403
|
+
{ id: 'settings', icon: Settings, label: 'Settings' },
|
|
404
|
+
];
|
|
405
|
+
|
|
406
|
+
if (isDesktop) {
|
|
407
|
+
return (
|
|
408
|
+
<div
|
|
409
|
+
ref={ref}
|
|
410
|
+
className="absolute bottom-6 left-1/2 -translate-x-1/2 z-30"
|
|
411
|
+
>
|
|
412
|
+
<div className="glass-pill flex items-center gap-1 p-1.5 rounded-full">
|
|
413
|
+
{tabs.map((t) => {
|
|
414
|
+
const active = tab === t.id;
|
|
415
|
+
const Icon = t.icon;
|
|
416
|
+
return (
|
|
417
|
+
<button
|
|
418
|
+
key={t.id}
|
|
419
|
+
type="button"
|
|
420
|
+
onClick={() => onChange(t.id)}
|
|
421
|
+
className={`flex items-center gap-1.5 px-4 py-2 rounded-full text-sm font-medium transition ${
|
|
422
|
+
active
|
|
423
|
+
? 'glass-inner-active text-white'
|
|
424
|
+
: 'text-white/70 hover:text-white hover:bg-white/5'
|
|
425
|
+
}`}
|
|
426
|
+
>
|
|
427
|
+
<Icon className="h-4 w-4" />
|
|
428
|
+
<span>{t.label}</span>
|
|
429
|
+
</button>
|
|
430
|
+
);
|
|
431
|
+
})}
|
|
432
|
+
</div>
|
|
433
|
+
</div>
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Mobile: rounded-top dock with upward shadow, sheet sits flush.
|
|
438
|
+
return (
|
|
439
|
+
<div
|
|
440
|
+
ref={ref}
|
|
441
|
+
className="absolute bottom-0 left-0 right-0 z-30 bg-white"
|
|
442
|
+
style={{
|
|
443
|
+
paddingBottom: 'env(safe-area-inset-bottom)',
|
|
444
|
+
borderTopLeftRadius: 24,
|
|
445
|
+
borderTopRightRadius: 24,
|
|
446
|
+
boxShadow: '0 -10px 24px -8px rgba(0, 0, 0, 0.18)',
|
|
447
|
+
}}
|
|
448
|
+
>
|
|
449
|
+
<div className="mx-auto max-w-[720px] flex items-center justify-around px-2 pt-2.5 pb-2">
|
|
450
|
+
{tabs.map((t) => {
|
|
451
|
+
const active = tab === t.id;
|
|
452
|
+
const Icon = t.icon;
|
|
453
|
+
return (
|
|
454
|
+
<button
|
|
455
|
+
key={t.id}
|
|
456
|
+
type="button"
|
|
457
|
+
onClick={() => onChange(t.id)}
|
|
458
|
+
className={`flex flex-col items-center gap-0.5 px-4 py-1.5 rounded-xl transition ${
|
|
459
|
+
active ? 'text-zinc-900' : 'text-zinc-400 hover:text-zinc-700'
|
|
460
|
+
}`}
|
|
461
|
+
>
|
|
462
|
+
<Icon className="h-5 w-5" />
|
|
463
|
+
<span className={`text-[11px] ${active ? 'font-semibold' : 'font-medium'}`}>{t.label}</span>
|
|
464
|
+
</button>
|
|
465
|
+
);
|
|
466
|
+
})}
|
|
467
|
+
</div>
|
|
468
|
+
</div>
|
|
469
|
+
);
|
|
470
|
+
});
|