@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.
@@ -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 alternate-method link and the wallet switcher
17
- * disclosure (catalog + custom URL — the entire legacy selector,
18
- * demoted to an option).
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
@@ -48,6 +58,22 @@ const REMINT_SAFETY_MS = 30000;
48
58
  const REMINT_MIN_MS = 10000;
49
59
  /** Pairing code length (matches the wallet's generated PIN). */
50
60
  const PIN_LENGTH = 4;
61
+ /** Catalog size beyond which the wallet switcher shows its search bar. */
62
+ const WALLET_SEARCH_THRESHOLD = 4; // one full row of tiles
63
+ /** Wallet tiles shown in the collapsed identity row (plus the More tile). */
64
+ const WALLET_ROW_SIZE = 3;
65
+ /** Design-token custom property behind each palette key. */
66
+ const PALETTE_TOKENS = {
67
+ background: '--ec-bg',
68
+ surface: '--ec-surface',
69
+ text: '--ec-text',
70
+ textMuted: '--ec-muted',
71
+ border: '--ec-border',
72
+ accent: '--ec-accent',
73
+ accentContrast: '--ec-accent-contrast',
74
+ danger: '--ec-danger',
75
+ success: '--ec-success',
76
+ };
51
77
  /**
52
78
  * Resolves a wallet origin to its relay `connectServerUrl` via the wallet's
53
79
  * `/.well-known/enbox-connect` document ({@link fetchWalletWellKnown} — the
@@ -146,7 +172,7 @@ export function runConnectModal(options) {
146
172
  throw new Error('[@enbox/browser] The connect modal is only available in browser environments.');
147
173
  }
148
174
  const deps = Object.assign(Object.assign({}, defaultDeps()), options.deps);
149
- const wallets = (_a = options.wallets) !== null && _a !== void 0 ? _a : [];
175
+ const wallets = dedupeWalletsByUrl((_a = options.wallets) !== null && _a !== void 0 ? _a : []);
150
176
  const rememberChoice = (_b = options.rememberChoice) !== null && _b !== void 0 ? _b : true;
151
177
  const appName = (_c = options.appName) !== null && _c !== void 0 ? _c : window.location.host;
152
178
  const relayWalletPath = (_d = options.relayWalletPath) !== null && _d !== void 0 ? _d : '/connect/app';
@@ -158,14 +184,13 @@ export function runConnectModal(options) {
158
184
  // Method resolution order: explicit option → remembered → phone.
159
185
  let method = (_j = (_h = options.preferredMethod) !== null && _h !== void 0 ? _h : lastChoice === null || lastChoice === void 0 ? void 0 : lastChoice.method) !== null && _j !== void 0 ? _j : 'phone';
160
186
  return new Promise((resolve, reject) => {
161
- var _a, _b;
187
+ var _a, _b, _c, _d;
162
188
  // ── Host + shadow root ─────────────────────────────────────
163
189
  const host = document.createElement('div');
164
190
  host.id = 'enbox-connect-modal';
165
191
  const shadow = host.attachShadow({ mode: 'open' });
166
- const isDark = (_b = (_a = window.matchMedia) === null || _a === void 0 ? void 0 : _a.call(window, '(prefers-color-scheme: dark)').matches) !== null && _b !== void 0 ? _b : false;
167
192
  const style = document.createElement('style');
168
- style.textContent = buildStyles(isDark);
193
+ style.textContent = MODAL_STYLES;
169
194
  shadow.appendChild(style);
170
195
  const overlay = document.createElement('div');
171
196
  overlay.className = 'overlay';
@@ -174,6 +199,42 @@ export function runConnectModal(options) {
174
199
  modal.setAttribute('role', 'dialog');
175
200
  modal.setAttribute('aria-modal', 'true');
176
201
  modal.setAttribute('aria-label', `Connect to ${appName}`);
202
+ // ── Theme ──────────────────────────────────────────────────
203
+ // System light/dark is pure CSS (`prefers-color-scheme`), so it tracks
204
+ // the visitor live. A forced appearance pins the token block instead.
205
+ const theme = (_a = options.theme) !== null && _a !== void 0 ? _a : {};
206
+ const appearance = (_b = theme.appearance) !== null && _b !== void 0 ? _b : 'auto';
207
+ if (appearance !== 'auto') {
208
+ modal.setAttribute('data-appearance', appearance);
209
+ }
210
+ // Dapp palette overrides land as inline custom properties: setProperty
211
+ // parses each value as a lone declaration, so a hostile string cannot
212
+ // escape into the stylesheet.
213
+ const systemDark = (_c = window.matchMedia) === null || _c === void 0 ? void 0 : _c.call(window, '(prefers-color-scheme: dark)');
214
+ const hasPaletteOverrides = theme.accent !== undefined || theme.accentContrast !== undefined
215
+ || theme.light !== undefined || theme.dark !== undefined;
216
+ const applyPaletteTokens = () => {
217
+ const scheme = appearance === 'auto'
218
+ ? ((systemDark === null || systemDark === void 0 ? void 0 : systemDark.matches) === true ? 'dark' : 'light')
219
+ : appearance;
220
+ const palette = Object.assign({ accent: theme.accent, accentContrast: theme.accentContrast }, theme[scheme]);
221
+ for (const key of Object.keys(PALETTE_TOKENS)) {
222
+ const value = palette[key];
223
+ if (typeof value === 'string' && value.trim() !== '') {
224
+ modal.style.setProperty(PALETTE_TOKENS[key], value);
225
+ }
226
+ else {
227
+ modal.style.removeProperty(PALETTE_TOKENS[key]);
228
+ }
229
+ }
230
+ };
231
+ const onSchemeChange = () => applyPaletteTokens();
232
+ if (hasPaletteOverrides) {
233
+ applyPaletteTokens();
234
+ if (appearance === 'auto') {
235
+ (_d = systemDark === null || systemDark === void 0 ? void 0 : systemDark.addEventListener) === null || _d === void 0 ? void 0 : _d.call(systemDark, 'change', onSchemeChange);
236
+ }
237
+ }
177
238
  // ── Session state ──────────────────────────────────────────
178
239
  let settled = false;
179
240
  let relaySession;
@@ -181,14 +242,40 @@ export function runConnectModal(options) {
181
242
  let popupBusy = false;
182
243
  let pinResolve;
183
244
  const discoveryCache = new Map();
245
+ // Collapsed identity row: the selected wallet plus the next catalog
246
+ // wallets, never repeating one. Slots stay stable across in-row
247
+ // switches (only the highlight moves); the row recomposes only when a
248
+ // wallet from outside it — grid pick or custom URL — becomes selected,
249
+ // which puts that wallet in the first slot.
250
+ let rowWallets = [];
251
+ /** The catalog entry for `url`, or a synthesized option for custom URLs. */
252
+ const walletByUrl = (url) => { var _a, _b; return (_a = wallets.find((wallet) => wallet.url === url)) !== null && _a !== void 0 ? _a : { name: (_b = hostnameOf(url)) !== null && _b !== void 0 ? _b : url, url }; };
253
+ const composeWalletRow = () => {
254
+ if (walletUrl === undefined) {
255
+ rowWallets = wallets.slice(0, WALLET_ROW_SIZE);
256
+ return;
257
+ }
258
+ if (rowWallets.some((wallet) => wallet.url === walletUrl)) {
259
+ return;
260
+ }
261
+ const selected = walletByUrl(walletUrl);
262
+ rowWallets = [selected, ...wallets.filter((wallet) => wallet.url !== selected.url)].slice(0, WALLET_ROW_SIZE);
263
+ };
264
+ const selectWallet = (url) => {
265
+ walletUrl = url;
266
+ renderFooter();
267
+ void startMethod();
268
+ };
184
269
  const cleanup = () => {
270
+ var _a;
185
271
  relaySession === null || relaySession === void 0 ? void 0 : relaySession.cancel();
186
272
  clearTimeout(remintTimer);
187
273
  document.removeEventListener('keydown', onKeydown);
274
+ (_a = systemDark === null || systemDark === void 0 ? void 0 : systemDark.removeEventListener) === null || _a === void 0 ? void 0 : _a.call(systemDark, 'change', onSchemeChange);
188
275
  try {
189
276
  document.body.removeChild(host);
190
277
  }
191
- catch ( /* best effort */_a) { /* best effort */ }
278
+ catch ( /* best effort */_b) { /* best effort */ }
192
279
  };
