@stellar-expert/ui-framework 1.16.8 → 1.17.0
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/CLAUDE.md +35 -0
- package/README.md +1125 -3
- package/account/account-address.js +127 -127
- package/account/available-balance.js +22 -22
- package/account/identicon.js +90 -90
- package/account/signer-key.js +65 -64
- package/api/explorer-api-hooks.js +209 -202
- package/api/explorer-api-paginated-list-hooks.js +441 -440
- package/api/explorer-batch-info-loader.js +111 -85
- package/api/explorer-tx-api.js +28 -28
- package/api/ledger-stream.js +15 -0
- package/asset/amount.js +56 -56
- package/asset/asset-icon.js +41 -41
- package/asset/asset-issuer.js +21 -21
- package/asset/asset-link.js +107 -107
- package/asset/asset-list-hooks.js +59 -59
- package/asset/asset-meta-hooks.js +88 -88
- package/asset/asset-selector.js +72 -71
- package/claimable-balance/claimable-balance-claimants.js +23 -18
- package/contract/contract-api.js +15 -0
- package/contract/invocation-info-view.js +10 -2
- package/contract/sc-val.js +131 -107
- package/controls/button-group.js +25 -19
- package/controls/button.js +93 -78
- package/controls/code-block.js +42 -34
- package/controls/dropdown.js +318 -287
- package/controls/external-link.js +10 -4
- package/controls/info-tooltip.js +23 -16
- package/controls/slider.js +29 -19
- package/controls/tabs.js +94 -94
- package/controls/tooltip.js +244 -240
- package/controls/update-highlighter.js +32 -27
- package/date/date-selector.js +56 -54
- package/date/elapsed-time.js +28 -21
- package/dex/price-dynamic.js +53 -44
- package/directory/directory-hooks.js +109 -97
- package/effect/effect-description.js +346 -344
- package/errors/error-boundary.js +110 -97
- package/horizon/horizon-account-helpers.js +104 -104
- package/horizon/horizon-ledger-helpers.js +35 -35
- package/horizon/horizon-trades-helper.js +88 -88
- package/horizon/horizon-transaction-helpers.js +36 -36
- package/index.d.ts +1241 -0
- package/interaction/accordion.js +43 -35
- package/interaction/autofocus.js +13 -9
- package/interaction/block-select.js +64 -53
- package/interaction/copy-to-clipboard.js +25 -18
- package/interaction/inline-progress.js +20 -15
- package/interaction/qr-code.js +34 -34
- package/interaction/responsive.js +20 -20
- package/interaction/spoiler.js +52 -39
- package/interaction/theme-selector.js +13 -10
- package/ledger/ledger-entry-href-formatter.js +27 -26
- package/ledger/ledger-entry-link.js +93 -58
- package/ledger/ledger-info-parser.js +46 -18
- package/meta/page-meta-tags.js +243 -238
- package/module/dynamic-module.js +47 -47
- package/package.json +41 -40
- package/state/on-screen-hooks.js +22 -22
- package/state/page-visibility-helpers.js +29 -16
- package/state/page-visibility-hooks.js +13 -11
- package/state/screen-orientation-hooks.js +19 -15
- package/state/state-hooks.js +76 -76
- package/state/stellar-network-hooks.js +56 -44
- package/state/theme.js +29 -28
- package/stellar/key-type.js +92 -91
- package/stellar/ledger-generic-id.js +39 -39
- package/stellar/signature-hint-utils.js +65 -65
- package/toast/toast-notifications-block.js +59 -59
- package/tx/op-description-view.js +945 -942
- package/tx/op-icon.js +98 -98
- package/tx/parser/op-balance-changes.js +66 -66
- package/tx/parser/op-descriptor.js +51 -48
- package/tx/parser/tx-details-parser.js +81 -81
- package/tx/parser/tx-matcher.js +371 -371
- package/tx/parser/type-filter-matcher.js +126 -126
- package/tx/tx-list-hooks.js +32 -18
- package/tx/tx-operations-list.js +117 -116
package/date/date-selector.js
CHANGED
|
@@ -1,55 +1,57 @@
|
|
|
1
|
-
import React, {useCallback, useEffect, useState} from 'react'
|
|
2
|
-
import {debounce} from 'throttle-debounce'
|
|
3
|
-
import {normalizeDate, toUnixTimestamp} from '@stellar-expert/formatter'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
*
|
|
7
|
-
* @param {
|
|
8
|
-
* @param {
|
|
9
|
-
* @param {
|
|
10
|
-
* @param {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
*
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
1
|
+
import React, {useCallback, useEffect, useState} from 'react'
|
|
2
|
+
import {debounce} from 'throttle-debounce'
|
|
3
|
+
import {normalizeDate, toUnixTimestamp} from '@stellar-expert/formatter'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Date-time picker input
|
|
7
|
+
* @param {string} value - Current input value
|
|
8
|
+
* @param {DateSelector~OnChange} onChange - Onchange event handler
|
|
9
|
+
* @param {string} [min] - Minimum allowed date
|
|
10
|
+
* @param {string} [max] - Maximum allowed date
|
|
11
|
+
* @param {*} [ref]
|
|
12
|
+
*/
|
|
13
|
+
export function DateSelector({value, onChange, min, max, ref, ...otherProps}) {
|
|
14
|
+
const [date, setDate] = useState(value ? trimIsoDateSeconds(normalizeDate(value)) : '')
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (value) {
|
|
17
|
+
setDate(trimIsoDateSeconds(normalizeDate(value)))
|
|
18
|
+
} else {
|
|
19
|
+
setDate('')
|
|
20
|
+
}
|
|
21
|
+
}, [value])
|
|
22
|
+
|
|
23
|
+
const selectDate = useCallback(debounce(400, function (newDate) {
|
|
24
|
+
if (value !== newDate) {
|
|
25
|
+
setDate(newDate)
|
|
26
|
+
if (onChange) {
|
|
27
|
+
onChange(newDate ? toUnixTimestamp(normalizeDate(newDate)) : null)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}), [onChange, value])
|
|
31
|
+
|
|
32
|
+
if (max === undefined) {
|
|
33
|
+
max = trimIsoDateSeconds(new Date(new Date().getTime() + 24 * 60 * 60 * 1000))
|
|
34
|
+
}
|
|
35
|
+
return <input type="datetime-local" value={date} className="date-selector condensed" step={60} ref={ref}
|
|
36
|
+
min={min || minSelectableValue} max={max} onChange={e => selectDate(e.target.value)}
|
|
37
|
+
style={{width: '11em', overflow: 'hidden'}} {...otherProps}/>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Trim seconds and milliseconds for basic ISO date format representation
|
|
42
|
+
* @param {Date|number} date
|
|
43
|
+
* @return {string}
|
|
44
|
+
*/
|
|
45
|
+
export function trimIsoDateSeconds(date) {
|
|
46
|
+
if (typeof date === 'number') {
|
|
47
|
+
date = new Date(date)
|
|
48
|
+
}
|
|
49
|
+
return date.toISOString().replace(/:\d{2}\.\d*Z/, '')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const minSelectableValue = trimIsoDateSeconds(new Date('2015-09-30T16:46:00Z'))
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @callback DateSelector~OnChange
|
|
56
|
+
* @param {number|null} value - Selected value represented as a Unix timestamp or null for invalid date value
|
|
55
57
|
*/
|
package/date/elapsed-time.js
CHANGED
|
@@ -1,22 +1,29 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import TimeAgo from 'react-timeago'
|
|
3
|
-
|
|
4
|
-
const timeUnits = {
|
|
5
|
-
second: 's',
|
|
6
|
-
minute: 'm',
|
|
7
|
-
hour: 'h',
|
|
8
|
-
day: 'd',
|
|
9
|
-
week: 'w',
|
|
10
|
-
month: 'mo',
|
|
11
|
-
year: 'y'
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import TimeAgo from 'react-timeago'
|
|
3
|
+
|
|
4
|
+
const timeUnits = {
|
|
5
|
+
second: 's',
|
|
6
|
+
minute: 'm',
|
|
7
|
+
hour: 'h',
|
|
8
|
+
day: 'd',
|
|
9
|
+
week: 'w',
|
|
10
|
+
month: 'mo',
|
|
11
|
+
year: 'y'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Displays relative elapsed time since a given timestamp (e.g., "3m ago")
|
|
16
|
+
* @param {Object} props
|
|
17
|
+
* @param {Date|string|number} props.ts - Timestamp to measure elapsed time from
|
|
18
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
19
|
+
* @param {string} [props.suffix] - Text appended after the time (e.g., " ago")
|
|
20
|
+
*/
|
|
21
|
+
export const ElapsedTime = React.memo(function ElapsedTime({ts, className, suffix}) {
|
|
22
|
+
const then = Date.parse(ts)
|
|
23
|
+
return <span className={className}>
|
|
24
|
+
<TimeAgo date={ts} formatter={(v, unit) => `${v}${timeUnits[unit]}`} now={function () {
|
|
25
|
+
const now = Date.now()
|
|
26
|
+
return then > now ? then : now
|
|
27
|
+
}}/>{suffix}
|
|
28
|
+
</span>
|
|
22
29
|
})
|
package/dex/price-dynamic.js
CHANGED
|
@@ -1,45 +1,54 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import PropTypes from 'prop-types'
|
|
3
|
-
import cn from 'classnames'
|
|
4
|
-
import {formatPrice} from '@stellar-expert/formatter'
|
|
5
|
-
import './price-dynamic.scss'
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
if (
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if (change
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import PropTypes from 'prop-types'
|
|
3
|
+
import cn from 'classnames'
|
|
4
|
+
import {formatPrice} from '@stellar-expert/formatter'
|
|
5
|
+
import './price-dynamic.scss'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Displays a price change percentage with positive/negative styling
|
|
9
|
+
* @param {Object} props
|
|
10
|
+
* @param {number} [props.change] - Pre-calculated change percentage; auto-calculated from current/prev if omitted
|
|
11
|
+
* @param {number} [props.current] - Current price value (used to calculate change)
|
|
12
|
+
* @param {number} [props.prev] - Previous price value (used to calculate change)
|
|
13
|
+
* @param {boolean} [props.standalone] - Apply standalone styling
|
|
14
|
+
* @param {boolean} [props.allowZero] - Show "0%" instead of returning null for zero change
|
|
15
|
+
*/
|
|
16
|
+
export const PriceDynamic = React.memo(function PriceDynamic({change, current, prev, standalone, allowZero}) {
|
|
17
|
+
if (change === undefined) {
|
|
18
|
+
if (current === prev || !prev || !current) {
|
|
19
|
+
change = 0
|
|
20
|
+
} else {
|
|
21
|
+
change = 100 * (current - prev) / prev
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (Math.abs(change) > 10000)
|
|
25
|
+
return null
|
|
26
|
+
change = formatPrice(Math.abs(change), 2) + '%'
|
|
27
|
+
if (change === '0%' && !allowZero)
|
|
28
|
+
return null
|
|
29
|
+
let direction
|
|
30
|
+
if (parseInt(change) > 0) {
|
|
31
|
+
direction = 'positive'
|
|
32
|
+
}
|
|
33
|
+
if (parseInt(change) < 0) {
|
|
34
|
+
direction = 'negative'
|
|
35
|
+
}
|
|
36
|
+
const className = cn('price-change', getChangeDirection(change), {standalone})
|
|
37
|
+
return <span className={className} aria-label={` (${change})`}>{change}</span>
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
function getChangeDirection(change) {
|
|
41
|
+
change = parseInt(change, 10)
|
|
42
|
+
if (change > 0)
|
|
43
|
+
return 'positive'
|
|
44
|
+
if (change < 0)
|
|
45
|
+
return 'negative'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
PriceDynamic.propTypes = {
|
|
49
|
+
change: PropTypes.number,
|
|
50
|
+
current: PropTypes.number,
|
|
51
|
+
prev: PropTypes.number,
|
|
52
|
+
standalone: PropTypes.bool,
|
|
53
|
+
allowZero: PropTypes.bool
|
|
45
54
|
}
|
|
@@ -1,98 +1,110 @@
|
|
|
1
|
-
import {useEffect, useState} from 'react'
|
|
2
|
-
import {StrKey} from '@stellar/stellar-base'
|
|
3
|
-
import {stringifyQuery} from '@stellar-expert/navigation'
|
|
4
|
-
import {InMemoryClientCache} from '@stellar-expert/client-cache'
|
|
5
|
-
import {fetchExplorerApi} from '../api/explorer-api-call'
|
|
6
|
-
import {ExplorerBatchInfoLoader} from '../api/explorer-batch-info-loader'
|
|
7
|
-
|
|
8
|
-
const cache = new InMemoryClientCache({})
|
|
9
|
-
|
|
10
|
-
function isValidAddress(address) {
|
|
11
|
-
if (!address)
|
|
12
|
-
return false
|
|
13
|
-
if (!StrKey.isValidEd25519PublicKey(address) && !StrKey.isValidContract(address))
|
|
14
|
-
return false
|
|
15
|
-
return true
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const loader = new ExplorerBatchInfoLoader(batch => {
|
|
19
|
-
return fetchExplorerApi('directory' + stringifyQuery({address: batch}))
|
|
20
|
-
}, entry => {
|
|
21
|
-
cache.set(entry.address, entry)
|
|
22
|
-
return {key: entry.address, info: entry}
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
1
|
+
import {useEffect, useState} from 'react'
|
|
2
|
+
import {StrKey} from '@stellar/stellar-base'
|
|
3
|
+
import {stringifyQuery} from '@stellar-expert/navigation'
|
|
4
|
+
import {InMemoryClientCache} from '@stellar-expert/client-cache'
|
|
5
|
+
import {fetchExplorerApi} from '../api/explorer-api-call'
|
|
6
|
+
import {ExplorerBatchInfoLoader} from '../api/explorer-batch-info-loader'
|
|
7
|
+
|
|
8
|
+
const cache = new InMemoryClientCache({})
|
|
9
|
+
|
|
10
|
+
function isValidAddress(address) {
|
|
11
|
+
if (!address)
|
|
12
|
+
return false
|
|
13
|
+
if (!StrKey.isValidEd25519PublicKey(address) && !StrKey.isValidContract(address))
|
|
14
|
+
return false
|
|
15
|
+
return true
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const loader = new ExplorerBatchInfoLoader(batch => {
|
|
19
|
+
return fetchExplorerApi('directory' + stringifyQuery({address: batch}))
|
|
20
|
+
}, entry => {
|
|
21
|
+
cache.set(entry.address, entry)
|
|
22
|
+
return {key: entry.address, info: entry}
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Fetch a directory entry for a Stellar address
|
|
27
|
+
* @param {string} address - Stellar account or contract address
|
|
28
|
+
* @param {Object} [options]
|
|
29
|
+
* @param {boolean} [options.forceRefresh=false] - Bypass cache and fetch fresh data
|
|
30
|
+
* @param {boolean} [options.extended=false] - Fetch extended directory information
|
|
31
|
+
* @return {Promise<Object|null>} Directory entry or null if not found
|
|
32
|
+
*/
|
|
33
|
+
export async function getDirectoryEntry(address, options) {
|
|
34
|
+
const {forceRefresh = false, extended = false} = options || {}
|
|
35
|
+
//ignore invalid addresses
|
|
36
|
+
if (!isValidAddress(address))
|
|
37
|
+
return null
|
|
38
|
+
if (extended) {
|
|
39
|
+
return fetchExplorerApi(`directory/${address}` + stringifyQuery({
|
|
40
|
+
extended: true,
|
|
41
|
+
s: Math.random().toString(36).substr(2)
|
|
42
|
+
}))
|
|
43
|
+
.catch(err => {
|
|
44
|
+
console.error(err)
|
|
45
|
+
return null
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
//try to load from the shared cache
|
|
49
|
+
if (forceRefresh) {
|
|
50
|
+
const cachedEntry = cache.get(address)
|
|
51
|
+
if (cachedEntry && !cachedEntry.isExpired) return cachedEntry.data
|
|
52
|
+
}
|
|
53
|
+
//load from the server
|
|
54
|
+
return loader.loadEntry(address)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
*
|
|
59
|
+
* @param {string} address
|
|
60
|
+
* @param {{[forceRefresh]: boolean}} [options]
|
|
61
|
+
* @return {string|null}
|
|
62
|
+
*/
|
|
63
|
+
export function useDirectory(address, options) {
|
|
64
|
+
const {forceRefresh = false} = options || {}
|
|
65
|
+
const [directoryInfo, setDirectoryInfo] = useState(null)
|
|
66
|
+
let unloaded = false
|
|
67
|
+
useEffect(function () {
|
|
68
|
+
if (!isValidAddress(address))
|
|
69
|
+
return
|
|
70
|
+
if (!forceRefresh) {
|
|
71
|
+
const cachedEntry = cache.get(address)
|
|
72
|
+
if (cachedEntry && !cachedEntry.isStale) {
|
|
73
|
+
if (!cachedEntry.isExpired)
|
|
74
|
+
return setDirectoryInfo(cachedEntry.data) //everything is up to date - no need to re-fetch
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
//load from the server
|
|
78
|
+
loader.loadEntry(address)
|
|
79
|
+
.then(di => {
|
|
80
|
+
if (!unloaded) {
|
|
81
|
+
setDirectoryInfo(di)
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
return () => {
|
|
85
|
+
unloaded = true
|
|
86
|
+
}
|
|
87
|
+
}, [address, forceRefresh])
|
|
88
|
+
return directoryInfo
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* React hook that fetches and returns all available directory tags
|
|
93
|
+
* @return {Array<Object>} Array of directory tag objects
|
|
94
|
+
*/
|
|
95
|
+
export function useDirectoryTags() {
|
|
96
|
+
const [tags, setTags] = useState(() => {
|
|
97
|
+
const cachedEntry = cache.get('tags')
|
|
98
|
+
if (cachedEntry && !cachedEntry.isExpired) {
|
|
99
|
+
return cachedEntry.data
|
|
100
|
+
}
|
|
101
|
+
fetchExplorerApi('directory/tags')
|
|
102
|
+
.then(data => {
|
|
103
|
+
cache.set('tags', data, 10 * 60) //10 minutes
|
|
104
|
+
setTags(data)
|
|
105
|
+
})
|
|
106
|
+
.catch(err => console.error(err))
|
|
107
|
+
return []
|
|
108
|
+
})
|
|
109
|
+
return tags
|
|
98
110
|
}
|