@fernolab/wallet-adapter-svelte 0.1.0

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.
@@ -0,0 +1,152 @@
1
+ <script lang="ts">
2
+ import WalletModal from './WalletModal.svelte';
3
+ import { wallet } from '../wallet/wallet.svelte.js';
4
+
5
+ interface Props {
6
+ class?: string;
7
+ }
8
+
9
+ let { class: className = '' }: Props = $props();
10
+
11
+ let showModal = $state(false);
12
+ let menuOpen = $state(false);
13
+ let container: HTMLDivElement | null = null;
14
+
15
+ function togglePrimary(): void {
16
+ if (wallet.connected) {
17
+ menuOpen = !menuOpen;
18
+ } else {
19
+ showModal = true;
20
+ }
21
+ }
22
+
23
+ async function handleDisconnect(): Promise<void> {
24
+ await wallet.disconnect();
25
+ menuOpen = false;
26
+ }
27
+
28
+ function handleSwitch(): void {
29
+ menuOpen = false;
30
+ showModal = true;
31
+ }
32
+ </script>
33
+
34
+ <div class={`wallet-control ${className}`} bind:this={container}>
35
+ <button
36
+ class={`wallet-button ${wallet.connected ? 'connected' : ''}`}
37
+ onclick={togglePrimary}
38
+ type="button"
39
+ aria-expanded={wallet.connected ? menuOpen : undefined}
40
+ aria-haspopup={wallet.connected ? 'menu' : undefined}
41
+ disabled={wallet.connecting}
42
+ >
43
+ {#if wallet.connected && wallet.standardWallet?.icon}
44
+ <img
45
+ alt={`${wallet.standardWallet.name} icon`}
46
+ class="wallet-icon"
47
+ src={wallet.standardWallet.icon}
48
+ />
49
+ {/if}
50
+ <span class="label">
51
+ {wallet.connected ? wallet.shortAddress || 'Connected' : 'Connect wallet'}
52
+ </span>
53
+ {#if wallet.connected}
54
+ <span class="chevron">▾</span>
55
+ {/if}
56
+ </button>
57
+
58
+ {#if wallet.connected && menuOpen}
59
+ <div class="wallet-menu" role="menu">
60
+ <button type="button" role="menuitem" onclick={handleSwitch}>Switch wallet</button>
61
+ <button type="button" role="menuitem" onclick={handleDisconnect}>Disconnect</button>
62
+ </div>
63
+ {/if}
64
+
65
+ <WalletModal bind:show={showModal} onModalClosed={() => (menuOpen = false)} />
66
+ </div>
67
+
68
+ <style>
69
+ .wallet-control {
70
+ position: relative;
71
+ display: inline-flex;
72
+ align-items: center;
73
+ }
74
+
75
+ .wallet-button {
76
+ display: inline-flex;
77
+ align-items: center;
78
+ gap: 8px;
79
+ min-height: 38px;
80
+ padding: 8px 12px;
81
+ border-radius: 6px;
82
+ border: 1px solid #111111;
83
+ background: #ffffff;
84
+ color: #111111;
85
+ font: inherit;
86
+ font-weight: 700;
87
+ cursor: pointer;
88
+ transition: background 0.15s ease, color 0.15s ease;
89
+ }
90
+
91
+ .wallet-button.connected {
92
+ background: #111111;
93
+ color: #ffffff;
94
+ }
95
+
96
+ .wallet-button:disabled {
97
+ opacity: 0.6;
98
+ cursor: not-allowed;
99
+ }
100
+
101
+ .wallet-button:hover:not(:disabled) {
102
+ background: #111111;
103
+ color: #ffffff;
104
+ }
105
+
106
+ .wallet-icon {
107
+ width: 22px;
108
+ height: 22px;
109
+ border-radius: 5px;
110
+ object-fit: cover;
111
+ }
112
+
113
+ .label {
114
+ white-space: nowrap;
115
+ }
116
+
117
+ .chevron {
118
+ font-size: 0.8rem;
119
+ opacity: 0.8;
120
+ }
121
+
122
+ .wallet-menu {
123
+ position: absolute;
124
+ top: calc(100% + 6px);
125
+ right: 0;
126
+ display: grid;
127
+ padding: 6px;
128
+ background: #ffffff;
129
+ border: 1px solid #d8d8d8;
130
+ border-radius: 8px;
131
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
132
+ min-width: 150px;
133
+ z-index: 20;
134
+ }
135
+
136
+ .wallet-menu button {
137
+ background: none;
138
+ border: none;
139
+ text-align: left;
140
+ padding: 8px 10px;
141
+ border-radius: 6px;
142
+ font-weight: 600;
143
+ font-size: 14px;
144
+ color: #111111;
145
+ cursor: pointer;
146
+ transition: background 0.15s ease;
147
+ }
148
+
149
+ .wallet-menu button:hover {
150
+ background: #f7f7f7;
151
+ }
152
+ </style>
@@ -0,0 +1,6 @@
1
+ interface Props {
2
+ class?: string;
3
+ }
4
+ declare const WalletButton: import("svelte").Component<Props, {}, "">;
5
+ type WalletButton = ReturnType<typeof WalletButton>;
6
+ export default WalletButton;
@@ -0,0 +1,48 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ icon: string;
4
+ name: string;
5
+ onclick?: () => void;
6
+ }
7
+
8
+ let { icon, name, onclick }: Props = $props();
9
+ </script>
10
+
11
+ <button class="wallet-list-button" type="button" onclick={onclick}>
12
+ <img src={icon} alt={`${name} icon`} />
13
+ <span>{name}</span>
14
+ </button>
15
+
16
+ <style>
17
+ .wallet-list-button {
18
+ width: 100%;
19
+ display: flex;
20
+ align-items: center;
21
+ gap: 12px;
22
+ padding: 11px 12px;
23
+ border-radius: 8px;
24
+ border: 1px solid #d8d8d8;
25
+ background: #ffffff;
26
+ color: #111111;
27
+ cursor: pointer;
28
+ font: inherit;
29
+ transition: border-color 0.15s ease, background 0.15s ease;
30
+ }
31
+
32
+ .wallet-list-button:hover {
33
+ background: #f7f7f7;
34
+ border-color: #b8b8b8;
35
+ }
36
+
37
+ .wallet-list-button img {
38
+ width: 34px;
39
+ height: 34px;
40
+ border-radius: 7px;
41
+ object-fit: cover;
42
+ }
43
+
44
+ .wallet-list-button span {
45
+ font-weight: 700;
46
+ color: #111111;
47
+ }
48
+ </style>
@@ -0,0 +1,8 @@
1
+ interface Props {
2
+ icon: string;
3
+ name: string;
4
+ onclick?: () => void;
5
+ }
6
+ declare const WalletListButton: import("svelte").Component<Props, {}, "">;
7
+ type WalletListButton = ReturnType<typeof WalletListButton>;
8
+ export default WalletListButton;
@@ -0,0 +1,360 @@
1
+ <script lang="ts">
2
+ import { wallet } from "../wallet/wallet.svelte.js";
3
+ import type { StandardWallet } from "../wallet/types.js";
4
+ import WalletListButton from "./WalletListButton.svelte";
5
+
6
+ interface Props {
7
+ show?: boolean;
8
+ onModalClosed?: () => void;
9
+ }
10
+
11
+ let { show = $bindable(false), onModalClosed }: Props = $props();
12
+
13
+ let connectError = $state<string | null>(null);
14
+
15
+ const standardWallets = $derived(
16
+ wallet.availableWallets.filter(
17
+ (standardWallet) => standardWallet.name !== wallet.mobileWalletAdapterName
18
+ )
19
+ );
20
+ const showMobileActions = $derived(
21
+ Boolean(wallet.mobileWalletAdapterWallet || wallet.detectedWalletBrowserName)
22
+ );
23
+ const hasWallets = $derived(standardWallets.length > 0);
24
+
25
+ function getWalletDisplayName(name: string): string {
26
+ return name === wallet.mobileWalletAdapterName ? wallet.mobileWalletAdapterDisplayName : name;
27
+ }
28
+
29
+ async function handleWalletSelect(
30
+ selectedWallet: StandardWallet
31
+ ): Promise<void> {
32
+ connectError = null;
33
+ try {
34
+ await wallet.connectStandard(selectedWallet);
35
+ show = false;
36
+ onModalClosed && onModalClosed();
37
+ } catch (error) {
38
+ connectError =
39
+ error instanceof Error
40
+ ? error.message
41
+ : "Failed to connect with the selected wallet.";
42
+ }
43
+ }
44
+
45
+ async function handleMobileWalletAdapter(): Promise<void> {
46
+ connectError = null;
47
+ try {
48
+ await wallet.connectMobileWalletAdapter();
49
+ show = false;
50
+ onModalClosed && onModalClosed();
51
+ } catch (error) {
52
+ connectError =
53
+ error instanceof Error
54
+ ? error.message
55
+ : "Mobile Wallet Adapter is not available.";
56
+ }
57
+ }
58
+
59
+ async function handleWalletBrowser(): Promise<void> {
60
+ connectError = null;
61
+ try {
62
+ await wallet.connectWalletBrowser();
63
+ show = false;
64
+ onModalClosed && onModalClosed();
65
+ } catch (error) {
66
+ connectError =
67
+ error instanceof Error
68
+ ? error.message
69
+ : "Wallet browser connection failed.";
70
+ }
71
+ }
72
+
73
+ async function handleRefresh(): Promise<void> {
74
+ connectError = null;
75
+ try {
76
+ await wallet.refreshAvailableWallets();
77
+ } catch (error) {
78
+ connectError =
79
+ error instanceof Error ? error.message : "Could not refresh wallets.";
80
+ }
81
+ }
82
+
83
+ function handleClose(): void {
84
+ show = false;
85
+ connectError = null;
86
+ onModalClosed && onModalClosed();
87
+ }
88
+
89
+ function handleOverlayKeydown(event: KeyboardEvent): void {
90
+ if (event.key === "Escape" || event.key === "Enter" || event.key === " ") {
91
+ event.preventDefault();
92
+ handleClose();
93
+ }
94
+ }
95
+
96
+ function handleOverlayClick(event: MouseEvent): void {
97
+ if (event.target === event.currentTarget) {
98
+ handleClose();
99
+ }
100
+ }
101
+ </script>
102
+
103
+ {#if show}
104
+ <div
105
+ class="modal-overlay"
106
+ role="presentation"
107
+ onclick={handleOverlayClick}
108
+ onkeydown={handleOverlayKeydown}
109
+ tabindex="-1"
110
+ >
111
+ <div class="modal" role="dialog" aria-modal="true" tabindex="-1">
112
+ <div class="modal-header">
113
+ <div>
114
+ <h2>Connect wallet</h2>
115
+ <p>{wallet.runtimeLabel}</p>
116
+ </div>
117
+ <button
118
+ class="close-button"
119
+ type="button"
120
+ onclick={handleClose}
121
+ aria-label="Close">×</button
122
+ >
123
+ </div>
124
+
125
+ <div class="modal-content">
126
+ {#if showMobileActions}
127
+ <div class="mobile-actions">
128
+ {#if wallet.detectedWalletBrowserName}
129
+ <button
130
+ class="action-button primary"
131
+ type="button"
132
+ disabled={wallet.connecting}
133
+ onclick={handleWalletBrowser}
134
+ >
135
+ Connect {wallet.detectedWalletBrowserName}
136
+ </button>
137
+ {/if}
138
+
139
+ {#if wallet.mobileWalletAdapterWallet}
140
+ <button
141
+ class="action-button"
142
+ type="button"
143
+ disabled={wallet.connecting}
144
+ onclick={handleMobileWalletAdapter}
145
+ >
146
+ {wallet.mobileWalletAdapterDisplayName}
147
+ </button>
148
+ {/if}
149
+ </div>
150
+ {/if}
151
+
152
+ {#if wallet.mobileRuntime.isMobile && wallet.mobileWalletLinks.length > 0}
153
+ <div class="mobile-links" aria-label="Open in wallet browser">
154
+ {#each wallet.mobileWalletLinks as link}
155
+ <a href={link.url} rel="noreferrer">{link.name}</a>
156
+ {/each}
157
+ </div>
158
+ {/if}
159
+
160
+ <div class="wallet-list">
161
+ {#if standardWallets.length > 0}
162
+ <div class="section-title">Wallets</div>
163
+ {#each standardWallets as w (w.name)}
164
+ <WalletListButton
165
+ onclick={() => handleWalletSelect(w)}
166
+ icon={w.icon}
167
+ name={getWalletDisplayName(w.name)}
168
+ />
169
+ {/each}
170
+ {/if}
171
+
172
+ {#if !hasWallets}
173
+ <div class="no-wallets">
174
+ <p>No wallets detected.</p>
175
+ </div>
176
+ {/if}
177
+
178
+ <button class="refresh-button" type="button" onclick={handleRefresh}>
179
+ Refresh wallets
180
+ </button>
181
+
182
+ {#if connectError}
183
+ <p class="error" role="alert">{connectError}</p>
184
+ {/if}
185
+ </div>
186
+ </div>
187
+ </div>
188
+ </div>
189
+ {/if}
190
+
191
+ <style>
192
+ .modal-overlay {
193
+ position: fixed;
194
+ inset: 0;
195
+ display: flex;
196
+ align-items: center;
197
+ justify-content: center;
198
+ padding: 20px;
199
+ background: rgba(0, 0, 0, 0.48);
200
+ z-index: 40;
201
+ }
202
+
203
+ .modal {
204
+ background: white;
205
+ border-radius: 10px;
206
+ border: 1px solid #d8d8d8;
207
+ width: min(430px, 100%);
208
+ max-height: 85vh;
209
+ display: flex;
210
+ flex-direction: column;
211
+ overflow: hidden;
212
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
213
+ }
214
+
215
+ .modal-header {
216
+ display: flex;
217
+ align-items: center;
218
+ justify-content: space-between;
219
+ padding: 16px;
220
+ border-bottom: 1px solid #e5e5e5;
221
+ gap: 16px;
222
+ }
223
+
224
+ .close-button {
225
+ background: none;
226
+ border: 1px solid transparent;
227
+ font-size: 1.25rem;
228
+ cursor: pointer;
229
+ color: #111111;
230
+ border-radius: 8px;
231
+ width: 32px;
232
+ height: 32px;
233
+ line-height: 1;
234
+ }
235
+
236
+ .close-button:hover {
237
+ background: #f7f7f7;
238
+ border-color: #d8d8d8;
239
+ }
240
+
241
+ h2 {
242
+ margin: 0;
243
+ font-size: 1.1rem;
244
+ font-weight: 700;
245
+ color: #111111;
246
+ }
247
+
248
+ .modal-header p {
249
+ margin: 3px 0 0;
250
+ color: #666666;
251
+ font-size: 0.86rem;
252
+ }
253
+
254
+ .modal-content {
255
+ padding: 16px;
256
+ overflow-y: auto;
257
+ flex: 1;
258
+ }
259
+
260
+ .mobile-actions {
261
+ display: grid;
262
+ gap: 8px;
263
+ margin-bottom: 12px;
264
+ }
265
+
266
+ .action-button,
267
+ .refresh-button {
268
+ width: 100%;
269
+ border-radius: 8px;
270
+ border: 1px solid #d8d8d8;
271
+ background: #ffffff;
272
+ color: #111111;
273
+ cursor: pointer;
274
+ font-weight: 700;
275
+ padding: 11px 14px;
276
+ transition: background 0.15s ease, border-color 0.15s ease;
277
+ }
278
+
279
+ .action-button:hover:not(:disabled),
280
+ .refresh-button:hover {
281
+ background: #f7f7f7;
282
+ border-color: #b8b8b8;
283
+ }
284
+
285
+ .action-button.primary {
286
+ background: #111111;
287
+ border-color: #111111;
288
+ color: #ffffff;
289
+ }
290
+
291
+ .action-button.primary:hover:not(:disabled) {
292
+ background: #000000;
293
+ border-color: #000000;
294
+ }
295
+
296
+ .action-button:disabled {
297
+ cursor: not-allowed;
298
+ opacity: 0.5;
299
+ }
300
+
301
+ .mobile-links {
302
+ display: flex;
303
+ flex-wrap: wrap;
304
+ gap: 8px;
305
+ margin-bottom: 14px;
306
+ }
307
+
308
+ .mobile-links a {
309
+ border-radius: 8px;
310
+ border: 1px solid #d8d8d8;
311
+ color: #111111;
312
+ font-size: 0.86rem;
313
+ font-weight: 650;
314
+ padding: 7px 10px;
315
+ text-decoration: none;
316
+ }
317
+
318
+ .mobile-links a:hover {
319
+ background: #f7f7f7;
320
+ border-color: #b8b8b8;
321
+ }
322
+
323
+ .wallet-list {
324
+ display: grid;
325
+ gap: 8px;
326
+ }
327
+
328
+ .section-title {
329
+ color: #666666;
330
+ font-size: 0.88rem;
331
+ font-weight: 700;
332
+ margin-top: 4px;
333
+ }
334
+
335
+ .no-wallets {
336
+ padding: 14px;
337
+ background: #fafafa;
338
+ border-radius: 8px;
339
+ border: 1px solid #d8d8d8;
340
+ color: #666666;
341
+ font-size: 0.9rem;
342
+ }
343
+
344
+ .no-wallets p {
345
+ margin: 0;
346
+ }
347
+
348
+ .refresh-button {
349
+ margin-top: 4px;
350
+ padding: 9px 12px;
351
+ font-size: 0.9rem;
352
+ }
353
+
354
+ .error {
355
+ margin: 4px 0 0;
356
+ color: #111111;
357
+ font-weight: 600;
358
+ font-size: 0.85rem;
359
+ }
360
+ </style>
@@ -0,0 +1,7 @@
1
+ interface Props {
2
+ show?: boolean;
3
+ onModalClosed?: () => void;
4
+ }
5
+ declare const WalletModal: import("svelte").Component<Props, {}, "show">;
6
+ type WalletModal = ReturnType<typeof WalletModal>;
7
+ export default WalletModal;
@@ -0,0 +1,25 @@
1
+ <script lang="ts">
2
+ import { setContext, untrack } from "svelte";
3
+ import type { Snippet } from "svelte";
4
+ import { PUBLIC_SOLANA_RPC_ENDPOINT } from "../config.js";
5
+ import { wallet } from "../wallet/wallet.svelte.js";
6
+
7
+ let { rpcEndpoint = PUBLIC_SOLANA_RPC_ENDPOINT, children } = $props<{
8
+ children?: Snippet;
9
+ rpcEndpoint?: string;
10
+ }>();
11
+
12
+ $effect(() => {
13
+ const endpoint = rpcEndpoint;
14
+
15
+ untrack(() => {
16
+ void wallet.initialize(endpoint).catch(() => undefined);
17
+ });
18
+ });
19
+
20
+ setContext("wallet", wallet);
21
+ </script>
22
+
23
+ {#if children}
24
+ {@render children()}
25
+ {/if}
@@ -0,0 +1,8 @@
1
+ import type { Snippet } from "svelte";
2
+ type $$ComponentProps = {
3
+ children?: Snippet;
4
+ rpcEndpoint?: string;
5
+ };
6
+ declare const WalletProvider: import("svelte").Component<$$ComponentProps, {}, "">;
7
+ type WalletProvider = ReturnType<typeof WalletProvider>;
8
+ export default WalletProvider;
@@ -0,0 +1,3 @@
1
+ export { default as WalletProvider } from './WalletProvider.svelte';
2
+ export { default as WalletButton } from './WalletButton.svelte';
3
+ export { default as WalletModal } from './WalletModal.svelte';
@@ -0,0 +1,3 @@
1
+ export { default as WalletProvider } from './WalletProvider.svelte';
2
+ export { default as WalletButton } from './WalletButton.svelte';
3
+ export { default as WalletModal } from './WalletModal.svelte';
@@ -0,0 +1,2 @@
1
+ export declare const DEFAULT_PUBLIC_SOLANA_RPC = "https://api.mainnet-beta.solana.com";
2
+ export declare const PUBLIC_SOLANA_RPC_ENDPOINT = "https://api.mainnet-beta.solana.com";
package/dist/config.js ADDED
@@ -0,0 +1,2 @@
1
+ export const DEFAULT_PUBLIC_SOLANA_RPC = 'https://api.mainnet-beta.solana.com';
2
+ export const PUBLIC_SOLANA_RPC_ENDPOINT = DEFAULT_PUBLIC_SOLANA_RPC;
@@ -0,0 +1 @@
1
+ export declare const browser: boolean;
@@ -0,0 +1 @@
1
+ export const browser = typeof window !== 'undefined' && typeof document !== 'undefined';
@@ -0,0 +1,5 @@
1
+ export * from './components/index.js';
2
+ export { createWalletStore, wallet, type WalletStore } from './wallet/wallet.svelte.js';
3
+ export * from './wallet/mobile-detection.js';
4
+ export * from '@fernolab/wallet-adapter-core';
5
+ export type { WalletWithStandardCapabilities } from './wallet/types.js';
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export * from './components/index.js';
2
+ export { createWalletStore, wallet } from './wallet/wallet.svelte.js';
3
+ export * from './wallet/mobile-detection.js';
4
+ export * from '@fernolab/wallet-adapter-core';
@@ -0,0 +1,3 @@
1
+ export declare function isMobile(): boolean;
2
+ export declare function supportsWalletMobileProtocol(): boolean;
3
+ export declare function getDeviceType(): 'mobile' | 'tablet' | 'desktop';
@@ -0,0 +1,27 @@
1
+ import { detectMobileRuntime } from '@fernolab/wallet-adapter-core';
2
+ import { browser } from '../environment.js';
3
+ function getRuntime() {
4
+ return detectMobileRuntime({
5
+ maxTouchPoints: browser ? navigator.maxTouchPoints : 0
6
+ });
7
+ }
8
+ export function isMobile() {
9
+ if (!browser)
10
+ return false;
11
+ return getRuntime().isMobile;
12
+ }
13
+ export function supportsWalletMobileProtocol() {
14
+ if (!browser)
15
+ return false;
16
+ return getRuntime().supportsMobileWalletAdapter;
17
+ }
18
+ export function getDeviceType() {
19
+ if (!browser)
20
+ return 'desktop';
21
+ if (!isMobile())
22
+ return 'desktop';
23
+ const width = window.innerWidth;
24
+ if (width < 768)
25
+ return 'mobile';
26
+ return 'tablet';
27
+ }
@@ -0,0 +1,3 @@
1
+ import type { StandardWallet as SharedStandardWallet, StandardWalletProvider } from '@fernolab/wallet-adapter-core';
2
+ export type WalletWithStandardCapabilities = StandardWalletProvider;
3
+ export type StandardWallet = SharedStandardWallet<StandardWalletProvider>;
@@ -0,0 +1 @@
1
+ export {};