|
1 | 1 | <script lang="ts"> |
2 | | - import type { ChartPairs, TradingPairs } from 'trade-executor/schemas/chart'; |
| 2 | + import type { ChartKind, ChartPairs, TradingPairs } from 'trade-executor/schemas/chart'; |
3 | 3 | import { slide } from 'svelte/transition'; |
4 | 4 | import fsm from 'svelte-fsm'; |
5 | 5 | import Button from '$lib/components/Button.svelte'; |
| 6 | + import TextInput from '$lib/components/TextInput.svelte'; |
6 | 7 |
|
7 | 8 | interface Props { |
8 | 9 | selectedPairIds: number[]; |
9 | 10 | tradingPairs: ChartPairs; |
10 | | - disabled?: boolean; |
| 11 | + chartKind: ChartKind | undefined; |
11 | 12 | onchange?: (ids: number[]) => void; |
12 | 13 | } |
13 | 14 |
|
14 | | - let { selectedPairIds, tradingPairs, disabled = false, onchange }: Props = $props(); |
| 15 | + let { selectedPairIds, tradingPairs, chartKind, onchange }: Props = $props(); |
| 16 | +
|
| 17 | + let singlePair = $derived(chartKind?.includes('_single_')); |
| 18 | + let multiPair = $derived(chartKind?.includes('_multi_')); |
| 19 | + let disabled = $derived(!(singlePair || multiPair)); |
15 | 20 |
|
16 | 21 | // selected pair ids during editing, prior to committing (save) or reverting (cancel) |
17 | | - let provisionalPairIds = $state(selectedPairIds); |
| 22 | + let multiPairIds = $derived(selectedPairIds); |
| 23 | + let singlePairId = $derived(selectedPairIds[0]); |
18 | 24 |
|
19 | 25 | let provisionalPairs = $derived( |
20 | 26 | tradingPairs.all_pairs.filter((p) => { |
21 | | - return provisionalPairIds.includes(p.internal_id!); |
| 27 | + if (singlePair) { |
| 28 | + return p.internal_id! === singlePairId; |
| 29 | + } |
| 30 | + return multiPairIds.includes(p.internal_id!); |
22 | 31 | }) |
23 | 32 | ); |
24 | 33 |
|
| 34 | + let search = $state(''); |
| 35 | +
|
25 | 36 | const pairSelector = fsm('ready', { |
26 | 37 | ready: { |
27 | | - edit: 'editing' |
| 38 | + _enter() { |
| 39 | + search = ''; |
| 40 | + }, |
| 41 | +
|
| 42 | + edit() { |
| 43 | + return singlePair ? 'editingSingle' : 'editingMulti'; |
| 44 | + } |
| 45 | + }, |
| 46 | +
|
| 47 | + editingSingle: { |
| 48 | + select(pairs: TradingPairs) { |
| 49 | + singlePairId = pairs[0]?.internal_id; |
| 50 | + }, |
| 51 | +
|
| 52 | + save() { |
| 53 | + onchange?.([singlePairId]); |
| 54 | + return 'ready'; |
| 55 | + }, |
| 56 | +
|
| 57 | + cancel() { |
| 58 | + singlePairId = selectedPairIds[0]; |
| 59 | + return 'ready'; |
| 60 | + } |
28 | 61 | }, |
29 | 62 |
|
30 | | - editing: { |
| 63 | + editingMulti: { |
31 | 64 | select(pairs: TradingPairs) { |
32 | | - provisionalPairIds = pairs.map((p) => p.internal_id!); |
| 65 | + multiPairIds = pairs.map((p) => p.internal_id!); |
33 | 66 | }, |
34 | 67 |
|
35 | 68 | save() { |
36 | | - onchange?.(provisionalPairIds); |
| 69 | + onchange?.(multiPairIds); |
37 | 70 | return 'ready'; |
38 | 71 | }, |
39 | 72 |
|
40 | 73 | cancel() { |
41 | | - provisionalPairIds = selectedPairIds; |
| 74 | + multiPairIds = selectedPairIds; |
42 | 75 | return 'ready'; |
43 | 76 | } |
44 | 77 | } |
45 | 78 | }); |
46 | 79 |
|
47 | | - let editing = $derived($pairSelector === 'editing'); |
| 80 | + let editing = $derived($pairSelector.startsWith('editing')); |
48 | 81 | </script> |
49 | 82 |
|
50 | 83 | <div class="pairs-selector"> |
51 | 84 | <label class={['current-selection', editing && 'editing', disabled && 'disabled']}> |
52 | | - <span class="title">Pairs:</span> |
| 85 | + <span class="title"> |
| 86 | + {singlePair ? 'Pair' : 'Pairs'}: |
| 87 | + </span> |
53 | 88 | <span class="selected-pairs"> |
54 | | - {#if tradingPairs.all_pairs.length === 0} |
| 89 | + {#if disabled} |
| 90 | + No pairs required |
| 91 | + {:else if tradingPairs.all_pairs.length === 0} |
55 | 92 | No pairs loaded |
56 | 93 | {:else if provisionalPairs.length === 0} |
57 | 94 | No pairs selected |
|
67 | 104 | <div class="inner"> |
68 | 105 | <header> |
69 | 106 | <h4>Select pairs</h4> |
70 | | - <div class="button-group"> |
| 107 | + <div class="button-group quick-select"> |
71 | 108 | <Button size="xs" tertiary on:click={() => pairSelector.select(tradingPairs.default_pairs)}>Default</Button> |
72 | | - <Button size="xs" tertiary on:click={() => pairSelector.select(tradingPairs.all_pairs)}>All</Button> |
| 109 | + {#if multiPair} |
| 110 | + <Button size="xs" tertiary on:click={() => pairSelector.select(tradingPairs.all_pairs)}>All</Button> |
| 111 | + {/if} |
73 | 112 | <Button size="xs" tertiary on:click={() => pairSelector.select([])}>None</Button> |
74 | 113 | </div> |
75 | | - <div class="button-group"> |
| 114 | + <div class="search"> |
| 115 | + <TextInput bind:value={search} type="search" size="sm" placeholder="Search" /> |
| 116 | + </div> |
| 117 | + <div class="button-group primary-controls"> |
76 | 118 | <Button size="xs" ghost on:click={pairSelector.cancel}>Cancel</Button> |
77 | | - <Button size="xs" secondary on:click={pairSelector.save}>Save</Button> |
| 119 | + <Button size="xs" secondary disabled={multiPairIds.length === 0} on:click={pairSelector.save}>Save</Button> |
78 | 120 | </div> |
79 | 121 | </header> |
80 | 122 | <div class="pairs"> |
81 | 123 | {#each tradingPairs.all_pairs as pair (pair.internal_id)} |
82 | | - <label> |
83 | | - <input type="checkbox" value={pair.internal_id} bind:group={provisionalPairIds} /> |
| 124 | + <label hidden={!pair.symbol.toLowerCase().includes(search.toLowerCase())}> |
| 125 | + {#if singlePair} |
| 126 | + <input type="radio" value={pair.internal_id} bind:group={singlePairId} /> |
| 127 | + {:else} |
| 128 | + <input type="checkbox" value={pair.internal_id} bind:group={multiPairIds} /> |
| 129 | + {/if} |
84 | 130 | {pair.symbol} |
85 | 131 | </label> |
86 | 132 | {/each} |
|
155 | 201 |
|
156 | 202 | header { |
157 | 203 | display: grid; |
158 | | - grid-template-columns: auto 1fr auto; |
159 | | - gap: 1.5rem; |
| 204 | + grid-template-columns: |
| 205 | + [title] auto |
| 206 | + [quick-select] 1fr |
| 207 | + [search] auto |
| 208 | + [primary-controls] auto; |
| 209 | + grid-template-rows: auto; |
| 210 | + gap: 0.75rem 1.25rem; |
160 | 211 | align-items: center; |
161 | 212 |
|
| 213 | + @media (--viewport-sm-down) { |
| 214 | + grid-template-columns: |
| 215 | + [title quick-select] auto |
| 216 | + [search primary-controls] 1fr; |
| 217 | + } |
| 218 | +
|
162 | 219 | h4 { |
| 220 | + grid-column-start: title; |
163 | 221 | font: var(--f-heading-xs-medium); |
| 222 | + white-space: nowrap; |
| 223 | + } |
| 224 | +
|
| 225 | + .search { |
| 226 | + grid-column-start: search; |
| 227 | + text-align: right; |
| 228 | + } |
| 229 | +
|
| 230 | + .quick-select { |
| 231 | + grid-column-start: quick-select; |
| 232 | + } |
| 233 | +
|
| 234 | + .primary-controls { |
| 235 | + grid-row-start: 1; |
| 236 | + grid-column-start: primary-controls; |
| 237 | + justify-content: end; |
164 | 238 | } |
165 | 239 |
|
166 | 240 | .button-group { |
|
188 | 262 | &:hover { |
189 | 263 | background: var(--c-box-2); |
190 | 264 | } |
| 265 | +
|
| 266 | + &[hidden] { |
| 267 | + display: none; |
| 268 | + } |
191 | 269 | } |
192 | 270 | } |
193 | 271 | } |
|
0 commit comments