@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/interaction/accordion.js
CHANGED
|
@@ -1,36 +1,44 @@
|
|
|
1
|
-
import React, {useCallback, useEffect, useState} from 'react'
|
|
2
|
-
import cn from 'classnames'
|
|
3
|
-
import './accordion.scss'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
*
|
|
7
|
-
* @
|
|
8
|
-
* @
|
|
9
|
-
* @
|
|
10
|
-
*/
|
|
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
|
-
|
|
1
|
+
import React, {useCallback, useEffect, useState} from 'react'
|
|
2
|
+
import cn from 'classnames'
|
|
3
|
+
import './accordion.scss'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {Object} AccordionOption
|
|
7
|
+
* @property {string} [key] - Unique key for the panel (falls back to title)
|
|
8
|
+
* @property {string|JSX.Element} title - Panel header content
|
|
9
|
+
* @property {*} content - Panel body content
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Group of collapsible panels where only one panel is expanded at a time
|
|
14
|
+
* @param {Object} props
|
|
15
|
+
* @param {AccordionOption[]} props.options - Accordion panel definitions
|
|
16
|
+
* @param {string} [props.collapsedSymbol='+'] - Prefix shown for collapsed panels
|
|
17
|
+
* @param {string} [props.expandedSymbol='-'] - Prefix shown for the expanded panel
|
|
18
|
+
*/
|
|
19
|
+
export function Accordion({options, collapsedSymbol = '+', expandedSymbol = '-', ...otherProps}) {
|
|
20
|
+
const [selectedOption, setSelectedOption] = useState()
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
const firstOption = options[0]
|
|
23
|
+
setSelectedOption(firstOption.key || firstOption.title)
|
|
24
|
+
}, [options])
|
|
25
|
+
const changeSelected = useCallback(e => setSelectedOption(e.currentTarget.dataset.key), [options])
|
|
26
|
+
return <div className="accordion" {...otherProps}>
|
|
27
|
+
{options.map(({key, title, content}) => {
|
|
28
|
+
const expanded = key ?
|
|
29
|
+
selectedOption === key :
|
|
30
|
+
selectedOption === title
|
|
31
|
+
return <div key={key || title} className={cn('option', {expanded})}>
|
|
32
|
+
<div className="accordion-header" data-collapsed={collapsedSymbol} data-expanded={expandedSymbol} data-key={key || title}
|
|
33
|
+
onClick={changeSelected}>
|
|
34
|
+
{title || key}
|
|
35
|
+
</div>
|
|
36
|
+
<div className="accordion-collapse">
|
|
37
|
+
<div className="accordion-body">
|
|
38
|
+
{content}
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
})}
|
|
43
|
+
</div>
|
|
36
44
|
}
|
package/interaction/autofocus.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Auto-focuses the given DOM element after a short delay (ref callback)
|
|
3
|
+
* @param {HTMLElement} inputRef - DOM element to focus
|
|
4
|
+
*/
|
|
5
|
+
export function useAutoFocusRef(inputRef) {
|
|
6
|
+
if (!inputRef)
|
|
7
|
+
return
|
|
8
|
+
setTimeout(() => {
|
|
9
|
+
if (document.body.contains(inputRef)) {
|
|
10
|
+
inputRef.focus()
|
|
11
|
+
}
|
|
12
|
+
}, 200)
|
|
13
|
+
}
|
|
@@ -1,54 +1,65 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import PropTypes from 'prop-types'
|
|
3
|
-
import cn from 'classnames'
|
|
4
|
-
import './block-select.scss'
|
|
5
|
-
|
|
6
|
-
function select(target) {
|
|
7
|
-
//handle textarea and inputs
|
|
8
|
-
if (target.nodeName.match(/^(INPUT|TEXTAREA)$/i)) {
|
|
9
|
-
target.focus()
|
|
10
|
-
target.select()
|
|
11
|
-
return
|
|
12
|
-
}
|
|
13
|
-
//handle selection inside text elements
|
|
14
|
-
const range = document.createRange()
|
|
15
|
-
range.selectNodeContents(target)
|
|
16
|
-
const sel = window.getSelection()
|
|
17
|
-
if (typeof sel.setBaseAndExtent === 'function') {
|
|
18
|
-
// Safari
|
|
19
|
-
sel.setBaseAndExtent(target, 0, target, 1)
|
|
20
|
-
}
|
|
21
|
-
sel.removeAllRanges()
|
|
22
|
-
sel.addRange(range)
|
|
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
|
-
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import PropTypes from 'prop-types'
|
|
3
|
+
import cn from 'classnames'
|
|
4
|
+
import './block-select.scss'
|
|
5
|
+
|
|
6
|
+
function select(target) {
|
|
7
|
+
//handle textarea and inputs
|
|
8
|
+
if (target.nodeName.match(/^(INPUT|TEXTAREA)$/i)) {
|
|
9
|
+
target.focus()
|
|
10
|
+
target.select()
|
|
11
|
+
return
|
|
12
|
+
}
|
|
13
|
+
//handle selection inside text elements
|
|
14
|
+
const range = document.createRange()
|
|
15
|
+
range.selectNodeContents(target)
|
|
16
|
+
const sel = window.getSelection()
|
|
17
|
+
if (typeof sel.setBaseAndExtent === 'function') {
|
|
18
|
+
// Safari
|
|
19
|
+
sel.setBaseAndExtent(target, 0, target, 1)
|
|
20
|
+
}
|
|
21
|
+
sel.removeAllRanges()
|
|
22
|
+
sel.addRange(range)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Block that selects all its text content on focus/click
|
|
27
|
+
* @param {Object} props
|
|
28
|
+
* @param {string} [props.as='span'] - HTML tag to render
|
|
29
|
+
* @param {*} props.children - Content to display
|
|
30
|
+
* @param {string} [props.title] - HTML title attribute
|
|
31
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
32
|
+
* @param {boolean} [props.wrap] - Enable text wrapping (true) or disable it (false)
|
|
33
|
+
* @param {boolean} [props.inline] - Force inline display
|
|
34
|
+
* @param {Object} [props.style] - Inline styles
|
|
35
|
+
*/
|
|
36
|
+
export const BlockSelect = React.memo(function BlockSelect({as = 'span', children, title, className, wrap, inline, style, ...op}) {
|
|
37
|
+
const props = {
|
|
38
|
+
className: cn('block-select', className),
|
|
39
|
+
onFocus: e => select(e.target),
|
|
40
|
+
tabIndex: '-1',
|
|
41
|
+
style,
|
|
42
|
+
title,
|
|
43
|
+
...op
|
|
44
|
+
}
|
|
45
|
+
if (wrap) {
|
|
46
|
+
props.style = {...props.style, whiteSpace: 'normal', overflow: 'visible'}
|
|
47
|
+
}
|
|
48
|
+
if (wrap === false) {
|
|
49
|
+
props.style = {...props.style, whiteSpace: 'nowrap', overflow: 'hidden'}
|
|
50
|
+
}
|
|
51
|
+
if (inline) {
|
|
52
|
+
props.style = {...props.style, display: 'inline'}
|
|
53
|
+
}
|
|
54
|
+
return React.createElement(as, props, children)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
BlockSelect.propTypes = {
|
|
58
|
+
children: PropTypes.any.isRequired,
|
|
59
|
+
title: PropTypes.string,
|
|
60
|
+
className: PropTypes.string,
|
|
61
|
+
style: PropTypes.object,
|
|
62
|
+
wrap: PropTypes.bool,
|
|
63
|
+
inline: PropTypes.bool,
|
|
64
|
+
as: PropTypes.string
|
|
54
65
|
}
|
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
.block-select {
|
|
2
|
-
display: inline-block;
|
|
3
|
-
width: auto;
|
|
4
|
-
overflow: auto;
|
|
5
|
-
text-overflow: ellipsis;
|
|
6
|
-
vertical-align: bottom;
|
|
7
|
-
|
|
8
|
-
&:focus {
|
|
9
|
-
outline: none;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
&textarea {
|
|
13
|
-
width: 100%;
|
|
14
|
-
height: 7em;
|
|
15
|
-
margin: 0;
|
|
16
|
-
resize: vertical;
|
|
17
|
-
|
|
18
|
-
&.code {
|
|
19
|
-
font-family: $font-face-mono;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
1
|
+
.block-select {
|
|
2
|
+
display: inline-block;
|
|
3
|
+
width: auto;
|
|
4
|
+
overflow: auto;
|
|
5
|
+
text-overflow: ellipsis;
|
|
6
|
+
vertical-align: bottom;
|
|
7
|
+
|
|
8
|
+
&:focus {
|
|
9
|
+
outline: none;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
&textarea {
|
|
13
|
+
width: 100%;
|
|
14
|
+
height: 7em;
|
|
15
|
+
margin: 0;
|
|
16
|
+
resize: vertical;
|
|
17
|
+
|
|
18
|
+
&.code {
|
|
19
|
+
font-family: $font-face-mono;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
22
|
}
|
|
@@ -1,19 +1,26 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import PropTypes from 'prop-types'
|
|
3
|
-
import {CopyToClipboard as Copy} from 'react-copy-to-clipboard'
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
})
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import PropTypes from 'prop-types'
|
|
3
|
+
import {CopyToClipboard as Copy} from 'react-copy-to-clipboard'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Copy-to-clipboard wrapper; shows a default copy icon if no children are provided
|
|
7
|
+
* @param {Object} props
|
|
8
|
+
* @param {string} props.text - Text to copy to clipboard
|
|
9
|
+
* @param {*} [props.children] - Custom trigger element (defaults to a copy icon)
|
|
10
|
+
* @param {string} [props.title='Copy to clipboard'] - Tooltip text
|
|
11
|
+
*/
|
|
12
|
+
export const CopyToClipboard = React.memo(function CopyToClipboard({text, children, title}) {
|
|
13
|
+
return <Copy text={text}>
|
|
14
|
+
{children ? children : <a href="#" className="icon-copy active-icon" title={title}/>}
|
|
15
|
+
</Copy>
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
CopyToClipboard.defaultProps = {
|
|
19
|
+
title: 'Copy to clipboard'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
CopyToClipboard.propTypes = {
|
|
23
|
+
text: PropTypes.string.isRequired,
|
|
24
|
+
title: PropTypes.string,
|
|
25
|
+
children: PropTypes.any
|
|
19
26
|
}
|
|
@@ -1,16 +1,21 @@
|
|
|
1
|
-
import React, {useEffect, useRef, useState} from 'react'
|
|
2
|
-
import PropTypes from 'prop-types'
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
1
|
+
import React, {useEffect, useRef, useState} from 'react'
|
|
2
|
+
import PropTypes from 'prop-types'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Animated inline progress indicator that cycles through dots
|
|
6
|
+
* @param {Object} props
|
|
7
|
+
* @param {number} [props.dots=5] - Maximum number of dots to display
|
|
8
|
+
*/
|
|
9
|
+
export const InlineProgress = React.memo(function InlineProgress({dots = 5}) {
|
|
10
|
+
const [progress, setProgress] = useState(0),
|
|
11
|
+
intervalRef = useRef(0)
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
intervalRef.current = setInterval(() => setProgress(p => ++p), 500)
|
|
14
|
+
return () => clearInterval(intervalRef.current)
|
|
15
|
+
}, [])
|
|
16
|
+
return <span>{'.'.repeat(progress % (dots + 1))}</span>
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
InlineProgress.propTypes = {
|
|
20
|
+
dots: PropTypes.number
|
|
16
21
|
}
|
package/interaction/qr-code.js
CHANGED
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
import React, {useMemo, useRef} from 'react'
|
|
2
|
-
import {QRCodeCanvas} from 'qrcode.react'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* QrCode renderer
|
|
6
|
-
* @param {
|
|
7
|
-
* @param {
|
|
8
|
-
* @param {
|
|
9
|
-
* @param {
|
|
10
|
-
* @param {
|
|
11
|
-
* @return {JSX.Element}
|
|
12
|
-
*/
|
|
13
|
-
export const QrCode = React.memo(function QrCode({value, caption, size = 320, embeddedImage, embeddedSize}) {
|
|
14
|
-
const foreground = useMemo(() => getComputedStyle(document.documentElement).getPropertyValue('--color-primary'))
|
|
15
|
-
const containerRef = useRef()
|
|
16
|
-
return <div className="text-center" ref={containerRef}>
|
|
17
|
-
<QRCodeCanvas value={value} size={256} level="Q" includeMargin imageSettings={embedImage(embeddedImage, embeddedSize, size)}
|
|
18
|
-
fgColor={foreground} style={{width: size + 'px', height: size + 'px', display: 'block', margin: 'auto'}}/>
|
|
19
|
-
{!!caption && <div className="text-small dimmed condensed word-break">{caption}</div>}
|
|
20
|
-
</div>
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
function embedImage(src, size, qrSize) {
|
|
24
|
-
if (!src)
|
|
25
|
-
return undefined
|
|
26
|
-
if (!size) {
|
|
27
|
-
size = qrSize * 0.1
|
|
28
|
-
}
|
|
29
|
-
return {
|
|
30
|
-
src,
|
|
31
|
-
height: size,
|
|
32
|
-
width: size,
|
|
33
|
-
excavate: true
|
|
34
|
-
}
|
|
1
|
+
import React, {useMemo, useRef} from 'react'
|
|
2
|
+
import {QRCodeCanvas} from 'qrcode.react'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* QrCode renderer
|
|
6
|
+
* @param {string} value - Value to encode
|
|
7
|
+
* @param {string} [caption] - Additional caption under QR code
|
|
8
|
+
* @param {number} [size] - Width|height fo the rendered QR code image
|
|
9
|
+
* @param {string} [embeddedImage] - Optional logo to render in the center of QR code
|
|
10
|
+
* @param {number} [embeddedSize] - Embedded logo size (by default 10% of QR code size)
|
|
11
|
+
* @return {JSX.Element}
|
|
12
|
+
*/
|
|
13
|
+
export const QrCode = React.memo(function QrCode({value, caption, size = 320, embeddedImage, embeddedSize}) {
|
|
14
|
+
const foreground = useMemo(() => getComputedStyle(document.documentElement).getPropertyValue('--color-primary'))
|
|
15
|
+
const containerRef = useRef()
|
|
16
|
+
return <div className="text-center" ref={containerRef}>
|
|
17
|
+
<QRCodeCanvas value={value} size={256} level="Q" includeMargin imageSettings={embedImage(embeddedImage, embeddedSize, size)}
|
|
18
|
+
fgColor={foreground} style={{width: size + 'px', height: size + 'px', display: 'block', margin: 'auto'}}/>
|
|
19
|
+
{!!caption && <div className="text-small dimmed condensed word-break">{caption}</div>}
|
|
20
|
+
</div>
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
function embedImage(src, size, qrSize) {
|
|
24
|
+
if (!src)
|
|
25
|
+
return undefined
|
|
26
|
+
if (!size) {
|
|
27
|
+
size = qrSize * 0.1
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
src,
|
|
31
|
+
height: size,
|
|
32
|
+
width: size,
|
|
33
|
+
excavate: true
|
|
34
|
+
}
|
|
35
35
|
}
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import {useState, useEffect} from 'react'
|
|
2
|
-
import {throttle} from 'throttle-debounce'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Measures window client width
|
|
6
|
-
* @return {
|
|
7
|
-
*/
|
|
8
|
-
export function useWindowWidth() {
|
|
9
|
-
const [width, setWidth] = useState(window.innerWidth)
|
|
10
|
-
|
|
11
|
-
const onResize = throttle(100, function () {
|
|
12
|
-
setWidth(window.innerWidth)
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
useEffect(() => {
|
|
16
|
-
window.addEventListener('resize', onResize)
|
|
17
|
-
return () => window.removeEventListener('resize', onResize)
|
|
18
|
-
}, [])
|
|
19
|
-
|
|
20
|
-
return width
|
|
1
|
+
import {useState, useEffect} from 'react'
|
|
2
|
+
import {throttle} from 'throttle-debounce'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Measures window client width
|
|
6
|
+
* @return {number}
|
|
7
|
+
*/
|
|
8
|
+
export function useWindowWidth() {
|
|
9
|
+
const [width, setWidth] = useState(window.innerWidth)
|
|
10
|
+
|
|
11
|
+
const onResize = throttle(100, function () {
|
|
12
|
+
setWidth(window.innerWidth)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
window.addEventListener('resize', onResize)
|
|
17
|
+
return () => window.removeEventListener('resize', onResize)
|
|
18
|
+
}, [])
|
|
19
|
+
|
|
20
|
+
return width
|
|
21
21
|
}
|
package/interaction/spoiler.js
CHANGED
|
@@ -1,40 +1,53 @@
|
|
|
1
|
-
import React, {useCallback, useEffect, useState} from 'react'
|
|
2
|
-
import PropTypes from 'prop-types'
|
|
3
|
-
import cn from 'classnames'
|
|
4
|
-
import './spoiler.scss'
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
1
|
+
import React, {useCallback, useEffect, useState} from 'react'
|
|
2
|
+
import PropTypes from 'prop-types'
|
|
3
|
+
import cn from 'classnames'
|
|
4
|
+
import './spoiler.scss'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Expandable/collapsible content toggle
|
|
8
|
+
* @param {Object} props
|
|
9
|
+
* @param {boolean} [props.expanded] - Whether the content is initially expanded
|
|
10
|
+
* @param {string} [props.showMore='Show more'] - Label for the expand action
|
|
11
|
+
* @param {string} [props.showLess='Show less'] - Label for the collapse action
|
|
12
|
+
* @param {function({expanded: boolean}): void} [props.onChange] - Callback invoked on toggle
|
|
13
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
14
|
+
* @param {boolean} [props.micro] - Show only an icon without text label
|
|
15
|
+
* @param {Object} [props.style] - Inline styles
|
|
16
|
+
* @param {boolean} [props.active] - Highlight the toggle link
|
|
17
|
+
* @param {*} [props.children] - Content revealed when expanded
|
|
18
|
+
*/
|
|
19
|
+
export const Spoiler = React.memo(function Spoiler({expanded, showMore = 'Show more', showLess = 'Show less', onChange, className, micro, style, active, children}) {
|
|
20
|
+
const [expandedState, setExpandedState] = useState(expanded || false)
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
setExpandedState(expanded)
|
|
23
|
+
}, [expanded])
|
|
24
|
+
|
|
25
|
+
const toggle = useCallback(() => {
|
|
26
|
+
setExpandedState(prevState => {
|
|
27
|
+
const newState = !prevState
|
|
28
|
+
setExpandedState(newState)
|
|
29
|
+
onChange && onChange({expanded: newState})
|
|
30
|
+
})
|
|
31
|
+
}, [onChange])
|
|
32
|
+
|
|
33
|
+
const text = expandedState ? showLess : showMore
|
|
34
|
+
return <>
|
|
35
|
+
<span className={cn('spoiler', className)}>
|
|
36
|
+
<a href="#" className={!active ? 'dimmed' : undefined} title={micro ? text : undefined} style={style} onClick={toggle}>
|
|
37
|
+
{!micro && <span className="spoiler-text">{text}</span>}
|
|
38
|
+
<i className={`icon ${expandedState ? 'icon-less' : 'icon-more'}`}/>
|
|
39
|
+
</a>
|
|
40
|
+
</span>
|
|
41
|
+
{expandedState ? children : null}
|
|
42
|
+
</>
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
Spoiler.propTypes = {
|
|
46
|
+
expanded: PropTypes.bool,
|
|
47
|
+
micro: PropTypes.bool,
|
|
48
|
+
onChange: PropTypes.func,
|
|
49
|
+
showMore: PropTypes.string,
|
|
50
|
+
showLess: PropTypes.string,
|
|
51
|
+
active: PropTypes.bool,
|
|
52
|
+
style: PropTypes.object
|
|
40
53
|
}
|
package/interaction/spoiler.scss
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
.spoiler {
|
|
2
|
-
padding: 0.3em 0;
|
|
3
|
-
|
|
4
|
-
.spoiler-text {
|
|
5
|
-
border-bottom: 1px dotted;
|
|
6
|
-
}
|
|
1
|
+
.spoiler {
|
|
2
|
+
padding: 0.3em 0;
|
|
3
|
+
|
|
4
|
+
.spoiler-text {
|
|
5
|
+
border-bottom: 1px dotted;
|
|
6
|
+
}
|
|
7
7
|
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import {useTheme} from '../state/theme'
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import {useTheme} from '../state/theme'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Theme toggle button that switches between day (light) and night (dark) themes
|
|
6
|
+
*/
|
|
7
|
+
export const ThemeSelector = React.memo(function ThemeSelector() {
|
|
8
|
+
const [theme, setTheme] = useTheme()
|
|
9
|
+
return <a href="#" onClick={() => setTheme(current => current === 'day' ? 'night' : 'day')}>
|
|
10
|
+
{theme === 'day' ?
|
|
11
|
+
<><i className="icon icon-night"/> Dark theme</> :
|
|
12
|
+
<><i className="icon icon-day"/> Light theme</>}
|
|
13
|
+
</a>
|
|
11
14
|
})
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import {getCurrentStellarNetwork} from '../state/stellar-network-hooks'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
+
* Build URL path to StellarExpert explorer page for given ledger entry
|
|
4
5
|
* @param {'account'|'asset'|'ledger'|'tx'|'op'|'offer'|'contract'|'liquidity-pool'|'claimable-balance'} type
|
|
5
|
-
* @param {
|
|
6
|
-
* @param {
|
|
7
|
-
* @return {
|
|
6
|
+
* @param {string|number} id
|
|
7
|
+
* @param {string} [network]
|
|
8
|
+
* @return {string}
|
|
8
9
|
*/
|
|
9
10
|
export function formatExplorerLink(type, id, network = null) {
|
|
10
11
|
if (typeof window.explorerLinkFormatter === 'function') {
|