@1001-digital/components 1.2.2 → 1.3.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.
- package/package.json +2 -22
- package/src/base/components/AppShell.vue +60 -0
- package/src/base/components/BottomNav.vue +44 -0
- package/src/base/components/Sidebar.vue +282 -0
- package/src/index.ts +3 -3
- package/src/evm/assets/wallets/coinbase.svg +0 -4
- package/src/evm/assets/wallets/in-app.svg +0 -5
- package/src/evm/assets/wallets/metamask.svg +0 -1
- package/src/evm/assets/wallets/phantom.svg +0 -4
- package/src/evm/assets/wallets/rabby.svg +0 -24
- package/src/evm/assets/wallets/rainbow.svg +0 -59
- package/src/evm/assets/wallets/safe.png +0 -0
- package/src/evm/assets/wallets/walletconnect.svg +0 -1
- package/src/evm/components/EvmAccount.vue +0 -28
- package/src/evm/components/EvmAvatar.vue +0 -62
- package/src/evm/components/EvmConnect.vue +0 -303
- package/src/evm/components/EvmConnectDialog.vue +0 -75
- package/src/evm/components/EvmConnectionStatus.vue +0 -13
- package/src/evm/components/EvmConnectorQR.vue +0 -86
- package/src/evm/components/EvmInAppWalletSetup.vue +0 -251
- package/src/evm/components/EvmMetaMaskQR.vue +0 -34
- package/src/evm/components/EvmProfile.vue +0 -186
- package/src/evm/components/EvmSeedPhraseInput.vue +0 -193
- package/src/evm/components/EvmSiwe.vue +0 -190
- package/src/evm/components/EvmSiweDialog.vue +0 -93
- package/src/evm/components/EvmSwitchNetwork.vue +0 -132
- package/src/evm/components/EvmTransactionFlow.vue +0 -353
- package/src/evm/components/EvmWalletConnectQR.vue +0 -13
- package/src/evm/components/EvmWalletConnectWallets.vue +0 -200
- package/src/evm/composables/base.ts +0 -7
- package/src/evm/composables/chainId.ts +0 -42
- package/src/evm/composables/ens.ts +0 -113
- package/src/evm/composables/gasPrice.ts +0 -37
- package/src/evm/composables/priceFeed.ts +0 -116
- package/src/evm/composables/siwe.ts +0 -89
- package/src/evm/composables/uri.ts +0 -12
- package/src/evm/composables/walletExplorer.ts +0 -130
- package/src/evm/config.ts +0 -35
- package/src/evm/connectors/inAppWallet.ts +0 -5
- package/src/evm/index.ts +0 -60
- package/src/evm/utils/addresses.ts +0 -6
- package/src/evm/utils/cache.ts +0 -59
- package/src/evm/utils/chains.ts +0 -32
- package/src/evm/utils/ens.ts +0 -116
- package/src/evm/utils/format-eth.ts +0 -15
- package/src/evm/utils/price.ts +0 -17
- package/src/evm/utils/siwe.ts +0 -70
- package/src/evm/utils/uri.ts +0 -24
|
@@ -1,251 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="in-app-wallet-setup">
|
|
3
|
-
<!-- Step: Choose -->
|
|
4
|
-
<div
|
|
5
|
-
v-if="step === 'choose'"
|
|
6
|
-
class="setup-step"
|
|
7
|
-
>
|
|
8
|
-
<p class="muted font-sm">{{ note }}</p>
|
|
9
|
-
|
|
10
|
-
<div class="setup-options">
|
|
11
|
-
<Button
|
|
12
|
-
class="block"
|
|
13
|
-
@click="startGenerate"
|
|
14
|
-
>
|
|
15
|
-
<Icon type="plus" />
|
|
16
|
-
<span>Create New Wallet</span>
|
|
17
|
-
</Button>
|
|
18
|
-
<Button
|
|
19
|
-
class="block"
|
|
20
|
-
@click="step = 'restore'"
|
|
21
|
-
>
|
|
22
|
-
<Icon type="key" />
|
|
23
|
-
<span>Use Existing Recovery Key</span>
|
|
24
|
-
</Button>
|
|
25
|
-
</div>
|
|
26
|
-
<Button
|
|
27
|
-
class="link muted small"
|
|
28
|
-
@click="$emit('back')"
|
|
29
|
-
>
|
|
30
|
-
<Icon type="chevron-left" />
|
|
31
|
-
<span>Back</span>
|
|
32
|
-
</Button>
|
|
33
|
-
</div>
|
|
34
|
-
|
|
35
|
-
<!-- Step: Generate -->
|
|
36
|
-
<div
|
|
37
|
-
v-else-if="step === 'generate'"
|
|
38
|
-
class="setup-step"
|
|
39
|
-
>
|
|
40
|
-
<p class="muted font-sm">
|
|
41
|
-
Write down these 12 words in order. Think of them as your secure
|
|
42
|
-
password so keep them safe - you will need them to restore your account.
|
|
43
|
-
They will not be shown again.
|
|
44
|
-
</p>
|
|
45
|
-
|
|
46
|
-
<div class="generated-words">
|
|
47
|
-
<div
|
|
48
|
-
v-for="(word, i) in generatedWords"
|
|
49
|
-
:key="i"
|
|
50
|
-
class="generated-word"
|
|
51
|
-
>
|
|
52
|
-
<span class="word-number">{{ i + 1 }}</span>
|
|
53
|
-
<span class="word-text">{{ word }}</span>
|
|
54
|
-
</div>
|
|
55
|
-
</div>
|
|
56
|
-
|
|
57
|
-
<div>
|
|
58
|
-
<FormCheckbox v-model="backupConfirmed">
|
|
59
|
-
I've saved my seed phrase
|
|
60
|
-
</FormCheckbox>
|
|
61
|
-
</div>
|
|
62
|
-
|
|
63
|
-
<Button
|
|
64
|
-
class="block"
|
|
65
|
-
:disabled="!backupConfirmed"
|
|
66
|
-
@click="confirmGenerated"
|
|
67
|
-
>
|
|
68
|
-
Continue
|
|
69
|
-
</Button>
|
|
70
|
-
<Button
|
|
71
|
-
class="link muted small"
|
|
72
|
-
@click="step = 'choose'"
|
|
73
|
-
>
|
|
74
|
-
<Icon type="chevron-left" />
|
|
75
|
-
<span>Back</span>
|
|
76
|
-
</Button>
|
|
77
|
-
</div>
|
|
78
|
-
|
|
79
|
-
<!-- Step: Restore -->
|
|
80
|
-
<div
|
|
81
|
-
v-else-if="step === 'restore'"
|
|
82
|
-
class="setup-step"
|
|
83
|
-
>
|
|
84
|
-
<p class="muted font-sm">
|
|
85
|
-
Enter your 12-word seed phrase to restore your wallet.
|
|
86
|
-
</p>
|
|
87
|
-
|
|
88
|
-
<EvmSeedPhraseInput
|
|
89
|
-
v-model="restorePhrase"
|
|
90
|
-
@valid="restoreValid = $event"
|
|
91
|
-
@submit="restoreWallet"
|
|
92
|
-
/>
|
|
93
|
-
|
|
94
|
-
<Button
|
|
95
|
-
class="block"
|
|
96
|
-
:disabled="!restoreValid"
|
|
97
|
-
@click="restoreWallet"
|
|
98
|
-
>
|
|
99
|
-
Restore Wallet
|
|
100
|
-
</Button>
|
|
101
|
-
<Button
|
|
102
|
-
class="link muted small"
|
|
103
|
-
@click="step = 'choose'"
|
|
104
|
-
>
|
|
105
|
-
<Icon type="chevron-left" />
|
|
106
|
-
<span>Back</span>
|
|
107
|
-
</Button>
|
|
108
|
-
</div>
|
|
109
|
-
|
|
110
|
-
<!-- Step: Connecting -->
|
|
111
|
-
<div
|
|
112
|
-
v-else-if="step === 'connecting'"
|
|
113
|
-
class="setup-step"
|
|
114
|
-
>
|
|
115
|
-
<Loading
|
|
116
|
-
txt="Connecting wallet..."
|
|
117
|
-
spinner
|
|
118
|
-
stacked
|
|
119
|
-
/>
|
|
120
|
-
</div>
|
|
121
|
-
</div>
|
|
122
|
-
</template>
|
|
123
|
-
|
|
124
|
-
<script setup lang="ts">
|
|
125
|
-
import { ref, computed } from 'vue'
|
|
126
|
-
import { useConnect, useConnectors } from '@wagmi/vue'
|
|
127
|
-
import Button from '../../base/components/Button.vue'
|
|
128
|
-
import Icon from '../../base/components/Icon.vue'
|
|
129
|
-
import Alert from '../../base/components/Alert.vue'
|
|
130
|
-
import FormCheckbox from '../../base/components/FormCheckbox.vue'
|
|
131
|
-
import Loading from '../../base/components/Loading.vue'
|
|
132
|
-
import EvmSeedPhraseInput from './EvmSeedPhraseInput.vue'
|
|
133
|
-
import { prepareInAppWallet } from '../connectors/inAppWallet'
|
|
134
|
-
|
|
135
|
-
const props = withDefaults(
|
|
136
|
-
defineProps<{
|
|
137
|
-
note?: string
|
|
138
|
-
}>(),
|
|
139
|
-
{
|
|
140
|
-
note: 'Create a browser-based wallet stored locally on this device. Only you have access to your keys.',
|
|
141
|
-
},
|
|
142
|
-
)
|
|
143
|
-
|
|
144
|
-
const emit = defineEmits<{
|
|
145
|
-
connected: []
|
|
146
|
-
back: []
|
|
147
|
-
}>()
|
|
148
|
-
|
|
149
|
-
const connectors = useConnectors()
|
|
150
|
-
const { mutateAsync: connectAsync } = useConnect()
|
|
151
|
-
const inAppConnector = computed(() =>
|
|
152
|
-
connectors.value.find((c) => c.type === 'inAppWallet'),
|
|
153
|
-
)
|
|
154
|
-
|
|
155
|
-
type Step = 'choose' | 'generate' | 'restore' | 'connecting'
|
|
156
|
-
const step = ref<Step>('choose')
|
|
157
|
-
|
|
158
|
-
// Generate state
|
|
159
|
-
const generatedMnemonic = ref('')
|
|
160
|
-
const generatedWords = ref<string[]>([])
|
|
161
|
-
const backupConfirmed = ref(false)
|
|
162
|
-
|
|
163
|
-
// Restore state
|
|
164
|
-
const restorePhrase = ref('')
|
|
165
|
-
const restoreValid = ref(false)
|
|
166
|
-
|
|
167
|
-
async function startGenerate() {
|
|
168
|
-
const { generateMnemonic, english } = await import('viem/accounts')
|
|
169
|
-
generatedMnemonic.value = generateMnemonic(english)
|
|
170
|
-
generatedWords.value = generatedMnemonic.value.split(' ')
|
|
171
|
-
backupConfirmed.value = false
|
|
172
|
-
step.value = 'generate'
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
async function connectWithMnemonic(mnemonic: string) {
|
|
176
|
-
await prepareInAppWallet(mnemonic)
|
|
177
|
-
await connectAsync({ connector: inAppConnector.value! })
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
async function confirmGenerated() {
|
|
181
|
-
step.value = 'connecting'
|
|
182
|
-
try {
|
|
183
|
-
await connectWithMnemonic(generatedMnemonic.value)
|
|
184
|
-
emit('connected')
|
|
185
|
-
} catch (e) {
|
|
186
|
-
console.error('Failed to connect in-app wallet:', e)
|
|
187
|
-
step.value = 'generate'
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
async function restoreWallet() {
|
|
192
|
-
if (!restoreValid.value) return
|
|
193
|
-
step.value = 'connecting'
|
|
194
|
-
try {
|
|
195
|
-
await connectWithMnemonic(restorePhrase.value)
|
|
196
|
-
emit('connected')
|
|
197
|
-
} catch (e) {
|
|
198
|
-
console.error('Failed to restore in-app wallet:', e)
|
|
199
|
-
step.value = 'restore'
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
</script>
|
|
203
|
-
|
|
204
|
-
<style scoped>
|
|
205
|
-
.in-app-wallet-setup {
|
|
206
|
-
display: grid;
|
|
207
|
-
gap: var(--spacer);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
.setup-step {
|
|
211
|
-
display: grid;
|
|
212
|
-
gap: var(--spacer);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
.setup-options {
|
|
216
|
-
display: grid;
|
|
217
|
-
gap: var(--spacer);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
.generated-words {
|
|
221
|
-
display: grid;
|
|
222
|
-
grid-template-columns: repeat(3, 1fr);
|
|
223
|
-
gap: var(--spacer-sm);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
.generated-word {
|
|
227
|
-
display: flex;
|
|
228
|
-
align-items: center;
|
|
229
|
-
gap: var(--spacer-sm);
|
|
230
|
-
border: var(--border);
|
|
231
|
-
border-radius: var(--border-radius);
|
|
232
|
-
padding: var(--spacer-sm);
|
|
233
|
-
|
|
234
|
-
.word-number {
|
|
235
|
-
font-size: var(--font-xs);
|
|
236
|
-
color: var(--muted);
|
|
237
|
-
min-width: 1.5em;
|
|
238
|
-
text-align: right;
|
|
239
|
-
user-select: none;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
.word-text {
|
|
243
|
-
font-size: var(--font-sm);
|
|
244
|
-
font-family: var(--font-mono, monospace);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
.link.muted {
|
|
249
|
-
justify-self: center;
|
|
250
|
-
}
|
|
251
|
-
</style>
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<EvmConnectorQR :uri="uri">
|
|
3
|
-
<template #instruction>
|
|
4
|
-
Scan the code in your MetaMask mobile app
|
|
5
|
-
</template>
|
|
6
|
-
</EvmConnectorQR>
|
|
7
|
-
<Button
|
|
8
|
-
class="link muted small"
|
|
9
|
-
@click="$emit('back')"
|
|
10
|
-
>
|
|
11
|
-
<Icon type="chevron-left" />
|
|
12
|
-
<span>Back</span>
|
|
13
|
-
</Button>
|
|
14
|
-
</template>
|
|
15
|
-
|
|
16
|
-
<script setup lang="ts">
|
|
17
|
-
import EvmConnectorQR from './EvmConnectorQR.vue'
|
|
18
|
-
import Button from '../../base/components/Button.vue'
|
|
19
|
-
import Icon from '../../base/components/Icon.vue'
|
|
20
|
-
|
|
21
|
-
defineProps<{
|
|
22
|
-
uri: string
|
|
23
|
-
}>()
|
|
24
|
-
|
|
25
|
-
defineEmits<{
|
|
26
|
-
back: []
|
|
27
|
-
}>()
|
|
28
|
-
</script>
|
|
29
|
-
|
|
30
|
-
<style scoped>
|
|
31
|
-
.link.muted {
|
|
32
|
-
justify-self: center;
|
|
33
|
-
}
|
|
34
|
-
</style>
|
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<Button
|
|
3
|
-
@click="dialogOpen = true"
|
|
4
|
-
:class="className"
|
|
5
|
-
>
|
|
6
|
-
<slot
|
|
7
|
-
:address="address"
|
|
8
|
-
:display="display"
|
|
9
|
-
:ens-name="ensName"
|
|
10
|
-
:ens-avatar="ensAvatar"
|
|
11
|
-
>
|
|
12
|
-
{{ display }}
|
|
13
|
-
</slot>
|
|
14
|
-
</Button>
|
|
15
|
-
|
|
16
|
-
<Dialog
|
|
17
|
-
v-model:open="dialogOpen"
|
|
18
|
-
class="evm-profile"
|
|
19
|
-
title="Account"
|
|
20
|
-
compat
|
|
21
|
-
>
|
|
22
|
-
<div class="profile-header">
|
|
23
|
-
<div
|
|
24
|
-
class="banner"
|
|
25
|
-
:style="
|
|
26
|
-
ensHeader ? { backgroundImage: `url(${ensHeader})` } : undefined
|
|
27
|
-
"
|
|
28
|
-
/>
|
|
29
|
-
<div class="avatar-wrapper">
|
|
30
|
-
<EvmAvatar
|
|
31
|
-
:address="address"
|
|
32
|
-
large
|
|
33
|
-
/>
|
|
34
|
-
</div>
|
|
35
|
-
</div>
|
|
36
|
-
|
|
37
|
-
<div class="profile-identity">
|
|
38
|
-
<strong v-if="ensName">{{ ensName }}</strong>
|
|
39
|
-
<Button
|
|
40
|
-
class="link muted small"
|
|
41
|
-
@click="copyAddress"
|
|
42
|
-
>
|
|
43
|
-
<span>{{ shortAddr }}</span>
|
|
44
|
-
<Icon :type="copied ? 'check' : 'copy'" />
|
|
45
|
-
</Button>
|
|
46
|
-
</div>
|
|
47
|
-
|
|
48
|
-
<div class="profile-actions">
|
|
49
|
-
<slot name="actions" />
|
|
50
|
-
|
|
51
|
-
<EvmSwitchNetwork class-name="block">
|
|
52
|
-
<template #default="{ currentChain }">
|
|
53
|
-
<Icon type="wallet" />
|
|
54
|
-
<span>Switch Network ({{ currentChain?.name || 'Unknown' }})</span>
|
|
55
|
-
</template>
|
|
56
|
-
</EvmSwitchNetwork>
|
|
57
|
-
|
|
58
|
-
<Button
|
|
59
|
-
v-if="ensName"
|
|
60
|
-
class="block"
|
|
61
|
-
:to="`https://app.ens.domains/${ensName}`"
|
|
62
|
-
target="_blank"
|
|
63
|
-
>
|
|
64
|
-
<Icon type="link" />
|
|
65
|
-
<span>Manage ENS</span>
|
|
66
|
-
</Button>
|
|
67
|
-
|
|
68
|
-
<Button
|
|
69
|
-
class="block danger"
|
|
70
|
-
@click="disconnect"
|
|
71
|
-
>
|
|
72
|
-
<span>Disconnect</span>
|
|
73
|
-
</Button>
|
|
74
|
-
</div>
|
|
75
|
-
</Dialog>
|
|
76
|
-
</template>
|
|
77
|
-
|
|
78
|
-
<script setup lang="ts">
|
|
79
|
-
import { ref, computed, nextTick } from 'vue'
|
|
80
|
-
import { useConnection, useDisconnect } from '@wagmi/vue'
|
|
81
|
-
import { useClipboard } from '@vueuse/core'
|
|
82
|
-
import { useConfirm } from '../../base/composables/confirm'
|
|
83
|
-
import { useEnsProfile } from '../composables/ens'
|
|
84
|
-
import { useResolveUri } from '../composables/uri'
|
|
85
|
-
import { shortAddress } from '../utils/addresses'
|
|
86
|
-
import Button from '../../base/components/Button.vue'
|
|
87
|
-
import Dialog from '../../base/components/Dialog.vue'
|
|
88
|
-
import Icon from '../../base/components/Icon.vue'
|
|
89
|
-
import EvmAvatar from './EvmAvatar.vue'
|
|
90
|
-
import EvmSwitchNetwork from './EvmSwitchNetwork.vue'
|
|
91
|
-
|
|
92
|
-
defineProps<{
|
|
93
|
-
className?: string
|
|
94
|
-
}>()
|
|
95
|
-
|
|
96
|
-
const emit = defineEmits<{
|
|
97
|
-
disconnected: []
|
|
98
|
-
}>()
|
|
99
|
-
|
|
100
|
-
const { address } = useConnection()
|
|
101
|
-
const { mutate: disconnectWallet } = useDisconnect()
|
|
102
|
-
const { confirm } = useConfirm()
|
|
103
|
-
const { data: ensData } = useEnsProfile(address)
|
|
104
|
-
|
|
105
|
-
const { copy, copied } = useClipboard()
|
|
106
|
-
const resolve = useResolveUri()
|
|
107
|
-
|
|
108
|
-
const ensName = computed(() => ensData.value?.ens || null)
|
|
109
|
-
const ensAvatar = computed(() => resolve(ensData.value?.data?.avatar))
|
|
110
|
-
const ensHeader = computed(() => resolve(ensData.value?.data?.header))
|
|
111
|
-
|
|
112
|
-
const shortAddr = computed(() =>
|
|
113
|
-
address.value ? shortAddress(address.value) : '',
|
|
114
|
-
)
|
|
115
|
-
|
|
116
|
-
const display = computed(() => ensName.value || shortAddr.value)
|
|
117
|
-
|
|
118
|
-
const dialogOpen = ref(false)
|
|
119
|
-
|
|
120
|
-
const copyAddress = () => {
|
|
121
|
-
if (address.value) copy(address.value)
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const disconnect = async () => {
|
|
125
|
-
const confirmed = await confirm({
|
|
126
|
-
title: 'Disconnect Wallet',
|
|
127
|
-
description: 'Are you sure you want to disconnect your wallet?',
|
|
128
|
-
okText: 'Disconnect',
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
if (!confirmed) return
|
|
132
|
-
|
|
133
|
-
dialogOpen.value = false
|
|
134
|
-
await nextTick()
|
|
135
|
-
disconnectWallet()
|
|
136
|
-
emit('disconnected')
|
|
137
|
-
}
|
|
138
|
-
</script>
|
|
139
|
-
|
|
140
|
-
<style scoped>
|
|
141
|
-
.profile-header {
|
|
142
|
-
position: relative;
|
|
143
|
-
margin: calc(var(--spacer) * -3) calc(var(--spacer) * -1) 0;
|
|
144
|
-
margin-bottom: 0;
|
|
145
|
-
z-index: -1;
|
|
146
|
-
|
|
147
|
-
.banner {
|
|
148
|
-
aspect-ratio: 3 / 1;
|
|
149
|
-
width: 100%;
|
|
150
|
-
height: auto;
|
|
151
|
-
background-color: var(--gray-z-1);
|
|
152
|
-
background-size: cover;
|
|
153
|
-
background-position: center;
|
|
154
|
-
border-bottom: var(--border);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
.avatar-wrapper {
|
|
158
|
-
display: flex;
|
|
159
|
-
justify-content: center;
|
|
160
|
-
margin-top: -10%;
|
|
161
|
-
position: relative;
|
|
162
|
-
z-index: 1;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
:deep(.evm-avatar) {
|
|
166
|
-
border: 3px solid var(--background);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
.profile-identity {
|
|
171
|
-
display: grid;
|
|
172
|
-
justify-items: center;
|
|
173
|
-
gap: var(--spacer-sm);
|
|
174
|
-
|
|
175
|
-
> strong {
|
|
176
|
-
font-size: var(--font-lg);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
.profile-actions {
|
|
181
|
-
display: grid;
|
|
182
|
-
gap: var(--spacer);
|
|
183
|
-
padding-block-start: var(--spacer);
|
|
184
|
-
|
|
185
|
-
}
|
|
186
|
-
</style>
|
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="seed-phrase-input">
|
|
3
|
-
<div class="seed-phrase-grid">
|
|
4
|
-
<div
|
|
5
|
-
v-for="(_, i) in 12"
|
|
6
|
-
:key="i"
|
|
7
|
-
class="seed-word"
|
|
8
|
-
:class="{ invalid: words[i] && !isValidWord(words[i]) }"
|
|
9
|
-
>
|
|
10
|
-
<label :for="`seed-word-${i}`">{{ i + 1 }}</label>
|
|
11
|
-
<input
|
|
12
|
-
:id="`seed-word-${i}`"
|
|
13
|
-
:ref="
|
|
14
|
-
(el) => {
|
|
15
|
-
if (el) inputRefs[i] = el as HTMLInputElement
|
|
16
|
-
}
|
|
17
|
-
"
|
|
18
|
-
v-model="words[i]"
|
|
19
|
-
type="text"
|
|
20
|
-
autocomplete="off"
|
|
21
|
-
autocapitalize="none"
|
|
22
|
-
spellcheck="false"
|
|
23
|
-
:disabled="disabled"
|
|
24
|
-
@keydown="onKeydown($event, i)"
|
|
25
|
-
@paste="onPaste($event, i)"
|
|
26
|
-
@input="onInput(i)"
|
|
27
|
-
/>
|
|
28
|
-
</div>
|
|
29
|
-
</div>
|
|
30
|
-
</div>
|
|
31
|
-
</template>
|
|
32
|
-
|
|
33
|
-
<script setup lang="ts">
|
|
34
|
-
import { ref, watch, computed, onMounted } from 'vue'
|
|
35
|
-
import { english } from 'viem/accounts'
|
|
36
|
-
|
|
37
|
-
const props = defineProps<{
|
|
38
|
-
modelValue?: string
|
|
39
|
-
disabled?: boolean
|
|
40
|
-
}>()
|
|
41
|
-
|
|
42
|
-
const emit = defineEmits<{
|
|
43
|
-
'update:modelValue': [value: string]
|
|
44
|
-
valid: [isValid: boolean]
|
|
45
|
-
submit: []
|
|
46
|
-
}>()
|
|
47
|
-
|
|
48
|
-
const wordSet = new Set(english)
|
|
49
|
-
|
|
50
|
-
const words = ref<string[]>(Array.from({ length: 12 }, () => ''))
|
|
51
|
-
const inputRefs = ref<HTMLInputElement[]>([])
|
|
52
|
-
|
|
53
|
-
function isValidWord(word: string): boolean {
|
|
54
|
-
return wordSet.has(word.trim().toLowerCase())
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const isValid = computed(() =>
|
|
58
|
-
words.value.every((w) => w.trim() !== '' && isValidWord(w)),
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
const phrase = computed(() =>
|
|
62
|
-
words.value.map((w) => w.trim().toLowerCase()).join(' '),
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
watch(
|
|
66
|
-
() => props.modelValue,
|
|
67
|
-
(val) => {
|
|
68
|
-
if (!val) return
|
|
69
|
-
const incoming = val.trim().split(/\s+/)
|
|
70
|
-
if (incoming.length === 12) {
|
|
71
|
-
for (let i = 0; i < 12; i++) {
|
|
72
|
-
words.value[i] = incoming[i]!
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
},
|
|
76
|
-
{ immediate: true },
|
|
77
|
-
)
|
|
78
|
-
|
|
79
|
-
watch(phrase, (val) => {
|
|
80
|
-
emit('update:modelValue', val)
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
watch(isValid, (val) => {
|
|
84
|
-
emit('valid', val)
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
function focusInput(index: number) {
|
|
88
|
-
const el = inputRefs.value[index]
|
|
89
|
-
if (el) {
|
|
90
|
-
el.focus()
|
|
91
|
-
el.select()
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function onKeydown(event: KeyboardEvent, index: number) {
|
|
96
|
-
if (event.key === ' ' || (event.key === 'Enter' && index < 11)) {
|
|
97
|
-
event.preventDefault()
|
|
98
|
-
focusInput(index + 1)
|
|
99
|
-
} else if (event.key === 'Enter' && index === 11 && isValid.value) {
|
|
100
|
-
event.preventDefault()
|
|
101
|
-
emit('submit')
|
|
102
|
-
} else if (
|
|
103
|
-
event.key === 'Backspace' &&
|
|
104
|
-
words.value[index] === '' &&
|
|
105
|
-
index > 0
|
|
106
|
-
) {
|
|
107
|
-
event.preventDefault()
|
|
108
|
-
focusInput(index - 1)
|
|
109
|
-
} else if (event.key === 'Tab' && !event.shiftKey && index === 11) {
|
|
110
|
-
// Allow natural tab out
|
|
111
|
-
} else if (event.key === 'Tab' && event.shiftKey && index === 0) {
|
|
112
|
-
// Allow natural tab out
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function onPaste(event: ClipboardEvent, index: number) {
|
|
117
|
-
const text = event.clipboardData?.getData('text')
|
|
118
|
-
if (!text) return
|
|
119
|
-
|
|
120
|
-
const pasted = text.trim().split(/\s+/)
|
|
121
|
-
if (pasted.length > 1) {
|
|
122
|
-
event.preventDefault()
|
|
123
|
-
for (let i = 0; i < pasted.length && index + i < 12; i++) {
|
|
124
|
-
words.value[index + i] = pasted[i]!.toLowerCase()
|
|
125
|
-
}
|
|
126
|
-
const nextIndex = Math.min(index + pasted.length, 11)
|
|
127
|
-
focusInput(nextIndex)
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function onInput(index: number) {
|
|
132
|
-
// Auto-advance if the word contains a space (mobile autocomplete)
|
|
133
|
-
const val = words.value[index]
|
|
134
|
-
if (val?.includes(' ')) {
|
|
135
|
-
const parts = val.trim().split(/\s+/)
|
|
136
|
-
words.value[index] = parts[0]!
|
|
137
|
-
if (parts.length > 1 && index < 11) {
|
|
138
|
-
for (let i = 1; i < parts.length && index + i < 12; i++) {
|
|
139
|
-
words.value[index + i] = parts[i]!
|
|
140
|
-
}
|
|
141
|
-
focusInput(Math.min(index + parts.length, 11))
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
onMounted(() => {
|
|
147
|
-
if (!props.disabled) {
|
|
148
|
-
focusInput(0)
|
|
149
|
-
}
|
|
150
|
-
})
|
|
151
|
-
</script>
|
|
152
|
-
|
|
153
|
-
<style scoped>
|
|
154
|
-
.seed-phrase-grid {
|
|
155
|
-
display: grid;
|
|
156
|
-
grid-template-columns: repeat(3, 1fr);
|
|
157
|
-
gap: var(--spacer-sm);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
.seed-word {
|
|
162
|
-
display: flex;
|
|
163
|
-
align-items: center;
|
|
164
|
-
gap: var(--spacer-sm);
|
|
165
|
-
border: var(--border);
|
|
166
|
-
border-radius: var(--border-radius);
|
|
167
|
-
padding: var(--spacer-sm);
|
|
168
|
-
transition: border-color var(--speed);
|
|
169
|
-
|
|
170
|
-
&:focus-within {
|
|
171
|
-
border-color: var(--accent);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
&.invalid {
|
|
175
|
-
border-color: var(--error);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
label {
|
|
179
|
-
font-size: var(--font-xs);
|
|
180
|
-
color: var(--muted);
|
|
181
|
-
min-width: 1.5em;
|
|
182
|
-
text-align: right;
|
|
183
|
-
user-select: none;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
input {
|
|
187
|
-
all: unset;
|
|
188
|
-
width: 100%;
|
|
189
|
-
font-size: var(--font-sm);
|
|
190
|
-
font-family: var(--font-mono, monospace);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
</style>
|