@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,281 @@
|
|
|
1
|
+
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'
|
|
2
|
+
import {createPortal} from 'react-dom'
|
|
3
|
+
import PropTypes from 'prop-types'
|
|
4
|
+
import cn from 'classnames'
|
|
5
|
+
import {throttle} from 'throttle-debounce'
|
|
6
|
+
import {useDependantState} from '../state/state-hooks'
|
|
7
|
+
import './dropdown.scss'
|
|
8
|
+
|
|
9
|
+
export const Dropdown = React.memo(function Dropdown({
|
|
10
|
+
options,
|
|
11
|
+
title,
|
|
12
|
+
value,
|
|
13
|
+
disabled,
|
|
14
|
+
className,
|
|
15
|
+
onChange,
|
|
16
|
+
hint,
|
|
17
|
+
showToggle,
|
|
18
|
+
solo,
|
|
19
|
+
hideSelected,
|
|
20
|
+
header,
|
|
21
|
+
footer,
|
|
22
|
+
expanded,
|
|
23
|
+
onScroll,
|
|
24
|
+
onOpen,
|
|
25
|
+
onClose,
|
|
26
|
+
maxHeight
|
|
27
|
+
}) {
|
|
28
|
+
const headerRef = useRef()
|
|
29
|
+
const listRef = useRef()
|
|
30
|
+
const [listOpen, updateListOpen] = useState(false)
|
|
31
|
+
const [alignRigth, setAlignRight] = useState(false)
|
|
32
|
+
//collapse dropdown handler
|
|
33
|
+
const collapseDropdown = useCallback(function () {
|
|
34
|
+
updateListOpen(open => {
|
|
35
|
+
if (!open)
|
|
36
|
+
return false
|
|
37
|
+
onClose?.call(this, this)
|
|
38
|
+
return false
|
|
39
|
+
})
|
|
40
|
+
}, [onClose])
|
|
41
|
+
|
|
42
|
+
//collapse dropdwon list on click
|
|
43
|
+
const [selectedValue, updateSelectedValue] = useDependantState(() => {
|
|
44
|
+
if (listOpen) {
|
|
45
|
+
setTimeout(() => {
|
|
46
|
+
document.addEventListener('click', collapseDropdown)
|
|
47
|
+
}, 200)
|
|
48
|
+
}
|
|
49
|
+
return value
|
|
50
|
+
}, [value, listOpen], () => {
|
|
51
|
+
document.removeEventListener('click', collapseDropdown)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
//close/open dropdown on header cick
|
|
55
|
+
const toggleList = useCallback(function toggleList(e) {
|
|
56
|
+
e && e.nativeEvent.stopImmediatePropagation()
|
|
57
|
+
updateListOpen(prevState => {
|
|
58
|
+
if (disabled)
|
|
59
|
+
return false;
|
|
60
|
+
(prevState ? onClose : onOpen)?.call(this, this)
|
|
61
|
+
return !prevState
|
|
62
|
+
})
|
|
63
|
+
}, [disabled, onClose, onOpen])
|
|
64
|
+
|
|
65
|
+
//handle item select action
|
|
66
|
+
const select = useCallback(function (option) {
|
|
67
|
+
collapseDropdown()
|
|
68
|
+
if (disabled)
|
|
69
|
+
return
|
|
70
|
+
onChange && onChange(option.value || option)
|
|
71
|
+
updateSelectedValue(option)
|
|
72
|
+
}, [collapseDropdown, onChange])
|
|
73
|
+
|
|
74
|
+
//handle user scroll action for infinit scroll support
|
|
75
|
+
const scrollList = useMemo(() => throttle(200, e => {
|
|
76
|
+
if (!onScroll)
|
|
77
|
+
return
|
|
78
|
+
const {target} = e
|
|
79
|
+
const pos = {position: target.scrollTop, rel: 'middle'}
|
|
80
|
+
if (target.scrollTop === 0)
|
|
81
|
+
return onScroll({...pos, rel: 'top'})
|
|
82
|
+
if (Math.ceil(target.scrollHeight - target.scrollTop - 8) < target.clientHeight)
|
|
83
|
+
return onScroll({...pos, rel: 'bottom'})
|
|
84
|
+
onScroll(pos)
|
|
85
|
+
}), [onScroll])
|
|
86
|
+
|
|
87
|
+
//locate currently selectd option
|
|
88
|
+
const {option: selectedItem, isDefault} = getSelectedOption([value, selectedValue], options)
|
|
89
|
+
|
|
90
|
+
//check dropdown container alignement
|
|
91
|
+
if (listOpen) {
|
|
92
|
+
setTimeout(() => setAlignRight(isAlignedRight(listRef.current)), 200)
|
|
93
|
+
}
|
|
94
|
+
//show a dropdown after the initial render
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
if (expanded === true) {
|
|
97
|
+
setTimeout(() => updateListOpen(true), 200)
|
|
98
|
+
}
|
|
99
|
+
}, [])
|
|
100
|
+
|
|
101
|
+
const ddTitle = title || selectedItem?.title || selectedItem?.value || selectedItem
|
|
102
|
+
|
|
103
|
+
return <div className={cn('dd-wrapper', {disabled}, className)} title={hint}>
|
|
104
|
+
<a href="#" className="dd-header" onClick={toggleList} ref={headerRef}>
|
|
105
|
+
{ddTitle}{!!showToggle && <span className={cn('dd-toggle', {visible: listOpen})}/>}
|
|
106
|
+
</a>
|
|
107
|
+
{!!listOpen && createPortal(<div className={className}>
|
|
108
|
+
<div className={cn('dd-backdrop', {solo})}/>
|
|
109
|
+
<div className={cn('dd-list', {solo, visible: listOpen && !disabled, 'align-right': alignRigth})}
|
|
110
|
+
style={getListPosition(headerRef.current, solo)} ref={listRef}>
|
|
111
|
+
{!!header && <>
|
|
112
|
+
<div className="dd-list-header" onClick={preventClosing}>{header}</div>
|
|
113
|
+
<hr/>
|
|
114
|
+
</>}
|
|
115
|
+
<ul onScroll={scrollList} style={{maxHeight: `min(70vh, ${maxHeight})`}}>
|
|
116
|
+
{options.filter(opt => !opt.hidden).map((option, i) => {
|
|
117
|
+
if (option === '-') return <li className="dd-list-item" key={i + '-'}>
|
|
118
|
+
<hr className="flare"/>
|
|
119
|
+
</li>
|
|
120
|
+
const key = option.value || option.href || option
|
|
121
|
+
const isSelected = !isDefault && option === selectedItem
|
|
122
|
+
const style = isSelected && hideSelected ? {display: 'none'} : {}
|
|
123
|
+
return <DropdownOption {...{key, option, select, isSelected, style}} />
|
|
124
|
+
})}
|
|
125
|
+
</ul>
|
|
126
|
+
{!!footer && <>
|
|
127
|
+
<hr/>
|
|
128
|
+
<div className="dd-list-footer" onClick={preventClosing}>{footer}</div>
|
|
129
|
+
</>}
|
|
130
|
+
</div>
|
|
131
|
+
</div>, document.body)}
|
|
132
|
+
</div>
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
const DropdownOption = React.memo(function DropdownOption({option, isSelected, select, style}) {
|
|
136
|
+
let {value, title, href, className} = option
|
|
137
|
+
if (typeof option === 'string') {
|
|
138
|
+
value = title = option
|
|
139
|
+
}
|
|
140
|
+
if (!title) {
|
|
141
|
+
title = value
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const selectOption = useCallback(function (e) {
|
|
145
|
+
if (option.disabled) {
|
|
146
|
+
e.stopPropagation()
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
if (!option.href) {
|
|
150
|
+
e.preventDefault()
|
|
151
|
+
}
|
|
152
|
+
select(option)
|
|
153
|
+
}, [select, option])
|
|
154
|
+
|
|
155
|
+
return <li className="dd-list-item" key={value || href} onClick={selectOption} style={style}>
|
|
156
|
+
<a href={href || '#'} className={cn({className, selected: isSelected})}>{title}</a>
|
|
157
|
+
</li>
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
function getSelectedOption(values, options) {
|
|
161
|
+
for (let v of values) {
|
|
162
|
+
if (v !== null && v !== undefined) {
|
|
163
|
+
const option = options.find(item => item === v || item.value === v)
|
|
164
|
+
if (option) return {option, isDefault: false}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return {option: options.find(opt => typeof opt === 'string' || !opt.disabled), isDefault: true}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function getListPosition(header, solo) {
|
|
171
|
+
if (solo || !header)
|
|
172
|
+
return undefined
|
|
173
|
+
const rect = header.getBoundingClientRect()
|
|
174
|
+
return {
|
|
175
|
+
top: rect.bottom + window.scrollY,
|
|
176
|
+
left: rect.left + window.scrollX
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function isAlignedRight(list) {
|
|
181
|
+
if (!list)
|
|
182
|
+
return
|
|
183
|
+
const rect = list.getBoundingClientRect()
|
|
184
|
+
return window.innerWidth - rect.right < 0 && rect.left - rect.width >= 0
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function preventClosing(e) {
|
|
188
|
+
e.stopPropagation()
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
Dropdown.defaultProps = {
|
|
192
|
+
showToggle: true,
|
|
193
|
+
disabled: false,
|
|
194
|
+
hideSelected: false,
|
|
195
|
+
solo: false,
|
|
196
|
+
maxHeight: '35em'
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
Dropdown.propTypes = {
|
|
200
|
+
/**
|
|
201
|
+
* Available options
|
|
202
|
+
*/
|
|
203
|
+
options: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.shape({
|
|
204
|
+
/**
|
|
205
|
+
* Internal value used for the item identification
|
|
206
|
+
*/
|
|
207
|
+
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
208
|
+
/**
|
|
209
|
+
* Link href - for dropdown menus
|
|
210
|
+
*/
|
|
211
|
+
href: PropTypes.string,
|
|
212
|
+
/**
|
|
213
|
+
* Optional friendly item title
|
|
214
|
+
*/
|
|
215
|
+
title: PropTypes.any
|
|
216
|
+
}), PropTypes.string])).isRequired,
|
|
217
|
+
/**
|
|
218
|
+
* Selected value
|
|
219
|
+
*/
|
|
220
|
+
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
221
|
+
/**
|
|
222
|
+
* Title to display instead of the selected value if needed
|
|
223
|
+
*/
|
|
224
|
+
title: PropTypes.any,
|
|
225
|
+
/**
|
|
226
|
+
* OnChange handler
|
|
227
|
+
*/
|
|
228
|
+
onChange: PropTypes.func,
|
|
229
|
+
/**
|
|
230
|
+
* Whether the dropdown interaction is disabled or not
|
|
231
|
+
*/
|
|
232
|
+
disabled: PropTypes.bool,
|
|
233
|
+
/**
|
|
234
|
+
* HTML hover title attribute for the control
|
|
235
|
+
*/
|
|
236
|
+
hint: PropTypes.string,
|
|
237
|
+
/**
|
|
238
|
+
* Additional HTML classes
|
|
239
|
+
*/
|
|
240
|
+
className: PropTypes.string,
|
|
241
|
+
/**
|
|
242
|
+
* Whether to show toggle icon
|
|
243
|
+
*/
|
|
244
|
+
showToggle: PropTypes.bool,
|
|
245
|
+
/**
|
|
246
|
+
* Show a dropdown list in centered dialog
|
|
247
|
+
*/
|
|
248
|
+
solo: PropTypes.bool,
|
|
249
|
+
/**
|
|
250
|
+
* Do not show selected item in the dropdown list
|
|
251
|
+
*/
|
|
252
|
+
hideSelected: PropTypes.bool,
|
|
253
|
+
/**
|
|
254
|
+
* Optional dropdown list header
|
|
255
|
+
*/
|
|
256
|
+
header: PropTypes.any,
|
|
257
|
+
/**
|
|
258
|
+
* Optional dropdown list footer
|
|
259
|
+
*/
|
|
260
|
+
footer: PropTypes.any,
|
|
261
|
+
/**
|
|
262
|
+
* Initially collapsed or open
|
|
263
|
+
*/
|
|
264
|
+
expanded: PropTypes.bool,
|
|
265
|
+
/**
|
|
266
|
+
* List scroll handler - fires only if the options list has overflow
|
|
267
|
+
*/
|
|
268
|
+
onScroll: PropTypes.func,
|
|
269
|
+
/**
|
|
270
|
+
* List open handler
|
|
271
|
+
*/
|
|
272
|
+
onOpen: PropTypes.func,
|
|
273
|
+
/**
|
|
274
|
+
* Lsi close handler
|
|
275
|
+
*/
|
|
276
|
+
onClose: PropTypes.func,
|
|
277
|
+
/**
|
|
278
|
+
* Maximum dropdown list height
|
|
279
|
+
*/
|
|
280
|
+
maxHeight: PropTypes.string
|
|
281
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
@use "../basic-styles/variables";
|
|
2
|
+
|
|
3
|
+
.dd-wrapper {
|
|
4
|
+
display: inline-block;
|
|
5
|
+
position: relative;
|
|
6
|
+
padding-right: 0.3em;
|
|
7
|
+
|
|
8
|
+
&.reset {
|
|
9
|
+
//reset styles
|
|
10
|
+
font-size: $font-size-base;
|
|
11
|
+
font-weight: $font-weight-base;
|
|
12
|
+
line-height: $font-line-height-base;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.dd-toggle {
|
|
16
|
+
$toggle-size: 0.4em;
|
|
17
|
+
display: inline-block;
|
|
18
|
+
width: $toggle-size;
|
|
19
|
+
height: $toggle-size;
|
|
20
|
+
border: 1px solid currentColor;
|
|
21
|
+
border-bottom-color: transparent;
|
|
22
|
+
border-left-color: transparent;
|
|
23
|
+
transform: rotateZ(135deg) translate($toggle-size*0.4, -$toggle-size*0.4);
|
|
24
|
+
transform-origin: top right;
|
|
25
|
+
transition: transform 0.2s;
|
|
26
|
+
|
|
27
|
+
&.visible {
|
|
28
|
+
transform: rotateZ(135deg) scale(-1) translate($toggle-size*0.2, -$toggle-size*0.2);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.dd-header {
|
|
33
|
+
white-space: nowrap;
|
|
34
|
+
cursor: pointer;
|
|
35
|
+
display: inline-block;
|
|
36
|
+
|
|
37
|
+
&.button {
|
|
38
|
+
color: var(--color-bg);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.button .dd-toggle {
|
|
43
|
+
margin-left: 0.2em;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
&:hover {
|
|
48
|
+
a {
|
|
49
|
+
color: var(--color-highlight);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
&.disabled {
|
|
54
|
+
.dd-toggle {
|
|
55
|
+
border-top-color: var(--color-contrast-border);
|
|
56
|
+
border-right-color: var(--color-contrast-border);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.dd-list {
|
|
62
|
+
font-size: $font-size-base;
|
|
63
|
+
font-weight: $font-weight-base;
|
|
64
|
+
line-height: $font-line-height-base;
|
|
65
|
+
letter-spacing: normal;
|
|
66
|
+
position: absolute;
|
|
67
|
+
z-index: 1000;
|
|
68
|
+
visibility: hidden;
|
|
69
|
+
text-align: left;
|
|
70
|
+
top: calc(100% - 0.1em);
|
|
71
|
+
background: var(--color-bg);
|
|
72
|
+
border: 1px solid var(--color-contrast-border);
|
|
73
|
+
border-radius: $border-radius-input;
|
|
74
|
+
box-shadow: 2px 3px 6px -1px rgba(0, 0, 0, .3);
|
|
75
|
+
text-transform: none;
|
|
76
|
+
user-select: none;
|
|
77
|
+
margin-left: -0.8em;
|
|
78
|
+
|
|
79
|
+
&.align-right {
|
|
80
|
+
right: -0.2em
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
> ul {
|
|
84
|
+
display: block;
|
|
85
|
+
overflow-y: auto;
|
|
86
|
+
color: var(--color-primary);
|
|
87
|
+
margin: 0;
|
|
88
|
+
height: 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.dd-list-header, .dd-list-footer {
|
|
92
|
+
padding: .7em;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.dd-list-header + hr {
|
|
96
|
+
margin-top: 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
&.visible {
|
|
100
|
+
visibility: visible;
|
|
101
|
+
|
|
102
|
+
> ul {
|
|
103
|
+
height: auto;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.dd-list-item {
|
|
108
|
+
cursor: pointer;
|
|
109
|
+
padding: 0;
|
|
110
|
+
margin: 0;
|
|
111
|
+
|
|
112
|
+
> a {
|
|
113
|
+
padding: .4em .7em;
|
|
114
|
+
display: block;
|
|
115
|
+
color: inherit;
|
|
116
|
+
white-space: nowrap;
|
|
117
|
+
|
|
118
|
+
&:hover {
|
|
119
|
+
background: var(--color-alt-bg)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
&.selected {
|
|
123
|
+
cursor: default;
|
|
124
|
+
color: var(--color-text);
|
|
125
|
+
background: var(--color-alt-bg);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
hr {
|
|
131
|
+
margin: 0.5em 0;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
&.solo {
|
|
135
|
+
position: fixed;
|
|
136
|
+
left: 50%;
|
|
137
|
+
top: 50%;
|
|
138
|
+
transform: translate(-50%, -50%);
|
|
139
|
+
margin: 0;
|
|
140
|
+
max-height: 90vh !important;
|
|
141
|
+
max-width: 40rem;
|
|
142
|
+
z-index: 1002;
|
|
143
|
+
|
|
144
|
+
@media (max-width: $responsive-mobile-browser-width) {
|
|
145
|
+
min-width: 50vw;
|
|
146
|
+
max-width: 90vw;
|
|
147
|
+
width: 90vw;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.dd-backdrop.solo {
|
|
153
|
+
background: var(--color-backdrop);
|
|
154
|
+
display: block;
|
|
155
|
+
position: fixed;
|
|
156
|
+
z-index: 1001;
|
|
157
|
+
left: 0;
|
|
158
|
+
right: 0;
|
|
159
|
+
bottom: 0;
|
|
160
|
+
top: 0;
|
|
161
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import PropTypes from 'prop-types'
|
|
3
|
+
import cn from 'classnames'
|
|
4
|
+
import {Tooltip} from './tooltip'
|
|
5
|
+
import './info-tooltip.scss'
|
|
6
|
+
|
|
7
|
+
export const InfoTooltip = React.memo(function InfoTooltip({children, link, icon = 'icon-help'}) {
|
|
8
|
+
return <Tooltip trigger={<i className={cn('trigger icon info-tooltip small', icon)}/>}>
|
|
9
|
+
{children}
|
|
10
|
+
{!!link && <a href={link} className="info-tooltip-link" target="_blank">Read more…</a>}
|
|
11
|
+
</Tooltip>
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
InfoTooltip.propTypes = {
|
|
15
|
+
children: PropTypes.any.isRequired,
|
|
16
|
+
link: PropTypes.string
|
|
17
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React, {useCallback, useMemo, useState} from 'react'
|
|
2
|
+
import {throttle} from 'throttle-debounce'
|
|
3
|
+
import './slider.scss'
|
|
4
|
+
|
|
5
|
+
export function Slider({value, categroies, onChange, min = 0, max = 100, step = 1, ...otherProps}) {
|
|
6
|
+
const [inputValue, setInputValue] = useState(value || min)
|
|
7
|
+
const change = useMemo(() => throttle(300, onChange), [onChange])
|
|
8
|
+
const onSlide = useCallback(function (e) {
|
|
9
|
+
const value = parseFloat(e.target.value)
|
|
10
|
+
setInputValue(value)
|
|
11
|
+
change(value)
|
|
12
|
+
}, [change])
|
|
13
|
+
|
|
14
|
+
return <div className="slider dimmed text-small" {...otherProps}>
|
|
15
|
+
{categroies && <datalist className="categories dimmed condensed">
|
|
16
|
+
{categroies.map((category, index) => <option key={index + category} value={category} label={category}/>)}
|
|
17
|
+
</datalist>}
|
|
18
|
+
<input type="range" min={min} max={max} step={step} value={inputValue} onChange={onSlide}/>
|
|
19
|
+
</div>
|
|
20
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
.slider {
|
|
2
|
+
.categories {
|
|
3
|
+
display: flex;
|
|
4
|
+
justify-content: space-between;
|
|
5
|
+
width: 100%;
|
|
6
|
+
font-size: 0.85*$font-size-base;
|
|
7
|
+
line-height: 1;
|
|
8
|
+
|
|
9
|
+
option {
|
|
10
|
+
padding: 0;
|
|
11
|
+
text-align: center;
|
|
12
|
+
|
|
13
|
+
&:first-child {
|
|
14
|
+
text-align: left;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
&:last-child {
|
|
18
|
+
text-align: right;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
package/controls/tabs.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import React, {useEffect} from 'react'
|
|
2
|
+
import cn from 'classnames'
|
|
3
|
+
import {navigation} from '@stellar-expert/navigation'
|
|
4
|
+
import {useDependantState} from '../state/state-hooks'
|
|
5
|
+
import './tabs.scss'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Tabs control
|
|
9
|
+
* @param {TabDescriptor[]} tabs - Tabs list
|
|
10
|
+
* @param {String} selectedTab? - Currently selected tab
|
|
11
|
+
* @param {Function} onChange? - Handler for tabChanged event
|
|
12
|
+
* @param {String} queryParam? - Associated query param name
|
|
13
|
+
* @param {String} className? - Optional CSS class name
|
|
14
|
+
* @param {Boolean} right? - Inline tabs to the right within the header
|
|
15
|
+
* @param {*} children? - Additional content to render in the tabs header
|
|
16
|
+
* @constructor
|
|
17
|
+
*/
|
|
18
|
+
export function Tabs({tabs, selectedTab, queryParam, className, onChange, right, children}) {
|
|
19
|
+
const [internallySelectedTab, setSelectedTab] = useDependantState(() => {
|
|
20
|
+
//return the props-derived tab name if available
|
|
21
|
+
if (selectedTab) return selectedTab
|
|
22
|
+
//try to get from query string
|
|
23
|
+
if (queryParam) {
|
|
24
|
+
const tab = navigation.query[queryParam]
|
|
25
|
+
if (findTabByName(tab)) return tab
|
|
26
|
+
}
|
|
27
|
+
//return first tab
|
|
28
|
+
const firstTab = tabs[0]
|
|
29
|
+
return firstTab ? firstTab.name : null
|
|
30
|
+
}, [selectedTab])
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (!queryParam) return
|
|
34
|
+
const stopListeningQueryChanges = navigation.listen(({query}) => {
|
|
35
|
+
const tab = query[queryParam] || (tabs.find(t => t.isDefault) || tabs[0])?.name
|
|
36
|
+
selectTab(tab)
|
|
37
|
+
})
|
|
38
|
+
return () => {
|
|
39
|
+
stopListeningQueryChanges && stopListeningQueryChanges()
|
|
40
|
+
}
|
|
41
|
+
}, [tabs, queryParam])
|
|
42
|
+
|
|
43
|
+
function findTabByName(tabName) {
|
|
44
|
+
return tabs.find(t => t.name === tabName)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function selectTab(tabName) {
|
|
48
|
+
const tab = findTabByName(tabName)
|
|
49
|
+
if (!tab) {
|
|
50
|
+
if (selectedTab === undefined) {
|
|
51
|
+
setSelectedTab(null)
|
|
52
|
+
}
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (internallySelectedTab === tabName || selectedTab === tabName) return
|
|
57
|
+
|
|
58
|
+
if (onChange) {
|
|
59
|
+
onChange(tabName, this)
|
|
60
|
+
}
|
|
61
|
+
if (selectedTab === undefined) {
|
|
62
|
+
setSelectedTab(tabName)
|
|
63
|
+
}
|
|
64
|
+
if (queryParam) {
|
|
65
|
+
navigation.updateQuery({[queryParam]: tab.isDefault ? undefined : tabName})
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const s = selectedTab || internallySelectedTab
|
|
70
|
+
const tabToRender = tabs.find(({name}) => name === s) || tabs[0]
|
|
71
|
+
|
|
72
|
+
return <div className={cn('tabs', className, {'inline-right': right})}>
|
|
73
|
+
<div className="tabs-header">
|
|
74
|
+
<div>
|
|
75
|
+
{tabs.map(({name, title}) => <a href="#" key={name} onClick={() => selectTab(name)}
|
|
76
|
+
className={cn('tabs-item', 'condensed', {selected: s === name})}>
|
|
77
|
+
<span className="tabs-item-text">{title || name}</span></a>)}
|
|
78
|
+
</div>
|
|
79
|
+
{children}
|
|
80
|
+
</div>
|
|
81
|
+
<hr className="flare"/>
|
|
82
|
+
{!!tabToRender.render && <div className="tabs-body">
|
|
83
|
+
{tabToRender.render()}
|
|
84
|
+
</div>}
|
|
85
|
+
</div>
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @typedef {{}} TabDescriptor
|
|
90
|
+
* @property {String} name - Unique table name
|
|
91
|
+
* @property {String} title? - Optional display name (if differs from tab name)
|
|
92
|
+
* @property {Function} render? - Render callback
|
|
93
|
+
* @property {Boolean} isDefault? - Whether this tab should be displayed by default
|
|
94
|
+
*/
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
@use "../basic-styles/variables";
|
|
2
|
+
|
|
3
|
+
.tabs {
|
|
4
|
+
.tabs-header {
|
|
5
|
+
margin-top: 1em;
|
|
6
|
+
display: flex;
|
|
7
|
+
justify-content: space-between;
|
|
8
|
+
flex-wrap: wrap;
|
|
9
|
+
|
|
10
|
+
.tabs-item {
|
|
11
|
+
color: var(--color-bg);
|
|
12
|
+
font-family: $font-face-header;
|
|
13
|
+
font-weight: normal;
|
|
14
|
+
line-height: 2.6;
|
|
15
|
+
display: inline-block;
|
|
16
|
+
font-size: $font-size-base;
|
|
17
|
+
text-decoration: none;
|
|
18
|
+
padding: 0 0.8em;
|
|
19
|
+
position: relative;
|
|
20
|
+
text-align: center;
|
|
21
|
+
min-width: 4em;
|
|
22
|
+
height: 4rem;
|
|
23
|
+
|
|
24
|
+
@media (max-width: $responsive-mobile-browser-width) {
|
|
25
|
+
font-size: 0.9*$font-size-base;
|
|
26
|
+
min-width: 3.5em;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
&:before {
|
|
30
|
+
content: '';
|
|
31
|
+
display: block;
|
|
32
|
+
background: var(--color-highlight);
|
|
33
|
+
position: absolute;
|
|
34
|
+
left: 0;
|
|
35
|
+
right: 0;
|
|
36
|
+
height: 100%;
|
|
37
|
+
transform: skew(-20deg);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
&:hover:before {
|
|
41
|
+
background: var(--color-primary);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
+ .tabs-item:before {
|
|
45
|
+
border-left: 2px solid var(--color-primary);
|
|
46
|
+
margin-left: -1px;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
&.selected {
|
|
50
|
+
color: var(--color-text);
|
|
51
|
+
cursor: pointer;
|
|
52
|
+
|
|
53
|
+
&:before {
|
|
54
|
+
background: var(--color-primary);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.tabs-item-text {
|
|
59
|
+
position: relative;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
&.inline-right {
|
|
65
|
+
.tabs-header {
|
|
66
|
+
justify-content: right;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
> hr.flare {
|
|
71
|
+
margin-top: 0;
|
|
72
|
+
}
|
|
73
|
+
}
|