@1001-digital/layers.evm 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/.editorconfig ADDED
@@ -0,0 +1,12 @@
1
+ root = true
2
+
3
+ [*]
4
+ indent_size = 2
5
+ indent_style = space
6
+ end_of_line = lf
7
+ charset = utf-8
8
+ trim_trailing_whitespace = true
9
+ insert_final_newline = true
10
+
11
+ [*.md]
12
+ trim_trailing_whitespace = false
package/.nuxtrc ADDED
@@ -0,0 +1 @@
1
+ typescript.includeWorkspace = true
@@ -0,0 +1,7 @@
1
+ NUXT_PUBLIC_TITLE="EVM Layer Playground"
2
+ NUXT_PUBLIC_CHAIN_ID=1
3
+ NUXT_PUBLIC_BLOCK_EXPLORER="https://etherscan.io"
4
+ NUXT_PUBLIC_RPC1=""
5
+ NUXT_PUBLIC_RPC2=""
6
+ NUXT_PUBLIC_RPC3=""
7
+ NUXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=""
@@ -0,0 +1,5 @@
1
+ export default defineAppConfig({
2
+ myLayer: {
3
+ name: 'My amazing Nuxt layer (overwritten)'
4
+ }
5
+ })
@@ -0,0 +1,3 @@
1
+ <template>
2
+ <NuxtPage />
3
+ </template>
@@ -0,0 +1,106 @@
1
+ <template>
2
+ <Card v-if="isConnected">
3
+ <h2>Disconnect?</h2>
4
+ <Button @click="disconnect">Disconnect</Button>
5
+ </Card>
6
+
7
+ <Card v-if="isConnected">
8
+ <h2>Account Info</h2>
9
+ <p>Address: {{ address }}</p>
10
+ <p>Chain ID: {{ chainId }}</p>
11
+ </Card>
12
+
13
+ <Card v-if="isConnected">
14
+ <h2>Transaction Flow Example</h2>
15
+ <p>Send 0 ETH to your own address</p>
16
+
17
+ <EvmTransactionFlow :request="sendTransaction" :text="{
18
+ title: {
19
+ confirm: 'Send Transaction',
20
+ requesting: 'Requesting...',
21
+ waiting: 'Waiting for confirmation...',
22
+ complete: 'Transaction Complete!',
23
+ error: 'Transaction Error',
24
+ },
25
+ lead: {
26
+ confirm: 'This will send 0 ETH to your address as a test transaction.',
27
+ requesting: 'Please confirm the transaction in your wallet.',
28
+ waiting: 'Your transaction is being processed...',
29
+ complete: 'Your transaction has been confirmed on-chain.',
30
+ error: 'An error occurred while processing your transaction.',
31
+ },
32
+ action: {
33
+ confirm: 'Send Transaction',
34
+ error: 'Try Again',
35
+ },
36
+ }" @complete="onTransactionComplete" @cancel="onTransactionCancel">
37
+ <template #start="{ start }">
38
+ <Actions>
39
+ <Button @click="start">Start Transaction</Button>
40
+ </Actions>
41
+ </template>
42
+
43
+ <template #confirm>
44
+ <div class="tx-details">
45
+ <p><strong>To:</strong> {{ address }}</p>
46
+ <p><strong>Amount:</strong> 0 ETH</p>
47
+ <p><strong>Chain:</strong> {{ chainId }}</p>
48
+ </div>
49
+ </template>
50
+
51
+ <template #complete>
52
+ <p class="success">Transaction confirmed successfully!</p>
53
+ </template>
54
+ </EvmTransactionFlow>
55
+ </Card>
56
+ </template>
57
+
58
+ <script setup lang="ts">
59
+ import { useConnection, useDisconnect } from '@wagmi/vue'
60
+ import { sendTransaction as sendTx } from '@wagmi/core'
61
+ import { parseEther } from 'viem'
62
+ import type { Config } from '@wagmi/vue'
63
+ import type { TransactionReceipt } from 'viem'
64
+
65
+ const { $wagmi } = useNuxtApp()
66
+ const { address, isConnected, chainId } = useConnection()
67
+ const { disconnect } = useDisconnect()
68
+
69
+ const sendTransaction = async () => {
70
+ const hash = await sendTx($wagmi as Config, {
71
+ to: address.value!,
72
+ value: parseEther('0'),
73
+ })
74
+ return hash
75
+ }
76
+
77
+ const onTransactionComplete = (receipt: TransactionReceipt) => {
78
+ console.log('Transaction complete:', receipt)
79
+ }
80
+
81
+ const onTransactionCancel = () => {
82
+ console.log('Transaction cancelled')
83
+ }
84
+ </script>
85
+
86
+ <style scoped>
87
+ .tx-details {
88
+ padding: var(--size-4);
89
+ background: var(--gray-z-1);
90
+ border-radius: var(--radius);
91
+ display: grid;
92
+ gap: var(--size-3);
93
+ }
94
+
95
+ .tx-details p {
96
+ margin: 0;
97
+ font-family: var(--font-mono);
98
+ font-size: var(--font-sm);
99
+ }
100
+
101
+ .success {
102
+ color: var(--success);
103
+ font-weight: 600;
104
+ text-align: center;
105
+ }
106
+ </style>
@@ -0,0 +1,12 @@
1
+ import { fileURLToPath } from 'node:url'
2
+
3
+ export default defineNuxtConfig({
4
+ extends: ['..'],
5
+ modules: ['@nuxt/eslint'],
6
+ eslint: {
7
+ config: {
8
+ // Use the generated ESLint config for lint root project as well
9
+ rootDir: fileURLToPath(new URL('..', import.meta.url))
10
+ }
11
+ }
12
+ })
@@ -0,0 +1,32 @@
1
+ <template>
2
+ <div class="playground">
3
+ <h1>EVM Layer Playground</h1>
4
+
5
+ <Card>
6
+ <h2>Wallet Connection</h2>
7
+ <EvmConnect>
8
+ <template #connected="{ address }">
9
+ <p>
10
+ Connected:
11
+ <EvmAccount :address="address" />
12
+ </p>
13
+ </template>
14
+ </EvmConnect>
15
+ </Card>
16
+
17
+ <SignedIn />
18
+ </div>
19
+ </template>
20
+
21
+ <script setup lang="ts">
22
+ </script>
23
+
24
+ <style scoped>
25
+ .playground {
26
+ max-width: 50rem;
27
+ margin: 0 auto;
28
+ padding: var(--spacer);
29
+ display: grid;
30
+ gap: var(--spacer);
31
+ }
32
+ </style>
package/AGENTS.md ADDED
@@ -0,0 +1,72 @@
1
+ # AGENTS.md
2
+
3
+ Nuxt layer for building dAPPs (Ethereum-powered applications). Extends `@1001-digital/layers.base`.
4
+
5
+ ## Setup commands
6
+
7
+ - Install deps: `pnpm install`
8
+ - Start dev server: `pnpm dev`
9
+ - Prepare types: `pnpm dev:prepare`
10
+
11
+ ## Dependencies
12
+
13
+ - `@wagmi/vue` (0.4.x) - Wallet connection, contract reads/writes, account state
14
+ - `viem` - Type-safe Ethereum utilities, ABI encoding, transaction simulation
15
+ - `@tanstack/vue-query` - Caching and synchronization of blockchain data
16
+ - `qrcode` - QR code generation for wallet connect URIs
17
+
18
+ ## Wagmi Configuration
19
+
20
+ Uses modern wagmi 0.4.x patterns:
21
+ - `useConnection` (not deprecated `useAccount`)
22
+ - `useConnectionEffect` (not deprecated `useAccountEffect`)
23
+ - `useSwitchConnection` (not deprecated `useSwitchAccount`)
24
+
25
+ Configured chains: mainnet, sepolia, holesky, localhost
26
+
27
+ Connectors: injected, coinbaseWallet, metaMask, walletConnect
28
+
29
+ ## Components
30
+
31
+ - `EvmConnect.client.vue` - Wallet connection button with modal
32
+ - `EvmAccount.client.vue` - Address display with ENS resolution
33
+ - `EvmTransactionFlow.vue` - Guided transaction execution flow
34
+ - `EvmConnectorQR.client.vue` - Base QR code renderer
35
+ - `EvmWalletConnectQR.client.vue` - WalletConnect QR wrapper
36
+ - `EvmMetaMaskQR.client.vue` - MetaMask QR wrapper
37
+
38
+ ## Composables
39
+
40
+ - `useMainChainId()` - Get configured chain ID from runtime config
41
+ - `useEnsureChainIdCheck()` - Validate/switch chain before transactions
42
+ - `useBaseURL()` - Get base URL with trailing slash
43
+ - `useClipboard()` - Copy text to clipboard with copied state
44
+
45
+ ## Utilities
46
+
47
+ - `shortAddress(address, length)` - Truncate address for display
48
+ - `formatETH(value, maxDecimals)` - Format ETH values
49
+
50
+ ## Environment Variables
51
+
52
+ ```bash
53
+ NUXT_PUBLIC_TITLE="App Name"
54
+ NUXT_PUBLIC_CHAIN_ID=1
55
+ NUXT_PUBLIC_BLOCK_EXPLORER="https://etherscan.io"
56
+ NUXT_PUBLIC_RPC1=""
57
+ NUXT_PUBLIC_RPC2=""
58
+ NUXT_PUBLIC_RPC3=""
59
+ NUXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=""
60
+ ```
61
+
62
+ ## Key directories
63
+
64
+ ```
65
+ app/
66
+ ├── components/ # Vue components (Evm* prefixed)
67
+ ├── composables/ # Composables (chainId, helpers)
68
+ ├── plugins/ # Wagmi plugin configuration
69
+ └── utils/ # Utility functions
70
+ public/
71
+ └── icons/wallets/ # Wallet connector icons
72
+ ```
package/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # Nuxt Layer Starter
2
+
3
+ Create Nuxt extendable layer with this GitHub template.
4
+
5
+ ## Setup
6
+
7
+ Make sure to install the dependencies:
8
+
9
+ ```bash
10
+ pnpm install
11
+ ```
12
+
13
+ ## Working on your layer
14
+
15
+ Your layer is at the root of this repository, it is exactly like a regular Nuxt project, except you can publish it on NPM.
16
+
17
+ The `.playground` directory should help you on trying your layer during development.
18
+
19
+ Running `pnpm dev` will prepare and boot `.playground` directory, which imports your layer itself.
20
+
21
+ ## Distributing your layer
22
+
23
+ Your Nuxt layer is shaped exactly the same as any other Nuxt project, except you can publish it on NPM.
24
+
25
+ To do so, you only have to check if `files` in `package.json` are valid, then run:
26
+
27
+ ```bash
28
+ npm publish --access public
29
+ ```
30
+
31
+ Once done, your users will only have to run:
32
+
33
+ ```bash
34
+ npm install --save your-layer
35
+ ```
36
+
37
+ Then add the dependency to their `extends` in `nuxt.config`:
38
+
39
+ ```ts
40
+ defineNuxtConfig({
41
+ extends: 'your-layer'
42
+ })
43
+ ```
44
+
45
+ ## Development Server
46
+
47
+ Start the development server on http://localhost:3000
48
+
49
+ ```bash
50
+ pnpm dev
51
+ ```
52
+
53
+ ## Production
54
+
55
+ Build the application for production:
56
+
57
+ ```bash
58
+ pnpm build
59
+ ```
60
+
61
+ Or statically generate it with:
62
+
63
+ ```bash
64
+ pnpm generate
65
+ ```
66
+
67
+ Locally preview production build:
68
+
69
+ ```bash
70
+ pnpm preview
71
+ ```
72
+
73
+ Checkout the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
@@ -0,0 +1,28 @@
1
+ <template>
2
+ <slot :display="display" :is-current="isCurrent">
3
+ <span>{{ display }}</span>
4
+ </slot>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ import type { Address } from 'viem'
9
+ import { useConnection, useEnsName } from '@wagmi/vue'
10
+
11
+ const props = defineProps<{
12
+ address?: Address
13
+ }>()
14
+ const address = computed(() => props.address)
15
+
16
+ const { address: currentAddress } = useConnection()
17
+
18
+ const isCurrent = computed<boolean>(
19
+ () => currentAddress.value?.toLowerCase() === address.value?.toLowerCase(),
20
+ )
21
+
22
+ const { data: ens } = useEnsName({
23
+ address,
24
+ chainId: 1,
25
+ })
26
+
27
+ const display = computed<string>(() => ens.value || shortAddress(address.value!))
28
+ </script>
@@ -0,0 +1,176 @@
1
+ <template>
2
+ <Button v-if="showConnect" @click="chooseModalOpen = true" :class="className">
3
+ <slot>Connect Wallet</slot>
4
+ </Button>
5
+ <slot v-else name="connected" :address="address">
6
+ <EvmAccount :address="address" />
7
+ </slot>
8
+
9
+ <Teleport to="body">
10
+ <Dialog v-if="showConnect" title="Connect Wallet" v-model:open="chooseModalOpen" @closed="onModalClosed">
11
+ <Alert v-if="errorMessage" type="error">
12
+ {{ errorMessage }}
13
+ </Alert>
14
+ <EvmWalletConnectQR v-if="walletConnectUri" :uri="walletConnectUri" />
15
+ <EvmMetaMaskQR v-else-if="metaMaskUri" :uri="metaMaskUri" />
16
+ <template v-else-if="isConnecting">
17
+ <Loading txt="Waiting for wallet confirmation..." spinner />
18
+ </template>
19
+ <div v-else class="wallet-options">
20
+ <Button v-for="connector in shownConnectors" :key="connector.uid" @click="() => login(connector)"
21
+ class="choose-connector">
22
+ <img v-if="ICONS[connector.name]" :src="connector.icon || `${base}icons/wallets/${ICONS[connector.name]}`"
23
+ :alt="connector.name" />
24
+ <span>{{ connector.name }}</span>
25
+ </Button>
26
+ <Button to="https://ethereum.org/wallets/" target="_blank" class="link muted small">
27
+ <Icon type="help" />
28
+ <span>New to wallets?</span>
29
+ </Button>
30
+ </div>
31
+ </Dialog>
32
+ </Teleport>
33
+ </template>
34
+
35
+ <script setup lang="ts">
36
+ import type { Connector } from '@wagmi/vue'
37
+ import { useConnection, useConnect, useChainId } from '@wagmi/vue'
38
+
39
+ const ICONS: Record<string, string> = {
40
+ 'Coinbase Wallet': 'coinbase.svg',
41
+ MetaMask: 'metamask.svg',
42
+ Phantom: 'phantom.svg',
43
+ 'Rabby Wallet': 'rabby.svg',
44
+ Rainbow: 'rainbow.svg',
45
+ WalletConnect: 'walletconnect.svg',
46
+ }
47
+
48
+ const PRIORITY: Record<string, number> = {
49
+ 'WalletConnect': 20,
50
+ 'Coinbase Wallet': 10,
51
+ }
52
+
53
+ const props = defineProps<{
54
+ className?: string
55
+ }>()
56
+ const emit = defineEmits<{
57
+ connected: [{ address: `0x${string}` | undefined }]
58
+ disconnected: []
59
+ }>()
60
+ const base = useBaseURL()
61
+
62
+ const chainId = useChainId()
63
+ const { connectors, connectAsync } = useConnect()
64
+ const { address, isConnected } = useConnection()
65
+
66
+ const showConnect = computed(() => !isConnected.value)
67
+ const shownConnectors = computed(() => {
68
+ const unique = Array.from(
69
+ new Map(connectors?.map((connector) => [connector.name, connector])).values(),
70
+ )
71
+
72
+ const filtered = unique.length > 1 ? unique.filter((c) => c.id !== 'injected') : unique
73
+
74
+ return filtered.sort((a, b) => {
75
+ const priorityA = PRIORITY[a.name] ?? 5
76
+ const priorityB = PRIORITY[b.name] ?? 5
77
+ return priorityA - priorityB
78
+ })
79
+ })
80
+
81
+ const chooseModalOpen = ref(false)
82
+ const errorMessage = ref('')
83
+ const isConnecting = ref(false)
84
+ const walletConnectUri = ref('')
85
+ const metaMaskUri = ref('')
86
+
87
+ const login = async (connector: Connector) => {
88
+ errorMessage.value = ''
89
+ isConnecting.value = true
90
+ walletConnectUri.value = ''
91
+ metaMaskUri.value = ''
92
+
93
+ const handleMessage = (event: { type: string; data?: string }) => {
94
+ if (event.type === 'display_uri' && event.data) {
95
+ if (connector.id === 'walletConnect') {
96
+ walletConnectUri.value = event.data
97
+ } else if (connector.id === 'metaMaskSDK') {
98
+ metaMaskUri.value = event.data
99
+ }
100
+ }
101
+ }
102
+
103
+ if (connector.id === 'walletConnect' || connector.id === 'metaMaskSDK') {
104
+ connector.emitter.on('message', handleMessage)
105
+ }
106
+
107
+ try {
108
+ await connectAsync({ connector, chainId: chainId.value })
109
+
110
+ setTimeout(() => {
111
+ chooseModalOpen.value = false
112
+ isConnecting.value = false
113
+ walletConnectUri.value = ''
114
+ metaMaskUri.value = ''
115
+ }, 100)
116
+ } catch (error: unknown) {
117
+ isConnecting.value = false
118
+ walletConnectUri.value = ''
119
+ metaMaskUri.value = ''
120
+
121
+ const errorMsg = error instanceof Error ? error.message : ''
122
+ if (errorMsg.includes('User rejected') || errorMsg.includes('rejected') || errorMsg.includes('denied')) {
123
+ errorMessage.value = 'Connection cancelled. Please try again.'
124
+ } else {
125
+ errorMessage.value = 'Failed to connect. Please try again.'
126
+ }
127
+ console.error('Wallet connection error:', error)
128
+ } finally {
129
+ if (connector.id === 'walletConnect' || connector.id === 'metaMaskSDK') {
130
+ connector.emitter.off('message', handleMessage)
131
+ }
132
+ }
133
+ }
134
+
135
+ const onModalClosed = () => {
136
+ errorMessage.value = ''
137
+ isConnecting.value = false
138
+ walletConnectUri.value = ''
139
+ metaMaskUri.value = ''
140
+ }
141
+
142
+ const check = () =>
143
+ isConnected.value ? emit('connected', { address: address.value }) : emit('disconnected')
144
+ watch(isConnected, () => check())
145
+ onMounted(() => check())
146
+ </script>
147
+
148
+ <style scoped>
149
+ .wallet-options {
150
+ display: grid;
151
+ gap: var(--spacer);
152
+
153
+ button.choose-connector {
154
+ width: 100%;
155
+ inline-size: auto;
156
+ justify-content: flex-start;
157
+
158
+ img {
159
+ margin: -1rem 0 -1rem -0.6rem;
160
+ width: var(--size-5);
161
+ height: var(--size-5);
162
+ }
163
+
164
+ span {
165
+ border-left: var(--border);
166
+ padding-left: var(--spacer-sm);
167
+ }
168
+ }
169
+
170
+ }
171
+
172
+ .link.muted {
173
+ justify-self: center;
174
+ font-size: var(--font-xs);
175
+ }
176
+ </style>
@@ -0,0 +1,108 @@
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 @click="copyUri" class="copy-button" :class="{ copied: isCopied }">
12
+ <Icon :type="isCopied ? 'checkmark' : 'copy'" />
13
+ </Button>
14
+ </div>
15
+ </template>
16
+
17
+ <script setup lang="ts">
18
+ import QRCode from 'qrcode'
19
+
20
+ const props = defineProps<{
21
+ uri: string
22
+ }>()
23
+
24
+ const qrCanvas = ref<HTMLCanvasElement | null>(null)
25
+ const { copy, copied: isCopied } = useClipboard()
26
+
27
+ const generateQR = async () => {
28
+ if (!qrCanvas.value || !props.uri) return
29
+
30
+ try {
31
+ await QRCode.toCanvas(qrCanvas.value, props.uri, {
32
+ width: 300,
33
+ margin: 2,
34
+ color: {
35
+ dark: '#000000',
36
+ light: '#FFFFFF',
37
+ },
38
+ })
39
+ } catch (error) {
40
+ console.error('Failed to generate QR code:', error)
41
+ }
42
+ }
43
+
44
+ const copyUri = () => copy(props.uri)
45
+
46
+ watch(() => props.uri, generateQR, { immediate: true })
47
+
48
+ onMounted(() => {
49
+ generateQR()
50
+ })
51
+ </script>
52
+
53
+ <style scoped>
54
+ p {
55
+ text-align: center;
56
+ @mixin ui-font;
57
+ color: var(--muted);
58
+ font-size: var(--font-sm);
59
+ }
60
+
61
+ .qr-frame {
62
+ background: white;
63
+ padding: var(--spacer-sm);
64
+ max-width: 15rem;
65
+ max-height: 15rem;
66
+ border: var(--border);
67
+ border-radius: var(--border-radius-sm, 0.5rem);
68
+ margin: 0 auto;
69
+
70
+ canvas {
71
+ width: 100% !important;
72
+ height: 100% !important;
73
+ }
74
+ }
75
+
76
+ .uri-display {
77
+ display: flex;
78
+ align-items: center;
79
+ gap: var(--spacer-xs);
80
+ background: var(--color-bg-secondary);
81
+ border: var(--border);
82
+ border-radius: var(--border-radius-sm);
83
+ overflow: hidden;
84
+ height: min-content;
85
+ padding: 0;
86
+
87
+ code {
88
+ flex: 1;
89
+ font-size: var(--font-xs);
90
+ font-family: monospace;
91
+ white-space: nowrap;
92
+ overflow: hidden;
93
+ padding: 0 var(--spacer-sm);
94
+ color: var(--muted);
95
+ }
96
+
97
+ .copy-button {
98
+ flex-shrink: 0;
99
+ padding: var(--spacer-xs);
100
+ min-width: auto;
101
+ margin: -1px;
102
+
103
+ &.copied {
104
+ color: var(--color-success);
105
+ }
106
+ }
107
+ }
108
+ </style>
@@ -0,0 +1,13 @@
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
+ defineProps<{
11
+ uri: string
12
+ }>()
13
+ </script>
@@ -0,0 +1,237 @@
1
+ <template>
2
+ <slot :start="start" name="start"></slot>
3
+
4
+ <Dialog v-model:open="open" :x-close="false" class="transaction-flow">
5
+ <slot name="before" />
6
+
7
+ <h1 v-if="text.title[step]">{{ text.title[step] }}</h1>
8
+
9
+ <div class="text">
10
+ <p v-if="text.lead[step]">{{ text.lead[step] }}</p>
11
+ <p v-if="error">{{ error }}</p>
12
+ </div>
13
+
14
+ <slot :name="step" :cancel="cancel"></slot>
15
+
16
+ <Button v-if="step === 'waiting'" :to="txLink" target="_blank" class="block-explorer">
17
+ <Icon type="loader" class="spin" />
18
+ <span>View on Block Explorer</span>
19
+ </Button>
20
+
21
+ <Actions v-if="step === 'chain'">
22
+ <Button @click="cancel" class="secondary">Cancel</Button>
23
+ </Actions>
24
+
25
+ <Actions v-if="step === 'confirm' || step === 'error'">
26
+ <Button @click="cancel" class="secondary">Cancel</Button>
27
+ <Button @click="() => initializeRequest()">{{ text.action[step] || 'Execute' }}</Button>
28
+ </Actions>
29
+ </Dialog>
30
+ </template>
31
+
32
+ <script setup lang="ts">
33
+ import { waitForTransactionReceipt, watchChainId } from '@wagmi/core'
34
+ import type { Config } from '@wagmi/vue'
35
+ import type { TransactionReceipt, Hash } from 'viem'
36
+
37
+ interface TextConfig {
38
+ title: Record<string, string>
39
+ lead: Record<string, string>
40
+ action: Record<string, string>
41
+ }
42
+
43
+ const checkChain = useEnsureChainIdCheck()
44
+
45
+ const { $wagmi } = useNuxtApp()
46
+ const config = useRuntimeConfig()
47
+
48
+ const props = withDefaults(defineProps<{
49
+ text?: TextConfig
50
+ request?: () => Promise<Hash>
51
+ delayAfter?: number
52
+ delayAutoclose?: number
53
+ skipConfirmation?: boolean
54
+ autoCloseSuccess?: boolean
55
+ }>(), {
56
+ text: () => ({
57
+ title: {
58
+ confirm: 'Confirm Transaction',
59
+ },
60
+ lead: {
61
+ confirm: 'Please review and confirm this transaction.',
62
+ },
63
+ action: {
64
+ confirm: 'Execute',
65
+ },
66
+ }),
67
+ delayAfter: 2000,
68
+ delayAutoclose: 2000,
69
+ skipConfirmation: false,
70
+ autoCloseSuccess: false,
71
+ })
72
+
73
+ const emit = defineEmits<{
74
+ complete: [receipt: TransactionReceipt]
75
+ cancel: []
76
+ }>()
77
+
78
+ const open = ref(false)
79
+
80
+ const switchChain = ref(false)
81
+ watchChainId($wagmi as Config, {
82
+ async onChange() {
83
+ if (!switchChain.value) return
84
+
85
+ if (await checkChain()) {
86
+ switchChain.value = false
87
+ initializeRequest()
88
+ } else {
89
+ switchChain.value = true
90
+ }
91
+ },
92
+ })
93
+
94
+ const cachedRequest = ref(props.request)
95
+ watch(() => props.request, () => {
96
+ cachedRequest.value = props.request
97
+ })
98
+
99
+ const requesting = ref(false)
100
+ const waiting = ref(false)
101
+ const complete = ref(false)
102
+ const error = ref('')
103
+ const tx = ref<Hash | null>(null)
104
+ const receipt = ref<TransactionReceipt | null>(null)
105
+ const txLink = computed(() => `${config.public.blockExplorer}/tx/${tx.value}`)
106
+
107
+ const step = computed(() => {
108
+ if (
109
+ open.value &&
110
+ !requesting.value &&
111
+ !switchChain.value &&
112
+ !waiting.value &&
113
+ !complete.value
114
+ ) {
115
+ return 'confirm'
116
+ }
117
+
118
+ if (switchChain.value) {
119
+ return 'chain'
120
+ }
121
+
122
+ if (requesting.value) {
123
+ return 'requesting'
124
+ }
125
+
126
+ if (waiting.value) {
127
+ return 'waiting'
128
+ }
129
+
130
+ if (complete.value) {
131
+ return 'complete'
132
+ }
133
+
134
+ return 'error'
135
+ })
136
+
137
+ const initializeRequest = async (request = cachedRequest.value) => {
138
+ cachedRequest.value = request
139
+ complete.value = false
140
+ open.value = true
141
+ error.value = ''
142
+ tx.value = null
143
+ receipt.value = null
144
+
145
+ if (!(await checkChain())) {
146
+ switchChain.value = true
147
+ return
148
+ } else {
149
+ switchChain.value = false
150
+ }
151
+
152
+ if (requesting.value) return
153
+
154
+ try {
155
+ requesting.value = true
156
+ tx.value = await request!()
157
+ requesting.value = false
158
+ waiting.value = true
159
+ const [receiptObject] = await Promise.all([
160
+ waitForTransactionReceipt($wagmi as Config, { hash: tx.value }),
161
+ ])
162
+ await delay(props.delayAfter)
163
+ receipt.value = receiptObject
164
+ emit('complete', receiptObject)
165
+ complete.value = true
166
+ } catch (e: unknown) {
167
+ const err = e as { cause?: { code?: number }; shortMessage?: string }
168
+ if (err?.cause?.code === 4001) {
169
+ open.value = false
170
+ } else {
171
+ error.value = err.shortMessage || 'Error submitting transaction request.'
172
+ }
173
+ console.log(e)
174
+ }
175
+
176
+ requesting.value = false
177
+ waiting.value = false
178
+
179
+ if (props.autoCloseSuccess && step.value === 'complete') {
180
+ await delay(props.delayAutoclose)
181
+ open.value = false
182
+ await delay(300)
183
+ }
184
+
185
+ return receipt.value
186
+ }
187
+
188
+ const start = () => {
189
+ if (props.skipConfirmation && !open.value) {
190
+ initializeRequest()
191
+ }
192
+
193
+ open.value = true
194
+ }
195
+
196
+ const cancel = () => {
197
+ open.value = false
198
+
199
+ emit('cancel')
200
+ }
201
+
202
+ defineExpose({
203
+ initializeRequest,
204
+ })
205
+ </script>
206
+
207
+ <style>
208
+ .transaction-flow {
209
+ display: grid;
210
+ gap: var(--spacer);
211
+
212
+ .spinner {
213
+ width: var(--size-7);
214
+ height: var(--size-7);
215
+ margin: calc(-1 * var(--size-4)) 0 var(--size-3);
216
+ }
217
+
218
+ .text {
219
+ width: 100%;
220
+ height: min-content;
221
+ }
222
+
223
+ h1 {
224
+ font-size: var(--font-lg);
225
+ margin-bottom: var(--size-4);
226
+ }
227
+
228
+ p {
229
+ white-space: pre-wrap;
230
+ width: 100%;
231
+
232
+ a {
233
+ text-decoration: underline;
234
+ }
235
+ }
236
+ }
237
+ </style>
@@ -0,0 +1,13 @@
1
+ <template>
2
+ <EvmConnectorQR :uri="uri">
3
+ <template #instruction>
4
+ Scan the code in your wallet application
5
+ </template>
6
+ </EvmConnectorQR>
7
+ </template>
8
+
9
+ <script setup lang="ts">
10
+ defineProps<{
11
+ uri: string
12
+ }>()
13
+ </script>
@@ -0,0 +1,5 @@
1
+ export const useBaseURL = () => {
2
+ const config = useRuntimeConfig()
3
+
4
+ return config.app.baseURL.endsWith('/') ? config.app.baseURL : config.app.baseURL + '/'
5
+ }
@@ -0,0 +1,25 @@
1
+ import { useConnection, useSwitchChain } from '@wagmi/vue'
2
+
3
+ export const useMainChainId = () => {
4
+ const config = useRuntimeConfig()
5
+
6
+ return config.public.chainId as 1 | 11155111 | 17000 | 1337 | 31337
7
+ }
8
+
9
+ export const useEnsureChainIdCheck = () => {
10
+ const chainId = useMainChainId()
11
+ const { switchChain } = useSwitchChain()
12
+ const { chainId: currentChainId } = useConnection()
13
+
14
+ return async () => {
15
+ if (chainId !== currentChainId.value) {
16
+ switchChain({ chainId })
17
+ }
18
+
19
+ if (chainId === currentChainId.value) {
20
+ return true
21
+ }
22
+
23
+ return false
24
+ }
25
+ }
@@ -0,0 +1,26 @@
1
+ export const useClipboard = () => {
2
+ const copied = ref(false)
3
+ let timeout: ReturnType<typeof setTimeout> | null = null
4
+
5
+ const copy = async (text: string) => {
6
+ try {
7
+ await navigator.clipboard.writeText(text)
8
+ copied.value = true
9
+
10
+ if (timeout) clearTimeout(timeout)
11
+ timeout = setTimeout(() => {
12
+ copied.value = false
13
+ }, 2000)
14
+
15
+ return true
16
+ } catch (error) {
17
+ console.error('Failed to copy to clipboard:', error)
18
+ return false
19
+ }
20
+ }
21
+
22
+ return {
23
+ copy,
24
+ copied,
25
+ }
26
+ }
@@ -0,0 +1 @@
1
+ export const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
@@ -0,0 +1,81 @@
1
+ import { VueQueryPlugin } from '@tanstack/vue-query'
2
+ import {
3
+ http,
4
+ cookieStorage,
5
+ createConfig,
6
+ createStorage,
7
+ WagmiPlugin,
8
+ fallback,
9
+ type Config,
10
+ type CreateConnectorFn,
11
+ } from '@wagmi/vue'
12
+ import { mainnet, sepolia, holesky, localhost } from '@wagmi/vue/chains'
13
+ import { coinbaseWallet, injected, metaMask, walletConnect } from '@wagmi/vue/connectors'
14
+ import type { CustomTransport, Transport } from 'viem'
15
+
16
+ export default defineNuxtPlugin((nuxtApp) => {
17
+ const title = nuxtApp.$config.public.title || 'EVM Layer'
18
+ const mainChainId = nuxtApp.$config.public.chainId
19
+
20
+ const connectors: CreateConnectorFn[] = [
21
+ injected(),
22
+ coinbaseWallet({
23
+ appName: title,
24
+ appLogoUrl: '',
25
+ }),
26
+ metaMask({
27
+ headless: true,
28
+ dappMetadata: {
29
+ name: title,
30
+ iconUrl: '',
31
+ url: '',
32
+ },
33
+ }),
34
+ ]
35
+
36
+ if (import.meta.client && nuxtApp.$config.public.walletConnectProjectId)
37
+ connectors.push(
38
+ walletConnect({
39
+ projectId: nuxtApp.$config.public.walletConnectProjectId,
40
+ showQrModal: false,
41
+ }),
42
+ )
43
+
44
+ const transportDefinitions: CustomTransport | Transport[] = []
45
+
46
+ if (nuxtApp.$config.public.rpc1)
47
+ transportDefinitions.push(http(nuxtApp.$config.public.rpc1 as string))
48
+ if (nuxtApp.$config.public.rpc2)
49
+ transportDefinitions.push(http(nuxtApp.$config.public.rpc2 as string))
50
+ if (nuxtApp.$config.public.rpc3)
51
+ transportDefinitions.push(http(nuxtApp.$config.public.rpc3 as string))
52
+ transportDefinitions.push(http())
53
+
54
+ const transports = fallback(transportDefinitions)
55
+
56
+ const wagmiConfig: Config = createConfig({
57
+ chains: [mainnet, sepolia, holesky, localhost],
58
+ batch: {
59
+ multicall: true,
60
+ },
61
+ connectors,
62
+ storage: createStorage({
63
+ storage: cookieStorage,
64
+ }),
65
+ ssr: true,
66
+ transports: {
67
+ [mainnet.id]: mainChainId == 1 ? transports : http(),
68
+ [sepolia.id]: transports,
69
+ [holesky.id]: transports,
70
+ [localhost.id]: transports,
71
+ },
72
+ })
73
+
74
+ nuxtApp.vueApp.use(WagmiPlugin, { config: wagmiConfig }).use(VueQueryPlugin, {})
75
+
76
+ return {
77
+ provide: {
78
+ wagmi: wagmiConfig,
79
+ },
80
+ }
81
+ })
@@ -0,0 +1,4 @@
1
+ import type { Address } from 'viem'
2
+
3
+ export const shortAddress = (address: Address, length: number = 3) =>
4
+ address.substring(0, length + 2) + '...' + address.substring(address.length - length)
@@ -0,0 +1,12 @@
1
+ export function formatETH(value: string | number, maxDecimals: number = 3): string {
2
+ const numberValue = typeof value === 'string' ? parseFloat(value) : value
3
+
4
+ if (isNaN(numberValue)) {
5
+ throw new Error('Invalid number input')
6
+ }
7
+
8
+ return new Intl.NumberFormat('en-US', {
9
+ minimumFractionDigits: 0,
10
+ maximumFractionDigits: maxDecimals,
11
+ }).format(numberValue)
12
+ }
package/app.config.ts ADDED
@@ -0,0 +1,14 @@
1
+ export default defineAppConfig({
2
+ myLayer: {
3
+ name: 'Hello from Nuxt layer'
4
+ }
5
+ })
6
+
7
+ declare module '@nuxt/schema' {
8
+ interface AppConfigInput {
9
+ myLayer?: {
10
+ /** Project name */
11
+ name?: string
12
+ }
13
+ }
14
+ }
@@ -0,0 +1,3 @@
1
+ import withNuxt from './.playground/.nuxt/eslint.config.mjs'
2
+
3
+ export default withNuxt()
package/nuxt.config.ts ADDED
@@ -0,0 +1,41 @@
1
+ // https://nuxt.com/docs/api/configuration/nuxt-config
2
+ export default defineNuxtConfig({
3
+ extends: ['@1001-digital/layers.base'],
4
+
5
+ modules: ['@wagmi/vue/nuxt'],
6
+
7
+ ssr: process.env.NUXT_SSR !== 'false',
8
+
9
+ runtimeConfig: {
10
+ public: {
11
+ title: 'EVM Layer',
12
+ blockExplorer: 'https://etherscan.io',
13
+ chainId: 1,
14
+ rpc1: '',
15
+ rpc2: '',
16
+ rpc3: '',
17
+ walletConnectProjectId: '',
18
+ },
19
+ },
20
+
21
+ vite: {
22
+ optimizeDeps: {
23
+ include: [
24
+ '@1001-digital/layers.evm > @metamask/sdk',
25
+ '@1001-digital/layers.evm > eventemitter3',
26
+ '@1001-digital/layers.evm > qrcode',
27
+ ],
28
+ },
29
+ },
30
+
31
+ nitro: {
32
+ preset: 'node-cluster',
33
+ esbuild: {
34
+ options: {
35
+ target: 'esnext',
36
+ },
37
+ },
38
+ },
39
+
40
+ compatibilityDate: '2024-11-01',
41
+ })
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@1001-digital/layers.evm",
3
+ "type": "module",
4
+ "version": "0.0.1",
5
+ "main": "./nuxt.config.ts",
6
+ "devDependencies": {
7
+ "@nuxt/eslint": "latest",
8
+ "@types/node": "^24.10.4",
9
+ "@types/qrcode": "^1.5.6",
10
+ "eslint": "^9.39.2",
11
+ "nuxt": "^4.2.2",
12
+ "typescript": "^5.9.3",
13
+ "vue": "latest"
14
+ },
15
+ "dependencies": {
16
+ "@metamask/sdk": "~0.33.1",
17
+ "@tanstack/vue-query": "^5.92.1",
18
+ "@wagmi/vue": "^0.4.3",
19
+ "@walletconnect/ethereum-provider": "~2.21.1",
20
+ "qrcode": "^1.5.4",
21
+ "viem": "~2.42.1",
22
+ "@1001-digital/layers.base": "^0.0.2"
23
+ },
24
+ "scripts": {
25
+ "dev": "nuxi dev .playground",
26
+ "dev:prepare": "nuxt prepare .playground",
27
+ "build": "nuxt build .playground",
28
+ "generate": "nuxt generate .playground",
29
+ "preview": "nuxt preview .playground",
30
+ "lint": "eslint ."
31
+ }
32
+ }
@@ -0,0 +1,4 @@
1
+ <svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <rect width="1024" height="1024" fill="#0052FF"/>
3
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M152 512C152 710.823 313.177 872 512 872C710.823 872 872 710.823 872 512C872 313.177 710.823 152 512 152C313.177 152 152 313.177 152 512ZM420 396C406.745 396 396 406.745 396 420V604C396 617.255 406.745 628 420 628H604C617.255 628 628 617.255 628 604V420C628 406.745 617.255 396 604 396H420Z" fill="white"/>
4
+ </svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" height="33" viewBox="0 0 35 33" width="35" xmlns="http://www.w3.org/2000/svg"><g stroke-linecap="round" stroke-linejoin="round" stroke-width=".25"><path d="m32.9582 1-13.1341 9.7183 2.4424-5.72731z" fill="#e17726" stroke="#e17726"/><g fill="#e27625" stroke="#e27625"><path d="m2.66296 1 13.01714 9.809-2.3254-5.81802z"/><path d="m28.2295 23.5335-3.4947 5.3386 7.4829 2.0603 2.1436-7.2823z"/><path d="m1.27281 23.6501 2.13055 7.2823 7.46994-2.0603-3.48166-5.3386z"/><path d="m10.4706 14.5149-2.0786 3.1358 7.405.3369-.2469-7.969z"/><path d="m25.1505 14.5149-5.1575-4.58704-.1688 8.05974 7.4049-.3369z"/><path d="m10.8733 28.8721 4.4819-2.1639-3.8583-3.0062z"/><path d="m20.2659 26.7082 4.4689 2.1639-.6105-5.1701z"/></g><path d="m24.7348 28.8721-4.469-2.1639.3638 2.9025-.039 1.231z" fill="#d5bfb2" stroke="#d5bfb2"/><path d="m10.8732 28.8721 4.1572 1.9696-.026-1.231.3508-2.9025z" fill="#d5bfb2" stroke="#d5bfb2"/><path d="m15.1084 21.7842-3.7155-1.0884 2.6243-1.2051z" fill="#233447" stroke="#233447"/><path d="m20.5126 21.7842 1.0913-2.2935 2.6372 1.2051z" fill="#233447" stroke="#233447"/><path d="m10.8733 28.8721.6495-5.3386-4.13117.1167z" fill="#cc6228" stroke="#cc6228"/><path d="m24.0982 23.5335.6366 5.3386 3.4946-5.2219z" fill="#cc6228" stroke="#cc6228"/><path d="m27.2291 17.6507-7.405.3369.6885 3.7966 1.0913-2.2935 2.6372 1.2051z" fill="#cc6228" stroke="#cc6228"/><path d="m11.3929 20.6958 2.6242-1.2051 1.0913 2.2935.6885-3.7966-7.40495-.3369z" fill="#cc6228" stroke="#cc6228"/><path d="m8.392 17.6507 3.1049 6.0513-.1039-3.0062z" fill="#e27525" stroke="#e27525"/><path d="m24.2412 20.6958-.1169 3.0062 3.1049-6.0513z" fill="#e27525" stroke="#e27525"/><path d="m15.797 17.9876-.6886 3.7967.8704 4.4833.1949-5.9087z" fill="#e27525" stroke="#e27525"/><path d="m19.8242 17.9876-.3638 2.3584.1819 5.9216.8704-4.4833z" fill="#e27525" stroke="#e27525"/><path d="m20.5127 21.7842-.8704 4.4834.6236.4406 3.8584-3.0062.1169-3.0062z" fill="#f5841f" stroke="#f5841f"/><path d="m11.3929 20.6958.104 3.0062 3.8583 3.0062.6236-.4406-.8704-4.4834z" fill="#f5841f" stroke="#f5841f"/><path d="m20.5906 30.8417.039-1.231-.3378-.2851h-4.9626l-.3248.2851.026 1.231-4.1572-1.9696 1.4551 1.1921 2.9489 2.0344h5.0536l2.962-2.0344 1.442-1.1921z" fill="#c0ac9d" stroke="#c0ac9d"/><path d="m20.2659 26.7082-.6236-.4406h-3.6635l-.6236.4406-.3508 2.9025.3248-.2851h4.9626l.3378.2851z" fill="#161616" stroke="#161616"/><path d="m33.5168 11.3532 1.1043-5.36447-1.6629-4.98873-12.6923 9.3944 4.8846 4.1205 6.8983 2.0085 1.52-1.7752-.6626-.4795 1.0523-.9588-.8054-.622 1.0523-.8034z" fill="#763e1a" stroke="#763e1a"/><path d="m1 5.98873 1.11724 5.36447-.71451.5313 1.06527.8034-.80545.622 1.05228.9588-.66255.4795 1.51997 1.7752 6.89835-2.0085 4.8846-4.1205-12.69233-9.3944z" fill="#763e1a" stroke="#763e1a"/><path d="m32.0489 16.5234-6.8983-2.0085 2.0786 3.1358-3.1049 6.0513 4.1052-.0519h6.1318z" fill="#f5841f" stroke="#f5841f"/><path d="m10.4705 14.5149-6.89828 2.0085-2.29944 7.1267h6.11883l4.10519.0519-3.10487-6.0513z" fill="#f5841f" stroke="#f5841f"/><path d="m19.8241 17.9876.4417-7.5932 2.0007-5.4034h-8.9119l2.0006 5.4034.4417 7.5932.1689 2.3842.013 5.8958h3.6635l.013-5.8958z" fill="#f5841f" stroke="#f5841f"/></g></svg>
@@ -0,0 +1,4 @@
1
+ <svg viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <rect width="128" height="128" fill="#AB9FF2"/>
3
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M55.6416 82.1477C50.8744 89.4525 42.8862 98.6966 32.2568 98.6966C27.232 98.6966 22.4004 96.628 22.4004 87.6424C22.4004 64.7584 53.6445 29.3335 82.6339 29.3335C99.1257 29.3335 105.697 40.7755 105.697 53.7689C105.697 70.4471 94.8739 89.5171 84.1156 89.5171C80.7013 89.5171 79.0264 87.6424 79.0264 84.6688C79.0264 83.8931 79.1552 83.0527 79.4129 82.1477C75.7409 88.4182 68.6546 94.2361 62.0192 94.2361C57.1877 94.2361 54.7397 91.1979 54.7397 86.9314C54.7397 85.3799 55.0618 83.7638 55.6416 82.1477ZM80.6133 53.3182C80.6133 57.1044 78.3795 58.9975 75.8806 58.9975C73.3438 58.9975 71.1479 57.1044 71.1479 53.3182C71.1479 49.532 73.3438 47.6389 75.8806 47.6389C78.3795 47.6389 80.6133 49.532 80.6133 53.3182ZM94.8102 53.3184C94.8102 57.1046 92.5763 58.9977 90.0775 58.9977C87.5407 58.9977 85.3447 57.1046 85.3447 53.3184C85.3447 49.5323 87.5407 47.6392 90.0775 47.6392C92.5763 47.6392 94.8102 49.5323 94.8102 53.3184Z" fill="#FFFDF8"/>
4
+ </svg>
@@ -0,0 +1,24 @@
1
+ <svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M438.47 279.097C452.952 246.637 381.359 155.948 312.964 118.165C269.853 88.895 224.93 92.9162 215.832 105.768C195.865 133.972 281.948 157.871 339.518 185.759C327.143 191.152 315.481 200.83 308.623 213.207C287.16 189.697 240.052 169.451 184.777 185.759C147.528 196.749 116.571 222.658 104.606 261.791C101.699 260.495 98.4799 259.774 95.0934 259.774C82.1436 259.774 71.6456 270.308 71.6456 283.301C71.6456 296.295 82.1436 306.828 95.0934 306.828C97.4937 306.828 104.999 305.213 104.999 305.213L224.93 306.085C176.967 382.43 139.063 393.59 139.063 406.817C139.063 420.043 175.331 416.459 188.948 411.529C254.138 387.928 324.155 314.373 336.17 293.199C386.625 299.515 429.028 300.262 438.47 279.097Z" fill="url(#paint0_linear_1758_656)"/>
3
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M339.513 185.763C339.516 185.764 339.519 185.766 339.522 185.767C342.191 184.712 341.759 180.758 341.026 177.652C339.342 170.515 310.284 141.724 282.997 128.829C245.815 111.257 218.435 112.163 214.39 120.262C221.964 135.837 257.077 150.461 293.748 165.733C309.394 172.249 325.323 178.883 339.519 185.76C339.517 185.761 339.515 185.762 339.513 185.763Z" fill="url(#paint1_linear_1758_656)"/>
4
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M292.329 342.523C284.809 339.64 276.315 336.994 266.658 334.594C276.955 316.108 279.115 288.74 269.391 271.437C255.743 247.153 238.612 234.228 198.802 234.228C176.907 234.228 117.955 241.628 116.909 291.006C116.799 296.187 116.906 300.935 117.28 305.301L224.93 306.084C210.417 329.185 196.825 346.318 184.926 359.345C199.213 363.019 211.003 366.103 221.828 368.934C232.098 371.62 241.499 374.079 251.339 376.598C266.182 365.748 280.135 353.917 292.329 342.523Z" fill="url(#paint2_linear_1758_656)"/>
5
+ <path d="M103.169 300.228C107.567 337.737 128.813 352.437 172.227 356.788C215.641 361.138 240.544 358.22 273.698 361.246C301.389 363.774 326.113 377.932 335.285 373.04C343.539 368.636 338.921 352.728 327.876 342.521C313.558 329.291 293.742 320.093 258.875 316.828C265.824 297.739 263.877 270.973 253.085 256.411C237.481 235.355 208.68 225.836 172.227 229.995C134.143 234.34 97.6504 253.153 103.169 300.228Z" fill="url(#paint3_linear_1758_656)"/>
6
+ <defs>
7
+ <linearGradient id="paint0_linear_1758_656" x1="180.439" y1="250.352" x2="435.479" y2="322.433" gradientUnits="userSpaceOnUse">
8
+ <stop stop-color="#8697FF"/>
9
+ <stop offset="1" stop-color="#ABB7FF"/>
10
+ </linearGradient>
11
+ <linearGradient id="paint1_linear_1758_656" x1="392.428" y1="245.489" x2="207.876" y2="61.1077" gradientUnits="userSpaceOnUse">
12
+ <stop stop-color="#8697FF"/>
13
+ <stop offset="1" stop-color="#5156D8" stop-opacity="0"/>
14
+ </linearGradient>
15
+ <linearGradient id="paint2_linear_1758_656" x1="297.446" y1="348.967" x2="120.465" y2="247.558" gradientUnits="userSpaceOnUse">
16
+ <stop stop-color="#465EED"/>
17
+ <stop offset="1" stop-color="#8697FF" stop-opacity="0"/>
18
+ </linearGradient>
19
+ <linearGradient id="paint3_linear_1758_656" x1="195.658" y1="248.443" x2="315.581" y2="400.306" gradientUnits="userSpaceOnUse">
20
+ <stop stop-color="#8898FF"/>
21
+ <stop offset="0.983895" stop-color="#6277F1"/>
22
+ </linearGradient>
23
+ </defs>
24
+ </svg>
@@ -0,0 +1,59 @@
1
+ <svg viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <g clip-path="url(#clip0_8_458)">
3
+ <rect width="120" height="120" fill="url(#paint0_linear_8_458)"/>
4
+ <path d="M20 38H26C56.9279 38 82 63.0721 82 94V100H94C97.3137 100 100 97.3137 100 94C100 53.1309 66.8691 20 26 20C22.6863 20 20 22.6863 20 26V38Z" fill="url(#paint1_radial_8_458)"/>
5
+ <path d="M84 94H100C100 97.3137 97.3137 100 94 100H84V94Z" fill="url(#paint2_linear_8_458)"/>
6
+ <path d="M26 20L26 36H20L20 26C20 22.6863 22.6863 20 26 20Z" fill="url(#paint3_linear_8_458)"/>
7
+ <path d="M20 36H26C58.0325 36 84 61.9675 84 94V100H66V94C66 71.9086 48.0914 54 26 54H20V36Z" fill="url(#paint4_radial_8_458)"/>
8
+ <path d="M68 94H84V100H68V94Z" fill="url(#paint5_linear_8_458)"/>
9
+ <path d="M20 52L20 36L26 36L26 52H20Z" fill="url(#paint6_linear_8_458)"/>
10
+ <path d="M20 62C20 65.3137 22.6863 68 26 68C40.3594 68 52 79.6406 52 94C52 97.3137 54.6863 100 58 100H68V94C68 70.804 49.196 52 26 52H20V62Z" fill="url(#paint7_radial_8_458)"/>
11
+ <path d="M52 94H68V100H58C54.6863 100 52 97.3137 52 94Z" fill="url(#paint8_radial_8_458)"/>
12
+ <path d="M26 68C22.6863 68 20 65.3137 20 62L20 52L26 52L26 68Z" fill="url(#paint9_radial_8_458)"/>
13
+ </g>
14
+ <defs>
15
+ <linearGradient id="paint0_linear_8_458" x1="60" y1="0" x2="60" y2="120" gradientUnits="userSpaceOnUse">
16
+ <stop stop-color="#174299"/>
17
+ <stop offset="1" stop-color="#001E59"/>
18
+ </linearGradient>
19
+ <radialGradient id="paint1_radial_8_458" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(26 94) rotate(-90) scale(74)">
20
+ <stop offset="0.770277" stop-color="#FF4000"/>
21
+ <stop offset="1" stop-color="#8754C9"/>
22
+ </radialGradient>
23
+ <linearGradient id="paint2_linear_8_458" x1="83" y1="97" x2="100" y2="97" gradientUnits="userSpaceOnUse">
24
+ <stop stop-color="#FF4000"/>
25
+ <stop offset="1" stop-color="#8754C9"/>
26
+ </linearGradient>
27
+ <linearGradient id="paint3_linear_8_458" x1="23" y1="20" x2="23" y2="37" gradientUnits="userSpaceOnUse">
28
+ <stop stop-color="#8754C9"/>
29
+ <stop offset="1" stop-color="#FF4000"/>
30
+ </linearGradient>
31
+ <radialGradient id="paint4_radial_8_458" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(26 94) rotate(-90) scale(58)">
32
+ <stop offset="0.723929" stop-color="#FFF700"/>
33
+ <stop offset="1" stop-color="#FF9901"/>
34
+ </radialGradient>
35
+ <linearGradient id="paint5_linear_8_458" x1="68" y1="97" x2="84" y2="97" gradientUnits="userSpaceOnUse">
36
+ <stop stop-color="#FFF700"/>
37
+ <stop offset="1" stop-color="#FF9901"/>
38
+ </linearGradient>
39
+ <linearGradient id="paint6_linear_8_458" x1="23" y1="52" x2="23" y2="36" gradientUnits="userSpaceOnUse">
40
+ <stop stop-color="#FFF700"/>
41
+ <stop offset="1" stop-color="#FF9901"/>
42
+ </linearGradient>
43
+ <radialGradient id="paint7_radial_8_458" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(26 94) rotate(-90) scale(42)">
44
+ <stop offset="0.59513" stop-color="#00AAFF"/>
45
+ <stop offset="1" stop-color="#01DA40"/>
46
+ </radialGradient>
47
+ <radialGradient id="paint8_radial_8_458" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(51 97) scale(17 45.3333)">
48
+ <stop stop-color="#00AAFF"/>
49
+ <stop offset="1" stop-color="#01DA40"/>
50
+ </radialGradient>
51
+ <radialGradient id="paint9_radial_8_458" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(23 69) rotate(-90) scale(17 322.37)">
52
+ <stop stop-color="#00AAFF"/>
53
+ <stop offset="1" stop-color="#01DA40"/>
54
+ </radialGradient>
55
+ <clipPath id="clip0_8_458">
56
+ <rect width="120" height="120" fill="white"/>
57
+ </clipPath>
58
+ </defs>
59
+ </svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" height="400" viewBox="0 0 400 400" width="400" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><clipPath id="a"><path d="m0 0h400v400h-400z"/></clipPath><g clip-path="url(#a)"><circle cx="200" cy="200" fill="#3396ff" r="199.5" stroke="#66b1ff"/><path d="m122.519 148.965c42.791-41.729 112.171-41.729 154.962 0l5.15 5.022c2.14 2.086 2.14 5.469 0 7.555l-17.617 17.18c-1.07 1.043-2.804 1.043-3.874 0l-7.087-6.911c-29.853-29.111-78.253-29.111-108.106 0l-7.59 7.401c-1.07 1.043-2.804 1.043-3.874 0l-17.617-17.18c-2.14-2.086-2.14-5.469 0-7.555zm191.397 35.529 15.679 15.29c2.14 2.086 2.14 5.469 0 7.555l-70.7 68.944c-2.139 2.087-5.608 2.087-7.748 0l-50.178-48.931c-.535-.522-1.402-.522-1.937 0l-50.178 48.931c-2.139 2.087-5.608 2.087-7.748 0l-70.7015-68.945c-2.1396-2.086-2.1396-5.469 0-7.555l15.6795-15.29c2.1396-2.086 5.6085-2.086 7.7481 0l50.1789 48.932c.535.522 1.402.522 1.937 0l50.177-48.932c2.139-2.087 5.608-2.087 7.748 0l50.179 48.932c.535.522 1.402.522 1.937 0l50.179-48.931c2.139-2.087 5.608-2.087 7.748 0z" fill="#fff"/></g></svg>
package/tsconfig.json ADDED
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": "./.playground/.nuxt/tsconfig.json"
3
+ }