diff --git a/frontend/src/App.css b/frontend/src/App.css index 53b788a..d862999 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -11502,15 +11502,23 @@ html.theme-light .tam-disclaimer { color: #90a4ae; } overflow: hidden; } .tnl-page--with-right .tnl-right { + flex: 0 0 0; + width: 0; + min-width: 0; + display: flex; + flex-direction: column; + border-left: none; + background: var(--bg2); + overflow: hidden; + transition: width 0.22s cubic-bezier(0.4, 0, 0.2, 1), flex-basis 0.22s cubic-bezier(0.4, 0, 0.2, 1); +} + +.tnl-page--with-right .tnl-side-wrap--right.tnl-side-wrap--open .tnl-right.tnl-right--open { flex: 0 0 min(380px, 38vw); width: min(380px, 38vw); min-width: 300px; max-width: 420px; - display: flex; - flex-direction: column; border-left: 1px solid var(--border); - background: var(--bg2); - overflow: hidden; } .tnl-right-tabs { flex-shrink: 0; diff --git a/frontend/src/components/TradeNotificationListPage.tsx b/frontend/src/components/TradeNotificationListPage.tsx index 2699ea8..8278fe5 100644 --- a/frontend/src/components/TradeNotificationListPage.tsx +++ b/frontend/src/components/TradeNotificationListPage.tsx @@ -13,8 +13,10 @@ import PaperSplitPanel from './paper/PaperSplitPanel'; import { useUpbitOrderbook } from '../hooks/useUpbitOrderbook'; import { useUpbitRecentTrades } from '../hooks/useUpbitRecentTrades'; import { useAppSettings } from '../hooks/useAppSettings'; +import { readStoredBool, storeBool } from './strategyEditor/usePanelResize'; import '../styles/strategyEditorTheme.css'; import '../styles/virtualTradingDashboard.css'; +import '../styles/builderPageShell.css'; import '../styles/tradeNotificationList.css'; import TradeNotificationListRow from './tradeNotification/TradeNotificationListRow'; import TradeNotificationGridLayoutPicker from './tradeNotification/TradeNotificationGridLayoutPicker'; @@ -32,6 +34,8 @@ import { type RightTab = 'trade' | 'orderbook'; +const TNL_RIGHT_OPEN_KEY = 'tnl-right-open'; + const IcTrash = () => ( @@ -106,6 +110,7 @@ export const TradeNotificationListPage: React.FC = ({ const [summary, setSummary] = useState(null); const orderAnchorRef = useRef(null); const [refreshing, setRefreshing] = useState(false); + const [rightOpen, setRightOpen] = useState(() => readStoredBool(TNL_RIGHT_OPEN_KEY, true)); const { settings: appSettings } = useAppSettings(); const chartLiveReceiveHighlight = appSettings.chartLiveReceiveHighlight ?? true; @@ -244,11 +249,30 @@ export const TradeNotificationListPage: React.FC = ({ saveTradeNotificationGridPreset(preset); }, []); + const toggleRightPanel = useCallback(() => { + setRightOpen(prev => { + const next = !prev; + storeBool(TNL_RIGHT_OPEN_KEY, next); + return next; + }); + }, []); + const isListView = listLayout === 'list'; const gridCols = getTradeNotificationGridPreset(gridPreset).cols; return ( -
+
@@ -375,91 +399,123 @@ export const TradeNotificationListPage: React.FC = ({
- + )} +
+ +
); diff --git a/frontend/src/components/TradingChart.tsx b/frontend/src/components/TradingChart.tsx index 4d67e69..ebb08e4 100644 --- a/frontend/src/components/TradingChart.tsx +++ b/frontend/src/components/TradingChart.tsx @@ -135,6 +135,8 @@ interface TradingChartProps { paneSeparatorOptions?: ChartPaneSeparatorOptions; /** true: pane 합산 높이로 chart-container 를 키우지 않고 래퍼 높이에 맞춤 (미니 차트·알림 목록) */ paneLayoutClamp?: boolean; + /** 보조지표 pane 레전드(이름·값 오버레이) 표시 (기본 true, 알림 미니차트는 false) */ + showPaneLegend?: boolean; } const TradingChart: React.FC = ({ @@ -167,6 +169,7 @@ const TradingChart: React.FC = ({ chartVisible = true, paneSeparatorOptions, paneLayoutClamp = false, + showPaneLegend = true, }) => { const containerRef = useRef(null); const wrapperRef = useRef(null); // 스크롤 래퍼 @@ -1396,7 +1399,7 @@ const TradingChart: React.FC = ({ onRestore={() => setCandleOnlyMode(false)} /> )} - {chartMgr && !candleOnlyMode && ( + {chartMgr && !candleOnlyMode && showPaneLegend && ( = ({ className = '', label, children, + clipPeekLabels = false, }) => { const viewportRef = useRef(null); const [canScroll, setCanScroll] = useState(false); @@ -46,6 +51,31 @@ const TradeNotificationHScrollPane: React.FC setAtEnd(!overflow || el.scrollLeft >= max - 2); }, []); + const bindPeekLabelClip = useCallback(() => { + const viewport = viewportRef.current; + if (!viewport || !clipPeekLabels) return () => {}; + + const applyPeek = (card: HTMLElement, ratio: number) => { + card.classList.toggle('tnl-chart-card--peek', ratio < PEEK_LABEL_THRESHOLD); + }; + + const cards = Array.from(viewport.querySelectorAll('.tnl-chart-card')); + if (cards.length === 0) return () => {}; + + const io = new IntersectionObserver( + entries => { + entries.forEach(entry => applyPeek(entry.target as HTMLElement, entry.intersectionRatio)); + }, + { root: viewport, threshold: [0, 0.5, 0.99, 1] }, + ); + + cards.forEach(card => io.observe(card)); + return () => { + io.disconnect(); + cards.forEach(card => card.classList.remove('tnl-chart-card--peek')); + }; + }, [clipPeekLabels]); + useEffect(() => { const el = viewportRef.current; if (!el) return; @@ -60,6 +90,23 @@ const TradeNotificationHScrollPane: React.FC }; }, [syncScrollState, children]); + useEffect(() => { + if (!clipPeekLabels) return; + let cleanup = bindPeekLabelClip(); + const viewport = viewportRef.current; + if (!viewport) return cleanup; + + const mo = new MutationObserver(() => { + cleanup(); + cleanup = bindPeekLabelClip(); + }); + mo.observe(viewport, { childList: true, subtree: true }); + return () => { + mo.disconnect(); + cleanup(); + }; + }, [clipPeekLabels, bindPeekLabelClip, children]); + const scrollByStep = useCallback((dir: -1 | 1) => { const el = viewportRef.current; if (!el) return; diff --git a/frontend/src/components/tradeNotification/TradeNotificationListRow.tsx b/frontend/src/components/tradeNotification/TradeNotificationListRow.tsx index c0fcb7f..8e21923 100644 --- a/frontend/src/components/tradeNotification/TradeNotificationListRow.tsx +++ b/frontend/src/components/tradeNotification/TradeNotificationListRow.tsx @@ -399,6 +399,7 @@ const TradeNotificationListRow: React.FC = ({ indicatorScrollNeeded ? 'tnl-row-gallery-indicators--scroll' : '', ].filter(Boolean).join(' ')} label="보조지표" + clipPeekLabels={indicatorScrollNeeded} >
{indicatorCards.map(card => ( diff --git a/frontend/src/components/tradeNotification/TradeSignalMiniChart.tsx b/frontend/src/components/tradeNotification/TradeSignalMiniChart.tsx index df49106..c0dc94c 100644 --- a/frontend/src/components/tradeNotification/TradeSignalMiniChart.tsx +++ b/frontend/src/components/tradeNotification/TradeSignalMiniChart.tsx @@ -225,6 +225,7 @@ const TradeSignalMiniChart: React.FC = ({ showHoverToolbar={false} seriesPriceLabelsEnabled={false} paneLayoutClamp + showPaneLegend={false} />
); diff --git a/frontend/src/styles/tradeNotificationList.css b/frontend/src/styles/tradeNotificationList.css index 7cb48e1..c759c55 100644 --- a/frontend/src/styles/tradeNotificationList.css +++ b/frontend/src/styles/tradeNotificationList.css @@ -148,6 +148,7 @@ background: color-mix(in srgb, var(--bg) 40%, var(--bg2)); overflow: hidden; box-sizing: border-box; + isolation: isolate; } .tnl-hscroll-pane__viewport { @@ -217,6 +218,8 @@ } .tnl-hscroll-pane__rail { + position: relative; + z-index: 4; flex: 0 0 32px; width: 32px; display: flex; @@ -789,6 +792,12 @@ font-weight: 700; } +/* 보조지표 스크롤 — 뷰포트에 완전히 들어오지 않은 카드는 라벨 숨김 */ +.tnl-chart-card--peek .tnl-chart-card-head { + visibility: hidden; + pointer-events: none; +} + .tnl-chart-card-meta { font-size: 10px; color: var(--text3); @@ -907,6 +916,34 @@ gap: 12px; } +/* 우측 매매·호가 패널 접기/펼치기 */ +.tnl-side-wrap--right { + display: flex; + flex-direction: row; + align-items: stretch; + flex-shrink: 0; + min-height: 0; + height: 100%; + position: relative; + z-index: 8; +} + +.tnl-page--right-collapsed .tnl-main { + flex: 1; + min-width: 0; +} + +.tnl-side-wrap--right .tnl-right-tabs, +.tnl-side-wrap--right .tnl-right-body { + min-width: min(380px, 38vw); +} + +.tnl-side-wrap--right:not(.tnl-side-wrap--open) .tnl-right-tabs, +.tnl-side-wrap--right:not(.tnl-side-wrap--open) .tnl-right-body { + visibility: hidden; + pointer-events: none; +} + /* 툴바 — 좌: 필터·액션 · 우: 목록/그리드·배치·삭제 */ .tnl-page--with-right .tnl-toolbar { display: flex;