@lukso/transaction-view-core 0.3.1-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 (62) hide show
  1. package/.turbo/turbo-build.log +33 -0
  2. package/CHANGELOG.md +104 -0
  3. package/LICENSE +201 -0
  4. package/README.md +264 -0
  5. package/dist/index.cjs +4573 -0
  6. package/dist/index.cjs.map +1 -0
  7. package/dist/index.d.cts +493 -0
  8. package/dist/index.d.ts +493 -0
  9. package/dist/index.js +4534 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/shared/index.cjs +2480 -0
  12. package/dist/shared/index.cjs.map +1 -0
  13. package/dist/shared/index.d.cts +887 -0
  14. package/dist/shared/index.d.ts +887 -0
  15. package/dist/shared/index.js +2437 -0
  16. package/dist/shared/index.js.map +1 -0
  17. package/dist/styles/index.cjs +58 -0
  18. package/dist/styles/index.cjs.map +1 -0
  19. package/dist/styles/index.d.cts +9 -0
  20. package/dist/styles/index.d.ts +9 -0
  21. package/dist/styles/index.js +31 -0
  22. package/dist/styles/index.js.map +1 -0
  23. package/package.json +57 -0
  24. package/publish.log +0 -0
  25. package/src/components/asset-card.ts +426 -0
  26. package/src/components/contract-card.ts +238 -0
  27. package/src/components/description-view.ts +605 -0
  28. package/src/components/index.ts +20 -0
  29. package/src/components/info-panel.ts +437 -0
  30. package/src/components/permissions-block.ts +138 -0
  31. package/src/components/transaction-playback.ts +429 -0
  32. package/src/components/universal-token-view.ts +281 -0
  33. package/src/declaration.d.ts +4 -0
  34. package/src/examples/INTEGRATION_GUIDE.md +240 -0
  35. package/src/examples/TransactionContentWithReplacement.vue +269 -0
  36. package/src/index.ts +38 -0
  37. package/src/registry/index.ts +31 -0
  38. package/src/registry/lit-view-loader.ts +191 -0
  39. package/src/shared/address-display.ts +370 -0
  40. package/src/shared/address-view.ts +385 -0
  41. package/src/shared/custom-select.ts +164 -0
  42. package/src/shared/function-name-view.ts +89 -0
  43. package/src/shared/gas-provider-view.ts +218 -0
  44. package/src/shared/icon-view.ts +288 -0
  45. package/src/shared/image-list.ts +261 -0
  46. package/src/shared/image-loader-controller.ts +160 -0
  47. package/src/shared/index.ts +34 -0
  48. package/src/shared/load-web-components.ts +56 -0
  49. package/src/shared/notification-view.ts +132 -0
  50. package/src/shared/progress-view.ts +86 -0
  51. package/src/shared/safe-custom-element.ts +35 -0
  52. package/src/shared/scroll-to-bottom-button.ts +55 -0
  53. package/src/shared/signal-controller.ts +124 -0
  54. package/src/shared/transaction-view-controller.ts +345 -0
  55. package/src/shared/url-view.ts +129 -0
  56. package/src/shared/value-view.ts +510 -0
  57. package/src/styles/index.ts +28 -0
  58. package/src/styles/styles.css +1 -0
  59. package/src/vue-entry.ts +15 -0
  60. package/tailwind.config.cjs +9 -0
  61. package/tsconfig.json +22 -0
  62. package/tsup.config.ts +90 -0
