@stellar-expert/ui-framework 1.9.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/.npmrc +1 -0
- package/LICENSE +21 -0
- package/README.md +3 -0
- package/account/account-address.js +127 -0
- package/account/account-address.scss +20 -0
- package/account/available-balance.js +21 -0
- package/account/identicon.js +91 -0
- package/account/identicon.scss +6 -0
- package/account/signer-key.js +65 -0
- package/api/api-cache.js +5 -0
- package/api/explorer-api-call.js +37 -0
- package/api/explorer-api-hooks.js +202 -0
- package/api/explorer-api-paginated-list-hooks.js +439 -0
- package/api/explorer-batch-info-loader.js +85 -0
- package/api/explorer-tx-api.js +29 -0
- package/asset/amount.js +53 -0
- package/asset/asset-icon.js +34 -0
- package/asset/asset-issuer.js +22 -0
- package/asset/asset-link.js +93 -0
- package/asset/asset-link.scss +26 -0
- package/asset/asset-list-hooks.js +60 -0
- package/asset/asset-meta-hooks.js +78 -0
- package/asset/asset-selector.js +72 -0
- package/asset/asset-selector.scss +41 -0
- package/basic-styles/base.scss +53 -0
- package/basic-styles/form.scss +90 -0
- package/basic-styles/grid.scss +388 -0
- package/basic-styles/link.scss +17 -0
- package/basic-styles/lists.scss +68 -0
- package/basic-styles/loader.scss +22 -0
- package/basic-styles/responsive.scss +29 -0
- package/basic-styles/scrollbars.scss +19 -0
- package/basic-styles/spacing.scss +50 -0
- package/basic-styles/table.scss +104 -0
- package/basic-styles/themes.scss +59 -0
- package/basic-styles/typography.scss +191 -0
- package/basic-styles/variables.scss +26 -0
- package/claimable-balance/claimable-balance-claimants.js +12 -0
- package/contract/sc-val.js +70 -0
- package/contract/wasm-section-parser.js +159 -0
- package/contract/wasm-section-reader.js +79 -0
- package/contract/xdr-reader.js +148 -0
- package/controls/active-icon.scss +23 -0
- package/controls/button-group.js +20 -0
- package/controls/button-group.scss +48 -0
- package/controls/button.js +74 -0
- package/controls/button.scss +148 -0
- package/controls/code-block.js +35 -0
- package/controls/code-block.scss +74 -0
- package/controls/dropdown.js +281 -0
- package/controls/dropdown.scss +161 -0
- package/controls/external-link.js +5 -0
- package/controls/info-tooltip.js +17 -0
- package/controls/info-tooltip.scss +4 -0
- package/controls/slider.js +20 -0
- package/controls/slider.scss +22 -0
- package/controls/tabs.js +94 -0
- package/controls/tabs.scss +73 -0
- package/controls/tooltip.js +214 -0
- package/controls/tooltip.scss +97 -0
- package/controls/update-highlighter.js +28 -0
- package/controls/update-highlighter.scss +10 -0
- package/date/date-selector.js +44 -0
- package/date/elapsed-time.js +22 -0
- package/date/utc-timestamp.js +23 -0
- package/dex/price-dynamic.js +45 -0
- package/dex/price-dynamic.scss +36 -0
- package/directory/directory-hooks.js +89 -0
- package/effect/effect-description.js +289 -0
- package/effect/op-effects-view.js +11 -0
- package/effect/op-effects.scss +8 -0
- package/errors/error-boundary.js +98 -0
- package/fonts/font.scss +23 -0
- package/fonts/icons/config.json +1858 -0
- package/fonts/icons/icons-embedded.scss +644 -0
- package/horizon/horizon-account-helpers.js +104 -0
- package/horizon/horizon-client-helpers.js +52 -0
- package/horizon/horizon-ledger-helpers.js +36 -0
- package/horizon/horizon-orderbook-helpers.js +14 -0
- package/horizon/horizon-trades-helper.js +89 -0
- package/horizon/horizon-transaction-helpers.js +36 -0
- package/index.js +85 -0
- package/index.scss +16 -0
- package/interaction/autofocus.js +9 -0
- package/interaction/block-select.js +54 -0
- package/interaction/block-select.scss +24 -0
- package/interaction/copy-to-clipboard.js +19 -0
- package/interaction/inline-progress.js +16 -0
- package/interaction/qr-code.js +45 -0
- package/interaction/responsive.js +21 -0
- package/interaction/spoiler.js +40 -0
- package/interaction/spoiler.scss +9 -0
- package/interaction/theme-selector.js +11 -0
- package/ledger/ledger-entry-href-formatter.js +22 -0
- package/ledger/ledger-entry-link.js +59 -0
- package/meta/page-meta-tags.js +229 -0
- package/module/dynamic-module.js +48 -0
- package/package.json +41 -0
- package/state/on-screen-hooks.js +23 -0
- package/state/page-visibility-helpers.js +17 -0
- package/state/page-visibility-hooks.js +12 -0
- package/state/screen-orientation-hooks.js +16 -0
- package/state/state-hooks.js +77 -0
- package/state/stellar-network-hooks.js +45 -0
- package/state/theme.js +29 -0
- package/stellar/key-type.js +92 -0
- package/stellar/ledger-generic-id.js +40 -0
- package/stellar/signature-hint-utils.js +65 -0
- package/toast/toast-notification-instance.js +43 -0
- package/toast/toast-notification.js +46 -0
- package/toast/toast-notifications-block.js +55 -0
- package/toast/toast-notifications.scss +166 -0
- package/tx/op-accounting-changes.js +27 -0
- package/tx/op-description-view.js +925 -0
- package/tx/op-description.scss +98 -0
- package/tx/op-icon.js +93 -0
- package/tx/parser/op-balance-changes.js +62 -0
- package/tx/parser/op-descriptor.js +49 -0
- package/tx/parser/tx-details-parser.js +75 -0
- package/tx/parser/tx-matcher.js +372 -0
- package/tx/parser/type-filter-matcher.js +127 -0
- package/tx/tx-fee-effect.js +14 -0
- package/tx/tx-list-hooks.js +19 -0
- package/tx/tx-operations-list.js +105 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import {useTheme} from '../state/theme'
|
|
3
|
+
|
|
4
|
+
export const ThemeSelector = React.memo(function ThemeSelector() {
|
|
5
|
+
const [theme, setTheme] = useTheme()
|
|
6
|
+
return <a href="#" onClick={() => setTheme(current => current === 'day' ? 'night' : 'day')}>
|
|
7
|
+
{theme === 'day' ?
|
|
8
|
+
<><i className="icon icon-night"/> Dark theme</> :
|
|
9
|
+
<><i className="icon icon-day"/> Light theme</>}
|
|
10
|
+
</a>
|
|
11
|
+
})
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import {getCurrentStellarNetwork} from '../state/stellar-network-hooks'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {'account'|'asset'|'ledger'|'tx'|'op'|'offer'} type
|
|
5
|
+
* @param {String|Number} id
|
|
6
|
+
* @param {String} network?
|
|
7
|
+
* @return {String}
|
|
8
|
+
*/
|
|
9
|
+
export function formatExplorerLink(type, id, network = null) {
|
|
10
|
+
const segments = [
|
|
11
|
+
explorerFrontendOrigin !== window.origin ? explorerFrontendOrigin : '',
|
|
12
|
+
'explorer',
|
|
13
|
+
network || getCurrentStellarNetwork()
|
|
14
|
+
]
|
|
15
|
+
if (type) {
|
|
16
|
+
segments.push(type)
|
|
17
|
+
}
|
|
18
|
+
if (id) {
|
|
19
|
+
segments.push(id)
|
|
20
|
+
}
|
|
21
|
+
return segments.join('/')
|
|
22
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import PropTypes from 'prop-types'
|
|
3
|
+
import {formatExplorerLink} from './ledger-entry-href-formatter'
|
|
4
|
+
import {useStellarNetwork} from '../state/stellar-network-hooks'
|
|
5
|
+
|
|
6
|
+
function LedgerEntryLink({type, id, network, children}) {
|
|
7
|
+
const globallySetNetwork = useStellarNetwork()
|
|
8
|
+
return <a href={formatExplorerLink(type, id, network || globallySetNetwork)} target="_blank">{children || id}</a>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const TxLink = React.memo(function TxLink({tx, network, children}) {
|
|
12
|
+
return React.createElement(LedgerEntryLink, {type: 'tx', id: tx, network, children})
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
TxLink.propTypes = {
|
|
16
|
+
tx: PropTypes.string.isRequired,
|
|
17
|
+
network: PropTypes.string,
|
|
18
|
+
children: PropTypes.any
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const OpLink = React.memo(function OpLink({op, network, children}) {
|
|
22
|
+
return React.createElement(LedgerEntryLink, {type: 'op', id: op, network, children})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
OpLink.propTypes = {
|
|
26
|
+
op: PropTypes.string.isRequired,
|
|
27
|
+
network: PropTypes.string,
|
|
28
|
+
children: PropTypes.any
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const LedgerLink = React.memo(function LedgerLink({sequence, network, children}) {
|
|
32
|
+
return React.createElement(LedgerEntryLink, {type: 'ledger', id: sequence, network, children})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
LedgerLink.propTypes = {
|
|
36
|
+
sequence: PropTypes.number.isRequired,
|
|
37
|
+
network: PropTypes.string,
|
|
38
|
+
children: PropTypes.any
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const OfferLink = React.memo(function OfferLink({offer, network, children}) {
|
|
42
|
+
return React.createElement(LedgerEntryLink, {type: 'offer', id: offer, network, children})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
OfferLink.propTypes = {
|
|
46
|
+
offer: PropTypes.string.isRequired,
|
|
47
|
+
network: PropTypes.string,
|
|
48
|
+
children: PropTypes.any
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const PoolLink = React.memo(function PoolLink({pool, network, children}) {
|
|
52
|
+
return React.createElement(LedgerEntryLink, {type: 'pool', id: pool, network, children})
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
PoolLink.propTypes = {
|
|
56
|
+
pool: PropTypes.string.isRequired,
|
|
57
|
+
network: PropTypes.string,
|
|
58
|
+
children: PropTypes.any
|
|
59
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import {useEffect} from 'react'
|
|
2
|
+
import isEqual from 'react-fast-compare'
|
|
3
|
+
|
|
4
|
+
const {origin} = window.location
|
|
5
|
+
const domain = origin.replace(/https?:\/\//, '')
|
|
6
|
+
|
|
7
|
+
const metaProps = {
|
|
8
|
+
serviceTitle: '',
|
|
9
|
+
description: '',
|
|
10
|
+
facebookImage: '',
|
|
11
|
+
twitterImage: '',
|
|
12
|
+
twitterUsername: ''
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Initialize default meta properties for the application
|
|
17
|
+
* @param {{serviceTitle: String, description: String, facebookImage: String, twitterImage: String, twitterUsername: String}} appMetaProps
|
|
18
|
+
*/
|
|
19
|
+
export function initMeta(appMetaProps) {
|
|
20
|
+
Object.assign(metaProps, appMetaProps)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function formatPageTitle(title) {
|
|
24
|
+
if (!title)
|
|
25
|
+
return metaProps.serviceTitle
|
|
26
|
+
if (title.includes(metaProps.serviceTitle))
|
|
27
|
+
return title
|
|
28
|
+
return `${title} ${metaProps.serviceTitle}`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function generateCanonicalLink(params, canonicalUrl) {
|
|
32
|
+
return {
|
|
33
|
+
tag: 'link',
|
|
34
|
+
locator: 'rel',
|
|
35
|
+
tags: [
|
|
36
|
+
{name: 'canonical', content: canonicalUrl, attribute: 'href'}
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function generateDescriptionMeta({description}) {
|
|
42
|
+
return {
|
|
43
|
+
locator: 'name',
|
|
44
|
+
tags: [
|
|
45
|
+
{name: 'description', content: description || metaProps.description}
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function generateTwitterMeta({description, title, twitterImage}) {
|
|
51
|
+
return {
|
|
52
|
+
locator: 'name',
|
|
53
|
+
tags: [
|
|
54
|
+
{name: 'twitter:card', content: 'summary_large_image'},
|
|
55
|
+
{name: 'twitter:site', content: metaProps.twitterUsername},
|
|
56
|
+
{name: 'twitter:title', content: formatPageTitle(title)},
|
|
57
|
+
{name: 'twitter:description', content: description || metaProps.description},
|
|
58
|
+
{name: 'twitter:image:src', content: twitterImage || metaProps.twitterImage}
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function generateOpenGraphMeta({description, title, facebookImage}, canonicalUrl) {
|
|
64
|
+
let tags = [
|
|
65
|
+
{name: 'og:title', content: formatPageTitle(title)},
|
|
66
|
+
{name: 'og:url', content: canonicalUrl},
|
|
67
|
+
{name: 'og:site_name', content: formatPageTitle(metaProps.serviceTitle)},
|
|
68
|
+
{name: 'og:description', content: description || metaProps.description},
|
|
69
|
+
{name: 'og:type', content: 'website'}
|
|
70
|
+
]
|
|
71
|
+
if (facebookImage) {
|
|
72
|
+
tags.push({name: 'og:image', content: facebookImage})
|
|
73
|
+
} else {
|
|
74
|
+
tags.push({name: 'og:image', content: metaProps.facebookImage})
|
|
75
|
+
/*tags = tags.concat([
|
|
76
|
+
{name: 'og:image', content: metaProps.facebookImage},
|
|
77
|
+
{name: 'og:image:width', content: 1200},
|
|
78
|
+
{name: 'og:image:height', content: 630}])*/
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
locator: 'property',
|
|
82
|
+
tags
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function generateItemPropSchema({description, title, image}) {
|
|
87
|
+
return {
|
|
88
|
+
locator: 'itemprop',
|
|
89
|
+
tags: [
|
|
90
|
+
{name: 'name', content: title},
|
|
91
|
+
{name: 'description', content: description || metaProps.description},
|
|
92
|
+
{name: 'image', content: image || metaProps.facebookImage}
|
|
93
|
+
]
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function generateLdJsonSchema({title}) {
|
|
98
|
+
return {
|
|
99
|
+
tag: 'script',
|
|
100
|
+
locator: 'type',
|
|
101
|
+
tags: [
|
|
102
|
+
{
|
|
103
|
+
name: 'application/ld+json',
|
|
104
|
+
content: JSON.stringify({
|
|
105
|
+
'@context': 'http://schema.org',
|
|
106
|
+
'@type': 'WebSite',
|
|
107
|
+
'name': domain,
|
|
108
|
+
'alternateName': title,
|
|
109
|
+
'url': origin
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
]
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* @param {MetaTagReplacement} replacement
|
|
118
|
+
*/
|
|
119
|
+
function replaceMetaTags(replacement) {
|
|
120
|
+
const {tag, tags, locator} = replacement
|
|
121
|
+
const selector = tag || 'meta'
|
|
122
|
+
tags.forEach(tagSettings => {
|
|
123
|
+
let tag = document.querySelector(`${selector}[${locator}="${tagSettings.name}"]`)
|
|
124
|
+
if (!tag) {
|
|
125
|
+
tag = createTag(selector)
|
|
126
|
+
tag.setAttribute(locator, tagSettings.name)
|
|
127
|
+
}
|
|
128
|
+
if (selector === 'meta') {
|
|
129
|
+
tag.content = tagSettings.content
|
|
130
|
+
} else if (tagSettings.attribute) {
|
|
131
|
+
tag[tagSettings.attribute] = tagSettings.content
|
|
132
|
+
} else {
|
|
133
|
+
tag.innerText = tagSettings.content
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function createTag(tagName, props) {
|
|
139
|
+
let tag = document.createElement(tagName)
|
|
140
|
+
for (let key in props) {
|
|
141
|
+
tag[key] = props[key]
|
|
142
|
+
}
|
|
143
|
+
document.head.appendChild(tag)
|
|
144
|
+
return tag
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function removeTag(selector) {
|
|
148
|
+
let tag = document.querySelector(selector)
|
|
149
|
+
if (tag) {
|
|
150
|
+
tag.parentElement.removeChild(tag)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let pageMeta = {}
|
|
155
|
+
|
|
156
|
+
const tagReplacerPipeline = [generateCanonicalLink, generateDescriptionMeta, generateOpenGraphMeta, generateTwitterMeta, generateLdJsonSchema] // generateItemPropSchema
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Update page metadata tags
|
|
160
|
+
* @param {PageMeta} meta - Page metadata
|
|
161
|
+
*/
|
|
162
|
+
export function setPageMetadata(meta) {
|
|
163
|
+
if (isEqual(pageMeta, meta))
|
|
164
|
+
return
|
|
165
|
+
const canonicalUrl = origin + location.pathname// + location.search
|
|
166
|
+
document.title = formatPageTitle(meta.title)
|
|
167
|
+
for (const replacer of tagReplacerPipeline) {
|
|
168
|
+
replaceMetaTags(replacer(meta, canonicalUrl))
|
|
169
|
+
}
|
|
170
|
+
if (meta.customMeta) {
|
|
171
|
+
replaceMetaTags(meta.customMeta)
|
|
172
|
+
}
|
|
173
|
+
pageMeta = meta
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Reset page metadata tags to their default values
|
|
178
|
+
* @param {PageMeta} meta - Page metadata
|
|
179
|
+
*/
|
|
180
|
+
export function resetPageMetadata(meta) {
|
|
181
|
+
setPageMetadata({
|
|
182
|
+
title: metaProps.serviceTitle,
|
|
183
|
+
description: metaProps.description,
|
|
184
|
+
twitterImage: metaProps.twitterImage,
|
|
185
|
+
facebookImage: metaProps.facebookImage
|
|
186
|
+
})
|
|
187
|
+
//TODO: add logic to cleanup custom page meta tags on page unload
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/*export function setPageNoIndex(noIndex) {
|
|
191
|
+
if (!noIndex) {
|
|
192
|
+
removeTag('meta[name=robots]')
|
|
193
|
+
} else {
|
|
194
|
+
replaceMetaTags({
|
|
195
|
+
locator: 'name',
|
|
196
|
+
tags: [
|
|
197
|
+
{name: 'robots', content: 'noindex,nofollow'}
|
|
198
|
+
]
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
}*/
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* React hook for setting page metadata
|
|
205
|
+
* @param {PageMeta} meta - Page metadata
|
|
206
|
+
* @param {*[]} dependencies
|
|
207
|
+
*/
|
|
208
|
+
export function usePageMetadata(meta, dependencies = []) {
|
|
209
|
+
useEffect(() => {
|
|
210
|
+
setPageMetadata(meta)
|
|
211
|
+
return () => resetPageMetadata(meta)
|
|
212
|
+
}, [JSON.stringify(meta), ...dependencies])
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* @typedef {Object} PageMeta
|
|
217
|
+
* @property {String} title - Page title
|
|
218
|
+
* @property {String} description - Contents description
|
|
219
|
+
* @property {String} [twitterImage] - Twitter image url
|
|
220
|
+
* @property {String} [facebookImage] - Facebook image url
|
|
221
|
+
* @property {MetaTagReplacement} [customMeta] - Custom metadata tags
|
|
222
|
+
*/
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* @typedef {Object} MetaTagReplacement
|
|
226
|
+
* @property {String} [tag] - Tag name to search for ("meta" by default)
|
|
227
|
+
* @property {String} locator - Tag attribute to match
|
|
228
|
+
* @property {{name: String, content: String, [attribute]: String}[]} tags - Tag properties to set
|
|
229
|
+
*/
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import React, {useEffect, useState} from 'react'
|
|
2
|
+
import PropTypes from 'prop-types'
|
|
3
|
+
import {withErrorBoundary} from '../errors/error-boundary'
|
|
4
|
+
|
|
5
|
+
const loadedModules = new Map()
|
|
6
|
+
|
|
7
|
+
export const DynamicModule = withErrorBoundary(
|
|
8
|
+
/**
|
|
9
|
+
* Dynamically loadable module
|
|
10
|
+
* @param {Function} load - Dynamic load function, e.g. ()=>import('./dynamic_module_import')
|
|
11
|
+
* @param {String} module - Module unique name for aching purpose
|
|
12
|
+
* @param {*} [otherProps]
|
|
13
|
+
* @constructor
|
|
14
|
+
*/
|
|
15
|
+
function DynamicModule({load, module, ...otherProps}) {
|
|
16
|
+
const [error, setError] = useState(undefined)
|
|
17
|
+
const key = module || load
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (loadedModules.get(key))
|
|
21
|
+
return //skip if module is already loaded
|
|
22
|
+
load()
|
|
23
|
+
.then(dynamicModule => {
|
|
24
|
+
//use default export for ES modules or top-level export for CJS
|
|
25
|
+
if (dynamicModule.__esModule) {
|
|
26
|
+
dynamicModule = dynamicModule.default
|
|
27
|
+
}
|
|
28
|
+
//add to cache
|
|
29
|
+
loadedModules.set(key, dynamicModule)
|
|
30
|
+
setError(null)
|
|
31
|
+
})
|
|
32
|
+
.catch(error => {
|
|
33
|
+
error.message = 'Failed to load module. ' + error.message
|
|
34
|
+
setError(error)
|
|
35
|
+
})
|
|
36
|
+
}, [module || load])
|
|
37
|
+
if (error)
|
|
38
|
+
throw error
|
|
39
|
+
const resolvedModule = loadedModules.get(key)
|
|
40
|
+
if (!resolvedModule)
|
|
41
|
+
return <div className="loader"/>
|
|
42
|
+
return React.createElement(resolvedModule, otherProps)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
DynamicModule.propTypes = {
|
|
46
|
+
load: PropTypes.func.isRequired,
|
|
47
|
+
module: PropTypes.string
|
|
48
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stellar-expert/ui-framework",
|
|
3
|
+
"version": "1.9.0",
|
|
4
|
+
"description": "StellarExpert shared UI components library",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"module": "./index.js",
|
|
7
|
+
"sideEffects": [
|
|
8
|
+
"*.scss"
|
|
9
|
+
],
|
|
10
|
+
"keywords": [
|
|
11
|
+
"StellarExpert",
|
|
12
|
+
"UI"
|
|
13
|
+
],
|
|
14
|
+
"author": "orbitlens <orbit@stellar.expert>",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"peerDependencies": {
|
|
17
|
+
"@stellar/stellar-base": "^10.0.1",
|
|
18
|
+
"@stellar/stellar-sdk": "^11.1.0",
|
|
19
|
+
"@stellar-expert/asset-descriptor": "^1.3.0",
|
|
20
|
+
"@stellar-expert/claimable-balance-utils": "^1.4.1",
|
|
21
|
+
"@stellar-expert/client-cache": "github:stellar-expert/client-cache",
|
|
22
|
+
"@stellar-expert/formatter": "^2.3.0",
|
|
23
|
+
"@stellar-expert/navigation": "github:stellar-expert/navigation#v1.0.2",
|
|
24
|
+
"@stellar-expert/tx-meta-effects-parser": "5.0.0-beta10",
|
|
25
|
+
"classnames": ">=2",
|
|
26
|
+
"prop-types": ">=15",
|
|
27
|
+
"qrcode.react": "^3.1.0",
|
|
28
|
+
"react": ">=17",
|
|
29
|
+
"react-dom": ">=17",
|
|
30
|
+
"react-fast-compare": ">=3",
|
|
31
|
+
"throttle-debounce": ">=5.0.0"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"highlight.js": "^11.8.0",
|
|
35
|
+
"history": "^4.10.1",
|
|
36
|
+
"react-datetime-picker": "5.5.2",
|
|
37
|
+
"react-day-picker": "8.8.2",
|
|
38
|
+
"react-copy-to-clipboard": "^5.1.0",
|
|
39
|
+
"react-timeago": "^7.1.0"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {useEffect, useState} from 'react'
|
|
2
|
+
|
|
3
|
+
//TODO consider using a single instance of an IntersectionObserver to track all changes instead of creating many instances
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Hook for determining the visibility of the element inside the scroll context using the Intersection Observer API
|
|
7
|
+
* @param {MutableRefObject<Element>} root - Scroll parent ref
|
|
8
|
+
* @param {String} [rootMargin] - Visibility margin, in pixels or percents (default 0)
|
|
9
|
+
* @returns {Boolean} - Whether an element is visible or not
|
|
10
|
+
*/
|
|
11
|
+
export function useOnScreen(root, rootMargin) {
|
|
12
|
+
const [isVisible, setVisibility] = useState(false)
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (!root.current)
|
|
16
|
+
return
|
|
17
|
+
const observer = new IntersectionObserver(([entry]) => setVisibility(entry.isIntersecting), {rootMargin})
|
|
18
|
+
observer.observe(root.current)
|
|
19
|
+
return () => observer.disconnect()
|
|
20
|
+
}, [root.current])
|
|
21
|
+
|
|
22
|
+
return isVisible
|
|
23
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const isDocumentVisibilitySupported = 'hidden' in document
|
|
2
|
+
|
|
3
|
+
export function isDocumentVisible() {
|
|
4
|
+
if (!isDocumentVisibilitySupported) return true
|
|
5
|
+
return !document.hidden
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function addVisibilityChangeListener(listener) {
|
|
9
|
+
function wrappedListener() {
|
|
10
|
+
listener(isDocumentVisible())
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
document.addEventListener('visibilitychange', wrappedListener)
|
|
14
|
+
return function () {
|
|
15
|
+
document.removeEventListener('visibilitychange', wrappedListener)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import {useState, useEffect} from 'react'
|
|
2
|
+
import {isDocumentVisible, addVisibilityChangeListener, isDocumentVisibilitySupported} from './page-visibility-helpers'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @returns {Boolean} - True if the page is currently visible or false otherwise
|
|
6
|
+
*/
|
|
7
|
+
export function usePageVisibility() {
|
|
8
|
+
const [visible, setVisible] = useState(isDocumentVisible)
|
|
9
|
+
if (!isDocumentVisibilitySupported || !document.addEventListener) return true
|
|
10
|
+
useEffect(()=>addVisibilityChangeListener(() => setVisible(isDocumentVisible())))
|
|
11
|
+
return visible
|
|
12
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import {useState, useEffect} from 'react'
|
|
2
|
+
|
|
3
|
+
export function useScreenOrientation() {
|
|
4
|
+
const [orientation, setOrientation] = useState(window.screen.orientation.type)
|
|
5
|
+
|
|
6
|
+
function onChange() {
|
|
7
|
+
setOrientation(window.screen.orientation.type)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
window.addEventListener('orientationchange', onChange)
|
|
12
|
+
return () => window.removeEventListener('orientationchange', onChange)
|
|
13
|
+
}, [])
|
|
14
|
+
|
|
15
|
+
return orientation
|
|
16
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import {useState, useEffect, useRef} from 'react'
|
|
2
|
+
import isEqual from 'react-fast-compare'
|
|
3
|
+
|
|
4
|
+
function ensureDependencies(dependencies) {
|
|
5
|
+
if (!(dependencies instanceof Array))
|
|
6
|
+
throw new Error('Parameter dependencies is required for useDependantState hook')
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* React hook that automatically re-inits state when one of the dependencies changed
|
|
11
|
+
* @param {Function|any} stateInitializer
|
|
12
|
+
* @param {Array} dependencies
|
|
13
|
+
* @param {Function} [finalizer]
|
|
14
|
+
* @returns {[Object, Function]}
|
|
15
|
+
*/
|
|
16
|
+
export function useDependantState(stateInitializer, dependencies, finalizer) {
|
|
17
|
+
//ensureDependencies(dependencies)
|
|
18
|
+
const [state, updateState] = useState(function () {
|
|
19
|
+
return typeof stateInitializer === 'function' ? stateInitializer(dependencies) : stateInitializer
|
|
20
|
+
})
|
|
21
|
+
//pin dependencies object to invoke effect update only if dependencies changed
|
|
22
|
+
const pinnedDeps = useRef(dependencies)
|
|
23
|
+
let dependenciesChanged = !isEqual(dependencies, pinnedDeps.current)
|
|
24
|
+
//check that dependencies really changed
|
|
25
|
+
if (dependenciesChanged) {
|
|
26
|
+
pinnedDeps.current = dependencies
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
//effect invokes the initializer each time dependencies changed
|
|
30
|
+
useEffect(function () {
|
|
31
|
+
//check that dependencies really changed
|
|
32
|
+
if (dependenciesChanged) {
|
|
33
|
+
//re-initialize state when any of the dependencies changed
|
|
34
|
+
updateState(typeof stateInitializer === 'function' ? stateInitializer(dependencies, state) : stateInitializer)
|
|
35
|
+
}
|
|
36
|
+
return finalizer || undefined
|
|
37
|
+
}, pinnedDeps.current)
|
|
38
|
+
|
|
39
|
+
return [state, function (newState) {
|
|
40
|
+
//use deep compare - as React used to in good old times
|
|
41
|
+
updateState(current => {
|
|
42
|
+
if (typeof newState === 'function') {
|
|
43
|
+
newState = newState(current)
|
|
44
|
+
}
|
|
45
|
+
if (isEqual(current, newState)) return current
|
|
46
|
+
return newState
|
|
47
|
+
})
|
|
48
|
+
}]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Simple force update hook.
|
|
53
|
+
* @return {Function}
|
|
54
|
+
*/
|
|
55
|
+
export function useForceUpdate() {
|
|
56
|
+
const [, updateNonce] = useState(0)
|
|
57
|
+
return function () {
|
|
58
|
+
updateNonce(nonce => nonce + 1)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* React hook that automatically reacts on dependency changes using deep comparison
|
|
64
|
+
* @param {Function} effect
|
|
65
|
+
* @param {Array} dependencies
|
|
66
|
+
*/
|
|
67
|
+
export function useDeepEffect(effect, dependencies) {
|
|
68
|
+
ensureDependencies(dependencies)
|
|
69
|
+
//pin dependencies object to invoke effect update only if dependencies changed
|
|
70
|
+
const pinnedDeps = useRef([])
|
|
71
|
+
//check that dependencies really changed
|
|
72
|
+
if (!isEqual(dependencies, pinnedDeps.current)) {
|
|
73
|
+
pinnedDeps.current = dependencies
|
|
74
|
+
}
|
|
75
|
+
//effect invokes the initializer each time dependencies changed
|
|
76
|
+
useEffect(effect, pinnedDeps.current)
|
|
77
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {useState, useEffect} from 'react'
|
|
2
|
+
|
|
3
|
+
const listeners = []
|
|
4
|
+
let currentNetwork = 'public'
|
|
5
|
+
|
|
6
|
+
setStellarNetwork(currentNetwork)
|
|
7
|
+
|
|
8
|
+
function removeListener(callback) {
|
|
9
|
+
const idx = listeners.indexOf(callback)
|
|
10
|
+
if (~idx) listeners.splice(idx, 1)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function getCurrentStellarNetwork() {
|
|
14
|
+
return currentNetwork
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Set current Stellar network.
|
|
19
|
+
* @param {'testnet'|'public'} network
|
|
20
|
+
*/
|
|
21
|
+
export function setStellarNetwork(network) {
|
|
22
|
+
if (currentNetwork === network) return
|
|
23
|
+
currentNetwork = network
|
|
24
|
+
for (const listener of listeners) {
|
|
25
|
+
listener(network)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function subscribeToStellarNetworkChange(onChange) {
|
|
30
|
+
removeListener(onChange)
|
|
31
|
+
listeners.push(onChange)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* React hook for reacting on Stellar network changes.
|
|
36
|
+
* @return {'testnet'|'public'}
|
|
37
|
+
*/
|
|
38
|
+
export function useStellarNetwork() {
|
|
39
|
+
const [state, updateState] = useState(currentNetwork)
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
subscribeToStellarNetworkChange(updateState)
|
|
42
|
+
return () => removeListener(updateState)
|
|
43
|
+
}, [])
|
|
44
|
+
return state
|
|
45
|
+
}
|
package/state/theme.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {useState} from 'react'
|
|
2
|
+
|
|
3
|
+
function setGlobalTheme(theme) {
|
|
4
|
+
localStorage.setItem('preferred-color-theme', theme)
|
|
5
|
+
document.documentElement.attributes['data-theme'].value = theme
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const preferredTheme = localStorage.getItem('preferred-color-theme')
|
|
9
|
+
if (preferredTheme) {
|
|
10
|
+
setGlobalTheme(preferredTheme)
|
|
11
|
+
} else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
12
|
+
setGlobalTheme('night')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @returns {[colorTheme: String, setTheme: Function]}
|
|
17
|
+
*/
|
|
18
|
+
export function useTheme() {
|
|
19
|
+
const [theme, setTheme] = useState(localStorage.getItem('preferred-color-theme') || 'day')
|
|
20
|
+
return [theme, function (newTheme) {
|
|
21
|
+
setTheme(current => {
|
|
22
|
+
if (typeof newTheme === 'function') {
|
|
23
|
+
newTheme = newTheme(current)
|
|
24
|
+
}
|
|
25
|
+
setGlobalTheme(newTheme)
|
|
26
|
+
return newTheme
|
|
27
|
+
})
|
|
28
|
+
}]
|
|
29
|
+
}
|