@enbox/browser 0.3.43 → 0.3.45
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 +161 -155
- package/dist/browser.mjs.map +4 -4
- package/dist/esm/browser-connect-handler.js +17 -20
- package/dist/esm/browser-connect-handler.js.map +1 -1
- package/dist/esm/dweb-connect-client.js +1 -1
- package/dist/esm/dweb-connect-client.js.map +1 -1
- package/dist/esm/index.js +4 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/relay-connect-runner.js +128 -0
- package/dist/esm/relay-connect-runner.js.map +1 -0
- package/dist/esm/ui/connect-modal.js +990 -0
- package/dist/esm/ui/connect-modal.js.map +1 -0
- package/dist/esm/ui/qr.js +468 -0
- package/dist/esm/ui/qr.js.map +1 -0
- package/dist/esm/ui/wallet-icon.js +55 -0
- package/dist/esm/ui/wallet-icon.js.map +1 -0
- package/dist/esm/ui/wallet-well-known.js +61 -0
- package/dist/esm/ui/wallet-well-known.js.map +1 -0
- package/dist/types/browser-connect-handler.d.ts +32 -7
- package/dist/types/browser-connect-handler.d.ts.map +1 -1
- package/dist/types/dweb-connect-client.d.ts +3 -1
- package/dist/types/dweb-connect-client.d.ts.map +1 -1
- package/dist/types/index.d.ts +8 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/relay-connect-runner.d.ts +82 -0
- package/dist/types/relay-connect-runner.d.ts.map +1 -0
- package/dist/types/ui/connect-modal.d.ts +108 -0
- package/dist/types/ui/connect-modal.d.ts.map +1 -0
- package/dist/types/ui/qr.d.ts +55 -0
- package/dist/types/ui/qr.d.ts.map +1 -0
- package/dist/types/ui/wallet-icon.d.ts +13 -0
- package/dist/types/ui/wallet-icon.d.ts.map +1 -0
- package/dist/types/ui/wallet-well-known.d.ts +33 -0
- package/dist/types/ui/wallet-well-known.d.ts.map +1 -0
- package/package.json +7 -5
- package/src/browser-connect-handler.ts +51 -21
- package/src/dweb-connect-client.ts +1 -1
- package/src/index.ts +8 -2
- package/src/relay-connect-runner.ts +209 -0
- package/src/ui/connect-modal.ts +1201 -0
- package/src/ui/qr.ts +549 -0
- package/src/ui/wallet-icon.ts +63 -0
- package/src/ui/wallet-well-known.ts +57 -0
- package/dist/esm/ui/wallet-selector.js +0 -585
- package/dist/esm/ui/wallet-selector.js.map +0 -1
- package/dist/types/ui/wallet-selector.d.ts +0 -44
- package/dist/types/ui/wallet-selector.d.ts.map +0 -1
- package/src/ui/wallet-selector.ts +0 -658
|
@@ -1,658 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Wallet selector modal — a self-contained, framework-agnostic UI component.
|
|
3
|
-
*
|
|
4
|
-
* Injected into the DOM as a Shadow DOM element to prevent style conflicts.
|
|
5
|
-
* Inspired by WalletConnect's Web3Modal pattern.
|
|
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
|
-
*
|
|
18
|
-
* @module
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import type { WalletOption } from '../browser-connect-handler.js';
|
|
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
|
-
|
|
63
|
-
/** Shows the wallet selector modal and resolves with the chosen wallet URL. */
|
|
64
|
-
export function showWalletSelector(
|
|
65
|
-
wallets: WalletOption[],
|
|
66
|
-
options: WalletSelectorOptions = {},
|
|
67
|
-
): Promise<string> {
|
|
68
|
-
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
|
69
|
-
throw new Error('[@enbox/browser] Wallet selector is only available in browser environments.');
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const validateWalletUrl = options.validateWalletUrl ?? probeWalletWellKnown;
|
|
73
|
-
|
|
74
|
-
return new Promise<string>((resolve, reject) => {
|
|
75
|
-
// Create the host element with Shadow DOM isolation.
|
|
76
|
-
const host = document.createElement('div');
|
|
77
|
-
host.id = 'enbox-wallet-selector';
|
|
78
|
-
const shadow = host.attachShadow({ mode: 'open' });
|
|
79
|
-
|
|
80
|
-
let onKeydown: ((e: KeyboardEvent) => void) | undefined;
|
|
81
|
-
const cleanup = (): void => {
|
|
82
|
-
if (onKeydown !== undefined) {
|
|
83
|
-
document.removeEventListener('keydown', onKeydown);
|
|
84
|
-
onKeydown = undefined;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
try { document.body.removeChild(host); } catch { /* best effort */ }
|
|
88
|
-
};
|
|
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
|
-
|
|
100
|
-
// Detect dark mode.
|
|
101
|
-
const isDark = window.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false;
|
|
102
|
-
|
|
103
|
-
// Build styles.
|
|
104
|
-
const style = document.createElement('style');
|
|
105
|
-
style.textContent = buildStyles(isDark);
|
|
106
|
-
shadow.appendChild(style);
|
|
107
|
-
|
|
108
|
-
// Build modal DOM.
|
|
109
|
-
const overlay = document.createElement('div');
|
|
110
|
-
overlay.className = 'overlay';
|
|
111
|
-
|
|
112
|
-
const modal = document.createElement('div');
|
|
113
|
-
modal.className = 'modal';
|
|
114
|
-
|
|
115
|
-
modal.appendChild(buildHeader(settleReject));
|
|
116
|
-
|
|
117
|
-
const recommended = wallets[0];
|
|
118
|
-
if (recommended !== undefined) {
|
|
119
|
-
modal.appendChild(buildQuickConnect(recommended, settleResolve));
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
modal.appendChild(buildWalletGrid(wallets, settleResolve));
|
|
123
|
-
modal.appendChild(buildSeparator());
|
|
124
|
-
modal.appendChild(buildCustomUrl(validateWalletUrl, settleResolve));
|
|
125
|
-
|
|
126
|
-
// Close on overlay click.
|
|
127
|
-
overlay.addEventListener('click', (e) => {
|
|
128
|
-
if (e.target === overlay) {
|
|
129
|
-
settleReject();
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
// Close on Escape.
|
|
134
|
-
onKeydown = (e: KeyboardEvent): void => {
|
|
135
|
-
if (e.key === 'Escape') {
|
|
136
|
-
settleReject();
|
|
137
|
-
}
|
|
138
|
-
};
|
|
139
|
-
document.addEventListener('keydown', onKeydown);
|
|
140
|
-
|
|
141
|
-
overlay.appendChild(modal);
|
|
142
|
-
shadow.appendChild(overlay);
|
|
143
|
-
document.body.appendChild(host);
|
|
144
|
-
|
|
145
|
-
// Focus the search box for keyboard-first users.
|
|
146
|
-
shadow.querySelector<HTMLInputElement>('.wallet-search')?.focus();
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// ─── DOM builders ────────────────────────────────────────────────
|
|
151
|
-
|
|
152
|
-
function buildHeader(onCancel: () => void): HTMLDivElement {
|
|
153
|
-
const header = document.createElement('div');
|
|
154
|
-
header.className = 'header';
|
|
155
|
-
|
|
156
|
-
const title = document.createElement('h2');
|
|
157
|
-
title.textContent = 'Connect a wallet';
|
|
158
|
-
|
|
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);
|
|
164
|
-
|
|
165
|
-
header.appendChild(title);
|
|
166
|
-
header.appendChild(closeBtn);
|
|
167
|
-
return header;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function buildQuickConnect(recommended: WalletOption, onSelect: (url: string) => void): HTMLButtonElement {
|
|
171
|
-
const quick = document.createElement('button');
|
|
172
|
-
quick.className = 'quick-connect';
|
|
173
|
-
|
|
174
|
-
const bolt = document.createElement('span');
|
|
175
|
-
bolt.className = 'quick-bolt';
|
|
176
|
-
bolt.textContent = '⚡'; // ⚡
|
|
177
|
-
|
|
178
|
-
const text = document.createElement('span');
|
|
179
|
-
text.className = 'quick-text';
|
|
180
|
-
|
|
181
|
-
const label = document.createElement('span');
|
|
182
|
-
label.className = 'quick-label';
|
|
183
|
-
label.textContent = 'Quick connect';
|
|
184
|
-
|
|
185
|
-
const sub = document.createElement('span');
|
|
186
|
-
sub.className = 'quick-sub';
|
|
187
|
-
sub.textContent = `Connect with ${recommended.name}`;
|
|
188
|
-
|
|
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
|
-
}
|
|
196
|
-
|
|
197
|
-
function buildWalletGrid(wallets: WalletOption[], onSelect: (url: string) => void): HTMLDivElement {
|
|
198
|
-
const section = document.createElement('div');
|
|
199
|
-
section.className = 'grid-section';
|
|
200
|
-
|
|
201
|
-
const search = document.createElement('input');
|
|
202
|
-
search.type = 'search';
|
|
203
|
-
search.className = 'wallet-search';
|
|
204
|
-
search.placeholder = 'Search wallets…';
|
|
205
|
-
|
|
206
|
-
const grid = document.createElement('div');
|
|
207
|
-
grid.className = 'wallet-grid';
|
|
208
|
-
|
|
209
|
-
const empty = document.createElement('div');
|
|
210
|
-
empty.className = 'grid-empty';
|
|
211
|
-
empty.textContent = 'No wallets match your search.';
|
|
212
|
-
empty.style.display = 'none';
|
|
213
|
-
|
|
214
|
-
const tiles = wallets.map((wallet) => {
|
|
215
|
-
const tile = buildWalletTile(wallet, onSelect);
|
|
216
|
-
grid.appendChild(tile);
|
|
217
|
-
return { name: wallet.name.toLowerCase(), tile };
|
|
218
|
-
});
|
|
219
|
-
|
|
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;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
empty.style.display = visible === 0 ? '' : 'none';
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
section.appendChild(search);
|
|
234
|
-
section.appendChild(grid);
|
|
235
|
-
section.appendChild(empty);
|
|
236
|
-
return section;
|
|
237
|
-
}
|
|
238
|
-
|
|
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;
|
|
243
|
-
|
|
244
|
-
tile.appendChild(buildWalletIcon(wallet));
|
|
245
|
-
|
|
246
|
-
const name = document.createElement('span');
|
|
247
|
-
name.className = 'wallet-name';
|
|
248
|
-
name.textContent = wallet.name;
|
|
249
|
-
tile.appendChild(name);
|
|
250
|
-
|
|
251
|
-
tile.addEventListener('click', () => onSelect(wallet.url));
|
|
252
|
-
return tile;
|
|
253
|
-
}
|
|
254
|
-
|
|
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
|
-
}
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
img.src = sources[index];
|
|
291
|
-
wrap.appendChild(img);
|
|
292
|
-
return wrap;
|
|
293
|
-
}
|
|
294
|
-
|
|
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
|
-
}
|
|
307
|
-
|
|
308
|
-
return [`${origin}/favicon.svg`, `${origin}/favicon.ico`, `${origin}/favicon.png`];
|
|
309
|
-
}
|
|
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
|
-
|
|
414
|
-
function isValidUrl(value: string): boolean {
|
|
415
|
-
try {
|
|
416
|
-
const url = new URL(value.includes('://') ? value : `https://${value}`);
|
|
417
|
-
return url.protocol === 'https:' || url.protocol === 'http:';
|
|
418
|
-
} catch {
|
|
419
|
-
return false;
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
function normalizeUrl(value: string): string {
|
|
424
|
-
const withScheme = value.includes('://') ? value : `https://${value}`;
|
|
425
|
-
// Return origin only (strip path/trailing slash).
|
|
426
|
-
return new URL(withScheme).origin;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
function buildStyles(isDark: boolean): string {
|
|
430
|
-
const bg = isDark ? '#1a1a2e' : '#ffffff';
|
|
431
|
-
const text = isDark ? '#e0e0e0' : '#1a1a2e';
|
|
432
|
-
const muted = isDark ? '#888' : '#666';
|
|
433
|
-
const border = isDark ? '#333' : '#e0e0e0';
|
|
434
|
-
const itemBg = isDark ? '#16213e' : '#f8f9fa';
|
|
435
|
-
const itemHover = isDark ? '#0f3460' : '#e9ecef';
|
|
436
|
-
const accent = isDark ? '#4a9eff' : '#0066cc';
|
|
437
|
-
const accentAlt = isDark ? '#7b5cff' : '#5b8def';
|
|
438
|
-
const danger = isDark ? '#ff6b6b' : '#c0392b';
|
|
439
|
-
const overlayBg = 'rgba(0, 0, 0, 0.5)';
|
|
440
|
-
|
|
441
|
-
return `
|
|
442
|
-
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
443
|
-
|
|
444
|
-
.overlay {
|
|
445
|
-
position: fixed;
|
|
446
|
-
inset: 0;
|
|
447
|
-
z-index: 2147483647;
|
|
448
|
-
display: flex;
|
|
449
|
-
align-items: center;
|
|
450
|
-
justify-content: center;
|
|
451
|
-
background: ${overlayBg};
|
|
452
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
453
|
-
animation: fadeIn 0.15s ease-out;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
@keyframes fadeIn {
|
|
457
|
-
from { opacity: 0; }
|
|
458
|
-
to { opacity: 1; }
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
@keyframes slideUp {
|
|
462
|
-
from { transform: translateY(20px); opacity: 0; }
|
|
463
|
-
to { transform: translateY(0); opacity: 1; }
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
.modal {
|
|
467
|
-
background: ${bg};
|
|
468
|
-
color: ${text};
|
|
469
|
-
border-radius: 16px;
|
|
470
|
-
width: 420px;
|
|
471
|
-
max-width: 92vw;
|
|
472
|
-
max-height: 86vh;
|
|
473
|
-
overflow-y: auto;
|
|
474
|
-
padding: 24px;
|
|
475
|
-
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
|
476
|
-
animation: slideUp 0.2s ease-out;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
.header {
|
|
480
|
-
display: flex;
|
|
481
|
-
justify-content: space-between;
|
|
482
|
-
align-items: center;
|
|
483
|
-
margin-bottom: 18px;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
h2 { font-size: 18px; font-weight: 600; }
|
|
487
|
-
|
|
488
|
-
.close-btn {
|
|
489
|
-
background: none;
|
|
490
|
-
border: none;
|
|
491
|
-
font-size: 24px;
|
|
492
|
-
cursor: pointer;
|
|
493
|
-
color: ${muted};
|
|
494
|
-
padding: 4px 8px;
|
|
495
|
-
border-radius: 8px;
|
|
496
|
-
line-height: 1;
|
|
497
|
-
}
|
|
498
|
-
.close-btn:hover { color: ${text}; background: ${itemBg}; }
|
|
499
|
-
|
|
500
|
-
.quick-connect {
|
|
501
|
-
display: flex;
|
|
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;
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
.wallet-item {
|
|
545
|
-
display: flex;
|
|
546
|
-
flex-direction: column;
|
|
547
|
-
align-items: center;
|
|
548
|
-
justify-content: center;
|
|
549
|
-
gap: 8px;
|
|
550
|
-
padding: 14px 8px;
|
|
551
|
-
background: ${itemBg};
|
|
552
|
-
border: 1px solid ${border};
|
|
553
|
-
border-radius: 12px;
|
|
554
|
-
cursor: pointer;
|
|
555
|
-
color: ${text};
|
|
556
|
-
transition: background 0.15s, border-color 0.15s;
|
|
557
|
-
}
|
|
558
|
-
.wallet-item:hover { background: ${itemHover}; border-color: ${accent}; }
|
|
559
|
-
|
|
560
|
-
.wallet-icon {
|
|
561
|
-
position: relative;
|
|
562
|
-
width: 40px;
|
|
563
|
-
height: 40px;
|
|
564
|
-
display: flex;
|
|
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};
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
.wallet-name {
|
|
590
|
-
font-size: 13px;
|
|
591
|
-
font-weight: 500;
|
|
592
|
-
max-width: 100%;
|
|
593
|
-
overflow: hidden;
|
|
594
|
-
text-overflow: ellipsis;
|
|
595
|
-
white-space: nowrap;
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
.grid-empty {
|
|
599
|
-
grid-column: 1 / -1;
|
|
600
|
-
padding: 16px;
|
|
601
|
-
text-align: center;
|
|
602
|
-
color: ${muted};
|
|
603
|
-
font-size: 13px;
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
.separator {
|
|
607
|
-
display: flex;
|
|
608
|
-
align-items: center;
|
|
609
|
-
gap: 12px;
|
|
610
|
-
margin: 20px 0;
|
|
611
|
-
color: ${muted};
|
|
612
|
-
font-size: 13px;
|
|
613
|
-
}
|
|
614
|
-
.sep-line { flex: 1; height: 1px; background: ${border}; }
|
|
615
|
-
|
|
616
|
-
.custom-label {
|
|
617
|
-
display: block;
|
|
618
|
-
font-size: 13px;
|
|
619
|
-
color: ${muted};
|
|
620
|
-
margin-bottom: 8px;
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
.input-group { display: flex; gap: 8px; }
|
|
624
|
-
|
|
625
|
-
.url-input {
|
|
626
|
-
flex: 1;
|
|
627
|
-
padding: 10px 14px;
|
|
628
|
-
border: 1px solid ${border};
|
|
629
|
-
border-radius: 10px;
|
|
630
|
-
font-size: 14px;
|
|
631
|
-
background: ${itemBg};
|
|
632
|
-
color: ${text};
|
|
633
|
-
outline: none;
|
|
634
|
-
}
|
|
635
|
-
.url-input:focus { border-color: ${accent}; }
|
|
636
|
-
.url-input::placeholder { color: ${muted}; }
|
|
637
|
-
|
|
638
|
-
.go-btn {
|
|
639
|
-
padding: 10px 18px;
|
|
640
|
-
border: none;
|
|
641
|
-
border-radius: 10px;
|
|
642
|
-
background: ${accent};
|
|
643
|
-
color: #fff;
|
|
644
|
-
font-size: 14px;
|
|
645
|
-
font-weight: 500;
|
|
646
|
-
cursor: pointer;
|
|
647
|
-
white-space: nowrap;
|
|
648
|
-
}
|
|
649
|
-
.go-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
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
|
-
}
|
|
657
|
-
`;
|
|
658
|
-
}
|