@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.
Files changed (124) hide show
  1. package/.npmrc +1 -0
  2. package/LICENSE +21 -0
  3. package/README.md +3 -0
  4. package/account/account-address.js +127 -0
  5. package/account/account-address.scss +20 -0
  6. package/account/available-balance.js +21 -0
  7. package/account/identicon.js +91 -0
  8. package/account/identicon.scss +6 -0
  9. package/account/signer-key.js +65 -0
  10. package/api/api-cache.js +5 -0
  11. package/api/explorer-api-call.js +37 -0
  12. package/api/explorer-api-hooks.js +202 -0
  13. package/api/explorer-api-paginated-list-hooks.js +439 -0
  14. package/api/explorer-batch-info-loader.js +85 -0
  15. package/api/explorer-tx-api.js +29 -0
  16. package/asset/amount.js +53 -0
  17. package/asset/asset-icon.js +34 -0
  18. package/asset/asset-issuer.js +22 -0
  19. package/asset/asset-link.js +93 -0
  20. package/asset/asset-link.scss +26 -0
  21. package/asset/asset-list-hooks.js +60 -0
  22. package/asset/asset-meta-hooks.js +78 -0
  23. package/asset/asset-selector.js +72 -0
  24. package/asset/asset-selector.scss +41 -0
  25. package/basic-styles/base.scss +53 -0
  26. package/basic-styles/form.scss +90 -0
  27. package/basic-styles/grid.scss +388 -0
  28. package/basic-styles/link.scss +17 -0
  29. package/basic-styles/lists.scss +68 -0
  30. package/basic-styles/loader.scss +22 -0
  31. package/basic-styles/responsive.scss +29 -0
  32. package/basic-styles/scrollbars.scss +19 -0
  33. package/basic-styles/spacing.scss +50 -0
  34. package/basic-styles/table.scss +104 -0
  35. package/basic-styles/themes.scss +59 -0
  36. package/basic-styles/typography.scss +191 -0
  37. package/basic-styles/variables.scss +26 -0
  38. package/claimable-balance/claimable-balance-claimants.js +12 -0
  39. package/contract/sc-val.js +70 -0
  40. package/contract/wasm-section-parser.js +159 -0
  41. package/contract/wasm-section-reader.js +79 -0
  42. package/contract/xdr-reader.js +148 -0
  43. package/controls/active-icon.scss +23 -0
  44. package/controls/button-group.js +20 -0
  45. package/controls/button-group.scss +48 -0
  46. package/controls/button.js +74 -0
  47. package/controls/button.scss +148 -0
  48. package/controls/code-block.js +35 -0
  49. package/controls/code-block.scss +74 -0
  50. package/controls/dropdown.js +281 -0
  51. package/controls/dropdown.scss +161 -0
  52. package/controls/external-link.js +5 -0
  53. package/controls/info-tooltip.js +17 -0
  54. package/controls/info-tooltip.scss +4 -0
  55. package/controls/slider.js +20 -0
  56. package/controls/slider.scss +22 -0
  57. package/controls/tabs.js +94 -0
  58. package/controls/tabs.scss +73 -0
  59. package/controls/tooltip.js +214 -0
  60. package/controls/tooltip.scss +97 -0
  61. package/controls/update-highlighter.js +28 -0
  62. package/controls/update-highlighter.scss +10 -0
  63. package/date/date-selector.js +44 -0
  64. package/date/elapsed-time.js +22 -0
  65. package/date/utc-timestamp.js +23 -0
  66. package/dex/price-dynamic.js +45 -0
  67. package/dex/price-dynamic.scss +36 -0
  68. package/directory/directory-hooks.js +89 -0
  69. package/effect/effect-description.js +289 -0
  70. package/effect/op-effects-view.js +11 -0
  71. package/effect/op-effects.scss +8 -0
  72. package/errors/error-boundary.js +98 -0
  73. package/fonts/font.scss +23 -0
  74. package/fonts/icons/config.json +1858 -0
  75. package/fonts/icons/icons-embedded.scss +644 -0
  76. package/horizon/horizon-account-helpers.js +104 -0
  77. package/horizon/horizon-client-helpers.js +52 -0
  78. package/horizon/horizon-ledger-helpers.js +36 -0
  79. package/horizon/horizon-orderbook-helpers.js +14 -0
  80. package/horizon/horizon-trades-helper.js +89 -0
  81. package/horizon/horizon-transaction-helpers.js +36 -0
  82. package/index.js +85 -0
  83. package/index.scss +16 -0
  84. package/interaction/autofocus.js +9 -0
  85. package/interaction/block-select.js +54 -0
  86. package/interaction/block-select.scss +24 -0
  87. package/interaction/copy-to-clipboard.js +19 -0
  88. package/interaction/inline-progress.js +16 -0
  89. package/interaction/qr-code.js +45 -0
  90. package/interaction/responsive.js +21 -0
  91. package/interaction/spoiler.js +40 -0
  92. package/interaction/spoiler.scss +9 -0
  93. package/interaction/theme-selector.js +11 -0
  94. package/ledger/ledger-entry-href-formatter.js +22 -0
  95. package/ledger/ledger-entry-link.js +59 -0
  96. package/meta/page-meta-tags.js +229 -0
  97. package/module/dynamic-module.js +48 -0
  98. package/package.json +41 -0
  99. package/state/on-screen-hooks.js +23 -0
  100. package/state/page-visibility-helpers.js +17 -0
  101. package/state/page-visibility-hooks.js +12 -0
  102. package/state/screen-orientation-hooks.js +16 -0
  103. package/state/state-hooks.js +77 -0
  104. package/state/stellar-network-hooks.js +45 -0
  105. package/state/theme.js +29 -0
  106. package/stellar/key-type.js +92 -0
  107. package/stellar/ledger-generic-id.js +40 -0
  108. package/stellar/signature-hint-utils.js +65 -0
  109. package/toast/toast-notification-instance.js +43 -0
  110. package/toast/toast-notification.js +46 -0
  111. package/toast/toast-notifications-block.js +55 -0
  112. package/toast/toast-notifications.scss +166 -0
  113. package/tx/op-accounting-changes.js +27 -0
  114. package/tx/op-description-view.js +925 -0
  115. package/tx/op-description.scss +98 -0
  116. package/tx/op-icon.js +93 -0
  117. package/tx/parser/op-balance-changes.js +62 -0
  118. package/tx/parser/op-descriptor.js +49 -0
  119. package/tx/parser/tx-details-parser.js +75 -0
  120. package/tx/parser/tx-matcher.js +372 -0
  121. package/tx/parser/type-filter-matcher.js +127 -0
  122. package/tx/tx-fee-effect.js +14 -0
  123. package/tx/tx-list-hooks.js +19 -0
  124. package/tx/tx-operations-list.js +105 -0
@@ -0,0 +1,9 @@
1
+ @use "../basic-styles/variables";
2
+
3
+ .spoiler {
4
+ padding: 0.3em 0;
5
+
6
+ .spoiler-text {
7
+ border-bottom: 1px dotted;
8
+ }
9
+ }
@@ -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
+ }