271 lines
7.8 KiB
TypeScript
271 lines
7.8 KiB
TypeScript
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
|
import { createPortal } from 'react-dom';
|
|
import { useDraggablePanel } from '../hooks/useDraggablePanel';
|
|
import type { HLineStyle } from '../utils/indicatorRegistry';
|
|
import {
|
|
parsePlotColor,
|
|
formatPlotColor,
|
|
plotColorCss,
|
|
LINE_WIDTH_OPTIONS,
|
|
LINE_STYLE_OPTIONS,
|
|
LINE_STYLE_LABELS,
|
|
} from '../utils/plotColorUtils';
|
|
import ColorPickerPanel from './ColorPickerPanel';
|
|
|
|
export interface PlotLineStyleValue {
|
|
color: string;
|
|
lineWidth?: number;
|
|
lineStyle?: HLineStyle;
|
|
}
|
|
|
|
interface PlotLineStylePickerProps {
|
|
value: PlotLineStyleValue;
|
|
disabled?: boolean;
|
|
/** 팝업 제목 (예: MA3) */
|
|
title?: string;
|
|
/** false면 색상만 (히스토그램 등) */
|
|
showLineControls?: boolean;
|
|
onChange: (patch: Partial<PlotLineStyleValue>) => void;
|
|
}
|
|
|
|
interface PlotLineStylePopupProps {
|
|
title?: string;
|
|
showLineControls: boolean;
|
|
hex6: string;
|
|
alpha: number;
|
|
lineWidth: number;
|
|
lineStyle: HLineStyle;
|
|
onChange: (patch: Partial<PlotLineStyleValue>) => void;
|
|
onClose: () => void;
|
|
patchColor: (hex: string, alphaPct: number) => void;
|
|
}
|
|
|
|
const PlotLineStylePopup: React.FC<PlotLineStylePopupProps> = ({
|
|
title,
|
|
showLineControls,
|
|
hex6,
|
|
alpha,
|
|
lineWidth,
|
|
lineStyle,
|
|
onChange,
|
|
onClose,
|
|
patchColor,
|
|
}) => {
|
|
const {
|
|
panelRef,
|
|
dragging,
|
|
onHeaderPointerDown, headerTouchStyle,
|
|
panelStyle,
|
|
headerCursor,
|
|
} = useDraggablePanel({ centerOnMount: true });
|
|
|
|
return (
|
|
<div className="plsp-portal-root">
|
|
<div
|
|
className="plsp-overlay"
|
|
role="presentation"
|
|
onMouseDown={onClose}
|
|
/>
|
|
<div
|
|
ref={panelRef}
|
|
className="plsp-popup"
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-label={title ? `${title} 선 스타일` : '선 스타일'}
|
|
style={{
|
|
...panelStyle,
|
|
cursor: dragging ? 'grabbing' : undefined,
|
|
}}
|
|
onMouseDown={e => e.stopPropagation()}
|
|
>
|
|
<div
|
|
className="gc-popup-header plsp-popup-header"
|
|
onPointerDown={onHeaderPointerDown}
|
|
style={{ cursor: headerCursor, ...headerTouchStyle }}
|
|
>
|
|
<span className="gc-popup-title plsp-popup-title">
|
|
{title ? `${title} · ` : ''}색상 · 선 스타일
|
|
</span>
|
|
<button type="button" className="gc-popup-close plsp-popup-close" onClick={onClose} aria-label="닫기">
|
|
✕
|
|
</button>
|
|
</div>
|
|
|
|
<ColorPickerPanel
|
|
hex6={hex6}
|
|
onHexChange={h => patchColor(h, alpha)}
|
|
/>
|
|
|
|
<div className="plsp-section">
|
|
<span className="plsp-section-label">불투명성</span>
|
|
<div className="plsp-opacity-row">
|
|
<input
|
|
type="range"
|
|
className="plsp-opacity-slider"
|
|
min={0}
|
|
max={100}
|
|
value={alpha}
|
|
onChange={e => patchColor(hex6, Number(e.target.value))}
|
|
/>
|
|
<span className="plsp-opacity-pct">{alpha}%</span>
|
|
</div>
|
|
</div>
|
|
|
|
{showLineControls && (
|
|
<>
|
|
<div className="plsp-section">
|
|
<span className="plsp-section-label">두께</span>
|
|
<div className="plsp-btn-group">
|
|
{LINE_WIDTH_OPTIONS.map(w => (
|
|
<button
|
|
key={w}
|
|
type="button"
|
|
className={`plsp-icon-btn${lineWidth === w ? ' active' : ''}`}
|
|
title={`${w}px`}
|
|
onClick={() => onChange({ lineWidth: w })}
|
|
>
|
|
<svg width="24" height="14" viewBox="0 0 24 14" aria-hidden>
|
|
<line x1="2" y1="7" x2="22" y2="7" stroke="currentColor" strokeWidth={w} strokeLinecap="round" />
|
|
</svg>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="plsp-section">
|
|
<span className="plsp-section-label">라인 스타일</span>
|
|
<div className="plsp-btn-group">
|
|
{LINE_STYLE_OPTIONS.map(s => (
|
|
<button
|
|
key={s}
|
|
type="button"
|
|
className={`plsp-icon-btn${lineStyle === s ? ' active' : ''}`}
|
|
title={LINE_STYLE_LABELS[s]}
|
|
onClick={() => onChange({ lineStyle: s })}
|
|
>
|
|
<LineStyleIcon style={s} />
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
<div className="plsp-popup-footer">
|
|
<button type="button" className="plsp-popup-ok" onClick={onClose}>
|
|
확인
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const LineStyleIcon: React.FC<{ style: HLineStyle }> = ({ style }) => (
|
|
<svg width="28" height="14" viewBox="0 0 28 14" aria-hidden>
|
|
<line
|
|
x1="2" y1="7" x2="26" y2="7"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
strokeDasharray={
|
|
style === 'dashed' ? '6 4' : style === 'dotted' ? '2 3' : undefined
|
|
}
|
|
/>
|
|
</svg>
|
|
);
|
|
|
|
const PlotLineStylePicker: React.FC<PlotLineStylePickerProps> = ({
|
|
value,
|
|
disabled = false,
|
|
title,
|
|
showLineControls = true,
|
|
onChange,
|
|
}) => {
|
|
const [open, setOpen] = useState(false);
|
|
const swatchRef = useRef<HTMLButtonElement>(null);
|
|
const { hex6, alpha } = parsePlotColor(value.color);
|
|
const lineWidth = value.lineWidth ?? 1;
|
|
const lineStyle = value.lineStyle ?? 'solid';
|
|
|
|
const close = useCallback(() => setOpen(false), []);
|
|
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
const onKey = (e: KeyboardEvent) => {
|
|
if (e.key === 'Escape') close();
|
|
};
|
|
window.addEventListener('keydown', onKey);
|
|
return () => window.removeEventListener('keydown', onKey);
|
|
}, [open, close]);
|
|
|
|
const patchColor = useCallback((hex: string, alphaPct: number) => {
|
|
onChange({ color: formatPlotColor(hex, alphaPct) });
|
|
}, [onChange]);
|
|
|
|
const popup = open ? (
|
|
<PlotLineStylePopup
|
|
title={title}
|
|
showLineControls={showLineControls}
|
|
hex6={hex6}
|
|
alpha={alpha}
|
|
lineWidth={lineWidth}
|
|
lineStyle={lineStyle}
|
|
onChange={onChange}
|
|
onClose={close}
|
|
patchColor={patchColor}
|
|
/>
|
|
) : null;
|
|
|
|
return (
|
|
<div className={`plsp-inline${disabled ? ' plsp-inline--disabled' : ''}`}>
|
|
<button
|
|
ref={swatchRef}
|
|
type="button"
|
|
className="plsp-swatch"
|
|
disabled={disabled}
|
|
title="색상·선 스타일"
|
|
onClick={() => !disabled && setOpen(true)}
|
|
>
|
|
<span
|
|
className="plsp-swatch-inner"
|
|
style={{ background: plotColorCss(value.color) }}
|
|
/>
|
|
</button>
|
|
{showLineControls && (
|
|
<>
|
|
<select
|
|
className="plsp-inline-select"
|
|
value={lineWidth}
|
|
disabled={disabled}
|
|
title="선 굵기"
|
|
aria-label="선 굵기"
|
|
onChange={e => onChange({ lineWidth: Number(e.target.value) })}
|
|
>
|
|
{LINE_WIDTH_OPTIONS.map(w => (
|
|
<option key={w} value={w}>{w}</option>
|
|
))}
|
|
</select>
|
|
<select
|
|
className="plsp-inline-select plsp-inline-select--style"
|
|
value={lineStyle}
|
|
disabled={disabled}
|
|
title="선 유형"
|
|
aria-label="선 유형"
|
|
onChange={e => onChange({ lineStyle: e.target.value as HLineStyle })}
|
|
>
|
|
{LINE_STYLE_OPTIONS.map(s => (
|
|
<option key={s} value={s}>{LINE_STYLE_LABELS[s]}</option>
|
|
))}
|
|
</select>
|
|
</>
|
|
)}
|
|
{typeof document !== 'undefined' && popup
|
|
? createPortal(popup, document.body)
|
|
: null}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default PlotLineStylePicker;
|