@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,426 @@
|
|
|
1
|
+
import { html } from 'lit'
|
|
2
|
+
import { property, state } from 'lit/decorators.js'
|
|
3
|
+
import { repeat } from 'lit/directives/repeat.js'
|
|
4
|
+
import { when } from 'lit/directives/when.js'
|
|
5
|
+
|
|
6
|
+
import '../shared/icon-view.js'
|
|
7
|
+
import '../shared/address-view.js'
|
|
8
|
+
import '../shared/value-view.js'
|
|
9
|
+
import '../shared/image-list.js'
|
|
10
|
+
// Import LUKSO web components
|
|
11
|
+
import '@lukso/web-components'
|
|
12
|
+
import { Task } from '@lit/task'
|
|
13
|
+
import {
|
|
14
|
+
ResolvedAddress,
|
|
15
|
+
useAddressResolver,
|
|
16
|
+
} from '@lukso/transaction-view-headless'
|
|
17
|
+
import { safeCustomElement } from '../shared/safe-custom-element.js'
|
|
18
|
+
import { CoreLitElement } from '../styles'
|
|
19
|
+
|
|
20
|
+
// Value formatting utilities
|
|
21
|
+
function formatUnits(value: bigint, decimals = 18): string {
|
|
22
|
+
const divisor = 10n ** BigInt(decimals)
|
|
23
|
+
const quotient = value / divisor
|
|
24
|
+
const remainder = value % divisor
|
|
25
|
+
|
|
26
|
+
if (remainder === 0n) {
|
|
27
|
+
return quotient.toString()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const remainderStr = remainder.toString().padStart(decimals, '0')
|
|
31
|
+
const trimmed = remainderStr.replace(/0+$/, '')
|
|
32
|
+
return `${quotient}.${trimmed}`
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Asset card component for displaying token/NFT information
|
|
37
|
+
* Replaces the DigitalAssetInfo helper component
|
|
38
|
+
*/
|
|
39
|
+
@safeCustomElement('asset-card')
|
|
40
|
+
export class AssetCard extends CoreLitElement {
|
|
41
|
+
@property() address: string | undefined
|
|
42
|
+
|
|
43
|
+
@property({ type: String, attribute: 'token-id' })
|
|
44
|
+
tokenId: string | undefined
|
|
45
|
+
|
|
46
|
+
@property({ type: String })
|
|
47
|
+
amount: string | undefined
|
|
48
|
+
|
|
49
|
+
/** Function name associated with the asset (e.g., "LSP4DigitalAsset") */
|
|
50
|
+
@property({ type: String, attribute: 'function-name' })
|
|
51
|
+
functionName: string | undefined
|
|
52
|
+
|
|
53
|
+
/** Standard associated with the asset (e.g., "LSP4") */
|
|
54
|
+
@property({ type: String, attribute: 'standard' })
|
|
55
|
+
standard: string | undefined
|
|
56
|
+
|
|
57
|
+
/** Function icon name */
|
|
58
|
+
@property({ type: String, attribute: 'function-icon' })
|
|
59
|
+
functionIcon: string = 'code-outline'
|
|
60
|
+
|
|
61
|
+
@property({ type: String })
|
|
62
|
+
size: 'small' | 'medium' | 'large' = 'medium'
|
|
63
|
+
|
|
64
|
+
@property({ type: Boolean, attribute: 'show-info-button' })
|
|
65
|
+
showInfoButton = true
|
|
66
|
+
|
|
67
|
+
@property({ type: Boolean, attribute: 'show-creator' })
|
|
68
|
+
showCreator = true
|
|
69
|
+
|
|
70
|
+
@state()
|
|
71
|
+
private displayDetails = false
|
|
72
|
+
|
|
73
|
+
@state()
|
|
74
|
+
private _resolvedData?: ResolvedAddress
|
|
75
|
+
|
|
76
|
+
@state()
|
|
77
|
+
private _imageError = false
|
|
78
|
+
|
|
79
|
+
private _handleImageError() {
|
|
80
|
+
this._imageError = true
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private _handleInfoClick() {
|
|
84
|
+
this.displayDetails = !this.displayDetails
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Composables
|
|
88
|
+
private addressResolver = useAddressResolver()
|
|
89
|
+
|
|
90
|
+
private _resolutionTask = new Task(this, {
|
|
91
|
+
task: async ([address]) => {
|
|
92
|
+
if (!address) return null
|
|
93
|
+
|
|
94
|
+
const { resolveAddress } = this.addressResolver
|
|
95
|
+
const data = await resolveAddress(address as string)
|
|
96
|
+
this._resolvedData = data
|
|
97
|
+
return data
|
|
98
|
+
},
|
|
99
|
+
args: () => [this.address] as const,
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
private _extractBaseAddress(addressOrComposite: string): string {
|
|
103
|
+
return addressOrComposite.split('-')[0]
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private _extractTokenId(addressOrComposite: string): string | undefined {
|
|
107
|
+
const parts = addressOrComposite.split('-')
|
|
108
|
+
return parts.length > 1 ? parts[1] : undefined
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private _copyToClipboard(text: string) {
|
|
112
|
+
if (navigator.clipboard) {
|
|
113
|
+
navigator.clipboard.writeText(text)
|
|
114
|
+
// Could dispatch a 'copied' event here for toast notifications
|
|
115
|
+
this.dispatchEvent(new CustomEvent('copied', { detail: { text } }))
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private _renderAddressSection() {
|
|
120
|
+
const data = this._resolutionTask.value
|
|
121
|
+
if (!data) return ''
|
|
122
|
+
|
|
123
|
+
return html`
|
|
124
|
+
<div class="flex items-center justify-between cursor-pointer">
|
|
125
|
+
${when(
|
|
126
|
+
data.lsp4TokenSymbol,
|
|
127
|
+
() => html`
|
|
128
|
+
<address-view
|
|
129
|
+
address=${this._extractBaseAddress(data.address)}
|
|
130
|
+
size="medium"
|
|
131
|
+
max-width=${100}
|
|
132
|
+
.showIdenticon=${false}
|
|
133
|
+
?is-card-view=${false}
|
|
134
|
+
?show-copy-button=${false}
|
|
135
|
+
address-color="neutral-60"
|
|
136
|
+
></address-view>
|
|
137
|
+
`
|
|
138
|
+
)}
|
|
139
|
+
${when(
|
|
140
|
+
this.showInfoButton,
|
|
141
|
+
() => html`
|
|
142
|
+
<lukso-icon name="information" size="small" @click=${this._handleInfoClick}></lukso-icon>
|
|
143
|
+
`
|
|
144
|
+
)}
|
|
145
|
+
</div>
|
|
146
|
+
`
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private _renderCreatorSection() {
|
|
150
|
+
const owner = (this._resolvedData as any)?.owner_id
|
|
151
|
+
const creators = (this._resolvedData as any)?.lsp4Creators || []
|
|
152
|
+
const resolvedCreator = owner || creators[0]?.profile_id
|
|
153
|
+
if (!resolvedCreator) return ''
|
|
154
|
+
|
|
155
|
+
return html`
|
|
156
|
+
<div class="gap-2">
|
|
157
|
+
<div class="text-neutral-60 text-[12px] font-normal">Created by</div>
|
|
158
|
+
<div class="flex justify-center flex-col gap-1 text-[12px] font-bold font-mono">
|
|
159
|
+
<icon-view
|
|
160
|
+
address=${owner}
|
|
161
|
+
size="small"
|
|
162
|
+
name-position="beside"
|
|
163
|
+
></icon-view>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
`
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private _renderContractAddress() {
|
|
170
|
+
const data = this._resolutionTask.value
|
|
171
|
+
|
|
172
|
+
return html`
|
|
173
|
+
<div class="section">
|
|
174
|
+
${when(
|
|
175
|
+
data.lsp4TokenSymbol,
|
|
176
|
+
() => html`
|
|
177
|
+
<address-view
|
|
178
|
+
address=${data.address}
|
|
179
|
+
size="medium"
|
|
180
|
+
label=${data.lsp4TokenName}
|
|
181
|
+
max-width=${100}
|
|
182
|
+
.showIdenticon=${false}
|
|
183
|
+
?is-card-view=${true}
|
|
184
|
+
?show-copy-button=${true}
|
|
185
|
+
address-color="neutral-60"
|
|
186
|
+
></address-view>
|
|
187
|
+
`
|
|
188
|
+
)}
|
|
189
|
+
</div>
|
|
190
|
+
`
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private _renderAssetLongName() {
|
|
194
|
+
const data = this._resolutionTask.value
|
|
195
|
+
|
|
196
|
+
return html`
|
|
197
|
+
${when(
|
|
198
|
+
data?.lsp4TokenName,
|
|
199
|
+
() => html`
|
|
200
|
+
<value-view
|
|
201
|
+
?is-card-view=${true}
|
|
202
|
+
?show-copy-button=${true}
|
|
203
|
+
value=${data.lsp4TokenName}
|
|
204
|
+
label="Token Name"
|
|
205
|
+
></value-view>
|
|
206
|
+
`
|
|
207
|
+
)}
|
|
208
|
+
`
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private _renderAssetSymbol() {
|
|
212
|
+
const data = this._resolutionTask.value
|
|
213
|
+
|
|
214
|
+
return html`
|
|
215
|
+
${when(
|
|
216
|
+
data?.lsp4TokenSymbol,
|
|
217
|
+
() => html`
|
|
218
|
+
<value-view
|
|
219
|
+
?is-card-view=${true}
|
|
220
|
+
?show-copy-button=${true}
|
|
221
|
+
value=${data.lsp4TokenSymbol}
|
|
222
|
+
label="Symbol"
|
|
223
|
+
></value-view>
|
|
224
|
+
`
|
|
225
|
+
)}
|
|
226
|
+
`
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private get displayedTokenId() {
|
|
230
|
+
const formattedTokenId = (this._resolvedData as any)?.formattedTokenId
|
|
231
|
+
const displayTokenId = formattedTokenId ? `${formattedTokenId}` : ''
|
|
232
|
+
if (displayTokenId) return displayTokenId
|
|
233
|
+
// If its a collection, do not show tokenId
|
|
234
|
+
return this.tokenId && !this._resolvedData?.isCollection
|
|
235
|
+
? `${parseInt(this.tokenId, 16)}`
|
|
236
|
+
: undefined
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private _renderTokenId() {
|
|
240
|
+
const displayTokenId = this.displayedTokenId
|
|
241
|
+
if (!displayTokenId) return ''
|
|
242
|
+
|
|
243
|
+
return html`
|
|
244
|
+
<value-view
|
|
245
|
+
?is-card-view=${true}
|
|
246
|
+
?show-copy-button=${true}
|
|
247
|
+
value=${displayTokenId}
|
|
248
|
+
label="Token ID"
|
|
249
|
+
></value-view>
|
|
250
|
+
`
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private _renderDescription() {
|
|
254
|
+
const description = (this._resolvedData as any)?.description
|
|
255
|
+
if (!description) return ''
|
|
256
|
+
|
|
257
|
+
return html`
|
|
258
|
+
<value-view
|
|
259
|
+
?is-card-view=${true}
|
|
260
|
+
?show-copy-button=${true}
|
|
261
|
+
value=${description}
|
|
262
|
+
label="Description"
|
|
263
|
+
>
|
|
264
|
+
</value-view>
|
|
265
|
+
`
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private _renderLinks() {
|
|
269
|
+
const links = (this._resolvedData as any)?.links || []
|
|
270
|
+
if (!links.length) return ''
|
|
271
|
+
|
|
272
|
+
return html`
|
|
273
|
+
<div class="section">
|
|
274
|
+
<div class="section-title">🔗 Links</div>
|
|
275
|
+
<ul class="links-list">
|
|
276
|
+
${repeat(
|
|
277
|
+
links,
|
|
278
|
+
(link: any) => link.url,
|
|
279
|
+
(link: any) => html`
|
|
280
|
+
<li class="link-item">
|
|
281
|
+
<a
|
|
282
|
+
class="external-link"
|
|
283
|
+
href="${link.url}"
|
|
284
|
+
target="_blank"
|
|
285
|
+
rel="noopener noreferrer"
|
|
286
|
+
>
|
|
287
|
+
${link.title || link.url}
|
|
288
|
+
<span>↗</span>
|
|
289
|
+
</a>
|
|
290
|
+
</li>
|
|
291
|
+
`
|
|
292
|
+
)}
|
|
293
|
+
</ul>
|
|
294
|
+
</div>
|
|
295
|
+
`
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private _renderImages() {
|
|
299
|
+
const images = (this._resolvedData as any)?.images || []
|
|
300
|
+
if (images.length === 0) return ''
|
|
301
|
+
|
|
302
|
+
return html`
|
|
303
|
+
<image-list
|
|
304
|
+
address=${this.address}
|
|
305
|
+
image-hint="images"
|
|
306
|
+
size="medium"
|
|
307
|
+
?is-card-view=${true}
|
|
308
|
+
></image-list>
|
|
309
|
+
`
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private _renderAssets() {
|
|
313
|
+
const assets = (this._resolvedData as any)?.assets || []
|
|
314
|
+
if (assets.length === 0) return ''
|
|
315
|
+
|
|
316
|
+
return html`
|
|
317
|
+
<div class="section">
|
|
318
|
+
<div class="section-title">Assets</div>
|
|
319
|
+
<image-list
|
|
320
|
+
address=${this.address}
|
|
321
|
+
image-hint="assets"
|
|
322
|
+
size="medium"
|
|
323
|
+
></image-list>
|
|
324
|
+
</div>
|
|
325
|
+
`
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private _renderStandards() {
|
|
329
|
+
return html`
|
|
330
|
+
<div class="flex justify-between items-center my-4">
|
|
331
|
+
<function-name-view
|
|
332
|
+
label=${this.functionName}
|
|
333
|
+
size="small"
|
|
334
|
+
icon=${this.functionIcon ? this.functionIcon : 'code-outline'}
|
|
335
|
+
></function-name-view>
|
|
336
|
+
<function-name-view label=${this._resolvedData?.standard} size="small"></function-name-view>
|
|
337
|
+
</div>
|
|
338
|
+
`
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
private _renderTokenDetails() {
|
|
342
|
+
if (!this.displayDetails) return ''
|
|
343
|
+
return html`
|
|
344
|
+
<div class="p-4 mt-6 flex flex-col border border-neutral-90 rounded-xl gap-4">
|
|
345
|
+
${this._renderContractAddress()}
|
|
346
|
+
${this._renderAssetLongName()}
|
|
347
|
+
${this._renderAssetSymbol()}
|
|
348
|
+
${this._renderTokenId()}
|
|
349
|
+
${this._renderDescription()}
|
|
350
|
+
${this._renderLinks()}
|
|
351
|
+
${this._renderImages()}
|
|
352
|
+
${this._renderAssets()}
|
|
353
|
+
</div>
|
|
354
|
+
`
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
private renderFormattedTokenIdBadge() {
|
|
358
|
+
if (!this.displayedTokenId) return ''
|
|
359
|
+
return html`
|
|
360
|
+
<span class="absolute top-3 left-3 z-10 text-[12px] text-neutral-20 font-mono font-bold bg-neutral-98 px-2 rounded-full">#${this.displayedTokenId}</span>
|
|
361
|
+
`
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
render() {
|
|
365
|
+
return html`
|
|
366
|
+
<div >
|
|
367
|
+
${this._renderStandards()}
|
|
368
|
+
<div class="mt-6 flex-col">
|
|
369
|
+
<div class="flex items-center justify-center gap-4">
|
|
370
|
+
<div class="flex justify-end w-[48%]">
|
|
371
|
+
<div class="border relative border-neutral-90 rounded-xl">
|
|
372
|
+
${this.renderFormattedTokenIdBadge()}
|
|
373
|
+
<div class="p-2 relative">
|
|
374
|
+
<icon-view
|
|
375
|
+
address="${this.address}"
|
|
376
|
+
size="2x-large"
|
|
377
|
+
.showName=${false}
|
|
378
|
+
></icon-view>
|
|
379
|
+
</div>
|
|
380
|
+
<div style="height: 1px;" class="bg-neutral-90 w-full"></div>
|
|
381
|
+
<div class="px-2">
|
|
382
|
+
${this._renderAddressSection()}
|
|
383
|
+
</div>
|
|
384
|
+
</div>
|
|
385
|
+
</div>
|
|
386
|
+
|
|
387
|
+
<div class="flex flex-col gap-1 w-[51%]">
|
|
388
|
+
<div class="text-[14px] font-bold text-neutral-20">
|
|
389
|
+
${this._resolvedData?.lsp4TokenName || 'UNKNOWN'}
|
|
390
|
+
</div>
|
|
391
|
+
<div class="text-[17px] font-semibold text-neutral-20">
|
|
392
|
+
${when(
|
|
393
|
+
this.amount,
|
|
394
|
+
() => html`
|
|
395
|
+
<span class="">${formatUnits(BigInt(this.amount!), this._resolvedData?.decimals)}</span>
|
|
396
|
+
<span class="ml-1 text-neutral-60 max-w-[60px] whitespace-nowrap overflow-hidden text-ellipsis text-[14px] font-semibold">${
|
|
397
|
+
this._resolvedData?.lsp4TokenSymbol || ''
|
|
398
|
+
}</span>
|
|
399
|
+
`,
|
|
400
|
+
() => html`${this._resolvedData?.lsp4TokenSymbol || ''}`
|
|
401
|
+
)}
|
|
402
|
+
</div>
|
|
403
|
+
|
|
404
|
+
${when(
|
|
405
|
+
this.displayedTokenId,
|
|
406
|
+
() =>
|
|
407
|
+
html`<div class="text-[14px] font-medium text-neutral-60"><span class="font-bold">#${this.displayedTokenId}</span></div>`
|
|
408
|
+
)}
|
|
409
|
+
<div class="mt-4">
|
|
410
|
+
${when(this.showCreator, () => this._renderCreatorSection())}
|
|
411
|
+
</div>
|
|
412
|
+
</div>
|
|
413
|
+
</div>
|
|
414
|
+
<div class="border-neutral-90 pt-2">
|
|
415
|
+
${this._renderTokenDetails()}
|
|
416
|
+
</div>
|
|
417
|
+
</div>
|
|
418
|
+
`
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
declare global {
|
|
423
|
+
interface HTMLElementTagNameMap {
|
|
424
|
+
'asset-card': AssetCard
|
|
425
|
+
}
|
|
426
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { Task } from '@lit/task'
|
|
2
|
+
import {
|
|
3
|
+
type ResolvedAddress,
|
|
4
|
+
useAddressResolver,
|
|
5
|
+
useImageLoader,
|
|
6
|
+
} from '@lukso/transaction-view-headless'
|
|
7
|
+
import { css, html, LitElement, nothing } from 'lit'
|
|
8
|
+
import { property, state } from 'lit/decorators.js'
|
|
9
|
+
import { safeCustomElement } from '../shared/safe-custom-element'
|
|
10
|
+
import '../shared/load-web-components'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* ContractCard Component
|
|
14
|
+
*
|
|
15
|
+
* A wrapper around lukso-card that auto-resolves contract/token metadata from an address or address-tokenId.
|
|
16
|
+
* Provides the same variants and styling as lukso-card with automatic data population.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```html
|
|
20
|
+
* <!-- Basic contract card -->
|
|
21
|
+
* <contract-card address="0x1234..."></contract-card>
|
|
22
|
+
*
|
|
23
|
+
* <!-- Profile variant with token ID -->
|
|
24
|
+
* <contract-card
|
|
25
|
+
* address="0x1234..."
|
|
26
|
+
* variant="profile"
|
|
27
|
+
* ></contract-card>
|
|
28
|
+
*
|
|
29
|
+
* <!-- With custom slots -->
|
|
30
|
+
* <contract-card address="0x1234..." variant="with-header">
|
|
31
|
+
* <div slot="header">Custom Header</div>
|
|
32
|
+
* <div slot="content">Custom Content</div>
|
|
33
|
+
* </contract-card>
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* @slot header - Custom header content (for with-header variant)
|
|
37
|
+
* @slot content - Custom content area
|
|
38
|
+
* @slot bottom - Bottom slot content
|
|
39
|
+
*
|
|
40
|
+
* @fires address-click - Fired when the card is clicked (if clickable=true)
|
|
41
|
+
*/
|
|
42
|
+
@safeCustomElement('contract-card')
|
|
43
|
+
export class ContractCard extends LitElement {
|
|
44
|
+
/**
|
|
45
|
+
* Contract address or address-tokenId format
|
|
46
|
+
*/
|
|
47
|
+
@property() address!: string
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Pre-resolved address data (optional, for performance optimization)
|
|
51
|
+
*/
|
|
52
|
+
@property({ type: Object }) resolved?: ResolvedAddress
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Card variant (maps to lukso-card variants)
|
|
56
|
+
* - basic: Simple card with content slot
|
|
57
|
+
* - with-header: Card with header and content sections
|
|
58
|
+
* - profile: Profile card layout
|
|
59
|
+
* - profile-2: Alternative profile layout
|
|
60
|
+
* - hero: Large hero-style card
|
|
61
|
+
* - dapp: DApp-specific card layout
|
|
62
|
+
*/
|
|
63
|
+
@property() variant:
|
|
64
|
+
| 'basic'
|
|
65
|
+
| 'with-header'
|
|
66
|
+
| 'profile'
|
|
67
|
+
| 'profile-2'
|
|
68
|
+
| 'hero'
|
|
69
|
+
| 'dapp' = 'basic'
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Border radius size
|
|
73
|
+
*/
|
|
74
|
+
@property({ attribute: 'border-radius' }) borderRadius:
|
|
75
|
+
| 'small'
|
|
76
|
+
| 'medium'
|
|
77
|
+
| 'none' = 'medium'
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Shadow size
|
|
81
|
+
*/
|
|
82
|
+
@property() shadow: 'small' | 'medium' | 'large' = 'large'
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Card width in pixels
|
|
86
|
+
*/
|
|
87
|
+
@property({ type: Number }) width?: number
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Card height in pixels
|
|
91
|
+
*/
|
|
92
|
+
@property({ type: Number }) height?: number
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Whether the card is hoverable
|
|
96
|
+
*/
|
|
97
|
+
@property({ type: Boolean, attribute: 'is-hoverable' }) isHoverable = false
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Whether the card has an overlay
|
|
101
|
+
*/
|
|
102
|
+
@property({ type: Boolean, attribute: 'has-overlay' }) hasOverlay = false
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Custom CSS class for the card
|
|
106
|
+
*/
|
|
107
|
+
@property({ attribute: 'custom-class' }) customClass = ''
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Custom CSS class for the header
|
|
111
|
+
*/
|
|
112
|
+
@property({ attribute: 'header-class' }) headerClass = ''
|
|
113
|
+
|
|
114
|
+
@state() private _resolvedData?: ResolvedAddress
|
|
115
|
+
@state() private _profileImageUrl?: string
|
|
116
|
+
@state() private _backgroundImageUrl?: string
|
|
117
|
+
|
|
118
|
+
private _resolutionTask = new Task(this, {
|
|
119
|
+
task: async ([address, resolved]) => {
|
|
120
|
+
if (resolved) {
|
|
121
|
+
this._resolvedData = resolved
|
|
122
|
+
return resolved
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const { resolveAddress } = useAddressResolver()
|
|
126
|
+
const baseAddress = this._extractBaseAddress(address as string)
|
|
127
|
+
const data = await resolveAddress(baseAddress)
|
|
128
|
+
this._resolvedData = data
|
|
129
|
+
return data
|
|
130
|
+
},
|
|
131
|
+
args: () => [this.address, this.resolved] as const,
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
private _profileImageTask = new Task(this, {
|
|
135
|
+
task: async ([resolved]) => {
|
|
136
|
+
const { loadProfileImage } = useImageLoader()
|
|
137
|
+
const data = resolved || this._resolvedData
|
|
138
|
+
|
|
139
|
+
if (!data) return null
|
|
140
|
+
|
|
141
|
+
const image = await loadProfileImage(data, {
|
|
142
|
+
width: 256,
|
|
143
|
+
height: 256,
|
|
144
|
+
dpr: window.devicePixelRatio || 1,
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
if (image?.src) {
|
|
148
|
+
this._profileImageUrl = image.src
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return image
|
|
152
|
+
},
|
|
153
|
+
args: () => [this._resolvedData] as const,
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
private _backgroundImageTask = new Task(this, {
|
|
157
|
+
task: async ([resolved]) => {
|
|
158
|
+
const { loadBackgroundImage } = useImageLoader()
|
|
159
|
+
const data = resolved || this._resolvedData
|
|
160
|
+
|
|
161
|
+
if (!data) return null
|
|
162
|
+
|
|
163
|
+
const image = await loadBackgroundImage(data, {
|
|
164
|
+
width: 800,
|
|
165
|
+
height: 200,
|
|
166
|
+
dpr: window.devicePixelRatio || 1,
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
if (image?.src) {
|
|
170
|
+
this._backgroundImageUrl = image.src
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return image
|
|
174
|
+
},
|
|
175
|
+
args: () => [this._resolvedData] as const,
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
private _extractBaseAddress(addressOrComposite: string): string {
|
|
179
|
+
return addressOrComposite.split('-')[0]
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private _handleClick() {
|
|
183
|
+
this.dispatchEvent(
|
|
184
|
+
new CustomEvent('address-click', {
|
|
185
|
+
detail: {
|
|
186
|
+
address: this.address,
|
|
187
|
+
resolved: this._resolvedData,
|
|
188
|
+
},
|
|
189
|
+
bubbles: true,
|
|
190
|
+
composed: true,
|
|
191
|
+
})
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
static styles = css`
|
|
196
|
+
:host {
|
|
197
|
+
display: block;
|
|
198
|
+
}
|
|
199
|
+
`
|
|
200
|
+
|
|
201
|
+
render() {
|
|
202
|
+
const baseAddress = this._extractBaseAddress(this.address)
|
|
203
|
+
// is-eoa should ONLY be true when there's no __gqltype at all (actual EOA with no on-chain data)
|
|
204
|
+
// For Profiles, Tokens, NFTs - they all have __gqltype, so is-eoa should be false
|
|
205
|
+
// This ensures lukso-card shows the resolved profile-url image instead of blockie identicon
|
|
206
|
+
const isEoa = !this._resolvedData?.__gqltype
|
|
207
|
+
|
|
208
|
+
return html`
|
|
209
|
+
<lukso-card
|
|
210
|
+
variant="${this.variant}"
|
|
211
|
+
border-radius="${this.borderRadius}"
|
|
212
|
+
shadow="${this.shadow}"
|
|
213
|
+
?is-hoverable="${this.isHoverable}"
|
|
214
|
+
?has-overlay="${this.hasOverlay}"
|
|
215
|
+
?is-eoa="${isEoa}"
|
|
216
|
+
custom-class="${this.customClass}"
|
|
217
|
+
header-class="${this.headerClass}"
|
|
218
|
+
profile-url="${this._profileImageUrl || ''}"
|
|
219
|
+
background-url="${this._backgroundImageUrl || ''}"
|
|
220
|
+
profile-address="${baseAddress}"
|
|
221
|
+
.width="${this.width}"
|
|
222
|
+
.height="${this.height}"
|
|
223
|
+
@click="${this._handleClick}"
|
|
224
|
+
>
|
|
225
|
+
<!-- Pass through all slots -->
|
|
226
|
+
<slot name="header" slot="header"></slot>
|
|
227
|
+
<slot name="content" slot="content"></slot>
|
|
228
|
+
<slot name="bottom" slot="bottom"></slot>
|
|
229
|
+
</lukso-card>
|
|
230
|
+
`
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
declare global {
|
|
235
|
+
interface HTMLElementTagNameMap {
|
|
236
|
+
'contract-card': ContractCard
|
|
237
|
+
}
|
|
238
|
+
}
|