@stellar-expert/ui-framework 1.17.0 → 1.19.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/README.md CHANGED
@@ -1011,6 +1011,49 @@ React hook for transaction details by ID or hash.
1011
1011
 
1012
1012
  ---
1013
1013
 
1014
+ ### Filters
1015
+
1016
+ #### `FilterView`
1017
+
1018
+ Composable filter panel for managing search filters with URL query parameter sync. Supports multiple filter types including accounts, assets, operation types, timestamps, offers, and liquidity pools.
1019
+
1020
+ | Prop | Type | Default | Description |
1021
+ |------|------|---------|-------------|
1022
+ | `presetFilter` | `object` | — | Initial base filters always applied |
1023
+ | `fields` | `object` | `{}` | Filter field definitions keyed by field name |
1024
+ | `onChange` | `function` | — | Callback receiving merged preset + user filters |
1025
+
1026
+ Each entry in `fields` is a `FilterFieldDescriptor`:
1027
+
1028
+ | Property | Type | Default | Description |
1029
+ |----------|------|---------|-------------|
1030
+ | `icon` | `string` | — | Icon class suffix (prepended with `icon-`) |
1031
+ | `title` | `string` | — | Short title in filter condition |
1032
+ | `description` | `string` | — | Tooltip / "Add filter" dropdown text |
1033
+ | `multi` | `boolean` | `true` | Allow multiple values for this field |
1034
+
1035
+ Supported field keys: `type`, `account`, `source`, `destination`, `asset`, `src_asset`, `dest_asset`, `from`, `to`, `memo`, `offer`, `pool`, `topic`.
1036
+
1037
+ ```jsx
1038
+ import {FilterView, parseFiltersFromQuery} from '@stellar-expert/ui-framework'
1039
+
1040
+ <FilterView
1041
+ presetFilter={{account: [address]}}
1042
+ fields={{
1043
+ type: {icon: 'hexagon', title: 'Type', description: 'Operation type'},
1044
+ asset: {icon: 'asset', title: 'Asset', description: 'Asset involved'},
1045
+ from: {icon: 'clock', title: 'From', description: 'Start date', multi: false},
1046
+ to: {icon: 'clock', title: 'To', description: 'End date', multi: false}
1047
+ }}
1048
+ onChange={filters => setActiveFilters(filters)}/>
1049
+ ```
1050
+
1051
+ #### `parseFiltersFromQuery()`
1052
+
1053
+ Parses and validates filter parameters from the current URL query string against registered field definitions. Returns an object with matched filter key-value pairs.
1054
+
1055
+ ---
1056
+
1014
1057
  ### Effects
1015
1058
 
1016
1059
  #### `EffectDescription`
@@ -1098,9 +1141,9 @@ Convert a signature hint Buffer to a StrKey mask string.
1098
1141
 
1099
1142
  Format a signature hint for display.
1100
1143
 
1101
- #### `singatureHintMatchesKey(hint, key)`
1144
+ #### `signatureHintMatchesKey(hint, key)`
1102
1145
 
1103
- Check if a hint matches a key. Note: function name contains a typo ("singature").
1146
+ Check if a hint matches a key.
1104
1147
 
1105
1148
  #### `findKeyBySignatureHint(hint, allKeys)`
1106
1149
 
@@ -1,4 +1,4 @@
1
- import React, {useCallback, useEffect, useState} from 'react'
1
+ import React, {forwardRef, useCallback, useEffect, useState} from 'react'
2
2
  import {debounce} from 'throttle-debounce'
3
3
  import {normalizeDate, toUnixTimestamp} from '@stellar-expert/formatter'
4
4
 
@@ -10,7 +10,7 @@ import {normalizeDate, toUnixTimestamp} from '@stellar-expert/formatter'
10
10
  * @param {string} [max] - Maximum allowed date
11
11
  * @param {*} [ref]
12
12
  */
