@enbox/browser 0.3.41 → 0.3.42

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.
@@ -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(wallets: WalletOption[]): Promise<string> {
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
- // Header
50
- const header = document.createElement('div');
51
- header.className = 'header';
115
+ modal.appendChild(buildHeader(settleReject));
52
116
 
53
- const title = document.createElement('h2');
54
- title.textContent = 'Connect Wallet';
117
+ const recommended = wallets[0];
118
+ if (recommended !== undefined) {
119
+ modal.appendChild(buildQuickConnect(recommended, settleResolve));
120
+ }
55
121
 
56
- const closeBtn = document.createElement('button');
57
- closeBtn.className = 'close-btn';
58
- closeBtn.innerHTML = '&times;';
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
- header.appendChild(title);
65
- header.appendChild(closeBtn);
66
- modal.appendChild(header);
126
+ // Close on overlay click.
127
+ overlay.addEventListener('click', (e) => {
128
+ if (e.target === overlay) {
129
+ settleReject();
130
+ }
131
+ });
67
132
 
68
- // Wallet list
69
- const list = document.createElement('div');
70
- list.className = 'wallet-list';
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
- for (const wallet of wallets) {
73
- const item = document.createElement('button');
74
- item.className = 'wallet-item';
141
+ overlay.appendChild(modal);
142
+ shadow.appendChild(overlay);
143
+ document.body.appendChild(host);
75
144
 
76
- const icon = document.createElement('img');
77
- icon.src = wallet.icon ?? faviconUrl(wallet.url);
78
- icon.alt = wallet.name;
79
- icon.width = 32;
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
- const textGroup = document.createElement('div');
84
- textGroup.className = 'wallet-text';
150
+ // ─── DOM builders ────────────────────────────────────────────────
85
151
 
86
- const name = document.createElement('span');
87
- name.className = 'wallet-name';
88
- name.textContent = wallet.name;
152
+ function buildHeader(onCancel: () => void): HTMLDivElement {
153
+ const header = document.createElement('div');
154
+ header.className = 'header';
89
155
 
90
- textGroup.appendChild(name);
156
+ const title = document.createElement('h2');
157
+ title.textContent = 'Connect a wallet';
91
158
 
92
- if (wallet.description) {
93
- const desc = document.createElement('span');
94
- desc.className = 'wallet-description';
95
- desc.textContent = wallet.description;
96
- textGroup.appendChild(desc);
97
- }
159
+ const closeBtn = document.createElement('button');
160
+ closeBtn.className = 'close-btn';
161
+ closeBtn.setAttribute('aria-label', 'Close');
162
+ closeBtn.innerHTML = '&times;';
163
+ closeBtn.addEventListener('click', onCancel);
98
164
 
99
- const arrow = document.createElement('span');
100
- arrow.className = 'arrow';
101
- arrow.textContent = '\u203A'; // ›
165
+ header.appendChild(title);
166
+ header.appendChild(closeBtn);
167
+ return header;
168
+ }
102
169
 
103
- item.appendChild(icon);
104
- item.appendChild(textGroup);
105
- item.appendChild(arrow);
170
+ function buildQuickConnect(recommended: WalletOption, onSelect: (url: string) => void): HTMLButtonElement {
171
+ const quick = document.createElement('button');
172
+ quick.className = 'quick-connect';
106
173
 
107
- item.addEventListener('click', () => {
108
- cleanup();
109
- resolve(wallet.url);
110
- });
174
+ const bolt = document.createElement('span');
175
+ bolt.className = 'quick-bolt';
176
+ bolt.textContent = '⚡'; // ⚡
111
177
 
112
- list.appendChild(item);
113
- }
178
+ const text = document.createElement('span');
179
+ text.className = 'quick-text';
114
180
 
115
- modal.appendChild(list);
181
+ const label = document.createElement('span');
182
+ label.className = 'quick-label';
183
+ label.textContent = 'Quick connect';
116
184
 
117
- // Separator
118
- const sep = document.createElement('div');
119
- sep.className = 'separator';
185
+ const sub = document.createElement('span');
186
+ sub.className = 'quick-sub';
187
+ sub.textContent = `Connect with ${recommended.name}`;
120
188
 
121
- const sepLine1 = document.createElement('div');
122
- sepLine1.className = 'sep-line';
123
- const sepText = document.createElement('span');
124
- sepText.textContent = 'or';
125
- const sepLine2 = document.createElement('div');
126
- sepLine2.className = 'sep-line';
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
- sep.appendChild(sepLine1);
129
- sep.appendChild(sepText);
130
- sep.appendChild(sepLine2);
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
- // Custom URL input
134
- const inputGroup = document.createElement('div');
135
- inputGroup.className = 'input-group';
201
+ const search = document.createElement('input');
202
+ search.type = 'search';
203
+ search.className = 'wallet-search';
204
+ search.placeholder = 'Search wallets…';
136
205
 
137
- const input = document.createElement('input');
138
- input.type = 'url';
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
- const goBtn = document.createElement('button');
143
- goBtn.className = 'go-btn';
144
- goBtn.textContent = 'Connect';
145
- goBtn.disabled = true;
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
- input.addEventListener('input', () => {
148
- goBtn.disabled = !isValidUrl(input.value);
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
- input.addEventListener('keydown', (e) => {
152
- if (e.key === 'Enter' && isValidUrl(input.value)) {
153
- cleanup();
154
- resolve(normalizeUrl(input.value));
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
- goBtn.addEventListener('click', () => {
159
- if (isValidUrl(input.value)) {
160
- cleanup();
161
- resolve(normalizeUrl(input.value));
162
- }
163
- });
233
+ section.appendChild(search);
234
+ section.appendChild(grid);
235
+ section.appendChild(empty);
236
+ return section;
237
+ }
164
238
 
165
- inputGroup.appendChild(input);
166
- inputGroup.appendChild(goBtn);
167
- modal.appendChild(inputGroup);
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
- // Close on overlay click.
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
- // Close on Escape.
178
- onKeydown = (e: KeyboardEvent): void => {
179
- if (e.key === 'Escape') {
180
- cleanup();
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
- overlay.appendChild(modal);
187
- shadow.appendChild(overlay);
188
- document.body.appendChild(host);
251
+ tile.addEventListener('click', () => onSelect(wallet.url));
252
+ return tile;
253
+ }
189
254
 
190
- // Focus the input for keyboard-first users.
191
- input.focus();
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
- // ─── Helpers ─────────────────────────────────────────────────────
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
- function faviconUrl(walletUrl: string): string {
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
- if (!value.includes('://')) {
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 url.origin;
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: 380px;
259
- max-width: 90vw;
260
- max-height: 80vh;
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: 20px;
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
- .wallet-list {
500
+ .quick-connect {
292
501
  display: flex;
293
- flex-direction: column;
294
- gap: 8px;
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
- gap: 12px;
301
- padding: 12px 16px;
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-text {
315
- flex: 1;
560
+ .wallet-icon {
561
+ position: relative;
562
+ width: 40px;
563
+ height: 40px;
316
564
  display: flex;
317
- flex-direction: column;
318
- gap: 2px;
319
- min-width: 0;
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 { font-weight: 500; }
323
-
324
- .wallet-description {
325
- font-size: 12px;
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
- .arrow {
333
- font-size: 20px;
598
+ .grid-empty {
599
+ grid-column: 1 / -1;
600
+ padding: 16px;
601
+ text-align: center;
334
602
  color: ${muted};
335
- flex-shrink: 0;
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
- .input-group {
349
- display: flex;
350
- gap: 8px;
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
  }