@1001-digital/components 0.0.3 → 0.0.5

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 (32) hide show
  1. package/package.json +11 -2
  2. package/src/base/components/Button.vue +2 -2
  3. package/src/base/components/Calendar.vue +227 -0
  4. package/src/base/components/Combobox.vue +241 -0
  5. package/src/base/components/ConfirmDialog.vue +33 -0
  6. package/src/base/components/Dialog.vue +7 -1
  7. package/src/base/components/FormDateField.vue +111 -0
  8. package/src/base/components/FormDatePicker.vue +382 -0
  9. package/src/base/components/FormSlider.vue +142 -0
  10. package/src/base/components/FormSwitch.vue +103 -0
  11. package/src/base/components/Globals.vue +9 -0
  12. package/src/base/components/Opepicon.vue +45 -0
  13. package/src/base/components/PinInput.vue +105 -0
  14. package/src/base/components/Progress.vue +66 -0
  15. package/src/base/components/Toasts.vue +6 -1
  16. package/src/base/composables/confirm.ts +29 -0
  17. package/src/base/composables/toast.ts +1 -0
  18. package/src/base/icons.ts +3 -1
  19. package/src/evm/components/EvmAvatar.vue +62 -0
  20. package/src/evm/components/EvmConnect.vue +83 -32
  21. package/src/evm/components/EvmConnectorQR.vue +12 -42
  22. package/src/evm/components/EvmProfile.vue +183 -0
  23. package/src/evm/components/EvmSwitchNetwork.vue +130 -0
  24. package/src/evm/components/EvmTransactionFlow.vue +41 -11
  25. package/src/evm/components/EvmWalletConnectWallets.vue +199 -0
  26. package/src/evm/composables/chainId.ts +9 -8
  27. package/src/evm/composables/uri.ts +11 -0
  28. package/src/evm/composables/walletExplorer.ts +130 -0
  29. package/src/evm/config.ts +1 -0
  30. package/src/evm/index.ts +9 -0
  31. package/src/evm/utils/uri.ts +24 -0
  32. package/src/index.ts +18 -0
