@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,325 @@
1
+ import { AnimatePresence, motion } from "framer-motion";
2
+ import type { HyperliquidWallet } from "hypurr-grpc/ts/hypurr/wallet";
3
+ import {
4
+ useCallback,
5
+ useEffect,
6
+ useState,
7
+ type CSSProperties,
8
+ type ReactNode,
9
+ } from "react";
10
+ import {
11
+ AlertTriangle,
12
+ Loader2,
13
+ Pencil,
14
+ SpinKeyframes,
15
+ X,
16
+ } from "./icons/lucide";
17
+ import {
18
+ closeBtnStyle as makeCloseBtnStyle,
19
+ fontFamily,
20
+ modalBackdropStyle,
21
+ modalHeaderStyle,
22
+ modalPanelStyle,
23
+ modalWrapperStyle,
24
+ profileColors,
25
+ raisedButtonStyle,
26
+ titleStyle,
27
+ upperLabelStyle,
28
+ } from "./profileStyles";
29
+
30
+ export interface RenameWalletModalProps {
31
+ isOpen: boolean;
32
+ onClose: () => void;
33
+ wallet: HyperliquidWallet | null;
34
+ onConfirm: (walletId: number, name: string) => Promise<void>;
35
+ onNotify?: (n: { type: "success" | "error"; message: string }) => void;
36
+ /** Kept for compatibility. Profile modal actions now use the shared app raised button style. */
37
+ accentColor?: string;
38
+ }
39
+
40
+ const DANGER_BORDER = "rgba(248,113,113,0.2)";
41
+ const WALLET_NAME_REGEX = /^[a-zA-Z0-9/]+$/;
42
+
43
+ const backdropStyle = modalBackdropStyle(110);
44
+ const wrapperStyle = modalWrapperStyle(111, 16);
45
+ const panelStyle: CSSProperties = {
46
+ ...modalPanelStyle(true),
47
+ border: `1px solid ${profileColors.surfaceBd}`,
48
+ };
49
+ const headerStyle: CSSProperties = {
50
+ ...modalHeaderStyle,
51
+ borderBottom: "1px solid rgba(255,255,255,0.06)",
52
+ };
53
+
54
+ const bodyStyle: CSSProperties = {
55
+ padding: "20px 24px",
56
+ display: "flex",
57
+ flexDirection: "column",
58
+ gap: 16,
59
+ };
60
+
61
+ const infoBoxStyle: CSSProperties = {
62
+ padding: "10px 12px",
63
+ background: "rgba(255,255,255,0.03)",
64
+ border: "1px solid rgba(255,255,255,0.06)",
65
+ borderRadius: 8,
66
+ };
67
+
68
+ const labelStyle: CSSProperties = {
69
+ display: "block",
70
+ fontSize: 12.5,
71
+ lineHeight: "1rem",
72
+ color: profileColors.muted,
73
+ marginBottom: 8,
74
+ };
75
+
76
+ const inputStyle = (hasError: boolean, disabled: boolean): CSSProperties => ({
77
+ width: "100%",
78
+ background: "rgba(13,18,25,0.9)",
79
+ border: `1px solid ${
80
+ hasError ? "rgba(248,113,113,0.5)" : profileColors.surfaceBd
81
+ }`,
82
+ borderRadius: 8,
83
+ padding: "10px 12px",
84
+ color: profileColors.text,
85
+ fontFamily: fontFamily.mono,
86
+ fontSize: 12.5,
87
+ lineHeight: "1rem",
88
+ outline: "none",
89
+ opacity: disabled ? 0.5 : 1,
90
+ boxSizing: "border-box",
91
+ transition: "border-color 150ms, background-color 150ms",
92
+ });
93
+
94
+ const footerStyle: CSSProperties = {
95
+ padding: "0 24px 24px",
96
+ };
97
+
98
+ const saveButtonStyle = (
99
+ enabled: boolean,
100
+ hovered: boolean,
101
+ ): CSSProperties => ({
102
+ ...raisedButtonStyle(enabled ? (hovered ? "hover" : "default") : "disabled"),
103
+ width: "100%",
104
+ padding: "8px 0",
105
+ borderRadius: 8,
106
+ fontSize: 12.5,
107
+ lineHeight: "1rem",
108
+ fontWeight: 500,
109
+ display: "flex",
110
+ alignItems: "center",
111
+ justifyContent: "center",
112
+ gap: 8,
113
+ });
114
+
115
+ export function RenameWalletModal({
116
+ isOpen,
117
+ onClose,
118
+ wallet,
119
+ onConfirm,
120
+ onNotify,
121
+ }: RenameWalletModalProps): ReactNode {
122
+ const [name, setName] = useState("");
123
+ const [isRenaming, setIsRenaming] = useState(false);
124
+ const [error, setError] = useState<string | null>(null);
125
+ const [saveHovered, setSaveHovered] = useState(false);
126
+
127
+ const currentName = wallet?.name || "Unnamed";
128
+ const trimmedName = name.trim();
129
+ const isNameChanged = trimmedName !== currentName;
130
+ const isNameValid = WALLET_NAME_REGEX.test(trimmedName);
131
+ const canSubmit = isNameValid && isNameChanged && !isRenaming;
132
+
133
+ useEffect(() => {
134
+ if (!wallet) return;
135
+ setName(wallet.name || "");
136
+ setError(null);
137
+ }, [wallet]);
138
+
139
+ const handleClose = useCallback(() => {
140
+ if (isRenaming) return;
141
+ setError(null);
142
+ onClose();
143
+ }, [isRenaming, onClose]);
144
+
145
+ const handleRename = useCallback(async () => {
146
+ if (!wallet || !isNameValid || !isNameChanged) return;
147
+ setError(null);
148
+ setIsRenaming(true);
149
+ try {
150
+ await onConfirm(wallet.id, trimmedName);
151
+ onNotify?.({ type: "success", message: "Wallet renamed successfully" });
152
+ onClose();
153
+ } catch (e: unknown) {
154
+ setError(e instanceof Error ? e.message : "Failed to rename wallet");
155
+ } finally {
156
+ setIsRenaming(false);
157
+ }
158
+ }, [
159
+ wallet,
160
+ isNameValid,
161
+ isNameChanged,
162
+ onConfirm,
163
+ trimmedName,
164
+ onClose,
165
+ onNotify,
166
+ ]);
167
+
168
+ return (
169
+ <AnimatePresence>
170
+ {isOpen && wallet && (
171
+ <>
172
+ <SpinKeyframes />
173
+ <motion.div
174
+ key="backdrop"
175
+ style={backdropStyle}
176
+ initial={{ opacity: 0 }}
177
+ animate={{ opacity: 1 }}
178
+ exit={{ opacity: 0 }}
179
+ transition={{ duration: 0.15 }}
180
+ onClick={handleClose}
181
+ />
182
+ <div style={wrapperStyle}>
183
+ <motion.div
184
+ key="panel"
185
+ style={panelStyle}
186
+ initial={{ opacity: 0, y: 8 }}
187
+ animate={{ opacity: 1, y: 0 }}
188
+ exit={{ opacity: 0, y: 8 }}
189
+ transition={{ duration: 0.18, ease: "easeOut" }}
190
+ onClick={(e) => e.stopPropagation()}
191
+ >
192
+ <div style={headerStyle}>
193
+ <h3 style={titleStyle}>Rename Wallet</h3>
194
+ <button
195
+ onClick={handleClose}
196
+ disabled={isRenaming}
197
+ style={makeCloseBtnStyle(isRenaming)}
198
+ aria-label="Close"
199
+ >
200
+ <X size={16} />
201
+ </button>
202
+ </div>
203
+
204
+ <div style={bodyStyle}>
205
+ <div style={infoBoxStyle}>
206
+ <p
207
+ style={{
208
+ margin: "0 0 4px",
209
+ color: profileColors.muted,
210
+ ...upperLabelStyle,
211
+ }}
212
+ >
213
+ Wallet
214
+ </p>
215
+ <p
216
+ style={{
217
+ margin: 0,
218
+ fontSize: 12.5,
219
+ lineHeight: "1rem",
220
+ fontWeight: 500,
221
+ color: profileColors.text,
222
+ }}
223
+ >
224
+ {currentName}
225
+ </p>
226
+ <p
227
+ style={{
228
+ margin: "2px 0 0",
229
+ fontSize: 12.5,
230
+ lineHeight: "1rem",
231
+ color: profileColors.muted,
232
+ fontFamily: fontFamily.mono,
233
+ wordBreak: "break-all",
234
+ }}
235
+ >
236
+ {wallet.ethereumAddress}
237
+ </p>
238
+ </div>
239
+
240
+ <div>
241
+ <label style={labelStyle}>Wallet name</label>
242
+ <input
243
+ type="text"
244
+ value={name}
245
+ onChange={(e) => {
246
+ setName(e.target.value);
247
+ setError(null);
248
+ }}
249
+ onKeyDown={(e) => {
250
+ if (e.key === "Enter") handleRename();
251
+ if (e.key === "Escape") handleClose();
252
+ }}
253
+ disabled={isRenaming}
254
+ autoFocus
255
+ style={inputStyle(!!error, isRenaming)}
256
+ />
257
+ <p
258
+ style={{
259
+ margin: "8px 0 0",
260
+ fontSize: 12.5,
261
+ lineHeight: "1rem",
262
+ color: profileColors.subdued,
263
+ }}
264
+ >
265
+ Use letters, numbers, and / only.
266
+ </p>
267
+ </div>
268
+
269
+ {error && (
270
+ <div
271
+ style={{
272
+ display: "flex",
273
+ alignItems: "flex-start",
274
+ gap: 8,
275
+ padding: 12,
276
+ background: "rgba(248,113,113,0.08)",
277
+ border: `1px solid ${DANGER_BORDER}`,
278
+ borderRadius: 8,
279
+ }}
280
+ >
281
+ <AlertTriangle
282
+ size={14}
283
+ color={profileColors.danger}
284
+ style={{ flexShrink: 0, marginTop: 2 }}
285
+ />
286
+ <p
287
+ style={{
288
+ margin: 0,
289
+ fontSize: 12.5,
290
+ lineHeight: "1rem",
291
+ color: profileColors.danger,
292
+ }}
293
+ >
294
+ {error}
295
+ </p>
296
+ </div>
297
+ )}
298
+ </div>
299
+
300
+ <div style={footerStyle}>
301
+ <button
302
+ onClick={handleRename}
303
+ disabled={!canSubmit}
304
+ onMouseEnter={() => setSaveHovered(true)}
305
+ onMouseLeave={() => setSaveHovered(false)}
306
+ style={saveButtonStyle(canSubmit, saveHovered)}
307
+ >
308
+ {isRenaming ? (
309
+ <>
310
+ <Loader2 size={14} /> Saving...
311
+ </>
312
+ ) : (
313
+ <>
314
+ <Pencil size={14} /> Save Name
315
+ </>
316
+ )}
317
+ </button>
318
+ </div>
319
+ </motion.div>
320
+ </div>
321
+ </>
322
+ )}
323
+ </AnimatePresence>
324
+ );
325
+ }