@enbox/browser 0.3.46 → 0.3.48
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 +93 -16
- package/dist/browser.mjs.map +3 -3
- package/dist/esm/ui/connect-modal.js +185 -40
- package/dist/esm/ui/connect-modal.js.map +1 -1
- package/dist/types/ui/connect-modal.d.ts +4 -3
- package/dist/types/ui/connect-modal.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/ui/connect-modal.ts +197 -43
package/src/ui/connect-modal.ts
CHANGED
|
@@ -13,9 +13,10 @@
|
|
|
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** —
|
|
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.
|
|
19
20
|
*
|
|
20
21
|
* Theming: the stylesheet is static and reads `--ec-*` design tokens.
|
|
21
22
|
* Light/dark follows the visitor's system via `prefers-color-scheme` and
|
|
@@ -68,6 +69,9 @@ const PIN_LENGTH = 4;
|
|
|
68
69
|
/** Catalog size beyond which the wallet switcher shows its search bar. */
|
|
69
70
|
const WALLET_SEARCH_THRESHOLD = 4; // one full row of tiles
|
|
70
71
|
|
|
72
|
+
/** Wallet tiles shown in the collapsed identity row (plus the More tile). */
|
|
73
|
+
const WALLET_ROW_SIZE = 3;
|
|
74
|
+
|
|
71
75
|
/** Palette overrides for one appearance's design tokens. */
|
|
72
76
|
export interface ConnectModalPalette {
|
|
73
77
|
/** Modal surface. */
|
|
@@ -334,7 +338,7 @@ export function runConnectModal(options: ConnectModalOptions): Promise<ConnectRe
|
|
|
334
338
|
}
|
|
335
339
|
|
|
336
340
|
const deps: ConnectModalDeps = { ...defaultDeps(), ...options.deps };
|
|
337
|
-
const wallets = options.wallets ?? [];
|
|
341
|
+
const wallets = dedupeWalletsByUrl(options.wallets ?? []);
|
|
338
342
|
const rememberChoice = options.rememberChoice ?? true;
|
|
339
343
|
const appName = options.appName ?? window.location.host;
|
|
340
344
|
const relayWalletPath = options.relayWalletPath ?? '/connect/app';
|
|
@@ -418,6 +422,35 @@ export function runConnectModal(options: ConnectModalOptions): Promise<ConnectRe
|
|
|
418
422
|
let pinResolve: ((pin: string) => void) | undefined;
|
|
419
423
|
const discoveryCache = new Map<string, Promise<string | undefined>>();
|
|
420
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
|
+
|
|
421
454
|
const cleanup = (): void => {
|
|
422
455
|
relaySession?.cancel();
|
|
423
456
|
clearTimeout(remintTimer);
|
|
@@ -469,14 +502,20 @@ export function runConnectModal(options: ConnectModalOptions): Promise<ConnectRe
|
|
|
469
502
|
};
|
|
470
503
|
|
|
471
504
|
const renderQr = (handoff: WalletUriHandoff): void => {
|
|
505
|
+
const wallet = walletUrl !== undefined ? walletByUrl(walletUrl) : undefined;
|
|
506
|
+
const walletName = wallet?.name ?? 'your wallet';
|
|
507
|
+
|
|
472
508
|
if (isMobile) {
|
|
473
509
|
const link = document.createElement('a');
|
|
474
510
|
link.className = 'deep-link';
|
|
475
511
|
link.href = handoff.walletUri;
|
|
476
|
-
|
|
512
|
+
if (wallet !== undefined) {
|
|
513
|
+
link.appendChild(buildWalletIcon(wallet));
|
|
514
|
+
}
|
|
515
|
+
link.appendChild(el('span', 'deep-link-label', `Continue in ${walletName}`));
|
|
477
516
|
setStage(
|
|
478
517
|
link,
|
|
479
|
-
el('p', 'stage-caption',
|
|
518
|
+
el('p', 'stage-caption', `${walletName} opens to approve this connection.`),
|
|
480
519
|
el('p', 'stage-subline', 'Come back here when you’re done.'),
|
|
481
520
|
);
|
|
482
521
|
return;
|
|
@@ -492,10 +531,20 @@ export function runConnectModal(options: ConnectModalOptions): Promise<ConnectRe
|
|
|
492
531
|
quietZone : 2,
|
|
493
532
|
}));
|
|
494
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
|
+
|
|
495
544
|
setStage(
|
|
496
545
|
qrBox,
|
|
497
546
|
el('p', 'stage-caption', 'Scan with your phone’s camera'),
|
|
498
|
-
el('p', 'stage-subline',
|
|
547
|
+
el('p', 'stage-subline', `${walletName} stays on your phone.`),
|
|
499
548
|
);
|
|
500
549
|
};
|
|
501
550
|
|
|
@@ -809,10 +858,20 @@ export function runConnectModal(options: ConnectModalOptions): Promise<ConnectRe
|
|
|
809
858
|
await startMethod();
|
|
810
859
|
};
|
|
811
860
|
|
|
812
|
-
// ── Footer:
|
|
861
|
+
// ── Footer: identity row (+ catalog panel), method link below ──
|
|
813
862
|
const renderFooter = (): void => {
|
|
814
863
|
footer.replaceChildren();
|
|
815
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
|
+
|
|
816
875
|
const row = document.createElement('div');
|
|
817
876
|
row.className = 'footer-row';
|
|
818
877
|
|
|
@@ -831,32 +890,58 @@ export function runConnectModal(options: ConnectModalOptions): Promise<ConnectRe
|
|
|
831
890
|
row.appendChild(alt);
|
|
832
891
|
}
|
|
833
892
|
|
|
834
|
-
let panel: HTMLElement | undefined;
|
|
835
|
-
if (!lockedWallet && wallets.length > 0) {
|
|
836
|
-
const switcher = buildWalletSwitcher();
|
|
837
|
-
row.appendChild(switcher.toggle);
|
|
838
|
-
panel = switcher.panel;
|
|
839
|
-
}
|
|
840
|
-
|
|
841
893
|
if (row.childElementCount > 0) {
|
|
842
894
|
footer.appendChild(row);
|
|
843
895
|
}
|
|
844
|
-
if (panel !== undefined) {
|
|
845
|
-
footer.appendChild(panel);
|
|
846
|
-
}
|
|
847
896
|
};
|
|
848
897
|
|
|
849
|
-
const buildWalletSwitcher = (): {
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
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
|
+
}
|
|
855
925
|
|
|
856
926
|
const panel = document.createElement('div');
|
|
857
927
|
panel.className = 'wallet-panel';
|
|
858
928
|
panel.hidden = true;
|
|
859
929
|
|
|
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
|
+
|
|
860
945
|
// Search filter — appears once the catalog outgrows one grid row.
|
|
861
946
|
const tiles: Array<{ wallet: (typeof wallets)[number]; el: HTMLButtonElement }> = [];
|
|
862
947
|
const empty = document.createElement('div');
|
|
@@ -906,9 +991,7 @@ export function runConnectModal(options: ConnectModalOptions): Promise<ConnectRe
|
|
|
906
991
|
tile.setAttribute('aria-pressed', 'true');
|
|
907
992
|
}
|
|
908
993
|
tile.addEventListener('click', () => {
|
|
909
|
-
|
|
910
|
-
renderFooter();
|
|
911
|
-
void startMethod();
|
|
994
|
+
selectWallet(wallet.url);
|
|
912
995
|
});
|
|
913
996
|
tiles.push({ wallet, el: tile });
|
|
914
997
|
grid.appendChild(tile);
|
|
@@ -936,12 +1019,6 @@ export function runConnectModal(options: ConnectModalOptions): Promise<ConnectRe
|
|
|
936
1019
|
// should not be permanently blocked.
|
|
937
1020
|
let allowUnverified = false;
|
|
938
1021
|
|
|
939
|
-
const useWallet = (origin: string): void => {
|
|
940
|
-
walletUrl = origin;
|
|
941
|
-
renderFooter();
|
|
942
|
-
void startMethod();
|
|
943
|
-
};
|
|
944
|
-
|
|
945
1022
|
const submitCustom = async (): Promise<void> => {
|
|
946
1023
|
const value = input.value.trim();
|
|
947
1024
|
if (value === '') { return; }
|
|
@@ -953,7 +1030,7 @@ export function runConnectModal(options: ConnectModalOptions): Promise<ConnectRe
|
|
|
953
1030
|
return;
|
|
954
1031
|
}
|
|
955
1032
|
if (allowUnverified) {
|
|
956
|
-
|
|
1033
|
+
selectWallet(origin);
|
|
957
1034
|
return;
|
|
958
1035
|
}
|
|
959
1036
|
go.disabled = true;
|
|
@@ -963,7 +1040,7 @@ export function runConnectModal(options: ConnectModalOptions): Promise<ConnectRe
|
|
|
963
1040
|
go.disabled = false;
|
|
964
1041
|
if (valid) {
|
|
965
1042
|
go.textContent = 'Use';
|
|
966
|
-
|
|
1043
|
+
selectWallet(origin);
|
|
967
1044
|
return;
|
|
968
1045
|
}
|
|
969
1046
|
allowUnverified = true;
|
|
@@ -988,12 +1065,7 @@ export function runConnectModal(options: ConnectModalOptions): Promise<ConnectRe
|
|
|
988
1065
|
panel.appendChild(custom);
|
|
989
1066
|
panel.appendChild(urlError);
|
|
990
1067
|
|
|
991
|
-
|
|
992
|
-
panel.hidden = !panel.hidden;
|
|
993
|
-
toggle.setAttribute('aria-expanded', panel.hidden ? 'false' : 'true');
|
|
994
|
-
});
|
|
995
|
-
|
|
996
|
-
return { toggle, panel };
|
|
1068
|
+
return { label, row, panel };
|
|
997
1069
|
};
|
|
998
1070
|
|
|
999
1071
|
// ── Global interactions ────────────────────────────────────
|
|
@@ -1029,6 +1101,11 @@ function walletInCatalog(wallets: WalletOption[], url: string): boolean {
|
|
|
1029
1101
|
return wallets.some((wallet) => wallet.url === url);
|
|
1030
1102
|
}
|
|
1031
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
|
+
|
|
1032
1109
|
/** Compact display form of a wallet URL for the switcher toggle. */
|
|
1033
1110
|
function hostnameOf(url: string | undefined): string | undefined {
|
|
1034
1111
|
if (url === undefined) {
|
|
@@ -1243,6 +1320,7 @@ const MODAL_STYLES = `
|
|
|
1243
1320
|
.stage.fade-in > * { animation: stageIn 0.15s ease-out; }
|
|
1244
1321
|
|
|
1245
1322
|
.qr-box {
|
|
1323
|
+
position: relative;
|
|
1246
1324
|
width: 200px;
|
|
1247
1325
|
height: 200px;
|
|
1248
1326
|
padding: 10px;
|
|
@@ -1252,6 +1330,25 @@ const MODAL_STYLES = `
|
|
|
1252
1330
|
}
|
|
1253
1331
|
.qr-box svg { width: 100%; height: 100%; display: block; }
|
|
1254
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
|
+
|
|
1255
1352
|
.claimed-pulse {
|
|
1256
1353
|
width: 64px; height: 64px; border-radius: 50%;
|
|
1257
1354
|
background: var(--ec-accent-soft); position: relative;
|
|
@@ -1267,7 +1364,10 @@ const MODAL_STYLES = `
|
|
|
1267
1364
|
.stage-error { font-size: 13px; color: var(--ec-danger); }
|
|
1268
1365
|
|
|
1269
1366
|
.deep-link {
|
|
1270
|
-
display:
|
|
1367
|
+
display: flex;
|
|
1368
|
+
align-items: center;
|
|
1369
|
+
justify-content: center;
|
|
1370
|
+
gap: 10px;
|
|
1271
1371
|
width: 100%;
|
|
1272
1372
|
padding: 13px 16px;
|
|
1273
1373
|
border-radius: 12px;
|
|
@@ -1277,6 +1377,13 @@ const MODAL_STYLES = `
|
|
|
1277
1377
|
font-size: 15px;
|
|
1278
1378
|
text-decoration: none;
|
|
1279
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; }
|
|
1280
1387
|
|
|
1281
1388
|
.pin-row { display: flex; gap: 10px; }
|
|
1282
1389
|
.pin-row.shake { animation: shake 0.4s ease-in-out; }
|
|
@@ -1350,7 +1457,54 @@ const MODAL_STYLES = `
|
|
|
1350
1457
|
}
|
|
1351
1458
|
.footer-link:hover { color: var(--ec-text); background: var(--ec-surface); }
|
|
1352
1459
|
.method-link { color: var(--ec-accent); }
|
|
1353
|
-
|
|
1460
|
+
|
|
1461
|
+
.wallet-row-label { font-size: 12px; color: var(--ec-muted); padding: 2px 2px 0; }
|
|
1462
|
+
|
|
1463
|
+
.wallet-row {
|
|
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
|
+
}
|
|
1354
1508
|
|
|
1355
1509
|
.wallet-panel { width: 100%; padding: 4px 0 2px; display: flex; flex-direction: column; gap: 8px; }
|
|
1356
1510
|
.wallet-search {
|