@enbox/browser 0.3.45 → 0.3.47
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 +245 -94
- package/dist/browser.mjs.map +3 -3
- package/dist/esm/browser-connect-handler.js +2 -1
- package/dist/esm/browser-connect-handler.js.map +1 -1
- package/dist/esm/ui/connect-modal.js +469 -159
- package/dist/esm/ui/connect-modal.js.map +1 -1
- package/dist/types/browser-connect-handler.d.ts +8 -1
- package/dist/types/browser-connect-handler.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/ui/connect-modal.d.ts +57 -3
- package/dist/types/ui/connect-modal.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/browser-connect-handler.ts +11 -1
- package/src/index.ts +1 -1
- package/src/ui/connect-modal.ts +549 -162
package/src/ui/connect-modal.ts
CHANGED
|
@@ -13,9 +13,19 @@
|
|
|
13
13
|
* 2. **Stage** — a height-stable region where states crossfade: the QR
|
|
14
14
|
* (or deep link on phones), the pairing-code input, progress, success,
|
|
15
15
|
* and error states.
|
|
16
|
-
* 3. **Footer** — the
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* 3. **Footer** — the wallet identity row ("Connecting with" + the selected
|
|
17
|
+
* wallet and the next catalog wallets as tiles, capped so the same wallet
|
|
18
|
+
* never appears twice), a More tile that expands the full catalog grid
|
|
19
|
+
* with search + custom URL in place, and the alternate-method link.
|
|
20
|
+
*
|
|
21
|
+
* Theming: the stylesheet is static and reads `--ec-*` design tokens.
|
|
22
|
+
* Light/dark follows the visitor's system via `prefers-color-scheme` and
|
|
23
|
+
* updates live; the embedding app can force an appearance or override
|
|
24
|
+
* palette tokens through {@link ConnectModalOptions.theme}. Overrides are
|
|
25
|
+
* applied as inline custom properties — never interpolated into the
|
|
26
|
+
* stylesheet — so a hostile string can't escape its declaration. The QR
|
|
27
|
+
* card alone is theme-invariant (dark modules on white scan most
|
|
28
|
+
* reliably).
|
|
19
29
|
*
|
|
20
30
|
* The phone path drives the `@enbox/connect` relay transport via
|
|
21
31
|
* {@link runRelayConnect}; the browser path drives the popup transport via
|
|
@@ -56,6 +66,77 @@ const REMINT_MIN_MS = 10_000;
|
|
|
56
66
|
/** Pairing code length (matches the wallet's generated PIN). */
|
|
57
67
|
const PIN_LENGTH = 4;
|
|
58
68
|
|
|
69
|
+
/** Catalog size beyond which the wallet switcher shows its search bar. */
|
|
70
|
+
const WALLET_SEARCH_THRESHOLD = 4; // one full row of tiles
|
|
71
|
+
|
|
72
|
+
/** Wallet tiles shown in the collapsed identity row (plus the More tile). */
|
|
73
|
+
const WALLET_ROW_SIZE = 3;
|
|
74
|
+
|
|
75
|
+
/** Palette overrides for one appearance's design tokens. */
|
|
76
|
+
export interface ConnectModalPalette {
|
|
77
|
+
/** Modal surface. */
|
|
78
|
+
background?: string;
|
|
79
|
+
|
|
80
|
+
/** Inset surfaces: tiles, inputs, hover fills. */
|
|
81
|
+
surface?: string;
|
|
82
|
+
|
|
83
|
+
/** Primary text. */
|
|
84
|
+
text?: string;
|
|
85
|
+
|
|
86
|
+
/** Secondary text. */
|
|
87
|
+
textMuted?: string;
|
|
88
|
+
|
|
89
|
+
/** Hairline borders and dividers. */
|
|
90
|
+
border?: string;
|
|
91
|
+
|
|
92
|
+
/** Brand accent: buttons, links, focus rings, selection. */
|
|
93
|
+
accent?: string;
|
|
94
|
+
|
|
95
|
+
/** Text/icon color on accent surfaces. */
|
|
96
|
+
accentContrast?: string;
|
|
97
|
+
|
|
98
|
+
/** Error text and invalid-input borders. */
|
|
99
|
+
danger?: string;
|
|
100
|
+
|
|
101
|
+
/** Success surfaces (the connected check). */
|
|
102
|
+
success?: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Optional color scheme provided by the embedding app. */
|
|
106
|
+
export interface ConnectModalTheme {
|
|
107
|
+
/**
|
|
108
|
+
* Follow the visitor's system appearance (`'auto'`, live-updating) or
|
|
109
|
+
* force one.
|
|
110
|
+
* @default 'auto'
|
|
111
|
+
*/
|
|
112
|
+
appearance?: 'auto' | 'light' | 'dark';
|
|
113
|
+
|
|
114
|
+
/** Brand accent applied in both appearances (per-scheme palettes win). */
|
|
115
|
+
accent?: string;
|
|
116
|
+
|
|
117
|
+
/** Text/icon color on accent surfaces, in both appearances. */
|
|
118
|
+
accentContrast?: string;
|
|
119
|
+
|
|
120
|
+
/** Token overrides applied while the light appearance is active. */
|
|
121
|
+
light?: ConnectModalPalette;
|
|
122
|
+
|
|
123
|
+
/** Token overrides applied while the dark appearance is active. */
|
|
124
|
+
dark?: ConnectModalPalette;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Design-token custom property behind each palette key. */
|
|
128
|
+
const PALETTE_TOKENS: Record<keyof ConnectModalPalette, string> = {
|
|
129
|
+
background : '--ec-bg',
|
|
130
|
+
surface : '--ec-surface',
|
|
131
|
+
text : '--ec-text',
|
|
132
|
+
textMuted : '--ec-muted',
|
|
133
|
+
border : '--ec-border',
|
|
134
|
+
accent : '--ec-accent',
|
|
135
|
+
accentContrast : '--ec-accent-contrast',
|
|
136
|
+
danger : '--ec-danger',
|
|
137
|
+
success : '--ec-success',
|
|
138
|
+
};
|
|
139
|
+
|
|
59
140
|
/** Options for {@link runConnectModal}. */
|
|
60
141
|
export interface ConnectModalOptions {
|
|
61
142
|
/** Wallet catalog shown in the wallet switcher. */
|
|
@@ -99,6 +180,13 @@ export interface ConnectModalOptions {
|
|
|
99
180
|
/** Timeout in milliseconds for either method's handshake. */
|
|
100
181
|
timeout?: number;
|
|
101
182
|
|
|
183
|
+
/**
|
|
184
|
+
* Color scheme. By default the modal follows the visitor's system
|
|
185
|
+
* light/dark appearance; apps can force an appearance and/or override
|
|
186
|
+
* palette tokens per scheme to match their brand.
|
|
187
|
+
*/
|
|
188
|
+
theme?: ConnectModalTheme;
|
|
189
|
+
|
|
102
190
|
/** DWN protocols and permission scopes being requested. */
|
|
103
191
|
permissionRequests: ConnectPermissionRequest[];
|
|
104
192
|
|
|
@@ -250,7 +338,7 @@ export function runConnectModal(options: ConnectModalOptions): Promise<ConnectRe
|
|
|
250
338
|
}
|
|
251
339
|
|
|
252
340
|
const deps: ConnectModalDeps = { ...defaultDeps(), ...options.deps };
|
|
253
|
-
const wallets = options.wallets ?? [];
|
|
341
|
+
const wallets = dedupeWalletsByUrl(options.wallets ?? []);
|
|
254
342
|
const rememberChoice = options.rememberChoice ?? true;
|
|
255
343
|
const appName = options.appName ?? window.location.host;
|
|
256
344
|
const relayWalletPath = options.relayWalletPath ?? '/connect/app';
|
|
@@ -273,9 +361,8 @@ export function runConnectModal(options: ConnectModalOptions): Promise<ConnectRe
|
|
|
273
361
|
host.id = 'enbox-connect-modal';
|
|
274
362
|
const shadow = host.attachShadow({ mode: 'open' });
|
|
275
363
|
|
|
276
|
-
const isDark = window.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false;
|
|
277
364
|
const style = document.createElement('style');
|
|
278
|
-
style.textContent =
|
|
365
|
+
style.textContent = MODAL_STYLES;
|
|
279
366
|
shadow.appendChild(style);
|
|
280
367
|
|
|
281
368
|
const overlay = document.createElement('div');
|
|
@@ -286,6 +373,47 @@ export function runConnectModal(options: ConnectModalOptions): Promise<ConnectRe
|
|
|
286
373
|
modal.setAttribute('aria-modal', 'true');
|
|
287
374
|
modal.setAttribute('aria-label', `Connect to ${appName}`);
|
|
288
375
|
|
|
376
|
+
// ── Theme ──────────────────────────────────────────────────
|
|
377
|
+
// System light/dark is pure CSS (`prefers-color-scheme`), so it tracks
|
|
378
|
+
// the visitor live. A forced appearance pins the token block instead.
|
|
379
|
+
const theme = options.theme ?? {};
|
|
380
|
+
const appearance = theme.appearance ?? 'auto';
|
|
381
|
+
if (appearance !== 'auto') {
|
|
382
|
+
modal.setAttribute('data-appearance', appearance);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Dapp palette overrides land as inline custom properties: setProperty
|
|
386
|
+
// parses each value as a lone declaration, so a hostile string cannot
|
|
387
|
+
// escape into the stylesheet.
|
|
388
|
+
const systemDark = window.matchMedia?.('(prefers-color-scheme: dark)');
|
|
389
|
+
const hasPaletteOverrides = theme.accent !== undefined || theme.accentContrast !== undefined
|
|
390
|
+
|| theme.light !== undefined || theme.dark !== undefined;
|
|
391
|
+
const applyPaletteTokens = (): void => {
|
|
392
|
+
const scheme = appearance === 'auto'
|
|
393
|
+
? (systemDark?.matches === true ? 'dark' : 'light')
|
|
394
|
+
: appearance;
|
|
395
|
+
const palette: ConnectModalPalette = {
|
|
396
|
+
accent : theme.accent,
|
|
397
|
+
accentContrast : theme.accentContrast,
|
|
398
|
+
...theme[scheme],
|
|
399
|
+
};
|
|
400
|
+
for (const key of Object.keys(PALETTE_TOKENS) as Array<keyof ConnectModalPalette>) {
|
|
401
|
+
const value = palette[key];
|
|
402
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
403
|
+
modal.style.setProperty(PALETTE_TOKENS[key], value);
|
|
404
|
+
} else {
|
|
405
|
+
modal.style.removeProperty(PALETTE_TOKENS[key]);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
const onSchemeChange = (): void => applyPaletteTokens();
|
|
410
|
+
if (hasPaletteOverrides) {
|
|
411
|
+
applyPaletteTokens();
|
|
412
|
+
if (appearance === 'auto') {
|
|
413
|
+
systemDark?.addEventListener?.('change', onSchemeChange);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
289
417
|
// ── Session state ──────────────────────────────────────────
|
|
290
418
|
let settled = false;
|
|
291
419
|
let relaySession: RelaySession | undefined;
|
|
@@ -294,10 +422,40 @@ export function runConnectModal(options: ConnectModalOptions): Promise<ConnectRe
|
|
|
294
422
|
let pinResolve: ((pin: string) => void) | undefined;
|
|
295
423
|
const discoveryCache = new Map<string, Promise<string | undefined>>();
|
|
296
424
|
|
|
425
|
+
// Collapsed identity row: the selected wallet plus the next catalog
|
|
426
|
+
// wallets, never repeating one. Slots stay stable across in-row
|
|
427
|
+
// switches (only the highlight moves); the row recomposes only when a
|
|
428
|
+
// wallet from outside it — grid pick or custom URL — becomes selected,
|
|
429
|
+
// which puts that wallet in the first slot.
|
|
430
|
+
let rowWallets: WalletOption[] = [];
|
|
431
|
+
|
|
432
|
+
/** The catalog entry for `url`, or a synthesized option for custom URLs. */
|
|
433
|
+
const walletByUrl = (url: string): WalletOption =>
|
|
434
|
+
wallets.find((wallet) => wallet.url === url) ?? { name: hostnameOf(url) ?? url, url };
|
|
435
|
+
|
|
436
|
+
const composeWalletRow = (): void => {
|
|
437
|
+
if (walletUrl === undefined) {
|
|
438
|
+
rowWallets = wallets.slice(0, WALLET_ROW_SIZE);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
if (rowWallets.some((wallet) => wallet.url === walletUrl)) {
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
const selected = walletByUrl(walletUrl);
|
|
445
|
+
rowWallets = [selected, ...wallets.filter((wallet) => wallet.url !== selected.url)].slice(0, WALLET_ROW_SIZE);
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
const selectWallet = (url: string): void => {
|
|
449
|
+
walletUrl = url;
|
|
450
|
+
renderFooter();
|
|
451
|
+
void startMethod();
|
|
452
|
+
};
|
|
453
|
+
|
|
297
454
|
const cleanup = (): void => {
|
|
298
455
|
relaySession?.cancel();
|
|
299
456
|
clearTimeout(remintTimer);
|
|
300
457
|
document.removeEventListener('keydown', onKeydown);
|
|
458
|
+
systemDark?.removeEventListener?.('change', onSchemeChange);
|
|
301
459
|
try { document.body.removeChild(host); } catch { /* best effort */ }
|
|
302
460
|
};
|
|
303
461
|
|
|
@@ -344,14 +502,20 @@ export function runConnectModal(options: ConnectModalOptions): Promise<ConnectRe
|
|
|
344
502
|
};
|
|
345
503
|
|
|
346
504
|
const renderQr = (handoff: WalletUriHandoff): void => {
|
|
505
|
+
const wallet = walletUrl !== undefined ? walletByUrl(walletUrl) : undefined;
|
|
506
|
+
const walletName = wallet?.name ?? 'your wallet';
|
|
507
|
+
|
|
347
508
|
if (isMobile) {
|
|
348
509
|
const link = document.createElement('a');
|
|
349
510
|
link.className = 'deep-link';
|
|
350
511
|
link.href = handoff.walletUri;
|
|
351
|
-
|
|
512
|
+
if (wallet !== undefined) {
|
|
513
|
+
link.appendChild(buildWalletIcon(wallet));
|
|
514
|
+
}
|
|
515
|
+
link.appendChild(el('span', 'deep-link-label', `Continue in ${walletName}`));
|
|
352
516
|
setStage(
|
|
353
517
|
link,
|
|
354
|
-
el('p', 'stage-caption',
|
|
518
|
+
el('p', 'stage-caption', `${walletName} opens to approve this connection.`),
|
|
355
519
|
el('p', 'stage-subline', 'Come back here when you’re done.'),
|
|
356
520
|
);
|
|
357
521
|
return;
|
|
@@ -359,16 +523,28 @@ export function runConnectModal(options: ConnectModalOptions): Promise<ConnectRe
|
|
|
359
523
|
|
|
360
524
|
const qrBox = document.createElement('div');
|
|
361
525
|
qrBox.className = 'qr-box';
|
|
526
|
+
// Fixed colors on the white card: dark-on-light QRs scan reliably in
|
|
527
|
+
// both appearances (inverted codes trip up some camera apps).
|
|
362
528
|
qrBox.replaceChildren(qrToSvg(encodeQr(handoff.walletUri), {
|
|
363
|
-
dark :
|
|
529
|
+
dark : '#14141f',
|
|
364
530
|
light : 'transparent',
|
|
365
531
|
quietZone : 2,
|
|
366
532
|
}));
|
|
367
533
|
|
|
534
|
+
// The selected wallet's mark sits at the QR centre — the "this is the
|
|
535
|
+
// wallet you're connecting to" cue. The 44px plate over the ~180px
|
|
536
|
+
// symbol occludes ~5% of modules, well inside ECC level M's 15%
|
|
537
|
+
// recovery budget.
|
|
538
|
+
if (wallet !== undefined) {
|
|
539
|
+
const logo = el('div', 'qr-logo');
|
|
540
|
+
logo.appendChild(buildWalletIcon(wallet));
|
|
541
|
+
qrBox.appendChild(logo);
|
|
542
|
+
}
|
|
543
|
+
|
|
368
544
|
setStage(
|
|
369
545
|
qrBox,
|
|
370
546
|
el('p', 'stage-caption', 'Scan with your phone’s camera'),
|
|
371
|
-
el('p', 'stage-subline',
|
|
547
|
+
el('p', 'stage-subline', `${walletName} stays on your phone.`),
|
|
372
548
|
);
|
|
373
549
|
};
|
|
374
550
|
|
|
@@ -682,64 +858,111 @@ export function runConnectModal(options: ConnectModalOptions): Promise<ConnectRe
|
|
|
682
858
|
await startMethod();
|
|
683
859
|
};
|
|
684
860
|
|
|
685
|
-
// ── Footer:
|
|
861
|
+
// ── Footer: identity row (+ catalog panel), method link below ──
|
|
686
862
|
const renderFooter = (): void => {
|
|
687
863
|
footer.replaceChildren();
|
|
688
864
|
|
|
865
|
+
if (!lockedWallet) {
|
|
866
|
+
composeWalletRow();
|
|
867
|
+
const switcher = buildWalletSwitcher();
|
|
868
|
+
if (switcher.label !== undefined) {
|
|
869
|
+
footer.appendChild(switcher.label);
|
|
870
|
+
}
|
|
871
|
+
footer.appendChild(switcher.row);
|
|
872
|
+
footer.appendChild(switcher.panel);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
const row = document.createElement('div');
|
|
876
|
+
row.className = 'footer-row';
|
|
877
|
+
|
|
689
878
|
if (method === 'phone') {
|
|
690
879
|
const alt = document.createElement('button');
|
|
691
880
|
alt.className = 'footer-link method-link';
|
|
692
|
-
alt.textContent = 'No phone
|
|
881
|
+
alt.textContent = 'No phone? Use this browser →';
|
|
693
882
|
// startPopup runs synchronously in this click handler.
|
|
694
883
|
alt.addEventListener('click', () => { startPopup(); });
|
|
695
|
-
|
|
884
|
+
row.appendChild(alt);
|
|
696
885
|
} else if (!isMobile) {
|
|
697
886
|
const alt = document.createElement('button');
|
|
698
887
|
alt.className = 'footer-link method-link';
|
|
699
888
|
alt.textContent = 'Use your phone instead →';
|
|
700
889
|
alt.addEventListener('click', () => { void switchMethod('phone'); });
|
|
701
|
-
|
|
890
|
+
row.appendChild(alt);
|
|
702
891
|
}
|
|
703
892
|
|
|
704
|
-
if (
|
|
705
|
-
footer.appendChild(
|
|
893
|
+
if (row.childElementCount > 0) {
|
|
894
|
+
footer.appendChild(row);
|
|
706
895
|
}
|
|
707
896
|
};
|
|
708
897
|
|
|
709
|
-
const buildWalletSwitcher = (): HTMLElement => {
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
898
|
+
const buildWalletSwitcher = (): { label?: HTMLElement; row: HTMLDivElement; panel: HTMLDivElement } => {
|
|
899
|
+
// Identity row: selected wallet + the next catalog wallets, then the
|
|
900
|
+
// More tile expanding the full catalog in place.
|
|
901
|
+
const row = document.createElement('div');
|
|
902
|
+
row.className = 'wallet-row';
|
|
903
|
+
row.setAttribute('role', 'radiogroup');
|
|
904
|
+
row.setAttribute('aria-label', 'Wallet');
|
|
905
|
+
|
|
906
|
+
for (const wallet of rowWallets) {
|
|
907
|
+
const tile = document.createElement('button');
|
|
908
|
+
tile.className = 'row-tile';
|
|
909
|
+
tile.title = wallet.description ?? wallet.name;
|
|
910
|
+
tile.setAttribute('role', 'radio');
|
|
911
|
+
const isSelected = wallet.url === walletUrl;
|
|
912
|
+
tile.setAttribute('aria-checked', isSelected ? 'true' : 'false');
|
|
913
|
+
if (isSelected) {
|
|
914
|
+
tile.classList.add('selected');
|
|
915
|
+
}
|
|
916
|
+
tile.appendChild(buildWalletIcon(wallet));
|
|
917
|
+
tile.appendChild(el('span', 'row-tile-name', wallet.name));
|
|
918
|
+
tile.addEventListener('click', () => {
|
|
919
|
+
if (wallet.url !== walletUrl) {
|
|
920
|
+
selectWallet(wallet.url);
|
|
921
|
+
}
|
|
922
|
+
});
|
|
923
|
+
row.appendChild(tile);
|
|
924
|
+
}
|
|
718
925
|
|
|
719
926
|
const panel = document.createElement('div');
|
|
720
927
|
panel.className = 'wallet-panel';
|
|
721
928
|
panel.hidden = true;
|
|
722
929
|
|
|
723
|
-
|
|
724
|
-
const
|
|
930
|
+
const hiddenCount = wallets.filter((wallet) => !rowWallets.some((shown) => shown.url === wallet.url)).length;
|
|
931
|
+
const more = document.createElement('button');
|
|
932
|
+
more.className = 'row-tile more-tile';
|
|
933
|
+
more.title = 'All wallets';
|
|
934
|
+
more.setAttribute('aria-expanded', 'false');
|
|
935
|
+
more.appendChild(el('span', 'more-count', hiddenCount > 0 ? `+${hiddenCount}` : '⋯'));
|
|
936
|
+
more.appendChild(el('span', 'row-tile-name', 'More'));
|
|
937
|
+
more.addEventListener('click', () => {
|
|
938
|
+
panel.hidden = !panel.hidden;
|
|
939
|
+
more.setAttribute('aria-expanded', panel.hidden ? 'false' : 'true');
|
|
940
|
+
});
|
|
941
|
+
row.appendChild(more);
|
|
942
|
+
|
|
943
|
+
const label = rowWallets.length > 0 ? el('div', 'wallet-row-label', 'Connecting with') : undefined;
|
|
944
|
+
|
|
945
|
+
// Search filter — appears once the catalog outgrows one grid row.
|
|
946
|
+
const tiles: Array<{ wallet: (typeof wallets)[number]; el: HTMLButtonElement }> = [];
|
|
725
947
|
const empty = document.createElement('div');
|
|
726
948
|
empty.className = 'wallet-empty';
|
|
727
949
|
empty.textContent = 'No wallets match your search.';
|
|
728
950
|
empty.hidden = true;
|
|
729
951
|
|
|
730
|
-
if (wallets.length >
|
|
952
|
+
if (wallets.length > WALLET_SEARCH_THRESHOLD) {
|
|
731
953
|
const search = document.createElement('input');
|
|
732
954
|
search.type = 'search';
|
|
733
955
|
search.className = 'wallet-search';
|
|
734
956
|
search.placeholder = 'Search wallets…';
|
|
957
|
+
search.setAttribute('aria-label', 'Search wallets');
|
|
735
958
|
search.addEventListener('input', () => {
|
|
736
959
|
const query = search.value.trim().toLowerCase();
|
|
737
960
|
let visible = 0;
|
|
738
|
-
for (const { wallet, el } of
|
|
961
|
+
for (const { wallet, el: tile } of tiles) {
|
|
739
962
|
const match = query === ''
|
|
740
963
|
|| wallet.name.toLowerCase().includes(query)
|
|
741
964
|
|| wallet.url.toLowerCase().includes(query);
|
|
742
|
-
|
|
965
|
+
tile.hidden = !match;
|
|
743
966
|
if (match) { visible += 1; }
|
|
744
967
|
}
|
|
745
968
|
empty.hidden = visible > 0;
|
|
@@ -747,26 +970,35 @@ export function runConnectModal(options: ConnectModalOptions): Promise<ConnectRe
|
|
|
747
970
|
panel.appendChild(search);
|
|
748
971
|
}
|
|
749
972
|
|
|
973
|
+
// Square tiles, three per row; past two rows the grid scrolls in
|
|
974
|
+
// place so a large catalog never stretches the modal.
|
|
975
|
+
const scroll = document.createElement('div');
|
|
976
|
+
scroll.className = 'wallet-scroll';
|
|
977
|
+
const grid = document.createElement('div');
|
|
978
|
+
grid.className = 'wallet-grid';
|
|
979
|
+
|
|
750
980
|
for (const wallet of wallets) {
|
|
751
|
-
const
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
981
|
+
const tile = document.createElement('button');
|
|
982
|
+
tile.className = 'wallet-tile';
|
|
983
|
+
tile.title = wallet.name;
|
|
984
|
+
tile.appendChild(buildWalletIcon(wallet));
|
|
985
|
+
const tileName = document.createElement('span');
|
|
986
|
+
tileName.className = 'wallet-tile-name';
|
|
987
|
+
tileName.textContent = wallet.name;
|
|
988
|
+
tile.appendChild(tileName);
|
|
758
989
|
if (wallet.url === walletUrl) {
|
|
759
|
-
|
|
990
|
+
tile.classList.add('selected');
|
|
991
|
+
tile.setAttribute('aria-pressed', 'true');
|
|
760
992
|
}
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
renderFooter();
|
|
764
|
-
void startMethod();
|
|
993
|
+
tile.addEventListener('click', () => {
|
|
994
|
+
selectWallet(wallet.url);
|
|
765
995
|
});
|
|
766
|
-
|
|
767
|
-
|
|
996
|
+
tiles.push({ wallet, el: tile });
|
|
997
|
+
grid.appendChild(tile);
|
|
768
998
|
}
|
|
769
|
-
|
|
999
|
+
scroll.appendChild(grid);
|
|
1000
|
+
scroll.appendChild(empty);
|
|
1001
|
+
panel.appendChild(scroll);
|
|
770
1002
|
|
|
771
1003
|
// Custom wallet URL (the power-user path, demoted to the disclosure).
|
|
772
1004
|
const custom = document.createElement('div');
|
|
@@ -787,12 +1019,6 @@ export function runConnectModal(options: ConnectModalOptions): Promise<ConnectRe
|
|
|
787
1019
|
// should not be permanently blocked.
|
|
788
1020
|
let allowUnverified = false;
|
|
789
1021
|
|
|
790
|
-
const useWallet = (origin: string): void => {
|
|
791
|
-
walletUrl = origin;
|
|
792
|
-
renderFooter();
|
|
793
|
-
void startMethod();
|
|
794
|
-
};
|
|
795
|
-
|
|
796
1022
|
const submitCustom = async (): Promise<void> => {
|
|
797
1023
|
const value = input.value.trim();
|
|
798
1024
|
if (value === '') { return; }
|
|
@@ -804,7 +1030,7 @@ export function runConnectModal(options: ConnectModalOptions): Promise<ConnectRe
|
|
|
804
1030
|
return;
|
|
805
1031
|
}
|
|
806
1032
|
if (allowUnverified) {
|
|
807
|
-
|
|
1033
|
+
selectWallet(origin);
|
|
808
1034
|
return;
|
|
809
1035
|
}
|
|
810
1036
|
go.disabled = true;
|
|
@@ -814,7 +1040,7 @@ export function runConnectModal(options: ConnectModalOptions): Promise<ConnectRe
|
|
|
814
1040
|
go.disabled = false;
|
|
815
1041
|
if (valid) {
|
|
816
1042
|
go.textContent = 'Use';
|
|
817
|
-
|
|
1043
|
+
selectWallet(origin);
|
|
818
1044
|
return;
|
|
819
1045
|
}
|
|
820
1046
|
allowUnverified = true;
|
|
@@ -839,14 +1065,7 @@ export function runConnectModal(options: ConnectModalOptions): Promise<ConnectRe
|
|
|
839
1065
|
panel.appendChild(custom);
|
|
840
1066
|
panel.appendChild(urlError);
|
|
841
1067
|
|
|
842
|
-
|
|
843
|
-
panel.hidden = !panel.hidden;
|
|
844
|
-
toggle.setAttribute('aria-expanded', panel.hidden ? 'false' : 'true');
|
|
845
|
-
});
|
|
846
|
-
|
|
847
|
-
wrap.appendChild(toggle);
|
|
848
|
-
wrap.appendChild(panel);
|
|
849
|
-
return wrap;
|
|
1068
|
+
return { label, row, panel };
|
|
850
1069
|
};
|
|
851
1070
|
|
|
852
1071
|
// ── Global interactions ────────────────────────────────────
|
|
@@ -882,6 +1101,23 @@ function walletInCatalog(wallets: WalletOption[], url: string): boolean {
|
|
|
882
1101
|
return wallets.some((wallet) => wallet.url === url);
|
|
883
1102
|
}
|
|
884
1103
|
|
|
1104
|
+
/** Drops catalog entries that repeat an earlier wallet's URL. */
|
|
1105
|
+
function dedupeWalletsByUrl(wallets: WalletOption[]): WalletOption[] {
|
|
1106
|
+
return wallets.filter((wallet, index) => wallets.findIndex((candidate) => candidate.url === wallet.url) === index);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
/** Compact display form of a wallet URL for the switcher toggle. */
|
|
1110
|
+
function hostnameOf(url: string | undefined): string | undefined {
|
|
1111
|
+
if (url === undefined) {
|
|
1112
|
+
return undefined;
|
|
1113
|
+
}
|
|
1114
|
+
try {
|
|
1115
|
+
return new URL(url).hostname;
|
|
1116
|
+
} catch {
|
|
1117
|
+
return url;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
885
1121
|
function el(tag: string, className: string, text?: string): HTMLElement {
|
|
886
1122
|
const node = document.createElement(tag);
|
|
887
1123
|
node.className = className;
|
|
@@ -963,21 +1199,45 @@ function trapFocus(shadow: ShadowRoot, event: KeyboardEvent): void {
|
|
|
963
1199
|
}
|
|
964
1200
|
|
|
965
1201
|
// ─── Styles ──────────────────────────────────────────────────────
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1202
|
+
//
|
|
1203
|
+
// One static sheet; every color reads a `--ec-*` token. Light values sit on
|
|
1204
|
+
// `.modal`, dark values apply under `prefers-color-scheme` (so the system
|
|
1205
|
+
// appearance tracks live) and under a forced `data-appearance`. Dapp
|
|
1206
|
+
// palette overrides land as inline custom properties, which outrank all of
|
|
1207
|
+
// these — nothing user-provided is ever concatenated into this string.
|
|
1208
|
+
|
|
1209
|
+
/** Light-appearance design tokens. */
|
|
1210
|
+
const LIGHT_TOKENS = `
|
|
1211
|
+
--ec-bg: #ffffff;
|
|
1212
|
+
--ec-surface: #f5f6f8;
|
|
1213
|
+
--ec-text: #17171f;
|
|
1214
|
+
--ec-muted: #63636e;
|
|
1215
|
+
--ec-border: #e3e4e9;
|
|
1216
|
+
--ec-accent: #0a62d0;
|
|
1217
|
+
--ec-accent-contrast: #ffffff;
|
|
1218
|
+
--ec-danger: #c0392b;
|
|
1219
|
+
--ec-success: #1e9e5a;`;
|
|
1220
|
+
|
|
1221
|
+
/** Dark-appearance design tokens. */
|
|
1222
|
+
const DARK_TOKENS = `
|
|
1223
|
+
--ec-bg: #181822;
|
|
1224
|
+
--ec-surface: #23232f;
|
|
1225
|
+
--ec-text: #e9e9f0;
|
|
1226
|
+
--ec-muted: #9a9aa6;
|
|
1227
|
+
--ec-border: #33333f;
|
|
1228
|
+
--ec-accent: #58a6ff;
|
|
1229
|
+
--ec-accent-contrast: #0b1526;
|
|
1230
|
+
--ec-danger: #ff7a7a;
|
|
1231
|
+
--ec-success: #43d17c;`;
|
|
1232
|
+
|
|
1233
|
+
const MODAL_STYLES = `
|
|
979
1234
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
980
1235
|
|
|
1236
|
+
/* Keep the hidden attribute authoritative: author display values (e.g.
|
|
1237
|
+
the panel's flex) would otherwise override the UA's [hidden] rule and
|
|
1238
|
+
leave "collapsed" sections permanently expanded. */
|
|
1239
|
+
[hidden] { display: none !important; }
|
|
1240
|
+
|
|
981
1241
|
.overlay {
|
|
982
1242
|
position: fixed;
|
|
983
1243
|
inset: 0;
|
|
@@ -985,6 +1245,7 @@ function buildStyles(isDark: boolean): string {
|
|
|
985
1245
|
display: flex;
|
|
986
1246
|
align-items: center;
|
|
987
1247
|
justify-content: center;
|
|
1248
|
+
padding: 16px;
|
|
988
1249
|
background: rgba(0, 0, 0, 0.5);
|
|
989
1250
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
990
1251
|
animation: fadeIn 0.15s ease-out;
|
|
@@ -999,115 +1260,159 @@ function buildStyles(isDark: boolean): string {
|
|
|
999
1260
|
40%, 80% { transform: translateX(6px); }
|
|
1000
1261
|
}
|
|
1001
1262
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
1263
|
+
@keyframes claimedPulse {
|
|
1264
|
+
0%, 100% { transform: scale(0.85); opacity: 0.7; }
|
|
1265
|
+
50% { transform: scale(1); opacity: 1; }
|
|
1266
|
+
}
|
|
1002
1267
|
|
|
1003
|
-
.modal {
|
|
1004
|
-
|
|
1005
|
-
|
|
1268
|
+
.modal {${LIGHT_TOKENS}
|
|
1269
|
+
--ec-accent-soft: color-mix(in srgb, var(--ec-accent) 14%, transparent);
|
|
1270
|
+
background: var(--ec-bg);
|
|
1271
|
+
color: var(--ec-text);
|
|
1006
1272
|
border-radius: 16px;
|
|
1007
1273
|
width: 400px;
|
|
1008
|
-
max-width:
|
|
1009
|
-
max-height:
|
|
1274
|
+
max-width: 100%;
|
|
1275
|
+
max-height: calc(100vh - 32px);
|
|
1010
1276
|
overflow-y: auto;
|
|
1011
|
-
padding: 20px
|
|
1012
|
-
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.
|
|
1277
|
+
padding: 16px 20px 12px;
|
|
1278
|
+
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.35);
|
|
1013
1279
|
animation: slideUp 0.2s ease-out;
|
|
1014
1280
|
display: flex;
|
|
1015
1281
|
flex-direction: column;
|
|
1016
1282
|
}
|
|
1283
|
+
@media (prefers-color-scheme: dark) {
|
|
1284
|
+
.modal {${DARK_TOKENS}
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
.modal[data-appearance='light'] {${LIGHT_TOKENS}
|
|
1288
|
+
}
|
|
1289
|
+
.modal[data-appearance='dark'] {${DARK_TOKENS}
|
|
1290
|
+
}
|
|
1017
1291
|
|
|
1018
|
-
|
|
1292
|
+
button, input { font-family: inherit; }
|
|
1293
|
+
button:focus-visible, a:focus-visible {
|
|
1294
|
+
outline: 2px solid var(--ec-accent);
|
|
1295
|
+
outline-offset: 2px;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
.header { display: flex; justify-content: space-between; align-items: center; gap: 8px; }
|
|
1019
1299
|
.header-identity { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
1020
|
-
.header-icon { border-radius: 6px; }
|
|
1300
|
+
.header-icon { border-radius: 6px; flex-shrink: 0; }
|
|
1021
1301
|
h2 { font-size: 16px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1022
1302
|
|
|
1023
1303
|
.close-btn {
|
|
1024
1304
|
background: none; border: none; font-size: 24px; cursor: pointer;
|
|
1025
|
-
color:
|
|
1305
|
+
color: var(--ec-muted); padding: 4px 8px; border-radius: 8px; line-height: 1;
|
|
1306
|
+
flex-shrink: 0;
|
|
1026
1307
|
}
|
|
1027
|
-
.close-btn:hover { color:
|
|
1308
|
+
.close-btn:hover { color: var(--ec-text); background: var(--ec-surface); }
|
|
1028
1309
|
|
|
1029
1310
|
.stage {
|
|
1030
|
-
min-height:
|
|
1311
|
+
min-height: 240px;
|
|
1031
1312
|
display: flex;
|
|
1032
1313
|
flex-direction: column;
|
|
1033
1314
|
align-items: center;
|
|
1034
1315
|
justify-content: center;
|
|
1035
|
-
gap:
|
|
1316
|
+
gap: 10px;
|
|
1036
1317
|
text-align: center;
|
|
1037
|
-
padding:
|
|
1318
|
+
padding: 12px 0 10px;
|
|
1038
1319
|
}
|
|
1039
1320
|
.stage.fade-in > * { animation: stageIn 0.15s ease-out; }
|
|
1040
1321
|
|
|
1041
1322
|
.qr-box {
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1323
|
+
position: relative;
|
|
1324
|
+
width: 200px;
|
|
1325
|
+
height: 200px;
|
|
1326
|
+
padding: 10px;
|
|
1045
1327
|
border-radius: 12px;
|
|
1046
|
-
background:
|
|
1328
|
+
background: #ffffff;
|
|
1329
|
+
border: 1px solid #e3e4e9;
|
|
1330
|
+
}
|
|
1331
|
+
.qr-box svg { width: 100%; height: 100%; display: block; }
|
|
1332
|
+
|
|
1333
|
+
/* Selected wallet's mark on the QR centre. Like the card, the plate is
|
|
1334
|
+
theme-invariant white so the logo sits on a scannable ground. */
|
|
1335
|
+
.qr-logo {
|
|
1336
|
+
position: absolute;
|
|
1337
|
+
top: 50%;
|
|
1338
|
+
left: 50%;
|
|
1339
|
+
transform: translate(-50%, -50%);
|
|
1340
|
+
width: 44px;
|
|
1341
|
+
height: 44px;
|
|
1342
|
+
border-radius: 10px;
|
|
1343
|
+
background: #ffffff;
|
|
1344
|
+
border: 1px solid #e3e4e9;
|
|
1345
|
+
display: flex;
|
|
1346
|
+
align-items: center;
|
|
1347
|
+
justify-content: center;
|
|
1348
|
+
}
|
|
1349
|
+
.qr-logo .wallet-icon { width: 30px; height: 30px; }
|
|
1350
|
+
.qr-logo .wallet-badge { border-radius: 8px; font-size: 15px; }
|
|
1351
|
+
|
|
1047
1352
|
.claimed-pulse {
|
|
1048
1353
|
width: 64px; height: 64px; border-radius: 50%;
|
|
1049
|
-
background:
|
|
1354
|
+
background: var(--ec-accent-soft); position: relative;
|
|
1050
1355
|
}
|
|
1051
1356
|
.claimed-pulse::after {
|
|
1052
1357
|
content: ''; position: absolute; inset: 18px; border-radius: 50%;
|
|
1053
|
-
background:
|
|
1054
|
-
animation:
|
|
1055
|
-
}
|
|
1056
|
-
@keyframes enbox-claimed-pulse {
|
|
1057
|
-
0%, 100% { transform: scale(0.85); opacity: 0.7; }
|
|
1058
|
-
50% { transform: scale(1); opacity: 1; }
|
|
1358
|
+
background: var(--ec-accent);
|
|
1359
|
+
animation: claimedPulse 1.6s ease-in-out infinite;
|
|
1059
1360
|
}
|
|
1060
|
-
@media (prefers-reduced-motion: reduce) {
|
|
1061
|
-
.claimed-pulse::after { animation: none; }
|
|
1062
|
-
};
|
|
1063
|
-
border: 1px solid ${border};
|
|
1064
|
-
}
|
|
1065
|
-
.qr-box svg { width: 100%; height: 100%; display: block; }
|
|
1066
1361
|
|
|
1067
1362
|
.stage-caption { font-size: 15px; font-weight: 600; }
|
|
1068
|
-
.stage-subline { font-size: 13px; color:
|
|
1069
|
-
.stage-error { font-size: 13px; color:
|
|
1363
|
+
.stage-subline { font-size: 13px; color: var(--ec-muted); max-width: 300px; line-height: 1.45; }
|
|
1364
|
+
.stage-error { font-size: 13px; color: var(--ec-danger); }
|
|
1070
1365
|
|
|
1071
1366
|
.deep-link {
|
|
1072
|
-
display:
|
|
1367
|
+
display: flex;
|
|
1368
|
+
align-items: center;
|
|
1369
|
+
justify-content: center;
|
|
1370
|
+
gap: 10px;
|
|
1073
1371
|
width: 100%;
|
|
1074
|
-
padding:
|
|
1372
|
+
padding: 13px 16px;
|
|
1075
1373
|
border-radius: 12px;
|
|
1076
|
-
background:
|
|
1077
|
-
color:
|
|
1374
|
+
background: var(--ec-accent);
|
|
1375
|
+
color: var(--ec-accent-contrast);
|
|
1078
1376
|
font-weight: 600;
|
|
1079
1377
|
font-size: 15px;
|
|
1080
1378
|
text-decoration: none;
|
|
1081
1379
|
}
|
|
1380
|
+
.deep-link .wallet-icon { width: 24px; height: 24px; }
|
|
1381
|
+
.deep-link .wallet-badge {
|
|
1382
|
+
border-radius: 6px;
|
|
1383
|
+
font-size: 12px;
|
|
1384
|
+
background: var(--ec-accent-contrast);
|
|
1385
|
+
}
|
|
1386
|
+
.deep-link .wallet-img { border-radius: 6px; }
|
|
1082
1387
|
|
|
1083
1388
|
.pin-row { display: flex; gap: 10px; }
|
|
1084
1389
|
.pin-row.shake { animation: shake 0.4s ease-in-out; }
|
|
1085
1390
|
.pin-input {
|
|
1086
|
-
width:
|
|
1391
|
+
width: 46px; height: 54px;
|
|
1087
1392
|
text-align: center;
|
|
1088
1393
|
font-size: 24px; font-weight: 600;
|
|
1089
|
-
color:
|
|
1090
|
-
background:
|
|
1091
|
-
border: 1px solid
|
|
1394
|
+
color: var(--ec-text);
|
|
1395
|
+
background: var(--ec-surface);
|
|
1396
|
+
border: 1px solid var(--ec-border);
|
|
1092
1397
|
border-radius: 10px;
|
|
1093
1398
|
outline: none;
|
|
1094
1399
|
}
|
|
1095
|
-
.pin-input:focus { border-color:
|
|
1400
|
+
.pin-input:focus { border-color: var(--ec-accent); }
|
|
1096
1401
|
|
|
1097
1402
|
.check {
|
|
1098
1403
|
width: 56px; height: 56px;
|
|
1099
1404
|
display: flex; align-items: center; justify-content: center;
|
|
1100
1405
|
border-radius: 50%;
|
|
1101
|
-
background:
|
|
1102
|
-
color: #
|
|
1406
|
+
background: var(--ec-success);
|
|
1407
|
+
color: #ffffff;
|
|
1103
1408
|
font-size: 28px;
|
|
1104
1409
|
}
|
|
1105
1410
|
|
|
1106
1411
|
.spinner {
|
|
1107
1412
|
width: 32px; height: 32px;
|
|
1108
1413
|
border-radius: 50%;
|
|
1109
|
-
border: 3px solid
|
|
1110
|
-
border-top-color:
|
|
1414
|
+
border: 3px solid var(--ec-border);
|
|
1415
|
+
border-top-color: var(--ec-accent);
|
|
1111
1416
|
animation: spin 0.8s linear infinite;
|
|
1112
1417
|
}
|
|
1113
1418
|
|
|
@@ -1115,87 +1420,169 @@ function buildStyles(isDark: boolean): string {
|
|
|
1115
1420
|
padding: 10px 20px;
|
|
1116
1421
|
border: none;
|
|
1117
1422
|
border-radius: 10px;
|
|
1118
|
-
background:
|
|
1119
|
-
color:
|
|
1423
|
+
background: var(--ec-accent);
|
|
1424
|
+
color: var(--ec-accent-contrast);
|
|
1120
1425
|
font-size: 14px;
|
|
1121
1426
|
font-weight: 500;
|
|
1122
1427
|
cursor: pointer;
|
|
1123
1428
|
}
|
|
1124
|
-
.stage-btn:hover { filter: brightness(1.
|
|
1429
|
+
.stage-btn:hover { filter: brightness(1.08); }
|
|
1125
1430
|
|
|
1126
1431
|
.footer {
|
|
1127
|
-
border-top: 1px solid
|
|
1128
|
-
padding-top:
|
|
1432
|
+
border-top: 1px solid var(--ec-border);
|
|
1433
|
+
padding-top: 8px;
|
|
1129
1434
|
display: flex;
|
|
1130
1435
|
flex-direction: column;
|
|
1131
1436
|
gap: 6px;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
.footer-row {
|
|
1440
|
+
display: flex;
|
|
1132
1441
|
align-items: center;
|
|
1442
|
+
justify-content: space-between;
|
|
1443
|
+
flex-wrap: wrap;
|
|
1444
|
+
gap: 2px 8px;
|
|
1133
1445
|
}
|
|
1446
|
+
.footer-row > * { min-width: 0; }
|
|
1447
|
+
.footer-row > :only-child { margin-inline: auto; }
|
|
1134
1448
|
|
|
1135
1449
|
.footer-link {
|
|
1136
1450
|
background: none;
|
|
1137
1451
|
border: none;
|
|
1138
|
-
color:
|
|
1452
|
+
color: var(--ec-muted);
|
|
1139
1453
|
font-size: 13px;
|
|
1140
1454
|
cursor: pointer;
|
|
1141
|
-
padding:
|
|
1455
|
+
padding: 5px 8px;
|
|
1142
1456
|
border-radius: 8px;
|
|
1143
1457
|
}
|
|
1144
|
-
.footer-link:hover { color:
|
|
1145
|
-
.method-link { color:
|
|
1458
|
+
.footer-link:hover { color: var(--ec-text); background: var(--ec-surface); }
|
|
1459
|
+
.method-link { color: var(--ec-accent); }
|
|
1460
|
+
|
|
1461
|
+
.wallet-row-label { font-size: 12px; color: var(--ec-muted); padding: 2px 2px 0; }
|
|
1146
1462
|
|
|
1147
|
-
.wallet-switcher { width: 100%; display: flex; flex-direction: column; align-items: center; }
|
|
1148
|
-
.wallet-panel { width: 100%; padding: 6px 0; display: flex; flex-direction: column; gap: 4px; }
|
|
1149
1463
|
.wallet-row {
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1464
|
+
display: grid;
|
|
1465
|
+
grid-template-columns: repeat(4, 1fr);
|
|
1466
|
+
gap: 8px;
|
|
1467
|
+
}
|
|
1468
|
+
.row-tile {
|
|
1469
|
+
display: flex;
|
|
1470
|
+
flex-direction: column;
|
|
1471
|
+
align-items: center;
|
|
1472
|
+
justify-content: center;
|
|
1473
|
+
gap: 6px;
|
|
1474
|
+
padding: 10px 6px;
|
|
1475
|
+
min-width: 0;
|
|
1476
|
+
background: var(--ec-surface);
|
|
1477
|
+
border: 1px solid var(--ec-border);
|
|
1478
|
+
border-radius: 12px;
|
|
1479
|
+
color: var(--ec-text);
|
|
1480
|
+
font-size: 11px;
|
|
1481
|
+
cursor: pointer;
|
|
1482
|
+
}
|
|
1483
|
+
.row-tile:hover { border-color: var(--ec-accent); }
|
|
1484
|
+
.row-tile.selected {
|
|
1485
|
+
border-color: var(--ec-accent);
|
|
1486
|
+
background: var(--ec-accent-soft);
|
|
1487
|
+
box-shadow: inset 0 0 0 1px var(--ec-accent);
|
|
1488
|
+
}
|
|
1489
|
+
.row-tile.selected .row-tile-name { font-weight: 600; }
|
|
1490
|
+
.row-tile .wallet-icon { width: 28px; height: 28px; }
|
|
1491
|
+
.row-tile-name { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1492
|
+
|
|
1493
|
+
.more-tile { color: var(--ec-muted); }
|
|
1494
|
+
.more-tile:hover { color: var(--ec-text); }
|
|
1495
|
+
.more-tile[aria-expanded='true'] { border-color: var(--ec-accent); }
|
|
1496
|
+
.more-count {
|
|
1497
|
+
width: 28px;
|
|
1498
|
+
height: 28px;
|
|
1499
|
+
border-radius: 8px;
|
|
1500
|
+
display: inline-flex;
|
|
1501
|
+
align-items: center;
|
|
1502
|
+
justify-content: center;
|
|
1503
|
+
font-size: 12px;
|
|
1504
|
+
font-weight: 600;
|
|
1505
|
+
background: var(--ec-accent-soft);
|
|
1506
|
+
color: var(--ec-accent);
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
.wallet-panel { width: 100%; padding: 4px 0 2px; display: flex; flex-direction: column; gap: 8px; }
|
|
1510
|
+
.wallet-search {
|
|
1511
|
+
width: 100%; padding: 8px 12px;
|
|
1512
|
+
border-radius: 10px; border: 1px solid var(--ec-border); background: transparent;
|
|
1513
|
+
color: inherit; font: inherit; font-size: 13px;
|
|
1514
|
+
}
|
|
1515
|
+
.wallet-search:focus { outline: none; border-color: var(--ec-accent); }
|
|
1516
|
+
|
|
1517
|
+
.wallet-scroll { max-height: 260px; overflow-y: auto; }
|
|
1518
|
+
.wallet-grid {
|
|
1519
|
+
display: grid;
|
|
1520
|
+
grid-template-columns: repeat(3, 1fr);
|
|
1521
|
+
gap: 8px;
|
|
1522
|
+
}
|
|
1523
|
+
.wallet-tile {
|
|
1524
|
+
aspect-ratio: 1;
|
|
1525
|
+
display: flex;
|
|
1526
|
+
flex-direction: column;
|
|
1527
|
+
align-items: center;
|
|
1528
|
+
justify-content: center;
|
|
1529
|
+
gap: 8px;
|
|
1530
|
+
padding: 8px;
|
|
1531
|
+
min-width: 0;
|
|
1532
|
+
background: var(--ec-surface);
|
|
1533
|
+
border: 1px solid var(--ec-border);
|
|
1534
|
+
border-radius: 12px;
|
|
1535
|
+
color: var(--ec-text);
|
|
1536
|
+
font-size: 12px;
|
|
1158
1537
|
cursor: pointer;
|
|
1159
1538
|
}
|
|
1160
|
-
.wallet-
|
|
1539
|
+
.wallet-tile:hover { border-color: var(--ec-accent); }
|
|
1540
|
+
.wallet-tile.selected {
|
|
1541
|
+
border-color: var(--ec-accent);
|
|
1542
|
+
background: var(--ec-accent-soft);
|
|
1543
|
+
box-shadow: inset 0 0 0 1px var(--ec-accent);
|
|
1544
|
+
}
|
|
1545
|
+
.wallet-tile-name { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1161
1546
|
|
|
1162
1547
|
.wallet-icon { position: relative; width: 28px; height: 28px; flex-shrink: 0; display: inline-flex; }
|
|
1548
|
+
.wallet-tile .wallet-icon { width: 40px; height: 40px; }
|
|
1163
1549
|
.wallet-badge {
|
|
1164
|
-
width:
|
|
1550
|
+
width: 100%; height: 100%; border-radius: 8px; display: inline-flex;
|
|
1165
1551
|
align-items: center; justify-content: center; font-size: 13px;
|
|
1166
|
-
font-weight: 600; background:
|
|
1552
|
+
font-weight: 600; background: var(--ec-accent-soft); color: var(--ec-accent);
|
|
1167
1553
|
}
|
|
1168
|
-
.wallet-
|
|
1169
|
-
.wallet-
|
|
1170
|
-
.wallet-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
}
|
|
1175
|
-
.wallet-search:focus { outline: none; border-color: ${accent}; }
|
|
1176
|
-
.wallet-empty { padding: 8px 10px; font-size: 12px; opacity: 0.7; }
|
|
1177
|
-
.url-error { padding: 4px 2px 0; font-size: 12px; color: ${danger}; }
|
|
1178
|
-
.wallet-custom { display: flex; gap: 6px; margin-top: 4px; }
|
|
1554
|
+
.wallet-tile .wallet-badge { border-radius: 10px; font-size: 17px; }
|
|
1555
|
+
.wallet-img { position: absolute; inset: 0; width: 100%; height: 100%; border-radius: 8px; object-fit: cover; }
|
|
1556
|
+
.wallet-tile .wallet-img { border-radius: 10px; }
|
|
1557
|
+
|
|
1558
|
+
.wallet-empty { padding: 10px 4px; font-size: 12px; color: var(--ec-muted); text-align: center; }
|
|
1559
|
+
|
|
1560
|
+
.wallet-custom { display: flex; gap: 6px; }
|
|
1179
1561
|
.url-input {
|
|
1180
1562
|
flex: 1;
|
|
1181
1563
|
padding: 8px 12px;
|
|
1182
|
-
border: 1px solid
|
|
1564
|
+
border: 1px solid var(--ec-border);
|
|
1183
1565
|
border-radius: 10px;
|
|
1184
1566
|
font-size: 13px;
|
|
1185
|
-
background:
|
|
1186
|
-
color:
|
|
1567
|
+
background: var(--ec-surface);
|
|
1568
|
+
color: var(--ec-text);
|
|
1187
1569
|
outline: none;
|
|
1188
1570
|
}
|
|
1189
|
-
.url-input:focus { border-color:
|
|
1190
|
-
.url-input.invalid { border-color:
|
|
1571
|
+
.url-input:focus { border-color: var(--ec-accent); }
|
|
1572
|
+
.url-input.invalid { border-color: var(--ec-danger); }
|
|
1191
1573
|
.url-go {
|
|
1192
1574
|
padding: 8px 14px;
|
|
1193
1575
|
border: none;
|
|
1194
1576
|
border-radius: 10px;
|
|
1195
|
-
background:
|
|
1196
|
-
color:
|
|
1577
|
+
background: var(--ec-accent);
|
|
1578
|
+
color: var(--ec-accent-contrast);
|
|
1197
1579
|
font-size: 13px;
|
|
1198
1580
|
cursor: pointer;
|
|
1199
1581
|
}
|
|
1200
|
-
|
|
1201
|
-
|
|
1582
|
+
.url-error { font-size: 12px; color: var(--ec-danger); }
|
|
1583
|
+
|
|
1584
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1585
|
+
.overlay, .modal, .stage.fade-in > *, .pin-row.shake { animation: none; }
|
|
1586
|
+
.claimed-pulse::after { animation: none; }
|
|
1587
|
+
}
|
|
1588
|
+
`;
|