From 2a295e8db9d4aa060150b667a9dd47f7b2e3942e Mon Sep 17 00:00:00 2001 From: KevDydx Date: Wed, 10 Dec 2025 16:33:46 -0500 Subject: [PATCH 01/27] feat - simpleui wip --- src/App.tsx | 24 +- src/constants/routes.ts | 1 + src/hooks/useCurrentMarketId.ts | 15 +- src/hooks/useSimpleUiEnabled.ts | 10 +- src/layout/Footer/FooterMobile.tsx | 134 +----- src/pages/trade/LaunchableMarket.tsx | 2 +- src/pages/trade/Trade.tsx | 2 +- src/pages/trade/TradeHeaderMobile.tsx | 116 ----- src/pages/trade/mobile-web/Trade.tsx | 77 +++ src/pages/trade/mobile-web/TradeForm.tsx | 439 ++++++++++++++++++ .../trade/mobile-web/TradeFormHeader.tsx | 76 +++ src/pages/trade/mobile-web/TradeHeader.tsx | 65 +++ src/state/dialogs.ts | 6 + src/state/dialogsSelectors.ts | 2 + src/views/PositionInfo.tsx | 2 +- .../dialogs/MarketsDialog/MarketFilterRow.tsx | 103 ++++ src/views/dialogs/MarketsDialog/MarketRow.tsx | 92 ++++ .../dialogs/MarketsDialog/MarketsDialog.tsx | 49 ++ .../dialogs/MarketsDialog/MarketsList.tsx | 318 +++++++++++++ src/views/dialogs/MobileUserMenuDialog.tsx | 343 ++++++++++++++ src/views/mobile/MobileTradeAssetSelector.tsx | 84 ++++ 21 files changed, 1698 insertions(+), 262 deletions(-) delete mode 100644 src/pages/trade/TradeHeaderMobile.tsx create mode 100644 src/pages/trade/mobile-web/Trade.tsx create mode 100644 src/pages/trade/mobile-web/TradeForm.tsx create mode 100644 src/pages/trade/mobile-web/TradeFormHeader.tsx create mode 100644 src/pages/trade/mobile-web/TradeHeader.tsx create mode 100644 src/views/dialogs/MarketsDialog/MarketFilterRow.tsx create mode 100644 src/views/dialogs/MarketsDialog/MarketRow.tsx create mode 100644 src/views/dialogs/MarketsDialog/MarketsDialog.tsx create mode 100644 src/views/dialogs/MarketsDialog/MarketsList.tsx create mode 100644 src/views/dialogs/MobileUserMenuDialog.tsx create mode 100644 src/views/mobile/MobileTradeAssetSelector.tsx diff --git a/src/App.tsx b/src/App.tsx index 5e88788717..f4983f6c0c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -93,6 +93,10 @@ const VaultPage = lazy(() => import('@/pages/vaults/VaultPage')); const SimpleMarketsPage = lazy(() => import('@/pages/markets/simple-ui/MarketsMobile')); const SimpleAssetPage = lazy(() => import('@/pages/trade/simple-ui/AssetPage')); +// Mobile Web +const MobileWebTradePage = lazy(() => import('@/pages/trade/mobile-web/Trade')); +const MobileWebTradeFormPage = lazy(() => import('@/pages/trade/mobile-web/TradeForm')); + const Content = () => { useInitializePage(); useAnalytics(); @@ -205,28 +209,28 @@ const Content = () => { } /> - } /> - } /> + : } /> + : } + /> {isSpotEnabled && ( } /> )} + + } /> + } /> + + } /> } /> - {isTablet && ( - <> - } /> - } /> - } /> - - )} - }> } /> diff --git a/src/constants/routes.ts b/src/constants/routes.ts index 0c2e7e9cc1..76a21d4e40 100644 --- a/src/constants/routes.ts +++ b/src/constants/routes.ts @@ -5,6 +5,7 @@ export enum AppRoute { Vault = '/vault', Portfolio = '/portfolio', Trade = '/trade', + TradeForm = '/trade-form', Spot = '/spot', Profile = '/profile', Alerts = '/alerts', diff --git a/src/hooks/useCurrentMarketId.ts b/src/hooks/useCurrentMarketId.ts index f6b0921c35..d53f1c0a55 100644 --- a/src/hooks/useCurrentMarketId.ts +++ b/src/hooks/useCurrentMarketId.ts @@ -25,9 +25,9 @@ import { getLaunchedMarketIds, getMarketIds } from '@/state/perpetualsSelectors' import { useMarketsData } from './useMarketsData'; import { useAppSelectorWithArgs } from './useParameterizedSelector'; -export const useCurrentMarketId = () => { +export const useCurrentMarketId = (route: AppRoute = AppRoute.Trade) => { const navigate = useNavigate(); - const match = useMatch(`/${AppRoute.Trade}/:marketId`); + const match = useMatch(`/${route}/:marketId`); const { marketId } = match?.params ?? {}; const dispatch = useAppDispatch(); const selectedNetwork = useAppSelector(getSelectedNetwork); @@ -98,7 +98,7 @@ export const useCurrentMarketId = () => { dispatch(setCurrentMarketId(validId)); if (validId !== marketId) { - navigate(`${AppRoute.Trade}/${validId}`, { + navigate(`${route}/${validId}`, { replace: true, }); } @@ -136,7 +136,14 @@ export const useCurrentMarketId = () => { dispatch(closeDialogInTradeBox()); } } - }, [hasMarketIds, hasLoadedLaunchableMarkets, isViewingUnlaunchedMarket, marketId, navigate]); + }, [ + hasMarketIds, + hasLoadedLaunchableMarkets, + isViewingUnlaunchedMarket, + marketId, + navigate, + route, + ]); useEffect(() => { if (isViewingUnlaunchedMarket) { diff --git a/src/hooks/useSimpleUiEnabled.ts b/src/hooks/useSimpleUiEnabled.ts index 378c25fdd6..d4efb59d09 100644 --- a/src/hooks/useSimpleUiEnabled.ts +++ b/src/hooks/useSimpleUiEnabled.ts @@ -1,11 +1,3 @@ -import { useBreakpoints } from '@/hooks/useBreakpoints'; - -import { testFlags } from '@/lib/testFlags'; - export const useSimpleUiEnabled = () => { - const { isTablet } = useBreakpoints(); - const forcedSimpleUiValue = testFlags.simpleUi; - const isSimpleUi = isTablet ? (forcedSimpleUiValue ?? true) : false; - - return isSimpleUi; + return false; }; diff --git a/src/layout/Footer/FooterMobile.tsx b/src/layout/Footer/FooterMobile.tsx index 68cd8866b9..cdddfa5cfa 100644 --- a/src/layout/Footer/FooterMobile.tsx +++ b/src/layout/Footer/FooterMobile.tsx @@ -1,41 +1,20 @@ -import styled, { css } from 'styled-components'; +import styled from 'styled-components'; import tw from 'twin.macro'; -import { DialogTypes } from '@/constants/dialogs'; import { STRING_KEYS } from '@/constants/localization'; -import { DEFAULT_MARKETID } from '@/constants/markets'; import { AppRoute } from '@/constants/routes'; -import { useComplianceState } from '@/hooks/useComplianceState'; -import { useNotifications } from '@/hooks/useNotifications'; -import { useShouldShowFooter } from '@/hooks/useShouldShowFooter'; import { useStringGetter } from '@/hooks/useStringGetter'; -import { BellIcon, MarketsIcon, PortfolioIcon, ProfileIcon } from '@/icons'; +import { MarketsIcon, PortfolioIcon, TradeIcon } from '@/icons'; import { layoutMixins } from '@/styles/layoutMixins'; -import { Icon, IconName } from '@/components/Icon'; +import { Icon } from '@/components/Icon'; import { NavigationMenu } from '@/components/NavigationMenu'; -import { calculateCanAccountTrade } from '@/state/accountCalculators'; -import { useAppDispatch, useAppSelector } from '@/state/appTypes'; -import { getCurrentMarketId } from '@/state/currentMarketSelectors'; -import { openDialog } from '@/state/dialogs'; - export const FooterMobile = () => { - const dispatch = useAppDispatch(); - const stringGetter = useStringGetter(); - const canAccountTrade = useAppSelector(calculateCanAccountTrade); - - const marketId = useAppSelector(getCurrentMarketId); - - const { disableConnectButton } = useComplianceState(); - const { hasUnreadNotifications } = useNotifications(); - - if (!useShouldShowFooter()) return null; - return ( <$MobileNav> <$NavigationMenu @@ -43,58 +22,23 @@ export const FooterMobile = () => { { group: 'navigation', items: [ - canAccountTrade - ? { - value: 'trade', - label: stringGetter({ key: STRING_KEYS.TRADE }), - slotBefore: ( - <$StartIcon> - - - ), - href: `${AppRoute.Trade}/${marketId ?? DEFAULT_MARKETID}`, - } - : { - value: 'onboarding', - label: stringGetter({ key: STRING_KEYS.ONBOARDING }), - slotBefore: ( - <$StartIcon disabled={disableConnectButton}> - - - ), - onClick: () => - !disableConnectButton && dispatch(openDialog(DialogTypes.Onboarding())), - }, { - value: 'portfolio', - label: stringGetter({ key: STRING_KEYS.PORTFOLIO }), - slotBefore: <$Icon iconComponent={PortfolioIcon as any} />, - href: AppRoute.Portfolio, - }, - { - value: 'markets', + value: 'trade', label: stringGetter({ key: STRING_KEYS.MARKETS }), - slotBefore: <$Icon iconComponent={MarketsIcon as any} />, - href: AppRoute.Markets, + slotBefore: <$Icon size="0.75em" iconComponent={MarketsIcon as any} />, + href: AppRoute.Trade, }, { - value: 'alerts', - label: stringGetter({ key: STRING_KEYS.ALERTS }), - slotBefore: ( -
- <$Icon iconComponent={BellIcon as any} /> - {hasUnreadNotifications && ( - <$UnreadIndicator tw="relative right-[-0.35rem] top-[-0.55rem] place-self-center" /> - )} -
- ), - href: AppRoute.Alerts, + value: 'trade-from', + label: stringGetter({ key: STRING_KEYS.TRADE }), + slotBefore: <$Icon size="0.75em" iconComponent={TradeIcon as any} />, + href: AppRoute.TradeForm, }, { - value: 'profile', - label: stringGetter({ key: STRING_KEYS.PROFILE }), - slotBefore: <$Icon iconComponent={ProfileIcon as any} />, - href: AppRoute.Profile, + value: 'portfolio', + label: stringGetter({ key: STRING_KEYS.PORTFOLIO }), + slotBefore: <$Icon size="0.75em" iconComponent={PortfolioIcon as any} />, + href: AppRoute.Portfolio, }, ], }, @@ -133,25 +77,6 @@ const $NavigationMenu = styled(NavigationMenu)` > li { flex: 1; - - &[data-item='onboarding'], - &[data-item='trade'] { - span { - content-visibility: hidden; - position: absolute; - - @supports not (content-visibility: hidden) { - opacity: 0; - width: 0; - overflow: hidden; - } - } - - + *, - + * + * { - order: -1; - } - } } > li:not(:last-child):after { @@ -168,34 +93,3 @@ const $NavigationMenu = styled(NavigationMenu)` `; const $Icon = tw(Icon)`text-[1.5rem]`; - -const $StartIcon = styled.div<{ disabled?: boolean }>` - display: inline-flex; - flex-direction: row; - justify-content: center; - align-items: center; - margin-top: -0.25rem; - - width: 3.5rem; - height: 3.5rem; - - color: var(--color-text-2); - background-color: var(--color-accent); - border: solid var(--border-width) var(--color-border-white); - border-radius: 50%; - - ${({ disabled }) => - disabled && - css` - background-color: var(--color-layer-2); - color: var(--color-text-0); - `} -`; - -const $UnreadIndicator = styled.div` - width: 0.9rem; - height: 0.9rem; - border-radius: 50%; - background-color: var(--color-accent); - border: 2px solid var(--color-layer-2); -`; diff --git a/src/pages/trade/LaunchableMarket.tsx b/src/pages/trade/LaunchableMarket.tsx index 77ee0ff03e..2562237f05 100644 --- a/src/pages/trade/LaunchableMarket.tsx +++ b/src/pages/trade/LaunchableMarket.tsx @@ -32,7 +32,7 @@ import { InnerPanel } from './InnerPanel'; import { MarketSelectorAndStats } from './MarketSelectorAndStats'; import { MobileBottomPanel } from './MobileBottomPanel'; import { MobileTopPanel } from './MobileTopPanel'; -import { TradeHeaderMobile } from './TradeHeaderMobile'; +import { TradeHeaderMobile } from './mobile-web/TradeHeader'; import { useResizablePanel } from './useResizablePanel'; const LaunchableMarket = () => { diff --git a/src/pages/trade/Trade.tsx b/src/pages/trade/Trade.tsx index dfadbfb470..f8a9be883e 100644 --- a/src/pages/trade/Trade.tsx +++ b/src/pages/trade/Trade.tsx @@ -32,8 +32,8 @@ import { MarketSelectorAndStats } from './MarketSelectorAndStats'; import { MobileBottomPanel } from './MobileBottomPanel'; import { MobileTopPanel } from './MobileTopPanel'; import { TradeDialogTrigger } from './TradeDialogTrigger'; -import { TradeHeaderMobile } from './TradeHeaderMobile'; import { VerticalPanel } from './VerticalPanel'; +import { TradeHeaderMobile } from './mobile-web/TradeHeader'; import { useResizablePanel } from './useResizablePanel'; const TradePage = () => { diff --git a/src/pages/trade/TradeHeaderMobile.tsx b/src/pages/trade/TradeHeaderMobile.tsx deleted file mode 100644 index b4ed2cbe04..0000000000 --- a/src/pages/trade/TradeHeaderMobile.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import { BonsaiHelpers } from '@/bonsai/ontology'; -import { useNavigate } from 'react-router-dom'; -import styled from 'styled-components'; - -import { AppRoute } from '@/constants/routes'; - -import { useAppSelectorWithArgs } from '@/hooks/useParameterizedSelector'; - -import { layoutMixins } from '@/styles/layoutMixins'; - -import { AssetIcon } from '@/components/AssetIcon'; -import { BackButton } from '@/components/BackButton'; -import { Output, OutputType } from '@/components/Output'; -import { MidMarketPrice } from '@/views/MidMarketPrice'; - -import { useAppSelector } from '@/state/appTypes'; - -import { getAssetFromMarketId, getDisplayableAssetFromBaseAsset } from '@/lib/assetUtils'; -import { mapIfPresent } from '@/lib/do'; -import { MustBigNumber } from '@/lib/numbers'; -import { orEmptyObj } from '@/lib/typeUtils'; - -export const TradeHeaderMobile = ({ launchableMarketId }: { launchableMarketId?: string }) => { - const id = useAppSelector(BonsaiHelpers.currentMarket.assetId); - const name = useAppSelector(BonsaiHelpers.currentMarket.assetName); - const imageUrl = useAppSelector(BonsaiHelpers.currentMarket.assetLogo); - - const navigate = useNavigate(); - - const { displayableTicker, priceChange24H, percentChange24h } = orEmptyObj( - useAppSelector(BonsaiHelpers.currentMarket.marketInfo) - ); - - const launchableAsset = useAppSelectorWithArgs( - BonsaiHelpers.assets.selectAssetInfo, - mapIfPresent(launchableMarketId, getAssetFromMarketId) - ); - - const assetRow = launchableAsset ? ( -
- {launchableAsset.name} - <$Name> -

{launchableAsset.name}

- {getDisplayableAssetFromBaseAsset(launchableAsset.assetId)} - -
- ) : ( -
- - <$Name> -

{name}

- {displayableTicker} - -
- ); - - return ( - <$Header> - navigate(AppRoute.Markets)} /> - - {assetRow} - - <$Right> - - <$PriceChange - type={OutputType.Percent} - value={MustBigNumber(percentChange24h).abs()} - isNegative={MustBigNumber(priceChange24H).isNegative()} - /> - - - ); -}; -const $Header = styled.header` - ${layoutMixins.contentSectionDetachedScrollable} - - ${layoutMixins.stickyHeader} - z-index: 2; - - ${layoutMixins.row} - - padding-left: 1rem; - padding-right: 1.5rem; - gap: 1rem; - - color: var(--color-text-2); - background-color: var(--color-layer-2); -`; -const $Name = styled.div` - ${layoutMixins.rowColumn} - - h3 { - font: var(--font-large-medium); - } - - > :nth-child(2) { - font: var(--font-mini-book); - color: var(--color-text-0); - } -`; - -const $Right = styled.div` - margin-left: auto; - - ${layoutMixins.rowColumn} - justify-items: flex-end; -`; - -const $PriceChange = styled(Output)<{ isNegative?: boolean }>` - font: var(--font-small-book); - color: ${({ isNegative }) => (isNegative ? `var(--color-negative)` : `var(--color-positive)`)}; -`; diff --git a/src/pages/trade/mobile-web/Trade.tsx b/src/pages/trade/mobile-web/Trade.tsx new file mode 100644 index 0000000000..c412d1d756 --- /dev/null +++ b/src/pages/trade/mobile-web/Trade.tsx @@ -0,0 +1,77 @@ +import { useCallback } from 'react'; + +import styled from 'styled-components'; + +import { HORIZONTAL_PANEL_MAX_HEIGHT, HORIZONTAL_PANEL_MIN_HEIGHT } from '@/constants/layout'; + +import { useCurrentMarketId } from '@/hooks/useCurrentMarketId'; +import { usePageTitlePriceUpdates } from '@/hooks/usePageTitlePriceUpdates'; + +import { layoutMixins } from '@/styles/layoutMixins'; + +import { DetachedSection } from '@/components/ContentSection'; +import { MarketsMenuDialog } from '@/views/dialogs/MarketsDialog/MarketsDialog'; +import { UserMenuDialog } from '@/views/dialogs/MobileUserMenuDialog'; + +import { calculateCanAccountTrade } from '@/state/accountCalculators'; +import { useAppDispatch, useAppSelector } from '@/state/appTypes'; +import { setHorizontalPanelHeightPx } from '@/state/appUiConfigs'; +import { getHorizontalPanelHeightPx } from '@/state/appUiConfigsSelectors'; + +import LaunchableMarket from '../LaunchableMarket'; +import { MobileBottomPanel } from '../MobileBottomPanel'; +import { MobileTopPanel } from '../MobileTopPanel'; +import { useResizablePanel } from '../useResizablePanel'; +import { TradeHeaderMobile } from './TradeHeader'; + +const TradePage = () => { + const { isViewingUnlaunchedMarket } = useCurrentMarketId(); + const canAccountTrade = useAppSelector(calculateCanAccountTrade); + + const horizontalPanelHeightPxBase = useAppSelector(getHorizontalPanelHeightPx); + const dispatch = useAppDispatch(); + const setPanelHeight = useCallback( + (h: number) => { + dispatch(setHorizontalPanelHeightPx(h)); + }, + [dispatch] + ); + const { handleMouseDown } = useResizablePanel(horizontalPanelHeightPxBase, setPanelHeight, { + min: HORIZONTAL_PANEL_MIN_HEIGHT, + max: HORIZONTAL_PANEL_MAX_HEIGHT, + }); + + usePageTitlePriceUpdates(); + + if (isViewingUnlaunchedMarket) { + return ; + } + + return ( + <$TradeLayoutMobile> + + + <$MobileContent> + + + + + + + + + + + ); +}; + +export default TradePage; + +const $TradeLayoutMobile = styled.div` + ${layoutMixins.expandingColumnWithHeader} + min-height: 100%; +`; + +const $MobileContent = styled.article` + ${layoutMixins.contentContainerPage} +`; diff --git a/src/pages/trade/mobile-web/TradeForm.tsx b/src/pages/trade/mobile-web/TradeForm.tsx new file mode 100644 index 0000000000..0d1875ecb1 --- /dev/null +++ b/src/pages/trade/mobile-web/TradeForm.tsx @@ -0,0 +1,439 @@ +import { useCallback, useEffect, useState } from 'react'; + +import { OrderSide, OrderSizeInputs, TradeFormType } from '@/bonsai/forms/trade/types'; +import { PlaceOrderPayload } from '@/bonsai/forms/triggers/types'; +import { BonsaiHelpers } from '@/bonsai/ontology'; +import { ChevronDownIcon } from '@radix-ui/react-icons'; +import styled from 'styled-components'; +import tw from 'twin.macro'; + +import { AnalyticsEvents } from '@/constants/analytics'; +import { ButtonAction, ButtonSize, ButtonType } from '@/constants/buttons'; +import { ComplianceStates } from '@/constants/compliance'; +import { DialogTypes } from '@/constants/dialogs'; +import { STRING_KEYS } from '@/constants/localization'; +import { TOKEN_DECIMALS, USD_DECIMALS } from '@/constants/numbers'; +import { AppRoute } from '@/constants/routes'; +import { + DisplayUnit, + QUICK_LIMIT_OPTIONS, + QuickLimitOption, + SimpleUiTradeDialogSteps, +} from '@/constants/trade'; + +import { useTradeErrors } from '@/hooks/TradingForm/useTradeErrors'; +import { TradeFormSource, useTradeForm } from '@/hooks/TradingForm/useTradeForm'; +import { useComplianceState } from '@/hooks/useComplianceState'; +import { useCurrentMarketId } from '@/hooks/useCurrentMarketId'; +import { useStringGetter } from '@/hooks/useStringGetter'; + +import { Button } from '@/components/Button'; +import { Icon, IconName } from '@/components/Icon'; +import { InputType } from '@/components/Input'; +import { MarginUsageTag } from '@/components/MarginUsageTag'; +import { Output, OutputType, ShowSign } from '@/components/Output'; +import { HorizontalSeparatorFiller } from '@/components/Separator'; +import { SimpleUiPopover } from '@/components/SimpleUiPopover'; +import { TradeFormMessages } from '@/views/TradeFormMessages/TradeFormMessages'; +import { MarketsMenuDialog } from '@/views/dialogs/MarketsDialog/MarketsDialog'; +import { useTradeTypeOptions } from '@/views/forms/TradeForm/useTradeTypeOptions'; + +import { useAppDispatch, useAppSelector } from '@/state/appTypes'; +import { setDisplayUnit } from '@/state/appUiConfigs'; +import { getSelectedDisplayUnit } from '@/state/appUiConfigsSelectors'; +import { openDialog } from '@/state/dialogs'; +import { tradeFormActions } from '@/state/tradeForm'; +import { getTradeFormSummary, getTradeFormValues } from '@/state/tradeFormSelectors'; + +import { track } from '@/lib/analytics/analytics'; +import { AttemptBigNumber, BIG_NUMBERS, MustBigNumber } from '@/lib/numbers'; +import { orEmptyObj } from '@/lib/typeUtils'; + +import { TradeFormHeaderMobile } from './TradeFormHeader'; + +const TradeForm = ({ + currentStep, + setCurrentStep, + onClose, +}: { + currentStep: SimpleUiTradeDialogSteps; + setCurrentStep: (step: SimpleUiTradeDialogSteps) => void; + onClose: () => void; +}) => { + const dispatch = useAppDispatch(); + const stringGetter = useStringGetter(); + + const { isViewingUnlaunchedMarket } = useCurrentMarketId(AppRoute.TradeForm); + + const [placeOrderPayload, setPlaceOrderPayload] = useState(); + + const { selectedTradeType } = useTradeTypeOptions(); + + const displayUnit = useAppSelector(getSelectedDisplayUnit); + const tradeValues = useAppSelector(getTradeFormValues); + const fullFormSummary = useAppSelector(getTradeFormSummary); + const { complianceState } = useComplianceState(); + const { summary } = fullFormSummary; + const midPrice = useAppSelector(BonsaiHelpers.currentMarket.midPrice.data); + const buyingPower = useAppSelector(BonsaiHelpers.currentMarket.account.buyingPower); + const { assetId, displayableAsset, stepSizeDecimals, ticker, tickSizeDecimals } = orEmptyObj( + useAppSelector(BonsaiHelpers.currentMarket.stableMarketInfo) + ); + + const marginUsage = summary.accountDetailsAfter?.account?.marginUsage; + const effectiveSizes = orEmptyObj(summary.tradeInfo.inputSummary.size); + + useEffect(() => { + if (ticker) { + dispatch(tradeFormActions.setMarketId(ticker)); + } + }, [ticker, dispatch]); + + const onLastOrderIndexed = useCallback(() => { + if (currentStep === SimpleUiTradeDialogSteps.Submit) { + setCurrentStep(SimpleUiTradeDialogSteps.Confirm); + } + }, [currentStep, setCurrentStep]); + + const { placeOrderError, placeOrder, unIndexedClientId, shouldEnableTrade } = useTradeForm({ + source: TradeFormSource.SimpleTradeForm, + fullFormSummary, + onLastOrderIndexed, + }); + + const { + shouldPromptUserToPlaceLimitOrder, + isErrorShownInOrderStatusToast, + primaryAlert, + shortAlertKey, + } = useTradeErrors({ + placeOrderError, + }); + + const onSubmitOrder = async () => { + const payload = summary.tradePayload; + if (payload == null) { + return; + } + setCurrentStep(SimpleUiTradeDialogSteps.Submit); + placeOrder({ + onPlaceOrder: (tradePayload) => { + setPlaceOrderPayload(tradePayload); + dispatch(tradeFormActions.reset()); + }, + onFailure: () => { + setCurrentStep(SimpleUiTradeDialogSteps.Error); + }, + }); + }; + + const onUSDCInput = ({ formattedValue }: { floatValue?: number; formattedValue: string }) => { + const formattedValueBN = MustBigNumber(formattedValue); + if ((formattedValueBN.decimalPlaces() ?? 0) > USD_DECIMALS) { + return; + } + dispatch(tradeFormActions.setSizeUsd(formattedValue)); + }; + + const onSizeInput = ({ formattedValue }: { floatValue?: number; formattedValue: string }) => { + const formattedValueBN = MustBigNumber(formattedValue); + if ((formattedValueBN.decimalPlaces() ?? 0) > (stepSizeDecimals ?? TOKEN_DECIMALS)) { + return; + } + dispatch(tradeFormActions.setSizeToken(formattedValue)); + }; + + const onLimitPriceInput = ({ + formattedValue, + }: { + floatValue?: number; + formattedValue: string; + }) => { + const formattedValueBN = MustBigNumber(formattedValue); + if ((formattedValueBN.decimalPlaces() ?? 0) > (tickSizeDecimals ?? TOKEN_DECIMALS)) { + return; + } + dispatch(tradeFormActions.setLimitPrice(formattedValue)); + }; + + const decimals = stepSizeDecimals ?? TOKEN_DECIMALS; + + const inputConfigs = { + [DisplayUnit.Asset]: { + onInput: onSizeInput, + type: InputType.Number, + outputType: OutputType.Number, + decimals, + inputUnit: DisplayUnit.Asset, + value: + tradeValues.size != null && OrderSizeInputs.is.SIZE(tradeValues.size) + ? tradeValues.size.value.value + : tradeValues.size == null || tradeValues.size.value.value === '' + ? '' + : (AttemptBigNumber(effectiveSizes.size)?.toFixed(decimals) ?? ''), + }, + [DisplayUnit.Fiat]: { + onInput: onUSDCInput, + type: InputType.Number, + outputType: OutputType.Fiat, + decimals: USD_DECIMALS, + inputUnit: DisplayUnit.Fiat, + value: + tradeValues.size != null && OrderSizeInputs.is.USDC_SIZE(tradeValues.size) + ? tradeValues.size.value.value + : tradeValues.size == null || tradeValues.size.value.value === '' + ? '' + : (AttemptBigNumber(effectiveSizes.usdcSize)?.toFixed(USD_DECIMALS) ?? ''), + }, + }; + + const limitPriceInput = ( +
+ When {displayableAsset} price reaches +
+ ); + + const toggleDisplayUnit = () => { + if (!assetId) return; + + dispatch( + setDisplayUnit({ + newDisplayUnit: displayUnit === DisplayUnit.Asset ? DisplayUnit.Fiat : DisplayUnit.Asset, + entryPoint: 'simpleUiTradeDialog', + assetId, + }) + ); + }; + + const priceImpact = summary.tradeInfo.inputSummary.worstFillPrice + ? midPrice + ? midPrice.minus(summary.tradeInfo.inputSummary.worstFillPrice) + : 0 + : 0; + + const receiptArea = ( +
+
+ {stringGetter({ key: STRING_KEYS.BUYING_POWER })} + +
+
+ + + + {stringGetter({ key: STRING_KEYS.ESTIMATED_COST })} + +
+ {stringGetter({ key: STRING_KEYS.FEE })} + +
+ {selectedTradeType === TradeFormType.MARKET && ( +
+ + {stringGetter({ key: STRING_KEYS.PRICE_IMPACT })} + + +
+ )} + +
+ {stringGetter({ key: STRING_KEYS.TOTAL })} + +
+
+ } + > + <$FeeTrigger> + {stringGetter({ key: STRING_KEYS.FEES })} + + +
+ + ); + + const toggleConfig = + inputConfigs[displayUnit === DisplayUnit.Asset ? DisplayUnit.Fiat : DisplayUnit.Asset]; + const toggleValue = toggleConfig.value.trim().length > 0 ? toggleConfig.value : 0; + + const sizeToggle = ( + + ); + + const hasMarginUsageError = primaryAlert?.code === 'INVALID_NEW_ACCOUNT_MARGIN_USAGE'; + + const isDepositNeeded = + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + (summary.accountDetailsBefore?.account?.freeCollateral.lte(0) || hasMarginUsageError) && + complianceState === ComplianceStates.FULL_ACCESS; + + const onDepositFunds = () => { + dispatch(openDialog(DialogTypes.Deposit2({}))); + onClose(); + }; + + const placeOrderButton = isDepositNeeded ? ( + + ) : ( + + ); + + const onQuickLimitClick = (quickLimit: QuickLimitOption) => { + const percentBN = MustBigNumber(quickLimit).div(100); + + const multiplier = + tradeValues.side === OrderSide.BUY + ? BIG_NUMBERS.ONE.minus(percentBN) + : BIG_NUMBERS.ONE.plus(percentBN); + + dispatch( + tradeFormActions.setLimitPrice( + midPrice?.times(multiplier).toFixed(tickSizeDecimals ?? TOKEN_DECIMALS) ?? '' + ) + ); + + track( + AnalyticsEvents.TradeQuickLimitOptionClick({ + quickLimit, + side: tradeValues.side, + marketId: ticker ?? '', + }) + ); + }; + + const quickLimitSizeButtons = ( +
+ {QUICK_LIMIT_OPTIONS.map((quickLimit: QuickLimitOption) => ( + + ))} +
+ ); + + return ( +
+ +
{sizeToggle}
+ {tradeValues.type === TradeFormType.LIMIT && ( +
+ {limitPriceInput} + {quickLimitSizeButtons} +
+ )} + +
+ + {receiptArea} + {placeOrderButton} +
+ + +
+ ); +}; + +export default TradeForm; + +const $FeeTrigger = styled.button.attrs({ + type: 'button', +})` + ${tw`row gap-0.25 text-color-text-2`} + + svg { + color: var(--color-text-0); + } + + &[data-state='open'] { + svg { + transition: rotate 0.3s var(--ease-out-expo); + rotate: -0.5turn; + } + } +`; diff --git a/src/pages/trade/mobile-web/TradeFormHeader.tsx b/src/pages/trade/mobile-web/TradeFormHeader.tsx new file mode 100644 index 0000000000..47edd0d21f --- /dev/null +++ b/src/pages/trade/mobile-web/TradeFormHeader.tsx @@ -0,0 +1,76 @@ +import { BonsaiHelpers } from '@/bonsai/ontology'; +import styled from 'styled-components'; + +import { useAppSelectorWithArgs } from '@/hooks/useParameterizedSelector'; + +import { layoutMixins } from '@/styles/layoutMixins'; + +import { Output, OutputType, ShowSign } from '@/components/Output'; +import { MidMarketPrice } from '@/views/MidMarketPrice'; +import { MobileTradeAssetSelector } from '@/views/mobile/MobileTradeAssetSelector'; + +import { useAppSelector } from '@/state/appTypes'; + +import { MustBigNumber } from '@/lib/numbers'; + +export const TradeFormHeaderMobile = ({ launchableMarketId }: { launchableMarketId?: string }) => { + const assetId = useAppSelector(BonsaiHelpers.currentMarket.assetId); + + const launchableAsset = useAppSelectorWithArgs(BonsaiHelpers.assets.selectAssetInfo, assetId); + + const percentChange24h = launchableAsset?.percentChange24h + ? MustBigNumber(launchableAsset.percentChange24h).div(100).toNumber() + : undefined; + + const assetPrice = launchableAsset ? ( + + ) : ( + + ); + + return ( + <$Header> + + + <$Right> +
+ {assetPrice} + +
+ + + ); +}; + +const $Header = styled.header` + ${layoutMixins.contentSectionDetachedScrollable} + + ${layoutMixins.stickyHeader} + z-index: 2; + + ${layoutMixins.row} + + gap: 1rem; + + color: var(--color-text-2); + background-color: var(--color-layer-2); +`; + +const $Right = styled.div` + margin-left: auto; + + ${layoutMixins.rowColumn} + justify-items: flex-end; +`; diff --git a/src/pages/trade/mobile-web/TradeHeader.tsx b/src/pages/trade/mobile-web/TradeHeader.tsx new file mode 100644 index 0000000000..ba94a226b6 --- /dev/null +++ b/src/pages/trade/mobile-web/TradeHeader.tsx @@ -0,0 +1,65 @@ +import { BonsaiHelpers } from '@/bonsai/ontology'; +import styled from 'styled-components'; + +import { ButtonShape, ButtonSize } from '@/constants/buttons'; + +import { layoutMixins } from '@/styles/layoutMixins'; + +import { Button } from '@/components/Button'; +import { Icon, IconName } from '@/components/Icon'; +import { MobileTradeAssetSelector } from '@/views/mobile/MobileTradeAssetSelector'; +import { FavoriteButton } from '@/views/tables/MarketsTable/FavoriteButton'; + +import { useAppDispatch, useAppSelector } from '@/state/appTypes'; +import { setIsUserMenuOpen } from '@/state/dialogs'; + +export const TradeHeaderMobile = ({ launchableMarketId }: { launchableMarketId?: string }) => { + const id = useAppSelector(BonsaiHelpers.currentMarket.assetId); + const dispatch = useAppDispatch(); + + const openUserMenu = () => { + dispatch(setIsUserMenuOpen(true)); + }; + + return ( + <$Header> + {id && } + + + + <$Right> + + + + ); +}; + +const $Header = styled.header` + ${layoutMixins.contentSectionDetachedScrollable} + + ${layoutMixins.stickyHeader} + z-index: 2; + + ${layoutMixins.row} + + padding-left: 1rem; + padding-right: 1.5rem; + gap: 1rem; + + color: var(--color-text-2); + background-color: var(--color-layer-2); +`; + +const $Right = styled.div` + margin-left: auto; + + ${layoutMixins.rowColumn} + justify-items: flex-end; +`; diff --git a/src/state/dialogs.ts b/src/state/dialogs.ts index 6f5d1f0297..996e664e59 100644 --- a/src/state/dialogs.ts +++ b/src/state/dialogs.ts @@ -6,6 +6,7 @@ export interface DialogsState { activeDialog?: DialogType; activeTradeBoxDialog?: TradeBoxDialogType; isUserMenuOpen: boolean; + isMarketsMenuOpen: boolean; dialogQueue: DialogType[]; } @@ -13,6 +14,7 @@ const initialState: DialogsState = { activeDialog: undefined, activeTradeBoxDialog: undefined, isUserMenuOpen: false, + isMarketsMenuOpen: false, dialogQueue: [], }; @@ -47,6 +49,9 @@ export const dialogsSlice = createSlice({ setIsUserMenuOpen: (state, action: PayloadAction) => { state.isUserMenuOpen = action.payload; }, + setIsMarketsMenuOpen: (state, action: PayloadAction) => { + state.isMarketsMenuOpen = action.payload; + }, }, }); @@ -57,4 +62,5 @@ export const { openDialog, openDialogInTradeBox, setIsUserMenuOpen, + setIsMarketsMenuOpen, } = dialogsSlice.actions; diff --git a/src/state/dialogsSelectors.ts b/src/state/dialogsSelectors.ts index fb4d189784..0f00411e89 100644 --- a/src/state/dialogsSelectors.ts +++ b/src/state/dialogsSelectors.ts @@ -5,3 +5,5 @@ export const getActiveDialog = (state: RootState) => state.dialogs.activeDialog; export const getActiveTradeBoxDialog = (state: RootState) => state.dialogs.activeTradeBoxDialog; export const getIsUserMenuOpen = (state: RootState) => state.dialogs.isUserMenuOpen; + +export const getIsMarketsMenuOpen = (state: RootState) => state.dialogs.isMarketsMenuOpen; diff --git a/src/views/PositionInfo.tsx b/src/views/PositionInfo.tsx index 2c8720d5fa..9c5992f86d 100644 --- a/src/views/PositionInfo.tsx +++ b/src/views/PositionInfo.tsx @@ -452,7 +452,7 @@ const $MobileDetails = styled(Details)` } `; -const $Actions = styled.footer` +const $Actions = styled.div` display: flex; flex-wrap: wrap; gap: 0.5rem; diff --git a/src/views/dialogs/MarketsDialog/MarketFilterRow.tsx b/src/views/dialogs/MarketsDialog/MarketFilterRow.tsx new file mode 100644 index 0000000000..3b85bc59a3 --- /dev/null +++ b/src/views/dialogs/MarketsDialog/MarketFilterRow.tsx @@ -0,0 +1,103 @@ +import { ButtonShape, ButtonSize } from '@/constants/buttons'; +import { STRING_KEYS } from '@/constants/localization'; +import { MarketsSortType } from '@/constants/marketList'; +import { MARKET_FILTER_OPTIONS, MarketFilters } from '@/constants/markets'; +import { MenuItem } from '@/constants/menus'; + +import { useStringGetter } from '@/hooks/useStringGetter'; + +import { Button } from '@/components/Button'; +import type { DropdownMenuItem } from '@/components/DropdownMenu'; +import { Icon, IconName } from '@/components/Icon'; +import { IconButton } from '@/components/IconButton'; +import { SearchInput } from '@/components/SearchInput'; +import { SimpleUiDropdownMenu } from '@/components/SimpleUiDropdownMenu'; +import { SortIcon } from '@/components/SortIcon'; +import { NewTag } from '@/components/Tag'; +import { ToggleGroup } from '@/components/ToggleGroup'; + +const MarketFilterRow = ({ + filter, + marketFilters, + onCloseSearch, + setFilter, + setSearchFilter, + sortItems, + sortTypeLabel, +}: { + filter: MarketFilters; + marketFilters: MarketFilters[]; + onCloseSearch: () => void; + setFilter: (filter: MarketFilters) => void; + setSearchFilter: (searchFilter: string) => void; + sortItems: DropdownMenuItem[]; + sortTypeLabel: string; +}) => { + const stringGetter = useStringGetter(); + return ( +
+
+
+ + + {stringGetter({ key: STRING_KEYS.MARKETS })} + +
+
+ {sortTypeLabel} + Sort by} + > + + +
+
+
+ + { + const { labelIconName, labelStringKey, isNew } = MARKET_FILTER_OPTIONS[value]; + return { + label: labelIconName ? ( + + ) : ( + stringGetter({ + key: labelStringKey, + fallback: value, + }) + ), + slotAfter: isNew && {stringGetter({ key: STRING_KEYS.NEW })}, + value, + }; + }) satisfies MenuItem[] + } + css={{ + '--button-toggle-on-border': 'none', + '--button-toggle-off-border': 'solid var(--default-border-width) var(--color-border)', + '--button-toggle-off-backgroundColor': 'transparent', + '--button-toggle-on-backgroundColor': 'var(--color-layer-4)', + }} + value={filter} + onValueChange={setFilter} + overflow="scroll" + /> +
+
+ ); +}; + +export default MarketFilterRow; diff --git a/src/views/dialogs/MarketsDialog/MarketRow.tsx b/src/views/dialogs/MarketsDialog/MarketRow.tsx new file mode 100644 index 0000000000..cdb641dbec --- /dev/null +++ b/src/views/dialogs/MarketsDialog/MarketRow.tsx @@ -0,0 +1,92 @@ +import { Link } from 'react-router-dom'; + +import { STRING_KEYS } from '@/constants/localization'; +import { MarketData } from '@/constants/markets'; +import { AppRoute } from '@/constants/routes'; + +import { useStringGetter } from '@/hooks/useStringGetter'; + +import { AssetIcon } from '@/components/AssetIcon'; +import { Output, OutputType } from '@/components/Output'; +import { Tag } from '@/components/Tag'; + +import { useAppDispatch } from '@/state/appTypes'; +import { setIsMarketsMenuOpen } from '@/state/dialogs'; + +import { calculateMarketMaxLeverage } from '@/lib/marketsHelpers'; + +export const MarketRow = ({ className, market }: { className?: string; market: MarketData }) => { + const stringGetter = useStringGetter(); + const dispatch = useAppDispatch(); + + const closeMarketsMenu = () => { + dispatch(setIsMarketsMenuOpen(false)); + }; + + const percentChangeColor = market.percentChange24h + ? market.percentChange24h >= 0 + ? 'var(--color-positive)' + : 'var(--color-negative)' + : 'var(--color-text-1)'; + + return ( + +
+ +
+
+ + {market.displayableAsset} + + + + + +
+ + {stringGetter({ key: STRING_KEYS.MARKET })} + + } + /> +
+
+ +
+ + +
+ + ); +}; diff --git a/src/views/dialogs/MarketsDialog/MarketsDialog.tsx b/src/views/dialogs/MarketsDialog/MarketsDialog.tsx new file mode 100644 index 0000000000..7da05bcaa3 --- /dev/null +++ b/src/views/dialogs/MarketsDialog/MarketsDialog.tsx @@ -0,0 +1,49 @@ +import styled from 'styled-components'; + +import { STRING_KEYS } from '@/constants/localization'; + +import { useStringGetter } from '@/hooks/useStringGetter'; + +import { Dialog, DialogPlacement } from '@/components/Dialog'; + +import { useAppDispatch, useAppSelector } from '@/state/appTypes'; +import { setIsMarketsMenuOpen } from '@/state/dialogs'; +import { getIsMarketsMenuOpen } from '@/state/dialogsSelectors'; + +import { MarketList } from './MarketsList'; + +const Content = () => { + return ( +
+ +
+ ); +}; + +export const MarketsMenuDialog = () => { + const stringGetter = useStringGetter(); + const dispatch = useAppDispatch(); + const isMarketsMenuOpen = useAppSelector(getIsMarketsMenuOpen); + + const toggleMarketsMenu = (isOpen: boolean) => { + dispatch(setIsMarketsMenuOpen(isOpen)); + }; + + return ( + <$Dialog + isOpen={isMarketsMenuOpen} + title={stringGetter({ key: STRING_KEYS.MARKETS })} + setIsOpen={toggleMarketsMenu} + placement={DialogPlacement.Inline} + > + + + ); +}; + +const $Dialog = styled(Dialog)` + --dialog-backgroundColor: var(--color-layer-1); + --dialog-header-backgroundColor: var(--color-layer-1); + box-shadow: none; + z-index: 3; +`; diff --git a/src/views/dialogs/MarketsDialog/MarketsList.tsx b/src/views/dialogs/MarketsDialog/MarketsList.tsx new file mode 100644 index 0000000000..049989833c --- /dev/null +++ b/src/views/dialogs/MarketsDialog/MarketsList.tsx @@ -0,0 +1,318 @@ +import { useCallback, useMemo, useRef, useState } from 'react'; + +import type { Range } from '@tanstack/react-virtual'; +import { defaultRangeExtractor, useVirtualizer } from '@tanstack/react-virtual'; +import orderBy from 'lodash/orderBy'; + +import { ButtonShape, ButtonSize } from '@/constants/buttons'; +import { STRING_KEYS } from '@/constants/localization'; +import { ListItem, MarketsSortType } from '@/constants/marketList'; +import { MARKET_FILTER_OPTIONS, MarketData, MarketFilters } from '@/constants/markets'; +import { MenuItem } from '@/constants/menus'; + +import { useMarketsData } from '@/hooks/useMarketsData'; +import { useStringGetter } from '@/hooks/useStringGetter'; + +import { Button } from '@/components/Button'; +import { Icon, IconName } from '@/components/Icon'; +import { LoadingSpace } from '@/components/Loading/LoadingSpinner'; +import { SearchInput } from '@/components/SearchInput'; +import { SimpleUiDropdownMenu } from '@/components/SimpleUiDropdownMenu'; +import { SortIcon } from '@/components/SortIcon'; +import { NewTag } from '@/components/Tag'; +import { ToggleGroup } from '@/components/ToggleGroup'; + +import { useAppDispatch, useAppSelector } from '@/state/appTypes'; +import { setSimpleUISortMarketsBy } from '@/state/appUiConfigs'; +import { getSimpleUISortMarketsBy } from '@/state/appUiConfigsSelectors'; +import { setMarketFilter } from '@/state/perpetuals'; +import { getMarketFilter } from '@/state/perpetualsSelectors'; + +import { isPresent } from '@/lib/typeUtils'; + +import { MarketRow } from './MarketRow'; + +const sortMarkets = (markets: MarketData[], sortType: MarketsSortType) => { + switch (sortType) { + case MarketsSortType.MarketCap: + return orderBy(markets, (market) => market.marketCap ?? 0, ['desc']); + case MarketsSortType.Volume: + return orderBy(markets, (market) => market.volume24h ?? 0, ['desc']); + case MarketsSortType.Gainers: + return orderBy(markets, (market) => market.percentChange24h ?? 0, ['desc']); + case MarketsSortType.Losers: + return orderBy(markets, (market) => market.percentChange24h ?? 0, ['asc']); + case MarketsSortType.Favorites: + return orderBy(markets, (market) => market.isFavorite, ['desc']); + default: + return markets; + } +}; + +const POSITION_ROW_HEIGHT = 60; +const MARKET_ROW_HEIGHT = 60; +const HEADER_ROW_HEIGHT = 52; + +const getItemHeight = (item: ListItem) => { + const customHeight = item.customHeight; + const itemType = item.itemType; + + if (itemType === 'custom') { + if (customHeight == null) { + throw new Error(`MarketList: ${item} has no custom height`); + } + + return customHeight; + } + + return ( + customHeight ?? + { + position: POSITION_ROW_HEIGHT, + market: MARKET_ROW_HEIGHT, + header: HEADER_ROW_HEIGHT, + }[itemType] + ); +}; + +export const MarketList = () => { + const stringGetter = useStringGetter(); + const dispatch = useAppDispatch(); + + // Filters and sort + const filter: MarketFilters = useAppSelector(getMarketFilter); + const [searchFilter, setSearchFilter] = useState(); + const sortType = useAppSelector(getSimpleUISortMarketsBy); + + const setSortType = useCallback( + (newSortType: MarketsSortType) => { + dispatch(setSimpleUISortMarketsBy(newSortType)); + }, + [dispatch] + ); + + // Markets and Filters + const { filteredMarkets, hasMarketIds, marketFilters } = useMarketsData({ + filter, + searchFilter, + }); + + const setFilter = useCallback( + (newFilter: MarketFilters) => { + dispatch(setMarketFilter(newFilter)); + }, + [dispatch] + ); + + const sortedMarkets = sortMarkets(filteredMarkets, sortType); + + const sortItems = useMemo( + () => [ + { + label: stringGetter({ key: STRING_KEYS.MARKET_CAP }), + value: MarketsSortType.MarketCap, + active: sortType === MarketsSortType.MarketCap, + icon: , + onSelect: () => setSortType(MarketsSortType.MarketCap), + }, + { + label: stringGetter({ key: STRING_KEYS.VOLUME }), + value: MarketsSortType.Volume, + active: sortType === MarketsSortType.Volume, + icon: , + onSelect: () => setSortType(MarketsSortType.Volume), + }, + { + label: stringGetter({ key: STRING_KEYS.GAINERS }), + value: MarketsSortType.Gainers, + active: sortType === MarketsSortType.Gainers, + icon: , + onSelect: () => setSortType(MarketsSortType.Gainers), + }, + { + label: stringGetter({ key: STRING_KEYS.LOSERS }), + value: MarketsSortType.Losers, + active: sortType === MarketsSortType.Losers, + icon: , + onSelect: () => setSortType(MarketsSortType.Losers), + }, + { + label: stringGetter({ key: STRING_KEYS.FAVORITES }), + value: MarketsSortType.Favorites, + active: sortType === MarketsSortType.Favorites, + icon: , + onSelect: () => setSortType(MarketsSortType.Favorites), + }, + ], + [stringGetter, sortType, setSortType] + ); + + const searchViewItems: ListItem[] = useMemo( + () => [ + ...sortedMarkets.map((market) => ({ + itemType: 'market' as const, + item: market, + })), + ], + [sortedMarkets] + ); + + const items = searchViewItems; + + const parentRef = useRef(null); + + const activeStickyIndexRef = useRef(0); + + const stickyIndexes = useMemo( + () => items.map((item, idx) => (item.isSticky ? idx : undefined)).filter(isPresent), + [items] + ); + + const isSticky = (index: number) => stickyIndexes.includes(index); + + const isActiveSticky = (index: number) => activeStickyIndexRef.current === index; + + const rowVirtualizer = useVirtualizer({ + count: items.length, + estimateSize: (index: number) => getItemHeight(items[index]!), + getScrollElement: () => parentRef.current, + rangeExtractor: useCallback( + (range: Range) => { + activeStickyIndexRef.current = + [...stickyIndexes].reverse().find((index) => range.startIndex >= index) ?? 0; + + const next = new Set([activeStickyIndexRef.current, ...defaultRangeExtractor(range)]); + + return [...next].sort((a, b) => a - b); + }, + [stickyIndexes] + ), + }); + + if (!hasMarketIds) { + return ; + } + + const sortTypeLabel = sortItems.find((item) => item.value === sortType)?.label ?? ''; + + return ( +
+
+
+
+ +
+ {sortTypeLabel} + Sort by} + > + + +
+
+ { + const { labelIconName, labelStringKey, isNew } = MARKET_FILTER_OPTIONS[value]; + return { + label: labelIconName ? ( + + ) : ( + stringGetter({ + key: labelStringKey, + fallback: value, + }) + ), + slotAfter: isNew && {stringGetter({ key: STRING_KEYS.NEW })}, + value, + }; + }) satisfies MenuItem[] + } + css={{ + '--button-toggle-on-border': 'none', + '--button-toggle-off-border': 'solid var(--default-border-width) var(--color-border)', + '--button-toggle-off-backgroundColor': 'transparent', + '--button-toggle-on-backgroundColor': 'var(--color-layer-4)', + }} + value={filter} + onValueChange={setFilter} + overflow="scroll" + /> +
+
+
+ {rowVirtualizer.getVirtualItems().map((virtualRow) => ( +
+ +
+ ))} +
+
+ ); +}; + +const ItemRenderer = ({ listItem }: { listItem: ListItem }) => { + const height = getItemHeight(listItem); + + if (listItem.itemType === 'market') { + return ; + } + + if (listItem.itemType === 'custom') { + return
{listItem.item}
; + } + + return ( +
+ {listItem.item} + {listItem.slotRight} +
+ ); +}; diff --git a/src/views/dialogs/MobileUserMenuDialog.tsx b/src/views/dialogs/MobileUserMenuDialog.tsx new file mode 100644 index 0000000000..c245d7144a --- /dev/null +++ b/src/views/dialogs/MobileUserMenuDialog.tsx @@ -0,0 +1,343 @@ +import { useState } from 'react'; + +import { BonsaiCore } from '@/bonsai/ontology'; +import { useNavigate } from 'react-router-dom'; +import styled from 'styled-components'; + +import { OnboardingState } from '@/constants/account'; +import { ButtonAction, ButtonShape, ButtonSize, ButtonStyle } from '@/constants/buttons'; +import { ComplianceStates } from '@/constants/compliance'; +import { MODERATE_DEBOUNCE_MS } from '@/constants/debounce'; +import { DialogTypes } from '@/constants/dialogs'; +import { STRING_KEYS } from '@/constants/localization'; +import { AppRoute } from '@/constants/routes'; + +import { useAccounts } from '@/hooks/useAccounts'; +import { useComplianceState } from '@/hooks/useComplianceState'; +import { useStringGetter } from '@/hooks/useStringGetter'; + +import { Button } from '@/components/Button'; +import { Dialog, DialogPlacement } from '@/components/Dialog'; +import { Icon, IconName } from '@/components/Icon'; +import { IconButton } from '@/components/IconButton'; +import { MixedColorFiatOutput } from '@/components/MixedColorFiatOutput'; +import { OnboardingTriggerButton } from '@/views/dialogs/OnboardingTriggerButton'; + +import { calculateCanAccountTrade } from '@/state/accountCalculators'; +import { getOnboardingState } from '@/state/accountSelectors'; +import { useAppDispatch, useAppSelector } from '@/state/appTypes'; +import { openDialog, setIsUserMenuOpen } from '@/state/dialogs'; +import { getIsUserMenuOpen } from '@/state/dialogsSelectors'; +import { selectIsTurnkeyConnected } from '@/state/walletSelectors'; + +import { isTruthy } from '@/lib/isTruthy'; +import { orEmptyObj } from '@/lib/typeUtils'; +import { truncateAddress } from '@/lib/wallet'; + +const UserMenuContent = () => { + const stringGetter = useStringGetter(); + const dispatch = useAppDispatch(); + const navigate = useNavigate(); + const onboardingState = useAppSelector(getOnboardingState); + const { complianceState } = useComplianceState(); + const canAccountTrade = useAppSelector(calculateCanAccountTrade); + const isTurnkeyConnected = useAppSelector(selectIsTurnkeyConnected); + const { equity, freeCollateral } = orEmptyObj( + useAppSelector(BonsaiCore.account.parentSubaccountSummary.data) + ); + + const isLoading = + useAppSelector(BonsaiCore.account.parentSubaccountSummary.loading) === 'pending'; + + const { + sourceAccount: { address }, + dydxAddress, + } = useAccounts(); + + const [walletDisplay, setWalletDisplay] = useState<'chain' | 'source'>('chain'); + + const [copied, setCopied] = useState(false); + + const onCopy = () => { + if (walletDisplay === 'chain') { + if (!dydxAddress) return; + setCopied(true); + navigator.clipboard.writeText(dydxAddress); + setTimeout(() => setCopied(false), MODERATE_DEBOUNCE_MS); + } else { + if (!address) return; + setCopied(true); + navigator.clipboard.writeText(address); + setTimeout(() => setCopied(false), MODERATE_DEBOUNCE_MS); + } + }; + + const menuItems = [ + onboardingState === OnboardingState.AccountConnected && { + key: 'history', + label: stringGetter({ key: STRING_KEYS.HISTORY }), + icon: , + onClick: () => { + navigate(AppRoute.Portfolio); + }, + }, + onboardingState === OnboardingState.AccountConnected && { + key: 'settings', + label: stringGetter({ key: STRING_KEYS.SETTINGS }), + icon: , + onClick: () => { + navigate(AppRoute.Settings); + }, + }, + { + key: 'help', + label: stringGetter({ key: STRING_KEYS.HELP }), + icon: , + onClick: () => { + dispatch(openDialog(DialogTypes.Help())); + }, + }, + onboardingState === OnboardingState.AccountConnected && + (isTurnkeyConnected + ? { + key: 'export-turnkey', + label: stringGetter({ key: STRING_KEYS.ACCOUNT_MANAGEMENT }), + icon: , + onClick: () => { + dispatch(openDialog(DialogTypes.ManageAccount())); + }, + } + : { + key: 'export-keys', + label: stringGetter({ key: STRING_KEYS.EXPORT_DYDX_WALLET }), + icon: , + onClick: () => { + dispatch(openDialog(DialogTypes.MnemonicExport())); + }, + }), + onboardingState !== OnboardingState.Disconnected && { + key: 'disconnect-wallet', + label: stringGetter({ key: STRING_KEYS.SIGN_OUT }), + highlightColor: 'var(--color-red)', + icon: , + withoutCaret: true, + onClick: () => { + dispatch(openDialog(DialogTypes.DisconnectWallet())); + }, + }, + ].filter(isTruthy); + + const walletDisplayRow = canAccountTrade ? ( +
+ + +
+ ) : ( + + ); + + const userContent = ( + <$UserContent> +
+
+
+ profile + + dydx + +
+ +
+ + + {stringGetter({ key: STRING_KEYS.BALANCE })} + +
+
+
+ {walletDisplayRow} + + ); + + const isTransferDisabled = !canAccountTrade || isLoading; + const hasBalance = freeCollateral?.gt(0); + const showWithdrawOnly = canAccountTrade && hasBalance; + const showAllTransferOptions = + canAccountTrade && complianceState === ComplianceStates.FULL_ACCESS; + + const transferContent = showAllTransferOptions ? ( +
+ + { + dispatch(openDialog(DialogTypes.Withdraw2())); + }} + /> + { + dispatch(openDialog(DialogTypes.Transfer({}))); + }} + /> +
+ ) : showWithdrawOnly ? ( +
+ +
+ ) : null; + + const menuContent = ( + <$MenuContent> + {menuItems.map((item) => ( + + ))} + + ); + + return ( +
+ {userContent} + {transferContent} + {menuContent} +
+ ); +}; + +export const UserMenuDialog = () => { + const stringGetter = useStringGetter(); + const dispatch = useAppDispatch(); + const isUserMenuOpen = useAppSelector(getIsUserMenuOpen); + + const toggleUserMenu = (isOpen: boolean) => { + dispatch(setIsUserMenuOpen(isOpen)); + }; + + return ( + <$Dialog + isOpen={isUserMenuOpen} + title={stringGetter({ key: STRING_KEYS.MENU })} + setIsOpen={toggleUserMenu} + placement={DialogPlacement.Inline} + > + + + ); +}; + +const $UserContent = styled.div` + display: flex; + flex-direction: column; + gap: 1rem; + border-radius: 1rem; + border: solid 1.6px var(--color-layer-2); + padding: 1rem; +`; + +const $MenuContent = styled.div` + display: flex; + flex-direction: column; + border-radius: 1rem; + overflow: hidden; + gap: 1px; +`; + +const $Caret = styled(Icon)` + --icon-size: 0.5rem; + transform: rotate(0.75turn); +`; + +const $Dialog = styled(Dialog)` + --dialog-backgroundColor: var(--color-layer-1); + --dialog-header-backgroundColor: var(--color-layer-1); + box-shadow: none; + z-index: 3; +`; diff --git a/src/views/mobile/MobileTradeAssetSelector.tsx b/src/views/mobile/MobileTradeAssetSelector.tsx new file mode 100644 index 0000000000..36ffd45e95 --- /dev/null +++ b/src/views/mobile/MobileTradeAssetSelector.tsx @@ -0,0 +1,84 @@ +import { BonsaiHelpers } from '@/bonsai/ontology'; +import styled from 'styled-components'; + +import { ButtonStyle } from '@/constants/buttons'; + +import { useAppSelectorWithArgs } from '@/hooks/useParameterizedSelector'; + +import { layoutMixins } from '@/styles/layoutMixins'; + +import { AssetIcon } from '@/components/AssetIcon'; +import { Button } from '@/components/Button'; +import { Icon, IconName } from '@/components/Icon'; + +import { useAppDispatch, useAppSelector } from '@/state/appTypes'; +import { setIsMarketsMenuOpen } from '@/state/dialogs'; + +import { getAssetFromMarketId, getDisplayableAssetFromBaseAsset } from '@/lib/assetUtils'; +import { mapIfPresent } from '@/lib/do'; +import { orEmptyObj } from '@/lib/typeUtils'; + +export const MobileTradeAssetSelector = ({ + launchableMarketId, +}: { + launchableMarketId?: string; +}) => { + const id = useAppSelector(BonsaiHelpers.currentMarket.assetId); + const imageUrl = useAppSelector(BonsaiHelpers.currentMarket.assetLogo); + const leverage = useAppSelector(BonsaiHelpers.currentMarket.effectiveSelectedLeverage); + + const dispatch = useAppDispatch(); + + const { displayableTicker } = orEmptyObj(useAppSelector(BonsaiHelpers.currentMarket.marketInfo)); + + const launchableAsset = useAppSelectorWithArgs( + BonsaiHelpers.assets.selectAssetInfo, + mapIfPresent(launchableMarketId, getAssetFromMarketId) + ); + + const assetRow = launchableAsset ? ( +
+ {launchableAsset.name} + <$Name> +

{launchableAsset.name}

+ {getDisplayableAssetFromBaseAsset(launchableAsset.assetId)} + +
+ ) : ( +
+ + <$Name> +

{displayableTicker}

+ {Math.round(leverage)}x + + +
+ ); + + const openMarketsDialog = () => { + dispatch(setIsMarketsMenuOpen(true)); + }; + + return ( + + ); +}; + +const $Name = styled.div` + ${layoutMixins.inlineRow} + + h3 { + font: var(--font-large-medium); + } + + > :nth-child(2) { + font: var(--font-mini-book); + color: var(--color-text-0); + } +`; From 2189d3a2047109dabf3b2c2de3214db63eeb795c Mon Sep 17 00:00:00 2001 From: KevDydx Date: Mon, 12 Jan 2026 15:14:06 +0700 Subject: [PATCH 02/27] updates --- src/bonsai/forms/trade/fields.ts | 11 +- src/bonsai/forms/trade/reducer.ts | 6 + src/bonsai/forms/trade/types.ts | 2 + src/components/Collapsible.tsx | 3 + src/components/CollapsibleTabs.tsx | 24 +- src/components/MobileDropdownMenu.tsx | 112 +++++ src/constants/statsig.ts | 1 + src/hooks/tradingView/useTradingView.ts | 60 +-- src/hooks/useCurrentMarketId.ts | 1 + src/hooks/useSimpleUiEnabled.ts | 9 + src/lib/tradingView/utils.ts | 3 + src/pages/trade/HorizontalPanel.tsx | 109 ++++- src/pages/trade/MobileTopPanel.tsx | 60 +-- src/pages/trade/Trade.tsx | 7 +- src/pages/trade/mobile-web/CloseTradeForm.tsx | 371 +++++++++++++++ .../trade/mobile-web/RegularTradeForm.tsx | 422 +++++++++++++++++ src/pages/trade/mobile-web/Trade.tsx | 95 +++- src/pages/trade/mobile-web/TradeForm.tsx | 425 +----------------- .../trade/mobile-web/TradeFormHeader.tsx | 8 +- src/pages/trade/mobile-web/TradeHeader.tsx | 252 ++++++++++- src/views/MarketStatsDetails.tsx | 32 +- src/views/charts/TradingView/TvChart.tsx | 4 +- src/views/dialogs/MarketsDialog/MarketRow.tsx | 2 +- .../dialogs/MarketsDialog/MarketsList.tsx | 2 +- src/views/dialogs/MobileUserMenuDialog.tsx | 4 +- src/views/dialogs/SetMarketLeverageDialog.tsx | 2 +- .../TradeForm/PlaceOrderButtonAndReceipt.tsx | 61 +-- src/views/mobile/MobileTradeAssetSelector.tsx | 26 +- src/views/tables/MobilePositionsTable.tsx | 336 ++++++++++++++ .../PositionsTable/PositionsTriggersCell.tsx | 33 +- 30 files changed, 1864 insertions(+), 619 deletions(-) create mode 100644 src/components/MobileDropdownMenu.tsx create mode 100644 src/pages/trade/mobile-web/CloseTradeForm.tsx create mode 100644 src/pages/trade/mobile-web/RegularTradeForm.tsx create mode 100644 src/views/tables/MobilePositionsTable.tsx diff --git a/src/bonsai/forms/trade/fields.ts b/src/bonsai/forms/trade/fields.ts index abd2ad6a25..55d811e53e 100644 --- a/src/bonsai/forms/trade/fields.ts +++ b/src/bonsai/forms/trade/fields.ts @@ -76,6 +76,7 @@ export function getTradeFormFieldStates( goodTil: DEFAULT_GOOD_TIL_TIME, stopLossOrder: undefined, takeProfitOrder: undefined, + isClosingPosition: false, }; // Initialize all fields as not visible @@ -151,7 +152,14 @@ export function getTradeFormFieldStates( makeTriggersVisibleIfPossible(result); switch (type) { case TradeFormType.MARKET: - makeVisible(result, ['marketId', 'side', 'size', 'marginMode', 'reduceOnly']); + makeVisible(result, [ + 'marketId', + 'side', + 'size', + 'marginMode', + 'reduceOnly', + 'isClosingPosition', + ]); setMarginMode(result); disableReduceOnlyIfIncreasingMarketOrder(result); @@ -166,6 +174,7 @@ export function getTradeFormFieldStates( 'marginMode', 'reduceOnly', 'postOnly', + 'isClosingPosition', ]); defaultSizeIfSizeInputIsInvalid(result); setMarginMode(result); diff --git a/src/bonsai/forms/trade/reducer.ts b/src/bonsai/forms/trade/reducer.ts index 5f8148666d..d950fc8e03 100644 --- a/src/bonsai/forms/trade/reducer.ts +++ b/src/bonsai/forms/trade/reducer.ts @@ -32,6 +32,7 @@ const getMinimumRequiredFields = ( triggerPrice: undefined, stopLossOrder: undefined, takeProfitOrder: undefined, + isClosingPosition: undefined, }; // Add marketId if provided @@ -86,6 +87,11 @@ export const tradeFormReducer = createVanillaReducer({ marginMode, }), + setIsClosingPosition: (state, isClosingPosition: boolean) => ({ + ...state, + isClosingPosition, + }), + // Size related actions setSizeToken: (state, value: string) => ({ ...state, diff --git a/src/bonsai/forms/trade/types.ts b/src/bonsai/forms/trade/types.ts index fa003bf60c..0997a47dff 100644 --- a/src/bonsai/forms/trade/types.ts +++ b/src/bonsai/forms/trade/types.ts @@ -120,6 +120,8 @@ export type TradeForm = { // additional triggers stopLossOrder: TriggerOrderState | undefined; takeProfitOrder: TriggerOrderState | undefined; + + isClosingPosition?: boolean; }; // Define the FieldState type with conditional properties diff --git a/src/components/Collapsible.tsx b/src/components/Collapsible.tsx index 177700f781..8c6c9af6f8 100644 --- a/src/components/Collapsible.tsx +++ b/src/components/Collapsible.tsx @@ -85,6 +85,9 @@ const $Root = styled(Root)` &[data-state='open'] { gap: 0.5rem; } + + padding-bottom: 1rem; + border-bottom: solid 1px var(--color-border); `; const $Trigger = styled(Trigger)` diff --git a/src/components/CollapsibleTabs.tsx b/src/components/CollapsibleTabs.tsx index 29efedd0ed..89c77645a7 100644 --- a/src/components/CollapsibleTabs.tsx +++ b/src/components/CollapsibleTabs.tsx @@ -90,17 +90,19 @@ export const CollapsibleTabs = ({ ))} - - {currentTab?.slotToolbar ?? slotToolbar} - - <$IconButton - iconName={IconName.Caret} - isToggle - buttonStyle={ButtonStyle.WithoutBackground} - shape={ButtonShape.Square} - /> - - + {slotToolbar && ( + + {currentTab?.slotToolbar ?? slotToolbar} + + <$IconButton + iconName={IconName.Caret} + isToggle + buttonStyle={ButtonStyle.WithoutBackground} + shape={ButtonShape.Square} + /> + + + )} diff --git a/src/components/MobileDropdownMenu.tsx b/src/components/MobileDropdownMenu.tsx new file mode 100644 index 0000000000..013d0d5f36 --- /dev/null +++ b/src/components/MobileDropdownMenu.tsx @@ -0,0 +1,112 @@ +import { forwardRef } from 'react'; + +import { Content, Item, Portal, Root, Separator, Trigger } from '@radix-ui/react-dropdown-menu'; +import { CaretDownIcon } from '@radix-ui/react-icons'; +import { Fragment } from 'react/jsx-runtime'; +import styled from 'styled-components'; + +import { forwardRefFn } from '@/lib/genericFunctionalComponentUtils'; + +import { Button, ButtonProps } from './Button'; +import { DropdownMenuItem } from './DropdownMenu'; + +type StyleProps = { + align?: 'center' | 'start' | 'end'; + side?: 'top' | 'bottom'; + sideOffset?: number; + className?: string; + withPortal?: boolean; +}; + +export const MobileDropdownMenu = forwardRefFn( + ({ + className, + children, + items, + slotTop, + align, + side, + sideOffset, + withPortal = true, + }: StyleProps & { + children: React.ReactNode; + items: DropdownMenuItem[]; + slotTop?: React.ReactNode; + }) => { + const content = ( + + {slotTop && ( + <> +
{slotTop}
+ + + )} + {items.map((item, idx) => ( + + + {item.label} + {item.icon && {item.icon}} + + {idx !== items.length - 1 && ( + + )} + + ))} +
+ ); + + const dropdownContent = withPortal ? {content} : content; + + return ( + + + {children} + + + {dropdownContent} + + ); + } +); + +export const DropdownMenuTrigger = forwardRef( + ({ children, ...props }, ref) => ( + + {children} + + ) +); + +const DropdownMenuButton = styled(Button)` + display: flex; + justify-content: space-between; + align-items: center; + border-radius: 4px; + + &[data-state='open'] { + svg { + transition: rotate 0.3s var(--ease-out-expo); + rotate: -0.5turn; + } + } +`; diff --git a/src/constants/statsig.ts b/src/constants/statsig.ts index afeb6bd730..7e37fc5bf1 100644 --- a/src/constants/statsig.ts +++ b/src/constants/statsig.ts @@ -18,6 +18,7 @@ export enum StatsigFlags { ffSwapEnabled = 'ff_swap_ui_web', ffSpot = 'ff_spot', abPopupDeposit = 'ab_popup_deposit', + ffMobileWeb = 'ff_mobile_web', } export enum CustomFlags { diff --git a/src/hooks/tradingView/useTradingView.ts b/src/hooks/tradingView/useTradingView.ts index 675401efd2..a870e921dc 100644 --- a/src/hooks/tradingView/useTradingView.ts +++ b/src/hooks/tradingView/useTradingView.ts @@ -11,7 +11,7 @@ import { import { DEFAULT_RESOLUTION } from '@/constants/candles'; import { TOGGLE_ACTIVE_CLASS_NAME } from '@/constants/charts'; -import { STRING_KEYS, SUPPORTED_LOCALE_MAP } from '@/constants/localization'; +import { SUPPORTED_LOCALE_MAP } from '@/constants/localization'; import type { TvWidget } from '@/constants/tvchart'; import { store } from '@/state/_store'; @@ -140,35 +140,35 @@ export const useTradingView = ({ // Initialize additional right-click-menu options tvChartWidget!.onContextMenu(tradingViewLimitOrder); - tvChartWidget!.headerReady().then(() => { - // Order Lines - initializeToggle({ - toggleRef: orderLineToggleRef, - widget: tvChartWidget!, - isOn: orderLinesToggleOn, - setToggleOn: setOrderLinesToggleOn, - label: stringGetter({ - key: STRING_KEYS.ORDER_LINES, - }), - tooltip: stringGetter({ - key: STRING_KEYS.ORDER_LINES_TOOLTIP, - }), - }); - - // Buy/Sell Marks - initializeToggle({ - toggleRef: buySellMarksToggleRef, - widget: tvChartWidget!, - isOn: buySellMarksToggleOn, - setToggleOn: setBuySellMarksToggleOn, - label: stringGetter({ - key: STRING_KEYS.BUYS_SELLS_TOGGLE, - }), - tooltip: stringGetter({ - key: STRING_KEYS.BUYS_SELLS_TOGGLE_TOOLTIP, - }), - }); - }); + // tvChartWidget!.headerReady().then(() => { + // // Order Lines + // initializeToggle({ + // toggleRef: orderLineToggleRef, + // widget: tvChartWidget!, + // isOn: orderLinesToggleOn, + // setToggleOn: setOrderLinesToggleOn, + // label: stringGetter({ + // key: STRING_KEYS.ORDER_LINES, + // }), + // tooltip: stringGetter({ + // key: STRING_KEYS.ORDER_LINES_TOOLTIP, + // }), + // }); + + // // Buy/Sell Marks + // initializeToggle({ + // toggleRef: buySellMarksToggleRef, + // widget: tvChartWidget!, + // isOn: buySellMarksToggleOn, + // setToggleOn: setBuySellMarksToggleOn, + // label: stringGetter({ + // key: STRING_KEYS.BUYS_SELLS_TOGGLE, + // }), + // tooltip: stringGetter({ + // key: STRING_KEYS.BUYS_SELLS_TOGGLE_TOOLTIP, + // }), + // }); + // }); tvChartWidget!.subscribe('onAutoSaveNeeded', () => tvChartWidget!.save((chartConfig: object) => { diff --git a/src/hooks/useCurrentMarketId.ts b/src/hooks/useCurrentMarketId.ts index d53f1c0a55..9aade9f6cb 100644 --- a/src/hooks/useCurrentMarketId.ts +++ b/src/hooks/useCurrentMarketId.ts @@ -165,6 +165,7 @@ export const useCurrentMarketId = (route: AppRoute = AppRoute.Trade) => { }, []); return { + marketId: validId, isViewingUnlaunchedMarket, hasLoadedMarkets: hasLoadedLaunchableMarkets && hasMarketIds, }; diff --git a/src/hooks/useSimpleUiEnabled.ts b/src/hooks/useSimpleUiEnabled.ts index d4efb59d09..04a4ad0239 100644 --- a/src/hooks/useSimpleUiEnabled.ts +++ b/src/hooks/useSimpleUiEnabled.ts @@ -1,3 +1,12 @@ +// import { useBreakpoints } from '@/hooks/useBreakpoints'; + +// import { testFlags } from '@/lib/testFlags'; + export const useSimpleUiEnabled = () => { return false; + // const { isTablet } = useBreakpoints(); + // const forcedSimpleUiValue = testFlags.simpleUi; + // const isSimpleUi = isTablet ? (forcedSimpleUiValue ?? true) : false; + + // return isSimpleUi; }; diff --git a/src/lib/tradingView/utils.ts b/src/lib/tradingView/utils.ts index 9b80da2663..9cf92a8aca 100644 --- a/src/lib/tradingView/utils.ts +++ b/src/lib/tradingView/utils.ts @@ -225,6 +225,8 @@ export const getWidgetOptions = ( 'header_resolutions', ]; + const disabledFeaturesForTablet: TradingTerminalFeatureset[] = ['header_widget']; + const disabledFeatures: TradingTerminalFeatureset[] = [ 'header_symbol_search', 'header_compare', @@ -236,6 +238,7 @@ export const getWidgetOptions = ( 'trading_account_manager', ...(isViewingUnlaunchedMarket ? disabledFeaturesForUnlaunchedMarket : []), ...(isSimpleUi ? disabledFeaturesForSimpleUi : []), + ...(isTablet ? disabledFeaturesForTablet : []), ]; // Needed for iframe loading on some mobile browsers diff --git a/src/pages/trade/HorizontalPanel.tsx b/src/pages/trade/HorizontalPanel.tsx index 0e6a242911..76856aad6b 100644 --- a/src/pages/trade/HorizontalPanel.tsx +++ b/src/pages/trade/HorizontalPanel.tsx @@ -4,6 +4,7 @@ import { BonsaiCore } from '@/bonsai/ontology'; import { useNavigate } from 'react-router-dom'; import styled from 'styled-components'; +import { DialogTypes } from '@/constants/dialogs'; import { STRING_KEYS } from '@/constants/localization'; import { EMPTY_ARR } from '@/constants/objects'; import { AppRoute } from '@/constants/routes'; @@ -14,15 +15,16 @@ import { useShouldShowTriggers } from '@/hooks/useShouldShowTriggers'; import { useStringGetter } from '@/hooks/useStringGetter'; import { CollapsibleTabs } from '@/components/CollapsibleTabs'; +import { Icon, IconName } from '@/components/Icon'; import { LoadingSpinner } from '@/components/Loading/LoadingSpinner'; -import { MobileTabs } from '@/components/Tabs'; +import { Output, OutputType } from '@/components/Output'; import { Tag, TagType } from '@/components/Tag'; import { FundingPaymentsTable, FundingPaymentsTableColumnKey, } from '@/pages/funding/FundingPaymentsTable'; -import { PositionInfo } from '@/views/PositionInfo'; import { FillsTable, FillsTableColumnKey } from '@/views/tables/FillsTable'; +import { MobilePositionsTable } from '@/views/tables/MobilePositionsTable'; import { OrdersTable, OrdersTableColumnKey } from '@/views/tables/OrdersTable'; import { PositionsTable, PositionsTableColumnKey } from '@/views/tables/PositionsTable'; @@ -36,19 +38,22 @@ import { createGetUnseenOpenOrdersCount, createGetUnseenOrderHistoryCount, } from '@/state/accountSelectors'; -import { useAppSelector } from '@/state/appTypes'; +import { useAppDispatch, useAppSelector } from '@/state/appTypes'; import { getDefaultToAllMarketsInPositionsOrdersFills } from '@/state/appUiConfigsSelectors'; import { getCurrentMarketId } from '@/state/currentMarketSelectors'; +import { openDialog } from '@/state/dialogs'; import { getHasUncommittedOrders } from '@/state/localOrdersSelectors'; import { isTruthy } from '@/lib/isTruthy'; import { shortenNumberForDisplay } from '@/lib/numbers'; +import { orEmptyObj } from '@/lib/typeUtils'; import { TradeTableSettings } from './TradeTableSettings'; import { MaybeUnopenedIsolatedPositionsDrawer } from './UnopenedIsolatedPositions'; import { MarketTypeFilter, PanelView } from './types'; enum InfoSection { + Details = 'Details', Position = 'Position', Orders = 'Orders', OrderHistory = 'OrderHistory', @@ -59,12 +64,12 @@ enum InfoSection { type ElementProps = { isOpen?: boolean; setIsOpen?: (isOpen: boolean) => void; - handleStartResize?: (e: React.MouseEvent) => void; }; -export const HorizontalPanel = ({ isOpen = true, setIsOpen, handleStartResize }: ElementProps) => { +export const HorizontalPanel = ({ isOpen = true, setIsOpen }: ElementProps) => { const stringGetter = useStringGetter(); const navigate = useNavigate(); + const dispatch = useAppDispatch(); const { isTablet } = useBreakpoints(); const allMarkets = useAppSelector(getDefaultToAllMarketsInPositionsOrdersFills); @@ -89,6 +94,7 @@ export const HorizontalPanel = ({ isOpen = true, setIsOpen, handleStartResize }: showCurrentMarket ? currentMarketId : undefined ); const orderHistoryTagNumber = shortenNumberForDisplay(numUnseenOrderHistory); + const subAccount = orEmptyObj(useAppSelector(BonsaiCore.account.parentSubaccountSummary.data)); const openOrdersCount = useAppSelectorWithArgs( createGetOpenOrdersCount, @@ -115,6 +121,8 @@ export const HorizontalPanel = ({ isOpen = true, setIsOpen, handleStartResize }: const initialPageSize = 20; + const { freeCollateral: availableBalance } = subAccount; + const onViewOrders = useCallback( (market: string) => { navigate(`${AppRoute.Trade}/${market}`, { @@ -128,6 +136,17 @@ export const HorizontalPanel = ({ isOpen = true, setIsOpen, handleStartResize }: [navigate] ); + const detailsTabItem = useMemo( + () => ({ + value: InfoSection.Details, + label: stringGetter({ + key: STRING_KEYS.DETAILS, + }), + content: null, + }), + [stringGetter] + ); + const positionTabItem = useMemo( () => ({ value: InfoSection.Position, @@ -138,7 +157,30 @@ export const HorizontalPanel = ({ isOpen = true, setIsOpen, handleStartResize }: tag: showCurrentMarket ? null : shortenNumberForDisplay(numTotalPositions), content: isTablet ? ( - + ) : ( [positionTabItem, ordersTabItem, fillsTabItem, orderHistoryTabItem, paymentsTabItem], - [positionTabItem, fillsTabItem, ordersTabItem, orderHistoryTabItem, paymentsTabItem] + () => [ + detailsTabItem, + positionTabItem, + ordersTabItem, + fillsTabItem, + orderHistoryTabItem, + paymentsTabItem, + ], + [ + detailsTabItem, + positionTabItem, + fillsTabItem, + ordersTabItem, + orderHistoryTabItem, + paymentsTabItem, + ] ); const slotBottom = { + [InfoSection.Details]: null, [InfoSection.Position]: ( ), @@ -397,12 +454,22 @@ export const HorizontalPanel = ({ isOpen = true, setIsOpen, handleStartResize }: [InfoSection.Payments]: null, }[tab]; - return isTablet ? ( - - ) : ( + return ( <> - {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */} - <$DragHandle onMouseDown={handleStartResize} /> +
+
dispatch(openDialog(DialogTypes.Deposit2({})))} + > + + {stringGetter({ key: STRING_KEYS.AVAILABLE_TO_TRADE })} + +
+ + +
+
+
<$CollapsibleTabs defaultTab={InfoSection.Position} @@ -412,13 +479,15 @@ export const HorizontalPanel = ({ isOpen = true, setIsOpen, handleStartResize }: onOpenChange={setIsOpen} dividerStyle="underline" slotToolbar={ - + isTablet ? null : ( + + ) } tabItems={tabItems} /> diff --git a/src/pages/trade/MobileTopPanel.tsx b/src/pages/trade/MobileTopPanel.tsx index d4cc08f171..96ebd94d98 100644 --- a/src/pages/trade/MobileTopPanel.tsx +++ b/src/pages/trade/MobileTopPanel.tsx @@ -9,11 +9,8 @@ import { useStringGetter } from '@/hooks/useStringGetter'; import { layoutMixins } from '@/styles/layoutMixins'; -import { Icon, IconName } from '@/components/Icon'; import { Tabs } from '@/components/Tabs'; import { ToggleButton } from '@/components/ToggleButton'; -import { AccountInfo } from '@/views/AccountInfo'; -import { CanvasOrderbook } from '@/views/CanvasOrderbook/CanvasOrderbook'; import { DepthChart } from '@/views/charts/DepthChart'; import { FundingChart } from '@/views/charts/FundingChart'; import { TvChart } from '@/views/charts/TradingView/TvChart'; @@ -35,10 +32,9 @@ enum Tab { LiveTrades = 'LiveTrades', } -const TabButton = ({ value, label, icon }: { value: Tab; label: string; icon: IconName }) => ( +const TabButton = ({ value, label }: { value: Tab; label: string }) => ( <$TabButton> - {label} @@ -55,40 +51,16 @@ export const MobileTopPanel = ({ const [value, setValue] = useState(Tab.Price); const items = [ - { - content: , - label: stringGetter({ key: STRING_KEYS.WALLET }), - value: Tab.Account, - icon: IconName.Coins, - }, { content: , forceMount: true, - label: stringGetter({ key: STRING_KEYS.PRICE }), + label: 'Chart', // stringGetter({ key: STRING_KEYS.CHART }), value: Tab.Price, - icon: IconName.PriceChart, }, !isViewingUnlaunchedMarket && { content: , label: stringGetter({ key: STRING_KEYS.DEPTH_CHART_SHORT }), value: Tab.Depth, - icon: IconName.DepthChart, - }, - !isViewingUnlaunchedMarket && { - content: , - label: stringGetter({ key: STRING_KEYS.FUNDING_RATE_CHART_SHORT }), - value: Tab.Funding, - icon: IconName.FundingChart, - }, - !isViewingUnlaunchedMarket && { - content: ( - <$ScrollableTableContainer> - - - ), - label: stringGetter({ key: STRING_KEYS.ORDERBOOK_SHORT }), - value: Tab.OrderBook, - icon: IconName.Orderbook, }, !isViewingUnlaunchedMarket && { content: ( @@ -96,9 +68,13 @@ export const MobileTopPanel = ({ ), - label: stringGetter({ key: STRING_KEYS.RECENT }), + label: stringGetter({ key: STRING_KEYS.TRADES }), value: Tab.LiveTrades, - icon: IconName.Clock, + }, + !isViewingUnlaunchedMarket && { + content: , + label: stringGetter({ key: STRING_KEYS.FUNDING_RATE_CHART_SHORT }), + value: Tab.Funding, }, ].filter(isTruthy); @@ -109,11 +85,9 @@ export const MobileTopPanel = ({ onValueChange={setValue} items={items.map((item) => ({ ...item, - customTrigger: ( - - ), + customTrigger: , }))} - side="bottom" + side="top" /> ); }; @@ -122,7 +96,7 @@ type TabsStyleProps = { $shortMode?: boolean }; const TabsTypeTemp = getSimpleStyledOutputType(Tabs, {} as TabsStyleProps); const $Tabs = styled(Tabs)` - --scrollArea-height: ${({ $shortMode }) => ($shortMode ? '19rem' : '38rem')}; + --scrollArea-height: ${({ $shortMode }) => ($shortMode ? '19rem' : '27rem')}; --stickyArea0-background: var(--color-layer-2); --tabContent-height: calc(var(--scrollArea-height) - 2rem - var(--tabs-currentHeight)); @@ -143,21 +117,11 @@ const $Tabs = styled(Tabs)` const $TabButton = styled(ToggleButton)` padding: 0 0.5rem; + height: 2.25rem; span { transition: 0.25s var(--ease-out-expo); } - - &[data-state='inactive'] { - --button-width: var(--button-height); - - gap: 0; - - span { - font-size: 0; - opacity: 0; - } - } `; const $ScrollableTableContainer = styled.div` ${layoutMixins.scrollArea} diff --git a/src/pages/trade/Trade.tsx b/src/pages/trade/Trade.tsx index f8a9be883e..2d64d7a0bb 100644 --- a/src/pages/trade/Trade.tsx +++ b/src/pages/trade/Trade.tsx @@ -30,7 +30,6 @@ import { InnerPanel } from './InnerPanel'; import LaunchableMarket from './LaunchableMarket'; import { MarketSelectorAndStats } from './MarketSelectorAndStats'; import { MobileBottomPanel } from './MobileBottomPanel'; -import { MobileTopPanel } from './MobileTopPanel'; import { TradeDialogTrigger } from './TradeDialogTrigger'; import { VerticalPanel } from './VerticalPanel'; import { TradeHeaderMobile } from './mobile-web/TradeHeader'; @@ -74,11 +73,7 @@ const TradePage = () => {
- - - - - + diff --git a/src/pages/trade/mobile-web/CloseTradeForm.tsx b/src/pages/trade/mobile-web/CloseTradeForm.tsx new file mode 100644 index 0000000000..476d028f37 --- /dev/null +++ b/src/pages/trade/mobile-web/CloseTradeForm.tsx @@ -0,0 +1,371 @@ +import { useCallback, useEffect, useState } from 'react'; + +import { OrderSide, OrderSizeInputs, TradeFormType } from '@/bonsai/forms/trade/types'; +import { BonsaiHelpers } from '@/bonsai/ontology'; +import BigNumber from 'bignumber.js'; +import styled, { css } from 'styled-components'; + +import { ButtonAction, ButtonShape, ButtonSize } from '@/constants/buttons'; +import { STRING_KEYS } from '@/constants/localization'; +import { TOKEN_DECIMALS, USD_DECIMALS } from '@/constants/numbers'; +import { MobilePlaceOrderSteps } from '@/constants/trade'; + +import { useTradeErrors } from '@/hooks/TradingForm/useTradeErrors'; +import { TradeFormSource, useTradeForm } from '@/hooks/TradingForm/useTradeForm'; +import { useClosePositionFormInputs } from '@/hooks/useClosePositionFormInputs'; +import { useIsFirstRender } from '@/hooks/useIsFirstRender'; +import { useStringGetter } from '@/hooks/useStringGetter'; + +import { formMixins } from '@/styles/formMixins'; + +import { Button } from '@/components/Button'; +import { FormInput } from '@/components/FormInput'; +import { InputType } from '@/components/Input'; +import { DropdownMenuTrigger, MobileDropdownMenu } from '@/components/MobileDropdownMenu'; +import { Tag } from '@/components/Tag'; +import { WithTooltip } from '@/components/WithTooltip'; +import { TradeFormMessages } from '@/views/TradeFormMessages/TradeFormMessages'; +import { AllocationSlider } from '@/views/forms/TradeForm/AllocationSlider'; +import { PlaceOrderButtonAndReceipt } from '@/views/forms/TradeForm/PlaceOrderButtonAndReceipt'; + +import { getCurrentMarketPositionData } from '@/state/accountSelectors'; +import { useAppDispatch, useAppSelector } from '@/state/appTypes'; +import { closePositionFormActions } from '@/state/closePositionForm'; +import { + getClosePositionFormSummary, + getClosePositionFormValues, +} from '@/state/tradeFormSelectors'; + +import { mapIfPresent } from '@/lib/do'; +import { AttemptBigNumber, MaybeBigNumber } from '@/lib/numbers'; +import { orEmptyObj } from '@/lib/typeUtils'; + +import { TradeFormHeaderMobile } from './TradeFormHeader'; + +type Props = { + market: string; +}; + +const CloseTradeForm = ({ market }: Props) => { + const dispatch = useAppDispatch(); + const isFirstRender = useIsFirstRender(); + const stringGetter = useStringGetter(); + + const [currentStep, setCurrentStep] = useState(); + + const { stepSizeDecimals, tickSizeDecimals, displayableAsset } = orEmptyObj( + useAppSelector(BonsaiHelpers.currentMarket.stableMarketInfo) + ); + const { signedSize: positionSize } = orEmptyObj(useAppSelector(getCurrentMarketPositionData)); + + const tradeValues = useAppSelector(getClosePositionFormValues); + const fullSummary = useAppSelector(getClosePositionFormSummary); + const { summary } = fullSummary; + const effectiveSizes = summary.tradeInfo.inputSummary.size; + + const { + amountInput, + onAmountInput, + setLimitPriceToMidPrice, + limitPriceInput, + onLimitPriceInput, + } = useClosePositionFormInputs(); + + // default to market + useEffect(() => { + dispatch(closePositionFormActions.setOrderType(TradeFormType.MARKET)); + dispatch(closePositionFormActions.setSizeAvailablePercent('1')); + }, [dispatch]); + + useEffect(() => { + dispatch(closePositionFormActions.setMarketId(market)); + dispatch(closePositionFormActions.setSizeAvailablePercent('1')); + }, [market, currentStep, dispatch]); + + const onLastOrderIndexed = useCallback(() => { + // if (!isFirstRender) { + // dispatch(closePositionFormActions.setOrderType(TradeFormType.MARKET)); + // dispatch(closePositionFormActions.reset()); + // dispatch(closePositionFormActions.setSizeAvailablePercent('1')); + // if (currentStep === MobilePlaceOrderSteps.PlacingOrder) { + // setCurrentStep?.(MobilePlaceOrderSteps.Confirmation); + // } + // } + }, [currentStep, dispatch, isFirstRender, setCurrentStep]); + + const { + placeOrderError: closePositionError, + placeOrder, + shouldEnableTrade, + tradingUnavailable, + hasValidationErrors, + } = useTradeForm({ + source: TradeFormSource.ClosePositionForm, + fullFormSummary: fullSummary, + onLastOrderIndexed, + }); + + const { + shouldPromptUserToPlaceLimitOrder, + isErrorShownInOrderStatusToast, + primaryAlert, + shortAlertKey, + } = useTradeErrors({ + placeOrderError: closePositionError, + isClosingPosition: true, + }); + + const { + type: selectedTradeType, + side = OrderSide.BUY, + // isClosingPosition, + } = tradeValues; + + const useLimit = selectedTradeType === TradeFormType.LIMIT; + + const onTradeTypeChange = useCallback( + (tradeType: TradeFormType) => { + // if (isClosingPosition === true) { + // return; + // } + + console.log(tradeType); + dispatch(closePositionFormActions.setOrderType(tradeType)); + }, + [dispatch, side] + ); + + const onClearInputs = () => { + dispatch(closePositionFormActions.setOrderType(TradeFormType.MARKET)); + dispatch(closePositionFormActions.reset()); + }; + + const formattedPositionSize = + (positionSize?.toNumber() ?? 0) / 10 ** (stepSizeDecimals ?? TOKEN_DECIMALS); + + const midMarketPriceButton = ( + <$MidPriceButton onClick={setLimitPriceToMidPrice} size={ButtonSize.XSmall}> + {stringGetter({ key: STRING_KEYS.MID_MARKET_PRICE_SHORT })} + + ); + + return ( +
+ +
Close {displayableAsset} Position
+ onTradeTypeChange(TradeFormType.MARKET), + }, + { + value: TradeFormType.LIMIT, + active: selectedTradeType === TradeFormType.LIMIT, + label: stringGetter({ key: STRING_KEYS.LIMIT_ORDER_SHORT }), + onSelect: () => onTradeTypeChange(TradeFormType.LIMIT), + }, + ]} + > + + {selectedTradeType === TradeFormType.MARKET + ? stringGetter({ key: STRING_KEYS.MARKET_ORDER_SHORT }) + : stringGetter({ key: STRING_KEYS.LIMIT_ORDER_SHORT })} + + + +
+

Current Position

+ <$PositionSize isLong={positionSize?.isGreaterThanOrEqualTo(0) ?? false}> + {Math.abs(positionSize?.toNumber() ?? 0)} {displayableAsset} + +
+ + {stringGetter({ key: STRING_KEYS.AMOUNT })}} + decimals={stepSizeDecimals ?? TOKEN_DECIMALS} + onInput={onAmountInput} + type={InputType.Number} + value={amountInput} + tw="w-full" + /> + + {useLimit && ( + + + {stringGetter({ key: STRING_KEYS.LIMIT_PRICE })} + + USD + + } + onChange={onLimitPriceInput} + value={limitPriceInput} + decimals={tickSizeDecimals ?? USD_DECIMALS} + slotRight={setLimitPriceToMidPrice ? midMarketPriceButton : undefined} + tw="w-full" + /> + )} + + { + dispatch( + closePositionFormActions.setSizeAvailablePercent( + mapIfPresent(value, (v) => MaybeBigNumber(v)?.div(100).toFixed(2)) ?? '' + ) + ); + }} + /> + +
+ + {/* {receiptArea} */} + <$PlaceOrderButtonAndReceipt + summary={summary} + actionStringKey={shortAlertKey} + currentStep={currentStep} + hasInput={!!amountInput} + hasValidationErrors={hasValidationErrors} + onClearInputs={onClearInputs} + shouldEnableTrade={shouldEnableTrade} + showDeposit={false} + tradingUnavailable={tradingUnavailable} + confirmButtonConfig={{ + stringKey: STRING_KEYS.CLOSE_ORDER, + buttonTextStringKey: STRING_KEYS.CLOSE_POSITION, + buttonAction: ButtonAction.Destroy, + }} + /> +
+
+ ); +}; + +export default CloseTradeForm; + +const StyledButton = styled(Button)<{ $isActive?: boolean }>` + ${({ $isActive }) => + $isActive + ? `--button-textColor: var(--color-text-2);` + : `--button-textColor: var(--color-text-0);`} +`; + +const LongButton = styled(Button)<{ $isLong?: boolean }>` + ${({ $isLong }) => + $isLong ? `background-color: var(--color-gradient-positive);` : 'background-color: transparent'} +`; + +const ShortButton = styled(Button)<{ $isShort?: boolean }>` + ${({ $isShort }) => + $isShort + ? `background-color: var(--color-gradient-negative);` + : 'background-color: transparent'} +`; + +const $InputsColumn = styled.div` + ${formMixins.inputsColumn} + + width: 100%; +`; + +const AvailableRow = styled.div` + display: flex; + + justify-content: space-between; + + align-items: center; + + padding: 8 px 4 px; + + margin-bottom: 8 px; + + width: 100%; +`; +const AvailableLabel = styled.span` + color: #6b7280; + + font-size: 15 px; +`; +const AvailableValue = styled.div` + display: flex; + + align-items: center; + + gap: 8 px; + + color: #6b7280; + + font-size: 15 px; +`; + +const ToggleRow = styled.div` + display: flex; + + justify-content: space-between; + + align-items: center; + + padding: 0px 0px; + + width: 100%; +`; +const ToggleLabel = styled.span` + color: #9ca3af; + + font-size: 16 px; + + font-weight: 400; +`; + +const $PlaceOrderButtonAndReceipt = styled(PlaceOrderButtonAndReceipt)` + --withReceipt-backgroundColor: transparent; +`; + +const $PositionSize = styled.div<{ isLong: boolean }>` + ${({ isLong }) => + isLong + ? css` + color: var(--color-positive) !important; + ` + : css` + color: var(--color-negative) !important; + `} +`; + +const $MidPriceButton = styled(Button)` + ${formMixins.inputInnerButton} +`; diff --git a/src/pages/trade/mobile-web/RegularTradeForm.tsx b/src/pages/trade/mobile-web/RegularTradeForm.tsx new file mode 100644 index 0000000000..5d8050f01c --- /dev/null +++ b/src/pages/trade/mobile-web/RegularTradeForm.tsx @@ -0,0 +1,422 @@ +import { useCallback, useState } from 'react'; + +import { + ExecutionType, + MarginMode, + OrderSide, + TimeInForce, + TradeFormType, +} from '@/bonsai/forms/trade/types'; +import { BonsaiHelpers } from '@/bonsai/ontology'; +import BigNumber from 'bignumber.js'; +import styled from 'styled-components'; + +import { ButtonAction, ButtonShape, ButtonSize, ButtonStyle } from '@/constants/buttons'; +import { DialogTypes } from '@/constants/dialogs'; +import { STRING_KEYS } from '@/constants/localization'; +import { MobilePlaceOrderSteps, ORDER_TYPE_STRINGS } from '@/constants/trade'; + +import { useTradeErrors } from '@/hooks/TradingForm/useTradeErrors'; +import { TradeFormSource, useTradeForm } from '@/hooks/TradingForm/useTradeForm'; +import { useStringGetter } from '@/hooks/useStringGetter'; + +import { formMixins } from '@/styles/formMixins'; + +import { Button } from '@/components/Button'; +import { Icon, IconName } from '@/components/Icon'; +import { DropdownMenuTrigger, MobileDropdownMenu } from '@/components/MobileDropdownMenu'; +import { Output, OutputType, ShowSign } from '@/components/Output'; +import { VerticalSeparator } from '@/components/Separator'; +import { Switch } from '@/components/Switch'; +import { TradeFormMessages } from '@/views/TradeFormMessages/TradeFormMessages'; +import { MarketsMenuDialog } from '@/views/dialogs/MarketsDialog/MarketsDialog'; +import { PlaceOrderButtonAndReceipt } from '@/views/forms/TradeForm/PlaceOrderButtonAndReceipt'; +import { TradeFormInputs } from '@/views/forms/TradeForm/TradeFormInputs'; +import { TradeSizeInputs } from '@/views/forms/TradeForm/TradeSizeInputs'; +import { TradeTriggerOrderInputs } from '@/views/forms/TradeForm/TradeTriggerInput'; + +import { useAppDispatch, useAppSelector } from '@/state/appTypes'; +import { openDialog } from '@/state/dialogs'; +import { tradeFormActions } from '@/state/tradeForm'; +import { + getTradeFormRawState, + getTradeFormSummary, + getTradeFormValues, +} from '@/state/tradeFormSelectors'; + +import { orEmptyObj } from '@/lib/typeUtils'; + +import { TradeFormHeaderMobile } from './TradeFormHeader'; + +const RegularTradeForm = () => { + const dispatch = useAppDispatch(); + const stringGetter = useStringGetter(); + + const [currentStep, setCurrentStep] = useState(); + + const rawInput = useAppSelector(getTradeFormRawState); + const tradeValues = useAppSelector(getTradeFormValues); + const fullFormSummary = useAppSelector(getTradeFormSummary); + const { summary } = fullFormSummary; + const { ticker, tickSizeDecimals } = orEmptyObj( + useAppSelector(BonsaiHelpers.currentMarket.stableMarketInfo) + ); + const effectiveSelectedLeverage = useAppSelector( + BonsaiHelpers.currentMarket.effectiveSelectedLeverage + ); + const { stopLossOrder: stopLossSummary, takeProfitOrder: takeProfitSummary } = orEmptyObj( + summary.triggersSummary + ); + + const { showMarginMode: needsMarginMode, triggerOrdersChecked } = summary.options; + + const isInputFilled = + [ + rawInput.triggerPrice, + rawInput.limitPrice, + rawInput.reduceOnly, + rawInput.goodTil, + rawInput.execution, + rawInput.postOnly, + rawInput.timeInForce, + ].some((v) => v != null && v !== '') || (rawInput.size?.value.value.trim() ?? '') !== ''; + + const availableBalance = ( + summary.accountDetailsBefore?.account?.freeCollateral ?? BigNumber(0) + ).toNumber(); + + const onLastOrderIndexed = useCallback(() => { + if (currentStep === MobilePlaceOrderSteps.PlacingOrder) { + setCurrentStep(MobilePlaceOrderSteps.Confirmation); + } + }, [currentStep, setCurrentStep]); + + const { placeOrderError, tradingUnavailable, shouldEnableTrade, hasValidationErrors } = + useTradeForm({ + source: TradeFormSource.SimpleTradeForm, + fullFormSummary, + onLastOrderIndexed, + }); + + const { isErrorShownInOrderStatusToast, primaryAlert, shortAlertKey } = useTradeErrors({ + placeOrderError, + }); + + const { + type: selectedTradeType, + stopLossOrder, + takeProfitOrder, + reduceOnly, + marginMode = MarginMode.CROSS, + side = OrderSide.BUY, + // isClosingPosition, + } = tradeValues; + + const setMarginMode = (mode: MarginMode) => { + dispatch(tradeFormActions.setMarginMode(mode)); + }; + + const setTradeSide = (newSide: OrderSide) => { + dispatch(tradeFormActions.setSide(newSide)); + }; + + const openLeverageDialog = () => { + if (ticker) { + dispatch(openDialog(DialogTypes.SetMarketLeverage({ marketId: ticker }))); + } + }; + + const onTradeTypeChange = useCallback( + (tradeType: TradeFormType) => { + // if (isClosingPosition === true) { + // return; + // } + + dispatch(tradeFormActions.reset()); + dispatch(tradeFormActions.setSide(side)); + dispatch(tradeFormActions.setOrderType(tradeType)); + + if (tradeType === TradeFormType.MARKET) { + dispatch(tradeFormActions.setExecution(ExecutionType.IOC)); + } else { + dispatch(tradeFormActions.setTimeInForce(TimeInForce.GTT)); + } + }, + [dispatch, side] + ); + + const orderSideAction = { + [OrderSide.BUY]: ButtonAction.Create, + [OrderSide.SELL]: ButtonAction.Destroy, + }[side]; + + return ( +
+ +
+
+ setMarginMode(MarginMode.CROSS)} + disabled={!needsMarginMode && marginMode !== MarginMode.CROSS} + > + {stringGetter({ + key: STRING_KEYS.CROSS, + })} + + + setMarginMode(MarginMode.ISOLATED)} + disabled={!needsMarginMode && marginMode !== MarginMode.ISOLATED} + > + {stringGetter({ + key: STRING_KEYS.ISOLATED, + })} + +
+ + +
+
+ setTradeSide(OrderSide.BUY)} + tw="w-full border-0" + > + {stringGetter({ key: STRING_KEYS.LONG_POSITION_SHORT })} + + setTradeSide(OrderSide.SELL)} + tw="w-full border-0" + > + {stringGetter({ key: STRING_KEYS.SHORT_POSITION_SHORT })} + +
+ onTradeTypeChange(TradeFormType.MARKET), + }, + { + value: TradeFormType.LIMIT, + active: selectedTradeType === TradeFormType.LIMIT, + label: stringGetter({ key: STRING_KEYS.LIMIT_ORDER_SHORT }), + onSelect: () => onTradeTypeChange(TradeFormType.LIMIT), + }, + ]} + > + + {selectedTradeType === TradeFormType.MARKET + ? stringGetter({ key: STRING_KEYS.MARKET_ORDER_SHORT }) + : stringGetter({ key: STRING_KEYS.LIMIT_ORDER_SHORT })} + + + + Available + + {availableBalance.toLocaleString('en-US', { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })}{' '} + USDC + + + <$InputsColumn> + + + + + Triggers + + dispatch(checked ? tradeFormActions.showTriggers() : tradeFormActions.hideTriggers()) + } + tw="font-mini-book" + /> + + {triggerOrdersChecked && ( +
+ + +
+ )} + + {stringGetter({ key: STRING_KEYS.REDUCE_ONLY })} + dispatch(tradeFormActions.setReduceOnly(checked))} + tw="font-mini-book" + /> + +
+ + {/* {receiptArea} */} + <$PlaceOrderButtonAndReceipt + summary={summary} + actionStringKey={shortAlertKey} + confirmButtonConfig={{ + stringKey: ORDER_TYPE_STRINGS[selectedTradeType].orderTypeKey, + buttonTextStringKey: STRING_KEYS.PLACE_ORDER, + buttonAction: orderSideAction as ButtonAction, + }} + currentStep={currentStep} + hasInput={ + isInputFilled && (!currentStep || currentStep === MobilePlaceOrderSteps.EditOrder) + } + hasValidationErrors={hasValidationErrors} + onClearInputs={() => dispatch(tradeFormActions.resetPrimaryInputs())} + shouldEnableTrade={shouldEnableTrade} + showDeposit={false} + tradingUnavailable={tradingUnavailable} + /> + + {/* <$StyledWithDetailsReceipt detailItems={items} hideReceipt={!isReceiptOpen}> + {placeOrderButton} + */} +
+ +
+ ); +}; + +export default RegularTradeForm; + +const StyledButton = styled(Button)<{ $isActive?: boolean }>` + ${({ $isActive }) => + $isActive + ? `--button-textColor: var(--color-text-2);` + : `--button-textColor: var(--color-text-0);`} +`; + +const LongButton = styled(Button)<{ $isLong?: boolean }>` + ${({ $isLong }) => + $isLong ? `background-color: var(--color-gradient-positive);` : 'background-color: transparent'} +`; + +const ShortButton = styled(Button)<{ $isShort?: boolean }>` + ${({ $isShort }) => + $isShort + ? `background-color: var(--color-gradient-negative);` + : 'background-color: transparent'} +`; + +const $InputsColumn = styled.div` + ${formMixins.inputsColumn} + + width: 100%; +`; + +const AvailableRow = styled.div` + display: flex; + + justify-content: space-between; + + align-items: center; + + padding: 8 px 4 px; + + margin-bottom: 8 px; + + width: 100%; +`; +const AvailableLabel = styled.span` + color: #6b7280; + + font-size: 15 px; +`; +const AvailableValue = styled.div` + display: flex; + + align-items: center; + + gap: 8 px; + + color: #6b7280; + + font-size: 15 px; +`; + +const ToggleRow = styled.div` + display: flex; + + justify-content: space-between; + + align-items: center; + + padding: 0px 0px; + + width: 100%; +`; +const ToggleLabel = styled.span` + color: #9ca3af; + + font-size: 16 px; + + font-weight: 400; +`; + +const $PlaceOrderButtonAndReceipt = styled(PlaceOrderButtonAndReceipt)` + --withReceipt-backgroundColor: transparent; +`; diff --git a/src/pages/trade/mobile-web/Trade.tsx b/src/pages/trade/mobile-web/Trade.tsx index c412d1d756..91526cd855 100644 --- a/src/pages/trade/mobile-web/Trade.tsx +++ b/src/pages/trade/mobile-web/Trade.tsx @@ -1,45 +1,41 @@ -import { useCallback } from 'react'; - +import { OrderSide } from '@/bonsai/forms/trade/types'; +import { useNavigate } from 'react-router-dom'; import styled from 'styled-components'; -import { HORIZONTAL_PANEL_MAX_HEIGHT, HORIZONTAL_PANEL_MIN_HEIGHT } from '@/constants/layout'; +import { ButtonAction, ButtonShape, ButtonSize, ButtonState } from '@/constants/buttons'; +import { ComplianceStates } from '@/constants/compliance'; +import { STRING_KEYS } from '@/constants/localization'; +import { AppRoute } from '@/constants/routes'; import { useCurrentMarketId } from '@/hooks/useCurrentMarketId'; import { usePageTitlePriceUpdates } from '@/hooks/usePageTitlePriceUpdates'; +import { usePerpetualsComplianceState } from '@/hooks/usePerpetualsComplianceState'; +import { useStringGetter } from '@/hooks/useStringGetter'; import { layoutMixins } from '@/styles/layoutMixins'; +import { Button } from '@/components/Button'; import { DetachedSection } from '@/components/ContentSection'; import { MarketsMenuDialog } from '@/views/dialogs/MarketsDialog/MarketsDialog'; import { UserMenuDialog } from '@/views/dialogs/MobileUserMenuDialog'; +import { OnboardingTriggerButton } from '@/views/dialogs/OnboardingTriggerButton'; import { calculateCanAccountTrade } from '@/state/accountCalculators'; import { useAppDispatch, useAppSelector } from '@/state/appTypes'; -import { setHorizontalPanelHeightPx } from '@/state/appUiConfigs'; -import { getHorizontalPanelHeightPx } from '@/state/appUiConfigsSelectors'; +import { tradeFormActions } from '@/state/tradeForm'; +import { HorizontalPanel } from '../HorizontalPanel'; import LaunchableMarket from '../LaunchableMarket'; -import { MobileBottomPanel } from '../MobileBottomPanel'; import { MobileTopPanel } from '../MobileTopPanel'; -import { useResizablePanel } from '../useResizablePanel'; import { TradeHeaderMobile } from './TradeHeader'; const TradePage = () => { + const dispatch = useAppDispatch(); const { isViewingUnlaunchedMarket } = useCurrentMarketId(); + const { complianceState } = usePerpetualsComplianceState(); const canAccountTrade = useAppSelector(calculateCanAccountTrade); - - const horizontalPanelHeightPxBase = useAppSelector(getHorizontalPanelHeightPx); - const dispatch = useAppDispatch(); - const setPanelHeight = useCallback( - (h: number) => { - dispatch(setHorizontalPanelHeightPx(h)); - }, - [dispatch] - ); - const { handleMouseDown } = useResizablePanel(horizontalPanelHeightPxBase, setPanelHeight, { - min: HORIZONTAL_PANEL_MIN_HEIGHT, - max: HORIZONTAL_PANEL_MAX_HEIGHT, - }); + const stringGetter = useStringGetter(); + const navigate = useNavigate(); usePageTitlePriceUpdates(); @@ -47,6 +43,48 @@ const TradePage = () => { return ; } + const isDisabled = complianceState !== ComplianceStates.FULL_ACCESS; + + const footerContent = isDisabled ? ( + + ) : canAccountTrade ? ( + <> + + + + + ) : ( + + ); + return ( <$TradeLayoutMobile> @@ -56,9 +94,24 @@ const TradePage = () => {
- + + + + + {/* + + */} +
+ {footerContent} +
+ diff --git a/src/pages/trade/mobile-web/TradeForm.tsx b/src/pages/trade/mobile-web/TradeForm.tsx index 0d1875ecb1..daf83e7c4f 100644 --- a/src/pages/trade/mobile-web/TradeForm.tsx +++ b/src/pages/trade/mobile-web/TradeForm.tsx @@ -1,439 +1,40 @@ -import { useCallback, useEffect, useState } from 'react'; +import { useEffect } from 'react'; -import { OrderSide, OrderSizeInputs, TradeFormType } from '@/bonsai/forms/trade/types'; -import { PlaceOrderPayload } from '@/bonsai/forms/triggers/types'; import { BonsaiHelpers } from '@/bonsai/ontology'; -import { ChevronDownIcon } from '@radix-ui/react-icons'; -import styled from 'styled-components'; -import tw from 'twin.macro'; -import { AnalyticsEvents } from '@/constants/analytics'; -import { ButtonAction, ButtonSize, ButtonType } from '@/constants/buttons'; -import { ComplianceStates } from '@/constants/compliance'; -import { DialogTypes } from '@/constants/dialogs'; -import { STRING_KEYS } from '@/constants/localization'; -import { TOKEN_DECIMALS, USD_DECIMALS } from '@/constants/numbers'; import { AppRoute } from '@/constants/routes'; -import { - DisplayUnit, - QUICK_LIMIT_OPTIONS, - QuickLimitOption, - SimpleUiTradeDialogSteps, -} from '@/constants/trade'; -import { useTradeErrors } from '@/hooks/TradingForm/useTradeErrors'; -import { TradeFormSource, useTradeForm } from '@/hooks/TradingForm/useTradeForm'; -import { useComplianceState } from '@/hooks/useComplianceState'; import { useCurrentMarketId } from '@/hooks/useCurrentMarketId'; -import { useStringGetter } from '@/hooks/useStringGetter'; - -import { Button } from '@/components/Button'; -import { Icon, IconName } from '@/components/Icon'; -import { InputType } from '@/components/Input'; -import { MarginUsageTag } from '@/components/MarginUsageTag'; -import { Output, OutputType, ShowSign } from '@/components/Output'; -import { HorizontalSeparatorFiller } from '@/components/Separator'; -import { SimpleUiPopover } from '@/components/SimpleUiPopover'; -import { TradeFormMessages } from '@/views/TradeFormMessages/TradeFormMessages'; -import { MarketsMenuDialog } from '@/views/dialogs/MarketsDialog/MarketsDialog'; -import { useTradeTypeOptions } from '@/views/forms/TradeForm/useTradeTypeOptions'; import { useAppDispatch, useAppSelector } from '@/state/appTypes'; -import { setDisplayUnit } from '@/state/appUiConfigs'; -import { getSelectedDisplayUnit } from '@/state/appUiConfigsSelectors'; -import { openDialog } from '@/state/dialogs'; import { tradeFormActions } from '@/state/tradeForm'; -import { getTradeFormSummary, getTradeFormValues } from '@/state/tradeFormSelectors'; +import { getTradeFormValues } from '@/state/tradeFormSelectors'; -import { track } from '@/lib/analytics/analytics'; -import { AttemptBigNumber, BIG_NUMBERS, MustBigNumber } from '@/lib/numbers'; import { orEmptyObj } from '@/lib/typeUtils'; -import { TradeFormHeaderMobile } from './TradeFormHeader'; +import CloseTradeForm from './CloseTradeForm'; +import RegularTradeForm from './RegularTradeForm'; -const TradeForm = ({ - currentStep, - setCurrentStep, - onClose, -}: { - currentStep: SimpleUiTradeDialogSteps; - setCurrentStep: (step: SimpleUiTradeDialogSteps) => void; - onClose: () => void; -}) => { +const TradeForm = () => { const dispatch = useAppDispatch(); - const stringGetter = useStringGetter(); - - const { isViewingUnlaunchedMarket } = useCurrentMarketId(AppRoute.TradeForm); - - const [placeOrderPayload, setPlaceOrderPayload] = useState(); + const { marketId } = useCurrentMarketId(AppRoute.TradeForm); - const { selectedTradeType } = useTradeTypeOptions(); - - const displayUnit = useAppSelector(getSelectedDisplayUnit); const tradeValues = useAppSelector(getTradeFormValues); - const fullFormSummary = useAppSelector(getTradeFormSummary); - const { complianceState } = useComplianceState(); - const { summary } = fullFormSummary; - const midPrice = useAppSelector(BonsaiHelpers.currentMarket.midPrice.data); - const buyingPower = useAppSelector(BonsaiHelpers.currentMarket.account.buyingPower); - const { assetId, displayableAsset, stepSizeDecimals, ticker, tickSizeDecimals } = orEmptyObj( - useAppSelector(BonsaiHelpers.currentMarket.stableMarketInfo) - ); - - const marginUsage = summary.accountDetailsAfter?.account?.marginUsage; - const effectiveSizes = orEmptyObj(summary.tradeInfo.inputSummary.size); + const { ticker } = orEmptyObj(useAppSelector(BonsaiHelpers.currentMarket.stableMarketInfo)); useEffect(() => { - if (ticker) { + if (marketId !== tradeValues.marketId) { dispatch(tradeFormActions.setMarketId(ticker)); } }, [ticker, dispatch]); - const onLastOrderIndexed = useCallback(() => { - if (currentStep === SimpleUiTradeDialogSteps.Submit) { - setCurrentStep(SimpleUiTradeDialogSteps.Confirm); - } - }, [currentStep, setCurrentStep]); - - const { placeOrderError, placeOrder, unIndexedClientId, shouldEnableTrade } = useTradeForm({ - source: TradeFormSource.SimpleTradeForm, - fullFormSummary, - onLastOrderIndexed, - }); - - const { - shouldPromptUserToPlaceLimitOrder, - isErrorShownInOrderStatusToast, - primaryAlert, - shortAlertKey, - } = useTradeErrors({ - placeOrderError, - }); - - const onSubmitOrder = async () => { - const payload = summary.tradePayload; - if (payload == null) { - return; - } - setCurrentStep(SimpleUiTradeDialogSteps.Submit); - placeOrder({ - onPlaceOrder: (tradePayload) => { - setPlaceOrderPayload(tradePayload); - dispatch(tradeFormActions.reset()); - }, - onFailure: () => { - setCurrentStep(SimpleUiTradeDialogSteps.Error); - }, - }); - }; - - const onUSDCInput = ({ formattedValue }: { floatValue?: number; formattedValue: string }) => { - const formattedValueBN = MustBigNumber(formattedValue); - if ((formattedValueBN.decimalPlaces() ?? 0) > USD_DECIMALS) { - return; - } - dispatch(tradeFormActions.setSizeUsd(formattedValue)); - }; - - const onSizeInput = ({ formattedValue }: { floatValue?: number; formattedValue: string }) => { - const formattedValueBN = MustBigNumber(formattedValue); - if ((formattedValueBN.decimalPlaces() ?? 0) > (stepSizeDecimals ?? TOKEN_DECIMALS)) { - return; - } - dispatch(tradeFormActions.setSizeToken(formattedValue)); - }; - - const onLimitPriceInput = ({ - formattedValue, - }: { - floatValue?: number; - formattedValue: string; - }) => { - const formattedValueBN = MustBigNumber(formattedValue); - if ((formattedValueBN.decimalPlaces() ?? 0) > (tickSizeDecimals ?? TOKEN_DECIMALS)) { - return; - } - dispatch(tradeFormActions.setLimitPrice(formattedValue)); - }; - - const decimals = stepSizeDecimals ?? TOKEN_DECIMALS; - - const inputConfigs = { - [DisplayUnit.Asset]: { - onInput: onSizeInput, - type: InputType.Number, - outputType: OutputType.Number, - decimals, - inputUnit: DisplayUnit.Asset, - value: - tradeValues.size != null && OrderSizeInputs.is.SIZE(tradeValues.size) - ? tradeValues.size.value.value - : tradeValues.size == null || tradeValues.size.value.value === '' - ? '' - : (AttemptBigNumber(effectiveSizes.size)?.toFixed(decimals) ?? ''), - }, - [DisplayUnit.Fiat]: { - onInput: onUSDCInput, - type: InputType.Number, - outputType: OutputType.Fiat, - decimals: USD_DECIMALS, - inputUnit: DisplayUnit.Fiat, - value: - tradeValues.size != null && OrderSizeInputs.is.USDC_SIZE(tradeValues.size) - ? tradeValues.size.value.value - : tradeValues.size == null || tradeValues.size.value.value === '' - ? '' - : (AttemptBigNumber(effectiveSizes.usdcSize)?.toFixed(USD_DECIMALS) ?? ''), - }, - }; - - const limitPriceInput = ( -
- When {displayableAsset} price reaches -
- ); - - const toggleDisplayUnit = () => { - if (!assetId) return; - - dispatch( - setDisplayUnit({ - newDisplayUnit: displayUnit === DisplayUnit.Asset ? DisplayUnit.Fiat : DisplayUnit.Asset, - entryPoint: 'simpleUiTradeDialog', - assetId, - }) - ); - }; + const isClosingPosition = tradeValues.isClosingPosition; - const priceImpact = summary.tradeInfo.inputSummary.worstFillPrice - ? midPrice - ? midPrice.minus(summary.tradeInfo.inputSummary.worstFillPrice) - : 0 - : 0; - - const receiptArea = ( -
-
- {stringGetter({ key: STRING_KEYS.BUYING_POWER })} - -
-
- - - - {stringGetter({ key: STRING_KEYS.ESTIMATED_COST })} - -
- {stringGetter({ key: STRING_KEYS.FEE })} - -
- {selectedTradeType === TradeFormType.MARKET && ( -
- - {stringGetter({ key: STRING_KEYS.PRICE_IMPACT })} - - -
- )} - -
- {stringGetter({ key: STRING_KEYS.TOTAL })} - -
-
- } - > - <$FeeTrigger> - {stringGetter({ key: STRING_KEYS.FEES })} - - -
-
- ); - - const toggleConfig = - inputConfigs[displayUnit === DisplayUnit.Asset ? DisplayUnit.Fiat : DisplayUnit.Asset]; - const toggleValue = toggleConfig.value.trim().length > 0 ? toggleConfig.value : 0; - - const sizeToggle = ( - - ); - - const hasMarginUsageError = primaryAlert?.code === 'INVALID_NEW_ACCOUNT_MARGIN_USAGE'; - - const isDepositNeeded = - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - (summary.accountDetailsBefore?.account?.freeCollateral.lte(0) || hasMarginUsageError) && - complianceState === ComplianceStates.FULL_ACCESS; - - const onDepositFunds = () => { - dispatch(openDialog(DialogTypes.Deposit2({}))); - onClose(); - }; - - const placeOrderButton = isDepositNeeded ? ( - - ) : ( - - ); - - const onQuickLimitClick = (quickLimit: QuickLimitOption) => { - const percentBN = MustBigNumber(quickLimit).div(100); - - const multiplier = - tradeValues.side === OrderSide.BUY - ? BIG_NUMBERS.ONE.minus(percentBN) - : BIG_NUMBERS.ONE.plus(percentBN); - - dispatch( - tradeFormActions.setLimitPrice( - midPrice?.times(multiplier).toFixed(tickSizeDecimals ?? TOKEN_DECIMALS) ?? '' - ) - ); - - track( - AnalyticsEvents.TradeQuickLimitOptionClick({ - quickLimit, - side: tradeValues.side, - marketId: ticker ?? '', - }) - ); - }; - - const quickLimitSizeButtons = ( -
- {QUICK_LIMIT_OPTIONS.map((quickLimit: QuickLimitOption) => ( - - ))} -
- ); - - return ( -
- -
{sizeToggle}
- {tradeValues.type === TradeFormType.LIMIT && ( -
- {limitPriceInput} - {quickLimitSizeButtons} -
- )} - -
- - {receiptArea} - {placeOrderButton} -
+ if (isClosingPosition) { + return ; + } - -
- ); + return ; }; export default TradeForm; - -const $FeeTrigger = styled.button.attrs({ - type: 'button', -})` - ${tw`row gap-0.25 text-color-text-2`} - - svg { - color: var(--color-text-0); - } - - &[data-state='open'] { - svg { - transition: rotate 0.3s var(--ease-out-expo); - rotate: -0.5turn; - } - } -`; diff --git a/src/pages/trade/mobile-web/TradeFormHeader.tsx b/src/pages/trade/mobile-web/TradeFormHeader.tsx index 47edd0d21f..1e43c63918 100644 --- a/src/pages/trade/mobile-web/TradeFormHeader.tsx +++ b/src/pages/trade/mobile-web/TradeFormHeader.tsx @@ -54,7 +54,7 @@ export const TradeFormHeaderMobile = ({ launchableMarketId }: { launchableMarket ); }; -const $Header = styled.header` +const $Header = styled.div` ${layoutMixins.contentSectionDetachedScrollable} ${layoutMixins.stickyHeader} @@ -66,6 +66,12 @@ const $Header = styled.header` color: var(--color-text-2); background-color: var(--color-layer-2); + padding-top: 1rem; + + border-bottom: 1px solid var(--color-border); + + width: 100%; + padding-bottom: 1rem; `; const $Right = styled.div` diff --git a/src/pages/trade/mobile-web/TradeHeader.tsx b/src/pages/trade/mobile-web/TradeHeader.tsx index ba94a226b6..1ebf647040 100644 --- a/src/pages/trade/mobile-web/TradeHeader.tsx +++ b/src/pages/trade/mobile-web/TradeHeader.tsx @@ -1,33 +1,100 @@ -import { BonsaiHelpers } from '@/bonsai/ontology'; -import styled from 'styled-components'; +import { BonsaiCore, BonsaiHelpers } from '@/bonsai/ontology'; +import styled, { css } from 'styled-components'; import { ButtonShape, ButtonSize } from '@/constants/buttons'; +import { STRING_KEYS } from '@/constants/localization'; +import { FUNDING_DECIMALS, LARGE_TOKEN_DECIMALS } from '@/constants/numbers'; +import { DisplayUnit } from '@/constants/trade'; +import { useStringGetter } from '@/hooks/useStringGetter'; + +import breakpoints from '@/styles/breakpoints'; import { layoutMixins } from '@/styles/layoutMixins'; import { Button } from '@/components/Button'; +import { Details } from '@/components/Details'; +import { DisplayUnitTag } from '@/components/DisplayUnitTag'; import { Icon, IconName } from '@/components/Icon'; +import { Output, OutputType } from '@/components/Output'; +import { TriangleIndicator } from '@/components/TriangleIndicator'; +import { WithTooltip } from '@/components/WithTooltip'; +import { MarketStatsDetails } from '@/views/MarketStatsDetails'; +import { NextFundingTimer } from '@/views/NextFundingTimer'; import { MobileTradeAssetSelector } from '@/views/mobile/MobileTradeAssetSelector'; import { FavoriteButton } from '@/views/tables/MarketsTable/FavoriteButton'; import { useAppDispatch, useAppSelector } from '@/state/appTypes'; +import { getSelectedDisplayUnit } from '@/state/appUiConfigsSelectors'; import { setIsUserMenuOpen } from '@/state/dialogs'; +import { MustBigNumber } from '@/lib/numbers'; +import { orEmptyObj } from '@/lib/typeUtils'; + +enum MarketStats { + OraclePrice = 'OraclePrice', + PriceChange24H = 'PriceChange24H', + Volume24H = 'Volume24H', + Trades24H = 'Trades24H', + OpenInterest = 'OpenInterest', + Funding1H = 'Funding1H', + NextFunding = 'NextFunding', +} + export const TradeHeaderMobile = ({ launchableMarketId }: { launchableMarketId?: string }) => { const id = useAppSelector(BonsaiHelpers.currentMarket.assetId); const dispatch = useAppDispatch(); + const stringGetter = useStringGetter(); + + const marketInfo = orEmptyObj(useAppSelector(BonsaiHelpers.currentMarket.marketInfo)); + const loadingState = useAppSelector(BonsaiCore.markets.markets.loading); + const isLoading = loadingState === 'pending'; + + const { + displayableAsset, + nextFundingRate, + openInterest, + openInterestUSDC, + oraclePrice, + percentChange24h, + priceChange24H, + tickSizeDecimals, + trades24H, + volume24H, + } = orEmptyObj(marketInfo); + + const displayUnit = useAppSelector(getSelectedDisplayUnit); + + const valueMap = { + [MarketStats.OraclePrice]: oraclePrice, + [MarketStats.NextFunding]: undefined, // hardcoded + [MarketStats.Funding1H]: nextFundingRate, + [MarketStats.OpenInterest]: displayUnit === DisplayUnit.Fiat ? openInterestUSDC : openInterest, + [MarketStats.PriceChange24H]: priceChange24H, + [MarketStats.Trades24H]: trades24H, + [MarketStats.Volume24H]: volume24H, + }; + + const labelMap = { + [MarketStats.OraclePrice]: stringGetter({ key: STRING_KEYS.ORACLE_PRICE }), + [MarketStats.NextFunding]: stringGetter({ key: STRING_KEYS.NEXT_FUNDING }), + [MarketStats.Funding1H]: stringGetter({ key: STRING_KEYS.FUNDING_RATE_1H_SHORT }), + [MarketStats.OpenInterest]: stringGetter({ key: STRING_KEYS.OPEN_INTEREST }), + [MarketStats.PriceChange24H]: stringGetter({ key: STRING_KEYS.CHANGE_24H }), + [MarketStats.Trades24H]: stringGetter({ key: STRING_KEYS.TRADES_24H }), + [MarketStats.Volume24H]: stringGetter({ key: STRING_KEYS.VOLUME_24H }), + }; const openUserMenu = () => { dispatch(setIsUserMenuOpen(true)); }; return ( - <$Header> - {id && } +
+ <$TopHeader> + {id && } - + - <$Right> - - + + +
); }; +const DetailsItem = ({ + value, + stat, + tickSizeDecimals, + assetId, + isLoading, + priceChange24HPercent, + useFiatDisplayUnit, +}: { + value: string | number | null | undefined; + stat: MarketStats; + tickSizeDecimals: number | null | undefined; + assetId: string; + isLoading: boolean; + priceChange24HPercent: number | null | undefined; + useFiatDisplayUnit: boolean; +}) => { + const valueBN = MustBigNumber(value); + const stringGetter = useStringGetter(); + + const color = valueBN.isNegative() ? 'var(--color-negative)' : 'var(--color-positive)'; + + switch (stat) { + case MarketStats.OraclePrice: { + return <$Output type={OutputType.Fiat} value={value} fractionDigits={tickSizeDecimals} />; + } + case MarketStats.OpenInterest: { + return ( + <$Output + type={OutputType.Number} + value={value} + fractionDigits={useFiatDisplayUnit ? 0 : LARGE_TOKEN_DECIMALS} + slotRight={ + + } + /> + ); + } + case MarketStats.Funding1H: { + return ( + +
+ {stringGetter({ key: STRING_KEYS.ANNUALIZED })}: + +
+ + } + > + <$Output + type={OutputType.Percent} + value={value} + color={!isLoading ? color : undefined} + fractionDigits={FUNDING_DECIMALS} + /> +
+ ); + } + case MarketStats.NextFunding: { + return ; + } + case MarketStats.PriceChange24H: { + return ( + <$RowSpan color={!isLoading ? color : undefined}> + {!isLoading && } + <$Output + withSubscript + type={OutputType.Fiat} + value={valueBN.abs()} + fractionDigits={tickSizeDecimals} + /> + {!isLoading && ( + <$Output + type={OutputType.Percent} + value={MustBigNumber(priceChange24HPercent).abs()} + withParentheses + /> + )} + + ); + } + case MarketStats.Trades24H: { + return <$Output type={OutputType.Number} value={value} fractionDigits={0} />; + } + case MarketStats.Volume24H: { + // $ with no decimals + return <$Output type={OutputType.Fiat} value={value} fractionDigits={0} />; + } + default: { + // Default renderer + return <$Output type={OutputType.Text} value={value} />; + } + } +}; + const $Header = styled.header` ${layoutMixins.contentSectionDetachedScrollable} + ${layoutMixins.column} + + border-bottom: 1px solid var(--color-border); +`; + +const $TopHeader = styled.header` ${layoutMixins.stickyHeader} z-index: 2; @@ -51,10 +226,45 @@ const $Header = styled.header` padding-left: 1rem; padding-right: 1.5rem; + padding-top: 1rem; + padding-bottom: 1rem; gap: 1rem; + justify-content: space-between; + color: var(--color-text-2); background-color: var(--color-layer-2); + + border-bottom: 1px solid var(--color-border); +`; + +const $StatsHeader = styled.div` + @media ${breakpoints.notTablet} { + ${layoutMixins.scrollArea} + ${layoutMixins.row} + isolation: isolate; + + align-items: stretch; + margin-left: 1px; + } + + @media ${breakpoints.tablet} { + border-bottom: solid var(--border-width) var(--color-border); + } +`; + +const $Details = styled(Details)` + font: var(--font-mini-book); + + @media ${breakpoints.tablet} { + ${layoutMixins.withOuterAndInnerBorders} + + font: var(--font-small-book); + + > * { + padding: 0.625rem 1rem; + } + } `; const $Right = styled.div` @@ -63,3 +273,29 @@ const $Right = styled.div` ${layoutMixins.rowColumn} justify-items: flex-end; `; + +const $Output = styled(Output)<{ color?: string }>` + ${layoutMixins.row} + + ${({ color }) => + color && + css` + color: ${color}; + `} +`; + +const $RowSpan = styled.span<{ color?: string }>` + ${layoutMixins.row} + + ${({ color }) => + color && + css` + color: ${color}; + `} + + > span { + ${layoutMixins.row} + } + + gap: 0.25rem; +`; diff --git a/src/views/MarketStatsDetails.tsx b/src/views/MarketStatsDetails.tsx index 43b9b1e752..f0e37933d7 100644 --- a/src/views/MarketStatsDetails.tsx +++ b/src/views/MarketStatsDetails.tsx @@ -31,6 +31,8 @@ import { MidMarketPrice } from './MidMarketPrice'; type ElementProps = { showMidMarketPrice?: boolean; + horizontal?: boolean; + withSubscript?: boolean; }; enum MarketStats { @@ -44,7 +46,11 @@ enum MarketStats { MaxLeverage = 'MaxLeverage', } -export const MarketStatsDetails = ({ showMidMarketPrice = true }: ElementProps) => { +export const MarketStatsDetails = ({ + showMidMarketPrice = true, + horizontal = false, + withSubscript, +}: ElementProps) => { const stringGetter = useStringGetter(); const { isTablet } = useBreakpoints(); @@ -120,12 +126,14 @@ export const MarketStatsDetails = ({ showMidMarketPrice = true }: ElementProps) initialMarginFraction={initialMarginFraction} effectiveInitialMarginFraction={effectiveInitialMarginFraction} useFiatDisplayUnit={displayUnit === DisplayUnit.Fiat} + withSubscript={withSubscript} /> ), }))} isLoading={isLoading} - layout={isTablet ? 'grid' : 'rowColumns'} + layout={horizontal ? 'rowColumns' : isTablet ? 'grid' : 'rowColumns'} withSeparators={!isTablet} + $horizontalScroll={horizontal} /> ); @@ -145,17 +153,29 @@ const $MarketDetailsItems = styled.div` } `; -const $Details = styled(Details)` +const $Details = styled(Details)<{ $horizontalScroll?: boolean }>` font: var(--font-mini-book); @media ${breakpoints.tablet} { ${layoutMixins.withOuterAndInnerBorders} - font: var(--font-small-book); > * { padding: 0.625rem 1rem; } + + ${({ $horizontalScroll }) => + $horizontalScroll && + css` + max-width: 100vw; + width: 100%; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + + > * { + flex-shrink: 0; + } + `} } `; @@ -206,6 +226,7 @@ const DetailsItem = ({ initialMarginFraction, effectiveInitialMarginFraction, useFiatDisplayUnit, + withSubscript = true, }: { value: string | number | null | undefined; stat: MarketStats; @@ -216,6 +237,7 @@ const DetailsItem = ({ initialMarginFraction: string | null | undefined; effectiveInitialMarginFraction: number | null | undefined; useFiatDisplayUnit: boolean; + withSubscript?: boolean; }) => { const valueBN = MustBigNumber(value); const stringGetter = useStringGetter(); @@ -226,7 +248,7 @@ const DetailsItem = ({ case MarketStats.OraclePrice: { return ( <$Output - withSubscript + withSubscript={withSubscript} type={OutputType.Fiat} value={value} fractionDigits={tickSizeDecimals} diff --git a/src/views/charts/TradingView/TvChart.tsx b/src/views/charts/TradingView/TvChart.tsx index c83e40fddd..806b2592a9 100644 --- a/src/views/charts/TradingView/TvChart.tsx +++ b/src/views/charts/TradingView/TvChart.tsx @@ -9,6 +9,7 @@ import { useChartMarketAndResolution } from '@/hooks/tradingView/useChartMarketA import { useTradingView } from '@/hooks/tradingView/useTradingView'; import { useTradingViewTheme } from '@/hooks/tradingView/useTradingViewTheme'; import { useTradingViewToggles } from '@/hooks/tradingView/useTradingViewToggles'; +import { useBreakpoints } from '@/hooks/useBreakpoints'; import { useSimpleUiEnabled } from '@/hooks/useSimpleUiEnabled'; import { useAppSelector } from '@/state/appTypes'; @@ -21,6 +22,7 @@ export const TvChart = () => { const [tvWidget, setTvWidget] = useState(); + const { isTablet } = useBreakpoints(); const isSimpleUi = useSimpleUiEnabled(); const orderLineToggleRef = useRef(null); @@ -64,5 +66,5 @@ export const TvChart = () => { }); useTradingViewTheme({ tvWidget, chartLines }); - return ; + return ; }; diff --git a/src/views/dialogs/MarketsDialog/MarketRow.tsx b/src/views/dialogs/MarketsDialog/MarketRow.tsx index cdb641dbec..312b45a98c 100644 --- a/src/views/dialogs/MarketsDialog/MarketRow.tsx +++ b/src/views/dialogs/MarketsDialog/MarketRow.tsx @@ -32,7 +32,7 @@ export const MarketRow = ({ className, market }: { className?: string; market: M return ( diff --git a/src/views/dialogs/MarketsDialog/MarketsList.tsx b/src/views/dialogs/MarketsDialog/MarketsList.tsx index 049989833c..f778e57ad3 100644 --- a/src/views/dialogs/MarketsDialog/MarketsList.tsx +++ b/src/views/dialogs/MarketsDialog/MarketsList.tsx @@ -216,7 +216,7 @@ export const MarketList = () => { placeholder={`${stringGetter({ key: STRING_KEYS.SEARCH })}...`} onTextChange={setSearchFilter} /> -
+
{sortTypeLabel} { const dispatch = useAppDispatch(); const navigate = useNavigate(); const onboardingState = useAppSelector(getOnboardingState); - const { complianceState } = useComplianceState(); + const { complianceState } = usePerpetualsComplianceState(); const canAccountTrade = useAppSelector(calculateCanAccountTrade); const isTurnkeyConnected = useAppSelector(selectIsTurnkeyConnected); const { equity, freeCollateral } = orEmptyObj( diff --git a/src/views/dialogs/SetMarketLeverageDialog.tsx b/src/views/dialogs/SetMarketLeverageDialog.tsx index b775a33b4b..661b6abcde 100644 --- a/src/views/dialogs/SetMarketLeverageDialog.tsx +++ b/src/views/dialogs/SetMarketLeverageDialog.tsx @@ -214,7 +214,7 @@ export const SetMarketLeverageDialog = ({ isOpen setIsOpen={setIsOpen} title={stringGetter({ key: STRING_KEYS.SET_MARKET_LEVERAGE })} - tw="[--dialog-header-paddingBottom:1.5rem] [--dialog-width:25rem]" + tw="[--dialog-header-paddingBottom:1.5rem]" >
diff --git a/src/views/forms/TradeForm/PlaceOrderButtonAndReceipt.tsx b/src/views/forms/TradeForm/PlaceOrderButtonAndReceipt.tsx index e1a1221158..39f5c2878b 100644 --- a/src/views/forms/TradeForm/PlaceOrderButtonAndReceipt.tsx +++ b/src/views/forms/TradeForm/PlaceOrderButtonAndReceipt.tsx @@ -12,6 +12,7 @@ import { StatsigFlags } from '@/constants/statsig'; import { MobilePlaceOrderSteps } from '@/constants/trade'; import { IndexerPositionSide } from '@/types/indexer/indexerApiGen'; +import { useBreakpoints } from '@/hooks/useBreakpoints'; import { usePerpetualsComplianceState } from '@/hooks/usePerpetualsComplianceState'; import { useStatsigGateValue } from '@/hooks/useStatsig'; import { useStringGetter } from '@/hooks/useStringGetter'; @@ -74,6 +75,7 @@ export const PlaceOrderButtonAndReceipt = ({ summary, tradingUnavailable, }: ElementProps) => { + const { isTablet } = useBreakpoints(); const stringGetter = useStringGetter(); const dispatch = useAppDispatch(); const { chainTokenImage, chainTokenLabel } = useTokenConfigs(); @@ -335,35 +337,41 @@ export const PlaceOrderButtonAndReceipt = ({ return ( <$Footer> -
- <$WithSeparators layout="row"> - {[ - hasInput && ( - + ), + <$HideButton + slotRight={} shape={ButtonShape.Pill} size={ButtonSize.XSmall} - onClick={onClearInputs} - key="clear" + onPressedChange={setIsReceiptOpen} + isPressed={isReceiptOpen} + key="hide" > - {stringGetter({ key: STRING_KEYS.CLEAR })} - - ), - <$HideButton - slotRight={} - shape={ButtonShape.Pill} - size={ButtonSize.XSmall} - onPressedChange={setIsReceiptOpen} - isPressed={isReceiptOpen} - key="hide" - > - {stringGetter({ key: STRING_KEYS.RECEIPT })} - , - ].filter(isTruthy)} - -
- + {stringGetter({ key: STRING_KEYS.RECEIPT })} + , + ].filter(isTruthy)} + +
+ )} + {!canAccountTrade ? ( ) : showDeposit && complianceState === ComplianceStates.FULL_ACCESS ? ( @@ -379,7 +387,6 @@ export const PlaceOrderButtonAndReceipt = ({ const $Footer = styled.footer` ${formMixins.footer} padding-bottom: var(--dialog-content-paddingBottom); - ${layoutMixins.column} `; diff --git a/src/views/mobile/MobileTradeAssetSelector.tsx b/src/views/mobile/MobileTradeAssetSelector.tsx index 36ffd45e95..b3d9cc26ac 100644 --- a/src/views/mobile/MobileTradeAssetSelector.tsx +++ b/src/views/mobile/MobileTradeAssetSelector.tsx @@ -41,19 +41,21 @@ export const MobileTradeAssetSelector = ({ {launchableAsset.name} <$Name> -

{launchableAsset.name}

+

{launchableAsset.name}

{getDisplayableAssetFromBaseAsset(launchableAsset.assetId)}
) : (
- + <$Name> -

{displayableTicker}

- {Math.round(leverage)}x +

{displayableTicker}

+ <$Leverage> + {Math.round(leverage)}x +
@@ -77,8 +79,14 @@ const $Name = styled.div` font: var(--font-large-medium); } - > :nth-child(2) { - font: var(--font-mini-book); - color: var(--color-text-0); - } + color: var(--color-text-2); +`; + +const $Leverage = styled.div` + border-radius: 8px; + padding: 3px 6px; + background-color: #7774ff14; + + font: var(--font-mini-medium); + color: var(--color-accent); `; diff --git a/src/views/tables/MobilePositionsTable.tsx b/src/views/tables/MobilePositionsTable.tsx new file mode 100644 index 0000000000..8f82c799db --- /dev/null +++ b/src/views/tables/MobilePositionsTable.tsx @@ -0,0 +1,336 @@ +import { forwardRef, useCallback, useMemo } from 'react'; + +import { MarginMode } from '@/bonsai/forms/trade/types'; +import { BonsaiCore } from '@/bonsai/ontology'; +import { + PerpetualMarketSummary, + SubaccountOrder, + SubaccountPosition, +} from '@/bonsai/types/summaryTypes'; +import { Trigger } from '@radix-ui/react-collapsible'; +import type { ColumnSize } from '@react-types/table'; +import { useNavigate } from 'react-router-dom'; +import styled from 'styled-components'; + +import { NumberSign, TOKEN_DECIMALS, USD_DECIMALS } from '@/constants/numbers'; +import { EMPTY_ARR } from '@/constants/objects'; +import { AppRoute } from '@/constants/routes'; +import { IndexerPositionSide } from '@/types/indexer/indexerApiGen'; + +import { useEnvFeatures } from '@/hooks/useEnvFeatures'; +import { useAppSelectorWithArgs } from '@/hooks/useParameterizedSelector'; + +import { AssetIcon } from '@/components/AssetIcon'; +import { Button } from '@/components/Button'; +import { Collapsible } from '@/components/Collapsible'; +import { Icon, IconName } from '@/components/Icon'; +import { Output, OutputType, ShowSign } from '@/components/Output'; +import { PageSize } from '@/components/Table/TablePaginationRow'; +import { marginModeMatchesFilter, MarketTypeFilter } from '@/pages/trade/types'; + +import { calculateIsAccountViewOnly } from '@/state/accountCalculators'; +import { getSubaccountConditionalOrders } from '@/state/accountSelectors'; +import { useAppDispatch, useAppSelector } from '@/state/appTypes'; +import { tradeFormActions } from '@/state/tradeForm'; + +import { getNumberSign, MaybeNumber } from '@/lib/numbers'; +import { orEmptyRecord } from '@/lib/typeUtils'; + +import { PositionsTriggersCell } from './PositionsTable/PositionsTriggersCell'; + +export enum PositionsTableColumnKey { + Details = 'Details', + IndexEntry = 'IndexEntry', + + Market = 'Market', + Leverage = 'Leverage', + Type = 'Type', + Size = 'Size', + Value = 'Value', + PnL = 'PnL', + Margin = 'Margin', + AverageOpen = 'AverageOpen', + Oracle = 'Oracle', + Liquidation = 'Liquidation', + Triggers = 'Triggers', + NetFunding = 'NetFunding', + Actions = 'Actions', +} + +type PositionTableRow = { + marketSummary: PerpetualMarketSummary | undefined; + stopLossOrders: SubaccountOrder[]; + takeProfitOrders: SubaccountOrder[]; + stepSizeDecimals: number; + tickSizeDecimals: number; + oraclePrice: number | undefined; +} & SubaccountPosition; + +type ElementProps = { + columnKeys: PositionsTableColumnKey[]; + columnWidths?: Partial>; + currentRoute?: string; + currentMarket?: string; + marketTypeFilter?: MarketTypeFilter; + showClosePositionAction: boolean; + initialPageSize?: PageSize; + onNavigate?: () => void; + navigateToOrders: (market: string) => void; +}; + +type StyleProps = { + withOuterBorder?: boolean; +}; + +export const MobilePositionsTable = forwardRef( + ( + { + currentRoute, + currentMarket, + marketTypeFilter, + onNavigate, + navigateToOrders, + }: ElementProps & StyleProps, + _ref + ) => { + const dispatch = useAppDispatch(); + const navigate = useNavigate(); + const { isSlTpLimitOrdersEnabled } = useEnvFeatures(); + + const isAccountViewOnly = useAppSelector(calculateIsAccountViewOnly); + // todo this uses the old subaccount id for now + const marketSummaries = orEmptyRecord(useAppSelector(BonsaiCore.markets.markets.data)); + + const openPositions = + useAppSelector(BonsaiCore.account.parentSubaccountPositions.data) ?? EMPTY_ARR; + + const positions = useMemo(() => { + return openPositions.filter((position) => { + const matchesMarket = currentMarket == null || position.market === currentMarket; + const marginType = position.marginMode; + const matchesType = marginModeMatchesFilter(marginType, marketTypeFilter); + return matchesMarket && matchesType; + }); + }, [currentMarket, marketTypeFilter, openPositions]); + + const tpslOrdersByPositionUniqueId = useAppSelectorWithArgs( + getSubaccountConditionalOrders, + isSlTpLimitOrdersEnabled + ); + + const positionsData = useMemo( + () => + positions.map((position: SubaccountPosition): PositionTableRow => { + const marketSummary = marketSummaries[position.market]; + return { + marketSummary, + stopLossOrders: + tpslOrdersByPositionUniqueId[position.uniqueId]?.stopLossOrders ?? EMPTY_ARR, + takeProfitOrders: + tpslOrdersByPositionUniqueId[position.uniqueId]?.takeProfitOrders ?? EMPTY_ARR, + stepSizeDecimals: marketSummary?.stepSizeDecimals ?? TOKEN_DECIMALS, + tickSizeDecimals: marketSummary?.tickSizeDecimals ?? USD_DECIMALS, + oraclePrice: MaybeNumber(marketSummary?.oraclePrice) ?? undefined, + ...position, + }; + }), + [positions, tpslOrdersByPositionUniqueId, marketSummaries] + ); + + const navigateToMarket = useCallback( + (market: string) => { + if (!currentMarket) { + navigate(`${AppRoute.Trade}/${market}`, { + state: { from: currentRoute }, + }); + onNavigate?.(); + } + }, + [currentMarket] + ); + + const onCloseButtonToggle = (marketId: string) => { + dispatch(tradeFormActions.setMarketId(marketId)); + dispatch(tradeFormActions.setIsClosingPosition(true)); + navigate(`${AppRoute.TradeForm}/${marketId}`); + }; + + return ( +
+ {positionsData.map((position) => { + const isLong = position.side === IndexerPositionSide.LONG; + return ( +
+ +
navigateToMarket(position.market)} + > +
+ +
{position.marketSummary?.displayableAsset}
+
+ {isLong ? 'Long' : 'Short'} +
+
+
+
+ {position.marginMode === MarginMode.CROSS ? 'Cross' : 'Isolated'} +
+
+ {Math.round(position.effectiveSelectedLeverage.toNumber())}x +
+
+
+
+
+ {position.unsignedSize.toNumber()}{' '} + {position.marketSummary?.displayableAsset} +
+ +
+
+ <$OutputSigned + sign={getNumberSign(position.updatedUnrealizedPnl)} + type={OutputType.Fiat} + value={position.updatedUnrealizedPnl} + showSign={ShowSign.Negative} + /> + <$OutputSigned + sign={getNumberSign(position.updatedUnrealizedPnlPercent)} + type={OutputType.Percent} + value={position.updatedUnrealizedPnlPercent} + showSign={ShowSign.Negative} + fractionDigits={0} + withParentheses + /> +
+
+ } + label={ + <$Trigger> + <$TriggerIcon> + + + + } + > +
+
+
+ Entry Price + +
+
+ Mark Price + +
+
+ Liq. Price + +
+
+
+
+ Funding + <$OutputSigned + sign={getNumberSign(position.netFunding)} + type={OutputType.Fiat} + value={position.netFunding} + showSign={ShowSign.Negative} + /> +
+
+ <$TriggersContainer tw="">TP/SL + +
+
+ +
+
+
+ +
+ ); + })} +
+ ); + } +); + +const $OutputSigned = styled(Output)<{ sign: NumberSign }>` + color: ${({ sign }) => + ({ + [NumberSign.Positive]: `var(--color-positive)`, + [NumberSign.Negative]: `var(--color-negative)`, + [NumberSign.Neutral]: `var(--color-text-2)`, + })[sign]}; +`; + +const $TriggersContainer = styled.div` + font: var(--font-small-book); + color: var(--tableStickyRow-textColor, var(--color-text-0)); +`; + +const $Trigger = styled(Trigger)` + --trigger-textColor: inherit; + --trigger-icon-width: 0.75em; + --trigger-icon-color: inherit; + --icon-size: var(--trigger-icon-width); + padding-right: 16px; +`; + +const $TriggerIcon = styled.span` + display: inline-flex; + transition: rotate 0.3s var(--ease-out-expo); + color: var(--trigger-icon-color); + + ${$Trigger}[data-state='open'] & { + rotate: -0.5turn; + } +`; diff --git a/src/views/tables/PositionsTable/PositionsTriggersCell.tsx b/src/views/tables/PositionsTable/PositionsTriggersCell.tsx index 2356361901..e7b4d75755 100644 --- a/src/views/tables/PositionsTable/PositionsTriggersCell.tsx +++ b/src/views/tables/PositionsTable/PositionsTriggersCell.tsx @@ -9,6 +9,7 @@ import { DialogTypes } from '@/constants/dialogs'; import { STRING_KEYS } from '@/constants/localization'; import { IndexerPositionSide } from '@/types/indexer/indexerApiGen'; +import { useBreakpoints } from '@/hooks/useBreakpoints'; import { useEnvFeatures } from '@/hooks/useEnvFeatures'; import { usePerpetualsComplianceState } from '@/hooks/usePerpetualsComplianceState'; import { useStringGetter } from '@/hooks/useStringGetter'; @@ -59,6 +60,7 @@ export const PositionsTriggersCell = ({ const stringGetter = useStringGetter(); const dispatch = useAppDispatch(); const { isSlTpLimitOrdersEnabled } = useEnvFeatures(); + const { isTablet } = useBreakpoints(); const { complianceState } = usePerpetualsComplianceState(); @@ -93,7 +95,7 @@ export const PositionsTriggersCell = ({ }; const viewOrdersButton = (align: 'left' | 'right') => ( - <$Container $align={align}> + <$Container $align={align} $isTablet={isTablet}> <$ViewOrdersButton action={ButtonAction.Navigation} size={ButtonSize.XSmall} @@ -130,8 +132,12 @@ export const PositionsTriggersCell = ({ }) => { if (orders.length === 0) { return ( - <$Container $align={align} onClick={openTriggersDialog}> - <$Output type={OutputType.Fiat} value={null} $withLiquidationWarning={false} /> + <$Container $align={align} onClick={openTriggersDialog} $isTablet={isTablet}> + {isTablet ? ( + '--' + ) : ( + <$Output type={OutputType.Fiat} value={null} $withLiquidationWarning={false} /> + )} ); } @@ -154,7 +160,7 @@ export const PositionsTriggersCell = ({ ); return ( - <$Container $align={align} onClick={openTriggersDialog}> + <$Container $align={align} onClick={openTriggersDialog} $isTablet={isTablet}> {liquidationWarningSide != null ? ( - {renderOutput({ align: 'right', orders: takeProfitOrders })} - <$VerticalSeparator /> + <$TableCell $isTablet={isTablet}> + {renderOutput({ align: isTablet ? 'left' : 'right', orders: takeProfitOrders })} + {isTablet ?
/
: <$VerticalSeparator />} {renderOutput({ align: 'left', orders: stopLossOrders })} {!isDisabled && complianceState === ComplianceStates.FULL_ACCESS && editButton} ); }; -const $TableCell = styled(TableCell)` +const $TableCell = styled(TableCell)<{ $isTablet: boolean }>` align-items: stretch; - gap: 0.75em; - justify-content: center; - --output-width: 70px; + ${({ $isTablet }) => + `${$isTablet ? '--output-width: 30px; gap: 0.25em;' : '--output-width: 70px; gap: 0.75em; justify-content: center;'}`} `; -const $Container = styled.div<{ $align: 'right' | 'left' }>` - display: inline-flex; +const $Container = styled.div<{ $isTablet: boolean; $align: 'right' | 'left' }>` align-items: center; gap: 0.25em; - width: var(--output-width); + + ${({ $isTablet }) => ($isTablet ? `` : 'width: var(--output-width); display: inline-flex;')} ${({ $align }) => $align && From a34860322141a2e1066fb615ed5aaa3a257b7fa1 Mon Sep 17 00:00:00 2001 From: KevDydx Date: Tue, 13 Jan 2026 16:08:46 +0700 Subject: [PATCH 03/27] Trade header fixes + remove web available to trade --- src/pages/trade/HorizontalPanel.tsx | 26 +- src/pages/trade/mobile-web/Trade.tsx | 9 +- src/pages/trade/mobile-web/TradeHeader.tsx | 261 +-------------------- 3 files changed, 25 insertions(+), 271 deletions(-) diff --git a/src/pages/trade/HorizontalPanel.tsx b/src/pages/trade/HorizontalPanel.tsx index 76856aad6b..c655498aba 100644 --- a/src/pages/trade/HorizontalPanel.tsx +++ b/src/pages/trade/HorizontalPanel.tsx @@ -456,20 +456,22 @@ export const HorizontalPanel = ({ isOpen = true, setIsOpen }: ElementProps) => { return ( <> -
-
dispatch(openDialog(DialogTypes.Deposit2({})))} - > - - {stringGetter({ key: STRING_KEYS.AVAILABLE_TO_TRADE })} - -
- - + {isTablet && ( +
+
dispatch(openDialog(DialogTypes.Deposit2({})))} + > + + {stringGetter({ key: STRING_KEYS.AVAILABLE_TO_TRADE })} + +
+ + +
-
+ )} <$CollapsibleTabs defaultTab={InfoSection.Position} diff --git a/src/pages/trade/mobile-web/Trade.tsx b/src/pages/trade/mobile-web/Trade.tsx index 91526cd855..87d932e79d 100644 --- a/src/pages/trade/mobile-web/Trade.tsx +++ b/src/pages/trade/mobile-web/Trade.tsx @@ -86,7 +86,7 @@ const TradePage = () => { ); return ( - <$TradeLayoutMobile> +
<$MobileContent> @@ -114,17 +114,12 @@ const TradePage = () => { - +
); }; export default TradePage; -const $TradeLayoutMobile = styled.div` - ${layoutMixins.expandingColumnWithHeader} - min-height: 100%; -`; - const $MobileContent = styled.article` ${layoutMixins.contentContainerPage} `; diff --git a/src/pages/trade/mobile-web/TradeHeader.tsx b/src/pages/trade/mobile-web/TradeHeader.tsx index 1ebf647040..1bff46fdf7 100644 --- a/src/pages/trade/mobile-web/TradeHeader.tsx +++ b/src/pages/trade/mobile-web/TradeHeader.tsx @@ -1,96 +1,30 @@ -import { BonsaiCore, BonsaiHelpers } from '@/bonsai/ontology'; -import styled, { css } from 'styled-components'; +import { BonsaiHelpers } from '@/bonsai/ontology'; +import styled from 'styled-components'; import { ButtonShape, ButtonSize } from '@/constants/buttons'; -import { STRING_KEYS } from '@/constants/localization'; -import { FUNDING_DECIMALS, LARGE_TOKEN_DECIMALS } from '@/constants/numbers'; -import { DisplayUnit } from '@/constants/trade'; -import { useStringGetter } from '@/hooks/useStringGetter'; - -import breakpoints from '@/styles/breakpoints'; import { layoutMixins } from '@/styles/layoutMixins'; import { Button } from '@/components/Button'; -import { Details } from '@/components/Details'; -import { DisplayUnitTag } from '@/components/DisplayUnitTag'; import { Icon, IconName } from '@/components/Icon'; -import { Output, OutputType } from '@/components/Output'; -import { TriangleIndicator } from '@/components/TriangleIndicator'; -import { WithTooltip } from '@/components/WithTooltip'; import { MarketStatsDetails } from '@/views/MarketStatsDetails'; -import { NextFundingTimer } from '@/views/NextFundingTimer'; import { MobileTradeAssetSelector } from '@/views/mobile/MobileTradeAssetSelector'; import { FavoriteButton } from '@/views/tables/MarketsTable/FavoriteButton'; import { useAppDispatch, useAppSelector } from '@/state/appTypes'; -import { getSelectedDisplayUnit } from '@/state/appUiConfigsSelectors'; import { setIsUserMenuOpen } from '@/state/dialogs'; -import { MustBigNumber } from '@/lib/numbers'; -import { orEmptyObj } from '@/lib/typeUtils'; - -enum MarketStats { - OraclePrice = 'OraclePrice', - PriceChange24H = 'PriceChange24H', - Volume24H = 'Volume24H', - Trades24H = 'Trades24H', - OpenInterest = 'OpenInterest', - Funding1H = 'Funding1H', - NextFunding = 'NextFunding', -} - export const TradeHeaderMobile = ({ launchableMarketId }: { launchableMarketId?: string }) => { const id = useAppSelector(BonsaiHelpers.currentMarket.assetId); const dispatch = useAppDispatch(); - const stringGetter = useStringGetter(); - - const marketInfo = orEmptyObj(useAppSelector(BonsaiHelpers.currentMarket.marketInfo)); - const loadingState = useAppSelector(BonsaiCore.markets.markets.loading); - const isLoading = loadingState === 'pending'; - - const { - displayableAsset, - nextFundingRate, - openInterest, - openInterestUSDC, - oraclePrice, - percentChange24h, - priceChange24H, - tickSizeDecimals, - trades24H, - volume24H, - } = orEmptyObj(marketInfo); - - const displayUnit = useAppSelector(getSelectedDisplayUnit); - - const valueMap = { - [MarketStats.OraclePrice]: oraclePrice, - [MarketStats.NextFunding]: undefined, // hardcoded - [MarketStats.Funding1H]: nextFundingRate, - [MarketStats.OpenInterest]: displayUnit === DisplayUnit.Fiat ? openInterestUSDC : openInterest, - [MarketStats.PriceChange24H]: priceChange24H, - [MarketStats.Trades24H]: trades24H, - [MarketStats.Volume24H]: volume24H, - }; - - const labelMap = { - [MarketStats.OraclePrice]: stringGetter({ key: STRING_KEYS.ORACLE_PRICE }), - [MarketStats.NextFunding]: stringGetter({ key: STRING_KEYS.NEXT_FUNDING }), - [MarketStats.Funding1H]: stringGetter({ key: STRING_KEYS.FUNDING_RATE_1H_SHORT }), - [MarketStats.OpenInterest]: stringGetter({ key: STRING_KEYS.OPEN_INTEREST }), - [MarketStats.PriceChange24H]: stringGetter({ key: STRING_KEYS.CHANGE_24H }), - [MarketStats.Trades24H]: stringGetter({ key: STRING_KEYS.TRADES_24H }), - [MarketStats.Volume24H]: stringGetter({ key: STRING_KEYS.VOLUME_24H }), - }; const openUserMenu = () => { dispatch(setIsUserMenuOpen(true)); }; return ( -
- <$TopHeader> + <$TopHeader> +
{id && } @@ -103,199 +37,22 @@ export const TradeHeaderMobile = ({ launchableMarketId }: { launchableMarketId?: > - +
-
+ ); }; -const DetailsItem = ({ - value, - stat, - tickSizeDecimals, - assetId, - isLoading, - priceChange24HPercent, - useFiatDisplayUnit, -}: { - value: string | number | null | undefined; - stat: MarketStats; - tickSizeDecimals: number | null | undefined; - assetId: string; - isLoading: boolean; - priceChange24HPercent: number | null | undefined; - useFiatDisplayUnit: boolean; -}) => { - const valueBN = MustBigNumber(value); - const stringGetter = useStringGetter(); - - const color = valueBN.isNegative() ? 'var(--color-negative)' : 'var(--color-positive)'; - - switch (stat) { - case MarketStats.OraclePrice: { - return <$Output type={OutputType.Fiat} value={value} fractionDigits={tickSizeDecimals} />; - } - case MarketStats.OpenInterest: { - return ( - <$Output - type={OutputType.Number} - value={value} - fractionDigits={useFiatDisplayUnit ? 0 : LARGE_TOKEN_DECIMALS} - slotRight={ - - } - /> - ); - } - case MarketStats.Funding1H: { - return ( - -
- {stringGetter({ key: STRING_KEYS.ANNUALIZED })}: - -
- - } - > - <$Output - type={OutputType.Percent} - value={value} - color={!isLoading ? color : undefined} - fractionDigits={FUNDING_DECIMALS} - /> -
- ); - } - case MarketStats.NextFunding: { - return ; - } - case MarketStats.PriceChange24H: { - return ( - <$RowSpan color={!isLoading ? color : undefined}> - {!isLoading && } - <$Output - withSubscript - type={OutputType.Fiat} - value={valueBN.abs()} - fractionDigits={tickSizeDecimals} - /> - {!isLoading && ( - <$Output - type={OutputType.Percent} - value={MustBigNumber(priceChange24HPercent).abs()} - withParentheses - /> - )} - - ); - } - case MarketStats.Trades24H: { - return <$Output type={OutputType.Number} value={value} fractionDigits={0} />; - } - case MarketStats.Volume24H: { - // $ with no decimals - return <$Output type={OutputType.Fiat} value={value} fractionDigits={0} />; - } - default: { - // Default renderer - return <$Output type={OutputType.Text} value={value} />; - } - } -}; - -const $Header = styled.header` +const $TopHeader = styled.header` ${layoutMixins.contentSectionDetachedScrollable} - ${layoutMixins.column} - - border-bottom: 1px solid var(--color-border); -`; - -const $TopHeader = styled.header` ${layoutMixins.stickyHeader} z-index: 2; - ${layoutMixins.row} - - padding-left: 1rem; - padding-right: 1.5rem; - padding-top: 1rem; - padding-bottom: 1rem; - gap: 1rem; + ${layoutMixins.column} - justify-content: space-between; + width: 100%; color: var(--color-text-2); background-color: var(--color-layer-2); - - border-bottom: 1px solid var(--color-border); -`; - -const $StatsHeader = styled.div` - @media ${breakpoints.notTablet} { - ${layoutMixins.scrollArea} - ${layoutMixins.row} - isolation: isolate; - - align-items: stretch; - margin-left: 1px; - } - - @media ${breakpoints.tablet} { - border-bottom: solid var(--border-width) var(--color-border); - } -`; - -const $Details = styled(Details)` - font: var(--font-mini-book); - - @media ${breakpoints.tablet} { - ${layoutMixins.withOuterAndInnerBorders} - - font: var(--font-small-book); - - > * { - padding: 0.625rem 1rem; - } - } -`; - -const $Right = styled.div` - margin-left: auto; - - ${layoutMixins.rowColumn} - justify-items: flex-end; -`; - -const $Output = styled(Output)<{ color?: string }>` - ${layoutMixins.row} - - ${({ color }) => - color && - css` - color: ${color}; - `} -`; - -const $RowSpan = styled.span<{ color?: string }>` - ${layoutMixins.row} - - ${({ color }) => - color && - css` - color: ${color}; - `} - - > span { - ${layoutMixins.row} - } - - gap: 0.25rem; `; From b157a74dcd40d7766444ba182a3b71012cf65180 Mon Sep 17 00:00:00 2001 From: KevDydx Date: Tue, 13 Jan 2026 16:22:33 +0700 Subject: [PATCH 04/27] fix footer --- src/hooks/useShouldShowFooter.ts | 2 +- src/pages/trade/mobile-web/Trade.tsx | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/hooks/useShouldShowFooter.ts b/src/hooks/useShouldShowFooter.ts index 4a7bef78e8..1e78b3218d 100644 --- a/src/hooks/useShouldShowFooter.ts +++ b/src/hooks/useShouldShowFooter.ts @@ -13,7 +13,7 @@ export const useShouldShowFooter = () => { const canAccountTrade = useAppSelector(calculateCanAccountTrade); return ( - !isTablet || + isTablet || !( !!(matchPath(TRADE_ROUTE, pathname) && canAccountTrade) || !!(matchPath(AppRoute.Vault, pathname) && canAccountTrade) diff --git a/src/pages/trade/mobile-web/Trade.tsx b/src/pages/trade/mobile-web/Trade.tsx index 87d932e79d..4caac635d3 100644 --- a/src/pages/trade/mobile-web/Trade.tsx +++ b/src/pages/trade/mobile-web/Trade.tsx @@ -104,7 +104,7 @@ const TradePage = () => {
Date: Tue, 13 Jan 2026 16:43:28 +0700 Subject: [PATCH 05/27] fix trade forms --- src/pages/trade/mobile-web/CloseTradeForm.tsx | 38 ++++++++++++-- .../trade/mobile-web/RegularTradeForm.tsx | 49 +++++++++++++++---- 2 files changed, 75 insertions(+), 12 deletions(-) diff --git a/src/pages/trade/mobile-web/CloseTradeForm.tsx b/src/pages/trade/mobile-web/CloseTradeForm.tsx index 476d028f37..05f441fbf8 100644 --- a/src/pages/trade/mobile-web/CloseTradeForm.tsx +++ b/src/pages/trade/mobile-web/CloseTradeForm.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from 'react'; +import { FormEvent, useCallback, useEffect, useState } from 'react'; import { OrderSide, OrderSizeInputs, TradeFormType } from '@/bonsai/forms/trade/types'; import { BonsaiHelpers } from '@/bonsai/ontology'; @@ -149,8 +149,40 @@ const CloseTradeForm = ({ market }: Props) => { ); + const onSubmit = (e: FormEvent) => { + e.preventDefault(); + + switch (currentStep) { + case MobilePlaceOrderSteps.EditOrder: { + setCurrentStep?.(MobilePlaceOrderSteps.PreviewOrder); + break; + } + case MobilePlaceOrderSteps.PlacingOrder: + case MobilePlaceOrderSteps.PlaceOrderFailed: + case MobilePlaceOrderSteps.Confirmation: { + break; + } + case MobilePlaceOrderSteps.PreviewOrder: + default: { + placeOrder({ + onFailure: () => { + setCurrentStep?.(MobilePlaceOrderSteps.PlaceOrderFailed); + }, + onPlaceOrder: () => { + onClearInputs(); + }, + }); + setCurrentStep?.(MobilePlaceOrderSteps.PlacingOrder); + break; + } + } + }; + return ( -
+
Close {displayableAsset} Position
{ }} />
-
+ ); }; diff --git a/src/pages/trade/mobile-web/RegularTradeForm.tsx b/src/pages/trade/mobile-web/RegularTradeForm.tsx index 5d8050f01c..ec932ee4ff 100644 --- a/src/pages/trade/mobile-web/RegularTradeForm.tsx +++ b/src/pages/trade/mobile-web/RegularTradeForm.tsx @@ -1,4 +1,4 @@ -import { useCallback, useState } from 'react'; +import { FormEvent, useCallback, useState } from 'react'; import { ExecutionType, @@ -91,12 +91,17 @@ const RegularTradeForm = () => { } }, [currentStep, setCurrentStep]); - const { placeOrderError, tradingUnavailable, shouldEnableTrade, hasValidationErrors } = - useTradeForm({ - source: TradeFormSource.SimpleTradeForm, - fullFormSummary, - onLastOrderIndexed, - }); + const { + placeOrderError, + placeOrder, + tradingUnavailable, + shouldEnableTrade, + hasValidationErrors, + } = useTradeForm({ + source: TradeFormSource.SimpleTradeForm, + fullFormSummary, + onLastOrderIndexed, + }); const { isErrorShownInOrderStatusToast, primaryAlert, shortAlertKey } = useTradeErrors({ placeOrderError, @@ -145,13 +150,39 @@ const RegularTradeForm = () => { [dispatch, side] ); + const onSubmit = async (e: FormEvent) => { + e.preventDefault(); + + switch (currentStep) { + case MobilePlaceOrderSteps.EditOrder: { + setCurrentStep?.(MobilePlaceOrderSteps.PreviewOrder); + break; + } + case MobilePlaceOrderSteps.PlacingOrder: + case MobilePlaceOrderSteps.PlaceOrderFailed: + case MobilePlaceOrderSteps.Confirmation: { + break; + } + case MobilePlaceOrderSteps.PreviewOrder: + default: { + placeOrder({ + onPlaceOrder: () => { + dispatch(tradeFormActions.resetPrimaryInputs()); + }, + }); + setCurrentStep?.(MobilePlaceOrderSteps.PlacingOrder); + break; + } + } + }; + const orderSideAction = { [OrderSide.BUY]: ButtonAction.Create, [OrderSide.SELL]: ButtonAction.Destroy, }[side]; return ( -
+
@@ -337,7 +368,7 @@ const RegularTradeForm = () => { */}
-
+ ); }; From cd2055096f33d596140f411be2c35957ece5d90b Mon Sep 17 00:00:00 2001 From: KevDydx Date: Tue, 13 Jan 2026 16:51:31 +0700 Subject: [PATCH 06/27] fixes --- src/bonsai/forms/trade/fields.ts | 2 +- src/bonsai/forms/trade/summary.ts | 2 +- src/pages/trade/LaunchableMarket.tsx | 22 ++++++++----------- src/pages/trade/Trade.tsx | 22 ++++++++----------- .../dialogs/MarketsDialog/MarketsList.tsx | 2 +- 5 files changed, 21 insertions(+), 29 deletions(-) diff --git a/src/bonsai/forms/trade/fields.ts b/src/bonsai/forms/trade/fields.ts index 55d811e53e..5cc9cf734d 100644 --- a/src/bonsai/forms/trade/fields.ts +++ b/src/bonsai/forms/trade/fields.ts @@ -104,7 +104,7 @@ export function getTradeFormFieldStates( states[key] = { ...(states[key] as any), state: 'enabled', - effectiveValue: states[key].effectiveValue ?? states[key].rawValue ?? defaults[key], + effectiveValue: states[key]?.effectiveValue ?? states[key]?.rawValue ?? defaults[key], }; }); } diff --git a/src/bonsai/forms/trade/summary.ts b/src/bonsai/forms/trade/summary.ts index 972d38c88d..5a3b35ff7c 100644 --- a/src/bonsai/forms/trade/summary.ts +++ b/src/bonsai/forms/trade/summary.ts @@ -80,7 +80,7 @@ export function calculateTradeSummary( const fieldStates = getTradeFormFieldStates(state, accountData, baseAccount); - const effectiveTrade = mapValues(fieldStates, (s) => s.effectiveValue) as TradeForm; + const effectiveTrade = mapValues(fieldStates, (s) => s?.effectiveValue) as TradeForm; const options = calculateTradeFormOptions(state.type, fieldStates, baseAccount); diff --git a/src/pages/trade/LaunchableMarket.tsx b/src/pages/trade/LaunchableMarket.tsx index 2562237f05..08114f1979 100644 --- a/src/pages/trade/LaunchableMarket.tsx +++ b/src/pages/trade/LaunchableMarket.tsx @@ -51,14 +51,14 @@ const LaunchableMarket = () => { }, [dispatch] ); - const { - handleMouseDown, - panelHeight: horizontalPanelHeight, - isDragging, - } = useResizablePanel(horizontalPanelHeightPxBase, setPanelHeight, { - min: HORIZONTAL_PANEL_MIN_HEIGHT, - max: HORIZONTAL_PANEL_MAX_HEIGHT, - }); + const { panelHeight: horizontalPanelHeight, isDragging } = useResizablePanel( + horizontalPanelHeightPxBase, + setPanelHeight, + { + min: HORIZONTAL_PANEL_MIN_HEIGHT, + max: HORIZONTAL_PANEL_MAX_HEIGHT, + } + ); useEffect(() => { if (marketId) { track( @@ -109,11 +109,7 @@ const LaunchableMarket = () => { <$GridSection gridArea="Horizontal"> - + ); diff --git a/src/pages/trade/Trade.tsx b/src/pages/trade/Trade.tsx index 2d64d7a0bb..f4340ca146 100644 --- a/src/pages/trade/Trade.tsx +++ b/src/pages/trade/Trade.tsx @@ -51,14 +51,14 @@ const TradePage = () => { }, [dispatch] ); - const { - handleMouseDown, - panelHeight: horizontalPanelHeight, - isDragging, - } = useResizablePanel(horizontalPanelHeightPxBase, setPanelHeight, { - min: HORIZONTAL_PANEL_MIN_HEIGHT, - max: HORIZONTAL_PANEL_MAX_HEIGHT, - }); + const { panelHeight: horizontalPanelHeight, isDragging } = useResizablePanel( + horizontalPanelHeightPxBase, + setPanelHeight, + { + min: HORIZONTAL_PANEL_MIN_HEIGHT, + max: HORIZONTAL_PANEL_MAX_HEIGHT, + } + ); const [isHorizontalPanelOpen, setIsHorizontalPanelOpen] = useState(true); usePageTitlePriceUpdates(); @@ -109,11 +109,7 @@ const TradePage = () => { <$GridSection gridArea="Horizontal"> - + ); diff --git a/src/views/dialogs/MarketsDialog/MarketsList.tsx b/src/views/dialogs/MarketsDialog/MarketsList.tsx index f778e57ad3..80774c13fe 100644 --- a/src/views/dialogs/MarketsDialog/MarketsList.tsx +++ b/src/views/dialogs/MarketsDialog/MarketsList.tsx @@ -312,7 +312,7 @@ const ItemRenderer = ({ listItem }: { listItem: ListItem }) => { css={{ height }} > {listItem.item} - {listItem.slotRight} + {(listItem as any)?.slotRight}
); }; From 5fc105e16217e1004749593044ed003499118dee Mon Sep 17 00:00:00 2001 From: KevDydx Date: Thu, 15 Jan 2026 00:35:36 +0700 Subject: [PATCH 07/27] fix sticky top --- src/views/tables/LiveTrades.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/views/tables/LiveTrades.tsx b/src/views/tables/LiveTrades.tsx index 6c76b12ec6..d0e53397dc 100644 --- a/src/views/tables/LiveTrades.tsx +++ b/src/views/tables/LiveTrades.tsx @@ -185,6 +185,10 @@ const liveTradesTableType = getSimpleStyledOutputType(OrderbookTradesTable, {} a const $LiveTradesTable = styled(OrderbookTradesTable)` background: var(--color-layer-2); + thead { + --stickyArea-totalInsetTop: 0; + } + tr { --histogram-bucket-size: 1; From be6866b8510a467f55ecc78de52839c369151e16 Mon Sep 17 00:00:00 2001 From: KevDydx Date: Tue, 20 Jan 2026 11:03:34 -0500 Subject: [PATCH 08/27] updates based on feedback --- src/components/MobileDropdownMenu.tsx | 3 +- src/components/NavigationMenu.tsx | 5 +- src/layout/Footer/FooterMobile.tsx | 2 +- src/pages/trade/HorizontalPanel.tsx | 10 ++- src/pages/trade/MobileTopPanel.tsx | 23 ++++- src/pages/trade/mobile-web/CloseTradeForm.tsx | 88 +------------------ .../trade/mobile-web/RegularTradeForm.tsx | 3 +- src/pages/trade/mobile-web/Trade.tsx | 4 +- src/styles/constants.css | 2 +- src/views/charts/TradingView/TvChart.tsx | 2 - .../TradeForm/PlaceOrderButtonAndReceipt.tsx | 1 + 11 files changed, 42 insertions(+), 101 deletions(-) diff --git a/src/components/MobileDropdownMenu.tsx b/src/components/MobileDropdownMenu.tsx index 013d0d5f36..be6941ffbc 100644 --- a/src/components/MobileDropdownMenu.tsx +++ b/src/components/MobileDropdownMenu.tsx @@ -101,7 +101,8 @@ const DropdownMenuButton = styled(Button)` display: flex; justify-content: space-between; align-items: center; - border-radius: 4px; + border-radius: 8px; + border-width: 1.5px; &[data-state='open'] { svg { diff --git a/src/components/NavigationMenu.tsx b/src/components/NavigationMenu.tsx index b23912bc4f..29922d7607 100644 --- a/src/components/NavigationMenu.tsx +++ b/src/components/NavigationMenu.tsx @@ -15,6 +15,8 @@ import styled, { css, keyframes } from 'styled-components'; import { MenuConfig, MenuItem } from '@/constants/menus'; +import { useBreakpoints } from '@/hooks/useBreakpoints'; + import { layoutMixins } from '@/styles/layoutMixins'; import { popoverMixins } from '@/styles/popoverMixins'; @@ -63,6 +65,7 @@ const NavItemWithRef = ( ref: Ref ) => { const location = useLocation(); + const { isTablet } = useBreakpoints(); const children = ( <> @@ -95,7 +98,7 @@ const NavItemWithRef = ( tw="whitespace-nowrap" {...props} > - {children} + {isTablet ?
{children}
: children} ) : props.onClick ? ( diff --git a/src/layout/Footer/FooterMobile.tsx b/src/layout/Footer/FooterMobile.tsx index cdddfa5cfa..bf2fb1bd6e 100644 --- a/src/layout/Footer/FooterMobile.tsx +++ b/src/layout/Footer/FooterMobile.tsx @@ -44,7 +44,7 @@ export const FooterMobile = () => { }, ]} orientation="horizontal" - itemOrientation="vertical" + itemOrientation="horizontal" /> ); diff --git a/src/pages/trade/HorizontalPanel.tsx b/src/pages/trade/HorizontalPanel.tsx index c655498aba..a8f83a97ae 100644 --- a/src/pages/trade/HorizontalPanel.tsx +++ b/src/pages/trade/HorizontalPanel.tsx @@ -226,7 +226,7 @@ export const HorizontalPanel = ({ isOpen = true, setIsOpen }: ElementProps) => { () => ({ asChild: true, value: InfoSection.Orders, - label: stringGetter({ key: STRING_KEYS.OPEN_ORDERS_HEADER }), + label: stringGetter({ key: isTablet ? STRING_KEYS.ORDERS : STRING_KEYS.OPEN_ORDERS_HEADER }), slotRight: areOrdersLoading || isWaitingForOrderToIndex ? ( @@ -283,7 +283,9 @@ export const HorizontalPanel = ({ isOpen = true, setIsOpen }: ElementProps) => { () => ({ asChild: true, value: InfoSection.OrderHistory, - label: stringGetter({ key: STRING_KEYS.ORDER_HISTORY_HEADER }), + label: stringGetter({ + key: isTablet ? STRING_KEYS.HISTORY : STRING_KEYS.ORDER_HISTORY_HEADER, + }), slotRight: areOrdersLoading ? ( @@ -394,7 +396,9 @@ export const HorizontalPanel = ({ isOpen = true, setIsOpen }: ElementProps) => { () => ({ asChild: true, value: InfoSection.Payments, - label: stringGetter({ key: STRING_KEYS.FUNDING_PAYMENTS }), + label: stringGetter({ + key: isTablet ? STRING_KEYS.FUNDING_PAYMENTS_SHORT : STRING_KEYS.FUNDING_PAYMENTS, + }), content: ( ( ); +enum HeightMode { + Short = 'Short', + Normal = 'Normal', + Mobile = 'Mobile', +} + export const MobileTopPanel = ({ isViewingUnlaunchedMarket, }: { @@ -47,6 +54,7 @@ export const MobileTopPanel = ({ }) => { const stringGetter = useStringGetter(); const selectedLocale = useAppSelector(getSelectedLocale); + const { isTablet } = useBreakpoints(); const [value, setValue] = useState(Tab.Price); @@ -81,7 +89,9 @@ export const MobileTopPanel = ({ return ( <$Tabs value={value} - $shortMode={value === Tab.Account} + $heightMode={ + isTablet ? HeightMode.Mobile : value === Tab.Account ? HeightMode.Short : HeightMode.Normal + } onValueChange={setValue} items={items.map((item) => ({ ...item, @@ -92,11 +102,16 @@ export const MobileTopPanel = ({ ); }; -type TabsStyleProps = { $shortMode?: boolean }; +type TabsStyleProps = { $heightMode?: HeightMode }; const TabsTypeTemp = getSimpleStyledOutputType(Tabs, {} as TabsStyleProps); const $Tabs = styled(Tabs)` - --scrollArea-height: ${({ $shortMode }) => ($shortMode ? '19rem' : '27rem')}; + --scrollArea-height: ${({ $heightMode }) => + $heightMode === HeightMode.Mobile + ? '23rem' + : $heightMode === HeightMode.Short + ? '19rem' + : '27rem'}; --stickyArea0-background: var(--color-layer-2); --tabContent-height: calc(var(--scrollArea-height) - 2rem - var(--tabs-currentHeight)); @@ -105,7 +120,7 @@ const $Tabs = styled(Tabs)` gap: var(--border-width); > div > header { - padding: 1rem 1.25rem; + ${({ $heightMode }) => ($heightMode !== HeightMode.Mobile ? 'padding: 1rem 1.25rem;' : '')} > [role='tablist'] { margin: auto; diff --git a/src/pages/trade/mobile-web/CloseTradeForm.tsx b/src/pages/trade/mobile-web/CloseTradeForm.tsx index 05f441fbf8..8306c86f4a 100644 --- a/src/pages/trade/mobile-web/CloseTradeForm.tsx +++ b/src/pages/trade/mobile-web/CloseTradeForm.tsx @@ -105,12 +105,7 @@ const CloseTradeForm = ({ market }: Props) => { onLastOrderIndexed, }); - const { - shouldPromptUserToPlaceLimitOrder, - isErrorShownInOrderStatusToast, - primaryAlert, - shortAlertKey, - } = useTradeErrors({ + const { isErrorShownInOrderStatusToast, primaryAlert, shortAlertKey } = useTradeErrors({ placeOrderError: closePositionError, isClosingPosition: true, }); @@ -129,7 +124,6 @@ const CloseTradeForm = ({ market }: Props) => { // return; // } - console.log(tradeType); dispatch(closePositionFormActions.setOrderType(tradeType)); }, [dispatch, side] @@ -140,9 +134,6 @@ const CloseTradeForm = ({ market }: Props) => { dispatch(closePositionFormActions.reset()); }; - const formattedPositionSize = - (positionSize?.toNumber() ?? 0) / 10 ** (stepSizeDecimals ?? TOKEN_DECIMALS); - const midMarketPriceButton = ( <$MidPriceButton onClick={setLimitPriceToMidPrice} size={ButtonSize.XSmall}> {stringGetter({ key: STRING_KEYS.MID_MARKET_PRICE_SHORT })} @@ -187,6 +178,7 @@ const CloseTradeForm = ({ market }: Props) => {
Close {displayableAsset} Position
{ />
{ export default CloseTradeForm; -const StyledButton = styled(Button)<{ $isActive?: boolean }>` - ${({ $isActive }) => - $isActive - ? `--button-textColor: var(--color-text-2);` - : `--button-textColor: var(--color-text-0);`} -`; - -const LongButton = styled(Button)<{ $isLong?: boolean }>` - ${({ $isLong }) => - $isLong ? `background-color: var(--color-gradient-positive);` : 'background-color: transparent'} -`; - -const ShortButton = styled(Button)<{ $isShort?: boolean }>` - ${({ $isShort }) => - $isShort - ? `background-color: var(--color-gradient-negative);` - : 'background-color: transparent'} -`; - -const $InputsColumn = styled.div` - ${formMixins.inputsColumn} - - width: 100%; -`; - -const AvailableRow = styled.div` - display: flex; - - justify-content: space-between; - - align-items: center; - - padding: 8 px 4 px; - - margin-bottom: 8 px; - - width: 100%; -`; -const AvailableLabel = styled.span` - color: #6b7280; - - font-size: 15 px; -`; -const AvailableValue = styled.div` - display: flex; - - align-items: center; - - gap: 8 px; - - color: #6b7280; - - font-size: 15 px; -`; - -const ToggleRow = styled.div` - display: flex; - - justify-content: space-between; - - align-items: center; - - padding: 0px 0px; - - width: 100%; -`; -const ToggleLabel = styled.span` - color: #9ca3af; - - font-size: 16 px; - - font-weight: 400; -`; - const $PlaceOrderButtonAndReceipt = styled(PlaceOrderButtonAndReceipt)` --withReceipt-backgroundColor: transparent; `; diff --git a/src/pages/trade/mobile-web/RegularTradeForm.tsx b/src/pages/trade/mobile-web/RegularTradeForm.tsx index ec932ee4ff..431fb75ab3 100644 --- a/src/pages/trade/mobile-web/RegularTradeForm.tsx +++ b/src/pages/trade/mobile-web/RegularTradeForm.tsx @@ -243,6 +243,7 @@ const RegularTradeForm = () => {
{ />
{ ) : canAccountTrade ? ( <> +
+
+ setTradeSide(OrderSide.BUY)} + tw="w-full border-0" + > + {stringGetter({ key: STRING_KEYS.LONG_POSITION_SHORT })} + + setTradeSide(OrderSide.SELL)} + tw="w-full border-0" > - {stringGetter({ - key: STRING_KEYS.ISOLATED, - })} - + {stringGetter({ key: STRING_KEYS.SHORT_POSITION_SHORT })} +
- - -
-
- setTradeSide(OrderSide.BUY)} - tw="w-full border-0" - > - {stringGetter({ key: STRING_KEYS.LONG_POSITION_SHORT })} - - setTradeSide(OrderSide.SELL)} - tw="w-full border-0" - > - {stringGetter({ key: STRING_KEYS.SHORT_POSITION_SHORT })} - -
- onTradeTypeChange(TradeFormType.MARKET), - }, - { - value: TradeFormType.LIMIT, - active: selectedTradeType === TradeFormType.LIMIT, - label: stringGetter({ key: STRING_KEYS.LIMIT_ORDER_SHORT }), - onSelect: () => onTradeTypeChange(TradeFormType.LIMIT), - }, - ]} - > - + {triggerOrdersChecked && ( +
+ + +
+ )} + + {stringGetter({ key: STRING_KEYS.REDUCE_ONLY })} + dispatch(tradeFormActions.setReduceOnly(checked))} + tw="font-mini-book" + /> + +
- {selectedTradeType === TradeFormType.MARKET - ? stringGetter({ key: STRING_KEYS.MARKET_ORDER_SHORT }) - : stringGetter({ key: STRING_KEYS.LIMIT_ORDER_SHORT })} - - - - Available - - {availableBalance.toLocaleString('en-US', { - minimumFractionDigits: 2, - maximumFractionDigits: 2, - })}{' '} - USDC - - - <$InputsColumn> - - - - - Triggers - - dispatch(checked ? tradeFormActions.showTriggers() : tradeFormActions.hideTriggers()) - } - tw="font-mini-book" - /> - - {triggerOrdersChecked && ( -
- - dispatch(tradeFormActions.resetPrimaryInputs())} + shouldEnableTrade={shouldEnableTrade} + showDeposit={false} + tradingUnavailable={tradingUnavailable} />
- )} - - {stringGetter({ key: STRING_KEYS.REDUCE_ONLY })} - dispatch(tradeFormActions.setReduceOnly(checked))} - tw="font-mini-book" - /> - -
- - {/* {receiptArea} */} - <$PlaceOrderButtonAndReceipt - summary={summary} - actionStringKey={shortAlertKey} - confirmButtonConfig={{ - stringKey: ORDER_TYPE_STRINGS[selectedTradeType].orderTypeKey, - buttonTextStringKey: STRING_KEYS.PLACE_ORDER, - buttonAction: orderSideAction as ButtonAction, - }} - currentStep={currentStep} - hasInput={ - isInputFilled && (!currentStep || currentStep === MobilePlaceOrderSteps.EditOrder) - } - hasValidationErrors={hasValidationErrors} - onClearInputs={() => dispatch(tradeFormActions.resetPrimaryInputs())} - shouldEnableTrade={shouldEnableTrade} - showDeposit={false} - tradingUnavailable={tradingUnavailable} - /> - - {/* <$StyledWithDetailsReceipt detailItems={items} hideReceipt={!isReceiptOpen}> - {placeOrderButton} - */}
+ ); @@ -384,14 +384,20 @@ const StyledButton = styled(Button)<{ $isActive?: boolean }>` const LongButton = styled(Button)<{ $isLong?: boolean }>` ${({ $isLong }) => - $isLong ? `background-color: var(--color-gradient-positive);` : 'background-color: transparent'} + $isLong + ? `--button-textColor: var(--color-green); --button-backgroundColor: var(--color-gradient-positive);` + : '--button-backgroundColor: transparent'} + + border-radius: 0.375rem; `; const ShortButton = styled(Button)<{ $isShort?: boolean }>` ${({ $isShort }) => $isShort - ? `background-color: var(--color-gradient-negative);` - : 'background-color: transparent'} + ? `--button-textColor: var(--color-red); --button-backgroundColor: var(--color-gradient-negative);` + : '--button-backgroundColor: transparent'} + + border-radius: 0.375rem; `; const $InputsColumn = styled.div` diff --git a/src/pages/trade/mobile-web/Trade.tsx b/src/pages/trade/mobile-web/Trade.tsx index 6319ffd783..d378ccc069 100644 --- a/src/pages/trade/mobile-web/Trade.tsx +++ b/src/pages/trade/mobile-web/Trade.tsx @@ -45,46 +45,6 @@ const TradePage = () => { const isDisabled = complianceState !== ComplianceStates.FULL_ACCESS; - const footerContent = isDisabled ? ( - - ) : canAccountTrade ? ( - <> - - - - - ) : ( - - ); - return (
@@ -104,12 +64,52 @@ const TradePage = () => {
- {footerContent} + {isDisabled ? ( + + ) : canAccountTrade ? ( + <> + + + + + ) : ( + + )}
diff --git a/src/pages/trade/mobile-web/TradeFormHeader.tsx b/src/pages/trade/mobile-web/TradeFormHeader.tsx index 1e43c63918..af803af18c 100644 --- a/src/pages/trade/mobile-web/TradeFormHeader.tsx +++ b/src/pages/trade/mobile-web/TradeFormHeader.tsx @@ -23,9 +23,9 @@ export const TradeFormHeaderMobile = ({ launchableMarketId }: { launchableMarket : undefined; const assetPrice = launchableAsset ? ( - + ) : ( - + ); return ( @@ -36,7 +36,7 @@ export const TradeFormHeaderMobile = ({ launchableMarketId }: { launchableMarket
{assetPrice} -
+
{id && } diff --git a/src/views/charts/TradingView/ResolutionSelector.tsx b/src/views/charts/TradingView/ResolutionSelector.tsx index afa1730c31..95047d727b 100644 --- a/src/views/charts/TradingView/ResolutionSelector.tsx +++ b/src/views/charts/TradingView/ResolutionSelector.tsx @@ -8,6 +8,7 @@ import { RESOLUTION_STRING_TO_LABEL, } from '@/constants/candles'; +import { useBreakpoints } from '@/hooks/useBreakpoints'; import { useStringGetter } from '@/hooks/useStringGetter'; import { objectKeys } from '@/lib/objectHelpers'; @@ -21,6 +22,7 @@ export const ResolutionSelector = ({ onResolutionChange: (resolution: ResolutionString) => void; currentResolution?: ResolutionString; }) => { + const { isTablet } = useBreakpoints(); const stringGetter = useStringGetter(); const getLabel = useCallback( @@ -42,10 +44,11 @@ export const ResolutionSelector = ({ {objectKeys(isLaunchable ? LAUNCHABLE_MARKET_RESOLUTION_CONFIGS : RESOLUTION_MAP).map( (resolution) => (
<$InnerInputContainer> { - commitValue(formattedValue); + setLocalValue(formattedValue); + setAllocationInput(formattedValue); }} /> @@ -100,6 +94,7 @@ const $InnerInputContainer = styled.div` const $AllocationSlider = styled(Slider)` height: 1.375rem; + --slider-track-background: linear-gradient( 90deg, var(--color-layer-7) 0%, From 3a7003de82c13e30a6a1f0fb13f7630e51bce3fb Mon Sep 17 00:00:00 2001 From: KevDydx Date: Tue, 27 Jan 2026 09:35:29 -0500 Subject: [PATCH 14/27] fixes --- src/pages/trade/mobile-web/Trade.tsx | 4 ++-- src/views/dialogs/MobileUserMenuDialog.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/trade/mobile-web/Trade.tsx b/src/pages/trade/mobile-web/Trade.tsx index d378ccc069..06d7f007a4 100644 --- a/src/pages/trade/mobile-web/Trade.tsx +++ b/src/pages/trade/mobile-web/Trade.tsx @@ -7,9 +7,9 @@ import { ComplianceStates } from '@/constants/compliance'; import { STRING_KEYS } from '@/constants/localization'; import { AppRoute } from '@/constants/routes'; +import { useComplianceState } from '@/hooks/useComplianceState'; import { useCurrentMarketId } from '@/hooks/useCurrentMarketId'; import { usePageTitlePriceUpdates } from '@/hooks/usePageTitlePriceUpdates'; -import { usePerpetualsComplianceState } from '@/hooks/usePerpetualsComplianceState'; import { useStringGetter } from '@/hooks/useStringGetter'; import { layoutMixins } from '@/styles/layoutMixins'; @@ -32,7 +32,7 @@ import { TradeHeaderMobile } from './TradeHeader'; const TradePage = () => { const dispatch = useAppDispatch(); const { isViewingUnlaunchedMarket } = useCurrentMarketId(); - const { complianceState } = usePerpetualsComplianceState(); + const { complianceState } = useComplianceState(); const canAccountTrade = useAppSelector(calculateCanAccountTrade); const stringGetter = useStringGetter(); const navigate = useNavigate(); diff --git a/src/views/dialogs/MobileUserMenuDialog.tsx b/src/views/dialogs/MobileUserMenuDialog.tsx index ea97b5a373..c245d7144a 100644 --- a/src/views/dialogs/MobileUserMenuDialog.tsx +++ b/src/views/dialogs/MobileUserMenuDialog.tsx @@ -13,7 +13,7 @@ import { STRING_KEYS } from '@/constants/localization'; import { AppRoute } from '@/constants/routes'; import { useAccounts } from '@/hooks/useAccounts'; -import { usePerpetualsComplianceState } from '@/hooks/usePerpetualsComplianceState'; +import { useComplianceState } from '@/hooks/useComplianceState'; import { useStringGetter } from '@/hooks/useStringGetter'; import { Button } from '@/components/Button'; @@ -39,7 +39,7 @@ const UserMenuContent = () => { const dispatch = useAppDispatch(); const navigate = useNavigate(); const onboardingState = useAppSelector(getOnboardingState); - const { complianceState } = usePerpetualsComplianceState(); + const { complianceState } = useComplianceState(); const canAccountTrade = useAppSelector(calculateCanAccountTrade); const isTurnkeyConnected = useAppSelector(selectIsTurnkeyConnected); const { equity, freeCollateral } = orEmptyObj( From 64ccd91298d9ed38db0c5d28e2782a5b3b94716e Mon Sep 17 00:00:00 2001 From: KevDydx Date: Tue, 27 Jan 2026 09:39:07 -0500 Subject: [PATCH 15/27] revert tradingview comment --- src/hooks/tradingView/useTradingView.ts | 60 ++++++++++++------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/src/hooks/tradingView/useTradingView.ts b/src/hooks/tradingView/useTradingView.ts index a870e921dc..675401efd2 100644 --- a/src/hooks/tradingView/useTradingView.ts +++ b/src/hooks/tradingView/useTradingView.ts @@ -11,7 +11,7 @@ import { import { DEFAULT_RESOLUTION } from '@/constants/candles'; import { TOGGLE_ACTIVE_CLASS_NAME } from '@/constants/charts'; -import { SUPPORTED_LOCALE_MAP } from '@/constants/localization'; +import { STRING_KEYS, SUPPORTED_LOCALE_MAP } from '@/constants/localization'; import type { TvWidget } from '@/constants/tvchart'; import { store } from '@/state/_store'; @@ -140,35 +140,35 @@ export const useTradingView = ({ // Initialize additional right-click-menu options tvChartWidget!.onContextMenu(tradingViewLimitOrder); - // tvChartWidget!.headerReady().then(() => { - // // Order Lines - // initializeToggle({ - // toggleRef: orderLineToggleRef, - // widget: tvChartWidget!, - // isOn: orderLinesToggleOn, - // setToggleOn: setOrderLinesToggleOn, - // label: stringGetter({ - // key: STRING_KEYS.ORDER_LINES, - // }), - // tooltip: stringGetter({ - // key: STRING_KEYS.ORDER_LINES_TOOLTIP, - // }), - // }); - - // // Buy/Sell Marks - // initializeToggle({ - // toggleRef: buySellMarksToggleRef, - // widget: tvChartWidget!, - // isOn: buySellMarksToggleOn, - // setToggleOn: setBuySellMarksToggleOn, - // label: stringGetter({ - // key: STRING_KEYS.BUYS_SELLS_TOGGLE, - // }), - // tooltip: stringGetter({ - // key: STRING_KEYS.BUYS_SELLS_TOGGLE_TOOLTIP, - // }), - // }); - // }); + tvChartWidget!.headerReady().then(() => { + // Order Lines + initializeToggle({ + toggleRef: orderLineToggleRef, + widget: tvChartWidget!, + isOn: orderLinesToggleOn, + setToggleOn: setOrderLinesToggleOn, + label: stringGetter({ + key: STRING_KEYS.ORDER_LINES, + }), + tooltip: stringGetter({ + key: STRING_KEYS.ORDER_LINES_TOOLTIP, + }), + }); + + // Buy/Sell Marks + initializeToggle({ + toggleRef: buySellMarksToggleRef, + widget: tvChartWidget!, + isOn: buySellMarksToggleOn, + setToggleOn: setBuySellMarksToggleOn, + label: stringGetter({ + key: STRING_KEYS.BUYS_SELLS_TOGGLE, + }), + tooltip: stringGetter({ + key: STRING_KEYS.BUYS_SELLS_TOGGLE_TOOLTIP, + }), + }); + }); tvChartWidget!.subscribe('onAutoSaveNeeded', () => tvChartWidget!.save((chartConfig: object) => { From a66d875e78d55c0298ad050b07ba5f491ae3622e Mon Sep 17 00:00:00 2001 From: KevDydx Date: Tue, 27 Jan 2026 10:08:08 -0500 Subject: [PATCH 16/27] updates --- src/App.tsx | 15 +++++++++++---- src/hooks/useMobileWebEnabled.ts | 12 ++++++++++++ src/lib/testFlags.ts | 4 ++++ src/pages/trade/HorizontalPanel.tsx | 5 ++++- src/pages/trade/LaunchableMarket.tsx | 22 +++++++++++++--------- src/pages/trade/Trade.tsx | 24 ++++++++++++++---------- 6 files changed, 58 insertions(+), 24 deletions(-) create mode 100644 src/hooks/useMobileWebEnabled.ts diff --git a/src/App.tsx b/src/App.tsx index dd1d5e34be..d25d901b4f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -54,6 +54,7 @@ import { useCommandMenu } from './hooks/useCommandMenu'; import { useComplianceState } from './hooks/useComplianceState'; import { useInitializePage } from './hooks/useInitializePage'; import { useLocalStorage } from './hooks/useLocalStorage'; +import { useMobileWebEnabled } from './hooks/useMobileWebEnabled'; import { useReferralCode } from './hooks/useReferralCode'; import { useShouldShowFooter } from './hooks/useShouldShowFooter'; import { useSimpleUiEnabled } from './hooks/useSimpleUiEnabled'; @@ -115,6 +116,7 @@ const Content = () => { const isShowingFooter = useShouldShowFooter(); const abDefaultToMarkets = useCustomFlagValue(CustomFlags.abDefaultToMarkets); const isSimpleUi = useSimpleUiEnabled(); + const isMobileWebEnabled = useMobileWebEnabled(); const { showComplianceBanner } = useComplianceState(); const isSimpleUiUserMenuOpen = useAppSelector(getIsUserMenuOpen); @@ -132,7 +134,9 @@ const Content = () => { const { dialogAreaRef } = useDialogArea() ?? {}; - if (isSimpleUi) { + const showMobileWeb = isMobileWebEnabled && isTablet; + + if (isSimpleUi && !showMobileWeb) { const matchMarkets = matchPath(AppRoute.Markets, location.pathname); const backgroundColor = matchMarkets && isSimpleUiUserMenuOpen ? 'var(--color-layer-1)' : 'transparent'; @@ -207,10 +211,13 @@ const Content = () => { } /> - : } /> + : } + /> : } + element={showMobileWeb ? : } /> @@ -254,7 +261,7 @@ const Content = () => { - {isTablet ? : } + {showMobileWeb ? : } diff --git a/src/hooks/useMobileWebEnabled.ts b/src/hooks/useMobileWebEnabled.ts new file mode 100644 index 0000000000..63aaadb52f --- /dev/null +++ b/src/hooks/useMobileWebEnabled.ts @@ -0,0 +1,12 @@ +import { StatsigFlags } from '@/constants/statsig'; + +import { testFlags } from '@/lib/testFlags'; + +import { useStatsigGateValue } from './useStatsig'; + +export const useMobileWebEnabled = () => { + const forceMobileWeb = testFlags.enableMobileWeb; + const mobileWebbFF = useStatsigGateValue(StatsigFlags.ffMobileWeb); + + return forceMobileWeb || mobileWebbFF; +}; diff --git a/src/lib/testFlags.ts b/src/lib/testFlags.ts index 19198fb8e8..6857e3ef87 100644 --- a/src/lib/testFlags.ts +++ b/src/lib/testFlags.ts @@ -71,6 +71,10 @@ class TestFlags { get spot() { return this.booleanFlag(this.queryParams.spot); } + + get enableMobileWeb() { + return this.booleanFlag(this.queryParams.enable_mobile_web); + } } export const testFlags = new TestFlags(); diff --git a/src/pages/trade/HorizontalPanel.tsx b/src/pages/trade/HorizontalPanel.tsx index 1c3f1c7694..ffb6a80f7e 100644 --- a/src/pages/trade/HorizontalPanel.tsx +++ b/src/pages/trade/HorizontalPanel.tsx @@ -65,9 +65,10 @@ enum InfoSection { type ElementProps = { isOpen?: boolean; setIsOpen?: (isOpen: boolean) => void; + handleStartResize?: (e: React.MouseEvent) => void; }; -export const HorizontalPanel = ({ isOpen = true, setIsOpen }: ElementProps) => { +export const HorizontalPanel = ({ isOpen = true, setIsOpen, handleStartResize }: ElementProps) => { const stringGetter = useStringGetter(); const navigate = useNavigate(); const dispatch = useAppDispatch(); @@ -461,6 +462,8 @@ export const HorizontalPanel = ({ isOpen = true, setIsOpen }: ElementProps) => { return ( <> + {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */} + <$DragHandle onMouseDown={handleStartResize} /> {isTablet && (
{ }, [dispatch] ); - const { panelHeight: horizontalPanelHeight, isDragging } = useResizablePanel( - horizontalPanelHeightPxBase, - setPanelHeight, - { - min: HORIZONTAL_PANEL_MIN_HEIGHT, - max: HORIZONTAL_PANEL_MAX_HEIGHT, - } - ); + const { + panelHeight: horizontalPanelHeight, + isDragging, + handleMouseDown, + } = useResizablePanel(horizontalPanelHeightPxBase, setPanelHeight, { + min: HORIZONTAL_PANEL_MIN_HEIGHT, + max: HORIZONTAL_PANEL_MAX_HEIGHT, + }); useEffect(() => { if (marketId) { track( @@ -109,7 +109,11 @@ const LaunchableMarket = () => { <$GridSection gridArea="Horizontal"> - + ); diff --git a/src/pages/trade/Trade.tsx b/src/pages/trade/Trade.tsx index f4340ca146..10f51bae58 100644 --- a/src/pages/trade/Trade.tsx +++ b/src/pages/trade/Trade.tsx @@ -51,14 +51,14 @@ const TradePage = () => { }, [dispatch] ); - const { panelHeight: horizontalPanelHeight, isDragging } = useResizablePanel( - horizontalPanelHeightPxBase, - setPanelHeight, - { - min: HORIZONTAL_PANEL_MIN_HEIGHT, - max: HORIZONTAL_PANEL_MAX_HEIGHT, - } - ); + const { + panelHeight: horizontalPanelHeight, + isDragging, + handleMouseDown, + } = useResizablePanel(horizontalPanelHeightPxBase, setPanelHeight, { + min: HORIZONTAL_PANEL_MIN_HEIGHT, + max: HORIZONTAL_PANEL_MAX_HEIGHT, + }); const [isHorizontalPanelOpen, setIsHorizontalPanelOpen] = useState(true); usePageTitlePriceUpdates(); @@ -73,7 +73,7 @@ const TradePage = () => {
- + @@ -109,7 +109,11 @@ const TradePage = () => { <$GridSection gridArea="Horizontal"> - + ); From 31eb08f262e1bd1c16ca4414be3f3b9f3c624ecc Mon Sep 17 00:00:00 2001 From: KevDydx Date: Tue, 27 Jan 2026 10:39:09 -0500 Subject: [PATCH 17/27] revert simpleui change --- src/hooks/useSimpleUiEnabled.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/hooks/useSimpleUiEnabled.ts b/src/hooks/useSimpleUiEnabled.ts index 04a4ad0239..378c25fdd6 100644 --- a/src/hooks/useSimpleUiEnabled.ts +++ b/src/hooks/useSimpleUiEnabled.ts @@ -1,12 +1,11 @@ -// import { useBreakpoints } from '@/hooks/useBreakpoints'; +import { useBreakpoints } from '@/hooks/useBreakpoints'; -// import { testFlags } from '@/lib/testFlags'; +import { testFlags } from '@/lib/testFlags'; export const useSimpleUiEnabled = () => { - return false; - // const { isTablet } = useBreakpoints(); - // const forcedSimpleUiValue = testFlags.simpleUi; - // const isSimpleUi = isTablet ? (forcedSimpleUiValue ?? true) : false; + const { isTablet } = useBreakpoints(); + const forcedSimpleUiValue = testFlags.simpleUi; + const isSimpleUi = isTablet ? (forcedSimpleUiValue ?? true) : false; - // return isSimpleUi; + return isSimpleUi; }; From 0efe77d65f8818577f14827014140d8fdb1748fc Mon Sep 17 00:00:00 2001 From: KevDydx Date: Tue, 27 Jan 2026 10:50:48 -0500 Subject: [PATCH 18/27] review updates --- src/views/charts/TradingView/ResolutionSelector.tsx | 5 +++++ src/views/tables/MobilePositionsTable.tsx | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/views/charts/TradingView/ResolutionSelector.tsx b/src/views/charts/TradingView/ResolutionSelector.tsx index 95047d727b..5cd22ef3b3 100644 --- a/src/views/charts/TradingView/ResolutionSelector.tsx +++ b/src/views/charts/TradingView/ResolutionSelector.tsx @@ -49,6 +49,11 @@ export const ResolutionSelector = ({ css={{ borderColor: currentResolution !== resolution ? 'transparent' : 'var(--color-accent)', height: isTablet ? '2rem' : '2.75rem', + ...(currentResolution !== resolution + ? { + color: 'var(--color-layer-7)', + } + : {}), }} key={resolution} onClick={() => onResolutionChange(resolution)} diff --git a/src/views/tables/MobilePositionsTable.tsx b/src/views/tables/MobilePositionsTable.tsx index 8f82c799db..33e4d0d643 100644 --- a/src/views/tables/MobilePositionsTable.tsx +++ b/src/views/tables/MobilePositionsTable.tsx @@ -183,7 +183,7 @@ export const MobilePositionsTable = forwardRef(
{position.marginMode === MarginMode.CROSS ? 'Cross' : 'Isolated'}
-
+
{Math.round(position.effectiveSelectedLeverage.toNumber())}x
@@ -285,10 +285,10 @@ export const MobilePositionsTable = forwardRef(
From 78d28137d030dcec80dbd02045816cce1948850c Mon Sep 17 00:00:00 2001 From: KevDydx Date: Tue, 27 Jan 2026 10:57:20 -0500 Subject: [PATCH 19/27] remove filter on nav menu item --- src/layout/Footer/FooterMobile.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/layout/Footer/FooterMobile.tsx b/src/layout/Footer/FooterMobile.tsx index b31679705a..b5c41996bc 100644 --- a/src/layout/Footer/FooterMobile.tsx +++ b/src/layout/Footer/FooterMobile.tsx @@ -68,6 +68,7 @@ const $NavigationMenu = styled(NavigationMenu)` --navigationMenu-item-highlighted-textColor: var(--color-accent); --navigationMenu-item-radius: 0px; --navigationMenu-item-padding: 0px; + --hover-filter-base: none; font: var(--font-tiny-book); From 42e2155609d556dcbde471b8649dd92c486f1ef6 Mon Sep 17 00:00:00 2001 From: KevDydx Date: Tue, 27 Jan 2026 11:00:15 -0500 Subject: [PATCH 20/27] fix long short text color when not selected --- src/pages/trade/mobile-web/RegularTradeForm.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/trade/mobile-web/RegularTradeForm.tsx b/src/pages/trade/mobile-web/RegularTradeForm.tsx index e14b7aaf7f..886cac2370 100644 --- a/src/pages/trade/mobile-web/RegularTradeForm.tsx +++ b/src/pages/trade/mobile-web/RegularTradeForm.tsx @@ -360,7 +360,7 @@ const LongButton = styled(Button)<{ $isLong?: boolean }>` ${({ $isLong }) => $isLong ? `--button-textColor: var(--color-green); --button-backgroundColor: var(--color-gradient-positive);` - : '--button-backgroundColor: transparent'} + : '--button-textColor: var(--color-layer-7); --button-backgroundColor: transparent'} border-radius: 0.375rem; `; @@ -369,7 +369,7 @@ const ShortButton = styled(Button)<{ $isShort?: boolean }>` ${({ $isShort }) => $isShort ? `--button-textColor: var(--color-red); --button-backgroundColor: var(--color-gradient-negative);` - : '--button-backgroundColor: transparent'} + : '--button-textColor: var(--color-layer-7); --button-backgroundColor: transparent'} border-radius: 0.375rem; `; From 8757f9d931759861a5c55a271f35f3abcb3f9ba7 Mon Sep 17 00:00:00 2001 From: KevDydx Date: Tue, 27 Jan 2026 15:30:58 -0500 Subject: [PATCH 21/27] fix portfolio --- src/pages/portfolio/Portfolio.tsx | 175 +++++++++++++++--------------- 1 file changed, 89 insertions(+), 86 deletions(-) diff --git a/src/pages/portfolio/Portfolio.tsx b/src/pages/portfolio/Portfolio.tsx index 71d372fc94..713212108d 100644 --- a/src/pages/portfolio/Portfolio.tsx +++ b/src/pages/portfolio/Portfolio.tsx @@ -16,6 +16,7 @@ import { useAccountBalance } from '@/hooks/useAccountBalance'; import { useBreakpoints } from '@/hooks/useBreakpoints'; import { useComplianceState } from '@/hooks/useComplianceState'; import { useDocumentTitle } from '@/hooks/useDocumentTitle'; +import { useMobileWebEnabled } from '@/hooks/useMobileWebEnabled'; import { useSimpleUiEnabled } from '@/hooks/useSimpleUiEnabled'; import { useStringGetter } from '@/hooks/useStringGetter'; @@ -66,6 +67,7 @@ const PortfolioPage = () => { const { complianceState } = useComplianceState(); const initialPageSize = 20; + const isMobileWebEnabled = useMobileWebEnabled(); const isSimpleUi = useSimpleUiEnabled(); const onboardingState = useAppSelector(getOnboardingState); @@ -84,94 +86,95 @@ const PortfolioPage = () => { useDocumentTitle(stringGetter({ key: STRING_KEYS.PORTFOLIO })); - const routesComponent = isSimpleUi ? ( - }> - - }> - } /> - } /> - } /> - } /> - } /> - - } - /> - - - ) : ( - }> - - } /> - } /> - } /> - } /> - } /> - }> - } /> - - } - /> - - } - /> - - } - /> + const routesComponent = + isSimpleUi && !isMobileWebEnabled ? ( + }> + + }> + } /> + } /> + } /> + } /> + } /> + - } + path="*" + element={} /> - - } /> - - - ); + + + ) : ( + }> + + } /> + } /> + } /> + } /> + } /> + }> + } /> + + } + /> + + } + /> + + } + /> + + } + /> + + } /> + + + ); if (isSimpleUi) { return routesComponent; From 6823512b3bef9e3a874c0a73177ef5c2d8fa81f0 Mon Sep 17 00:00:00 2001 From: KevDydx Date: Wed, 28 Jan 2026 11:16:04 -0500 Subject: [PATCH 22/27] comment --- src/hooks/useMobileWebEnabled.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/hooks/useMobileWebEnabled.ts b/src/hooks/useMobileWebEnabled.ts index 63aaadb52f..6e9f8e1349 100644 --- a/src/hooks/useMobileWebEnabled.ts +++ b/src/hooks/useMobileWebEnabled.ts @@ -4,6 +4,7 @@ import { testFlags } from '@/lib/testFlags'; import { useStatsigGateValue } from './useStatsig'; +// Flag for determining whether we show the new mobile web or not. Disables simpleui if enabled export const useMobileWebEnabled = () => { const forceMobileWeb = testFlags.enableMobileWeb; const mobileWebbFF = useStatsigGateValue(StatsigFlags.ffMobileWeb); From 558d0bac91aa40fe837f7ff49c7de454082e97df Mon Sep 17 00:00:00 2001 From: KevDydx Date: Wed, 28 Jan 2026 11:27:30 -0500 Subject: [PATCH 23/27] remove unused props --- src/views/tables/MobilePositionsTable.tsx | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/src/views/tables/MobilePositionsTable.tsx b/src/views/tables/MobilePositionsTable.tsx index 33e4d0d643..2971cfcf34 100644 --- a/src/views/tables/MobilePositionsTable.tsx +++ b/src/views/tables/MobilePositionsTable.tsx @@ -8,7 +8,6 @@ import { SubaccountPosition, } from '@/bonsai/types/summaryTypes'; import { Trigger } from '@radix-ui/react-collapsible'; -import type { ColumnSize } from '@react-types/table'; import { useNavigate } from 'react-router-dom'; import styled from 'styled-components'; @@ -25,7 +24,6 @@ import { Button } from '@/components/Button'; import { Collapsible } from '@/components/Collapsible'; import { Icon, IconName } from '@/components/Icon'; import { Output, OutputType, ShowSign } from '@/components/Output'; -import { PageSize } from '@/components/Table/TablePaginationRow'; import { marginModeMatchesFilter, MarketTypeFilter } from '@/pages/trade/types'; import { calculateIsAccountViewOnly } from '@/state/accountCalculators'; @@ -67,30 +65,16 @@ type PositionTableRow = { } & SubaccountPosition; type ElementProps = { - columnKeys: PositionsTableColumnKey[]; - columnWidths?: Partial>; currentRoute?: string; currentMarket?: string; marketTypeFilter?: MarketTypeFilter; - showClosePositionAction: boolean; - initialPageSize?: PageSize; onNavigate?: () => void; navigateToOrders: (market: string) => void; }; -type StyleProps = { - withOuterBorder?: boolean; -}; - export const MobilePositionsTable = forwardRef( ( - { - currentRoute, - currentMarket, - marketTypeFilter, - onNavigate, - navigateToOrders, - }: ElementProps & StyleProps, + { currentRoute, currentMarket, marketTypeFilter, onNavigate, navigateToOrders }: ElementProps, _ref ) => { const dispatch = useAppDispatch(); From ec4cda27a24be1e7068ac009de513eaa1c56e607 Mon Sep 17 00:00:00 2001 From: KevDydx Date: Wed, 28 Jan 2026 11:32:50 -0500 Subject: [PATCH 24/27] lint fies --- src/App.tsx | 1 - src/pages/trade/mobile-web/CloseTradeForm.tsx | 2 -- 2 files changed, 3 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index d25d901b4f..70010a72ca 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -80,7 +80,6 @@ import breakpoints from './styles/breakpoints'; const MarketsPage = lazy(() => import('@/pages/markets/Markets')); const PortfolioPage = lazy(() => import('@/pages/portfolio/Portfolio')); const AlertsPage = lazy(() => import('@/pages/AlertsPage')); -const ProfilePage = lazy(() => import('@/pages/Profile')); const SettingsPage = lazy(() => import('@/pages/settings/Settings')); const TradePage = lazy(() => import('@/pages/trade/Trade')); const SpotPage = lazy(() => import('@/pages/spot/Spot')); diff --git a/src/pages/trade/mobile-web/CloseTradeForm.tsx b/src/pages/trade/mobile-web/CloseTradeForm.tsx index 12da0a3bbf..01493fbd6f 100644 --- a/src/pages/trade/mobile-web/CloseTradeForm.tsx +++ b/src/pages/trade/mobile-web/CloseTradeForm.tsx @@ -12,7 +12,6 @@ import { TOKEN_DECIMALS, USD_DECIMALS } from '@/constants/numbers'; import { useTradeErrors } from '@/hooks/TradingForm/useTradeErrors'; import { TradeFormSource, useTradeForm } from '@/hooks/TradingForm/useTradeForm'; import { useClosePositionFormInputs } from '@/hooks/useClosePositionFormInputs'; -import { useIsFirstRender } from '@/hooks/useIsFirstRender'; import { useStringGetter } from '@/hooks/useStringGetter'; import { formMixins } from '@/styles/formMixins'; @@ -47,7 +46,6 @@ type Props = { const CloseTradeForm = ({ market }: Props) => { const dispatch = useAppDispatch(); - const isFirstRender = useIsFirstRender(); const stringGetter = useStringGetter(); const { stepSizeDecimals, tickSizeDecimals, displayableAsset } = orEmptyObj( From e3f3c38aff88d0c4da638f8f3be7db134922342e Mon Sep 17 00:00:00 2001 From: KevDydx Date: Wed, 28 Jan 2026 11:40:40 -0500 Subject: [PATCH 25/27] fix bbuild --- src/pages/trade/HorizontalPanel.tsx | 25 +------------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/src/pages/trade/HorizontalPanel.tsx b/src/pages/trade/HorizontalPanel.tsx index ffb6a80f7e..49b2b7c00d 100644 --- a/src/pages/trade/HorizontalPanel.tsx +++ b/src/pages/trade/HorizontalPanel.tsx @@ -159,30 +159,7 @@ export const HorizontalPanel = ({ isOpen = true, setIsOpen, handleStartResize }: tag: showCurrentMarket ? null : shortenNumberForDisplay(numTotalPositions), content: isTablet ? ( - + ) : ( Date: Tue, 10 Feb 2026 15:42:25 -0500 Subject: [PATCH 26/27] updates --- src/App.tsx | 1 + src/pages/portfolio/Portfolio.tsx | 14 +- src/pages/portfolio/PortfolioNavMobile.tsx | 157 ++++++---- src/pages/trade/HorizontalPanel.tsx | 3 +- .../trade/mobile-web/RegularTradeForm.tsx | 70 +++-- src/pages/trade/mobile-web/TradeHeader.tsx | 14 + src/views/tables/MobilePositionsTable.tsx | 274 +++++++++--------- 7 files changed, 308 insertions(+), 225 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 70010a72ca..f9f72aeb3b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -240,6 +240,7 @@ const Content = () => { } /> + } /> } /> diff --git a/src/pages/portfolio/Portfolio.tsx b/src/pages/portfolio/Portfolio.tsx index 713212108d..61d568f1ee 100644 --- a/src/pages/portfolio/Portfolio.tsx +++ b/src/pages/portfolio/Portfolio.tsx @@ -32,6 +32,7 @@ import { TradeHistoryList } from '@/views/Lists/Trade/TradeHistoryList'; import { AccountHistoryList } from '@/views/Lists/Transfers/AccountHistoryList'; import { FundingHistoryList } from '@/views/Lists/Transfers/FundingHistoryList'; import { VaultTransferList } from '@/views/Lists/Transfers/VaultTransferList'; +import { UserMenuDialog } from '@/views/dialogs/MobileUserMenuDialog'; import { FillsTable, FillsTableColumnKey } from '@/views/tables/FillsTable'; import { TransferHistoryTable } from '@/views/tables/TransferHistoryTable'; @@ -176,15 +177,18 @@ const PortfolioPage = () => { ); - if (isSimpleUi) { + if (isSimpleUi && !isMobileWebEnabled) { return routesComponent; } return isTablet ? ( - <$PortfolioMobile> - - <$MobileContent>{routesComponent} - + <> + <$PortfolioMobile> + + <$MobileContent>{routesComponent} + + + ) : ( { const stringGetter = useStringGetter(); - const { pathname } = useLocation(); + const dispatch = useDispatch(); const navigate = useNavigate(); + const onboardingState = useAppSelector(getOnboardingState); + const { hasUnreadNotifications } = useNotifications(); + const { dydxAddress } = useAccounts(); - const portfolioRouteItems = [ - { - value: `${AppRoute.Portfolio}/${PortfolioRoute.Overview}`, - label: stringGetter({ key: STRING_KEYS.OVERVIEW }), - description: stringGetter({ key: STRING_KEYS.OVERVIEW_DESCRIPTION }), - }, - { - value: `${AppRoute.Portfolio}/${PortfolioRoute.Positions}`, - label: stringGetter({ key: STRING_KEYS.POSITIONS }), - description: stringGetter({ key: STRING_KEYS.POSITIONS_DESCRIPTION }), - }, - { - value: `${AppRoute.Portfolio}/${PortfolioRoute.Orders}`, - label: stringGetter({ key: STRING_KEYS.ORDERS }), - description: stringGetter({ key: STRING_KEYS.ORDERS_DESCRIPTION }), - }, - { - value: `${AppRoute.Portfolio}/${PortfolioRoute.Fees}`, - label: stringGetter({ key: STRING_KEYS.FEES }), - description: stringGetter({ key: STRING_KEYS.FEE_STRUCTURE }), - }, - { - value: `${AppRoute.Portfolio}/${PortfolioRoute.EquityTiers}`, - label: stringGetter({ key: STRING_KEYS.EQUITY_TIERS }), - description: stringGetter({ key: STRING_KEYS.EQUITY_TIERS_DESCRIPTION }), - }, - { - value: `${AppRoute.Portfolio}/${PortfolioRoute.History}/${HistoryRoute.Trades}`, - label: stringGetter({ key: STRING_KEYS.TRADES }), - description: stringGetter({ key: STRING_KEYS.TRADES_DESCRIPTION }), - }, - { - value: `${AppRoute.Portfolio}/${PortfolioRoute.History}/${HistoryRoute.Transfers}`, - label: stringGetter({ key: STRING_KEYS.TRANSFERS }), - description: stringGetter({ key: STRING_KEYS.TRANSFERS_DESCRIPTION }), - }, - { - value: `${AppRoute.Portfolio}/${PortfolioRoute.History}/${HistoryRoute.VaultTransfers}`, - label: stringGetter({ key: STRING_KEYS.VAULT_TRANSFERS }), - description: stringGetter({ key: STRING_KEYS.MEGAVAULT_TRANSFERS_DESCRIPTION }), - }, - ]; - - const routeMap = Object.fromEntries( - portfolioRouteItems.map(({ value, label }) => [value, { value, label }]) - ); - - const currentRoute = routeMap[pathname]; + const openUserMenu = () => { + dispatch(setIsUserMenuOpen(true)); + }; return ( <$MobilePortfolioHeader> - value !== currentRoute?.value)} - onValueChange={navigate} - > - {currentRoute?.label} - + <$LogoLink to="/"> + + + +
+ {onboardingState === OnboardingState.AccountConnected && ( + + )} + + +

{truncateAddress(dydxAddress)}

+ +
+ ), + value: 'copy', + onSelect: () => navigator.clipboard.writeText(dydxAddress ?? ''), + }, + { + label: ( +
+

{stringGetter({ key: STRING_KEYS.SIGN_OUT })}

+ +
+ ), + value: 'sign-out', + onSelect: () => dispatch(openDialog(DialogTypes.DisconnectWallet())), + }, + ]} + > + + {truncateAddress(dydxAddress)} + + + +
); }; + const $MobilePortfolioHeader = styled.div` ${layoutMixins.stickyHeader} ${layoutMixins.withOuterBorder} @@ -84,4 +110,17 @@ const $MobilePortfolioHeader = styled.div` padding: 1rem; background-color: var(--color-layer-2); z-index: 2; + + justify-content: space-between; +`; + +const $LogoLink = styled(Link)` + display: flex; + align-self: stretch; + + > svg { + margin: auto; + width: 1.6rem; + height: 1.6rem; + } `; diff --git a/src/pages/trade/HorizontalPanel.tsx b/src/pages/trade/HorizontalPanel.tsx index 49b2b7c00d..ad27d7d52d 100644 --- a/src/pages/trade/HorizontalPanel.tsx +++ b/src/pages/trade/HorizontalPanel.tsx @@ -409,7 +409,7 @@ export const HorizontalPanel = ({ isOpen = true, setIsOpen, handleStartResize }: const tabItems = useMemo( () => [ - detailsTabItem, + ...(isTablet ? [detailsTabItem] : []), positionTabItem, ordersTabItem, fillsTabItem, @@ -423,6 +423,7 @@ export const HorizontalPanel = ({ isOpen = true, setIsOpen, handleStartResize }: ordersTabItem, orderHistoryTabItem, paymentsTabItem, + isTablet, ] ); diff --git a/src/pages/trade/mobile-web/RegularTradeForm.tsx b/src/pages/trade/mobile-web/RegularTradeForm.tsx index 886cac2370..d1e600f99e 100644 --- a/src/pages/trade/mobile-web/RegularTradeForm.tsx +++ b/src/pages/trade/mobile-web/RegularTradeForm.tsx @@ -9,7 +9,7 @@ import { } from '@/bonsai/forms/trade/types'; import { BonsaiHelpers } from '@/bonsai/ontology'; import BigNumber from 'bignumber.js'; -import styled from 'styled-components'; +import styled, { css } from 'styled-components'; import { ButtonAction, ButtonShape, ButtonSize, ButtonStyle } from '@/constants/buttons'; import { DialogTypes } from '@/constants/dialogs'; @@ -35,6 +35,7 @@ import { TradeFormInputs } from '@/views/forms/TradeForm/TradeFormInputs'; import { TradeSizeInputs } from '@/views/forms/TradeForm/TradeSizeInputs'; import { TradeTriggerOrderInputs } from '@/views/forms/TradeForm/TradeTriggerInput'; +import { getCurrentMarketPositionData } from '@/state/accountSelectors'; import { useAppDispatch, useAppSelector } from '@/state/appTypes'; import { openDialog } from '@/state/dialogs'; import { tradeFormActions } from '@/state/tradeForm'; @@ -56,12 +57,14 @@ const RegularTradeForm = () => { const tradeValues = useAppSelector(getTradeFormValues); const fullFormSummary = useAppSelector(getTradeFormSummary); const { summary } = fullFormSummary; - const { ticker, tickSizeDecimals } = orEmptyObj( + const { ticker, tickSizeDecimals, displayableAsset } = orEmptyObj( useAppSelector(BonsaiHelpers.currentMarket.stableMarketInfo) ); const effectiveSelectedLeverage = useAppSelector( BonsaiHelpers.currentMarket.effectiveSelectedLeverage ); + const { signedSize: positionSize } = orEmptyObj(useAppSelector(getCurrentMarketPositionData)); + const { stopLossOrder: stopLossSummary, takeProfitOrder: takeProfitSummary } = orEmptyObj( summary.triggersSummary ); @@ -248,16 +251,32 @@ const RegularTradeForm = () => { : stringGetter({ key: STRING_KEYS.LIMIT_ORDER_SHORT })} - - Available - - {availableBalance.toLocaleString('en-US', { - minimumFractionDigits: 2, - maximumFractionDigits: 2, - })}{' '} - USDC - - +
+ {positionSize && + !positionSize.eq(0) && + ((positionSize.gt(0) && side === OrderSide.SELL) || + (positionSize.lt(0) && side === OrderSide.BUY)) && ( +
+

Current Position

+ <$PositionSize + tw="text-small" + isLong={positionSize?.isGreaterThanOrEqualTo(0) ?? false} + > + {Math.abs(positionSize?.toNumber() ?? 0)} {displayableAsset} + +
+ )} + +

Available

+

+ {availableBalance.toLocaleString('en-US', { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })}{' '} + USDC +

+
+
<$InputsColumn> @@ -393,22 +412,6 @@ const AvailableRow = styled.div` width: 100%; `; -const AvailableLabel = styled.span` - color: #6b7280; - - font-size: 15 px; -`; -const AvailableValue = styled.div` - display: flex; - - align-items: center; - - gap: 8 px; - - color: #6b7280; - - font-size: 15 px; -`; const ToggleRow = styled.div` display: flex; @@ -432,3 +435,14 @@ const ToggleLabel = styled.span` const $PlaceOrderButtonAndReceipt = styled(PlaceOrderButtonAndReceipt)` --withReceipt-backgroundColor: transparent; `; + +const $PositionSize = styled.div<{ isLong: boolean }>` + ${({ isLong }) => + isLong + ? css` + color: var(--color-positive) !important; + ` + : css` + color: var(--color-negative) !important; + `} +`; diff --git a/src/pages/trade/mobile-web/TradeHeader.tsx b/src/pages/trade/mobile-web/TradeHeader.tsx index aaceaf1b5e..54d14efbf5 100644 --- a/src/pages/trade/mobile-web/TradeHeader.tsx +++ b/src/pages/trade/mobile-web/TradeHeader.tsx @@ -1,7 +1,9 @@ import { BonsaiHelpers } from '@/bonsai/ontology'; +import { useNavigate } from 'react-router-dom'; import styled from 'styled-components'; import { ButtonShape, ButtonSize } from '@/constants/buttons'; +import { AppRoute } from '@/constants/routes'; import { layoutMixins } from '@/styles/layoutMixins'; @@ -16,6 +18,8 @@ import { setIsUserMenuOpen } from '@/state/dialogs'; export const TradeHeaderMobile = ({ launchableMarketId }: { launchableMarketId?: string }) => { const id = useAppSelector(BonsaiHelpers.currentMarket.assetId); + + const navigate = useNavigate(); const dispatch = useAppDispatch(); const openUserMenu = () => { @@ -29,6 +33,16 @@ export const TradeHeaderMobile = ({ launchableMarketId }: { launchableMarketId?: + +
+
+ Funding + <$OutputSigned + sign={getNumberSign(position.netFunding)} + type={OutputType.Fiat} + value={position.netFunding} + showSign={ShowSign.Negative} + /> +
+
+ <$TriggersContainer tw="">TP/SL + +
+
+ +
-
- -
- ); - })} + +
+ ); + })}
); } From 2bb8b34c5e57ee5a2258498f5563d734c55677c0 Mon Sep 17 00:00:00 2001 From: KevDydx Date: Tue, 10 Feb 2026 15:46:23 -0500 Subject: [PATCH 27/27] fix --- src/pages/trade/mobile-web/TradeHeader.tsx | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/pages/trade/mobile-web/TradeHeader.tsx b/src/pages/trade/mobile-web/TradeHeader.tsx index 54d14efbf5..aaceaf1b5e 100644 --- a/src/pages/trade/mobile-web/TradeHeader.tsx +++ b/src/pages/trade/mobile-web/TradeHeader.tsx @@ -1,9 +1,7 @@ import { BonsaiHelpers } from '@/bonsai/ontology'; -import { useNavigate } from 'react-router-dom'; import styled from 'styled-components'; import { ButtonShape, ButtonSize } from '@/constants/buttons'; -import { AppRoute } from '@/constants/routes'; import { layoutMixins } from '@/styles/layoutMixins'; @@ -18,8 +16,6 @@ import { setIsUserMenuOpen } from '@/state/dialogs'; export const TradeHeaderMobile = ({ launchableMarketId }: { launchableMarketId?: string }) => { const id = useAppSelector(BonsaiHelpers.currentMarket.assetId); - - const navigate = useNavigate(); const dispatch = useAppDispatch(); const openUserMenu = () => { @@ -33,16 +29,6 @@ export const TradeHeaderMobile = ({ launchableMarketId }: { launchableMarketId?: -