@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.
- package/.turbo/turbo-build.log +33 -0
- package/CHANGELOG.md +104 -0
- package/LICENSE +201 -0
- package/README.md +264 -0
- package/dist/index.cjs +4573 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +493 -0
- package/dist/index.d.ts +493 -0
- package/dist/index.js +4534 -0
- package/dist/index.js.map +1 -0
- package/dist/shared/index.cjs +2480 -0
- package/dist/shared/index.cjs.map +1 -0
- package/dist/shared/index.d.cts +887 -0
- package/dist/shared/index.d.ts +887 -0
- package/dist/shared/index.js +2437 -0
- package/dist/shared/index.js.map +1 -0
- package/dist/styles/index.cjs +58 -0
- package/dist/styles/index.cjs.map +1 -0
- package/dist/styles/index.d.cts +9 -0
- package/dist/styles/index.d.ts +9 -0
- package/dist/styles/index.js +31 -0
- package/dist/styles/index.js.map +1 -0
- package/package.json +57 -0
- package/publish.log +0 -0
- package/src/components/asset-card.ts +426 -0
- package/src/components/contract-card.ts +238 -0
- package/src/components/description-view.ts +605 -0
- package/src/components/index.ts +20 -0
- package/src/components/info-panel.ts +437 -0
- package/src/components/permissions-block.ts +138 -0
- package/src/components/transaction-playback.ts +429 -0
- package/src/components/universal-token-view.ts +281 -0
- package/src/declaration.d.ts +4 -0
- package/src/examples/INTEGRATION_GUIDE.md +240 -0
- package/src/examples/TransactionContentWithReplacement.vue +269 -0
- package/src/index.ts +38 -0
- package/src/registry/index.ts +31 -0
- package/src/registry/lit-view-loader.ts +191 -0
- package/src/shared/address-display.ts +370 -0
- package/src/shared/address-view.ts +385 -0
- package/src/shared/custom-select.ts +164 -0
- package/src/shared/function-name-view.ts +89 -0
- package/src/shared/gas-provider-view.ts +218 -0
- package/src/shared/icon-view.ts +288 -0
- package/src/shared/image-list.ts +261 -0
- package/src/shared/image-loader-controller.ts +160 -0
- package/src/shared/index.ts +34 -0
- package/src/shared/load-web-components.ts +56 -0
- package/src/shared/notification-view.ts +132 -0
- package/src/shared/progress-view.ts +86 -0
- package/src/shared/safe-custom-element.ts +35 -0
- package/src/shared/scroll-to-bottom-button.ts +55 -0
- package/src/shared/signal-controller.ts +124 -0
- package/src/shared/transaction-view-controller.ts +345 -0
- package/src/shared/url-view.ts +129 -0
- package/src/shared/value-view.ts +510 -0
- package/src/styles/index.ts +28 -0
- package/src/styles/styles.css +1 -0
- package/src/vue-entry.ts +15 -0
- package/tailwind.config.cjs +9 -0
- package/tsconfig.json +22 -0
- package/tsup.config.ts +90 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { Task } from '@lit/task'
|
|
2
|
+
import { useAddressResolver } from '@lukso/transaction-view-headless'
|
|
3
|
+
import { html } from 'lit'
|
|
4
|
+
import { customElement, property, state } from 'lit/decorators.js'
|
|
5
|
+
import { repeat } from 'lit/directives/repeat.js'
|
|
6
|
+
import { safeCustomElement } from './safe-custom-element'
|
|
7
|
+
import './icon-view.js'
|
|
8
|
+
import { CoreLitElement } from '../styles'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Image list component for displaying arrays of images/assets
|
|
12
|
+
*
|
|
13
|
+
* Displays a list of images from a resolved address property (e.g., 'images', 'assets').
|
|
14
|
+
* If items have an 'index' property, shows only items with matching indices.
|
|
15
|
+
* Handles different file types (images, videos, audio, generic files).
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```html
|
|
19
|
+
* <!-- Display all images -->
|
|
20
|
+
* <image-list
|
|
21
|
+
* address="0x1234..."
|
|
22
|
+
* image-hint="images"
|
|
23
|
+
* ></image-list>
|
|
24
|
+
*
|
|
25
|
+
* <!-- Display images with index 0-5 -->
|
|
26
|
+
* <image-list
|
|
27
|
+
* address="0x1234..."
|
|
28
|
+
* image-hint="images"
|
|
29
|
+
* min-index="0"
|
|
30
|
+
* max-index="5"
|
|
31
|
+
* ></image-list>
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
@safeCustomElement('image-list')
|
|
35
|
+
export class ImageList extends CoreLitElement {
|
|
36
|
+
/**
|
|
37
|
+
* The address to display (required)
|
|
38
|
+
*/
|
|
39
|
+
@property() address!: string
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Property name to use for loading images (e.g., 'images', 'assets')
|
|
43
|
+
* This should point to an array property on the resolved data
|
|
44
|
+
*/
|
|
45
|
+
@property({ type: String, attribute: 'image-hint' }) imageHint!: string
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Minimum index to display (only applies if items have index property)
|
|
49
|
+
*/
|
|
50
|
+
@property({ type: Number, attribute: 'min-index' }) minIndex?: number
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Maximum index to display (only applies if items have index property)
|
|
54
|
+
*/
|
|
55
|
+
@property({ type: Number, attribute: 'max-index' }) maxIndex?: number
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Display size for images
|
|
59
|
+
*/
|
|
60
|
+
@property() size: 'x-small' | 'small' | 'medium' | 'large' = 'small'
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Optional card view: wrap list in a bordered, padded container
|
|
64
|
+
*/
|
|
65
|
+
@property({ type: Boolean, attribute: 'is-card-view' }) isCardView = false
|
|
66
|
+
|
|
67
|
+
// Internal state
|
|
68
|
+
@state() private isLoading = false
|
|
69
|
+
|
|
70
|
+
// Composables
|
|
71
|
+
private addressResolver = useAddressResolver()
|
|
72
|
+
|
|
73
|
+
// Task for resolving address data
|
|
74
|
+
private addressTask = new Task(this, {
|
|
75
|
+
task: async ([address], { signal }) => {
|
|
76
|
+
if (!address) return null
|
|
77
|
+
|
|
78
|
+
this.isLoading = true
|
|
79
|
+
try {
|
|
80
|
+
const resolved = await this.addressResolver.resolveAddress(address)
|
|
81
|
+
return resolved
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.warn('❌ ImageList: Failed to resolve address:', error)
|
|
84
|
+
return null
|
|
85
|
+
} finally {
|
|
86
|
+
this.isLoading = false
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
args: () => [this.address],
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get the list of items to display from resolved data
|
|
94
|
+
*/
|
|
95
|
+
private getDisplayItems(): any[] {
|
|
96
|
+
const resolved = this.addressTask.value
|
|
97
|
+
if (!resolved) return []
|
|
98
|
+
console.log('resolved images are', resolved)
|
|
99
|
+
const items = (resolved as any)[this.imageHint]
|
|
100
|
+
if (!Array.isArray(items)) return []
|
|
101
|
+
|
|
102
|
+
// Check if items have an index property
|
|
103
|
+
const hasIndex = items.length > 0 && items[0]?.index !== undefined
|
|
104
|
+
|
|
105
|
+
if (!hasIndex) {
|
|
106
|
+
// No index property, return all items
|
|
107
|
+
return items
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Filter by index range
|
|
111
|
+
return items.filter((item) => {
|
|
112
|
+
const index = item.index
|
|
113
|
+
if (this.minIndex !== undefined && index < this.minIndex) return false
|
|
114
|
+
if (this.maxIndex !== undefined && index > this.maxIndex) return false
|
|
115
|
+
return true
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Check if an item is an actual image based on fileType
|
|
121
|
+
*/
|
|
122
|
+
private isImageFile(item: any): boolean {
|
|
123
|
+
if (!item.fileType) return true // Assume image if no fileType
|
|
124
|
+
return item.fileType.startsWith('image/')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get file type icon for non-image files
|
|
129
|
+
*/
|
|
130
|
+
private getFileTypeIcon(item: any): string {
|
|
131
|
+
if (!item.fileType) return '📄'
|
|
132
|
+
|
|
133
|
+
if (item.fileType.startsWith('video/')) return '🎥'
|
|
134
|
+
if (item.fileType.startsWith('audio/')) return '🎵'
|
|
135
|
+
if (item.fileType.startsWith('application/pdf')) return '📕'
|
|
136
|
+
return '📄'
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Render a single image item
|
|
141
|
+
*/
|
|
142
|
+
private renderImageItem(item: any, index: number) {
|
|
143
|
+
const itemIndex = item.index !== undefined ? item.index : index
|
|
144
|
+
const isImage = this.isImageFile(item)
|
|
145
|
+
|
|
146
|
+
if (isImage) {
|
|
147
|
+
// Show broken image placeholder if image doesn't exist or can't load
|
|
148
|
+
const imageSrc = item.src || item.url
|
|
149
|
+
|
|
150
|
+
if (!imageSrc) {
|
|
151
|
+
return html`
|
|
152
|
+
<div class="image-item broken-image-item">
|
|
153
|
+
<div class="broken-image-icon">🖼️❌</div>
|
|
154
|
+
${
|
|
155
|
+
item.index !== undefined
|
|
156
|
+
? html`<div class="broken-image-label">#${item.index}</div>`
|
|
157
|
+
: ''
|
|
158
|
+
}
|
|
159
|
+
<div class="broken-image-text">No image</div>
|
|
160
|
+
</div>
|
|
161
|
+
`
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Use direct image display instead of icon-view to avoid identicons
|
|
165
|
+
return html`
|
|
166
|
+
<div class="">
|
|
167
|
+
<img
|
|
168
|
+
class="min-h-20 min-w-20 object-cover rounded-lg"
|
|
169
|
+
src=${imageSrc}
|
|
170
|
+
alt=${item.index !== undefined ? `#${item.index}` : 'Image'}
|
|
171
|
+
@error=${(e: Event) => {
|
|
172
|
+
const target = e.target as HTMLImageElement
|
|
173
|
+
target.style.display = 'none'
|
|
174
|
+
const parent = target.parentElement
|
|
175
|
+
if (parent) {
|
|
176
|
+
parent.innerHTML = `
|
|
177
|
+
<div class="broken-image-icon">🖼️❌</div>
|
|
178
|
+
<div class="broken-image-text">Failed to load</div>
|
|
179
|
+
`
|
|
180
|
+
}
|
|
181
|
+
}}
|
|
182
|
+
/>
|
|
183
|
+
${
|
|
184
|
+
item.index !== undefined
|
|
185
|
+
? html`<div class="image-label">#${item.index}</div>`
|
|
186
|
+
: ''
|
|
187
|
+
}
|
|
188
|
+
</div>
|
|
189
|
+
`
|
|
190
|
+
} else {
|
|
191
|
+
// Show file type icon for non-images
|
|
192
|
+
return html`
|
|
193
|
+
<div class="image-item file-item">
|
|
194
|
+
<div class="file-icon">${this.getFileTypeIcon(item)}</div>
|
|
195
|
+
${
|
|
196
|
+
item.index !== undefined
|
|
197
|
+
? html`<div class="file-label">#${item.index}</div>`
|
|
198
|
+
: ''
|
|
199
|
+
}
|
|
200
|
+
<div class="file-type">${item.fileType || 'Unknown'}</div>
|
|
201
|
+
</div>
|
|
202
|
+
`
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private getImageMinSizePx(): number {
|
|
207
|
+
switch (this.size) {
|
|
208
|
+
case 'x-small':
|
|
209
|
+
return 80
|
|
210
|
+
case 'small':
|
|
211
|
+
return 120
|
|
212
|
+
case 'medium':
|
|
213
|
+
return 160
|
|
214
|
+
case 'large':
|
|
215
|
+
return 240
|
|
216
|
+
default:
|
|
217
|
+
return 160
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
override render() {
|
|
222
|
+
if (this.isLoading) {
|
|
223
|
+
return html`
|
|
224
|
+
<div class="flex flex-col items-center justify-center py-10 px-5 text-neutral-60">
|
|
225
|
+
<div class="text-[24px] mb-2 animate-spin">⏳</div>
|
|
226
|
+
<div class="text-[14px]">Loading images...</div>
|
|
227
|
+
</div>
|
|
228
|
+
`
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const items = this.getDisplayItems()
|
|
232
|
+
|
|
233
|
+
if (items.length === 0) {
|
|
234
|
+
return html`
|
|
235
|
+
<div class="flex flex-col items-center justify-center py-10 px-5 text-neutral-70">
|
|
236
|
+
<div class="text-[14px]">No images found</div>
|
|
237
|
+
</div>
|
|
238
|
+
`
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const grid = html`
|
|
242
|
+
<div class="grid grid-cols-[repeat(auto-fill,minmax(${this.getImageMinSizePx()}px,300px))] gap-3 p-2">
|
|
243
|
+
${repeat(
|
|
244
|
+
items,
|
|
245
|
+
(item, index) => item.index ?? index,
|
|
246
|
+
(item, index) => this.renderImageItem(item, index)
|
|
247
|
+
)}
|
|
248
|
+
</div>
|
|
249
|
+
`
|
|
250
|
+
|
|
251
|
+
return this.isCardView
|
|
252
|
+
? html`<div class="flex flex-row flex-wrap border border-neutral-90 rounded-xl p-3 bg-neutral-100">${grid}</div>`
|
|
253
|
+
: grid
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
declare global {
|
|
258
|
+
interface HTMLElementTagNameMap {
|
|
259
|
+
'image-list': ImageList
|
|
260
|
+
}
|
|
261
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AddressImageData,
|
|
3
|
+
type ImageLoadOptions,
|
|
4
|
+
type ImageResult,
|
|
5
|
+
useImageLoader as useImageLoaderHeadless,
|
|
6
|
+
} from '@lukso/transaction-view-headless'
|
|
7
|
+
import type { Signal } from '@preact/signals-core'
|
|
8
|
+
import type { ReactiveController, ReactiveControllerHost } from 'lit'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Lit controller state for image loading
|
|
12
|
+
*/
|
|
13
|
+
export interface ImageLoadState {
|
|
14
|
+
result: ImageResult | null
|
|
15
|
+
loading: boolean
|
|
16
|
+
error: string | null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Lit Reactive Controller for image loading functionality
|
|
21
|
+
* Integrates the headless image loader with Lit elements
|
|
22
|
+
*/
|
|
23
|
+
export class ImageLoaderController implements ReactiveController {
|
|
24
|
+
private host: ReactiveControllerHost
|
|
25
|
+
private headlessLoader = useImageLoaderHeadless()
|
|
26
|
+
|
|
27
|
+
// Internal state
|
|
28
|
+
private _state: ImageLoadState = {
|
|
29
|
+
result: null,
|
|
30
|
+
loading: false,
|
|
31
|
+
error: null,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
constructor(host: ReactiveControllerHost) {
|
|
35
|
+
this.host = host
|
|
36
|
+
host.addController(this)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
hostConnected() {
|
|
40
|
+
// Controller is ready
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
hostDisconnected() {
|
|
44
|
+
// Cleanup if needed
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
get state(): ImageLoadState {
|
|
48
|
+
return this._state
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
get isLoading(): boolean {
|
|
52
|
+
return this._state.loading
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
get error(): string | null {
|
|
56
|
+
return this._state.error
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
get result(): ImageResult | null {
|
|
60
|
+
return this._state.result
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Load image from data array
|
|
65
|
+
*/
|
|
66
|
+
async loadImage(
|
|
67
|
+
imageData: any[],
|
|
68
|
+
options: ImageLoadOptions = {}
|
|
69
|
+
): Promise<ImageResult | null> {
|
|
70
|
+
this.updateState({ loading: true, error: null })
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const result = await this.headlessLoader.loadImage(imageData, options)
|
|
74
|
+
this.updateState({ result, loading: false })
|
|
75
|
+
return result
|
|
76
|
+
} catch (err) {
|
|
77
|
+
const error = err instanceof Error ? err.message : 'Failed to load image'
|
|
78
|
+
this.updateState({ loading: false, error })
|
|
79
|
+
return null
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Load profile image from address data
|
|
85
|
+
*/
|
|
86
|
+
async loadProfileImage(
|
|
87
|
+
addressData: AddressImageData,
|
|
88
|
+
options: ImageLoadOptions = {}
|
|
89
|
+
): Promise<ImageResult | null> {
|
|
90
|
+
this.updateState({ loading: true, error: null })
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const result =
|
|
94
|
+
(await this.headlessLoader.loadProfileImage(addressData, options)) ||
|
|
95
|
+
this.headlessLoader.getFallbackImage(addressData)
|
|
96
|
+
|
|
97
|
+
this.updateState({ result, loading: false })
|
|
98
|
+
return result
|
|
99
|
+
} catch (err) {
|
|
100
|
+
const error =
|
|
101
|
+
err instanceof Error ? err.message : 'Failed to load profile image'
|
|
102
|
+
const fallback = this.headlessLoader.getFallbackImage(addressData)
|
|
103
|
+
this.updateState({ result: fallback, loading: false, error })
|
|
104
|
+
return fallback
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Load image from address signal (reactive)
|
|
110
|
+
*/
|
|
111
|
+
loadFromSignal(
|
|
112
|
+
addressSignal: Signal<{ data?: AddressImageData }>,
|
|
113
|
+
options: ImageLoadOptions = {}
|
|
114
|
+
): void {
|
|
115
|
+
const addressData = addressSignal.value?.data
|
|
116
|
+
|
|
117
|
+
if (!addressData) {
|
|
118
|
+
const fallback = this.headlessLoader.getFallbackImage({})
|
|
119
|
+
this.updateState({ result: fallback, loading: false, error: null })
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
this.loadProfileImage(addressData, options)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get fallback image synchronously
|
|
128
|
+
*/
|
|
129
|
+
getFallbackImage(addressData: AddressImageData): ImageResult {
|
|
130
|
+
return this.headlessLoader.getFallbackImage(addressData)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Check if address is a contract
|
|
135
|
+
*/
|
|
136
|
+
isContractAddress(addressData: AddressImageData): boolean {
|
|
137
|
+
return this.headlessLoader.isContractAddress(addressData)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Clear current state
|
|
142
|
+
*/
|
|
143
|
+
clear(): void {
|
|
144
|
+
this.updateState({ result: null, loading: false, error: null })
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private updateState(updates: Partial<ImageLoadState>): void {
|
|
148
|
+
this._state = { ...this._state, ...updates }
|
|
149
|
+
this.host.requestUpdate()
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Utility function to create an image loader controller
|
|
155
|
+
*/
|
|
156
|
+
export function useImageLoader(
|
|
157
|
+
host: ReactiveControllerHost
|
|
158
|
+
): ImageLoaderController {
|
|
159
|
+
return new ImageLoaderController(host)
|
|
160
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Shared atomic components
|
|
2
|
+
|
|
3
|
+
export * from './address-display'
|
|
4
|
+
// Export types
|
|
5
|
+
export type { AddressData } from './address-display.js'
|
|
6
|
+
export * from './address-view'
|
|
7
|
+
export * from './custom-select'
|
|
8
|
+
export * from './function-name-view'
|
|
9
|
+
export * from './gas-provider-view'
|
|
10
|
+
export * from './icon-view'
|
|
11
|
+
export {
|
|
12
|
+
ImageLoaderController,
|
|
13
|
+
type ImageLoadState,
|
|
14
|
+
useImageLoader,
|
|
15
|
+
} from './image-loader-controller.js'
|
|
16
|
+
export * from './notification-view'
|
|
17
|
+
export * from './progress-view'
|
|
18
|
+
export * from './scroll-to-bottom-button'
|
|
19
|
+
// Export signal integration controllers and utilities
|
|
20
|
+
export {
|
|
21
|
+
MultiSignalController,
|
|
22
|
+
SignalController,
|
|
23
|
+
useSignal,
|
|
24
|
+
useSignals,
|
|
25
|
+
} from './signal-controller.js'
|
|
26
|
+
export {
|
|
27
|
+
TransactionSignalController,
|
|
28
|
+
TransactionViewController,
|
|
29
|
+
type TransactionViewState,
|
|
30
|
+
useTransactionSignal,
|
|
31
|
+
useTransactionView,
|
|
32
|
+
} from './transaction-view-controller.js'
|
|
33
|
+
export * from './url-view'
|
|
34
|
+
export * from './value-view'
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSR-safe centralized import point for @lukso/web-components.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this exists
|
|
5
|
+
*
|
|
6
|
+
* Web components crash during SSR, so apps use dynamic imports:
|
|
7
|
+
* ```ts
|
|
8
|
+
* // In an SSR app
|
|
9
|
+
* const { TransactionPlayback } = await import('@lukso/transaction-view-core')
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* However, if the app ALSO needs to import @lukso/web-components directly:
|
|
13
|
+
* ```ts
|
|
14
|
+
* const webComponents = await import('@lukso/web-components')
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* This creates TWO dynamic import paths to the same components, causing
|
|
18
|
+
* duplicate registration errors. This wrapper prevents that by making
|
|
19
|
+
* registration idempotent.
|
|
20
|
+
*
|
|
21
|
+
* ## Usage
|
|
22
|
+
*
|
|
23
|
+
* Import this module instead of directly importing '@lukso/web-components':
|
|
24
|
+
* ```ts
|
|
25
|
+
* import './load-web-components'
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
// Store the original customElements.define
|
|
30
|
+
const originalDefine =
|
|
31
|
+
typeof customElements !== 'undefined'
|
|
32
|
+
? customElements.define.bind(customElements)
|
|
33
|
+
: null
|
|
34
|
+
|
|
35
|
+
// Wrap customElements.define to catch duplicate registration errors
|
|
36
|
+
if (typeof customElements !== 'undefined' && originalDefine) {
|
|
37
|
+
customElements.define = function (
|
|
38
|
+
name: string,
|
|
39
|
+
constructor: CustomElementConstructor,
|
|
40
|
+
options?: ElementDefinitionOptions
|
|
41
|
+
) {
|
|
42
|
+
// Check if already defined
|
|
43
|
+
if (!customElements.get(name)) {
|
|
44
|
+
originalDefine(name, constructor, options)
|
|
45
|
+
}
|
|
46
|
+
// Silently ignore if already defined
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Now import the web components (they'll use our wrapped define)
|
|
51
|
+
import './load-web-components'
|
|
52
|
+
|
|
53
|
+
// Restore original define after import
|
|
54
|
+
if (originalDefine && typeof customElements !== 'undefined') {
|
|
55
|
+
customElements.define = originalDefine
|
|
56
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { html } from 'lit'
|
|
2
|
+
import { customElement, property, state } from 'lit/decorators.js'
|
|
3
|
+
import { safeCustomElement } from './safe-custom-element'
|
|
4
|
+
import './load-web-components'
|
|
5
|
+
import { CoreLitElement } from '../styles'
|
|
6
|
+
|
|
7
|
+
@safeCustomElement('notification-view')
|
|
8
|
+
export class NotificationView extends CoreLitElement {
|
|
9
|
+
/** Variant controls background/emphasis */
|
|
10
|
+
@property() variant: 'success' | 'warning' | 'error' | 'info' = 'info'
|
|
11
|
+
|
|
12
|
+
/** Notification message content */
|
|
13
|
+
@property() message = ''
|
|
14
|
+
|
|
15
|
+
/** Optional title shown above the description */
|
|
16
|
+
@property() title = ''
|
|
17
|
+
|
|
18
|
+
/** Whether the acknowledge button is shown */
|
|
19
|
+
@property({ type: Boolean, attribute: 'acknowledgeable' }) acknowledgeable =
|
|
20
|
+
false
|
|
21
|
+
|
|
22
|
+
/** Text shown next to acknowledge checkbox */
|
|
23
|
+
@property({ attribute: 'acknowledge-text' }) acknowledgeText =
|
|
24
|
+
'I understand and want to continue'
|
|
25
|
+
|
|
26
|
+
/** Internal acknowledged state (for default behavior) */
|
|
27
|
+
@state() private acknowledged = false
|
|
28
|
+
|
|
29
|
+
/** External control for acknowledge checkbox */
|
|
30
|
+
@property({ type: Boolean, reflect: true }) checked = false
|
|
31
|
+
|
|
32
|
+
private handleAcknowledgeChange(e: Event) {
|
|
33
|
+
e.stopPropagation()
|
|
34
|
+
const target: any = e.target as any
|
|
35
|
+
const checked = !!(target?.checked ?? (e as any).detail?.checked ?? false)
|
|
36
|
+
if (checked !== this.checked) {
|
|
37
|
+
this.checked = checked
|
|
38
|
+
}
|
|
39
|
+
if (this.checked) {
|
|
40
|
+
this.acknowledged = true
|
|
41
|
+
this.dispatchEvent(
|
|
42
|
+
new CustomEvent('notification-acknowledged', {
|
|
43
|
+
detail: {
|
|
44
|
+
variant: this.variant,
|
|
45
|
+
message: this.message,
|
|
46
|
+
title: this.title,
|
|
47
|
+
checked: this.checked,
|
|
48
|
+
},
|
|
49
|
+
bubbles: true,
|
|
50
|
+
})
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
override render() {
|
|
56
|
+
if (this.acknowledged) return html``
|
|
57
|
+
|
|
58
|
+
return html`
|
|
59
|
+
<div class="grid grid-cols-[24px_1fr] items-start gap-3 p-3 rounded-xl text-neutral-10 leading-snug ${this.getVariantBackgroundClass()}">
|
|
60
|
+
<div class="flex items-center justify-center text-[16px] text-neutral-20 self-center" aria-hidden="true">
|
|
61
|
+
<lukso-icon name="${this.renderIcon()}" size="large" color="${this.getVariantColor()}"></lukso-icon>
|
|
62
|
+
</div>
|
|
63
|
+
<div class="text-[14px] flex flex-col gap-1.5">
|
|
64
|
+
${
|
|
65
|
+
this.title
|
|
66
|
+
? html`<div class="font-semibold text-neutral-20 paragraph-inter-14-semi-bold">${this.title}</div>`
|
|
67
|
+
: ''
|
|
68
|
+
}
|
|
69
|
+
<div class="text-neutral-30"><slot>${this.message}</slot></div>
|
|
70
|
+
${
|
|
71
|
+
this.acknowledgeable
|
|
72
|
+
? html`
|
|
73
|
+
<div class="inline-flex items-center gap-2 mt-1 text-[12px] text-neutral-20 select-none cursor-pointer paragraph-inter-12-medium">
|
|
74
|
+
<lukso-checkbox
|
|
75
|
+
size="x-small"
|
|
76
|
+
?checked=${this.checked}
|
|
77
|
+
@change=${this.handleAcknowledgeChange}
|
|
78
|
+
><span class="paragraph-inter-14-semi-bold">${this.acknowledgeText}</span></lukso-checkbox>
|
|
79
|
+
</div>
|
|
80
|
+
`
|
|
81
|
+
: ''
|
|
82
|
+
}
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
`
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private renderIcon() {
|
|
89
|
+
switch (this.variant) {
|
|
90
|
+
case 'success':
|
|
91
|
+
return 'bulb-outline'
|
|
92
|
+
case 'warning':
|
|
93
|
+
return 'warning-round'
|
|
94
|
+
case 'error':
|
|
95
|
+
return 'warning-triangle'
|
|
96
|
+
default:
|
|
97
|
+
return 'bulb-outline'
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private getVariantColor() {
|
|
102
|
+
switch (this.variant) {
|
|
103
|
+
case 'success':
|
|
104
|
+
return 'green-45'
|
|
105
|
+
case 'warning':
|
|
106
|
+
return 'yellow-55'
|
|
107
|
+
case 'error':
|
|
108
|
+
return 'red-55'
|
|
109
|
+
default:
|
|
110
|
+
return 'blue-50'
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private getVariantBackgroundClass() {
|
|
115
|
+
switch (this.variant) {
|
|
116
|
+
case 'success':
|
|
117
|
+
return 'bg-green-85'
|
|
118
|
+
case 'warning':
|
|
119
|
+
return 'bg-yellow-85'
|
|
120
|
+
case 'error':
|
|
121
|
+
return 'bg-red-85'
|
|
122
|
+
default:
|
|
123
|
+
return 'bg-blue-85'
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
declare global {
|
|
129
|
+
interface HTMLElementTagNameMap {
|
|
130
|
+
'notification-view': NotificationView
|
|
131
|
+
}
|
|
132
|
+
}
|