Files
goldenChart/frontend/src/components/ChartRightToolbar.tsx
T
2026-06-17 02:01:05 +09:00

677 lines
24 KiB
TypeScript

/**
* 차트 우측 세로 툴바 — 캔들·보조지표 pane 과 높이 동기화.
* pane 하단: 드래그 핸들 + 펼침메뉴(분리·복사·전체보기·종료)
*/
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { createPortal } from 'react-dom';
import type { IndicatorConfig } from '../types';
import { ChartManager } from '../utils/ChartManager';
import { getPaneHostId } from '../utils/indicatorPaneMerge';
import { isIndicatorPaneFocused, resolveFocusedPaneLayoutId } from '../utils/indicatorPaneFocus';
import { buildPaneItems, resolvePaneItemLayout } from './paneLegendLayout';
import type { ChartOverlayToggleKey } from '../utils/chartOverlayVisibility';
export interface CandleOverlayToggleItem {
key: ChartOverlayToggleKey;
label: string;
visible: boolean;
}
export interface ChartRightToolbarProps {
manager: ChartManager;
indicators: IndicatorConfig[];
chartHeight: number;
paused?: boolean;
/** 캔들 pane 상단 — 이동평균·일목·볼린저 표시/숨김 (투자관리 차트 탭) */
candleOverlayToggles?: CandleOverlayToggleItem[];
onToggleCandleOverlay?: (key: ChartOverlayToggleKey) => void;
onSplit?: (hostId: string) => void;
onRemove?: (id: string) => void;
onDuplicate?: (id: string) => void;
onExpand?: (id: string) => void;
onRestore?: () => void;
focusedId?: string | null;
onDragStart?: (id: string, e: React.PointerEvent) => void;
/** 지표명 툴바 — 표시/숨기기 */
onToggleHidden?: (id: string) => void;
/** 지표명 툴바 — 설정 모달 */
onSettings?: (id: string) => void;
/** Custom / Custom1 오버레이 — 적용 여부 (동시 적용 불가, 설정은 슬롯별 분리) */
customOverlayActive?: boolean;
onCustomOverlayActiveChange?: (active: boolean) => void;
onOpenCustomOverlaySettings?: () => void;
custom1OverlayActive?: boolean;
onCustom1OverlayActiveChange?: (active: boolean) => void;
onOpenCustom1OverlaySettings?: () => void;
/** true — 캔들 pane 상단 오버레이 토글만 (미니 차트 등) */
overlayControlsOnly?: boolean;
/** 9·20일 신고가·신저가 라벨 표시 (실시간 차트) */
priceExtremeLabelsVisible?: boolean;
onTogglePriceExtremeLabels?: () => void;
}
const BTN_SIZE = 32;
const BTN_GAP = 2;
const IconCopy = () => (
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"
stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<rect x="4.5" y="1.5" width="8" height="8" rx="1"/>
<path d="M1.5 4.5v8h8"/>
</svg>
);
const IconSplit = () => (
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"
stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="1,7 5,3 5,11"/>
<polyline points="13,7 9,3 9,11"/>
<line x1="5" y1="7" x2="9" y2="7"/>
</svg>
);
const IconClose = () => (
<svg width="12" height="12" viewBox="0 0 11 11" fill="none"
stroke="currentColor" strokeWidth="2" strokeLinecap="round">
<line x1="1.5" y1="1.5" x2="9.5" y2="9.5"/>
<line x1="9.5" y1="1.5" x2="1.5" y2="9.5"/>
</svg>
);
const IconExpand = () => (
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"
stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="1,9 1,13 5,13"/>
<line x1="1" y1="13" x2="5.5" y2="8.5"/>
<polyline points="13,5 13,1 9,1"/>
<line x1="13" y1="1" x2="8.5" y2="5.5"/>
</svg>
);
const IconRestore = () => (
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"
stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="5,13 1,13 1,9"/>
<line x1="1" y1="13" x2="5.5" y2="8.5"/>
<polyline points="9,1 13,1 13,5"/>
<line x1="13" y1="1" x2="8.5" y2="5.5"/>
</svg>
);
const IconDrag = () => (
<svg width="10" height="14" viewBox="0 0 10 14" fill="currentColor">
<circle cx="2.5" cy="2.5" r="1.5"/>
<circle cx="7.5" cy="2.5" r="1.5"/>
<circle cx="2.5" cy="7" r="1.5"/>
<circle cx="7.5" cy="7" r="1.5"/>
<circle cx="2.5" cy="11.5" r="1.5"/>
<circle cx="7.5" cy="11.5" r="1.5"/>
</svg>
);
const IconMore = () => (
<svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor">
<circle cx="7" cy="3" r="1.3"/>
<circle cx="7" cy="7" r="1.3"/>
<circle cx="7" cy="11" r="1.3"/>
</svg>
);
const IconMa = () => (
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"
stroke="currentColor" strokeWidth="1.8" strokeLinecap="round">
<path d="M1 10 L5 4 L8 7 L13 2"/>
</svg>
);
const IconBollinger = () => (
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"
stroke="currentColor" strokeWidth="1.4" strokeLinecap="round">
<path d="M1 4 Q7 1 13 4"/>
<path d="M1 7 Q7 4 13 7"/>
<path d="M1 10 Q7 13 13 10"/>
</svg>
);
const IconCandle = () => (
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"
stroke="currentColor" strokeWidth="1.6" strokeLinecap="round">
<line x1="4" y1="2" x2="4" y2="12"/>
<rect x="2.5" y="5" width="3" height="4" rx="0.5" fill="currentColor" stroke="none"/>
<line x1="10" y1="3" x2="10" y2="11"/>
<rect x="8.5" y="4" width="3" height="5" rx="0.5" fill="currentColor" stroke="none"/>
</svg>
);
const IconIchimoku = () => (
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"
stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round">
<path d="M1 9 L7 5 L13 9" fill="rgba(239,83,80,0.25)"/>
<path d="M1 5 L7 9 L13 5" fill="rgba(38,166,154,0.25)"/>
<path d="M1 9 L7 5 L13 9"/>
<path d="M1 5 L7 9 L13 5"/>
</svg>
);
/** 신고가(상향)·신저가(하향) */
const IconPriceExtreme = () => (
<svg width="14" height="14" viewBox="0 0 14 14" aria-hidden>
<path d="M7 1.5 L10.5 6 H3.5 Z" fill="#4dabf7"/>
<path d="M7 12.5 L3.5 8 H10.5 Z" fill="#ef5350"/>
</svg>
);
const OVERLAY_ICONS: Record<ChartOverlayToggleKey, React.ReactNode> = {
ma: <IconMa />,
bollinger: <IconBollinger />,
ichimoku: <IconIchimoku />,
candle: <IconCandle />,
};
const IconEye = ({ hidden }: { hidden?: boolean }) => hidden ? (
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.5">
<path d="M1 7 Q4 2.5 7 2.5 Q10 2.5 13 7 Q10 11.5 7 11.5 Q4 11.5 1 7 Z" strokeOpacity="0.35"/>
<line x1="2" y1="2" x2="12" y2="12"/>
</svg>
) : (
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.5">
<path d="M1 7 Q4 2.5 7 2.5 Q10 2.5 13 7 Q10 11.5 7 11.5 Q4 11.5 1 7 Z"/>
<circle cx="7" cy="7" r="2"/>
</svg>
);
const CUSTOM_MENU_ID = '__custom_overlay__';
const CUSTOM1_MENU_ID = '__custom1_overlay__';
const IconCustom = () => (
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"
stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
<path d="M2 4h10M2 7h10M2 10h6"/>
<circle cx="10.5" cy="10" r="1.2" fill="currentColor" stroke="none"/>
</svg>
);
const IconCheck = () => (
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"
stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="2,7.5 5.5,11 12,3"/>
</svg>
);
const IconGear = () => (
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.4">
<circle cx="7" cy="7" r="2.3"/>
<path d="M7 1 L7.7 2.8 L9.8 2 L9.8 4.2 L11.8 5 L11 7 L11.8 9 L9.8 9.8 L9.8 12 L7.7 11.2 L7 13 L6.3 11.2 L4.2 12 L4.2 9.8 L2.2 9 L3 7 L2.2 5 L4.2 4.2 L4.2 2 L6.3 2.8 Z"/>
</svg>
);
const CustomOverlayFlyoutMenu: React.FC<{
title: string;
flyoutPos: { top: number; left: number };
overlayActive: boolean;
onToggleActive: () => void;
onOpenSettings: () => void;
onCancelClose: () => void;
onScheduleClose: () => void;
onCloseFlyout: () => void;
}> = ({
title,
flyoutPos,
overlayActive,
onToggleActive,
onOpenSettings,
onCancelClose,
onScheduleClose,
onCloseFlyout,
}) => createPortal(
<div
className="tv-side-flyout tv-side-flyout--right-anchor tv-side-flyout--custom-overlay"
style={{ top: flyoutPos.top, left: flyoutPos.left }}
onMouseEnter={onCancelClose}
onMouseLeave={onScheduleClose}
>
<div className="tv-side-flyout-header">{title}</div>
<button
type="button"
className={`tv-side-flyout-item${overlayActive ? ' active' : ''}`}
onClick={e => {
e.stopPropagation();
onToggleActive();
onCloseFlyout();
}}
>
<span className="tv-side-flyout-icon"><IconCheck /></span>
<span className="tv-side-flyout-title">
{overlayActive ? '미적용' : '적용'}
</span>
</button>
<button
type="button"
className="tv-side-flyout-item"
onClick={e => {
e.stopPropagation();
onOpenSettings();
onCloseFlyout();
}}
>
<span className="tv-side-flyout-icon"><IconGear /></span>
<span className="tv-side-flyout-title">설정</span>
</button>
</div>,
document.body,
);
interface MenuItemDef {
key: string;
title: string;
icon: React.ReactNode;
active?: boolean;
danger?: boolean;
onClick: () => void;
}
function buildMenuItems(
item: ReturnType<typeof buildPaneItems>[number],
indicators: IndicatorConfig[],
paneItems: ReturnType<typeof buildPaneItems>,
opts: {
focusedId?: string | null;
onSplit?: (hostId: string) => void;
onDuplicate?: (id: string) => void;
onExpand?: (id: string) => void;
onRestore?: () => void;
onRemove?: (id: string) => void;
onToggleHidden?: (id: string) => void;
onSettings?: (id: string) => void;
onDone?: () => void;
},
): MenuItemDef[] {
const samePaneItems = paneItems.filter(p => p.paneIndex === item.paneIndex);
const itemHostId = getPaneHostId(item.id, indicators);
const isMergedPane = samePaneItems.length > 1;
const showSplit = isMergedPane && item.id === itemHostId && !!opts.onSplit;
const isFocused = isIndicatorPaneFocused(item.id, opts.focusedId, indicators);
const showExpand = !!(opts.onExpand || (opts.focusedId && opts.onRestore));
const done = opts.onDone ?? (() => {});
const cfg = indicators.find(ind => ind.id === item.id);
const isHidden = cfg?.hidden === true;
const items: MenuItemDef[] = [];
if (opts.onToggleHidden) {
items.push({
key: 'visibility',
title: isHidden ? '표시' : '숨기기',
icon: <IconEye hidden={isHidden} />,
active: isHidden,
onClick: () => { opts.onToggleHidden?.(item.id); done(); },
});
}
if (opts.onSettings) {
items.push({
key: 'settings',
title: '지표 설정',
icon: <IconGear />,
onClick: () => { opts.onSettings?.(item.id); done(); },
});
}
if (showSplit) {
items.push({
key: 'split',
title: '보조지표 분리',
icon: <IconSplit />,
onClick: () => { opts.onSplit?.(itemHostId); done(); },
});
}
if (opts.onDuplicate) {
items.push({
key: 'copy',
title: '보조지표 복사',
icon: <IconCopy />,
onClick: () => { opts.onDuplicate?.(item.id); done(); },
});
}
if (showExpand) {
items.push({
key: 'expand',
title: isFocused ? '전체 보기로 복원' : '이 지표만 보기',
icon: isFocused ? <IconRestore /> : <IconExpand />,
active: isFocused,
onClick: () => {
if (isFocused) opts.onRestore?.();
else opts.onExpand?.(item.id);
done();
},
});
}
if (opts.onRemove) {
items.push({
key: 'remove',
title: '지표 제거',
icon: <IconClose />,
danger: true,
onClick: () => { opts.onRemove?.(item.id); done(); },
});
}
return items;
}
const ChartRightToolbar: React.FC<ChartRightToolbarProps> = ({
manager, indicators, chartHeight, paused = false,
candleOverlayToggles,
onToggleCandleOverlay,
customOverlayActive = false,
onCustomOverlayActiveChange,
onOpenCustomOverlaySettings,
custom1OverlayActive = false,
onCustom1OverlayActiveChange,
onOpenCustom1OverlaySettings,
onSplit, onRemove, onDuplicate,
onExpand, onRestore, focusedId,
onDragStart, onToggleHidden, onSettings,
overlayControlsOnly = false,
priceExtremeLabelsVisible = false,
onTogglePriceExtremeLabels,
}) => {
const [paneLayouts, setPaneLayouts] = useState<Array<{ paneIndex: number; topY: number; height: number }>>([]);
const [paneItems, setPaneItems] = useState<ReturnType<typeof buildPaneItems>>([]);
const [openMenuId, setOpenMenuId] = useState<string | null>(null);
const [flyoutPos, setFlyoutPos] = useState({ top: 0, left: 0 });
const indicatorsRef = useRef(indicators);
const openTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const sync = useCallback(() => {
setPaneLayouts(manager.getPaneLayouts());
setPaneItems(buildPaneItems(manager, indicatorsRef.current));
}, [manager]);
useEffect(() => { indicatorsRef.current = indicators; }, [indicators]);
useEffect(() => {
if (paused) {
setPaneItems([]);
return;
}
sync();
const unsub = manager.subscribePaneLayout(() => sync());
return unsub;
}, [manager, paused, sync, indicators]);
useEffect(() => () => {
if (openTimer.current) clearTimeout(openTimer.current);
if (closeTimer.current) clearTimeout(closeTimer.current);
}, []);
const closeFlyout = useCallback(() => {
setOpenMenuId(null);
if (openTimer.current) { clearTimeout(openTimer.current); openTimer.current = null; }
if (closeTimer.current) { clearTimeout(closeTimer.current); closeTimer.current = null; }
}, []);
const openFlyout = useCallback((itemId: string, el: HTMLElement) => {
if (closeTimer.current) { clearTimeout(closeTimer.current); closeTimer.current = null; }
if (openTimer.current) clearTimeout(openTimer.current);
openTimer.current = setTimeout(() => {
const rect = el.getBoundingClientRect();
setFlyoutPos({ top: rect.top, left: rect.left - 4 });
setOpenMenuId(itemId);
}, 80);
}, []);
const scheduleClose = useCallback(() => {
if (openTimer.current) { clearTimeout(openTimer.current); openTimer.current = null; }
closeTimer.current = setTimeout(() => setOpenMenuId(null), 200);
}, []);
const cancelClose = useCallback(() => {
if (closeTimer.current) { clearTimeout(closeTimer.current); closeTimer.current = null; }
}, []);
const candlePane = paneLayouts.find(l => l.paneIndex === 0);
const showOverlayIcons = !!(candleOverlayToggles?.length && candlePane && onToggleCandleOverlay);
const showPriceExtremeBtn = !!(candlePane && onTogglePriceExtremeLabels);
const showOverlayCluster = showOverlayIcons || showPriceExtremeBtn;
if (paused || chartHeight <= 0) return null;
if (overlayControlsOnly && !showOverlayCluster) return null;
const innerHeight = paneLayouts.reduce(
(max, l) => Math.max(max, l.topY + l.height),
chartHeight,
);
const allowDrag = !!(onDragStart && paneItems.length > 1);
const clusterHeight = (allowDrag ? BTN_SIZE + BTN_GAP : 0) + BTN_SIZE;
const openItem = openMenuId
? paneItems.find(p => p.id === openMenuId)
: null;
const showCustomBtn = !!(showOverlayIcons && onCustomOverlayActiveChange && onOpenCustomOverlaySettings);
const showCustom1Btn = !!(showOverlayIcons && onCustom1OverlayActiveChange && onOpenCustomOverlaySettings);
const customMenuOpen = openMenuId === CUSTOM_MENU_ID;
const custom1MenuOpen = openMenuId === CUSTOM1_MENU_ID;
const focusedLayoutId = focusedId
? resolveFocusedPaneLayoutId(focusedId, indicators)
: null;
const focusedPaneLayout = focusedLayoutId
? (() => {
const item = paneItems.find(p => isIndicatorPaneFocused(p.id, focusedId, indicators));
if (item) return resolvePaneItemLayout(manager, item);
return manager.getIndicatorPaneScreenLayout(focusedLayoutId);
})()
: null;
return (
<aside
className={`chart-right-toolbar${overlayControlsOnly ? ' chart-right-toolbar--overlay-only' : ''}`}
aria-label="차트 우측 도구"
>
<div
className="chart-right-toolbar-inner"
style={{ height: Math.max(innerHeight, chartHeight) }}
>
{focusedId && onRestore && focusedPaneLayout && (
<div
className="chart-right-toolbar-cluster chart-right-toolbar-cluster--focus-restore"
style={{ top: focusedPaneLayout.topY + 4 }}
>
<button
type="button"
className="tv-side-btn chart-right-toolbar-btn chart-right-toolbar-btn--restore active"
title="기본 보기로 복원 (모든 보조지표 표시)"
onClick={e => { e.stopPropagation(); onRestore(); }}
>
<IconRestore />
</button>
</div>
)}
{showOverlayCluster && (
<div
className="chart-right-toolbar-cluster chart-right-toolbar-cluster--overlay-icons"
style={{ top: candlePane!.topY + 4 }}
>
{candleOverlayToggles?.map((item, idx) => (
<button
key={item.key}
type="button"
className={`tv-side-btn chart-right-toolbar-btn chart-right-toolbar-btn--overlay${item.visible ? '' : ' chart-right-toolbar-btn--overlay-off'}`}
title={item.visible ? `${item.label} 숨기기` : `${item.label} 표시`}
style={{ marginTop: idx > 0 ? BTN_GAP : 0 }}
onClick={e => {
e.stopPropagation();
onToggleCandleOverlay?.(item.key);
}}
>
{OVERLAY_ICONS[item.key]}
</button>
))}
{showPriceExtremeBtn && (
<button
type="button"
className={`tv-side-btn chart-right-toolbar-btn chart-right-toolbar-btn--overlay chart-right-toolbar-btn--price-extreme${priceExtremeLabelsVisible ? '' : ' chart-right-toolbar-btn--overlay-off'}`}
title={priceExtremeLabelsVisible ? '9·20일 신고가·신저가 숨기기' : '9·20일 신고가·신저가 표시'}
style={{ marginTop: (candleOverlayToggles?.length ?? 0) > 0 ? BTN_GAP : 0 }}
onClick={e => {
e.stopPropagation();
onTogglePriceExtremeLabels?.();
}}
>
<IconPriceExtreme />
</button>
)}
{showCustomBtn && (
<button
type="button"
className={`tv-side-btn tv-side-group-btn chart-right-toolbar-btn chart-right-toolbar-btn--custom${customOverlayActive ? ' active' : ''}${customMenuOpen ? ' active' : ''}`}
title="Custom 표시"
style={{ marginTop: BTN_GAP }}
onMouseEnter={e => openFlyout(CUSTOM_MENU_ID, e.currentTarget)}
onMouseLeave={scheduleClose}
>
<IconCustom />
<span className="tv-side-expand-arrow" />
</button>
)}
{showCustom1Btn && (
<button
type="button"
className={`tv-side-btn tv-side-group-btn chart-right-toolbar-btn chart-right-toolbar-btn--custom1${custom1OverlayActive ? ' active' : ''}${custom1MenuOpen ? ' active' : ''}`}
title="Custom1 표시"
style={{ marginTop: BTN_GAP }}
onMouseEnter={e => openFlyout(CUSTOM1_MENU_ID, e.currentTarget)}
onMouseLeave={scheduleClose}
>
<IconCustom />
<span className="chart-right-toolbar-custom1-badge">1</span>
<span className="tv-side-expand-arrow" />
</button>
)}
</div>
)}
{!overlayControlsOnly && paneLayouts.map(layout => (
<div
key={layout.paneIndex}
className="chart-right-toolbar-segment"
style={{ top: layout.topY, height: layout.height }}
/>
))}
{!overlayControlsOnly && paneItems.map(item => {
const pl = resolvePaneItemLayout(manager, item);
if (!pl) return null;
const samePaneItems = paneItems.filter(p => p.paneIndex === item.paneIndex);
const stackIdx = samePaneItems.findIndex(p => p.id === item.id);
const paneBottom = pl.topY + pl.height;
const clusterTop = paneBottom - 4 - clusterHeight - stackIdx * (clusterHeight + 4);
const menuItems = buildMenuItems(item, indicators, paneItems, {
focusedId, onSplit, onDuplicate, onExpand, onRestore, onRemove,
onToggleHidden, onSettings,
});
if (menuItems.length === 0 && !allowDrag) return null;
return (
<div
key={item.id}
className="chart-right-toolbar-cluster"
style={{ top: clusterTop }}
>
{allowDrag && (
<button
type="button"
className="tv-side-btn chart-right-toolbar-btn chart-right-toolbar-btn--drag"
title="드래그하여 순서 변경"
onPointerDown={e => {
e.stopPropagation();
onDragStart?.(item.id, e);
}}
>
<IconDrag />
</button>
)}
{menuItems.length > 0 && (
<button
type="button"
className={`tv-side-btn tv-side-group-btn chart-right-toolbar-btn chart-right-toolbar-btn--menu${openMenuId === item.id ? ' active' : ''}`}
title="보조지표 메뉴"
onMouseEnter={e => openFlyout(item.id, e.currentTarget)}
onMouseLeave={scheduleClose}
>
<IconMore />
<span className="tv-side-expand-arrow" />
</button>
)}
</div>
);
})}
</div>
{customMenuOpen && showCustomBtn && (
<CustomOverlayFlyoutMenu
title="Custom"
flyoutPos={flyoutPos}
overlayActive={customOverlayActive}
onToggleActive={() => onCustomOverlayActiveChange?.(!customOverlayActive)}
onOpenSettings={() => onOpenCustomOverlaySettings?.()}
onCancelClose={cancelClose}
onScheduleClose={scheduleClose}
onCloseFlyout={closeFlyout}
/>
)}
{custom1MenuOpen && showCustom1Btn && (
<CustomOverlayFlyoutMenu
title="Custom1"
flyoutPos={flyoutPos}
overlayActive={custom1OverlayActive}
onToggleActive={() => onCustom1OverlayActiveChange?.(!custom1OverlayActive)}
onOpenSettings={() => onOpenCustom1OverlaySettings?.()}
onCancelClose={cancelClose}
onScheduleClose={scheduleClose}
onCloseFlyout={closeFlyout}
/>
)}
{openItem && openMenuId && openMenuId !== CUSTOM_MENU_ID && openMenuId !== CUSTOM1_MENU_ID && (() => {
const menuItems = buildMenuItems(openItem, indicators, paneItems, {
focusedId, onSplit, onDuplicate, onExpand, onRestore, onRemove,
onToggleHidden, onSettings,
onDone: closeFlyout,
});
if (menuItems.length === 0) return null;
return (
<div
className="tv-side-flyout tv-side-flyout--right-anchor"
style={{ top: flyoutPos.top, left: flyoutPos.left }}
onMouseEnter={cancelClose}
onMouseLeave={scheduleClose}
>
<div className="tv-side-flyout-header">{openItem.label}</div>
{menuItems.map(mi => (
<button
key={mi.key}
type="button"
className={`tv-side-flyout-item${mi.active ? ' active' : ''}${mi.danger ? ' tv-side-flyout-item--danger' : ''}`}
onClick={e => { e.stopPropagation(); mi.onClick(); }}
>
<span className="tv-side-flyout-icon">{mi.icon}</span>
<span className="tv-side-flyout-title">{mi.title}</span>
</button>
))}
</div>
);
})()}
</aside>
);
};
export default ChartRightToolbar;