@enbox/browser 0.3.45 → 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,14 @@ 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.'));
246
308
  };
247
309
  const renderClaimed = () => {
248
310
  const pulse = document.createElement('div');
@@ -499,59 +561,69 @@ export function runConnectModal(options) {
499
561
  method = next;
500
562
  yield startMethod();
501
563
  });
502
- // ── Footer: method link + wallet switcher ──────────────────
564
+ // ── Footer: one row (method link + wallet toggle), panel below ──
503
565
  const renderFooter = () => {
504
566
  footer.replaceChildren();
567
+ const row = document.createElement('div');
568
+ row.className = 'footer-row';
505
569
  if (method === 'phone') {
506
570
  const alt = document.createElement('button');
507
571
  alt.className = 'footer-link method-link';
508
- alt.textContent = 'No phone handy? Use this browser →';
572
+ alt.textContent = 'No phone? Use this browser →';
509
573
  // startPopup runs synchronously in this click handler.
510
574
  alt.addEventListener('click', () => { startPopup(); });
511
- footer.appendChild(alt);
575
+ row.appendChild(alt);
512
576
  }
513
577
  else if (!isMobile) {
514
578
  const alt = document.createElement('button');
515
579
  alt.className = 'footer-link method-link';
516
580
  alt.textContent = 'Use your phone instead →';
517
581
  alt.addEventListener('click', () => { void switchMethod('phone'); });
518
- footer.appendChild(alt);
582
+ row.appendChild(alt);
519
583
  }
584
+ let panel;
520
585
  if (!lockedWallet && wallets.length > 0) {
521
- 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);
522
595
  }
523
596
  };
