@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,385 @@
|
|
|
1
|
+
import { Task } from '@lit/task'
|
|
2
|
+
import { useAddressResolver } from '@lukso/transaction-view-headless'
|
|
3
|
+
import makeBlockie from 'ethereum-blockies-base64'
|
|
4
|
+
import { html } from 'lit'
|
|
5
|
+
import { property, state } from 'lit/decorators.js'
|
|
6
|
+
import { safeCustomElement } from './safe-custom-element'
|
|
7
|
+
|
|
8
|
+
// Import LUKSO web components
|
|
9
|
+
import './load-web-components'
|
|
10
|
+
import { CoreLitElement } from '../styles'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Address text view component (text only, no image)
|
|
14
|
+
*
|
|
15
|
+
* A text-only address display component that shows name and/or address.
|
|
16
|
+
* Use alongside <icon-view> for complete address presentation.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```html
|
|
20
|
+
* <address-view
|
|
21
|
+
* address="0x1234..."
|
|
22
|
+
* size="medium"
|
|
23
|
+
* ></address-view>
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
@safeCustomElement('address-view')
|
|
27
|
+
export class AddressView extends CoreLitElement {
|
|
28
|
+
/**
|
|
29
|
+
* The address to display (required)
|
|
30
|
+
*/
|
|
31
|
+
@property() address!: string
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Display size
|
|
35
|
+
*/
|
|
36
|
+
@property() size: 'x-small' | 'small' | 'medium' | 'large' = 'small'
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Whether to show only the name (no address)
|
|
40
|
+
*/
|
|
41
|
+
@property({ type: Boolean, attribute: 'name-only' }) nameOnly = false
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Whether to show only the address (no name)
|
|
45
|
+
*/
|
|
46
|
+
@property({ type: Boolean, attribute: 'address-only' }) addressOnly = false
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Card view: render with bordered container, header label and copy button
|
|
50
|
+
*/
|
|
51
|
+
@property({ type: Boolean, attribute: 'is-card-view' }) isCardView = false
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Optional label to show in card header (only when isCardView)
|
|
55
|
+
*/
|
|
56
|
+
@property() label?: string
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Whether to show a copy button (only relevant for card view)
|
|
60
|
+
*/
|
|
61
|
+
@property({ type: Boolean, attribute: 'show-copy-button' }) showCopyButton =
|
|
62
|
+
false
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Maximum width for the display (in pixels)
|
|
66
|
+
*/
|
|
67
|
+
@property({ type: Number, attribute: 'max-width' }) maxWidth = 200
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Whether to show the small identicon next to the name/address
|
|
71
|
+
*/
|
|
72
|
+
@property({ type: Boolean, attribute: 'show-identicon' }) showIdenticon = true
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Address text color token (e.g., "red-55")
|
|
76
|
+
*/
|
|
77
|
+
@property({ type: String, attribute: 'address-color' }) addressColor?: string
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Name text color token (e.g., "blue-50")
|
|
81
|
+
*/
|
|
82
|
+
@property({ type: String, attribute: 'name-color' }) nameColor?: string
|
|
83
|
+
|
|
84
|
+
// Internal state
|
|
85
|
+
@state() private isLoadingAddress = false
|
|
86
|
+
@state() private identicon?: string
|
|
87
|
+
|
|
88
|
+
// Composables
|
|
89
|
+
private addressResolver = useAddressResolver()
|
|
90
|
+
|
|
91
|
+
// Task for resolving address data
|
|
92
|
+
private addressTask = new Task(this, {
|
|
93
|
+
task: async ([address], { signal }) => {
|
|
94
|
+
if (!address) return null
|
|
95
|
+
|
|
96
|
+
this.isLoadingAddress = true
|
|
97
|
+
try {
|
|
98
|
+
// Pass the full compound address to resolver (e.g., "0x123...-0x456...")
|
|
99
|
+
const resolved = await this.addressResolver.resolveAddress(address)
|
|
100
|
+
this.requestUpdate() // Force re-render after resolution
|
|
101
|
+
return resolved
|
|
102
|
+
} catch (error) {
|
|
103
|
+
return null
|
|
104
|
+
} finally {
|
|
105
|
+
this.isLoadingAddress = false
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
args: () => [this.address],
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Get the display address (first segment only for compound addresses)
|
|
113
|
+
* e.g., "0x123...-0x456..." becomes "0x123..."
|
|
114
|
+
*/
|
|
115
|
+
private get displayAddress(): string {
|
|
116
|
+
return this.address.split('-')[0]
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Get display name for the address
|
|
121
|
+
*/
|
|
122
|
+
private getDisplayName(): string {
|
|
123
|
+
const resolved = this.addressTask.value
|
|
124
|
+
|
|
125
|
+
if (!resolved) {
|
|
126
|
+
return this.isLoadingAddress ? 'Loading...' : 'Anonymous'
|
|
127
|
+
}
|
|
128
|
+
const computedName = this.addressResolver.formatDisplayName(resolved)
|
|
129
|
+
return this.label || computedName || 'Anonymous'
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Check if this is a contract address (non-profile)
|
|
134
|
+
*/
|
|
135
|
+
private get isContract(): boolean {
|
|
136
|
+
const resolved = this.addressTask.value
|
|
137
|
+
return resolved ? resolved.__gqltype !== 'Profile' : false
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Check if this should show the address hash (profiles show hash, contracts don't)
|
|
142
|
+
*/
|
|
143
|
+
private get shouldShowAddressHash(): boolean {
|
|
144
|
+
const resolved = this.addressTask.value
|
|
145
|
+
return resolved ? resolved.__gqltype === 'Profile' : true // Default to true for fallback
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Get prefix for the address display
|
|
150
|
+
*/
|
|
151
|
+
private get addressPrefix(): string {
|
|
152
|
+
const resolved = this.addressTask.value
|
|
153
|
+
return resolved?.hasProfile ? '@' : ''
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Check if should show name colors
|
|
158
|
+
*/
|
|
159
|
+
private get shouldShowNameColor(): boolean {
|
|
160
|
+
const resolved = this.addressTask.value
|
|
161
|
+
return resolved ? this.addressResolver.shouldShowNameColor(resolved) : true
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Handle address click
|
|
166
|
+
*/
|
|
167
|
+
private handleClick() {
|
|
168
|
+
this.dispatchEvent(
|
|
169
|
+
new CustomEvent('address-click', {
|
|
170
|
+
detail: {
|
|
171
|
+
address: this.address,
|
|
172
|
+
resolved: this.addressTask.value,
|
|
173
|
+
},
|
|
174
|
+
bubbles: true,
|
|
175
|
+
})
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Generate identicon when address changes */
|
|
180
|
+
protected willUpdate(changed: Map<string, unknown>) {
|
|
181
|
+
if (changed.has('address') && this.address) {
|
|
182
|
+
try {
|
|
183
|
+
this.identicon = makeBlockie(this.displayAddress.toLowerCase())
|
|
184
|
+
} catch {
|
|
185
|
+
this.identicon = undefined
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Copy the display address to clipboard (card view) */
|
|
191
|
+
private handleCopy(event?: Event) {
|
|
192
|
+
// Prevent triggering parent click handlers
|
|
193
|
+
if (event) event.stopPropagation()
|
|
194
|
+
if (navigator.clipboard) {
|
|
195
|
+
navigator.clipboard.writeText(this.displayAddress)
|
|
196
|
+
this.dispatchEvent(
|
|
197
|
+
new CustomEvent('address-copied', {
|
|
198
|
+
detail: { address: this.displayAddress },
|
|
199
|
+
bubbles: true,
|
|
200
|
+
})
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** ========== RENDER HELPERS (Use Cases) ========== */
|
|
206
|
+
/** Pixel size mapping for identicon based on component size */
|
|
207
|
+
private getIdenticonSizePx(): number {
|
|
208
|
+
switch (this.size) {
|
|
209
|
+
case 'x-small':
|
|
210
|
+
return 10
|
|
211
|
+
case 'small':
|
|
212
|
+
return 12
|
|
213
|
+
case 'medium':
|
|
214
|
+
return 14
|
|
215
|
+
case 'large':
|
|
216
|
+
return 16
|
|
217
|
+
default:
|
|
218
|
+
return 14
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Render only the name (and optionally the address hash when allowed for profiles)
|
|
223
|
+
*/
|
|
224
|
+
private renderNameOnly(displayName: string) {
|
|
225
|
+
return html`
|
|
226
|
+
<!-- Name Only Use Case -->
|
|
227
|
+
<lukso-username
|
|
228
|
+
class="inline-block align-middle"
|
|
229
|
+
name=${displayName}
|
|
230
|
+
address=${this.address}
|
|
231
|
+
size=${this.size}
|
|
232
|
+
max-width=${this.maxWidth}
|
|
233
|
+
prefix=${this.addressPrefix}
|
|
234
|
+
address-color=${this.addressColor}
|
|
235
|
+
name-color=${this.nameColor}
|
|
236
|
+
></lukso-username>
|
|
237
|
+
`
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Render only the raw address (no name)
|
|
242
|
+
*/
|
|
243
|
+
private renderAddressOnly() {
|
|
244
|
+
return html`
|
|
245
|
+
<!-- Address Only Use Case -->
|
|
246
|
+
<lukso-username
|
|
247
|
+
class="inline-block align-middle"
|
|
248
|
+
address=${this.displayAddress}
|
|
249
|
+
size=${this.size}
|
|
250
|
+
max-width=${this.maxWidth}
|
|
251
|
+
has-name-color="false"
|
|
252
|
+
?has-address-color=${this.shouldShowNameColor}
|
|
253
|
+
></lukso-username>
|
|
254
|
+
`
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Render both name and (conditionally) the address hash (profiles show hash, contracts may hide it)
|
|
259
|
+
*/
|
|
260
|
+
private renderNameAndAddress(displayName: string) {
|
|
261
|
+
return html`
|
|
262
|
+
<!-- Name + Address Use Case -->
|
|
263
|
+
<lukso-username
|
|
264
|
+
class="inline-block align-middle"
|
|
265
|
+
name=${displayName}
|
|
266
|
+
address=${this.address}
|
|
267
|
+
size=${this.size}
|
|
268
|
+
max-width=${this.maxWidth}
|
|
269
|
+
prefix=${this.addressPrefix}
|
|
270
|
+
address-color=${this.addressColor}
|
|
271
|
+
name-color=${this.nameColor}
|
|
272
|
+
></lukso-username>
|
|
273
|
+
`
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private renderIdenticon() {
|
|
277
|
+
if (!this.showIdenticon || !this.identicon) return ''
|
|
278
|
+
const size = this.getIdenticonSizePx()
|
|
279
|
+
return html`
|
|
280
|
+
<div class="inline-flex items-center justify-center align-middle">
|
|
281
|
+
<img
|
|
282
|
+
class="inline-block rounded-full ring-1 ring-neutral-90 shrink-0"
|
|
283
|
+
src=${this.identicon}
|
|
284
|
+
alt="identicon"
|
|
285
|
+
style="width:${size}px;height:${size}px;image-rendering:pixelated;"
|
|
286
|
+
/>
|
|
287
|
+
</div>
|
|
288
|
+
`
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Map size to a typography class for the label */
|
|
292
|
+
private getLabelClass(): string {
|
|
293
|
+
switch (this.size) {
|
|
294
|
+
case 'x-small':
|
|
295
|
+
return 'paragraph-inter-12-semi-bold'
|
|
296
|
+
case 'small':
|
|
297
|
+
return 'heading-inter-14-bold'
|
|
298
|
+
case 'medium':
|
|
299
|
+
return 'heading-inter-17-semi-bold'
|
|
300
|
+
case 'large':
|
|
301
|
+
return 'heading-inter-21-semi-bold'
|
|
302
|
+
default:
|
|
303
|
+
return 'heading-inter-17-semi-bold'
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private renderDisplayName() {
|
|
308
|
+
const displayName = this.getDisplayName()
|
|
309
|
+
return this.label
|
|
310
|
+
? html`<div class="inline-flex items-center gap-2 font-semibold text-neutral-20 ${this.getLabelClass()}">${
|
|
311
|
+
this.label
|
|
312
|
+
}</div>`
|
|
313
|
+
: html`<lukso-username
|
|
314
|
+
class="inline-block align-middle"
|
|
315
|
+
name=${displayName}
|
|
316
|
+
size=${this.size}
|
|
317
|
+
?has-name-color=${this.shouldShowNameColor}
|
|
318
|
+
></lukso-username>`
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Decide which of the three core inline variants to render
|
|
323
|
+
*/
|
|
324
|
+
private renderInlineVariant(displayName: string) {
|
|
325
|
+
if (this.nameOnly) return this.renderNameOnly(displayName)
|
|
326
|
+
if (this.addressOnly) return this.renderAddressOnly()
|
|
327
|
+
return this.renderNameAndAddress(displayName)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
override render() {
|
|
331
|
+
const displayName = this.getDisplayName()
|
|
332
|
+
// Card view layout
|
|
333
|
+
if (this.isCardView) {
|
|
334
|
+
return html`
|
|
335
|
+
<div class="block border border-neutral-90 rounded-xl py-3 px-4 bg-neutral-100 min-w-[280px]" @click=${
|
|
336
|
+
this.handleClick
|
|
337
|
+
}>
|
|
338
|
+
<div class="flex items-center justify-between mb-2">
|
|
339
|
+
<div class="inline-flex items-center font-normal gap-1">
|
|
340
|
+
${this.renderIdenticon()}
|
|
341
|
+
${this.renderDisplayName()}
|
|
342
|
+
</div>
|
|
343
|
+
${
|
|
344
|
+
this.showCopyButton
|
|
345
|
+
? html`<button
|
|
346
|
+
class="bg-neutral-100 border-0 rounded-lg w-6 h-6 flex items-center justify-center cursor-pointer text-neutral-20 hover:bg-neutral-95"
|
|
347
|
+
@click=${(e: Event) => this.handleCopy(e)}
|
|
348
|
+
title="Copy address"
|
|
349
|
+
>
|
|
350
|
+
<lukso-icon name="copy" size="small"></lukso-icon>
|
|
351
|
+
</button>`
|
|
352
|
+
: html``
|
|
353
|
+
}
|
|
354
|
+
</div>
|
|
355
|
+
<div class="font-semibold text-neutral-20 break-all flex items-center gap-2">
|
|
356
|
+
|
|
357
|
+
<lukso-username
|
|
358
|
+
address=${this.displayAddress}
|
|
359
|
+
size=${this.size}
|
|
360
|
+
max-width=${this.maxWidth}
|
|
361
|
+
has-name-color="false"
|
|
362
|
+
slice-by="24"
|
|
363
|
+
?has-address-color=${this.shouldShowNameColor}
|
|
364
|
+
></lukso-username>
|
|
365
|
+
</div>
|
|
366
|
+
</div>
|
|
367
|
+
`
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Inline/simple view (default)
|
|
371
|
+
return html`
|
|
372
|
+
<div class="inline-flex items-center cursor-pointer gap-1" @click=${this.handleClick}>
|
|
373
|
+
${this.renderIdenticon()}
|
|
374
|
+
<!-- Inline Variants -->
|
|
375
|
+
${this.renderInlineVariant(displayName)}
|
|
376
|
+
</div>
|
|
377
|
+
`
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
declare global {
|
|
382
|
+
interface HTMLElementTagNameMap {
|
|
383
|
+
'address-view': AddressView
|
|
384
|
+
}
|
|
385
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { html } from 'lit'
|
|
2
|
+
import { customElement, property, query, state } from 'lit/decorators.js'
|
|
3
|
+
import { safeCustomElement } from './safe-custom-element'
|
|
4
|
+
import './load-web-components'
|
|
5
|
+
import makeBlockie from 'ethereum-blockies-base64'
|
|
6
|
+
import { CoreLitElement } from '../styles'
|
|
7
|
+
|
|
8
|
+
export interface SelectOption {
|
|
9
|
+
value: string
|
|
10
|
+
label: string
|
|
11
|
+
disabled: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@safeCustomElement('custom-option')
|
|
15
|
+
export class CustomOption extends CoreLitElement {
|
|
16
|
+
@property({ type: Boolean, reflect: true }) selected = false
|
|
17
|
+
@property({ type: Boolean, reflect: true }) disabled = false
|
|
18
|
+
@property({ type: Boolean, reflect: true }) hoverable = true
|
|
19
|
+
|
|
20
|
+
override render() {
|
|
21
|
+
const base =
|
|
22
|
+
'flex flex-row flex-1 items-center px-2 py-1 gap-2 h-7 rounded-md border-0 cursor-pointer text-[14px] text-neutral-20 w-full justify-between'
|
|
23
|
+
const hover = this.hoverable ? ' hover:bg-neutral-95' : ''
|
|
24
|
+
const selected = this.selected ? ' bg-neutral-90 rounded' : ''
|
|
25
|
+
const disabled = this.disabled ? ' text-neutral-60 cursor-not-allowed' : ''
|
|
26
|
+
return html`<button class="${base}${hover}${selected}${disabled}" ?disabled=${this.disabled}><slot></slot></button>`
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@safeCustomElement('custom-select')
|
|
31
|
+
export class CustomSelect extends CoreLitElement {
|
|
32
|
+
@property() value?: string
|
|
33
|
+
@property({ type: Boolean }) open = false
|
|
34
|
+
@property({ type: Array }) options: SelectOption[] = []
|
|
35
|
+
@property({ type: String }) size: 'x-small' | 'small' | 'medium' | 'large' =
|
|
36
|
+
'small'
|
|
37
|
+
|
|
38
|
+
private toggleOpen() {
|
|
39
|
+
this.open = !this.open
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private close() {
|
|
43
|
+
this.open = false
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private onOptionClick(opt: SelectOption) {
|
|
47
|
+
if (opt.disabled) return
|
|
48
|
+
this.value = opt.value
|
|
49
|
+
this.dispatchEvent(new Event('change', { bubbles: true }))
|
|
50
|
+
this.close()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private onKeyDown(e: KeyboardEvent) {
|
|
54
|
+
if (e.key === 'Escape') this.close()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private onOutsideClick = (e: MouseEvent) => {
|
|
58
|
+
if (!this.open) return
|
|
59
|
+
const path = e.composedPath() as Array<EventTarget & Element>
|
|
60
|
+
if (!path.includes(this)) this.close()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
override connectedCallback(): void {
|
|
64
|
+
super.connectedCallback()
|
|
65
|
+
window.addEventListener('click', this.onOutsideClick, { capture: true })
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
override disconnectedCallback(): void {
|
|
69
|
+
window.removeEventListener('click', this.onOutsideClick, { capture: true })
|
|
70
|
+
super.disconnectedCallback()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
override updated() {
|
|
74
|
+
// reflect initial selected by value
|
|
75
|
+
const selectedOption = this.getSelectedOption()
|
|
76
|
+
if (selectedOption) {
|
|
77
|
+
this.value = selectedOption.value
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private getSelectedOption() {
|
|
82
|
+
return this.options.find((o) => o.value === this.value)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private getIdenticonSizePx(): number {
|
|
86
|
+
switch (this.size) {
|
|
87
|
+
case 'x-small':
|
|
88
|
+
return 10
|
|
89
|
+
case 'small':
|
|
90
|
+
return 12
|
|
91
|
+
case 'medium':
|
|
92
|
+
return 14
|
|
93
|
+
case 'large':
|
|
94
|
+
return 16
|
|
95
|
+
default:
|
|
96
|
+
return 14
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private renderIdenticon(address: string) {
|
|
101
|
+
const size = this.getIdenticonSizePx()
|
|
102
|
+
const identicon = makeBlockie(address.toLowerCase())
|
|
103
|
+
return html`
|
|
104
|
+
<div class="inline-flex items-center p-1 justify-center rounded-lg bg-neutral-100 border border-neutral-90 overflow-hidden flex-none">
|
|
105
|
+
<img
|
|
106
|
+
class="block"
|
|
107
|
+
src=${identicon}
|
|
108
|
+
alt="identicon"
|
|
109
|
+
style="width:${size}px;height:${size}px;"
|
|
110
|
+
/>
|
|
111
|
+
</div>
|
|
112
|
+
`
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private renderOptionLabel(option: SelectOption) {
|
|
116
|
+
return option.value.startsWith('0x')
|
|
117
|
+
? html`<div class="inline-flex items-center gap-2 px-2.5 py-1.5 text-neutral-20 leading-tight font-semibold">${this.renderIdenticon(
|
|
118
|
+
option.value
|
|
119
|
+
)} <span class="whitespace-nowrap overflow-hidden text-ellipsis text-[14px]">${
|
|
120
|
+
option.label
|
|
121
|
+
}</span></div>`
|
|
122
|
+
: html`<url-view url=${option.value} label=${option.label} size="x-small"></url-view>`
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
override render() {
|
|
126
|
+
const selectedOption = this.getSelectedOption()
|
|
127
|
+
return html`
|
|
128
|
+
<div class="relative w-full min-w-[360px]" @keydown=${this.onKeyDown}>
|
|
129
|
+
<button class="box-border flex flex-row flex-1 w-full items-center h-[29px] bg-white border border-neutral-90 rounded-lg text-neutral-20 cursor-pointer justify-between" @click=${
|
|
130
|
+
this.toggleOpen
|
|
131
|
+
} aria-haspopup="listbox" aria-expanded=${this.open}>
|
|
132
|
+
<custom-option is-full-width .hoverable=${false}>
|
|
133
|
+
<div class="flex flex-row flex-1 items-center justify-between">
|
|
134
|
+
<span>${selectedOption ? this.renderOptionLabel(selectedOption) : 'Select option'}</span>
|
|
135
|
+
<lukso-icon class="transition-transform ${this.open ? 'rotate-180' : ''}" name="arrow-down-sm" size="small" color="neutral-20" aria-hidden="true">
|
|
136
|
+
</lukso-icon>
|
|
137
|
+
</div>
|
|
138
|
+
</custom-option>
|
|
139
|
+
</button>
|
|
140
|
+
|
|
141
|
+
${
|
|
142
|
+
this.open && this.options.length > 0
|
|
143
|
+
? html`<div class="absolute left-0 top-[calc(100%+8px)] w-full bg-white border border-neutral-90 shadow-[0_0_16px_rgba(189,206,219,0.4)] rounded-xl p-2.5 flex flex-col gap-1 z-[1000] overflow-hidden box-border" role="listbox">
|
|
144
|
+
${this.options.map(
|
|
145
|
+
(
|
|
146
|
+
opt
|
|
147
|
+
) => html`<custom-option .selected=${opt.value === this.value} .disabled=${opt.disabled} @click=${() => this.onOptionClick(opt)}>
|
|
148
|
+
${this.renderOptionLabel(opt)}
|
|
149
|
+
</custom-option>`
|
|
150
|
+
)}
|
|
151
|
+
</div>`
|
|
152
|
+
: ''
|
|
153
|
+
}
|
|
154
|
+
</div>
|
|
155
|
+
`
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
declare global {
|
|
160
|
+
interface HTMLElementTagNameMap {
|
|
161
|
+
'custom-select': CustomSelect
|
|
162
|
+
'custom-option': CustomOption
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { html } from 'lit'
|
|
2
|
+
import { customElement, property } from 'lit/decorators.js'
|
|
3
|
+
import { safeCustomElement } from './safe-custom-element'
|
|
4
|
+
import './load-web-components'
|
|
5
|
+
import { CoreLitElement } from '../styles'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Compact function name badge with icon
|
|
9
|
+
* Props:
|
|
10
|
+
* - label (deprecated): text to display; use default slot instead
|
|
11
|
+
* - icon: lukso-icon name
|
|
12
|
+
* - size: x-small | small | medium | large (affects paddings and font)
|
|
13
|
+
* - color: optional lukso-icon color token (e.g., "blue-50")
|
|
14
|
+
*/
|
|
15
|
+
@safeCustomElement('function-name-view')
|
|
16
|
+
export class FunctionNameView extends CoreLitElement {
|
|
17
|
+
/**
|
|
18
|
+
* Deprecated: Prefer slot content for the label. Will be used as fallback
|
|
19
|
+
* when no slot is provided to maintain backward compatibility.
|
|
20
|
+
*/
|
|
21
|
+
@property() label = ''
|
|
22
|
+
@property() icon: string = 'user'
|
|
23
|
+
@property() size: 'x-small' | 'small' | 'medium' | 'large' = 'small'
|
|
24
|
+
@property({ attribute: 'color' }) color?: string
|
|
25
|
+
|
|
26
|
+
private getPadding(): string {
|
|
27
|
+
switch (this.size) {
|
|
28
|
+
case 'x-small':
|
|
29
|
+
return '2px 8px'
|
|
30
|
+
case 'small':
|
|
31
|
+
return '4px 10px'
|
|
32
|
+
case 'medium':
|
|
33
|
+
return '6px 12px'
|
|
34
|
+
case 'large':
|
|
35
|
+
return '8px 14px'
|
|
36
|
+
default:
|
|
37
|
+
return '4px 10px'
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private getFontSize(): string {
|
|
42
|
+
switch (this.size) {
|
|
43
|
+
case 'x-small':
|
|
44
|
+
return '12px'
|
|
45
|
+
case 'small':
|
|
46
|
+
return '14px'
|
|
47
|
+
case 'medium':
|
|
48
|
+
return '16px'
|
|
49
|
+
case 'large':
|
|
50
|
+
return '18px'
|
|
51
|
+
default:
|
|
52
|
+
return '14px'
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private getIconSize(): 'x-small' | 'small' | 'medium' | 'large' {
|
|
57
|
+
return this.size
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private getHeight(): string {
|
|
61
|
+
switch (this.size) {
|
|
62
|
+
case 'x-small':
|
|
63
|
+
return 'h-6'
|
|
64
|
+
case 'small':
|
|
65
|
+
return 'h-8'
|
|
66
|
+
case 'medium':
|
|
67
|
+
return 'h-10'
|
|
68
|
+
case 'large':
|
|
69
|
+
return 'h-12'
|
|
70
|
+
default:
|
|
71
|
+
return 'h-8'
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
override render() {
|
|
76
|
+
return html`
|
|
77
|
+
<div class="inline-flex ${this.getHeight()} items-center gap-2 bg-neutral-90 text-neutral-20 rounded-xl leading-none font-semibold" style="padding:${this.getPadding()}; font-size:${this.getFontSize()};">
|
|
78
|
+
<lukso-icon name=${this.icon} size=${this.getIconSize()} color=${this.color || ''}></lukso-icon>
|
|
79
|
+
<span class="whitespace-nowrap"><slot>${this.label}</slot></span>
|
|
80
|
+
</div>
|
|
81
|
+
`
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
declare global {
|
|
86
|
+
interface HTMLElementTagNameMap {
|
|
87
|
+
'function-name-view': FunctionNameView
|
|
88
|
+
}
|
|
89
|
+
}
|