240 lines
7.8 KiB
TypeScript
240 lines
7.8 KiB
TypeScript
/**
|
|
* 보조지표 설정 — 스타일 탭 (플롯·수평선 색상/굵기/타입)
|
|
* 타이틀 열 너비를 섹션 내 최장 라벨에 맞춰 컨트롤 상하 정렬
|
|
*/
|
|
import React, { useLayoutEffect, useRef } from 'react';
|
|
import type { PlotDef, HLineDef, HLineStyle } from '../utils/indicatorRegistry';
|
|
import { getHLineLabel } from '../utils/indicatorRegistry';
|
|
import { getPlotLabel } from '../utils/indicatorLabels';
|
|
import { getHlinePriceSpec } from '../utils/indicatorParamSpec';
|
|
import NumericParamInput from './NumericParamInput';
|
|
import { ColorInput } from './IndicatorSettingsSections';
|
|
import { LINE_WIDTH_OPTIONS, LINE_STYLE_OPTIONS } from '../utils/plotColorUtils';
|
|
import { IchimokuCloudSection } from './IndicatorSettingsSections';
|
|
import { getIchimokuPlotTitle, type IchimokuCloudColors } from '../utils/ichimokuConfig';
|
|
|
|
/** 스타일 탭 선 유형 드롭다운 (업비트 표기) */
|
|
const LINE_STYLE_TAB_LABELS: Record<HLineStyle, string> = {
|
|
solid: '라인',
|
|
dashed: '점선',
|
|
dotted: '도트',
|
|
};
|
|
|
|
export interface IndicatorSettingsStyleSectionProps {
|
|
indicatorType: string;
|
|
plots: PlotDef[];
|
|
hlines: HLineDef[];
|
|
cloudColors?: IchimokuCloudColors;
|
|
showIchimokuCloud?: boolean;
|
|
onPlotStyle: (idx: number, patch: Partial<PlotDef>) => void;
|
|
onHLineStyle: (idx: number, patch: Partial<HLineDef>) => void;
|
|
onHLinePrice?: (idx: number, price: number) => void;
|
|
onCloudColors?: (patch: Partial<IchimokuCloudColors>) => void;
|
|
}
|
|
|
|
function measureTitleColumn(sectionEl: HTMLElement | null, measureEl: HTMLElement | null) {
|
|
if (!sectionEl || !measureEl) return;
|
|
let maxW = 0;
|
|
measureEl.querySelectorAll<HTMLElement>('.ism-plot-title').forEach(el => {
|
|
maxW = Math.max(maxW, el.offsetWidth);
|
|
});
|
|
if (maxW > 0) {
|
|
sectionEl.style.setProperty('--ism-plot-title-width', `${Math.ceil(maxW)}px`);
|
|
}
|
|
}
|
|
|
|
const PlotStyleRow: React.FC<{
|
|
label: string;
|
|
plot: PlotDef;
|
|
disabled?: boolean;
|
|
onStyle: (patch: Partial<PlotDef>) => void;
|
|
}> = ({ label, plot, disabled, onStyle }) => {
|
|
const isHistogram = plot.type === 'histogram';
|
|
const lineWidth = plot.lineWidth ?? 1;
|
|
const lineStyle = plot.lineStyle ?? 'solid';
|
|
|
|
return (
|
|
<div className="ism-style-row">
|
|
<div className="ism-plot-title-cell">
|
|
<span className="ism-plot-title">{label}</span>
|
|
</div>
|
|
<div className="ism-style-field">
|
|
<span className="ism-style-label">색상</span>
|
|
<ColorInput
|
|
value={plot.color}
|
|
onChange={v => onStyle({ color: v })}
|
|
/>
|
|
</div>
|
|
<div className="ism-style-field">
|
|
<span className="ism-style-label">굵기</span>
|
|
{isHistogram ? (
|
|
<span className="ism-style-field-spacer ism-num-combo-spacer" aria-hidden />
|
|
) : (
|
|
<select
|
|
className="ism-select ism-style-width-select"
|
|
value={lineWidth}
|
|
disabled={disabled}
|
|
onChange={e => onStyle({ lineWidth: Number(e.target.value) })}
|
|
>
|
|
{LINE_WIDTH_OPTIONS.map(w => (
|
|
<option key={w} value={w}>{w}</option>
|
|
))}
|
|
</select>
|
|
)}
|
|
</div>
|
|
<div className="ism-style-field">
|
|
<span className="ism-style-label">타입</span>
|
|
{isHistogram ? (
|
|
<span className="ism-style-type-static">히스토그램</span>
|
|
) : (
|
|
<select
|
|
className="ism-select ism-style-type-select"
|
|
value={lineStyle}
|
|
disabled={disabled}
|
|
onChange={e => onStyle({ lineStyle: e.target.value as HLineStyle })}
|
|
>
|
|
{LINE_STYLE_OPTIONS.map(s => (
|
|
<option key={s} value={s}>{LINE_STYLE_TAB_LABELS[s]}</option>
|
|
))}
|
|
</select>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const HLineStyleRow: React.FC<{
|
|
indicatorType: string;
|
|
label: string;
|
|
hl: HLineDef;
|
|
onStyle: (patch: Partial<HLineDef>) => void;
|
|
onPrice?: (price: number) => void;
|
|
}> = ({ indicatorType, label, hl, onStyle, onPrice }) => {
|
|
const isOn = hl.visible !== false;
|
|
const lineWidth = hl.lineWidth ?? 1;
|
|
const lineStyle = hl.lineStyle ?? 'dashed';
|
|
|
|
return (
|
|
<div className={`ism-hline-row${isOn ? '' : ' ism-hline-row--off'}`}>
|
|
<div className="ism-hline-name-cell">
|
|
<span className="ism-hline-label-text">{label}</span>
|
|
</div>
|
|
<div className="ism-hline-cell">
|
|
{onPrice != null ? (
|
|
<NumericParamInput
|
|
className="ism-input ism-narrow"
|
|
value={hl.price}
|
|
spec={getHlinePriceSpec(indicatorType, hl.price)}
|
|
disabled={!isOn}
|
|
onChange={onPrice}
|
|
/>
|
|
) : (
|
|
<span className="ism-hline-price-readonly">{hl.price}</span>
|
|
)}
|
|
</div>
|
|
<div className="ism-hline-cell ism-hline-cell--style">
|
|
<ColorInput value={hl.color} onChange={v => onStyle({ color: v })} />
|
|
<select
|
|
className="ism-select ism-style-width-select"
|
|
value={lineWidth}
|
|
disabled={!isOn}
|
|
title="굵기"
|
|
onChange={e => onStyle({ lineWidth: Number(e.target.value) })}
|
|
>
|
|
{LINE_WIDTH_OPTIONS.map(w => (
|
|
<option key={w} value={w}>{w}</option>
|
|
))}
|
|
</select>
|
|
<select
|
|
className="ism-select ism-style-type-select"
|
|
value={lineStyle}
|
|
disabled={!isOn}
|
|
title="타입"
|
|
onChange={e => onStyle({ lineStyle: e.target.value as HLineStyle })}
|
|
>
|
|
{LINE_STYLE_OPTIONS.map(s => (
|
|
<option key={s} value={s}>{LINE_STYLE_TAB_LABELS[s]}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const IndicatorSettingsStyleSection: React.FC<IndicatorSettingsStyleSectionProps> = ({
|
|
indicatorType,
|
|
plots,
|
|
hlines,
|
|
cloudColors,
|
|
showIchimokuCloud,
|
|
onPlotStyle,
|
|
onHLineStyle,
|
|
onHLinePrice,
|
|
onCloudColors,
|
|
}) => {
|
|
const sectionRef = useRef<HTMLDivElement>(null);
|
|
const measureRef = useRef<HTMLDivElement>(null);
|
|
const hlinePrices = hlines.map(h => h.price);
|
|
|
|
const plotLabel = (p: PlotDef) => indicatorType === 'IchimokuCloud'
|
|
? getIchimokuPlotTitle(p.id)
|
|
: getPlotLabel(p.title);
|
|
|
|
const titleLabels = [
|
|
...plots.map(plotLabel),
|
|
...hlines.map(h => h.label ?? getHLineLabel(h.price, hlinePrices)),
|
|
];
|
|
|
|
useLayoutEffect(() => {
|
|
measureTitleColumn(sectionRef.current, measureRef.current);
|
|
}, [titleLabels.join('\0'), plots.length, hlines.length]);
|
|
|
|
return (
|
|
<div className="ism-section ism-style-tab-section">
|
|
<div ref={measureRef} className="ism-plot-title-measure" aria-hidden>
|
|
{titleLabels.map((t, i) => (
|
|
<span key={i} className="ism-plot-title">{t}</span>
|
|
))}
|
|
</div>
|
|
|
|
<div ref={sectionRef} className="ism-style-section">
|
|
{plots.map((plot, idx) => (
|
|
<PlotStyleRow
|
|
key={plot.id}
|
|
label={plotLabel(plot)}
|
|
plot={plot}
|
|
onStyle={patch => onPlotStyle(idx, patch)}
|
|
/>
|
|
))}
|
|
|
|
{hlines.length > 0 && (
|
|
<>
|
|
<hr className="ism-hline-divider" />
|
|
<div className="ism-hline-header-row">
|
|
<span>이름</span>
|
|
<span>가격</span>
|
|
<span>색상 · 굵기 · 타입</span>
|
|
</div>
|
|
{hlines.map((hl, idx) => (
|
|
<HLineStyleRow
|
|
key={idx}
|
|
indicatorType={indicatorType}
|
|
label={hl.label ?? getHLineLabel(hl.price, hlinePrices)}
|
|
hl={hl}
|
|
onStyle={patch => onHLineStyle(idx, patch)}
|
|
onPrice={onHLinePrice ? v => onHLinePrice(idx, v) : undefined}
|
|
/>
|
|
))}
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
{showIchimokuCloud && cloudColors && onCloudColors && (
|
|
<IchimokuCloudSection cloudColors={cloudColors} onChange={onCloudColors} />
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default IndicatorSettingsStyleSection;
|