@enbox/browser 0.3.44 → 0.3.46

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,18 @@
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** — one row holding the alternate-method link and the wallet
17
+ * switcher disclosure (a tile grid with search + custom URL — the
18
+ * entire legacy selector, demoted to an option).
19
+ *
20
+ * Theming: the stylesheet is static and reads `--ec-*` design tokens.
21
+ * Light/dark follows the visitor's system via `prefers-color-scheme` and
22
+ * updates live; the embedding app can force an appearance or override
23
+ * palette tokens through {@link ConnectModalOptions.theme}. Overrides are
24
+ * applied as inline custom properties — never interpolated into the
25
+ * stylesheet — so a hostile string can't escape its declaration. The QR
26
+ * card alone is theme-invariant (dark modules on white scan most
27
+ * reliably).
19
28
  *
20
29
  * The phone path drives the `@enbox/connect` relay transport via
21
30
  * {@link runRelayConnect}; the browser path drives the popup transport via
@@ -48,6 +57,20 @@ const REMINT_SAFETY_MS = 30000;
48
57
  const REMINT_MIN_MS = 10000;
49
58
  /** Pairing code length (matches the wallet's generated PIN). */
50
59
  const PIN_LENGTH = 4;
60
+ /** Catalog size beyond which the wallet switcher shows its search bar. */
61
+ const WALLET_SEARCH_THRESHOLD = 4; // one full row of tiles
62
+ /** Design-token custom property behind each palette key. */
63
+ const PALETTE_TOKENS = {
64
+ background: '--ec-bg',
65
+ surface: '--ec-surface',
66
+ text: '--ec-text',
67
+ textMuted: '--ec-muted',
68
+ border: '--ec-border',
69
+ accent: '--ec-accent',
70
+ accentContrast: '--ec-accent-contrast',
71
+ danger: '--ec-danger',
72
+ success: '--ec-success',
73
+ };
51
74
  /**
52
75
  * Resolves a wallet origin to its relay `connectServerUrl` via the wallet's
53
76
  * `/.well-known/enbox-connect` document ({@link fetchWalletWellKnown} — the
@@ -158,14 +181,13 @@ export function runConnectModal(options) {
158
181
  // Method resolution order: explicit option → remembered → phone.
159
182
  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
183
  return new Promise((resolve, reject) => {
161
- var _a, _b;
184
+ var _a, _b, _c, _d;
162
185
  // ── Host + shadow root ─────────────────────────────────────
163
186
  const host = document.createElement('div');
164
187
  host.id = 'enbox-connect-modal';
165
188
  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
189
  const style = document.createElement('style');
168
- style.textContent = buildStyles(isDark);
190
+ style.textContent = MODAL_STYLES;
169
191
  shadow.appendChild(style);
170
192
  const overlay = document.createElement('div');
171
193
  overlay.className = 'overlay';
@@ -174,6 +196,42 @@ export function runConnectModal(options) {
174
196
  modal.setAttribute('role', 'dialog');
175
197
  modal.setAttribute('aria-modal', 'true');
176
198
  modal.setAttribute('aria-label', `Connect to ${appName}`);
199
+ // ── Theme ──────────────────────────────────────────────────
200
+ // System light/dark is pure CSS (`prefers-color-scheme`), so it tracks
201
+ // the visitor live. A forced appearance pins the token block instead.
202
+ const theme = (_a = options.theme) !== null && _a !== void 0 ? _a : {};
203
+ const appearance = (_b = theme.appearance) !== null && _b !== void 0 ? _b : 'auto';
204
+ if (appearance !== 'auto') {
205
+ modal.setAttribute('data-appearance', appearance);
206
+ }
207
+ // Dapp palette overrides land as inline custom properties: setProperty
208
+ // parses each value as a lone declaration, so a hostile string cannot
209
+ // escape into the stylesheet.
210
+ const systemDark = (_c = window.matchMedia) === null || _c === void 0 ? void 0 : _c.call(window, '(prefers-color-scheme: dark)');
211
+ const hasPaletteOverrides = theme.accent !== undefined || theme.accentContrast !== undefined
212
+ || theme.light !== undefined || theme.dark !== undefined;
213
+ const applyPaletteTokens = () => {
214
+ const scheme = appearance === 'auto'
215
+ ? ((systemDark === null || systemDark === void 0 ? void 0 : systemDark.matches) === true ? 'dark' : 'light')
216
+ : appearance;
217
+ const palette = Object.assign({ accent: theme.accent, accentContrast: theme.accentContrast }, theme[scheme]);
218
+ for (const key of Object.keys(PALETTE_TOKENS)) {
219
+ const value = palette[key];
220
+ if (typeof value === 'string' && value.trim() !== '') {
221
+ modal.style.setProperty(PALETTE_TOKENS[key], value);
222
+ }
223
+ else {
224
+ modal.style.removeProperty(PALETTE_TOKENS[key]);
225
+ }
226
+ }
227
+ };
228
+ const onSchemeChange = () => applyPaletteTokens();
229
+ if (hasPaletteOverrides) {
230
+ applyPaletteTokens();
231
+ if (appearance === 'auto') {
232
+ (_d = systemDark === null || systemDark === void 0 ? void 0 : systemDark.addEventListener) === null || _d === void 0 ? void 0 : _d.call(systemDark, 'change', onSchemeChange);
233
+ }
234
+ }
177
235
  // ── Session state ──────────────────────────────────────────
178
236
  let settled = false;
179
237
  let relaySession;
@@ -182,13 +240,15 @@ export function runConnectModal(options) {
182
240
  let pinResolve;
183
241
  const discoveryCache = new Map();
184
242
  const cleanup = () => {
243
+ var _a;
185
244
  relaySession === null || relaySession === void 0 ? void 0 : relaySession.cancel();
186
245
  clearTimeout(remintTimer);
187
246
  document.removeEventListener('keydown', onKeydown);
247
+ (_a = systemDark === null || systemDark === void 0 ? void 0 : systemDark.removeEventListener) === null || _a === void 0 ? void 0 : _a.call(systemDark, 'change', onSchemeChange);
188
248
  try {
189
249
  document.body.removeChild(host);
190
250
  }
191
- catch ( /* best effort */_a) { /* best effort */ }
251
+ catch ( /* best effort */_b) { /* best effort */ }
192
252
  };