@@ -0,0 +1,191 @@
1
+ import type {
2
+ FrameworkViewConfig,
3
+ ViewMatch,
4
+ } from '@lukso/transaction-view-headless'
5
+ import { ViewLoadingError } from '@lukso/transaction-view-headless'
6
+
7
+ /**
8
+ * Lit component view loader
9
+ * Handles loading of Lit web components for transaction views
10
+ */
11
+ export class LitViewLoader {
12
+ private loadedComponents = new Map<string, any>()
13
+
14
+ /**
15
+ * Load a component from a view match
16
+ */
17
+ async loadFromMatch(match: ViewMatch): Promise<any> {
18
+ if (!match.frameworkConfig) {
19
+ throw new ViewLoadingError(
20
+ `No framework config for view "${match.view.id}"`,
21
+ { type: 'custom-element', tagName: '' },
22
+ {} as FrameworkViewConfig
23
+ )
24
+ }
25
+
26
+ return this.loadComponent(match.frameworkConfig)
27
+ }
28
+
29
+ /**
30
+ * Load a component based on framework config
31
+ */
32
+ async loadComponent(config: FrameworkViewConfig): Promise<any> {
33
+ const { loader, component } = config
34
+
35
+ // Check if already loaded
36
+ if (this.loadedComponents.has(component)) {
37
+ return this.loadedComponents.get(component)
38
+ }
39
+
40
+ try {
41
+ let loadedComponent: any
42
+
43
+ switch (loader.type) {
44
+ case 'custom-element':
45
+ loadedComponent = await this.loadCustomElement(loader.tagName)
46
+ break
47
+ case 'dynamic-import':
48
+ loadedComponent = await this.loadDynamicImport(loader.path)
49
+ break
50
+ case 'static-import':
51
+ loadedComponent = await this.loadStaticImport(loader.path)
52
+ break
53
+ case 'custom':
54
+ loadedComponent = await loader.loader(config)
55
+ break
56
+ default:
57
+ throw new ViewLoadingError(
58
+ `Unsupported loader type: ${(loader as any).type}`,
59
+ loader,
60
+ config
61
+ )
62
+ }
63
+
64
+ // Cache the loaded component
65
+ this.loadedComponents.set(component, loadedComponent)
66
+ return loadedComponent
67
+ } catch (error) {
68
+ throw new ViewLoadingError(
69
+ `Failed to load component "${component}"`,
70
+ loader,
71
+ config,
72
+ error instanceof Error ? error : new Error(String(error))
73
+ )
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Load a custom element (Web Component)
79
+ */
80
+ private async loadCustomElement(tagName: string): Promise<any> {
81
+ // Check if the custom element is already defined
82
+ if (customElements.get(tagName)) {
83
+ return customElements.get(tagName)
84
+ }
85
+
86
+ // For our core components, they should be imported already
87
+ // This is a fallback to wait for definition
88
+ return new Promise((resolve, reject) => {
89
+ const timeout = setTimeout(() => {
90
+ reject(
91
+ new Error(`Custom element "${tagName}" not found after 5s timeout`)
92
+ )
93
+ }, 5000)
94
+
95
+ customElements
96
+ .whenDefined(tagName)
97
+ .then(() => {
98
+ clearTimeout(timeout)
99
+ resolve(customElements.get(tagName))
100
+ })
101
+ .catch(reject)
102
+ })
103
+ }
104
+
105
+ /**
106
+ * Load via dynamic import
107
+ */
108
+ private async loadDynamicImport(
109
+ path: string | (() => Promise<any>)
110
+ ): Promise<any> {
111
+ // Handle callback functions (lazy loading)
112
+ if (typeof path === 'function') {
113
+ const module = await path()
114
+ return module.default || module
115
+ }
116
+
117
+ // Handle string paths
118
+ const module = await import(/* @vite-ignore */ path)
119
+ return module.default || module
120
+ }
121
+
122
+ /**
123
+ * Load via static import (not really possible at runtime, but for completeness)
124
+ */
125
+ private async loadStaticImport(
126
+ path: string | (() => Promise<any>)
127
+ ): Promise<any> {
128
+ // This would typically be handled at build time
129
+ // For runtime, fallback to dynamic import
130
+ return this.loadDynamicImport(path)
131
+ }
132
+
133
+ /**
134
+ * Create a component instance with data
135
+ */
136
+ createInstance(componentClass: any, data: any): HTMLElement {
137
+ const element = new componentClass()
138
+
139
+ // Set data properties
140
+ Object.keys(data).forEach((key) => {
141
+ if (key in element) {
142
+ element[key] = data[key]
143
+ } else {
144
+ element.setAttribute(
145
+ key,
146
+ typeof data[key] === 'object'
147
+ ? JSON.stringify(data[key])
148
+ : String(data[key])
149
+ )
150
+ }
151
+ })
152
+
153
+ return element
154
+ }
155
+
156
+ /**
157
+ * Clear component cache
158
+ */
159
+ clearCache(): void {
160
+ this.loadedComponents.clear()
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Global Lit view loader instance
166
+ */
167
+ let globalLitLoader: LitViewLoader | null = null
168
+
169
+ /**
170
+ * Get the global Lit view loader
171
+ */
172
+ export function getGlobalLitLoader(): LitViewLoader {
173
+ if (!globalLitLoader) {
174
+ globalLitLoader = new LitViewLoader()
175
+ }
176
+ return globalLitLoader
177
+ }
178
+
179
+ /**
180
+ * Set a custom global Lit loader (useful for testing)
181
+ */
182
+ export function setGlobalLitLoader(loader: LitViewLoader): void {
183
+ globalLitLoader = loader
184
+ }
185
+
186
+ /**
187
+ * Reset the global Lit loader (mainly for testing)
188
+ */
189
+ export function resetGlobalLitLoader(): void {
190
+ globalLitLoader = null
191
+ }
@@ -0,0 +1,370 @@
1
+ import { Task } from '@lit/task'
2
+ import {
3
+ useAddressResolver,
4
+ useImageLoader,
5
+ } from '@lukso/transaction-view-headless'
6
+ import { css, html, LitElement } from 'lit'
7
+ import { customElement, property, state } from 'lit/decorators.js'
8
+ import { safeCustomElement } from './safe-custom-element'
9
+
10
+ // LUKSO web components are loaded dynamically via plugin
11
+
12
+ /**
13
+ * Address data interface for display
14
+ */
15
+ export interface AddressData {
16
+ address: string
17
+ name?: string
18
+ lsp4TokenName?: string
19
+ lsp4TokenSymbol?: string
20
+ baseAsset?: {
21
+ lsp4TokenName?: string
22
+ lsp4TokenSymbol?: string
23
+ }
24
+ profileImages?: any[]
25
+ avatar?: any[]
26
+ icon?: any[]
27
+ images?: any[]
28
+ __gqltype?: string
29
+ __identity?: string
30
+ identity?: string
31
+ }
32
+
33
+ /**
34
+ * Universal address display component with profile resolution
35
+ *
36
+ * Displays an address with profile image and name, supporting both
37
+ * Universal Profiles and contract addresses with proper styling.
38
+ *
39
+ * @example
40
+ * ```html
41
+ * <address-display
42
+ * address="0x1234..."
43
+ * .resolved="${resolvedData}"
44
+ * size="medium"
45
+ * show-copy-button
46
+ * ></address-display>
47
+ * ```
48
+ */
49
+ @safeCustomElement('address-display')
50
+ export class AddressDisplay extends LitElement {
51
+ /**
52
+ * The address to display (required)
53
+ */
54
+ @property() value!: string
55
+
56
+ /**
57
+ * @deprecated Use `value` instead
58
+ */
59
+ @property() address?: string
60
+
61
+ /**
62
+ * Resolved address data (profile, images, etc.)
63
+ */
64
+ @property({ type: Object }) resolved?: AddressData
65
+
66
+ /**
67
+ * Display size
68
+ */
69
+ @property() size: 'x-small' | 'small' | 'medium' | 'large' = 'small'
70
+
71
+ /**
72
+ * Optional label to display above the address
73
+ */
74
+ @property() label?: string
75
+
76
+ /**
77
+ * Whether to show the copy button
78
+ */
79
+ @property({ type: Boolean, attribute: 'show-copy-button' }) showCopyButton =
80
+ false
81
+
82
+ /**
83
+ * Whether to show the full address or just the name
84
+ */
85
+ @property({ type: Boolean, attribute: 'name-only' }) nameOnly = false
86
+
87
+ /**
88
+ * Maximum width for the name display (in pixels)
89
+ */
90
+ @property({ type: Number, attribute: 'max-width' }) maxWidth = 200
91
+
92
+ /**
93
+ * Get the effective address value (supports both `value` and legacy `address` props)
94
+ */
95
+ private get effectiveAddress(): string {
96
+ return this.value || this.address || ''
97
+ }
98
+
99
+ // Internal state
100
+ @state() private imageData?: { src: string }
101
+ @state() private isLoadingImage = false
102
+
103
+ // Composables
104
+ private imageLoader = useImageLoader()
105
+ private addressResolver = useAddressResolver()
106
+
107
+ // Task for loading profile image
108
+ private imageTask = new Task(this, {
109
+ task: async ([resolved], { signal }) => {
110
+ if (!resolved) return null
111
+
112
+ this.isLoadingImage = true
113
+ try {
114
+ const image = await this.imageLoader.loadProfileImage(resolved, {
115
+ width: this.getSizePixels(),
116
+ height: this.getSizePixels(),
117
+ dpr: window.devicePixelRatio || 1,
118
+ })
119
+
120
+ return image || this.imageLoader.getFallbackImage(resolved)
121
+ } catch (error) {
122
+ console.warn('Failed to load address image:', error)
123
+ return this.imageLoader.getFallbackImage(resolved || {})
124
+ } finally {
125
+ this.isLoadingImage = false
126
+ }
127
+ },
128
+ args: () => [this.resolved],
129
+ })
130
+
131
+ /**
132
+ * Get pixel size for the current size setting
133
+ */
134
+ private getSizePixels(): number {
135
+ switch (this.size) {
136
+ case 'x-small':
137
+ return 16
138
+ case 'small':
139
+ return 24
140
+ case 'medium':
141
+ return 32
142
+ case 'large':
143
+ return 48
144
+ default:
145
+ return 24
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Get display name for the address
151
+ */
152
+ private getDisplayName(): string {
153
+ if (!this.resolved) return 'Anonymous'
154
+
155
+ const computedName = this.addressResolver.formatDisplayName(this.resolved)
156
+ return computedName || 'Anonymous'
157
+ }
158
+
159
+ /**
160
+ * Check if this is a contract address (shows square icon)
161
+ */
162
+ private get isContract(): boolean {
163
+ return this.resolved
164
+ ? this.imageLoader.isContractAddress(this.resolved)
165
+ : false
166
+ }
167
+
168
+ /**
169
+ * Get prefix for the address display
170
+ */
171
+ private get addressPrefix(): string {
172
+ return this.resolved
173
+ ? this.addressResolver.getAddressPrefix(this.resolved)
174
+ : '@'
175
+ }
176
+
177
+ /**
178
+ * Check if should show name colors
179
+ */
180
+ private get shouldShowNameColor(): boolean {
181
+ return this.resolved
182
+ ? this.addressResolver.shouldShowNameColor(this.resolved)
183
+ : true
184
+ }
185
+
186
+ /**
187
+ * Handle copy button click
188
+ */
189
+ private handleCopy() {
190
+ if (navigator.clipboard) {
191
+ navigator.clipboard.writeText(this.effectiveAddress)
192
+ // Dispatch copy event for parent components
193
+ this.dispatchEvent(
194
+ new CustomEvent('address-copied', {
195
+ detail: { address: this.effectiveAddress },
196
+ bubbles: true,
197
+ })
198
+ )
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Handle address click
204
+ */
205
+ private handleAddressClick() {
206
+ this.dispatchEvent(
207
+ new CustomEvent('address-click', {
208
+ detail: {
209
+ address: this.effectiveAddress,
210
+ resolved: this.resolved,
211
+ },
212
+ bubbles: true,
213
+ })
214
+ )
215
+ }
216
+
217
+ override render() {
218
+ const imageResult = this.imageTask.value
219
+
220
+ return html`
221
+ <div class="address-container">
222
+ ${this.label ? html`<div class="label">${this.label}</div>` : ''}
223
+
224
+ <div class="address-display" @click=${this.handleAddressClick}>
225
+ <!-- Identity prefix (if present) -->
226
+ ${
227
+ this.resolved?.__identity
228
+ ? html`
229
+ <span class="identity">
230
+ ${this.resolved.__identity}
231
+ <lukso-username
232
+ address=${this.resolved.identity}
233
+ size=${this.size}
234
+ max-width="200"
235
+ has-name-color="false"
236
+ has-address-color="false"
237
+ ></lukso-username>
238
+ of
239
+ </span>
240
+ `
241
+ : ''
242
+ }
243
+
244
+ <!-- Profile image and name -->
245
+ <lukso-profile
246
+ size=${this.size}
247
+ profile-address=${this.effectiveAddress}
248
+ profile-url=${imageResult?.src || ''}
249
+ has-identicon
250
+ ?is-square=${this.isContract}
251
+ ></lukso-profile>
252
+
253
+ <!-- Address/Name display -->
254
+ ${
255
+ this.nameOnly
256
+ ? html`
257
+ <lukso-username
258
+ name=${this.getDisplayName()}
259
+ size=${this.size}
260
+ max-width=${this.maxWidth}
261
+ prefix=${this.addressPrefix}
262
+ ?has-name-color=${this.shouldShowNameColor}
263
+ has-address-color="false"
264
+ ></lukso-username>
265
+ `
266
+ : html`
267
+ <lukso-username
268
+ name=${this.getDisplayName()}
269
+ address=${!this.isContract ? this.effectiveAddress : undefined}
270
+ size=${this.size}
271
+ max-width=${this.maxWidth}
272
+ prefix=${this.addressPrefix}
273
+ ?has-name-color=${this.shouldShowNameColor}
274
+ ?has-address-color=${this.shouldShowNameColor}
275
+ ></lukso-username>
276
+ `
277
+ }
278
+
279
+
280
+ <!-- Copy button -->
281
+ ${
282
+ this.showCopyButton
283
+ ? html`
284
+ <button
285
+ class="copy-button"
286
+ @click=${this.handleCopy}
287
+ title="Copy address"
288
+ >
289
+ 📋
290
+ </button>
291
+ `
292
+ : ''
293
+ }
294
+ </div>
295
+ </div>
296
+ `
297
+ }
298
+
299
+ static override styles = css`
300
+ :host {
301
+ display: inline-block;
302
+ }
303
+
304
+ .address-container {
305
+ display: flex;
306
+ flex-direction: column;
307
+ gap: 4px;
308
+ }
309
+
310
+ .label {
311
+ font-size: 12px;
312
+ color: var(--neutral-60, #666);
313
+ font-weight: 500;
314
+ }
315
+
316
+ .address-display {
317
+ display: inline-flex;
318
+ align-items: center;
319
+ gap: 8px;
320
+ cursor: pointer;
321
+ padding: 4px;
322
+ border-radius: 4px;
323
+ transition: background-color 0.2s;
324
+ min-width: 200px;
325
+ }
326
+
327
+ .address-display:hover {
328
+ background-color: var(--neutral-95, #f5f5f5);
329
+ }
330
+
331
+ .identity {
332
+ font-size: 12px;
333
+ color: var(--neutral-60, #666);
334
+ margin-right: 4px;
335
+ }
336
+
337
+ .copy-button {
338
+ background: none;
339
+ border: 1px solid var(--neutral-80, #ccc);
340
+ border-radius: 4px;
341
+ padding: 2px 4px;
342
+ cursor: pointer;
343
+ font-size: 12px;
344
+ opacity: 0;
345
+ transition: opacity 0.2s;
346
+ }
347
+
348
+ .copy-button:hover {
349
+ background-color: var(--neutral-90, #eee);
350
+ }
351
+
352
+ .address-display:hover .copy-button {
353
+ opacity: 1;
354
+ }
355
+
356
+
357
+ /* Ensure LUKSO web components are properly styled */
358
+ lukso-profile,
359
+ lukso-username {
360
+ display: inline-flex;
361
+ align-items: center;
362
+ }
363
+ `
364
+ }
365
+
366
+ declare global {
367
+ interface HTMLElementTagNameMap {
368
+ 'address-display': AddressDisplay
369
+ }
370
+ }