goldenChat base source add

This commit is contained in:
aidev
2026-05-23 15:11:48 +09:00
commit a4ea7762b5
2081 changed files with 1155760 additions and 0 deletions
+162
View File
@@ -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;