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.
Files changed (30) hide show
  1. package/package.json +1 -1
  2. package/supervisor/harnesses/codex.ts +34 -0
  3. package/worker/prompts/bloby-system-prompt.txt +2 -2
  4. package/workspace/client/index.html +3 -0
  5. package/workspace/client/public/.well-known/assetlinks.json +8 -0
  6. package/workspace/client/public/bloby-cyberpunk.png +0 -0
  7. package/workspace/client/public/brand/blackrock.svg +8 -0
  8. package/workspace/client/public/kid-breakfast.png +0 -0
  9. package/workspace/client/public/wallpapers/bg.jpg +0 -0
  10. package/workspace/client/public/wallpapers/crypto_bg.png +0 -0
  11. package/workspace/client/public/wallpapers/wp-dusk.jpg +0 -0
  12. package/workspace/client/public/wallpapers/wp-mountain.jpg +0 -0
  13. package/workspace/client/public/wallpapers/wp-ocean.jpg +0 -0
  14. package/workspace/client/src/App.tsx +20 -9
  15. package/workspace/client/src/components/Dashboard/AiChatPage.tsx +145 -0
  16. package/workspace/client/src/components/Dashboard/CryptoPage.tsx +470 -0
  17. package/workspace/client/src/components/Dashboard/DashboardPage.tsx +117 -113
  18. package/workspace/client/src/components/Dashboard/WishlistPage.tsx +464 -0
  19. package/workspace/client/src/components/Layout/DashboardLayout.tsx +32 -34
  20. package/workspace/client/src/components/Layout/MiniSidebar.tsx +64 -0
  21. package/workspace/client/src/components/Layout/MobileNav.tsx +103 -6
  22. package/workspace/client/src/components/Layout/Sidebar.tsx +11 -10
  23. package/workspace/client/src/components/Lock/PinInput.tsx +107 -0
  24. package/workspace/client/src/components/Lock/WorkspaceLock.tsx +484 -0
  25. package/workspace/client/src/components/StickyNotes/StickyNotesOverlay.tsx +396 -0
  26. package/workspace/client/src/components/StickyNotes/StickyNotesSettingsPage.tsx +427 -0
  27. package/workspace/client/src/components/Wallpaper/WallpaperBackground.tsx +12 -0
  28. package/workspace/client/src/components/Wallpaper/WallpaperContext.tsx +160 -0
  29. package/workspace/client/src/components/Wallpaper/WallpaperPicker.tsx +67 -0
  30. 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
+ });