@enbox/browser 0.3.41 → 0.3.43
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/dist/browser.mjs +110 -47
- package/dist/browser.mjs.map +3 -3
- package/dist/esm/index.js +18 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/ui/wallet-selector.js +391 -149
- package/dist/esm/ui/wallet-selector.js.map +1 -1
- package/dist/types/index.d.ts +8 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/ui/wallet-selector.d.ts +33 -1
- package/dist/types/ui/wallet-selector.d.ts.map +1 -1
- package/package.json +7 -6
- package/src/index.ts +26 -1
- package/src/ui/wallet-selector.ts +437 -162
|
@@ -4,17 +4,73 @@
|
|
|
4
4
|
* Injected into the DOM as a Shadow DOM element to prevent style conflicts.
|
|
5
5
|
* Inspired by WalletConnect's Web3Modal pattern.
|
|
6
6
|
*
|
|
7
|
+
* The modal is a single view with three zones:
|
|
8
|
+
*
|
|
9
|
+
* 1. **Quick connect** — a one-tap button that connects with the recommended
|
|
10
|
+
* (first) wallet without scrolling the grid.
|
|
11
|
+
* 2. **Wallet grid** — a scrollable grid of wallet tiles with a search filter.
|
|
12
|
+
* Each tile renders the wallet's own favicon, falling back to a letter
|
|
13
|
+
* badge (never a third-party favicon proxy).
|
|
14
|
+
* 3. **Custom URL** — paste any wallet URL; it is validated against the
|
|
15
|
+
* wallet's `/.well-known/enbox-connect` discovery document before the
|
|
16
|
+
* selection resolves.
|
|
17
|
+
*
|
|
7
18
|
* @module
|
|
8
19
|
*/
|
|
9
20
|
|
|
10
21
|
import type { WalletOption } from '../browser-connect-handler.js';
|
|
11
22
|
|
|
23
|
+
/** Path of the wallet's connect discovery document, relative to its origin. */
|
|
24
|
+
export const WALLET_WELL_KNOWN_PATH = '/.well-known/enbox-connect';
|
|
25
|
+
|
|
26
|
+
/** Timeout applied to the well-known validation fetch. */
|
|
27
|
+
const WELL_KNOWN_FETCH_TIMEOUT_MS = 6_000;
|
|
28
|
+
|
|
29
|
+
/** Options controlling {@link showWalletSelector} behavior. */
|
|
30
|
+
export interface WalletSelectorOptions {
|
|
31
|
+
/**
|
|
32
|
+
* Validate a pasted wallet origin before resolving with it.
|
|
33
|
+
*
|
|
34
|
+
* Defaults to {@link probeWalletWellKnown}, which fetches the wallet's
|
|
35
|
+
* `/.well-known/enbox-connect` document. Injectable for testing.
|
|
36
|
+
*/
|
|
37
|
+
validateWalletUrl?: (origin: string) => Promise<boolean>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Fetch a wallet's `/.well-known/enbox-connect` discovery document to confirm
|
|
42
|
+
* the origin hosts an Enbox-compatible wallet.
|
|
43
|
+
*
|
|
44
|
+
* Resolves `true` only when the document is reachable and names a
|
|
45
|
+
* `connectServerUrl`. Unreachable origins, non-2xx responses, CORS failures,
|
|
46
|
+
* and malformed documents resolve `false`.
|
|
47
|
+
*/
|
|
48
|
+
export async function probeWalletWellKnown(origin: string): Promise<boolean> {
|
|
49
|
+
try {
|
|
50
|
+
const wellKnownUrl = new URL(WALLET_WELL_KNOWN_PATH, origin).toString();
|
|
51
|
+
const response = await fetch(wellKnownUrl, { signal: AbortSignal.timeout(WELL_KNOWN_FETCH_TIMEOUT_MS) });
|
|
52
|
+
if (!response.ok) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const payload = await response.json() as { connectServerUrl?: unknown } | null;
|
|
57
|
+
return typeof payload?.connectServerUrl === 'string';
|
|
58
|
+
} catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
12
63
|
/** Shows the wallet selector modal and resolves with the chosen wallet URL. */
|
|
13
|
-
export function showWalletSelector(
|
|
64
|
+
export function showWalletSelector(
|
|
65
|
+
wallets: WalletOption[],
|
|
66
|
+
options: WalletSelectorOptions = {},
|
|
67
|
+
): Promise<string> {
|
|
14
68
|
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
|
15
69
|
throw new Error('[@enbox/browser] Wallet selector is only available in browser environments.');
|
|
16
70
|
}
|
|
17
71
|
|
|
72
|
+
const validateWalletUrl = options.validateWalletUrl ?? probeWalletWellKnown;
|
|
73
|
+
|
|
18
74
|
return new Promise<string>((resolve, reject) => {
|
|
19
75
|
// Create the host element with Shadow DOM isolation.
|
|
20
76
|
const host = document.createElement('div');
|
|
@@ -31,8 +87,18 @@ export function showWalletSelector(wallets: WalletOption[]): Promise<string> {
|
|
|
31
87
|
try { document.body.removeChild(host); } catch { /* best effort */ }
|
|
32
88
|
};
|
|
33
89
|
|
|
90
|
+
const settleResolve = (url: string): void => {
|
|
91
|
+
cleanup();
|
|
92
|
+
resolve(url);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const settleReject = (): void => {
|
|
96
|
+
cleanup();
|
|
97
|
+
reject(new Error('[@enbox/browser] Wallet selection cancelled.'));
|
|
98
|
+
};
|
|
99
|
+
|
|
34
100
|
// Detect dark mode.
|
|
35
|
-
const isDark = window.matchMedia?.('(prefers-color-scheme: dark)').matches;
|
|
101
|
+
const isDark = window.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false;
|
|
36
102
|
|
|
37
103
|
// Build styles.
|
|
38
104
|
const style = document.createElement('style');
|
|
@@ -46,158 +112,305 @@ export function showWalletSelector(wallets: WalletOption[]): Promise<string> {
|
|
|
46
112
|
const modal = document.createElement('div');
|
|
47
113
|
modal.className = 'modal';
|
|
48
114
|
|
|
49
|
-
|
|
50
|
-
const header = document.createElement('div');
|
|
51
|
-
header.className = 'header';
|
|
115
|
+
modal.appendChild(buildHeader(settleReject));
|
|
52
116
|
|
|
53
|
-
const
|
|
54
|
-
|
|
117
|
+
const recommended = wallets[0];
|
|
118
|
+
if (recommended !== undefined) {
|
|
119
|
+
modal.appendChild(buildQuickConnect(recommended, settleResolve));
|
|
120
|
+
}
|
|
55
121
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
closeBtn.addEventListener('click', () => {
|
|
60
|
-
cleanup();
|
|
61
|
-
reject(new Error('[@enbox/browser] Wallet selection cancelled.'));
|
|
62
|
-
});
|
|
122
|
+
modal.appendChild(buildWalletGrid(wallets, settleResolve));
|
|
123
|
+
modal.appendChild(buildSeparator());
|
|
124
|
+
modal.appendChild(buildCustomUrl(validateWalletUrl, settleResolve));
|
|
63
125
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
126
|
+
// Close on overlay click.
|
|
127
|
+
overlay.addEventListener('click', (e) => {
|
|
128
|
+
if (e.target === overlay) {
|
|
129
|
+
settleReject();
|
|
130
|
+
}
|
|
131
|
+
});
|
|
67
132
|
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
133
|
+
// Close on Escape.
|
|
134
|
+
onKeydown = (e: KeyboardEvent): void => {
|
|
135
|
+
if (e.key === 'Escape') {
|
|
136
|
+
settleReject();
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
document.addEventListener('keydown', onKeydown);
|
|
71
140
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
141
|
+
overlay.appendChild(modal);
|
|
142
|
+
shadow.appendChild(overlay);
|
|
143
|
+
document.body.appendChild(host);
|
|
75
144
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
icon.height = 32;
|
|
81
|
-
icon.onerror = (): void => { icon.style.display = 'none'; };
|
|
145
|
+
// Focus the search box for keyboard-first users.
|
|
146
|
+
shadow.querySelector<HTMLInputElement>('.wallet-search')?.focus();
|
|
147
|
+
});
|
|
148
|
+
}
|
|
82
149
|
|
|
83
|
-
|
|
84
|
-
textGroup.className = 'wallet-text';
|
|
150
|
+
// ─── DOM builders ────────────────────────────────────────────────
|
|
85
151
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
152
|
+
function buildHeader(onCancel: () => void): HTMLDivElement {
|
|
153
|
+
const header = document.createElement('div');
|
|
154
|
+
header.className = 'header';
|
|
89
155
|
|
|
90
|
-
|
|
156
|
+
const title = document.createElement('h2');
|
|
157
|
+
title.textContent = 'Connect a wallet';
|
|
91
158
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
159
|
+
const closeBtn = document.createElement('button');
|
|
160
|
+
closeBtn.className = 'close-btn';
|
|
161
|
+
closeBtn.setAttribute('aria-label', 'Close');
|
|
162
|
+
closeBtn.innerHTML = '×';
|
|
163
|
+
closeBtn.addEventListener('click', onCancel);
|
|
98
164
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
165
|
+
header.appendChild(title);
|
|
166
|
+
header.appendChild(closeBtn);
|
|
167
|
+
return header;
|
|
168
|
+
}
|
|
102
169
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
170
|
+
function buildQuickConnect(recommended: WalletOption, onSelect: (url: string) => void): HTMLButtonElement {
|
|
171
|
+
const quick = document.createElement('button');
|
|
172
|
+
quick.className = 'quick-connect';
|
|
106
173
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
});
|
|
174
|
+
const bolt = document.createElement('span');
|
|
175
|
+
bolt.className = 'quick-bolt';
|
|
176
|
+
bolt.textContent = '⚡'; // ⚡
|
|
111
177
|
|
|
112
|
-
|
|
113
|
-
|
|
178
|
+
const text = document.createElement('span');
|
|
179
|
+
text.className = 'quick-text';
|
|
114
180
|
|
|
115
|
-
|
|
181
|
+
const label = document.createElement('span');
|
|
182
|
+
label.className = 'quick-label';
|
|
183
|
+
label.textContent = 'Quick connect';
|
|
116
184
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
185
|
+
const sub = document.createElement('span');
|
|
186
|
+
sub.className = 'quick-sub';
|
|
187
|
+
sub.textContent = `Connect with ${recommended.name}`;
|
|
120
188
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
189
|
+
text.appendChild(label);
|
|
190
|
+
text.appendChild(sub);
|
|
191
|
+
quick.appendChild(bolt);
|
|
192
|
+
quick.appendChild(text);
|
|
193
|
+
quick.addEventListener('click', () => onSelect(recommended.url));
|
|
194
|
+
return quick;
|
|
195
|
+
}
|
|
127
196
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
modal.appendChild(sep);
|
|
197
|
+
function buildWalletGrid(wallets: WalletOption[], onSelect: (url: string) => void): HTMLDivElement {
|
|
198
|
+
const section = document.createElement('div');
|
|
199
|
+
section.className = 'grid-section';
|
|
132
200
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
201
|
+
const search = document.createElement('input');
|
|
202
|
+
search.type = 'search';
|
|
203
|
+
search.className = 'wallet-search';
|
|
204
|
+
search.placeholder = 'Search wallets…';
|
|
136
205
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
input.placeholder = 'Enter wallet URL...';
|
|
140
|
-
input.className = 'url-input';
|
|
206
|
+
const grid = document.createElement('div');
|
|
207
|
+
grid.className = 'wallet-grid';
|
|
141
208
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
209
|
+
const empty = document.createElement('div');
|
|
210
|
+
empty.className = 'grid-empty';
|
|
211
|
+
empty.textContent = 'No wallets match your search.';
|
|
212
|
+
empty.style.display = 'none';
|
|
146
213
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
214
|
+
const tiles = wallets.map((wallet) => {
|
|
215
|
+
const tile = buildWalletTile(wallet, onSelect);
|
|
216
|
+
grid.appendChild(tile);
|
|
217
|
+
return { name: wallet.name.toLowerCase(), tile };
|
|
218
|
+
});
|
|
150
219
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
220
|
+
search.addEventListener('input', () => {
|
|
221
|
+
const query = search.value.trim().toLowerCase();
|
|
222
|
+
let visible = 0;
|
|
223
|
+
for (const { name, tile } of tiles) {
|
|
224
|
+
const match = name.includes(query);
|
|
225
|
+
tile.style.display = match ? '' : 'none';
|
|
226
|
+
if (match) {
|
|
227
|
+
visible += 1;
|
|
155
228
|
}
|
|
156
|
-
}
|
|
229
|
+
}
|
|
230
|
+
empty.style.display = visible === 0 ? '' : 'none';
|
|
231
|
+
});
|
|
157
232
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
});
|
|
233
|
+
section.appendChild(search);
|
|
234
|
+
section.appendChild(grid);
|
|
235
|
+
section.appendChild(empty);
|
|
236
|
+
return section;
|
|
237
|
+
}
|
|
164
238
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
239
|
+
function buildWalletTile(wallet: WalletOption, onSelect: (url: string) => void): HTMLButtonElement {
|
|
240
|
+
const tile = document.createElement('button');
|
|
241
|
+
tile.className = 'wallet-item';
|
|
242
|
+
tile.title = wallet.description ?? wallet.name;
|
|
168
243
|
|
|
169
|
-
|
|
170
|
-
overlay.addEventListener('click', (e) => {
|
|
171
|
-
if (e.target === overlay) {
|
|
172
|
-
cleanup();
|
|
173
|
-
reject(new Error('[@enbox/browser] Wallet selection cancelled.'));
|
|
174
|
-
}
|
|
175
|
-
});
|
|
244
|
+
tile.appendChild(buildWalletIcon(wallet));
|
|
176
245
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
reject(new Error('[@enbox/browser] Wallet selection cancelled.'));
|
|
182
|
-
}
|
|
183
|
-
};
|
|
184
|
-
document.addEventListener('keydown', onKeydown);
|
|
246
|
+
const name = document.createElement('span');
|
|
247
|
+
name.className = 'wallet-name';
|
|
248
|
+
name.textContent = wallet.name;
|
|
249
|
+
tile.appendChild(name);
|
|
185
250
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
251
|
+
tile.addEventListener('click', () => onSelect(wallet.url));
|
|
252
|
+
return tile;
|
|
253
|
+
}
|
|
189
254
|
|
|
190
|
-
|
|
191
|
-
|
|
255
|
+
/**
|
|
256
|
+
* Build a wallet icon that prefers the wallet's own favicon and degrades to a
|
|
257
|
+
* letter badge. We never call a third-party favicon proxy: those 404 for many
|
|
258
|
+
* origins and leak the wallet URL to the proxy.
|
|
259
|
+
*/
|
|
260
|
+
function buildWalletIcon(wallet: WalletOption): HTMLElement {
|
|
261
|
+
const wrap = document.createElement('span');
|
|
262
|
+
wrap.className = 'wallet-icon';
|
|
263
|
+
|
|
264
|
+
const badge = document.createElement('span');
|
|
265
|
+
badge.className = 'wallet-badge';
|
|
266
|
+
badge.textContent = wallet.name.charAt(0).toUpperCase();
|
|
267
|
+
wrap.appendChild(badge);
|
|
268
|
+
|
|
269
|
+
const sources = iconCandidates(wallet);
|
|
270
|
+
if (sources.length === 0) {
|
|
271
|
+
return wrap;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const img = document.createElement('img');
|
|
275
|
+
img.className = 'wallet-img';
|
|
276
|
+
img.alt = '';
|
|
277
|
+
img.width = 40;
|
|
278
|
+
img.height = 40;
|
|
279
|
+
|
|
280
|
+
let index = 0;
|
|
281
|
+
img.addEventListener('error', () => {
|
|
282
|
+
index += 1;
|
|
283
|
+
if (index < sources.length) {
|
|
284
|
+
img.src = sources[index];
|
|
285
|
+
} else {
|
|
286
|
+
img.remove(); // fall back to the letter badge
|
|
287
|
+
}
|
|
192
288
|
});
|
|
289
|
+
|
|
290
|
+
img.src = sources[index];
|
|
291
|
+
wrap.appendChild(img);
|
|
292
|
+
return wrap;
|
|
193
293
|
}
|
|
194
294
|
|
|
195
|
-
|
|
295
|
+
/** Ordered favicon URLs to try for a wallet, most specific first. */
|
|
296
|
+
function iconCandidates(wallet: WalletOption): string[] {
|
|
297
|
+
if (wallet.icon !== undefined) {
|
|
298
|
+
return [wallet.icon];
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
let origin: string;
|
|
302
|
+
try {
|
|
303
|
+
origin = new URL(wallet.url).origin;
|
|
304
|
+
} catch {
|
|
305
|
+
return [];
|
|
306
|
+
}
|
|
196
307
|
|
|
197
|
-
|
|
198
|
-
return `https://t3.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${walletUrl}&size=128`;
|
|
308
|
+
return [`${origin}/favicon.svg`, `${origin}/favicon.ico`, `${origin}/favicon.png`];
|
|
199
309
|
}
|
|
200
310
|
|
|
311
|
+
function buildSeparator(): HTMLDivElement {
|
|
312
|
+
const sep = document.createElement('div');
|
|
313
|
+
sep.className = 'separator';
|
|
314
|
+
|
|
315
|
+
const line1 = document.createElement('div');
|
|
316
|
+
line1.className = 'sep-line';
|
|
317
|
+
const sepText = document.createElement('span');
|
|
318
|
+
sepText.textContent = 'or';
|
|
319
|
+
const line2 = document.createElement('div');
|
|
320
|
+
line2.className = 'sep-line';
|
|
321
|
+
|
|
322
|
+
sep.appendChild(line1);
|
|
323
|
+
sep.appendChild(sepText);
|
|
324
|
+
sep.appendChild(line2);
|
|
325
|
+
return sep;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function buildCustomUrl(
|
|
329
|
+
validateWalletUrl: (origin: string) => Promise<boolean>,
|
|
330
|
+
onSelect: (url: string) => void,
|
|
331
|
+
): HTMLDivElement {
|
|
332
|
+
const section = document.createElement('div');
|
|
333
|
+
section.className = 'custom-section';
|
|
334
|
+
|
|
335
|
+
const label = document.createElement('label');
|
|
336
|
+
label.className = 'custom-label';
|
|
337
|
+
label.textContent = 'Have a wallet URL?';
|
|
338
|
+
|
|
339
|
+
const inputGroup = document.createElement('div');
|
|
340
|
+
inputGroup.className = 'input-group';
|
|
341
|
+
|
|
342
|
+
const input = document.createElement('input');
|
|
343
|
+
input.type = 'url';
|
|
344
|
+
input.placeholder = 'https://your-wallet.example';
|
|
345
|
+
input.className = 'url-input';
|
|
346
|
+
|
|
347
|
+
const goBtn = document.createElement('button');
|
|
348
|
+
goBtn.className = 'go-btn';
|
|
349
|
+
goBtn.textContent = 'Connect';
|
|
350
|
+
goBtn.disabled = true;
|
|
351
|
+
|
|
352
|
+
const errorLine = document.createElement('div');
|
|
353
|
+
errorLine.className = 'url-error';
|
|
354
|
+
errorLine.style.display = 'none';
|
|
355
|
+
|
|
356
|
+
// When validation fails we let the user override, since the popup handshake
|
|
357
|
+
// is the ultimate gate — a valid wallet behind strict CORS should not be
|
|
358
|
+
// permanently blocked.
|
|
359
|
+
let allowUnverified = false;
|
|
360
|
+
|
|
361
|
+
const resetState = (): void => {
|
|
362
|
+
allowUnverified = false;
|
|
363
|
+
goBtn.textContent = 'Connect';
|
|
364
|
+
errorLine.style.display = 'none';
|
|
365
|
+
goBtn.disabled = !isValidUrl(input.value);
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
const submit = async (): Promise<void> => {
|
|
369
|
+
if (!isValidUrl(input.value)) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const origin = normalizeUrl(input.value);
|
|
374
|
+
if (allowUnverified) {
|
|
375
|
+
onSelect(origin);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
goBtn.disabled = true;
|
|
380
|
+
goBtn.textContent = 'Verifying…';
|
|
381
|
+
errorLine.style.display = 'none';
|
|
382
|
+
|
|
383
|
+
const valid = await validateWalletUrl(origin);
|
|
384
|
+
if (valid) {
|
|
385
|
+
onSelect(origin);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
allowUnverified = true;
|
|
390
|
+
goBtn.disabled = false;
|
|
391
|
+
goBtn.textContent = 'Connect anyway';
|
|
392
|
+
errorLine.textContent = `Couldn't verify an Enbox wallet at ${origin}.`;
|
|
393
|
+
errorLine.style.display = '';
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
input.addEventListener('input', resetState);
|
|
397
|
+
input.addEventListener('keydown', (e) => {
|
|
398
|
+
if (e.key === 'Enter') {
|
|
399
|
+
void submit();
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
goBtn.addEventListener('click', () => void submit());
|
|
403
|
+
|
|
404
|
+
inputGroup.appendChild(input);
|
|
405
|
+
inputGroup.appendChild(goBtn);
|
|
406
|
+
section.appendChild(label);
|
|
407
|
+
section.appendChild(inputGroup);
|
|
408
|
+
section.appendChild(errorLine);
|
|
409
|
+
return section;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// ─── Helpers ─────────────────────────────────────────────────────
|
|
413
|
+
|
|
201
414
|
function isValidUrl(value: string): boolean {
|
|
202
415
|
try {
|
|
203
416
|
const url = new URL(value.includes('://') ? value : `https://${value}`);
|
|
@@ -208,12 +421,9 @@ function isValidUrl(value: string): boolean {
|
|
|
208
421
|
}
|
|
209
422
|
|
|
210
423
|
function normalizeUrl(value: string): string {
|
|
211
|
-
|
|
212
|
-
value = `https://${value}`;
|
|
213
|
-
}
|
|
214
|
-
const url = new URL(value);
|
|
424
|
+
const withScheme = value.includes('://') ? value : `https://${value}`;
|
|
215
425
|
// Return origin only (strip path/trailing slash).
|
|
216
|
-
return
|
|
426
|
+
return new URL(withScheme).origin;
|
|
217
427
|
}
|
|
218
428
|
|
|
219
429
|
function buildStyles(isDark: boolean): string {
|
|
@@ -224,6 +434,8 @@ function buildStyles(isDark: boolean): string {
|
|
|
224
434
|
const itemBg = isDark ? '#16213e' : '#f8f9fa';
|
|
225
435
|
const itemHover = isDark ? '#0f3460' : '#e9ecef';
|
|
226
436
|
const accent = isDark ? '#4a9eff' : '#0066cc';
|
|
437
|
+
const accentAlt = isDark ? '#7b5cff' : '#5b8def';
|
|
438
|
+
const danger = isDark ? '#ff6b6b' : '#c0392b';
|
|
227
439
|
const overlayBg = 'rgba(0, 0, 0, 0.5)';
|
|
228
440
|
|
|
229
441
|
return `
|
|
@@ -255,9 +467,9 @@ function buildStyles(isDark: boolean): string {
|
|
|
255
467
|
background: ${bg};
|
|
256
468
|
color: ${text};
|
|
257
469
|
border-radius: 16px;
|
|
258
|
-
width:
|
|
259
|
-
max-width:
|
|
260
|
-
max-height:
|
|
470
|
+
width: 420px;
|
|
471
|
+
max-width: 92vw;
|
|
472
|
+
max-height: 86vh;
|
|
261
473
|
overflow-y: auto;
|
|
262
474
|
padding: 24px;
|
|
263
475
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
|
@@ -268,13 +480,10 @@ function buildStyles(isDark: boolean): string {
|
|
|
268
480
|
display: flex;
|
|
269
481
|
justify-content: space-between;
|
|
270
482
|
align-items: center;
|
|
271
|
-
margin-bottom:
|
|
483
|
+
margin-bottom: 18px;
|
|
272
484
|
}
|
|
273
485
|
|
|
274
|
-
h2 {
|
|
275
|
-
font-size: 18px;
|
|
276
|
-
font-weight: 600;
|
|
277
|
-
}
|
|
486
|
+
h2 { font-size: 18px; font-weight: 600; }
|
|
278
487
|
|
|
279
488
|
.close-btn {
|
|
280
489
|
background: none;
|
|
@@ -288,51 +497,110 @@ function buildStyles(isDark: boolean): string {
|
|
|
288
497
|
}
|
|
289
498
|
.close-btn:hover { color: ${text}; background: ${itemBg}; }
|
|
290
499
|
|
|
291
|
-
.
|
|
500
|
+
.quick-connect {
|
|
292
501
|
display: flex;
|
|
293
|
-
|
|
294
|
-
gap:
|
|
502
|
+
align-items: center;
|
|
503
|
+
gap: 12px;
|
|
504
|
+
width: 100%;
|
|
505
|
+
text-align: left;
|
|
506
|
+
padding: 14px 16px;
|
|
507
|
+
margin-bottom: 18px;
|
|
508
|
+
border: none;
|
|
509
|
+
border-radius: 12px;
|
|
510
|
+
cursor: pointer;
|
|
511
|
+
color: #fff;
|
|
512
|
+
background: linear-gradient(135deg, ${accent}, ${accentAlt});
|
|
513
|
+
transition: filter 0.15s;
|
|
514
|
+
}
|
|
515
|
+
.quick-connect:hover { filter: brightness(1.08); }
|
|
516
|
+
.quick-bolt { font-size: 22px; line-height: 1; }
|
|
517
|
+
.quick-text { display: flex; flex-direction: column; gap: 2px; }
|
|
518
|
+
.quick-label { font-weight: 600; font-size: 15px; }
|
|
519
|
+
.quick-sub { font-size: 12px; opacity: 0.85; }
|
|
520
|
+
|
|
521
|
+
.wallet-search {
|
|
522
|
+
width: 100%;
|
|
523
|
+
padding: 10px 14px;
|
|
524
|
+
margin-bottom: 12px;
|
|
525
|
+
border: 1px solid ${border};
|
|
526
|
+
border-radius: 10px;
|
|
527
|
+
font-size: 14px;
|
|
528
|
+
background: ${itemBg};
|
|
529
|
+
color: ${text};
|
|
530
|
+
outline: none;
|
|
531
|
+
}
|
|
532
|
+
.wallet-search:focus { border-color: ${accent}; }
|
|
533
|
+
.wallet-search::placeholder { color: ${muted}; }
|
|
534
|
+
|
|
535
|
+
.wallet-grid {
|
|
536
|
+
display: grid;
|
|
537
|
+
grid-template-columns: repeat(3, 1fr);
|
|
538
|
+
gap: 10px;
|
|
539
|
+
max-height: 216px;
|
|
540
|
+
overflow-y: auto;
|
|
541
|
+
padding: 2px;
|
|
295
542
|
}
|
|
296
543
|
|
|
297
544
|
.wallet-item {
|
|
298
545
|
display: flex;
|
|
546
|
+
flex-direction: column;
|
|
299
547
|
align-items: center;
|
|
300
|
-
|
|
301
|
-
|
|
548
|
+
justify-content: center;
|
|
549
|
+
gap: 8px;
|
|
550
|
+
padding: 14px 8px;
|
|
302
551
|
background: ${itemBg};
|
|
303
552
|
border: 1px solid ${border};
|
|
304
553
|
border-radius: 12px;
|
|
305
554
|
cursor: pointer;
|
|
306
|
-
font-size: 15px;
|
|
307
555
|
color: ${text};
|
|
308
|
-
transition: background 0.15s;
|
|
309
|
-
width: 100%;
|
|
310
|
-
text-align: left;
|
|
556
|
+
transition: background 0.15s, border-color 0.15s;
|
|
311
557
|
}
|
|
312
|
-
.wallet-item:hover { background: ${itemHover}; }
|
|
558
|
+
.wallet-item:hover { background: ${itemHover}; border-color: ${accent}; }
|
|
313
559
|
|
|
314
|
-
.wallet-
|
|
315
|
-
|
|
560
|
+
.wallet-icon {
|
|
561
|
+
position: relative;
|
|
562
|
+
width: 40px;
|
|
563
|
+
height: 40px;
|
|
316
564
|
display: flex;
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
565
|
+
align-items: center;
|
|
566
|
+
justify-content: center;
|
|
567
|
+
}
|
|
568
|
+
.wallet-badge {
|
|
569
|
+
position: absolute;
|
|
570
|
+
inset: 0;
|
|
571
|
+
display: flex;
|
|
572
|
+
align-items: center;
|
|
573
|
+
justify-content: center;
|
|
574
|
+
border-radius: 10px;
|
|
575
|
+
background: ${itemHover};
|
|
576
|
+
color: ${text};
|
|
577
|
+
font-size: 18px;
|
|
578
|
+
font-weight: 600;
|
|
579
|
+
}
|
|
580
|
+
.wallet-img {
|
|
581
|
+
position: relative;
|
|
582
|
+
width: 40px;
|
|
583
|
+
height: 40px;
|
|
584
|
+
border-radius: 10px;
|
|
585
|
+
object-fit: cover;
|
|
586
|
+
background: ${bg};
|
|
320
587
|
}
|
|
321
588
|
|
|
322
|
-
.wallet-name {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
color: ${muted};
|
|
327
|
-
white-space: nowrap;
|
|
589
|
+
.wallet-name {
|
|
590
|
+
font-size: 13px;
|
|
591
|
+
font-weight: 500;
|
|
592
|
+
max-width: 100%;
|
|
328
593
|
overflow: hidden;
|
|
329
594
|
text-overflow: ellipsis;
|
|
595
|
+
white-space: nowrap;
|
|
330
596
|
}
|
|
331
597
|
|
|
332
|
-
.
|
|
333
|
-
|
|
598
|
+
.grid-empty {
|
|
599
|
+
grid-column: 1 / -1;
|
|
600
|
+
padding: 16px;
|
|
601
|
+
text-align: center;
|
|
334
602
|
color: ${muted};
|
|
335
|
-
|
|
603
|
+
font-size: 13px;
|
|
336
604
|
}
|
|
337
605
|
|
|
338
606
|
.separator {
|
|
@@ -345,11 +613,15 @@ function buildStyles(isDark: boolean): string {
|
|
|
345
613
|
}
|
|
346
614
|
.sep-line { flex: 1; height: 1px; background: ${border}; }
|
|
347
615
|
|
|
348
|
-
.
|
|
349
|
-
display:
|
|
350
|
-
|
|
616
|
+
.custom-label {
|
|
617
|
+
display: block;
|
|
618
|
+
font-size: 13px;
|
|
619
|
+
color: ${muted};
|
|
620
|
+
margin-bottom: 8px;
|
|
351
621
|
}
|
|
352
622
|
|
|
623
|
+
.input-group { display: flex; gap: 8px; }
|
|
624
|
+
|
|
353
625
|
.url-input {
|
|
354
626
|
flex: 1;
|
|
355
627
|
padding: 10px 14px;
|
|
@@ -374,10 +646,13 @@ function buildStyles(isDark: boolean): string {
|
|
|
374
646
|
cursor: pointer;
|
|
375
647
|
white-space: nowrap;
|
|
376
648
|
}
|
|
377
|
-
.go-btn:disabled {
|
|
378
|
-
opacity: 0.4;
|
|
379
|
-
cursor: not-allowed;
|
|
380
|
-
}
|
|
649
|
+
.go-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
381
650
|
.go-btn:not(:disabled):hover { filter: brightness(1.1); }
|
|
651
|
+
|
|
652
|
+
.url-error {
|
|
653
|
+
margin-top: 8px;
|
|
654
|
+
font-size: 12px;
|
|
655
|
+
color: ${danger};
|
|
656
|
+
}
|
|
382
657
|
`;
|
|
383
658
|
}
|