@lukso/up-connector 0.4.0-dev.a8c9315
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/LICENSE +201 -0
- package/README.md +280 -0
- package/dist/account-modal.cjs +9 -0
- package/dist/account-modal.cjs.map +1 -0
- package/dist/account-modal.d.cts +16 -0
- package/dist/account-modal.d.ts +16 -0
- package/dist/account-modal.js +9 -0
- package/dist/account-modal.js.map +1 -0
- package/dist/auto-setup.cjs +17 -0
- package/dist/auto-setup.cjs.map +1 -0
- package/dist/auto-setup.d.cts +123 -0
- package/dist/auto-setup.d.ts +123 -0
- package/dist/auto-setup.js +17 -0
- package/dist/auto-setup.js.map +1 -0
- package/dist/avatar-CmUCtW_w.d.cts +205 -0
- package/dist/avatar-CmUCtW_w.d.ts +205 -0
- package/dist/avatar.cjs +12 -0
- package/dist/avatar.cjs.map +1 -0
- package/dist/avatar.d.cts +1 -0
- package/dist/avatar.d.ts +1 -0
- package/dist/avatar.js +12 -0
- package/dist/avatar.js.map +1 -0
- package/dist/backup-modal.cjs +9 -0
- package/dist/backup-modal.cjs.map +1 -0
- package/dist/backup-modal.d.cts +41 -0
- package/dist/backup-modal.d.ts +41 -0
- package/dist/backup-modal.js +9 -0
- package/dist/backup-modal.js.map +1 -0
- package/dist/chunk-3SGSPHOZ.js +595 -0
- package/dist/chunk-3SGSPHOZ.js.map +1 -0
- package/dist/chunk-6AYZOIFY.js +181 -0
- package/dist/chunk-6AYZOIFY.js.map +1 -0
- package/dist/chunk-6N35TCFT.js +852 -0
- package/dist/chunk-6N35TCFT.js.map +1 -0
- package/dist/chunk-7ETKG6KR.cjs +387 -0
- package/dist/chunk-7ETKG6KR.cjs.map +1 -0
- package/dist/chunk-EUXUH3YW.js +15 -0
- package/dist/chunk-EUXUH3YW.js.map +1 -0
- package/dist/chunk-GFVUWAG4.cjs +158 -0
- package/dist/chunk-GFVUWAG4.cjs.map +1 -0
- package/dist/chunk-IAKQFHFD.cjs +595 -0
- package/dist/chunk-IAKQFHFD.cjs.map +1 -0
- package/dist/chunk-MH7MP7XK.cjs +181 -0
- package/dist/chunk-MH7MP7XK.cjs.map +1 -0
- package/dist/chunk-NWCNJSG3.js +387 -0
- package/dist/chunk-NWCNJSG3.js.map +1 -0
- package/dist/chunk-NXU2DQAV.js +1128 -0
- package/dist/chunk-NXU2DQAV.js.map +1 -0
- package/dist/chunk-ORJK2YGG.cjs +852 -0
- package/dist/chunk-ORJK2YGG.cjs.map +1 -0
- package/dist/chunk-RFA6SEIS.cjs +1128 -0
- package/dist/chunk-RFA6SEIS.cjs.map +1 -0
- package/dist/chunk-XGIT7YUY.js +31 -0
- package/dist/chunk-XGIT7YUY.js.map +1 -0
- package/dist/chunk-XOKG3KIL.cjs +31 -0
- package/dist/chunk-XOKG3KIL.cjs.map +1 -0
- package/dist/chunk-YIWSPI4I.js +158 -0
- package/dist/chunk-YIWSPI4I.js.map +1 -0
- package/dist/chunk-ZBDE64SD.cjs +15 -0
- package/dist/chunk-ZBDE64SD.cjs.map +1 -0
- package/dist/connect-modal/index.cjs +20 -0
- package/dist/connect-modal/index.cjs.map +1 -0
- package/dist/connect-modal/index.d.cts +9 -0
- package/dist/connect-modal/index.d.ts +9 -0
- package/dist/connect-modal/index.js +20 -0
- package/dist/connect-modal/index.js.map +1 -0
- package/dist/index-D2orHGFi.d.cts +8 -0
- package/dist/index-D2orHGFi.d.ts +8 -0
- package/dist/index.cjs +793 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +189 -0
- package/dist/index.d.ts +189 -0
- package/dist/index.js +793 -0
- package/dist/index.js.map +1 -0
- package/dist/restore-modal.cjs +9 -0
- package/dist/restore-modal.cjs.map +1 -0
- package/dist/restore-modal.d.cts +68 -0
- package/dist/restore-modal.d.ts +68 -0
- package/dist/restore-modal.js +9 -0
- package/dist/restore-modal.js.map +1 -0
- package/dist/wagmi-CVuDs_0h.d.cts +386 -0
- package/dist/wagmi-CVuDs_0h.d.ts +386 -0
- package/package.json +158 -0
- package/src/account-modal.ts +142 -0
- package/src/auto-setup.ts +362 -0
- package/src/avatar.ts +1135 -0
- package/src/backup-modal.ts +439 -0
- package/src/connect-modal/components/connection-view.ts +398 -0
- package/src/connect-modal/components/eoa-connection-view.ts +408 -0
- package/src/connect-modal/components/qr-code-view.ts +71 -0
- package/src/connect-modal/connect-modal.base.ts +18 -0
- package/src/connect-modal/connect-modal.config.ts +27 -0
- package/src/connect-modal/connect-modal.templates.ts +21 -0
- package/src/connect-modal/connect-modal.ts +270 -0
- package/src/connect-modal/connect-modal.types.ts +104 -0
- package/src/connect-modal/images/up-cube-glass.png +0 -0
- package/src/connect-modal/index.ts +23 -0
- package/src/connect-modal/services/wagmi.ts +266 -0
- package/src/connect-modal/styles/styles.css +1 -0
- package/src/connect-modal/utils/walletConnectDeepLinkUrl.ts +43 -0
- package/src/connector.ts +544 -0
- package/src/index.ts +62 -0
- package/src/popup-instance.ts +537 -0
- package/src/restore-modal.ts +702 -0
- package/src/styles/index.ts +28 -0
- package/src/styles/styles.css +1 -0
- package/src/types/css-raw.d.ts +4 -0
- package/src/types/images.d.ts +4 -0
- package/src/types.ts +168 -0
package/src/avatar.ts
ADDED
|
@@ -0,0 +1,1135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Draggable Avatar Component for UP Connector
|
|
3
|
+
* Integrates with @lukso/transaction-view-core IconView
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { AddressData, AvatarOptions } from './types.js'
|
|
7
|
+
|
|
8
|
+
const DEFAULT_AVATAR_OPTIONS: Required<
|
|
9
|
+
Omit<
|
|
10
|
+
AvatarOptions,
|
|
11
|
+
| 'address'
|
|
12
|
+
| 'resolved'
|
|
13
|
+
| 'label'
|
|
14
|
+
| 'chainId'
|
|
15
|
+
| 'componentLoader'
|
|
16
|
+
| 'onPositionChange'
|
|
17
|
+
| 'onHide'
|
|
18
|
+
| 'onShow'
|
|
19
|
+
| 'onClick'
|
|
20
|
+
| 'onAddressClick'
|
|
21
|
+
| 'onTransactionStart'
|
|
22
|
+
| 'onTransactionComplete'
|
|
23
|
+
>
|
|
24
|
+
> = {
|
|
25
|
+
// IconView integration options
|
|
26
|
+
size: 'medium',
|
|
27
|
+
forceUnresolved: false,
|
|
28
|
+
|
|
29
|
+
// Fallback options
|
|
30
|
+
fallbackColor: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
|
31
|
+
fallbackText: '?',
|
|
32
|
+
fallbackImage: '',
|
|
33
|
+
|
|
34
|
+
// Avatar container options
|
|
35
|
+
avatarSize: 'medium' as const,
|
|
36
|
+
|
|
37
|
+
// Behavior options
|
|
38
|
+
initialPosition: 'top-left',
|
|
39
|
+
hideThreshold: 20,
|
|
40
|
+
hideOffset: 30,
|
|
41
|
+
|
|
42
|
+
// Animation options
|
|
43
|
+
transitionDuration: '0.3s',
|
|
44
|
+
transitionEasing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
|
45
|
+
|
|
46
|
+
// Container
|
|
47
|
+
container: typeof document !== 'undefined' ? document.body : (null as any),
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface SnapPosition {
|
|
51
|
+
x: number
|
|
52
|
+
y: number
|
|
53
|
+
side: 'left' | 'right'
|
|
54
|
+
name: string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class DraggableAvatar {
|
|
58
|
+
private options: AvatarOptions & typeof DEFAULT_AVATAR_OPTIONS
|
|
59
|
+
private overlay!: HTMLElement
|
|
60
|
+
private element!: HTMLElement
|
|
61
|
+
private iconViewElement?: HTMLElement
|
|
62
|
+
private snapPreviews: Map<string, HTMLElement> = new Map()
|
|
63
|
+
|
|
64
|
+
// Drag state
|
|
65
|
+
private isDragging = false
|
|
66
|
+
private hasDragged = false
|
|
67
|
+
private startX = 0
|
|
68
|
+
private startY = 0
|
|
69
|
+
private initialX = 0
|
|
70
|
+
private initialY = 0
|
|
71
|
+
private dragThreshold = 15 // pixels - movement below this is still considered a click
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Convert size name to pixel value
|
|
75
|
+
* Based on Tailwind classes: x-small=24px, small=40px, medium=56px, large=80px, x-large=96px, 2x-large=120px
|
|
76
|
+
*/
|
|
77
|
+
private getPixelSize(
|
|
78
|
+
size: 'x-small' | 'small' | 'medium' | 'large' | 'x-large' | '2x-large'
|
|
79
|
+
): number {
|
|
80
|
+
switch (size) {
|
|
81
|
+
case 'x-small':
|
|
82
|
+
return 24
|
|
83
|
+
case 'small':
|
|
84
|
+
return 40
|
|
85
|
+
case 'medium':
|
|
86
|
+
return 56
|
|
87
|
+
case 'large':
|
|
88
|
+
return 80
|
|
89
|
+
case 'x-large':
|
|
90
|
+
return 96
|
|
91
|
+
case '2x-large':
|
|
92
|
+
return 120
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get identicon overhang for a given profile size
|
|
98
|
+
* The identicon hangs off the bottom-right corner by half its size
|
|
99
|
+
*/
|
|
100
|
+
private getIdenticonOverhang(
|
|
101
|
+
profileSize:
|
|
102
|
+
| 'x-small'
|
|
103
|
+
| 'small'
|
|
104
|
+
| 'medium'
|
|
105
|
+
| 'large'
|
|
106
|
+
| 'x-large'
|
|
107
|
+
| '2x-large'
|
|
108
|
+
): number {
|
|
109
|
+
switch (profileSize) {
|
|
110
|
+
case 'x-small':
|
|
111
|
+
return 6 // w-3 = 12px, overhang = 6px
|
|
112
|
+
case 'small':
|
|
113
|
+
return 8 // w-4 = 16px, overhang = 8px
|
|
114
|
+
case 'medium':
|
|
115
|
+
return 10 // w-5 = 20px, overhang = 10px
|
|
116
|
+
case 'large':
|
|
117
|
+
return 12 // w-6 = 24px, overhang = 12px
|
|
118
|
+
case 'x-large':
|
|
119
|
+
return 14 // w-7 = 28px, overhang = 14px
|
|
120
|
+
case '2x-large':
|
|
121
|
+
return 18 // w-9 = 36px, overhang = 18px
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Position state
|
|
126
|
+
private currentPosition?: SnapPosition
|
|
127
|
+
private isHidden = false
|
|
128
|
+
private iconViewAvailable = false
|
|
129
|
+
|
|
130
|
+
// Animation state
|
|
131
|
+
private isThrobbing = false
|
|
132
|
+
private throbAnimation?: Animation
|
|
133
|
+
|
|
134
|
+
constructor(options: AvatarOptions = {}) {
|
|
135
|
+
this.options = { ...DEFAULT_AVATAR_OPTIONS, ...options }
|
|
136
|
+
|
|
137
|
+
// Validate options
|
|
138
|
+
if (
|
|
139
|
+
!this.options.address &&
|
|
140
|
+
!this.options.fallbackText &&
|
|
141
|
+
!this.options.fallbackImage
|
|
142
|
+
) {
|
|
143
|
+
console.warn(
|
|
144
|
+
'DraggableAvatar: No address, fallbackText, or fallbackImage provided. Avatar may be empty.'
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
this.init()
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private async init(): Promise<void> {
|
|
152
|
+
this.createStyles()
|
|
153
|
+
await this.checkIconViewAvailability()
|
|
154
|
+
this.createElement()
|
|
155
|
+
this.attachEventListeners()
|
|
156
|
+
this.setInitialPosition()
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Safely check for and load IconView component
|
|
161
|
+
*/
|
|
162
|
+
private async checkIconViewAvailability(): Promise<boolean> {
|
|
163
|
+
// First check if already registered
|
|
164
|
+
if (
|
|
165
|
+
typeof customElements !== 'undefined' &&
|
|
166
|
+
customElements.get('icon-view')
|
|
167
|
+
) {
|
|
168
|
+
this.iconViewAvailable = true
|
|
169
|
+
return true
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Try to load if not available and we have an address
|
|
173
|
+
if (this.options.address && !this.iconViewAvailable) {
|
|
174
|
+
try {
|
|
175
|
+
// Try custom loader first
|
|
176
|
+
if (this.options.componentLoader) {
|
|
177
|
+
await this.options.componentLoader()
|
|
178
|
+
}
|
|
179
|
+
// Try global loader
|
|
180
|
+
else if (
|
|
181
|
+
typeof window !== 'undefined' &&
|
|
182
|
+
(window as any).loadTransactionViewComponents
|
|
183
|
+
) {
|
|
184
|
+
await (window as any).loadTransactionViewComponents()
|
|
185
|
+
}
|
|
186
|
+
// Try direct import as fallback
|
|
187
|
+
else {
|
|
188
|
+
try {
|
|
189
|
+
await import('@lukso/transaction-view-core')
|
|
190
|
+
} catch (importError) {
|
|
191
|
+
console.warn(
|
|
192
|
+
'Failed to import @lukso/transaction-view-core:',
|
|
193
|
+
importError
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
this.iconViewAvailable =
|
|
199
|
+
typeof customElements !== 'undefined' &&
|
|
200
|
+
customElements.get('icon-view') !== undefined
|
|
201
|
+
} catch (error) {
|
|
202
|
+
console.warn('Failed to load IconView component:', error)
|
|
203
|
+
this.iconViewAvailable = false
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return this.iconViewAvailable
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private createStyles(): void {
|
|
211
|
+
if (typeof document === 'undefined') return
|
|
212
|
+
|
|
213
|
+
// Calculate sizes before template literal
|
|
214
|
+
const avatarPixelSize = this.getPixelSize(this.options.avatarSize)
|
|
215
|
+
const overhang = this.getIdenticonOverhang(this.options.avatarSize)
|
|
216
|
+
|
|
217
|
+
// Debug: Log our calculations
|
|
218
|
+
|
|
219
|
+
// Remove existing styles to allow updates
|
|
220
|
+
const existingStyles = document.querySelector('#up-connector-avatar-styles')
|
|
221
|
+
if (existingStyles) {
|
|
222
|
+
existingStyles.remove()
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const style = document.createElement('style')
|
|
226
|
+
style.id = 'up-connector-avatar-styles'
|
|
227
|
+
style.textContent = `
|
|
228
|
+
.up-avatar-overlay {
|
|
229
|
+
position: fixed;
|
|
230
|
+
top: 0;
|
|
231
|
+
left: 0;
|
|
232
|
+
width: 100vw;
|
|
233
|
+
height: 100vh;
|
|
234
|
+
pointer-events: none;
|
|
235
|
+
z-index: 9999;
|
|
236
|
+
overflow: hidden;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.up-avatar {
|
|
240
|
+
position: absolute;
|
|
241
|
+
width: ${avatarPixelSize}px;
|
|
242
|
+
height: ${avatarPixelSize}px;
|
|
243
|
+
border-radius: 50%;
|
|
244
|
+
cursor: move;
|
|
245
|
+
z-index: 1000;
|
|
246
|
+
overflow: visible;
|
|
247
|
+
display: flex;
|
|
248
|
+
align-items: center;
|
|
249
|
+
justify-content: center;
|
|
250
|
+
user-select: none;
|
|
251
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2), 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
252
|
+
transition: all ${this.options.transitionDuration} ${this.options.transitionEasing};
|
|
253
|
+
touch-action: none;
|
|
254
|
+
overflow: visible;
|
|
255
|
+
background: ${this.options.fallbackColor};
|
|
256
|
+
border: 3px solid rgba(255, 255, 255, 0.5) !important;
|
|
257
|
+
outline: none !important;
|
|
258
|
+
backdrop-filter: blur(10px);
|
|
259
|
+
pointer-events: auto;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.up-avatar * {
|
|
263
|
+
user-select: none !important;
|
|
264
|
+
pointer-events: none !important;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.up-avatar img {
|
|
268
|
+
draggable: false !important;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.up-avatar:hover {
|
|
272
|
+
transform: scale(1.05);
|
|
273
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25), 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.up-avatar:focus {
|
|
277
|
+
outline: none !important;
|
|
278
|
+
border: 3px solid rgba(255, 255, 255, 0.6) !important;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.up-avatar.dragging {
|
|
282
|
+
transform: scale(1.1);
|
|
283
|
+
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.3), 0 6px 16px rgba(0, 0, 0, 0.2);
|
|
284
|
+
cursor: grabbing;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.up-avatar.hidden-left {
|
|
288
|
+
left: -${this.options.hideOffset}px !important;
|
|
289
|
+
opacity: 0.8;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.up-avatar.hidden-right {
|
|
293
|
+
right: -${this.options.hideOffset}px !important;
|
|
294
|
+
left: auto !important;
|
|
295
|
+
opacity: 0.8;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.up-avatar.hidden-left:hover,
|
|
299
|
+
.up-avatar.hidden-right:hover {
|
|
300
|
+
transform: translateX(0) scale(1.05);
|
|
301
|
+
opacity: 1;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.up-avatar.hidden-left:hover {
|
|
305
|
+
left: -12px !important;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.up-avatar.hidden-right:hover {
|
|
309
|
+
right: -12px !important;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/* IconView positioned to allow identicon to extend beyond border */
|
|
313
|
+
.up-avatar icon-view {
|
|
314
|
+
position: absolute !important;
|
|
315
|
+
top: 0 !important;
|
|
316
|
+
left: 0 !important;
|
|
317
|
+
width: ${avatarPixelSize}px !important;
|
|
318
|
+
height: ${avatarPixelSize}px !important;
|
|
319
|
+
display: block !important;
|
|
320
|
+
z-index: 1 !important;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/* Additional auto-sizing for IconView components */
|
|
324
|
+
.up-avatar icon-view * {
|
|
325
|
+
max-width: ${avatarPixelSize}px !important;
|
|
326
|
+
max-height: ${avatarPixelSize}px !important;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/* Let IconView handle its own internal sizing */
|
|
330
|
+
|
|
331
|
+
.up-avatar icon-view img,
|
|
332
|
+
.up-avatar icon-view .icon,
|
|
333
|
+
.up-avatar icon-view .profile-image {
|
|
334
|
+
max-width: 100% !important;
|
|
335
|
+
max-height: 100% !important;
|
|
336
|
+
width: auto !important;
|
|
337
|
+
height: auto !important;
|
|
338
|
+
object-fit: cover;
|
|
339
|
+
draggable: false !important;
|
|
340
|
+
user-select: none !important;
|
|
341
|
+
pointer-events: none !important;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/* Fallback content */
|
|
345
|
+
.up-avatar .fallback-content {
|
|
346
|
+
width: 100%;
|
|
347
|
+
height: 100%;
|
|
348
|
+
display: flex;
|
|
349
|
+
align-items: center;
|
|
350
|
+
justify-content: center;
|
|
351
|
+
color: white;
|
|
352
|
+
font-weight: 600;
|
|
353
|
+
font-size: ${Math.floor(avatarPixelSize * 0.4)}px;
|
|
354
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
355
|
+
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.up-avatar .fallback-content img {
|
|
359
|
+
width: 100%;
|
|
360
|
+
height: 100%;
|
|
361
|
+
border-radius: 50%;
|
|
362
|
+
object-fit: cover;
|
|
363
|
+
draggable: false !important;
|
|
364
|
+
user-select: none !important;
|
|
365
|
+
pointer-events: none !important;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/* Snap preview */
|
|
369
|
+
.up-avatar-preview {
|
|
370
|
+
position: absolute;
|
|
371
|
+
width: ${avatarPixelSize}px;
|
|
372
|
+
height: ${avatarPixelSize}px;
|
|
373
|
+
border-radius: 50%;
|
|
374
|
+
z-index: 999;
|
|
375
|
+
pointer-events: none;
|
|
376
|
+
background: rgba(102, 126, 234, 0.1);
|
|
377
|
+
backdrop-filter: blur(5px);
|
|
378
|
+
box-shadow:
|
|
379
|
+
0 0 0 2px rgba(255, 255, 255, 0.3),
|
|
380
|
+
0 0 0 4px rgba(102, 126, 234, 0.3),
|
|
381
|
+
0 2px 8px rgba(0, 0, 0, 0.2);
|
|
382
|
+
opacity: 0.5;
|
|
383
|
+
transition: opacity 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
.up-avatar-preview.active {
|
|
387
|
+
background: rgba(102, 126, 234, 0.2);
|
|
388
|
+
box-shadow:
|
|
389
|
+
0 0 0 2px white,
|
|
390
|
+
0 0 0 4px #667eea,
|
|
391
|
+
0 4px 12px rgba(0, 0, 0, 0.3);
|
|
392
|
+
opacity: 1;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/* Hide IconView labels in avatar mode */
|
|
396
|
+
.up-avatar icon-view .label {
|
|
397
|
+
display: none;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/* Connection indicator */
|
|
401
|
+
.up-avatar::after {
|
|
402
|
+
content: '';
|
|
403
|
+
position: absolute;
|
|
404
|
+
bottom: 2px;
|
|
405
|
+
right: 2px;
|
|
406
|
+
width: 16px;
|
|
407
|
+
height: 16px;
|
|
408
|
+
border-radius: 50%;
|
|
409
|
+
background: #4ade80;
|
|
410
|
+
border: 2px solid white;
|
|
411
|
+
opacity: 0;
|
|
412
|
+
transform: scale(0);
|
|
413
|
+
transition: all 0.2s ease;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.up-avatar.connected::after {
|
|
417
|
+
opacity: 1;
|
|
418
|
+
transform: scale(1);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/* Throb animation for transactions */
|
|
422
|
+
.up-avatar.throbbing {
|
|
423
|
+
animation: avatar-throb 2s ease-in-out infinite;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
@keyframes avatar-throb {
|
|
427
|
+
0%, 100% {
|
|
428
|
+
transform: scale(1);
|
|
429
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
430
|
+
border-color: rgba(255, 255, 255, 0.1);
|
|
431
|
+
}
|
|
432
|
+
50% {
|
|
433
|
+
transform: scale(1.1);
|
|
434
|
+
box-shadow:
|
|
435
|
+
0 8px 24px rgba(0, 0, 0, 0.3),
|
|
436
|
+
0 0 0 4px rgba(102, 126, 234, 0.4),
|
|
437
|
+
0 0 0 8px rgba(102, 126, 234, 0.2),
|
|
438
|
+
0 0 20px rgba(102, 126, 234, 0.3);
|
|
439
|
+
border-color: rgba(102, 126, 234, 0.8);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
.up-avatar.throbbing::before {
|
|
444
|
+
content: '';
|
|
445
|
+
position: absolute;
|
|
446
|
+
top: -4px;
|
|
447
|
+
left: -4px;
|
|
448
|
+
right: -4px;
|
|
449
|
+
bottom: -4px;
|
|
450
|
+
border: 2px solid transparent;
|
|
451
|
+
border-radius: 50%;
|
|
452
|
+
background: conic-gradient(from 0deg, transparent, #667eea, #764ba2, transparent);
|
|
453
|
+
background-size: 200% 200%;
|
|
454
|
+
animation: avatar-rotate 1.5s linear infinite;
|
|
455
|
+
z-index: -1;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
@keyframes avatar-rotate {
|
|
459
|
+
0% { transform: rotate(0deg); }
|
|
460
|
+
100% { transform: rotate(360deg); }
|
|
461
|
+
}
|
|
462
|
+
`
|
|
463
|
+
document.head.appendChild(style)
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
private createElement(): void {
|
|
467
|
+
if (typeof document === 'undefined') return
|
|
468
|
+
|
|
469
|
+
// Create fixed overlay container
|
|
470
|
+
this.overlay = document.createElement('div')
|
|
471
|
+
this.overlay.className = 'up-avatar-overlay'
|
|
472
|
+
|
|
473
|
+
// Create avatar element
|
|
474
|
+
this.element = document.createElement('div')
|
|
475
|
+
this.element.className = 'up-avatar'
|
|
476
|
+
this.element.setAttribute('role', 'button')
|
|
477
|
+
this.element.setAttribute('tabindex', '0')
|
|
478
|
+
this.element.setAttribute('aria-label', 'Universal Profile avatar')
|
|
479
|
+
|
|
480
|
+
this.createContent()
|
|
481
|
+
|
|
482
|
+
// Append avatar to overlay, then overlay to container
|
|
483
|
+
this.overlay.appendChild(this.element)
|
|
484
|
+
this.options.container.appendChild(this.overlay)
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
private createContent(): void {
|
|
488
|
+
if (!this.element) return
|
|
489
|
+
|
|
490
|
+
// Clear existing content
|
|
491
|
+
this.element.innerHTML = ''
|
|
492
|
+
|
|
493
|
+
if (this.options.address && this.iconViewAvailable) {
|
|
494
|
+
// Use IconView component
|
|
495
|
+
this.iconViewElement = document.createElement('icon-view')
|
|
496
|
+
this.iconViewElement.setAttribute('address', this.options.address)
|
|
497
|
+
|
|
498
|
+
if (this.options.chainId) {
|
|
499
|
+
this.iconViewElement.setAttribute(
|
|
500
|
+
'chain-id',
|
|
501
|
+
this.options.chainId.toString()
|
|
502
|
+
)
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (this.options.resolved) {
|
|
506
|
+
;(this.iconViewElement as any).resolved = this.options.resolved
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Set size - avatar size now matches IconView sizes exactly
|
|
510
|
+
this.iconViewElement.setAttribute('size', this.options.avatarSize)
|
|
511
|
+
|
|
512
|
+
// Let CSS handle the positioning and sizing
|
|
513
|
+
|
|
514
|
+
if (this.options.label) {
|
|
515
|
+
this.iconViewElement.setAttribute('label', this.options.label)
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (this.options.forceUnresolved) {
|
|
519
|
+
this.iconViewElement.setAttribute('force-unresolved', '')
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Listen for address-click events
|
|
523
|
+
this.iconViewElement.addEventListener('address-click', (event) => {
|
|
524
|
+
event.stopPropagation()
|
|
525
|
+
if (this.options.onAddressClick) {
|
|
526
|
+
this.options.onAddressClick(event as CustomEvent)
|
|
527
|
+
}
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
this.element.appendChild(this.iconViewElement)
|
|
531
|
+
} else {
|
|
532
|
+
// Use fallback content
|
|
533
|
+
this.createFallbackContent()
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
private createFallbackContent(): void {
|
|
538
|
+
if (!this.element) return
|
|
539
|
+
|
|
540
|
+
const fallbackDiv = document.createElement('div')
|
|
541
|
+
fallbackDiv.className = 'fallback-content'
|
|
542
|
+
|
|
543
|
+
if (this.options.fallbackImage) {
|
|
544
|
+
const img = document.createElement('img')
|
|
545
|
+
img.src = this.options.fallbackImage
|
|
546
|
+
img.alt = 'Avatar'
|
|
547
|
+
img.onerror = () => {
|
|
548
|
+
// Fallback to text if image fails to load
|
|
549
|
+
img.remove()
|
|
550
|
+
fallbackDiv.textContent = this.options.fallbackText
|
|
551
|
+
}
|
|
552
|
+
fallbackDiv.appendChild(img)
|
|
553
|
+
} else {
|
|
554
|
+
fallbackDiv.textContent = this.options.fallbackText
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
this.element.appendChild(fallbackDiv)
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
private attachEventListeners(): void {
|
|
561
|
+
if (!this.element) return
|
|
562
|
+
|
|
563
|
+
// Mouse events
|
|
564
|
+
this.element.addEventListener('mousedown', this.handleMouseDown.bind(this))
|
|
565
|
+
document.addEventListener('mousemove', this.handleMouseMove.bind(this))
|
|
566
|
+
document.addEventListener('mouseup', this.handleMouseUp.bind(this))
|
|
567
|
+
|
|
568
|
+
// Touch events
|
|
569
|
+
this.element.addEventListener(
|
|
570
|
+
'touchstart',
|
|
571
|
+
this.handleTouchStart.bind(this),
|
|
572
|
+
{ passive: false }
|
|
573
|
+
)
|
|
574
|
+
document.addEventListener('touchmove', this.handleTouchMove.bind(this), {
|
|
575
|
+
passive: false,
|
|
576
|
+
})
|
|
577
|
+
document.addEventListener('touchend', this.handleTouchEnd.bind(this), {
|
|
578
|
+
passive: false,
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
// Click events
|
|
582
|
+
this.element.addEventListener('click', this.handleClick.bind(this))
|
|
583
|
+
|
|
584
|
+
// Window resize
|
|
585
|
+
window.addEventListener('resize', this.handleResize.bind(this))
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
private getSnapPositions(): SnapPosition[] {
|
|
589
|
+
const margin = 20
|
|
590
|
+
const size = this.getPixelSize(this.options.avatarSize)
|
|
591
|
+
|
|
592
|
+
const positions = [
|
|
593
|
+
{ x: margin, y: margin, side: 'left' as const, name: 'top-left' },
|
|
594
|
+
{
|
|
595
|
+
x: window.innerWidth - size - margin,
|
|
596
|
+
y: margin,
|
|
597
|
+
side: 'right' as const,
|
|
598
|
+
name: 'top-right',
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
x: margin,
|
|
602
|
+
y: window.innerHeight - size - margin,
|
|
603
|
+
side: 'left' as const,
|
|
604
|
+
name: 'bottom-left',
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
x: window.innerWidth - size - margin,
|
|
608
|
+
y: window.innerHeight - size - margin,
|
|
609
|
+
side: 'right' as const,
|
|
610
|
+
name: 'bottom-right',
|
|
611
|
+
},
|
|
612
|
+
]
|
|
613
|
+
|
|
614
|
+
return positions
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
private findClosestSnapPosition(x: number, y: number): SnapPosition {
|
|
618
|
+
const positions = this.getSnapPositions()
|
|
619
|
+
let closest = positions[0]
|
|
620
|
+
let minDistance = Infinity
|
|
621
|
+
|
|
622
|
+
positions.forEach((pos) => {
|
|
623
|
+
const distance = Math.sqrt(
|
|
624
|
+
Math.pow(x - pos.x, 2) + Math.pow(y - pos.y, 2)
|
|
625
|
+
)
|
|
626
|
+
if (distance < minDistance) {
|
|
627
|
+
minDistance = distance
|
|
628
|
+
closest = pos
|
|
629
|
+
}
|
|
630
|
+
})
|
|
631
|
+
|
|
632
|
+
return closest
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
private setInitialPosition(): void {
|
|
636
|
+
const positions = this.getSnapPositions()
|
|
637
|
+
const initialPos =
|
|
638
|
+
positions.find((p) => p.name === this.options.initialPosition) ||
|
|
639
|
+
positions[0]
|
|
640
|
+
this.snapToPosition(initialPos)
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
private snapToPosition(position: SnapPosition, shouldHide = false): void {
|
|
644
|
+
if (!this.element) return
|
|
645
|
+
|
|
646
|
+
this.element.className = 'up-avatar'
|
|
647
|
+
this.currentPosition = position
|
|
648
|
+
this.isHidden = shouldHide
|
|
649
|
+
|
|
650
|
+
if (shouldHide) {
|
|
651
|
+
if (position.side === 'left') {
|
|
652
|
+
this.element.classList.add('hidden-left')
|
|
653
|
+
this.element.style.left = `-${this.options.hideOffset}px`
|
|
654
|
+
this.element.style.right = 'auto'
|
|
655
|
+
this.element.style.top = position.y + 'px'
|
|
656
|
+
this.options.onHide?.()
|
|
657
|
+
} else if (position.side === 'right') {
|
|
658
|
+
this.element.classList.add('hidden-right')
|
|
659
|
+
this.element.style.right = `-${this.options.hideOffset}px`
|
|
660
|
+
this.element.style.left = 'auto'
|
|
661
|
+
this.element.style.top = position.y + 'px'
|
|
662
|
+
this.options.onHide?.()
|
|
663
|
+
}
|
|
664
|
+
} else {
|
|
665
|
+
this.element.style.left = position.x + 'px'
|
|
666
|
+
this.element.style.right = 'auto'
|
|
667
|
+
this.element.style.top = position.y + 'px'
|
|
668
|
+
if (this.isHidden) {
|
|
669
|
+
this.options.onShow?.()
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
this.options.onPositionChange?.(position.name)
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
private showAllSnapPreviews(): void {
|
|
677
|
+
if (typeof document === 'undefined') return
|
|
678
|
+
|
|
679
|
+
const positions = this.getSnapPositions()
|
|
680
|
+
|
|
681
|
+
// Create preview for each position if it doesn't exist
|
|
682
|
+
positions.forEach((pos) => {
|
|
683
|
+
if (!this.snapPreviews.has(pos.name)) {
|
|
684
|
+
const preview = document.createElement('div')
|
|
685
|
+
preview.className = 'up-avatar-preview'
|
|
686
|
+
preview.style.left = pos.x + 'px'
|
|
687
|
+
preview.style.top = pos.y + 'px'
|
|
688
|
+
this.overlay.appendChild(preview)
|
|
689
|
+
this.snapPreviews.set(pos.name, preview)
|
|
690
|
+
}
|
|
691
|
+
})
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
private updateActiveSnapPreview(
|
|
695
|
+
activePosition: SnapPosition,
|
|
696
|
+
shouldHide = false
|
|
697
|
+
): void {
|
|
698
|
+
if (typeof document === 'undefined') return
|
|
699
|
+
|
|
700
|
+
// Determine the active position key
|
|
701
|
+
let activeKey = activePosition.name
|
|
702
|
+
|
|
703
|
+
// Update all previews
|
|
704
|
+
this.snapPreviews.forEach((preview, key) => {
|
|
705
|
+
if (key === activeKey) {
|
|
706
|
+
// Highlight the active one
|
|
707
|
+
preview.classList.add('active')
|
|
708
|
+
|
|
709
|
+
// Update position for hidden state
|
|
710
|
+
if (shouldHide) {
|
|
711
|
+
if (activePosition.side === 'left') {
|
|
712
|
+
preview.style.left = `-${this.options.hideOffset}px`
|
|
713
|
+
preview.style.right = 'auto'
|
|
714
|
+
preview.style.top = activePosition.y + 'px'
|
|
715
|
+
} else if (activePosition.side === 'right') {
|
|
716
|
+
preview.style.right = `-${this.options.hideOffset}px`
|
|
717
|
+
preview.style.left = 'auto'
|
|
718
|
+
preview.style.top = activePosition.y + 'px'
|
|
719
|
+
}
|
|
720
|
+
} else {
|
|
721
|
+
preview.style.left = activePosition.x + 'px'
|
|
722
|
+
preview.style.right = 'auto'
|
|
723
|
+
preview.style.top = activePosition.y + 'px'
|
|
724
|
+
}
|
|
725
|
+
} else {
|
|
726
|
+
// Dim the others
|
|
727
|
+
preview.classList.remove('active')
|
|
728
|
+
|
|
729
|
+
// Reset to normal position (not hidden)
|
|
730
|
+
const pos = this.getSnapPositions().find((p) => p.name === key)
|
|
731
|
+
if (pos) {
|
|
732
|
+
preview.style.left = pos.x + 'px'
|
|
733
|
+
preview.style.right = 'auto'
|
|
734
|
+
preview.style.top = pos.y + 'px'
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
})
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
private hideAllSnapPreviews(): void {
|
|
741
|
+
this.snapPreviews.forEach((preview) => {
|
|
742
|
+
preview.remove()
|
|
743
|
+
})
|
|
744
|
+
this.snapPreviews.clear()
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// Event handlers
|
|
748
|
+
private handleMouseDown(e: MouseEvent): void {
|
|
749
|
+
// Only handle left mouse button (button 0)
|
|
750
|
+
if (e.button !== 0) {
|
|
751
|
+
return
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
this.isDragging = true
|
|
755
|
+
this.hasDragged = false // Reset for new interaction
|
|
756
|
+
this.startX = e.clientX
|
|
757
|
+
this.startY = e.clientY
|
|
758
|
+
|
|
759
|
+
const rect = this.element.getBoundingClientRect()
|
|
760
|
+
this.initialX = rect.left
|
|
761
|
+
this.initialY = rect.top
|
|
762
|
+
|
|
763
|
+
this.element.classList.add('dragging')
|
|
764
|
+
this.element.style.transition = 'none'
|
|
765
|
+
|
|
766
|
+
// Show all snap previews when drag starts
|
|
767
|
+
this.showAllSnapPreviews()
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
private handleMouseMove(e: MouseEvent): void {
|
|
771
|
+
if (!this.isDragging) return
|
|
772
|
+
|
|
773
|
+
e.preventDefault()
|
|
774
|
+
|
|
775
|
+
const currentX = this.initialX + (e.clientX - this.startX)
|
|
776
|
+
const currentY = this.initialY + (e.clientY - this.startY)
|
|
777
|
+
|
|
778
|
+
// Check if movement exceeds threshold
|
|
779
|
+
const deltaX = Math.abs(e.clientX - this.startX)
|
|
780
|
+
const deltaY = Math.abs(e.clientY - this.startY)
|
|
781
|
+
const totalMovement = Math.sqrt(deltaX * deltaX + deltaY * deltaY)
|
|
782
|
+
|
|
783
|
+
if (totalMovement > this.dragThreshold) {
|
|
784
|
+
this.hasDragged = true // Only mark as dragged if we exceed threshold
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// Always update position while dragging (for visual feedback)
|
|
788
|
+
this.element.className = 'up-avatar dragging'
|
|
789
|
+
this.element.style.left = currentX + 'px'
|
|
790
|
+
this.element.style.right = 'auto'
|
|
791
|
+
this.element.style.top = currentY + 'px'
|
|
792
|
+
|
|
793
|
+
const closestPosition = this.findClosestSnapPosition(currentX, currentY)
|
|
794
|
+
const shouldHideLeft = currentX < -this.options.hideThreshold
|
|
795
|
+
const shouldHideRight =
|
|
796
|
+
currentX >
|
|
797
|
+
window.innerWidth -
|
|
798
|
+
this.getPixelSize(this.options.avatarSize) +
|
|
799
|
+
this.options.hideThreshold
|
|
800
|
+
|
|
801
|
+
// Update active preview
|
|
802
|
+
this.updateActiveSnapPreview(
|
|
803
|
+
closestPosition,
|
|
804
|
+
shouldHideLeft || shouldHideRight
|
|
805
|
+
)
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
private handleMouseUp(): void {
|
|
809
|
+
if (this.isDragging) {
|
|
810
|
+
this.isDragging = false
|
|
811
|
+
this.element.classList.remove('dragging')
|
|
812
|
+
this.element.style.transition = `all ${this.options.transitionDuration} ${this.options.transitionEasing}`
|
|
813
|
+
|
|
814
|
+
// Hide all snap previews
|
|
815
|
+
this.hideAllSnapPreviews()
|
|
816
|
+
|
|
817
|
+
const rect = this.element.getBoundingClientRect()
|
|
818
|
+
const currentX = rect.left
|
|
819
|
+
const currentY = rect.top
|
|
820
|
+
|
|
821
|
+
const shouldHideLeft = currentX < -this.options.hideThreshold
|
|
822
|
+
const shouldHideRight =
|
|
823
|
+
currentX >
|
|
824
|
+
window.innerWidth -
|
|
825
|
+
this.getPixelSize(this.options.avatarSize) +
|
|
826
|
+
this.options.hideThreshold
|
|
827
|
+
|
|
828
|
+
const closestPosition = this.findClosestSnapPosition(currentX, currentY)
|
|
829
|
+
this.snapToPosition(closestPosition, shouldHideLeft || shouldHideRight)
|
|
830
|
+
|
|
831
|
+
// Reset hasDragged after click event has had a chance to fire
|
|
832
|
+
setTimeout(() => {
|
|
833
|
+
this.hasDragged = false
|
|
834
|
+
}, 0)
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
private handleTouchStart(e: TouchEvent): void {
|
|
839
|
+
e.preventDefault()
|
|
840
|
+
e.stopPropagation()
|
|
841
|
+
|
|
842
|
+
this.isDragging = true
|
|
843
|
+
this.hasDragged = false // Reset for new interaction
|
|
844
|
+
const touch = e.touches[0]
|
|
845
|
+
this.startX = touch.clientX
|
|
846
|
+
this.startY = touch.clientY
|
|
847
|
+
|
|
848
|
+
const rect = this.element.getBoundingClientRect()
|
|
849
|
+
this.initialX = rect.left
|
|
850
|
+
this.initialY = rect.top
|
|
851
|
+
|
|
852
|
+
this.element.classList.add('dragging')
|
|
853
|
+
this.element.style.transition = 'none'
|
|
854
|
+
|
|
855
|
+
// Show all snap previews when drag starts
|
|
856
|
+
this.showAllSnapPreviews()
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
private handleTouchMove(e: TouchEvent): void {
|
|
860
|
+
if (!this.isDragging) return
|
|
861
|
+
|
|
862
|
+
e.preventDefault()
|
|
863
|
+
e.stopPropagation()
|
|
864
|
+
|
|
865
|
+
const touch = e.touches[0]
|
|
866
|
+
const currentX = this.initialX + (touch.clientX - this.startX)
|
|
867
|
+
const currentY = this.initialY + (touch.clientY - this.startY)
|
|
868
|
+
|
|
869
|
+
// Check if movement exceeds threshold
|
|
870
|
+
const deltaX = Math.abs(touch.clientX - this.startX)
|
|
871
|
+
const deltaY = Math.abs(touch.clientY - this.startY)
|
|
872
|
+
const totalMovement = Math.sqrt(deltaX * deltaX + deltaY * deltaY)
|
|
873
|
+
|
|
874
|
+
if (totalMovement > this.dragThreshold) {
|
|
875
|
+
this.hasDragged = true // Only mark as dragged if we exceed threshold
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// Always update position while dragging (for visual feedback)
|
|
879
|
+
this.element.className = 'up-avatar dragging'
|
|
880
|
+
this.element.style.left = currentX + 'px'
|
|
881
|
+
this.element.style.right = 'auto'
|
|
882
|
+
this.element.style.top = currentY + 'px'
|
|
883
|
+
|
|
884
|
+
const closestPosition = this.findClosestSnapPosition(currentX, currentY)
|
|
885
|
+
const shouldHideLeft = currentX < -this.options.hideThreshold
|
|
886
|
+
const shouldHideRight =
|
|
887
|
+
currentX >
|
|
888
|
+
window.innerWidth -
|
|
889
|
+
this.getPixelSize(this.options.avatarSize) +
|
|
890
|
+
this.options.hideThreshold
|
|
891
|
+
|
|
892
|
+
// Update active preview
|
|
893
|
+
this.updateActiveSnapPreview(
|
|
894
|
+
closestPosition,
|
|
895
|
+
shouldHideLeft || shouldHideRight
|
|
896
|
+
)
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
private handleTouchEnd(e: TouchEvent): void {
|
|
900
|
+
if (this.isDragging) {
|
|
901
|
+
e.preventDefault()
|
|
902
|
+
e.stopPropagation()
|
|
903
|
+
|
|
904
|
+
this.isDragging = false
|
|
905
|
+
this.element.classList.remove('dragging')
|
|
906
|
+
this.element.style.transition = `all ${this.options.transitionDuration} ${this.options.transitionEasing}`
|
|
907
|
+
|
|
908
|
+
// Hide all snap previews
|
|
909
|
+
this.hideAllSnapPreviews()
|
|
910
|
+
|
|
911
|
+
const rect = this.element.getBoundingClientRect()
|
|
912
|
+
const currentX = rect.left
|
|
913
|
+
const currentY = rect.top
|
|
914
|
+
|
|
915
|
+
const shouldHideLeft = currentX < -this.options.hideThreshold
|
|
916
|
+
const shouldHideRight =
|
|
917
|
+
currentX >
|
|
918
|
+
window.innerWidth -
|
|
919
|
+
this.getPixelSize(this.options.avatarSize) +
|
|
920
|
+
this.options.hideThreshold
|
|
921
|
+
|
|
922
|
+
const closestPosition = this.findClosestSnapPosition(currentX, currentY)
|
|
923
|
+
this.snapToPosition(closestPosition, shouldHideLeft || shouldHideRight)
|
|
924
|
+
|
|
925
|
+
// Reset hasDragged after click event has had a chance to fire
|
|
926
|
+
setTimeout(() => {
|
|
927
|
+
this.hasDragged = false
|
|
928
|
+
}, 0)
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
private handleClick(e: MouseEvent): void {
|
|
933
|
+
// Only handle left mouse button (button 0)
|
|
934
|
+
if (e.button !== 0) {
|
|
935
|
+
return
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// Only trigger click if we haven't dragged during this interaction
|
|
939
|
+
if (!this.hasDragged) {
|
|
940
|
+
if (this.options.onClick) {
|
|
941
|
+
// Get resolved data from IconView if available
|
|
942
|
+
const resolvedData = (this.iconViewElement as any)?.resolved || null
|
|
943
|
+
this.options.onClick(e, {
|
|
944
|
+
address: this.options.address,
|
|
945
|
+
resolved: resolvedData,
|
|
946
|
+
})
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
private handleResize(): void {
|
|
952
|
+
if (this.currentPosition) {
|
|
953
|
+
const positions = this.getSnapPositions()
|
|
954
|
+
const newPosition =
|
|
955
|
+
positions.find((p) => p.name === this.currentPosition!.name) ||
|
|
956
|
+
positions[0]
|
|
957
|
+
this.snapToPosition(newPosition, this.isHidden)
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// Public API methods
|
|
962
|
+
public setPosition(positionName: string): void {
|
|
963
|
+
const positions = this.getSnapPositions()
|
|
964
|
+
const position = positions.find((p) => p.name === positionName)
|
|
965
|
+
if (position) {
|
|
966
|
+
this.snapToPosition(position)
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
public hide(): void {
|
|
971
|
+
if (this.currentPosition) {
|
|
972
|
+
this.snapToPosition(this.currentPosition, true)
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
public show(): void {
|
|
977
|
+
if (this.currentPosition) {
|
|
978
|
+
this.snapToPosition(this.currentPosition, false)
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
public setConnected(connected: boolean): void {
|
|
983
|
+
if (this.element) {
|
|
984
|
+
if (connected) {
|
|
985
|
+
this.element.classList.add('connected')
|
|
986
|
+
} else {
|
|
987
|
+
this.element.classList.remove('connected')
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
public updateAddress(
|
|
993
|
+
address: string,
|
|
994
|
+
resolved?: AddressData,
|
|
995
|
+
chainId?: number
|
|
996
|
+
): void {
|
|
997
|
+
this.options.address = address
|
|
998
|
+
this.options.resolved = resolved
|
|
999
|
+
this.options.chainId = chainId
|
|
1000
|
+
|
|
1001
|
+
if (this.iconViewElement) {
|
|
1002
|
+
this.iconViewElement.setAttribute('address', address)
|
|
1003
|
+
|
|
1004
|
+
if (chainId) {
|
|
1005
|
+
this.iconViewElement.setAttribute('chain-id', chainId.toString())
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
if (resolved) {
|
|
1009
|
+
;(this.iconViewElement as any).resolved = resolved
|
|
1010
|
+
}
|
|
1011
|
+
} else {
|
|
1012
|
+
// Recreate content if switching from fallback to IconView
|
|
1013
|
+
this.createContent()
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
public updateResolved(resolved: AddressData): void {
|
|
1018
|
+
this.options.resolved = resolved
|
|
1019
|
+
if (this.iconViewElement) {
|
|
1020
|
+
;(this.iconViewElement as any).resolved = resolved
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
public updateSize(
|
|
1025
|
+
newSize: 'x-small' | 'small' | 'medium' | 'large' | 'x-large' | '2x-large'
|
|
1026
|
+
): void {
|
|
1027
|
+
if (this.options.avatarSize === newSize) return
|
|
1028
|
+
|
|
1029
|
+
this.options.avatarSize = newSize
|
|
1030
|
+
|
|
1031
|
+
// Recreate styles with new size
|
|
1032
|
+
this.createStyles()
|
|
1033
|
+
|
|
1034
|
+
// Update element size using pixel conversion
|
|
1035
|
+
const pixelSize = this.getPixelSize(newSize)
|
|
1036
|
+
if (this.element) {
|
|
1037
|
+
this.element.style.width = `${pixelSize}px`
|
|
1038
|
+
this.element.style.height = `${pixelSize}px`
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// Force IconView to resize if available
|
|
1042
|
+
if (this.iconViewElement) {
|
|
1043
|
+
// Size attribute now matches avatarSize directly
|
|
1044
|
+
this.iconViewElement.setAttribute('size', newSize)
|
|
1045
|
+
|
|
1046
|
+
// Force IconView to re-render with new size
|
|
1047
|
+
const iconView = this.iconViewElement as any
|
|
1048
|
+
if (iconView.requestUpdate) {
|
|
1049
|
+
iconView.requestUpdate()
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// Remove any inline styles to let CSS handle sizing
|
|
1053
|
+
this.iconViewElement.style.width = ''
|
|
1054
|
+
this.iconViewElement.style.height = ''
|
|
1055
|
+
|
|
1056
|
+
// Update any nested lukso-profile elements
|
|
1057
|
+
const profiles = this.iconViewElement.querySelectorAll('lukso-profile')
|
|
1058
|
+
|
|
1059
|
+
profiles.forEach((profile) => {
|
|
1060
|
+
const element = profile as HTMLElement
|
|
1061
|
+
element.style.width = ''
|
|
1062
|
+
element.style.height = ''
|
|
1063
|
+
// Update the size attribute on lukso-profile
|
|
1064
|
+
element.setAttribute('size', newSize)
|
|
1065
|
+
})
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// Recreate content to update IconView sizing
|
|
1069
|
+
this.createContent()
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
public getElement(): HTMLElement | null {
|
|
1073
|
+
return this.element || null
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
public getPosition(): { x: number; y: number; name: string } | null {
|
|
1077
|
+
if (!this.element || !this.currentPosition) return null
|
|
1078
|
+
|
|
1079
|
+
const rect = this.element.getBoundingClientRect()
|
|
1080
|
+
return {
|
|
1081
|
+
x: rect.left,
|
|
1082
|
+
y: rect.top,
|
|
1083
|
+
name: this.currentPosition.name,
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
public startThrob(): void {
|
|
1088
|
+
if (!this.element || this.isThrobbing) return
|
|
1089
|
+
|
|
1090
|
+
this.isThrobbing = true
|
|
1091
|
+
this.element.classList.add('throbbing')
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
public stopThrob(): void {
|
|
1095
|
+
if (!this.element || !this.isThrobbing) return
|
|
1096
|
+
|
|
1097
|
+
this.isThrobbing = false
|
|
1098
|
+
this.element.classList.remove('throbbing')
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
public getAvatarRect(): DOMRect | null {
|
|
1102
|
+
return this.element ? this.element.getBoundingClientRect() : null
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
public destroy(): void {
|
|
1106
|
+
this.stopThrob()
|
|
1107
|
+
|
|
1108
|
+
if (this.overlay) {
|
|
1109
|
+
this.overlay.remove()
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// Remove styles if this was the last avatar overlay
|
|
1113
|
+
if (
|
|
1114
|
+
typeof document !== 'undefined' &&
|
|
1115
|
+
!document.querySelector('.up-avatar-overlay')
|
|
1116
|
+
) {
|
|
1117
|
+
const styles = document.querySelector('#up-connector-avatar-styles')
|
|
1118
|
+
styles?.remove()
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
/**
|
|
1124
|
+
* Factory function for creating draggable avatars
|
|
1125
|
+
*/
|
|
1126
|
+
export async function createAvatar(
|
|
1127
|
+
options: AvatarOptions = {}
|
|
1128
|
+
): Promise<DraggableAvatar> {
|
|
1129
|
+
const avatar = new DraggableAvatar(options)
|
|
1130
|
+
// The init() call is already handled in the constructor
|
|
1131
|
+
return avatar
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// Export the class as default
|
|
1135
|
+
export default DraggableAvatar
|