위젯 종료버튼 겹침문제 수정
This commit is contained in:
@@ -102,7 +102,11 @@ export const TopMenuBarNavGroups = memo(function TopMenuBarNavGroups({
|
||||
className={`tmb-nav-btn tmb-nav-group-btn${isActive ? ' tmb-nav-btn--active' : ''}${isOpen ? ' tmb-nav-group-btn--open' : ''}`}
|
||||
aria-expanded={isOpen}
|
||||
aria-haspopup="menu"
|
||||
onClick={() => setOpenGroupId(prev => (prev === group.id ? null : group.id))}
|
||||
onPointerDown={e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setOpenGroupId(prev => (prev === group.id ? null : group.id));
|
||||
}}
|
||||
>
|
||||
<span className="tmb-nav-icon">{group.icon}</span>
|
||||
<span className="tmb-nav-label">{group.label}</span>
|
||||
|
||||
@@ -37,6 +37,8 @@ import {
|
||||
ensurePaperChartOverlays,
|
||||
} from '../../utils/strategyToChartIndicators';
|
||||
import type { ChartOverlayToggleKey } from '../../utils/chartOverlayVisibility';
|
||||
import { WIDGET_CHART_TF_OPTIONS } from '../../utils/chartWidgetTimeframes';
|
||||
import WidgetTimeframePicker from '../../widgets/panels/WidgetTimeframePicker';
|
||||
|
||||
interface Props {
|
||||
symbol: string;
|
||||
@@ -61,6 +63,9 @@ interface Props {
|
||||
focusTimeSec?: number | null;
|
||||
/** 하단 확대·축소·좌우 이동 툴바 (위젯 등에서는 false) */
|
||||
showHoverToolbar?: boolean;
|
||||
/** 종목 우측 시간봉 선택 (위젯 전략 차트) */
|
||||
timeframeOptions?: Timeframe[];
|
||||
onTimeframeChange?: (tf: Timeframe) => void;
|
||||
}
|
||||
|
||||
const noop = () => {};
|
||||
@@ -117,6 +122,8 @@ const BacktestAnalysisChart: React.FC<Props> = ({
|
||||
reportDisabled = false,
|
||||
focusTimeSec = null,
|
||||
showHoverToolbar = true,
|
||||
timeframeOptions,
|
||||
onTimeframeChange,
|
||||
}) => {
|
||||
const { getParams, getVisualConfig } = useIndicatorSettings();
|
||||
const { defaults: appDefaults } = useAppSettings();
|
||||
@@ -414,7 +421,16 @@ const BacktestAnalysisChart: React.FC<Props> = ({
|
||||
<div className="btd-analysis-toolbar">
|
||||
<div className="btd-analysis-toolbar-left">
|
||||
<span className="btd-analysis-select btd-analysis-select--static">{koName}</span>
|
||||
<span className="btd-analysis-select btd-analysis-select--static">{tfKo}</span>
|
||||
{onTimeframeChange ? (
|
||||
<WidgetTimeframePicker
|
||||
options={timeframeOptions ?? WIDGET_CHART_TF_OPTIONS}
|
||||
value={chartTimeframe}
|
||||
onChange={onTimeframeChange}
|
||||
className="btd-analysis-tf-row"
|
||||
/>
|
||||
) : (
|
||||
<span className="btd-analysis-select btd-analysis-select--static">{tfKo}</span>
|
||||
)}
|
||||
<span className="btd-analysis-select btd-analysis-select--static">{chartTypeKo}</span>
|
||||
</div>
|
||||
<div className="btd-analysis-toolbar-right">
|
||||
|
||||
@@ -9,9 +9,10 @@ import {
|
||||
} from '../../utils/backendApi';
|
||||
import { loadAnalysisCandles } from '../../utils/analysisChartData';
|
||||
import { normalizeChartTimeframe } from '../../utils/backtestUiUtils';
|
||||
import { mergeWidgetTimeframeOptions } from '../../utils/chartWidgetTimeframes';
|
||||
import { resolveStrategyPrimaryTimeframe } from '../../utils/strategyToChartIndicators';
|
||||
import { resolveEvaluationFromLoadedBars } from '../../utils/backtestWarmup';
|
||||
import type { OHLCVBar, Theme } from '../../types';
|
||||
import type { OHLCVBar, Theme, Timeframe } from '../../types';
|
||||
import { useIndicatorSettings } from '../../hooks/useIndicatorSettings';
|
||||
import {
|
||||
DEFAULT_PAPER_OVERLAY_VISIBILITY,
|
||||
@@ -28,6 +29,8 @@ interface Props {
|
||||
theme?: Theme;
|
||||
/** 위젯 임베드 시 하단 줌/패닝 툴바 숨김 */
|
||||
showHoverToolbar?: boolean;
|
||||
/** 위젯 — 종목 우측 시간봉 선택 */
|
||||
enableTimeframePicker?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -35,12 +38,16 @@ interface Props {
|
||||
* 좌측(과거) 스크롤 시 추가 캔들·보조지표 로드 후 전체 구간 시그널 재계산.
|
||||
*/
|
||||
const PaperAnalysisChart: React.FC<Props> = ({
|
||||
market, strategyId, theme = 'dark', showHoverToolbar = true,
|
||||
market,
|
||||
strategyId,
|
||||
theme = 'dark',
|
||||
showHoverToolbar = true,
|
||||
enableTimeframePicker = false,
|
||||
}) => {
|
||||
const { getParams } = useIndicatorSettings();
|
||||
const [signals, setSignals] = useState<BacktestSignal[]>([]);
|
||||
const [strategy, setStrategy] = useState<StrategyDto | undefined>();
|
||||
const [timeframe, setTimeframe] = useState('1h');
|
||||
const [timeframe, setTimeframe] = useState<Timeframe>('1h');
|
||||
const [running, setRunning] = useState(false);
|
||||
const [historyLoading, setHistoryLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -53,8 +60,15 @@ const PaperAnalysisChart: React.FC<Props> = ({
|
||||
const historyDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const strategyRef = useRef(strategy);
|
||||
const strategyIdRef = useRef(strategyId);
|
||||
const timeframeRef = useRef(timeframe);
|
||||
strategyRef.current = strategy;
|
||||
strategyIdRef.current = strategyId;
|
||||
timeframeRef.current = timeframe;
|
||||
|
||||
const timeframeOptions = useMemo(
|
||||
() => mergeWidgetTimeframeOptions(strategy),
|
||||
[strategy],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!strategyId) {
|
||||
@@ -71,6 +85,11 @@ const PaperAnalysisChart: React.FC<Props> = ({
|
||||
return () => { cancelled = true; };
|
||||
}, [strategyId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!strategy) return;
|
||||
setTimeframe(normalizeChartTimeframe(resolveStrategyPrimaryTimeframe(strategy)));
|
||||
}, [strategyId, strategy?.id]);
|
||||
|
||||
const runSignalsForBars = useCallback(async (
|
||||
bars: OHLCVBar[],
|
||||
tf: string,
|
||||
@@ -139,8 +158,7 @@ const PaperAnalysisChart: React.FC<Props> = ({
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
const tf = normalizeChartTimeframe(resolveStrategyPrimaryTimeframe(strategy));
|
||||
setTimeframe(tf);
|
||||
const tf = normalizeChartTimeframe(timeframe);
|
||||
setError(null);
|
||||
|
||||
void (async () => {
|
||||
@@ -162,7 +180,7 @@ const PaperAnalysisChart: React.FC<Props> = ({
|
||||
cancelled = true;
|
||||
++backtestGenRef.current;
|
||||
};
|
||||
}, [market, strategyId, strategy, toTimeSec, runSignalsForBars]);
|
||||
}, [market, strategyId, strategy, timeframe, toTimeSec, runSignalsForBars]);
|
||||
|
||||
const handleToggleOverlay = useCallback((key: PaperOverlayToggleKey) => {
|
||||
setOverlayVisibility(prev => ({ ...prev, [key]: !prev[key] }));
|
||||
@@ -172,7 +190,7 @@ const PaperAnalysisChart: React.FC<Props> = ({
|
||||
const strat = strategyRef.current;
|
||||
const sid = strategyIdRef.current;
|
||||
if (!strat || !sid) return;
|
||||
const tf = normalizeChartTimeframe(resolveStrategyPrimaryTimeframe(strat));
|
||||
const tf = normalizeChartTimeframe(timeframeRef.current);
|
||||
if (historyDebounceRef.current) clearTimeout(historyDebounceRef.current);
|
||||
historyDebounceRef.current = setTimeout(() => {
|
||||
historyDebounceRef.current = null;
|
||||
@@ -180,6 +198,10 @@ const PaperAnalysisChart: React.FC<Props> = ({
|
||||
}, HISTORY_BACKTEST_DEBOUNCE_MS);
|
||||
}, [runSignalsForBars]);
|
||||
|
||||
const handleTimeframeChange = useCallback((tf: Timeframe) => {
|
||||
setTimeframe(tf);
|
||||
}, []);
|
||||
|
||||
useEffect(() => () => {
|
||||
if (historyDebounceRef.current) clearTimeout(historyDebounceRef.current);
|
||||
}, []);
|
||||
@@ -195,6 +217,7 @@ const PaperAnalysisChart: React.FC<Props> = ({
|
||||
}
|
||||
|
||||
const statusBusy = running || historyLoading;
|
||||
const chartTimeframe = normalizeChartTimeframe(timeframe);
|
||||
|
||||
return (
|
||||
<div className="ptd-analysis-chart ptd-analysis-chart--backtest">
|
||||
@@ -208,7 +231,7 @@ const PaperAnalysisChart: React.FC<Props> = ({
|
||||
)}
|
||||
<BacktestAnalysisChart
|
||||
symbol={market}
|
||||
timeframe={timeframe}
|
||||
timeframe={chartTimeframe}
|
||||
toTimeSec={toTimeSec}
|
||||
barCount={BAR_COUNT}
|
||||
signals={signals}
|
||||
@@ -219,6 +242,8 @@ const PaperAnalysisChart: React.FC<Props> = ({
|
||||
overlayVisibility={overlayVisibility}
|
||||
onToggleOverlay={handleToggleOverlay}
|
||||
showHoverToolbar={showHoverToolbar}
|
||||
timeframeOptions={enableTimeframePicker ? timeframeOptions : undefined}
|
||||
onTimeframeChange={enableTimeframePicker ? handleTimeframeChange : undefined}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,8 +2,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { createChart, CandlestickSeries, type IChartApi, type ISeriesApi, type Time, ColorType } from 'lightweight-charts';
|
||||
import type { Theme, Timeframe } from '../../types';
|
||||
import { fetchUpbitCandles } from '../../utils/upbitApi';
|
||||
|
||||
const TF_OPTIONS: Timeframe[] = ['1m', '5m', '10m', '15m', '1h', '4h', '1D'];
|
||||
import { WIDGET_CHART_TF_OPTIONS } from '../../utils/chartWidgetTimeframes';
|
||||
|
||||
interface Props {
|
||||
market: string;
|
||||
@@ -11,6 +10,10 @@ interface Props {
|
||||
wsLabel?: string;
|
||||
timeframe?: Timeframe;
|
||||
onTimeframeChange?: (tf: Timeframe) => void;
|
||||
/** 위젯 헤더에 시간봉이 있을 때 내부 행 숨김 */
|
||||
hideTimeframeRow?: boolean;
|
||||
/** 위젯 임베드 시 상단 타이틀·배지 숨김 */
|
||||
hideChartHead?: boolean;
|
||||
}
|
||||
|
||||
function readChartTheme(container: HTMLElement | null) {
|
||||
@@ -33,6 +36,8 @@ const PaperMiniChart: React.FC<Props> = ({
|
||||
wsLabel = 'WebSocket <100ms',
|
||||
timeframe: timeframeProp,
|
||||
onTimeframeChange,
|
||||
hideTimeframeRow = false,
|
||||
hideChartHead = false,
|
||||
}) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const chartRef = useRef<IChartApi | null>(null);
|
||||
@@ -159,24 +164,28 @@ const PaperMiniChart: React.FC<Props> = ({
|
||||
|
||||
return (
|
||||
<div className="ptd-chart-wrap">
|
||||
<div className="ptd-chart-head">
|
||||
<span className="ptd-chart-title">실시간 분석 차트</span>
|
||||
<span className="ptd-ws-badge">{wsLabel} 실시간 데이터 반영</span>
|
||||
</div>
|
||||
{!hideChartHead && (
|
||||
<div className="ptd-chart-head">
|
||||
<span className="ptd-chart-title">실시간 분석 차트</span>
|
||||
<span className="ptd-ws-badge">{wsLabel} 실시간 데이터 반영</span>
|
||||
</div>
|
||||
)}
|
||||
<div ref={containerRef} className="ptd-chart-canvas" />
|
||||
{loading && <div className="ptd-chart-loading">차트 로딩…</div>}
|
||||
<div className="ptd-tf-row">
|
||||
{TF_OPTIONS.map(tf => (
|
||||
<button
|
||||
key={tf}
|
||||
type="button"
|
||||
className={`ptd-tf-btn${timeframe === tf ? ' ptd-tf-btn--active' : ''}`}
|
||||
onClick={() => setTimeframe(tf)}
|
||||
>
|
||||
{tf}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{!hideTimeframeRow && (
|
||||
<div className="ptd-tf-row">
|
||||
{WIDGET_CHART_TF_OPTIONS.map(tf => (
|
||||
<button
|
||||
key={tf}
|
||||
type="button"
|
||||
className={`ptd-tf-btn${timeframe === tf ? ' ptd-tf-btn--active' : ''}`}
|
||||
onClick={() => setTimeframe(tf)}
|
||||
>
|
||||
{tf}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -47,7 +47,7 @@ const WidgetSlotView: React.FC<Props> = ({
|
||||
</div>
|
||||
</header>
|
||||
)}
|
||||
<div className="wd-slot-body">
|
||||
<div className={['wd-slot-body', Host && onClearWidget ? 'wd-slot-body--has-clear' : ''].filter(Boolean).join(' ')}>
|
||||
{Host ? (
|
||||
<Host config={slot.config} />
|
||||
) : (
|
||||
|
||||
@@ -1507,6 +1507,12 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.btd-analysis-toolbar-right {
|
||||
flex-shrink: 1;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.btd-analysis-select--static {
|
||||
@@ -1519,6 +1525,14 @@
|
||||
background: color-mix(in srgb, var(--btd-bg) 60%, transparent);
|
||||
}
|
||||
|
||||
.btd-analysis-tf-row {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
flex-wrap: nowrap;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.btd-analysis-tool {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
|
||||
@@ -164,16 +164,53 @@
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
z-index: 3;
|
||||
z-index: 10;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border: 1px solid var(--wd-slot-border);
|
||||
border-radius: 4px;
|
||||
background: color-mix(in srgb, var(--bg2) 88%, transparent);
|
||||
background: color-mix(in srgb, var(--bg2) 92%, transparent);
|
||||
color: var(--se-text-muted);
|
||||
font-size: 0.65rem;
|
||||
cursor: pointer;
|
||||
opacity: 0.55;
|
||||
opacity: 0.85;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
/* 위젯 ✕ 버튼 — 차트·패널 헤더 텍스트와 겹침 방지 */
|
||||
.wd-slot-body--has-clear .wd-candle-chart-header,
|
||||
.wd-slot-body--has-clear .btd-analysis-toolbar,
|
||||
.wd-slot-body--has-clear .wd-panel-header,
|
||||
.wd-slot-body--has-clear .wd-chart-symbol-tf-bar {
|
||||
padding-right: 36px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.wd-slot-body--has-clear .btd-analysis-toolbar-left {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wd-slot-body--has-clear .btd-analysis-toolbar-right {
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
max-width: 42%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wd-slot-body--has-clear .btd-analysis-count {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.wd-slot-body--has-clear .wd-candle-chart-header-actions {
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
max-width: min(100%, 220px);
|
||||
}
|
||||
|
||||
.wd-slot-overlay-clear:hover {
|
||||
@@ -447,6 +484,33 @@
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wd-chart-symbol-tf-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
padding: 4px 10px 6px;
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--se-border, var(--border)) 55%, transparent);
|
||||
}
|
||||
|
||||
.wd-chart-symbol-label {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
color: var(--se-text, var(--text));
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: min(220px, 42%);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.wd-chart-symbol-tf-bar .wd-candle-chart-tf-row {
|
||||
flex: 1;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.wd-candle-chart-tf {
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { Timeframe } from '../types';
|
||||
import type { StrategyDto } from './backendApi';
|
||||
import { resolveStrategyTimeframes } from './strategyToChartIndicators';
|
||||
|
||||
/** 위젯 차트 — 종목 우측 시간봉 선택 기본 옵션 */
|
||||
export const WIDGET_CHART_TF_OPTIONS: Timeframe[] = [
|
||||
'1m', '3m', '5m', '10m', '15m', '30m', '1h', '4h', '1D',
|
||||
];
|
||||
|
||||
const TF_ORDER = new Map<Timeframe, number>(
|
||||
WIDGET_CHART_TF_OPTIONS.map((tf, i) => [tf, i]),
|
||||
);
|
||||
|
||||
/** 전략 DSL 시간봉 + 위젯 기본 옵션 병합 (중복 제거, 표준 순서) */
|
||||
export function mergeWidgetTimeframeOptions(strategy?: StrategyDto): Timeframe[] {
|
||||
const merged = new Set<Timeframe>([...WIDGET_CHART_TF_OPTIONS, ...resolveStrategyTimeframes(strategy)]);
|
||||
return [...merged].sort((a, b) => {
|
||||
const ai = TF_ORDER.get(a) ?? 999;
|
||||
const bi = TF_ORDER.get(b) ?? 999;
|
||||
if (ai !== bi) return ai - bi;
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
}
|
||||
@@ -22,8 +22,9 @@ import type { ChartManager } from '../utils/ChartManager';
|
||||
import {
|
||||
buildVirtualTradingChartIndicators,
|
||||
resolveStrategyPrimaryTimeframe,
|
||||
resolveStrategyTimeframes,
|
||||
} from '../utils/strategyToChartIndicators';
|
||||
import { mergeWidgetTimeframeOptions } from '../utils/chartWidgetTimeframes';
|
||||
import WidgetTimeframePicker from './panels/WidgetTimeframePicker';
|
||||
import { timeframeToCandleType } from '../utils/chartCandleType';
|
||||
import { DISPLAY_COUNT } from '../hooks/useUpbitData';
|
||||
import { getKoreanName } from '../utils/marketNameCache';
|
||||
@@ -76,7 +77,7 @@ const WidgetCandleChartPanel: React.FC<Props> = ({
|
||||
chartRealtimeSource = 'BACKEND_STOMP',
|
||||
}) => {
|
||||
const { getParams, getVisualConfig } = useIndicatorSettings();
|
||||
const tfOptions = useMemo(() => resolveStrategyTimeframes(strategy), [strategy]);
|
||||
const tfOptions = useMemo(() => mergeWidgetTimeframeOptions(strategy), [strategy]);
|
||||
const [timeframe, setTimeframe] = useState<Timeframe>(
|
||||
() => resolveStrategyPrimaryTimeframe(strategy),
|
||||
);
|
||||
@@ -402,20 +403,11 @@ const WidgetCandleChartPanel: React.FC<Props> = ({
|
||||
<span className="wd-candle-chart-market-sym">{market}</span>
|
||||
</button>
|
||||
|
||||
{tfOptions.length > 1 && (
|
||||
<div className="wd-candle-chart-tf-row">
|
||||
{tfOptions.map(tf => (
|
||||
<button
|
||||
key={tf}
|
||||
type="button"
|
||||
className={`wd-candle-chart-tf${timeframe === tf ? ' wd-candle-chart-tf--on' : ''}`}
|
||||
onClick={() => setTimeframe(tf)}
|
||||
>
|
||||
{tf}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<WidgetTimeframePicker
|
||||
options={tfOptions}
|
||||
value={timeframe}
|
||||
onChange={setTimeframe}
|
||||
/>
|
||||
|
||||
<div className="wd-candle-chart-header-actions">
|
||||
<div className="wd-candle-chart-toolbar-toggles" role="group" aria-label="차트 툴바 표시">
|
||||
|
||||
@@ -1,19 +1,39 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import PaperMiniChart from '../../components/paper/PaperMiniChart';
|
||||
import { useWidgetDashboard } from '../WidgetDashboardContext';
|
||||
import WidgetPanelHeader from '../panels/WidgetPanelHeader';
|
||||
import WidgetTimeframePicker from '../panels/WidgetTimeframePicker';
|
||||
import { WIDGET_CHART_TF_OPTIONS } from '../../utils/chartWidgetTimeframes';
|
||||
import { resolveName } from '../widgetMarketUtils';
|
||||
import type { Timeframe } from '../../types';
|
||||
|
||||
const MiniChartWidgetHost: React.FC = () => {
|
||||
const { selectedMarket, theme, tickers } = useWidgetDashboard();
|
||||
const ticker = tickers.get(selectedMarket);
|
||||
const [timeframe, setTimeframe] = useState<Timeframe>('1h');
|
||||
|
||||
return (
|
||||
<div className="wd-widget-host wd-widget-host--panel wd-widget-host--mini-chart">
|
||||
<WidgetPanelHeader title="미니 차트" />
|
||||
<div className="wd-panel-subline">{resolveName(selectedMarket, ticker)} · {selectedMarket}</div>
|
||||
<div className="wd-chart-symbol-tf-bar">
|
||||
<span className="wd-chart-symbol-label">
|
||||
{resolveName(selectedMarket, ticker)} · {selectedMarket}
|
||||
</span>
|
||||
<WidgetTimeframePicker
|
||||
options={WIDGET_CHART_TF_OPTIONS}
|
||||
value={timeframe}
|
||||
onChange={setTimeframe}
|
||||
/>
|
||||
</div>
|
||||
<div className="wd-mini-chart-wrap">
|
||||
<PaperMiniChart market={selectedMarket} theme={theme} />
|
||||
<PaperMiniChart
|
||||
market={selectedMarket}
|
||||
theme={theme}
|
||||
timeframe={timeframe}
|
||||
onTimeframeChange={setTimeframe}
|
||||
hideChartHead
|
||||
hideTimeframeRow
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -23,6 +23,7 @@ const StrategyChartWidgetHost: React.FC<{ config?: Record<string, unknown> }> =
|
||||
strategyId={strategyId}
|
||||
theme={theme}
|
||||
showHoverToolbar={false}
|
||||
enableTimeframePicker
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import type { Timeframe } from '../../types';
|
||||
|
||||
interface Props {
|
||||
options: Timeframe[];
|
||||
value: Timeframe;
|
||||
onChange: (tf: Timeframe) => void;
|
||||
className?: string;
|
||||
'aria-label'?: string;
|
||||
}
|
||||
|
||||
/** 위젯 차트 — 종목 우측 시간봉 버튼 그룹 */
|
||||
const WidgetTimeframePicker: React.FC<Props> = ({
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
className = '',
|
||||
'aria-label': ariaLabel = '시간봉',
|
||||
}) => (
|
||||
<div
|
||||
className={`wd-candle-chart-tf-row${className ? ` ${className}` : ''}`}
|
||||
role="group"
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
{options.map(tf => (
|
||||
<button
|
||||
key={tf}
|
||||
type="button"
|
||||
className={`wd-candle-chart-tf${value === tf ? ' wd-candle-chart-tf--on' : ''}`}
|
||||
aria-pressed={value === tf}
|
||||
onClick={() => onChange(tf)}
|
||||
>
|
||||
{tf}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default WidgetTimeframePicker;
|
||||
Reference in New Issue
Block a user