@stellar-expert/ui-framework 1.16.8 → 1.19.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.
Files changed (85) hide show
  1. package/README.md +1168 -3
  2. package/account/account-address.js +127 -127
  3. package/account/available-balance.js +22 -22
  4. package/account/identicon.js +90 -90
  5. package/account/signer-key.js +65 -64
  6. package/api/explorer-api-hooks.js +209 -202
  7. package/api/explorer-api-paginated-list-hooks.js +441 -440
  8. package/api/explorer-batch-info-loader.js +111 -85
  9. package/api/explorer-tx-api.js +28 -28
  10. package/api/ledger-stream.js +15 -0
  11. package/asset/amount.js +56 -56
  12. package/asset/asset-icon.js +41 -41
  13. package/asset/asset-issuer.js +21 -21
  14. package/asset/asset-link.js +107 -107
  15. package/asset/asset-list-hooks.js +59 -59
  16. package/asset/asset-meta-hooks.js +88 -88
  17. package/asset/asset-selector.js +72 -71
  18. package/claimable-balance/claimable-balance-claimants.js +23 -18
  19. package/contract/contract-api.js +15 -0
  20. package/contract/invocation-info-view.js +10 -2
  21. package/contract/sc-val.js +131 -107
  22. package/controls/button-group.js +25 -19
  23. package/controls/button.js +93 -78
  24. package/controls/code-block.js +42 -34
  25. package/controls/dropdown.js +318 -287
  26. package/controls/external-link.js +10 -4
  27. package/controls/info-tooltip.js +23 -16
  28. package/controls/slider.js +29 -19
  29. package/controls/tabs.js +94 -94
  30. package/controls/tooltip.js +244 -240
  31. package/controls/update-highlighter.js +32 -27
  32. package/date/date-selector.js +56 -54
  33. package/date/elapsed-time.js +28 -21
  34. package/dex/price-dynamic.js +53 -44
  35. package/directory/directory-hooks.js +109 -97
  36. package/effect/effect-description.js +346 -344
  37. package/errors/error-boundary.js +110 -97
  38. package/filter/editors/account-filter-view.js +20 -0
  39. package/filter/editors/asset-filter-view.js +9 -0
  40. package/filter/editors/timestamp-filter-view.js +34 -0
  41. package/filter/editors/type-filter-view.js +148 -0
  42. package/filter/filter-editors.js +102 -0
  43. package/filter/filter-view.js +218 -0
  44. package/filter/filter.scss +57 -0
  45. package/horizon/horizon-account-helpers.js +104 -104
  46. package/horizon/horizon-ledger-helpers.js +35 -35
  47. package/horizon/horizon-trades-helper.js +88 -88
  48. package/horizon/horizon-transaction-helpers.js +36 -36
  49. package/index.d.ts +1271 -0
  50. package/index.js +95 -93
  51. package/interaction/accordion.js +43 -35
  52. package/interaction/autofocus.js +13 -9
  53. package/interaction/block-select.js +64 -53
  54. package/interaction/copy-to-clipboard.js +25 -18
  55. package/interaction/inline-progress.js +20 -15
  56. package/interaction/qr-code.js +34 -34
  57. package/interaction/responsive.js +20 -20
  58. package/interaction/spoiler.js +52 -39
  59. package/interaction/theme-selector.js +13 -10
  60. package/ledger/ledger-entry-href-formatter.js +27 -26
  61. package/ledger/ledger-entry-link.js +93 -58
  62. package/ledger/ledger-info-parser.js +46 -18
  63. package/meta/page-meta-tags.js +243 -238
  64. package/module/dynamic-module.js +47 -47
  65. package/package.json +71 -40
  66. package/state/on-screen-hooks.js +22 -22
  67. package/state/page-visibility-helpers.js +29 -16
  68. package/state/page-visibility-hooks.js +13 -11
  69. package/state/screen-orientation-hooks.js +19 -15
  70. package/state/state-hooks.js +76 -76
  71. package/state/stellar-network-hooks.js +56 -44
  72. package/state/theme.js +29 -28
  73. package/stellar/key-type.js +92 -91
  74. package/stellar/ledger-generic-id.js +39 -39
  75. package/stellar/signature-hint-utils.js +65 -65
  76. package/toast/toast-notifications-block.js +59 -59
  77. package/tx/op-description-view.js +945 -942
  78. package/tx/op-icon.js +98 -98
  79. package/tx/parser/op-balance-changes.js +66 -66
  80. package/tx/parser/op-descriptor.js +51 -48
  81. package/tx/parser/tx-details-parser.js +81 -81
  82. package/tx/parser/tx-matcher.js +371 -371
  83. package/tx/parser/type-filter-matcher.js +126 -126
  84. package/tx/tx-list-hooks.js +32 -18
  85. package/tx/tx-operations-list.js +117 -116