524
597
  const buildWalletSwitcher = () => {
525
598
  var _a, _b;
526
- const wrap = document.createElement('div');
527
- wrap.className = 'wallet-switcher';
528
599
  const current = wallets.find((wallet) => wallet.url === walletUrl);
529
600
  const toggle = document.createElement('button');
530
601
  toggle.className = 'footer-link wallet-toggle';
531
602
  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'} ▾`;
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'} ▾`;
533
604
  const panel = document.createElement('div');
534
605
  panel.className = 'wallet-panel';
535
606
  panel.hidden = true;
536
- // Search filter — only worth the chrome once the catalog grows.
537
- const rows = [];
607
+ // Search filter — appears once the catalog outgrows one grid row.
608
+ const tiles = [];
538
609
  const empty = document.createElement('div');
539
610
  empty.className = 'wallet-empty';
540
611
  empty.textContent = 'No wallets match your search.';
541
612
  empty.hidden = true;
542
- if (wallets.length > 5) {
613
+ if (wallets.length > WALLET_SEARCH_THRESHOLD) {
543
614
  const search = document.createElement('input');
544
615
  search.type = 'search';
545
616
  search.className = 'wallet-search';
546
617
  search.placeholder = 'Search wallets…';
618
+ search.setAttribute('aria-label', 'Search wallets');
547
619
  search.addEventListener('input', () => {
548
620
  const query = search.value.trim().toLowerCase();
549
621
  let visible = 0;
550
- for (const { wallet, el } of rows) {
622
+ for (const { wallet, el: tile } of tiles) {
551
623
  const match = query === ''
552
624
  || wallet.name.toLowerCase().includes(query)
553
625
  || wallet.url.toLowerCase().includes(query);
554
- el.hidden = !match;
626
+ tile.hidden = !match;
555
627
  if (match) {
556
628
  visible += 1;
557
629
  }
@@ -560,26 +632,36 @@ export function runConnectModal(options) {
560
632
  });
561
633
  panel.appendChild(search);
562
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';
563
641
  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);
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);
571
650
  if (wallet.url === walletUrl) {
572
- row.classList.add('selected');
651
+ tile.classList.add('selected');
652
+ tile.setAttribute('aria-pressed', 'true');
573
653
  }
574
- row.addEventListener('click', () => {
654
+ tile.addEventListener('click', () => {
575
655
  walletUrl = wallet.url;
576
656
  renderFooter();
577
657
  void startMethod();
578
658
  });
579
- rows.push({ wallet, el: row });
580
- panel.appendChild(row);
659
+ tiles.push({ wallet, el: tile });
660
+ grid.appendChild(tile);
581
661
  }
582
- panel.appendChild(empty);
662
+ scroll.appendChild(grid);
663
+ scroll.appendChild(empty);
664
+ panel.appendChild(scroll);
583
665
  // Custom wallet URL (the power-user path, demoted to the disclosure).
584
666
  const custom = document.createElement('div');
585
667
  custom.className = 'wallet-custom';
@@ -655,9 +737,7 @@ export function runConnectModal(options) {
655
737
  panel.hidden = !panel.hidden;
656
738
  toggle.setAttribute('aria-expanded', panel.hidden ? 'false' : 'true');
657
739
  });
658
- wrap.appendChild(toggle);
659
- wrap.appendChild(panel);
660
- return wrap;
740
+ return { toggle, panel };
661
741
  };
662
742
  // ── Global interactions ────────────────────────────────────
663
743
  overlay.addEventListener('click', (event) => {
@@ -686,6 +766,18 @@ export function runConnectModal(options) {
686
766
  function walletInCatalog(wallets, url) {
687
767
  return wallets.some((wallet) => wallet.url === url);
688
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
+ }
689
781
  function el(tag, className, text) {
690
782
  const node = document.createElement(tag);
691
783
  node.className = className;
@@ -753,19 +845,42 @@ function trapFocus(shadow, event) {
753
845
  }
754
846
  }
755
847
  // ─── 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 `
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 = `
767
877
  * { box-sizing: border-box; margin: 0; padding: 0; }
768
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
+
769
884
  .overlay {
770
885
  position: fixed;
771
886
  inset: 0;
@@ -773,6 +888,7 @@ function buildStyles(isDark) {
773
888
  display: flex;
774
889
  align-items: center;
775
890
  justify-content: center;
891
+ padding: 16px;
776
892
  background: rgba(0, 0, 0, 0.5);
777
893
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
778
894
  animation: fadeIn 0.15s ease-out;
@@ -787,82 +903,96 @@ function buildStyles(isDark) {
787
903
  40%, 80% { transform: translateX(6px); }
788
904
  }
789
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
+ }
790
910
 
791
- .modal {
792
- background: ${bg};
793
- 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);
794
915
  border-radius: 16px;
795
916
  width: 400px;
796
- max-width: 92vw;
797
- max-height: 88vh;
917
+ max-width: 100%;
918
+ max-height: calc(100vh - 32px);
798
919
  overflow-y: auto;
799
- padding: 20px 24px 16px;
800
- 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);
801
922
  animation: slideUp 0.2s ease-out;
802
923
  display: flex;
803
924
  flex-direction: column;
804
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
+ }
805
934
 
806
- .header { display: flex; justify-content: space-between; align-items: center; }
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
+ }
940
+
941
+ .header { display: flex; justify-content: space-between; align-items: center; gap: 8px; }
807
942
  .header-identity { display: flex; align-items: center; gap: 10px; min-width: 0; }
808
- .header-icon { border-radius: 6px; }
943
+ .header-icon { border-radius: 6px; flex-shrink: 0; }
809
944
  h2 { font-size: 16px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
810
945
 
811
946
  .close-btn {
812
947
  background: none; border: none; font-size: 24px; cursor: pointer;
813
- 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;
814
950
  }
815
- .close-btn:hover { color: ${text}; background: ${itemBg}; }
951
+ .close-btn:hover { color: var(--ec-text); background: var(--ec-surface); }
816
952
 
817
953
  .stage {
818
- min-height: 300px;
954
+ min-height: 240px;
819
955
  display: flex;
820
956
  flex-direction: column;
821
957
  align-items: center;
822
958
  justify-content: center;
823
- gap: 12px;
959
+ gap: 10px;
824
960
  text-align: center;
825
- padding: 16px 0;
961
+ padding: 12px 0 10px;
826
962
  }
827
963
  .stage.fade-in > * { animation: stageIn 0.15s ease-out; }
828
964
 
829
965
  .qr-box {
830
- width: 216px;
831
- height: 216px;
832
- padding: 12px;
966
+ width: 200px;
967
+ height: 200px;
968
+ padding: 10px;
833
969
  border-radius: 12px;
834
- background: ${isDark ? '#10192e' : '#ffffff'}
970
+ background: #ffffff;
971
+ border: 1px solid #e3e4e9;
972
+ }
973
+ .qr-box svg { width: 100%; height: 100%; display: block; }
974
+
835
975
  .claimed-pulse {
836
976
  width: 64px; height: 64px; border-radius: 50%;
837
- background: ${accentSoft}; position: relative;
977
+ background: var(--ec-accent-soft); position: relative;
838
978
  }
839
979
  .claimed-pulse::after {
840
980
  content: ''; position: absolute; inset: 18px; border-radius: 50%;
841
- background: ${accent};
842
- animation: enbox-claimed-pulse 1.6s ease-in-out infinite;
843
- }
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};
981
+ background: var(--ec-accent);
982
+ animation: claimedPulse 1.6s ease-in-out infinite;
852
983
  }
853
- .qr-box svg { width: 100%; height: 100%; display: block; }
854
984
 
855
985
  .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}; }
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); }
858
988
 
859
989
  .deep-link {
860
990
  display: block;
861
991
  width: 100%;
862
- padding: 14px 16px;
992
+ padding: 13px 16px;
863
993
  border-radius: 12px;
864
- background: ${accent};
865
- color: #fff;
994
+ background: var(--ec-accent);
995
+ color: var(--ec-accent-contrast);
866
996
  font-weight: 600;
867
997
  font-size: 15px;
868
998
  text-decoration: none;
@@ -871,31 +1001,31 @@ function buildStyles(isDark) {
871
1001
  .pin-row { display: flex; gap: 10px; }
872
1002
  .pin-row.shake { animation: shake 0.4s ease-in-out; }
873
1003
  .pin-input {
874
- width: 48px; height: 56px;
1004
+ width: 46px; height: 54px;
875
1005
  text-align: center;
876
1006
  font-size: 24px; font-weight: 600;
877
- color: ${text};
878
- background: ${itemBg};
879
- border: 1px solid ${border};
1007
+ color: var(--ec-text);
1008
+ background: var(--ec-surface);
1009
+ border: 1px solid var(--ec-border);
880
1010
  border-radius: 10px;
881
1011
  outline: none;
882
1012
  }
883
- .pin-input:focus { border-color: ${accent}; }
1013
+ .pin-input:focus { border-color: var(--ec-accent); }
884
1014
 
885
1015
  .check {
886
1016
  width: 56px; height: 56px;
887
1017
  display: flex; align-items: center; justify-content: center;
888
1018
  border-radius: 50%;
889
- background: ${success};
890
- color: #fff;
1019
+ background: var(--ec-success);
1020
+ color: #ffffff;
891
1021
  font-size: 28px;
892
1022
  }
893
1023
 
894
1024
  .spinner {
895
1025
  width: 32px; height: 32px;
896
1026
  border-radius: 50%;
897
- border: 3px solid ${border};
898
- border-top-color: ${accent};
1027
+ border: 3px solid var(--ec-border);
1028
+ border-top-color: var(--ec-accent);
899
1029
  animation: spin 0.8s linear infinite;
900
1030
  }
901
1031
 
@@ -903,88 +1033,123 @@ function buildStyles(isDark) {
903
1033
  padding: 10px 20px;
904
1034
  border: none;
905
1035
  border-radius: 10px;
906
- background: ${accent};
907
- color: #fff;
1036
+ background: var(--ec-accent);
1037
+ color: var(--ec-accent-contrast);
908
1038
  font-size: 14px;
909
1039
  font-weight: 500;
910
1040
  cursor: pointer;
911
1041
  }
912
- .stage-btn:hover { filter: brightness(1.1); }
1042
+ .stage-btn:hover { filter: brightness(1.08); }
913
1043
 
914
1044
  .footer {
915
- border-top: 1px solid ${border};
916
- padding-top: 10px;
1045
+ border-top: 1px solid var(--ec-border);
1046
+ padding-top: 8px;
917
1047
  display: flex;
918
1048
  flex-direction: column;
919
1049
  gap: 6px;
1050
+ }
1051
+
1052
+ .footer-row {
1053
+ display: flex;
920
1054
  align-items: center;
1055
+ justify-content: space-between;
1056
+ flex-wrap: wrap;
1057
+ gap: 2px 8px;
921
1058
  }
1059
+ .footer-row > * { min-width: 0; }
1060
+ .footer-row > :only-child { margin-inline: auto; }
922
1061
 
923
1062
  .footer-link {
924
1063
  background: none;
925
1064
  border: none;
926
- color: ${muted};
1065
+ color: var(--ec-muted);
927
1066
  font-size: 13px;
928
1067
  cursor: pointer;
929
- padding: 4px 8px;
1068
+ padding: 5px 8px;
930
1069
  border-radius: 8px;
931
1070
  }
932
- .footer-link:hover { color: ${text}; background: ${itemBg}; }
933
- .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; }
934
1074
 
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
- .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;
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;
946
1103
  cursor: pointer;
947
1104
  }
948
- .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; }
949
1112
 
950
1113
  .wallet-icon { position: relative; width: 28px; height: 28px; flex-shrink: 0; display: inline-flex; }
1114
+ .wallet-tile .wallet-icon { width: 40px; height: 40px; }
951
1115
  .wallet-badge {
952
- width: 28px; height: 28px; border-radius: 8px; display: inline-flex;
1116
+ width: 100%; height: 100%; border-radius: 8px; display: inline-flex;
953
1117
  align-items: center; justify-content: center; font-size: 13px;
954
- font-weight: 600; background: ${accentSoft}; color: ${accent};
1118
+ font-weight: 600; background: var(--ec-accent-soft); color: var(--ec-accent);
955
1119
  }
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; }
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; }
967
1127
  .url-input {
968
1128
  flex: 1;
969
1129
  padding: 8px 12px;
970
- border: 1px solid ${border};
1130
+ border: 1px solid var(--ec-border);
971
1131
  border-radius: 10px;
972
1132
  font-size: 13px;
973
- background: ${itemBg};
974
- color: ${text};
1133
+ background: var(--ec-surface);
1134
+ color: var(--ec-text);
975
1135
  outline: none;
976
1136
  }
977
- .url-input:focus { border-color: ${accent}; }
978
- .url-input.invalid { border-color: ${danger}; }
1137
+ .url-input:focus { border-color: var(--ec-accent); }
1138
+ .url-input.invalid { border-color: var(--ec-danger); }
979
1139
  .url-go {
980
1140
  padding: 8px 14px;
981
1141
  border: none;
982
1142
  border-radius: 10px;
983
- background: ${accent};
984
- color: #fff;
1143
+ background: var(--ec-accent);
1144
+ color: var(--ec-accent-contrast);
985
1145
  font-size: 13px;
986
1146
  cursor: pointer;
987
1147
  }
988
- `;
989
- }
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
+ `;
990
1155
  //# sourceMappingURL=connect-modal.js.map