diff --git a/src/components/Canvas.tsx b/src/components/Canvas.tsx new file mode 100644 index 0000000..c36a6aa --- /dev/null +++ b/src/components/Canvas.tsx @@ -0,0 +1,16 @@ +import { forwardRef } from 'react'; + +type StyleProps = { + className?: string; +}; + +type ElementProps = { + width: number; + height: number; +}; + +export const Canvas = forwardRef( + ({ className, width, height }, canvasRef) => ( + + ) +); diff --git a/src/components/Tabs.tsx b/src/components/Tabs.tsx index bb6360e..f6b9cd3 100644 --- a/src/components/Tabs.tsx +++ b/src/components/Tabs.tsx @@ -21,6 +21,7 @@ export type TabItem = { content?: React.ReactNode; subitems?: TabItem[]; customTrigger?: ReactNode; + asChild?: boolean; }; type ElementProps = { @@ -107,13 +108,14 @@ export const Tabs = ({ sharedContent ) : ( - {items.map(({ value, content, forceMount }) => ( + {items.map(({ asChild, value, content, forceMount }) => ( {content} diff --git a/src/hooks/orderbook/index.ts b/src/hooks/orderbook/index.ts new file mode 100644 index 0000000..54c448a --- /dev/null +++ b/src/hooks/orderbook/index.ts @@ -0,0 +1,11 @@ +import { useCalculateOrderbookData } from './useCalculateOrderbookData'; +import { useCenterOrderbook } from './useCenterOrderbook'; +import { useDrawOrderbookHistograms } from './useDrawOrderbookHistograms'; +import { useSpreadRowScrollListener } from './useSpreadRowScrollListener'; + +export { + useCalculateOrderbookData, + useCenterOrderbook, + useDrawOrderbookHistograms, + useSpreadRowScrollListener, +}; diff --git a/src/hooks/orderbook/useCalculateOrderbookData.ts b/src/hooks/orderbook/useCalculateOrderbookData.ts new file mode 100644 index 0000000..ff782f4 --- /dev/null +++ b/src/hooks/orderbook/useCalculateOrderbookData.ts @@ -0,0 +1,55 @@ +import { useMemo } from 'react'; +import { useSelector, shallowEqual } from 'react-redux'; +import { OrderSide } from '@dydxprotocol/v4-client-js'; + +import { OrderbookLine } from '@/constants/abacus'; + +import { getCurrentMarketOrderbook } from '@/state/perpetualsSelectors'; +import { getSubaccountOpenOrdersBySideAndPrice } from '@/state/accountSelectors'; + +import type { RowData } from '@/views/Orderbook/OrderbookRow'; + +import { MustBigNumber } from '@/lib/numbers'; + +export const useCalculateOrderbookData = ({ maxRowsPerSide }: { maxRowsPerSide: number }) => { + const orderbook = useSelector(getCurrentMarketOrderbook, shallowEqual); + + const openOrdersBySideAndPrice = + useSelector(getSubaccountOpenOrdersBySideAndPrice, shallowEqual) || {}; + + return useMemo(() => { + const asks = (orderbook?.asks?.toArray() ?? []) + .map( + (row: OrderbookLine) => + ({ + side: 'ask', + mine: openOrdersBySideAndPrice[OrderSide.SELL]?.[row.price]?.size, + ...row, + } as RowData) + ) + .slice(0, maxRowsPerSide); + + const bids = (orderbook?.bids?.toArray() ?? []) + .map( + (row: OrderbookLine) => + ({ + side: 'bid', + mine: openOrdersBySideAndPrice[OrderSide.BUY]?.[row.price]?.size, + ...row, + } as RowData) + ) + .slice(0, maxRowsPerSide); + + const spread = + asks[0] && bids[0] ? MustBigNumber(asks[0]?.price ?? 0).minus(bids[0]?.price ?? 0) : null; + + const spreadPercent = orderbook?.spreadPercent; + + const histogramRange = Math.max( + Number(bids[bids.length - 1]?.depth), + Number(asks[asks.length - 1]?.depth) + ); + + return { asks, bids, spread, spreadPercent, histogramRange, hasOrderbook: !!orderbook }; + }, [orderbook, openOrdersBySideAndPrice]); +}; diff --git a/src/hooks/orderbook/useCenterOrderbook.ts b/src/hooks/orderbook/useCenterOrderbook.ts new file mode 100644 index 0000000..2247ec8 --- /dev/null +++ b/src/hooks/orderbook/useCenterOrderbook.ts @@ -0,0 +1,19 @@ +import { type RefObject, useEffect } from 'react'; + +type ElementProps = { + marketId: string; + orderbookRef: RefObject; +}; + +/** + * @description Center Orderbook on load and market change + * Assumed that the two sides are the same height + */ +export const useCenterOrderbook = ({ marketId, orderbookRef }: ElementProps) => { + useEffect(() => { + if (orderbookRef.current) { + const { clientHeight, scrollHeight } = orderbookRef.current; + orderbookRef.current.scrollTo({ top: (scrollHeight - clientHeight) / 2 }); + } + }, [orderbookRef.current, marketId]); +}; diff --git a/src/hooks/orderbook/useDrawOrderbookHistograms.ts b/src/hooks/orderbook/useDrawOrderbookHistograms.ts new file mode 100644 index 0000000..fba9a32 --- /dev/null +++ b/src/hooks/orderbook/useDrawOrderbookHistograms.ts @@ -0,0 +1,193 @@ +import { useEffect, useRef, useState } from 'react'; +import { useSelector } from 'react-redux'; + +import type { Nullable } from '@/constants/abacus'; +import { SMALL_USD_DECIMALS, TOKEN_DECIMALS } from '@/constants/numbers'; + +import { getAppTheme } from '@/state/configsSelectors'; + +import type { RowData } from '@/views/Orderbook/OrderbookRow'; + +import { MustBigNumber } from '@/lib/numbers'; + +export const useDrawOrderbookHistograms = ({ + data, + histogramRange, + stepSizeDecimals, + tickSizeDecimals, + to = 'left', + hoveredRow, +}: { + data: RowData[]; + histogramRange: number; + stepSizeDecimals: Nullable; + tickSizeDecimals: Nullable; + to?: 'left' | 'right'; + hoveredRow?: number; +}) => { + const selectedTheme = useSelector(getAppTheme); + const canvasRef = useRef(null); + const canvas = canvasRef.current; + + useEffect(() => { + const ctx = canvas?.getContext('2d'); + + if (!canvas || !ctx) return; + + const devicePixelRatio = window.devicePixelRatio || 1; + + const backingStoreRatio = + // @ts-ignore + ctx.webkitBackingStorePixelRatio || + // @ts-ignore + ctx.mozBackingStorePixelRatio || + // @ts-ignore + ctx.msBackingStorePixelRatio || + // @ts-ignore + ctx.oBackingStorePixelRatio || + // @ts-ignore + ctx.backingStorePixelRatio || + 1; + + const ratio = devicePixelRatio / backingStoreRatio; + + canvas.width = canvas.offsetWidth * ratio; + canvas.height = canvas.offsetHeight * ratio; + + // Scale the context + ctx.scale(ratio, ratio); + + // Clear canvas before redraw + ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); + const canvasWidth = canvas.width / ratio; + const columnX = canvasWidth / 3; + + // Draw histograms + data.forEach(({ depth, mine, price, size, side }, idx) => { + const isHovered = hoveredRow === idx && size !== undefined; + let histogramAlpha = 0.15; + + if (isHovered) { + ctx.fillStyle = 'rgba(255, 255, 255, 0.02)'; + ctx.fillRect(0, idx * 20, canvasWidth, 20); + histogramAlpha = 0.75; + } + + const histogramBarWidth = ctx.canvas.width - 2; + const histogramAccentColor = + side === 'bid' + ? `hsla(159, 67%, 39%, ${histogramAlpha})` + : `hsla(360, 73%, 61%, ${histogramAlpha})`; + + /** + * to === 'right' to === 'left' + * |===================| |===================| + * bar: a b c d + * a = 0 + * b = depthBarWidth + * c = canvasWidth - depthBarWidth + * d = canvasWidth + * + */ + const getXYValues = ({ + barWidth, + gradientMultiplier, + }: { + barWidth: number; + gradientMultiplier: number; + }) => { + const gradient = { + x1: to === 'right' ? barWidth : canvasWidth - barWidth, + x2: + to === 'right' + ? 0 - (canvasWidth * gradientMultiplier - canvasWidth) + : canvasWidth * gradientMultiplier, + }; + const bar = { + x1: to === 'right' ? 0 : canvasWidth - barWidth, + x2: to === 'right' ? Math.min(barWidth, canvasWidth - 2) : canvasWidth - 2, + }; + + return { + bar, + gradient, + }; + }; + + const drawBars = ({ + barWidth, + gradientMultiplier, + }: { + barWidth: number; + gradientMultiplier: number; + }) => { + const { gradient, bar } = getXYValues({ barWidth, gradientMultiplier }); + const linearGradient = ctx.createLinearGradient(gradient.x1, 10, gradient.x2, 10); + linearGradient.addColorStop(0, histogramAccentColor); + linearGradient.addColorStop(1, 'rgba(0, 0, 0, 0)'); + ctx.fillStyle = linearGradient; + ctx.beginPath(); + + ctx.roundRect + ? ctx.roundRect?.( + bar.x1, + idx * 20 + 1, + bar.x2, + 18, + to === 'left' ? [2, 0, 0, 2] : [0, 2, 2, 0] + ) + : ctx.rect(bar.x1, idx * 20 + 1, bar.x2, 18); + ctx.fill(); + }; + + // Depth Bar + const depthBarWidth = depth ? (depth / histogramRange) * histogramBarWidth : 0; + drawBars({ barWidth: depthBarWidth, gradientMultiplier: 2 }); + + // Size Bar + const sizeBarWidth = size ? (size / histogramRange) * histogramBarWidth : 0; + drawBars({ barWidth: sizeBarWidth, gradientMultiplier: 5 }); + + const drawText = () => { + ctx.font = `12.5px Satoshi`; + ctx.textAlign = 'right'; + ctx.textBaseline = 'middle'; + ctx.imageSmoothingQuality = 'high'; + + // Size text + if (size) { + ctx.fillStyle = 'white'; + ctx.fillText( + MustBigNumber(size).toFixed(stepSizeDecimals ?? TOKEN_DECIMALS), + columnX - 8, + idx * 20 + 10 + ); + } + + // Price text + if (price) { + ctx.fillStyle = 'white'; + ctx.fillText( + MustBigNumber(price).toFixed(tickSizeDecimals ?? SMALL_USD_DECIMALS), + columnX * 2 - 8, + idx * 20 + 10 + ); + } + + // Mine text + if (mine) { + ctx.fillStyle = 'white'; + ctx.fillText( + MustBigNumber(mine).toFixed(stepSizeDecimals ?? TOKEN_DECIMALS), + columnX * 3 - 8, + idx * 20 + 10 + ); + } + }; + + drawText(); + }); + }, [data, histogramRange, hoveredRow, selectedTheme, stepSizeDecimals, tickSizeDecimals, to]); + + return { canvasRef }; +}; diff --git a/src/hooks/orderbook/useSpreadRowScrollListener.ts b/src/hooks/orderbook/useSpreadRowScrollListener.ts new file mode 100644 index 0000000..50712e6 --- /dev/null +++ b/src/hooks/orderbook/useSpreadRowScrollListener.ts @@ -0,0 +1,40 @@ +import { type RefObject, useEffect, useState } from 'react'; + +export const useSpreadRowScrollListener = ({ + orderbookRef, + spreadRowRef, +}: { + orderbookRef: RefObject; + spreadRowRef: RefObject; +}) => { + const [displaySide, setDisplaySide] = useState(); + + useEffect(() => { + const onScroll = () => { + if (spreadRowRef.current) { + const { top } = spreadRowRef.current.getBoundingClientRect(); + const { scrollHeight, scrollTop } = orderbookRef.current || {}; + + if (scrollHeight !== undefined && scrollTop !== undefined) { + if (top > scrollHeight / 2) { + setDisplaySide('bottom'); + } else if (scrollTop > scrollHeight / 2) { + setDisplaySide('top'); + } else { + setDisplaySide(undefined); + } + } else { + setDisplaySide(undefined); + } + } + }; + + orderbookRef.current?.addEventListener('scroll', onScroll, false); + + return () => { + orderbookRef.current?.removeEventListener('scroll', onScroll, false); + }; + }, [orderbookRef.current]); + + return { displaySide }; +}; diff --git a/src/pages/trade/VerticalPanel.tsx b/src/pages/trade/VerticalPanel.tsx index d35848c..6e952f2 100644 --- a/src/pages/trade/VerticalPanel.tsx +++ b/src/pages/trade/VerticalPanel.tsx @@ -1,6 +1,4 @@ -import { useEffect, useState } from 'react'; -import { useSelector } from 'react-redux'; -import styled, { AnyStyledComponent } from 'styled-components'; +import { useState } from 'react'; import { TradeLayouts } from '@/constants/layout'; import { STRING_KEYS } from '@/constants/localization'; @@ -9,11 +7,9 @@ import { useStringGetter } from '@/hooks'; import { Tabs } from '@/components/Tabs'; -import { Orderbook, orderbookMixins, OrderbookScrollBehavior } from '@/views/tables/Orderbook'; +import { Orderbook } from '@/views/Orderbook'; import { LiveTrades } from '@/views/tables/LiveTrades'; -import { getCurrentMarketId } from '@/state/perpetualsSelectors'; - enum Tab { Orderbook = 'Orderbook', Trades = 'Trades', @@ -27,29 +23,21 @@ const HISTOGRAM_SIDES_BY_LAYOUT = { export const VerticalPanel = ({ tradeLayout }: { tradeLayout: TradeLayouts }) => { const stringGetter = useStringGetter(); - const [value, setValue] = useState(Tab.Orderbook); - const [scrollBehavior, setScrollBehavior] = useState('snapToCenter'); - - const marketId = useSelector(getCurrentMarketId); - - useEffect(() => { - setScrollBehavior('snapToCenter'); - }, [marketId]); return ( - { - setScrollBehavior('snapToCenter'); setValue(value); }} items={[ { - content: , + content: , label: stringGetter({ key: STRING_KEYS.ORDERBOOK_SHORT }), value: Tab.Orderbook, + asChild: true, }, { content: , @@ -57,26 +45,7 @@ export const VerticalPanel = ({ tradeLayout }: { tradeLayout: TradeLayouts }) => value: Tab.Trades, }, ]} - onWheel={() => setScrollBehavior('free')} withTransitions={false} - isShowingOrderbook={value === Tab.Orderbook} - scrollBehavior={scrollBehavior} - // style={{ - // scrollSnapType: { - // 'snapToCenter': 'y mandatory', - // 'free': 'none', - // 'snapToCenterUnlessHovered': 'none', - // }[scrollBehavior], - // }} /> ); }; - -const Styled: Record = {}; - -Styled.Tabs = styled(Tabs)<{ - isShowingOrderbook: boolean; - scrollBehavior: OrderbookScrollBehavior; -}>` - ${orderbookMixins.scrollArea} -`; diff --git a/src/views/Orderbook.tsx b/src/views/Orderbook.tsx new file mode 100644 index 0000000..a02d055 --- /dev/null +++ b/src/views/Orderbook.tsx @@ -0,0 +1,279 @@ +import { useCallback, useMemo, useRef, useState } from 'react'; +import { shallowEqual, useDispatch, useSelector } from 'react-redux'; +import styled, { css } from 'styled-components'; + +import { Nullable } from '@/constants/abacus'; +import { STRING_KEYS } from '@/constants/localization'; + +import { useBreakpoints, useStringGetter } from '@/hooks'; +import { + useCalculateOrderbookData, + useCenterOrderbook, + useDrawOrderbookHistograms, + useSpreadRowScrollListener, +} from '@/hooks/orderbook'; + +import { calculateCanViewAccount } from '@/state/accountCalculators'; +import { getCurrentMarketAssetData } from '@/state/assetsSelectors'; +import { getCurrentMarketConfig } from '@/state/perpetualsSelectors'; +import { setTradeFormInputs } from '@/state/inputs'; +import { getCurrentInput } from '@/state/inputsSelectors'; + +import { Canvas } from '@/components/Canvas'; +import { LoadingSpace } from '@/components/Loading/LoadingSpinner'; +import { Tag } from '@/components/Tag'; + +import { Row, type RowData } from './Orderbook/OrderbookRow'; +import { SpreadRow } from './Orderbook/SpreadRow'; + +const ORDERBOOK_MAX_ROWS_PER_SIDE = 35; + +type ElementProps = { + maxRowsPerSide?: number; + layout?: 'vertical' | 'horizontal'; +}; + +type StyleProps = { + hideHeader?: boolean; + histogramSide?: 'left' | 'right'; + className?: string; +}; + +const getEmptyRow = (side: 'bid' | 'ask') => ({ + side, + size: undefined, + price: undefined, + offset: 0, + depth: undefined, +}); + +export const Orderbook = ({ + maxRowsPerSide = ORDERBOOK_MAX_ROWS_PER_SIDE, +}: ElementProps & StyleProps) => { + const dispatch = useDispatch(); + const stringGetter = useStringGetter(); + const { isTablet } = useBreakpoints(); + + const showMineColumn = useSelector(calculateCanViewAccount) && !isTablet; + const currentInput = useSelector(getCurrentInput); + const { id = '' } = useSelector(getCurrentMarketAssetData, shallowEqual) ?? {}; + const { stepSizeDecimals, tickSizeDecimals } = + useSelector(getCurrentMarketConfig, shallowEqual) ?? {}; + + const { asks, bids, spread, spreadPercent, histogramRange, hasOrderbook } = + useCalculateOrderbookData({ + maxRowsPerSide, + }); + + const { askLevels, asksSlice, bidLevels, bidsSlice, numRows } = useMemo(() => { + const bidsSlice = bids.slice(0, maxRowsPerSide).reverse(); + const asksSlice = asks.slice(0, maxRowsPerSide); + + const numRows = maxRowsPerSide; // Math.max(bidsSlice.length, asksSlice.length); + + if (asksSlice.length < numRows) { + const emptyRows = new Array(numRows - asksSlice.length).fill(getEmptyRow('ask')); + asksSlice.push(...emptyRows); + } else if (bidsSlice.length < numRows) { + const emptyRows = new Array(numRows - bidsSlice.length).fill(getEmptyRow('bid')); + bidsSlice.unshift(...emptyRows); + } + + const askLevels = new Set(asksSlice.map(({ price }: { price: number }) => price)); + const bidLevels = new Set(bidsSlice.map(({ price }: { price: number }) => price)); + + return { askLevels, asksSlice, bidLevels, bidsSlice, numRows }; + }, [asks, bids]); + + const orderbookRef = useRef(null); + useCenterOrderbook({ orderbookRef, marketId: id }); + + /** + * Display top or bottom spreadRow when center spreadRow is off screen + */ + const spreadRowRef = useRef(null); + const { displaySide } = useSpreadRowScrollListener({ + orderbookRef, + spreadRowRef, + }); + + /** + * Row action and hover + */ + const onRowAction = useCallback( + (price: Nullable) => { + if (currentInput === 'trade' && price) { + dispatch(setTradeFormInputs({ limitPriceInput: price?.toString() })); + } + }, + [currentInput] + ); + + const [hoveredRow, setHoveredRow] = useState<{ + idx: number; + side: 'bid' | 'ask'; + }>(); + + const { canvasRef: asksCanvasRef } = useDrawOrderbookHistograms({ + data: asksSlice.reverse(), + histogramRange, + stepSizeDecimals, + tickSizeDecimals, + hoveredRow: hoveredRow?.side === 'ask' ? hoveredRow.idx : undefined, + }); + + const { canvasRef: bidsCanvasRef } = useDrawOrderbookHistograms({ + data: bidsSlice.reverse(), + histogramRange, + stepSizeDecimals, + tickSizeDecimals, + hoveredRow: hoveredRow?.side === 'bid' ? hoveredRow.idx : undefined, + }); + + return ( + <$OrderbookContainer> + <$OrderbookContent isLoading={!hasOrderbook}> + <$Header> + + {stringGetter({ key: STRING_KEYS.SIZE })} {id && {id}} + + + {stringGetter({ key: STRING_KEYS.PRICE })} USD + + {showMineColumn && stringGetter({ key: STRING_KEYS.MINE })} + + {displaySide === 'top' && ( + + )} + <$OrderbookWrapper ref={orderbookRef}> + {!hasOrderbook ? ( + + ) : ( + <> + <$OrderbookSideContainer + numRows={numRows} + side="ask" + onMouseLeave={() => setHoveredRow(undefined)} + > + <$HistogramCanvas + ref={asksCanvasRef} + width={asksCanvasRef.current?.clientWidth ?? 0} + height={asksCanvasRef.current?.clientHeight ?? 0} + /> + + {asksSlice.map((row: RowData, idx) => ( + <$Row + key={idx} + title={row.price ? `${row.price}` : undefined} + onClick={() => (row.price ? onRowAction(row.price) : {})} + onMouseOver={(e) => { + setHoveredRow({ idx, side: 'ask' }); + }} + /> + ))} + + + + + <$OrderbookSideContainer + numRows={numRows} + side="bid" + onMouseLeave={() => setHoveredRow(undefined)} + > + <$HistogramCanvas + ref={bidsCanvasRef} + width={bidsCanvasRef.current?.clientWidth ?? 0} + height={bidsCanvasRef.current?.clientHeight ?? 0} + /> + + {bidsSlice.map((row: RowData, idx) => ( + <$Row + key={idx} + title={row.price ? `${row.price}` : undefined} + onClick={() => (row.price ? onRowAction(row.price) : {})} + onMouseOver={(e) => { + setHoveredRow({ idx, side: 'bid' }); + }} + /> + ))} + + + )} + + {displaySide === 'bottom' && ( + + )} + + + ); +}; + +const $OrderbookContainer = styled.div` + display: flex; + flex: 1 1 0%; + flex-direction: column; + overflow: hidden; +`; + +const $OrderbookContent = styled.div<{ isLoading?: boolean }>` + max-height: 100%; + display: flex; + flex-direction: column; + position: relative; + + ${({ isLoading }) => isLoading && 'flex: 1;'} +`; + +const $Header = styled(Row)` + height: 2rem; + border-bottom: 1px solid var(--color-border); + color: var(--color-text-0); +`; + +const $OrderbookWrapper = styled.div` + overflow-y: auto; + display: flex; + flex-direction: column; + flex: 1 1 0%; +`; + +const $OrderbookSideContainer = styled.div<{ numRows: number; side: 'bid' | 'ask' }>` + ${({ numRows }) => css` + min-height: calc(${numRows} * 1.25rem); + `} + + ${({ side }) => css` + --accent-color: ${side === 'bid' ? 'var(--color-positive)' : 'var(--color-negative)'}; + `} + position: relative; +`; + +const $HistogramCanvas = styled(Canvas)` + width: 100%; + height: 100%; + + position: absolute; + top: 0; + right: 0; + + font-feature-settings: var(--fontFeature-monoNumbers); +`; + +const $Row = styled(Row)<{ onClick?: () => void }>` + ${({ onClick }) => (onClick ? 'cursor: pointer;' : 'cursor: default;')} +`; diff --git a/src/views/Orderbook/OrderbookRow.tsx b/src/views/Orderbook/OrderbookRow.tsx new file mode 100644 index 0000000..0b418b6 --- /dev/null +++ b/src/views/Orderbook/OrderbookRow.tsx @@ -0,0 +1,37 @@ +import { memo, useEffect, useRef, useState } from 'react'; +import styled from 'styled-components'; + +import { OrderbookLine } from '@/constants/abacus'; + +import { Output, OutputType } from '@/components/Output'; + +export type RowData = OrderbookLine & { side: 'bid' | 'ask'; mine?: number }; + +export const ROW_HEIGHT = 20; + +export const Row = styled.div` + display: flex; + flex-shrink: 0; + align-items: center; + height: ${ROW_HEIGHT}px; + min-height: ${ROW_HEIGHT}px; + + font: var(--font-mini-book); + cursor: pointer; + + position: relative; + padding-right: 0.5rem; + + > span { + flex: 1 1 0%; + text-align: right; + padding-bottom: 2px; + } +`; + +const $OrderbookRow = styled(Row)` + &:hover { + // color: var(--color-text-2); + background-color: var(--color-layer-3); + } +`; diff --git a/src/views/Orderbook/SpreadRow.tsx b/src/views/Orderbook/SpreadRow.tsx new file mode 100644 index 0000000..a1a1d94 --- /dev/null +++ b/src/views/Orderbook/SpreadRow.tsx @@ -0,0 +1,61 @@ +import { forwardRef } from 'react'; +import styled, { css } from 'styled-components'; +import BigNumber from 'bignumber.js'; + +import type { Nullable } from '@/constants/abacus'; +import { STRING_KEYS } from '@/constants/localization'; +import { useStringGetter } from '@/hooks'; + +import { Output, OutputType } from '@/components/Output'; +import { WithTooltip } from '@/components/WithTooltip'; + +import { Row } from './OrderbookRow'; + +type StyleProps = { + side?: 'top' | 'bottom'; +}; + +type ElementProps = { + spread: BigNumber | null; + spreadPercent: Nullable; + tickSizeDecimals: Nullable; +}; + +export const SpreadRow = forwardRef( + ({ side, spread, spreadPercent, tickSizeDecimals }, ref) => { + const stringGetter = useStringGetter(); + + return ( + <$SpreadRow ref={ref} side={side}> + + + {stringGetter({ key: STRING_KEYS.ORDERBOOK_SPREAD })} + + + + + + + + + + ); + } +); + +const $SpreadRow = styled(Row)<{ side?: 'top' | 'bottom' }>` + height: 2rem; + border-top: 1px solid var(--color-border); + border-bottom: 1px solid var(--color-border); + + ${({ side }) => + side && + { + top: css` + border-top: none; + `, + bottom: css` + border-bottom: none; + `, + }[side]} +`;