Feat/1089 override slippage (#1283)
* feat(console-lite): add traffic light component * fix(console-lite): add slippage to deal ticket estimates * feat(console-lite): refactor deal ticket steps and add slippage dialog * feat(console-lite): add e2e tests for slippage and fix broken ones * fix(console-lite): fix styling issue after theme update
This commit is contained in:
parent
65528a8007
commit
37aa5be725
@ -170,7 +170,50 @@ describe('Market trade', () => {
|
|||||||
.find('button')
|
.find('button')
|
||||||
.should('have.text', '2');
|
.should('have.text', '2');
|
||||||
cy.get('button').contains('Max').click();
|
cy.get('button').contains('Max').click();
|
||||||
cy.getByTestId('price-slippage-value').should('have.text', '0.02%');
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('slippage value should be displayed', () => {
|
||||||
|
if (markets?.length) {
|
||||||
|
cy.visit(`/trading/${markets[1].id}`);
|
||||||
|
connectVegaWallet();
|
||||||
|
cy.get('#step-1-control [aria-label^="Selected value"]').click();
|
||||||
|
cy.get('button[aria-label="Open short position"]').click();
|
||||||
|
cy.get('#step-2-control').click();
|
||||||
|
cy.get('button').contains('Max').click();
|
||||||
|
cy.get('#step-2-panel')
|
||||||
|
.find('dl')
|
||||||
|
.eq(2)
|
||||||
|
.find('dd')
|
||||||
|
.should('have.text', '0.02%');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allow slippage value to be adjusted', () => {
|
||||||
|
if (markets?.length) {
|
||||||
|
cy.visit(`/trading/${markets[1].id}`);
|
||||||
|
connectVegaWallet();
|
||||||
|
cy.get('#step-1-control [aria-label^="Selected value"]').click();
|
||||||
|
cy.get('button[aria-label="Open short position"]').click();
|
||||||
|
cy.get('#step-2-control').click();
|
||||||
|
cy.get('button').contains('Max').click();
|
||||||
|
cy.get('#step-2-panel')
|
||||||
|
.find('dl')
|
||||||
|
.eq(2)
|
||||||
|
.find('dd')
|
||||||
|
.should('have.text', '0.02%');
|
||||||
|
cy.get('#step-2-panel').find('dl').eq(2).find('button').click();
|
||||||
|
cy.get('#input-order-slippage')
|
||||||
|
.focus()
|
||||||
|
.type('{backspace}{backspace}{backspace}1');
|
||||||
|
|
||||||
|
cy.getByTestId('slippage-dialog').find('button').click();
|
||||||
|
|
||||||
|
cy.get('#step-2-panel')
|
||||||
|
.find('dl')
|
||||||
|
.eq(2)
|
||||||
|
.find('dd')
|
||||||
|
.should('have.text', '1%');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -195,9 +238,9 @@ describe('Market trade', () => {
|
|||||||
cy.get('#step-2-panel').find('dd').eq(0).find('button').click();
|
cy.get('#step-2-panel').find('dd').eq(0).find('button').click();
|
||||||
cy.get('#step-2-panel')
|
cy.get('#step-2-panel')
|
||||||
.find('dt')
|
.find('dt')
|
||||||
.eq(3)
|
.eq(2)
|
||||||
.should('have.text', 'Est. Position Size (tDAI)');
|
.should('have.text', 'Est. Position Size (tDAI)');
|
||||||
cy.get('#step-2-panel').find('dd').eq(3).should('have.text', '197.86012');
|
cy.get('#step-2-panel').find('dd').eq(2).should('have.text', '197.86012');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -210,11 +253,11 @@ describe('Market trade', () => {
|
|||||||
cy.get('#step-2-control').click();
|
cy.get('#step-2-control').click();
|
||||||
cy.get('#step-2-panel')
|
cy.get('#step-2-panel')
|
||||||
.find('dt')
|
.find('dt')
|
||||||
.eq(4)
|
.eq(3)
|
||||||
.should('have.text', 'Est. Fees (tDAI)');
|
.should('have.text', 'Est. Fees (tDAI)');
|
||||||
cy.get('#step-2-panel')
|
cy.get('#step-2-panel')
|
||||||
.find('dd')
|
.find('dd')
|
||||||
.eq(4)
|
.eq(3)
|
||||||
.should('have.text', '3.00000 (3.03%)');
|
.should('have.text', '3.00000 (3.03%)');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -4,6 +4,7 @@ import { t } from '@vegaprotocol/react-helpers';
|
|||||||
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
|
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||||
import { IconNames } from '@blueprintjs/icons';
|
import { IconNames } from '@blueprintjs/icons';
|
||||||
import * as constants from './constants';
|
import * as constants from './constants';
|
||||||
|
import { TrafficLight } from '../traffic-light';
|
||||||
|
|
||||||
interface DealTicketEstimatesProps {
|
interface DealTicketEstimatesProps {
|
||||||
quoteName?: string;
|
quoteName?: string;
|
||||||
@ -13,6 +14,7 @@ interface DealTicketEstimatesProps {
|
|||||||
fees?: string;
|
fees?: string;
|
||||||
notionalSize?: string;
|
notionalSize?: string;
|
||||||
size?: string;
|
size?: string;
|
||||||
|
slippage?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DataTitleProps {
|
interface DataTitleProps {
|
||||||
@ -20,7 +22,7 @@ interface DataTitleProps {
|
|||||||
quoteName?: string;
|
quoteName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DataTitle = ({ children, quoteName = '' }: DataTitleProps) => (
|
export const DataTitle = ({ children, quoteName = '' }: DataTitleProps) => (
|
||||||
<dt>
|
<dt>
|
||||||
{children}
|
{children}
|
||||||
{quoteName && <small> ({quoteName})</small>}
|
{quoteName && <small> ({quoteName})</small>}
|
||||||
@ -28,14 +30,20 @@ const DataTitle = ({ children, quoteName = '' }: DataTitleProps) => (
|
|||||||
);
|
);
|
||||||
|
|
||||||
interface ValueTooltipProps {
|
interface ValueTooltipProps {
|
||||||
value: string;
|
value?: string;
|
||||||
|
children?: ReactNode;
|
||||||
description: string;
|
description: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ValueTooltipRow = ({ value, description, id }: ValueTooltipProps) => (
|
export const ValueTooltipRow = ({
|
||||||
|
value,
|
||||||
|
children,
|
||||||
|
description,
|
||||||
|
id,
|
||||||
|
}: ValueTooltipProps) => (
|
||||||
<dd className="flex gap-x-2 items-center">
|
<dd className="flex gap-x-2 items-center">
|
||||||
{value}
|
{value || children}
|
||||||
<Tooltip align="center" description={description}>
|
<Tooltip align="center" description={description}>
|
||||||
<div className="cursor-help" id={id || ''} tabIndex={-1}>
|
<div className="cursor-help" id={id || ''} tabIndex={-1}>
|
||||||
<Icon
|
<Icon
|
||||||
@ -56,6 +64,7 @@ export const DealTicketEstimates = ({
|
|||||||
fees,
|
fees,
|
||||||
notionalSize,
|
notionalSize,
|
||||||
size,
|
size,
|
||||||
|
slippage,
|
||||||
}: DealTicketEstimatesProps) => (
|
}: DealTicketEstimatesProps) => (
|
||||||
<dl className="text-black dark:text-white">
|
<dl className="text-black dark:text-white">
|
||||||
{size && (
|
{size && (
|
||||||
@ -93,7 +102,7 @@ export const DealTicketEstimates = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{estMargin && (
|
{estMargin && (
|
||||||
<div className="flex justify-between mb-8">
|
<div className="flex justify-between mb-2">
|
||||||
<DataTitle quoteName={quoteName}>{t('Est. Margin')}</DataTitle>
|
<DataTitle quoteName={quoteName}>{t('Est. Margin')}</DataTitle>
|
||||||
<ValueTooltipRow
|
<ValueTooltipRow
|
||||||
value={estMargin}
|
value={estMargin}
|
||||||
@ -102,17 +111,23 @@ export const DealTicketEstimates = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{estCloseOut && (
|
{estCloseOut && (
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between mb-2">
|
||||||
<dt>
|
<DataTitle quoteName={quoteName}>{t('Est. Close out')}</DataTitle>
|
||||||
<span>{t('Est. Close out')}</span>
|
|
||||||
|
|
||||||
<small>({quoteName})</small>
|
|
||||||
</dt>
|
|
||||||
<ValueTooltipRow
|
<ValueTooltipRow
|
||||||
value={estCloseOut}
|
value={estCloseOut}
|
||||||
description={constants.EST_CLOSEOUT_TOOLTIP_TEXT}
|
description={constants.EST_CLOSEOUT_TOOLTIP_TEXT}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{slippage && (
|
||||||
|
<div className="flex justify-between mb-2">
|
||||||
|
<DataTitle>{t('Est. Price Impact / Slippage')}</DataTitle>
|
||||||
|
<ValueTooltipRow description={constants.EST_SLIPPAGE}>
|
||||||
|
<TrafficLight value={parseFloat(slippage)} q1={1} q2={5}>
|
||||||
|
{slippage}%
|
||||||
|
</TrafficLight>
|
||||||
|
</ValueTooltipRow>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</dl>
|
</dl>
|
||||||
);
|
);
|
||||||
|
@ -0,0 +1,149 @@
|
|||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import { BigNumber } from 'bignumber.js';
|
||||||
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
|
import {
|
||||||
|
SliderRoot,
|
||||||
|
SliderThumb,
|
||||||
|
SliderTrack,
|
||||||
|
SliderRange,
|
||||||
|
FormGroup,
|
||||||
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { InputSetter } from '../input-setter';
|
||||||
|
|
||||||
|
interface DealTicketSizeInputProps {
|
||||||
|
step: number;
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
value: number;
|
||||||
|
onValueChange: (value: number) => void;
|
||||||
|
positionDecimalPlaces: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSizeLabel = (value: number): string => {
|
||||||
|
const MIN_LABEL = 'Min';
|
||||||
|
const MAX_LABEL = 'Max';
|
||||||
|
if (value === 0) {
|
||||||
|
return MIN_LABEL;
|
||||||
|
} else if (value === 100) {
|
||||||
|
return MAX_LABEL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${value}%`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DealTicketSizeInput = ({
|
||||||
|
value,
|
||||||
|
step,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
onValueChange,
|
||||||
|
positionDecimalPlaces,
|
||||||
|
}: DealTicketSizeInputProps) => {
|
||||||
|
const sizeRatios = [0, 25, 50, 75, 100];
|
||||||
|
const [inputValue, setInputValue] = useState(value);
|
||||||
|
|
||||||
|
const onInputValueChange = useCallback(
|
||||||
|
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
let value = parseFloat(event.target.value);
|
||||||
|
const isLessThanMin = value < min;
|
||||||
|
const isMoreThanMax = value > max;
|
||||||
|
if (isLessThanMin) {
|
||||||
|
value = min;
|
||||||
|
} else if (isMoreThanMax) {
|
||||||
|
value = max;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
onValueChange(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
setInputValue(value);
|
||||||
|
},
|
||||||
|
[min, max, onValueChange, setInputValue]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onButtonValueChange = (size: number) => {
|
||||||
|
const newVal = new BigNumber(size)
|
||||||
|
.decimalPlaces(positionDecimalPlaces)
|
||||||
|
.toNumber();
|
||||||
|
onValueChange(newVal);
|
||||||
|
setInputValue(newVal);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSliderValueChange = useCallback(
|
||||||
|
(value: number[]) => {
|
||||||
|
const val = value[0];
|
||||||
|
setInputValue(val);
|
||||||
|
onValueChange(val);
|
||||||
|
},
|
||||||
|
[onValueChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between text-black dark:text-white mb-2">
|
||||||
|
<span data-testid="min-label">{min}</span>
|
||||||
|
<span data-testid="max-label">{max}</span>
|
||||||
|
</div>
|
||||||
|
<SliderRoot
|
||||||
|
className="mb-2"
|
||||||
|
value={[value]}
|
||||||
|
onValueChange={onSliderValueChange}
|
||||||
|
step={step}
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
>
|
||||||
|
<SliderTrack className="bg-lightGrey dark:bg-offBlack">
|
||||||
|
<SliderRange className="!bg-black dark:!bg-white" />
|
||||||
|
</SliderTrack>
|
||||||
|
<SliderThumb />
|
||||||
|
</SliderRoot>
|
||||||
|
|
||||||
|
<div
|
||||||
|
data-testid="percentage-selector"
|
||||||
|
className="flex w-full justify-between text-black dark:text-white mb-6"
|
||||||
|
>
|
||||||
|
{sizeRatios.map((size, index) => {
|
||||||
|
const proportionalSize = size ? (size / 100) * max : min;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className="no-underline hover:underline text-blue"
|
||||||
|
onClick={() => onButtonValueChange(proportionalSize)}
|
||||||
|
type="button"
|
||||||
|
key={index}
|
||||||
|
>
|
||||||
|
{getSizeLabel(size)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dl className="text-black dark:text-white">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<dt>{t('Contracts')}</dt>
|
||||||
|
<dd className="flex justify-end w-full">
|
||||||
|
<FormGroup
|
||||||
|
hideLabel={true}
|
||||||
|
label="Enter Size"
|
||||||
|
labelFor="trade-size-input"
|
||||||
|
className="mb-1"
|
||||||
|
>
|
||||||
|
<InputSetter
|
||||||
|
id="input-order-size-market"
|
||||||
|
type="number"
|
||||||
|
step={step}
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
className="w-full"
|
||||||
|
value={inputValue}
|
||||||
|
onChange={onInputValueChange}
|
||||||
|
>
|
||||||
|
{inputValue}
|
||||||
|
</InputSetter>
|
||||||
|
</FormGroup>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -1,27 +1,13 @@
|
|||||||
import React, { useCallback, useState } from 'react';
|
import React from 'react';
|
||||||
import classNames from 'classnames';
|
|
||||||
import { IconNames } from '@blueprintjs/icons';
|
|
||||||
import { t } from '@vegaprotocol/react-helpers';
|
|
||||||
import {
|
|
||||||
SliderRoot,
|
|
||||||
SliderThumb,
|
|
||||||
SliderTrack,
|
|
||||||
SliderRange,
|
|
||||||
FormGroup,
|
|
||||||
Icon,
|
|
||||||
Tooltip,
|
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
|
||||||
import { BigNumber } from 'bignumber.js';
|
|
||||||
import { DealTicketEstimates } from './deal-ticket-estimates';
|
import { DealTicketEstimates } from './deal-ticket-estimates';
|
||||||
import { InputSetter } from '../input-setter';
|
import { DealTicketSizeInput } from './deal-ticket-size-input';
|
||||||
import * as constants from './constants';
|
|
||||||
|
|
||||||
interface DealTicketSizeProps {
|
interface DealTicketSizeProps {
|
||||||
step: number;
|
step: number;
|
||||||
min: number;
|
min: number;
|
||||||
max: number;
|
max: number;
|
||||||
value: number;
|
size: number;
|
||||||
onValueChange: (value: number[]) => void;
|
onSizeChange: (value: number) => void;
|
||||||
name: string;
|
name: string;
|
||||||
quoteName: string;
|
quoteName: string;
|
||||||
price: string;
|
price: string;
|
||||||
@ -30,173 +16,33 @@ interface DealTicketSizeProps {
|
|||||||
fees: string;
|
fees: string;
|
||||||
positionDecimalPlaces: number;
|
positionDecimalPlaces: number;
|
||||||
notionalSize: string;
|
notionalSize: string;
|
||||||
slippage: string | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSizeLabel = (value: number): string => {
|
|
||||||
const MIN_LABEL = 'Min';
|
|
||||||
const MAX_LABEL = 'Max';
|
|
||||||
if (value === 0) {
|
|
||||||
return MIN_LABEL;
|
|
||||||
} else if (value === 100) {
|
|
||||||
return MAX_LABEL;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${value}%`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DealTicketSize = ({
|
export const DealTicketSize = ({
|
||||||
value,
|
|
||||||
step,
|
step,
|
||||||
min,
|
min,
|
||||||
max,
|
max,
|
||||||
price,
|
price,
|
||||||
quoteName,
|
quoteName,
|
||||||
onValueChange,
|
size,
|
||||||
|
onSizeChange,
|
||||||
estCloseOut,
|
estCloseOut,
|
||||||
positionDecimalPlaces,
|
positionDecimalPlaces,
|
||||||
fees,
|
fees,
|
||||||
notionalSize,
|
notionalSize,
|
||||||
slippage,
|
|
||||||
}: DealTicketSizeProps) => {
|
}: DealTicketSizeProps) => {
|
||||||
const sizeRatios = [0, 25, 50, 75, 100];
|
|
||||||
const [inputValue, setInputValue] = useState(value);
|
|
||||||
|
|
||||||
const onInputValueChange = useCallback(
|
|
||||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const value = parseFloat(event.target.value);
|
|
||||||
const isLessThanMin = value < min;
|
|
||||||
const isMoreThanMax = value > max;
|
|
||||||
if (value) {
|
|
||||||
if (isLessThanMin) {
|
|
||||||
onValueChange([min]);
|
|
||||||
} else if (isMoreThanMax) {
|
|
||||||
onValueChange([max]);
|
|
||||||
} else {
|
|
||||||
onValueChange([value]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setInputValue(value);
|
|
||||||
},
|
|
||||||
[min, max, onValueChange, setInputValue]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onButtonValueChange = useCallback(
|
|
||||||
(size: number) => {
|
|
||||||
const newVal = new BigNumber(size)
|
|
||||||
.decimalPlaces(positionDecimalPlaces)
|
|
||||||
.toNumber();
|
|
||||||
onValueChange([newVal]);
|
|
||||||
setInputValue(newVal);
|
|
||||||
},
|
|
||||||
[onValueChange, positionDecimalPlaces]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onSliderValueChange = useCallback(
|
|
||||||
(value: number[]) => {
|
|
||||||
setInputValue(value[0]);
|
|
||||||
onValueChange(value);
|
|
||||||
},
|
|
||||||
[onValueChange]
|
|
||||||
);
|
|
||||||
|
|
||||||
return max === 0 ? (
|
return max === 0 ? (
|
||||||
<p>Not enough balance to trade</p>
|
<p>Not enough balance to trade</p>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex justify-between text-black dark:text-white mb-2">
|
<DealTicketSizeInput
|
||||||
<span data-testid="min-label">{min}</span>
|
|
||||||
<span data-testid="max-label">{max}</span>
|
|
||||||
</div>
|
|
||||||
<SliderRoot
|
|
||||||
className="mb-2"
|
|
||||||
value={[value]}
|
|
||||||
onValueChange={onSliderValueChange}
|
|
||||||
step={step}
|
step={step}
|
||||||
min={min}
|
min={min}
|
||||||
max={max}
|
max={max}
|
||||||
>
|
value={size}
|
||||||
<SliderTrack className="bg-lightGrey dark:bg-offBlack">
|
onValueChange={onSizeChange}
|
||||||
<SliderRange className="!bg-black dark:!bg-white" />
|
positionDecimalPlaces={positionDecimalPlaces}
|
||||||
</SliderTrack>
|
/>
|
||||||
<SliderThumb />
|
|
||||||
</SliderRoot>
|
|
||||||
|
|
||||||
<div
|
|
||||||
data-testid="percentage-selector"
|
|
||||||
className="flex w-full justify-between text-black dark:text-white mb-6"
|
|
||||||
>
|
|
||||||
{sizeRatios.map((size, index) => {
|
|
||||||
const proportionalSize = size ? (size / 100) * max : min;
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
className="no-underline hover:underline text-blue"
|
|
||||||
onClick={() => onButtonValueChange(proportionalSize)}
|
|
||||||
type="button"
|
|
||||||
key={index}
|
|
||||||
>
|
|
||||||
{getSizeLabel(size)}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<dl className="text-black dark:text-white">
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<dt>{t('Contracts')}</dt>
|
|
||||||
<dd className="flex justify-end w-full">
|
|
||||||
<FormGroup
|
|
||||||
hideLabel={true}
|
|
||||||
label="Enter Size"
|
|
||||||
labelFor="trade-size-input"
|
|
||||||
>
|
|
||||||
<InputSetter
|
|
||||||
id="input-order-size-market"
|
|
||||||
type="number"
|
|
||||||
step={step}
|
|
||||||
min={min}
|
|
||||||
max={max}
|
|
||||||
className="w-full"
|
|
||||||
value={inputValue}
|
|
||||||
onChange={onInputValueChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
</dd>
|
|
||||||
</div>
|
|
||||||
</dl>
|
|
||||||
{slippage && (
|
|
||||||
<dl className="text-black dark:text-white">
|
|
||||||
<div className="flex items-center justify-between mb-8">
|
|
||||||
<dt>{t('Est. Price Impact / Slippage')}</dt>
|
|
||||||
<dd
|
|
||||||
className="flex justify-end gap-x-5"
|
|
||||||
data-testid="price-slippage-value"
|
|
||||||
aria-label={t('Est. Price Impact / Slippage')}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={classNames({
|
|
||||||
'text-darkerGreen dark:text-lightGreen':
|
|
||||||
parseFloat(slippage) < 1,
|
|
||||||
'text-amber':
|
|
||||||
parseFloat(slippage) >= 1 && parseFloat(slippage) < 5,
|
|
||||||
'text-vega-red': parseFloat(slippage) >= 5,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{slippage}%
|
|
||||||
</span>
|
|
||||||
<Tooltip align="center" description={constants.EST_SLIPPAGE}>
|
|
||||||
<div className="cursor-help" tabIndex={-1}>
|
|
||||||
<Icon
|
|
||||||
name={IconNames.ISSUE}
|
|
||||||
className="block rotate-180"
|
|
||||||
ariaLabel={constants.EST_SLIPPAGE}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</dd>
|
|
||||||
</div>
|
|
||||||
</dl>
|
|
||||||
)}
|
|
||||||
<DealTicketEstimates
|
<DealTicketEstimates
|
||||||
quoteName={quoteName}
|
quoteName={quoteName}
|
||||||
fees={fees}
|
fees={fees}
|
||||||
|
@ -0,0 +1,104 @@
|
|||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
|
import * as constants from './constants';
|
||||||
|
import { TrafficLight } from '../traffic-light';
|
||||||
|
import { Dialog, Icon, Intent, Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { InputSetter } from '../../components/input-setter';
|
||||||
|
import { IconNames } from '@blueprintjs/icons';
|
||||||
|
import { DataTitle, ValueTooltipRow } from './deal-ticket-estimates';
|
||||||
|
|
||||||
|
interface DealTicketSlippageProps {
|
||||||
|
step?: number;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
value: number;
|
||||||
|
onValueChange(value: number): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DealTicketSlippage = ({
|
||||||
|
value,
|
||||||
|
step = 0.01,
|
||||||
|
min = 0,
|
||||||
|
max = 50,
|
||||||
|
onValueChange,
|
||||||
|
}: DealTicketSlippageProps) => {
|
||||||
|
const [isDialogVisible, setIsDialogVisible] = useState(false);
|
||||||
|
|
||||||
|
const onChange = useCallback(
|
||||||
|
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = event.target.value;
|
||||||
|
const numericValue = parseFloat(value);
|
||||||
|
onValueChange(numericValue);
|
||||||
|
},
|
||||||
|
[onValueChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
const toggleDialog = useCallback(() => {
|
||||||
|
setIsDialogVisible(!isDialogVisible);
|
||||||
|
}, [isDialogVisible]);
|
||||||
|
|
||||||
|
const formLabel = (
|
||||||
|
<label className="flex items-center mb-1">
|
||||||
|
<span className="mr-1">{t('Adjust slippage tolerance')}</span>
|
||||||
|
<Tooltip align="center" description={constants.EST_SLIPPAGE}>
|
||||||
|
<div className="cursor-help" tabIndex={-1}>
|
||||||
|
<Icon
|
||||||
|
name={IconNames.ISSUE}
|
||||||
|
className="block rotate-180"
|
||||||
|
ariaLabel={constants.EST_SLIPPAGE}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Dialog
|
||||||
|
open={isDialogVisible}
|
||||||
|
onChange={toggleDialog}
|
||||||
|
intent={Intent.None}
|
||||||
|
title={t('Transaction Settings')}
|
||||||
|
>
|
||||||
|
<div data-testid="slippage-dialog">
|
||||||
|
{formLabel}
|
||||||
|
<InputSetter
|
||||||
|
id="input-order-slippage"
|
||||||
|
isInputVisible
|
||||||
|
hasError={!value}
|
||||||
|
type="number"
|
||||||
|
step={step}
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
className="w-full"
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
>
|
||||||
|
{value}%
|
||||||
|
</InputSetter>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
<dl className="text-black dark:text-white">
|
||||||
|
<div className="flex justify-between mb-2">
|
||||||
|
<DataTitle>{t('Est. Price Impact / Slippage')}</DataTitle>
|
||||||
|
<div className="flex">
|
||||||
|
<div className="mr-1">
|
||||||
|
<ValueTooltipRow description={constants.EST_SLIPPAGE}>
|
||||||
|
<TrafficLight value={value} q1={1} q2={5}>
|
||||||
|
{value}%
|
||||||
|
</TrafficLight>
|
||||||
|
</ValueTooltipRow>
|
||||||
|
</div>
|
||||||
|
<button type="button" onClick={toggleDialog}>
|
||||||
|
<Icon
|
||||||
|
name={IconNames.COG}
|
||||||
|
className="block rotate-180"
|
||||||
|
ariaLabel={t('Override slippage value')}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useForm, Controller } from 'react-hook-form';
|
import { useForm, Controller } from 'react-hook-form';
|
||||||
import { Stepper } from '../stepper';
|
import { Stepper } from '../stepper';
|
||||||
@ -10,7 +10,7 @@ import type { Order } from '@vegaprotocol/orders';
|
|||||||
import { useVegaWallet, VegaTxStatus } from '@vegaprotocol/wallet';
|
import { useVegaWallet, VegaTxStatus } from '@vegaprotocol/wallet';
|
||||||
import {
|
import {
|
||||||
t,
|
t,
|
||||||
addDecimal,
|
addDecimalsFormatNumber,
|
||||||
toDecimal,
|
toDecimal,
|
||||||
removeDecimal,
|
removeDecimal,
|
||||||
} from '@vegaprotocol/react-helpers';
|
} from '@vegaprotocol/react-helpers';
|
||||||
@ -33,6 +33,8 @@ import useOrderCloseOut from '../../hooks/use-order-closeout';
|
|||||||
import useOrderMargin from '../../hooks/use-order-margin';
|
import useOrderMargin from '../../hooks/use-order-margin';
|
||||||
import useMaximumPositionSize from '../../hooks/use-maximum-position-size';
|
import useMaximumPositionSize from '../../hooks/use-maximum-position-size';
|
||||||
import useCalculateSlippage from '../../hooks/use-calculate-slippage';
|
import useCalculateSlippage from '../../hooks/use-calculate-slippage';
|
||||||
|
import { Side, OrderType } from '@vegaprotocol/types';
|
||||||
|
import { DealTicketSlippage } from './deal-ticket-slippage';
|
||||||
|
|
||||||
interface DealTicketMarketProps {
|
interface DealTicketMarketProps {
|
||||||
market: DealTicketQuery_market;
|
market: DealTicketQuery_market;
|
||||||
@ -62,28 +64,26 @@ export const DealTicketSteps = ({
|
|||||||
defaultValues: getDefaultOrder(market),
|
defaultValues: getDefaultOrder(market),
|
||||||
});
|
});
|
||||||
|
|
||||||
const [max, setMax] = useState<number | null>(null);
|
const emptyString = ' - ';
|
||||||
const step = toDecimal(market.positionDecimalPlaces);
|
const step = toDecimal(market.positionDecimalPlaces);
|
||||||
const orderType = watch('type');
|
const orderType = watch('type');
|
||||||
const orderTimeInForce = watch('timeInForce');
|
const orderTimeInForce = watch('timeInForce');
|
||||||
const orderSide = watch('side');
|
const orderSide = watch('side');
|
||||||
const orderSize = watch('size');
|
const orderSize = watch('size');
|
||||||
const order = watch();
|
const order = watch();
|
||||||
const estCloseOut = useOrderCloseOut({ order, market, partyData });
|
const { message: invalidText, isDisabled } = useOrderValidation({
|
||||||
|
market,
|
||||||
|
orderType,
|
||||||
|
orderTimeInForce,
|
||||||
|
fieldErrors: errors,
|
||||||
|
});
|
||||||
|
const { submit, transaction, finalizedOrder, Dialog } = useOrderSubmit();
|
||||||
const { keypair } = useVegaWallet();
|
const { keypair } = useVegaWallet();
|
||||||
const estMargin = useOrderMargin({
|
const estMargin = useOrderMargin({
|
||||||
order,
|
order,
|
||||||
market,
|
market,
|
||||||
partyId: keypair?.pub || '',
|
partyId: keypair?.pub || '',
|
||||||
});
|
});
|
||||||
const value = new BigNumber(orderSize).toNumber();
|
|
||||||
const price =
|
|
||||||
market.depth.lastTrade &&
|
|
||||||
addDecimal(market.depth.lastTrade.price, market.decimalPlaces);
|
|
||||||
const emptyString = ' - ';
|
|
||||||
|
|
||||||
const [notionalSize, setNotionalSize] = useState<string | null>(null);
|
|
||||||
const [fees, setFees] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const maxTrade = useMaximumPositionSize({
|
const maxTrade = useMaximumPositionSize({
|
||||||
partyId: keypair?.pub || '',
|
partyId: keypair?.pub || '',
|
||||||
@ -94,45 +94,48 @@ export const DealTicketSteps = ({
|
|||||||
price: market?.depth?.lastTrade?.price,
|
price: market?.depth?.lastTrade?.price,
|
||||||
order,
|
order,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const estCloseOut = useOrderCloseOut({ order, market, partyData });
|
||||||
const slippage = useCalculateSlippage({ marketId: market.id, order });
|
const slippage = useCalculateSlippage({ marketId: market.id, order });
|
||||||
useEffect(() => {
|
const [slippageValue, setSlippageValue] = useState(
|
||||||
setMax(
|
slippage ? parseFloat(slippage) : 0
|
||||||
new BigNumber(maxTrade)
|
);
|
||||||
.decimalPlaces(market.positionDecimalPlaces)
|
const transactionStatus =
|
||||||
.toNumber()
|
transaction.status === VegaTxStatus.Requested ||
|
||||||
);
|
transaction.status === VegaTxStatus.Pending
|
||||||
}, [maxTrade, market.positionDecimalPlaces]);
|
? 'pending'
|
||||||
|
: 'default';
|
||||||
const { message: invalidText, isDisabled } = useOrderValidation({
|
|
||||||
market,
|
|
||||||
orderType,
|
|
||||||
orderTimeInForce,
|
|
||||||
fieldErrors: errors,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { submit, transaction, finalizedOrder, Dialog } = useOrderSubmit();
|
|
||||||
|
|
||||||
const onSizeChange = (value: number[]) => {
|
|
||||||
const newVal = new BigNumber(value[0])
|
|
||||||
.decimalPlaces(market.positionDecimalPlaces)
|
|
||||||
.toString();
|
|
||||||
const isValid = validateSize(step)(newVal);
|
|
||||||
if (isValid !== 'step') {
|
|
||||||
setValue('size', newVal);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (market?.depth?.lastTrade?.price) {
|
setSlippageValue(slippage ? parseFloat(slippage) : 0);
|
||||||
const size = new BigNumber(market.depth.lastTrade.price)
|
}, [slippage]);
|
||||||
.multipliedBy(value)
|
|
||||||
|
const price = useMemo(() => {
|
||||||
|
if (slippage && market?.depth?.lastTrade?.price) {
|
||||||
|
const isLong = order.side === Side.SIDE_BUY;
|
||||||
|
const multiplier = new BigNumber(1)[isLong ? 'plus' : 'minus'](
|
||||||
|
parseFloat(slippage) / 100
|
||||||
|
);
|
||||||
|
return new BigNumber(market?.depth?.lastTrade?.price)
|
||||||
|
.multipliedBy(multiplier)
|
||||||
.toNumber();
|
.toNumber();
|
||||||
|
|
||||||
setNotionalSize(addDecimal(size, market.decimalPlaces));
|
|
||||||
}
|
}
|
||||||
}, [market, value]);
|
return null;
|
||||||
|
}, [market?.depth?.lastTrade?.price, order.side, slippage]);
|
||||||
|
|
||||||
useEffect(() => {
|
const formattedPrice =
|
||||||
|
price && addDecimalsFormatNumber(price, market.decimalPlaces);
|
||||||
|
|
||||||
|
const notionalSize = useMemo(() => {
|
||||||
|
if (price) {
|
||||||
|
const size = new BigNumber(price).multipliedBy(orderSize).toNumber();
|
||||||
|
|
||||||
|
return addDecimalsFormatNumber(size, market.decimalPlaces);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [market.decimalPlaces, orderSize, price]);
|
||||||
|
|
||||||
|
const fees = useMemo(() => {
|
||||||
if (estMargin?.fees && notionalSize) {
|
if (estMargin?.fees && notionalSize) {
|
||||||
const percentage = new BigNumber(estMargin?.fees)
|
const percentage = new BigNumber(estMargin?.fees)
|
||||||
.dividedBy(notionalSize)
|
.dividedBy(notionalSize)
|
||||||
@ -140,17 +143,66 @@ export const DealTicketSteps = ({
|
|||||||
.decimalPlaces(2)
|
.decimalPlaces(2)
|
||||||
.toString();
|
.toString();
|
||||||
|
|
||||||
setFees(`${estMargin.fees} (${percentage}%)`);
|
return `${estMargin.fees} (${percentage}%)`;
|
||||||
}
|
}
|
||||||
}, [estMargin, notionalSize]);
|
|
||||||
|
|
||||||
const transactionStatus =
|
return null;
|
||||||
transaction.status === VegaTxStatus.Requested ||
|
}, [estMargin?.fees, notionalSize]);
|
||||||
transaction.status === VegaTxStatus.Pending
|
|
||||||
? 'pending'
|
|
||||||
: 'default';
|
|
||||||
|
|
||||||
const onSubmit = React.useCallback(
|
const max = useMemo(() => {
|
||||||
|
return new BigNumber(maxTrade)
|
||||||
|
.decimalPlaces(market.positionDecimalPlaces)
|
||||||
|
.toNumber();
|
||||||
|
}, [market.positionDecimalPlaces, maxTrade]);
|
||||||
|
|
||||||
|
const onSizeChange = useCallback(
|
||||||
|
(value: number) => {
|
||||||
|
const newVal = new BigNumber(value)
|
||||||
|
.decimalPlaces(market.positionDecimalPlaces)
|
||||||
|
.toString();
|
||||||
|
const isValid = validateSize(step)(newVal);
|
||||||
|
if (isValid !== 'step') {
|
||||||
|
setValue('size', newVal);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[market.positionDecimalPlaces, setValue, step]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSlippageChange = useCallback(
|
||||||
|
(value: number) => {
|
||||||
|
if (market?.depth?.lastTrade?.price) {
|
||||||
|
if (value) {
|
||||||
|
const isLong = order.side === Side.SIDE_BUY;
|
||||||
|
const multiplier = new BigNumber(1)[isLong ? 'plus' : 'minus'](
|
||||||
|
value / 100
|
||||||
|
);
|
||||||
|
const bestAskPrice = new BigNumber(market?.depth?.lastTrade?.price)
|
||||||
|
.multipliedBy(multiplier)
|
||||||
|
.decimalPlaces(market.decimalPlaces)
|
||||||
|
.toString();
|
||||||
|
|
||||||
|
setValue('price', bestAskPrice);
|
||||||
|
|
||||||
|
if (orderType === OrderType.TYPE_MARKET) {
|
||||||
|
setValue('type', OrderType.TYPE_LIMIT);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setValue('type', OrderType.TYPE_MARKET);
|
||||||
|
setValue('price', market?.depth?.lastTrade?.price);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setSlippageValue(value);
|
||||||
|
},
|
||||||
|
[
|
||||||
|
market.decimalPlaces,
|
||||||
|
market?.depth?.lastTrade?.price,
|
||||||
|
order.side,
|
||||||
|
orderType,
|
||||||
|
setValue,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSubmit = useCallback(
|
||||||
(order: Order) => {
|
(order: Order) => {
|
||||||
if (transactionStatus !== 'pending') {
|
if (transactionStatus !== 'pending') {
|
||||||
submit({
|
submit({
|
||||||
@ -198,25 +250,30 @@ export const DealTicketSteps = ({
|
|||||||
label: t('Choose Position Size'),
|
label: t('Choose Position Size'),
|
||||||
component:
|
component:
|
||||||
max !== null ? (
|
max !== null ? (
|
||||||
<DealTicketSize
|
<>
|
||||||
step={step}
|
<DealTicketSize
|
||||||
min={step}
|
step={step}
|
||||||
max={max}
|
min={step}
|
||||||
onValueChange={onSizeChange}
|
max={max}
|
||||||
value={new BigNumber(orderSize).toNumber()}
|
onSizeChange={onSizeChange}
|
||||||
name="size"
|
size={new BigNumber(orderSize).toNumber()}
|
||||||
price={price || emptyString}
|
name="size"
|
||||||
positionDecimalPlaces={market.positionDecimalPlaces}
|
price={formattedPrice || emptyString}
|
||||||
quoteName={
|
positionDecimalPlaces={market.positionDecimalPlaces}
|
||||||
market.tradableInstrument.instrument.product.settlementAsset
|
quoteName={
|
||||||
.symbol
|
market.tradableInstrument.instrument.product.settlementAsset
|
||||||
}
|
.symbol
|
||||||
notionalSize={notionalSize || emptyString}
|
}
|
||||||
estCloseOut={estCloseOut}
|
notionalSize={notionalSize || emptyString}
|
||||||
fees={fees || emptyString}
|
estCloseOut={estCloseOut}
|
||||||
estMargin={estMargin?.margin || emptyString}
|
fees={fees || emptyString}
|
||||||
slippage={slippage}
|
estMargin={estMargin?.margin || emptyString}
|
||||||
/>
|
/>
|
||||||
|
<DealTicketSlippage
|
||||||
|
value={slippageValue}
|
||||||
|
onValueChange={onSlippageChange}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
'loading...'
|
'loading...'
|
||||||
),
|
),
|
||||||
@ -240,13 +297,14 @@ export const DealTicketSteps = ({
|
|||||||
order={order}
|
order={order}
|
||||||
estCloseOut={estCloseOut}
|
estCloseOut={estCloseOut}
|
||||||
estMargin={estMargin?.margin || emptyString}
|
estMargin={estMargin?.margin || emptyString}
|
||||||
price={price || emptyString}
|
price={formattedPrice || emptyString}
|
||||||
quoteName={
|
quoteName={
|
||||||
market.tradableInstrument.instrument.product.settlementAsset
|
market.tradableInstrument.instrument.product.settlementAsset
|
||||||
.symbol
|
.symbol
|
||||||
}
|
}
|
||||||
notionalSize={notionalSize || emptyString}
|
notionalSize={notionalSize || emptyString}
|
||||||
fees={fees || emptyString}
|
fees={fees || emptyString}
|
||||||
|
slippage={slippageValue}
|
||||||
/>
|
/>
|
||||||
<Dialog
|
<Dialog
|
||||||
title={getOrderDialogTitle(finalizedOrder?.status)}
|
title={getOrderDialogTitle(finalizedOrder?.status)}
|
||||||
|
@ -43,6 +43,7 @@ interface Props {
|
|||||||
price: string;
|
price: string;
|
||||||
fees: string;
|
fees: string;
|
||||||
notionalSize: string;
|
notionalSize: string;
|
||||||
|
slippage: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ({
|
export default ({
|
||||||
@ -55,6 +56,7 @@ export default ({
|
|||||||
fees,
|
fees,
|
||||||
price,
|
price,
|
||||||
notionalSize,
|
notionalSize,
|
||||||
|
slippage,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { data: tagsData } = useQuery<MarketTags, MarketTagsVariables>(
|
const { data: tagsData } = useQuery<MarketTags, MarketTagsVariables>(
|
||||||
MARKET_TAGS_QUERY,
|
MARKET_TAGS_QUERY,
|
||||||
@ -105,6 +107,7 @@ export default ({
|
|||||||
fees={fees}
|
fees={fees}
|
||||||
estCloseOut={estCloseOut}
|
estCloseOut={estCloseOut}
|
||||||
notionalSize={notionalSize}
|
notionalSize={notionalSize}
|
||||||
|
slippage={slippage.toString()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="mt-12 max-w-sm">
|
<div className="mt-12 max-w-sm">
|
||||||
|
@ -1,20 +1,19 @@
|
|||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
import { t } from '@vegaprotocol/react-helpers';
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
import { Input } from '@vegaprotocol/ui-toolkit';
|
import { Input } from '@vegaprotocol/ui-toolkit';
|
||||||
import type { InputProps } from '@vegaprotocol/ui-toolkit';
|
import type { InputProps } from '@vegaprotocol/ui-toolkit';
|
||||||
|
|
||||||
interface InputSetterProps {
|
interface InputSetterProps {
|
||||||
buttonLabel?: string;
|
buttonLabel?: string;
|
||||||
value: string | number;
|
|
||||||
isInputVisible?: boolean;
|
isInputVisible?: boolean;
|
||||||
onValueChange?: () => string;
|
children?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InputSetter = ({
|
export const InputSetter = ({
|
||||||
buttonLabel = t('set'),
|
buttonLabel = t('set'),
|
||||||
value = '',
|
|
||||||
isInputVisible = false,
|
isInputVisible = false,
|
||||||
onValueChange,
|
children,
|
||||||
...props
|
...props
|
||||||
}: InputSetterProps & InputProps) => {
|
}: InputSetterProps & InputProps) => {
|
||||||
const [isInputToggled, setIsInputToggled] = useState(isInputVisible);
|
const [isInputToggled, setIsInputToggled] = useState(isInputVisible);
|
||||||
@ -35,7 +34,7 @@ export const InputSetter = ({
|
|||||||
|
|
||||||
return isInputToggled ? (
|
return isInputToggled ? (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Input {...props} value={value} onKeyDown={onInputEnter} />
|
<Input {...props} onKeyDown={onInputEnter} />
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="no-underline hover:underline text-blue ml-2"
|
className="no-underline hover:underline text-blue ml-2"
|
||||||
@ -47,10 +46,10 @@ export const InputSetter = ({
|
|||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="no-underline hover:underline text-blue"
|
className="no-underline hover:underline text-blue py-1.5"
|
||||||
onClick={toggleInput}
|
onClick={toggleInput}
|
||||||
>
|
>
|
||||||
{value}
|
{children || props.value}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -88,7 +88,7 @@ export const Stepper = ({ steps }: StepperProps) => {
|
|||||||
'md:mt-0 font-alpha uppercase text-black dark:text-white',
|
'md:mt-0 font-alpha uppercase text-black dark:text-white',
|
||||||
{
|
{
|
||||||
'mt-2 text-md md:text-2xl': isActive,
|
'mt-2 text-md md:text-2xl': isActive,
|
||||||
'mt-4 text-sm md:text-lg md:ml-8': !isActive,
|
'mt-4 text-sm md:text-lg md:ml-2': !isActive,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
export * from './traffic-light';
|
@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
interface TrafficLightProps {
|
||||||
|
value: number;
|
||||||
|
q1: number;
|
||||||
|
q2: number;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TrafficLight = ({
|
||||||
|
value,
|
||||||
|
q1,
|
||||||
|
q2,
|
||||||
|
children,
|
||||||
|
}: TrafficLightProps) => {
|
||||||
|
const slippageClassName = classNames({
|
||||||
|
'text-darkerGreen dark:text-lightGreen': value < q1,
|
||||||
|
'text-amber': value >= q1 && value < q2,
|
||||||
|
'text-vega-red': value >= q2,
|
||||||
|
});
|
||||||
|
|
||||||
|
return <div className={slippageClassName}>{children || value}</div>;
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user