@1001-digital/components 0.0.1
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 +45 -0
- package/src/base/components/Actions.vue +57 -0
- package/src/base/components/Alert.vue +90 -0
- package/src/base/components/Button.vue +260 -0
- package/src/base/components/Card.vue +78 -0
- package/src/base/components/CardLink.vue +56 -0
- package/src/base/components/Dialog.vue +274 -0
- package/src/base/components/Dropdown.vue +167 -0
- package/src/base/components/DropdownCheckboxItem.vue +30 -0
- package/src/base/components/DropdownGroup.vue +9 -0
- package/src/base/components/DropdownItem.vue +23 -0
- package/src/base/components/DropdownLabel.vue +9 -0
- package/src/base/components/DropdownRadioGroup.vue +15 -0
- package/src/base/components/DropdownRadioItem.vue +29 -0
- package/src/base/components/DropdownSeparator.vue +7 -0
- package/src/base/components/DropdownSub.vue +58 -0
- package/src/base/components/Form.vue +27 -0
- package/src/base/components/FormCheckbox.vue +92 -0
- package/src/base/components/FormGroup.vue +39 -0
- package/src/base/components/FormInputGroup.vue +55 -0
- package/src/base/components/FormItem.vue +89 -0
- package/src/base/components/FormLabel.vue +39 -0
- package/src/base/components/FormRadioGroup.vue +118 -0
- package/src/base/components/FormSelect.vue +160 -0
- package/src/base/components/FormTextarea.vue +38 -0
- package/src/base/components/Icon.vue +29 -0
- package/src/base/components/Loading.vue +81 -0
- package/src/base/components/Popover.vue +182 -0
- package/src/base/components/Tag.vue +56 -0
- package/src/base/components/Tags.vue +13 -0
- package/src/base/components/Toasts.vue +254 -0
- package/src/base/components/Tooltip.vue +100 -0
- package/src/base/composables/time.ts +82 -0
- package/src/base/composables/toast.ts +40 -0
- package/src/base/icons.ts +20 -0
- package/src/base/link.ts +4 -0
- package/src/base/utils/format-number.ts +29 -0
- package/src/base/utils/time.ts +20 -0
- package/src/evm/components/EvmAccount.vue +28 -0
- package/src/evm/components/EvmConnect.vue +254 -0
- package/src/evm/components/EvmConnectorQR.vue +116 -0
- package/src/evm/components/EvmMetaMaskQR.vue +15 -0
- package/src/evm/components/EvmTransactionFlow.vue +327 -0
- package/src/evm/components/EvmWalletConnectQR.vue +13 -0
- package/src/evm/composables/base.ts +7 -0
- package/src/evm/composables/chainId.ts +41 -0
- package/src/evm/config.ts +32 -0
- package/src/evm/index.ts +25 -0
- package/src/evm/utils/addresses.ts +6 -0
- package/src/evm/utils/chains.ts +32 -0
- package/src/evm/utils/format-eth.ts +15 -0
- package/src/index.ts +68 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { ref, computed, watch } from 'vue'
|
|
2
|
+
import type { Ref } from 'vue'
|
|
3
|
+
import { DateTime } from 'luxon'
|
|
4
|
+
import { nowInSeconds } from '../utils/time'
|
|
5
|
+
|
|
6
|
+
let nowInterval: ReturnType<typeof setInterval> | undefined
|
|
7
|
+
|
|
8
|
+
const now = ref<number>(nowInSeconds())
|
|
9
|
+
|
|
10
|
+
export const useSeconds = () => {
|
|
11
|
+
if (typeof window !== 'undefined' && !nowInterval) {
|
|
12
|
+
nowInterval = setInterval(() => {
|
|
13
|
+
now.value = nowInSeconds()
|
|
14
|
+
}, 1000)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return now
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const useCountDown = (
|
|
21
|
+
s: Ref<number | bigint>,
|
|
22
|
+
showSecondsWithin: number = 60,
|
|
23
|
+
) => {
|
|
24
|
+
const duration = computed(() => Math.abs(Number(s.value)))
|
|
25
|
+
|
|
26
|
+
const seconds = computed(() => duration.value % 60)
|
|
27
|
+
const minutes = computed(() => Math.floor(duration.value / 60) % 60)
|
|
28
|
+
const hours = computed(() => Math.floor(duration.value / 60 / 60) % 24)
|
|
29
|
+
const days = computed(() => Math.floor(duration.value / 60 / 60 / 24))
|
|
30
|
+
|
|
31
|
+
const str = computed(() =>
|
|
32
|
+
[
|
|
33
|
+
days.value ? `${days.value}d` : null,
|
|
34
|
+
hours.value ? `${hours.value}h` : null,
|
|
35
|
+
minutes.value ? `${minutes.value}m` : null,
|
|
36
|
+
duration.value < showSecondsWithin && seconds.value
|
|
37
|
+
? `${seconds.value}s`
|
|
38
|
+
: null,
|
|
39
|
+
]
|
|
40
|
+
.filter((s) => !!s)
|
|
41
|
+
.join(' '),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
seconds,
|
|
46
|
+
minutes,
|
|
47
|
+
hours,
|
|
48
|
+
days,
|
|
49
|
+
str,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const useTimeAgo = (time: Ref<string | undefined>) => {
|
|
54
|
+
const ago = ref<string>()
|
|
55
|
+
const nowRef = useSeconds()
|
|
56
|
+
|
|
57
|
+
watch(
|
|
58
|
+
nowRef,
|
|
59
|
+
() => {
|
|
60
|
+
if (time.value) {
|
|
61
|
+
ago.value =
|
|
62
|
+
DateTime.fromISO(time.value).toRelative({
|
|
63
|
+
style: 'short',
|
|
64
|
+
locale: 'en',
|
|
65
|
+
}) ?? undefined
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
immediate: true,
|
|
70
|
+
},
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
return ago
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** @deprecated Use `useTimeAgo` instead. */
|
|
77
|
+
export const useSecondsAgo = (...args: Parameters<typeof useTimeAgo>) => {
|
|
78
|
+
console.warn(
|
|
79
|
+
'[deprecated] useSecondsAgo is deprecated, use useTimeAgo instead.',
|
|
80
|
+
)
|
|
81
|
+
return useTimeAgo(...args)
|
|
82
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
export interface ToastAction {
|
|
4
|
+
label: string
|
|
5
|
+
onClick: () => void
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type ToastVariant = 'info' | 'success' | 'error'
|
|
9
|
+
|
|
10
|
+
export interface Toast {
|
|
11
|
+
id: string
|
|
12
|
+
title?: string
|
|
13
|
+
description?: string
|
|
14
|
+
variant?: ToastVariant
|
|
15
|
+
action?: ToastAction
|
|
16
|
+
duration?: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const toasts = ref<Toast[]>([])
|
|
20
|
+
|
|
21
|
+
export const useToast = () => {
|
|
22
|
+
const add = (toast: Omit<Toast, 'id'>) => {
|
|
23
|
+
const id = crypto.randomUUID()
|
|
24
|
+
toasts.value.push({ ...toast, id })
|
|
25
|
+
return id
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const update = (id: string, partial: Partial<Omit<Toast, 'id'>>) => {
|
|
29
|
+
const index = toasts.value.findIndex((t) => t.id === id)
|
|
30
|
+
if (index !== -1) {
|
|
31
|
+
toasts.value[index] = { ...toasts.value[index], ...partial, id }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const dismiss = (id: string) => {
|
|
36
|
+
toasts.value = toasts.value.filter((t) => t.id !== id)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return { toasts, add, update, dismiss }
|
|
40
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { InjectionKey } from 'vue'
|
|
2
|
+
|
|
3
|
+
export type IconAliases = Record<string, string>
|
|
4
|
+
|
|
5
|
+
export const IconAliasesKey: InjectionKey<IconAliases> = Symbol('IconAliases')
|
|
6
|
+
|
|
7
|
+
export const defaultIconAliases: IconAliases = {
|
|
8
|
+
add: 'lucide:plus',
|
|
9
|
+
check: 'lucide:check',
|
|
10
|
+
close: 'lucide:x',
|
|
11
|
+
'chevron-down': 'lucide:chevron-down',
|
|
12
|
+
'chevron-right': 'lucide:chevron-right',
|
|
13
|
+
copy: 'lucide:copy',
|
|
14
|
+
edit: 'lucide:pencil',
|
|
15
|
+
help: 'lucide:circle-question-mark',
|
|
16
|
+
home: 'lucide:house',
|
|
17
|
+
link: 'lucide:link',
|
|
18
|
+
loader: 'lucide:loader-2',
|
|
19
|
+
wallet: 'lucide:wallet',
|
|
20
|
+
}
|
package/src/base/link.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export const formatNumber = (num: number) => num?.toLocaleString('en-US')
|
|
2
|
+
|
|
3
|
+
export const roundAndFormatNumber = (num: number, decimals: number = 2) => {
|
|
4
|
+
const multiplier = Math.pow(10, decimals)
|
|
5
|
+
const rounded = Math.round(num * multiplier) / multiplier
|
|
6
|
+
return formatNumber(rounded === num ? num : rounded)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const asPercentageOf = (num: number = 0, base: number = 1) => {
|
|
10
|
+
return formatNumber(Math.round((num / base) * 100))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function formatUSD(
|
|
14
|
+
value: string | number,
|
|
15
|
+
fractionDigits: number = 0,
|
|
16
|
+
): string {
|
|
17
|
+
const numberValue = typeof value === 'string' ? parseFloat(value) : value
|
|
18
|
+
|
|
19
|
+
if (isNaN(numberValue)) {
|
|
20
|
+
throw new Error('Invalid number input')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return new Intl.NumberFormat('en-US', {
|
|
24
|
+
style: 'currency',
|
|
25
|
+
currency: 'USD',
|
|
26
|
+
minimumFractionDigits: fractionDigits,
|
|
27
|
+
maximumFractionDigits: fractionDigits,
|
|
28
|
+
}).format(numberValue)
|
|
29
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { DateTime } from 'luxon'
|
|
2
|
+
|
|
3
|
+
export const delay = (ms: number): Promise<void> =>
|
|
4
|
+
new Promise((resolve) => setTimeout(resolve, ms))
|
|
5
|
+
|
|
6
|
+
export const daysInSeconds = (days: number): number => days * 60 * 60 * 24
|
|
7
|
+
|
|
8
|
+
export const nowInSeconds = (): number => Math.floor(Date.now() / 1000)
|
|
9
|
+
|
|
10
|
+
export const asUTCDate = (date: Date | null) =>
|
|
11
|
+
date
|
|
12
|
+
? DateTime.utc(
|
|
13
|
+
date.getFullYear(),
|
|
14
|
+
date.getMonth() + 1,
|
|
15
|
+
date.getDate(),
|
|
16
|
+
date.getHours(),
|
|
17
|
+
date.getMinutes(),
|
|
18
|
+
date.getSeconds(),
|
|
19
|
+
)
|
|
20
|
+
: null
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<slot
|
|
3
|
+
:display="display"
|
|
4
|
+
:is-current="isCurrent"
|
|
5
|
+
>
|
|
6
|
+
<span>{{ display }}</span>
|
|
7
|
+
</slot>
|
|
8
|
+
</template>
|
|
9
|
+
|
|
10
|
+
<script setup lang="ts">
|
|
11
|
+
import { computed } from 'vue'
|
|
12
|
+
import type { Address } from 'viem'
|
|
13
|
+
import { useConnection } from '@wagmi/vue'
|
|
14
|
+
import { shortAddress } from '../utils/addresses'
|
|
15
|
+
|
|
16
|
+
const props = defineProps<{
|
|
17
|
+
address?: Address
|
|
18
|
+
}>()
|
|
19
|
+
const address = computed(() => props.address)
|
|
20
|
+
|
|
21
|
+
const { address: currentAddress } = useConnection()
|
|
22
|
+
|
|
23
|
+
const isCurrent = computed<boolean>(
|
|
24
|
+
() => currentAddress.value?.toLowerCase() === address.value?.toLowerCase(),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
const display = computed<string>(() => shortAddress(address.value!))
|
|
28
|
+
</script>
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Button
|
|
3
|
+
v-if="showConnect"
|
|
4
|
+
@click="chooseModalOpen = true"
|
|
5
|
+
:class="className"
|
|
6
|
+
>
|
|
7
|
+
<slot>Connect Wallet</slot>
|
|
8
|
+
</Button>
|
|
9
|
+
<slot
|
|
10
|
+
v-else
|
|
11
|
+
name="connected"
|
|
12
|
+
:address="address"
|
|
13
|
+
>
|
|
14
|
+
<EvmAccount :address="address" />
|
|
15
|
+
</slot>
|
|
16
|
+
|
|
17
|
+
<Dialog
|
|
18
|
+
v-if="showConnect"
|
|
19
|
+
title="Connect Wallet"
|
|
20
|
+
v-model:open="chooseModalOpen"
|
|
21
|
+
@closed="onModalClosed"
|
|
22
|
+
>
|
|
23
|
+
<Alert
|
|
24
|
+
v-if="errorMessage"
|
|
25
|
+
type="error"
|
|
26
|
+
>
|
|
27
|
+
{{ errorMessage }}
|
|
28
|
+
</Alert>
|
|
29
|
+
<EvmWalletConnectQR
|
|
30
|
+
v-if="walletConnectUri"
|
|
31
|
+
:uri="walletConnectUri"
|
|
32
|
+
/>
|
|
33
|
+
<EvmMetaMaskQR
|
|
34
|
+
v-else-if="metaMaskUri"
|
|
35
|
+
:uri="metaMaskUri"
|
|
36
|
+
/>
|
|
37
|
+
<template v-else-if="isConnecting">
|
|
38
|
+
<Loading
|
|
39
|
+
txt="Waiting for wallet confirmation..."
|
|
40
|
+
spinner
|
|
41
|
+
stacked
|
|
42
|
+
/>
|
|
43
|
+
</template>
|
|
44
|
+
<div
|
|
45
|
+
v-else
|
|
46
|
+
class="wallet-options"
|
|
47
|
+
>
|
|
48
|
+
<Button
|
|
49
|
+
v-for="connector in shownConnectors"
|
|
50
|
+
:key="connector.uid"
|
|
51
|
+
@click="() => login(connector)"
|
|
52
|
+
class="choose-connector"
|
|
53
|
+
>
|
|
54
|
+
<img
|
|
55
|
+
v-if="ICONS[connector.name]"
|
|
56
|
+
:src="
|
|
57
|
+
connector.icon || `${base}icons/wallets/${ICONS[connector.name]}`
|
|
58
|
+
"
|
|
59
|
+
:alt="connector.name"
|
|
60
|
+
/>
|
|
61
|
+
<div
|
|
62
|
+
v-else
|
|
63
|
+
class="default-wallet-icon"
|
|
64
|
+
>
|
|
65
|
+
<Icon type="wallet" />
|
|
66
|
+
</div>
|
|
67
|
+
<span>{{ connector.name }}</span>
|
|
68
|
+
</Button>
|
|
69
|
+
<Button
|
|
70
|
+
to="https://ethereum.org/wallets/"
|
|
71
|
+
target="_blank"
|
|
72
|
+
class="link muted small"
|
|
73
|
+
>
|
|
74
|
+
<Icon type="help" />
|
|
75
|
+
<span>New to wallets?</span>
|
|
76
|
+
</Button>
|
|
77
|
+
</div>
|
|
78
|
+
</Dialog>
|
|
79
|
+
</template>
|
|
80
|
+
|
|
81
|
+
<script setup lang="ts">
|
|
82
|
+
import { ref, computed, watch, onMounted } from 'vue'
|
|
83
|
+
import type { Connector } from '@wagmi/vue'
|
|
84
|
+
import {
|
|
85
|
+
useConnection,
|
|
86
|
+
useConnect,
|
|
87
|
+
useConnectors,
|
|
88
|
+
useChainId,
|
|
89
|
+
} from '@wagmi/vue'
|
|
90
|
+
import Button from '../../base/components/Button.vue'
|
|
91
|
+
import Dialog from '../../base/components/Dialog.vue'
|
|
92
|
+
import Icon from '../../base/components/Icon.vue'
|
|
93
|
+
import Alert from '../../base/components/Alert.vue'
|
|
94
|
+
import Loading from '../../base/components/Loading.vue'
|
|
95
|
+
import EvmAccount from './EvmAccount.vue'
|
|
96
|
+
import EvmWalletConnectQR from './EvmWalletConnectQR.vue'
|
|
97
|
+
import EvmMetaMaskQR from './EvmMetaMaskQR.vue'
|
|
98
|
+
import { useBaseURL } from '../composables/base'
|
|
99
|
+
|
|
100
|
+
const ICONS: Record<string, string> = {
|
|
101
|
+
'Coinbase Wallet': 'coinbase.svg',
|
|
102
|
+
MetaMask: 'metamask.svg',
|
|
103
|
+
Phantom: 'phantom.svg',
|
|
104
|
+
'Rabby Wallet': 'rabby.svg',
|
|
105
|
+
Rainbow: 'rainbow.svg',
|
|
106
|
+
WalletConnect: 'walletconnect.svg',
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const PRIORITY: Record<string, number> = {
|
|
110
|
+
WalletConnect: 20,
|
|
111
|
+
'Coinbase Wallet': 10,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
defineProps<{
|
|
115
|
+
className?: string
|
|
116
|
+
}>()
|
|
117
|
+
const emit = defineEmits<{
|
|
118
|
+
connected: [{ address: `0x${string}` | undefined }]
|
|
119
|
+
disconnected: []
|
|
120
|
+
}>()
|
|
121
|
+
const base = useBaseURL()
|
|
122
|
+
|
|
123
|
+
const chainId = useChainId()
|
|
124
|
+
const connectors = useConnectors()
|
|
125
|
+
const { mutateAsync: connectAsync } = useConnect()
|
|
126
|
+
const { address, isConnected } = useConnection()
|
|
127
|
+
|
|
128
|
+
const showConnect = computed(() => !isConnected.value)
|
|
129
|
+
const shownConnectors = computed(() => {
|
|
130
|
+
const unique = Array.from(
|
|
131
|
+
new Map(
|
|
132
|
+
connectors.value?.map((connector) => [connector.name, connector]),
|
|
133
|
+
).values(),
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
const filtered =
|
|
137
|
+
unique.length > 1 ? unique.filter((c) => c.id !== 'injected') : unique
|
|
138
|
+
|
|
139
|
+
return filtered.sort((a, b) => {
|
|
140
|
+
const priorityA = PRIORITY[a.name] ?? 5
|
|
141
|
+
const priorityB = PRIORITY[b.name] ?? 5
|
|
142
|
+
return priorityA - priorityB
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
const chooseModalOpen = ref(false)
|
|
147
|
+
const errorMessage = ref('')
|
|
148
|
+
const isConnecting = ref(false)
|
|
149
|
+
const walletConnectUri = ref('')
|
|
150
|
+
const metaMaskUri = ref('')
|
|
151
|
+
|
|
152
|
+
const login = async (connector: Connector) => {
|
|
153
|
+
errorMessage.value = ''
|
|
154
|
+
isConnecting.value = true
|
|
155
|
+
walletConnectUri.value = ''
|
|
156
|
+
metaMaskUri.value = ''
|
|
157
|
+
|
|
158
|
+
const handleMessage = (event: { type: string; data?: unknown }) => {
|
|
159
|
+
if (event.type === 'display_uri' && typeof event.data === 'string') {
|
|
160
|
+
if (connector.id === 'walletConnect') {
|
|
161
|
+
walletConnectUri.value = event.data
|
|
162
|
+
} else if (connector.id === 'metaMaskSDK') {
|
|
163
|
+
metaMaskUri.value = event.data
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (connector.id === 'walletConnect' || connector.id === 'metaMaskSDK') {
|
|
169
|
+
connector.emitter.on('message', handleMessage)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
await connectAsync({ connector, chainId: chainId.value })
|
|
174
|
+
|
|
175
|
+
setTimeout(() => {
|
|
176
|
+
chooseModalOpen.value = false
|
|
177
|
+
isConnecting.value = false
|
|
178
|
+
walletConnectUri.value = ''
|
|
179
|
+
metaMaskUri.value = ''
|
|
180
|
+
}, 100)
|
|
181
|
+
} catch (error: unknown) {
|
|
182
|
+
isConnecting.value = false
|
|
183
|
+
walletConnectUri.value = ''
|
|
184
|
+
metaMaskUri.value = ''
|
|
185
|
+
|
|
186
|
+
const errorMsg = error instanceof Error ? error.message : ''
|
|
187
|
+
if (
|
|
188
|
+
errorMsg.includes('User rejected') ||
|
|
189
|
+
errorMsg.includes('rejected') ||
|
|
190
|
+
errorMsg.includes('denied')
|
|
191
|
+
) {
|
|
192
|
+
errorMessage.value = 'Connection cancelled. Please try again.'
|
|
193
|
+
} else {
|
|
194
|
+
errorMessage.value = 'Failed to connect. Please try again.'
|
|
195
|
+
}
|
|
196
|
+
console.error('Wallet connection error:', error)
|
|
197
|
+
} finally {
|
|
198
|
+
if (connector.id === 'walletConnect' || connector.id === 'metaMaskSDK') {
|
|
199
|
+
connector.emitter.off('message', handleMessage)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const onModalClosed = () => {
|
|
205
|
+
errorMessage.value = ''
|
|
206
|
+
isConnecting.value = false
|
|
207
|
+
walletConnectUri.value = ''
|
|
208
|
+
metaMaskUri.value = ''
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const check = () =>
|
|
212
|
+
isConnected.value
|
|
213
|
+
? emit('connected', { address: address.value })
|
|
214
|
+
: emit('disconnected')
|
|
215
|
+
watch(isConnected, () => check())
|
|
216
|
+
onMounted(() => check())
|
|
217
|
+
</script>
|
|
218
|
+
|
|
219
|
+
<style scoped>
|
|
220
|
+
.wallet-options {
|
|
221
|
+
display: grid;
|
|
222
|
+
gap: var(--spacer);
|
|
223
|
+
|
|
224
|
+
button.choose-connector {
|
|
225
|
+
width: 100%;
|
|
226
|
+
inline-size: auto;
|
|
227
|
+
justify-content: flex-start;
|
|
228
|
+
|
|
229
|
+
img,
|
|
230
|
+
.default-wallet-icon {
|
|
231
|
+
margin: -1rem 0 -1rem -0.6rem;
|
|
232
|
+
width: var(--size-5);
|
|
233
|
+
height: var(--size-5);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.default-wallet-icon {
|
|
237
|
+
display: flex;
|
|
238
|
+
align-items: center;
|
|
239
|
+
justify-content: center;
|
|
240
|
+
background: var(--gray-z-2);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
span:last-child {
|
|
244
|
+
border-left: var(--border);
|
|
245
|
+
padding-left: var(--spacer-sm);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.link.muted {
|
|
251
|
+
justify-self: center;
|
|
252
|
+
font-size: var(--font-xs);
|
|
253
|
+
}
|
|
254
|
+
</style>
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<p>
|
|
3
|
+
<slot name="instruction">Scan the code in your wallet application</slot>
|
|
4
|
+
</p>
|
|
5
|
+
<div class="qr-frame">
|
|
6
|
+
<canvas ref="qrCanvas"></canvas>
|
|
7
|
+
</div>
|
|
8
|
+
<p class="uri-label">Or copy the connection URI:</p>
|
|
9
|
+
<div class="uri-display">
|
|
10
|
+
<code>{{ uri }}</code>
|
|
11
|
+
<Button
|
|
12
|
+
@click="copyUri"
|
|
13
|
+
class="copy-button"
|
|
14
|
+
:class="{ copied: isCopied }"
|
|
15
|
+
>
|
|
16
|
+
<Icon :type="isCopied ? 'check' : 'copy'" />
|
|
17
|
+
</Button>
|
|
18
|
+
</div>
|
|
19
|
+
</template>
|
|
20
|
+
|
|
21
|
+
<script setup lang="ts">
|
|
22
|
+
import { ref, watch, onMounted } from 'vue'
|
|
23
|
+
import QRCode from 'qrcode'
|
|
24
|
+
import { useClipboard } from '@vueuse/core'
|
|
25
|
+
import Button from '../../base/components/Button.vue'
|
|
26
|
+
import Icon from '../../base/components/Icon.vue'
|
|
27
|
+
|
|
28
|
+
const props = defineProps<{
|
|
29
|
+
uri: string
|
|
30
|
+
}>()
|
|
31
|
+
|
|
32
|
+
const qrCanvas = ref<HTMLCanvasElement | null>(null)
|
|
33
|
+
const { copy, copied: isCopied } = useClipboard()
|
|
34
|
+
|
|
35
|
+
const generateQR = async () => {
|
|
36
|
+
if (!qrCanvas.value || !props.uri) return
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
await QRCode.toCanvas(qrCanvas.value, props.uri, {
|
|
40
|
+
width: 300,
|
|
41
|
+
margin: 2,
|
|
42
|
+
color: {
|
|
43
|
+
dark: '#000000',
|
|
44
|
+
light: '#FFFFFF',
|
|
45
|
+
},
|
|
46
|
+
})
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error('Failed to generate QR code:', error)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const copyUri = () => copy(props.uri)
|
|
53
|
+
|
|
54
|
+
watch(() => props.uri, generateQR, { immediate: true })
|
|
55
|
+
|
|
56
|
+
onMounted(() => {
|
|
57
|
+
generateQR()
|
|
58
|
+
})
|
|
59
|
+
</script>
|
|
60
|
+
|
|
61
|
+
<style scoped>
|
|
62
|
+
p {
|
|
63
|
+
text-align: center;
|
|
64
|
+
@mixin ui-font;
|
|
65
|
+
color: var(--muted);
|
|
66
|
+
font-size: var(--font-sm);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.qr-frame {
|
|
70
|
+
background: white;
|
|
71
|
+
padding: var(--spacer-sm);
|
|
72
|
+
max-width: 15rem;
|
|
73
|
+
max-height: 15rem;
|
|
74
|
+
border: var(--border);
|
|
75
|
+
border-radius: var(--border-radius);
|
|
76
|
+
margin: 0 auto;
|
|
77
|
+
|
|
78
|
+
canvas {
|
|
79
|
+
width: 100% !important;
|
|
80
|
+
height: 100% !important;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.uri-display {
|
|
85
|
+
display: flex;
|
|
86
|
+
align-items: center;
|
|
87
|
+
gap: var(--spacer-xs);
|
|
88
|
+
background: var(--color-bg-secondary);
|
|
89
|
+
border: var(--border);
|
|
90
|
+
border-radius: var(--border-radius-sm);
|
|
91
|
+
overflow: hidden;
|
|
92
|
+
height: min-content;
|
|
93
|
+
padding: 0;
|
|
94
|
+
|
|
95
|
+
code {
|
|
96
|
+
flex: 1;
|
|
97
|
+
font-size: var(--font-xs);
|
|
98
|
+
font-family: monospace;
|
|
99
|
+
white-space: nowrap;
|
|
100
|
+
overflow: hidden;
|
|
101
|
+
padding: 0 var(--spacer-sm);
|
|
102
|
+
color: var(--muted);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.copy-button {
|
|
106
|
+
flex-shrink: 0;
|
|
107
|
+
padding: var(--spacer-xs);
|
|
108
|
+
min-width: auto;
|
|
109
|
+
margin: -1px;
|
|
110
|
+
|
|
111
|
+
&.copied {
|
|
112
|
+
color: var(--color-success);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<EvmConnectorQR :uri="uri">
|
|
3
|
+
<template #instruction>
|
|
4
|
+
Scan the code in your MetaMask mobile app
|
|
5
|
+
</template>
|
|
6
|
+
</EvmConnectorQR>
|
|
7
|
+
</template>
|
|
8
|
+
|
|
9
|
+
<script setup lang="ts">
|
|
10
|
+
import EvmConnectorQR from './EvmConnectorQR.vue'
|
|
11
|
+
|
|
12
|
+
defineProps<{
|
|
13
|
+
uri: string
|
|
14
|
+
}>()
|
|
15
|
+
</script>
|