@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.
- package/dist/browser.mjs +172 -82
- 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/relay-connect-runner.js +1 -0
- package/dist/esm/relay-connect-runner.js.map +1 -1
- package/dist/esm/ui/connect-modal.js +318 -124
- 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/relay-connect-runner.d.ts +6 -0
- package/dist/types/relay-connect-runner.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 +5 -5
- package/src/browser-connect-handler.ts +11 -1
- package/src/index.ts +1 -1
- package/src/relay-connect-runner.ts +8 -0
- package/src/ui/connect-modal.ts +393 -126
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,17 @@ 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.'),
|
|
499
|
+
);
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
const renderClaimed = (): void => {
|
|
503
|
+
const pulse = document.createElement('div');
|
|
504
|
+
pulse.className = 'claimed-pulse';
|
|
505
|
+
setStage(
|
|
506
|
+
pulse,
|
|
507
|
+
el('p', 'stage-caption', 'Phone connected — finish there'),
|
|
508
|
+
el('p', 'stage-subline', 'Approve the request on your phone, then come back here.'),
|
|
372
509
|
);
|
|
373
510
|
};
|
|
374
511
|
|
|
@@ -562,6 +699,14 @@ export function runConnectModal(options: ConnectModalOptions): Promise<ConnectRe
|
|
|
562
699
|
scheduleRemint(handoff.expiresIn);
|
|
563
700
|
}
|
|
564
701
|
},
|
|
702
|
+
onClaimed: (): void => {
|
|
703
|
+
if (session.active && !settled) {
|
|
704
|
+
// The phone has the request — stop re-minting (a new pointer
|
|
705
|
+
// would orphan the approval in progress) and show progress.
|
|
706
|
+
clearTimeout(remintTimer);
|
|
707
|
+
renderClaimed();
|
|
708
|
+
}
|
|
709
|
+
},
|
|
565
710
|
requestPin: (attempt, previousError): Promise<string> => {
|
|
566
711
|
clearTimeout(remintTimer);
|
|
567
712
|
return renderPin(attempt, previousError);
|
|
@@ -664,64 +809,75 @@ export function runConnectModal(options: ConnectModalOptions): Promise<ConnectRe
|
|
|
664
809
|
await startMethod();
|
|
665
810
|
};
|
|
666
811
|
|
|
667
|
-
// ── Footer: method link + wallet
|
|
812
|
+
// ── Footer: one row (method link + wallet toggle), panel below ──
|
|
668
813
|
const renderFooter = (): void => {
|
|
669
814
|
footer.replaceChildren();
|
|
670
815
|
|
|
816
|
+
const row = document.createElement('div');
|
|
817
|
+
row.className = 'footer-row';
|
|
818
|
+
|
|
671
819
|
if (method === 'phone') {
|
|
672
820
|
const alt = document.createElement('button');
|
|
673
821
|
alt.className = 'footer-link method-link';
|
|
674
|
-
alt.textContent = 'No phone
|
|
822
|
+
alt.textContent = 'No phone? Use this browser →';
|
|
675
823
|
// startPopup runs synchronously in this click handler.
|
|
676
824
|
alt.addEventListener('click', () => { startPopup(); });
|
|
677
|
-
|
|
825
|
+
row.appendChild(alt);
|
|
678
826
|
} else if (!isMobile) {
|
|
679
827
|
const alt = document.createElement('button');
|
|
680
828
|
alt.className = 'footer-link method-link';
|
|
681
829
|
alt.textContent = 'Use your phone instead →';
|
|
682
830
|
alt.addEventListener('click', () => { void switchMethod('phone'); });
|
|
683
|
-
|
|
831
|
+
row.appendChild(alt);
|
|
684
832
|
}
|
|
685
833
|
|
|
834
|
+
let panel: HTMLElement | undefined;
|
|
686
835
|
if (!lockedWallet && wallets.length > 0) {
|
|
687
|
-
|
|
836
|
+
const switcher = buildWalletSwitcher();
|
|
837
|
+
row.appendChild(switcher.toggle);
|
|
838
|
+
panel = switcher.panel;
|
|
688
839
|
}
|
|
689
|
-
};
|
|
690
840
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
841
|
+
if (row.childElementCount > 0) {
|
|
842
|
+
footer.appendChild(row);
|
|
843
|
+
}
|
|
844
|
+
if (panel !== undefined) {
|
|
845
|
+
footer.appendChild(panel);
|
|
846
|
+
}
|
|
847
|
+
};
|
|
694
848
|
|
|
849
|
+
const buildWalletSwitcher = (): { toggle: HTMLButtonElement; panel: HTMLDivElement } => {
|
|
695
850
|
const current = wallets.find((wallet) => wallet.url === walletUrl);
|
|
696
851
|
const toggle = document.createElement('button');
|
|
697
852
|
toggle.className = 'footer-link wallet-toggle';
|
|
698
853
|
toggle.setAttribute('aria-expanded', 'false');
|
|
699
|
-
toggle.textContent = `Wallet: ${current?.name ?? walletUrl ?? 'Choose'} ▾`;
|
|
854
|
+
toggle.textContent = `Wallet: ${current?.name ?? hostnameOf(walletUrl) ?? 'Choose'} ▾`;
|
|
700
855
|
|
|
701
856
|
const panel = document.createElement('div');
|
|
702
857
|
panel.className = 'wallet-panel';
|
|
703
858
|
panel.hidden = true;
|
|
704
859
|
|
|
705
|
-
// Search filter —
|
|
706
|
-
const
|
|
860
|
+
// Search filter — appears once the catalog outgrows one grid row.
|
|
861
|
+
const tiles: Array<{ wallet: (typeof wallets)[number]; el: HTMLButtonElement }> = [];
|
|
707
862
|
const empty = document.createElement('div');
|
|
708
863
|
empty.className = 'wallet-empty';
|
|
709
864
|
empty.textContent = 'No wallets match your search.';
|
|
710
865
|
empty.hidden = true;
|
|
711
866
|
|
|
712
|
-
if (wallets.length >
|
|
867
|
+
if (wallets.length > WALLET_SEARCH_THRESHOLD) {
|
|
713
868
|
const search = document.createElement('input');
|
|
714
869
|
search.type = 'search';
|
|
715
870
|
search.className = 'wallet-search';
|
|
716
871
|
search.placeholder = 'Search wallets…';
|
|
872
|
+
search.setAttribute('aria-label', 'Search wallets');
|
|
717
873
|
search.addEventListener('input', () => {
|
|
718
874
|
const query = search.value.trim().toLowerCase();
|
|
719
875
|
let visible = 0;
|
|
720
|
-
for (const { wallet, el } of
|
|
876
|
+
for (const { wallet, el: tile } of tiles) {
|
|
721
877
|
const match = query === ''
|
|
722
878
|
|| wallet.name.toLowerCase().includes(query)
|
|
723
879
|
|| wallet.url.toLowerCase().includes(query);
|
|
724
|
-
|
|
880
|
+
tile.hidden = !match;
|
|
725
881
|
if (match) { visible += 1; }
|
|
726
882
|
}
|
|
727
883
|
empty.hidden = visible > 0;
|
|
@@ -729,26 +885,37 @@ export function runConnectModal(options: ConnectModalOptions): Promise<ConnectRe
|
|
|
729
885
|
panel.appendChild(search);
|
|
730
886
|
}
|
|
731
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
|
+
|
|
732
895
|
for (const wallet of wallets) {
|
|
733
|
-
const
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
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);
|
|
740
904
|
if (wallet.url === walletUrl) {
|
|
741
|
-
|
|
905
|
+
tile.classList.add('selected');
|
|
906
|
+
tile.setAttribute('aria-pressed', 'true');
|
|
742
907
|
}
|
|
743
|
-
|
|
908
|
+
tile.addEventListener('click', () => {
|
|
744
909
|
walletUrl = wallet.url;
|
|
745
910
|
renderFooter();
|
|
746
911
|
void startMethod();
|
|
747
912
|
});
|
|
748
|
-
|
|
749
|
-
|
|
913
|
+
tiles.push({ wallet, el: tile });
|
|
914
|
+
grid.appendChild(tile);
|
|
750
915
|
}
|
|
751
|
-
|
|
916
|
+
scroll.appendChild(grid);
|
|
917
|
+
scroll.appendChild(empty);
|
|
918
|
+
panel.appendChild(scroll);
|
|
752
919
|
|
|
753
920
|
// Custom wallet URL (the power-user path, demoted to the disclosure).
|
|
754
921
|
const custom = document.createElement('div');
|
|
@@ -826,9 +993,7 @@ export function runConnectModal(options: ConnectModalOptions): Promise<ConnectRe
|
|
|
826
993
|
toggle.setAttribute('aria-expanded', panel.hidden ? 'false' : 'true');
|
|
827
994
|
});
|
|
828
995
|
|
|
829
|
-
|
|
830
|
-
wrap.appendChild(panel);
|
|
831
|
-
return wrap;
|
|
996
|
+
return { toggle, panel };
|
|
832
997
|
};
|
|
833
998
|
|
|
834
999
|
// ── Global interactions ────────────────────────────────────
|
|
@@ -864,6 +1029,18 @@ function walletInCatalog(wallets: WalletOption[], url: string): boolean {
|
|
|
864
1029
|
return wallets.some((wallet) => wallet.url === url);
|
|
865
1030
|
}
|
|
866
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
|
+
|
|
867
1044
|
function el(tag: string, className: string, text?: string): HTMLElement {
|
|
868
1045
|
const node = document.createElement(tag);
|
|
869
1046
|
node.className = className;
|
|
@@ -945,21 +1122,45 @@ function trapFocus(shadow: ShadowRoot, event: KeyboardEvent): void {
|
|
|
945
1122
|
}
|
|
946
1123
|
|
|
947
1124
|
// ─── Styles ──────────────────────────────────────────────────────
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
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 = `
|
|
961
1157
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
962
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
|
+
|
|
963
1164
|
.overlay {
|
|
964
1165
|
position: fixed;
|
|
965
1166
|
inset: 0;
|
|
@@ -967,6 +1168,7 @@ function buildStyles(isDark: boolean): string {
|
|
|
967
1168
|
display: flex;
|
|
968
1169
|
align-items: center;
|
|
969
1170
|
justify-content: center;
|
|
1171
|
+
padding: 16px;
|
|
970
1172
|
background: rgba(0, 0, 0, 0.5);
|
|
971
1173
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
972
1174
|
animation: fadeIn 0.15s ease-out;
|
|
@@ -981,66 +1183,96 @@ function buildStyles(isDark: boolean): string {
|
|
|
981
1183
|
40%, 80% { transform: translateX(6px); }
|
|
982
1184
|
}
|
|
983
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
|
+
}
|
|
984
1190
|
|
|
985
|
-
.modal {
|
|
986
|
-
|
|
987
|
-
|
|
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);
|
|
988
1195
|
border-radius: 16px;
|
|
989
1196
|
width: 400px;
|
|
990
|
-
max-width:
|
|
991
|
-
max-height:
|
|
1197
|
+
max-width: 100%;
|
|
1198
|
+
max-height: calc(100vh - 32px);
|
|
992
1199
|
overflow-y: auto;
|
|
993
|
-
padding: 20px
|
|
994
|
-
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);
|
|
995
1202
|
animation: slideUp 0.2s ease-out;
|
|
996
1203
|
display: flex;
|
|
997
1204
|
flex-direction: column;
|
|
998
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
|
+
}
|
|
999
1220
|
|
|
1000
|
-
.header { display: flex; justify-content: space-between; align-items: center; }
|
|
1221
|
+
.header { display: flex; justify-content: space-between; align-items: center; gap: 8px; }
|
|
1001
1222
|
.header-identity { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
1002
|
-
.header-icon { border-radius: 6px; }
|
|
1223
|
+
.header-icon { border-radius: 6px; flex-shrink: 0; }
|
|
1003
1224
|
h2 { font-size: 16px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1004
1225
|
|
|
1005
1226
|
.close-btn {
|
|
1006
1227
|
background: none; border: none; font-size: 24px; cursor: pointer;
|
|
1007
|
-
color:
|
|
1228
|
+
color: var(--ec-muted); padding: 4px 8px; border-radius: 8px; line-height: 1;
|
|
1229
|
+
flex-shrink: 0;
|
|
1008
1230
|
}
|
|
1009
|
-
.close-btn:hover { color:
|
|
1231
|
+
.close-btn:hover { color: var(--ec-text); background: var(--ec-surface); }
|
|
1010
1232
|
|
|
1011
1233
|
.stage {
|
|
1012
|
-
min-height:
|
|
1234
|
+
min-height: 240px;
|
|
1013
1235
|
display: flex;
|
|
1014
1236
|
flex-direction: column;
|
|
1015
1237
|
align-items: center;
|
|
1016
1238
|
justify-content: center;
|
|
1017
|
-
gap:
|
|
1239
|
+
gap: 10px;
|
|
1018
1240
|
text-align: center;
|
|
1019
|
-
padding:
|
|
1241
|
+
padding: 12px 0 10px;
|
|
1020
1242
|
}
|
|
1021
1243
|
.stage.fade-in > * { animation: stageIn 0.15s ease-out; }
|
|
1022
1244
|
|
|
1023
1245
|
.qr-box {
|
|
1024
|
-
width:
|
|
1025
|
-
height:
|
|
1026
|
-
padding:
|
|
1246
|
+
width: 200px;
|
|
1247
|
+
height: 200px;
|
|
1248
|
+
padding: 10px;
|
|
1027
1249
|
border-radius: 12px;
|
|
1028
|
-
background:
|
|
1029
|
-
border: 1px solid
|
|
1250
|
+
background: #ffffff;
|
|
1251
|
+
border: 1px solid #e3e4e9;
|
|
1030
1252
|
}
|
|
1031
1253
|
.qr-box svg { width: 100%; height: 100%; display: block; }
|
|
1032
1254
|
|
|
1255
|
+
.claimed-pulse {
|
|
1256
|
+
width: 64px; height: 64px; border-radius: 50%;
|
|
1257
|
+
background: var(--ec-accent-soft); position: relative;
|
|
1258
|
+
}
|
|
1259
|
+
.claimed-pulse::after {
|
|
1260
|
+
content: ''; position: absolute; inset: 18px; border-radius: 50%;
|
|
1261
|
+
background: var(--ec-accent);
|
|
1262
|
+
animation: claimedPulse 1.6s ease-in-out infinite;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1033
1265
|
.stage-caption { font-size: 15px; font-weight: 600; }
|
|
1034
|
-
.stage-subline { font-size: 13px; color:
|
|
1035
|
-
.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); }
|
|
1036
1268
|
|
|
1037
1269
|
.deep-link {
|
|
1038
1270
|
display: block;
|
|
1039
1271
|
width: 100%;
|
|
1040
|
-
padding:
|
|
1272
|
+
padding: 13px 16px;
|
|
1041
1273
|
border-radius: 12px;
|
|
1042
|
-
background:
|
|
1043
|
-
color:
|
|
1274
|
+
background: var(--ec-accent);
|
|
1275
|
+
color: var(--ec-accent-contrast);
|
|
1044
1276
|
font-weight: 600;
|
|
1045
1277
|
font-size: 15px;
|
|
1046
1278
|
text-decoration: none;
|
|
@@ -1049,31 +1281,31 @@ function buildStyles(isDark: boolean): string {
|
|
|
1049
1281
|
.pin-row { display: flex; gap: 10px; }
|
|
1050
1282
|
.pin-row.shake { animation: shake 0.4s ease-in-out; }
|
|
1051
1283
|
.pin-input {
|
|
1052
|
-
width:
|
|
1284
|
+
width: 46px; height: 54px;
|
|
1053
1285
|
text-align: center;
|
|
1054
1286
|
font-size: 24px; font-weight: 600;
|
|
1055
|
-
color:
|
|
1056
|
-
background:
|
|
1057
|
-
border: 1px solid
|
|
1287
|
+
color: var(--ec-text);
|
|
1288
|
+
background: var(--ec-surface);
|
|
1289
|
+
border: 1px solid var(--ec-border);
|
|
1058
1290
|
border-radius: 10px;
|
|
1059
1291
|
outline: none;
|
|
1060
1292
|
}
|
|
1061
|
-
.pin-input:focus { border-color:
|
|
1293
|
+
.pin-input:focus { border-color: var(--ec-accent); }
|
|
1062
1294
|
|
|
1063
1295
|
.check {
|
|
1064
1296
|
width: 56px; height: 56px;
|
|
1065
1297
|
display: flex; align-items: center; justify-content: center;
|
|
1066
1298
|
border-radius: 50%;
|
|
1067
|
-
background:
|
|
1068
|
-
color: #
|
|
1299
|
+
background: var(--ec-success);
|
|
1300
|
+
color: #ffffff;
|
|
1069
1301
|
font-size: 28px;
|
|
1070
1302
|
}
|
|
1071
1303
|
|
|
1072
1304
|
.spinner {
|
|
1073
1305
|
width: 32px; height: 32px;
|
|
1074
1306
|
border-radius: 50%;
|
|
1075
|
-
border: 3px solid
|
|
1076
|
-
border-top-color:
|
|
1307
|
+
border: 3px solid var(--ec-border);
|
|
1308
|
+
border-top-color: var(--ec-accent);
|
|
1077
1309
|
animation: spin 0.8s linear infinite;
|
|
1078
1310
|
}
|
|
1079
1311
|
|
|
@@ -1081,87 +1313,122 @@ function buildStyles(isDark: boolean): string {
|
|
|
1081
1313
|
padding: 10px 20px;
|
|
1082
1314
|
border: none;
|
|
1083
1315
|
border-radius: 10px;
|
|
1084
|
-
background:
|
|
1085
|
-
color:
|
|
1316
|
+
background: var(--ec-accent);
|
|
1317
|
+
color: var(--ec-accent-contrast);
|
|
1086
1318
|
font-size: 14px;
|
|
1087
1319
|
font-weight: 500;
|
|
1088
1320
|
cursor: pointer;
|
|
1089
1321
|
}
|
|
1090
|
-
.stage-btn:hover { filter: brightness(1.
|
|
1322
|
+
.stage-btn:hover { filter: brightness(1.08); }
|
|
1091
1323
|
|
|
1092
1324
|
.footer {
|
|
1093
|
-
border-top: 1px solid
|
|
1094
|
-
padding-top:
|
|
1325
|
+
border-top: 1px solid var(--ec-border);
|
|
1326
|
+
padding-top: 8px;
|
|
1095
1327
|
display: flex;
|
|
1096
1328
|
flex-direction: column;
|
|
1097
1329
|
gap: 6px;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
.footer-row {
|
|
1333
|
+
display: flex;
|
|
1098
1334
|
align-items: center;
|
|
1335
|
+
justify-content: space-between;
|
|
1336
|
+
flex-wrap: wrap;
|
|
1337
|
+
gap: 2px 8px;
|
|
1099
1338
|
}
|
|
1339
|
+
.footer-row > * { min-width: 0; }
|
|
1340
|
+
.footer-row > :only-child { margin-inline: auto; }
|
|
1100
1341
|
|
|
1101
1342
|
.footer-link {
|
|
1102
1343
|
background: none;
|
|
1103
1344
|
border: none;
|
|
1104
|
-
color:
|
|
1345
|
+
color: var(--ec-muted);
|
|
1105
1346
|
font-size: 13px;
|
|
1106
1347
|
cursor: pointer;
|
|
1107
|
-
padding:
|
|
1348
|
+
padding: 5px 8px;
|
|
1108
1349
|
border-radius: 8px;
|
|
1109
1350
|
}
|
|
1110
|
-
.footer-link:hover { color:
|
|
1111
|
-
.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; }
|
|
1112
1354
|
|
|
1113
|
-
.wallet-
|
|
1114
|
-
.wallet-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
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;
|
|
1124
1383
|
cursor: pointer;
|
|
1125
1384
|
}
|
|
1126
|
-
.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; }
|
|
1127
1392
|
|
|
1128
1393
|
.wallet-icon { position: relative; width: 28px; height: 28px; flex-shrink: 0; display: inline-flex; }
|
|
1394
|
+
.wallet-tile .wallet-icon { width: 40px; height: 40px; }
|
|
1129
1395
|
.wallet-badge {
|
|
1130
|
-
width:
|
|
1396
|
+
width: 100%; height: 100%; border-radius: 8px; display: inline-flex;
|
|
1131
1397
|
align-items: center; justify-content: center; font-size: 13px;
|
|
1132
|
-
font-weight: 600; background:
|
|
1133
|
-
}
|
|
1134
|
-
.wallet-img { position: absolute; inset: 0; width: 28px; height: 28px; border-radius: 8px; object-fit: cover; }
|
|
1135
|
-
.wallet-row-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1136
|
-
.wallet-search {
|
|
1137
|
-
width: 100%; box-sizing: border-box; padding: 8px 10px; margin-bottom: 4px;
|
|
1138
|
-
border-radius: 8px; border: 1px solid ${border}; background: transparent;
|
|
1139
|
-
color: inherit; font: inherit;
|
|
1398
|
+
font-weight: 600; background: var(--ec-accent-soft); color: var(--ec-accent);
|
|
1140
1399
|
}
|
|
1141
|
-
.wallet-
|
|
1142
|
-
.wallet-
|
|
1143
|
-
.
|
|
1144
|
-
|
|
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; }
|
|
1145
1407
|
.url-input {
|
|
1146
1408
|
flex: 1;
|
|
1147
1409
|
padding: 8px 12px;
|
|
1148
|
-
border: 1px solid
|
|
1410
|
+
border: 1px solid var(--ec-border);
|
|
1149
1411
|
border-radius: 10px;
|
|
1150
1412
|
font-size: 13px;
|
|
1151
|
-
background:
|
|
1152
|
-
color:
|
|
1413
|
+
background: var(--ec-surface);
|
|
1414
|
+
color: var(--ec-text);
|
|
1153
1415
|
outline: none;
|
|
1154
1416
|
}
|
|
1155
|
-
.url-input:focus { border-color:
|
|
1156
|
-
.url-input.invalid { border-color:
|
|
1417
|
+
.url-input:focus { border-color: var(--ec-accent); }
|
|
1418
|
+
.url-input.invalid { border-color: var(--ec-danger); }
|
|
1157
1419
|
.url-go {
|
|
1158
1420
|
padding: 8px 14px;
|
|
1159
1421
|
border: none;
|
|
1160
1422
|
border-radius: 10px;
|
|
1161
|
-
background:
|
|
1162
|
-
color:
|
|
1423
|
+
background: var(--ec-accent);
|
|
1424
|
+
color: var(--ec-accent-contrast);
|
|
1163
1425
|
font-size: 13px;
|
|
1164
1426
|
cursor: pointer;
|
|
1165
1427
|
}
|
|
1166
|
-
|
|
1167
|
-
|
|
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
|
+
`;
|