chore(trading): orderbook adjustments (#4293)
Co-authored-by: Ben <ben@vega.xyz> Co-authored-by: Mikołaj Młodzikowski <mikolaj.mlodzikowski@gmail.com> Co-authored-by: Joe Tsang <30622993+jtsang586@users.noreply.github.com> Co-authored-by: Dexter Edwards <dexter.edwards93@gmail.com> Co-authored-by: m.ray <16125548+MadalinaRaicu@users.noreply.github.com> Co-authored-by: Art <artur@vegaprotocol.io> Co-authored-by: Bartłomiej Głownia <bglownia@gmail.com> Co-authored-by: Gordsport <83510148+gordsport@users.noreply.github.com>
This commit is contained in:
parent
ce6873fe54
commit
5b8df4c414
@ -101,13 +101,14 @@ describe('order book', { tags: '@smoke' }, () => {
|
|||||||
'1,000',
|
'1,000',
|
||||||
'10,000',
|
'10,000',
|
||||||
];
|
];
|
||||||
cy.getByTestId(priceResolution)
|
cy.getByTestId(priceResolution).click();
|
||||||
.find('option')
|
cy.get('[role="menu"]')
|
||||||
|
.find('[role="menuitem"]')
|
||||||
.each(($el, index) => {
|
.each(($el, index) => {
|
||||||
expect($el.text()).to.equal(resolutions[index]);
|
expect($el.text()).to.equal(resolutions[index]);
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.getByTestId(priceResolution).select('0.0');
|
cy.get('[role="menuitem"]').eq(4).click();
|
||||||
cy.getByTestId(resPrice).should('have.text', '99.0');
|
cy.getByTestId(resPrice).should('have.text', '99.0');
|
||||||
cy.getByTestId(askPrice).should('not.exist');
|
cy.getByTestId(askPrice).should('not.exist');
|
||||||
cy.getByTestId(bidPrice).should('not.exist');
|
cy.getByTestId(bidPrice).should('not.exist');
|
||||||
|
@ -13,8 +13,12 @@ interface OrderbookRowProps {
|
|||||||
price: string;
|
price: string;
|
||||||
onClick?: (args: { price?: string; size?: string }) => void;
|
onClick?: (args: { price?: string; size?: string }) => void;
|
||||||
type: VolumeType;
|
type: VolumeType;
|
||||||
|
width: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const HIDE_VOL_WIDTH = 150;
|
||||||
|
const HIDE_CUMULATIVE_VOL_WIDTH = 220;
|
||||||
|
|
||||||
const CumulationBar = ({
|
const CumulationBar = ({
|
||||||
cumulativeValue = 0,
|
cumulativeValue = 0,
|
||||||
type,
|
type,
|
||||||
@ -26,7 +30,7 @@ const CumulationBar = ({
|
|||||||
<div
|
<div
|
||||||
data-testid={`${VolumeType.bid === type ? 'bid' : 'ask'}-bar`}
|
data-testid={`${VolumeType.bid === type ? 'bid' : 'ask'}-bar`}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'absolute top-0 left-0 h-full transition-all',
|
'absolute top-0 left-0 h-full',
|
||||||
type === VolumeType.bid
|
type === VolumeType.bid
|
||||||
? 'bg-market-green-300 dark:bg-market-green/50'
|
? 'bg-market-green-300 dark:bg-market-green/50'
|
||||||
: 'bg-market-red-300 dark:bg-market-red/30'
|
: 'bg-market-red-300 dark:bg-market-red/30'
|
||||||
@ -90,12 +94,18 @@ export const OrderbookRow = React.memo(
|
|||||||
price,
|
price,
|
||||||
onClick,
|
onClick,
|
||||||
type,
|
type,
|
||||||
|
width,
|
||||||
}: OrderbookRowProps) => {
|
}: OrderbookRowProps) => {
|
||||||
const txtId = type === VolumeType.bid ? 'bid' : 'ask';
|
const txtId = type === VolumeType.bid ? 'bid' : 'ask';
|
||||||
|
const cols =
|
||||||
|
width >= HIDE_CUMULATIVE_VOL_WIDTH ? 3 : width >= HIDE_VOL_WIDTH ? 2 : 1;
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative pr-1">
|
||||||
<CumulationBar cumulativeValue={cumulativeRelativeValue} type={type} />
|
<CumulationBar cumulativeValue={cumulativeRelativeValue} type={type} />
|
||||||
<div className="grid gap-1 text-right grid-cols-3">
|
<div
|
||||||
|
data-testid={`${txtId}-rows-container`}
|
||||||
|
className={classNames('grid gap-1 text-right', `grid-cols-${cols}`)}
|
||||||
|
>
|
||||||
<PriceCell
|
<PriceCell
|
||||||
testId={`price-${price}`}
|
testId={`price-${price}`}
|
||||||
value={BigInt(price)}
|
value={BigInt(price)}
|
||||||
@ -109,6 +119,7 @@ export const OrderbookRow = React.memo(
|
|||||||
: 'text-market-green-600 dark:text-market-green'
|
: 'text-market-green-600 dark:text-market-green'
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
{width >= HIDE_VOL_WIDTH && (
|
||||||
<PriceCell
|
<PriceCell
|
||||||
testId={`${txtId}-vol-${price}`}
|
testId={`${txtId}-vol-${price}`}
|
||||||
onClick={(value) =>
|
onClick={(value) =>
|
||||||
@ -124,6 +135,8 @@ export const OrderbookRow = React.memo(
|
|||||||
positionDecimalPlaces
|
positionDecimalPlaces
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
{width >= HIDE_CUMULATIVE_VOL_WIDTH && (
|
||||||
<CumulativeVol
|
<CumulativeVol
|
||||||
testId={`cumulative-vol-${price}`}
|
testId={`cumulative-vol-${price}`}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
@ -136,6 +149,7 @@ export const OrderbookRow = React.memo(
|
|||||||
positionDecimalPlaces={positionDecimalPlaces}
|
positionDecimalPlaces={positionDecimalPlaces}
|
||||||
cumulativeValue={cumulativeValue}
|
cumulativeValue={cumulativeValue}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { render, fireEvent, waitFor, screen } from '@testing-library/react';
|
import { render, waitFor, screen } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
import { generateMockData, VolumeType } from './orderbook-data';
|
import { generateMockData, VolumeType } from './orderbook-data';
|
||||||
import { Orderbook } from './orderbook';
|
import { Orderbook } from './orderbook';
|
||||||
import * as orderbookData from './orderbook-data';
|
import * as orderbookData from './orderbook-data';
|
||||||
@ -33,6 +34,7 @@ describe('Orderbook', () => {
|
|||||||
const decimalPlaces = 3;
|
const decimalPlaces = 3;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
mockOffsetSize(800, 768);
|
mockOffsetSize(800, 768);
|
||||||
});
|
});
|
||||||
it('markPrice should be in the middle', async () => {
|
it('markPrice should be in the middle', async () => {
|
||||||
@ -69,12 +71,17 @@ describe('Orderbook', () => {
|
|||||||
await screen.findByTestId(`middle-mark-price-${params.midPrice}`)
|
await screen.findByTestId(`middle-mark-price-${params.midPrice}`)
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
// Before resolution change the price is 122.934
|
// Before resolution change the price is 122.934
|
||||||
await fireEvent.click(await screen.getByTestId('price-122901'));
|
await userEvent.click(await screen.getByTestId('price-122901'));
|
||||||
expect(onClickSpy).toBeCalledWith({ price: '122.901' });
|
expect(onClickSpy).toBeCalledWith({ price: '122.901' });
|
||||||
const resolutionSelect = screen.getByTestId(
|
|
||||||
'resolution'
|
await userEvent.click(screen.getByTestId('resolution'));
|
||||||
) as HTMLSelectElement;
|
|
||||||
await fireEvent.change(resolutionSelect, { target: { value: '10' } });
|
await waitFor(() => {
|
||||||
|
expect(screen.getByRole('menu')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
await userEvent.click(screen.getAllByRole('menuitem')[1]);
|
||||||
|
|
||||||
expect(orderbookData.compactRows).toHaveBeenCalledWith(
|
expect(orderbookData.compactRows).toHaveBeenCalledWith(
|
||||||
mockedData.bids,
|
mockedData.bids,
|
||||||
VolumeType.bid,
|
VolumeType.bid,
|
||||||
@ -85,7 +92,88 @@ describe('Orderbook', () => {
|
|||||||
VolumeType.ask,
|
VolumeType.ask,
|
||||||
10
|
10
|
||||||
);
|
);
|
||||||
await fireEvent.click(await screen.getByTestId('price-12294'));
|
await userEvent.click(await screen.getByTestId('price-12294'));
|
||||||
expect(onClickSpy).toBeCalledWith({ price: '122.94' });
|
expect(onClickSpy).toBeCalledWith({ price: '122.94' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('plus - minus buttons should change resolution', async () => {
|
||||||
|
const onClickSpy = jest.fn();
|
||||||
|
jest.spyOn(orderbookData, 'compactRows');
|
||||||
|
const mockedData = generateMockData(params);
|
||||||
|
render(
|
||||||
|
<Orderbook
|
||||||
|
decimalPlaces={decimalPlaces}
|
||||||
|
positionDecimalPlaces={0}
|
||||||
|
onClick={onClickSpy}
|
||||||
|
{...mockedData}
|
||||||
|
assetSymbol="USD"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect((orderbookData.compactRows as jest.Mock).mock.lastCall[2]).toEqual(
|
||||||
|
1
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId('minus-button')).toBeDisabled();
|
||||||
|
userEvent.click(screen.getByTestId('plus-button'));
|
||||||
|
await waitFor(() => {
|
||||||
|
expect((orderbookData.compactRows as jest.Mock).mock.lastCall[2]).toEqual(
|
||||||
|
10
|
||||||
|
);
|
||||||
|
});
|
||||||
|
expect(screen.getByTestId('minus-button')).not.toBeDisabled();
|
||||||
|
userEvent.click(screen.getByTestId('minus-button'));
|
||||||
|
await waitFor(() => {
|
||||||
|
expect((orderbookData.compactRows as jest.Mock).mock.lastCall[2]).toEqual(
|
||||||
|
1
|
||||||
|
);
|
||||||
|
});
|
||||||
|
expect(screen.getByTestId('minus-button')).toBeDisabled();
|
||||||
|
await userEvent.click(screen.getByTestId('resolution'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByRole('menu')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
await userEvent.click(screen.getAllByRole('menuitem')[5]);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect((orderbookData.compactRows as jest.Mock).mock.lastCall[2]).toEqual(
|
||||||
|
100000
|
||||||
|
);
|
||||||
|
});
|
||||||
|
expect(screen.getByTestId('plus-button')).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('two columns', () => {
|
||||||
|
mockOffsetSize(200, 768);
|
||||||
|
const onClickSpy = jest.fn();
|
||||||
|
const mockedData = generateMockData(params);
|
||||||
|
render(
|
||||||
|
<Orderbook
|
||||||
|
decimalPlaces={decimalPlaces}
|
||||||
|
positionDecimalPlaces={0}
|
||||||
|
onClick={onClickSpy}
|
||||||
|
{...mockedData}
|
||||||
|
assetSymbol="USD"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
screen.getAllByTestId('bid-rows-container').forEach((item) => {
|
||||||
|
expect(item).toHaveClass('grid-cols-2');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('one column', () => {
|
||||||
|
mockOffsetSize(140, 768);
|
||||||
|
const onClickSpy = jest.fn();
|
||||||
|
const mockedData = generateMockData(params);
|
||||||
|
render(
|
||||||
|
<Orderbook
|
||||||
|
decimalPlaces={decimalPlaces}
|
||||||
|
positionDecimalPlaces={0}
|
||||||
|
onClick={onClickSpy}
|
||||||
|
{...mockedData}
|
||||||
|
assetSymbol="USD"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
screen.getAllByTestId('ask-rows-container').forEach((item) => {
|
||||||
|
expect(item).toHaveClass('grid-cols-1');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,16 +1,25 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo, useRef, useState } from 'react';
|
||||||
import ReactVirtualizedAutoSizer from 'react-virtualized-auto-sizer';
|
import ReactVirtualizedAutoSizer from 'react-virtualized-auto-sizer';
|
||||||
import {
|
import {
|
||||||
addDecimalsFormatNumber,
|
addDecimalsFormatNumber,
|
||||||
formatNumberFixed,
|
formatNumberFixed,
|
||||||
} from '@vegaprotocol/utils';
|
} from '@vegaprotocol/utils';
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
|
import { usePrevious } from '@vegaprotocol/react-helpers';
|
||||||
import { OrderbookRow } from './orderbook-row';
|
import { OrderbookRow } from './orderbook-row';
|
||||||
import type { OrderbookRowData } from './orderbook-data';
|
import type { OrderbookRowData } from './orderbook-data';
|
||||||
import { compactRows, VolumeType } from './orderbook-data';
|
import { compactRows, VolumeType } from './orderbook-data';
|
||||||
import { Splash } from '@vegaprotocol/ui-toolkit';
|
import {
|
||||||
|
Button,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
Splash,
|
||||||
|
VegaIcon,
|
||||||
|
VegaIconNames,
|
||||||
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useState } from 'react';
|
|
||||||
import type { PriceLevelFieldsFragment } from './__generated__/MarketDepth';
|
import type { PriceLevelFieldsFragment } from './__generated__/MarketDepth';
|
||||||
|
|
||||||
// Sets row height, will be used to calculate number of rows that can be
|
// Sets row height, will be used to calculate number of rows that can be
|
||||||
@ -19,6 +28,19 @@ export const rowHeight = 17;
|
|||||||
const rowGap = 1;
|
const rowGap = 1;
|
||||||
const midHeight = 30;
|
const midHeight = 30;
|
||||||
|
|
||||||
|
type PriceChange = 'up' | 'down' | 'none';
|
||||||
|
|
||||||
|
const PRICE_CHANGE_ICON_MAP: Readonly<Record<PriceChange, VegaIconNames>> = {
|
||||||
|
up: VegaIconNames.ARROW_UP,
|
||||||
|
down: VegaIconNames.ARROW_DOWN,
|
||||||
|
none: VegaIconNames.BULLET,
|
||||||
|
};
|
||||||
|
const PRICE_CHANGE_CLASS_MAP: Readonly<Record<PriceChange, string>> = {
|
||||||
|
up: 'text-market-green-600 dark:text-market-green',
|
||||||
|
down: 'text-market-red dark:text-market-red',
|
||||||
|
none: 'text-vega-blue-500',
|
||||||
|
};
|
||||||
|
|
||||||
const OrderbookTable = ({
|
const OrderbookTable = ({
|
||||||
rows,
|
rows,
|
||||||
resolution,
|
resolution,
|
||||||
@ -26,6 +48,7 @@ const OrderbookTable = ({
|
|||||||
decimalPlaces,
|
decimalPlaces,
|
||||||
positionDecimalPlaces,
|
positionDecimalPlaces,
|
||||||
onClick,
|
onClick,
|
||||||
|
width,
|
||||||
}: {
|
}: {
|
||||||
rows: OrderbookRowData[];
|
rows: OrderbookRowData[];
|
||||||
resolution: number;
|
resolution: number;
|
||||||
@ -33,6 +56,7 @@ const OrderbookTable = ({
|
|||||||
positionDecimalPlaces: number;
|
positionDecimalPlaces: number;
|
||||||
type: VolumeType;
|
type: VolumeType;
|
||||||
onClick?: (args: { price?: string; size?: string }) => void;
|
onClick?: (args: { price?: string; size?: string }) => void;
|
||||||
|
width: number;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -59,6 +83,7 @@ const OrderbookTable = ({
|
|||||||
cumulativeValue={data.cumulativeVol.value}
|
cumulativeValue={data.cumulativeVol.value}
|
||||||
cumulativeRelativeValue={data.cumulativeVol.relativeValue}
|
cumulativeRelativeValue={data.cumulativeVol.relativeValue}
|
||||||
type={type}
|
type={type}
|
||||||
|
width={width}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -99,12 +124,50 @@ export const Orderbook = ({
|
|||||||
const groupedBids = useMemo(() => {
|
const groupedBids = useMemo(() => {
|
||||||
return compactRows(bids, VolumeType.bid, resolution);
|
return compactRows(bids, VolumeType.bid, resolution);
|
||||||
}, [bids, resolution]);
|
}, [bids, resolution]);
|
||||||
|
const [isOpen, setOpen] = useState(false);
|
||||||
|
const previousMidPrice = usePrevious(midPrice);
|
||||||
|
const priceChangeRef = useRef<'up' | 'down' | 'none'>('none');
|
||||||
|
if (midPrice && previousMidPrice !== midPrice) {
|
||||||
|
priceChangeRef.current =
|
||||||
|
(previousMidPrice || '') > midPrice ? 'down' : 'up';
|
||||||
|
}
|
||||||
|
|
||||||
|
const priceChangeIcon = (
|
||||||
|
<span
|
||||||
|
className={classNames(PRICE_CHANGE_CLASS_MAP[priceChangeRef.current])}
|
||||||
|
>
|
||||||
|
<VegaIcon name={PRICE_CHANGE_ICON_MAP[priceChangeRef.current]} />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
const formatResolution = (r: number) => {
|
||||||
|
return formatNumberFixed(
|
||||||
|
Math.log10(r) - decimalPlaces > 0
|
||||||
|
? Math.pow(10, Math.log10(r) - decimalPlaces)
|
||||||
|
: 0,
|
||||||
|
decimalPlaces - Math.log10(r)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const increaseResolution = () => {
|
||||||
|
const index = resolutions.indexOf(resolution);
|
||||||
|
if (index < resolutions.length - 1) {
|
||||||
|
setResolution(resolutions[index + 1]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const decreaseResolution = () => {
|
||||||
|
const index = resolutions.indexOf(resolution);
|
||||||
|
if (index > 0) {
|
||||||
|
setResolution(resolutions[index - 1]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full pl-1 text-xs grid grid-rows-[1fr_min-content]">
|
<div className="h-full pl-1 text-xs grid grid-rows-[1fr_min-content]">
|
||||||
<div>
|
<div>
|
||||||
<ReactVirtualizedAutoSizer disableWidth>
|
<ReactVirtualizedAutoSizer>
|
||||||
{({ height }) => {
|
{({ width, height }) => {
|
||||||
const limit = Math.max(
|
const limit = Math.max(
|
||||||
1,
|
1,
|
||||||
Math.floor((height - midHeight) / 2 / (rowHeight + rowGap))
|
Math.floor((height - midHeight) / 2 / (rowHeight + rowGap))
|
||||||
@ -116,6 +179,7 @@ export const Orderbook = ({
|
|||||||
className="overflow-hidden grid"
|
className="overflow-hidden grid"
|
||||||
data-testid="orderbook-grid-element"
|
data-testid="orderbook-grid-element"
|
||||||
style={{
|
style={{
|
||||||
|
width: width + 'px',
|
||||||
height: height + 'px',
|
height: height + 'px',
|
||||||
gridTemplateRows: `1fr ${midHeight}px 1fr`, // cannot use tailwind here as tailwind will not parse a class string with interpolation
|
gridTemplateRows: `1fr ${midHeight}px 1fr`, // cannot use tailwind here as tailwind will not parse a class string with interpolation
|
||||||
}}
|
}}
|
||||||
@ -129,6 +193,7 @@ export const Orderbook = ({
|
|||||||
decimalPlaces={decimalPlaces}
|
decimalPlaces={decimalPlaces}
|
||||||
positionDecimalPlaces={positionDecimalPlaces}
|
positionDecimalPlaces={positionDecimalPlaces}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
width={width}
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2">
|
||||||
{midPrice && (
|
{midPrice && (
|
||||||
@ -140,6 +205,7 @@ export const Orderbook = ({
|
|||||||
{addDecimalsFormatNumber(midPrice, decimalPlaces)}
|
{addDecimalsFormatNumber(midPrice, decimalPlaces)}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-base">{assetSymbol}</span>
|
<span className="text-base">{assetSymbol}</span>
|
||||||
|
{priceChangeIcon}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -150,6 +216,7 @@ export const Orderbook = ({
|
|||||||
decimalPlaces={decimalPlaces}
|
decimalPlaces={decimalPlaces}
|
||||||
positionDecimalPlaces={positionDecimalPlaces}
|
positionDecimalPlaces={positionDecimalPlaces}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
width={width}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@ -162,26 +229,61 @@ export const Orderbook = ({
|
|||||||
}}
|
}}
|
||||||
</ReactVirtualizedAutoSizer>
|
</ReactVirtualizedAutoSizer>
|
||||||
</div>
|
</div>
|
||||||
<div className="border-t border-default">
|
<div className="border-t border-default flex">
|
||||||
<select
|
<Button
|
||||||
onChange={(e) => {
|
onClick={increaseResolution}
|
||||||
setResolution(Number(e.currentTarget.value));
|
size="xs"
|
||||||
}}
|
disabled={resolutions.indexOf(resolution) >= resolutions.length - 1}
|
||||||
value={resolution}
|
className="text-black dark:text-white rounded-none border-y-0 border-l-0 flex items-center border-r-1"
|
||||||
className="block bg-neutral-100 dark:bg-neutral-700 font-mono text-right"
|
data-testid="plus-button"
|
||||||
data-testid="resolution"
|
|
||||||
>
|
>
|
||||||
|
<VegaIcon size={12} name={VegaIconNames.PLUS} />
|
||||||
|
</Button>
|
||||||
|
<DropdownMenu
|
||||||
|
open={isOpen}
|
||||||
|
onOpenChange={(open) => setOpen(open)}
|
||||||
|
trigger={
|
||||||
|
<DropdownMenuTrigger
|
||||||
|
data-testid="resolution"
|
||||||
|
className="flex justify-between px-1 items-center"
|
||||||
|
style={{
|
||||||
|
width: `${
|
||||||
|
Math.max.apply(
|
||||||
|
null,
|
||||||
|
resolutions.map((item) => formatResolution(item).length)
|
||||||
|
) + 3
|
||||||
|
}ch`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<VegaIcon
|
||||||
|
size={12}
|
||||||
|
name={
|
||||||
|
isOpen ? VegaIconNames.CHEVRON_UP : VegaIconNames.CHEVRON_DOWN
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div className="text-xs text-left">
|
||||||
|
{formatResolution(resolution)}
|
||||||
|
</div>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<DropdownMenuContent align="start">
|
||||||
{resolutions.map((r) => (
|
{resolutions.map((r) => (
|
||||||
<option key={r} value={r}>
|
<DropdownMenuItem key={r} onClick={() => setResolution(r)}>
|
||||||
{formatNumberFixed(
|
{formatResolution(r)}
|
||||||
Math.log10(r) - decimalPlaces > 0
|
</DropdownMenuItem>
|
||||||
? Math.pow(10, Math.log10(r) - decimalPlaces)
|
|
||||||
: 0,
|
|
||||||
decimalPlaces - Math.log10(r)
|
|
||||||
)}
|
|
||||||
</option>
|
|
||||||
))}
|
))}
|
||||||
</select>
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
<Button
|
||||||
|
onClick={decreaseResolution}
|
||||||
|
size="xs"
|
||||||
|
disabled={resolutions.indexOf(resolution) <= 0}
|
||||||
|
className="text-black dark:text-white rounded-none border-y-0 border-l-1 flex items-center"
|
||||||
|
data-testid="minus-button"
|
||||||
|
>
|
||||||
|
<VegaIcon size={12} name={VegaIconNames.MINUS} />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
export const IconArrowUp = ({ size = 16 }: { size: number }) => {
|
||||||
|
return (
|
||||||
|
<svg width={size} height={size} viewBox="0 0 16 16">
|
||||||
|
<path
|
||||||
|
d="M 7.47,3.63
|
||||||
|
C 7.47,3.63 2.37,8.72 2.37,8.72
|
||||||
|
2.37,8.72 1.63,7.98 1.63,7.98
|
||||||
|
1.63,7.98 8.00,1.60 8.00,1.60
|
||||||
|
8.00,1.60 14.37,7.98 14.37,7.98
|
||||||
|
14.37,7.98 13.63,8.72 13.63,8.72
|
||||||
|
13.63,8.72 8.53,3.63 8.53,3.63
|
||||||
|
8.53,3.63 8.53,14.35 8.53,14.35
|
||||||
|
8.53,14.35 7.47,14.35 7.47,14.35
|
||||||
|
7.47,14.35 7.47,3.63 7.47,3.63 Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,7 @@
|
|||||||
|
export const IconBullet = ({ size = 16 }: { size: number }) => {
|
||||||
|
return (
|
||||||
|
<svg width={size} height={size} viewBox="0 0 16 16">
|
||||||
|
<circle cx="8" cy="8" r="6" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,13 @@
|
|||||||
|
export const IconMinus = ({ size = 16 }: { size: number }) => {
|
||||||
|
return (
|
||||||
|
<svg width={size} height={size} viewBox="0 0 16 16">
|
||||||
|
<path
|
||||||
|
d="M 0.92,8.58
|
||||||
|
C 0.92,8.58 0.92,7.48 0.92,7.48
|
||||||
|
0.92,7.48 15.01,7.48 15.01,7.48
|
||||||
|
15.01,7.48 15.01,8.58 15.01,8.58
|
||||||
|
15.01,8.58 0.92,8.58 0.92,8.58 Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,21 @@
|
|||||||
|
export const IconPlus = ({ size = 16 }: { size: number }) => {
|
||||||
|
return (
|
||||||
|
<svg width={size} height={size} viewBox="0 0 16 16">
|
||||||
|
<path
|
||||||
|
d="M 7.43,15.24
|
||||||
|
C 7.43,15.24 7.43,8.58 7.43,8.58
|
||||||
|
7.43,8.58 0.92,8.58 0.92,8.58
|
||||||
|
0.92,8.58 0.92,7.48 0.92,7.48
|
||||||
|
0.92,7.48 7.43,7.48 7.43,7.48
|
||||||
|
7.43,7.48 7.43,0.85 7.43,0.85
|
||||||
|
7.43,0.85 8.48,0.85 8.48,0.85
|
||||||
|
8.48,0.85 8.48,7.48 8.48,7.48
|
||||||
|
8.48,7.48 15.01,7.48 15.01,7.48
|
||||||
|
15.01,7.48 15.01,8.58 15.01,8.58
|
||||||
|
15.01,8.58 8.48,8.58 8.48,8.58
|
||||||
|
8.48,8.58 8.48,15.24 8.48,15.24
|
||||||
|
8.48,15.24 7.43,15.24 7.43,15.24 Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
@ -1,6 +1,8 @@
|
|||||||
import { IconArrowDown } from './svg-icons/icon-arrow-down';
|
import { IconArrowDown } from './svg-icons/icon-arrow-down';
|
||||||
|
import { IconArrowUp } from './svg-icons/icon-arrow-up';
|
||||||
import { IconArrowRight } from './svg-icons/icon-arrow-right';
|
import { IconArrowRight } from './svg-icons/icon-arrow-right';
|
||||||
import { IconBreakdown } from './svg-icons/icon-breakdown';
|
import { IconBreakdown } from './svg-icons/icon-breakdown';
|
||||||
|
import { IconBullet } from './svg-icons/icon-bullet';
|
||||||
import { IconChevronDown } from './svg-icons/icon-chevron-down';
|
import { IconChevronDown } from './svg-icons/icon-chevron-down';
|
||||||
import { IconChevronLeft } from './svg-icons/icon-chevron-left';
|
import { IconChevronLeft } from './svg-icons/icon-chevron-left';
|
||||||
import { IconChevronUp } from './svg-icons/icon-chevron-up';
|
import { IconChevronUp } from './svg-icons/icon-chevron-up';
|
||||||
@ -13,9 +15,11 @@ import { IconGlobe } from './svg-icons/icon-globe';
|
|||||||
import { IconInfo } from './svg-icons/icon-info';
|
import { IconInfo } from './svg-icons/icon-info';
|
||||||
import { IconKebab } from './svg-icons/icon-kebab';
|
import { IconKebab } from './svg-icons/icon-kebab';
|
||||||
import { IconLinkedIn } from './svg-icons/icon-linkedin';
|
import { IconLinkedIn } from './svg-icons/icon-linkedin';
|
||||||
|
import { IconMinus } from './svg-icons/icon-minus';
|
||||||
import { IconMoon } from './svg-icons/icon-moon';
|
import { IconMoon } from './svg-icons/icon-moon';
|
||||||
import { IconOpenExternal } from './svg-icons/icon-open-external';
|
import { IconOpenExternal } from './svg-icons/icon-open-external';
|
||||||
import { IconQuestionMark } from './svg-icons/icon-question-mark';
|
import { IconQuestionMark } from './svg-icons/icon-question-mark';
|
||||||
|
import { IconPlus } from './svg-icons/icon-plus';
|
||||||
import { IconTick } from './svg-icons/icon-tick';
|
import { IconTick } from './svg-icons/icon-tick';
|
||||||
import { IconTransfer } from './svg-icons/icon-transfer';
|
import { IconTransfer } from './svg-icons/icon-transfer';
|
||||||
import { IconTrendUp } from './svg-icons/icon-trend-up';
|
import { IconTrendUp } from './svg-icons/icon-trend-up';
|
||||||
@ -24,8 +28,10 @@ import { IconWithdraw } from './svg-icons/icon-withdraw';
|
|||||||
|
|
||||||
export enum VegaIconNames {
|
export enum VegaIconNames {
|
||||||
ARROW_DOWN = 'arrow-down',
|
ARROW_DOWN = 'arrow-down',
|
||||||
|
ARROW_UP = 'arrow-up',
|
||||||
ARROW_RIGHT = 'arrow-right',
|
ARROW_RIGHT = 'arrow-right',
|
||||||
BREAKDOWN = 'breakdown',
|
BREAKDOWN = 'breakdown',
|
||||||
|
BULLET = 'bullet',
|
||||||
CHEVRON_DOWN = 'chevron-down',
|
CHEVRON_DOWN = 'chevron-down',
|
||||||
CHEVRON_LEFT = 'chevron-left',
|
CHEVRON_LEFT = 'chevron-left',
|
||||||
CHEVRON_UP = 'chevron-up',
|
CHEVRON_UP = 'chevron-up',
|
||||||
@ -38,9 +44,11 @@ export enum VegaIconNames {
|
|||||||
INFO = 'info',
|
INFO = 'info',
|
||||||
KEBAB = 'kebab',
|
KEBAB = 'kebab',
|
||||||
LINKEDIN = 'linkedin',
|
LINKEDIN = 'linkedin',
|
||||||
|
MINUS = 'minus',
|
||||||
MOON = 'moon',
|
MOON = 'moon',
|
||||||
OPEN_EXTERNAL = 'open-external',
|
OPEN_EXTERNAL = 'open-external',
|
||||||
QUESTION_MARK = 'question-mark',
|
QUESTION_MARK = 'question-mark',
|
||||||
|
PLUS = 'plus',
|
||||||
TICK = 'tick',
|
TICK = 'tick',
|
||||||
TRANSFER = 'transfer',
|
TRANSFER = 'transfer',
|
||||||
TREND_UP = 'trend-up',
|
TREND_UP = 'trend-up',
|
||||||
@ -53,6 +61,7 @@ export const VegaIconNameMap: Record<
|
|||||||
({ size }: { size: number }) => JSX.Element
|
({ size }: { size: number }) => JSX.Element
|
||||||
> = {
|
> = {
|
||||||
'arrow-down': IconArrowDown,
|
'arrow-down': IconArrowDown,
|
||||||
|
'arrow-up': IconArrowUp,
|
||||||
'arrow-right': IconArrowRight,
|
'arrow-right': IconArrowRight,
|
||||||
'chevron-down': IconChevronDown,
|
'chevron-down': IconChevronDown,
|
||||||
'chevron-left': IconChevronLeft,
|
'chevron-left': IconChevronLeft,
|
||||||
@ -61,6 +70,7 @@ export const VegaIconNameMap: Record<
|
|||||||
'question-mark': IconQuestionMark,
|
'question-mark': IconQuestionMark,
|
||||||
'trend-up': IconTrendUp,
|
'trend-up': IconTrendUp,
|
||||||
breakdown: IconBreakdown,
|
breakdown: IconBreakdown,
|
||||||
|
bullet: IconBullet,
|
||||||
copy: IconCopy,
|
copy: IconCopy,
|
||||||
cross: IconCross,
|
cross: IconCross,
|
||||||
deposit: IconDeposit,
|
deposit: IconDeposit,
|
||||||
@ -70,7 +80,9 @@ export const VegaIconNameMap: Record<
|
|||||||
info: IconInfo,
|
info: IconInfo,
|
||||||
kebab: IconKebab,
|
kebab: IconKebab,
|
||||||
linkedin: IconLinkedIn,
|
linkedin: IconLinkedIn,
|
||||||
|
minus: IconMinus,
|
||||||
moon: IconMoon,
|
moon: IconMoon,
|
||||||
|
plus: IconPlus,
|
||||||
tick: IconTick,
|
tick: IconTick,
|
||||||
transfer: IconTransfer,
|
transfer: IconTransfer,
|
||||||
twitter: IconTwitter,
|
twitter: IconTwitter,
|
||||||
|
@ -23,6 +23,7 @@ export const Tabs = ({
|
|||||||
}
|
}
|
||||||
return children[0].props.id;
|
return children[0].props.id;
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TabsPrimitive.Root
|
<TabsPrimitive.Root
|
||||||
{...props}
|
{...props}
|
||||||
@ -30,7 +31,7 @@ export const Tabs = ({
|
|||||||
onValueChange={onValueChange || setActiveTab}
|
onValueChange={onValueChange || setActiveTab}
|
||||||
className="h-full grid grid-rows-[min-content_1fr]"
|
className="h-full grid grid-rows-[min-content_1fr]"
|
||||||
>
|
>
|
||||||
<div className="border-b border-default">
|
<div className="border-b border-default min-w-0">
|
||||||
<TabsPrimitive.List
|
<TabsPrimitive.List
|
||||||
className="flex flex-nowrap overflow-visible"
|
className="flex flex-nowrap overflow-visible"
|
||||||
role="tablist"
|
role="tablist"
|
||||||
|
Loading…
Reference in New Issue
Block a user