@hfunlabs/hypurr-connect 0.1.12 → 0.1.13

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.
@@ -0,0 +1,982 @@
1
+ import { AnimatePresence, motion } from "framer-motion";
2
+ import type { HyperliquidWallet } from "hypurr-grpc/ts/hypurr/wallet";
3
+ import {
4
+ useCallback,
5
+ useState,
6
+ type CSSProperties,
7
+ type ReactNode,
8
+ } from "react";
9
+ import { DeleteWalletModal } from "./DeleteWalletModal";
10
+ import { useHypurrConnectInternal } from "./HypurrConnectProvider";
11
+ import { RenameWalletModal } from "./RenameWalletModal";
12
+ import {
13
+ Copy,
14
+ LayoutDashboard,
15
+ Pencil,
16
+ SpinKeyframes,
17
+ Star,
18
+ Trash2,
19
+ TrendingUp,
20
+ User,
21
+ Wallet,
22
+ X,
23
+ Zap,
24
+ } from "./icons/lucide";
25
+ import {
26
+ closeBtnStyle as makeCloseBtnStyle,
27
+ fontFamily,
28
+ modalBackdropStyle,
29
+ modalHeaderStyle,
30
+ modalPanelStyle,
31
+ modalWrapperStyle,
32
+ type PrincipalColorOverrides,
33
+ type PrincipalColors,
34
+ profileColors,
35
+ raisedButtonStyle,
36
+ resolvePrincipalColors,
37
+ titleStyle,
38
+ upperLabelStyle,
39
+ } from "./profileStyles";
40
+
41
+ type SettingsTab = "ui" | "trading" | "wallets";
42
+
43
+ export interface SlippageOption {
44
+ label: string;
45
+ value: number;
46
+ }
47
+
48
+ const DEFAULT_SLIPPAGE_OPTIONS: SlippageOption[] = [
49
+ { label: "0.5%", value: 0.005 },
50
+ { label: "1%", value: 0.01 },
51
+ { label: "2%", value: 0.02 },
52
+ { label: "5%", value: 0.05 },
53
+ { label: "10%", value: 0.1 },
54
+ ];
55
+
56
+ export interface UserProfileModalProps {
57
+ isOpen: boolean;
58
+ onClose: () => void;
59
+ /** Current value for the "Animations" toggle. */
60
+ animationsEnabled: boolean;
61
+ /** Called when the user flips the animations toggle. */
62
+ onToggleAnimations: () => void;
63
+ /** Current default slippage value (e.g. 0.01 = 1%). */
64
+ defaultSlippage: number;
65
+ /** Called when the user picks a slippage option. */
66
+ onSlippageChange: (value: number) => void;
67
+ /** Override the slippage choices. Defaults to 0.5/1/2/5/10%. */
68
+ slippageOptions?: SlippageOption[];
69
+ /**
70
+ * Fired after the SDK successfully deletes a wallet. Use this for extra
71
+ * side effects on the host (e.g. invalidating React Query caches).
72
+ */
73
+ onWalletDeleted?: (walletId: number) => void;
74
+ /**
75
+ * Fired after the SDK successfully renames a wallet. Use this for extra
76
+ * side effects on the host (e.g. invalidating React Query caches).
77
+ */
78
+ onWalletRenamed?: (walletId: number, name: string) => void;
79
+ /**
80
+ * If provided, each wallet row in the Wallets tab gets a "View portfolio"
81
+ * button that fires this callback. The host is expected to open its own
82
+ * portfolio UI in response.
83
+ */
84
+ onShowPortfolio?: (wallet: HyperliquidWallet) => void;
85
+ /** Optional toast callback. SDK fires success on rename/delete; errors stay inline in sub-modals. */
86
+ onNotify?: (n: { type: "success" | "error"; message: string }) => void;
87
+ /** Shorthand for `principalColors.accent`. Defaults to `#a855f7`. */
88
+ accentColor?: string;
89
+ /** Principal accent colors used for switches, action text, and wallet icon surfaces. */
90
+ principalColors?: PrincipalColorOverrides;
91
+ }
92
+
93
+ const backdropStyle = modalBackdropStyle(100);
94
+ const wrapperStyle = modalWrapperStyle(101);
95
+ const panelStyle = modalPanelStyle(false);
96
+ const headerStyle = modalHeaderStyle;
97
+
98
+ const profileSectionStyle: CSSProperties = {
99
+ padding: "20px 24px",
100
+ borderBottom: `1px solid ${profileColors.border}`,
101
+ };
102
+
103
+ const settingsSectionStyle: CSSProperties = {
104
+ padding: "20px 24px",
105
+ };
106
+
107
+ const tabRowStyle: CSSProperties = {
108
+ display: "flex",
109
+ gap: 6,
110
+ marginBottom: 20,
111
+ };
112
+
113
+ const tabButtonLayoutStyle: CSSProperties = {
114
+ flex: 1,
115
+ padding: "6px 0",
116
+ borderRadius: 4,
117
+ fontSize: 12.5,
118
+ lineHeight: "1rem",
119
+ fontWeight: 500,
120
+ display: "flex",
121
+ alignItems: "center",
122
+ justifyContent: "center",
123
+ gap: 6,
124
+ cursor: "pointer",
125
+ transition: "background-color 150ms, color 150ms, box-shadow 150ms",
126
+ };
127
+
128
+ const statBoxStyle: CSSProperties = {
129
+ background: "rgba(255,255,255,0.03)",
130
+ borderRadius: 8,
131
+ padding: 12,
132
+ border: `1px solid ${profileColors.border}`,
133
+ };
134
+
135
+ const slippageBtnBaseStyle: CSSProperties = {
136
+ flex: 1,
137
+ padding: "4px 0",
138
+ borderRadius: 4,
139
+ fontSize: 12.5,
140
+ lineHeight: "1rem",
141
+ fontFamily: fontFamily.mono,
142
+ fontWeight: 500,
143
+ cursor: "pointer",
144
+ transition: "background-color 150ms, color 150ms, border-color 150ms",
145
+ };
146
+
147
+ const walletRowStyle: CSSProperties = {
148
+ display: "flex",
149
+ alignItems: "center",
150
+ gap: 12,
151
+ padding: "10px 12px",
152
+ borderRadius: 8,
153
+ background: profileColors.surfaceBtn,
154
+ border: `1px solid ${profileColors.surfaceBd}`,
155
+ transition: "background-color 150ms, border-color 150ms",
156
+ };
157
+
158
+ function ToggleSwitch({
159
+ checked,
160
+ onChange,
161
+ accentColor,
162
+ }: {
163
+ checked: boolean;
164
+ onChange: () => void;
165
+ accentColor: string;
166
+ }) {
167
+ return (
168
+ <button
169
+ onClick={onChange}
170
+ role="switch"
171
+ aria-checked={checked}
172
+ style={{
173
+ position: "relative",
174
+ display: "inline-flex",
175
+ height: 20,
176
+ width: 36,
177
+ alignItems: "center",
178
+ borderRadius: 9999,
179
+ border: "none",
180
+ background: checked ? accentColor : "rgba(255,255,255,0.08)",
181
+ cursor: "pointer",
182
+ padding: 0,
183
+ transition: "background 150ms",
184
+ }}
185
+ >
186
+ <span
187
+ style={{
188
+ display: "inline-block",
189
+ height: 12,
190
+ width: 12,
191
+ borderRadius: "50%",
192
+ background: profileColors.text,
193
+ transform: checked ? "translateX(20px)" : "translateX(4px)",
194
+ transition: "transform 150ms",
195
+ }}
196
+ />
197
+ </button>
198
+ );
199
+ }
200
+
201
+ function RaisedButton({
202
+ selected = false,
203
+ onClick,
204
+ children,
205
+ style,
206
+ }: {
207
+ selected?: boolean;
208
+ onClick: () => void;
209
+ children: ReactNode;
210
+ style?: CSSProperties;
211
+ }) {
212
+ const [hovered, setHovered] = useState(false);
213
+ return (
214
+ <button
215
+ type="button"
216
+ onClick={onClick}
217
+ onMouseEnter={() => setHovered(true)}
218
+ onMouseLeave={() => setHovered(false)}
219
+ style={{
220
+ ...tabButtonLayoutStyle,
221
+ ...raisedButtonStyle(
222
+ selected ? "active" : hovered ? "hover" : "default",
223
+ ),
224
+ ...style,
225
+ }}
226
+ >
227
+ {children}
228
+ </button>
229
+ );
230
+ }
231
+
232
+ function SlippageButton({
233
+ selected,
234
+ onClick,
235
+ children,
236
+ }: {
237
+ selected: boolean;
238
+ onClick: () => void;
239
+ children: ReactNode;
240
+ }) {
241
+ const [hovered, setHovered] = useState(false);
242
+ return (
243
+ <button
244
+ type="button"
245
+ onClick={onClick}
246
+ onMouseEnter={() => setHovered(true)}
247
+ onMouseLeave={() => setHovered(false)}
248
+ style={{
249
+ ...slippageBtnBaseStyle,
250
+ background: selected
251
+ ? profileColors.surfaceBtnActive
252
+ : hovered
253
+ ? profileColors.surfaceBtnHover
254
+ : profileColors.surfaceBtn,
255
+ border: `1px solid ${
256
+ selected
257
+ ? profileColors.surfaceBdActive
258
+ : hovered
259
+ ? profileColors.surfaceBdHover
260
+ : profileColors.surfaceBd
261
+ }`,
262
+ color: selected
263
+ ? profileColors.text
264
+ : hovered
265
+ ? "#d1d5db"
266
+ : profileColors.muted,
267
+ }}
268
+ >
269
+ {children}
270
+ </button>
271
+ );
272
+ }
273
+
274
+ export function UserProfileModal({
275
+ isOpen,
276
+ onClose,
277
+ animationsEnabled,
278
+ onToggleAnimations,
279
+ defaultSlippage,
280
+ onSlippageChange,
281
+ slippageOptions = DEFAULT_SLIPPAGE_OPTIONS,
282
+ onWalletDeleted,
283
+ onWalletRenamed,
284
+ onShowPortfolio,
285
+ onNotify,
286
+ accentColor,
287
+ principalColors,
288
+ }: UserProfileModalProps): ReactNode {
289
+ const {
290
+ user,
291
+ wallets: userWallets,
292
+ deleteWallet,
293
+ renameWallet,
294
+ authMethod,
295
+ } = useHypurrConnectInternal();
296
+
297
+ const [settingsTab, setSettingsTab] = useState<SettingsTab>("ui");
298
+ const [walletToDelete, setWalletToDelete] =
299
+ useState<HyperliquidWallet | null>(null);
300
+ const [walletToRename, setWalletToRename] =
301
+ useState<HyperliquidWallet | null>(null);
302
+
303
+ const profilePic = user?.photoUrl || "";
304
+ const displayName = user?.displayName || "";
305
+ const hfunScore = user?.hfunScore ?? null;
306
+ const reputationScore = user?.reputationScore ?? null;
307
+ const telegramId = user?.telegramId;
308
+ const wallets = userWallets ?? [];
309
+ const colors = resolvePrincipalColors(accentColor, principalColors);
310
+
311
+ const handleDeleteWallet = useCallback(
312
+ async (walletId: number) => {
313
+ await deleteWallet(walletId);
314
+ onWalletDeleted?.(walletId);
315
+ },
316
+ [deleteWallet, onWalletDeleted],
317
+ );
318
+
319
+ const handleRenameWallet = useCallback(
320
+ async (walletId: number, name: string) => {
321
+ await renameWallet(walletId, name);
322
+ onWalletRenamed?.(walletId, name);
323
+ },
324
+ [renameWallet, onWalletRenamed],
325
+ );
326
+
327
+ const handleCopyAddress = useCallback(() => {
328
+ if (!displayName) return;
329
+ navigator.clipboard.writeText(displayName);
330
+ onNotify?.({ type: "success", message: "Address copied" });
331
+ }, [displayName, onNotify]);
332
+
333
+ return (
334
+ <>
335
+ <AnimatePresence>
336
+ {isOpen && (
337
+ <>
338
+ <SpinKeyframes />
339
+ <motion.div
340
+ key="backdrop"
341
+ style={backdropStyle}
342
+ initial={{ opacity: 0 }}
343
+ animate={{ opacity: 1 }}
344
+ exit={{ opacity: 0 }}
345
+ transition={{ duration: 0.15 }}
346
+ onClick={onClose}
347
+ />
348
+ <div style={wrapperStyle} onClick={onClose}>
349
+ <motion.div
350
+ key="panel"
351
+ style={panelStyle}
352
+ initial={{ opacity: 0, y: 8 }}
353
+ animate={{ opacity: 1, y: 0 }}
354
+ exit={{ opacity: 0, y: 8 }}
355
+ transition={{ duration: 0.18, ease: "easeOut" }}
356
+ onClick={(e) => e.stopPropagation()}
357
+ >
358
+ {/* Header */}
359
+ <div style={headerStyle}>
360
+ <h3 style={titleStyle}>Profile & Settings</h3>
361
+ <button
362
+ onClick={onClose}
363
+ style={makeCloseBtnStyle()}
364
+ aria-label="Close"
365
+ >
366
+ <X size={16} />
367
+ </button>
368
+ </div>
369
+
370
+ {/* Profile Section */}
371
+ <div style={profileSectionStyle}>
372
+ {authMethod === "eoa" && !profilePic ? (
373
+ <div
374
+ style={{
375
+ padding: "10px 12px",
376
+ background: "rgba(255,255,255,0.03)",
377
+ border: `1px solid ${profileColors.border}`,
378
+ borderRadius: 8,
379
+ }}
380
+ >
381
+ <p
382
+ style={{
383
+ margin: "0 0 6px",
384
+ color: profileColors.muted,
385
+ ...upperLabelStyle,
386
+ }}
387
+ >
388
+ Wallet
389
+ </p>
390
+ <div
391
+ style={{
392
+ display: "flex",
393
+ alignItems: "center",
394
+ justifyContent: "space-between",
395
+ gap: 8,
396
+ }}
397
+ >
398
+ <p
399
+ style={{
400
+ margin: 0,
401
+ fontSize: 12.5,
402
+ lineHeight: "1rem",
403
+ color: profileColors.text,
404
+ fontFamily: fontFamily.mono,
405
+ wordBreak: "break-all",
406
+ }}
407
+ >
408
+ {displayName}
409
+ </p>
410
+ <button
411
+ onClick={handleCopyAddress}
412
+ style={{
413
+ background: "transparent",
414
+ border: "none",
415
+ color: profileColors.muted,
416
+ cursor: "pointer",
417
+ flexShrink: 0,
418
+ display: "flex",
419
+ }}
420
+ aria-label="Copy address"
421
+ >
422
+ <Copy size={13} />
423
+ </button>
424
+ </div>
425
+ </div>
426
+ ) : (
427
+ <div
428
+ style={{
429
+ display: "flex",
430
+ alignItems: "center",
431
+ gap: 16,
432
+ }}
433
+ >
434
+ {profilePic ? (
435
+ <img
436
+ src={profilePic}
437
+ alt="Profile"
438
+ style={{
439
+ height: 56,
440
+ width: 56,
441
+ borderRadius: "50%",
442
+ border: `1px solid ${profileColors.surfaceBd}`,
443
+ }}
444
+ />
445
+ ) : (
446
+ <div
447
+ style={{
448
+ height: 56,
449
+ width: 56,
450
+ borderRadius: "50%",
451
+ border: `1px solid ${profileColors.surfaceBd}`,
452
+ background: profileColors.surfaceBd,
453
+ display: "flex",
454
+ alignItems: "center",
455
+ justifyContent: "center",
456
+ fontSize: 18,
457
+ lineHeight: "1.75rem",
458
+ fontWeight: 700,
459
+ color: profileColors.text,
460
+ }}
461
+ >
462
+ {displayName?.charAt(0)}
463
+ </div>
464
+ )}
465
+ <div style={{ flex: 1, minWidth: 0 }}>
466
+ <h4
467
+ style={{
468
+ margin: 0,
469
+ fontSize: 14,
470
+ lineHeight: "1.25rem",
471
+ fontWeight: 600,
472
+ color: profileColors.text,
473
+ overflow: "hidden",
474
+ textOverflow: "ellipsis",
475
+ whiteSpace: "nowrap",
476
+ }}
477
+ >
478
+ {displayName}
479
+ </h4>
480
+ {telegramId && (
481
+ <p
482
+ style={{
483
+ margin: "2px 0 0",
484
+ fontSize: 12.5,
485
+ lineHeight: "1rem",
486
+ color: profileColors.muted,
487
+ }}
488
+ >
489
+ ID: {telegramId}
490
+ </p>
491
+ )}
492
+ </div>
493
+ </div>
494
+ )}
495
+
496
+ {user && (
497
+ <div
498
+ style={{
499
+ marginTop: 16,
500
+ display: "grid",
501
+ gridTemplateColumns: "1fr 1fr",
502
+ gap: 10,
503
+ }}
504
+ >
505
+ <div style={statBoxStyle}>
506
+ <div
507
+ style={{
508
+ display: "flex",
509
+ alignItems: "center",
510
+ gap: 6,
511
+ color: "#eab308",
512
+ marginBottom: 6,
513
+ }}
514
+ >
515
+ <Star size={13} color="#eab308" fill="#eab308" />
516
+ <span style={upperLabelStyle}>Hfun Score</span>
517
+ </div>
518
+ <p
519
+ style={{
520
+ margin: 0,
521
+ fontSize: 18,
522
+ lineHeight: "1.75rem",
523
+ fontWeight: 700,
524
+ color: profileColors.text,
525
+ fontFamily: fontFamily.mono,
526
+ }}
527
+ >
528
+ {hfunScore !== null ? hfunScore.toFixed(0) : "--"}
529
+ </p>
530
+ </div>
531
+ <div style={statBoxStyle}>
532
+ <div
533
+ style={{
534
+ display: "flex",
535
+ alignItems: "center",
536
+ gap: 6,
537
+ color: profileColors.muted,
538
+ marginBottom: 6,
539
+ }}
540
+ >
541
+ <User size={13} />
542
+ <span style={upperLabelStyle}>Reputation</span>
543
+ </div>
544
+ <p
545
+ style={{
546
+ margin: 0,
547
+ fontSize: 18,
548
+ lineHeight: "1.75rem",
549
+ fontWeight: 700,
550
+ color: profileColors.text,
551
+ fontFamily: fontFamily.mono,
552
+ }}
553
+ >
554
+ {reputationScore !== null
555
+ ? reputationScore.toFixed(0)
556
+ : "--"}
557
+ </p>
558
+ </div>
559
+ </div>
560
+ )}
561
+ </div>
562
+
563
+ {/* Settings Section */}
564
+ <div style={settingsSectionStyle}>
565
+ <div style={tabRowStyle}>
566
+ <RaisedButton
567
+ selected={settingsTab === "ui"}
568
+ onClick={() => setSettingsTab("ui")}
569
+ >
570
+ <Zap size={13} /> UI
571
+ </RaisedButton>
572
+ <RaisedButton
573
+ selected={settingsTab === "trading"}
574
+ onClick={() => setSettingsTab("trading")}
575
+ >
576
+ <TrendingUp size={13} /> Trading
577
+ </RaisedButton>
578
+ <RaisedButton
579
+ selected={settingsTab === "wallets"}
580
+ onClick={() => setSettingsTab("wallets")}
581
+ >
582
+ <Wallet size={13} /> Wallets
583
+ </RaisedButton>
584
+ </div>
585
+
586
+ {settingsTab === "ui" && (
587
+ <div
588
+ style={{
589
+ display: "flex",
590
+ flexDirection: "column",
591
+ gap: 12,
592
+ }}
593
+ >
594
+ <div
595
+ style={{
596
+ display: "flex",
597
+ alignItems: "center",
598
+ justifyContent: "space-between",
599
+ padding: "8px 0",
600
+ }}
601
+ >
602
+ <div
603
+ style={{
604
+ display: "flex",
605
+ alignItems: "center",
606
+ gap: 12,
607
+ }}
608
+ >
609
+ <Zap size={16} color={profileColors.muted} />
610
+ <div>
611
+ <p
612
+ style={{
613
+ margin: 0,
614
+ fontSize: 12.5,
615
+ lineHeight: "1rem",
616
+ fontWeight: 500,
617
+ color: profileColors.text,
618
+ }}
619
+ >
620
+ Animations
621
+ </p>
622
+ <p
623
+ style={{
624
+ margin: 0,
625
+ fontSize: 12.5,
626
+ lineHeight: "1rem",
627
+ color: profileColors.muted,
628
+ }}
629
+ >
630
+ Enable chart and UI animations
631
+ </p>
632
+ </div>
633
+ </div>
634
+ <ToggleSwitch
635
+ checked={animationsEnabled}
636
+ onChange={onToggleAnimations}
637
+ accentColor={colors.accent}
638
+ />
639
+ </div>
640
+ </div>
641
+ )}
642
+
643
+ {settingsTab === "trading" && (
644
+ <div style={{ padding: "8px 0" }}>
645
+ <div
646
+ style={{
647
+ display: "flex",
648
+ alignItems: "center",
649
+ gap: 12,
650
+ marginBottom: 12,
651
+ }}
652
+ >
653
+ <TrendingUp size={16} color={profileColors.muted} />
654
+ <div>
655
+ <p
656
+ style={{
657
+ margin: 0,
658
+ fontSize: 12.5,
659
+ lineHeight: "1rem",
660
+ fontWeight: 500,
661
+ color: profileColors.text,
662
+ }}
663
+ >
664
+ Default Slippage
665
+ </p>
666
+ <p
667
+ style={{
668
+ margin: 0,
669
+ fontSize: 12.5,
670
+ lineHeight: "1rem",
671
+ color: profileColors.muted,
672
+ }}
673
+ >
674
+ Max price deviation for market orders
675
+ </p>
676
+ </div>
677
+ </div>
678
+ <div style={{ display: "flex", gap: 6 }}>
679
+ {slippageOptions.map((option) => (
680
+ <SlippageButton
681
+ key={option.value}
682
+ onClick={() => onSlippageChange(option.value)}
683
+ selected={defaultSlippage === option.value}
684
+ >
685
+ {option.label}
686
+ </SlippageButton>
687
+ ))}
688
+ </div>
689
+ </div>
690
+ )}
691
+
692
+ {settingsTab === "wallets" && (
693
+ <div
694
+ style={{
695
+ display: "flex",
696
+ flexDirection: "column",
697
+ gap: 8,
698
+ }}
699
+ >
700
+ {wallets.length === 0 ? (
701
+ <div
702
+ style={{
703
+ textAlign: "center",
704
+ padding: "32px 0",
705
+ }}
706
+ >
707
+ <div
708
+ style={{
709
+ width: 40,
710
+ height: 40,
711
+ borderRadius: "50%",
712
+ background: "rgba(255,255,255,0.04)",
713
+ display: "flex",
714
+ alignItems: "center",
715
+ justifyContent: "center",
716
+ margin: "0 auto 12px",
717
+ }}
718
+ >
719
+ <Wallet size={18} color={profileColors.muted} />
720
+ </div>
721
+ <p
722
+ style={{
723
+ margin: 0,
724
+ fontSize: 12.5,
725
+ lineHeight: "1rem",
726
+ color: profileColors.muted,
727
+ }}
728
+ >
729
+ No wallets yet
730
+ </p>
731
+ </div>
732
+ ) : (
733
+ <div
734
+ style={{
735
+ maxHeight: 208,
736
+ overflowY: "auto",
737
+ display: "flex",
738
+ flexDirection: "column",
739
+ gap: 6,
740
+ }}
741
+ >
742
+ {wallets.map((wallet) => (
743
+ <WalletRow
744
+ key={wallet.id}
745
+ wallet={wallet}
746
+ colors={colors}
747
+ onRename={() => setWalletToRename(wallet)}
748
+ onDelete={() => setWalletToDelete(wallet)}
749
+ onShowPortfolio={
750
+ onShowPortfolio
751
+ ? () => onShowPortfolio(wallet)
752
+ : undefined
753
+ }
754
+ />
755
+ ))}
756
+ </div>
757
+ )}
758
+ <p
759
+ style={{
760
+ margin: "4px 0 0",
761
+ fontSize: 12.5,
762
+ lineHeight: "1rem",
763
+ color: profileColors.subdued,
764
+ textAlign: "center",
765
+ }}
766
+ >
767
+ {wallets.length} wallet
768
+ {wallets.length !== 1 ? "s" : ""} linked
769
+ </p>
770
+ </div>
771
+ )}
772
+ </div>
773
+
774
+ {/* Footer */}
775
+ <div style={{ padding: "0 24px 24px" }}>
776
+ <button
777
+ onClick={onClose}
778
+ style={{
779
+ ...raisedButtonStyle("default"),
780
+ width: "100%",
781
+ padding: "6px 0",
782
+ fontSize: 12.5,
783
+ lineHeight: "1rem",
784
+ fontWeight: 500,
785
+ borderRadius: 8,
786
+ cursor: "pointer",
787
+ }}
788
+ >
789
+ Close
790
+ </button>
791
+ </div>
792
+ </motion.div>
793
+ </div>
794
+ </>
795
+ )}
796
+ </AnimatePresence>
797
+
798
+ <DeleteWalletModal
799
+ isOpen={!!walletToDelete}
800
+ onClose={() => setWalletToDelete(null)}
801
+ wallet={walletToDelete}
802
+ onConfirm={handleDeleteWallet}
803
+ onNotify={onNotify}
804
+ />
805
+ <RenameWalletModal
806
+ isOpen={!!walletToRename}
807
+ onClose={() => setWalletToRename(null)}
808
+ wallet={walletToRename}
809
+ onConfirm={handleRenameWallet}
810
+ onNotify={onNotify}
811
+ accentColor={colors.accent}
812
+ />
813
+ </>
814
+ );
815
+ }
816
+
817
+ function WalletRow({
818
+ wallet,
819
+ colors,
820
+ onRename,
821
+ onDelete,
822
+ onShowPortfolio,
823
+ }: {
824
+ wallet: HyperliquidWallet;
825
+ colors: PrincipalColors;
826
+ onRename: () => void;
827
+ onDelete: () => void;
828
+ onShowPortfolio?: () => void;
829
+ }) {
830
+ const [hovered, setHovered] = useState(false);
831
+ return (
832
+ <div
833
+ style={{
834
+ ...walletRowStyle,
835
+ background: hovered
836
+ ? profileColors.surfaceBtnHover
837
+ : profileColors.surfaceBtn,
838
+ borderColor: hovered
839
+ ? profileColors.surfaceBdHover
840
+ : profileColors.surfaceBd,
841
+ }}
842
+ onMouseEnter={() => setHovered(true)}
843
+ onMouseLeave={() => setHovered(false)}
844
+ >
845
+ <div
846
+ style={{
847
+ width: 32,
848
+ height: 32,
849
+ borderRadius: 6,
850
+ background: colors.accentBackground,
851
+ border: `1px solid ${colors.accentBorder}`,
852
+ display: "flex",
853
+ alignItems: "center",
854
+ justifyContent: "center",
855
+ flexShrink: 0,
856
+ }}
857
+ >
858
+ <span
859
+ style={{
860
+ fontSize: 12.5,
861
+ lineHeight: "1rem",
862
+ fontWeight: 600,
863
+ color: colors.accentText,
864
+ }}
865
+ >
866
+ {(wallet.name || "W")[0].toUpperCase()}
867
+ </span>
868
+ </div>
869
+ <div style={{ flex: 1, minWidth: 0 }}>
870
+ <p
871
+ style={{
872
+ margin: 0,
873
+ fontSize: 12.5,
874
+ lineHeight: "1rem",
875
+ fontWeight: 500,
876
+ color: profileColors.text,
877
+ overflow: "hidden",
878
+ textOverflow: "ellipsis",
879
+ whiteSpace: "nowrap",
880
+ }}
881
+ >
882
+ {wallet.name || "Unnamed"}
883
+ </p>
884
+ <p
885
+ style={{
886
+ margin: 0,
887
+ fontSize: 12.5,
888
+ lineHeight: "1rem",
889
+ color: profileColors.muted,
890
+ fontFamily: fontFamily.mono,
891
+ }}
892
+ >
893
+ {wallet.ethereumAddress?.slice(0, 6)}...
894
+ {wallet.ethereumAddress?.slice(-4)}
895
+ </p>
896
+ </div>
897
+ <div
898
+ style={{
899
+ display: "flex",
900
+ alignItems: "center",
901
+ gap: 4,
902
+ flexShrink: 0,
903
+ opacity: hovered ? 1 : 0,
904
+ transition: "opacity 120ms",
905
+ }}
906
+ >
907
+ {onShowPortfolio && (
908
+ <IconBtn
909
+ color={colors.accent}
910
+ hoverBackgroundColor={colors.accentHoverBackground}
911
+ title="View portfolio"
912
+ ariaLabel={`View portfolio for ${wallet.name || "wallet"}`}
913
+ onClick={onShowPortfolio}
914
+ >
915
+ <LayoutDashboard size={13} />
916
+ </IconBtn>
917
+ )}
918
+ <IconBtn
919
+ color={colors.accent}
920
+ hoverBackgroundColor={colors.accentHoverBackground}
921
+ title="Rename wallet"
922
+ ariaLabel={`Rename ${wallet.name || "wallet"}`}
923
+ onClick={onRename}
924
+ >
925
+ <Pencil size={13} />
926
+ </IconBtn>
927
+ <IconBtn
928
+ color="#ef4444"
929
+ hoverBackgroundColor="rgba(239,68,68,0.1)"
930
+ title="Delete wallet"
931
+ ariaLabel={`Delete ${wallet.name || "wallet"}`}
932
+ onClick={onDelete}
933
+ >
934
+ <Trash2 size={13} />
935
+ </IconBtn>
936
+ </div>
937
+ </div>
938
+ );
939
+ }
940
+
941
+ function IconBtn({
942
+ children,
943
+ color,
944
+ hoverBackgroundColor,
945
+ title,
946
+ ariaLabel,
947
+ onClick,
948
+ }: {
949
+ children: ReactNode;
950
+ color: string;
951
+ hoverBackgroundColor?: string;
952
+ title: string;
953
+ ariaLabel: string;
954
+ onClick: () => void;
955
+ }) {
956
+ const [hovered, setHovered] = useState(false);
957
+ return (
958
+ <button
959
+ onMouseEnter={() => setHovered(true)}
960
+ onMouseLeave={() => setHovered(false)}
961
+ onClick={onClick}
962
+ title={title}
963
+ aria-label={ariaLabel}
964
+ style={{
965
+ padding: 6,
966
+ borderRadius: 4,
967
+ background: hovered
968
+ ? (hoverBackgroundColor ?? "transparent")
969
+ : "transparent",
970
+ border: "none",
971
+ color: hovered ? color : profileColors.disabled,
972
+ cursor: "pointer",
973
+ display: "flex",
974
+ alignItems: "center",
975
+ justifyContent: "center",
976
+ transition: "color 120ms, background 120ms",
977
+ }}
978
+ >
979
+ {children}
980
+ </button>
981
+ );
982
+ }