@hfunlabs/hypurr-connect 0.1.11 → 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,797 @@
1
+ import { AnimatePresence, motion } from "framer-motion";
2
+ import type { HyperliquidWallet } from "hypurr-grpc/ts/hypurr/wallet";
3
+ import {
4
+ Fragment,
5
+ useCallback,
6
+ useMemo,
7
+ useState,
8
+ type CSSProperties,
9
+ type MouseEvent as ReactMouseEvent,
10
+ type ReactNode,
11
+ } from "react";
12
+ import { useHypurrConnectInternal } from "./HypurrConnectProvider";
13
+ import { UserProfileModal, type SlippageOption } from "./UserProfileModal";
14
+ import {
15
+ Copy,
16
+ Crown,
17
+ Folder,
18
+ LayoutDashboard,
19
+ LogOut,
20
+ Plus,
21
+ Star,
22
+ User,
23
+ Wallet,
24
+ } from "./icons/lucide";
25
+ import {
26
+ type PrincipalColorOverrides,
27
+ type PrincipalColors,
28
+ profileColors,
29
+ resolvePrincipalColors,
30
+ } from "./profileStyles";
31
+
32
+ export interface WalletSelectorDropdownProps {
33
+ isOpen: boolean;
34
+ onClose: () => void;
35
+ /** Called when the user clicks "Add Wallet". Host renders the add-wallet UI. */
36
+ onAddWallet?: () => void;
37
+ /** Called when the user clicks the portfolio icon on a wallet row. */
38
+ onShowPortfolio?: (wallet: HyperliquidWallet) => void;
39
+ /**
40
+ * Called before the SDK's `logout()` runs. Host can do extra cleanup
41
+ * (e.g. wagmi `disconnect()`).
42
+ */
43
+ onLogout?: () => void;
44
+ /** Toast callback. Fires "Address copied" success on copy-address clicks. */
45
+ onNotify?: (n: { type: "success" | "error"; message: string }) => void;
46
+ /** HFun score threshold for the VIP crown. Defaults to 10. */
47
+ vipThreshold?: number;
48
+
49
+ // ─── Profile & Settings modal (rendered internally) ────────────
50
+ /** Current value for the "Animations" toggle. */
51
+ animationsEnabled: boolean;
52
+ /** Called when the user flips the animations toggle. */
53
+ onToggleAnimations: () => void;
54
+ /** Current default slippage value (e.g. 0.01 = 1%). */
55
+ defaultSlippage: number;
56
+ /** Called when the user picks a slippage option. */
57
+ onSlippageChange: (value: number) => void;
58
+ /** Override the slippage choices. Defaults to 0.5/1/2/5/10%. */
59
+ slippageOptions?: SlippageOption[];
60
+ /** Fired after the SDK successfully deletes a wallet. */
61
+ onWalletDeleted?: (walletId: number) => void;
62
+ /** Fired after the SDK successfully renames a wallet. */
63
+ onWalletRenamed?: (walletId: number, name: string) => void;
64
+ /** Shorthand for `principalColors.accent`. Defaults to `#a855f7`. */
65
+ accentColor?: string;
66
+ /** Principal accent colors used for add-wallet text, wallet icon surfaces, and the Profile & Settings modal. */
67
+ principalColors?: PrincipalColorOverrides;
68
+ /** CSS color used as the dropdown panel background. Defaults to `rgba(20,20,20,0.95)`. */
69
+ backgroundColor?: string;
70
+ }
71
+
72
+ const DEFAULT_BACKGROUND_COLOR = "rgba(20,20,20,0.95)";
73
+
74
+ interface DropdownWallet {
75
+ id: number;
76
+ name?: string | null;
77
+ ethereumAddress?: string;
78
+ }
79
+
80
+ interface WalletListItem {
81
+ wallet: DropdownWallet;
82
+ label: string | null;
83
+ }
84
+
85
+ type WalletListEntry =
86
+ | { type: "wallet"; item: WalletListItem }
87
+ | { type: "group"; path: string; items: WalletListItem[] };
88
+
89
+ const getWalletNameParts = (name?: string | null) =>
90
+ (name ?? "")
91
+ .split("/")
92
+ .map((part) => part.trim())
93
+ .filter(Boolean);
94
+
95
+ const buildWalletListEntries = (
96
+ wallets: DropdownWallet[] | undefined,
97
+ ): WalletListEntry[] => {
98
+ const standalone: WalletListItem[] = [];
99
+ const grouped = new Map<string, WalletListItem[]>();
100
+ const walletParts = (wallets ?? []).map((wallet) => ({
101
+ wallet,
102
+ parts: getWalletNameParts(wallet.name),
103
+ }));
104
+ const prefixCounts = new Map<string, number>();
105
+
106
+ walletParts.forEach(({ parts }) => {
107
+ for (let index = 1; index < parts.length; index += 1) {
108
+ const prefix = parts.slice(0, index).join("/");
109
+ prefixCounts.set(prefix, (prefixCounts.get(prefix) ?? 0) + 1);
110
+ }
111
+ });
112
+
113
+ walletParts.forEach(({ wallet, parts }) => {
114
+ const groupPath = (() => {
115
+ for (let index = parts.length - 1; index > 0; index -= 1) {
116
+ const prefix = parts.slice(0, index).join("/");
117
+ if ((prefixCounts.get(prefix) ?? 0) > 1) {
118
+ return prefix;
119
+ }
120
+ }
121
+ return null;
122
+ })();
123
+ const groupDepth = groupPath ? groupPath.split("/").length : 0;
124
+ const item: WalletListItem = {
125
+ wallet,
126
+ label: parts.length > 0 ? parts.slice(groupDepth).join("/") : null,
127
+ };
128
+
129
+ if (!groupPath) {
130
+ standalone.push(item);
131
+ return;
132
+ }
133
+
134
+ const groupItems = grouped.get(groupPath) ?? [];
135
+ groupItems.push(item);
136
+ grouped.set(groupPath, groupItems);
137
+ });
138
+
139
+ const entries: WalletListEntry[] = standalone.map((item) => ({
140
+ type: "wallet",
141
+ item,
142
+ }));
143
+
144
+ grouped.forEach((items, path) => {
145
+ if (items.length === 1) {
146
+ entries.push({
147
+ type: "wallet",
148
+ item: { ...items[0], label: `${path}/${items[0].label ?? ""}` },
149
+ });
150
+ return;
151
+ }
152
+
153
+ entries.push({
154
+ type: "group",
155
+ path,
156
+ items: [...items].sort((a, b) =>
157
+ (a.label ?? "").localeCompare(b.label ?? ""),
158
+ ),
159
+ });
160
+ });
161
+
162
+ return entries.sort((a, b) => {
163
+ const aLabel = a.type === "group" ? a.path : (a.item.label ?? "");
164
+ const bLabel = b.type === "group" ? b.path : (b.item.label ?? "");
165
+ return aLabel.localeCompare(bLabel);
166
+ });
167
+ };
168
+
169
+ const formatCompactAddress = (addr: string | undefined) => {
170
+ if (!addr) return "";
171
+ return `${addr.slice(0, 4)}...${addr.slice(-2)}`;
172
+ };
173
+
174
+ const rootStyle: CSSProperties = {
175
+ position: "absolute",
176
+ right: 0,
177
+ top: "calc(100% + 4px)",
178
+ width: 240,
179
+ backdropFilter: "blur(10px)",
180
+ WebkitBackdropFilter: "blur(10px)",
181
+ borderRadius: 9,
182
+ boxShadow: "0 8px 40px rgba(0,0,0,0.85)",
183
+ border: "1px solid rgba(255,255,255,0.08)",
184
+ zIndex: 50,
185
+ transformOrigin: "top right",
186
+ overflow: "hidden",
187
+ };
188
+
189
+ const sectionBorder = "1px solid rgba(255,255,255,0.06)";
190
+
191
+ const sectionHeaderStyle: CSSProperties = {
192
+ padding: "8px 16px",
193
+ fontSize: 12,
194
+ fontWeight: 600,
195
+ color: "#9ca3af",
196
+ textTransform: "uppercase",
197
+ letterSpacing: "0.05em",
198
+ };
199
+
200
+ const footerBtnStyle: CSSProperties = {
201
+ width: "100%",
202
+ textAlign: "left",
203
+ padding: "10px 16px",
204
+ fontSize: 14,
205
+ background: "transparent",
206
+ border: "none",
207
+ cursor: "pointer",
208
+ display: "flex",
209
+ alignItems: "center",
210
+ gap: 8,
211
+ color: "#9ca3af",
212
+ transition: "background 120ms, color 120ms",
213
+ };
214
+
215
+ export function WalletSelectorDropdown({
216
+ isOpen,
217
+ onClose,
218
+ onAddWallet,
219
+ onShowPortfolio,
220
+ onLogout,
221
+ onNotify,
222
+ vipThreshold = 10,
223
+ animationsEnabled,
224
+ onToggleAnimations,
225
+ defaultSlippage,
226
+ onSlippageChange,
227
+ slippageOptions,
228
+ onWalletDeleted,
229
+ onWalletRenamed,
230
+ accentColor,
231
+ principalColors,
232
+ backgroundColor = DEFAULT_BACKGROUND_COLOR,
233
+ }: WalletSelectorDropdownProps): ReactNode {
234
+ const { user, wallets, selectedWalletId, selectWallet, logout, authMethod } =
235
+ useHypurrConnectInternal();
236
+
237
+ const [profileOpen, setProfileOpen] = useState(false);
238
+ const openProfile = useCallback(() => {
239
+ setProfileOpen(true);
240
+ onClose();
241
+ }, [onClose]);
242
+
243
+ const walletListEntries = useMemo(
244
+ () => buildWalletListEntries(Array.isArray(wallets) ? wallets : undefined),
245
+ [wallets],
246
+ );
247
+
248
+ const handleCopyAddress = useCallback(
249
+ (address: string | undefined) => {
250
+ if (!address) return;
251
+ navigator.clipboard.writeText(address);
252
+ onNotify?.({ type: "success", message: "Address copied" });
253
+ },
254
+ [onNotify],
255
+ );
256
+
257
+ const handleLogout = useCallback(() => {
258
+ onLogout?.();
259
+ logout();
260
+ onClose();
261
+ }, [logout, onClose, onLogout]);
262
+
263
+ const profilePic = user?.photoUrl;
264
+ const displayName = user?.displayName ?? "";
265
+ const hfunScore = user?.hfunScore ?? 0;
266
+ const isVip = hfunScore > vipThreshold;
267
+ const colors = resolvePrincipalColors(accentColor, principalColors);
268
+
269
+ const renderWalletRow = (item: WalletListItem, depth: number): ReactNode => {
270
+ const { wallet, label } = item;
271
+ const isSelected = wallet.id === selectedWalletId;
272
+ const compactAddress = formatCompactAddress(wallet.ethereumAddress);
273
+ return (
274
+ <WalletRow
275
+ key={wallet.id}
276
+ depth={depth}
277
+ isSelected={isSelected}
278
+ label={label}
279
+ compactAddress={compactAddress}
280
+ onSelect={() => {
281
+ selectWallet(wallet.id);
282
+ onClose();
283
+ }}
284
+ onShowPortfolio={
285
+ onShowPortfolio
286
+ ? () => {
287
+ onShowPortfolio(wallet as HyperliquidWallet);
288
+ onClose();
289
+ }
290
+ : undefined
291
+ }
292
+ onCopy={() => handleCopyAddress(wallet.ethereumAddress)}
293
+ colors={colors}
294
+ />
295
+ );
296
+ };
297
+
298
+ return (
299
+ <>
300
+ <AnimatePresence>
301
+ {isOpen && (
302
+ <motion.div
303
+ style={{ ...rootStyle, background: backgroundColor }}
304
+ initial={{ opacity: 0, scale: 0.95 }}
305
+ animate={{ opacity: 1, scale: 1 }}
306
+ exit={{ opacity: 0, scale: 0.95 }}
307
+ transition={{ duration: 0.15, ease: "easeOut" }}
308
+ >
309
+ {/* Profile header */}
310
+ <button
311
+ onClick={openProfile}
312
+ style={{
313
+ width: "100%",
314
+ padding: "12px 16px",
315
+ borderBottom: sectionBorder,
316
+ display: "flex",
317
+ alignItems: "center",
318
+ gap: 12,
319
+ background: "transparent",
320
+ border: "none",
321
+ borderBottomColor: "rgba(255,255,255,0.06)",
322
+ borderBottomStyle: "solid",
323
+ borderBottomWidth: 1,
324
+ cursor: "pointer",
325
+ textAlign: "left",
326
+ transition: "background 120ms",
327
+ }}
328
+ onMouseEnter={(e) =>
329
+ (e.currentTarget.style.background = "rgba(255,255,255,0.06)")
330
+ }
331
+ onMouseLeave={(e) =>
332
+ (e.currentTarget.style.background = "transparent")
333
+ }
334
+ >
335
+ {profilePic ? (
336
+ <div
337
+ style={{
338
+ position: "relative",
339
+ padding: isVip ? 2 : 0,
340
+ borderRadius: "50%",
341
+ background: isVip
342
+ ? "linear-gradient(to right, #fde047, #eab308, #ca8a04)"
343
+ : "transparent",
344
+ }}
345
+ >
346
+ <img
347
+ src={profilePic}
348
+ alt="Profile"
349
+ style={{
350
+ height: 32,
351
+ width: 32,
352
+ borderRadius: "50%",
353
+ display: "block",
354
+ }}
355
+ />
356
+ {isVip && (
357
+ <Crown
358
+ size={12}
359
+ color="#fde047"
360
+ fill="#eab308"
361
+ style={{
362
+ position: "absolute",
363
+ top: -4,
364
+ right: -2,
365
+ filter: "drop-shadow(0 1px 2px rgba(0,0,0,0.5))",
366
+ }}
367
+ />
368
+ )}
369
+ </div>
370
+ ) : authMethod === "eoa" ? (
371
+ <div
372
+ style={{
373
+ height: 32,
374
+ width: 32,
375
+ borderRadius: 7,
376
+ background: colors.accentBackground,
377
+ border: `1px solid ${colors.accentBorder}`,
378
+ display: "flex",
379
+ alignItems: "center",
380
+ justifyContent: "center",
381
+ flexShrink: 0,
382
+ }}
383
+ >
384
+ <Wallet size={14} color={colors.accentText} />
385
+ </div>
386
+ ) : (
387
+ <div
388
+ style={{
389
+ position: "relative",
390
+ height: 32,
391
+ width: 32,
392
+ borderRadius: "50%",
393
+ background: isVip
394
+ ? "linear-gradient(135deg, #ca8a04, #fde047)"
395
+ : colors.accentBackground,
396
+ border: `1px solid ${
397
+ isVip ? "rgba(255,255,255,0.08)" : colors.accentBorder
398
+ }`,
399
+ display: "flex",
400
+ alignItems: "center",
401
+ justifyContent: "center",
402
+ color: isVip ? profileColors.text : colors.accentText,
403
+ fontSize: 14,
404
+ fontWeight: 700,
405
+ }}
406
+ >
407
+ {displayName.charAt(0)}
408
+ {isVip && (
409
+ <Crown
410
+ size={12}
411
+ color="#fde047"
412
+ fill="#eab308"
413
+ style={{
414
+ position: "absolute",
415
+ top: -4,
416
+ right: -2,
417
+ filter: "drop-shadow(0 1px 2px rgba(0,0,0,0.5))",
418
+ }}
419
+ />
420
+ )}
421
+ </div>
422
+ )}
423
+ <div style={{ overflow: "hidden", flex: 1 }}>
424
+ <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
425
+ <p
426
+ style={{
427
+ margin: 0,
428
+ fontSize: 14,
429
+ fontWeight: 500,
430
+ color: "#fff",
431
+ overflow: "hidden",
432
+ textOverflow: "ellipsis",
433
+ whiteSpace: "nowrap",
434
+ }}
435
+ >
436
+ {displayName}
437
+ </p>
438
+ {hfunScore > 0 && (
439
+ <span
440
+ style={{
441
+ display: "inline-flex",
442
+ alignItems: "center",
443
+ gap: 2,
444
+ fontSize: 12,
445
+ color: "#eab308",
446
+ fontWeight: 500,
447
+ background: "rgba(234,179,8,0.1)",
448
+ padding: "2px 6px",
449
+ borderRadius: 4,
450
+ }}
451
+ >
452
+ <Star size={10} color="#eab308" fill="#eab308" />
453
+ {hfunScore.toFixed(0)}
454
+ </span>
455
+ )}
456
+ </div>
457
+ </div>
458
+ </button>
459
+
460
+ {/* Wallets section */}
461
+ <div style={{ borderBottom: sectionBorder }}>
462
+ <div style={sectionHeaderStyle}>Wallets</div>
463
+ <div
464
+ style={{
465
+ maxHeight: 256,
466
+ overflowY: "auto",
467
+ paddingBottom: 4,
468
+ }}
469
+ >
470
+ {walletListEntries.map((entry) => {
471
+ if (entry.type === "wallet") {
472
+ return renderWalletRow(entry.item, 0);
473
+ }
474
+ const isSelectedGroup = entry.items.some(
475
+ (item) => item.wallet.id === selectedWalletId,
476
+ );
477
+ return (
478
+ <Fragment key={entry.path}>
479
+ <div
480
+ style={{
481
+ display: "flex",
482
+ alignItems: "center",
483
+ gap: 6,
484
+ padding: "8px 16px 4px",
485
+ fontSize: 11,
486
+ fontWeight: 600,
487
+ textTransform: "uppercase",
488
+ letterSpacing: "0.05em",
489
+ color: isSelectedGroup ? "#e5e7eb" : "#6b7280",
490
+ }}
491
+ >
492
+ <Folder size={12} style={{ flexShrink: 0 }} />
493
+ <span
494
+ style={{
495
+ minWidth: 0,
496
+ flex: 1,
497
+ overflow: "hidden",
498
+ textOverflow: "ellipsis",
499
+ whiteSpace: "nowrap",
500
+ }}
501
+ >
502
+ {entry.path}
503
+ </span>
504
+ <span
505
+ style={{
506
+ flexShrink: 0,
507
+ fontVariantNumeric: "tabular-nums",
508
+ color: "#4b5563",
509
+ }}
510
+ >
511
+ {entry.items.length}
512
+ </span>
513
+ </div>
514
+ {entry.items.map((item) => renderWalletRow(item, 1))}
515
+ </Fragment>
516
+ );
517
+ })}
518
+ </div>
519
+ {authMethod !== "eoa" && onAddWallet && (
520
+ <button
521
+ onClick={() => {
522
+ onAddWallet();
523
+ onClose();
524
+ }}
525
+ style={{
526
+ width: "100%",
527
+ textAlign: "left",
528
+ padding: "8px 16px",
529
+ fontSize: 14,
530
+ background: "transparent",
531
+ border: "none",
532
+ cursor: "pointer",
533
+ display: "flex",
534
+ alignItems: "center",
535
+ gap: 8,
536
+ color: colors.accentText,
537
+ transition: "background 120ms",
538
+ }}
539
+ onMouseEnter={(e) =>
540
+ (e.currentTarget.style.background =
541
+ "rgba(255,255,255,0.06)")
542
+ }
543
+ onMouseLeave={(e) =>
544
+ (e.currentTarget.style.background = "transparent")
545
+ }
546
+ >
547
+ <Plus size={14} />
548
+ Add Wallet
549
+ </button>
550
+ )}
551
+ </div>
552
+
553
+ {/* Footer actions */}
554
+ <div>
555
+ <FooterBtn
556
+ onClick={openProfile}
557
+ icon={<User size={14} />}
558
+ label="Profile & Settings"
559
+ />
560
+ <FooterBtn
561
+ onClick={handleLogout}
562
+ icon={<LogOut size={14} />}
563
+ label="Logout"
564
+ hoverColor="#ef4444"
565
+ />
566
+ </div>
567
+ </motion.div>
568
+ )}
569
+ </AnimatePresence>
570
+
571
+ <UserProfileModal
572
+ isOpen={profileOpen}
573
+ onClose={() => setProfileOpen(false)}
574
+ animationsEnabled={animationsEnabled}
575
+ onToggleAnimations={onToggleAnimations}
576
+ defaultSlippage={defaultSlippage}
577
+ onSlippageChange={onSlippageChange}
578
+ slippageOptions={slippageOptions}
579
+ accentColor={accentColor}
580
+ principalColors={principalColors}
581
+ onWalletDeleted={onWalletDeleted}
582
+ onWalletRenamed={onWalletRenamed}
583
+ onShowPortfolio={
584
+ onShowPortfolio
585
+ ? (wallet) => {
586
+ onShowPortfolio(wallet);
587
+ setProfileOpen(false);
588
+ }
589
+ : undefined
590
+ }
591
+ onNotify={onNotify}
592
+ />
593
+ </>
594
+ );
595
+ }
596
+
597
+ function FooterBtn({
598
+ onClick,
599
+ icon,
600
+ label,
601
+ hoverColor = "#fff",
602
+ }: {
603
+ onClick: () => void;
604
+ icon: ReactNode;
605
+ label: string;
606
+ hoverColor?: string;
607
+ }) {
608
+ return (
609
+ <button
610
+ onClick={onClick}
611
+ style={footerBtnStyle}
612
+ onMouseEnter={(e) => {
613
+ e.currentTarget.style.background = "rgba(255,255,255,0.06)";
614
+ e.currentTarget.style.color = hoverColor;
615
+ }}
616
+ onMouseLeave={(e) => {
617
+ e.currentTarget.style.background = "transparent";
618
+ e.currentTarget.style.color = "#9ca3af";
619
+ }}
620
+ >
621
+ {icon}
622
+ {label}
623
+ </button>
624
+ );
625
+ }
626
+
627
+ function WalletRow({
628
+ depth,
629
+ isSelected,
630
+ label,
631
+ compactAddress,
632
+ onSelect,
633
+ onShowPortfolio,
634
+ onCopy,
635
+ colors,
636
+ }: {
637
+ depth: number;
638
+ isSelected: boolean;
639
+ label: string | null;
640
+ compactAddress: string;
641
+ onSelect: () => void;
642
+ onShowPortfolio?: () => void;
643
+ onCopy: () => void;
644
+ colors: PrincipalColors;
645
+ }) {
646
+ return (
647
+ <div
648
+ style={{
649
+ width: "100%",
650
+ paddingLeft: 16 + depth * 14,
651
+ paddingRight: 16,
652
+ paddingTop: 6,
653
+ paddingBottom: 6,
654
+ fontSize: 14,
655
+ color: "#d1d5db",
656
+ display: "flex",
657
+ alignItems: "center",
658
+ background: isSelected ? "rgba(255,255,255,0.06)" : "transparent",
659
+ transition: "background 120ms",
660
+ }}
661
+ onMouseEnter={(e) => {
662
+ e.currentTarget.style.background = "rgba(255,255,255,0.06)";
663
+ const actions =
664
+ e.currentTarget.querySelector<HTMLDivElement>("[data-row-actions]");
665
+ if (actions) actions.style.opacity = "1";
666
+ }}
667
+ onMouseLeave={(e) => {
668
+ e.currentTarget.style.background = isSelected
669
+ ? "rgba(255,255,255,0.06)"
670
+ : "transparent";
671
+ const actions =
672
+ e.currentTarget.querySelector<HTMLDivElement>("[data-row-actions]");
673
+ if (actions) actions.style.opacity = "0";
674
+ }}
675
+ >
676
+ <button
677
+ onClick={onSelect}
678
+ style={{
679
+ flex: 1,
680
+ textAlign: "left",
681
+ minWidth: 0,
682
+ display: "flex",
683
+ alignItems: "center",
684
+ gap: 4,
685
+ background: "transparent",
686
+ border: "none",
687
+ color: "inherit",
688
+ cursor: "pointer",
689
+ padding: 0,
690
+ }}
691
+ >
692
+ {label ? (
693
+ <>
694
+ <span
695
+ style={{
696
+ overflow: "hidden",
697
+ textOverflow: "ellipsis",
698
+ whiteSpace: "nowrap",
699
+ color: isSelected ? "#fff" : undefined,
700
+ fontWeight: isSelected ? 500 : undefined,
701
+ }}
702
+ >
703
+ {label}
704
+ </span>
705
+ <span
706
+ style={{
707
+ fontSize: 12,
708
+ fontWeight: 400,
709
+ flexShrink: 0,
710
+ color: "#6b7280",
711
+ }}
712
+ >
713
+ ({compactAddress})
714
+ </span>
715
+ </>
716
+ ) : (
717
+ <span
718
+ style={{
719
+ fontSize: 12,
720
+ color: isSelected ? "#fff" : "#9ca3af",
721
+ fontWeight: isSelected ? 500 : undefined,
722
+ }}
723
+ >
724
+ {compactAddress}
725
+ </span>
726
+ )}
727
+ </button>
728
+ <div
729
+ data-row-actions
730
+ style={{
731
+ display: "flex",
732
+ alignItems: "center",
733
+ opacity: 0,
734
+ transition: "opacity 120ms",
735
+ }}
736
+ >
737
+ {onShowPortfolio && (
738
+ <RowIconBtn
739
+ title="View portfolio"
740
+ hoverColor={colors.accent}
741
+ onClick={(e) => {
742
+ e.stopPropagation();
743
+ onShowPortfolio();
744
+ }}
745
+ >
746
+ <LayoutDashboard size={12} />
747
+ </RowIconBtn>
748
+ )}
749
+ <RowIconBtn
750
+ title="Copy address"
751
+ hoverColor={colors.accent}
752
+ onClick={(e) => {
753
+ e.stopPropagation();
754
+ onCopy();
755
+ }}
756
+ >
757
+ <Copy size={12} />
758
+ </RowIconBtn>
759
+ </div>
760
+ </div>
761
+ );
762
+ }
763
+
764
+ function RowIconBtn({
765
+ children,
766
+ title,
767
+ onClick,
768
+ hoverColor = profileColors.text,
769
+ }: {
770
+ children: ReactNode;
771
+ title: string;
772
+ onClick: (e: ReactMouseEvent<HTMLButtonElement>) => void;
773
+ hoverColor?: string;
774
+ }) {
775
+ return (
776
+ <button
777
+ onClick={onClick}
778
+ title={title}
779
+ style={{
780
+ padding: 4,
781
+ marginLeft: 4,
782
+ background: "transparent",
783
+ border: "none",
784
+ color: "#4b5563",
785
+ cursor: "pointer",
786
+ display: "flex",
787
+ alignItems: "center",
788
+ justifyContent: "center",
789
+ transition: "color 120ms",
790
+ }}
791
+ onMouseEnter={(e) => (e.currentTarget.style.color = hoverColor)}
792
+ onMouseLeave={(e) => (e.currentTarget.style.color = "#4b5563")}
793
+ >
794
+ {children}
795
+ </button>
796
+ );
797
+ }