goldenChat base source add
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* LayoutPicker
|
||||
* TradingView 스타일 레이아웃 선택 팝업.
|
||||
*/
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import type { LayoutDef, SyncOptions } from '../utils/layoutTypes';
|
||||
import { LAYOUTS } from '../utils/layoutTypes';
|
||||
|
||||
// grid-template-areas 문자열을 파싱해 셀 rect 배열 반환
|
||||
function parseAreas(def: LayoutDef, bw: number, bh: number, pad: number) {
|
||||
const areaGrid: string[][] = def.areas
|
||||
.split('"')
|
||||
.map(s => s.trim())
|
||||
.filter(Boolean)
|
||||
.map(row => row.split(/\s+/).filter(Boolean));
|
||||
|
||||
if (areaGrid.length === 0) return [];
|
||||
|
||||
const rowCount = areaGrid.length;
|
||||
const colCount = areaGrid[0]?.length ?? 1;
|
||||
const colW = bw / colCount;
|
||||
const rowH = bh / rowCount;
|
||||
|
||||
const cellMap = new Map<string, { c1: number; r1: number; c2: number; r2: number }>();
|
||||
for (let ri = 0; ri < areaGrid.length; ri++) {
|
||||
for (let ci = 0; ci < (areaGrid[ri]?.length ?? 0); ci++) {
|
||||
const name = areaGrid[ri][ci];
|
||||
if (!name || name === '.') continue;
|
||||
const existing = cellMap.get(name);
|
||||
if (!existing) {
|
||||
cellMap.set(name, { c1: ci, r1: ri, c2: ci + 1, r2: ri + 1 });
|
||||
} else {
|
||||
cellMap.set(name, {
|
||||
c1: Math.min(existing.c1, ci),
|
||||
r1: Math.min(existing.r1, ri),
|
||||
c2: Math.max(existing.c2, ci + 1),
|
||||
r2: Math.max(existing.r2, ri + 1),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(cellMap.values()).map(r => ({
|
||||
x: pad + r.c1 * colW,
|
||||
y: pad + r.r1 * rowH,
|
||||
w: (r.c2 - r.c1) * colW,
|
||||
h: (r.r2 - r.r1) * rowH,
|
||||
}));
|
||||
}
|
||||
|
||||
// ── 레이아웃 미니 아이콘 (SVG) ────────────────────────────────────────────────
|
||||
const LayoutIcon: React.FC<{ def: LayoutDef; size?: number }> = ({ def, size = 34 }) => {
|
||||
const S = size;
|
||||
const PAD = 2;
|
||||
const BW = S - PAD * 2;
|
||||
const BH = S - PAD * 2;
|
||||
const rects = parseAreas(def, BW, BH, PAD);
|
||||
|
||||
return (
|
||||
<svg width={S} height={S} viewBox={`0 0 ${S} ${S}`} fill="none">
|
||||
<rect x={PAD} y={PAD} width={BW} height={BH} rx="2"
|
||||
stroke="currentColor" strokeWidth="1" opacity="0.4" />
|
||||
{rects.map((r, i) => (
|
||||
<rect key={i}
|
||||
x={r.x + 1} y={r.y + 1}
|
||||
width={r.w - 2} height={r.h - 2}
|
||||
rx="1"
|
||||
stroke="currentColor" strokeWidth="1"
|
||||
fill="currentColor" fillOpacity="0.15"
|
||||
/>
|
||||
))}
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
// ── 토글 스위치 ──────────────────────────────────────────────────────────────
|
||||
const Toggle: React.FC<{ checked: boolean; onChange: () => void }> = ({ checked, onChange }) => (
|
||||
<button
|
||||
className={`lp-toggle${checked ? ' on' : ''}`}
|
||||
onClick={onChange}
|
||||
aria-pressed={checked}
|
||||
/>
|
||||
);
|
||||
|
||||
// 행 그룹: 같은 count 단위로 묶음
|
||||
const ROW_GROUPS = [1, 2, 3, 4, 5, 6, 8];
|
||||
|
||||
// ── Main Component ───────────────────────────────────────────────────────────
|
||||
interface LayoutPickerProps {
|
||||
currentId: string;
|
||||
syncOptions: SyncOptions;
|
||||
onSelect: (def: LayoutDef) => void;
|
||||
onSyncChange: (key: keyof SyncOptions, value: boolean) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const LayoutPicker: React.FC<LayoutPickerProps> = ({
|
||||
currentId, syncOptions, onSelect, onSyncChange, onClose,
|
||||
}) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: MouseEvent) => {
|
||||
if (ref.current && !ref.current.contains(e.target as Node)) onClose();
|
||||
};
|
||||
setTimeout(() => document.addEventListener('mousedown', handler), 80);
|
||||
return () => document.removeEventListener('mousedown', handler);
|
||||
}, [onClose]);
|
||||
|
||||
return (
|
||||
<div ref={ref} className="lp-popup">
|
||||
{/* 레이아웃 그리드 */}
|
||||
<div className="lp-grid-section">
|
||||
{ROW_GROUPS.map(count => {
|
||||
const group = LAYOUTS.filter(l => l.count === count);
|
||||
if (group.length === 0) return null;
|
||||
return (
|
||||
<div key={count} className="lp-row">
|
||||
<span className="lp-row-num">{count}</span>
|
||||
<div className="lp-row-icons">
|
||||
{group.map(def => (
|
||||
<button
|
||||
key={def.id}
|
||||
className={`lp-icon-btn${currentId === def.id ? ' active' : ''}`}
|
||||
title={def.label}
|
||||
onClick={() => { onSelect(def); }}
|
||||
>
|
||||
<LayoutIcon def={def} size={34} />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="lp-divider" />
|
||||
|
||||
{/* SYNC IN LAYOUT */}
|
||||
<div className="lp-sync-section">
|
||||
<div className="lp-sync-label">SYNC IN LAYOUT</div>
|
||||
{([
|
||||
['symbol', 'Symbol'],
|
||||
['interval', 'Interval'],
|
||||
['crosshair', 'Crosshair'],
|
||||
['time', 'Time'],
|
||||
['dateRange', 'Date range'],
|
||||
] as [keyof SyncOptions, string][]).map(([key, label]) => (
|
||||
<div key={key} className="lp-sync-row">
|
||||
<span className="lp-sync-name">{label}</span>
|
||||
<Toggle
|
||||
checked={syncOptions[key]}
|
||||
onChange={() => onSyncChange(key, !syncOptions[key])}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LayoutPicker;
|
||||
Reference in New Issue
Block a user