@stellar-expert/ui-framework 1.16.7 → 1.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +35 -0
- package/README.md +1125 -3
- package/account/account-address.js +127 -127
- package/account/available-balance.js +22 -22
- package/account/identicon.js +90 -90
- package/account/signer-key.js +65 -64
- package/api/explorer-api-hooks.js +209 -202
- package/api/explorer-api-paginated-list-hooks.js +441 -440
- package/api/explorer-batch-info-loader.js +111 -85
- package/api/explorer-tx-api.js +28 -28
- package/api/ledger-stream.js +15 -0
- package/asset/amount.js +56 -56
- package/asset/asset-icon.js +41 -41
- package/asset/asset-issuer.js +21 -21
- package/asset/asset-link.js +107 -107
- package/asset/asset-list-hooks.js +59 -59
- package/asset/asset-meta-hooks.js +88 -88
- package/asset/asset-selector.js +72 -71
- package/basic-styles/base.scss +50 -50
- package/basic-styles/grid.scss +1 -1
- package/basic-styles/table.scss +1 -1
- package/claimable-balance/claimable-balance-claimants.js +5 -0
- package/contract/contract-api.js +15 -0
- package/contract/invocation-info-view.js +10 -2
- package/contract/sc-val.js +24 -0
- package/controls/button-group.js +25 -19
- package/controls/button-group.scss +46 -46
- package/controls/button.js +93 -78
- package/controls/button.scss +173 -173
- package/controls/code-block.js +42 -34
- package/controls/code-block.scss +71 -71
- package/controls/dropdown.js +318 -287
- package/controls/dropdown.scss +159 -159
- package/controls/external-link.js +10 -4
- package/controls/info-tooltip.js +23 -16
- package/controls/slider.js +29 -19
- package/controls/tabs.js +94 -94
- package/controls/tabs.scss +70 -70
- package/controls/tooltip.js +244 -240
- package/controls/tooltip.scss +116 -116
- package/controls/update-highlighter.js +32 -27
- package/controls/update-highlighter.scss +7 -7
- package/date/date-selector.js +56 -54
- package/date/elapsed-time.js +28 -21
- package/dex/price-dynamic.js +53 -44
- package/dex/price-dynamic.scss +33 -33
- package/directory/directory-hooks.js +109 -97
- package/effect/effect-description.js +5 -3
- package/errors/error-boundary.js +110 -97
- package/horizon/horizon-account-helpers.js +104 -104
- package/horizon/horizon-ledger-helpers.js +35 -35
- package/horizon/horizon-trades-helper.js +88 -88
- package/horizon/horizon-transaction-helpers.js +36 -36
- package/index.d.ts +1241 -0
- package/interaction/accordion.js +43 -35
- package/interaction/autofocus.js +13 -9
- package/interaction/block-select.js +64 -53
- package/interaction/block-select.scss +21 -21
- package/interaction/copy-to-clipboard.js +25 -18
- package/interaction/inline-progress.js +20 -15
- package/interaction/qr-code.js +34 -34
- package/interaction/responsive.js +20 -20
- package/interaction/spoiler.js +52 -39
- package/interaction/spoiler.scss +6 -6
- package/interaction/theme-selector.js +13 -10
- package/ledger/ledger-entry-href-formatter.js +4 -3
- package/ledger/ledger-entry-link.js +93 -58
- package/ledger/ledger-info-parser.js +28 -0
- package/meta/page-meta-tags.js +243 -238
- package/module/dynamic-module.js +47 -47
- package/package.json +3 -2
- package/state/on-screen-hooks.js +22 -22
- package/state/page-visibility-helpers.js +29 -16
- package/state/page-visibility-hooks.js +13 -11
- package/state/screen-orientation-hooks.js +19 -15
- package/state/state-hooks.js +76 -76
- package/state/stellar-network-hooks.js +56 -44
- package/state/theme.js +29 -28
- package/stellar/key-type.js +92 -91
- package/stellar/ledger-generic-id.js +39 -39
- package/stellar/signature-hint-utils.js +65 -65
- package/toast/toast-notifications-block.js +59 -59
- package/toast/toast-notifications.scss +1 -1
- package/tx/op-description-view.js +84 -81
- package/tx/op-icon.js +98 -98
- package/tx/parser/op-balance-changes.js +66 -66
- package/tx/parser/op-descriptor.js +51 -48
- package/tx/parser/tx-details-parser.js +81 -81
- package/tx/parser/tx-matcher.js +371 -371
- package/tx/parser/type-filter-matcher.js +126 -126
- package/tx/tx-list-hooks.js +32 -18
- package/tx/tx-operations-list.js +117 -116
package/asset/asset-selector.js
CHANGED
|
@@ -1,72 +1,73 @@
|
|
|
1
|
-
import React, {useCallback, useRef, useState} from 'react'
|
|
2
|
-
import {Dropdown} from '../controls/dropdown'
|
|
3
|
-
import {AssetLink} from './asset-link'
|
|
4
|
-
import {useAssetList} from './asset-list-hooks'
|
|
5
|
-
import './asset-selector.scss'
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
*
|
|
9
|
-
* @param {
|
|
10
|
-
* @param {
|
|
11
|
-
* @param {
|
|
12
|
-
* @param {
|
|
13
|
-
* @param {
|
|
14
|
-
* @
|
|
15
|
-
* @
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
<
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
* @
|
|
1
|
+
import React, {useCallback, useRef, useState} from 'react'
|
|
2
|
+
import {Dropdown} from '../controls/dropdown'
|
|
3
|
+
import {AssetLink} from './asset-link'
|
|
4
|
+
import {useAssetList} from './asset-list-hooks'
|
|
5
|
+
import './asset-selector.scss'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Asset picker dropdown with search
|
|
9
|
+
* @param {AssetSelectorOnAssetChanged} onChange - On asset selected callback
|
|
10
|
+
* @param {string} [value] - Selected asset
|
|
11
|
+
* @param {string[]} [predefinedAssets] - Optional lists of predefined assets that should be shown at the top of the dropdown list
|
|
12
|
+
* @param {boolean} [restricted] - If set, the selector is limited to the predefined assets list only
|
|
13
|
+
* @param {string} [title] - Dropdown selector title
|
|
14
|
+
* @param {string} [expanded] - Expanded by default
|
|
15
|
+
* @return {JSX.Element}
|
|
16
|
+
* @constructor
|
|
17
|
+
*/
|
|
18
|
+
export function AssetSelector({value, predefinedAssets, onChange, restricted, title, expanded}) {
|
|
19
|
+
const [search, setSearch] = useState('')
|
|
20
|
+
const searchRef = useRef()
|
|
21
|
+
const options = []
|
|
22
|
+
|
|
23
|
+
const focusSearch = useCallback(() => {
|
|
24
|
+
setTimeout(() => searchRef.current?.focus(), 200)
|
|
25
|
+
}, [])
|
|
26
|
+
|
|
27
|
+
if (predefinedAssets) {
|
|
28
|
+
for (const asset of predefinedAssets) {
|
|
29
|
+
options.push({
|
|
30
|
+
value: asset,
|
|
31
|
+
title: <AssetLink link={false} asset={asset}/>,
|
|
32
|
+
hidden: search && !asset.split('-')[0].toLowerCase().includes(search.toLowerCase())
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let loadNextPage
|
|
38
|
+
if (!restricted) {
|
|
39
|
+
const {assets, loadPage, loading} = useAssetList({search: search?.trim() || undefined})
|
|
40
|
+
for (let {asset} of assets) {
|
|
41
|
+
if (!predefinedAssets || !predefinedAssets.includes(asset)) {
|
|
42
|
+
options.push({value: asset, title: <AssetLink link={false} asset={asset}/>})
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (!options.filter(opt => !opt.hidden).length) {
|
|
46
|
+
if (loading) {
|
|
47
|
+
options.push({value: '...', disabled: true, title: <div className="loader"/>})
|
|
48
|
+
} else {
|
|
49
|
+
options.push({
|
|
50
|
+
value: 'no',
|
|
51
|
+
disabled: true,
|
|
52
|
+
title: <div className="dimmed text-center text-small">(not found)</div>
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
loadNextPage = loadPage
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return <Dropdown solo className="asset-selector" options={options} value={value} onOpen={focusSearch} title={title} expanded={expanded}
|
|
60
|
+
showToggle={!title} onChange={onChange} onScroll={e => e.rel === 'bottom' && loadNextPage?.call(this)} header={<>
|
|
61
|
+
<h3>Select an asset</h3>
|
|
62
|
+
<div className="relative">
|
|
63
|
+
<input type="text" value={search} ref={searchRef} onChange={e => setSearch(e.target.value)}
|
|
64
|
+
placeholder="Search by asset code or website"/>
|
|
65
|
+
<i className="icon-search dimmed"/>
|
|
66
|
+
</div>
|
|
67
|
+
</>}/>
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @callback AssetSelectorOnAssetChanged
|
|
72
|
+
* @param {string} value
|
|
72
73
|
*/
|
package/basic-styles/base.scss
CHANGED
|
@@ -1,51 +1,51 @@
|
|
|
1
|
-
*,
|
|
2
|
-
*:after,
|
|
3
|
-
*:before {
|
|
4
|
-
box-sizing: border-box;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
* {
|
|
8
|
-
@include scrollbars(6px, var(--color-dimmed), var(--color-contrast-border));
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
html {
|
|
12
|
-
font-size: 62.5%;
|
|
13
|
-
height: 100%;
|
|
14
|
-
-ms-text-size-adjust: 100%;
|
|
15
|
-
-webkit-text-size-adjust: 100%;
|
|
16
|
-
scroll-behavior: smooth;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
body {
|
|
20
|
-
color: var(--color-text);
|
|
21
|
-
background: var(--color-bg);
|
|
22
|
-
font-family: $font-face-primary;
|
|
23
|
-
font-size: $font-size-base;
|
|
24
|
-
font-weight: $font-weight-base;
|
|
25
|
-
letter-spacing: 0.01em;
|
|
26
|
-
line-height: $font-line-height-base;
|
|
27
|
-
height: 100%;
|
|
28
|
-
overflow-x: hidden;
|
|
29
|
-
min-width: 320px;
|
|
30
|
-
margin: 0;
|
|
31
|
-
font-smoothing: antialiased;
|
|
32
|
-
-webkit-font-smoothing: antialiased;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
::selection {
|
|
36
|
-
background-color:
|
|
37
|
-
color: var(--color-text);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
.page-container {
|
|
41
|
-
min-height: calc(100vh - 8rem);
|
|
42
|
-
display: flex;
|
|
43
|
-
flex-direction: column;
|
|
44
|
-
/*@media (min-width: $responsive-mobile-browser-width) {
|
|
45
|
-
justify-content: center;
|
|
46
|
-
}*/
|
|
47
|
-
|
|
48
|
-
> .no-center {
|
|
49
|
-
margin-bottom: auto;
|
|
50
|
-
}
|
|
1
|
+
*,
|
|
2
|
+
*:after,
|
|
3
|
+
*:before {
|
|
4
|
+
box-sizing: border-box;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
* {
|
|
8
|
+
@include scrollbars(6px, var(--color-dimmed), var(--color-contrast-border));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
html {
|
|
12
|
+
font-size: 62.5%;
|
|
13
|
+
height: 100%;
|
|
14
|
+
-ms-text-size-adjust: 100%;
|
|
15
|
+
-webkit-text-size-adjust: 100%;
|
|
16
|
+
scroll-behavior: smooth;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
body {
|
|
20
|
+
color: var(--color-text);
|
|
21
|
+
background: var(--color-bg);
|
|
22
|
+
font-family: $font-face-primary;
|
|
23
|
+
font-size: $font-size-base;
|
|
24
|
+
font-weight: $font-weight-base;
|
|
25
|
+
letter-spacing: 0.01em;
|
|
26
|
+
line-height: $font-line-height-base;
|
|
27
|
+
height: 100%;
|
|
28
|
+
overflow-x: hidden;
|
|
29
|
+
min-width: 320px;
|
|
30
|
+
margin: 0;
|
|
31
|
+
font-smoothing: antialiased;
|
|
32
|
+
-webkit-font-smoothing: antialiased;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
::selection {
|
|
36
|
+
background-color: rgba($color-primary, 0.8) !important;
|
|
37
|
+
color: var(--color-text);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.page-container {
|
|
41
|
+
min-height: calc(100vh - 8rem);
|
|
42
|
+
display: flex;
|
|
43
|
+
flex-direction: column;
|
|
44
|
+
/*@media (min-width: $responsive-mobile-browser-width) {
|
|
45
|
+
justify-content: center;
|
|
46
|
+
}*/
|
|
47
|
+
|
|
48
|
+
> .no-center {
|
|
49
|
+
margin-bottom: auto;
|
|
50
|
+
}
|
|
51
51
|
}
|
package/basic-styles/grid.scss
CHANGED
package/basic-styles/table.scss
CHANGED
|
@@ -3,6 +3,11 @@ import {xdrParseClaimant} from '@stellar-expert/claimable-balance-utils'
|
|
|
3
3
|
import {AccountAddress} from '../account/account-address'
|
|
4
4
|
import {CodeBlock} from '../controls/code-block'
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Displays a list of claimable balance claimants with their addresses and predicates
|
|
8
|
+
* @param {Object} props
|
|
9
|
+
* @param {Array<Object>} props.claimants - Array of claimant objects (raw XDR or parsed)
|
|
10
|
+
*/
|
|
6
11
|
export const ClaimableBalanceClaimants = React.memo(function ClaimableBalanceClaimants({claimants}) {
|
|
7
12
|
const parsed = claimants.map(c => {
|
|
8
13
|
if (c.destination && c.predicate && !c._predicate)
|
package/contract/contract-api.js
CHANGED
|
@@ -2,6 +2,11 @@ import {useEffect, useState} from 'react'
|
|
|
2
2
|
import {useExplorerApi} from '../api/explorer-api-hooks'
|
|
3
3
|
import {getCurrentStellarNetwork} from '../state/stellar-network-hooks'
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* React hook that fetches contract information from the Explorer API
|
|
7
|
+
* @param {string} address - Contract address
|
|
8
|
+
* @return {ExplorerApiResult}
|
|
9
|
+
*/
|
|
5
10
|
export function useContractInfo(address) {
|
|
6
11
|
return useExplorerApi('contract/' + address, {
|
|
7
12
|
processResult(data) {
|
|
@@ -14,10 +19,20 @@ export function useContractInfo(address) {
|
|
|
14
19
|
})
|
|
15
20
|
}
|
|
16
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Generate a URL to download the WASM source for a contract
|
|
24
|
+
* @param {string} hash - Contract WASM hash
|
|
25
|
+
* @return {string} Full URL to the contract WASM source
|
|
26
|
+
*/
|
|
17
27
|
export function generateContractSourceLink(hash) {
|
|
18
28
|
return `${explorerApiOrigin}/explorer/${getCurrentStellarNetwork()}/contract/wasm/${hash}`
|
|
19
29
|
}
|
|
20
30
|
|
|
31
|
+
/**
|
|
32
|
+
* React hook that fetches contract WASM binary source by hash
|
|
33
|
+
* @param {string} hash - Contract WASM hash
|
|
34
|
+
* @return {ArrayBuffer|null|undefined} WASM binary (undefined while loading, null if not found)
|
|
35
|
+
*/
|
|
21
36
|
export function useContractSource(hash) {
|
|
22
37
|
const [source, setSource] = useState()
|
|
23
38
|
useEffect(() => {
|
|
@@ -8,6 +8,14 @@ import {parseScValValue, primitiveTypes, ScVal, ScValStruct} from './sc-val'
|
|
|
8
8
|
import {useContractSource} from './contract-api'
|
|
9
9
|
import {sacInterface} from './sac-interface'
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Basic Soroban contract invocation info
|
|
13
|
+
* @param {string} [contract] - Contract address
|
|
14
|
+
* @param {string} func - Invoked function name
|
|
15
|
+
* @param {Array|string} args - Function arguments (ScVal array or XDR string)
|
|
16
|
+
* @param {string|xdr.ScVal} [result] - Invocation result
|
|
17
|
+
* @param {string} [sac] - Stellar Asset Contract identifier
|
|
18
|
+
*/
|
|
11
19
|
export default function InvocationInfoView({contract, func, args, result, sac}) {
|
|
12
20
|
if (typeof args === 'string') {
|
|
13
21
|
args = parseScValValue(args)
|
|
@@ -45,13 +53,13 @@ const ExtendedInvocationInfoView = React.memo(function ({contract, func, args, r
|
|
|
45
53
|
const fd = meta.functions[func]
|
|
46
54
|
if (!fd)
|
|
47
55
|
return <div className="error"><i className="icon-warning-circle"/> Failed to parse contract code</div>
|
|
48
|
-
const shouldIndent = args.length > 2 ||
|
|
56
|
+
const shouldIndent = args.length > 2 || fd.inputs.some(i => !primitiveTypes.has(i.type)) // multi-line display for large argument number or complex types
|
|
49
57
|
const contractInfo = sac ?
|
|
50
58
|
<> from asset <AssetLink asset={sac}/></> :
|
|
51
59
|
<span className="text-tiny dimmed">
|
|
52
60
|
 SDK v{meta.sdkVersion.split('#')[0]} RUST v{shortenString(meta.rustVersion, 12)}
|
|
53
61
|
</span>
|
|
54
|
-
const contractArgNames =
|
|
62
|
+
const contractArgNames = fd.inputs.map(i => i.name)
|
|
55
63
|
//TODO: show contract validation info
|
|
56
64
|
return <div>
|
|
57
65
|
<div>
|
package/contract/sc-val.js
CHANGED
|
@@ -6,6 +6,14 @@ import {shortenString} from '@stellar-expert/formatter'
|
|
|
6
6
|
import {AccountAddress} from '../account/account-address'
|
|
7
7
|
import './sc-val.scss'
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Renders a Stellar smart contract value (ScVal) in a human-readable format
|
|
11
|
+
* @param {Object} props
|
|
12
|
+
* @param {string|xdr.ScVal|Array} props.value - ScVal as base64 XDR string, parsed XDR object, or array
|
|
13
|
+
* @param {boolean} [props.nested=false] - Whether this is a nested rendering (internal use)
|
|
14
|
+
* @param {boolean} [props.indent=false] - Enable block-level indentation for nested structures
|
|
15
|
+
* @param {boolean} [props.wrapObjects=true] - Wrap maps and arrays with brackets
|
|
16
|
+
*/
|
|
9
17
|
export const ScVal = React.memo(function ScVal({value, nested = false, indent = false, wrapObjects = true}) {
|
|
10
18
|
if (!nested)
|
|
11
19
|
return <code className={cn('sc-val', {block: indent})}><ScVal value={value} indent={indent} nested/></code>
|
|
@@ -88,6 +96,11 @@ export const ScVal = React.memo(function ScVal({value, nested = false, indent =
|
|
|
88
96
|
}
|
|
89
97
|
})
|
|
90
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Parse a base64-encoded XDR ScVal string into an xdr.ScVal object
|
|
101
|
+
* @param {string} value - Base64-encoded XDR string
|
|
102
|
+
* @return {xdr.ScVal}
|
|
103
|
+
*/
|
|
91
104
|
export function parseScValValue(value) {
|
|
92
105
|
return xdr.ScVal.fromXDR(value, 'base64')
|
|
93
106
|
}
|
|
@@ -96,6 +109,13 @@ const ScValType = React.memo(function ScValType({type}) {
|
|
|
96
109
|
return <sub className="dimmed text-tiny" style={{padding: '0 0.2em'}}>{type}</sub>
|
|
97
110
|
})
|
|
98
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Structural wrapper for ScVal elements, handling indentation and separators
|
|
114
|
+
* @param {Object} props
|
|
115
|
+
* @param {boolean} [props.indent] - Enable block-level indentation
|
|
116
|
+
* @param {*} props.children - Nested content
|
|
117
|
+
* @param {number} [props.separate] - Show comma separator when > 1
|
|
118
|
+
*/
|
|
99
119
|
export const ScValStruct = React.memo(function ScValStruct({indent, children, separate}) {
|
|
100
120
|
const separator = separate > 1 ? <>, </> : null
|
|
101
121
|
if (!indent)
|
|
@@ -105,4 +125,8 @@ export const ScValStruct = React.memo(function ScValStruct({indent, children, se
|
|
|
105
125
|
</div>
|
|
106
126
|
})
|
|
107
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Set of primitive ScVal type identifiers
|
|
130
|
+
* @type {Set<string>}
|
|
131
|
+
*/
|
|
108
132
|
export const primitiveTypes = new Set(['b', 'i32', 'u32', 'i256', 'u256', 'i128', 'u128', 'i64', 'u64', 'timepoint', 'duration', 'address', 'bytes', 'str', 'sym', 'nonceKey', 'contractId', 'instance'])
|
package/controls/button-group.js
CHANGED
|
@@ -1,20 +1,26 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import PropTypes from 'prop-types'
|
|
3
|
-
import './button-group.scss'
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
ButtonGroup
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import PropTypes from 'prop-types'
|
|
3
|
+
import './button-group.scss'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Groups multiple buttons together with consistent spacing
|
|
7
|
+
* @param {Object} props
|
|
8
|
+
* @param {boolean} [props.inline] - Render as inline element (`<span>`) instead of block (`<div>`)
|
|
9
|
+
* @param {*} props.children - Nested buttons
|
|
10
|
+
*/
|
|
11
|
+
export const ButtonGroup = React.memo(function ButtonGroup({inline, children, ...otherProps}) {
|
|
12
|
+
if (inline)
|
|
13
|
+
return <span className="button-group" {...otherProps}>{children}</span>
|
|
14
|
+
return <div className="button-group" {...otherProps}>{children}</div>
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
ButtonGroup.propTypes = {
|
|
18
|
+
/**
|
|
19
|
+
* Nested buttons
|
|
20
|
+
*/
|
|
21
|
+
children: PropTypes.any.isRequired,
|
|
22
|
+
/**
|
|
23
|
+
* Whether to render the group as inline element (span)
|
|
24
|
+
*/
|
|
25
|
+
inline: PropTypes.bool
|
|
20
26
|
}
|
|
@@ -1,47 +1,47 @@
|
|
|
1
|
-
@import "./button";
|
|
2
|
-
|
|
3
|
-
.button-group {
|
|
4
|
-
> * {
|
|
5
|
-
vertical-align: top;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
&:not(:last-child) {
|
|
9
|
-
margin-right: $button-spacing;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
& > button,
|
|
13
|
-
& > .button {
|
|
14
|
-
+ .button {
|
|
15
|
-
margin-left: -0.76em;
|
|
16
|
-
|
|
17
|
-
&.small {
|
|
18
|
-
margin-left: -0.96em;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
&:before {
|
|
22
|
-
border-left: 2px solid var(--color-primary);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
&.selected {
|
|
27
|
-
&.disabled,
|
|
28
|
-
&[disabled] {
|
|
29
|
-
cursor: pointer;
|
|
30
|
-
color: var(--color-text);
|
|
31
|
-
opacity: 1;
|
|
32
|
-
|
|
33
|
-
&:before {
|
|
34
|
-
background: var(--color-primary);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
&:not(:last-child) {;
|
|
40
|
-
margin-right: 0;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
& > :not(:last-child) > .button {
|
|
45
|
-
margin-right: 0;
|
|
46
|
-
}
|
|
1
|
+
@import "./button";
|
|
2
|
+
|
|
3
|
+
.button-group {
|
|
4
|
+
> * {
|
|
5
|
+
vertical-align: top;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
&:not(:last-child) {
|
|
9
|
+
margin-right: $button-spacing;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
& > button,
|
|
13
|
+
& > .button {
|
|
14
|
+
+ .button {
|
|
15
|
+
margin-left: -0.76em;
|
|
16
|
+
|
|
17
|
+
&.small {
|
|
18
|
+
margin-left: -0.96em;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
&:before {
|
|
22
|
+
border-left: 2px solid var(--color-primary);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
&.selected {
|
|
27
|
+
&.disabled,
|
|
28
|
+
&[disabled] {
|
|
29
|
+
cursor: pointer;
|
|
30
|
+
color: var(--color-text);
|
|
31
|
+
opacity: 1;
|
|
32
|
+
|
|
33
|
+
&:before {
|
|
34
|
+
background: var(--color-primary);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
&:not(:last-child) {;
|
|
40
|
+
margin-right: 0;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
& > :not(:last-child) > .button {
|
|
45
|
+
margin-right: 0;
|
|
46
|
+
}
|
|
47
47
|
}
|