@@ -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
- * @param {String} value - Current input value
7
- * @param {DateSelector~OnChange} onChange - Onchange event handler
8
- * @param {String} [min] - Minimum allowed date
9
- * @param {String} [max] - Maximum allowed date
10
- * @param {*} [ref]
11
- */
12
- export function DateSelector({value, onChange, min, max, ref, ...otherProps}) {
13
- const [date, setDate] = useState(value ? trimIsoDateSeconds(normalizeDate(value)) : '')
14
- useEffect(() => {
15
- if (value) {
16
- setDate(trimIsoDateSeconds(normalizeDate(value)))
17
- } else {
18
- setDate('')
19
- }
20
- }, [value])
21
-
22
- const selectDate = useCallback(debounce(400, function (newDate) {
23
- if (value !== newDate) {
24
- setDate(newDate)
25
- if (onChange) {
26
- onChange(newDate ? toUnixTimestamp(normalizeDate(newDate)) : null)
27
- }
28
- }
29
- }), [onChange, value])
30
-
31
- if (max === undefined) {
32
- max = trimIsoDateSeconds(new Date(new Date().getTime() + 24 * 60 * 60 * 1000))
33
- }
34
- return <input type="datetime-local" value={date} className="date-selector condensed" step={60} ref={ref}
35
- min={min || minSelectableValue} max={max} onChange={e => selectDate(e.target.value)}
36
- style={{width: '11em', overflow: 'hidden'}} {...otherProps}/>
37
- }
38
-
39
- /**
40
- * @param {Date|Number} date
41
- * @return {String}
42
- */
43
- export function trimIsoDateSeconds(date) {
44
- if (typeof date === 'number') {
45
- date = new Date(date)
46
- }
47
- return date.toISOString().replace(/:\d{2}\.\d*Z/, '')
48
- }
49
-
50
- const minSelectableValue = trimIsoDateSeconds(new Date('2015-09-30T16:46:00Z'))
51
-
52
- /**
53
- * @callback DateSelector~OnChange
54
- * @param {number|null} value - Selected value represented as a Unix timestamp or null for invalid date value
1
+ import React, {forwardRef, 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 const DateSelector = forwardRef(({value, onChange, min, max, ...otherProps}, ref) => {
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
  */
@@ -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
- export const ElapsedTime = React.memo(function ElapsedTime({ts, className, suffix}) {
15
- const then = Date.parse(ts)
16
- return <span className={className}>
17
- <TimeAgo date={ts} formatter={(v, unit) => `${v}${timeUnits[unit]}`} now={function () {
18
- const now = Date.now()
19
- return then > now ? then : now
20
- }}/>{suffix}
21
- </span>
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
  })
@@ -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
- export const PriceDynamic = React.memo(function PriceDynamic({change, current, prev, standalone, allowZero}) {
8
- if (change === undefined) {
9
- if (current === prev || !prev || !current) {
10
- change = 0
11
- } else {
12
- change = 100 * (current - prev) / prev
13
- }
14
- }
15
- if (Math.abs(change) > 10000)
16
- return null
17
- change = formatPrice(Math.abs(change), 2) + '%'
18
- if (change === '0%' && !allowZero)
19
- return null
20
- let direction
21
- if (parseInt(change) > 0) {
22
- direction = 'positive'
23
- }
24
- if (parseInt(change) < 0) {
25
- direction = 'negative'
26
- }
27
- const className = cn('price-change', getChangeDirection(change), {standalone})
28
- return <span className={className} aria-label={` (${change})`}>{change}</span>
29
- })
30
-
31
- function getChangeDirection(change) {
32
- change = parseInt(change, 10)
33
- if (change > 0)
34
- return 'positive'
35
- if (change < 0)
36
- return 'negative'
37
- }
38
-
39
- PriceDynamic.propTypes = {
40
- change: PropTypes.number,
41
- current: PropTypes.number,
42
- prev: PropTypes.number,
43
- standalone: PropTypes.bool,
44
- allowZero: PropTypes.bool
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
- export async function getDirectoryEntry(address, options) {
26
- const {forceRefresh = false, extended = false} = options || {}
27
- //ignore invalid addresses
28
- if (!isValidAddress(address))
29
- return null
30
- if (extended) {
31
- return fetchExplorerApi(`directory/${address}` + stringifyQuery({
32
- extended: true,
33
- s: Math.random().toString(36).substr(2)
34
- }))
35
- .catch(err => {
36
- console.error(err)
37
- return null
38
- })
39
- }
40
- //try to load from the shared cache
41
- if (forceRefresh) {
42
- const cachedEntry = cache.get(address)
43
- if (cachedEntry && !cachedEntry.isExpired) return cachedEntry.data
44
- }
45
- //load from the server
46
- return loader.loadEntry(address)
47
- }
48
-
49
- /**
50
- *
51
- * @param {String} address
52
- * @param {{[forceRefresh]: Boolean}} [options]
53
- * @return {String|null}
54
- */
55
- export function useDirectory(address, options) {
56
- const {forceRefresh = false} = options || {}
57
- const [directoryInfo, setDirectoryInfo] = useState(null)
58
- let unloaded = false
59
- useEffect(function () {
60
- if (!isValidAddress(address))
61
- return
62
- if (!forceRefresh) {
63
- const cachedEntry = cache.get(address)
64
- if (cachedEntry && !cachedEntry.isStale) {
65
- if (!cachedEntry.isExpired)
66
- return setDirectoryInfo(cachedEntry.data) //everything is up to date - no need to re-fetch
67
- }
68
- }
69
- //load from the server
70
- loader.loadEntry(address)
71
- .then(di => {
72
- if (!unloaded) {
73
- setDirectoryInfo(di)
74
- }
75
- })
76
- return () => {
77
- unloaded = true
78
- }
79
- }, [address, forceRefresh])
80
- return directoryInfo
81
- }
82
-
83
- export function useDirectoryTags() {
84
- const [tags, setTags] = useState(() => {
85
- const cachedEntry = cache.get('tags')
86
- if (cachedEntry && !cachedEntry.isExpired) {
87
- return cachedEntry.data
88
- }
89
- fetchExplorerApi('directory/tags')
90
- .then(data => {
91
- cache.set('tags', data, 10 * 60) //10 minutes
92
- setTags(data)
93
- })
94
- .catch(err => console.error(err))
95
- return []
96
- })
97
- return tags
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
  }