bug: lỗi scrollbar ngang vẫn tồn tại

This commit is contained in:
2026-07-18 21:45:38 +07:00
parent 50bb87d50a
commit d408f30b82
2 changed files with 304 additions and 1 deletions
+1 -1
View File
@@ -2918,7 +2918,7 @@
{/* Left Column: TCP */}
<div
onClick={() => setSelectedTrackId(track.id)}
className={`w-[300px] sticky left-0 z-10 p-2.5 flex flex-col justify-between bg-[#1e1e1e] border-r border-zinc-900 shrink-0 cursor-pointer border-l-4 overflow-hidden ${isSelected ? 'border-cyan-500 bg-[#252525]' : 'border-transparent hover:bg-zinc-800/20'}`}
className={`w-[300px] sticky left-0 z-30 p-2.5 flex flex-col justify-between bg-[#1e1e1e] border-r border-zinc-900 shrink-0 cursor-pointer border-l-4 overflow-hidden ${isSelected ? 'border-cyan-500 bg-[#252525]' : 'border-transparent hover:bg-zinc-800/20'}`}
>
<div className="flex items-start justify-between">
<div className="flex items-center gap-2">
+303
View File
@@ -0,0 +1,303 @@
<div
className="flex-1 flex overflow-hidden select-none daw-bg relative"
style={{ display: activeTab !== 'main' ? 'none' : '' }}
>
{/* ══ LEFT COLUMN: TCP PANEL ══ */}
<div
ref={tcpContainerRef}
className="w-[300px] shrink-0 relative z-20 bg-[#262626] overflow-hidden flex flex-col border-r border-zinc-900"
>
<span className="text-[10px] font-bold text-zinc-500 uppercase shrink-0 mr-2">Kênh</span>
{/* Timeline Toolbar */}
<div className="flex items-center gap-0.5 bg-zinc-900 border border-zinc-800 rounded px-1 py-0.5 shadow-sm">
<button
onClick={() => { setActiveTool('select'); showToast('Công cụ chọn (Select Tool) đã kích hoạt.', 'info'); }}
className={`p-1 rounded transition text-xs 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 (Giữ Alt kéo để di chuyển nhanh clip)"
>
<i data-lucide="mouse-pointer" className="w-3 h-3"></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-xs 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-3 h-3"></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-xs 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-3 h-3 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>
{/* Separator */}
<div className="w-[1px] h-3 bg-zinc-800 mx-0.5"></div>
{/* Quick Actions (Glue, Cut, Copy, Paste) */}
<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"
>
<i data-lucide="link" className="w-3 h-3"></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)"
>
<i data-lucide="scissors" className="w-3 h-3"></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)"
>
<i data-lucide="copy" className="w-3 h-3"></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)"
>
<i data-lucide="clipboard" className="w-3 h-3"></i>
</button>
</div>
{/* Snap Section */}
<div className="w-[1px] h-3 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>
</select>
</div>
</div>
{/* Right Side: Time Ruler */}
<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>
);
})}
</div>
</div>
{/* [ZONE A] Tempo Track Row */}
<div className="sticky top-8 z-30 flex h-[40px] border-b border-purple-500/30 bg-[#1a1a2e] shrink-0">
{/* Left Side: Tempo TCP */}
<div className="w-[300px] sticky left-0 z-50 bg-[#1a1a2e] border-r border-zinc-900 p-2 flex flex-col justify-between border-l-4 border-purple-500 shrink-0">
<div className="flex items-center justify-between">
<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>
</div>
<div className="flex items-center gap-1">
<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"
/>
<span className="text-[9px] text-zinc-500">BPM</span>
</div>
</div>
</div>
{/* Right Side: Tempo Lane */}
<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 Track List Workspace */}
<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} className={`h-[96px] flex hover:bg-zinc-850/5 transition-colors ${isSelected ? 'bg-zinc-800/10' : ''}`}>
{/* Left Column: TCP */}
<div
onClick={() => setSelectedTrackId(track.id)}
className={`w-[300px] sticky left-0 z-10 p-2.5 flex flex-col justify-between bg-[#1e1e1e] border-r border-zinc-900 shrink-0 cursor-pointer border-l-4 overflow-hidden ${isSelected ? 'border-cyan-500 bg-[#252525]' : 'border-transparent hover:bg-zinc-800/20'}`}
>
<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-[120px]" 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>
</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 Drum</button>
<button
onClick={() => generateSynthToTrack(track.id, 'synth')}
className="px-1 bg-zinc-800 hover:bg-zinc-700 rounded border border-zinc-700"
>Arpeggiator</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)} />
<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>
</div>
</div>
</div>
{/* Right Column: Waveform Lane */}
<div
className="flex-1 relative overflow-hidden h-full"
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}
onTrackLaneMouseDown={handleTrackLaneMouseDown}
onContextMenu={handleContextMenu}
onClipDragStart={handleClipDragStart}
activeTool={activeTool}
onSplitTrackAtTime={handleSplitTrackAtTime}
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">
<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>
</div>
)}
</div>
</div>
);
})}
{/* Bottom Drop Zone to create new track */}
<div className="h-[48px] flex border-t border-dashed border-zinc-800">
<div className="w-[300px] sticky left-0 z-10 bg-[#1e1e1e]/50 border-r border-zinc-900 shrink-0"></div>
<div
className="flex-1 flex items-center justify-center text-xs text-zinc-500 hover:bg-zinc-900/20 cursor-pointer select-none"
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>
</div>
{/* Selection Overlay - only for Global mode (LOOP_EDITOR.md §1.2: local draws on canvas per-track) */}
{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 + 300}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>
</div>
)}
{/* Playhead */}
<div className="absolute top-0 bottom-0 w-[2px] bg-[#ef4444] z-20 pointer-events-none"
style={{ left: `${playheadLeftPos + 300}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>
{/* ── Footer ── */}