Skip to content

Commit

Permalink
feat: improve orderbook order card (#556)
Browse files Browse the repository at this point in the history
* feat: move Swap/Limit and Buy/Sell controls to radio group controls

* feat: remove "HALF" wallet amount shortcut button from token input

* feat: allow custom header text on token input

* feat: allow token change to be optional on token input

* feat: replace token amount with amountIn amountOut input groups

* feat: allow maxValue=0 to remove "MAX" shortcut button on token input

* feat: improve estimated trade price

* feat: improve estimated (validating) trade price part 2

* feat: enforce order type change with price tab type change

* feat: refine Expiration control setting in Limit Order card

* feat: reduce recomputation on expiration time changes

* feat: put Swap/Limit Buy/Sell buttons in card header

* Revert "feat: put Swap/Limit Buy/Sell buttons in card header"

This reverts commit d31a947.

* feat: focus on input control when click input group

* feat: remove less useful simulation result information

* fix: initial state then type amountOut fix, price was 0

* feat: make easier to focus on text inputs from anywhere in group

* feat: add basic styling of limit price input

* feat: refine design of Expiry control to single click to custom

* feat: enable RadioButtonGroup to skip buttons of no description

* feat: add LimitPrice input, connected to all other input fields

* feat: remove Swap/Limit buttons to make card header more compact

* Revert "feat: remove Swap/Limit buttons to make card header more compact"

This reverts commit 49fa8cb.

* feat: control LimitPrice visibility with Swap tab only

* feat: put Swap/Limit Buy/Sell buttons in card header

* feat: stop LimitOrder card from scrolling

* refactor: remove grid layout from TokenInputGroup

* refactor: don't redefine border width and style twice

* fix: align header text and header button text using extra margins

* feat: allow extra subheader fields to TokenInputGroup

* feat: display error state if user has insufficient funds for order

* feat: display zero balance to explain why there is no trade preview

* fix: prevent estimated trades from going above user's input balance:

    - this can be a jarring experience because the estimated trade
      exists temporarily and will be corrected with "simulate" results
    - when this value goes outside the user's balance it triggers
      displayed error states, so an estimate outside the bounds
      usually appears as a temporary flash of an error state
      before the simulated response is seen

* fix: make order confirmation button state based on simulation result

    - the button should be enabled if the simulation result is of the
      requested user input (even if the result is refreshing due to
      liquidity state changes)
    - the button should be disabled if the simulation result is not of
      the requested user input, because the estimated result may be
      very inaccurate (though hopefully it is not)

* feat: add inactive state to token input to indicate validating results

* feat: improve unconnected wallet state

* fix: improve isLoading state:

    - isPending stays true when react-query request is not enabled
    - leaving an unconnected wallet in isPending but !isLoading state

* fix: estimation limit should be in base denom not display denom

* feat: make chart a dynamic height

* feat: make chart row wider if possible

* feat: create space for a chart-depth connection element

* feat: use custom colors on TradingView chart

* feat: decouple chart from depth

* feat: add more chart resolutions, default to 5 minute wicks

* feat: hide the pools tab (but keep the page available)

    - the page is still accessible through other navigation elements

* Revert "feat: add more chart resolutions, default to 5 minute wicks"

This reverts commit f6fd635.

* feat: switch "Order" heading to "Trade"

* refactor: use NumberInput for limit order input:

    - remove previous "formatNumericAmount" that was only needed to
      add a non-numeric placeholder value in commit:
      52de5fc

* feat: add error style for "0" limit price

* feat: abstract out LimitPriceInput component

* fix: tabbing in TokenInputGroup where Tokens cannot be changed

* feat: allow input focus on LimitPrice group click

* fix: allow unrounded numbers in Custom time amount input

* feat: allow "Custom" PriceLimit shortcut button:

    - to make it obvious that the user can select a custom limit price

* feat: select all limit price input on "Custom" price limit shortcut

* feat: refine expiration time shortcuts

* feat: allow expiration time shortcut to prefill custom expiration time

* fix: change form values before animation makes the change visible

* feat: refine wording of "My Orders" table

* feat: make focusability of limit order inputs more visible:

    - add border color highlight when highlighted or focused

* Revert "Revert "feat: add more chart resolutions, default to 5 minute wicks""

This reverts commit c1d5a48.

* feat: add normal page-card background to OrderbookChart card

* feat: visually connect the chart and depth cards with background color

* feat: add order depth price indication tracking

* fix: higher prices should be at the top of the depth table

* feat: show priceIndication on depth table

* feat: add depth price indication line on depth table hover

* fix: Tab components were causing abrupt re-renders when not needed

* fix: ensure chart style overrides are applied properly

* feat: show Orderbook depth price indication on Orderbook chart

* fix: switch to useState for chart tracking: to be able to react to

* feat: connect chart and depth components by price

* fix: remove non-zero depth rows

* feat: add pixel to overlap depth table border

* feat: use a bezier curve to join lines

* feat: ease up on connection line sharpness

* feat: add connection area drawings

* feat: combine price indication and offset together

* feat: improve indexer to add each new update in onAccumulated metadata

* feat: add useBuckets hook for bucketing to a specific price resolution

* fix: remove unused bucket transition style calculations

* fix: chart-connector height should grow and shrink depending on its container

* fix: allow dynamic row count in Orderbook depth table

* fix: align the set price line from the Orderbook depth with connector

* refactor: make column widths easier to understand

* feat: use buckets hook for Orderbook depth table

* refactor: simplify PriceOffset type

* feat: expand ConnectionArea type

* fix: table row keys were inefficient or not present

* feat: add tracking for bucket price position offsets of Depth table

* fix: make prices explicit: price was being confused for displayPrice

* refactor: store Buckets in inner/outer bounds rather than lower/upper:

    - it can help keep the logic more directionless

* feat: draw bucket price connections

* feat: add depthPriceIndication hover state to bucket connections

* feat: draw liquidity and cumulative liquidity buckets in connector

* feat: add half-pixel indicator of liquidity

* feat: make conenction areas less distracting

* fix: prevent text-wrapping in Orderbook depth current price cell

* perf: don't require sharp points for fill area drawings

* perf: abstract out connection curve point math

* feat: add dynamic decimal places for Orderbook depth amount

* feat: add directional color to Orderbook depth

* refactor: move token valuation higher for better efficiency

* feat: add Orderbook depth colored cell backgrounds

* feat: limit area connectors to only visible Orderbook depth rows

* feat: refine bucket connection opacities

* perf: reduce number of chart liquidity buckets drawn

* feat: add bucket resolution control

* feat: set a reasonable first bucket resolution on first pair price

* fix: format price values correctly

* fix: allow undefined bucketResolution when liquidity is not yet loaded

* feat: control connection areas to display only with Depth table tab

* feat: add more room for depth chart

* feat: allow Trades list to display enough rows to scroll

* feat: balance red/green brightnesses better, align to text colors

* fix: flip red/green colors

* feat: add brightness corrected colors to connection areas

* feat: ensure depth numbers are drawn over background colors

* fix: draw depth price indication connection line with existing offsets

* refactor: abstract out depth price indication line creation

* refactor: name ConnectionLine better to represent what it looks like:

    - previously connectionPoint referred to a "y point" of the chart
      but this was inconsistent with the ConnectionArea type which
      was named after looking like an area shape

* feat: add active row styling for when current price row is hovered

* feat: remove price indication for current price on chart

    - it is redundant because the chart has a last price indicator

* feat: re-enable chart scrolling

* feat: allow chart axis updates to run faster when chart is focused

* feat: enable click to select limit price

* fix: track "hovered" style using current price indication:

    - so the state can be controlled by values from other components

* feat: allow setting price indication from LimitOrder form

* fix: RadioButtonGroupInput can send strings when numbers were expected

    - this is due to numbers changing to strings in the Record type

* feat: highlight top of current price bucket when trading in it

* fix: ensure active buckets are consistently calculated

* feat: allow toggling the display of the connector depth chart

* feat: add space for connector visualization only when control is active

* feat: add more options to connector width

* feat: refine colors and bucket widths to be less intrusive

* fix: draw connection lines over chart bucket areas

    - add duplicate to draw over connection areas and chart areas

* feat: make chart connector default width even smaller

* feat: make chart connector inidividual buckets width smaller

* feat: make cumulative depth indicating more subtle

* fix: alignment of connector side was off by border-width

* perf: only use sharp points for price chart axis connection, dimension

* feat: hide left toolbar of chart by default

* fix: remove price indication line when nothing to connect to

* feat: move connection button controls into the Depth table toolbar

* refactor: simplify filteredBuckets usage in depth table

* feat: add show Buy/Sell only buttons to Depth toolbar

* feat: balance red and greens more

* fix: prevent thrown exception when changing between chart timescales

* feat: fix possible large width of average price description

* fix: fix value-suffix spacing layout by combining to one inline-block

* feat: make Trade card even smaller: sized to header buttons

* fix: ensure that warnings and errors don't resize the card width

* feat: add depth reserves % bar indication

* fix: fix accidental mutation of stateful arrays

* fix: check chart state before doing some chart functions

* feat: add Depth table transition effects

* feat: add price-slippage indication to Orderbook chart

* fix: opacity admjustments should be applied just before drawing:

    - the connectionAreas was reused for the chartArea calculations

* feat: reduce component logic by asking chart to create own empty bars

* fix: reduce initial chart renderings on component start

* feat: make order line color white to match orderbook depth table

* feat: refine price connection area opacity

* feat: allow priceIndication to be 0 but disallow price to be 0

* Revert "feat: hide the pools tab (but keep the page available)"

This reverts commit 13a0886.

* fix: fix inconsistent calculation leading to max update depth error

* fix: all txs were showing in "My Orders" with unconnected wallets

* feat: add better empty My Orders table description

* feat: add empty My Orders table description for unconnected state

* fix: buy/sell percentage must take into account reserve prices

* fix: ensure undefined bucketOffsets are handled correctly

* refactor: make default undefined bucketOffset more visibly consistent

* fix: incorrect end price could sometimes be calculated

* refactor: combine two effects for setting price indiciation into one

* fix: limit market order should set limit at current market price

* fix: prevent slippage tolerance result from looking negative

* fix: remove connection lines/areas when chart is being removed

* feat: add empty data states for recent orders list

* feat: make trades list refresh occasionally

* feat: abstract useOnDexMsg to add general subscriptions to dex actions in tables

* feat: add click chart to set limit price

* fix: avoid chart re-rendering with cached chart pairs

* feat: remove tokenPairs dependency from Orderbook chart

* feat: flip or reset order trade state when changing tokens

* fix: change all usages of BigNumber .toPrecision method to .precision

    - .toPrecision can return scientitific notation strings which will
      not pass validation on some numeric input fields
    - not all need to be changed but its best to change all anyway

* fix: account=null and account=undefined react-queries were colliding

* fix: make zero-result hook behavior more obvious: allow null parameter

    - null parameter means to return an empty data set
    - a non-null parameter means to request a data set
    - hopefully this is clearer

* feat: ensure Expiry shortcuts are open for all limit orders:

    - not just GOOD_TIL_TIME but also GOOD_TIL_CANCELLED

* refactor: allow useTransactionTableData page state to be set by parent

* fix: use consistent end display price of txs calculations

* fix: use consistent end display price of txs calculations, part 2

* refactor: rename form slippage to include percentage indication

* fix: replace some tickIndex->basePrice with tickIndex->displayPrice

    - in the Orderbook mostly display tokens are expected
  • Loading branch information
dib542 authored Jun 5, 2024
1 parent f74db7a commit 45ad9c9
Show file tree
Hide file tree
Showing 41 changed files with 3,231 additions and 1,143 deletions.
4 changes: 4 additions & 0 deletions public/tradingview.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
:root {
--tv-color-platform-background: var(--page-card, hsl(212deg, 28%, 17%));
--tv-color-pane-background: var(--default, hsl(219deg, 40%, 11%));
}
25 changes: 20 additions & 5 deletions src/components/RadioButtonGroupInput/RadioButtonGroupInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,17 @@ function useSelectedButtonBackgroundMove<T extends string | number>(
if (movingButton && targetButton) {
movingButton.style.width = `${targetButton.offsetWidth}px`;
movingButton.style.left = `${targetButton.offsetLeft}px`;
if (newValue !== undefined) {
if (newValue !== undefined && movingButton.style.opacity !== '0') {
movingButton.classList.add('transition-ready');
} else {
movingButton?.classList.remove('transition-ready');
}
movingButton.style.opacity = '1';
}
// "remove" button if the target was not found
else if (movingButton) {
movingButton?.classList.remove('transition-ready');
movingButton.style.opacity = '0';
}
},
[value, refsByValue, movingButton]
Expand All @@ -72,7 +78,10 @@ function useSelectedButtonBackgroundMove<T extends string | number>(
interface Props<T extends string | number> {
className?: string;
buttonClassName?: string;
values: { [value in T]: ReactNode } | Map<T, ReactNode> | T[];
values:
| { readonly [value in T]: ReactNode }
| Map<T, ReactNode>
| readonly T[];
value: T;
onChange: (value: T) => void;
}
Expand All @@ -87,21 +96,24 @@ export default function RadioButtonGroupInput<T extends string | number>({
const [movingAssetRef, createRefForValue] =
useSelectedButtonBackgroundMove<T>(value);
const entries = useMemo(() => {
const valueIsNumber = typeof value === 'number';
return Array.isArray(values)
? values.map<[T, string]>((value) => [value, `${value}`])
? values.filter(Boolean).map<[T, string]>((value) => [value, `${value}`])
: values instanceof Map
? Array.from(values.entries())
: (Object.entries(values).map(([value, description]) => [
value,
valueIsNumber ? Number(value) : value,
description,
]) as [T, string][]);
}, [values]);
}, [value, values]);
const selectedIndex = entries.findIndex(
([entryValue]) => entryValue === value
);
const includedIndexes = useMemo(() => {
return (
entries
// do not display falsy description buttons
.filter(([_key, value]) => !!value)
.map((_, index, entries) => {
// cumulate weightings
let result = 0;
Expand Down Expand Up @@ -156,6 +168,9 @@ export default function RadioButtonGroupInput<T extends string | number>({
const currentIndex = includedIndexes.includes(index);
const nextIndex = includedIndexes.includes(index + 1);

// skip those with no description
if (!description) return [];

// include button if required or if excluding it
// will not reduce the number of shown buttons
if (currentIndex || (previousIndex && nextIndex)) {
Expand Down
10 changes: 7 additions & 3 deletions src/components/Table/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export default function Table<
getRowKey,
context,
rowDescription = 'Data',
messageOnEmptyData,
filtered = false,
}: {
className?: string;
Expand All @@ -27,6 +28,7 @@ export default function Table<
getRowKey?: (row: DataRow) => string | number;
context: Context;
rowDescription?: string;
messageOnEmptyData?: ReactNode;
filtered?: boolean;
}) {
if (columns.length !== headings.length) {
Expand Down Expand Up @@ -85,9 +87,11 @@ export default function Table<
<tr>
<td colSpan={columns.length}>
{data ? (
<>
No {filtered ? 'Matching' : ''} {rowDescription} Found
</>
messageOnEmptyData || (
<>
No {filtered ? 'Matching' : ''} {rowDescription} Found
</>
)
) : (
<>Loading...</>
)}
Expand Down
8 changes: 3 additions & 5 deletions src/components/Tabs/Tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import './Tabs.scss';

export interface Tab {
nav: ReactNode;
Tab: React.FunctionComponent;
tab?: ReactNode;
}
export default function Tabs({
tabs,
Expand All @@ -23,7 +23,7 @@ export default function Tabs({
givenTabIndex !== undefined
? [givenTabIndex, givenSetTabIndex]
: [defaultTabIndex, defaultSetTabIndex];
const { Tab } = tabs[tabIndex];
const { tab } = tabs[tabIndex];

return (
<div className={['tabs col gap-4', className].filter(Boolean).join(' ')}>
Expand All @@ -47,9 +47,7 @@ export default function Tabs({
})}
</div>
<div className="tabs__tab flex row">
<div className="flex col">
<Tab />
</div>
<div className="flex col">{tab}</div>
</div>
</div>
);
Expand Down
26 changes: 1 addition & 25 deletions src/components/TokenInputGroup/TokenInputGroup.scss
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@
border-radius: 1rem;
border: 1px solid transparent;
background-color: var(--default);
grid-template-columns: auto 1fr;
grid-template-rows: auto 1fr auto;
color: var(--default-alt);
display: grid;
row-gap: 2px;
Expand All @@ -21,7 +19,7 @@
background-color: hsla(0, 73%, 97%, 1);
&,
.token-group-balance button {
border: 1px solid var(--error);
border-color: var(--error);
}
.token-group-title,
.token-picker-toggle,
Expand All @@ -34,41 +32,24 @@
}

.token-picker-toggle {
grid-column: 1;
text-align: start;
grid-row-start: 2;
grid-row-end: 4;
grid-row: 2 / 4;
row-gap: 2px;

.token-chain {
margin-bottom: -2px;
}
}

.token-group-title {
grid-column: 1;
font-size: font-size.$text-m;
text-align: start;
grid-row: 1;
font-weight: normal;
align-self: center;
}

.token-group-balance {
font-size: font-size.$text-m;
grid-column: 2;
grid-row: 1;
text-align: end;
display: flex;
gap: paddings.$p-3;
margin-left: auto;
}

.token-group-value {
font-size: font-size.$text-m;
grid-column: 2;
grid-row: 3;
text-align: end;
}

Expand All @@ -78,12 +59,7 @@
background-color: transparent;
border: 0px none transparent;
font-size: font-size.$input;
color: var(--default-alt);
grid-column: 2;
text-align: end;
grid-row: 2;
outline: none;
margin-top: margins.$m-2;
}

&:disabled {
Expand Down
150 changes: 86 additions & 64 deletions src/components/TokenInputGroup/TokenInputGroup.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback, useMemo } from 'react';
import React, { ReactNode, useCallback, useMemo, useRef } from 'react';
import BigNumber from 'bignumber.js';

import TokenPicker from '../TokenPicker';
Expand Down Expand Up @@ -29,6 +29,9 @@ interface InputGroupProps {
disabled?: boolean;
disabledInput?: boolean;
disabledToken?: boolean;
inactive?: boolean;
header?: ReactNode;
subHeader?: ReactNode;
maxValue?: number;
}

Expand All @@ -46,9 +49,12 @@ export default function TokenInputGroup({
token,
denom = token?.base,
defaultAssetMode,
header,
subHeader,
disabled = false,
disabledInput = disabled,
disabledToken = disabled,
inactive = false,
maxValue: givenMaxValue,
}: InputGroupProps) {
const onPickerChange = useCallback(
Expand All @@ -72,7 +78,8 @@ export default function TokenInputGroup({
}, [value, price]);

const { data: balance } = useBankBalanceDisplayAmount(denom);
const maxValue = givenMaxValue || balance;
const maxValue = givenMaxValue ?? balance;
const inputRef = useRef<HTMLInputElement>(null);
return (
<div
className={[
Expand All @@ -82,70 +89,85 @@ export default function TokenInputGroup({
]
.filter(Boolean)
.join(' ')}
onClick={() => inputRef.current?.focus()}
onKeyDown={() => undefined}
role="button"
tabIndex={-1}
>
{maxValue && (
<h5 className="token-group-title">
Available{' '}
{formatAmount(maxValue, {
useGrouping: true,
})}
</h5>
)}
{!disabledInput && token && maxValue && Number(maxValue) > 0 && (
<span className="token-group-balance">
<button
type="button"
className="badge badge-light"
onClick={() =>
onValueChanged?.(
// allow max value be as long as it needs to be to perfectly fit user's balance
roundToBaseUnit(token, maxValue) || ''
)
}
<div className="row gap-3">
<div className="col flex my-1">
<h5 className="token-group-title">
{header ||
(maxValue &&
`Available ${formatAmount(maxValue, {
useGrouping: true,
})}`) || <>&nbsp;</>}
</h5>
</div>
{subHeader && (
<div className="col token-group-sub-header my-1">{subHeader}</div>
)}
{!disabledInput && token && !!maxValue && Number(maxValue) > 0 && (
<div className="col token-group-balance gap-3">
<button
type="button"
className="badge badge-light"
onClick={() =>
onValueChanged?.(
// allow max value be as long as it needs to be to perfectly fit user's balance
roundToBaseUnit(token, maxValue) || ''
)
}
>
MAX
</button>
</div>
)}
</div>
<div className="row">
<div className="col flex">
<TokenPicker
className="gutter-l-3"
value={token}
onChange={onTokenChanged ? onPickerChange : undefined}
exclusion={exclusion}
disabled={disabledToken}
defaultAssetMode={defaultAssetMode}
/>
</div>
<div className="col flex mt-sm">
<NumberInput
type="text"
innerRef={inputRef}
className={[
'token-group-input',
!Number(value) && 'input--zero',
inactive && 'text-muted',
]
.filter(Boolean)
.join(' ')}
value={value}
placeholder={placeholder}
onChange={onValueChanged}
onClick={selectAll}
disabled={disabledInput}
style={useMemo(() => {
return {
// set width as minimum amount available
minWidth: '100%',
width: 0,
};
}, [])}
/>
<span
className={['token-group-value', inactive && 'text-muted']
.filter(Boolean)
.join(' ')}
>
MAX
</button>
<button
type="button"
className="badge badge-light"
onClick={() =>
// allow rounding on half of balance because we don't need an exact target
onValueChanged?.(
roundToBaseUnit(token, Number(maxValue) / 2) || ''
)
}
>
HALF
</button>
</span>
)}
<TokenPicker
className="gutter-l-3"
value={token}
onChange={onPickerChange}
exclusion={exclusion}
disabled={disabledToken}
defaultAssetMode={defaultAssetMode}
/>
<NumberInput
type="text"
className={['token-group-input', !Number(value) && 'input--zero']
.filter(Boolean)
.join(' ')}
value={value}
placeholder={placeholder}
onChange={onValueChanged}
onClick={selectAll}
disabled={disabledInput}
style={useMemo(() => {
return {
// set width as minimum amount available
minWidth: '100%',
width: 0,
};
}, [])}
/>
<span className="token-group-value">{secondaryValue}</span>
{secondaryValue}
</span>
</div>
</div>
</div>
);
}
Loading

0 comments on commit 45ad9c9

Please sign in to comment.