193
280
  const settle = (settler) => {
194
281
  if (settled) {
@@ -227,22 +314,39 @@ export function runConnectModal(options) {
227
314
  setStage(el('div', 'spinner'), el('p', 'stage-caption', message));
228
315
  };
229
316
  const renderQr = (handoff) => {
317
+ var _a;
318
+ const wallet = walletUrl !== undefined ? walletByUrl(walletUrl) : undefined;
319
+ const walletName = (_a = wallet === null || wallet === void 0 ? void 0 : wallet.name) !== null && _a !== void 0 ? _a : 'your wallet';
230
320
  if (isMobile) {
231
321
  const link = document.createElement('a');
232
322
  link.className = 'deep-link';
233
323
  link.href = handoff.walletUri;
234
- link.textContent = 'Continue in your wallet';
235
- setStage(link, el('p', 'stage-caption', 'Your wallet opens to approve this connection.'), el('p', 'stage-subline', 'Come back here when you’re done.'));
324
+ if (wallet !== undefined) {
325
+ link.appendChild(buildWalletIcon(wallet));
326
+ }
327
+ link.appendChild(el('span', 'deep-link-label', `Continue in ${walletName}`));
328
+ setStage(link, el('p', 'stage-caption', `${walletName} opens to approve this connection.`), el('p', 'stage-subline', 'Come back here when you’re done.'));
236
329
  return;
237
330
  }
238
331
  const qrBox = document.createElement('div');
239
332
  qrBox.className = 'qr-box';
333
+ // Fixed colors on the white card: dark-on-light QRs scan reliably in
334
+ // both appearances (inverted codes trip up some camera apps).
240
335
  qrBox.replaceChildren(qrToSvg(encodeQr(handoff.walletUri), {
241
- dark: isDark ? '#e8e8f0' : '#14141f',
336
+ dark: '#14141f',
242
337
  light: 'transparent',
243
338
  quietZone: 2,
244
339
  }));
245
- setStage(qrBox, el('p', 'stage-caption', 'Scan with your phone’s camera'), el('p', 'stage-subline', 'Your wallet stays on your phone, so you can approve things anywhere.'));
340
+ // The selected wallet's mark sits at the QR centre — the "this is the
341
+ // wallet you're connecting to" cue. The 44px plate over the ~180px
342
+ // symbol occludes ~5% of modules, well inside ECC level M's 15%
343
+ // recovery budget.
344
+ if (wallet !== undefined) {
345
+ const logo = el('div', 'qr-logo');
346
+ logo.appendChild(buildWalletIcon(wallet));
347
+ qrBox.appendChild(logo);
348
+ }
349
+ setStage(qrBox, el('p', 'stage-caption', 'Scan with your phone’s camera'), el('p', 'stage-subline', `${walletName} stays on your phone.`));
246
350
  };
247
351
  const renderClaimed = () => {
248
352
  const pulse = document.createElement('div');
@@ -499,59 +603,102 @@ export function runConnectModal(options) {
499
603
  method = next;
500
604
  yield startMethod();
501
605
  });
502
- // ── Footer: method link + wallet switcher ──────────────────
606
+ // ── Footer: identity row (+ catalog panel), method link below ──
503
607
  const renderFooter = () => {
504
608
  footer.replaceChildren();
609
+ if (!lockedWallet) {
610
+ composeWalletRow();
611
+ const switcher = buildWalletSwitcher();
612
+ if (switcher.label !== undefined) {
613
+ footer.appendChild(switcher.label);
614
+ }
615
+ footer.appendChild(switcher.row);
616
+ footer.appendChild(switcher.panel);
617
+ }
618
+ const row = document.createElement('div');
619
+ row.className = 'footer-row';
505
620
  if (method === 'phone') {
506
621
  const alt = document.createElement('button');
507
622
  alt.className = 'footer-link method-link';
508
- alt.textContent = 'No phone handy? Use this browser →';
623
+ alt.textContent = 'No phone? Use this browser →';
509
624
  // startPopup runs synchronously in this click handler.
510
625
  alt.addEventListener('click', () => { startPopup(); });
511
- footer.appendChild(alt);
626
+ row.appendChild(alt);
512
627
  }
513
628
  else if (!isMobile) {
514
629
  const alt = document.createElement('button');
515
630
  alt.className = 'footer-link method-link';
516
631
  alt.textContent = 'Use your phone instead →';
517
632
  alt.addEventListener('click', () => { void switchMethod('phone'); });
518
- footer.appendChild(alt);
633
+ row.appendChild(alt);
519
634
  }
520
- if (!lockedWallet && wallets.length > 0) {
521
- footer.appendChild(buildWalletSwitcher());
635
+ if (row.childElementCount > 0) {
636
+ footer.appendChild(row);
522
637
  }
523
638
  };
524
639
  const buildWalletSwitcher = () => {
525
- var _a, _b;
526
- const wrap = document.createElement('div');
527
- wrap.className = 'wallet-switcher';
528
- const current = wallets.find((wallet) => wallet.url === walletUrl);
529
- const toggle = document.createElement('button');
530
- toggle.className = 'footer-link wallet-toggle';
531
- toggle.setAttribute('aria-expanded', 'false');
532
- toggle.textContent = `Wallet: ${(_b = (_a = current === null || current === void 0 ? void 0 : current.name) !== null && _a !== void 0 ? _a : walletUrl) !== null && _b !== void 0 ? _b : 'Choose'} ▾`;
640
+ var _a;
641
+ // Identity row: selected wallet + the next catalog wallets, then the
642
+ // More tile expanding the full catalog in place.
643
+ const row = document.createElement('div');
644
+ row.className = 'wallet-row';
645
+ row.setAttribute('role', 'radiogroup');
646
+ row.setAttribute('aria-label', 'Wallet');
647
+ for (const wallet of rowWallets) {
648
+ const tile = document.createElement('button');
649
+ tile.className = 'row-tile';
650
+ tile.title = (_a = wallet.description) !== null && _a !== void 0 ? _a : wallet.name;
651
+ tile.setAttribute('role', 'radio');
652
+ const isSelected = wallet.url === walletUrl;
653
+ tile.setAttribute('aria-checked', isSelected ? 'true' : 'false');
654
+ if (isSelected) {
655
+ tile.classList.add('selected');
656
+ }
657
+ tile.appendChild(buildWalletIcon(wallet));
658
+ tile.appendChild(el('span', 'row-tile-name', wallet.name));
659
+ tile.addEventListener('click', () => {
660
+ if (wallet.url !== walletUrl) {
661
+ selectWallet(wallet.url);
662
+ }
663
+ });
664
+ row.appendChild(tile);
665
+ }
533
666
  const panel = document.createElement('div');
534
667
  panel.className = 'wallet-panel';
535
668
  panel.hidden = true;
536
- // Search filter — only worth the chrome once the catalog grows.
537
- const rows = [];
669
+ const hiddenCount = wallets.filter((wallet) => !rowWallets.some((shown) => shown.url === wallet.url)).length;
670
+ const more = document.createElement('button');
671
+ more.className = 'row-tile more-tile';
672
+ more.title = 'All wallets';
673
+ more.setAttribute('aria-expanded', 'false');
674
+ more.appendChild(el('span', 'more-count', hiddenCount > 0 ? `+${hiddenCount}` : '⋯'));
675
+ more.appendChild(el('span', 'row-tile-name', 'More'));
676
+ more.addEventListener('click', () => {
677
+ panel.hidden = !panel.hidden;
678
+ more.setAttribute('aria-expanded', panel.hidden ? 'false' : 'true');
679
+ });
680
+ row.appendChild(more);
681
+ const label = rowWallets.length > 0 ? el('div', 'wallet-row-label', 'Connecting with') : undefined;
682
+ // Search filter — appears once the catalog outgrows one grid row.
683
+ const tiles = [];
538
684
  const empty = document.createElement('div');
539
685
  empty.className = 'wallet-empty';
540
686
  empty.textContent = 'No wallets match your search.';
541
687
  empty.hidden = true;
542
- if (wallets.length > 5) {
688
+ if (wallets.length > WALLET_SEARCH_THRESHOLD) {
543
689
  const search = document.createElement('input');
544
690
  search.type = 'search';
545
691
  search.className = 'wallet-search';
546
692
  search.placeholder = 'Search wallets…';
693
+ search.setAttribute('aria-label', 'Search wallets');
547
694
  search.addEventListener('input', () => {
548
695
  const query = search.value.trim().toLowerCase();
549
696
  let visible = 0;
550
- for (const { wallet, el } of rows) {
697
+ for (const { wallet, el: tile } of tiles) {
551
698
  const match = query === ''
552
699
  || wallet.name.toLowerCase().includes(query)
553
700
  || wallet.url.toLowerCase().includes(query);
554
- el.hidden = !match;
701
+ tile.hidden = !match;
555
702
  if (match) {
556
703
  visible += 1;
557
704
  }
@@ -560,26 +707,34 @@ export function runConnectModal(options) {
560
707
  });
561
708
  panel.appendChild(search);
562
709
  }
710
+ // Square tiles, three per row; past two rows the grid scrolls in
711
+ // place so a large catalog never stretches the modal.
712
+ const scroll = document.createElement('div');
713
+ scroll.className = 'wallet-scroll';
714
+ const grid = document.createElement('div');
715
+ grid.className = 'wallet-grid';
563
716
  for (const wallet of wallets) {
564
- const row = document.createElement('button');
565
- row.className = 'wallet-row';
566
- row.appendChild(buildWalletIcon(wallet));
567
- const rowName = document.createElement('span');
568
- rowName.className = 'wallet-row-name';
569
- rowName.textContent = wallet.name;
570
- row.appendChild(rowName);
717
+ const tile = document.createElement('button');
718
+ tile.className = 'wallet-tile';
719
+ tile.title = wallet.name;
720
+ tile.appendChild(buildWalletIcon(wallet));
721
+ const tileName = document.createElement('span');
722
+ tileName.className = 'wallet-tile-name';
723
+ tileName.textContent = wallet.name;
724
+ tile.appendChild(tileName);
571
725
  if (wallet.url === walletUrl) {
572
- row.classList.add('selected');
726
+ tile.classList.add('selected');
727
+ tile.setAttribute('aria-pressed', 'true');
573
728
  }
574
- row.addEventListener('click', () => {
575
- walletUrl = wallet.url;
576
- renderFooter();
577
- void startMethod();
729
+ tile.addEventListener('click', () => {
730
+ selectWallet(wallet.url);
578
731
  });
579
- rows.push({ wallet, el: row });
580
- panel.appendChild(row);
732
+ tiles.push({ wallet, el: tile });
733
+ grid.appendChild(tile);
581
734
  }
582
- panel.appendChild(empty);
735
+ scroll.appendChild(grid);
736
+ scroll.appendChild(empty);
737
+ panel.appendChild(scroll);
583
738
  // Custom wallet URL (the power-user path, demoted to the disclosure).
584
739
  const custom = document.createElement('div');
585
740
  custom.className = 'wallet-custom';
@@ -597,11 +752,6 @@ export function runConnectModal(options) {
597
752
  // handshake is the ultimate gate — a valid wallet behind strict CORS
598
753
  // should not be permanently blocked.
599
754
  let allowUnverified = false;
600
- const useWallet = (origin) => {
601
- walletUrl = origin;
602
- renderFooter();
603
- void startMethod();
604
- };
605
755
  const submitCustom = () => __awaiter(this, void 0, void 0, function* () {
606
756
  const value = input.value.trim();
607
757
  if (value === '') {
@@ -616,7 +766,7 @@ export function runConnectModal(options) {
616
766
  return;
617
767
  }
618
768
  if (allowUnverified) {
619
- useWallet(origin);
769
+ selectWallet(origin);
620
770
  return;
621
771
  }
622
772
  go.disabled = true;
@@ -626,7 +776,7 @@ export function runConnectModal(options) {
626
776
  go.disabled = false;
627
777
  if (valid) {
628
778
  go.textContent = 'Use';
629
- useWallet(origin);
779
+ selectWallet(origin);
630
780
  return;
631
781
  }
632
782
  allowUnverified = true;
@@ -651,13 +801,7 @@ export function runConnectModal(options) {
651
801
  custom.appendChild(go);
652
802
  panel.appendChild(custom);
653
803
  panel.appendChild(urlError);
654
- toggle.addEventListener('click', () => {
655
- panel.hidden = !panel.hidden;
656
- toggle.setAttribute('aria-expanded', panel.hidden ? 'false' : 'true');
657
- });
658
- wrap.appendChild(toggle);
659
- wrap.appendChild(panel);
660
- return wrap;
804
+ return { label, row, panel };
661
805
  };
662
806
  // ── Global interactions ────────────────────────────────────
663
807
  overlay.addEventListener('click', (event) => {
@@ -686,6 +830,22 @@ export function runConnectModal(options) {
686
830
  function walletInCatalog(wallets, url) {
687
831
  return wallets.some((wallet) => wallet.url === url);
688
832
  }
833
+ /** Drops catalog entries that repeat an earlier wallet's URL. */
834
+ function dedupeWalletsByUrl(wallets) {
835
+ return wallets.filter((wallet, index) => wallets.findIndex((candidate) => candidate.url === wallet.url) === index);
836
+ }
837
+ /** Compact display form of a wallet URL for the switcher toggle. */
838
+ function hostnameOf(url) {
839
+ if (url === undefined) {
840
+ return undefined;
841
+ }
842
+ try {
843
+ return new URL(url).hostname;
844
+ }
845
+ catch (_a) {
846
+ return url;
847
+ }
848
+ }
689
849
  function el(tag, className, text) {
690
850
  const node = document.createElement(tag);
691
851
  node.className = className;
@@ -753,19 +913,42 @@ function trapFocus(shadow, event) {
753
913
  }
754
914
  }
755
915
  // ─── Styles ──────────────────────────────────────────────────────
756
- function buildStyles(isDark) {
757
- const bg = isDark ? '#1a1a2e' : '#ffffff';
758
- const text = isDark ? '#e0e0e0' : '#1a1a2e';
759
- const muted = isDark ? '#888' : '#666';
760
- const border = isDark ? '#333' : '#e0e0e0';
761
- const itemBg = isDark ? '#16213e' : '#f8f9fa';
762
- const accent = isDark ? '#4a9eff' : '#0066cc';
763
- const accentSoft = isDark ? 'rgba(74, 158, 255, 0.16)' : 'rgba(0, 102, 204, 0.1)';
764
- const danger = isDark ? '#ff6b6b' : '#c0392b';
765
- const success = isDark ? '#43d17c' : '#1e9e5a';
766
- return `
916
+ //
917
+ // One static sheet; every color reads a `--ec-*` token. Light values sit on
918
+ // `.modal`, dark values apply under `prefers-color-scheme` (so the system
919
+ // appearance tracks live) and under a forced `data-appearance`. Dapp
920
+ // palette overrides land as inline custom properties, which outrank all of
921
+ // these — nothing user-provided is ever concatenated into this string.
922
+ /** Light-appearance design tokens. */
923
+ const LIGHT_TOKENS = `
924
+ --ec-bg: #ffffff;
925
+ --ec-surface: #f5f6f8;
926
+ --ec-text: #17171f;
927
+ --ec-muted: #63636e;
928
+ --ec-border: #e3e4e9;
929
+ --ec-accent: #0a62d0;
930
+ --ec-accent-contrast: #ffffff;
931
+ --ec-danger: #c0392b;
932
+ --ec-success: #1e9e5a;`;
933
+ /** Dark-appearance design tokens. */
934
+ const DARK_TOKENS = `
935
+ --ec-bg: #181822;
936
+ --ec-surface: #23232f;
937
+ --ec-text: #e9e9f0;
938
+ --ec-muted: #9a9aa6;
939
+ --ec-border: #33333f;
940
+ --ec-accent: #58a6ff;
941
+ --ec-accent-contrast: #0b1526;
942
+ --ec-danger: #ff7a7a;
943
+ --ec-success: #43d17c;`;
944
+ const MODAL_STYLES = `
767
945
  * { box-sizing: border-box; margin: 0; padding: 0; }
768
946
 
947
+ /* Keep the hidden attribute authoritative: author display values (e.g.
948
+ the panel's flex) would otherwise override the UA's [hidden] rule and
949
+ leave "collapsed" sections permanently expanded. */
950
+ [hidden] { display: none !important; }
951
+
769
952
  .overlay {
770
953
  position: fixed;
771
954
  inset: 0;
@@ -773,6 +956,7 @@ function buildStyles(isDark) {
773
956
  display: flex;
774
957
  align-items: center;
775
958
  justify-content: center;
959
+ padding: 16px;
776
960
  background: rgba(0, 0, 0, 0.5);
777
961
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
778
962
  animation: fadeIn 0.15s ease-out;
@@ -787,115 +971,159 @@ function buildStyles(isDark) {
787
971
  40%, 80% { transform: translateX(6px); }
788
972
  }
789
973
  @keyframes spin { to { transform: rotate(360deg); } }
974
+ @keyframes claimedPulse {
975
+ 0%, 100% { transform: scale(0.85); opacity: 0.7; }
976
+ 50% { transform: scale(1); opacity: 1; }
977
+ }
790
978
 
791
- .modal {
792
- background: ${bg};
793
- color: ${text};
979
+ .modal {${LIGHT_TOKENS}
980
+ --ec-accent-soft: color-mix(in srgb, var(--ec-accent) 14%, transparent);
981
+ background: var(--ec-bg);
982
+ color: var(--ec-text);
794
983
  border-radius: 16px;
795
984
  width: 400px;
796
- max-width: 92vw;
797
- max-height: 88vh;
985
+ max-width: 100%;
986
+ max-height: calc(100vh - 32px);
798
987
  overflow-y: auto;
799
- padding: 20px 24px 16px;
800
- box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
988
+ padding: 16px 20px 12px;
989
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.35);
801
990
  animation: slideUp 0.2s ease-out;
802
991
  display: flex;
803
992
  flex-direction: column;
804
993
  }
994
+ @media (prefers-color-scheme: dark) {
995
+ .modal {${DARK_TOKENS}
996
+ }
997
+ }
998
+ .modal[data-appearance='light'] {${LIGHT_TOKENS}
999
+ }
1000
+ .modal[data-appearance='dark'] {${DARK_TOKENS}
1001
+ }
1002
+
1003
+ button, input { font-family: inherit; }
1004
+ button:focus-visible, a:focus-visible {
1005
+ outline: 2px solid var(--ec-accent);
1006
+ outline-offset: 2px;
1007
+ }
805
1008
 
806
- .header { display: flex; justify-content: space-between; align-items: center; }
1009
+ .header { display: flex; justify-content: space-between; align-items: center; gap: 8px; }
807
1010
  .header-identity { display: flex; align-items: center; gap: 10px; min-width: 0; }
808
- .header-icon { border-radius: 6px; }
1011
+ .header-icon { border-radius: 6px; flex-shrink: 0; }
809
1012
  h2 { font-size: 16px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
810
1013
 
811
1014
  .close-btn {
812
1015
  background: none; border: none; font-size: 24px; cursor: pointer;
813
- color: ${muted}; padding: 4px 8px; border-radius: 8px; line-height: 1;
1016
+ color: var(--ec-muted); padding: 4px 8px; border-radius: 8px; line-height: 1;
1017
+ flex-shrink: 0;
814
1018
  }
815
- .close-btn:hover { color: ${text}; background: ${itemBg}; }
1019
+ .close-btn:hover { color: var(--ec-text); background: var(--ec-surface); }
816
1020
 
817
1021
  .stage {
818
- min-height: 300px;
1022
+ min-height: 240px;
819
1023
  display: flex;
820
1024
  flex-direction: column;
821
1025
  align-items: center;
822
1026
  justify-content: center;
823
- gap: 12px;
1027
+ gap: 10px;
824
1028
  text-align: center;
825
- padding: 16px 0;
1029
+ padding: 12px 0 10px;
826
1030
  }
827
1031
  .stage.fade-in > * { animation: stageIn 0.15s ease-out; }
828
1032
 
829
1033
  .qr-box {
830
- width: 216px;
831
- height: 216px;
832
- padding: 12px;
1034
+ position: relative;
1035
+ width: 200px;
1036
+ height: 200px;
1037
+ padding: 10px;
833
1038
  border-radius: 12px;
834
- background: ${isDark ? '#10192e' : '#ffffff'}
1039
+ background: #ffffff;
1040
+ border: 1px solid #e3e4e9;
1041
+ }
1042
+ .qr-box svg { width: 100%; height: 100%; display: block; }
1043
+
1044
+ /* Selected wallet's mark on the QR centre. Like the card, the plate is
1045
+ theme-invariant white so the logo sits on a scannable ground. */
1046
+ .qr-logo {
1047
+ position: absolute;
1048
+ top: 50%;
1049
+ left: 50%;
1050
+ transform: translate(-50%, -50%);
1051
+ width: 44px;
1052
+ height: 44px;
1053
+ border-radius: 10px;
1054
+ background: #ffffff;
1055
+ border: 1px solid #e3e4e9;
1056
+ display: flex;
1057
+ align-items: center;
1058
+ justify-content: center;
1059
+ }
1060
+ .qr-logo .wallet-icon { width: 30px; height: 30px; }
1061
+ .qr-logo .wallet-badge { border-radius: 8px; font-size: 15px; }
1062
+
835
1063
  .claimed-pulse {
836
1064
  width: 64px; height: 64px; border-radius: 50%;
837
- background: ${accentSoft}; position: relative;
1065
+ background: var(--ec-accent-soft); position: relative;
838
1066
  }
839
1067
  .claimed-pulse::after {
840
1068
  content: ''; position: absolute; inset: 18px; border-radius: 50%;
841
- background: ${accent};
842
- animation: enbox-claimed-pulse 1.6s ease-in-out infinite;
1069
+ background: var(--ec-accent);
1070
+ animation: claimedPulse 1.6s ease-in-out infinite;
843
1071
  }
844
- @keyframes enbox-claimed-pulse {
845
- 0%, 100% { transform: scale(0.85); opacity: 0.7; }
846
- 50% { transform: scale(1); opacity: 1; }
847
- }
848
- @media (prefers-reduced-motion: reduce) {
849
- .claimed-pulse::after { animation: none; }
850
- };
851
- border: 1px solid ${border};
852
- }
853
- .qr-box svg { width: 100%; height: 100%; display: block; }
854
1072
 
855
1073
  .stage-caption { font-size: 15px; font-weight: 600; }
856
- .stage-subline { font-size: 13px; color: ${muted}; max-width: 300px; line-height: 1.45; }
857
- .stage-error { font-size: 13px; color: ${danger}; }
1074
+ .stage-subline { font-size: 13px; color: var(--ec-muted); max-width: 300px; line-height: 1.45; }
1075
+ .stage-error { font-size: 13px; color: var(--ec-danger); }
858
1076
 
859
1077
  .deep-link {
860
- display: block;
1078
+ display: flex;
1079
+ align-items: center;
1080
+ justify-content: center;
1081
+ gap: 10px;
861
1082
  width: 100%;
862
- padding: 14px 16px;
1083
+ padding: 13px 16px;
863
1084
  border-radius: 12px;
864
- background: ${accent};
865
- color: #fff;
1085
+ background: var(--ec-accent);
1086
+ color: var(--ec-accent-contrast);
866
1087
  font-weight: 600;
867
1088
  font-size: 15px;
868
1089
  text-decoration: none;
869
1090
  }
1091
+ .deep-link .wallet-icon { width: 24px; height: 24px; }
1092
+ .deep-link .wallet-badge {
1093
+ border-radius: 6px;
1094
+ font-size: 12px;
1095
+ background: var(--ec-accent-contrast);
1096
+ }
1097
+ .deep-link .wallet-img { border-radius: 6px; }
870
1098
 
871
1099
  .pin-row { display: flex; gap: 10px; }
872
1100
  .pin-row.shake { animation: shake 0.4s ease-in-out; }
873
1101
  .pin-input {
874
- width: 48px; height: 56px;
1102
+ width: 46px; height: 54px;
875
1103
  text-align: center;
876
1104
  font-size: 24px; font-weight: 600;
877
- color: ${text};
878
- background: ${itemBg};
879
- border: 1px solid ${border};
1105
+ color: var(--ec-text);
1106
+ background: var(--ec-surface);
1107
+ border: 1px solid var(--ec-border);
880
1108
  border-radius: 10px;
881
1109
  outline: none;
882
1110
  }
883
- .pin-input:focus { border-color: ${accent}; }
1111
+ .pin-input:focus { border-color: var(--ec-accent); }
884
1112
 
885
1113
  .check {
886
1114
  width: 56px; height: 56px;
887
1115
  display: flex; align-items: center; justify-content: center;
888
1116
  border-radius: 50%;
889
- background: ${success};
890
- color: #fff;
1117
+ background: var(--ec-success);
1118
+ color: #ffffff;
891
1119
  font-size: 28px;
892
1120
  }
893
1121
 
894
1122
  .spinner {
895
1123
  width: 32px; height: 32px;
896
1124
  border-radius: 50%;
897
- border: 3px solid ${border};
898
- border-top-color: ${accent};
1125
+ border: 3px solid var(--ec-border);
1126
+ border-top-color: var(--ec-accent);
899
1127
  animation: spin 0.8s linear infinite;
900
1128
  }
901
1129
 
@@ -903,88 +1131,170 @@ function buildStyles(isDark) {
903
1131
  padding: 10px 20px;
904
1132
  border: none;
905
1133
  border-radius: 10px;
906
- background: ${accent};
907
- color: #fff;
1134
+ background: var(--ec-accent);
1135
+ color: var(--ec-accent-contrast);
908
1136
  font-size: 14px;
909
1137
  font-weight: 500;
910
1138
  cursor: pointer;
911
1139
  }
912
- .stage-btn:hover { filter: brightness(1.1); }
1140
+ .stage-btn:hover { filter: brightness(1.08); }
913
1141
 
914
1142
  .footer {
915
- border-top: 1px solid ${border};
916
- padding-top: 10px;
1143
+ border-top: 1px solid var(--ec-border);
1144
+ padding-top: 8px;
917
1145
  display: flex;
918
1146
  flex-direction: column;
919
1147
  gap: 6px;
1148
+ }
1149
+
1150
+ .footer-row {
1151
+ display: flex;
920
1152
  align-items: center;
1153
+ justify-content: space-between;
1154
+ flex-wrap: wrap;
1155
+ gap: 2px 8px;
921
1156
  }
1157
+ .footer-row > * { min-width: 0; }
1158
+ .footer-row > :only-child { margin-inline: auto; }
922
1159
 
923
1160
  .footer-link {
924
1161
  background: none;
925
1162
  border: none;
926
- color: ${muted};
1163
+ color: var(--ec-muted);
927
1164
  font-size: 13px;
928
1165
  cursor: pointer;
929
- padding: 4px 8px;
1166
+ padding: 5px 8px;
930
1167
  border-radius: 8px;
931
1168
  }
932
- .footer-link:hover { color: ${text}; background: ${itemBg}; }
933
- .method-link { color: ${accent}; }
1169
+ .footer-link:hover { color: var(--ec-text); background: var(--ec-surface); }
1170
+ .method-link { color: var(--ec-accent); }
1171
+
1172
+ .wallet-row-label { font-size: 12px; color: var(--ec-muted); padding: 2px 2px 0; }
934
1173
 
935
- .wallet-switcher { width: 100%; display: flex; flex-direction: column; align-items: center; }
936
- .wallet-panel { width: 100%; padding: 6px 0; display: flex; flex-direction: column; gap: 4px; }
937
1174
  .wallet-row {
938
- width: 100%;
939
- text-align: left;
940
- padding: 8px 12px;
941
- background: ${itemBg};
942
- border: 1px solid ${border};
943
- border-radius: 10px;
944
- color: ${text};
945
- font-size: 13px;
1175
+ display: grid;
1176
+ grid-template-columns: repeat(4, 1fr);
1177
+ gap: 8px;
1178
+ }
1179
+ .row-tile {
1180
+ display: flex;
1181
+ flex-direction: column;
1182
+ align-items: center;
1183
+ justify-content: center;
1184
+ gap: 6px;
1185
+ padding: 10px 6px;
1186
+ min-width: 0;
1187
+ background: var(--ec-surface);
1188
+ border: 1px solid var(--ec-border);
1189
+ border-radius: 12px;
1190
+ color: var(--ec-text);
1191
+ font-size: 11px;
946
1192
  cursor: pointer;
947
1193
  }
948
- .wallet-row:hover, .wallet-row.selected { border-color: ${accent}; }
1194
+ .row-tile:hover { border-color: var(--ec-accent); }
1195
+ .row-tile.selected {
1196
+ border-color: var(--ec-accent);
1197
+ background: var(--ec-accent-soft);
1198
+ box-shadow: inset 0 0 0 1px var(--ec-accent);
1199
+ }
1200
+ .row-tile.selected .row-tile-name { font-weight: 600; }
1201
+ .row-tile .wallet-icon { width: 28px; height: 28px; }
1202
+ .row-tile-name { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1203
+
1204
+ .more-tile { color: var(--ec-muted); }
1205
+ .more-tile:hover { color: var(--ec-text); }
1206
+ .more-tile[aria-expanded='true'] { border-color: var(--ec-accent); }
1207
+ .more-count {
1208
+ width: 28px;
1209
+ height: 28px;
1210
+ border-radius: 8px;
1211
+ display: inline-flex;
1212
+ align-items: center;
1213
+ justify-content: center;
1214
+ font-size: 12px;
1215
+ font-weight: 600;
1216
+ background: var(--ec-accent-soft);
1217
+ color: var(--ec-accent);
1218
+ }
1219
+
1220
+ .wallet-panel { width: 100%; padding: 4px 0 2px; display: flex; flex-direction: column; gap: 8px; }
1221
+ .wallet-search {
1222
+ width: 100%; padding: 8px 12px;
1223
+ border-radius: 10px; border: 1px solid var(--ec-border); background: transparent;
1224
+ color: inherit; font: inherit; font-size: 13px;
1225
+ }
1226
+ .wallet-search:focus { outline: none; border-color: var(--ec-accent); }
1227
+
1228
+ .wallet-scroll { max-height: 260px; overflow-y: auto; }
1229
+ .wallet-grid {
1230
+ display: grid;
1231
+ grid-template-columns: repeat(3, 1fr);
1232
+ gap: 8px;
1233
+ }
1234
+ .wallet-tile {
1235
+ aspect-ratio: 1;
1236
+ display: flex;
1237
+ flex-direction: column;
1238
+ align-items: center;
1239
+ justify-content: center;
1240
+ gap: 8px;
1241
+ padding: 8px;
1242
+ min-width: 0;
1243
+ background: var(--ec-surface);
1244
+ border: 1px solid var(--ec-border);
1245
+ border-radius: 12px;
1246
+ color: var(--ec-text);
1247
+ font-size: 12px;
1248
+ cursor: pointer;
1249
+ }
1250
+ .wallet-tile:hover { border-color: var(--ec-accent); }
1251
+ .wallet-tile.selected {
1252
+ border-color: var(--ec-accent);
1253
+ background: var(--ec-accent-soft);
1254
+ box-shadow: inset 0 0 0 1px var(--ec-accent);
1255
+ }
1256
+ .wallet-tile-name { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
949
1257
 
950
1258
  .wallet-icon { position: relative; width: 28px; height: 28px; flex-shrink: 0; display: inline-flex; }
1259
+ .wallet-tile .wallet-icon { width: 40px; height: 40px; }
951
1260
  .wallet-badge {
952
- width: 28px; height: 28px; border-radius: 8px; display: inline-flex;
1261
+ width: 100%; height: 100%; border-radius: 8px; display: inline-flex;
953
1262
  align-items: center; justify-content: center; font-size: 13px;
954
- font-weight: 600; background: ${accentSoft}; color: ${accent};
1263
+ font-weight: 600; background: var(--ec-accent-soft); color: var(--ec-accent);
955
1264
  }
956
- .wallet-img { position: absolute; inset: 0; width: 28px; height: 28px; border-radius: 8px; object-fit: cover; }
957
- .wallet-row-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
958
- .wallet-search {
959
- width: 100%; box-sizing: border-box; padding: 8px 10px; margin-bottom: 4px;
960
- border-radius: 8px; border: 1px solid ${border}; background: transparent;
961
- color: inherit; font: inherit;
962
- }
963
- .wallet-search:focus { outline: none; border-color: ${accent}; }
964
- .wallet-empty { padding: 8px 10px; font-size: 12px; opacity: 0.7; }
965
- .url-error { padding: 4px 2px 0; font-size: 12px; color: ${danger}; }
966
- .wallet-custom { display: flex; gap: 6px; margin-top: 4px; }
1265
+ .wallet-tile .wallet-badge { border-radius: 10px; font-size: 17px; }
1266
+ .wallet-img { position: absolute; inset: 0; width: 100%; height: 100%; border-radius: 8px; object-fit: cover; }
1267
+ .wallet-tile .wallet-img { border-radius: 10px; }
1268
+
1269
+ .wallet-empty { padding: 10px 4px; font-size: 12px; color: var(--ec-muted); text-align: center; }
1270
+
1271
+ .wallet-custom { display: flex; gap: 6px; }
967
1272
  .url-input {
968
1273
  flex: 1;
969
1274
  padding: 8px 12px;
970
- border: 1px solid ${border};
1275
+ border: 1px solid var(--ec-border);
971
1276
  border-radius: 10px;
972
1277
  font-size: 13px;
973
- background: ${itemBg};
974
- color: ${text};
1278
+ background: var(--ec-surface);
1279
+ color: var(--ec-text);
975
1280
  outline: none;
976
1281
  }
977
- .url-input:focus { border-color: ${accent}; }
978
- .url-input.invalid { border-color: ${danger}; }
1282
+ .url-input:focus { border-color: var(--ec-accent); }
1283
+ .url-input.invalid { border-color: var(--ec-danger); }
979
1284
  .url-go {
980
1285
  padding: 8px 14px;
981
1286
  border: none;
982
1287
  border-radius: 10px;
983
- background: ${accent};
984
- color: #fff;
1288
+ background: var(--ec-accent);
1289
+ color: var(--ec-accent-contrast);
985
1290
  font-size: 13px;
986
1291
  cursor: pointer;
987
1292
  }
988
- `;
989
- }
1293
+ .url-error { font-size: 12px; color: var(--ec-danger); }
1294
+
1295
+ @media (prefers-reduced-motion: reduce) {
1296
+ .overlay, .modal, .stage.fade-in > *, .pin-row.shake { animation: none; }
1297
+ .claimed-pulse::after { animation: none; }
1298
+ }
1299
+ `;
990
1300
  //# sourceMappingURL=connect-modal.js.map