@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,86 @@
1
+ import { html } from 'lit'
2
+ import { customElement, property } from 'lit/decorators.js'
3
+ import { CoreLitElement } from '../styles'
4
+ import { safeCustomElement } from './safe-custom-element'
5
+
6
+ export type ProgressSegment = {
7
+ value: number
8
+ color?: string
9
+ title?: string
10
+ }
11
+
12
+ @safeCustomElement('progress-view')
13
+ export class ProgressView extends CoreLitElement {
14
+ @property({ type: Array }) data: ProgressSegment[] = []
15
+ @property() label?: string
16
+ @property() unit?: string
17
+ @property() address?: string
18
+ @property({ attribute: 'label-position' }) labelPosition: 'left' | 'right' =
19
+ 'right'
20
+
21
+ private renderGasProviderLabel() {
22
+ return html`
23
+ ${
24
+ this.label
25
+ ? html`
26
+ <span class="progress-label-value">${this.label}</span>
27
+ ${
28
+ this.unit
29
+ ? html`<span class="progress-label-unit" data-testid="unit">${this.unit}</span>`
30
+ : ''
31
+ }
32
+ `
33
+ : html`
34
+ <slot></slot>
35
+ ${
36
+ this.unit
37
+ ? html`<span class="progress-label-unit" data-testid="unit">${this.unit}</span>`
38
+ : ''
39
+ }
40
+ `
41
+ }
42
+ `
43
+ }
44
+
45
+ private renderAddressLabel() {
46
+ return html`
47
+ <value-view
48
+ .value=${this.address}
49
+ .unit=${this.unit}
50
+ ></value-view>
51
+ `
52
+ }
53
+
54
+ override render() {
55
+ const isError = (this.className || '').split(/\s+/).includes('error')
56
+ return html`
57
+ <div class="relative grid w-full ${this.className || ''}">
58
+ <div class="flex bg-neutral-95 h-6 rounded-lg relative col-[1] row-[1] overflow-hidden" style=${isError ? 'background-color: var(--red-65);' : ''} role="progressbar">
59
+ ${this.data.map((seg, index) => {
60
+ const width = `${Math.max(0, Math.min(100, Number(seg.value || 0)))}%`
61
+ const backgroundColor = seg.color || 'var(--neutral-90)'
62
+ const title = seg.title || ''
63
+ return html`<div
64
+ class="h-full flex items-center justify-center text-[10px] font-semibold"
65
+ style=${`width:${width};background-color:${backgroundColor};`}
66
+ data-testid=${`value-${index}`}
67
+ >${title}</div>`
68
+ })}
69
+ </div>
70
+ <div class=${`flex items-center absolute h-6 rounded-lg overflow-visible w-full col-[1] row-[1] top-0 left-0 ${
71
+ this.labelPosition === 'left' ? 'justify-start' : 'justify-end'
72
+ }`} data-testid="progress-labels">
73
+ <div class="text-[13px] font-semibold text-white mix-blend-difference h-inherit flex items-center p-0 right-4 absolute" data-testid="label">
74
+ ${this.renderGasProviderLabel()}
75
+ </div>
76
+ </div>
77
+ </div>
78
+ `
79
+ }
80
+ }
81
+
82
+ declare global {
83
+ interface HTMLElementTagNameMap {
84
+ 'progress-view': ProgressView
85
+ }
86
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * SSR-safe custom element decorator that prevents duplicate registration errors.
3
+ *
4
+ * ## Why this exists
5
+ *
6
+ * The standard `@customElement()` decorator from Lit will throw an error if
7
+ * a component is registered twice. This can happen in SSR environments where:
8
+ * - Components are dynamically imported via multiple paths
9
+ * - HMR causes modules to be re-executed during development
10
+ * - Server and client bundles both try to register components
11
+ *
12
+ * ## Usage
13
+ *
14
+ * Use this instead of `@customElement()`:
15
+ * ```ts
16
+ * import { safeCustomElement } from './safe-custom-element'
17
+ *
18
+ * @safeCustomElement('my-component')
19
+ * export class MyComponent extends LitElement {
20
+ * // ...
21
+ * }
22
+ * ```
23
+ *
24
+ * @param tagName - The custom element tag name (must contain a hyphen)
25
+ * @returns A class decorator that safely registers the element
26
+ */
27
+ export function safeCustomElement(tagName: string) {
28
+ return <T extends CustomElementConstructor>(classOrDescriptor: T): T => {
29
+ // Only register if not already defined
30
+ if (typeof customElements !== 'undefined' && !customElements.get(tagName)) {
31
+ customElements.define(tagName, classOrDescriptor)
32
+ }
33
+ return classOrDescriptor
34
+ }
35
+ }
@@ -0,0 +1,55 @@
1
+ import { html } from 'lit'
2
+ import { customElement, property } from 'lit/decorators.js'
3
+ import { CoreLitElement } from '../styles'
4
+ import { safeCustomElement } from './safe-custom-element'
5
+
6
+ /**
7
+ * ScrollToBottomButton (core) - reusable scroll helper.
8
+ * Emits `scroll-to-bottom` when clicked.
9
+ */
10
+ @safeCustomElement('scroll-to-bottom-button')
11
+ export class ScrollToBottomButton extends CoreLitElement {
12
+ /** Controls visibility */
13
+ @property({ type: Boolean, attribute: 'show' }) show = false
14
+
15
+ /** Icon color (CSS var or token) */
16
+ @property() color: string = 'neutral-20'
17
+
18
+ /** Icon size token */
19
+ @property({ attribute: 'icon-size' }) iconSize:
20
+ | 'x-small'
21
+ | 'small'
22
+ | 'medium'
23
+ | 'large' = 'medium'
24
+
25
+ /** Icon name (attribute: icon-name) */
26
+ @property({ attribute: 'icon-name' }) iconName: string = 'arrow-down-lg'
27
+
28
+ private handleClick() {
29
+ this.dispatchEvent(new CustomEvent('scroll-to-bottom', { bubbles: true }))
30
+ }
31
+
32
+ override render() {
33
+ return html`
34
+ <div class="h-14 w-[140px] self-center rounded-t-[120px] bg-[radial-gradient(ellipse_75px_52px_at_bottom_center,rgba(255,255,255,0.78)_0%,rgba(255,255,255,0)_112.5%)] flex justify-center items-center transition-opacity duration-700 ease-in-out ${
35
+ this.show ? 'opacity-100 z-10' : 'opacity-0 -z-10'
36
+ }">
37
+ <div class="flex justify-center items-center w-10 h-10 rounded-full border border-neutral-90 shadow-[0_0_16px_rgba(189,206,219,0.4)] bg-neutral-100 cursor-pointer animate-bounce" @click=${
38
+ this.handleClick
39
+ }>
40
+ <lukso-icon
41
+ name=${this.iconName}
42
+ size=${this.iconSize}
43
+ color=${this.color}
44
+ ></lukso-icon>
45
+ </div>
46
+ </div>
47
+ `
48
+ }
49
+ }
50
+
51
+ declare global {
52
+ interface HTMLElementTagNameMap {
53
+ 'scroll-to-bottom-button': ScrollToBottomButton
54
+ }
55
+ }
@@ -0,0 +1,124 @@
1
+ import type { Signal } from '@preact/signals-core'
2
+ import type { ReactiveController, ReactiveControllerHost } from 'lit'
3
+
4
+ /**
5
+ * Lit Reactive Controller for @preact/signals-core integration
6
+ * This controller allows Lit elements to reactively update when signals change
7
+ */
8
+ export class SignalController<T = any> implements ReactiveController {
9
+ private host: ReactiveControllerHost
10
+ private signal: Signal<T>
11
+ private unsubscribe?: () => void
12
+
13
+ constructor(host: ReactiveControllerHost, signal: Signal<T>) {
14
+ this.host = host
15
+ this.signal = signal
16
+ host.addController(this)
17
+ }
18
+
19
+ hostConnected() {
20
+ // In a real implementation, you would use signal.subscribe()
21
+ // For now, we'll use a polling approach
22
+ let lastValue = this.signal.value
23
+
24
+ const check = () => {
25
+ const currentValue = this.signal.value
26
+ if (currentValue !== lastValue) {
27
+ lastValue = currentValue
28
+ this.host.requestUpdate()
29
+ }
30
+ }
31
+
32
+ // Set up polling
33
+ const interval = setInterval(check, 50) // 20fps updates
34
+
35
+ this.unsubscribe = () => clearInterval(interval)
36
+ }
37
+
38
+ hostDisconnected() {
39
+ if (this.unsubscribe) {
40
+ this.unsubscribe()
41
+ this.unsubscribe = undefined
42
+ }
43
+ }
44
+
45
+ get value(): T {
46
+ return this.signal.value
47
+ }
48
+
49
+ /**
50
+ * Create a new SignalController for a different signal
51
+ */
52
+ static create<U>(
53
+ host: ReactiveControllerHost,
54
+ signal: Signal<U>
55
+ ): SignalController<U> {
56
+ return new SignalController(host, signal)
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Lit Reactive Controller for multiple signals
62
+ * Updates the host when any of the tracked signals change
63
+ */
64
+ export class MultiSignalController implements ReactiveController {
65
+ private host: ReactiveControllerHost
66
+ private signals: Signal<any>[]
67
+ private unsubscribes: (() => void)[] = []
68
+
69
+ constructor(host: ReactiveControllerHost, signals: Signal<any>[]) {
70
+ this.host = host
71
+ this.signals = signals
72
+ host.addController(this)
73
+ }
74
+
75
+ hostConnected() {
76
+ this.signals.forEach((signal, index) => {
77
+ let lastValue = signal.value
78
+
79
+ const check = () => {
80
+ const currentValue = signal.value
81
+ if (currentValue !== lastValue) {
82
+ lastValue = currentValue
83
+ this.host.requestUpdate()
84
+ }
85
+ }
86
+
87
+ const interval = setInterval(check, 50)
88
+ this.unsubscribes[index] = () => clearInterval(interval)
89
+ })
90
+ }
91
+
92
+ hostDisconnected() {
93
+ this.unsubscribes.forEach((unsubscribe) => unsubscribe())
94
+ this.unsubscribes = []
95
+ }
96
+
97
+ getValue(index: number): any {
98
+ return this.signals[index]?.value
99
+ }
100
+
101
+ getValues(): any[] {
102
+ return this.signals.map((signal) => signal.value)
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Utility function to create a signal controller
108
+ */
109
+ export function useSignal<T>(
110
+ host: ReactiveControllerHost,
111
+ signal: Signal<T>
112
+ ): SignalController<T> {
113
+ return new SignalController(host, signal)
114
+ }
115
+
116
+ /**
117
+ * Utility function to create a multi-signal controller
118
+ */
119
+ export function useSignals(
120
+ host: ReactiveControllerHost,
121
+ signals: Signal<any>[]
122
+ ): MultiSignalController {
123
+ return new MultiSignalController(host, signals)
124
+ }
@@ -0,0 +1,345 @@
1
+ import type { DecoderResult } from '@lukso/transaction-decoder'
2
+ import {
3
+ findAllTransactionViews,
4
+ findBestTransactionView,
5
+ type UseTransactionViewOptions,
6
+ useTransactionView as useTransactionViewHeadless,
7
+ type ViewMatch,
8
+ } from '@lukso/transaction-view-headless'
9
+ import type { Signal } from '@preact/signals-core'
10
+ import type { ReactiveController, ReactiveControllerHost } from 'lit'
11
+
12
+ /**
13
+ * Lit controller state for transaction view
14
+ */
15
+ export interface TransactionViewState {
16
+ matches: ViewMatch[]
17
+ bestMatch: ViewMatch | null
18
+ component: any
19
+ loading: boolean
20
+ error: Error | null
21
+ isResolvingAddresses: boolean
22
+ resolvedAddresses: Record<string, any>
23
+ }
24
+
25
+ /**
26
+ * Lit Reactive Controller for transaction view functionality
27
+ * Integrates the headless transaction view system with Lit elements
28
+ */
29
+ export class TransactionViewController implements ReactiveController {
30
+ private host: ReactiveControllerHost
31
+ private headlessInstance: any = null
32
+
33
+ // Internal state
34
+ private _state: TransactionViewState = {
35
+ matches: [],
36
+ bestMatch: null,
37
+ component: null,
38
+ loading: false,
39
+ error: null,
40
+ isResolvingAddresses: false,
41
+ resolvedAddresses: {},
42
+ }
43
+
44
+ private syncInterval?: number
45
+
46
+ constructor(
47
+ host: ReactiveControllerHost,
48
+ private transaction: DecoderResult,
49
+ private options: UseTransactionViewOptions
50
+ ) {
51
+ this.host = host
52
+ host.addController(this)
53
+ this.initialize()
54
+ }
55
+
56
+ hostConnected() {
57
+ // Set up periodic sync with headless instance
58
+ this.setupSync()
59
+ }
60
+
61
+ hostDisconnected() {
62
+ // Cleanup sync
63
+ if (this.syncInterval) {
64
+ clearInterval(this.syncInterval)
65
+ this.syncInterval = undefined
66
+ }
67
+ }
68
+
69
+ get state(): TransactionViewState {
70
+ return this._state
71
+ }
72
+
73
+ get matches(): ViewMatch[] {
74
+ return this._state.matches
75
+ }
76
+
77
+ get bestMatch(): ViewMatch | null {
78
+ return this._state.bestMatch
79
+ }
80
+
81
+ get component(): any {
82
+ return this._state.component
83
+ }
84
+
85
+ get isLoading(): boolean {
86
+ return this._state.loading
87
+ }
88
+
89
+ get error(): Error | null {
90
+ return this._state.error
91
+ }
92
+
93
+ get isResolvingAddresses(): boolean {
94
+ return this._state.isResolvingAddresses
95
+ }
96
+
97
+ get resolvedAddresses(): Record<string, any> {
98
+ return this._state.resolvedAddresses
99
+ }
100
+
101
+ /**
102
+ * Load a specific match
103
+ */
104
+ async loadMatch(match: ViewMatch): Promise<any> {
105
+ if (!this.headlessInstance) {
106
+ throw new Error('Transaction view not initialized')
107
+ }
108
+
109
+ try {
110
+ const result = await this.headlessInstance.loadMatch(match)
111
+ this.syncState()
112
+ return result
113
+ } catch (err) {
114
+ this.syncState()
115
+ throw err
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Load the best match
121
+ */
122
+ async loadBestMatch(): Promise<any> {
123
+ if (!this.headlessInstance) {
124
+ throw new Error('Transaction view not initialized')
125
+ }
126
+
127
+ try {
128
+ const result = await this.headlessInstance.loadBestMatch()
129
+ this.syncState()
130
+ return result
131
+ } catch (err) {
132
+ this.syncState()
133
+ throw err
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Refresh matches
139
+ */
140
+ refreshMatches(): void {
141
+ if (!this.headlessInstance) {
142
+ console.warn('Transaction view not initialized, cannot refresh matches')
143
+ return
144
+ }
145
+
146
+ this.headlessInstance.refreshMatches()
147
+ this.syncState()
148
+ }
149
+
150
+ /**
151
+ * Resolve addresses
152
+ */
153
+ async resolveAddresses(): Promise<void> {
154
+ if (!this.headlessInstance) {
155
+ throw new Error('Transaction view not initialized')
156
+ }
157
+
158
+ try {
159
+ await this.headlessInstance.resolveAddresses()
160
+ this.syncState()
161
+ } catch (err) {
162
+ this.syncState()
163
+ throw err
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Update transaction and options
169
+ */
170
+ update(transaction: DecoderResult, options: UseTransactionViewOptions): void {
171
+ this.transaction = transaction
172
+ this.options = options
173
+ this.initialize()
174
+ }
175
+
176
+ /**
177
+ * Get the best matching view without loading
178
+ */
179
+ static findBestMatch(
180
+ transaction: DecoderResult,
181
+ framework: 'lit' | 'rn' = 'lit'
182
+ ): ViewMatch | null {
183
+ return findBestTransactionView(transaction, framework)
184
+ }
185
+
186
+ /**
187
+ * Get all matching views without loading
188
+ */
189
+ static findAllMatches(
190
+ transaction: DecoderResult,
191
+ framework: 'lit' | 'rn' = 'lit'
192
+ ): ViewMatch[] {
193
+ return findAllTransactionViews(transaction, framework)
194
+ }
195
+
196
+ private initialize(): void {
197
+ try {
198
+ this.headlessInstance = useTransactionViewHeadless(
199
+ this.transaction,
200
+ this.options
201
+ )
202
+ this.syncState()
203
+ } catch (err) {
204
+ this.updateState({
205
+ error:
206
+ err instanceof Error
207
+ ? err
208
+ : new Error('Failed to initialize transaction view'),
209
+ })
210
+ console.error('Failed to initialize transaction view:', err)
211
+ }
212
+ }
213
+
214
+ private setupSync(): void {
215
+ // Sync immediately
216
+ this.syncState()
217
+
218
+ // Set up periodic sync
219
+ this.syncInterval = setInterval(() => {
220
+ this.syncState()
221
+ }, 100) as any // Type assertion for browser/node compatibility
222
+ }
223
+
224
+ private syncState(): void {
225
+ if (!this.headlessInstance) return
226
+
227
+ this.updateState({
228
+ matches: this.headlessInstance.matches,
229
+ bestMatch: this.headlessInstance.bestMatch,
230
+ component: this.headlessInstance.component,
231
+ loading: this.headlessInstance.isLoading,
232
+ error: this.headlessInstance.error,
233
+ isResolvingAddresses: this.headlessInstance.isResolvingAddresses,
234
+ resolvedAddresses: this.headlessInstance.resolvedAddresses,
235
+ })
236
+ }
237
+
238
+ private updateState(updates: Partial<TransactionViewState>): void {
239
+ this._state = { ...this._state, ...updates }
240
+ this.host.requestUpdate()
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Utility function to create a transaction view controller
246
+ */
247
+ export function useTransactionView(
248
+ host: ReactiveControllerHost,
249
+ transaction: DecoderResult,
250
+ options: UseTransactionViewOptions
251
+ ): TransactionViewController {
252
+ return new TransactionViewController(host, transaction, options)
253
+ }
254
+
255
+ /**
256
+ * Controller for working with transaction signals from the decoder
257
+ */
258
+ export class TransactionSignalController<T = any>
259
+ implements ReactiveController
260
+ {
261
+ private host: ReactiveControllerHost
262
+ private signal: Signal<{
263
+ data: DecoderResult
264
+ loading: boolean
265
+ error?: string
266
+ }>
267
+ private unsubscribe?: () => void
268
+
269
+ private _data: DecoderResult | null = null
270
+ private _loading = false
271
+ private _error: string | null = null
272
+
273
+ constructor(
274
+ host: ReactiveControllerHost,
275
+ signal: Signal<{ data: DecoderResult; loading: boolean; error?: string }>
276
+ ) {
277
+ this.host = host
278
+ this.signal = signal
279
+ host.addController(this)
280
+ }
281
+
282
+ hostConnected() {
283
+ this.setupSignalSync()
284
+ }
285
+
286
+ hostDisconnected() {
287
+ if (this.unsubscribe) {
288
+ this.unsubscribe()
289
+ this.unsubscribe = undefined
290
+ }
291
+ }
292
+
293
+ get data(): DecoderResult | null {
294
+ return this._data
295
+ }
296
+
297
+ get loading(): boolean {
298
+ return this._loading
299
+ }
300
+
301
+ get error(): string | null {
302
+ return this._error
303
+ }
304
+
305
+ get isReady(): boolean {
306
+ return !this._loading && this._data !== null && !this._error
307
+ }
308
+
309
+ private setupSignalSync(): void {
310
+ const sync = () => {
311
+ const signalValue = this.signal.value
312
+ const newData = signalValue?.data || null
313
+ const newLoading = signalValue?.loading || false
314
+ const newError = signalValue?.error || null
315
+
316
+ if (
317
+ newData !== this._data ||
318
+ newLoading !== this._loading ||
319
+ newError !== this._error
320
+ ) {
321
+ this._data = newData
322
+ this._loading = newLoading
323
+ this._error = newError
324
+ this.host.requestUpdate()
325
+ }
326
+ }
327
+
328
+ // Initial sync
329
+ sync()
330
+
331
+ // Set up periodic sync
332
+ const interval = setInterval(sync, 50)
333
+ this.unsubscribe = () => clearInterval(interval)
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Utility function to create a transaction signal controller
339
+ */
340
+ export function useTransactionSignal(
341
+ host: ReactiveControllerHost,
342
+ signal: Signal<{ data: DecoderResult; loading: boolean; error?: string }>
343
+ ): TransactionSignalController {
344
+ return new TransactionSignalController(host, signal)
345
+ }