@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.
Files changed (52) hide show
  1. package/package.json +45 -0
  2. package/src/base/components/Actions.vue +57 -0
  3. package/src/base/components/Alert.vue +90 -0
  4. package/src/base/components/Button.vue +260 -0
  5. package/src/base/components/Card.vue +78 -0
  6. package/src/base/components/CardLink.vue +56 -0
  7. package/src/base/components/Dialog.vue +274 -0
  8. package/src/base/components/Dropdown.vue +167 -0
  9. package/src/base/components/DropdownCheckboxItem.vue +30 -0
  10. package/src/base/components/DropdownGroup.vue +9 -0
  11. package/src/base/components/DropdownItem.vue +23 -0
  12. package/src/base/components/DropdownLabel.vue +9 -0
  13. package/src/base/components/DropdownRadioGroup.vue +15 -0
  14. package/src/base/components/DropdownRadioItem.vue +29 -0
  15. package/src/base/components/DropdownSeparator.vue +7 -0
  16. package/src/base/components/DropdownSub.vue +58 -0
  17. package/src/base/components/Form.vue +27 -0
  18. package/src/base/components/FormCheckbox.vue +92 -0
  19. package/src/base/components/FormGroup.vue +39 -0
  20. package/src/base/components/FormInputGroup.vue +55 -0
  21. package/src/base/components/FormItem.vue +89 -0
  22. package/src/base/components/FormLabel.vue +39 -0
  23. package/src/base/components/FormRadioGroup.vue +118 -0
  24. package/src/base/components/FormSelect.vue +160 -0
  25. package/src/base/components/FormTextarea.vue +38 -0
  26. package/src/base/components/Icon.vue +29 -0
  27. package/src/base/components/Loading.vue +81 -0
  28. package/src/base/components/Popover.vue +182 -0
  29. package/src/base/components/Tag.vue +56 -0
  30. package/src/base/components/Tags.vue +13 -0
  31. package/src/base/components/Toasts.vue +254 -0
  32. package/src/base/components/Tooltip.vue +100 -0
  33. package/src/base/composables/time.ts +82 -0
  34. package/src/base/composables/toast.ts +40 -0
  35. package/src/base/icons.ts +20 -0
  36. package/src/base/link.ts +4 -0
  37. package/src/base/utils/format-number.ts +29 -0
  38. package/src/base/utils/time.ts +20 -0
  39. package/src/evm/components/EvmAccount.vue +28 -0
  40. package/src/evm/components/EvmConnect.vue +254 -0
  41. package/src/evm/components/EvmConnectorQR.vue +116 -0
  42. package/src/evm/components/EvmMetaMaskQR.vue +15 -0
  43. package/src/evm/components/EvmTransactionFlow.vue +327 -0
  44. package/src/evm/components/EvmWalletConnectQR.vue +13 -0
  45. package/src/evm/composables/base.ts +7 -0
  46. package/src/evm/composables/chainId.ts +41 -0
  47. package/src/evm/config.ts +32 -0
  48. package/src/evm/index.ts +25 -0
  49. package/src/evm/utils/addresses.ts +6 -0
  50. package/src/evm/utils/chains.ts +32 -0
  51. package/src/evm/utils/format-eth.ts +15 -0
  52. package/src/index.ts +68 -0
