fix: bố cục lại UI với float panel

This commit is contained in:
2026-07-19 12:46:46 +07:00
parent 66b0811f55
commit 60dce0135e
+353 -375
View File
@@ -1015,6 +1015,15 @@
const [soloedTrackId, setSoloedTrackId] = useState(null);
const [toastMessage, setToastMessage] = useState(null);
const [showAIConfig, setShowAIConfig] = useState(false);
const [showExportPanel, setShowExportPanel] = useState(true);
const [showAIPanel, setShowAIPanel] = useState(true);
const [showSelectionPanel, setShowSelectionPanel] = useState(true);
const [panelPositions, setPanelPositions] = useState({ export: 'right', ai: 'right', selection: 'right' });
const [panelDropZone, setPanelDropZone] = useState(null);
const [dragGhostPos, setDragGhostPos] = useState(null);
const [dragGhostPanel, setDragGhostPanel] = useState(null);
const panelDragRef = useRef(null);
const workspaceRef = useRef(null);
const [aiConfig, setAiConfig] = useState({
baseUrl: localStorage.getItem('ai_base_url') || `${API_BASE_URL}`,
apiKey: localStorage.getItem('ai_api_key') || '',
@@ -1143,6 +1152,53 @@
tcpContainerRef.current.scrollTop = e.currentTarget.scrollTop;
}
};
const handleTCPScroll = (e) => {
if (timelineWrapperRef.current) {
timelineWrapperRef.current.scrollTop = e.currentTarget.scrollTop;
}
};
const panelDropZoneRef = useRef(null);
const startPanelDrag = (panelId, e) => {
panelDragRef.current = { panelId, startX: e.clientX, startY: e.clientY };
setPanelDropZone(null);
setDragGhostPanel(panelId);
setDragGhostPos({ x: e.clientX - 120, y: e.clientY - 20 });
const onMove = (ev) => {
if (!panelDragRef.current || !workspaceRef.current) return;
const rect = workspaceRef.current.getBoundingClientRect();
const x = ev.clientX - rect.left;
const y = ev.clientY - rect.top;
const w = rect.width;
const h = rect.height;
const margin = 60;
let zone = null;
if (x < margin && x > 10) zone = 'left';
else if (x > w - margin && x < w - 10) zone = 'right';
else if (y < margin && y > 10) zone = 'top';
else if (y > h - margin && y < h - 10) zone = 'bottom';
panelDropZoneRef.current = zone;
setPanelDropZone(zone);
setDragGhostPos({ x: ev.clientX - 120, y: ev.clientY - 20 });
};
const onUp = () => {
if (panelDragRef.current) {
const pid = panelDragRef.current.panelId;
const targetZone = panelDropZoneRef.current;
if (targetZone && targetZone !== panelPositions[pid]) {
setPanelPositions(prev => ({ ...prev, [pid]: targetZone }));
}
panelDragRef.current = null;
panelDropZoneRef.current = null;
setPanelDropZone(null);
setDragGhostPos(null);
setDragGhostPanel(null);
}
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
};
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
};
const rulerRef = useRef(null);
const activeSourcesRef = useRef([]);
const startOffsetTimeRef = useRef(0);
@@ -4043,299 +4099,358 @@
</div>
)}
{/* ── Workspace (Split Dual-Column — 11_REFACTOR_UI.md) ── */}
<div
className="flex-1 flex overflow-hidden select-none daw-bg relative"
style={{ display: activeTab !== 'main' ? 'none' : '' }}
>
{/* ══ LEFT COLUMN: TCP PANEL (fixed 300px, overflow hidden, z-30, solid bg) ══ */}
<div
ref={tcpContainerRef}
className="w-[300px] shrink-0 relative z-30 bg-[#262626] overflow-y-scroll no-scrollbar flex flex-col border-r border-zinc-900"
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
>
{/* [TCP] Header Row (Channels & Tools) — height 40px (h-10) */}
<div className="sticky top-0 z-50 flex items-center h-10 border-b border-zinc-900 bg-[#242424] px-2 justify-between select-none shrink-0">
<span className="text-[10px] font-bold text-zinc-500 uppercase shrink-0 mr-2">Kênh</span>
{/* Timeline Toolbar (scaled up buttons, height locked at 40px) */}
<div className="flex items-center gap-1 bg-zinc-900 border border-zinc-800 rounded px-1.5 py-1 shadow-sm h-8">
<button
onClick={() => { setActiveTool('select'); showToast('Công cụ chọn (Select Tool) đã kích hoạt.', 'info'); }}
className={`p-1 rounded transition text-sm flex items-center justify-center ${activeTool === 'select' ? 'bg-cyan-950/80 text-cyan-400 border border-cyan-800/50 font-bold' : 'text-zinc-400 hover:bg-zinc-800 border border-transparent'}`}
title="Select Tool: Chọn khoảng, Đặt playhead (Alt+Kéo để di chuyển clip, Ctrl+Kéo để nhân bản)"
>
<i data-lucide="mouse-pointer" className="w-4 h-4"></i>
{/* ── Transport Bar + Toolbar Overlay (full width, between tab bar and workspace) ── */}
<div className="h-9 bg-[#222] border-b border-zinc-800 flex items-center px-3 gap-2 shrink-0 select-none" style={{ display: activeTab !== 'main' ? 'none' : '' }}>
{/* Toolbar Overlay Panel — có thể kéo thả hoặc ghim sau này */}
<div className="flex items-center gap-1 bg-zinc-900 border border-zinc-700 rounded px-1.5 py-1 shadow-lg h-7 cursor-grab active:cursor-grabbing"
title="Kéo để di chuyển toolbar" style={{ cursor: 'grab' }}>
<div className="flex items-center gap-0.5 mr-1 text-zinc-600">
<i data-lucide="grip-vertical" className="w-3 h-3"></i>
</div>
<button onClick={() => { setActiveTool('select'); showToast('Select Tool', 'info'); }}
className={`p-0.5 rounded ${activeTool === 'select' ? 'bg-cyan-700 text-white' : 'text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800'}`}
title="Select Tool">
<i data-lucide="mouse-pointer" className="w-3.5 h-3.5"></i>
</button>
<button
onClick={() => { setActiveTool('grab'); showToast('Công cụ di chuyển (Hand Tool) đã kích hoạt.', 'info'); }}
className={`p-1 rounded transition text-sm flex items-center justify-center ${activeTool === 'grab' ? 'bg-cyan-950/80 text-cyan-400 border border-cyan-700/50 font-bold' : 'text-zinc-400 hover:bg-zinc-800 border border-transparent'}`}
title="Grab Tool: Click kéo trực tiếp để di chuyển clip"
>
<i data-lucide="hand" className="w-4 h-4"></i>
<button onClick={() => { setActiveTool('grab'); showToast('Grab Tool', 'info'); }}
className={`p-0.5 rounded ${activeTool === 'grab' ? 'bg-cyan-700 text-white' : 'text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800'}`}
title="Grab Tool">
<i data-lucide="hand" className="w-3.5 h-3.5"></i>
</button>
<button
onClick={() => { setActiveTool('razor'); showToast('Công cụ chia đoạn (Razor Tool) đã kích hoạt.', 'info'); }}
className={`p-1 rounded transition text-sm flex items-center justify-center ${activeTool === 'razor' ? 'bg-cyan-950/80 text-cyan-400 border border-cyan-700/50 font-bold' : 'text-zinc-400 hover:bg-zinc-800 border border-transparent'}`}
title="Razor Tool: Click trên clip để chia nhỏ tại điểm click"
>
<svg className="w-4 h-4 text-orange-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<button onClick={() => { setActiveTool('razor'); showToast('Razor Tool', 'info'); }}
className={`p-0.5 rounded ${activeTool === 'razor' ? 'bg-cyan-700 text-white' : 'text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800'}`}
title="Razor Tool">
<svg className="w-3.5 h-3.5 text-orange-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M6 3h12a2 2 0 0 1 2 2v2a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z"/>
<path d="M4 9h16l-3 9H7z"/>
<circle cx="12" cy="6" r="1"/>
</svg>
</button>
<div className="w-[1px] h-4 bg-zinc-800 mx-0.5"></div>
<button
onClick={handleGlueTracks}
className="p-0.5 rounded text-zinc-400 hover:bg-zinc-800 hover:text-purple-400 transition"
title="Glue: Gộp track hiện tại với track liền dưới"
>
<button onClick={() => { setActiveTool('pen'); showToast('Pen Tool', 'info'); }}
className={`p-0.5 rounded ${activeTool === 'pen' ? 'bg-cyan-700 text-white' : 'text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800'}`}
title="Pen Tool">
<i data-lucide="pen-tool" className="w-3.5 h-3.5"></i>
</button>
<div className="w-[1px] h-4 bg-zinc-700 mx-0.5"></div>
<button onClick={handleGlueTracks}
className="p-0.5 rounded text-zinc-400 hover:text-purple-400 hover:bg-zinc-800"
title="Glue Clips">
<i data-lucide="link" className="w-3.5 h-3.5"></i>
</button>
<button
onClick={handleCutTrack}
className="p-0.5 rounded text-zinc-400 hover:bg-zinc-800 hover:text-red-400 transition"
title="Cut Clip (Ctrl+X)"
>
<button onClick={handleCutTrack}
className="p-0.5 rounded text-zinc-400 hover:text-red-400 hover:bg-zinc-800"
title="Cut (Ctrl+X)">
<i data-lucide="scissors" className="w-3.5 h-3.5"></i>
</button>
<button
onClick={handleCopyTrack}
className="p-0.5 rounded text-zinc-400 hover:bg-zinc-800 hover:text-blue-400 transition"
title="Copy Clip (Ctrl+C)"
>
<button onClick={handleCopyTrack}
className="p-0.5 rounded text-zinc-400 hover:text-blue-400 hover:bg-zinc-800"
title="Copy (Ctrl+C)">
<i data-lucide="copy" className="w-3.5 h-3.5"></i>
</button>
<button
onClick={handlePasteTrack}
className="p-0.5 rounded text-zinc-400 hover:bg-zinc-800 hover:text-emerald-400 transition"
title="Paste Clip (Ctrl+V)"
>
<button onClick={handlePasteTrack}
className="p-0.5 rounded text-zinc-400 hover:text-emerald-400 hover:bg-zinc-800"
title="Paste (Ctrl+V)">
<i data-lucide="clipboard" className="w-3.5 h-3.5"></i>
</button>
</div>
<div className="w-[1px] h-4 bg-zinc-800 mx-0.5"></div>
<div className="w-[1px] h-5 bg-zinc-800 mx-0.5"></div>
{/* Transport Controls - gần toolbar */}
<button onClick={() => setCurrentTime(0)}
className="w-6 h-6 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition"
title="Quay lại đầu"><i data-lucide="skip-back" className="w-3 h-3"></i></button>
className="w-7 h-7 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition"
title="Quay lại đầu"><i data-lucide="skip-back" className="w-3.5 h-3.5"></i></button>
<button onClick={() => { if (selLeft !== null) setCurrentTime(selLeft); }}
className="w-7 h-7 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition"
title="Đầu vùng chọn"><i data-lucide="step-back" className="w-3.5 h-3.5"></i></button>
<button onClick={handlePlayPause}
className={`w-6 h-6 flex items-center justify-center rounded border transition ${
className={`w-7 h-7 flex items-center justify-center rounded border transition ${
isPlaying
? 'bg-emerald-600 text-black border-emerald-500 hover:bg-emerald-500'
: 'bg-cyan-600 text-white border-cyan-500 hover:bg-cyan-500'
}`}
title={isPlaying ? "Tạm dừng" : "Play"}>
<i data-lucide={isPlaying ? "pause" : "play"} className="w-3 h-3 fill-current"></i>
<i data-lucide={isPlaying ? "pause" : "play"} className="w-3.5 h-3.5 fill-current"></i>
</button>
<button onClick={handleStop}
className="w-6 h-6 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition"
title="Stop"><i data-lucide="square" className="w-3 h-3 fill-current"></i></button>
className="w-7 h-7 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition"
title="Stop"><i data-lucide="square" className="w-3.5 h-3.5 fill-current"></i></button>
<button onClick={() => { if (selRight !== null) setCurrentTime(selRight); }}
className="w-7 h-7 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition"
title="Cuối vùng chọn"><i data-lucide="step-forward" className="w-3.5 h-3.5"></i></button>
<button onClick={() => setCurrentTime(maxDuration)}
className="w-6 h-6 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition"
title="Đến cuối"><i data-lucide="skip-forward" className="w-3 h-3"></i></button>
className="w-7 h-7 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition"
title="Đến cuối"><i data-lucide="skip-forward" className="w-3.5 h-3.5"></i></button>
<div className="w-[1px] h-5 bg-zinc-800 mx-0.5"></div>
<button onClick={() => setIsLoopingSelection(prev => !prev)}
className={`w-6 h-6 flex items-center justify-center rounded border transition ${
className={`w-7 h-7 flex items-center justify-center rounded border transition ${
isLoopingSelection
? 'bg-amber-600 text-black border-amber-500 hover:bg-amber-500'
: 'bg-zinc-800 text-zinc-200 border-zinc-700 hover:bg-zinc-700'
}`} title="Bật/Tắt Lặp vùng chọn">
<i data-lucide="repeat" className="w-3 h-3"></i>
<i data-lucide="repeat" className="w-3.5 h-3.5"></i>
</button>
<div className="flex-1"></div>
<div className="text-xs font-bold font-mono text-zinc-100">{formatTime(currentTime)}</div>
</div>
<div className="w-[1px] h-4 bg-zinc-800 mx-0.5"></div>
<div className="flex items-center gap-1 pl-0.5 select-none">
<span className="text-[9px] text-zinc-500 font-bold uppercase">Snap</span>
<select
value={snapValue}
onChange={(e) => setSnapValue(e.target.value)}
className="bg-zinc-850 text-zinc-300 text-[10px] px-1 py-0.5 rounded border border-zinc-800 focus:outline-none focus:border-cyan-550 font-mono"
>
<option value="free">Free</option>
<option value="1">1</option>
<option value="1/2">1/2</option>
<option value="1/4">1/4</option>
<option value="1/8">1/8</option>
<option value="1/16">1/16</option>
<option value="1/32">1/32</option>
{/* ── Dock-Aware Workspace ── */}
{activeTab === 'main' && (() => {
const dockPanels = { top: [], right: [], bottom: [], left: [] };
const addPanel = (id, pos, visible) => { if (visible) dockPanels[pos].push(id); };
addPanel('export', panelPositions.export, showExportPanel);
addPanel('ai', panelPositions.ai, showAIPanel);
addPanel('selection', panelPositions.selection, showSelectionPanel);
const closePanel = (id) => {
if (id === 'export') setShowExportPanel(false);
else if (id === 'ai') setShowAIPanel(false);
else if (id === 'selection') setShowSelectionPanel(false);
};
const renderPanelContent = (panelId) => {
const h = (id) => (e) => { startPanelDrag(id, e); };
if (panelId === 'export') return (
<div className="flex flex-col h-full gap-1.5">
<div className="flex items-center justify-between shrink-0 cursor-grab active:cursor-grabbing select-none" onMouseDown={e => startPanelDrag('export', e)}>
<h3 className="font-bold text-[10px] text-zinc-200 flex items-center gap-1">
<i data-lucide="grip-vertical" className="w-3 h-3 text-zinc-500"></i>
<i data-lucide="save" className="w-3.5 h-3.5 text-cyan-400"></i> Export
</h3>
<button onClick={() => closePanel('export')} className="text-zinc-600 hover:text-zinc-300"><i data-lucide="x" className="w-3 h-3"></i></button>
</div>
<div className="grid grid-cols-3 gap-1">
<div>
<label className="block text-[7px] text-zinc-500 font-bold uppercase mb-0.5">SR</label>
<select value={exportSettings.sampleRate} onChange={e => setExportSettings(p => ({...p, sampleRate: e.target.value}))}
className="w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-[9px] text-zinc-300 focus:outline-none">
<option value="44100">44.1k</option><option value="48000">48k</option>
</select>
</div>
<div>
<label className="block text-[7px] text-zinc-500 font-bold uppercase mb-0.5">Bit</label>
<select value={exportSettings.bitDepth} onChange={e => setExportSettings(p => ({...p, bitDepth: e.target.value}))}
className="w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-[9px] text-zinc-300 focus:outline-none">
<option value="16">16</option><option value="24">24</option>
</select>
</div>
<div>
<label className="block text-[7px] text-zinc-500 font-bold uppercase mb-0.5">Fmt</label>
<select value={exportSettings.format} onChange={e => setExportSettings(p => ({...p, format: e.target.value}))}
className="w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-[9px] text-zinc-300 focus:outline-none">
<option value="wav">WAV</option>
</select>
</div>
</div>
<button onClick={triggerWavExport} disabled={isExporting}
className="w-full py-1 bg-cyan-700 hover:bg-cyan-600 text-zinc-100 font-bold rounded text-[10px] flex items-center justify-center gap-1">
<i data-lucide="download-cloud" className="w-3 h-3"></i>
{isExporting ? '...' : 'Export'}
</button>
</div>
);
if (panelId === 'ai') return (
<div className="flex flex-col h-full gap-1.5">
<div className="flex items-center justify-between shrink-0 cursor-grab active:cursor-grabbing select-none" onMouseDown={e => startPanelDrag('ai', e)}>
<h3 className="font-bold text-[10px] text-zinc-200 flex items-center gap-1">
<i data-lucide="grip-vertical" className="w-3 h-3 text-zinc-500"></i>
<i data-lucide="cpu" className="w-3.5 h-3.5 text-purple-400"></i> AI
</h3>
<button onClick={() => closePanel('ai')} className="text-zinc-600 hover:text-zinc-300"><i data-lucide="x" className="w-3 h-3"></i></button>
</div>
<div className="p-1.5 bg-[#141414] rounded border border-zinc-800 text-[9px] font-mono min-h-[24px]">
<div className="text-zinc-500">// <span className="text-zinc-300">{analysisState.status}</span></div>
{analysisState.data && <div className="text-emerald-500 font-semibold">BPM: {analysisState.data.bpm}</div>}
</div>
<div className="flex gap-1">
<button onClick={handleMarkSelection} className="flex-1 py-1 bg-purple-900 hover:bg-purple-800 text-purple-100 font-bold rounded text-[9px] border border-purple-700">Mark</button>
<button onClick={handleAICutToNewTrack} disabled={analysisState.isRunning} className="flex-1 py-1 bg-purple-700 hover:bg-purple-600 text-white font-bold rounded text-[9px]">Cut</button>
<button onClick={triggerAIAnalysis} disabled={analysisState.isRunning} className="p-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded border border-zinc-700"><i data-lucide="sparkles" className="w-3 h-3"></i></button>
<button onClick={handleUndo} disabled={undoStack.length === 0} className="px-1.5 py-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded border border-zinc-700 text-[9px]"><i data-lucide="undo" className="w-3 h-3"></i></button>
<button onClick={handleRedo} disabled={redoStack.length === 0} className="px-1.5 py-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded border border-zinc-700 text-[9px]"><i data-lucide="redo" className="w-3 h-3"></i></button>
</div>
</div>
);
if (panelId === 'selection') return (
<div className="flex flex-col h-full gap-1.5">
<div className="flex items-center justify-between shrink-0 cursor-grab active:cursor-grabbing select-none" onMouseDown={e => startPanelDrag('selection', e)}>
<span className="text-[10px] text-zinc-500 font-bold uppercase flex items-center gap-1">
<i data-lucide="grip-vertical" className="w-3 h-3 text-zinc-500"></i> Selection
</span>
<button onClick={() => closePanel('selection')} className="text-zinc-600 hover:text-zinc-300"><i data-lucide="x" className="w-3 h-3"></i></button>
</div>
<div className="grid grid-cols-3 gap-1 bg-[#141414] p-1.5 rounded border border-zinc-800">
<div>
<span className="block text-[7px] text-zinc-500 font-bold uppercase mb-0.5">Start</span>
<input type="number" step="0.01" value={selectionStats.start} onChange={e => handleSelectionInputChange('start', e.target.value)}
className="w-full bg-[#242424] text-amber-400 text-center font-mono text-[9px] rounded py-0.5 border border-zinc-700 focus:outline-none" />
</div>
<div>
<span className="block text-[7px] text-zinc-500 font-bold uppercase mb-0.5">End</span>
<input type="number" step="0.01" value={selectionStats.end} onChange={e => handleSelectionInputChange('end', e.target.value)}
className="w-full bg-[#242424] text-amber-400 text-center font-mono text-[9px] rounded py-0.5 border border-zinc-700 focus:outline-none" />
</div>
<div className="flex flex-col justify-center">
<span className="text-[7px] text-zinc-500 font-bold uppercase">Len</span>
<span className="text-zinc-200 font-mono text-[9px] font-semibold mt-0.5">{selectionStats.length}s</span>
</div>
</div>
</div>
);
return null;
};
{/* [TCP] Tempo Track Row — height 40px (h-[40px]) */}
<div className="sticky top-10 z-40 flex h-[40px] border-b border-purple-500/30 bg-[#1a1a2e] p-2 flex-col justify-between border-l-4 border-purple-500 shrink-0">
<div className="flex items-center justify-between">
const renderDock = (pos, title) => {
const panels = dockPanels[pos];
if (panels.length === 0) return null;
const isSide = pos === 'left' || pos === 'right';
const sideClass = isSide ? 'w-56 shrink-0 overflow-y-auto border-zinc-800' : 'shrink-0';
const borderClass = pos === 'left' ? 'border-r' : pos === 'right' ? 'border-l' : pos === 'top' ? 'border-b' : 'border-t';
const bgClass = 'bg-[#1e1e1e]';
const highlight = panelDragRef.current && panelDropZone === pos;
return (
<div className={`${sideClass} ${borderClass} ${bgClass} flex ${isSide ? 'flex-col p-2 gap-2' : 'flex-row p-1.5 gap-3'} select-none ${highlight ? 'ring-2 ring-cyan-500 ring-inset' : ''}`}>
{panels.map(p => (
<div key={p} className={`${isSide ? 'w-full' : 'flex-1 min-w-0'} border border-zinc-700 rounded-lg bg-[#262626] shadow-sm p-2`}>
{renderPanelContent(p)}
</div>
))}
</div>
);
};
return (
<div ref={workspaceRef} className="flex-1 flex flex-col overflow-hidden select-none daw-bg relative" style={{ display: activeTab !== 'main' ? 'none' : '' }}>
{panelDragRef.current && panelDropZone && (
<div className="absolute inset-0 z-50 pointer-events-none">
{panelDropZone === 'top' && <div className="absolute top-0 left-0 right-0 h-1 bg-cyan-400 shadow-[0_0_8px_#06b6d4]"></div>}
{panelDropZone === 'bottom' && <div className="absolute bottom-0 left-0 right-0 h-1 bg-cyan-400 shadow-[0_0_8px_#06b6d4]"></div>}
{panelDropZone === 'left' && <div className="absolute top-0 bottom-0 left-0 w-1 bg-cyan-400 shadow-[0_0_8px_#06b6d4]"></div>}
{panelDropZone === 'right' && <div className="absolute top-0 bottom-0 right-0 w-1 bg-cyan-400 shadow-[0_0_8px_#06b6d4]"></div>}
</div>
)}
{/* Drag Ghost */}
{dragGhostPanel && dragGhostPos && (
<div className="fixed z-[100] pointer-events-none opacity-80 border border-cyan-500 rounded-lg bg-[#262626] shadow-2xl p-3 w-56"
style={{ left: dragGhostPos.x, top: dragGhostPos.y }}>
<div className="flex items-center gap-2 text-[10px] text-zinc-200 font-bold">
<i data-lucide="move" className="w-3.5 h-3.5 text-cyan-400"></i>
{dragGhostPanel === 'export' ? 'Export Panel' : dragGhostPanel === 'ai' ? 'AI Panel' : 'Selection Panel'}
</div>
<div className="text-[8px] text-zinc-500 mt-1">Drop at edge to dock</div>
</div>
)}
{renderDock('top', 'Top')}
<div className="flex-1 flex overflow-hidden">
{renderDock('left', 'Left')}
<div className="flex-1 flex flex-col overflow-hidden min-w-0">
<div className="flex-1 flex overflow-hidden">
{/* ══ LEFT COLUMN: TCP PANEL (fixed 300px) ══ */}
<div
ref={tcpContainerRef}
onScroll={handleTCPScroll}
className="w-[300px] shrink-0 z-30 bg-[#262626] overflow-y-scroll no-scrollbar flex flex-col border-r border-zinc-900"
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
>
{/* [TCP] Header Row */}
<div className="sticky top-0 z-50 flex items-center h-10 border-b border-zinc-900 bg-[#242424] px-2 justify-between shrink-0">
<span className="text-[10px] font-bold text-zinc-500 uppercase">Kênh</span>
<div className="flex items-center gap-1">
<span className="text-[9px] text-zinc-500 font-bold uppercase">Snap</span>
<select value={snapValue} onChange={e => setSnapValue(e.target.value)}
className="bg-zinc-850 text-zinc-300 text-[10px] px-1 py-0.5 rounded border border-zinc-800 focus:outline-none focus:border-cyan-550 font-mono">
<option value="free">Free</option><option value="1">1</option><option value="1/2">1/2</option>
<option value="1/4">1/4</option><option value="1/8">1/8</option><option value="1/16">1/16</option><option value="1/32">1/32</option>
</select>
</div>
</div>
{/* Tempo Track Row */}
<div className="sticky top-10 z-40 flex h-[40px] border-b border-purple-500/30 bg-[#1a1a2e] p-2 border-l-4 border-purple-500 shrink-0">
<div className="flex items-center justify-between w-full">
<div className="flex items-center gap-2">
<span className="text-[10px] font-bold text-purple-400 font-mono">TM</span>
<span className="text-xs font-semibold text-zinc-300">Tempo Track</span>
<span className="text-xs font-semibold text-zinc-300">Tempo</span>
</div>
<div className="flex items-center gap-1">
<input
type="number"
value={bpm}
onChange={(e) => setBpm(e.target.value)}
<input type="number" value={bpm} onChange={e => setBpm(e.target.value)}
onBlur={() => localStorage.setItem('studio_bpm', bpm)}
className="w-12 bg-zinc-800 border border-zinc-700 rounded text-[10px] text-zinc-300 text-center font-mono focus:outline-none focus:border-purple-500"
min="40"
max="300"
/>
className="w-12 bg-zinc-800 border border-zinc-700 rounded text-[10px] text-zinc-300 text-center font-mono focus:outline-none focus:border-purple-500" min="40" max="300" />
<span className="text-[9px] text-zinc-500">BPM</span>
</div>
</div>
</div>
{/* [TCP] Dynamic Track Controls */}
{/* Dynamic Track Controls */}
<div className="flex-1 flex flex-col divide-y divide-[#141414] bg-[#262626]">
{tracks.map((track, idx) => {
const isSelected = selectedTrackId === track.id;
return (
<div
key={track.id}
style={{ height: `${track.height || 96}px` }}
<div key={track.id} style={{ height: `${track.height || 96}px` }}
className={`shrink-0 relative flex flex-col justify-between p-2.5 bg-[#1e1e1e] border-r border-zinc-900 cursor-pointer border-l-4 border-b border-[#141414] overflow-hidden ${isSelected ? 'border-cyan-500 bg-[#252525]' : 'border-transparent hover:bg-zinc-800/20'}`}
onClick={() => setSelectedTrackId(track.id)}
>
onClick={() => setSelectedTrackId(track.id)}>
<div className="flex items-start justify-between">
<div className="flex items-center gap-2">
<span className="text-[10px] font-bold text-zinc-500 font-mono">{(idx+1).toString().padStart(2, '0')}</span>
<div className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: track.color }} />
<span className="text-xs font-semibold text-zinc-300 truncate max-w-[100px]" title={track.name}>
{track.name}
</span>
<span className="text-xs font-semibold text-zinc-300 truncate max-w-[100px]" title={track.name}>{track.name}</span>
</div>
<div className="flex items-center gap-1">
<button
onClick={(e) => { e.stopPropagation(); toggleTrackMute(track.id); }}
className={`px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition ${
track.muted
? 'bg-red-950 text-red-400 border-red-700'
: 'bg-zinc-800 text-zinc-400 border-transparent hover:text-zinc-200'
}`}
>M</button>
<button
onClick={(e) => { e.stopPropagation(); toggleTrackSoloEvaluate(track.id); }}
className={`px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition ${
soloedTrackId === track.id || track.solo
? 'bg-amber-950 text-amber-400 border-amber-600'
: 'bg-zinc-800 text-zinc-400 border-transparent hover:text-zinc-200'
}`}
title="Solo nghe thử"
>S</button>
<button
onClick={(e) => { e.stopPropagation(); deleteTrack(track.id); }}
className="p-0.5 rounded text-red-500 hover:bg-red-950/40 hover:text-red-400 transition flex items-center justify-center"
title="Xóa Track"
>
<i data-lucide="trash-2" className="w-3.5 h-3.5"></i>
</button>
<button onClick={e => { e.stopPropagation(); toggleTrackMute(track.id); }}
className={`px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition ${track.muted ? 'bg-red-950 text-red-400 border-red-700' : 'bg-zinc-800 text-zinc-400 border-transparent hover:text-zinc-200'}`}>M</button>
<button onClick={e => { e.stopPropagation(); toggleTrackSoloEvaluate(track.id); }}
className={`px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition ${soloedTrackId === track.id || track.solo ? 'bg-amber-950 text-amber-400 border-amber-600' : 'bg-zinc-800 text-zinc-400 border-transparent hover:text-zinc-200'}`}>S</button>
<button onClick={e => { e.stopPropagation(); deleteTrack(track.id); }}
className="p-0.5 rounded text-red-500 hover:bg-red-950/40 hover:text-red-400 transition"><i data-lucide="trash-2" className="w-3.5 h-3.5"></i></button>
</div>
</div>
<div className="flex items-center gap-1 text-[9px] text-zinc-400" onClick={e => e.stopPropagation()}>
<span className="font-semibold uppercase text-[8px] text-zinc-500">Mô phỏng:</span>
<button
onClick={() => generateSynthToTrack(track.id, 'kick')}
className="px-1 bg-zinc-800 hover:bg-zinc-700 rounded border border-zinc-700"
>Kick</button>
<button
onClick={() => generateSynthToTrack(track.id, 'synth')}
className="px-1 bg-zinc-800 hover:bg-zinc-700 rounded border border-zinc-700"
>Synth</button>
<span className="font-semibold uppercase text-[8px] text-zinc-500">Synth:</span>
<button onClick={() => generateSynthToTrack(track.id, 'kick')} className="px-1 bg-zinc-800 hover:bg-zinc-700 rounded border border-zinc-700">Kick</button>
<button onClick={() => generateSynthToTrack(track.id, 'synth')} className="px-1 bg-zinc-800 hover:bg-zinc-700 rounded border border-zinc-700">Synth</button>
</div>
<div className="flex items-center justify-between gap-2" onClick={e => e.stopPropagation()}>
<div className="flex items-center gap-1.5">
<VolumeKnob value={track.volume} onChange={(v) => updateTrackVolume(track.id, v)} />
<VolumeKnob value={track.volume} onChange={v => updateTrackVolume(track.id, v)} />
<span className="text-[10px] font-mono text-zinc-500">Gain: {Math.round(track.volume * 100)}%</span>
</div>
<div>
<input
type="file"
id={`upload-${track.id}`}
accept="audio/*"
className="hidden"
onChange={(e) => loadFileOnTrack(track.id, e.target.files[0])}
/>
<label
htmlFor={`upload-${track.id}`}
className="px-2 py-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded text-[10px] flex items-center gap-1 cursor-pointer transition border border-zinc-700"
>
<i data-lucide="upload" className="w-3 h-3"></i> Tải file
</label>
<input type="file" id={`upload-${track.id}`} accept="audio/*" className="hidden" onChange={e => loadFileOnTrack(track.id, e.target.files[0])} />
<label htmlFor={`upload-${track.id}`} className="px-2 py-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded text-[10px] flex items-center gap-1 cursor-pointer border border-zinc-700"><i data-lucide="upload" className="w-3 h-3"></i> File</label>
</div>
</div>
{/* Resize handle at bottom of TCP */}
<div
onMouseDown={(e) => handleTrackResizeMouseDown(e, track.id)}
className="absolute bottom-0 left-0 right-0 h-1 cursor-ns-resize z-30 hover:bg-cyan-500/50 transition-colors"
onClick={e => e.stopPropagation()}
/>
<div onMouseDown={e => handleTrackResizeMouseDown(e, track.id)}
className="absolute bottom-0 left-0 right-0 h-1 cursor-ns-resize z-30 hover:bg-cyan-500/50 transition-colors" onClick={e => e.stopPropagation()} />
</div>
);
})}
</div>
{/* [TCP] Bottom Drop Zone spacer */}
<div className="h-[48px] border-r border-zinc-900 bg-[#1e1e1e]/50 shrink-0" />
</div>
{/* ══ RIGHT COLUMN: TIMELINE SCROLL VIEWPORT ══ */}
<div
ref={timelineWrapperRef}
onScroll={handleTimelineScroll}
className="flex-1 relative z-10 overflow-x-auto overflow-y-auto min-w-0"
>
<div ref={timelineWrapperRef} onScroll={handleTimelineScroll}
className="flex-1 relative z-10 overflow-x-auto overflow-y-auto min-w-0">
<div style={{ width: `${timelineWidth}px` }} className="relative flex flex-col min-h-full">
{/* [ZONE A] Time Ruler — height 40px (h-10) */}
<div className="sticky top-0 z-45 flex h-10 border-b border-zinc-900 bg-[#242424] shrink-0">
<div ref={rulerRef} className="flex-1 relative h-full flex items-center select-none cursor-ew-resize overflow-hidden" onMouseDown={handleRulerMouseDown}>
{Array.from({ length: Math.ceil(maxDuration) }).map((_, i) => {
const sec = i;
const x = sec * zoom;
return (
<div key={i} className="absolute h-full border-l border-zinc-800 pl-1 pt-1 text-[9px] font-mono text-zinc-500 pointer-events-none" style={{ left: `${x}px` }}>
{formatTime(sec)}
</div>
);
const sec = i; const x = sec * zoom;
return (<div key={i} className="absolute h-full border-l border-zinc-800 pl-1 pt-1 text-[9px] font-mono text-zinc-500 pointer-events-none" style={{ left: `${x}px` }}>{formatTime(sec)}</div>);
})}
</div>
</div>
{/* [ZONE A] Tempo Lane — height 40px (h-[40px]) */}
<div className="sticky top-10 z-35 flex h-[40px] border-b border-purple-500/30 bg-[#1a1a2e] shrink-0">
<div className="flex-1 relative h-full overflow-hidden bg-[#1a1a2e]">
<TempoTrackLane bpm={parseInt(bpm) || 120} zoom={zoom} timelineWidth={timelineWidth}
onPlayheadSet={setCurrentTime} snapValue={snapValue} />
</div>
</div>
{/* [ZONE B] Dynamic Waveform Lanes */}
<div className="flex-1 flex flex-col divide-y divide-[#141414] relative bg-[#111111] min-h-full">
{tracks.map((track, idx) => {
const isSelected = selectedTrackId === track.id;
return (
<div
key={track.id}
style={{ height: `${track.height || 96}px` }}
<div key={track.id} style={{ height: `${track.height || 96}px` }}
className={`shrink-0 relative overflow-hidden border-b border-[#141414] hover:bg-zinc-850/5 transition-colors ${isSelected ? 'bg-zinc-800/10' : ''}`}
onDragOver={(e) => e.preventDefault()}
onDrop={(e) => {
e.preventDefault();
if (e.dataTransfer.files[0]) {
loadFileOnTrack(track.id, e.dataTransfer.files[0]);
}
}}
onMouseEnter={() => setHoveredTrackId(track.id)}
>
onDragOver={e => e.preventDefault()}
onDrop={e => { e.preventDefault(); if (e.dataTransfer.files[0]) loadFileOnTrack(track.id, e.dataTransfer.files[0]); }}
onMouseEnter={() => setHoveredTrackId(track.id)}>
<WaveformLane track={track} zoom={zoom} timelineWidth={timelineWidth}
onSelectRange={handleSelectRange} onPlayheadSet={setCurrentTime}
isSelected={isSelected} onSelectTrack={setSelectedTrackId} markers={track.markers}
@@ -4348,208 +4463,52 @@
activeTool={activeTool}
onSplitTrackAtTime={handleSplitTrackAtTime}
onEditClipInSubTab={handleEditClipInSubTab}
snapValue={snapValue}
bpm={bpm}
snapValue={snapValue} bpm={bpm}
selectionMode={selectionMode}
localSelectionTrackId={localSelectionTrackId}
localSelLeft={localSelectionStart !== null && localSelectionEnd !== null ? Math.min(localSelectionStart, localSelectionEnd) : null}
localSelRight={localSelectionStart !== null && localSelectionEnd !== null ? Math.max(localSelectionStart, localSelectionEnd) : null} />
{track.buffer && (
<div className="absolute right-2 top-2 flex items-center gap-1 z-10 opacity-70 hover:opacity-100 transition">
<div className="absolute right-2 top-2 flex items-center gap-1 z-10 opacity-70 hover:opacity-100">
<button onClick={() => handleSplitTrack(track.id)}
className="px-1.5 py-0.5 bg-zinc-900/95 text-zinc-300 rounded text-[9px] flex items-center gap-1 border border-zinc-700/50"
title="Cắt đoạn tại Playhead">
<i data-lucide="scissors" className="w-2.5 h-2.5 text-cyan-400"></i> Cắt
</button>
className="px-1.5 py-0.5 bg-zinc-900/95 text-zinc-300 rounded text-[9px] flex items-center gap-1 border border-zinc-700/50">
<i data-lucide="scissors" className="w-2.5 h-2.5 text-cyan-400"></i> Cắt</button>
</div>
)}
{/* Resize handle at bottom of Waveform Lane */}
<div
onMouseDown={(e) => handleTrackResizeMouseDown(e, track.id)}
className="absolute bottom-0 left-0 right-0 h-1 cursor-ns-resize z-30 hover:bg-cyan-500/50 transition-colors"
onClick={e => e.stopPropagation()}
/>
<div onMouseDown={e => handleTrackResizeMouseDown(e, track.id)}
className="absolute bottom-0 left-0 right-0 h-1 cursor-ns-resize z-30 hover:bg-cyan-500/50 transition-colors" onClick={e => e.stopPropagation()} />
</div>
);
})}
{/* Bottom Drop Zone to create new track */}
<div
className="h-[48px] flex items-center justify-center text-xs text-zinc-500 hover:bg-zinc-900/20 cursor-pointer select-none border-t border-dashed border-zinc-800"
onMouseEnter={() => {
if (draggedClipRef.current) {
const newId = addNewTrack();
setHoveredTrackId(newId);
}
}}
onClick={addNewTrack}
>
<span className="flex items-center gap-1 text-zinc-400">
<i data-lucide="plus" className="w-3.5 h-3.5"></i> Kéo clip xuống đây hoặc Click để tạo Track mới
</span>
<div className="h-[48px] flex items-center justify-center text-xs text-zinc-500 hover:bg-zinc-900/20 cursor-pointer select-none border-t border-dashed border-zinc-800"
onMouseEnter={() => { if (draggedClipRef.current) { setHoveredTrackId(addNewTrack()); } }}
onClick={addNewTrack}>
<span className="flex items-center gap-1 text-zinc-400"><i data-lucide="plus" className="w-3.5 h-3.5"></i> Kéo clip xuống hoặc Click tạo Track</span>
</div>
{/* Selection Overlay */}
{selectionMode !== 'local' && selLeft !== null && selRight !== null && selRight > selLeft && (
<div className="absolute top-0 bottom-0 border-l border-r border-amber-500 bg-amber-500/10 z-20 selection-interactive-box cursor-grab active:cursor-grabbing"
style={{ left: `${selLeft * zoom}px`, width: `${(selRight - selLeft) * zoom}px` }}
onMouseDown={handleSelectionBodyDragStart}
title="Kéo để di chuyển vùng chọn"
>
<div className="absolute -left-1.5 top-0 bottom-0 w-3 bg-amber-500 hover:bg-amber-400 cursor-ew-resize flex items-center justify-center z-30 transition-colors"
onMouseDown={(e) => handleHandleDragStart(e, 'left')}
title="Kéo giãn mốc bắt đầu">
<div className="w-[1.5px] h-4 bg-zinc-950/70 rounded"></div>
</div>
<div className="absolute -right-1.5 top-0 bottom-0 w-3 bg-amber-500 hover:bg-amber-400 cursor-ew-resize flex items-center justify-center z-30 transition-colors"
onMouseDown={(e) => handleHandleDragStart(e, 'right')}
title="Kéo giãn mốc kết thúc">
<div className="w-[1.5px] h-4 bg-zinc-950/70 rounded"></div>
</div>
onMouseDown={handleSelectionBodyDragStart}>
<div className="absolute -left-1.5 top-0 bottom-0 w-3 bg-amber-500 hover:bg-amber-400 cursor-ew-resize flex items-center justify-center z-30"
onMouseDown={e => handleHandleDragStart(e, 'left')}><div className="w-[1.5px] h-4 bg-zinc-950/70 rounded"></div></div>
<div className="absolute -right-1.5 top-0 bottom-0 w-3 bg-amber-500 hover:bg-amber-400 cursor-ew-resize flex items-center justify-center z-30"
onMouseDown={e => handleHandleDragStart(e, 'right')}><div className="w-[1.5px] h-4 bg-zinc-950/70 rounded"></div></div>
</div>
)}
{/* Playhead */}
<div className="absolute top-0 bottom-0 w-[2px] bg-[#ef4444] z-20 pointer-events-none"
style={{ left: `${playheadLeftPos}px` }}>
<div className="absolute top-0 bottom-0 w-[2px] bg-[#ef4444] z-20 pointer-events-none" style={{ left: `${playheadLeftPos}px` }}>
<div className="w-3 h-3 bg-[#ef4444] rotate-45 transform -translate-x-1/2 -translate-y-1/2 absolute top-0"></div>
</div>
</div>
</div>
</div>
</div>
{/* ── Footer ── */}
<footer className="h-44 bg-[#1c1c1c] border-t border-zinc-900 p-4 grid grid-cols-1 md:grid-cols-12 gap-4 text-xs shrink-0 select-none" style={{ display: activeTab !== 'main' ? 'none' : '' }}>
{/* Export Section */}
<section className="md:col-span-4 bg-[#262626] border border-zinc-800 rounded p-3 flex flex-col justify-between">
<div>
<h3 className="font-bold text-zinc-200 flex items-center gap-1.5 mb-1.5">
<i data-lucide="save" className="w-4 h-4 text-cyan-400"></i> Trộn & Xuất bản
</h3>
<div className="grid grid-cols-3 gap-2 mt-1">
<div>
<label className="block text-[8px] text-zinc-500 font-bold uppercase mb-0.5">Sample Rate</label>
<select value={exportSettings.sampleRate}
onChange={(e) => setExportSettings(prev => ({ ...prev, sampleRate: e.target.value }))}
className="w-full bg-[#141414] border border-zinc-800 rounded px-1.5 py-1 text-[10px] text-zinc-300 focus:outline-none">
<option value="22500">22.5 kHz</option>
<option value="44100">44.1 kHz</option>
<option value="48000">48.0 kHz</option>
</select>
</div>
<div>
<label className="block text-[8px] text-zinc-500 font-bold uppercase mb-0.5">Bit Depth</label>
<select value={exportSettings.bitDepth}
onChange={(e) => setExportSettings(prev => ({ ...prev, bitDepth: e.target.value }))}
className="w-full bg-[#141414] border border-zinc-800 rounded px-1.5 py-1 text-[10px] text-zinc-300 focus:outline-none">
<option value="8">8-bit</option>
<option value="16">16-bit</option>
<option value="24">24-bit</option>
</select>
{renderDock('right', 'Right')}
</div>
<div>
<label className="block text-[8px] text-zinc-500 font-bold uppercase mb-0.5">Mã Hóa</label>
<select value={exportSettings.format}
onChange={(e) => setExportSettings(prev => ({ ...prev, format: e.target.value }))}
className="w-full bg-[#141414] border border-zinc-800 rounded px-1.5 py-1 text-[10px] text-zinc-300 focus:outline-none">
<option value="wav">WAV (.wav)</option>
<option value="mp3">MP3 (.mp3)</option>
<option value="ogg">OGG (.ogg)</option>
</select>
{renderDock('bottom', 'Bottom')}
</div>
</div>
</div>
<button onClick={triggerWavExport} disabled={isExporting}
className="w-full py-1.5 mt-2 bg-cyan-700 hover:bg-cyan-600 active:bg-cyan-800 text-zinc-100 font-bold rounded flex items-center justify-center gap-1.5 transition text-xs shadow-md">
<i data-lucide="download-cloud" className="w-4 h-4"></i>
{isExporting ? 'Đang xuất...' : 'Xuất bản bản phối'}
</button>
</section>
{/* Selection Info Section */}
<section className="md:col-span-4 bg-[#242424] border border-zinc-800 rounded p-3 flex flex-col justify-between items-center text-center">
<div className="flex w-full items-center justify-between">
<span className="text-[10px] text-zinc-500 font-bold uppercase tracking-wider">Selection Info</span>
<div className="text-sm font-bold font-mono text-zinc-100">{formatTime(currentTime)}</div>
</div>
<div className="grid grid-cols-3 gap-2 w-full mt-1 bg-[#141414] p-1.5 rounded border border-zinc-800">
<div>
<span className="block text-[8px] text-zinc-500 font-bold uppercase mb-0.5">Vùng Chọn Đầu</span>
<input type="number" step="0.01" value={selectionStats.start}
onChange={(e) => handleSelectionInputChange('start', e.target.value)}
className="w-full bg-[#242424] text-amber-400 text-center font-mono text-[11px] rounded py-0.5 border border-zinc-700 focus:outline-none" />
</div>
<div>
<span className="block text-[8px] text-zinc-500 font-bold uppercase mb-0.5">Vùng Chọn Cuối</span>
<input type="number" step="0.01" value={selectionStats.end}
onChange={(e) => handleSelectionInputChange('end', e.target.value)}
className="w-full bg-[#242424] text-amber-400 text-center font-mono text-[11px] rounded py-0.5 border border-zinc-700 focus:outline-none" />
</div>
<div className="flex flex-col justify-center">
<span className="text-[8px] text-zinc-500 font-bold uppercase">Thời Lượng</span>
<span className="text-zinc-200 font-mono text-[11px] font-semibold mt-0.5">{selectionStats.length}s</span>
</div>
</div>
</section>
{/* AI Analysis & Edit Section */}
<section className="md:col-span-4 bg-[#262626] border border-zinc-800 rounded p-3 flex flex-col justify-between">
<div>
<h3 className="font-bold text-zinc-200 flex items-center gap-1.5 mb-1">
<i data-lucide="cpu" className="text-purple-400 w-4 h-4"></i> Edit & AI Engine
</h3>
<div className="p-1.5 bg-[#141414] rounded border border-zinc-800 text-[10px] font-mono min-h-[46px] flex flex-col justify-center">
<div className="text-zinc-500">// Status: <span className="text-zinc-300">{analysisState.status}</span></div>
{analysisState.data && (
<div className="text-emerald-500 font-semibold mt-0.5">
BPM: {analysisState.data.bpm} | Bars: {analysisState.data.bars}
</div>
)}
</div>
</div>
<div className="flex gap-2 mt-1.5">
<button onClick={handleMarkSelection}
className="flex-1 py-1 px-1 bg-purple-900 hover:bg-purple-800 text-purple-100 font-bold rounded flex items-center justify-center gap-1 border border-purple-700 transition"
title="Tạo Marker">
<i data-lucide="map-pin" className="w-3.5 h-3.5"></i> Mark
</button>
<button onClick={handleAICutToNewTrack} disabled={analysisState.isRunning}
className="flex-[2] py-1 px-1 bg-purple-700 hover:bg-purple-600 text-white font-bold rounded flex items-center justify-center gap-1 transition shadow-md"
title="AI Zero-crossing Cut">
<i data-lucide="scissors" className="w-3.5 h-3.5"></i> AI Cut
</button>
<button onClick={triggerAIAnalysis} disabled={analysisState.isRunning}
className="p-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded border border-zinc-700"
title="AI Analysis">
<i data-lucide="sparkles" className="w-4 h-4"></i>
</button>
</div>
<div className="flex gap-2 mt-1">
<button onClick={openTempTab}
className="flex-1 py-1 px-1 bg-amber-800 hover:bg-amber-700 text-amber-100 font-bold rounded flex items-center justify-center gap-1 border border-amber-700 transition text-[11px]"
title={selLeft !== null ? "Edit in Temp Tab (LOOP_EDITOR.md §2)" : "Select a region first"}>
<i data-lucide="file-edit" className="w-3.5 h-3.5"></i> Edit in Temp Tab
</button>
<button onClick={handleUndo} disabled={undoStack.length === 0}
className="px-2 py-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded border border-zinc-700 transition text-[11px]"
title="Undo (Ctrl+Z)">
<i data-lucide="undo" className="w-3.5 h-3.5"></i>
</button>
<button onClick={handleRedo} disabled={redoStack.length === 0}
className="px-2 py-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded border border-zinc-700 transition text-[11px]"
title="Redo (Ctrl+Y)">
<i data-lucide="redo" className="w-3.5 h-3.5"></i>
</button>
<span className="text-[9px] text-zinc-600 flex items-center font-mono">{undoStack.length}/{MAX_UNDO}</span>
</div>
</section>
</footer>
);
})()}
{/* ── Status Bar ── */}
<div className="h-6 bg-[#141414] border-t border-zinc-900 px-4 flex items-center justify-between text-[10px] text-zinc-500 select-none shrink-0" style={{ display: activeTab !== 'main' ? 'none' : '' }}>
@@ -4562,7 +4521,26 @@
{isLoopingSelection && selectionMode === 'local' && <span className="text-emerald-400 font-semibold uppercase text-[9px]">Solo Loop</span>}
{isLoopingSelection && selectionMode !== 'local' && <span className="text-cyan-400 font-semibold uppercase text-[9px]">Master Loop</span>}
</div>
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
<button onClick={() => setShowExportPanel(p => !p)}
className={`px-1.5 py-0.5 rounded text-[9px] font-semibold transition flex items-center gap-1 ${showExportPanel ? 'bg-cyan-900 text-cyan-300' : 'text-zinc-500 hover:text-zinc-300'}`}
title={`Export Panel (${panelPositions.export})`}>
<i data-lucide="save" className="w-3 h-3"></i>
<span className="text-[7px] opacity-60">{showExportPanel ? panelPositions.export[0].toUpperCase() : ''}</span>
</button>
<button onClick={() => setShowSelectionPanel(p => !p)}
className={`px-1.5 py-0.5 rounded text-[9px] font-semibold transition flex items-center gap-1 ${showSelectionPanel ? 'bg-amber-900 text-amber-300' : 'text-zinc-500 hover:text-zinc-300'}`}
title={`Selection Panel (${panelPositions.selection})`}>
<i data-lucide="sliders" className="w-3 h-3"></i>
<span className="text-[7px] opacity-60">{showSelectionPanel ? panelPositions.selection[0].toUpperCase() : ''}</span>
</button>
<button onClick={() => setShowAIPanel(p => !p)}
className={`px-1.5 py-0.5 rounded text-[9px] font-semibold transition flex items-center gap-1 ${showAIPanel ? 'bg-purple-900 text-purple-300' : 'text-zinc-500 hover:text-zinc-300'}`}
title={`AI Panel (${panelPositions.ai})`}>
<i data-lucide="cpu" className="w-3 h-3"></i>
<span className="text-[7px] opacity-60">{showAIPanel ? panelPositions.ai[0].toUpperCase() : ''}</span>
</button>
<span className="w-[1px] h-3 bg-zinc-800 mx-1"></span>
<span className="flex items-center gap-1">
<i data-lucide="info" className="w-3 h-3 text-zinc-600"></i> Scroll: Zoom
</span>