From 532d5212746de1880853fea7975378b8b1054eca Mon Sep 17 00:00:00 2001 From: Bards Date: Fri, 29 May 2026 12:06:31 +0100 Subject: [PATCH 1/3] feat: add injectable interceptor targets for trading-screen plugins --- .changeset/spicy-brooms-dress.md | 55 +++ .../desktop/dataList/dataList.injectable.tsx | 34 ++ .../desktop/dataList/dataList.ui.tsx | 5 + .../src/components/desktop/dataList/index.ts | 6 + .../trading/src/types/interceptorTargets.ts | 2 + .../src/components/header/index.tsx | 6 + .../src/components/orderEntry.injectabled.tsx | 76 +++- .../src/components/orderTypeSelect/index.tsx | 72 ++-- .../ui-order-entry/src/interceptorTargets.ts | 4 + packages/ui-order-entry/src/orderEntry.ui.tsx | 350 +++++++++--------- .../src/components/tradingview.injectable.tsx | 13 + .../src/components/tradingview.script.ts | 11 +- .../src/components/tradingview.ui.tsx | 11 +- packages/ui-tradingview/src/index.ts | 6 +- .../ui-tradingview/src/interceptorTargets.ts | 2 + packages/ui-tradingview/src/type.ts | 3 + 16 files changed, 444 insertions(+), 212 deletions(-) create mode 100644 .changeset/spicy-brooms-dress.md create mode 100644 packages/trading/src/components/desktop/dataList/dataList.injectable.tsx diff --git a/.changeset/spicy-brooms-dress.md b/.changeset/spicy-brooms-dress.md new file mode 100644 index 0000000000..e4a4e8b6fa --- /dev/null +++ b/.changeset/spicy-brooms-dress.md @@ -0,0 +1,55 @@ +--- +"@orderly.network/ui-order-entry": patch +"@orderly.network/ui-tradingview": patch +"@orderly.network/trading": patch +"@orderly.network/ai-docs": patch +"@orderly.network/storybook": patch +"@orderly.network/affiliate": patch +"@orderly.network/react-app": patch +"@orderly.network/chart": patch +"@orderly.network/devkit": patch +"@orderly.network/core": patch +"@orderly.network/default-evm-adapter": patch +"@orderly.network/default-solana-adapter": patch +"@orderly.network/hooks": patch +"@orderly.network/i18n": patch +"@orderly.network/layout-core": patch +"@orderly.network/layout-grid": patch +"@orderly.network/layout-split": patch +"@orderly.network/markets": patch +"@orderly.network/net": patch +"@orderly.network/perp": patch +"@orderly.network/plugin-core": patch +"@orderly.network/portfolio": patch +"@orderly.network/sdk-docs": patch +"storybook-theme-tool": patch +"@orderly.network/trading-leaderboard": patch +"@orderly.network/trading-next": patch +"@orderly.network/trading-rewards": patch +"tsconfig": patch +"@orderly.network/types": patch +"@orderly.network/ui": patch +"@orderly.network/ui-chain-selector": patch +"@orderly.network/ui-connector": patch +"@orderly.network/ui-leverage": patch +"@orderly.network/ui-notification": patch +"@orderly.network/ui-orders": patch +"@orderly.network/ui-positions": patch +"@orderly.network/ui-scaffold": patch +"@orderly.network/ui-share": patch +"@orderly.network/ui-tpsl": patch +"@orderly.network/ui-transfer": patch +"@orderly.network/utils": patch +"@orderly.network/vaults": patch +"@orderly.network/wallet-connector": patch +"@orderly.network/wallet-connector-privy": patch +"@orderly.network/web3-provider-ethers": patch +--- + +Add interceptor sockets so plugins can extend the trading screen without forking the SDK. Each is additive and defaults to passthrough, so behavior is unchanged when no plugin is installed. + +- `Trading.OrderEntry.AdvancedSelect`: add a custom entry to the order-type Advanced dropdown. A value that isn't a real OrderType is routed via `onExtraSelect`/`selectedExtraId` instead of `order_type`. +- `Trading.OrderEntry.Body`: replace the order-entry form body with a custom panel while the type selector stays in place. +- `Trading.OrderEntry.BuySellSwitch`: new `selectedCustomTypeId` prop lets a plugin hide the Buy/Sell switch for its own type only. +- `Trading.Chart.Overlay`: render over the chart once the TradingView widget is ready (receives the live widget and current symbol). +- `Trading.DataList.Desktop.Tabs`: add custom tabs to the desktop data-list strip. diff --git a/packages/trading/src/components/desktop/dataList/dataList.injectable.tsx b/packages/trading/src/components/desktop/dataList/dataList.injectable.tsx new file mode 100644 index 0000000000..cd0855ecd2 --- /dev/null +++ b/packages/trading/src/components/desktop/dataList/dataList.injectable.tsx @@ -0,0 +1,34 @@ +import React from "react"; +import { TabPanel, injectable } from "@orderly.network/ui"; + +/** Interceptor target for the desktop data-list tabs; plugins append custom tabs after the built-in ones. */ +export const DataListDesktopTabsTarget = "Trading.DataList.Desktop.Tabs"; + +export interface DataListDesktopTabItem { + /** Unique tab id (also its `value` in the Tabs context). */ + id: string; + title: React.ReactNode; + content: React.ReactNode; +} + +export interface DataListDesktopTabsProps { + items: DataListDesktopTabItem[]; +} + +// Renders a TabPanel per item. TabPanel self-registers into the parent Tabs, +// so appended tabs show up as triggers next to the built-in ones. +const DataListDesktopTabs: React.FC = ({ items }) => ( + <> + {items.map((tab) => ( + + {tab.content} + + ))} + +); + +export const InjectableDataListDesktopTabs = + injectable( + DataListDesktopTabs, + DataListDesktopTabsTarget, + ); diff --git a/packages/trading/src/components/desktop/dataList/dataList.ui.tsx b/packages/trading/src/components/desktop/dataList/dataList.ui.tsx index 37d43d7738..9c9fcbc15b 100644 --- a/packages/trading/src/components/desktop/dataList/dataList.ui.tsx +++ b/packages/trading/src/components/desktop/dataList/dataList.ui.tsx @@ -19,6 +19,7 @@ import { PositionHistoryWidget, PositionsWidget, } from "@orderly.network/ui-positions"; +import { InjectableDataListDesktopTabs } from "./dataList.injectable"; import { DataListState, DataListTabType } from "./dataList.script"; const LazySettingWidget = React.lazy(() => @@ -250,6 +251,10 @@ export const DataList: React.FC = (props) => { ); })} + {/* Plugin-appendable tabs. Empty by default; a plugin intercepts + `Trading.DataList.Desktop.Tabs` and appends `{ id, title, content }` + entries, which self-register into the Tabs context above. */} + ); }; diff --git a/packages/trading/src/components/desktop/dataList/index.ts b/packages/trading/src/components/desktop/dataList/index.ts index f5c226731b..d27b72f243 100644 --- a/packages/trading/src/components/desktop/dataList/index.ts +++ b/packages/trading/src/components/desktop/dataList/index.ts @@ -1,2 +1,8 @@ export { DataList } from "./dataList.ui"; export { DataListWidget } from "./dataList.widget"; +export { + DataListDesktopTabsTarget, + InjectableDataListDesktopTabs, + type DataListDesktopTabItem, + type DataListDesktopTabsProps, +} from "./dataList.injectable"; diff --git a/packages/trading/src/types/interceptorTargets.ts b/packages/trading/src/types/interceptorTargets.ts index bee7d4b034..5fd94f796b 100644 --- a/packages/trading/src/types/interceptorTargets.ts +++ b/packages/trading/src/types/interceptorTargets.ts @@ -5,6 +5,7 @@ */ /// import type { SymbolInfoBarFullProps } from "@orderly.network/markets"; +import type { DataListDesktopTabsProps } from "../components/desktop/dataList/dataList.injectable"; import type { Props as OrderBookDesktopAsksProps } from "../components/desktop/orderBook/asks.desktop"; import type { Props as OrderBookDesktopBidsProps } from "../components/desktop/orderBook/bids.desktop"; import type { AccountState } from "../components/mobile/bottomNavBar/account/account.script"; @@ -15,6 +16,7 @@ declare module "@orderly.network/plugin-core" { "Account.MobileAccountMenu": AccountState; "OrderBook.Desktop.Asks": OrderBookDesktopAsksProps; "OrderBook.Desktop.Bids": OrderBookDesktopBidsProps; + "Trading.DataList.Desktop.Tabs": DataListDesktopTabsProps; "Trading.Layout.Desktop": DesktopLayoutProps; "Trading.SymbolInfoBar.Desktop": SymbolInfoBarFullProps; } diff --git a/packages/ui-order-entry/src/components/header/index.tsx b/packages/ui-order-entry/src/components/header/index.tsx index 37de9c1967..394bfa7bf2 100644 --- a/packages/ui-order-entry/src/components/header/index.tsx +++ b/packages/ui-order-entry/src/components/header/index.tsx @@ -22,6 +22,9 @@ type OrderEntryHeaderProps = { marketOrderDisabled?: boolean; /** Tooltip when hovering over the disabled Market button. */ marketOrderDisabledTooltip?: string; + /** Active custom order-type id (null for a real OrderType). */ + selectedExtraId?: string | null; + onExtraSelect?: (id: string | null) => void; }; export function OrderEntryHeader(props: OrderEntryHeaderProps) { @@ -47,6 +50,8 @@ export function OrderEntryHeader(props: OrderEntryHeaderProps) { }} marketOrderDisabled={props.marketOrderDisabled} marketOrderDisabledTooltip={props.marketOrderDisabledTooltip} + selectedExtraId={props.selectedExtraId} + onExtraSelect={props.onExtraSelect} /> { props.setOrderValue("side", nextSide); }} + selectedCustomTypeId={props.selectedExtraId} /> ); diff --git a/packages/ui-order-entry/src/components/orderEntry.injectabled.tsx b/packages/ui-order-entry/src/components/orderEntry.injectabled.tsx index f1ee7becc4..7353cedcf4 100644 --- a/packages/ui-order-entry/src/components/orderEntry.injectabled.tsx +++ b/packages/ui-order-entry/src/components/orderEntry.injectabled.tsx @@ -1,11 +1,32 @@ +import type { ReactNode } from "react"; import { useTranslation } from "@orderly.network/i18n"; import { MarginMode, OrderSide, OrderType } from "@orderly.network/types"; -import { Button, cn, injectable, ThrottledButton } from "@orderly.network/ui"; +import { + Button, + cn, + injectable, + Select, + Text, + ThrottledButton, +} from "@orderly.network/ui"; import { AssetInfo } from "./assetInfo"; import { Available } from "./available"; import { OrderTypeSelect } from "./orderTypeSelect"; import { QuantitySlider } from "./quantitySlider"; +/** One entry in the Advanced order-type dropdown. */ +export type OrderTypeOption = { value: string; label: string }; + +/** Props for the Advanced order-type dropdown interceptor target. */ +export type OrderTypeAdvancedSelectProps = { + items: OrderTypeOption[]; + /** Current selection: a real OrderType value, or a custom-type id. */ + value: string; + placeholder: string; + disabled?: boolean; + onValueChange: (value: string) => void; +}; + /** Props exposed for plugin interceptors targeting OrderType tabs area. */ export type OrderEntryTypeTabsProps = { type: OrderType; @@ -14,6 +35,9 @@ export type OrderEntryTypeTabsProps = { onChange: (type: OrderType) => void; marketOrderDisabled?: boolean; marketOrderDisabledTooltip?: string; + /** Active custom order-type id (null for a real OrderType). */ + selectedExtraId?: string | null; + onExtraSelect?: (id: string | null) => void; }; /** Props exposed for plugin interceptors targeting Buy/Sell switch area. */ @@ -21,6 +45,16 @@ export type OrderEntryBuySellSwitchProps = { side: OrderSide; canTrade: boolean; onSideChange: (side: OrderSide) => void; + /** Active custom order-type id; a plugin can intercept to hide Buy/Sell for its own type. */ + selectedCustomTypeId?: string | null; +}; + +/** Props for the order-entry form-body interceptor target. */ +export type OrderEntryBodyProps = { + symbol: string; + side: OrderSide; + selectedCustomTypeId?: string | null; + children?: ReactNode; }; /** Props exposed for plugin interceptors targeting Available balance row. */ @@ -65,6 +99,44 @@ export type OrderEntrySubmitSectionProps = { }; }; +/** Default renderer for the Advanced order-type dropdown. */ +const OrderTypeAdvancedSelect = (props: OrderTypeAdvancedSelectProps) => ( + props.onValueChange(v)} + placeholder={props.placeholder} + disabled={props.disabled} + contentProps={{ className: "oui-bg-base-8" }} + classNames={{ + trigger: "oui-bg-base-7 oui-border-none oui-h-8 oui-rounded-md", + }} + valueFormatter={(value, option) => { + const item = props.items.find((i) => i.value === value); + return ( + + {item ? item.label : option.placeholder} + + ); + }} + size="md" + /> +); + +export const OrderTypeAdvancedSelectInjectabled = + injectable( + OrderTypeAdvancedSelect, + "Trading.OrderEntry.AdvancedSelect", + ); + +/** Wraps the order-entry form body; default renders children unchanged. */ +export const OrderEntryBodyInjectabled = injectable( + (props: OrderEntryBodyProps) => <>{props.children}, + "Trading.OrderEntry.Body", +); + export const OrderEntryTypeTabsInjectabled = injectable((props: OrderEntryTypeTabsProps) => { return ( @@ -76,6 +148,8 @@ export const OrderEntryTypeTabsInjectabled = onChange={props.onChange} marketOrderDisabled={props.marketOrderDisabled} marketOrderDisabledTooltip={props.marketOrderDisabledTooltip} + selectedExtraId={props.selectedExtraId} + onExtraSelect={props.onExtraSelect} /> ); diff --git a/packages/ui-order-entry/src/components/orderTypeSelect/index.tsx b/packages/ui-order-entry/src/components/orderTypeSelect/index.tsx index c70f7de9bb..b4a4737f6b 100644 --- a/packages/ui-order-entry/src/components/orderTypeSelect/index.tsx +++ b/packages/ui-order-entry/src/components/orderTypeSelect/index.tsx @@ -9,6 +9,10 @@ import { Tooltip, useScreen, } from "@orderly.network/ui"; +import { OrderTypeAdvancedSelectInjectabled } from "../orderEntry.injectabled"; + +const isRealOrderType = (value: string): value is OrderType => + (Object.values(OrderType) as string[]).includes(value); export const OrderTypeSelect = (props: { type: OrderType; @@ -19,6 +23,9 @@ export const OrderTypeSelect = (props: { marketOrderDisabled?: boolean; /** Tooltip text when hovering over the disabled Market button. */ marketOrderDisabledTooltip?: string; + /** Active custom order-type id (null for a real OrderType). */ + selectedExtraId?: string | null; + onExtraSelect?: (id: string | null) => void; }) => { const { t } = useTranslation(); const { isMobile } = useScreen(); @@ -93,9 +100,24 @@ export const OrderTypeSelect = (props: { ); const handleChange = (type: OrderType) => { + props.onExtraSelect?.(null); props.onChange(type); }; + const advancedItems = advancedOptions.map((o) => ({ + value: o.value as string, + label: o.label, + })); + const advancedValue = props.selectedExtraId ?? props.type; + const routeAdvancedChange = (value: string) => { + if (isRealOrderType(value)) { + props.onExtraSelect?.(null); + props.onChange(value); + } else { + props.onExtraSelect?.(value); + } + }; + return (
handleChange(OrderType.LIMIT)} disabled={!props.canTrade} data-testid="oui-testid-orderEntry-orderType-limit" @@ -137,11 +161,13 @@ export const OrderTypeSelect = (props: {
diff --git a/packages/ui-order-entry/src/interceptorTargets.ts b/packages/ui-order-entry/src/interceptorTargets.ts index 5d8bed935b..dd658cad3b 100644 --- a/packages/ui-order-entry/src/interceptorTargets.ts +++ b/packages/ui-order-entry/src/interceptorTargets.ts @@ -6,16 +6,20 @@ import type { OrderEntryBuySellSwitchProps, OrderEntryAvailableProps, + OrderEntryBodyProps, OrderEntryQuantitySliderProps, OrderEntrySubmitSectionProps, OrderEntryTypeTabsProps, + OrderTypeAdvancedSelectProps, } from "./components/orderEntry.injectabled"; import type { OrderEntryProps } from "./orderEntry.ui"; declare module "@orderly.network/plugin-core" { interface InterceptorTargetPropsMap { OrderEntry: OrderEntryProps; + "Trading.OrderEntry.AdvancedSelect": OrderTypeAdvancedSelectProps; "Trading.OrderEntry.Available": OrderEntryAvailableProps; + "Trading.OrderEntry.Body": OrderEntryBodyProps; "Trading.OrderEntry.BuySellSwitch": OrderEntryBuySellSwitchProps; "Trading.OrderEntry.QuantitySlider": OrderEntryQuantitySliderProps; "Trading.OrderEntry.SubmitSection": OrderEntrySubmitSectionProps; diff --git a/packages/ui-order-entry/src/orderEntry.ui.tsx b/packages/ui-order-entry/src/orderEntry.ui.tsx index 81fd439971..2c411c4cc8 100644 --- a/packages/ui-order-entry/src/orderEntry.ui.tsx +++ b/packages/ui-order-entry/src/orderEntry.ui.tsx @@ -44,6 +44,7 @@ import { scaledOrderConfirmDialogId } from "./components/dialog/scaledOrderConfi import { OrderEntryHeader } from "./components/header"; import { OrderEntryAvailableInjectabled, + OrderEntryBodyInjectabled, OrderEntryQuantitySliderInjectabled, OrderEntrySubmitSectionInjectabled, } from "./components/orderEntry.injectabled"; @@ -88,6 +89,7 @@ export const OrderEntry: React.FC = (props) => { symbol, } = props; const [maxQtyConfirmOpen, setMaxQtyConfirmOpen] = useState(false); + const [selectedExtraId, setSelectedExtraId] = useState(null); const [permissionlessAcknowledgedKeys, setPermissionlessAcknowledgedKeys] = useLocalStorage("orderly-permissionless-market-notice", []); @@ -465,193 +467,201 @@ export const OrderEntry: React.FC = (props) => { marketOrderDisabledTooltip={t( "orderEntry.orderType.symbolPostOnly.tooltip", )} + selectedExtraId={selectedExtraId} + onExtraSelect={setSelectedExtraId} /> - - - - - - - - - + selectedCustomTypeId={selectedExtraId} + > + - {/* TP SL switch and content */} - {hasAdvancedTPSLResult ? ( - { - setShowTPSLAdvanced(true); - }} - onDelete={() => { - onDeleteAdvancedTPSL(); + - ) : ( - { - manualSetOrderValue("reduce_only", checked); - }} - values={{ - position_type: - formattedOrder.position_type ?? PositionType.PARTIAL, - tp: { - trigger_price: formattedOrder.tp_trigger_price ?? "", - PnL: formattedOrder.tp_pnl ?? "", - Offset: formattedOrder.tp_offset ?? "", - "Offset%": formattedOrder.tp_offset_percentage ?? "", - OffsetFromMark: formattedOrder.tp_offset_from_mark ?? "", - PercentageFromMark: - formattedOrder.tp_offset_percentage_from_mark ?? "", - ROI: formattedOrder.tp_ROI ?? "", - }, - sl: { - trigger_price: formattedOrder.sl_trigger_price ?? "", - PnL: formattedOrder.sl_pnl ?? "", - Offset: formattedOrder.sl_offset ?? "", - "Offset%": formattedOrder.sl_offset_percentage ?? "", - OffsetFromMark: formattedOrder.sl_offset_from_mark ?? "", - PercentageFromMark: - formattedOrder.sl_offset_percentage_from_mark ?? "", - ROI: formattedOrder.sl_ROI ?? "", - }, - }} - showTPSLAdvanced={onShowTPSLAdvanced} - onChange={(key, value) => { - setOrderValue(key, value); + + + + - )} - {showReduceOnlySection && ( - - { + + + {/* TP SL switch and content */} + {hasAdvancedTPSLResult ? ( + { + setShowTPSLAdvanced(true); + }} + onDelete={() => { + onDeleteAdvancedTPSL(); + }} + /> + ) : ( + { manualSetOrderValue("reduce_only", checked); }} + values={{ + position_type: + formattedOrder.position_type ?? PositionType.PARTIAL, + tp: { + trigger_price: formattedOrder.tp_trigger_price ?? "", + PnL: formattedOrder.tp_pnl ?? "", + Offset: formattedOrder.tp_offset ?? "", + "Offset%": formattedOrder.tp_offset_percentage ?? "", + OffsetFromMark: formattedOrder.tp_offset_from_mark ?? "", + PercentageFromMark: + formattedOrder.tp_offset_percentage_from_mark ?? "", + ROI: formattedOrder.tp_ROI ?? "", + }, + sl: { + trigger_price: formattedOrder.sl_trigger_price ?? "", + PnL: formattedOrder.sl_pnl ?? "", + Offset: formattedOrder.sl_offset ?? "", + "Offset%": formattedOrder.sl_offset_percentage ?? "", + OffsetFromMark: formattedOrder.sl_offset_from_mark ?? "", + PercentageFromMark: + formattedOrder.sl_offset_percentage_from_mark ?? "", + ROI: formattedOrder.sl_ROI ?? "", + }, + }} + showTPSLAdvanced={onShowTPSLAdvanced} + onChange={(key, value) => { + setOrderValue(key, value); + }} /> - {!showSoundSection && extraButton} - - )} - {showSoundSection && ( - - - setSoundAlert(checked)} + )} + + {showReduceOnlySection && ( + + { + manualSetOrderValue("reduce_only", checked); + }} /> - + {!showSoundSection && extraButton} - {extraButton} - - )} - {!showSoundSection && - isMobile && - (formattedOrder.order_type == OrderType.LIMIT || - formattedOrder.order_type == OrderType.MARKET) && - !formattedOrder.reduce_only && - !pinned && ( - + )} + {showSoundSection && ( + + + setSoundAlert(checked)} + /> + + {extraButton} )} - {/* Additional info (fok,ioc、post only, order confirm hidden) */} - {pinned && ( - - - { - setPinned(false); - }} - className={cn( - "oui-additional-pin-btn", - "oui-group oui-absolute oui-right-2 oui-top-2", - )} - data-testid="oui-testid-orderEntry-pinned-button" - /> - - )} + {!showSoundSection && + isMobile && + (formattedOrder.order_type == OrderType.LIMIT || + formattedOrder.order_type == OrderType.MARKET) && + !formattedOrder.reduce_only && + !pinned && ( + + {extraButton} + + )} + {/* Additional info (fok,ioc、post only, order confirm hidden) */} + {pinned && ( + + + { + setPinned(false); + }} + className={cn( + "oui-additional-pin-btn", + "oui-group oui-absolute oui-right-2 oui-top-2", + )} + data-testid="oui-testid-orderEntry-pinned-button" + /> + + )} + ( + () => null, + "Trading.Chart.Overlay", +); diff --git a/packages/ui-tradingview/src/components/tradingview.script.ts b/packages/ui-tradingview/src/components/tradingview.script.ts index 5b5987a38e..b86a1d85a8 100644 --- a/packages/ui-tradingview/src/components/tradingview.script.ts +++ b/packages/ui-tradingview/src/components/tradingview.script.ts @@ -20,7 +20,10 @@ import { modal, toast, useOrderlyTheme, useScreen } from "@orderly.network/ui"; import { Decimal } from "@orderly.network/utils"; import { useCssVariables } from "../hooks/useCssVariables"; import getBrokerAdapter from "../tradingviewAdapter/broker/getBrokerAdapter"; -import { LoadingScreenOptions } from "../tradingviewAdapter/charting_library"; +import { + IChartingLibraryWidget, + LoadingScreenOptions, +} from "../tradingviewAdapter/charting_library"; import { Datafeed } from "../tradingviewAdapter/datafeed/datafeed"; import { WebsocketService } from "../tradingviewAdapter/datafeed/websocket.service"; import useBroker from "../tradingviewAdapter/hooks/useBroker"; @@ -78,6 +81,9 @@ export function useTradingviewScript(props: TradingviewWidgetPropsInterface) { const cssVariables = useCssVariables(theme); const chart = useRef(null); + const [readyWidget, setReadyWidget] = useState( + null, + ); const apiBaseUrl: string = useConfig("apiBaseUrl") as string; const { state: accountState } = useAccount(); const [side, setSide] = useState(OrderSide.SELL); @@ -382,6 +388,7 @@ export function useTradingviewScript(props: TradingviewWidgetPropsInterface) { return () => { chart.current?.remove(); + setReadyWidget(null); }; }, [ isMobile, @@ -423,6 +430,7 @@ export function useTradingviewScript(props: TradingviewWidgetPropsInterface) { useEffect(() => { if (chart.current && chart.current?.instance) { chart.current?.instance?.onChartReady(() => { + setReadyWidget(chart.current?.instance ?? null); if (isLoggedIn && chart.current?.instance) { createRenderer( chart.current.instance, @@ -465,5 +473,6 @@ export function useTradingviewScript(props: TradingviewWidgetPropsInterface) { onFullScreenChange, classNames, fullscreen, + readyWidget, }; } diff --git a/packages/ui-tradingview/src/components/tradingview.ui.tsx b/packages/ui-tradingview/src/components/tradingview.ui.tsx index d559edb624..404c9725ae 100644 --- a/packages/ui-tradingview/src/components/tradingview.ui.tsx +++ b/packages/ui-tradingview/src/components/tradingview.ui.tsx @@ -4,7 +4,10 @@ import { IndicatorsIcon, SettingIcon } from "../icons"; import type { TradingviewUIPropsInterface } from "../type"; import { NoTradingview } from "./noTradingview"; import TopBar from "./topBar"; -import { InjectableTradingviewDesktop } from "./tradingview.injectable"; +import { + InjectableChartOverlay, + InjectableTradingviewDesktop, +} from "./tradingview.injectable"; const LazyLineType = React.lazy(() => import("./lineType")); @@ -247,6 +250,12 @@ export const TradingviewUI = forwardRef< )} + {props.readyWidget && props.symbol && ( + + )} )} diff --git a/packages/ui-tradingview/src/index.ts b/packages/ui-tradingview/src/index.ts index 4c7bcddab0..96671fd5bf 100644 --- a/packages/ui-tradingview/src/index.ts +++ b/packages/ui-tradingview/src/index.ts @@ -5,7 +5,11 @@ import "./interceptorTargets"; export { TradingviewWidget } from "./components/tradingview.widget"; export { TradingviewUI } from "./components/tradingview.ui"; -export { InjectableTradingviewDesktop } from "./components/tradingview.injectable"; +export { + InjectableTradingviewDesktop, + InjectableChartOverlay, +} from "./components/tradingview.injectable"; +export type { ChartOverlayProps } from "./components/tradingview.injectable"; export { useTradingviewScript } from "./components/tradingview.script"; /** * Export display-control interceptor target + props for plugin consumers. diff --git a/packages/ui-tradingview/src/interceptorTargets.ts b/packages/ui-tradingview/src/interceptorTargets.ts index ae39a4e9dc..611f580209 100644 --- a/packages/ui-tradingview/src/interceptorTargets.ts +++ b/packages/ui-tradingview/src/interceptorTargets.ts @@ -5,9 +5,11 @@ */ /// import type { DesktopDisplayControlMenuListProps } from "./components/displayControl/common"; +import type { ChartOverlayProps } from "./components/tradingview.injectable"; declare module "@orderly.network/plugin-core" { interface InterceptorTargetPropsMap { "TradingView.DisplayControl.DesktopMenuList": DesktopDisplayControlMenuListProps; + "Trading.Chart.Overlay": ChartOverlayProps; } } diff --git a/packages/ui-tradingview/src/type.ts b/packages/ui-tradingview/src/type.ts index 6c125dfb96..0518a9482b 100644 --- a/packages/ui-tradingview/src/type.ts +++ b/packages/ui-tradingview/src/type.ts @@ -5,6 +5,7 @@ import { StudyOverrides, Overrides, type ChartingLibraryWidgetOptions, + type IChartingLibraryWidget, } from "./tradingviewAdapter/charting_library"; import { ChartMode, ColorConfigInterface } from "./tradingviewAdapter/type"; @@ -69,6 +70,8 @@ export interface TradingviewUIPropsInterface { content?: string; }; fullscreen?: boolean; + /** Live charting widget, available once the chart is ready. */ + readyWidget?: IChartingLibraryWidget | null; } export interface DisplayControlSettingInterface { From 76cd6f949cd373e8cd5784d8125d91ef7671083e Mon Sep 17 00:00:00 2001 From: Bards Date: Mon, 1 Jun 2026 11:43:48 +0100 Subject: [PATCH 2/3] feat: add mobile injectable interceptor targets for trading-screen plugins --- .changeset/spicy-brooms-dress.md | 2 + .../mobile/dataList/dataList.injectable.tsx | 33 +++++++++ .../mobile/dataList/dataList.ui.tsx | 2 + .../src/components/mobile/dataList/index.ts | 6 ++ .../trading/src/types/interceptorTargets.ts | 2 + .../src/components/orderEntry.injectabled.tsx | 47 +++++++++++- .../src/components/orderTypeSelect/index.tsx | 73 +++++-------------- .../ui-order-entry/src/interceptorTargets.ts | 2 + 8 files changed, 113 insertions(+), 54 deletions(-) create mode 100644 packages/trading/src/components/mobile/dataList/dataList.injectable.tsx diff --git a/.changeset/spicy-brooms-dress.md b/.changeset/spicy-brooms-dress.md index e4a4e8b6fa..f386066042 100644 --- a/.changeset/spicy-brooms-dress.md +++ b/.changeset/spicy-brooms-dress.md @@ -51,5 +51,7 @@ Add interceptor sockets so plugins can extend the trading screen without forking - `Trading.OrderEntry.AdvancedSelect`: add a custom entry to the order-type Advanced dropdown. A value that isn't a real OrderType is routed via `onExtraSelect`/`selectedExtraId` instead of `order_type`. - `Trading.OrderEntry.Body`: replace the order-entry form body with a custom panel while the type selector stays in place. - `Trading.OrderEntry.BuySellSwitch`: new `selectedCustomTypeId` prop lets a plugin hide the Buy/Sell switch for its own type only. +- `Trading.OrderEntry.MobileTypeSelect`: add a custom entry to the mobile order-type dropdown (same routing model as `AdvancedSelect`; preserves the `marketOrderDisabled` modal). - `Trading.Chart.Overlay`: render over the chart once the TradingView widget is ready (receives the live widget and current symbol). - `Trading.DataList.Desktop.Tabs`: add custom tabs to the desktop data-list strip. +- `Trading.DataList.Mobile.Tabs`: add custom tabs to the mobile data-list strip. diff --git a/packages/trading/src/components/mobile/dataList/dataList.injectable.tsx b/packages/trading/src/components/mobile/dataList/dataList.injectable.tsx new file mode 100644 index 0000000000..28850ae9e7 --- /dev/null +++ b/packages/trading/src/components/mobile/dataList/dataList.injectable.tsx @@ -0,0 +1,33 @@ +import React from "react"; +import { TabPanel, injectable } from "@orderly.network/ui"; + +/** Interceptor target for the mobile data-list tabs; plugins append custom tabs after the built-in ones. */ +export const DataListMobileTabsTarget = "Trading.DataList.Mobile.Tabs"; + +export interface DataListMobileTabItem { + /** Unique tab id (also its `value` in the Tabs context). */ + id: string; + title: React.ReactNode; + content: React.ReactNode; +} + +export interface DataListMobileTabsProps { + items: DataListMobileTabItem[]; +} + +// Renders a TabPanel per item. TabPanel self-registers into the parent Tabs, +// so appended tabs show up as triggers next to the built-in ones. +const DataListMobileTabs: React.FC = ({ items }) => ( + <> + {items.map((tab) => ( + + {tab.content} + + ))} + +); + +export const InjectableDataListMobileTabs = injectable( + DataListMobileTabs, + DataListMobileTabsTarget, +); diff --git a/packages/trading/src/components/mobile/dataList/dataList.ui.tsx b/packages/trading/src/components/mobile/dataList/dataList.ui.tsx index 3acec63ba2..955d40331c 100644 --- a/packages/trading/src/components/mobile/dataList/dataList.ui.tsx +++ b/packages/trading/src/components/mobile/dataList/dataList.ui.tsx @@ -21,6 +21,7 @@ import { MobilePositionsWidget, } from "@orderly.network/ui-positions"; import { formatSymbol } from "@orderly.network/utils"; +import { InjectableDataListMobileTabs } from "./dataList.injectable"; import { type DataListState, DataListTabSubType, @@ -263,6 +264,7 @@ export const DataList: React.FC = ( ); })} + ); }; diff --git a/packages/trading/src/components/mobile/dataList/index.ts b/packages/trading/src/components/mobile/dataList/index.ts index 7966cfee62..fdee9e9737 100644 --- a/packages/trading/src/components/mobile/dataList/index.ts +++ b/packages/trading/src/components/mobile/dataList/index.ts @@ -1,3 +1,9 @@ export { DataList } from "./dataList.ui"; export { DataListWidget } from "./dataList.widget"; export { useDataListScript } from "./dataList.script"; +export { + DataListMobileTabsTarget, + InjectableDataListMobileTabs, + type DataListMobileTabItem, + type DataListMobileTabsProps, +} from "./dataList.injectable"; diff --git a/packages/trading/src/types/interceptorTargets.ts b/packages/trading/src/types/interceptorTargets.ts index 5fd94f796b..97fdf08a0b 100644 --- a/packages/trading/src/types/interceptorTargets.ts +++ b/packages/trading/src/types/interceptorTargets.ts @@ -9,6 +9,7 @@ import type { DataListDesktopTabsProps } from "../components/desktop/dataList/da import type { Props as OrderBookDesktopAsksProps } from "../components/desktop/orderBook/asks.desktop"; import type { Props as OrderBookDesktopBidsProps } from "../components/desktop/orderBook/bids.desktop"; import type { AccountState } from "../components/mobile/bottomNavBar/account/account.script"; +import type { DataListMobileTabsProps } from "../components/mobile/dataList/dataList.injectable"; import type { DesktopLayoutProps } from "../pages/trading/trading.ui.desktop"; declare module "@orderly.network/plugin-core" { @@ -17,6 +18,7 @@ declare module "@orderly.network/plugin-core" { "OrderBook.Desktop.Asks": OrderBookDesktopAsksProps; "OrderBook.Desktop.Bids": OrderBookDesktopBidsProps; "Trading.DataList.Desktop.Tabs": DataListDesktopTabsProps; + "Trading.DataList.Mobile.Tabs": DataListMobileTabsProps; "Trading.Layout.Desktop": DesktopLayoutProps; "Trading.SymbolInfoBar.Desktop": SymbolInfoBarFullProps; } diff --git a/packages/ui-order-entry/src/components/orderEntry.injectabled.tsx b/packages/ui-order-entry/src/components/orderEntry.injectabled.tsx index 7353cedcf4..b26666f2e9 100644 --- a/packages/ui-order-entry/src/components/orderEntry.injectabled.tsx +++ b/packages/ui-order-entry/src/components/orderEntry.injectabled.tsx @@ -14,7 +14,7 @@ import { Available } from "./available"; import { OrderTypeSelect } from "./orderTypeSelect"; import { QuantitySlider } from "./quantitySlider"; -/** One entry in the Advanced order-type dropdown. */ +/** One entry in the order-type dropdown. */ export type OrderTypeOption = { value: string; label: string }; /** Props for the Advanced order-type dropdown interceptor target. */ @@ -27,6 +27,15 @@ export type OrderTypeAdvancedSelectProps = { onValueChange: (value: string) => void; }; +/** Props for the mobile order-type dropdown interceptor target. */ +export type MobileTypeSelectProps = { + items: OrderTypeOption[]; + /** Current selection: a real OrderType value, or a custom-type id. */ + value: string; + disabled?: boolean; + onValueChange: (value: string) => void; +}; + /** Props exposed for plugin interceptors targeting OrderType tabs area. */ export type OrderEntryTypeTabsProps = { type: OrderType; @@ -131,6 +140,42 @@ export const OrderTypeAdvancedSelectInjectabled = "Trading.OrderEntry.AdvancedSelect", ); +/** Default renderer for the mobile order-type dropdown. */ +const MobileTypeSelect = (props: MobileTypeSelectProps) => ( + props.onValueChange(v)} + disabled={props.disabled} + contentProps={{ + className: cn("oui-orderEntry-orderTypeSelect-content", "oui-bg-base-8"), + }} + classNames={{ + trigger: cn( + "oui-orderEntry-orderTypeSelect-btn", + "oui-bg-base-7 oui-border-line-12 oui-h-8 oui-rounded-md", + ), + }} + valueFormatter={(value, option) => { + const item = props.items.find((i) => i.value === value); + return ( + + {item ? item.label : option.placeholder} + + ); + }} + size="md" + /> +); + +export const OrderTypeMobileSelectInjectabled = + injectable( + MobileTypeSelect, + "Trading.OrderEntry.MobileTypeSelect", + ); + /** Wraps the order-entry form body; default renders children unchanged. */ export const OrderEntryBodyInjectabled = injectable( (props: OrderEntryBodyProps) => <>{props.children}, diff --git a/packages/ui-order-entry/src/components/orderTypeSelect/index.tsx b/packages/ui-order-entry/src/components/orderTypeSelect/index.tsx index b4a4737f6b..640eddccc3 100644 --- a/packages/ui-order-entry/src/components/orderTypeSelect/index.tsx +++ b/packages/ui-order-entry/src/components/orderTypeSelect/index.tsx @@ -1,15 +1,11 @@ import { useMemo } from "react"; import { useTranslation } from "@orderly.network/i18n"; import { OrderSide, OrderType } from "@orderly.network/types"; +import { cn, modal, Text, Tooltip, useScreen } from "@orderly.network/ui"; import { - cn, - modal, - Select, - Text, - Tooltip, - useScreen, -} from "@orderly.network/ui"; -import { OrderTypeAdvancedSelectInjectabled } from "../orderEntry.injectabled"; + OrderTypeAdvancedSelectInjectabled, + OrderTypeMobileSelectInjectabled, +} from "../orderEntry.injectabled"; const isRealOrderType = (value: string): value is OrderType => (Object.values(OrderType) as string[]).includes(value); @@ -72,17 +68,6 @@ export const OrderTypeSelect = (props: { ]; }, [t]); - const displayLabelMap = useMemo(() => { - return { - [OrderType.LIMIT]: t("orderEntry.orderType.limit"), - [OrderType.MARKET]: t("common.marketPrice"), - [OrderType.STOP_LIMIT]: t("orderEntry.orderType.stopLimit"), - [OrderType.STOP_MARKET]: t("orderEntry.orderType.stopMarket"), - [OrderType.SCALED]: t("orderEntry.orderType.scaledOrder"), - [OrderType.TRAILING_STOP]: t("orderEntry.orderType.trailingStop"), - }; - }, [t]); - // Must run on every render; do not place after `if (!isMobile) return` or hook order breaks when isMobile toggles. const mobileOptions = useMemo(() => allOptions, [allOptions]); @@ -192,7 +177,16 @@ export const OrderTypeSelect = (props: { ); } - const handleMobileValueChange = (value: OrderType) => { + const mobileItems = mobileOptions.map((o) => ({ + value: o.value as string, + label: o.label, + })); + const mobileValue = props.selectedExtraId ?? props.type; + const routeMobileChange = (value: string) => { + if (!isRealOrderType(value)) { + props.onExtraSelect?.(value); + return; + } if ( marketOrderDisabled && value === OrderType.MARKET && @@ -204,43 +198,16 @@ export const OrderTypeSelect = (props: { }); return; } + props.onExtraSelect?.(null); props.onChange(value); }; return ( - { - const item = allOptions.find((o) => o.value === value); - if (!item) { - return {option.placeholder}; - } - - const label = displayLabelMap[value as keyof typeof displayLabelMap]; - - return ( - - {label} - - ); - }} - size={"md"} + ); }; diff --git a/packages/ui-order-entry/src/interceptorTargets.ts b/packages/ui-order-entry/src/interceptorTargets.ts index dd658cad3b..778490faa8 100644 --- a/packages/ui-order-entry/src/interceptorTargets.ts +++ b/packages/ui-order-entry/src/interceptorTargets.ts @@ -4,6 +4,7 @@ * createInterceptor("Trading.OrderEntry.*", (Original, props, api) => ...). */ import type { + MobileTypeSelectProps, OrderEntryBuySellSwitchProps, OrderEntryAvailableProps, OrderEntryBodyProps, @@ -21,6 +22,7 @@ declare module "@orderly.network/plugin-core" { "Trading.OrderEntry.Available": OrderEntryAvailableProps; "Trading.OrderEntry.Body": OrderEntryBodyProps; "Trading.OrderEntry.BuySellSwitch": OrderEntryBuySellSwitchProps; + "Trading.OrderEntry.MobileTypeSelect": MobileTypeSelectProps; "Trading.OrderEntry.QuantitySlider": OrderEntryQuantitySliderProps; "Trading.OrderEntry.SubmitSection": OrderEntrySubmitSectionProps; "Trading.OrderEntry.TypeTabs": OrderEntryTypeTabsProps; From 328230f6787fba9fff55d4bdf0c1e47aee640988 Mon Sep 17 00:00:00 2001 From: Bards Date: Mon, 1 Jun 2026 22:57:52 +0100 Subject: [PATCH 3/3] feat: add Trading.Layout.Mobile interceptor target (symmetric with Desktop) --- .changeset/spicy-brooms-dress.md | 1 + packages/trading/src/pages/trading/trading.injectable.tsx | 6 ++++++ packages/trading/src/pages/trading/trading.ui.mobile.tsx | 4 +++- packages/trading/src/pages/trading/trading.ui.tsx | 8 +++++--- packages/trading/src/types/interceptorTargets.ts | 2 ++ 5 files changed, 17 insertions(+), 4 deletions(-) diff --git a/.changeset/spicy-brooms-dress.md b/.changeset/spicy-brooms-dress.md index f386066042..fa73da839b 100644 --- a/.changeset/spicy-brooms-dress.md +++ b/.changeset/spicy-brooms-dress.md @@ -55,3 +55,4 @@ Add interceptor sockets so plugins can extend the trading screen without forking - `Trading.Chart.Overlay`: render over the chart once the TradingView widget is ready (receives the live widget and current symbol). - `Trading.DataList.Desktop.Tabs`: add custom tabs to the desktop data-list strip. - `Trading.DataList.Mobile.Tabs`: add custom tabs to the mobile data-list strip. +- `Trading.Layout.Mobile`: mobile counterpart to `Trading.Layout.Desktop`. Lets a plugin mount a context provider (e.g. a grid-bot or basis-trade provider) above the mobile trading layout, so feature UI rendered by other interceptors (`OrderEntry.MobileTypeSelect`, `OrderEntry.Body`, `DataList.Mobile.Tabs`, `Chart.Overlay`) can read that context. diff --git a/packages/trading/src/pages/trading/trading.injectable.tsx b/packages/trading/src/pages/trading/trading.injectable.tsx index 7e34e0825a..320fda3aee 100644 --- a/packages/trading/src/pages/trading/trading.injectable.tsx +++ b/packages/trading/src/pages/trading/trading.injectable.tsx @@ -1,7 +1,13 @@ import { injectable } from "@orderly.network/ui"; import { DesktopLayout } from "./trading.ui.desktop"; +import { MobileLayout } from "./trading.ui.mobile"; export const InjectableDesktopLayout = injectable( DesktopLayout, "Trading.Layout.Desktop", ); + +export const InjectableMobileLayout = injectable( + MobileLayout, + "Trading.Layout.Mobile", +); diff --git a/packages/trading/src/pages/trading/trading.ui.mobile.tsx b/packages/trading/src/pages/trading/trading.ui.mobile.tsx index 679c10890f..2e07b97936 100644 --- a/packages/trading/src/pages/trading/trading.ui.mobile.tsx +++ b/packages/trading/src/pages/trading/trading.ui.mobile.tsx @@ -52,7 +52,9 @@ const MaybeEqual: React.FC = () => { ); }; -export const MobileLayout: React.FC = (props) => { +export type MobileLayoutProps = TradingState; + +export const MobileLayout: React.FC = (props) => { const { t } = useTranslation(); const { isRwa, open, closeTimeInterval } = useGetRwaSymbolInfo(props.symbol); diff --git a/packages/trading/src/pages/trading/trading.ui.tsx b/packages/trading/src/pages/trading/trading.ui.tsx index c9dac83440..9b485ff7db 100644 --- a/packages/trading/src/pages/trading/trading.ui.tsx +++ b/packages/trading/src/pages/trading/trading.ui.tsx @@ -2,9 +2,11 @@ import { FC, useEffect } from "react"; import { useSymbolsInfo } from "@orderly.network/hooks"; import { useTranslation } from "@orderly.network/i18n"; import { toast, useScreen } from "@orderly.network/ui"; -import { InjectableDesktopLayout } from "./trading.injectable"; +import { + InjectableDesktopLayout, + InjectableMobileLayout, +} from "./trading.injectable"; import type { TradingState } from "./trading.script"; -import { MobileLayout } from "./trading.ui.mobile"; export const Trading: FC = (props) => { const { isMobile } = useScreen(); @@ -29,7 +31,7 @@ export const Trading: FC = (props) => { // }, [symbol, symbolsInfo, t]); if (isMobile) { - return ; + return ; } return ( diff --git a/packages/trading/src/types/interceptorTargets.ts b/packages/trading/src/types/interceptorTargets.ts index 97fdf08a0b..e92b5ea9ae 100644 --- a/packages/trading/src/types/interceptorTargets.ts +++ b/packages/trading/src/types/interceptorTargets.ts @@ -11,6 +11,7 @@ import type { Props as OrderBookDesktopBidsProps } from "../components/desktop/o import type { AccountState } from "../components/mobile/bottomNavBar/account/account.script"; import type { DataListMobileTabsProps } from "../components/mobile/dataList/dataList.injectable"; import type { DesktopLayoutProps } from "../pages/trading/trading.ui.desktop"; +import type { MobileLayoutProps } from "../pages/trading/trading.ui.mobile"; declare module "@orderly.network/plugin-core" { interface InterceptorTargetPropsMap { @@ -20,6 +21,7 @@ declare module "@orderly.network/plugin-core" { "Trading.DataList.Desktop.Tabs": DataListDesktopTabsProps; "Trading.DataList.Mobile.Tabs": DataListMobileTabsProps; "Trading.Layout.Desktop": DesktopLayoutProps; + "Trading.Layout.Mobile": MobileLayoutProps; "Trading.SymbolInfoBar.Desktop": SymbolInfoBarFullProps; } }