@@ -0,0 +1,327 @@
1
+ <template>
2
+ <slot
3
+ :start="start"
4
+ :step="step"
5
+ :open="open"
6
+ name="start"
7
+ ></slot>
8
+
9
+ <Dialog
10
+ v-model:open="open"
11
+ :closable="canDismiss"
12
+ :click-outside="canDismiss"
13
+ :title="text.title[step]"
14
+ class="transaction-flow"
15
+ >
16
+ <slot name="before" />
17
+
18
+ <Loading
19
+ v-if="step === 'requesting'"
20
+ spinner
21
+ stacked
22
+ :txt="text.lead[step] || ''"
23
+ />
24
+
25
+ <p
26
+ v-if="
27
+ step !== 'requesting' &&
28
+ step !== 'error' &&
29
+ text.lead[step]
30
+ "
31
+ >
32
+ {{ text.lead[step] }}
33
+ </p>
34
+
35
+ <Alert
36
+ v-if="error"
37
+ type="error"
38
+ >
39
+ <p v-if="text.lead[step]">{{ text.lead[step] }}</p>
40
+ <p>{{ error }}</p>
41
+ </Alert>
42
+
43
+ <slot
44
+ :name="step"
45
+ :cancel="cancel"
46
+ ></slot>
47
+
48
+ <template #footer>
49
+ <template v-if="step === 'chain'">
50
+ <Button
51
+ @click="cancel"
52
+ class="secondary"
53
+ >Cancel</Button
54
+ >
55
+ </template>
56
+
57
+ <template v-if="step === 'confirm' || step === 'error'">
58
+ <Button
59
+ @click="cancel"
60
+ class="secondary"
61
+ >Cancel</Button
62
+ >
63
+ <Button @click="() => initializeRequest()">
64
+ {{ text.action[step] || 'Execute' }}
65
+ </Button>
66
+ </template>
67
+
68
+ <slot
69
+ name="actions"
70
+ :step="step"
71
+ :cancel="cancel"
72
+ :execute="() => initializeRequest()"
73
+ :tx-link="txLink"
74
+ />
75
+ </template>
76
+ </Dialog>
77
+ </template>
78
+
79
+ <script setup lang="ts">
80
+ import { ref, computed, watch, onBeforeUnmount } from 'vue'
81
+ import { waitForTransactionReceipt, watchChainId } from '@wagmi/core'
82
+ import { useConfig, type Config } from '@wagmi/vue'
83
+ import type { TransactionReceipt, Hash } from 'viem'
84
+ import Dialog from '../../base/components/Dialog.vue'
85
+ import Loading from '../../base/components/Loading.vue'
86
+ import Alert from '../../base/components/Alert.vue'
87
+ import Button from '../../base/components/Button.vue'
88
+ import { useEnsureChainIdCheck, useBlockExplorer } from '../composables/chainId'
89
+ import { useToast } from '../../base/composables/toast'
90
+ import { delay } from '../../base/utils/time'
91
+
92
+ interface TextConfig {
93
+ title?: Record<string, string>
94
+ lead?: Record<string, string>
95
+ action?: Record<string, string>
96
+ }
97
+
98
+ type Step =
99
+ | 'idle'
100
+ | 'confirm'
101
+ | 'chain'
102
+ | 'requesting'
103
+ | 'waiting'
104
+ | 'complete'
105
+ | 'error'
106
+
107
+ const defaultText = {
108
+ title: {
109
+ confirm: 'Confirm Transaction',
110
+ chain: 'Switch Network',
111
+ requesting: 'Requesting',
112
+ waiting: 'Processing',
113
+ complete: 'Complete',
114
+ error: 'Error',
115
+ },
116
+ lead: {
117
+ confirm: 'Please review and confirm this transaction.',
118
+ chain: 'Please switch to the correct network to continue.',
119
+ requesting: 'Requesting transaction signature...',
120
+ waiting: 'Waiting for transaction confirmation...',
121
+ complete: 'Transaction confirmed successfully.',
122
+ },
123
+ action: {
124
+ confirm: 'Execute',
125
+ error: 'Try Again',
126
+ },
127
+ } satisfies TextConfig
128
+
129
+ const checkChain = useEnsureChainIdCheck()
130
+
131
+ const wagmiConfig = useConfig()
132
+ const blockExplorer = useBlockExplorer()
133
+ const toast = useToast()
134
+
135
+ const props = withDefaults(
136
+ defineProps<{
137
+ text?: TextConfig
138
+ request?: () => Promise<Hash>
139
+ delayAfter?: number
140
+ delayAutoclose?: number
141
+ skipConfirmation?: boolean
142
+ autoCloseSuccess?: boolean
143
+ dismissable?: boolean
144
+ }>(),
145
+ {
146
+ delayAfter: 2000,
147
+ delayAutoclose: 2000,
148
+ skipConfirmation: false,
149
+ autoCloseSuccess: true,
150
+ dismissable: true,
151
+ },
152
+ )
153
+
154
+ const emit = defineEmits<{
155
+ complete: [receipt: TransactionReceipt]
156
+ cancel: []
157
+ }>()
158
+
159
+ const text = computed<Required<TextConfig>>(() => ({
160
+ title: { ...defaultText.title, ...props.text?.title },
161
+ lead: { ...defaultText.lead, ...props.text?.lead },
162
+ action: { ...defaultText.action, ...props.text?.action },
163
+ }))
164
+
165
+ const step = ref<Step>('idle')
166
+
167
+ const open = computed({
168
+ get: () => step.value !== 'idle',
169
+ set: (v) => {
170
+ if (!v) {
171
+ step.value = 'idle'
172
+ error.value = ''
173
+ }
174
+ },
175
+ })
176
+
177
+ watchChainId(wagmiConfig as Config, {
178
+ async onChange() {
179
+ if (step.value !== 'chain') return
180
+
181
+ if (await checkChain()) {
182
+ initializeRequest()
183
+ }
184
+ },
185
+ })
186
+
187
+ const cachedRequest = ref(props.request)
188
+ watch(
189
+ () => props.request,
190
+ (v) => {
191
+ cachedRequest.value = v
192
+ },
193
+ )
194
+
195
+ const error = ref('')
196
+ const tx = ref<Hash | null>(null)
197
+ const receipt = ref<TransactionReceipt | null>(null)
198
+ const txLink = computed(() => `${blockExplorer}/tx/${tx.value}`)
199
+
200
+ let mounted = true
201
+ onBeforeUnmount(() => {
202
+ mounted = false
203
+ })
204
+
205
+ const canDismiss = computed(
206
+ () =>
207
+ props.dismissable &&
208
+ step.value !== 'requesting',
209
+ )
210
+
211
+ const initializeRequest = async (request = cachedRequest.value) => {
212
+ cachedRequest.value = request
213
+ error.value = ''
214
+ tx.value = null
215
+ receipt.value = null
216
+ step.value = 'confirm'
217
+
218
+ if (!(await checkChain())) {
219
+ step.value = 'chain'
220
+ return
221
+ }
222
+
223
+ // Phase 1: Signing (dialog)
224
+ try {
225
+ step.value = 'requesting'
226
+ tx.value = await request!()
227
+ } catch (e: unknown) {
228
+ const err = e as { cause?: { code?: number }; shortMessage?: string }
229
+ if (err?.cause?.code === 4001) {
230
+ error.value = 'Transaction rejected by user.'
231
+ } else {
232
+ error.value = err.shortMessage || 'Error submitting transaction request.'
233
+ }
234
+ step.value = 'error'
235
+ console.log(e)
236
+ return
237
+ }
238
+
239
+ // Phase 2: Receipt (toast)
240
+ step.value = 'idle'
241
+
242
+ const link = `${blockExplorer}/tx/${tx.value}`
243
+ const toastId = toast.add({
244
+ variant: 'info',
245
+ title: text.value.title.waiting,
246
+ description: text.value.lead.waiting,
247
+ duration: Infinity,
248
+ action: {
249
+ label: 'View on Block Explorer',
250
+ onClick: () => window.open(link, '_blank'),
251
+ },
252
+ })
253
+
254
+ try {
255
+ const receiptObject = await waitForTransactionReceipt(
256
+ wagmiConfig as Config,
257
+ { hash: tx.value },
258
+ )
259
+ await delay(props.delayAfter)
260
+ receipt.value = receiptObject
261
+ emit('complete', receiptObject)
262
+
263
+ toast.update(toastId, {
264
+ variant: 'success',
265
+ title: text.value.title.complete,
266
+ description: text.value.lead.complete,
267
+ ...(props.autoCloseSuccess && { duration: props.delayAutoclose }),
268
+ })
269
+ } catch (e: unknown) {
270
+ const err = e as { shortMessage?: string }
271
+ if (mounted) {
272
+ toast.dismiss(toastId)
273
+ error.value = err.shortMessage || 'Transaction failed.'
274
+ step.value = 'error'
275
+ } else {
276
+ toast.update(toastId, {
277
+ variant: 'error',
278
+ title: text.value.title.error,
279
+ description: err.shortMessage || 'Transaction failed.',
280
+ })
281
+ }
282
+ console.log(e)
283
+ }
284
+
285
+ return receipt.value
286
+ }
287
+
288
+ const start = () => {
289
+ if (props.skipConfirmation && step.value === 'idle') {
290
+ initializeRequest()
291
+ return
292
+ }
293
+
294
+ step.value = 'confirm'
295
+ }
296
+
297
+ const cancel = () => {
298
+ step.value = 'idle'
299
+ error.value = ''
300
+ emit('cancel')
301
+ }
302
+
303
+ defineExpose({
304
+ initializeRequest,
305
+ })
306
+ </script>
307
+
308
+ <style>
309
+ .transaction-flow > section {
310
+ display: grid;
311
+ gap: var(--spacer);
312
+
313
+ .text {
314
+ width: 100%;
315
+ height: min-content;
316
+ }
317
+
318
+ p {
319
+ white-space: pre-wrap;
320
+ width: 100%;
321
+
322
+ a {
323
+ text-decoration: underline;
324
+ }
325
+ }
326
+ }
327
+ </style>
@@ -0,0 +1,13 @@
1
+ <template>
2
+ <EvmConnectorQR :uri="uri">
3
+ <template #instruction> Scan the code in your wallet application </template>
4
+ </EvmConnectorQR>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ import EvmConnectorQR from './EvmConnectorQR.vue'
9
+
10
+ defineProps<{
11
+ uri: string
12
+ }>()
13
+ </script>
@@ -0,0 +1,7 @@
1
+ import { useEvmConfig } from '../config'
2
+
3
+ export const useBaseURL = () => {
4
+ const config = useEvmConfig()
5
+ const base = config.baseURL || '/'
6
+ return base.endsWith('/') ? base : base + '/'
7
+ }
@@ -0,0 +1,41 @@
1
+ import { useConnection, useSwitchChain } from '@wagmi/vue'
2
+ import { useEvmConfig } from '../config'
3
+
4
+ interface ChainConfig {
5
+ id: number
6
+ blockExplorer: string
7
+ }
8
+
9
+ export const useChainConfig = (key?: string): ChainConfig => {
10
+ const evmConfig = useEvmConfig()
11
+ const resolvedKey = key || evmConfig.defaultChain || 'mainnet'
12
+ const chain = evmConfig.chains[resolvedKey]
13
+
14
+ return {
15
+ id: chain?.id ?? 1,
16
+ blockExplorer: chain?.blockExplorer ?? 'https://etherscan.io',
17
+ }
18
+ }
19
+
20
+ export const useMainChainId = () => useChainConfig().id
21
+
22
+ export const useBlockExplorer = (key?: string) =>
23
+ useChainConfig(key).blockExplorer
24
+
25
+ export const useEnsureChainIdCheck = () => {
26
+ const chainId = useMainChainId()
27
+ const { mutate: switchChain } = useSwitchChain()
28
+ const { chainId: currentChainId } = useConnection()
29
+
30
+ return async () => {
31
+ if (chainId !== currentChainId.value) {
32
+ switchChain({ chainId })
33
+ }
34
+
35
+ if (chainId === currentChainId.value) {
36
+ return true
37
+ }
38
+
39
+ return false
40
+ }
41
+ }
@@ -0,0 +1,32 @@
1
+ import { inject, type InjectionKey } from 'vue'
2
+
3
+ export interface EvmChainConfig {
4
+ id: number
5
+ blockExplorer?: string
6
+ }
7
+
8
+ export interface EvmConfig {
9
+ title?: string
10
+ defaultChain?: string
11
+ chains: Record<string, EvmChainConfig>
12
+ ens?: {
13
+ mode?: 'indexer' | 'chain'
14
+ indexerUrls?: string[]
15
+ }
16
+ baseURL?: string
17
+ }
18
+
19
+ export const EvmConfigKey: InjectionKey<EvmConfig> = Symbol('EvmConfig')
20
+
21
+ export const defaultEvmConfig: EvmConfig = {
22
+ title: 'EVM Layer',
23
+ defaultChain: 'mainnet',
24
+ chains: {
25
+ mainnet: { id: 1, blockExplorer: 'https://etherscan.io' },
26
+ },
27
+ ens: { mode: 'indexer' },
28
+ }
29
+
30
+ export const useEvmConfig = (): EvmConfig => {
31
+ return inject(EvmConfigKey, defaultEvmConfig)
32
+ }
@@ -0,0 +1,25 @@
1
+ // Config
2
+ export { EvmConfigKey, defaultEvmConfig, useEvmConfig } from './config'
3
+ export type { EvmConfig, EvmChainConfig } from './config'
4
+
5
+ // Utils
6
+ export { shortAddress } from './utils/addresses'
7
+ export { resolveChain } from './utils/chains'
8
+ export { formatETH } from './utils/format-eth'
9
+
10
+ // Composables
11
+ export { useBaseURL } from './composables/base'
12
+ export {
13
+ useChainConfig,
14
+ useMainChainId,
15
+ useBlockExplorer,
16
+ useEnsureChainIdCheck,
17
+ } from './composables/chainId'
18
+
19
+ // Components
20
+ export { default as EvmAccount } from './components/EvmAccount.vue'
21
+ export { default as EvmConnect } from './components/EvmConnect.vue'
22
+ export { default as EvmConnectorQR } from './components/EvmConnectorQR.vue'
23
+ export { default as EvmMetaMaskQR } from './components/EvmMetaMaskQR.vue'
24
+ export { default as EvmWalletConnectQR } from './components/EvmWalletConnectQR.vue'
25
+ export { default as EvmTransactionFlow } from './components/EvmTransactionFlow.vue'
@@ -0,0 +1,6 @@
1
+ import type { Address } from 'viem'
2
+
3
+ export const shortAddress = (address: Address, length: number = 3) =>
4
+ address.substring(0, length + 2) +
5
+ '...' +
6
+ address.substring(address.length - length)
@@ -0,0 +1,32 @@
1
+ import { defineChain, type Chain } from 'viem'
2
+ import {
3
+ mainnet,
4
+ sepolia,
5
+ holesky,
6
+ optimism,
7
+ arbitrum,
8
+ base,
9
+ polygon,
10
+ localhost,
11
+ } from 'viem/chains'
12
+
13
+ const KNOWN: Chain[] = [
14
+ mainnet,
15
+ sepolia,
16
+ holesky,
17
+ optimism,
18
+ arbitrum,
19
+ base,
20
+ polygon,
21
+ localhost,
22
+ ]
23
+ const byId = new Map<number, Chain>(KNOWN.map((c) => [c.id, c]))
24
+
25
+ export const resolveChain = (id: number): Chain =>
26
+ byId.get(id) ??
27
+ defineChain({
28
+ id,
29
+ name: `Chain ${id}`,
30
+ nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
31
+ rpcUrls: { default: { http: [] } },
32
+ })
@@ -0,0 +1,15 @@
1
+ export function formatETH(
2
+ value: string | number,
3
+ maxDecimals: number = 3,
4
+ ): string {
5
+ const numberValue = typeof value === 'string' ? parseFloat(value) : value
6
+
7
+ if (isNaN(numberValue)) {
8
+ throw new Error('Invalid number input')
9
+ }
10
+
11
+ return new Intl.NumberFormat('en-US', {
12
+ minimumFractionDigits: 0,
13
+ maximumFractionDigits: maxDecimals,
14
+ }).format(numberValue)
15
+ }
package/src/index.ts ADDED
@@ -0,0 +1,68 @@
1
+ // Components
2
+ export { default as Actions } from './base/components/Actions.vue'
3
+ export { default as Alert } from './base/components/Alert.vue'
4
+ export { default as Button } from './base/components/Button.vue'
5
+ export { default as Card } from './base/components/Card.vue'
6
+ export { default as CardLink } from './base/components/CardLink.vue'
7
+ export { default as Dialog } from './base/components/Dialog.vue'
8
+ export { default as Dropdown } from './base/components/Dropdown.vue'
9
+ export { default as DropdownCheckboxItem } from './base/components/DropdownCheckboxItem.vue'
10
+ export { default as DropdownGroup } from './base/components/DropdownGroup.vue'
11
+ export { default as DropdownItem } from './base/components/DropdownItem.vue'
12
+ export { default as DropdownLabel } from './base/components/DropdownLabel.vue'
13
+ export { default as DropdownRadioGroup } from './base/components/DropdownRadioGroup.vue'
14
+ export { default as DropdownRadioItem } from './base/components/DropdownRadioItem.vue'
15
+ export { default as DropdownSeparator } from './base/components/DropdownSeparator.vue'
16
+ export { default as DropdownSub } from './base/components/DropdownSub.vue'
17
+ export { default as Form } from './base/components/Form.vue'
18
+ export { default as FormCheckbox } from './base/components/FormCheckbox.vue'
19
+ export { default as FormGroup } from './base/components/FormGroup.vue'
20
+ export { default as FormInputGroup } from './base/components/FormInputGroup.vue'
21
+ export { default as FormItem } from './base/components/FormItem.vue'
22
+ export { default as FormLabel } from './base/components/FormLabel.vue'
23
+ export { default as FormRadioGroup } from './base/components/FormRadioGroup.vue'
24
+ export { default as FormSelect } from './base/components/FormSelect.vue'
25
+ export { default as FormTextarea } from './base/components/FormTextarea.vue'
26
+ export { default as Icon } from './base/components/Icon.vue'
27
+ export { default as Loading } from './base/components/Loading.vue'
28
+ export { default as Popover } from './base/components/Popover.vue'
29
+ export { default as Tag } from './base/components/Tag.vue'
30
+ export { default as Tags } from './base/components/Tags.vue'
31
+ export { default as Toasts } from './base/components/Toasts.vue'
32
+ export { default as Tooltip } from './base/components/Tooltip.vue'
33
+
34
+ // Composables
35
+ export { useToast } from './base/composables/toast'
36
+ export type {
37
+ Toast as ToastType,
38
+ ToastAction,
39
+ ToastVariant,
40
+ } from './base/composables/toast'
41
+ export {
42
+ useSeconds,
43
+ useCountDown,
44
+ useTimeAgo,
45
+ useSecondsAgo,
46
+ } from './base/composables/time'
47
+
48
+ // Utils
49
+ export {
50
+ formatNumber,
51
+ roundAndFormatNumber,
52
+ asPercentageOf,
53
+ formatUSD,
54
+ } from './base/utils/format-number'
55
+ export {
56
+ delay,
57
+ daysInSeconds,
58
+ nowInSeconds,
59
+ asUTCDate,
60
+ } from './base/utils/time'
61
+
62
+ // Injection keys & types
63
+ export { IconAliasesKey, defaultIconAliases } from './base/icons'
64
+ export type { IconAliases } from './base/icons'
65
+ export { LinkComponentKey } from './base/link'
66
+
67
+ // EVM
68
+ export * from './evm/index'