@@ -0,0 +1,105 @@
1
+ <template>
2
+ <PinInputRoot
3
+ v-model="model"
4
+ class="pin-input"
5
+ :disabled="disabled"
6
+ :placeholder="placeholder"
7
+ :mask="mask"
8
+ :otp="otp"
9
+ :type="type"
10
+ :name="name"
11
+ :required="required"
12
+ :dir="dir"
13
+ @complete="$emit('complete', $event)"
14
+ >
15
+ <PinInputInput
16
+ v-for="(_, i) in length"
17
+ :key="i"
18
+ :index="i"
19
+ :disabled="disabled"
20
+ class="pin-input-field"
21
+ />
22
+ </PinInputRoot>
23
+ </template>
24
+
25
+ <script setup lang="ts">
26
+ import { PinInputInput, PinInputRoot } from 'reka-ui'
27
+
28
+ const model = defineModel<string[]>()
29
+
30
+ withDefaults(
31
+ defineProps<{
32
+ length?: number
33
+ disabled?: boolean
34
+ placeholder?: string
35
+ mask?: boolean
36
+ otp?: boolean
37
+ type?: 'text' | 'number'
38
+ name?: string
39
+ required?: boolean
40
+ dir?: 'ltr' | 'rtl'
41
+ }>(),
42
+ {
43
+ length: 4,
44
+ placeholder: '',
45
+ type: 'text',
46
+ },
47
+ )
48
+
49
+ defineEmits<{
50
+ complete: [value: string[]]
51
+ }>()
52
+ </script>
53
+
54
+ <style scoped>
55
+ @layer components {
56
+ .pin-input {
57
+ display: flex;
58
+ gap: var(--pin-input-gap);
59
+ }
60
+
61
+ .pin-input-field {
62
+ all: unset;
63
+ display: flex;
64
+ align-items: center;
65
+ justify-content: center;
66
+ text-align: center;
67
+ inline-size: var(--pin-input-size);
68
+ block-size: var(--pin-input-size);
69
+ border-radius: var(--pin-input-border-radius);
70
+ box-shadow: var(--border-shadow);
71
+ background: var(--pin-input-background);
72
+ color: var(--pin-input-color);
73
+ font-family: var(--font-family);
74
+ font-size: var(--pin-input-font-size);
75
+ font-weight: var(--pin-input-font-weight);
76
+ text-transform: var(--ui-text-transform);
77
+ letter-spacing: var(--ui-letter-spacing);
78
+ transition:
79
+ box-shadow var(--speed),
80
+ background var(--speed);
81
+
82
+ &:is(:hover, :focus) {
83
+ box-shadow: var(--border-shadow-highlight);
84
+ }
85
+
86
+ &:focus {
87
+ background: var(--pin-input-background-focus);
88
+ outline: none;
89
+ }
90
+
91
+ &::placeholder {
92
+ color: var(--ui-placeholder-color);
93
+ }
94
+
95
+ &[data-disabled] {
96
+ opacity: 0.5;
97
+ cursor: not-allowed;
98
+ }
99
+
100
+ &[data-complete] {
101
+ box-shadow: 0 0 0 var(--border-width) var(--pin-input-complete-border-color);
102
+ }
103
+ }
104
+ }
105
+ </style>
@@ -0,0 +1,66 @@
1
+ <template>
2
+ <ProgressRoot
3
+ v-model="model"
4
+ class="progress"
5
+ :max="max"
6
+ >
7
+ <ProgressIndicator
8
+ class="progress-indicator"
9
+ :style="model != null ? { width: `${percentage}%` } : undefined"
10
+ />
11
+ </ProgressRoot>
12
+ </template>
13
+
14
+ <script setup lang="ts">
15
+ import { computed } from 'vue'
16
+ import { ProgressIndicator, ProgressRoot } from 'reka-ui'
17
+
18
+ const model = defineModel<number | null>()
19
+
20
+ const props = withDefaults(
21
+ defineProps<{
22
+ max?: number
23
+ }>(),
24
+ {
25
+ max: 100,
26
+ },
27
+ )
28
+
29
+ const percentage = computed(() =>
30
+ model.value != null ? (model.value / props.max) * 100 : 0,
31
+ )
32
+ </script>
33
+
34
+ <style scoped>
35
+ @layer components {
36
+ .progress {
37
+ position: relative;
38
+ inline-size: 100%;
39
+ block-size: var(--progress-height);
40
+ background: var(--progress-track-background);
41
+ border-radius: var(--progress-radius);
42
+ overflow: hidden;
43
+ }
44
+
45
+ .progress-indicator {
46
+ block-size: 100%;
47
+ background: var(--progress-indicator-background);
48
+ border-radius: var(--progress-radius);
49
+ transition: width var(--speed);
50
+ }
51
+
52
+ [data-state='indeterminate'] > .progress-indicator {
53
+ inline-size: 30%;
54
+ animation: progress-indeterminate 1.5s ease-in-out infinite;
55
+ }
56
+
57
+ @keyframes progress-indeterminate {
58
+ 0% {
59
+ translate: -100% 0;
60
+ }
61
+ 100% {
62
+ translate: 400% 0;
63
+ }
64
+ }
65
+ }
66
+ </style>
@@ -28,13 +28,17 @@
28
28
  <Icon type="close" />
29
29
  </ToastClose>
30
30
 
31
- <section v-if="toast.description || toast.action">
31
+ <section v-if="toast.description || toast.action || toast.progress">
32
32
  <ToastDescription
33
33
  v-if="toast.description"
34
34
  class="toast-description"
35
35
  >
36
36
  {{ toast.description }}
37
37
  </ToastDescription>
38
+ <Progress
39
+ v-if="toast.progress"
40
+ :model-value="toast.progress === true ? null : toast.progress"
41
+ />
38
42
  <ToastAction
39
43
  v-if="toast.action"
40
44
  :alt-text="toast.action.label"
@@ -62,6 +66,7 @@
62
66
  import Actions from './Actions.vue'
63
67
  import Button from './Button.vue'
64
68
  import Icon from './Icon.vue'
69
+ import Progress from './Progress.vue'
65
70
  import { useToast } from '../composables/toast'