193
253
  const settle = (settler) => {
194
254
  if (settled) {
@@ -237,12 +297,19 @@ export function runConnectModal(options) {
237
297
  }
238
298
  const qrBox = document.createElement('div');
239
299
  qrBox.className = 'qr-box';
300
+ // Fixed colors on the white card: dark-on-light QRs scan reliably in
301
+ // both appearances (inverted codes trip up some camera apps).
240
302
  qrBox.replaceChildren(qrToSvg(encodeQr(handoff.walletUri), {
241
- dark: isDark ? '#e8e8f0' : '#14141f',
303
+ dark: '#14141f',
242
304
  light: 'transparent',
243
305
  quietZone: 2,
244
306
  }));
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.'));
307
+ setStage(qrBox, el('p', 'stage-caption', 'Scan with your phone’s camera'), el('p', 'stage-subline', 'Your wallet stays on your phone.'));
308
+ };
309
+ const renderClaimed = () => {
310
+ const pulse = document.createElement('div');
311
+ pulse.className = 'claimed-pulse';
312
+ setStage(pulse, el('p', 'stage-caption', 'Phone connected — finish there'), el('p', 'stage-subline', 'Approve the request on your phone, then come back here.'));
246
313
  };
247
314
  const renderPin = (attempt, previousError) => {
248
315
  return new Promise((resolvePin) => {
@@ -388,6 +455,14 @@ export function runConnectModal(options) {
388
455
  scheduleRemint(handoff.expiresIn);
389
456
  }
390
457
  },
458
+ onClaimed: () => {
459
+ if (session.active && !settled) {
460
+ // The phone has the request — stop re-minting (a new pointer
461
+ // would orphan the approval in progress) and show progress.
462
+ clearTimeout(remintTimer);
463
+ renderClaimed();
464
+ }
465
+ },
391
466
  requestPin: (attempt, previousError) => {
392
467
  clearTimeout(remintTimer);
393
468
  return renderPin(attempt, previousError);
@@ -486,59 +561,69 @@ export function runConnectModal(options) {
486
561
  method = next;
487
562
  yield startMethod();
488
563
  });
489
- // ── Footer: method link + wallet switcher ──────────────────
564
+ // ── Footer: one row (method link + wallet toggle), panel below ──
490
565
  const renderFooter = () => {
491
566
  footer.replaceChildren();
567
+ const row = document.createElement('div');
568
+ row.className = 'footer-row';
492
569
  if (method === 'phone') {
493
570
  const alt = document.createElement('button');
494
571
  alt.className = 'footer-link method-link';
495
- alt.textContent = 'No phone handy? Use this browser →';
572
+ alt.textContent = 'No phone? Use this browser →';
496
573
  // startPopup runs synchronously in this click handler.
497
574
  alt.addEventListener('click', () => { startPopup(); });
498
- footer.appendChild(alt);
575
+ row.appendChild(alt);
499
576
  }
500
577
  else if (!isMobile) {
501
578
  const alt = document.createElement('button');
502
579
  alt.className = 'footer-link method-link';
503
580
  alt.textContent = 'Use your phone instead →';
504
581
  alt.addEventListener('click', () => { void switchMethod('phone'); });
505
- footer.appendChild(alt);
582
+ row.appendChild(alt);
506
583
  }
584
+ let panel;
507
585
  if (!lockedWallet && wallets.length > 0) {
508
- footer.appendChild(buildWalletSwitcher());
586
+ const switcher = buildWalletSwitcher();
587
+ row.appendChild(switcher.toggle);
588
+ panel = switcher.panel;
589
+ }
590
+ if (row.childElementCount > 0) {
591
+ footer.appendChild(row);
592
+ }
593
+ if (panel !== undefined) {
594
+ footer.appendChild(panel);
509
595
  }
510
596
  };
511
597
  const buildWalletSwitcher = () => {
512
598
  var _a, _b;
513
- const wrap = document.createElement('div');
514
- wrap.className = 'wallet-switcher';
515
599
  const current = wallets.find((wallet) => wallet.url === walletUrl);
516
600
  const toggle = document.createElement('button');
517
601
  toggle.className = 'footer-link wallet-toggle';
518
602
  toggle.setAttribute('aria-expanded', 'false');
519
- 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'} ▾`;
603
+ toggle.textContent = `Wallet: ${(_b = (_a = current === null || current === void 0 ? void 0 : current.name) !== null && _a !== void 0 ? _a : hostnameOf(walletUrl)) !== null && _b !== void 0 ? _b : 'Choose'} ▾`;
520
604
  const panel = document.createElement('div');
521
605
  panel.className = 'wallet-panel';
522
606
  panel.hidden = true;
523
- // Search filter — only worth the chrome once the catalog grows.
524
- const rows = [];
607
+ // Search filter — appears once the catalog outgrows one grid row.
608
+ const tiles = [];
525
609
  const empty = document.createElement('div');
526
610
  empty.className = 'wallet-empty';
527
611
  empty.textContent = 'No wallets match your search.';
528
612
  empty.hidden = true;
529
- if (wallets.length > 5) {
613
+ if (wallets.length > WALLET_SEARCH_THRESHOLD) {
530
614
  const search = document.createElement('input');
531
615
  search.type = 'search';
532
616
  search.className = 'wallet-search';
533
617
  search.placeholder = 'Search wallets…';
618
+ search.setAttribute('aria-label', 'Search wallets');
534
619
  search.addEventListener('input', () => {
535
620
  const query = search.value.trim().toLowerCase();
536
621
  let visible = 0;
537
- for (const { wallet, el } of rows) {
622
+ for (const { wallet, el: tile } of tiles) {
538
623
  const match = query === ''
539
624
  || wallet.name.toLowerCase().includes(query)
540
625
  || wallet.url.toLowerCase().includes(query);
541
- el.hidden = !match;
626
+ tile.hidden = !match;
542
627
  if (match) {
543
628
  visible += 1;
544
629
  }
@@ -547,26 +632,36 @@ export function runConnectModal(options) {
547
632
  });
548
633
  panel.appendChild(search);
549
634
  }
635
+ // Square tiles, three per row; past two rows the grid scrolls in
636
+ // place so a large catalog never stretches the modal.
637
+ const scroll = document.createElement('div');
638
+ scroll.className = 'wallet-scroll';
639
+ const grid = document.createElement('div');
640
+ grid.className = 'wallet-grid';
550
641
  for (const wallet of wallets) {
551
- const row = document.createElement('button');
552
- row.className = 'wallet-row';
553
- row.appendChild(buildWalletIcon(wallet));
554
- const rowName = document.createElement('span');
555
- rowName.className = 'wallet-row-name';
556
- rowName.textContent = wallet.name;
557
- row.appendChild(rowName);
642
+ const tile = document.createElement('button');
643
+ tile.className = 'wallet-tile';
644
+ tile.title = wallet.name;
645
+ tile.appendChild(buildWalletIcon(wallet));
646
+ const tileName = document.createElement('span');
647
+ tileName.className = 'wallet-tile-name';
648
+ tileName.textContent = wallet.name;
649
+ tile.appendChild(tileName);
558
650
  if (wallet.url === walletUrl) {
559
- row.classList.add('selected');
651
+ tile.classList.add('selected');
652
+ tile.setAttribute('aria-pressed', 'true');
560
653
  }
561
- row.addEventListener('click', () => {
654
+ tile.addEventListener('click', () => {
562
655
  walletUrl = wallet.url;
563
656
  renderFooter();
564
657
  void startMethod();
565
658
  });
566
- rows.push({ wallet, el: row });
567
- panel.appendChild(row);
659
+ tiles.push({ wallet, el: tile });
660
+ grid.appendChild(tile);
568
661
  }
569
- panel.appendChild(empty);
662
+ scroll.appendChild(grid);
663
+ scroll.appendChild(empty);
664
+ panel.appendChild(scroll);
570
665
  // Custom wallet URL (the power-user path, demoted to the disclosure).
571
666
  const custom = document.createElement('div');
572
667
  custom.className = 'wallet-custom';
@@ -642,9 +737,7 @@ export function runConnectModal(options) {
642
737
  panel.hidden = !panel.hidden;
643
738
  toggle.setAttribute('aria-expanded', panel.hidden ? 'false' : 'true');
644
739
  });
645
- wrap.appendChild(toggle);
646
- wrap.appendChild(panel);
647
- return wrap;
740
+ return { toggle, panel };
648
741
  };
649
742
  // ── Global interactions ────────────────────────────────────
650
743
  overlay.addEventListener('click', (event) => {
@@ -673,6 +766,18 @@ export function runConnectModal(options) {
673
766
  function walletInCatalog(wallets, url) {
674
767
  return wallets.some((wallet) => wallet.url === url);
675
768
  }
769
+ /** Compact display form of a wallet URL for the switcher toggle. */
770
+ function hostnameOf(url) {
771
+ if (url === undefined) {
772
+ return undefined;
773
+ }
774
+ try {
775
+ return new URL(url).hostname;
776
+ }
777
+ catch (_a) {
778
+ return url;
779
+ }
780
+ }
676
781
  function el(tag, className, text) {
677
782
  const node = document.createElement(tag);
678
783
  node.className = className;
@@ -740,19 +845,42 @@ function trapFocus(shadow, event) {
740
845
  }
741
846
  }
742
847
  // ─── Styles ──────────────────────────────────────────────────────
743
- function buildStyles(isDark) {
744
- const bg = isDark ? '#1a1a2e' : '#ffffff';
745
- const text = isDark ? '#e0e0e0' : '#1a1a2e';
746
- const muted = isDark ? '#888' : '#666';
747
- const border = isDark ? '#333' : '#e0e0e0';
748
- const itemBg = isDark ? '#16213e' : '#f8f9fa';
749
- const accent = isDark ? '#4a9eff' : '#0066cc';
750
- const accentSoft = isDark ? 'rgba(74, 158, 255, 0.16)' : 'rgba(0, 102, 204, 0.1)';
751
- const danger = isDark ? '#ff6b6b' : '#c0392b';
752
- const success = isDark ? '#43d17c' : '#1e9e5a';
753
- return `
848
+ //
849
+ // One static sheet; every color reads a `--ec-*` token. Light values sit on
850
+ // `.modal`, dark values apply under `prefers-color-scheme` (so the system
851
+ // appearance tracks live) and under a forced `data-appearance`. Dapp
852
+ // palette overrides land as inline custom properties, which outrank all of
853
+ // these — nothing user-provided is ever concatenated into this string.
854
+ /** Light-appearance design tokens. */
855
+ const LIGHT_TOKENS = `
856
+ --ec-bg: #ffffff;
857
+ --ec-surface: #f5f6f8;
858
+ --ec-text: #17171f;
859
+ --ec-muted: #63636e;
860
+ --ec-border: #e3e4e9;
861
+ --ec-accent: #0a62d0;
862
+ --ec-accent-contrast: #ffffff;
863
+ --ec-danger: #c0392b;
864
+ --ec-success: #1e9e5a;`;
865
+ /** Dark-appearance design tokens. */
866
+ const DARK_TOKENS = `
867
+ --ec-bg: #181822;
868
+ --ec-surface: #23232f;
869
+ --ec-text: #e9e9f0;
870
+ --ec-muted: #9a9aa6;
871
+ --ec-border: #33333f;
872
+ --ec-accent: #58a6ff;
873
+ --ec-accent-contrast: #0b1526;
874
+ --ec-danger: #ff7a7a;
875
+ --ec-success: #43d17c;`;
876
+ const MODAL_STYLES = `
754
877
  * { box-sizing: border-box; margin: 0; padding: 0; }
755
878
 
879
+ /* Keep the hidden attribute authoritative: author display values (e.g.
880
+ the panel's flex) would otherwise override the UA's [hidden] rule and
881
+ leave "collapsed" sections permanently expanded. */
882
+ [hidden] { display: none !important; }
883
+
756
884
  .overlay {
757
885
  position: fixed;
758
886
  inset: 0;
@@ -760,6 +888,7 @@ function buildStyles(isDark) {
760
888
  display: flex;
761
889
  align-items: center;
762
890
  justify-content: center;
891
+ padding: 16px;
763
892
  background: rgba(0, 0, 0, 0.5);
764
893
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
765
894
  animation: fadeIn 0.15s ease-out;
@@ -774,66 +903,96 @@ function buildStyles(isDark) {
774
903
  40%, 80% { transform: translateX(6px); }
775
904
  }
776
905
  @keyframes spin { to { transform: rotate(360deg); } }
906
+ @keyframes claimedPulse {
907
+ 0%, 100% { transform: scale(0.85); opacity: 0.7; }
908
+ 50% { transform: scale(1); opacity: 1; }
909
+ }
777
910
 
778
- .modal {
779
- background: ${bg};
780
- color: ${text};
911
+ .modal {${LIGHT_TOKENS}
912
+ --ec-accent-soft: color-mix(in srgb, var(--ec-accent) 14%, transparent);
913
+ background: var(--ec-bg);
914
+ color: var(--ec-text);
781
915
  border-radius: 16px;
782
916
  width: 400px;
783
- max-width: 92vw;
784
- max-height: 88vh;
917
+ max-width: 100%;
918
+ max-height: calc(100vh - 32px);
785
919
  overflow-y: auto;
786
- padding: 20px 24px 16px;
787
- box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
920
+ padding: 16px 20px 12px;
921
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.35);
788
922
  animation: slideUp 0.2s ease-out;
789
923
  display: flex;
790
924
  flex-direction: column;
791
925
  }
926
+ @media (prefers-color-scheme: dark) {
927
+ .modal {${DARK_TOKENS}
928
+ }
929
+ }
930
+ .modal[data-appearance='light'] {${LIGHT_TOKENS}
931
+ }
932
+ .modal[data-appearance='dark'] {${DARK_TOKENS}
933
+ }
934
+
935
+ button, input { font-family: inherit; }
936
+ button:focus-visible, a:focus-visible {
937
+ outline: 2px solid var(--ec-accent);
938
+ outline-offset: 2px;
939
+ }
792
940
 
793
- .header { display: flex; justify-content: space-between; align-items: center; }
941
+ .header { display: flex; justify-content: space-between; align-items: center; gap: 8px; }
794
942
  .header-identity { display: flex; align-items: center; gap: 10px; min-width: 0; }
795
- .header-icon { border-radius: 6px; }
943
+ .header-icon { border-radius: 6px; flex-shrink: 0; }
796
944
  h2 { font-size: 16px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
797
945
 
798
946
  .close-btn {
799
947
  background: none; border: none; font-size: 24px; cursor: pointer;
800
- color: ${muted}; padding: 4px 8px; border-radius: 8px; line-height: 1;
948
+ color: var(--ec-muted); padding: 4px 8px; border-radius: 8px; line-height: 1;
949
+ flex-shrink: 0;
801
950
  }
802
- .close-btn:hover { color: ${text}; background: ${itemBg}; }
951
+ .close-btn:hover { color: var(--ec-text); background: var(--ec-surface); }
803
952
 
804
953
  .stage {
805
- min-height: 300px;
954
+ min-height: 240px;
806
955
  display: flex;
807
956
  flex-direction: column;
808
957
  align-items: center;
809
958
  justify-content: center;
810
- gap: 12px;
959
+ gap: 10px;
811
960
  text-align: center;
812
- padding: 16px 0;
961
+ padding: 12px 0 10px;
813
962
  }
814
963
  .stage.fade-in > * { animation: stageIn 0.15s ease-out; }
815
964
 
816
965
  .qr-box {
817
- width: 216px;
818
- height: 216px;
819
- padding: 12px;
966
+ width: 200px;
967
+ height: 200px;
968
+ padding: 10px;
820
969
  border-radius: 12px;
821
- background: ${isDark ? '#10192e' : '#ffffff'};
822
- border: 1px solid ${border};
970
+ background: #ffffff;
971
+ border: 1px solid #e3e4e9;
823
972
  }
824
973
  .qr-box svg { width: 100%; height: 100%; display: block; }
825
974
 
975
+ .claimed-pulse {
976
+ width: 64px; height: 64px; border-radius: 50%;
977
+ background: var(--ec-accent-soft); position: relative;
978
+ }
979
+ .claimed-pulse::after {
980
+ content: ''; position: absolute; inset: 18px; border-radius: 50%;
981
+ background: var(--ec-accent);
982
+ animation: claimedPulse 1.6s ease-in-out infinite;
983
+ }
984
+
826
985
  .stage-caption { font-size: 15px; font-weight: 600; }
827
- .stage-subline { font-size: 13px; color: ${muted}; max-width: 300px; line-height: 1.45; }
828
- .stage-error { font-size: 13px; color: ${danger}; }
986
+ .stage-subline { font-size: 13px; color: var(--ec-muted); max-width: 300px; line-height: 1.45; }
987
+ .stage-error { font-size: 13px; color: var(--ec-danger); }
829
988
 
830
989
  .deep-link {
831
990
  display: block;
832
991
  width: 100%;
833
- padding: 14px 16px;
992
+ padding: 13px 16px;
834
993
  border-radius: 12px;
835
- background: ${accent};
836
- color: #fff;
994
+ background: var(--ec-accent);
995
+ color: var(--ec-accent-contrast);
837
996
  font-weight: 600;
838
997
  font-size: 15px;
839
998
  text-decoration: none;
@@ -842,31 +1001,31 @@ function buildStyles(isDark) {
842
1001
  .pin-row { display: flex; gap: 10px; }
843
1002
  .pin-row.shake { animation: shake 0.4s ease-in-out; }
844
1003
  .pin-input {
845
- width: 48px; height: 56px;
1004
+ width: 46px; height: 54px;
846
1005
  text-align: center;
847
1006
  font-size: 24px; font-weight: 600;
848
- color: ${text};
849
- background: ${itemBg};
850
- border: 1px solid ${border};
1007
+ color: var(--ec-text);
1008
+ background: var(--ec-surface);
1009
+ border: 1px solid var(--ec-border);
851
1010
  border-radius: 10px;
852
1011
  outline: none;
853
1012
  }
854
- .pin-input:focus { border-color: ${accent}; }
1013
+ .pin-input:focus { border-color: var(--ec-accent); }
855
1014
 
856
1015
  .check {
857
1016
  width: 56px; height: 56px;
858
1017
  display: flex; align-items: center; justify-content: center;
859
1018
  border-radius: 50%;
860
- background: ${success};
861
- color: #fff;
1019
+ background: var(--ec-success);
1020
+ color: #ffffff;
862
1021
  font-size: 28px;
863
1022
  }
864
1023
 
865
1024
  .spinner {
866
1025
  width: 32px; height: 32px;
867
1026
  border-radius: 50%;
868
- border: 3px solid ${border};
869
- border-top-color: ${accent};
1027
+ border: 3px solid var(--ec-border);
1028
+ border-top-color: var(--ec-accent);
870
1029
  animation: spin 0.8s linear infinite;
871
1030
  }
872
1031
 
@@ -874,88 +1033,123 @@ function buildStyles(isDark) {
874
1033
  padding: 10px 20px;
875
1034
  border: none;
876
1035
  border-radius: 10px;
877
- background: ${accent};
878
- color: #fff;
1036
+ background: var(--ec-accent);
1037
+ color: var(--ec-accent-contrast);
879
1038
  font-size: 14px;
880
1039
  font-weight: 500;
881
1040
  cursor: pointer;
882
1041
  }
883
- .stage-btn:hover { filter: brightness(1.1); }
1042
+ .stage-btn:hover { filter: brightness(1.08); }
884
1043
 
885
1044
  .footer {
886
- border-top: 1px solid ${border};
887
- padding-top: 10px;
1045
+ border-top: 1px solid var(--ec-border);
1046
+ padding-top: 8px;
888
1047
  display: flex;
889
1048
  flex-direction: column;
890
1049
  gap: 6px;
1050
+ }
1051
+
1052
+ .footer-row {
1053
+ display: flex;
891
1054
  align-items: center;
1055
+ justify-content: space-between;
1056
+ flex-wrap: wrap;
1057
+ gap: 2px 8px;
892
1058
  }
1059
+ .footer-row > * { min-width: 0; }
1060
+ .footer-row > :only-child { margin-inline: auto; }
893
1061
 
894
1062
  .footer-link {
895
1063
  background: none;
896
1064
  border: none;
897
- color: ${muted};
1065
+ color: var(--ec-muted);
898
1066
  font-size: 13px;
899
1067
  cursor: pointer;
900
- padding: 4px 8px;
1068
+ padding: 5px 8px;
901
1069
  border-radius: 8px;
902
1070
  }
903
- .footer-link:hover { color: ${text}; background: ${itemBg}; }
904
- .method-link { color: ${accent}; }
1071
+ .footer-link:hover { color: var(--ec-text); background: var(--ec-surface); }
1072
+ .method-link { color: var(--ec-accent); }
1073
+ .wallet-toggle { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
905
1074
 
906
- .wallet-switcher { width: 100%; display: flex; flex-direction: column; align-items: center; }
907
- .wallet-panel { width: 100%; padding: 6px 0; display: flex; flex-direction: column; gap: 4px; }
908
- .wallet-row {
909
- width: 100%;
910
- text-align: left;
911
- padding: 8px 12px;
912
- background: ${itemBg};
913
- border: 1px solid ${border};
914
- border-radius: 10px;
915
- color: ${text};
916
- font-size: 13px;
1075
+ .wallet-panel { width: 100%; padding: 4px 0 2px; display: flex; flex-direction: column; gap: 8px; }
1076
+ .wallet-search {
1077
+ width: 100%; padding: 8px 12px;
1078
+ border-radius: 10px; border: 1px solid var(--ec-border); background: transparent;
1079
+ color: inherit; font: inherit; font-size: 13px;
1080
+ }
1081
+ .wallet-search:focus { outline: none; border-color: var(--ec-accent); }
1082
+
1083
+ .wallet-scroll { max-height: 260px; overflow-y: auto; }
1084
+ .wallet-grid {
1085
+ display: grid;
1086
+ grid-template-columns: repeat(3, 1fr);
1087
+ gap: 8px;
1088
+ }
1089
+ .wallet-tile {
1090
+ aspect-ratio: 1;
1091
+ display: flex;
1092
+ flex-direction: column;
1093
+ align-items: center;
1094
+ justify-content: center;
1095
+ gap: 8px;
1096
+ padding: 8px;
1097
+ min-width: 0;
1098
+ background: var(--ec-surface);
1099
+ border: 1px solid var(--ec-border);
1100
+ border-radius: 12px;
1101
+ color: var(--ec-text);
1102
+ font-size: 12px;
917
1103
  cursor: pointer;
918
1104
  }
919
- .wallet-row:hover, .wallet-row.selected { border-color: ${accent}; }
1105
+ .wallet-tile:hover { border-color: var(--ec-accent); }
1106
+ .wallet-tile.selected {
1107
+ border-color: var(--ec-accent);
1108
+ background: var(--ec-accent-soft);
1109
+ box-shadow: inset 0 0 0 1px var(--ec-accent);
1110
+ }
1111
+ .wallet-tile-name { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
920
1112
 
921
1113
  .wallet-icon { position: relative; width: 28px; height: 28px; flex-shrink: 0; display: inline-flex; }
1114
+ .wallet-tile .wallet-icon { width: 40px; height: 40px; }
922
1115
  .wallet-badge {
923
- width: 28px; height: 28px; border-radius: 8px; display: inline-flex;
1116
+ width: 100%; height: 100%; border-radius: 8px; display: inline-flex;
924
1117
  align-items: center; justify-content: center; font-size: 13px;
925
- font-weight: 600; background: ${accentSoft}; color: ${accent};
1118
+ font-weight: 600; background: var(--ec-accent-soft); color: var(--ec-accent);
926
1119
  }
927
- .wallet-img { position: absolute; inset: 0; width: 28px; height: 28px; border-radius: 8px; object-fit: cover; }
928
- .wallet-row-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
929
- .wallet-search {
930
- width: 100%; box-sizing: border-box; padding: 8px 10px; margin-bottom: 4px;
931
- border-radius: 8px; border: 1px solid ${border}; background: transparent;
932
- color: inherit; font: inherit;
933
- }
934
- .wallet-search:focus { outline: none; border-color: ${accent}; }
935
- .wallet-empty { padding: 8px 10px; font-size: 12px; opacity: 0.7; }
936
- .url-error { padding: 4px 2px 0; font-size: 12px; color: ${danger}; }
937
- .wallet-custom { display: flex; gap: 6px; margin-top: 4px; }
1120
+ .wallet-tile .wallet-badge { border-radius: 10px; font-size: 17px; }
1121
+ .wallet-img { position: absolute; inset: 0; width: 100%; height: 100%; border-radius: 8px; object-fit: cover; }
1122
+ .wallet-tile .wallet-img { border-radius: 10px; }
1123
+
1124
+ .wallet-empty { padding: 10px 4px; font-size: 12px; color: var(--ec-muted); text-align: center; }
1125
+
1126
+ .wallet-custom { display: flex; gap: 6px; }
938
1127
  .url-input {
939
1128
  flex: 1;
940
1129
  padding: 8px 12px;
941
- border: 1px solid ${border};
1130
+ border: 1px solid var(--ec-border);
942
1131
  border-radius: 10px;
943
1132
  font-size: 13px;
944
- background: ${itemBg};
945
- color: ${text};
1133
+ background: var(--ec-surface);
1134
+ color: var(--ec-text);
946
1135
  outline: none;
947
1136
  }
948
- .url-input:focus { border-color: ${accent}; }
949
- .url-input.invalid { border-color: ${danger}; }
1137
+ .url-input:focus { border-color: var(--ec-accent); }
1138
+ .url-input.invalid { border-color: var(--ec-danger); }
950
1139
  .url-go {
951
1140
  padding: 8px 14px;
952
1141
  border: none;
953
1142
  border-radius: 10px;
954
- background: ${accent};
955
- color: #fff;
1143
+ background: var(--ec-accent);
1144
+ color: var(--ec-accent-contrast);
956
1145
  font-size: 13px;
957
1146
  cursor: pointer;
958
1147
  }
959
- `;
960
- }
1148
+ .url-error { font-size: 12px; color: var(--ec-danger); }
1149
+
1150
+ @media (prefers-reduced-motion: reduce) {
1151
+ .overlay, .modal, .stage.fade-in > *, .pin-row.shake { animation: none; }
1152
+ .claimed-pulse::after { animation: none; }
1153
+ }
1154
+ `;
961
1155
  //# sourceMappingURL=connect-modal.js.map