@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,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,5 @@
1
+ import React from 'react'
2
+
3
+ export const ExternalLink = React.memo(function ExternalLink({href, children, ...otherProps}) {
4
+ return <a href={href} target="_blank" rel="noreferrer noopener" {...otherProps}>{children}</a>
5
+ })
@@ -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&hellip;</a>}
11
+ </Tooltip>
12
+ })
13
+
14
+ InfoTooltip.propTypes = {
15
+ children: PropTypes.any.isRequired,
16
+ link: PropTypes.string
17
+ }
@@ -0,0 +1,4 @@
1
+ .info-tooltip-link {
2
+ display: block;
3
+ padding-top: 0.3em;
4
+ }
@@ -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
+ }
@@ -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
+ }