13
- export function DateSelector({value, onChange, min, max, ref, ...otherProps}) {
13
+ export const DateSelector = forwardRef(({value, onChange, min, max, ...otherProps}, ref) => {
14
14
  const [date, setDate] = useState(value ? trimIsoDateSeconds(normalizeDate(value)) : '')
15
15
  useEffect(() => {
16
16
  if (value) {
@@ -35,7 +35,7 @@ export function DateSelector({value, onChange, min, max, ref, ...otherProps}) {
35
35
  return <input type="datetime-local" value={date} className="date-selector condensed" step={60} ref={ref}
36
36
  min={min || minSelectableValue} max={max} onChange={e => selectDate(e.target.value)}
37
37
  style={{width: '11em', overflow: 'hidden'}} {...otherProps}/>
38
- }
38
+ })
39
39
 
40
40
  /**
41
41
  * Trim seconds and milliseconds for basic ISO date format representation
@@ -0,0 +1,20 @@
1
+ import React, {useCallback, useState} from 'react'
2
+ import {StrKey} from '@stellar/stellar-base'
3
+ import {AccountAddress} from '../../account/account-address'
4
+ import {useAutoFocusRef} from '../../interaction/autofocus'
5
+
6
+ export function AccountEditor({value, setValue}) {
7
+ const [internalValue, setInternalValue] = useState(value || '')
8
+ const onChange = useCallback(function (e) {
9
+ const v = e.target.value.trim()
10
+ setInternalValue(v)
11
+ if (StrKey.isValidEd25519PublicKey(v) || StrKey.isValidContract(v)) {
12
+ setValue(v)
13
+ }
14
+ }, [setValue])
15
+ if (!setValue)
16
+ return <AccountAddress account={value} link={false}/>
17
+
18
+ return <input type="text" value={internalValue} onChange={onChange} ref={useAutoFocusRef}
19
+ style={{width: '26em', maxWidth: '56vw'}} className="condensed"/>
20
+ }
@@ -0,0 +1,9 @@
1
+ import React from 'react'
2
+ import {AssetLink} from '../../asset/asset-link'
3
+ import {AssetSelector} from '../../asset/asset-selector'
4
+
5
+ export function AssetEditor({value, setValue}) {
6
+ if (!setValue)
7
+ return <AssetLink asset={value} link={false}/>
8
+ return <AssetSelector value={value} onChange={setValue} title={value ? undefined : 'Choose asset'} expanded/>
9
+ }
@@ -0,0 +1,34 @@
1
+ import React, {useCallback, useState} from 'react'
2
+ import {UtcTimestamp} from '../../date/utc-timestamp'
3
+ import {DateSelector} from '../../date/date-selector'
4
+ import {useAutoFocusRef} from '../../interaction/autofocus'
5
+
6
+ export function TimestampEditor({value, setValue, edit}) {
7
+ value = parseInt(value, 10)
8
+ const [internalValue, setInternalValue] = useState(value || undefined)
9
+
10
+ const confirm = useCallback(function () {
11
+ setInternalValue(internalValue => {
12
+ if (internalValue) {
13
+ setValue(internalValue)
14
+ }
15
+ return internalValue
16
+ })
17
+ }, [setValue])
18
+
19
+ const onKeyDown = useCallback(function (e) {
20
+ if (e.key === 'Enter') {
21
+ confirm()
22
+ }
23
+ if (e.key === 'Escape') {
24
+ setValue(value)
25
+ }
26
+ }, [confirm, setValue, value])
27
+ if (!setValue)
28
+ return <UtcTimestamp date={value} className="condensed"/>
29
+ return <>
30
+ <DateSelector value={internalValue} onChange={setInternalValue} onKeyDown={onKeyDown} ref={edit ? useAutoFocusRef : null}/>
31
+ <a href="#" className="icon-ok" onClick={confirm}/>
32
+ &nbsp;
33
+ </>
34
+ }
@@ -0,0 +1,148 @@
1
+ import React from 'react'
2
+ import {Dropdown} from '../../controls/dropdown'
3
+
4
+ export function OperationTypeEditor({value, setValue}) {
5
+ if (!setValue) {
6
+ const option = typeEditorOptions.find(opt => opt.value === value)
7
+ return <span>{option ? option.title : 'Unknown operation type'}</span>
8
+ }
9
+
10
+ return <Dropdown title="Choose operation type" expanded onChange={setValue}
11
+ options={typeEditorOptions.map(({title, value}) => ({value, title}))}/>
12
+ }
13
+
14
+ const typeEditorOptions = [
15
+ {
16
+ title: 'Group: payments',
17
+ description: 'Including CreateAccount, AccountMerge, Payment, PathPaymentStrictReceive, PathPaymentStrictSend, CreateClaimableBalance, ClaimClaimableBalance, Clawback, ClawbackClaimableBalance, Inflation operations',
18
+ value: 'payments'
19
+ },
20
+ {
21
+ title: 'Group: trustlines',
22
+ description: 'Including ChangeTrust, AllowTrust, SetTrustLineFlags operations',
23
+ value: 'trustlines'
24
+ },
25
+ {
26
+ title: 'Group: DEX trading',
27
+ description: 'Including ManageSellOffer, ManageBuyOffer, CreatePassiveSellOffer, LiquidityPoolDeposit, LiquidityPoolWithdraw operations',
28
+ value: 'dex'
29
+ },
30
+ {
31
+ title: 'Group: account settings',
32
+ description: 'Including CreateAccount, SetOptions, ChangeTrust, AllowTrust, AccountMerge, Inflation, ManageData, BumpSequence, BeginSponsoringFutureReserves, EndSponsoringFutureReserves, RevokeSponsorship, SetTrustLineFlags operations',
33
+ value: 'settings'
34
+ },
35
+ {
36
+ title: 'Group: smart contracts',
37
+ description: 'Including InvokeHostFunction, BumpFootprintExpiration, RestoreFootprint operations',
38
+ value: 'soroban'
39
+ },
40
+ {
41
+ title: 'CreateAccount',
42
+ value: '0'
43
+ },
44
+ {
45
+ title: 'Payment',
46
+ value: '1'
47
+ },
48
+ {
49
+ title: 'PathPaymentStrictReceive',
50
+ value: '2'
51
+ },
52
+ {
53
+ title: 'ManageSellOffer',
54
+ value: '3'
55
+ },
56
+ {
57
+ title: 'CreatePassiveSellOffer',
58
+ value: '4'
59
+ },
60
+ {
61
+ title: 'SetOptions',
62
+ value: '5'
63
+ },
64
+ {
65
+ title: 'ChangeTrust',
66
+ value: '6'
67
+ },
68
+ {
69
+ title: 'AllowTrust',
70
+ value: '7'
71
+ },
72
+ {
73
+ title: 'AccountMerge',
74
+ value: '8'
75
+ },
76
+ {
77
+ title: 'Inflation',
78
+ value: '9'
79
+ },
80
+ {
81
+ title: 'ManageData',
82
+ value: '10'
83
+ },
84
+ {
85
+ title: 'BumpSequence',
86
+ value: '11'
87
+ },
88
+ {
89
+ title: 'ManageBuyOffer',
90
+ value: '12'
91
+ },
92
+ {
93
+ title: 'PathPaymentStrictSend',
94
+ value: '13'
95
+ },
96
+ {
97
+ title: 'CreateClaimableBalance',
98
+ value: '14'
99
+ },
100
+ {
101
+ title: 'ClaimClaimableBalance',
102
+ value: '15'
103
+ },
104
+ {
105
+ title: 'BeginSponsoringFutureReserves',
106
+ value: '16'
107
+ },
108
+ {
109
+ title: 'EndSponsoringFutureReserves',
110
+ value: '17'
111
+ },
112
+ {
113
+ title: 'RevokeSponsorship',
114
+ value: '18'
115
+ },
116
+ {
117
+ title: 'Clawback',
118
+ value: '19'
119
+ },
120
+ {
121
+ title: 'ClawbackClaimableBalance',
122
+ value: '20'
123
+ },
124
+ {
125
+ title: 'SetTrustLineFlags',
126
+ value: '21'
127
+ },
128
+ {
129
+ title: 'LiquidityPoolDeposit',
130
+ value: '22'
131
+ },
132
+ {
133
+ title: 'LiquidityPoolWithdraw',
134
+ value: '23'
135
+ },
136
+ {
137
+ title: 'InvokeHostFunction',
138
+ value: '24'
139
+ },
140
+ {
141
+ title: 'BumpFootprintExpiration',
142
+ value: '25'
143
+ },
144
+ {
145
+ title: 'RestoreFootprint',
146
+ value: '26'
147
+ }
148
+ ]
@@ -0,0 +1,102 @@
1
+ import React, {useCallback, useState} from 'react'
2
+ import {StrKey} from '@stellar/stellar-base'
3
+ import {shortenString} from '@stellar-expert/formatter'
4
+ import {AssetLink} from '../asset/asset-link'
5
+ import {useAutoFocusRef} from '../interaction/autofocus'
6
+ import {OperationTypeEditor} from './editors/type-filter-view'
7
+ import {AssetEditor} from './editors/asset-filter-view'
8
+ import {AccountEditor} from './editors/account-filter-view'
9
+ import {TimestampEditor} from './editors/timestamp-filter-view'
10
+
11
+ export function TextEditor({value, setValue, mask}) {
12
+ const [internalValue, setInternalValue] = useState(value || '')
13
+
14
+ const onChange = useCallback(function (e) {
15
+ let v = e.target.value.trim()
16
+ if (mask instanceof RegExp) {
17
+ v = v.replace(mask, '')
18
+ }
19
+ if (typeof mask === 'function') {
20
+ v = mask(v)
21
+ }
22
+ setInternalValue(v)
23
+ }, [mask])
24
+
25
+ const confirm = useCallback(function () {
26
+ setInternalValue(internalValue => {
27
+ setValue(internalValue)
28
+ return internalValue
29
+ })
30
+ }, [setValue])
31
+
32
+ const onKeyDown = useCallback(function (e) {
33
+ if (e.key === 'Enter') {
34
+ confirm()
35
+ }
36
+ if (e.key === 'Escape') {
37
+ setValue(value)
38
+ }
39
+ }, [confirm, setValue, value])
40
+
41
+ if (!setValue) {
42
+ value = value.toString()
43
+ return <span>{value.length > 20 ? shortenString(value, 12) : value}</span>
44
+ }
45
+
46
+ return <>
47
+ <input type="text" value={internalValue} ref={useAutoFocusRef} onChange={onChange} onKeyDown={onKeyDown}
48
+ style={{width: '16em', maxWidth: '50vw'}}/>&nbsp;
49
+ <a href="#" className="icon-ok" onClick={confirm}/>
50
+ </>
51
+ }
52
+
53
+ function OfferIdEditor({value, setValue}) {
54
+ return <TextEditor value={value} setValue={setValue} mask={/[\D]/g}/>
55
+ }
56
+
57
+ function LiquidityPoolEditor({value, setValue}) {
58
+ if (!setValue) {
59
+ if (/^[a-f0-9]{64}$/.test(value)) { //remap hex format to pool id format
60
+ try {
61
+ value = StrKey.encodeLiquidityPool(Buffer.from(value, 'hex'))
62
+ } catch (_) {
63
+ }
64
+ }
65
+ if (!StrKey.isValidLiquidityPool(value))
66
+ return <span className="dimmed">(Invalid pool)</span>
67
+ if (value.startsWith('native'))
68
+ return <span className="dimmed">(Native pool)</span>
69
+ return <AssetLink asset={value} link={false}/>
70
+ }
71
+ return <TextEditor value={value} setValue={setValue} mask={/[\W]/g}/>
72
+ }
73
+
74
+ function MemoEditor({value, setValue}) {
75
+ return <TextEditor value={value} setValue={setValue} mask={v => v.substring(0, 64)}/>
76
+ }
77
+
78
+ export function TopicEditor({value, setValue}) {
79
+ if (!setValue)
80
+ return <code className="word-break">{value}</code>
81
+ return <TextEditor value={value} setValue={setValue}/>
82
+ }
83
+
84
+ const editorMapping = {
85
+ topic: TopicEditor,
86
+ type: OperationTypeEditor,
87
+ account: AccountEditor,
88
+ source: AccountEditor,
89
+ destination: AccountEditor,
90
+ asset: AssetEditor,
91
+ src_asset: AssetEditor,
92
+ dest_asset: AssetEditor,
93
+ from: TimestampEditor,
94
+ to: TimestampEditor,
95
+ memo: MemoEditor,
96
+ offer: OfferIdEditor,
97
+ pool: LiquidityPoolEditor
98
+ }
99
+
100
+ export function resolveFilterEditor(field) {
101
+ return editorMapping[field]
102
+ }
@@ -0,0 +1,218 @@
1
+ import React, {useCallback, useEffect, useState} from 'react'
2
+ import {parseQuery} from '@stellar-expert/navigation'
3
+ import deepmerge from 'deepmerge'
4
+ import {Dropdown} from '../controls/dropdown'
5
+ import {resolveFilterEditor} from './filter-editors'
6
+ import './filter.scss'
7
+
8
+ let fieldDescriptionMapping = {}
9
+
10
+ function FilterCondition({field, value, setValue, removeFilter, edit}) {
11
+ const updateValue = useCallback(function (value) {
12
+ setValue(field, value)
13
+ }, [field, setValue])
14
+
15
+ const removeValue = useCallback(function () {
16
+ removeFilter(field, value)
17
+ }, [field, value, removeFilter])
18
+
19
+ const childProps = {value, edit}
20
+ const filter = fieldDescriptionMapping[field]
21
+ if (edit || filter.multi === false) {
22
+ childProps.setValue = updateValue
23
+ }
24
+
25
+ const editor = resolveFilterEditor(field)
26
+ const title = !edit ? '' : (field === 'type' ? '' : filter.title)
27
+ return <span className="filter-condition condensed" title={edit ? '' : filter.description}>
28
+ <span className={'icon-' + filter.icon}/>
29
+ {title} {React.createElement(editor, childProps)}
30
+ {removeFilter ? <a href="#" className="icon-delete-circle" onClick={removeValue} title="Remove filter"/> : <>&emsp;</>}
31
+ </span>
32
+ }
33
+ /**
34
+ * Parses and validates filter parameters from the current URL query string
35
+ * @return {object}
36
+ */
37
+ export function parseFiltersFromQuery() {
38
+ const params = parseQuery()
39
+ const filters = {}
40
+ let isEmpty = true
41
+ for (const [key, value] of Object.entries(params)) {
42
+ const filterDescriptor = fieldDescriptionMapping[key]
43
+ if (!filterDescriptor)
44
+ continue //skip unrelated query parameters
45
+ filters[key] = value
46
+ isEmpty = false
47
+ }
48
+ return filters
49
+ }
50
+
51
+ function FiltersGroup({filters, replaceFilter, removeFilter, edit = false}) {
52
+ if (!filters)
53
+ return null
54
+ return <>{Object.entries(filters).map(([field, values]) => {
55
+ if (!(values instanceof Array)) {
56
+ return <FilterCondition key={field} field={field} value={values} edit={edit} setValue={replaceFilter}
57
+ removeFilter={removeFilter}/>
58
+ }
59
+ if (!values.length)
60
+ return null
61
+ return <React.Fragment key={field}>
62
+ {values.map(value => <FilterCondition key={value} field={field} value={value} edit={edit}
63
+ setValue={replaceFilter} removeFilter={removeFilter}/>)}
64
+ </React.Fragment>
65
+ })}</>
66
+ }
67
+
68
+ /**
69
+ * A component for managing search filters
70
+ * @param {object} [presetFilter] - Initial base filters
71
+ * @param {object} [fields] - Custom filter field definitions
72
+ * @param {func} [onChange] - Callback triggered when filters change
73
+ */
74
+ export function FilterView({presetFilter, fields = {}, onChange}) {
75
+ fieldDescriptionMapping = fields
76
+ const [filters, setFilters] = useState(presetFilter || {})
77
+ const [_, setSerializedFilter] = useState(JSON.stringify(filters))
78
+
79
+ const availableFields = []
80
+ const readyFilters = {}
81
+ const editFilters = {}
82
+ let editorMode = false
83
+ for (const [field, props = {}] of Object.entries(fieldDescriptionMapping)) {
84
+ const values = filters[field]
85
+ if (values !== undefined) {
86
+ if (values instanceof Array) {
87
+ const newValues = readyFilters[field] = []
88
+ for (let i = 0; i < values.length; i++) {
89
+ const value = values[i]
90
+ if (!value) {
91
+ editFilters[field] = [value]
92
+ editorMode = true
93
+ } else if (!presetFilter || !presetFilter[field]?.includes(value)) {
94
+ newValues.push(value)
95
+ }
96
+ }
97
+ } else {
98
+ if (!values) {
99
+ editFilters[field] = values
100
+ editorMode = true
101
+ } else {
102
+ readyFilters[field] = values
103
+ }
104
+ }
105
+ }
106
+ if (props.multi !== false || !values) {
107
+ availableFields.push({
108
+ value: field,
109
+ title: props.description,
110
+ icon: 'icon-' + props.icon
111
+ })
112
+ }
113
+ }
114
+
115
+ const updateExternalFilters = useCallback(function (newFilters) {
116
+ setSerializedFilter(prev => {
117
+ const newValue = JSON.stringify(newFilters)
118
+ if (prev !== newValue) {
119
+ setTimeout(() => onChange(deepmerge(presetFilter, newFilters)), 100)
120
+ return newValue
121
+ }
122
+ return prev
123
+ })
124
+ }, [onChange])
125
+
126
+ useEffect(() => {
127
+ const queryParams = parseFiltersFromQuery()
128
+ setFilters(queryParams)
129
+ updateExternalFilters(queryParams)
130
+ }, [])
131
+
132
+
133
+ const replaceFilter = useCallback((field, value) => setTimeout(() => setFilters(prev => {
134
+ const newFilters = {...prev}
135
+ const filter = fieldDescriptionMapping[field] || {}
136
+ //atomic values
137
+ if (filter.multi === false) {
138
+ if (value === null) {
139
+ delete newFilters[field]
140
+ updateExternalFilters(newFilters)
141
+ } else {
142
+ newFilters[field] = value
143
+ if (value) {
144
+ updateExternalFilters(newFilters)
145
+ }
146
+ }
147
+ return newFilters
148
+ }
149
+ //multi-values
150
+ let values = newFilters[field]
151
+ if (!values) {
152
+ values = newFilters[field] = []
153
+ }
154
+ if (value !== null) {
155
+ if (!values.includes(value)) {
156
+ if (values[values.length - 1] === '') {
157
+ values.pop()
158
+ }
159
+ values.push(value)
160
+ if (value) {
161
+ updateExternalFilters(newFilters)
162
+ }
163
+ }
164
+ } else {
165
+ const idx = values.indexOf('')
166
+ if (idx >= 0) {
167
+ values.splice(idx, 1)
168
+ updateExternalFilters(newFilters)
169
+ }
170
+ }
171
+ return newFilters
172
+ }), 100), [updateExternalFilters])
173
+
174
+ const removeFilter = useCallback((field, value) => setFilters(prev => {
175
+ const newFilters = {...prev}
176
+ const filter = fieldDescriptionMapping[field] || {}
177
+ //atomic value
178
+ if (filter.multi === false) {
179
+ if (newFilters[field] || newFilters[field] === '') {
180
+ newFilters[field] = undefined
181
+ updateExternalFilters(newFilters)
182
+ }
183
+ return newFilters
184
+ }
185
+ //multi-values
186
+ const values = newFilters[field]
187
+ const idx = values.indexOf(value)
188
+ if (idx >= 0) {
189
+ values.splice(idx, 1)
190
+ updateExternalFilters(newFilters)
191
+ }
192
+ return newFilters
193
+ }), [updateExternalFilters])
194
+
195
+ const addFilter = useCallback(field => {
196
+ replaceFilter(field, '')
197
+ }, [replaceFilter])
198
+
199
+ const title = <span className="nowrap"><span className="icon icon-add-circle"/>Add filter</span>
200
+
201
+ return <div className="op-filters">
202
+ <div className="mobile-only micro-space"/>
203
+ <span className="icon-filter"/>&nbsp;Filters&emsp;
204
+ <FiltersGroup filters={presetFilter}/>
205
+ <FiltersGroup filters={readyFilters} replaceFilter={replaceFilter} removeFilter={removeFilter}/>
206
+ {!editorMode ?
207
+ <Dropdown title={title} options={availableFields} onChange={addFilter}/> :
208
+ <div className="micro-space">
209
+ <FiltersGroup filters={editFilters} replaceFilter={replaceFilter} removeFilter={removeFilter} edit/>
210
+ </div>}
211
+ <div className="micro-space"/>
212
+ </div>
213
+ }
214
+
215
+ /**
216
+ * @callback FilterView~OnChange
217
+ * @param {Object} mergedFilters - The combination of preset filters and newly selected values.
218
+ */
@@ -0,0 +1,57 @@
1
+ .op-filters {
2
+ .icon-delete-circle {
3
+ margin-left: 0.1em;
4
+ }
5
+ }
6
+
7
+ .op-filter-container {
8
+ display: flex;
9
+ justify-content: flex-start;
10
+ background-color: var(--color-bg);
11
+ border: 1px solid var(--color-contrast-border);
12
+ border-radius: $border-radius-input;
13
+ color: var(--color-text);
14
+ height: 4rem;
15
+ padding: 0.2rem 1rem;
16
+ width: 100%;
17
+
18
+ &:focus {
19
+ border-color: var(--color-highlight);
20
+ }
21
+ }
22
+
23
+ .filter-condition {
24
+ display: inline-block;
25
+ margin-right: 0.4em;
26
+ cursor: pointer;
27
+
28
+ &:before {
29
+ content: '/';
30
+ color: var(--color-highlight);
31
+ }
32
+
33
+ .icon-delete-circle {
34
+ opacity: 0;
35
+ }
36
+
37
+ &:hover .icon-delete-circle {
38
+ opacity: 1;
39
+ }
40
+
41
+ > input {
42
+ margin-bottom: 0;
43
+ height: 3rem;
44
+ padding: 0 0.4rem;
45
+
46
+ + .icon-ok {
47
+ display: inline-block;
48
+ visibility: hidden;
49
+ transition: visibility 0.3s;
50
+ }
51
+
52
+ &:focus + .icon-ok {
53
+ visibility: visible;
54
+ margin-left: 0.2em;
55
+ }
56
+ }
57
+ }
package/index.d.ts CHANGED
@@ -1179,6 +1179,36 @@ export const ScValStruct: React.FC<ScValStructProps>;
1179
1179
  /** Set of primitive ScVal type identifiers */
1180
1180
  export const primitiveTypes: Set<string>;
1181
1181
 
1182
+ // ============================================================================
1183
+ // Filters
1184
+ // ============================================================================
1185
+
1186
+ export interface FilterFieldDescriptor {
1187
+ /** Icon CSS class suffix (prepended with "icon-") */
1188
+ icon: string;
1189
+ /** Short title displayed in the filter condition */
1190
+ title: string;
1191
+ /** Description shown in the "Add filter" dropdown and as tooltip */
1192
+ description: string;
1193
+ /** Whether multiple values are allowed for this field (default true) */
1194
+ multi?: boolean;
1195
+ }
1196
+
1197
+ export interface FilterViewProps {
1198
+ /** Initial base filters that are always applied */
1199
+ presetFilter?: Record<string, any>;
1200
+ /** Filter field definitions keyed by field name */
1201
+ fields?: Record<string, FilterFieldDescriptor>;
1202
+ /** Callback triggered when filters change, receives merged preset + user filters */
1203
+ onChange?: (mergedFilters: Record<string, any>) => void;
1204
+ }
1205
+
1206
+ /** Composable filter panel for managing search filters with URL query sync */
1207
+ export function FilterView(props: FilterViewProps): React.ReactElement;
1208
+
1209
+ /** Parse and validate filter parameters from the current URL query string */
1210
+ export function parseFiltersFromQuery(): Record<string, any>;
1211
+
1182
1212
  // ============================================================================
1183
1213
  // Stellar Utilities
1184
1214
  // ============================================================================
@@ -1208,7 +1238,7 @@ export function signatureHintToMask(hint: Buffer): string;
1208
1238
  export function formatSignatureHint(hint: Buffer): string;
1209
1239
 
1210
1240
  /** Check if a hint matches a specific key (note: function name has a typo) */
1211
- export function singatureHintMatchesKey(hint: Buffer, key: string): boolean;
1241
+ export function signatureHintMatchesKey(hint: Buffer, key: string): boolean;
1212
1242
 
1213
1243
  /** Find a key matching a signature hint */
1214
1244
  export function findKeyBySignatureHint(hint: Buffer, allKeys: string[]): string | null;
package/index.js CHANGED
@@ -1,93 +1,95 @@
1
- import './index.scss'
2
-
3
- window.explorerFrontendOrigin = window.explorerFrontendOrigin || 'https://stellar.expert'
4
- window.explorerApiOrigin = window.explorerApiOrigin || 'https://api.stellar.expert'
5
- window.horizonOrigin = window.horizonOrigin || 'https://horizon.stellar.org'
6
-
7
- //modules
8
- export * from './module/dynamic-module'
9
- //state management and utils
10
- export * from './state/state-hooks'
11
- export * from './state/on-screen-hooks'
12
- export * from './state/stellar-network-hooks'
13
- export * from './state/screen-orientation-hooks'
14
- export * from './state/page-visibility-helpers'
15
- export * from './state/theme'
16
- export * from './meta/page-meta-tags'
17
- //explorer API bindings
18
- export * from './api/explorer-api-hooks'
19
- export * from './api/explorer-api-paginated-list-hooks'
20
- export * from './api/explorer-tx-api'
21
- export * from './api/explorer-batch-info-loader'
22
- export * from './api/ledger-stream'
23
- //Horizon API binding and utils
24
- export * from './stellar/ledger-generic-id'
25
- export * from './horizon/horizon-client-helpers'
26
- export * from './horizon/horizon-ledger-helpers'
27
- export * from './horizon/horizon-transaction-helpers'
28
- export * from './horizon/horizon-account-helpers'
29
- export * from './horizon/horizon-orderbook-helpers'
30
- export * from './horizon/horizon-trades-helper'
31
- //basic UI controls
32
- export * from './controls/button'
33
- export * from './controls/button-group'
34
- export * from './controls/info-tooltip'
35
- export * from './controls/tooltip'
36
- export * from './controls/update-highlighter'
37
- export * from './controls/tabs'
38
- export * from './controls/dropdown'
39
- export * from './controls/code-block'
40
- export * from './controls/slider'
41
- export * from './controls/external-link'
42
- export * from './toast/toast-notifications-block'
43
- export * from './errors/error-boundary'
44
- //interaction
45
- export * from './interaction/autofocus'
46
- export * from './interaction/block-select'
47
- export * from './interaction/copy-to-clipboard'
48
- export * from './interaction/spoiler'
49
- export * from './interaction/accordion'
50
- export * from './interaction/theme-selector'
51
- export * from './interaction/inline-progress'
52
- export * from './interaction/responsive'
53
- export * from './interaction/qr-code'
54
- export * from './interaction/dialog'
55
- export * from './interaction/system-dialog'
56
- //date components
57
- export * from './date/utc-timestamp'
58
- export * from './date/elapsed-time'
59
- export * from './date/date-selector'
60
- //ledger-entries-related components
61
- export * from './ledger/ledger-entry-link'
62
- export * from './ledger/ledger-entry-href-formatter'
63
- export * from './ledger/ledger-info-parser'
64
- //account-related components
65
- export * from './account/identicon'
66
- export * from './account/account-address'
67
- export * from './account/signer-key'
68
- export * from './account/available-balance'
69
- //asset-related components
70
- export * from './asset/asset-link'
71
- export * from './asset/asset-issuer'
72
- export * from './asset/asset-icon'
73
- export * from './asset/asset-selector'
74
- export * from './asset/amount'
75
- export * from './asset/asset-meta-hooks'
76
- export * from './asset/asset-list-hooks'
77
- //claimable-balance-related components
78
- export * from './claimable-balance/claimable-balance-claimants'
79
- //DEX-related components
80
- export * from './dex/price-dynamic'
81
- //directory-related components
82
- export * from './directory/directory-hooks'
83
- //transaction/operation/effects components
84
- export * from './tx/tx-operations-list'
85
- export * from './tx/parser/tx-details-parser'
86
- export * from './tx/tx-list-hooks'
87
- export * from './effect/effect-description'
88
- //contract-related components
89
- export * from './contract/contract-api'
90
- //Stellar-specific utils
91
- export * from './stellar/key-type'
92
- export * from './stellar/signature-hint-utils'
93
- export * from './contract/sc-val'
1
+ import './index.scss'
2
+
3
+ window.explorerFrontendOrigin = window.explorerFrontendOrigin || 'https://stellar.expert'
4
+ window.explorerApiOrigin = window.explorerApiOrigin || 'https://api.stellar.expert'
5
+ window.horizonOrigin = window.horizonOrigin || 'https://horizon.stellar.org'
6
+
7
+ //modules
8
+ export * from './module/dynamic-module'
9
+ //state management and utils
10
+ export * from './state/state-hooks'
11
+ export * from './state/on-screen-hooks'
12
+ export * from './state/stellar-network-hooks'
13
+ export * from './state/screen-orientation-hooks'
14
+ export * from './state/page-visibility-helpers'
15
+ export * from './state/theme'
16
+ export * from './meta/page-meta-tags'
17
+ //explorer API bindings
18
+ export * from './api/explorer-api-hooks'
19
+ export * from './api/explorer-api-paginated-list-hooks'
20
+ export * from './api/explorer-tx-api'
21
+ export * from './api/explorer-batch-info-loader'
22
+ export * from './api/ledger-stream'
23
+ //Horizon API binding and utils
24
+ export * from './stellar/ledger-generic-id'
25
+ export * from './horizon/horizon-client-helpers'
26
+ export * from './horizon/horizon-ledger-helpers'
27
+ export * from './horizon/horizon-transaction-helpers'
28
+ export * from './horizon/horizon-account-helpers'
29
+ export * from './horizon/horizon-orderbook-helpers'
30
+ export * from './horizon/horizon-trades-helper'
31
+ //basic UI controls
32
+ export * from './controls/button'
33
+ export * from './controls/button-group'
34
+ export * from './controls/info-tooltip'
35
+ export * from './controls/tooltip'
36
+ export * from './controls/update-highlighter'
37
+ export * from './controls/tabs'
38
+ export * from './controls/dropdown'
39
+ export * from './controls/code-block'
40
+ export * from './controls/slider'
41
+ export * from './controls/external-link'
42
+ export * from './toast/toast-notifications-block'
43
+ export * from './errors/error-boundary'
44
+ //interaction
45
+ export * from './interaction/autofocus'
46
+ export * from './interaction/block-select'
47
+ export * from './interaction/copy-to-clipboard'
48
+ export * from './interaction/spoiler'
49
+ export * from './interaction/accordion'
50
+ export * from './interaction/theme-selector'
51
+ export * from './interaction/inline-progress'
52
+ export * from './interaction/responsive'
53
+ export * from './interaction/qr-code'
54
+ export * from './interaction/dialog'
55
+ export * from './interaction/system-dialog'
56
+ //date components
57
+ export * from './date/utc-timestamp'
58
+ export * from './date/elapsed-time'
59
+ export * from './date/date-selector'
60
+ //ledger-entries-related components
61
+ export * from './ledger/ledger-entry-link'
62
+ export * from './ledger/ledger-entry-href-formatter'
63
+ export * from './ledger/ledger-info-parser'
64
+ //account-related components
65
+ export * from './account/identicon'
66
+ export * from './account/account-address'
67
+ export * from './account/signer-key'
68
+ export * from './account/available-balance'
69
+ //asset-related components
70
+ export * from './asset/asset-link'
71
+ export * from './asset/asset-issuer'
72
+ export * from './asset/asset-icon'
73
+ export * from './asset/asset-selector'
74
+ export * from './asset/amount'
75
+ export * from './asset/asset-meta-hooks'
76
+ export * from './asset/asset-list-hooks'
77
+ //claimable-balance-related components
78
+ export * from './claimable-balance/claimable-balance-claimants'
79
+ //DEX-related components
80
+ export * from './dex/price-dynamic'
81
+ //directory-related components
82
+ export * from './directory/directory-hooks'
83
+ //transaction/operation/effects components
84
+ export * from './tx/tx-operations-list'
85
+ export * from './tx/parser/tx-details-parser'
86
+ export * from './tx/tx-list-hooks'
87
+ export * from './effect/effect-description'
88
+ //contract-related components
89
+ export * from './contract/contract-api'
90
+ //filter component
91
+ export * from './filter/filter-view'
92
+ //Stellar-specific utils
93
+ export * from './stellar/key-type'
94
+ export * from './stellar/signature-hint-utils'
95
+ export * from './contract/sc-val'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stellar-expert/ui-framework",
3
- "version": "1.17.0",
3
+ "version": "1.19.0",
4
4
  "description": "StellarExpert shared UI components library",
5
5
  "main": "index.js",
6
6
  "module": "./index.js",
@@ -15,8 +15,8 @@
15
15
  "author": "orbitlens <orbit@stellar.expert>",
16
16
  "license": "MIT",
17
17
  "peerDependencies": {
18
- "@stellar/stellar-base": "^14.0.0",
19
- "@stellar/stellar-sdk": "^14.0.0",
18
+ "@stellar/stellar-base": "^15.0.0",
19
+ "@stellar/stellar-sdk": "^15.0.1",
20
20
  "@stellar-expert/asset-descriptor": "^1.5.0",
21
21
  "@stellar-expert/claimable-balance-utils": "^1.4.1",
22
22
  "@stellar-expert/client-cache": "^1.1.0",
@@ -25,10 +25,11 @@
25
25
  "@stellar-expert/navigation": "^1.1.0",
26
26
  "@stellar-expert/tx-meta-effects-parser": "^8.3.3",
27
27
  "classnames": ">=2",
28
+ "deepmerge": "^4.3.1",
28
29
  "prop-types": ">=15",
29
30
  "qrcode.react": "^4.2.0",
30
- "react": ">=17",
31
- "react-dom": ">=17",
31
+ "react": ">=17 <18.0.0",
32
+ "react-dom": ">=17 <18.0.0",
32
33
  "react-fast-compare": ">=3",
33
34
  "throttle-debounce": ">=5.0.0"
34
35
  },
@@ -37,5 +38,34 @@
37
38
  "history": "^4.10.1",
38
39
  "react-copy-to-clipboard": "^5.1.0",
39
40
  "react-timeago": "^7.2.0"
40
- }
41
+ },
42
+ "files": [
43
+ "index.js",
44
+ "index.d.ts",
45
+ "index.scss",
46
+ ".npmrc",
47
+ "account/*",
48
+ "api/*",
49
+ "asset/*",
50
+ "basic-styles/*",
51
+ "claimable-balance/*",
52
+ "contract/*",
53
+ "controls/*",
54
+ "date/*",
55
+ "dex/*",
56
+ "directory/*",
57
+ "effect/*",
58
+ "errors/*",
59
+ "filter/*",
60
+ "fonts/*",
61
+ "horizon/*",
62
+ "interaction/*",
63
+ "ledger/*",
64
+ "meta/*",
65
+ "module/*",
66
+ "state/*",
67
+ "stellar/*",
68
+ "toast/*",
69
+ "tx/*"
70
+ ]
41
71
  }
@@ -28,7 +28,7 @@ export function formatSignatureHint(hint) {
28
28
  * @param {string} key - Key to compare
29
29
  * @return {boolean}
30
30
  */
31
- export function singatureHintMatchesKey(hint, key) {
31
+ export function signatureHintMatchesKey(hint, key) {
32
32
  return signatureHintToMask(hint).substring(47, 52) === key.substring(47, 52)
33
33
  }
34
34
 
@@ -39,7 +39,7 @@ export function singatureHintMatchesKey(hint, key) {
39
39
  * @return {string|null}
40
40
  */
41
41
  export function findKeyBySignatureHint(hint, allKeys) {
42
- return allKeys.find(key => singatureHintMatchesKey(hint, key))
42
+ return allKeys.find(key => signatureHintMatchesKey(hint, key))
43
43
  }
44
44
 
45
45
  /**
@@ -49,7 +49,7 @@ export function findKeyBySignatureHint(hint, allKeys) {
49
49
  * @returns {Signature}
50
50
  */
51
51
  export function findSignatureByKey(key, allSignatures = []) {
52
- return allSignatures.find(sig => singatureHintMatchesKey(sig.hint(), key))
52
+ return allSignatures.find(sig => signatureHintMatchesKey(sig.hint(), key))
53
53
  }
54
54
 
55
55
  /**
package/CLAUDE.md DELETED
@@ -1,35 +0,0 @@
1
- # CLAUDE.md
2
-
3
- This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
-
5
- ## Project Overview
6
-
7
- `@stellar-expert/ui-framework` is a shared React UI component library for the StellarExpert Stellar blockchain explorer. It is consumed as an npm package by other StellarExpert applications — it is not a standalone app.
8
-
9
- ## Development Setup
10
-
11
- - **Package manager:** PNPM (configured via `.npmrc` with `auto-install-peers=true`)
12
- - **Install dependencies:** `pnpm install`
13
- - **No build step:** Components are raw ES modules consumed directly via imports. There is no bundler, transpiler, or build script in this repo.
14
- - **No test suite:** No testing framework is configured.
15
-
16
- ## Architecture
17
-
18
- ### Module Organization
19
-
20
- Each feature domain lives in its own top-level directory (e.g., `account/`, `asset/`, `contract/`, `controls/`). The main entry point `index.js` re-exports everything and also sets global defaults (`window.explorerFrontendOrigin`, `window.explorerApiOrigin`, `window.horizonOrigin`).
21
-
22
- ### Key Patterns
23
-
24
- - **Functional React components** with `React.memo` for optimization and `PropTypes` for runtime type checking (no TypeScript).
25
- - **Custom state hooks** instead of Redux/MobX — see `state/state-hooks.js` for `useDependantState` (auto-reinits on deep-compared dependency changes), `useForceUpdate`, and `useDeepEffect`.
26
- - **SCSS styling** with CSS custom properties for theming. Global styles imported via `index.scss`. Theme system supports `day`/`night` modes via `data-theme` attribute on `<html>`. SCSS files are marked as side effects in `package.json`.
27
- - **API layer** in `api/` uses custom hooks (`useExplorerApi`) with built-in caching, auto-refresh on visibility, and Stellar network awareness via `useStellarNetwork`.
28
- - **Peer dependencies** are critical — the consuming app must provide React, Stellar SDK, and the `@stellar-expert/*` utility packages listed in `package.json`.
29
-
30
- ### Global Configuration
31
-
32
- The library expects these globals (with defaults set in `index.js`):
33
- - `window.explorerFrontendOrigin` — StellarExpert frontend URL
34
- - `window.explorerApiOrigin` — StellarExpert API URL
35
- - `window.horizonOrigin` — Stellar Horizon API URL