@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.
Files changed (109) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +280 -0
  3. package/dist/account-modal.cjs +9 -0
  4. package/dist/account-modal.cjs.map +1 -0
  5. package/dist/account-modal.d.cts +16 -0
  6. package/dist/account-modal.d.ts +16 -0
  7. package/dist/account-modal.js +9 -0
  8. package/dist/account-modal.js.map +1 -0
  9. package/dist/auto-setup.cjs +17 -0
  10. package/dist/auto-setup.cjs.map +1 -0
  11. package/dist/auto-setup.d.cts +123 -0
  12. package/dist/auto-setup.d.ts +123 -0
  13. package/dist/auto-setup.js +17 -0
  14. package/dist/auto-setup.js.map +1 -0
  15. package/dist/avatar-CmUCtW_w.d.cts +205 -0
  16. package/dist/avatar-CmUCtW_w.d.ts +205 -0
  17. package/dist/avatar.cjs +12 -0
  18. package/dist/avatar.cjs.map +1 -0
  19. package/dist/avatar.d.cts +1 -0
  20. package/dist/avatar.d.ts +1 -0
  21. package/dist/avatar.js +12 -0
  22. package/dist/avatar.js.map +1 -0
  23. package/dist/backup-modal.cjs +9 -0
  24. package/dist/backup-modal.cjs.map +1 -0
  25. package/dist/backup-modal.d.cts +41 -0
  26. package/dist/backup-modal.d.ts +41 -0
  27. package/dist/backup-modal.js +9 -0
  28. package/dist/backup-modal.js.map +1 -0
  29. package/dist/chunk-3SGSPHOZ.js +595 -0
  30. package/dist/chunk-3SGSPHOZ.js.map +1 -0
  31. package/dist/chunk-6AYZOIFY.js +181 -0
  32. package/dist/chunk-6AYZOIFY.js.map +1 -0
  33. package/dist/chunk-6N35TCFT.js +852 -0
  34. package/dist/chunk-6N35TCFT.js.map +1 -0
  35. package/dist/chunk-7ETKG6KR.cjs +387 -0
  36. package/dist/chunk-7ETKG6KR.cjs.map +1 -0
  37. package/dist/chunk-EUXUH3YW.js +15 -0
  38. package/dist/chunk-EUXUH3YW.js.map +1 -0
  39. package/dist/chunk-GFVUWAG4.cjs +158 -0
  40. package/dist/chunk-GFVUWAG4.cjs.map +1 -0
  41. package/dist/chunk-IAKQFHFD.cjs +595 -0
  42. package/dist/chunk-IAKQFHFD.cjs.map +1 -0
  43. package/dist/chunk-MH7MP7XK.cjs +181 -0
  44. package/dist/chunk-MH7MP7XK.cjs.map +1 -0
  45. package/dist/chunk-NWCNJSG3.js +387 -0
  46. package/dist/chunk-NWCNJSG3.js.map +1 -0
  47. package/dist/chunk-NXU2DQAV.js +1128 -0
  48. package/dist/chunk-NXU2DQAV.js.map +1 -0
  49. package/dist/chunk-ORJK2YGG.cjs +852 -0
  50. package/dist/chunk-ORJK2YGG.cjs.map +1 -0
  51. package/dist/chunk-RFA6SEIS.cjs +1128 -0
  52. package/dist/chunk-RFA6SEIS.cjs.map +1 -0
  53. package/dist/chunk-XGIT7YUY.js +31 -0
  54. package/dist/chunk-XGIT7YUY.js.map +1 -0
  55. package/dist/chunk-XOKG3KIL.cjs +31 -0
  56. package/dist/chunk-XOKG3KIL.cjs.map +1 -0
  57. package/dist/chunk-YIWSPI4I.js +158 -0
  58. package/dist/chunk-YIWSPI4I.js.map +1 -0
  59. package/dist/chunk-ZBDE64SD.cjs +15 -0
  60. package/dist/chunk-ZBDE64SD.cjs.map +1 -0
  61. package/dist/connect-modal/index.cjs +20 -0
  62. package/dist/connect-modal/index.cjs.map +1 -0
  63. package/dist/connect-modal/index.d.cts +9 -0
  64. package/dist/connect-modal/index.d.ts +9 -0
  65. package/dist/connect-modal/index.js +20 -0
  66. package/dist/connect-modal/index.js.map +1 -0
  67. package/dist/index-D2orHGFi.d.cts +8 -0
  68. package/dist/index-D2orHGFi.d.ts +8 -0
  69. package/dist/index.cjs +793 -0
  70. package/dist/index.cjs.map +1 -0
  71. package/dist/index.d.cts +189 -0
  72. package/dist/index.d.ts +189 -0
  73. package/dist/index.js +793 -0
  74. package/dist/index.js.map +1 -0
  75. package/dist/restore-modal.cjs +9 -0
  76. package/dist/restore-modal.cjs.map +1 -0
  77. package/dist/restore-modal.d.cts +68 -0
  78. package/dist/restore-modal.d.ts +68 -0
  79. package/dist/restore-modal.js +9 -0
  80. package/dist/restore-modal.js.map +1 -0
  81. package/dist/wagmi-CVuDs_0h.d.cts +386 -0
  82. package/dist/wagmi-CVuDs_0h.d.ts +386 -0
  83. package/package.json +158 -0
  84. package/src/account-modal.ts +142 -0
  85. package/src/auto-setup.ts +362 -0
  86. package/src/avatar.ts +1135 -0
  87. package/src/backup-modal.ts +439 -0
  88. package/src/connect-modal/components/connection-view.ts +398 -0
  89. package/src/connect-modal/components/eoa-connection-view.ts +408 -0
  90. package/src/connect-modal/components/qr-code-view.ts +71 -0
  91. package/src/connect-modal/connect-modal.base.ts +18 -0
  92. package/src/connect-modal/connect-modal.config.ts +27 -0
  93. package/src/connect-modal/connect-modal.templates.ts +21 -0
  94. package/src/connect-modal/connect-modal.ts +270 -0
  95. package/src/connect-modal/connect-modal.types.ts +104 -0
  96. package/src/connect-modal/images/up-cube-glass.png +0 -0
  97. package/src/connect-modal/index.ts +23 -0
  98. package/src/connect-modal/services/wagmi.ts +266 -0
  99. package/src/connect-modal/styles/styles.css +1 -0
  100. package/src/connect-modal/utils/walletConnectDeepLinkUrl.ts +43 -0
  101. package/src/connector.ts +544 -0
  102. package/src/index.ts +62 -0
  103. package/src/popup-instance.ts +537 -0
  104. package/src/restore-modal.ts +702 -0
  105. package/src/styles/index.ts +28 -0
  106. package/src/styles/styles.css +1 -0
  107. package/src/types/css-raw.d.ts +4 -0
  108. package/src/types/images.d.ts +4 -0
  109. 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