@stellar-expert/ui-framework 1.16.7 → 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/basic-styles/base.scss +50 -50
- package/basic-styles/grid.scss +1 -1
- package/basic-styles/table.scss +1 -1
- package/claimable-balance/claimable-balance-claimants.js +5 -0
- package/contract/contract-api.js +15 -0
- package/contract/invocation-info-view.js +10 -2
- package/contract/sc-val.js +24 -0
- package/controls/button-group.js +25 -19
- package/controls/button-group.scss +46 -46
- package/controls/button.js +93 -78
- package/controls/button.scss +173 -173
- package/controls/code-block.js +42 -34
- package/controls/code-block.scss +71 -71
- package/controls/dropdown.js +318 -287
- package/controls/dropdown.scss +159 -159
- 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/tabs.scss +70 -70
- package/controls/tooltip.js +244 -240
- package/controls/tooltip.scss +116 -116
- package/controls/update-highlighter.js +32 -27
- package/controls/update-highlighter.scss +7 -7
- package/date/date-selector.js +56 -54
- package/date/elapsed-time.js +28 -21
- package/dex/price-dynamic.js +53 -44
- package/dex/price-dynamic.scss +33 -33
- package/directory/directory-hooks.js +109 -97
- package/effect/effect-description.js +5 -3
- 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/block-select.scss +21 -21
- 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/spoiler.scss +6 -6
- package/interaction/theme-selector.js +13 -10
- package/ledger/ledger-entry-href-formatter.js +4 -3
- package/ledger/ledger-entry-link.js +93 -58
- package/ledger/ledger-info-parser.js +28 -0
- package/meta/page-meta-tags.js +243 -238
- package/module/dynamic-module.js +47 -47
- package/package.json +3 -2
- 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/toast/toast-notifications.scss +1 -1
- package/tx/op-description-view.js +84 -81
- 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/controls/tooltip.scss
CHANGED
|
@@ -1,117 +1,117 @@
|
|
|
1
|
-
$tooltip-notch-shadow-color: var(--color-border-shadow);
|
|
2
|
-
|
|
3
|
-
.tooltip-wrapper {
|
|
4
|
-
width: 40rem;
|
|
5
|
-
max-width: Min(40rem, 60vw);
|
|
6
|
-
margin: 0;
|
|
7
|
-
padding: .8rem;
|
|
8
|
-
position: absolute;
|
|
9
|
-
visibility: hidden;
|
|
10
|
-
opacity: 0;
|
|
11
|
-
transition: opacity 0.15s ease 0.4s, visibility 0.15s ease 0.4s;
|
|
12
|
-
z-index: 10;
|
|
13
|
-
background: transparent;
|
|
14
|
-
border: none;
|
|
15
|
-
|
|
16
|
-
&:popover-open {
|
|
17
|
-
visibility: visible;
|
|
18
|
-
opacity: 1;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
.tooltip {
|
|
23
|
-
position: relative;
|
|
24
|
-
font-size: 0.9*$font-size-base;
|
|
25
|
-
font-style: normal;
|
|
26
|
-
line-height: 1.3;
|
|
27
|
-
background: var(--color-bg);
|
|
28
|
-
border-color: var(--color-contrast-border);
|
|
29
|
-
border-radius: $border-radius-input;
|
|
30
|
-
box-shadow: 0 1px 6px 2px var(--color-backdrop);
|
|
31
|
-
padding: 1rem 1.4rem;
|
|
32
|
-
letter-spacing: normal;
|
|
33
|
-
word-break: normal;
|
|
34
|
-
visibility: hidden;
|
|
35
|
-
|
|
36
|
-
&:before {
|
|
37
|
-
content: "";
|
|
38
|
-
background: var(--color-bg);
|
|
39
|
-
height: .8rem;
|
|
40
|
-
width: .8rem;
|
|
41
|
-
position: absolute;
|
|
42
|
-
z-index: 9;
|
|
43
|
-
transform: rotate(45deg);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
&.top:before {
|
|
47
|
-
left: 50%;
|
|
48
|
-
bottom: -.2em;
|
|
49
|
-
box-shadow: 1px 1px 0 $tooltip-notch-shadow-color;
|
|
50
|
-
margin-left: -0.26rem;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
&.bottom:before {
|
|
54
|
-
left: 50%;
|
|
55
|
-
top: -.2em;
|
|
56
|
-
box-shadow: -1px -1px 0 $tooltip-notch-shadow-color;
|
|
57
|
-
margin-left: -0.26rem;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
&.left:before {
|
|
61
|
-
right: -.2em;
|
|
62
|
-
top: 50%;
|
|
63
|
-
margin-top: -0.26em;
|
|
64
|
-
box-shadow: 1px -1px 0 $tooltip-notch-shadow-color;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
&.right:before {
|
|
68
|
-
left: -.2em;
|
|
69
|
-
top: 50%;
|
|
70
|
-
margin-top: -0.26em;
|
|
71
|
-
box-shadow: -1px 1px 0 $tooltip-notch-shadow-color;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
.tooltip-content {
|
|
75
|
-
color: var(--color-text);
|
|
76
|
-
z-index: 5;
|
|
77
|
-
position: relative;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
&.visible {
|
|
81
|
-
visibility: visible;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
.trigger {
|
|
86
|
-
position: absolute;
|
|
87
|
-
font-size: 1.2rem;
|
|
88
|
-
margin-top: -.3rem;
|
|
89
|
-
margin-left: .1rem;
|
|
90
|
-
margin-right: .1rem;
|
|
91
|
-
color: var(--color-primary);
|
|
92
|
-
|
|
93
|
-
+ .trigger {
|
|
94
|
-
margin-left: 1em;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
&:before {
|
|
98
|
-
opacity: 0.4;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
&:after {
|
|
102
|
-
content: '';
|
|
103
|
-
position: absolute;
|
|
104
|
-
opacity: 0.01;
|
|
105
|
-
display: block;
|
|
106
|
-
width: 2em;
|
|
107
|
-
height: 2em;
|
|
108
|
-
left: -0.4em;
|
|
109
|
-
top: -0.2em;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
&:hover {
|
|
113
|
-
&:before {
|
|
114
|
-
opacity: 1;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
1
|
+
$tooltip-notch-shadow-color: var(--color-border-shadow);
|
|
2
|
+
|
|
3
|
+
.tooltip-wrapper {
|
|
4
|
+
width: 40rem;
|
|
5
|
+
max-width: Min(40rem, 60vw);
|
|
6
|
+
margin: 0;
|
|
7
|
+
padding: .8rem;
|
|
8
|
+
position: absolute;
|
|
9
|
+
visibility: hidden;
|
|
10
|
+
opacity: 0;
|
|
11
|
+
transition: opacity 0.15s ease 0.4s, visibility 0.15s ease 0.4s;
|
|
12
|
+
z-index: 10;
|
|
13
|
+
background: transparent;
|
|
14
|
+
border: none;
|
|
15
|
+
|
|
16
|
+
&:popover-open {
|
|
17
|
+
visibility: visible;
|
|
18
|
+
opacity: 1;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.tooltip {
|
|
23
|
+
position: relative;
|
|
24
|
+
font-size: 0.9*$font-size-base;
|
|
25
|
+
font-style: normal;
|
|
26
|
+
line-height: 1.3;
|
|
27
|
+
background: var(--color-bg);
|
|
28
|
+
border-color: var(--color-contrast-border);
|
|
29
|
+
border-radius: $border-radius-input;
|
|
30
|
+
box-shadow: 0 1px 6px 2px var(--color-backdrop);
|
|
31
|
+
padding: 1rem 1.4rem;
|
|
32
|
+
letter-spacing: normal;
|
|
33
|
+
word-break: normal;
|
|
34
|
+
visibility: hidden;
|
|
35
|
+
|
|
36
|
+
&:before {
|
|
37
|
+
content: "";
|
|
38
|
+
background: var(--color-bg);
|
|
39
|
+
height: .8rem;
|
|
40
|
+
width: .8rem;
|
|
41
|
+
position: absolute;
|
|
42
|
+
z-index: 9;
|
|
43
|
+
transform: rotate(45deg);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
&.top:before {
|
|
47
|
+
left: 50%;
|
|
48
|
+
bottom: -.2em;
|
|
49
|
+
box-shadow: 1px 1px 0 $tooltip-notch-shadow-color;
|
|
50
|
+
margin-left: -0.26rem;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
&.bottom:before {
|
|
54
|
+
left: 50%;
|
|
55
|
+
top: -.2em;
|
|
56
|
+
box-shadow: -1px -1px 0 $tooltip-notch-shadow-color;
|
|
57
|
+
margin-left: -0.26rem;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
&.left:before {
|
|
61
|
+
right: -.2em;
|
|
62
|
+
top: 50%;
|
|
63
|
+
margin-top: -0.26em;
|
|
64
|
+
box-shadow: 1px -1px 0 $tooltip-notch-shadow-color;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
&.right:before {
|
|
68
|
+
left: -.2em;
|
|
69
|
+
top: 50%;
|
|
70
|
+
margin-top: -0.26em;
|
|
71
|
+
box-shadow: -1px 1px 0 $tooltip-notch-shadow-color;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.tooltip-content {
|
|
75
|
+
color: var(--color-text);
|
|
76
|
+
z-index: 5;
|
|
77
|
+
position: relative;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
&.visible {
|
|
81
|
+
visibility: visible;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.trigger {
|
|
86
|
+
position: absolute;
|
|
87
|
+
font-size: 1.2rem;
|
|
88
|
+
margin-top: -.3rem;
|
|
89
|
+
margin-left: .1rem;
|
|
90
|
+
margin-right: .1rem;
|
|
91
|
+
color: var(--color-primary);
|
|
92
|
+
|
|
93
|
+
+ .trigger {
|
|
94
|
+
margin-left: 1em;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
&:before {
|
|
98
|
+
opacity: 0.4;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
&:after {
|
|
102
|
+
content: '';
|
|
103
|
+
position: absolute;
|
|
104
|
+
opacity: 0.01;
|
|
105
|
+
display: block;
|
|
106
|
+
width: 2em;
|
|
107
|
+
height: 2em;
|
|
108
|
+
left: -0.4em;
|
|
109
|
+
top: -0.2em;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
&:hover {
|
|
113
|
+
&:before {
|
|
114
|
+
opacity: 1;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
117
|
}
|
|
@@ -1,28 +1,33 @@
|
|
|
1
|
-
import React, {useRef} from 'react'
|
|
2
|
-
import PropTypes from 'prop-types'
|
|
3
|
-
import cn from 'classnames'
|
|
4
|
-
import {useDependantState} from '../state/state-hooks'
|
|
5
|
-
import './update-highlighter.scss'
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
//
|
|
16
|
-
timerRef.current
|
|
17
|
-
timerRef.current
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
1
|
+
import React, {useRef} from 'react'
|
|
2
|
+
import PropTypes from 'prop-types'
|
|
3
|
+
import cn from 'classnames'
|
|
4
|
+
import {useDependantState} from '../state/state-hooks'
|
|
5
|
+
import './update-highlighter.scss'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Briefly highlights its content with an animation when children change
|
|
9
|
+
* @param {Object} props
|
|
10
|
+
* @param {*} props.children - Content to monitor and highlight on update
|
|
11
|
+
*/
|
|
12
|
+
export const UpdateHighlighter = React.memo(function UpdateHighlighter({children}) {
|
|
13
|
+
const timerRef = useRef(0)
|
|
14
|
+
const [active, setActiveState] = useDependantState(() => {
|
|
15
|
+
//reset the timer in case if previous animation wasn't finished yet
|
|
16
|
+
if (timerRef.current) {
|
|
17
|
+
clearTimeout(timerRef.current)
|
|
18
|
+
useRef.current = 0
|
|
19
|
+
}
|
|
20
|
+
//schedule class reset
|
|
21
|
+
timerRef.current = setTimeout(function () {
|
|
22
|
+
timerRef.current = 0
|
|
23
|
+
setActiveState(false)
|
|
24
|
+
}, 600)
|
|
25
|
+
return true
|
|
26
|
+
}, [children])
|
|
27
|
+
|
|
28
|
+
return <span className={cn({highlighter: active})}>{children}</span>
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
UpdateHighlighter.propTypes = {
|
|
32
|
+
children: PropTypes.any.isRequired
|
|
28
33
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
.highlighter {
|
|
2
|
-
animation: highlighter 0.6s ease-in;
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
@keyframes highlighter {
|
|
6
|
-
from {background-color:
|
|
7
|
-
to {background-color: transparent;}
|
|
1
|
+
.highlighter {
|
|
2
|
+
animation: highlighter 0.6s ease-in;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
@keyframes highlighter {
|
|
6
|
+
from {background-color: rgba($color-primary, 0.5);}
|
|
7
|
+
to {background-color: transparent;}
|
|
8
8
|
}
|
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
|
}
|
package/dex/price-dynamic.scss
CHANGED
|
@@ -1,34 +1,34 @@
|
|
|
1
|
-
.price-change {
|
|
2
|
-
vertical-align: super;
|
|
3
|
-
line-height: 1;
|
|
4
|
-
font-size: 0.8em;
|
|
5
|
-
margin-right: -0.2em;
|
|
6
|
-
|
|
7
|
-
&:before {
|
|
8
|
-
font-size: .8em;
|
|
9
|
-
vertical-align: bottom;
|
|
10
|
-
padding: 0 .1em;
|
|
11
|
-
line-height: 2.2;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
&.positive {
|
|
15
|
-
color: var(--color-price-up);
|
|
16
|
-
|
|
17
|
-
&:before {
|
|
18
|
-
content: '\2191';
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
&.negative {
|
|
23
|
-
color: var(--color-price-down);
|
|
24
|
-
|
|
25
|
-
&:before {
|
|
26
|
-
content: '\2193';
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
&.standalone {
|
|
31
|
-
vertical-align: middle;
|
|
32
|
-
font-size: 0.9em;
|
|
33
|
-
}
|
|
1
|
+
.price-change {
|
|
2
|
+
vertical-align: super;
|
|
3
|
+
line-height: 1;
|
|
4
|
+
font-size: 0.8em;
|
|
5
|
+
margin-right: -0.2em;
|
|
6
|
+
|
|
7
|
+
&:before {
|
|
8
|
+
font-size: .8em;
|
|
9
|
+
vertical-align: bottom;
|
|
10
|
+
padding: 0 .1em;
|
|
11
|
+
line-height: 2.2;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
&.positive {
|
|
15
|
+
color: var(--color-price-up);
|
|
16
|
+
|
|
17
|
+
&:before {
|
|
18
|
+
content: '\2191';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
&.negative {
|
|
23
|
+
color: var(--color-price-down);
|
|
24
|
+
|
|
25
|
+
&:before {
|
|
26
|
+
content: '\2193';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
&.standalone {
|
|
31
|
+
vertical-align: middle;
|
|
32
|
+
font-size: 0.9em;
|
|
33
|
+
}
|
|
34
34
|
}
|