/** * 보조지표 설정 — 색상 + 투명도 (스와치 클릭 시 팔레트·고급 선택기) */ import React, { useState, useRef, useEffect, useCallback } from 'react'; import { createPortal } from 'react-dom'; import { parsePlotColor, formatPlotColor, plotColorCss } from '../utils/plotColorUtils'; import { getOpacityPercentSpec } from '../utils/indicatorParamSpec'; import NumericParamInput from './NumericParamInput'; import ColorPickerPanel from './ColorPickerPanel'; export interface ColorInputProps { value: string; onChange: (v: string) => void; disabled?: boolean; /** false — 인라인 네이티브 color input 숨김(설정 화면: OS 피커만 뜨는 문제 방지) */ showCompactNative?: boolean; /** false — 바깥 클릭으로 닫지 않음(확인·✕·Esc 로만 닫기) */ closeOnClickOutside?: boolean; } const ColorInput: React.FC = ({ value, onChange, disabled = false, showCompactNative = true, closeOnClickOutside = false, }) => { const { hex6, alpha } = parsePlotColor(value); const [open, setOpen] = useState(false); const anchorRef = useRef(null); const popoverRef = useRef(null); const [pos, setPos] = useState({ top: 0, left: 0 }); const updatePosition = useCallback(() => { const el = anchorRef.current; if (!el) return; const rect = el.getBoundingClientRect(); const popW = 280; let left = rect.left; if (left + popW > window.innerWidth - 12) { left = window.innerWidth - popW - 12; } setPos({ top: rect.bottom + 6, left: Math.max(8, left) }); }, []); useEffect(() => { if (!open) return; updatePosition(); const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') setOpen(false); }; const onScroll = () => updatePosition(); window.addEventListener('keydown', onKey); window.addEventListener('resize', updatePosition); window.addEventListener('scroll', onScroll, true); return () => { window.removeEventListener('keydown', onKey); window.removeEventListener('resize', updatePosition); window.removeEventListener('scroll', onScroll, true); }; }, [open, updatePosition]); useEffect(() => { if (!open || !closeOnClickOutside) return; const onDocPointer = (e: Event) => { const t = e.target as Node; if (anchorRef.current?.contains(t) || popoverRef.current?.contains(t)) return; setOpen(false); }; document.addEventListener('mousedown', onDocPointer, true); document.addEventListener('pointerdown', onDocPointer, true); return () => { document.removeEventListener('mousedown', onDocPointer, true); document.removeEventListener('pointerdown', onDocPointer, true); }; }, [open, closeOnClickOutside]); const patchHex = (h: string) => onChange(formatPlotColor(h, alpha)); const patchAlpha = (a: number) => onChange(formatPlotColor(hex6, a)); const closePopover = useCallback(() => setOpen(false), []); const popover = open && typeof document !== 'undefined' ? (
e.stopPropagation()} onPointerDown={e => e.stopPropagation()} >
색상 선택
{showCompactNative && (
)}
) : null; return (
{showCompactNative && ( patchHex(e.target.value)} /> )} % {popover ? createPortal(popover, document.body) : null}
); }; export default ColorInput;