66
71
  import {
67
72
  ToastAction,
@@ -0,0 +1,29 @@
1
+ import { ref } from 'vue'
2
+
3
+ export interface ConfirmOptions {
4
+ title: string
5
+ description?: string
6
+ okText?: string
7
+ cancelText?: string
8
+ }
9
+
10
+ export interface ConfirmState extends ConfirmOptions {
11
+ resolve: (value: boolean) => void
12
+ }
13
+
14
+ const state = ref<ConfirmState | null>(null)
15
+
16
+ export const useConfirm = () => {
17
+ const confirm = (options: ConfirmOptions) => {
18
+ return new Promise<boolean>((resolve) => {
19
+ state.value = { ...options, resolve }
20
+ })
21
+ }
22
+
23
+ const resolve = (value: boolean) => {
24
+ state.value?.resolve(value)
25
+ state.value = null
26
+ }
27
+
28
+ return { state, confirm, resolve }
29
+ }
@@ -15,6 +15,7 @@ export interface Toast {
15
15
  action?: ToastAction
16
16
  duration?: number
17
17
  loading?: boolean
18
+ progress?: number | boolean
18
19
  }
19
20
 
20
21
  const toasts = ref<Toast[]>([])
package/src/base/icons.ts CHANGED
@@ -6,10 +6,12 @@ export const IconAliasesKey: InjectionKey<IconAliases> = Symbol('IconAliases')
6
6
 
7
7
  export const defaultIconAliases: IconAliases = {
8
8
  add: 'lucide:plus',
9
+ calendar: 'lucide:calendar',
9
10
  check: 'lucide:check',
10
- close: 'lucide:x',
11
+ 'chevron-left': 'lucide:chevron-left',
11
12
  'chevron-down': 'lucide:chevron-down',
12
13
  'chevron-right': 'lucide:chevron-right',
14
+ close: 'lucide:x',
13
15
  copy: 'lucide:copy',
14
16
  edit: 'lucide:pencil',
15
17
  help: 'lucide:circle-question-mark',
@@ -0,0 +1,62 @@
1
+ <template>
2
+ <slot
3
+ :src="src"
4
+ :ens="ens"
5
+ :is-current="isCurrent"
6
+ >
7
+ <img
8
+ v-if="src"
9
+ :src="src"
10
+ :alt="ens || 'Avatar'"
11
+ :class="['evm-avatar', { large }]"
12
+ />
13
+ <Opepicon
14
+ v-else-if="address"
15
+ :seed="address"
16
+ :size="large ? 256 : 64"
17
+ :class="['evm-avatar', { large }]"
18
+ />
19
+ </slot>
20
+ </template>
21
+
22
+ <script setup lang="ts">
23
+ import { computed } from 'vue'
24
+ import type { Address } from 'viem'
25
+ import { useConnection } from '@wagmi/vue'
26
+ import { useEnsWithAvatar } from '../composables/ens'
27
+ import { useResolveUri } from '../composables/uri'
28
+ import Opepicon from '../../base/components/Opepicon.vue'
29
+
30
+ const props = defineProps<{
31
+ address?: Address
32
+ large?: boolean
33
+ }>()
34
+ const address = computed(() => props.address)
35
+
36
+ const { address: currentAddress } = useConnection()
37
+
38
+ const isCurrent = computed<boolean>(
39
+ () => currentAddress.value?.toLowerCase() === address.value?.toLowerCase(),
40
+ )
41
+
42
+ const { data: ensData } = useEnsWithAvatar(address)
43
+ const resolve = useResolveUri()
44
+
45
+ const ens = computed(() => ensData.value?.ens || null)
46
+ const src = computed(() => resolve(ensData.value?.data?.avatar))
47
+ </script>
48
+
49
+ <style scoped>
50
+ .evm-avatar {
51
+ width: var(--size-5);
52
+ height: var(--size-5);
53
+ border-radius: 50%;
54
+ background-color: var(--background);
55
+ object-fit: cover;
56
+
57
+ &.large {
58
+ width: var(--size-9);
59
+ height: var(--size-9);
60
+ }
61
+ }
62
+ </style>
@@ -26,17 +26,17 @@
26
26
  >
27
27
  {{ errorMessage }}
28
28
  </Alert>
29
- <EvmWalletConnectQR
30
- v-if="walletConnectUri"
31
- :uri="walletConnectUri"
32
- />
33
29
  <EvmMetaMaskQR
34
30
  v-else-if="metaMaskUri"
35
31
  :uri="metaMaskUri"
36
32
  />
33
+ <EvmWalletConnectWallets
34
+ v-else-if="walletConnectUri"
35
+ :uri="walletConnectUri"
36
+ />
37
37
  <template v-else-if="isConnecting">
38
38
  <Loading
39
- txt="Waiting for wallet confirmation..."
39
+ :txt="`Waiting for ${connectingWallet} confirmation...`"
40
40
  spinner
41
41
  stacked
42
42
  />
@@ -66,6 +66,17 @@
66
66
  </div>
67
67
  <span>{{ connector.name }}</span>
68
68
  </Button>
69
+ <Button
70
+ v-if="wcConnector"
71
+ @click="loginWithSafe"
72
+ class="choose-connector"
73
+ >
74
+ <img
75
+ :src="`${base}icons/wallets/safe.png`"
76
+ alt="Safe"
77
+ />
78
+ <span>Safe</span>
79
+ </Button>
69
80
  <Button
70
81
  to="https://ethereum.org/wallets/"
71
82
  target="_blank"
@@ -93,22 +104,23 @@ import Icon from '../../base/components/Icon.vue'
93
104
  import Alert from '../../base/components/Alert.vue'
94
105
  import Loading from '../../base/components/Loading.vue'
95
106
  import EvmAccount from './EvmAccount.vue'
96
- import EvmWalletConnectQR from './EvmWalletConnectQR.vue'
97
107
  import EvmMetaMaskQR from './EvmMetaMaskQR.vue'
108
+ import EvmWalletConnectWallets from './EvmWalletConnectWallets.vue'
98
109
  import { useBaseURL } from '../composables/base'
99
110
 
100
111
  const ICONS: Record<string, string> = {
101
- 'Coinbase Wallet': 'coinbase.svg',
112
+ 'Base Account': 'coinbase.svg',
102
113
  MetaMask: 'metamask.svg',
103
114
  Phantom: 'phantom.svg',
104
115
  'Rabby Wallet': 'rabby.svg',
105
116
  Rainbow: 'rainbow.svg',
117
+ Safe: 'safe.png',
106
118
  WalletConnect: 'walletconnect.svg',
107
119
  }
108
120
 
109
121
  const PRIORITY: Record<string, number> = {
110
122
  WalletConnect: 20,
111
- 'Coinbase Wallet': 10,
123
+ 'Base Account': 10,
112
124
  }
113
125
 
114
126
  defineProps<{
@@ -134,7 +146,9 @@ const shownConnectors = computed(() => {
134
146
  )
135
147
 
136
148
  const filtered =
137
- unique.length > 1 ? unique.filter((c) => c.id !== 'injected') : unique
149
+ unique.length > 1
150
+ ? unique.filter((c) => c.id !== 'injected' && c.id !== 'safe')
151
+ : unique
138
152
 
139
153
  return filtered.sort((a, b) => {
140
154
  const priorityA = PRIORITY[a.name] ?? 5
@@ -143,30 +157,55 @@ const shownConnectors = computed(() => {
143
157
  })
144
158
  })
145
159
 
160
+ const wcConnector = computed(() =>
161
+ connectors.value.find((c) => c.id === 'walletConnect'),
162
+ )
163
+
146
164
  const chooseModalOpen = ref(false)
147
165
  const errorMessage = ref('')
148
166
  const isConnecting = ref(false)
149
- const walletConnectUri = ref('')
167
+ const connectingWallet = ref('')
150
168
  const metaMaskUri = ref('')
169
+ const walletConnectUri = ref('')
170
+ const safeDeepLink = ref(false)
171
+
172
+ const loginWithSafe = () => {
173
+ if (!wcConnector.value) return
174
+ safeDeepLink.value = true
175
+ login(wcConnector.value)
176
+ }
151
177
 
152
178
  const login = async (connector: Connector) => {
153
179
  errorMessage.value = ''
154
180
  isConnecting.value = true
155
- walletConnectUri.value = ''
181
+ connectingWallet.value = safeDeepLink.value ? 'Safe' : connector.name
156
182
  metaMaskUri.value = ''
183
+ walletConnectUri.value = ''
157
184
 
158
- const handleMessage = (event: { type: string; data?: unknown }) => {
185
+ const handleMetaMaskMessage = (event: { type: string; data?: unknown }) => {
159
186
  if (event.type === 'display_uri' && typeof event.data === 'string') {
160
- if (connector.id === 'walletConnect') {
187
+ metaMaskUri.value = event.data
188
+ }
189
+ }
190
+
191
+ const handleWcMessage = (event: { type: string; data?: unknown }) => {
192
+ if (event.type === 'display_uri' && typeof event.data === 'string') {
193
+ if (safeDeepLink.value) {
194
+ window.open(
195
+ `https://app.safe.global/wc?uri=${encodeURIComponent(event.data)}`,
196
+ '_blank',
197
+ 'noreferrer',
198
+ )
199
+ } else {
161
200
  walletConnectUri.value = event.data
162
- } else if (connector.id === 'metaMaskSDK') {
163
- metaMaskUri.value = event.data
164
201
  }
165
202
  }
166
203
  }
167
204
 
168
- if (connector.id === 'walletConnect' || connector.id === 'metaMaskSDK') {
169
- connector.emitter.on('message', handleMessage)
205
+ if (connector.id === 'metaMaskSDK') {
206
+ connector.emitter.on('message', handleMetaMaskMessage)
207
+ } else if (connector.id === 'walletConnect') {
208
+ connector.emitter.on('message', handleWcMessage)
170
209
  }
171
210
 
172
211
  try {
@@ -175,28 +214,35 @@ const login = async (connector: Connector) => {
175
214
  setTimeout(() => {
176
215
  chooseModalOpen.value = false
177
216
  isConnecting.value = false
178
- walletConnectUri.value = ''
179
217
  metaMaskUri.value = ''
218
+ walletConnectUri.value = ''
219
+ safeDeepLink.value = false
180
220
  }, 100)
181
221
  } catch (error: unknown) {
182
222
  isConnecting.value = false
183
- walletConnectUri.value = ''
184
223
  metaMaskUri.value = ''
224
+ walletConnectUri.value = ''
225
+ safeDeepLink.value = false
185
226
 
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.'
227
+ // Only show errors if our dialog is still open
228
+ if (chooseModalOpen.value) {
229
+ const errorMsg = error instanceof Error ? error.message : ''
230
+ if (
231
+ errorMsg.includes('User rejected') ||
232
+ errorMsg.includes('rejected') ||
233
+ errorMsg.includes('denied')
234
+ ) {
235
+ errorMessage.value = 'Connection cancelled. Please try again.'
236
+ } else {
237
+ errorMessage.value = 'Failed to connect. Please try again.'
238
+ }
195
239
  }
196
240
  console.error('Wallet connection error:', error)
197
241
  } finally {
198
- if (connector.id === 'walletConnect' || connector.id === 'metaMaskSDK') {
199
- connector.emitter.off('message', handleMessage)
242
+ if (connector.id === 'metaMaskSDK') {
243
+ connector.emitter.off('message', handleMetaMaskMessage)
244
+ } else if (connector.id === 'walletConnect') {
245
+ connector.emitter.off('message', handleWcMessage)
200
246
  }
201
247
  }
202
248
  }
@@ -204,15 +250,20 @@ const login = async (connector: Connector) => {
204
250
  const onModalClosed = () => {
205
251
  errorMessage.value = ''
206
252
  isConnecting.value = false
207
- walletConnectUri.value = ''
253
+ connectingWallet.value = ''
208
254
  metaMaskUri.value = ''
255
+ walletConnectUri.value = ''
256
+ safeDeepLink.value = false
209
257
  }
210
258
 
211
259
  const check = () =>
212
260
  isConnected.value
213
261
  ? emit('connected', { address: address.value })
214
262
  : emit('disconnected')
215
- watch(isConnected, () => check())
263
+ watch(isConnected, () => {
264
+ check()
265
+ if (!isConnected.value) onModalClosed()
266
+ })
216
267
  onMounted(() => check())
217
268
  </script>
218
269
 
@@ -5,17 +5,15 @@
5
5
  <div class="qr-frame">
6
6
  <canvas ref="qrCanvas"></canvas>
7
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>
8
+ <Button
9
+ @click="copyUri"
10
+ class="copy-uri tertiary small"
11
+ >
12
+ <Icon :type="isCopied ? 'check' : 'copy'" />
13
+ <span>
14
+ {{ isCopied ? 'Copied' : 'Copy Link' }}
15
+ </span>
16
+ </Button>
19
17
  </template>
20
18
 
21
19
  <script setup lang="ts">
@@ -81,36 +79,8 @@ p {
81
79
  }
82
80
  }
83
81
 
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
- }
82
+ .copy-uri {
83
+ width: fit-content;
84
+ margin: 0 auto;
115
85
  }
116
86
  </style>