매매 시그널 알림 우측패널 동작 수정
This commit is contained in:
+12
-4
@@ -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;
|
||||
|
||||
@@ -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 = () => (
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
|
||||
<polyline points="3 6 5 6 21 6" />
|
||||
@@ -106,6 +110,7 @@ export const TradeNotificationListPage: React.FC<Props> = ({
|
||||
const [summary, setSummary] = useState<PaperSummaryDto | null>(null);
|
||||
const orderAnchorRef = useRef<HTMLDivElement>(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<Props> = ({
|
||||
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 (
|
||||
<div className={`tnl-page tnl-page--with-right bps-page--vtd se-page se-page--${theme} app ${theme}`}>
|
||||
<div
|
||||
className={[
|
||||
'tnl-page',
|
||||
'tnl-page--with-right',
|
||||
'bps-page--vtd',
|
||||
'se-page',
|
||||
`se-page--${theme}`,
|
||||
'app',
|
||||
theme,
|
||||
rightOpen ? '' : 'tnl-page--right-collapsed',
|
||||
].filter(Boolean).join(' ')}
|
||||
>
|
||||
<div className="tnl-body">
|
||||
<div className="tnl-main" style={{ containerType: 'inline-size', containerName: 'tnl-main' }}>
|
||||
<div className="tnl-header">
|
||||
@@ -375,7 +399,38 @@ export const TradeNotificationListPage: React.FC<Props> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside className="tnl-right" aria-label="매매·호가">
|
||||
<div className={`tnl-side-wrap tnl-side-wrap--right${rightOpen ? ' tnl-side-wrap--open' : ''}`}>
|
||||
<button
|
||||
type="button"
|
||||
className={`bps-panel-handle bps-panel-handle--right${rightOpen ? ' bps-panel-handle--open' : ''}`}
|
||||
onClick={toggleRightPanel}
|
||||
title={rightOpen ? '매매·호가 패널 닫기' : '매매·호가 패널 열기'}
|
||||
aria-expanded={rightOpen}
|
||||
aria-label={rightOpen ? '매매·호가 패널 닫기' : '매매·호가 패널 열기'}
|
||||
>
|
||||
<svg
|
||||
width="8"
|
||||
height="14"
|
||||
viewBox="0 0 8 14"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.8"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden
|
||||
>
|
||||
{rightOpen ? (
|
||||
<polyline points="2,1 6,7 2,13" />
|
||||
) : (
|
||||
<polyline points="6,1 2,7 6,13" />
|
||||
)}
|
||||
</svg>
|
||||
</button>
|
||||
<aside
|
||||
className={`tnl-right${rightOpen ? ' tnl-right--open' : ''}`}
|
||||
aria-label="매매·호가"
|
||||
aria-hidden={!rightOpen}
|
||||
>
|
||||
<div className="bps-right-tabs tnl-right-tabs">
|
||||
<button
|
||||
type="button"
|
||||
@@ -462,6 +517,7 @@ export const TradeNotificationListPage: React.FC<Props> = ({
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -135,6 +135,8 @@ interface TradingChartProps {
|
||||
paneSeparatorOptions?: ChartPaneSeparatorOptions;
|
||||
/** true: pane 합산 높이로 chart-container 를 키우지 않고 래퍼 높이에 맞춤 (미니 차트·알림 목록) */
|
||||
paneLayoutClamp?: boolean;
|
||||
/** 보조지표 pane 레전드(이름·값 오버레이) 표시 (기본 true, 알림 미니차트는 false) */
|
||||
showPaneLegend?: boolean;
|
||||
}
|
||||
|
||||
const TradingChart: React.FC<TradingChartProps> = ({
|
||||
@@ -167,6 +169,7 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
chartVisible = true,
|
||||
paneSeparatorOptions,
|
||||
paneLayoutClamp = false,
|
||||
showPaneLegend = true,
|
||||
}) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const wrapperRef = useRef<HTMLDivElement>(null); // 스크롤 래퍼
|
||||
@@ -1396,7 +1399,7 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
onRestore={() => setCandleOnlyMode(false)}
|
||||
/>
|
||||
)}
|
||||
{chartMgr && !candleOnlyMode && (
|
||||
{chartMgr && !candleOnlyMode && showPaneLegend && (
|
||||
<PaneLegendPortal
|
||||
manager={chartMgr}
|
||||
indicators={indicators}
|
||||
|
||||
@@ -22,12 +22,17 @@ export interface TradeNotificationHScrollPaneProps {
|
||||
/** 스크린리더용 영역 설명 */
|
||||
label: string;
|
||||
children: React.ReactNode;
|
||||
/** 스크롤 시 뷰포트에 완전히 보이지 않는 차트 카드 라벨 숨김 */
|
||||
clipPeekLabels?: boolean;
|
||||
}
|
||||
|
||||
const PEEK_LABEL_THRESHOLD = 0.99;
|
||||
|
||||
const TradeNotificationHScrollPane: React.FC<TradeNotificationHScrollPaneProps> = ({
|
||||
className = '',
|
||||
label,
|
||||
children,
|
||||
clipPeekLabels = false,
|
||||
}) => {
|
||||
const viewportRef = useRef<HTMLDivElement>(null);
|
||||
const [canScroll, setCanScroll] = useState(false);
|
||||
@@ -46,6 +51,31 @@ const TradeNotificationHScrollPane: React.FC<TradeNotificationHScrollPaneProps>
|
||||
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<HTMLElement>('.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<TradeNotificationHScrollPaneProps>
|
||||
};
|
||||
}, [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;
|
||||
|
||||
@@ -399,6 +399,7 @@ const TradeNotificationListRow: React.FC<Props> = ({
|
||||
indicatorScrollNeeded ? 'tnl-row-gallery-indicators--scroll' : '',
|
||||
].filter(Boolean).join(' ')}
|
||||
label="보조지표"
|
||||
clipPeekLabels={indicatorScrollNeeded}
|
||||
>
|
||||
<div className="tnl-charts-track tnl-charts-track--indicators-only">
|
||||
{indicatorCards.map(card => (
|
||||
|
||||
@@ -225,6 +225,7 @@ const TradeSignalMiniChart: React.FC<Props> = ({
|
||||
showHoverToolbar={false}
|
||||
seriesPriceLabelsEnabled={false}
|
||||
paneLayoutClamp
|
||||
showPaneLegend={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user