@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.
- package/README.md +0 -1
- package/dist/index.d.ts +128 -9
- package/dist/index.js +2568 -13
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/DeleteWalletModal.tsx +344 -0
- package/src/HypurrConnectProvider.tsx +29 -18
- package/src/RenameWalletModal.tsx +325 -0
- package/src/UserProfileModal.tsx +982 -0
- package/src/WalletSelectorDropdown.tsx +797 -0
- package/src/icons/lucide.tsx +197 -0
- package/src/index.ts +9 -0
- package/src/profileStyles.ts +213 -0
- package/src/types.ts +4 -10
- package/src/TelegramLoginWidget.tsx +0 -62
|
@@ -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
|
+
}
|