fix: subtab là một phần ui của main session
This commit is contained in:
+124
-136
@@ -723,7 +723,7 @@
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// Draw local playhead
|
||||
// Draw playhead
|
||||
if (currentTime !== null && currentTime >= 0 && currentTime <= buffer.duration) {
|
||||
const playheadPx = (currentTime / buffer.duration) * w;
|
||||
ctx.strokeStyle = '#ef4444';
|
||||
@@ -733,6 +733,7 @@
|
||||
ctx.lineTo(playheadPx, h);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
}, [buffer, currentTime, selectionStart, selectionEnd]);
|
||||
|
||||
const handleMouseDown = (e) => {
|
||||
@@ -2370,7 +2371,7 @@
|
||||
|
||||
timeline.addEventListener('wheel', handleWheel, { passive: false });
|
||||
return () => timeline.removeEventListener('wheel', handleWheel);
|
||||
}, [zoom, minZoom, maxDuration]);
|
||||
}, [zoom, minZoom, maxDuration, activeTab]);
|
||||
|
||||
// ── Update Playhead ──
|
||||
const updatePlayhead = () => {
|
||||
@@ -4102,8 +4103,8 @@
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── 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' : '' }}>
|
||||
{/* ── Transport Bar + Toolbar Overlay ── */}
|
||||
<div className="h-9 bg-[#222] border-b border-zinc-800 flex items-center px-3 gap-2 shrink-0 select-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' }}>
|
||||
@@ -4208,7 +4209,7 @@
|
||||
</div>
|
||||
|
||||
{/* ── 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);
|
||||
@@ -4337,7 +4338,7 @@
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={workspaceRef} className="flex-1 flex flex-col overflow-hidden select-none daw-bg relative" style={{ display: activeTab !== 'main' ? 'none' : '' }}>
|
||||
<div ref={workspaceRef} className="flex-1 flex flex-col overflow-hidden select-none daw-bg relative">
|
||||
{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>}
|
||||
@@ -4362,26 +4363,25 @@
|
||||
{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}
|
||||
{activeTab === 'main' ? (
|
||||
<>
|
||||
{/* ══ LEFT COLUMN: TCP PANEL (main session, all tracks) ══ */}
|
||||
<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 */}
|
||||
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}>
|
||||
<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>
|
||||
<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">
|
||||
@@ -4396,7 +4396,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* 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;
|
||||
@@ -4442,7 +4441,7 @@
|
||||
</div>
|
||||
<div className="h-[48px] border-r border-zinc-900 bg-[#1e1e1e]/50 shrink-0" />
|
||||
</div>
|
||||
{/* ══ RIGHT COLUMN: TIMELINE SCROLL VIEWPORT ══ */}
|
||||
{/* ══ RIGHT COLUMN: TIMELINE (main session, all tracks) ══ */}
|
||||
<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">
|
||||
@@ -4489,8 +4488,7 @@
|
||||
{track.buffer && (
|
||||
<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">
|
||||
<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>
|
||||
)}
|
||||
<div onMouseDown={e => handleTrackResizeMouseDown(e, track.id)}
|
||||
@@ -4519,6 +4517,110 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (() => {
|
||||
const st = subTabs.find(s => s.id === activeTab);
|
||||
if (!st) return null;
|
||||
const subTrack = tracks.find(t => t.id === st.trackId);
|
||||
const vTrack = subTrack ? { ...subTrack, buffer: st.buffer, isSubTab: true } : null;
|
||||
return (
|
||||
<>
|
||||
{/* ══ TCP PANEL (sub-tab, single track) ══ */}
|
||||
<div 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' }}>
|
||||
<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">Sub-Tab</span>
|
||||
<button onClick={() => closeSubTab(st.id)}
|
||||
className="px-1.5 py-0.5 text-[9px] bg-zinc-800 hover:bg-zinc-700 text-zinc-400 rounded border border-zinc-700 flex items-center gap-1">
|
||||
<i data-lucide="x" className="w-3 h-3"></i> Close
|
||||
</button>
|
||||
</div>
|
||||
{vTrack ? (
|
||||
<div key={vTrack.id} className="flex-1 flex flex-col p-2.5 bg-[#1e1e1e] border-r border-zinc-900 border-l-4 border-l-cyan-500">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: vTrack.color }} />
|
||||
<span className="text-xs font-semibold text-zinc-300 truncate max-w-[100px]">{vTrack.name}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<button onClick={e => { e.stopPropagation(); toggleTrackMute(vTrack.id); }}
|
||||
className={`px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border ${vTrack.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(vTrack.id); }}
|
||||
className={`px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border ${soloedTrackId === vTrack.id || vTrack.solo ? 'bg-amber-950 text-amber-400 border-amber-600' : 'bg-zinc-800 text-zinc-400 border-transparent hover:text-zinc-200'}`}>S</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 mb-2">
|
||||
<VolumeKnob value={vTrack.volume} onChange={v => updateTrackVolume(vTrack.id, v)} />
|
||||
<span className="text-[10px] font-mono text-zinc-500">Gain: {Math.round(vTrack.volume * 100)}%</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-[9px] text-zinc-400">
|
||||
<span className="text-[8px] text-zinc-500 font-semibold uppercase">Duration:</span>
|
||||
<span className="font-mono text-zinc-300">{st.buffer ? formatTime(st.buffer.duration) : '0s'}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-[9px] text-zinc-400">
|
||||
<span className="text-[8px] text-zinc-500 font-semibold uppercase">SR:</span>
|
||||
<span className="font-mono text-zinc-300">{st.buffer ? st.buffer.sampleRate : 0} Hz</span>
|
||||
</div>
|
||||
<div className="flex-1"></div>
|
||||
<div className="flex gap-1 mt-2">
|
||||
<button onClick={() => applySubTab(st.id)}
|
||||
className="flex-1 py-1 bg-amber-700 hover:bg-amber-600 text-white font-bold rounded text-[9px] flex items-center justify-center gap-1">
|
||||
<i data-lucide="check" className="w-3 h-3"></i> Apply
|
||||
</button>
|
||||
<button onClick={() => updateSubTabEffects(st.id, { reverse: !((st.effects || {}).reverse) })}
|
||||
className={`p-1 rounded text-[9px] border ${(st.effects || {}).reverse ? 'bg-amber-800 text-amber-100 border-amber-600' : 'bg-zinc-800 text-zinc-400 border-zinc-700'}`}>
|
||||
<i data-lucide="arrow-left-right" className="w-3 h-3"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 flex items-center justify-center text-[10px] text-zinc-500">Track not found</div>
|
||||
)}
|
||||
</div>
|
||||
{/* ══ TIMELINE (sub-tab, single track) ══ */}
|
||||
<div ref={timelineWrapperRef} 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">
|
||||
<div className="sticky top-0 z-45 flex h-10 border-b border-zinc-900 bg-[#242424] shrink-0">
|
||||
<div className="flex-1 relative h-full flex items-center select-none cursor-ew-resize overflow-hidden">
|
||||
{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>
|
||||
<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>
|
||||
<div className="flex-1 flex flex-col relative bg-[#111111] min-h-full">
|
||||
{vTrack && (
|
||||
<div className="flex-1 relative overflow-hidden border-b border-[#141414]">
|
||||
<SubTabWaveform
|
||||
buffer={st.buffer}
|
||||
subTabId={st.id}
|
||||
activeTab={activeTab}
|
||||
activeTool={activeTool}
|
||||
currentTime={st.currentTime}
|
||||
selectionStart={st.selectionStart}
|
||||
selectionEnd={st.selectionEnd}
|
||||
onSelectRange={(start, end) => setSubTabs(prev => prev.map(s => s.id === st.id ? {...s, selectionStart: start, selectionEnd: end} : s))}
|
||||
onPlayheadSet={(time) => setSubTabs(prev => prev.map(s => s.id === st.id ? {...s, currentTime: time} : s))}
|
||||
onContextMenu={(e, clickTime) => setContextMenu({x: e.clientX, y: e.clientY, isSubTab: true, subTabId: st.id, time: clickTime})}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{st.selectionStart !== null && st.selectionEnd !== null && st.selectionEnd > st.selectionStart && (
|
||||
<div className="absolute top-0 bottom-0 border-l border-r border-amber-500 bg-amber-500/10 z-20 pointer-events-none"
|
||||
style={{ left: `${Math.min(st.selectionStart, st.selectionEnd) * zoom}px`, width: `${Math.abs(st.selectionEnd - st.selectionStart) * zoom}px` }} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
{renderDock('right', 'Right')}
|
||||
@@ -4529,10 +4631,11 @@
|
||||
})()}
|
||||
|
||||
{/* ── 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' : '' }}>
|
||||
<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">
|
||||
<div className="flex items-center gap-4">
|
||||
<span>Status: {isPlaying ? 'Playing' : 'Stopped'}</span>
|
||||
<span className="text-cyan-400 font-semibold uppercase">Track: ID {selectedTrackId}</span>
|
||||
{activeTab !== 'main' && <span className="text-amber-400 font-semibold uppercase">Sub-Tab</span>}
|
||||
{activeTab === 'main' && <span className="text-cyan-400 font-semibold uppercase">Track: ID {selectedTrackId}</span>}
|
||||
{selectionMode === 'local' && <span className="text-amber-400 font-semibold uppercase text-[9px]">Local Sel</span>}
|
||||
{selectionMode === 'global' && <span className="text-purple-400 font-semibold uppercase text-[9px]">Global Sel</span>}
|
||||
{soloedTrackId && <span className="text-amber-500 font-semibold">Solo: ID {soloedTrackId}</span>}
|
||||
@@ -4569,121 +4672,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Sub-Tab Editor Panel (replaces workspace when active) ── */}
|
||||
{activeTab !== 'main' && (() => {
|
||||
const st = subTabs.find(s => s.id === activeTab);
|
||||
if (!st) return null;
|
||||
const fx = st.effects || {};
|
||||
return (
|
||||
<div className="flex-1 flex flex-col bg-[#1e1e1e]">
|
||||
<div className="h-8 bg-[#2a2a2a] border-b border-zinc-700 flex items-center px-3 gap-2 shrink-0">
|
||||
<i data-lucide="file-edit" className="w-4 h-4 text-amber-400"></i>
|
||||
<span className="text-xs font-bold text-zinc-200">{st.label}</span>
|
||||
<span className="text-[10px] text-zinc-500 font-mono">
|
||||
({formatTime(st.startTime)} - {formatTime(st.endTime)})
|
||||
| {st.buffer ? formatTime(st.buffer.duration) : '0s'}
|
||||
| {st.buffer ? st.buffer.sampleRate : 0} Hz
|
||||
</span>
|
||||
<div className="flex-1"></div>
|
||||
<button onClick={() => closeSubTab(st.id)}
|
||||
className="px-2 py-0.5 bg-zinc-800 hover:bg-zinc-700 text-zinc-400 rounded text-[10px] border border-zinc-700 transition">
|
||||
<i data-lucide="x" className="w-3 h-3"></i> Close
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col p-3 gap-3 overflow-y-auto">
|
||||
<SubTabToolbar
|
||||
st={st}
|
||||
activeTool={activeTool}
|
||||
setActiveTool={setActiveTool}
|
||||
handleSubTabNormalizeWithValue={handleSubTabNormalizeWithValue}
|
||||
handleSubTabGainWithValue={handleSubTabGainWithValue}
|
||||
handleSubTabPitch={handleSubTabPitch}
|
||||
handleSubTabStretch={handleSubTabStretch}
|
||||
handleSubTabFade={handleSubTabFadeState}
|
||||
onPlayPause={handlePlayPause}
|
||||
onStop={handleStop}
|
||||
onRewind={() => setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, currentTime: Math.max(0, (s.currentTime || 0) - 1) } : s))}
|
||||
onForward={() => setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, currentTime: Math.min(s.buffer?.duration || 0, (s.currentTime || 0) + 1) } : s))}
|
||||
onLoop={() => setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, isLooping: !s.isLooping } : s))}
|
||||
onRecord={() => setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, isRecording: !s.isRecording } : s))}
|
||||
onRateChange={(rate) => setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, playbackRate: rate } : s))}
|
||||
onCut={() => { const curTabId = activeTab; handleSubTabCut(curTabId); }}
|
||||
onCopy={() => { const curTabId = activeTab; handleSubTabCopy(curTabId); }}
|
||||
onPaste={() => { const curTabId = activeTab; handleSubTabPaste(curTabId); }}
|
||||
onGlue={() => showToast('Glue: Gộp clip trong subtab', 'info')}
|
||||
snapValue={snapValue}
|
||||
onSnapChange={setSnapValue}
|
||||
/>
|
||||
<SubTabWaveform
|
||||
buffer={st.buffer}
|
||||
subTabId={st.id}
|
||||
activeTab={activeTab}
|
||||
activeTool={activeTool}
|
||||
currentTime={st.currentTime}
|
||||
selectionStart={st.selectionStart}
|
||||
selectionEnd={st.selectionEnd}
|
||||
onSelectRange={(start, end) => {
|
||||
setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, selectionStart: start, selectionEnd: end } : s));
|
||||
}}
|
||||
onPlayheadSet={(time) => {
|
||||
setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, currentTime: time } : s));
|
||||
}}
|
||||
onContextMenu={(e, clickTime) => {
|
||||
setContextMenu({
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
isSubTab: true,
|
||||
subTabId: st.id,
|
||||
time: clickTime
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<div className="grid grid-cols-4 gap-3 max-w-2xl">
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-[9px] text-zinc-500 font-bold uppercase">Reverse</span>
|
||||
<button onClick={() => updateSubTabEffects(st.id, { reverse: !fx.reverse })}
|
||||
className={`py-2 rounded text-xs font-bold border transition ${
|
||||
fx.reverse
|
||||
? 'bg-amber-800 text-amber-100 border-amber-600'
|
||||
: 'bg-zinc-800 text-zinc-400 border-zinc-700 hover:text-zinc-200'
|
||||
}`}>
|
||||
<i data-lucide="arrow-left-right" className="w-4 h-4 mx-auto"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-[9px] text-zinc-500 font-bold uppercase">Gain (dB)</span>
|
||||
<input type="number" step="0.5" value={fx.gainDb || 0}
|
||||
onChange={(e) => updateSubTabEffects(st.id, { gainDb: parseFloat(e.target.value) || 0 })}
|
||||
className="bg-zinc-800 text-zinc-200 text-center font-mono text-xs rounded border border-zinc-700 p-1.5 focus:outline-none focus:border-amber-600" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-[9px] text-zinc-500 font-bold uppercase">Fade In (ms)</span>
|
||||
<input type="number" step="10" min="0" value={fx.fadeInMs || 0}
|
||||
onChange={(e) => updateSubTabEffects(st.id, { fadeInMs: parseInt(e.target.value) || 0 })}
|
||||
className="bg-zinc-800 text-zinc-200 text-center font-mono text-xs rounded border border-zinc-700 p-1.5 focus:outline-none focus:border-amber-600" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-[9px] text-zinc-500 font-bold uppercase">Fade Out (ms)</span>
|
||||
<input type="number" step="10" min="0" value={fx.fadeOutMs || 0}
|
||||
onChange={(e) => updateSubTabEffects(st.id, { fadeOutMs: parseInt(e.target.value) || 0 })}
|
||||
className="bg-zinc-800 text-zinc-200 text-center font-mono text-xs rounded border border-zinc-700 p-1.5 focus:outline-none focus:border-amber-600" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-10 bg-[#2a2a2a] border-t border-zinc-700 flex items-center justify-end px-3 gap-2 shrink-0">
|
||||
<button onClick={() => closeSubTab(st.id)}
|
||||
className="px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded text-xs font-bold border border-zinc-700 transition">
|
||||
Cancel
|
||||
</button>
|
||||
<button onClick={() => applySubTab(st.id)}
|
||||
className="px-3 py-1.5 bg-amber-700 hover:bg-amber-600 text-white rounded text-xs font-bold transition shadow-md">
|
||||
<i data-lucide="check" className="w-3.5 h-3.5 inline mr-1"></i> Apply & Merge
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* ── Context Menu ── */}
|
||||
{contextMenu && (
|
||||
contextMenu.isSubTab ? (
|
||||
|
||||
Reference in New Issue
Block a user