@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.
- package/dist/browser.mjs +168 -94
- package/dist/browser.mjs.map +3 -3
- package/dist/esm/browser-connect-handler.js +2 -1
- package/dist/esm/browser-connect-handler.js.map +1 -1
- package/dist/esm/ui/connect-modal.js +301 -136
- package/dist/esm/ui/connect-modal.js.map +1 -1
- package/dist/types/browser-connect-handler.d.ts +8 -1
- package/dist/types/browser-connect-handler.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/ui/connect-modal.d.ts +56 -3
- package/dist/types/ui/connect-modal.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/browser-connect-handler.ts +11 -1
- package/src/index.ts +1 -1
- package/src/ui/connect-modal.ts +371 -138
package/src/ui/connect-modal.ts
CHANGED
|
@@ -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
|
|
17
|
-
* disclosure (
|
|
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
|
|
@@ -56,6 +65,74 @@ const REMINT_MIN_MS = 10_000;
|
|
|
56
65
|
/** Pairing code length (matches the wallet's generated PIN). */
|
|
57
66
|
const PIN_LENGTH = 4;
|
|
58
67
|
|
|
68
|
+
/** Catalog size beyond which the wallet switcher shows its search bar. */
|
|
69
|
+
const WALLET_SEARCH_THRESHOLD = 4; // one full row of tiles
|
|
70
|
+
|
|
71
|
+
/** Palette overrides for one appearance's design tokens. */
|
|
72
|
+
export interface ConnectModalPalette {
|
|
73
|
+
/** Modal surface. */
|
|
74
|
+
background?: string;
|
|
75
|
+
|
|
76
|
+
/** Inset surfaces: tiles, inputs, hover fills. */
|
|
77
|
+
surface?: string;
|
|
78
|
+
|
|
79
|
+
/** Primary text. */
|
|
80
|
+
text?: string;
|
|
81
|
+
|
|
82
|
+
/** Secondary text. */
|
|
83
|
+
textMuted?: string;
|
|
84
|
+
|
|
85
|
+
/** Hairline borders and dividers. */
|
|
86
|
+
border?: string;
|
|
87
|
+
|
|
88
|
+
/** Brand accent: buttons, links, focus rings, selection. */
|
|
89
|
+
accent?: string;
|
|
90
|
+
|
|
91
|
+
/** Text/icon color on accent surfaces. */
|
|
92
|
+
accentContrast?: string;
|
|
93
|
+
|
|
94
|
+
/** Error text and invalid-input borders. */
|
|
95
|
+
danger?: string;
|
|
96
|
+
|
|
97
|
+
/** Success surfaces (the connected check). */
|
|
98
|
+
success?: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Optional color scheme provided by the embedding app. */
|
|
102
|
+
export interface ConnectModalTheme {
|
|
103
|
+
/**
|
|
104
|
+
* Follow the visitor's system appearance (`'auto'`, live-updating) or
|
|
105
|
+
* force one.
|
|
106
|
+
* @default 'auto'
|
|
107
|
+
*/
|
|
108
|
+
appearance?: 'auto' | 'light' | 'dark';
|
|
109
|
+
|
|
110
|
+
/** Brand accent applied in both appearances (per-scheme palettes win). */
|
|
111
|
+
accent?: string;
|
|
112
|
+
|
|
113
|
+
/** Text/icon color on accent surfaces, in both appearances. */
|
|
114
|
+
accentContrast?: string;
|
|
115
|
+
|
|
116
|
+
/** Token overrides applied while the light appearance is active. */
|
|
117
|
+
light?: ConnectModalPalette;
|
|
118
|
+
|
|
119
|
+
/** Token overrides applied while the dark appearance is active. */
|
|
120
|
+
dark?: ConnectModalPalette;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Design-token custom property behind each palette key. */
|
|
124
|
+
const PALETTE_TOKENS: Record<keyof ConnectModalPalette, string> = {
|
|
125
|
+
background : '--ec-bg',
|
|
126
|
+
surface : '--ec-surface',
|
|
127
|
+
text : '--ec-text',
|
|
128
|
+
textMuted : '--ec-muted',
|
|
129
|
+
border : '--ec-border',
|
|
130
|
+
accent : '--ec-accent',
|
|
131
|
+
accentContrast : '--ec-accent-contrast',
|
|
132
|
+
danger : '--ec-danger',
|
|
133
|
+
success : '--ec-success',
|
|
134
|
+
};
|
|
135
|
+
|
|
59
136
|
/** Options for {@link runConnectModal}. */
|
|
60
137
|
export interface ConnectModalOptions {
|
|
61
138
|
/** Wallet catalog shown in the wallet switcher. */
|
|
@@ -99,6 +176,13 @@ export interface ConnectModalOptions {
|
|
|
99
176
|
/** Timeout in milliseconds for either method's handshake. */
|
|
100
177
|
timeout?: number;
|
|
101
178
|
|
|
179
|
+
/**
|
|
180
|
+
* Color scheme. By default the modal follows the visitor's system
|
|
181
|
+
* light/dark appearance; apps can force an appearance and/or override
|
|
182
|
+
* palette tokens per scheme to match their brand.
|
|
183
|
+
*/
|
|
184
|
+
theme?: ConnectModalTheme;
|
|
185
|
+
|
|
102
186
|
/** DWN protocols and permission scopes being requested. */
|
|
103
187
|
permissionRequests: ConnectPermissionRequest[];
|
|
104
188
|
|
|
@@ -273,9 +357,8 @@ export function runConnectModal(options: ConnectModalOptions): Promise<ConnectRe
|
|
|
273
357
|
host.id = 'enbox-connect-modal';
|
|
274
358
|
const shadow = host.attachShadow({ mode: 'open' });
|
|
275
359
|
|
|
276
|
-
const isDark = window.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false;
|
|
277
360
|
const style = document.createElement('style');
|
|
278
|
-
style.textContent =
|
|
361
|
+
style.textContent = MODAL_STYLES;
|
|
279
362
|
shadow.appendChild(style);
|
|
280
363
|
|
|
281
364
|
const overlay = document.createElement('div');
|
|
@@ -286,6 +369,47 @@ export function runConnectModal(options: ConnectModalOptions): Promise<ConnectRe
|
|
|
286
369
|
modal.setAttribute('aria-modal', 'true');
|
|
287
370
|
modal.setAttribute('aria-label', `Connect to ${appName}`);
|
|
288
371
|
|
|
372
|
+
// ── Theme ──────────────────────────────────────────────────
|
|
373
|
+
// System light/dark is pure CSS (`prefers-color-scheme`), so it tracks
|
|
374
|
+
// the visitor live. A forced appearance pins the token block instead.
|
|
375
|
+
const theme = options.theme ?? {};
|
|
376
|
+
const appearance = theme.appearance ?? 'auto';
|
|
377
|
+
if (appearance !== 'auto') {
|
|
378
|
+
modal.setAttribute('data-appearance', appearance);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Dapp palette overrides land as inline custom properties: setProperty
|
|
382
|
+
// parses each value as a lone declaration, so a hostile string cannot
|
|
383
|
+
// escape into the stylesheet.
|
|
384
|
+
const systemDark = window.matchMedia?.('(prefers-color-scheme: dark)');
|
|
385
|
+
const hasPaletteOverrides = theme.accent !== undefined || theme.accentContrast !== undefined
|
|
386
|
+
|| theme.light !== undefined || theme.dark !== undefined;
|
|
387
|
+
const applyPaletteTokens = (): void => {
|
|
388
|
+
const scheme = appearance === 'auto'
|
|
389
|
+
? (systemDark?.matches === true ? 'dark' : 'light')
|
|
390
|
+
: appearance;
|
|
391
|
+
const palette: ConnectModalPalette = {
|
|
392
|
+
accent : theme.accent,
|
|
393
|
+
accentContrast : theme.accentContrast,
|
|
394
|
+
...theme[scheme],
|
|
395
|
+
};
|
|
396
|
+
for (const key of Object.keys(PALETTE_TOKENS) as Array<keyof ConnectModalPalette>) {
|
|
397
|
+
const value = palette[key];
|
|
398
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
399
|
+
modal.style.setProperty(PALETTE_TOKENS[key], value);
|
|
400
|
+
} else {
|
|
401
|
+
modal.style.removeProperty(PALETTE_TOKENS[key]);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
const onSchemeChange = (): void => applyPaletteTokens();
|
|
406
|
+
if (hasPaletteOverrides) {
|
|
407
|
+
applyPaletteTokens();
|
|
408
|
+
if (appearance === 'auto') {
|
|
409
|
+
systemDark?.addEventListener?.('change', onSchemeChange);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
289
413
|
// ── Session state ──────────────────────────────────────────
|
|
290
414
|
let settled = false;
|
|
291
415
|
let relaySession: RelaySession | undefined;
|
|
@@ -298,6 +422,7 @@ export function runConnectModal(options: ConnectModalOptions): Promise<ConnectRe
|
|
|
298
422
|
relaySession?.cancel();
|
|
299
423
|
clearTimeout(remintTimer);
|
|
300
424
|
document.removeEventListener('keydown', onKeydown);
|
|
425
|
+
systemDark?.removeEventListener?.('change', onSchemeChange);
|
|
301
426
|
try { document.body.removeChild(host); } catch { /* best effort */ }
|
|
302
427
|
};
|
|
303
428
|
|
|
@@ -359,8 +484,10 @@ export function runConnectModal(options: ConnectModalOptions): Promise<ConnectRe
|
|
|
359
484
|
|
|
360
485
|
const qrBox = document.createElement('div');
|
|
361
486
|
qrBox.className = 'qr-box';
|
|
487
|
+
// Fixed colors on the white card: dark-on-light QRs scan reliably in
|
|
488
|
+
// both appearances (inverted codes trip up some camera apps).
|
|
362
489
|
qrBox.replaceChildren(qrToSvg(encodeQr(handoff.walletUri), {
|
|
363
|
-
dark :
|
|
490
|
+
dark : '#14141f',
|
|
364
491
|
light : 'transparent',
|
|
365
492
|
quietZone : 2,
|
|
366
493
|
}));
|
|
@@ -368,7 +495,7 @@ export function runConnectModal(options: ConnectModalOptions): Promise<ConnectRe
|
|
|
368
495
|
setStage(
|
|
369
496
|
qrBox,
|
|
370
497
|
el('p', 'stage-caption', 'Scan with your phone’s camera'),
|
|
371
|
-
el('p', 'stage-subline', 'Your wallet stays on your phone
|
|
498
|
+
el('p', 'stage-subline', 'Your wallet stays on your phone.'),
|
|
372
499
|
);
|
|
373
500
|
};
|
|
374
501
|
|
|
@@ -682,64 +809,75 @@ export function runConnectModal(options: ConnectModalOptions): Promise<ConnectRe
|
|
|
682
809
|
await startMethod();
|
|
683
810
|
};
|
|
684
811
|
|
|
685
|
-
// ── Footer: method link + wallet
|
|
812
|
+
// ── Footer: one row (method link + wallet toggle), panel below ──
|
|
686
813
|
const renderFooter = (): void => {
|
|
687
814
|
footer.replaceChildren();
|
|
688
815
|
|
|
816
|
+
const row = document.createElement('div');
|
|
817
|
+
row.className = 'footer-row';
|
|
818
|
+
|
|
689
819
|
if (method === 'phone') {
|
|
690
820
|
const alt = document.createElement('button');
|
|
691
821
|
alt.className = 'footer-link method-link';
|
|
692
|
-
alt.textContent = 'No phone
|
|
822
|
+
alt.textContent = 'No phone? Use this browser →';
|
|
693
823
|
// startPopup runs synchronously in this click handler.
|
|
694
824
|
alt.addEventListener('click', () => { startPopup(); });
|
|
695
|
-
|
|
825
|
+
row.appendChild(alt);
|
|
696
826
|
} else if (!isMobile) {
|
|
697
827
|
const alt = document.createElement('button');
|
|
698
828
|
alt.className = 'footer-link method-link';
|
|
699
829
|
alt.textContent = 'Use your phone instead →';
|
|
700
830
|
alt.addEventListener('click', () => { void switchMethod('phone'); });
|
|
701
|
-
|
|
831
|
+
row.appendChild(alt);
|
|
702
832
|
}
|
|
703
833
|
|
|
834
|
+
let panel: HTMLElement | undefined;
|
|
704
835
|
if (!lockedWallet && wallets.length > 0) {
|
|
705
|
-
|
|
836
|
+
const switcher = buildWalletSwitcher();
|
|
837
|
+
row.appendChild(switcher.toggle);
|
|
838
|
+
panel = switcher.panel;
|
|
706
839
|
}
|
|
707
|
-
};
|
|
708
840
|
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
841
|
+
if (row.childElementCount > 0) {
|
|
842
|
+
footer.appendChild(row);
|
|
843
|
+
}
|
|
844
|
+
if (panel !== undefined) {
|
|
845
|
+
footer.appendChild(panel);
|
|
846
|
+
}
|
|
847
|
+
};
|
|
712
848
|
|
|
849
|
+
const buildWalletSwitcher = (): { toggle: HTMLButtonElement; panel: HTMLDivElement } => {
|
|
713
850
|
const current = wallets.find((wallet) => wallet.url === walletUrl);
|
|
714
851
|
const toggle = document.createElement('button');
|
|
715
852
|
toggle.className = 'footer-link wallet-toggle';
|
|
716
853
|
toggle.setAttribute('aria-expanded', 'false');
|
|
717
|
-
toggle.textContent = `Wallet: ${current?.name ?? walletUrl ?? 'Choose'} ▾`;
|
|
854
|
+
toggle.textContent = `Wallet: ${current?.name ?? hostnameOf(walletUrl) ?? 'Choose'} ▾`;
|
|
718
855
|
|
|
719
856
|
const panel = document.createElement('div');
|
|
720
857
|
panel.className = 'wallet-panel';
|
|
721
858
|
panel.hidden = true;
|
|
722
859
|
|
|
723
|
-
// Search filter —
|
|
724
|
-
const
|
|
860
|
+
// Search filter — appears once the catalog outgrows one grid row.
|
|
861
|
+
const tiles: Array<{ wallet: (typeof wallets)[number]; el: HTMLButtonElement }> = [];
|
|
725
862
|
const empty = document.createElement('div');
|
|
726
863
|
empty.className = 'wallet-empty';
|
|
727
864
|
empty.textContent = 'No wallets match your search.';
|
|
728
865
|
empty.hidden = true;
|
|
729
866
|
|
|
730
|
-
if (wallets.length >
|
|
867
|
+
if (wallets.length > WALLET_SEARCH_THRESHOLD) {
|
|
731
868
|
const search = document.createElement('input');
|
|
732
869
|
search.type = 'search';
|
|
733
870
|
search.className = 'wallet-search';
|
|
734
871
|
search.placeholder = 'Search wallets…';
|
|
872
|
+
search.setAttribute('aria-label', 'Search wallets');
|
|
735
873
|
search.addEventListener('input', () => {
|
|
736
874
|
const query = search.value.trim().toLowerCase();
|
|
737
875
|
let visible = 0;
|
|
738
|
-
for (const { wallet, el } of
|
|
876
|
+
for (const { wallet, el: tile } of tiles) {
|
|
739
877
|
const match = query === ''
|
|
740
878
|
|| wallet.name.toLowerCase().includes(query)
|
|
741
879
|
|| wallet.url.toLowerCase().includes(query);
|
|
742
|
-
|
|
880
|
+
tile.hidden = !match;
|
|
743
881
|
if (match) { visible += 1; }
|
|
744
882
|
}
|
|
745
883
|
empty.hidden = visible > 0;
|
|
@@ -747,26 +885,37 @@ export function runConnectModal(options: ConnectModalOptions): Promise<ConnectRe
|
|
|
747
885
|
panel.appendChild(search);
|
|
748
886
|
}
|
|
749
887
|
|
|
888
|
+
// Square tiles, three per row; past two rows the grid scrolls in
|
|
889
|
+
// place so a large catalog never stretches the modal.
|
|
890
|
+
const scroll = document.createElement('div');
|
|
891
|
+
scroll.className = 'wallet-scroll';
|
|
892
|
+
const grid = document.createElement('div');
|
|
893
|
+
grid.className = 'wallet-grid';
|
|
894
|
+
|
|
750
895
|
for (const wallet of wallets) {
|
|
751
|
-
const
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
896
|
+
const tile = document.createElement('button');
|
|
897
|
+
tile.className = 'wallet-tile';
|
|
898
|
+
tile.title = wallet.name;
|
|
899
|
+
tile.appendChild(buildWalletIcon(wallet));
|
|
900
|
+
const tileName = document.createElement('span');
|
|
901
|
+
tileName.className = 'wallet-tile-name';
|
|
902
|
+
tileName.textContent = wallet.name;
|
|
903
|
+
tile.appendChild(tileName);
|
|
758
904
|
if (wallet.url === walletUrl) {
|
|
759
|
-
|
|
905
|
+
tile.classList.add('selected');
|
|
906
|
+
tile.setAttribute('aria-pressed', 'true');
|
|
760
907
|
}
|
|
761
|
-
|
|
908
|
+
tile.addEventListener('click', () => {
|
|
762
909
|
walletUrl = wallet.url;
|
|
763
910
|
renderFooter();
|
|
764
911
|
void startMethod();
|
|
765
912
|
});
|
|
766
|
-
|
|
767
|
-
|
|
913
|
+
tiles.push({ wallet, el: tile });
|
|
914
|
+
grid.appendChild(tile);
|
|
768
915
|
}
|
|
769
|
-
|
|
916
|
+
scroll.appendChild(grid);
|
|
917
|
+
scroll.appendChild(empty);
|
|
918
|
+
panel.appendChild(scroll);
|
|
770
919
|
|
|
771
920
|
// Custom wallet URL (the power-user path, demoted to the disclosure).
|
|
772
921
|
const custom = document.createElement('div');
|
|
@@ -844,9 +993,7 @@ export function runConnectModal(options: ConnectModalOptions): Promise<ConnectRe
|
|
|
844
993
|
toggle.setAttribute('aria-expanded', panel.hidden ? 'false' : 'true');
|
|
845
994
|
});
|
|
846
995
|
|
|
847
|
-
|
|
848
|
-
wrap.appendChild(panel);
|
|
849
|
-
return wrap;
|
|
996
|
+
return { toggle, panel };
|
|
850
997
|
};
|
|
851
998
|
|
|
852
999
|
// ── Global interactions ────────────────────────────────────
|
|
@@ -882,6 +1029,18 @@ function walletInCatalog(wallets: WalletOption[], url: string): boolean {
|
|
|
882
1029
|
return wallets.some((wallet) => wallet.url === url);
|
|
883
1030
|
}
|
|
884
1031
|
|
|
1032
|
+
/** Compact display form of a wallet URL for the switcher toggle. */
|
|
1033
|
+
function hostnameOf(url: string | undefined): string | undefined {
|
|
1034
|
+
if (url === undefined) {
|
|
1035
|
+
return undefined;
|
|
1036
|
+
}
|
|
1037
|
+
try {
|
|
1038
|
+
return new URL(url).hostname;
|
|
1039
|
+
} catch {
|
|
1040
|
+
return url;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
885
1044
|
function el(tag: string, className: string, text?: string): HTMLElement {
|
|
886
1045
|
const node = document.createElement(tag);
|
|
887
1046
|
node.className = className;
|
|
@@ -963,21 +1122,45 @@ function trapFocus(shadow: ShadowRoot, event: KeyboardEvent): void {
|
|
|
963
1122
|
}
|
|
964
1123
|
|
|
965
1124
|
// ─── Styles ──────────────────────────────────────────────────────
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1125
|
+
//
|
|
1126
|
+
// One static sheet; every color reads a `--ec-*` token. Light values sit on
|
|
1127
|
+
// `.modal`, dark values apply under `prefers-color-scheme` (so the system
|
|
1128
|
+
// appearance tracks live) and under a forced `data-appearance`. Dapp
|
|
1129
|
+
// palette overrides land as inline custom properties, which outrank all of
|
|
1130
|
+
// these — nothing user-provided is ever concatenated into this string.
|
|
1131
|
+
|
|
1132
|
+
/** Light-appearance design tokens. */
|
|
1133
|
+
const LIGHT_TOKENS = `
|
|
1134
|
+
--ec-bg: #ffffff;
|
|
1135
|
+
--ec-surface: #f5f6f8;
|
|
1136
|
+
--ec-text: #17171f;
|
|
1137
|
+
--ec-muted: #63636e;
|
|
1138
|
+
--ec-border: #e3e4e9;
|
|
1139
|
+
--ec-accent: #0a62d0;
|
|
1140
|
+
--ec-accent-contrast: #ffffff;
|
|
1141
|
+
--ec-danger: #c0392b;
|
|
1142
|
+
--ec-success: #1e9e5a;`;
|
|
1143
|
+
|
|
1144
|
+
/** Dark-appearance design tokens. */
|
|
1145
|
+
const DARK_TOKENS = `
|
|
1146
|
+
--ec-bg: #181822;
|
|
1147
|
+
--ec-surface: #23232f;
|
|
1148
|
+
--ec-text: #e9e9f0;
|
|
1149
|
+
--ec-muted: #9a9aa6;
|
|
1150
|
+
--ec-border: #33333f;
|
|
1151
|
+
--ec-accent: #58a6ff;
|
|
1152
|
+
--ec-accent-contrast: #0b1526;
|
|
1153
|
+
--ec-danger: #ff7a7a;
|
|
1154
|
+
--ec-success: #43d17c;`;
|
|
1155
|
+
|
|
1156
|
+
const MODAL_STYLES = `
|
|
979
1157
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
980
1158
|
|
|
1159
|
+
/* Keep the hidden attribute authoritative: author display values (e.g.
|
|
1160
|
+
the panel's flex) would otherwise override the UA's [hidden] rule and
|
|
1161
|
+
leave "collapsed" sections permanently expanded. */
|
|
1162
|
+
[hidden] { display: none !important; }
|
|
1163
|
+
|
|
981
1164
|
.overlay {
|
|
982
1165
|
position: fixed;
|
|
983
1166
|
inset: 0;
|
|
@@ -985,6 +1168,7 @@ function buildStyles(isDark: boolean): string {
|
|
|
985
1168
|
display: flex;
|
|
986
1169
|
align-items: center;
|
|
987
1170
|
justify-content: center;
|
|
1171
|
+
padding: 16px;
|
|
988
1172
|
background: rgba(0, 0, 0, 0.5);
|
|
989
1173
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
990
1174
|
animation: fadeIn 0.15s ease-out;
|
|
@@ -999,82 +1183,96 @@ function buildStyles(isDark: boolean): string {
|
|
|
999
1183
|
40%, 80% { transform: translateX(6px); }
|
|
1000
1184
|
}
|
|
1001
1185
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
1186
|
+
@keyframes claimedPulse {
|
|
1187
|
+
0%, 100% { transform: scale(0.85); opacity: 0.7; }
|
|
1188
|
+
50% { transform: scale(1); opacity: 1; }
|
|
1189
|
+
}
|
|
1002
1190
|
|
|
1003
|
-
.modal {
|
|
1004
|
-
|
|
1005
|
-
|
|
1191
|
+
.modal {${LIGHT_TOKENS}
|
|
1192
|
+
--ec-accent-soft: color-mix(in srgb, var(--ec-accent) 14%, transparent);
|
|
1193
|
+
background: var(--ec-bg);
|
|
1194
|
+
color: var(--ec-text);
|
|
1006
1195
|
border-radius: 16px;
|
|
1007
1196
|
width: 400px;
|
|
1008
|
-
max-width:
|
|
1009
|
-
max-height:
|
|
1197
|
+
max-width: 100%;
|
|
1198
|
+
max-height: calc(100vh - 32px);
|
|
1010
1199
|
overflow-y: auto;
|
|
1011
|
-
padding: 20px
|
|
1012
|
-
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.
|
|
1200
|
+
padding: 16px 20px 12px;
|
|
1201
|
+
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.35);
|
|
1013
1202
|
animation: slideUp 0.2s ease-out;
|
|
1014
1203
|
display: flex;
|
|
1015
1204
|
flex-direction: column;
|
|
1016
1205
|
}
|
|
1206
|
+
@media (prefers-color-scheme: dark) {
|
|
1207
|
+
.modal {${DARK_TOKENS}
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
.modal[data-appearance='light'] {${LIGHT_TOKENS}
|
|
1211
|
+
}
|
|
1212
|
+
.modal[data-appearance='dark'] {${DARK_TOKENS}
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
button, input { font-family: inherit; }
|
|
1216
|
+
button:focus-visible, a:focus-visible {
|
|
1217
|
+
outline: 2px solid var(--ec-accent);
|
|
1218
|
+
outline-offset: 2px;
|
|
1219
|
+
}
|
|
1017
1220
|
|
|
1018
|
-
.header { display: flex; justify-content: space-between; align-items: center; }
|
|
1221
|
+
.header { display: flex; justify-content: space-between; align-items: center; gap: 8px; }
|
|
1019
1222
|
.header-identity { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
1020
|
-
.header-icon { border-radius: 6px; }
|
|
1223
|
+
.header-icon { border-radius: 6px; flex-shrink: 0; }
|
|
1021
1224
|
h2 { font-size: 16px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1022
1225
|
|
|
1023
1226
|
.close-btn {
|
|
1024
1227
|
background: none; border: none; font-size: 24px; cursor: pointer;
|
|
1025
|
-
color:
|
|
1228
|
+
color: var(--ec-muted); padding: 4px 8px; border-radius: 8px; line-height: 1;
|
|
1229
|
+
flex-shrink: 0;
|
|
1026
1230
|
}
|
|
1027
|
-
.close-btn:hover { color:
|
|
1231
|
+
.close-btn:hover { color: var(--ec-text); background: var(--ec-surface); }
|
|
1028
1232
|
|
|
1029
1233
|
.stage {
|
|
1030
|
-
min-height:
|
|
1234
|
+
min-height: 240px;
|
|
1031
1235
|
display: flex;
|
|
1032
1236
|
flex-direction: column;
|
|
1033
1237
|
align-items: center;
|
|
1034
1238
|
justify-content: center;
|
|
1035
|
-
gap:
|
|
1239
|
+
gap: 10px;
|
|
1036
1240
|
text-align: center;
|
|
1037
|
-
padding:
|
|
1241
|
+
padding: 12px 0 10px;
|
|
1038
1242
|
}
|
|
1039
1243
|
.stage.fade-in > * { animation: stageIn 0.15s ease-out; }
|
|
1040
1244
|
|
|
1041
1245
|
.qr-box {
|
|
1042
|
-
width:
|
|
1043
|
-
height:
|
|
1044
|
-
padding:
|
|
1246
|
+
width: 200px;
|
|
1247
|
+
height: 200px;
|
|
1248
|
+
padding: 10px;
|
|
1045
1249
|
border-radius: 12px;
|
|
1046
|
-
background:
|
|
1250
|
+
background: #ffffff;
|
|
1251
|
+
border: 1px solid #e3e4e9;
|
|
1252
|
+
}
|
|
1253
|
+
.qr-box svg { width: 100%; height: 100%; display: block; }
|
|
1254
|
+
|
|
1047
1255
|
.claimed-pulse {
|
|
1048
1256
|
width: 64px; height: 64px; border-radius: 50%;
|
|
1049
|
-
background:
|
|
1257
|
+
background: var(--ec-accent-soft); position: relative;
|
|
1050
1258
|
}
|
|
1051
1259
|
.claimed-pulse::after {
|
|
1052
1260
|
content: ''; position: absolute; inset: 18px; border-radius: 50%;
|
|
1053
|
-
background:
|
|
1054
|
-
animation:
|
|
1261
|
+
background: var(--ec-accent);
|
|
1262
|
+
animation: claimedPulse 1.6s ease-in-out infinite;
|
|
1055
1263
|
}
|
|
1056
|
-
@keyframes enbox-claimed-pulse {
|
|
1057
|
-
0%, 100% { transform: scale(0.85); opacity: 0.7; }
|
|
1058
|
-
50% { transform: scale(1); opacity: 1; }
|
|
1059
|
-
}
|
|
1060
|
-
@media (prefers-reduced-motion: reduce) {
|
|
1061
|
-
.claimed-pulse::after { animation: none; }
|
|
1062
|
-
};
|
|
1063
|
-
border: 1px solid ${border};
|
|
1064
|
-
}
|
|
1065
|
-
.qr-box svg { width: 100%; height: 100%; display: block; }
|
|
1066
1264
|
|
|
1067
1265
|
.stage-caption { font-size: 15px; font-weight: 600; }
|
|
1068
|
-
.stage-subline { font-size: 13px; color:
|
|
1069
|
-
.stage-error { font-size: 13px; color:
|
|
1266
|
+
.stage-subline { font-size: 13px; color: var(--ec-muted); max-width: 300px; line-height: 1.45; }
|
|
1267
|
+
.stage-error { font-size: 13px; color: var(--ec-danger); }
|
|
1070
1268
|
|
|
1071
1269
|
.deep-link {
|
|
1072
1270
|
display: block;
|
|
1073
1271
|
width: 100%;
|
|
1074
|
-
padding:
|
|
1272
|
+
padding: 13px 16px;
|
|
1075
1273
|
border-radius: 12px;
|
|
1076
|
-
background:
|
|
1077
|
-
color:
|
|
1274
|
+
background: var(--ec-accent);
|
|
1275
|
+
color: var(--ec-accent-contrast);
|
|
1078
1276
|
font-weight: 600;
|
|
1079
1277
|
font-size: 15px;
|
|
1080
1278
|
text-decoration: none;
|
|
@@ -1083,31 +1281,31 @@ function buildStyles(isDark: boolean): string {
|
|
|
1083
1281
|
.pin-row { display: flex; gap: 10px; }
|
|
1084
1282
|
.pin-row.shake { animation: shake 0.4s ease-in-out; }
|
|
1085
1283
|
.pin-input {
|
|
1086
|
-
width:
|
|
1284
|
+
width: 46px; height: 54px;
|
|
1087
1285
|
text-align: center;
|
|
1088
1286
|
font-size: 24px; font-weight: 600;
|
|
1089
|
-
color:
|
|
1090
|
-
background:
|
|
1091
|
-
border: 1px solid
|
|
1287
|
+
color: var(--ec-text);
|
|
1288
|
+
background: var(--ec-surface);
|
|
1289
|
+
border: 1px solid var(--ec-border);
|
|
1092
1290
|
border-radius: 10px;
|
|
1093
1291
|
outline: none;
|
|
1094
1292
|
}
|
|
1095
|
-
.pin-input:focus { border-color:
|
|
1293
|
+
.pin-input:focus { border-color: var(--ec-accent); }
|
|
1096
1294
|
|
|
1097
1295
|
.check {
|
|
1098
1296
|
width: 56px; height: 56px;
|
|
1099
1297
|
display: flex; align-items: center; justify-content: center;
|
|
1100
1298
|
border-radius: 50%;
|
|
1101
|
-
background:
|
|
1102
|
-
color: #
|
|
1299
|
+
background: var(--ec-success);
|
|
1300
|
+
color: #ffffff;
|
|
1103
1301
|
font-size: 28px;
|
|
1104
1302
|
}
|
|
1105
1303
|
|
|
1106
1304
|
.spinner {
|
|
1107
1305
|
width: 32px; height: 32px;
|
|
1108
1306
|
border-radius: 50%;
|
|
1109
|
-
border: 3px solid
|
|
1110
|
-
border-top-color:
|
|
1307
|
+
border: 3px solid var(--ec-border);
|
|
1308
|
+
border-top-color: var(--ec-accent);
|
|
1111
1309
|
animation: spin 0.8s linear infinite;
|
|
1112
1310
|
}
|
|
1113
1311
|
|
|
@@ -1115,87 +1313,122 @@ function buildStyles(isDark: boolean): string {
|
|
|
1115
1313
|
padding: 10px 20px;
|
|
1116
1314
|
border: none;
|
|
1117
1315
|
border-radius: 10px;
|
|
1118
|
-
background:
|
|
1119
|
-
color:
|
|
1316
|
+
background: var(--ec-accent);
|
|
1317
|
+
color: var(--ec-accent-contrast);
|
|
1120
1318
|
font-size: 14px;
|
|
1121
1319
|
font-weight: 500;
|
|
1122
1320
|
cursor: pointer;
|
|
1123
1321
|
}
|
|
1124
|
-
.stage-btn:hover { filter: brightness(1.
|
|
1322
|
+
.stage-btn:hover { filter: brightness(1.08); }
|
|
1125
1323
|
|
|
1126
1324
|
.footer {
|
|
1127
|
-
border-top: 1px solid
|
|
1128
|
-
padding-top:
|
|
1325
|
+
border-top: 1px solid var(--ec-border);
|
|
1326
|
+
padding-top: 8px;
|
|
1129
1327
|
display: flex;
|
|
1130
1328
|
flex-direction: column;
|
|
1131
1329
|
gap: 6px;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
.footer-row {
|
|
1333
|
+
display: flex;
|
|
1132
1334
|
align-items: center;
|
|
1335
|
+
justify-content: space-between;
|
|
1336
|
+
flex-wrap: wrap;
|
|
1337
|
+
gap: 2px 8px;
|
|
1133
1338
|
}
|
|
1339
|
+
.footer-row > * { min-width: 0; }
|
|
1340
|
+
.footer-row > :only-child { margin-inline: auto; }
|
|
1134
1341
|
|
|
1135
1342
|
.footer-link {
|
|
1136
1343
|
background: none;
|
|
1137
1344
|
border: none;
|
|
1138
|
-
color:
|
|
1345
|
+
color: var(--ec-muted);
|
|
1139
1346
|
font-size: 13px;
|
|
1140
1347
|
cursor: pointer;
|
|
1141
|
-
padding:
|
|
1348
|
+
padding: 5px 8px;
|
|
1142
1349
|
border-radius: 8px;
|
|
1143
1350
|
}
|
|
1144
|
-
.footer-link:hover { color:
|
|
1145
|
-
.method-link { color:
|
|
1351
|
+
.footer-link:hover { color: var(--ec-text); background: var(--ec-surface); }
|
|
1352
|
+
.method-link { color: var(--ec-accent); }
|
|
1353
|
+
.wallet-toggle { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1146
1354
|
|
|
1147
|
-
.wallet-
|
|
1148
|
-
.wallet-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1355
|
+
.wallet-panel { width: 100%; padding: 4px 0 2px; display: flex; flex-direction: column; gap: 8px; }
|
|
1356
|
+
.wallet-search {
|
|
1357
|
+
width: 100%; padding: 8px 12px;
|
|
1358
|
+
border-radius: 10px; border: 1px solid var(--ec-border); background: transparent;
|
|
1359
|
+
color: inherit; font: inherit; font-size: 13px;
|
|
1360
|
+
}
|
|
1361
|
+
.wallet-search:focus { outline: none; border-color: var(--ec-accent); }
|
|
1362
|
+
|
|
1363
|
+
.wallet-scroll { max-height: 260px; overflow-y: auto; }
|
|
1364
|
+
.wallet-grid {
|
|
1365
|
+
display: grid;
|
|
1366
|
+
grid-template-columns: repeat(3, 1fr);
|
|
1367
|
+
gap: 8px;
|
|
1368
|
+
}
|
|
1369
|
+
.wallet-tile {
|
|
1370
|
+
aspect-ratio: 1;
|
|
1371
|
+
display: flex;
|
|
1372
|
+
flex-direction: column;
|
|
1373
|
+
align-items: center;
|
|
1374
|
+
justify-content: center;
|
|
1375
|
+
gap: 8px;
|
|
1376
|
+
padding: 8px;
|
|
1377
|
+
min-width: 0;
|
|
1378
|
+
background: var(--ec-surface);
|
|
1379
|
+
border: 1px solid var(--ec-border);
|
|
1380
|
+
border-radius: 12px;
|
|
1381
|
+
color: var(--ec-text);
|
|
1382
|
+
font-size: 12px;
|
|
1158
1383
|
cursor: pointer;
|
|
1159
1384
|
}
|
|
1160
|
-
.wallet-
|
|
1385
|
+
.wallet-tile:hover { border-color: var(--ec-accent); }
|
|
1386
|
+
.wallet-tile.selected {
|
|
1387
|
+
border-color: var(--ec-accent);
|
|
1388
|
+
background: var(--ec-accent-soft);
|
|
1389
|
+
box-shadow: inset 0 0 0 1px var(--ec-accent);
|
|
1390
|
+
}
|
|
1391
|
+
.wallet-tile-name { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1161
1392
|
|
|
1162
1393
|
.wallet-icon { position: relative; width: 28px; height: 28px; flex-shrink: 0; display: inline-flex; }
|
|
1394
|
+
.wallet-tile .wallet-icon { width: 40px; height: 40px; }
|
|
1163
1395
|
.wallet-badge {
|
|
1164
|
-
width:
|
|
1396
|
+
width: 100%; height: 100%; border-radius: 8px; display: inline-flex;
|
|
1165
1397
|
align-items: center; justify-content: center; font-size: 13px;
|
|
1166
|
-
font-weight: 600; background:
|
|
1167
|
-
}
|
|
1168
|
-
.wallet-img { position: absolute; inset: 0; width: 28px; height: 28px; border-radius: 8px; object-fit: cover; }
|
|
1169
|
-
.wallet-row-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1170
|
-
.wallet-search {
|
|
1171
|
-
width: 100%; box-sizing: border-box; padding: 8px 10px; margin-bottom: 4px;
|
|
1172
|
-
border-radius: 8px; border: 1px solid ${border}; background: transparent;
|
|
1173
|
-
color: inherit; font: inherit;
|
|
1398
|
+
font-weight: 600; background: var(--ec-accent-soft); color: var(--ec-accent);
|
|
1174
1399
|
}
|
|
1175
|
-
.wallet-
|
|
1176
|
-
.wallet-
|
|
1177
|
-
.
|
|
1178
|
-
|
|
1400
|
+
.wallet-tile .wallet-badge { border-radius: 10px; font-size: 17px; }
|
|
1401
|
+
.wallet-img { position: absolute; inset: 0; width: 100%; height: 100%; border-radius: 8px; object-fit: cover; }
|
|
1402
|
+
.wallet-tile .wallet-img { border-radius: 10px; }
|
|
1403
|
+
|
|
1404
|
+
.wallet-empty { padding: 10px 4px; font-size: 12px; color: var(--ec-muted); text-align: center; }
|
|
1405
|
+
|
|
1406
|
+
.wallet-custom { display: flex; gap: 6px; }
|
|
1179
1407
|
.url-input {
|
|
1180
1408
|
flex: 1;
|
|
1181
1409
|
padding: 8px 12px;
|
|
1182
|
-
border: 1px solid
|
|
1410
|
+
border: 1px solid var(--ec-border);
|
|
1183
1411
|
border-radius: 10px;
|
|
1184
1412
|
font-size: 13px;
|
|
1185
|
-
background:
|
|
1186
|
-
color:
|
|
1413
|
+
background: var(--ec-surface);
|
|
1414
|
+
color: var(--ec-text);
|
|
1187
1415
|
outline: none;
|
|
1188
1416
|
}
|
|
1189
|
-
.url-input:focus { border-color:
|
|
1190
|
-
.url-input.invalid { border-color:
|
|
1417
|
+
.url-input:focus { border-color: var(--ec-accent); }
|
|
1418
|
+
.url-input.invalid { border-color: var(--ec-danger); }
|
|
1191
1419
|
.url-go {
|
|
1192
1420
|
padding: 8px 14px;
|
|
1193
1421
|
border: none;
|
|
1194
1422
|
border-radius: 10px;
|
|
1195
|
-
background:
|
|
1196
|
-
color:
|
|
1423
|
+
background: var(--ec-accent);
|
|
1424
|
+
color: var(--ec-accent-contrast);
|
|
1197
1425
|
font-size: 13px;
|
|
1198
1426
|
cursor: pointer;
|
|
1199
1427
|
}
|
|
1200
|
-
|
|
1201
|
-
|
|
1428
|
+
.url-error { font-size: 12px; color: var(--ec-danger); }
|
|
1429
|
+
|
|
1430
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1431
|
+
.overlay, .modal, .stage.fade-in > *, .pin-row.shake { animation: none; }
|
|
1432
|
+
.claimed-pulse::after { animation: none; }
|
|
1433
|
+
}
|
|
1434
|
+
`;
|