goldenChat base source add
This commit is contained in:
@@ -0,0 +1,270 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user