From b75ed62072f33a949a46e7d5c58627241e14547d Mon Sep 17 00:00:00 2001 From: Elmar <102954831+elmar-vega@users.noreply.github.com> Date: Wed, 10 Aug 2022 16:28:03 +0100 Subject: [PATCH] Feat/348 select size of trade (#943) * feat(ui-toolkit): add slider to ui-toolkit * feat(console-lite): add deal ticket size to use new slider * feat(console-lite): add use-maximum-position-size hook * feat(console-lite): add e2e tests * feat(console-lite): add position size value after selection * fix(console-lite): remove lingering console log * fix(console-lite): fix linting errors * fix(console-lite): fix cypress config with wrong app specified * fix(console-lite): fix react hooks bug after upgrade to react 18 * feat(console-lite): add proportional size selector and size amount input * fix(console-lite): add missing env variables * feat(console-lite): add missing tests and fix broken one after input button for size * fix(console-lite): fix async error for max trade size --- apps/simple-trading-app-e2e/cypress.config.js | 5 +- .../src/integration/market-trade.test.ts | 76 ++++++- .../mocks/generate-market-positions.ts | 2 + .../support/mocks/generate-party-balance.ts | 4 + apps/simple-trading-app/.env | 5 +- apps/simple-trading-app/.env.devnet | 4 + apps/simple-trading-app/.env.mainnet | 4 + apps/simple-trading-app/.env.stagnet1 | 4 + apps/simple-trading-app/.env.stagnet2 | 4 + apps/simple-trading-app/.env.testnet | 4 + .../__generated__/PartyBalanceQuery.ts | 6 + .../deal-ticket/deal-ticket-balance.spec.tsx | 2 + .../deal-ticket/deal-ticket-balance.tsx | 7 +- .../deal-ticket/deal-ticket-container.tsx | 1 + .../deal-ticket/deal-ticket-size.tsx | 201 ++++++++++++++++++ .../deal-ticket/deal-ticket-steps.tsx | 89 ++++++-- .../src/app/components/deal-ticket/index.ts | 3 +- .../components/deal-ticket/review-trade.tsx | 19 +- .../hooks/use-maximum-position-size.spec.ts | 117 ++++++++++ .../app/hooks/use-maximum-position-size.ts | 61 ++++++ .../app/hooks/use-settlement-account.spec.tsx | 35 ++- .../src/app/hooks/use-settlement-account.ts | 14 +- libs/ui-toolkit/src/components/index.ts | 1 + .../ui-toolkit/src/components/slider/index.ts | 1 + .../src/components/slider/slider.stories.tsx | 27 +++ .../src/components/slider/slider.tsx | 84 ++++++++ package.json | 1 + yarn.lock | 117 ++++++++++ 28 files changed, 853 insertions(+), 45 deletions(-) create mode 100644 apps/simple-trading-app/src/app/components/deal-ticket/deal-ticket-size.tsx create mode 100644 apps/simple-trading-app/src/app/hooks/use-maximum-position-size.spec.ts create mode 100644 apps/simple-trading-app/src/app/hooks/use-maximum-position-size.ts create mode 100644 libs/ui-toolkit/src/components/slider/index.ts create mode 100644 libs/ui-toolkit/src/components/slider/slider.stories.tsx create mode 100644 libs/ui-toolkit/src/components/slider/slider.tsx diff --git a/apps/simple-trading-app-e2e/cypress.config.js b/apps/simple-trading-app-e2e/cypress.config.js index 889458a22..bb8f92f16 100644 --- a/apps/simple-trading-app-e2e/cypress.config.js +++ b/apps/simple-trading-app-e2e/cypress.config.js @@ -12,8 +12,9 @@ module.exports = defineConfig({ supportFile: './src/support/index.ts', video: true, videoUploadOnPasses: false, - videosFolder: '../../dist/cypress/apps/explorer-e2e/videos', - screenshotsFolder: '../../dist/cypress/apps/explorer-e2e/screenshots', + videosFolder: '../../dist/cypress/apps/simple-trading-app-e2e/videos', + screenshotsFolder: + '../../dist/cypress/apps/simple-trading-app-e2e/screenshots', chromeWebSecurity: false, viewportWidth: 1440, viewportHeight: 900, diff --git a/apps/simple-trading-app-e2e/src/integration/market-trade.test.ts b/apps/simple-trading-app-e2e/src/integration/market-trade.test.ts index 859922bc7..08defc3fa 100644 --- a/apps/simple-trading-app-e2e/src/integration/market-trade.test.ts +++ b/apps/simple-trading-app-e2e/src/integration/market-trade.test.ts @@ -29,6 +29,7 @@ describe('Market trade', () => { } }); }); + it('side selector should work well', () => { if (markets?.length) { cy.visit(`/trading/${markets[0].id}`); @@ -46,7 +47,7 @@ describe('Market trade', () => { } }); - it('mobile view should work well', () => { + it('side selector mobile view should work well', () => { if (markets?.length) { cy.viewport('iphone-xr'); cy.visit(`/trading/${markets[0].id}`); @@ -78,6 +79,79 @@ describe('Market trade', () => { } }); + it('size slider should work well', () => { + 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('#step-2-panel') + .find('dd') + .eq(0) + .find('button') + .should('have.text', '1'); + cy.get('#step-2-panel').find('[role="slider"]').type('{rightarrow}'); + + cy.get('#step-2-panel') + .find('dd') + .eq(0) + .find('button') + .should('have.text', '2'); + } + }); + + it('percentage selection should work well', () => { + 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('#step-2-panel') + .find('dd') + .eq(0) + .find('button') + .should('have.text', '1'); + cy.getByTestId('percentage-selector') + .find('button') + .contains('Max') + .click(); + cy.get('#step-2-panel') + .find('dd') + .eq(0) + .find('button') + .should('have.text', '21'); + } + }); + + it('size input should work well', () => { + 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('#step-2-panel') + .find('dd') + .eq(0) + .find('button') + .should('have.text', '1'); + cy.get('#step-2-panel').find('dd').eq(0).find('button').click(); + cy.get('#step-2-panel') + .find('dd') + .eq(0) + .find('input') + .type('{backspace}2'); + cy.get('#step-2-panel').find('dd').eq(0).find('button').click(); + cy.get('#step-2-panel') + .find('dd') + .eq(0) + .find('button') + .should('have.text', '2'); + } + }); + it('order review should display proper calculations', () => { if (markets?.length) { cy.visit(`/trading/${markets[0].id}`); diff --git a/apps/simple-trading-app-e2e/src/support/mocks/generate-market-positions.ts b/apps/simple-trading-app-e2e/src/support/mocks/generate-market-positions.ts index 95648719a..bd3acdc3f 100644 --- a/apps/simple-trading-app-e2e/src/support/mocks/generate-market-positions.ts +++ b/apps/simple-trading-app-e2e/src/support/mocks/generate-market-positions.ts @@ -4,6 +4,7 @@ export const generateMarketPositions = () => { id: '2e1ef32e5804e14232406aebaad719087d326afa5c648b7824d0823d8a46c8d1', accounts: [ { + type: 'General', asset: { decimals: 5, }, @@ -14,6 +15,7 @@ export const generateMarketPositions = () => { }, }, { + type: 'Margin', asset: { decimals: 5, }, diff --git a/apps/simple-trading-app-e2e/src/support/mocks/generate-party-balance.ts b/apps/simple-trading-app-e2e/src/support/mocks/generate-party-balance.ts index c38801314..e42418135 100644 --- a/apps/simple-trading-app-e2e/src/support/mocks/generate-party-balance.ts +++ b/apps/simple-trading-app-e2e/src/support/mocks/generate-party-balance.ts @@ -4,6 +4,7 @@ export const generatePartyBalance = () => { accounts: [ { balance: '88474051', + type: 'General', asset: { id: '6d9d35f657589e40ddfb448b7ad4a7463b66efb307527fedd2aa7df1bbd5ea61', symbol: 'tDAI', @@ -15,6 +16,7 @@ export const generatePartyBalance = () => { }, { balance: '100000000', + type: 'General', asset: { id: '8b52d4a3a4b0ffe733cddbc2b67be273816cfeb6ca4c8b339bac03ffba08e4e4', symbol: 'tEURO', @@ -26,6 +28,7 @@ export const generatePartyBalance = () => { }, { balance: '3412867', + type: 'General', asset: { id: '6d9d35f657589e40ddfb448b7ad4a7463b66efb307527fedd2aa7df1bbd5ea61', symbol: 'tDAI', @@ -37,6 +40,7 @@ export const generatePartyBalance = () => { }, { balance: '70007', + type: 'General', asset: { id: '6d9d35f657589e40ddfb448b7ad4a7463b66efb307527fedd2aa7df1bbd5ea61', symbol: 'tDAI', diff --git a/apps/simple-trading-app/.env b/apps/simple-trading-app/.env index f572c2eeb..2c58ad9b3 100644 --- a/apps/simple-trading-app/.env +++ b/apps/simple-trading-app/.env @@ -17,8 +17,11 @@ NX_INCOMING_HOOK_BODY=$INCOMING_HOOK_BODY NX_URL=$URL NX_DEPLOY_URL=$DEPLOY_URL NX_DEPLOY_PRIME_URL=$DEPLOY_PRIME_URL - NX_VEGA_CONFIG_URL="https://static.vega.xyz/assets/testnet-network.json" NX_VEGA_ENV = 'TESTNET' NX_VEGA_URL="https://lb.testnet.vega.xyz/query" NX_VEGA_WALLET_URL=http://localhost:1789/api/v1 +NX_ETHEREUM_PROVIDER_URL=https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8 +NX_ETHERSCAN_URL=https://ropsten.etherscan.io +NX_VEGA_NETWORKS={\"MAINNET\":\"https://alpha.console.vega.xyz\"} +NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf diff --git a/apps/simple-trading-app/.env.devnet b/apps/simple-trading-app/.env.devnet index eee3a007a..7ba4cdd07 100644 --- a/apps/simple-trading-app/.env.devnet +++ b/apps/simple-trading-app/.env.devnet @@ -3,3 +3,7 @@ NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/devnet-network.json NX_VEGA_URL=https://n04.d.vega.xyz/query NX_VEGA_ENV=DEVNET NX_VEGA_REST=https://n04.d.vega.xyz/datanode/rest +NX_VEGA_NETWORKS={\"MAINNET\":\"https://alpha.console.vega.xyz\"} +NX_ETHEREUM_PROVIDER_URL=https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8 +NX_ETHERSCAN_URL=https://ropsten.etherscan.io +NX_VEGA_EXPLORER_URL=https://dev.explorer.vega.xyz diff --git a/apps/simple-trading-app/.env.mainnet b/apps/simple-trading-app/.env.mainnet index f064d6d24..d4d89f07e 100644 --- a/apps/simple-trading-app/.env.mainnet +++ b/apps/simple-trading-app/.env.mainnet @@ -3,3 +3,7 @@ NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/mainnet-network.json NX_VEGA_URL=https://api.token.vega.xyz/query NX_VEGA_ENV=MAINNET NX_VEGA_REST=https://api.token.vega.xyz/ +NX_VEGA_NETWORKS='{\"MAINNET\":\"https://alpha.console.vega.xyz\"}' +NX_ETHEREUM_PROVIDER_URL=https://mainnet.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8 +NX_ETHERSCAN_URL=https://etherscan.io +NX_VEGA_EXPLORER_URL=https://explorer.vega.xyz diff --git a/apps/simple-trading-app/.env.stagnet1 b/apps/simple-trading-app/.env.stagnet1 index df03774f2..0ec54a3c7 100644 --- a/apps/simple-trading-app/.env.stagnet1 +++ b/apps/simple-trading-app/.env.stagnet1 @@ -3,3 +3,7 @@ NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/stagnet1-network.json NX_VEGA_URL=https://n03.s.vega.xyz/query NX_VEGA_ENV=STAGNET NX_VEGA_REST=https://n03.s.vega.xyz/datanode/rest +NX_VEGA_NETWORKS='{\"MAINNET\":\"https://alpha.console.vega.xyz\"}' +NX_ETHEREUM_PROVIDER_URL=https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8 +NX_ETHERSCAN_URL=https://ropsten.etherscan.io +NX_VEGA_EXPLORER_URL=https://staging.explorer.vega.xyz diff --git a/apps/simple-trading-app/.env.stagnet2 b/apps/simple-trading-app/.env.stagnet2 index 665e0ad12..a6cd1a51b 100644 --- a/apps/simple-trading-app/.env.stagnet2 +++ b/apps/simple-trading-app/.env.stagnet2 @@ -3,3 +3,7 @@ NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/stagnet2-network.json NX_VEGA_URL=https://n03.stagnet2.vega.xyz/query NX_VEGA_ENV=STAGNET2 NX_VEGA_REST=https://n01.stagnet2.vega.xyz/datanode/rest +NX_VEGA_NETWORKS='{\"MAINNET\":\"https://alpha.console.vega.xyz\"}' +NX_ETHEREUM_PROVIDER_URL=https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8 +NX_ETHERSCAN_URL=https://ropsten.etherscan.io +NX_VEGA_EXPLORER_URL=https://staging2.explorer.vega.xyz diff --git a/apps/simple-trading-app/.env.testnet b/apps/simple-trading-app/.env.testnet index 1b572c593..8779709d9 100644 --- a/apps/simple-trading-app/.env.testnet +++ b/apps/simple-trading-app/.env.testnet @@ -3,3 +3,7 @@ NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/testnet-network.json NX_VEGA_URL=https://lb.testnet.vega.xyz/query NX_VEGA_ENV=TESTNET NX_VEGA_REST=https://lb.testnet.vega.xyz/datanode/rest +NX_VEGA_NETWORKS='{\"MAINNET\":\"https://alpha.console.vega.xyz\"}' +NX_ETHEREUM_PROVIDER_URL=https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8 +NX_ETHERSCAN_URL=https://ropsten.etherscan.io +NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf diff --git a/apps/simple-trading-app/src/app/components/deal-ticket/__generated__/PartyBalanceQuery.ts b/apps/simple-trading-app/src/app/components/deal-ticket/__generated__/PartyBalanceQuery.ts index b7f15b3c2..c78a46b9b 100644 --- a/apps/simple-trading-app/src/app/components/deal-ticket/__generated__/PartyBalanceQuery.ts +++ b/apps/simple-trading-app/src/app/components/deal-ticket/__generated__/PartyBalanceQuery.ts @@ -3,6 +3,8 @@ // @generated // This file was automatically generated and should not be edited. +import { AccountType } from "@vegaprotocol/types"; + // ==================================================== // GraphQL query operation: PartyBalanceQuery // ==================================================== @@ -29,6 +31,10 @@ export interface PartyBalanceQuery_party_accounts_asset { export interface PartyBalanceQuery_party_accounts { __typename: "Account"; + /** + * Account type (General, Margin, etc) + */ + type: AccountType; /** * Balance as string - current account balance (approx. as balances can be updated several times per second) */ diff --git a/apps/simple-trading-app/src/app/components/deal-ticket/deal-ticket-balance.spec.tsx b/apps/simple-trading-app/src/app/components/deal-ticket/deal-ticket-balance.spec.tsx index c16461b20..35c61e88f 100644 --- a/apps/simple-trading-app/src/app/components/deal-ticket/deal-ticket-balance.spec.tsx +++ b/apps/simple-trading-app/src/app/components/deal-ticket/deal-ticket-balance.spec.tsx @@ -5,6 +5,7 @@ import type { PartyBalanceQuery_party_accounts_asset, } from './__generated__/PartyBalanceQuery'; import { DealTicketBalance } from './deal-ticket-balance'; +import { AccountType } from '@vegaprotocol/types'; const tDAI: PartyBalanceQuery_party_accounts_asset = { __typename: 'Asset', @@ -17,6 +18,7 @@ const tDAI: PartyBalanceQuery_party_accounts_asset = { const accounts: PartyBalanceQuery_party_accounts[] = [ { __typename: 'Account', + type: AccountType.General, balance: '1000000', asset: tDAI, }, diff --git a/apps/simple-trading-app/src/app/components/deal-ticket/deal-ticket-balance.tsx b/apps/simple-trading-app/src/app/components/deal-ticket/deal-ticket-balance.tsx index 4d411c0de..b636260fd 100644 --- a/apps/simple-trading-app/src/app/components/deal-ticket/deal-ticket-balance.tsx +++ b/apps/simple-trading-app/src/app/components/deal-ticket/deal-ticket-balance.tsx @@ -4,6 +4,7 @@ import type { DealTicketQuery_market_tradableInstrument_instrument_product_settl import type { PartyBalanceQuery_party_accounts } from './__generated__/PartyBalanceQuery'; import { useSettlementAccount } from '../../hooks/use-settlement-account'; import { addDecimalsFormatNumber, t } from '@vegaprotocol/react-helpers'; +import { AccountType } from '@vegaprotocol/types'; interface DealTicketBalanceProps { settlementAsset: DealTicketQuery_market_tradableInstrument_instrument_product_settlementAsset; @@ -20,7 +21,11 @@ export const DealTicketBalance = ({ }: DealTicketBalanceProps) => { const settlementAssetId = settlementAsset?.id; const settlementAssetSymbol = settlementAsset?.symbol; - const settlementAccount = useSettlementAccount(settlementAssetId, accounts); + const settlementAccount = useSettlementAccount( + settlementAssetId, + accounts, + AccountType.General + ); const formatedNumber = settlementAccount?.balance && settlementAccount.asset.decimals && diff --git a/apps/simple-trading-app/src/app/components/deal-ticket/deal-ticket-container.tsx b/apps/simple-trading-app/src/app/components/deal-ticket/deal-ticket-container.tsx index f645feb52..553044211 100644 --- a/apps/simple-trading-app/src/app/components/deal-ticket/deal-ticket-container.tsx +++ b/apps/simple-trading-app/src/app/components/deal-ticket/deal-ticket-container.tsx @@ -17,6 +17,7 @@ const PARTY_BALANCE_QUERY = gql` query PartyBalanceQuery($partyId: ID!) { party(id: $partyId) { accounts { + type balance asset { id diff --git a/apps/simple-trading-app/src/app/components/deal-ticket/deal-ticket-size.tsx b/apps/simple-trading-app/src/app/components/deal-ticket/deal-ticket-size.tsx new file mode 100644 index 000000000..cc5c9ce64 --- /dev/null +++ b/apps/simple-trading-app/src/app/components/deal-ticket/deal-ticket-size.tsx @@ -0,0 +1,201 @@ +import React, { useCallback, useState } from 'react'; +import { t } from '@vegaprotocol/react-helpers'; +import { + SliderRoot, + SliderThumb, + SliderTrack, + SliderRange, + Button, + Input, + FormGroup, +} from '@vegaprotocol/ui-toolkit'; +import { BigNumber } from 'bignumber.js'; + +interface DealTicketSizeProps { + step: number; + min: number; + max: number; + value: number; + onValueChange: (value: number[]) => void; + name: string; + quoteName: string; + price: string; + estCloseOut: string; + estMargin: string; + 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 DealTicketSize = ({ + value, + step, + min, + max, + price, + quoteName, + onValueChange, + estCloseOut, + positionDecimalPlaces, +}: DealTicketSizeProps) => { + const sizeRatios = [0, 25, 50, 75, 100]; + const [inputValue, setInputValue] = useState(value); + const [isInputVisible, setIsInputVisible] = useState(false); + + const onInputValueChange = useCallback( + (event: React.ChangeEvent) => { + 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) => { + if (isInputVisible) { + setIsInputVisible(false); + } + const newVal = new BigNumber(size) + .decimalPlaces(positionDecimalPlaces) + .toNumber(); + onValueChange([newVal]); + setInputValue(newVal); + }, + [isInputVisible, onValueChange, positionDecimalPlaces] + ); + + const toggleInput = useCallback(() => { + setIsInputVisible(!isInputVisible); + }, [isInputVisible]); + + const onInputEnter = useCallback( + (event: React.KeyboardEvent) => { + if (event.key === 'Enter') { + event.stopPropagation(); + toggleInput(); + } + }, + [toggleInput] + ); + + return max === 0 ? ( +

Not enough balance to trade

+ ) : ( +
+
+ {min} + {max} +
+ + + + + + + +
+ {sizeRatios.map((size, index) => { + const proportionalSize = size ? (size / 100) * max : min; + return ( + + ); + })} +
+ +
+
+
+ {t('Size')} +   + ({quoteName}) +
+
+ + {isInputVisible ? ( + <> + + + + ) : ( + + )} + +
+
+
+
{t('Est. price')}
+
{price}
+
+
+
{t('Est. close out')}
+
{estCloseOut}
+
+
+
+ ); +}; diff --git a/apps/simple-trading-app/src/app/components/deal-ticket/deal-ticket-steps.tsx b/apps/simple-trading-app/src/app/components/deal-ticket/deal-ticket-steps.tsx index e756fc01d..80c3cfd57 100644 --- a/apps/simple-trading-app/src/app/components/deal-ticket/deal-ticket-steps.tsx +++ b/apps/simple-trading-app/src/app/components/deal-ticket/deal-ticket-steps.tsx @@ -1,29 +1,34 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { useForm, Controller } from 'react-hook-form'; import { Stepper } from '../stepper'; import type { DealTicketQuery_market } from '@vegaprotocol/deal-ticket'; import { InputError } from '@vegaprotocol/ui-toolkit'; +import { BigNumber } from 'bignumber.js'; import { - DealTicketAmount, getOrderDialogTitle, getOrderDialogIntent, getOrderDialogIcon, MarketSelector, } from '@vegaprotocol/deal-ticket'; import type { Order } from '@vegaprotocol/orders'; -import { VegaTxStatus } from '@vegaprotocol/wallet'; +import { useVegaWallet, VegaTxStatus } from '@vegaprotocol/wallet'; import { t, addDecimal, toDecimal } from '@vegaprotocol/react-helpers'; import { getDefaultOrder, useOrderValidation, useOrderSubmit, OrderFeedback, + validateSize, } from '@vegaprotocol/orders'; +import { DealTicketSize } from './deal-ticket-size'; import MarketNameRenderer from '../simple-market-list/simple-market-renderer'; import SideSelector, { SIDE_NAMES } from './side-selector'; import ReviewTrade from './review-trade'; import type { PartyBalanceQuery } from './__generated__/PartyBalanceQuery'; +import useOrderCloseOut from '../../hooks/use-order-closeout'; +import useOrderMargin from '../../hooks/use-order-margin'; +import useMaximumPositionSize from '../../hooks/use-maximum-position-size'; interface DealTicketMarketProps { market: DealTicketQuery_market; @@ -43,21 +48,48 @@ export const DealTicketSteps = ({ ); const { - register, control, handleSubmit, watch, + setValue, formState: { errors }, } = useForm({ mode: 'onChange', defaultValues: getDefaultOrder(market), }); + const [max, setMax] = useState(null); const step = toDecimal(market.positionDecimalPlaces); const orderType = watch('type'); const orderTimeInForce = watch('timeInForce'); const orderSide = watch('side'); + const orderSize = watch('size'); const order = watch(); + const estCloseOut = useOrderCloseOut({ order, market, partyData }); + const { keypair } = useVegaWallet(); + const estMargin = useOrderMargin({ + order, + market, + partyId: keypair?.pub || '', + }); + + const maxTrade = useMaximumPositionSize({ + partyId: keypair?.pub || '', + accounts: partyData?.party?.accounts || [], + marketId: market.id, + settlementAssetId: + market.tradableInstrument.instrument.product.settlementAsset.id, + price: market?.depth?.lastTrade?.price, + order, + }); + + useEffect(() => { + setMax( + new BigNumber(maxTrade) + .decimalPlaces(market.positionDecimalPlaces) + .toNumber() + ); + }, [maxTrade, market.positionDecimalPlaces]); const { message: invalidText, isDisabled } = useOrderValidation({ step, @@ -70,6 +102,16 @@ export const DealTicketSteps = ({ const { submit, transaction, finalizedOrder, TransactionDialog } = useOrderSubmit(market); + 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); + } + }; + const transactionStatus = transaction.status === VegaTxStatus.Requested || transaction.status === VegaTxStatus.Pending @@ -112,19 +154,29 @@ export const DealTicketSteps = ({ }, { label: t('Choose Position Size'), - component: ( - - ), + component: + max !== null ? ( + + ) : ( + 'loading...' + ), + value: orderSize, }, { label: t('Review Trade'), @@ -140,7 +192,8 @@ export const DealTicketSteps = ({ isDisabled={isDisabled} transactionStatus={transactionStatus} order={order} - partyData={partyData} + estCloseOut={estCloseOut} + estMargin={estMargin || ' - '} /> { - const { keypair } = useVegaWallet(); const { data: tagsData } = useQuery( MARKET_TAGS_QUERY, { variables: { marketId: market.id }, } ); - const estMargin = useOrderMargin({ - order, - market, - partyId: keypair?.pub || '', - }); - const estCloseOut = useOrderCloseOut({ order, market, partyData }); + return (
diff --git a/apps/simple-trading-app/src/app/hooks/use-maximum-position-size.spec.ts b/apps/simple-trading-app/src/app/hooks/use-maximum-position-size.spec.ts new file mode 100644 index 000000000..e9c70b130 --- /dev/null +++ b/apps/simple-trading-app/src/app/hooks/use-maximum-position-size.spec.ts @@ -0,0 +1,117 @@ +import { renderHook } from '@testing-library/react'; +import useMaximumPositionSize from './use-maximum-position-size'; +import type { PartyBalanceQuery_party_accounts } from '../components/deal-ticket/__generated__/PartyBalanceQuery'; +import { AccountType } from '@vegaprotocol/types'; +import type { PositionMargin } from './use-market-positions'; +import { BigNumber } from 'bignumber.js'; +import { + VegaWalletOrderSide, + VegaWalletOrderTimeInForce, + VegaWalletOrderType, +} from '@vegaprotocol/wallet'; + +const defaultMockMarketPositions = { + openVolume: new BigNumber(1), + balance: new BigNumber(100000), +}; + +let mockMarketPositions: PositionMargin | null = defaultMockMarketPositions; + +const mockAccount: PartyBalanceQuery_party_accounts = { + __typename: 'Account', + type: AccountType.General, + balance: '200000', + asset: { + __typename: 'Asset', + id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c', + symbol: 'tBTC', + name: 'tBTC TEST', + decimals: 5, + }, +}; + +const mockOrder = { + type: VegaWalletOrderType.Market, + size: '1', + side: VegaWalletOrderSide.Buy, + timeInForce: VegaWalletOrderTimeInForce.IOC, +}; + +jest.mock('./use-settlement-account', () => { + return { + useSettlementAccount: jest.fn(() => mockAccount), + }; +}); +jest.mock('./use-market-positions', () => jest.fn(() => mockMarketPositions)); + +describe('useMaximumPositionSize Hook', () => { + it('should return correct size when no open positions', () => { + mockMarketPositions = null; + const price = '50'; + const expected = 4000; + const { result } = renderHook(() => + useMaximumPositionSize({ + marketId: '', + partyId: '', + price, + settlementAssetId: '', + order: mockOrder, + accounts: [mockAccount], + }) + ); + expect(result.current).toBe(expected); + }); + + it('should return correct size when open positions and same side', () => { + const price = '50'; + mockMarketPositions = defaultMockMarketPositions; + const expected = 3999; + const { result } = renderHook(() => + useMaximumPositionSize({ + marketId: '', + partyId: '', + price, + settlementAssetId: '', + order: mockOrder, + accounts: [mockAccount], + }) + ); + expect(result.current).toBe(expected); + }); + + it('should return correct size when open positions and opposite side', () => { + const price = '50'; + mockOrder.side = VegaWalletOrderSide.Sell; + mockMarketPositions = defaultMockMarketPositions; + const expected = 4001; + const { result } = renderHook(() => + useMaximumPositionSize({ + marketId: '', + partyId: '', + price, + settlementAssetId: '', + order: mockOrder, + accounts: [mockAccount], + }) + ); + expect(result.current).toBe(expected); + }); + + it('should return zero if no account balance', () => { + mockAccount.balance = '0'; + const price = '50'; + mockMarketPositions = defaultMockMarketPositions; + const expected = 0; + const { result } = renderHook(() => + useMaximumPositionSize({ + marketId: '', + partyId: '', + price, + settlementAssetId: '', + order: mockOrder, + accounts: [], + }) + ); + expect(result.current).toBe(expected); + }); +}); diff --git a/apps/simple-trading-app/src/app/hooks/use-maximum-position-size.ts b/apps/simple-trading-app/src/app/hooks/use-maximum-position-size.ts new file mode 100644 index 000000000..603d2eafd --- /dev/null +++ b/apps/simple-trading-app/src/app/hooks/use-maximum-position-size.ts @@ -0,0 +1,61 @@ +import useMarketPositions from './use-market-positions'; +import type { Order } from '@vegaprotocol/orders'; +import type { PartyBalanceQuery_party_accounts } from '../components/deal-ticket/__generated__/PartyBalanceQuery'; +import { useSettlementAccount } from './use-settlement-account'; +import { AccountType } from '@vegaprotocol/types'; +import { VegaWalletOrderSide } from '@vegaprotocol/wallet'; +import { BigNumber } from 'bignumber.js'; + +interface Props { + partyId: string; + accounts: PartyBalanceQuery_party_accounts[]; + marketId: string; + price?: string; + settlementAssetId: string; + order: Order; +} + +const getSize = (balance: string, price: string) => + new BigNumber(balance).dividedBy(new BigNumber(price)); + +export default ({ + marketId, + accounts, + partyId, + price, + settlementAssetId, + order, +}: Props): number => { + const settlementAccount = useSettlementAccount( + settlementAssetId, + accounts, + AccountType.General + ); + + const marketPositions = useMarketPositions({ marketId: marketId, partyId }); + + if ( + !settlementAccount?.balance || + new BigNumber(settlementAccount?.balance || 0).isZero() + ) { + return 0; + } + + const size = getSize(settlementAccount.balance, price || ''); + + if (!marketPositions) { + return size.toNumber() || 0; + } + + const isSameSide = + (marketPositions.openVolume.isPositive() && + order.side === VegaWalletOrderSide.Buy) || + (marketPositions.openVolume.isNegative() && + order.side === VegaWalletOrderSide.Sell); + + const adjustedForVolume = new BigNumber(size)[isSameSide ? 'minus' : 'plus']( + marketPositions.openVolume + ); + + return adjustedForVolume.isNegative() ? 0 : adjustedForVolume.toNumber(); +}; diff --git a/apps/simple-trading-app/src/app/hooks/use-settlement-account.spec.tsx b/apps/simple-trading-app/src/app/hooks/use-settlement-account.spec.tsx index c11c03c98..de0e4b040 100644 --- a/apps/simple-trading-app/src/app/hooks/use-settlement-account.spec.tsx +++ b/apps/simple-trading-app/src/app/hooks/use-settlement-account.spec.tsx @@ -1,12 +1,14 @@ import { renderHook } from '@testing-library/react'; import { useSettlementAccount } from './use-settlement-account'; import type { PartyBalanceQuery_party_accounts } from '../components/deal-ticket/__generated__/PartyBalanceQuery'; +import { AccountType } from '@vegaprotocol/types'; describe('useSettlementAccount Hook', () => { it('should filter accounts by settlementAssetId', () => { const accounts: PartyBalanceQuery_party_accounts[] = [ { __typename: 'Account', + type: AccountType.General, balance: '2000000000000000000000', asset: { __typename: 'Asset', @@ -18,6 +20,7 @@ describe('useSettlementAccount Hook', () => { }, { __typename: 'Account', + type: AccountType.General, balance: '1000000000', asset: { __typename: 'Asset', @@ -29,6 +32,19 @@ describe('useSettlementAccount Hook', () => { }, { __typename: 'Account', + type: AccountType.General, + balance: '5000000000000000000', + asset: { + __typename: 'Asset', + id: 'fc7fd956078fb1fc9db5c19b88f0874c4299b2a7639ad05a47a28c0aef291b55', + symbol: 'VEGA', + name: 'Vega (testnet)', + decimals: 18, + }, + }, + { + __typename: 'Account', + type: AccountType.Margin, balance: '5000000000000000000', asset: { __typename: 'Asset', @@ -39,14 +55,23 @@ describe('useSettlementAccount Hook', () => { }, }, ]; - const settlementAssetId = + const tDAI = '6d9d35f657589e40ddfb448b7ad4a7463b66efb307527fedd2aa7df1bbd5ea61'; + const vega = + 'fc7fd956078fb1fc9db5c19b88f0874c4299b2a7639ad05a47a28c0aef291b55'; - const { result } = renderHook(() => - useSettlementAccount(settlementAssetId, accounts) + const { result: resultDai } = renderHook(() => + useSettlementAccount(tDAI, accounts) ); - expect(result.current?.balance).toBe(accounts[1].balance); - expect(result.current?.asset).toEqual(accounts[1].asset); + expect(resultDai.current?.balance).toBe(accounts[1].balance); + expect(resultDai.current?.asset).toEqual(accounts[1].asset); + + const { result: resultVega } = renderHook(() => + useSettlementAccount(vega, accounts, AccountType.Margin) + ); + + expect(resultVega.current?.balance).toBe(accounts[3].balance); + expect(resultVega.current?.asset).toEqual(accounts[3].asset); }); it('should return null if no accounts', () => { diff --git a/apps/simple-trading-app/src/app/hooks/use-settlement-account.ts b/apps/simple-trading-app/src/app/hooks/use-settlement-account.ts index 4acb04cbb..696071865 100644 --- a/apps/simple-trading-app/src/app/hooks/use-settlement-account.ts +++ b/apps/simple-trading-app/src/app/hooks/use-settlement-account.ts @@ -1,12 +1,20 @@ import type { PartyBalanceQuery_party_accounts } from '../components/deal-ticket/__generated__/PartyBalanceQuery'; +import type { AccountType } from '@vegaprotocol/types'; import { useMemo } from 'react'; export const useSettlementAccount = ( settlementAssetId: string, - accounts: PartyBalanceQuery_party_accounts[] + accounts: PartyBalanceQuery_party_accounts[], + type?: AccountType ): PartyBalanceQuery_party_accounts | null => { const callback = () => - accounts.find((account) => account.asset.id === settlementAssetId); - const account = useMemo(callback, [accounts, settlementAssetId]); + accounts.find((account) => { + if (type) { + return account.asset.id === settlementAssetId && account.type === type; + } + + return account.asset.id === settlementAssetId; + }); + const account = useMemo(callback, [accounts, settlementAssetId, type]); return account as PartyBalanceQuery_party_accounts; }; diff --git a/libs/ui-toolkit/src/components/index.ts b/libs/ui-toolkit/src/components/index.ts index 09061937a..782f2940f 100644 --- a/libs/ui-toolkit/src/components/index.ts +++ b/libs/ui-toolkit/src/components/index.ts @@ -31,3 +31,4 @@ export * from './theme-switcher'; export * from './toggle'; export * from './tooltip'; export * from './vega-logo'; +export * from './slider'; diff --git a/libs/ui-toolkit/src/components/slider/index.ts b/libs/ui-toolkit/src/components/slider/index.ts new file mode 100644 index 000000000..eb0742f80 --- /dev/null +++ b/libs/ui-toolkit/src/components/slider/index.ts @@ -0,0 +1 @@ +export * from './slider'; diff --git a/libs/ui-toolkit/src/components/slider/slider.stories.tsx b/libs/ui-toolkit/src/components/slider/slider.stories.tsx new file mode 100644 index 000000000..8d53b4d08 --- /dev/null +++ b/libs/ui-toolkit/src/components/slider/slider.stories.tsx @@ -0,0 +1,27 @@ +import type { Story, Meta } from '@storybook/react'; +import { Slider } from './slider'; +import { useState } from 'react'; + +export default { + component: Slider, + title: 'Slider', +} as Meta; + +const Template: Story = ({ value: val, ...args }) => { + const [value, setValue] = useState(val); + + const onValueChange = (val: [number]) => { + setValue(val); + }; + + return ; +}; + +export const Default = Template.bind({}); + +Default.args = { + min: 0, + max: 1000, + step: 100, + value: [100], +}; diff --git a/libs/ui-toolkit/src/components/slider/slider.tsx b/libs/ui-toolkit/src/components/slider/slider.tsx new file mode 100644 index 000000000..ffe1f0efa --- /dev/null +++ b/libs/ui-toolkit/src/components/slider/slider.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +import * as SliderPrimitive from '@radix-ui/react-slider'; +import type { + SliderProps, + SliderTrackProps, + SliderRangeProps, + SliderThumbProps, +} from '@radix-ui/react-slider'; +import classNames from 'classnames'; + +export const SliderRoot = ({ + children, + className, + orientation = 'horizontal', + ...props +}: SliderProps) => { + const defaultStyles = 'relative flex items-center select-none touch-none'; + const classes = classNames( + defaultStyles, + { + 'h-[20px] w-full': orientation === 'horizontal', + 'flex-col w-[20px] h-full': orientation === 'vertical', + }, + className + ); + + return ( + + {children} + + ); +}; + +export const SliderTrack = ({ + children, + className, + ...props +}: SliderTrackProps) => { + const defaultStyles = 'bg-black dark:bg-white relative grow h-[3px]'; + return ( + + {children} + + ); +}; + +export const SliderRange = ({ className, ...props }: SliderRangeProps) => { + const defaultStyles = 'absolute bg-blue h-full'; + return ( + + ); +}; + +export const SliderThumb = ({ className, ...props }: SliderThumbProps) => { + const defaultStyles = + 'block w-[20px] h-[20px] border-2 border-black dark:border-white bg-white dark:bg-black rounded-full'; + return ( + + ); +}; + +export const Slider = (props: SliderProps) => { + return ( + + + + + + + ); +}; diff --git a/package.json b/package.json index 7bb1c496e..40bc5956c 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@radix-ui/react-icons": "^1.1.1", "@radix-ui/react-radio-group": "^0.1.5", "@radix-ui/react-select": "^0.1.1", + "@radix-ui/react-slider": "^1.0.0", "@radix-ui/react-tabs": "^0.1.5", "@radix-ui/react-tooltip": "^0.1.7", "@sentry/nextjs": "^6.19.3", diff --git a/yarn.lock b/yarn.lock index 6236908ba..889c5fc3a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3281,6 +3281,13 @@ dependencies: "@babel/runtime" "^7.13.10" +"@radix-ui/number@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@radix-ui/number/-/number-1.0.0.tgz#4c536161d0de750b3f5d55860fc3de46264f897b" + integrity sha512-Ofwh/1HX69ZfJRiRBMTy7rgjAzHmwe4kW9C9Y99HTRUcYLUuVT0KESFj15rPjRgKJs20GPq8Bm5aEDJ8DuA3vA== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/popper@0.1.0": version "0.1.0" resolved "https://registry.yarnpkg.com/@radix-ui/popper/-/popper-0.1.0.tgz#c387a38f31b7799e1ea0d2bb1ca0c91c2931b063" @@ -3296,6 +3303,13 @@ dependencies: "@babel/runtime" "^7.13.10" +"@radix-ui/primitive@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.0.0.tgz#e1d8ef30b10ea10e69c76e896f608d9276352253" + integrity sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-accordion@^0.1.6": version "0.1.6" resolved "https://registry.yarnpkg.com/@radix-ui/react-accordion/-/react-accordion-0.1.6.tgz#b76613d56717ed24b8cf6cb1897cbd54f04714ed" @@ -3345,6 +3359,17 @@ "@radix-ui/react-primitive" "0.1.4" "@radix-ui/react-slot" "0.1.2" +"@radix-ui/react-collection@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.0.0.tgz#0ec4c72fabd35a03b5787075ac799e3b17ca5710" + integrity sha512-8i1pf5dKjnq90Z8udnnXKzdCEV3/FYrfw0n/b6NvB6piXEn3fO1bOh7HBcpG8XrnIXzxlYu2oCcR38QpyLS/mg== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-compose-refs" "1.0.0" + "@radix-ui/react-context" "1.0.0" + "@radix-ui/react-primitive" "1.0.0" + "@radix-ui/react-slot" "1.0.0" + "@radix-ui/react-compose-refs@0.1.0": version "0.1.0" resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-0.1.0.tgz#cff6e780a0f73778b976acff2c2a5b6551caab95" @@ -3352,6 +3377,13 @@ dependencies: "@babel/runtime" "^7.13.10" +"@radix-ui/react-compose-refs@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.0.tgz#37595b1f16ec7f228d698590e78eeed18ff218ae" + integrity sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-context@0.1.1": version "0.1.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-0.1.1.tgz#06996829ea124d9a1bc1dbe3e51f33588fab0875" @@ -3359,6 +3391,13 @@ dependencies: "@babel/runtime" "^7.13.10" +"@radix-ui/react-context@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.0.0.tgz#f38e30c5859a9fb5e9aa9a9da452ee3ed9e0aee0" + integrity sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-dialog@^0.1.5": version "0.1.7" resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-0.1.7.tgz#285414cf66f5bbf42bc9935314e0381abe01e7d0" @@ -3380,6 +3419,13 @@ aria-hidden "^1.1.1" react-remove-scroll "^2.4.0" +"@radix-ui/react-direction@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.0.0.tgz#a2e0b552352459ecf96342c79949dd833c1e6e45" + integrity sha512-2HV05lGUgYcA6xgLQ4BKPDmtL+QbIZYH5fCOTAOOcJ5O0QbWS3i9lKaurLzliYUDhORI2Qr3pyjhJh44lKA3rQ== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-dismissable-layer@0.1.5": version "0.1.5" resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-0.1.5.tgz#9379032351e79028d472733a5cc8ba4a0ea43314" @@ -3513,6 +3559,14 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-slot" "0.1.2" +"@radix-ui/react-primitive@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-1.0.0.tgz#376cd72b0fcd5e0e04d252ed33eb1b1f025af2b0" + integrity sha512-EyXe6mnRlHZ8b6f4ilTDrXmkLShICIuOTTj0GX4w1rp+wSxf3+TD05u1UOITC8VsJ2a9nwHvdXtOXEOl0Cw/zQ== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-slot" "1.0.0" + "@radix-ui/react-radio-group@^0.1.5": version "0.1.5" resolved "https://registry.yarnpkg.com/@radix-ui/react-radio-group/-/react-radio-group-0.1.5.tgz#ca8a676123a18b44804aff10af46129e2c2b37c3" @@ -3570,6 +3624,24 @@ aria-hidden "^1.1.1" react-remove-scroll "^2.4.0" +"@radix-ui/react-slider@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-slider/-/react-slider-1.0.0.tgz#4cabadd243aa088eb45ac710cd7cdc518fafb07e" + integrity sha512-LMZET7vn7HYwYSjsc9Jcen8Vn4cJXZZxQT7T+lGlqp+F+FofX+H86TBF2yDq+L51d99f1KLEsflTGBz9WRLSig== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/number" "1.0.0" + "@radix-ui/primitive" "1.0.0" + "@radix-ui/react-collection" "1.0.0" + "@radix-ui/react-compose-refs" "1.0.0" + "@radix-ui/react-context" "1.0.0" + "@radix-ui/react-direction" "1.0.0" + "@radix-ui/react-primitive" "1.0.0" + "@radix-ui/react-use-controllable-state" "1.0.0" + "@radix-ui/react-use-layout-effect" "1.0.0" + "@radix-ui/react-use-previous" "1.0.0" + "@radix-ui/react-use-size" "1.0.0" + "@radix-ui/react-slot@0.1.2": version "0.1.2" resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-0.1.2.tgz#e6f7ad9caa8ce81cc8d532c854c56f9b8b6307c8" @@ -3578,6 +3650,14 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-compose-refs" "0.1.0" +"@radix-ui/react-slot@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.0.0.tgz#7fa805b99891dea1e862d8f8fbe07f4d6d0fd698" + integrity sha512-3mrKauI/tWXo1Ll+gN5dHcxDPdm/Df1ufcDLCecn+pnCIVcdWE7CujXo8QaXOWRJyZyQWWbpB8eFwHzWXlv5mQ== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-compose-refs" "1.0.0" + "@radix-ui/react-tabs@^0.1.5": version "0.1.5" resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-0.1.5.tgz#ddcf860cc32e186d76477ae767dbb216d1944252" @@ -3627,6 +3707,13 @@ dependencies: "@babel/runtime" "^7.13.10" +"@radix-ui/react-use-callback-ref@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz#9e7b8b6b4946fe3cbe8f748c82a2cce54e7b6a90" + integrity sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-use-controllable-state@0.1.0": version "0.1.0" resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-0.1.0.tgz#4fced164acfc69a4e34fb9d193afdab973a55de1" @@ -3635,6 +3722,14 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-use-callback-ref" "0.1.0" +"@radix-ui/react-use-controllable-state@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.0.tgz#a64deaafbbc52d5d407afaa22d493d687c538b7f" + integrity sha512-FohDoZvk3mEXh9AWAVyRTYR4Sq7/gavuofglmiXB2g1aKyboUD4YtgWxKj8O5n+Uak52gXQ4wKz5IFST4vtJHg== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-use-callback-ref" "1.0.0" + "@radix-ui/react-use-direction@0.1.0": version "0.1.0" resolved "https://registry.yarnpkg.com/@radix-ui/react-use-direction/-/react-use-direction-0.1.0.tgz#97ac1d52e497c974389e7988f809238ed72e7df7" @@ -3657,6 +3752,13 @@ dependencies: "@babel/runtime" "^7.13.10" +"@radix-ui/react-use-layout-effect@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.0.tgz#2fc19e97223a81de64cd3ba1dc42ceffd82374dc" + integrity sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-use-previous@0.1.1": version "0.1.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-use-previous/-/react-use-previous-0.1.1.tgz#0226017f72267200f6e832a7103760e96a6db5d0" @@ -3664,6 +3766,13 @@ dependencies: "@babel/runtime" "^7.13.10" +"@radix-ui/react-use-previous@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-previous/-/react-use-previous-1.0.0.tgz#e48a69c3a7d8078a967084038df66d0d181c56ac" + integrity sha512-RG2K8z/K7InnOKpq6YLDmT49HGjNmrK+fr82UCVKT2sW0GYfVnYp4wZWBooT/EYfQ5faA9uIjvsuMMhH61rheg== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-use-rect@0.1.1": version "0.1.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-0.1.1.tgz#6c15384beee59c086e75b89a7e66f3d2e583a856" @@ -3679,6 +3788,14 @@ dependencies: "@babel/runtime" "^7.13.10" +"@radix-ui/react-use-size@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-size/-/react-use-size-1.0.0.tgz#a0b455ac826749419f6354dc733e2ca465054771" + integrity sha512-imZ3aYcoYCKhhgNpkNDh/aTiU05qw9hX+HHI1QDBTyIlcFjgeFlKKySNGMwTp7nYFLQg/j0VA2FmCY4WPDDHMg== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-use-layout-effect" "1.0.0" + "@radix-ui/react-visually-hidden@0.1.4": version "0.1.4" resolved "https://registry.yarnpkg.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-0.1.4.tgz#6c75eae34fb5d084b503506fbfc05587ced05f03"