fix: đã sửa lỗi bị mất không hiển thị midi piano grid

This commit is contained in:
2026-07-26 10:05:23 +07:00
parent a765998455
commit a7417b9bfa
4 changed files with 533 additions and 305 deletions
+306 -291
View File
@@ -5657,306 +5657,321 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
return labels;
};
const beatSec = 60.0 / (parseInt(bpm) || 120);
const beatSec = 60.0 / (parseInt(bpm) || 120);
const playHeadX = (st.currentTime || 0) / beatSec * pixelsPerBeat;
return React.createElement("div", {
className: "flex-1 flex overflow-hidden min-h-0 bg-[#1e1e1e] flex-col h-full"
}, React.createElement("div", {
className: "h-10 bg-[#282828] border-b border-zinc-900 flex items-center justify-between px-4 shrink-0 text-slate-200"
}, /*#__PURE__*/React.createElement("div", {
className: "flex items-center gap-4"
}, /*#__PURE__*/React.createElement("span", {
className: "text-xs font-bold text-yellow-500 uppercase flex items-center gap-1.5"
}, /*#__PURE__*/React.createElement("i", {
"data-lucide": "music",
className: "w-3.5 h-3.5"
}), st.label), /*#__PURE__*/React.createElement("div", {
className: "flex items-center gap-1 text-xs"
}, /*#__PURE__*/React.createElement("span", {
className: "text-zinc-500 font-semibold"
}, "Snap to Scale"), /*#__PURE__*/React.createElement("button", {
onClick: () => setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, snapToScale: !(s.snapToScale !== undefined ? s.snapToScale : true) } : s)),
className: `w-7 h-4 rounded-full transition-colors relative ${(st.snapToScale !== undefined ? st.snapToScale : true) ? 'bg-yellow-600' : 'bg-zinc-700'}`,
style: { padding: 0 }
}, /*#__PURE__*/React.createElement("div", {
className: `w-3 h-3 rounded-full bg-white absolute top-0.5 transition-transform ${(st.snapToScale !== undefined ? st.snapToScale : true) ? 'translate-x-3.5' : 'translate-x-0.5'}`
}))), /*#__PURE__*/React.createElement("div", {
className: "flex items-center gap-1 text-xs"
}, /*#__PURE__*/React.createElement("span", {
className: "text-zinc-500 font-semibold"
}, "Snap:"), /*#__PURE__*/React.createElement("select", {
value: snapVal,
onChange: e => setSnapVal(e.target.value),
className: "bg-zinc-800 border border-zinc-700 text-zinc-300 rounded px-1.5 py-0.5 outline-none focus:border-yellow-500"
}, ['free', '4', '1', '1/2', '1/4', '1/8', '1/16', '1/32'].map(v => /*#__PURE__*/React.createElement("option", {
key: v,
value: v
}, v)))), /*#__PURE__*/React.createElement("button", {
onClick: () => setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, isArmed: !s.isArmed } : s)),
className: `px-2 py-1 rounded text-xs font-bold ${st.isArmed ? 'bg-red-600 text-white' : 'bg-zinc-800 text-zinc-400'}`
}, "ARM"), /*#__PURE__*/React.createElement("select", {
value: selectedMidiInputId || '',
onChange: e => onMidiInputSelect(e.target.value),
className: "bg-zinc-800 border border-zinc-700 text-zinc-300 rounded px-1 py-0.5 text-xs outline-none max-w-[100px]"
}, /*#__PURE__*/React.createElement("option", { value: "" }, "Input"), /*#__PURE__*/React.createElement("option", { value: "ALL" }, "Omni"), (midiDevices || []).map(d => /*#__PURE__*/React.createElement("option", { key: d.id, value: d.id }, d.name || d.id))), /*#__PURE__*/React.createElement("button", {
onClick: () => onInstrumentSelect && onInstrumentSelect(st.trackId),
title: st.instrumentName || "Synth",
className: `px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition flex items-center gap-0.5 max-w-[50px] ${st.instrumentName ? 'bg-violet-900 text-violet-300 border-violet-700' : 'bg-zinc-800 text-zinc-500 border-transparent hover:text-zinc-300'}`
}, /*#__PURE__*/React.createElement("i", { "data-lucide": "music", className: "w-3 h-3 shrink-0" }), /*#__PURE__*/React.createElement("span", { className: "truncate text-[9px]" }, st.instrumentName || 'Synth')), /*#__PURE__*/React.createElement("div", {
className: "flex items-center gap-0.5 ml-1"
}, /*#__PURE__*/React.createElement("button", {
onClick: () => setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, currentTime: Math.max(0, (st.currentTime || 0) - beatSec * 4) } : s)),
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",
title: "Back 1 bar"
}, /*#__PURE__*/React.createElement("i", { "data-lucide": "step-back", className: "w-3 h-3" })), /*#__PURE__*/React.createElement("button", {
onClick: onPlayPause,
className: `w-6 h-6 flex items-center justify-center rounded border ${isPlaying ? 'bg-emerald-600 text-black' : 'bg-cyan-600 text-white'} border-cyan-500`,
title: isPlaying ? "Pause" : "Play"
}, /*#__PURE__*/React.createElement("i", { "data-lucide": isPlaying ? "pause" : "play", className: "w-3 h-3 fill-current" })), /*#__PURE__*/React.createElement("button", {
onClick: onStop,
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",
title: "Stop"
}, /*#__PURE__*/React.createElement("i", { "data-lucide": "square", className: "w-3 h-3 fill-current" })), /*#__PURE__*/React.createElement("button", {
onClick: onRecord,
className: `w-6 h-6 flex items-center justify-center rounded border ${recordingState === 'RECORDING' ? 'bg-red-600 text-white border-red-500 animate-pulse' : 'bg-zinc-800 text-red-500 border-zinc-700'}`
}, /*#__PURE__*/React.createElement("i", { "data-lucide": "circle", className: "w-3 h-3 fill-current" })), /*#__PURE__*/React.createElement("button", {
onClick: () => setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, currentTime: (st.currentTime || 0) + beatSec * 4 } : s)),
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",
title: "Forward 1 bar"
}, /*#__PURE__*/React.createElement("i", { "data-lucide": "step-forward", className: "w-3 h-3" }))), /*#__PURE__*/React.createElement("div", {
className: "flex items-center gap-1 ml-1 text-xs"
}, /*#__PURE__*/React.createElement("span", { className: "text-zinc-500" }, "AI:"), /*#__PURE__*/React.createElement("input", {
type: "number", value: aiBarStart, onChange: e => setAiBarStart(parseInt(e.target.value) || 0),
className: "w-8 bg-zinc-900 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 text-xs text-center"
}), /*#__PURE__*/React.createElement("span", { className: "text-zinc-500" }, "-"), /*#__PURE__*/React.createElement("input", {
type: "number", value: aiBarEnd, onChange: e => setAiBarEnd(parseInt(e.target.value) || 1),
className: "w-8 bg-zinc-900 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 text-xs text-center"
}), /*#__PURE__*/React.createElement("span", { className: "text-zinc-500" }, "bar"), /*#__PURE__*/React.createElement("button", {
onClick: () => {
setIsLooping(!isLooping);
setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, isLooping: !(s.isLooping || false) } : s));
},
className: `px-2 py-0.5 rounded text-xs ${isLooping ? 'bg-emerald-700 text-white border border-emerald-500' : 'bg-zinc-800 text-zinc-400'}`
}, /*#__PURE__*/React.createElement("i", { "data-lucide": "repeat", className: "w-3 h-3" }))), /*#__PURE__*/React.createElement("div", {
className: "flex bg-zinc-800 p-0.5 rounded border border-zinc-700 text-xs"
}, ['velocity', 'pan'].map(mode => /*#__PURE__*/React.createElement("button", {
key: mode,
onClick: () => setCcMode(mode),
className: `px-2.5 py-1 rounded capitalize ${ccMode === mode ? 'bg-purple-900/60 text-purple-300 font-bold border border-purple-700' : 'text-zinc-400 hover:text-zinc-200'}`
}, mode)))), /*#__PURE__*/React.createElement("button", {
onClick: () => setShowCC(!showCC),
className: `px-2 py-1 rounded text-xs ${showCC ? 'bg-purple-900/60 text-purple-300 border border-purple-700' : 'text-zinc-500 hover:text-zinc-300'}`
}, ccMode === 'pan' ? 'Pan' : 'Vel'), /*#__PURE__*/React.createElement("div", {
className: "flex items-center gap-1"
}, /*#__PURE__*/React.createElement("button", {
onClick: () => onSaveNotes(st.id, st.trackId, st.target_id, notes),
className: "px-2.5 py-1 bg-emerald-600 hover:bg-emerald-500 text-white rounded text-xs flex items-center gap-1 transition font-semibold"
}, /*#__PURE__*/React.createElement("i", { "data-lucide": "save", className: "w-3 h-3" }), "Lưu"), /*#__PURE__*/React.createElement("button", {
onClick: onClose,
className: "px-2.5 py-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 border border-zinc-700 rounded text-xs flex items-center gap-1 transition"
}, /*#__PURE__*/React.createElement("i", {
"data-lucide": "x",
className: "w-3 h-3"
}), "Đóng"))), /*#__PURE__*/React.createElement("div", {
className: "h-6 bg-[#1a1a1f] border-b border-zinc-900 flex shrink-0"
}, /*#__PURE__*/React.createElement("div", {
className: "w-[60px] bg-[#222222] border-r border-zinc-900 shrink-0"
}), /*#__PURE__*/React.createElement("div", {
ref: rulerScrollRef,
className: "flex-1 overflow-hidden",
onMouseDown: (e) => {
const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left + e.currentTarget.scrollLeft;
const clickBeat = x / pixelsPerBeat;
const clickTime = clickBeat * beatSec;
if (clickTime >= 0) {
setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, currentTime: clickTime } : s));
}
if (e.shiftKey) {
const beatSnap = getSnapBeat(clickBeat, snapVal);
if (loopStartBeat === null) {
setLoopStartBeat(Math.max(0, beatSnap - 4));
setLoopEndBeat(Math.max(4, beatSnap));
} else {
setLoopEndBeat(Math.max(loopStartBeat + 4, beatSnap));
},
/* 1. TOOLBAR HEADER */
React.createElement("div", {
className: "h-10 bg-[#282828] border-b border-zinc-900 flex items-center justify-between px-4 shrink-0 text-slate-200"
}, React.createElement("div", {
className: "flex items-center gap-4"
}, React.createElement("span", {
className: "text-xs font-bold text-yellow-500 uppercase flex items-center gap-1.5"
}, React.createElement("i", {
"data-lucide": "music",
className: "w-3.5 h-3.5"
}), st.label), React.createElement("div", {
className: "flex items-center gap-1 text-xs"
}, React.createElement("span", {
className: "text-zinc-500 font-semibold"
}, "Snap to Scale"), React.createElement("button", {
onClick: () => setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, snapToScale: !(s.snapToScale !== undefined ? s.snapToScale : true) } : s)),
className: `w-7 h-4 rounded-full transition-colors relative ${(st.snapToScale !== undefined ? st.snapToScale : true) ? 'bg-yellow-600' : 'bg-zinc-700'}`,
style: { padding: 0 }
}, React.createElement("div", {
className: `w-3 h-3 rounded-full bg-white absolute top-0.5 transition-transform ${(st.snapToScale !== undefined ? st.snapToScale : true) ? 'translate-x-3.5' : 'translate-x-0.5'}`
}))), React.createElement("div", {
className: "flex items-center gap-1 text-xs"
}, React.createElement("span", {
className: "text-zinc-500 font-semibold"
}, "Snap:"), React.createElement("select", {
value: snapVal,
onChange: e => setSnapVal(e.target.value),
className: "bg-zinc-800 border border-zinc-700 text-zinc-300 rounded px-1.5 py-0.5 outline-none focus:border-yellow-500"
}, ['free', '4', '1', '1/2', '1/4', '1/8', '1/16', '1/32'].map(v => React.createElement("option", {
key: v,
value: v
}, v)))), React.createElement("button", {
onClick: () => setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, isArmed: !s.isArmed } : s)),
className: `px-2 py-1 rounded text-xs font-bold ${st.isArmed ? 'bg-red-600 text-white' : 'bg-zinc-800 text-zinc-400'}`
}, "ARM"), React.createElement("select", {
value: selectedMidiInputId || '',
onChange: e => onMidiInputSelect(e.target.value),
className: "bg-zinc-800 border border-zinc-700 text-zinc-300 rounded px-1 py-0.5 text-xs outline-none max-w-[100px]"
}, React.createElement("option", { value: "" }, "Input"), React.createElement("option", { value: "ALL" }, "Omni"), (midiDevices || []).map(d => React.createElement("option", { key: d.id, value: d.id }, d.name || d.id))), React.createElement("button", {
onClick: () => onInstrumentSelect && onInstrumentSelect(st.trackId),
title: st.instrumentName || "Synth",
className: `px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition flex items-center gap-0.5 max-w-[50px] ${st.instrumentName ? 'bg-violet-900 text-violet-300 border-violet-700' : 'bg-zinc-800 text-zinc-500 border-transparent hover:text-zinc-300'}`
}, React.createElement("i", { "data-lucide": "music", className: "w-3 h-3 shrink-0" }), React.createElement("span", { className: "truncate text-[9px]" }, st.instrumentName || 'Synth')), React.createElement("div", {
className: "flex items-center gap-0.5 ml-1"
}, React.createElement("button", {
onClick: () => setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, currentTime: Math.max(0, (st.currentTime || 0) - beatSec * 4) } : s)),
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",
title: "Back 1 bar"
}, React.createElement("i", { "data-lucide": "step-back", className: "w-3 h-3" })), React.createElement("button", {
onClick: onPlayPause,
className: `w-6 h-6 flex items-center justify-center rounded border ${isPlaying ? 'bg-emerald-600 text-black' : 'bg-cyan-600 text-white'} border-cyan-500`,
title: isPlaying ? "Pause" : "Play"
}, React.createElement("i", { "data-lucide": isPlaying ? "pause" : "play", className: "w-3 h-3 fill-current" })), React.createElement("button", {
onClick: onStop,
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",
title: "Stop"
}, React.createElement("i", { "data-lucide": "square", className: "w-3 h-3 fill-current" })), React.createElement("button", {
onClick: onRecord,
className: `w-6 h-6 flex items-center justify-center rounded border ${recordingState === 'RECORDING' ? 'bg-red-600 text-white border-red-500 animate-pulse' : 'bg-zinc-800 text-red-500 border-zinc-700'}`
}, React.createElement("i", { "data-lucide": "circle", className: "w-3 h-3 fill-current" })), React.createElement("button", {
onClick: () => setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, currentTime: (st.currentTime || 0) + beatSec * 4 } : s)),
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",
title: "Forward 1 bar"
}, React.createElement("i", { "data-lucide": "step-forward", className: "w-3 h-3" }))), React.createElement("div", {
className: "flex items-center gap-1 ml-1 text-xs"
}, React.createElement("span", { className: "text-zinc-500" }, "AI:"), React.createElement("input", {
type: "number", value: aiBarStart, onChange: e => setAiBarStart(parseInt(e.target.value) || 0),
className: "w-8 bg-zinc-900 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 text-xs text-center"
}), React.createElement("span", { className: "text-zinc-500" }, "-"), React.createElement("input", {
type: "number", value: aiBarEnd, onChange: e => setAiBarEnd(parseInt(e.target.value) || 1),
className: "w-8 bg-zinc-900 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 text-xs text-center"
}), React.createElement("span", { className: "text-zinc-500" }, "bar"), React.createElement("button", {
onClick: () => {
setIsLooping(!isLooping);
setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, isLooping: !(s.isLooping || false) } : s));
},
className: `px-2 py-0.5 rounded text-xs ${isLooping ? 'bg-emerald-700 text-white border border-emerald-500' : 'bg-zinc-800 text-zinc-400'}`
}, React.createElement("i", { "data-lucide": "repeat", className: "w-3 h-3" }))), React.createElement("div", {
className: "flex bg-zinc-800 p-0.5 rounded border border-zinc-700 text-xs"
}, ['velocity', 'pan'].map(mode => React.createElement("button", {
key: mode,
onClick: () => setCcMode(mode),
className: `px-2.5 py-1 rounded capitalize ${ccMode === mode ? 'bg-purple-900/60 text-purple-300 font-bold border border-purple-700' : 'text-zinc-400 hover:text-zinc-200'}`
}, mode)))), React.createElement("button", {
onClick: () => setShowCC(!showCC),
className: `px-2 py-1 rounded text-xs ${showCC ? 'bg-purple-900/60 text-purple-300 border border-purple-700' : 'text-zinc-500 hover:text-zinc-300'}`
}, ccMode === 'pan' ? 'Pan' : 'Vel'), React.createElement("div", {
className: "flex items-center gap-1"
}, React.createElement("button", {
onClick: () => onSaveNotes(st.id, st.trackId, st.target_id, notes),
className: "px-2.5 py-1 bg-emerald-600 hover:bg-emerald-500 text-white rounded text-xs flex items-center gap-1 transition font-semibold"
}, React.createElement("i", { "data-lucide": "save", className: "w-3 h-3" }), "Lưu"), React.createElement("button", {
onClick: onClose,
className: "px-2.5 py-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 border border-zinc-700 rounded text-xs flex items-center gap-1 transition"
}, React.createElement("i", {
"data-lucide": "x",
className: "w-3 h-3"
}), "Đóng"))),
/* 2. BAR RULER (ĐÃ SỬA LẠI ĐÓNG NGOẶC ĐÚNG TẠI ĐÂY) */
React.createElement("div", {
className: "h-6 bg-[#1a1a1f] border-b border-zinc-900 flex shrink-0"
}, React.createElement("div", {
className: "w-[60px] bg-[#222222] border-r border-zinc-900 shrink-0"
}), React.createElement("div", {
ref: rulerScrollRef,
className: "flex-1 overflow-hidden",
onMouseDown: (e) => {
const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left + e.currentTarget.scrollLeft;
const clickBeat = x / pixelsPerBeat;
const clickTime = clickBeat * beatSec;
if (clickTime >= 0) {
setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, currentTime: clickTime } : s));
}
return;
}
const snappedStartBeat = getSnapBeat(clickBeat, snapVal);
const startData = { startX: e.clientX, startBeat: snappedStartBeat, scrollLeft: e.currentTarget.scrollLeft };
rulerDragRef.current = startData;
const onMove = (ev) => {
const r = rulerScrollRef.current;
if (!r || !rulerDragRef.current) return;
const rRect = r.getBoundingClientRect();
const bx = ev.clientX - rRect.left + rulerDragRef.current.scrollLeft;
const rawBeat = Math.max(0, bx / pixelsPerBeat);
const beat = getSnapBeat(rawBeat, snapVal);
if (Math.abs(ev.clientX - rulerDragRef.current.startX) > 5) {
const sBeat = Math.max(0, Math.min(rulerDragRef.current.startBeat, beat));
const eBeat = Math.max(sBeat + 1, Math.max(rulerDragRef.current.startBeat, beat));
setLoopStartBeat(sBeat);
setLoopEndBeat(eBeat);
setIsLooping(true);
setSubTabs(prev => prev.map(s => s.id === st.id ? {
...s,
selectionStart: sBeat * beatSec,
selectionEnd: eBeat * beatSec,
isLooping: true
} : s));
if (e.shiftKey) {
const beatSnap = getSnapBeat(clickBeat, snapVal);
if (loopStartBeat === null) {
setLoopStartBeat(Math.max(0, beatSnap - 4));
setLoopEndBeat(Math.max(4, beatSnap));
} else {
setLoopEndBeat(Math.max(loopStartBeat + 4, beatSnap));
}
return;
}
};
const onUp = () => { rulerDragRef.current = null; document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); };
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
}
}, /*#__PURE__*/React.createElement("div", {
style: {
width: `${viewWidth}px`,
height: '100%'
},
className: "relative h-full font-mono text-[9px] text-zinc-500 font-bold"
}, renderBarLabels(), loopStartBeat !== null && loopEndBeat !== null && loopEndBeat > loopStartBeat && /*#__PURE__*/React.createElement("div", {
style: {
left: `${loopStartBeat * pixelsPerBeat}px`,
width: `${(loopEndBeat - loopStartBeat) * pixelsPerBeat}px`,
top: 0, bottom: 0
},
className: "absolute bg-emerald-500/15 border-l border-r border-emerald-400"
}, /*#__PURE__*/React.createElement("div", {
style: { position: 'absolute', left: 0, top: 0, bottom: 0, width: '4px', cursor: 'ew-resize' },
onMouseDown: (e) => {
e.stopPropagation();
const startBeat = loopStartBeat;
const onMove = (ev) => {
const r = rulerScrollRef.current;
if (!r) return;
const rRect = r.getBoundingClientRect();
const bx = ev.clientX - rRect.left + r.scrollLeft;
const nBeat = Math.max(0, Math.min(loopEndBeat - 1, getSnapBeat(bx / pixelsPerBeat, snapVal)));
setLoopStartBeat(nBeat);
setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, selectionStart: nBeat * beatSec } : s));
};
const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); };
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
}
}), /*#__PURE__*/React.createElement("div", {
style: { position: 'absolute', right: 0, top: 0, bottom: 0, width: '4px', cursor: 'ew-resize' },
onMouseDown: (e) => {
e.stopPropagation();
const onMove = (ev) => {
const r = rulerScrollRef.current;
if (!r) return;
const rRect = r.getBoundingClientRect();
const bx = ev.clientX - rRect.left + r.scrollLeft;
const nBeat = Math.max(loopStartBeat + 1, getSnapBeat(bx / pixelsPerBeat, snapVal));
setLoopEndBeat(nBeat);
setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, selectionEnd: nBeat * beatSec } : s));
};
const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); };
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
}
}), /*#__PURE__*/React.createElement("div", {
style: { position: 'absolute', left: '4px', right: '4px', top: 0, bottom: 0, cursor: 'grab' },
onMouseDown: (e) => {
e.stopPropagation();
const startBeat = loopStartBeat;
const range = loopEndBeat - loopStartBeat;
const offsetBeat = startBeat + range / 2;
const onMove = (ev) => {
const r = rulerScrollRef.current;
if (!r) return;
const rRect = r.getBoundingClientRect();
const bx = ev.clientX - rRect.left + r.scrollLeft;
const centerBeat = getSnapBeat(bx / pixelsPerBeat, snapVal);
const halfRange = range / 2;
const newStart = Math.max(0, centerBeat - halfRange);
setLoopStartBeat(newStart);
setLoopEndBeat(newStart + range);
setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, selectionStart: newStart * beatSec, selectionEnd: (newStart + range) * beatSec } : s));
};
const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); };
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
}
})))), /*#__PURE__*/React.createElement("div", {
className: "flex-1 flex overflow-hidden min-h-0 relative"
}, /*#__PURE__*/React.createElement("div", {
ref: keybedRef,
onScroll: handleKeybedScroll,
className: "w-[60px] overflow-y-auto flex flex-col border-r border-zinc-900 shrink-0",
style: {
scrollbarWidth: 'none',
msOverflowStyle: 'none'
}
}, renderKeybed()), /*#__PURE__*/React.createElement("div", {
ref: gridScrollRef,
onScroll: handleScroll,
className: "flex-1 overflow-auto bg-[#141414] min-w-0"
}, /*#__PURE__*/React.createElement("div", {
const snappedStartBeat = getSnapBeat(clickBeat, snapVal);
const startData = { startX: e.clientX, startBeat: snappedStartBeat, scrollLeft: e.currentTarget.scrollLeft };
rulerDragRef.current = startData;
const onMove = (ev) => {
const r = rulerScrollRef.current;
if (!r || !rulerDragRef.current) return;
const rRect = r.getBoundingClientRect();
const bx = ev.clientX - rRect.left + rulerDragRef.current.scrollLeft;
const rawBeat = Math.max(0, bx / pixelsPerBeat);
const beat = getSnapBeat(rawBeat, snapVal);
if (Math.abs(ev.clientX - rulerDragRef.current.startX) > 5) {
const sBeat = Math.max(0, Math.min(rulerDragRef.current.startBeat, beat));
const eBeat = Math.max(sBeat + 1, Math.max(rulerDragRef.current.startBeat, beat));
setLoopStartBeat(sBeat);
setLoopEndBeat(eBeat);
setIsLooping(true);
setSubTabs(prev => prev.map(s => s.id === st.id ? {
...s,
selectionStart: sBeat * beatSec,
selectionEnd: eBeat * beatSec,
isLooping: true
} : s));
}
};
const onUp = () => { rulerDragRef.current = null; document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); };
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
}
}, React.createElement("div", {
style: {
width: `${viewWidth}px`,
height: '100%'
},
className: "relative h-full font-mono text-[9px] text-zinc-500 font-bold"
}, renderBarLabels(), loopStartBeat !== null && loopEndBeat !== null && loopEndBeat > loopStartBeat && React.createElement("div", {
style: {
left: `${loopStartBeat * pixelsPerBeat}px`,
width: `${(loopEndBeat - loopStartBeat) * pixelsPerBeat}px`,
top: 0, bottom: 0
},
className: "absolute bg-emerald-500/15 border-l border-r border-emerald-400"
}, React.createElement("div", {
style: { position: 'absolute', left: 0, top: 0, bottom: 0, width: '4px', cursor: 'ew-resize' },
onMouseDown: (e) => {
e.stopPropagation();
const startBeat = loopStartBeat;
const onMove = (ev) => {
const r = rulerScrollRef.current;
if (!r) return;
const rRect = r.getBoundingClientRect();
const bx = ev.clientX - rRect.left + r.scrollLeft;
const nBeat = Math.max(0, Math.min(loopEndBeat - 1, getSnapBeat(bx / pixelsPerBeat, snapVal)));
setLoopStartBeat(nBeat);
setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, selectionStart: nBeat * beatSec } : s));
};
const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); };
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
}
}), React.createElement("div", {
style: { position: 'absolute', right: 0, top: 0, bottom: 0, width: '4px', cursor: 'ew-resize' },
onMouseDown: (e) => {
e.stopPropagation();
const onMove = (ev) => {
const r = rulerScrollRef.current;
if (!r) return;
const rRect = r.getBoundingClientRect();
const bx = ev.clientX - rRect.left + r.scrollLeft;
const nBeat = Math.max(loopStartBeat + 1, getSnapBeat(bx / pixelsPerBeat, snapVal));
setLoopEndBeat(nBeat);
setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, selectionEnd: nBeat * beatSec } : s));
};
const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); };
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
}
}), React.createElement("div", {
style: { position: 'absolute', left: '4px', right: '4px', top: 0, bottom: 0, cursor: 'grab' },
onMouseDown: (e) => {
e.stopPropagation();
const startBeat = loopStartBeat;
const range = loopEndBeat - loopStartBeat;
const offsetBeat = startBeat + range / 2;
const onMove = (ev) => {
const r = rulerScrollRef.current;
if (!r) return;
const rRect = r.getBoundingClientRect();
const bx = ev.clientX - rRect.left + r.scrollLeft;
const centerBeat = getSnapBeat(bx / pixelsPerBeat, snapVal);
const halfRange = range / 2;
const newStart = Math.max(0, centerBeat - halfRange);
setLoopStartBeat(newStart);
setLoopEndBeat(newStart + range);
setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, selectionStart: newStart * beatSec, selectionEnd: (newStart + range) * beatSec } : s));
};
const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); };
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
}
}))))),
/* 3. MAIN PIANO ROLL GRID (KEYBOARD + CANVAS) */
React.createElement("div", {
className: "flex-1 flex overflow-hidden min-h-0 relative"
}, React.createElement("div", {
ref: keybedRef,
onScroll: handleKeybedScroll,
className: "w-[60px] overflow-y-auto flex flex-col border-r border-zinc-900 shrink-0",
style: {
scrollbarWidth: 'none',
msOverflowStyle: 'none'
}
}, renderKeybed()), React.createElement("div", {
ref: gridScrollRef,
onScroll: handleScroll,
className: "flex-1 overflow-auto bg-[#141414] min-w-0"
}, React.createElement("div", {
style: {
width: `${viewWidth}px`,
height: `${(128 - PITCH_START) * NoteHeight}px`
},
className: "relative"
}, /*#__PURE__*/React.createElement("canvas", {
ref: canvasRef,
onMouseDown: handleGridMouseDown,
onMouseMove: handleGridMouseMove,
onMouseUp: handleGridMouseUp,
onMouseLeave: handleGridMouseUp,
onContextMenu: handleContextMenu,
className: "absolute inset-0 cursor-crosshair"
}), loopStartBeat !== null && loopEndBeat !== null && loopEndBeat > loopStartBeat && /*#__PURE__*/React.createElement("div", {
style: {
left: `${loopStartBeat * pixelsPerBeat}px`,
width: `${(loopEndBeat - loopStartBeat) * pixelsPerBeat}px`,
top: 0, bottom: 0
},
className: "absolute bg-emerald-500/10 border-l border-r border-emerald-400/50 pointer-events-none"
})))), showCC && /*#__PURE__*/React.createElement("div", {
style: { height: `${ccHeight}px` },
className: "bg-[#161616] border-t border-zinc-900 flex shrink-0 relative"
}, /*#__PURE__*/React.createElement("div", {
onMouseDown: e => {
e.preventDefault();
const startY = e.clientY;
const startH = ccHeight;
const onMove = ev => { setCcHeight(Math.max(40, startH + startY - ev.clientY)); };
const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); };
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
},
className: "absolute top-0 left-0 right-0 h-1.5 cursor-n-resize z-10 hover:bg-purple-600/30"
}), /*#__PURE__*/React.createElement("div", {
className: "w-[60px] bg-[#222222] border-r border-zinc-900 flex items-center justify-center text-[10px] text-zinc-500 font-mono shrink-0 font-bold"
}, ccMode.toUpperCase()), /*#__PURE__*/React.createElement("div", {
ref: ccWrapperRef,
className: "flex-1 overflow-x-hidden min-w-0"
}, /*#__PURE__*/React.createElement("div", {
style: {
width: `${viewWidth}px`,
height: '100%'
},
className: "relative"
}, /*#__PURE__*/React.createElement("canvas", {
ref: ccCanvasRef,
onMouseDown: handleCCMouseDown,
onMouseMove: handleCCMouseMove,
onMouseUp: () => { if (ccDragRef.current) ccDragRef.current.selectedMode = false; ccDragRef.current = null; },
onMouseLeave: () => { if (ccDragRef.current) ccDragRef.current.selectedMode = false; ccDragRef.current = null; },
className: "absolute inset-0"
}), loopStartBeat !== null && loopEndBeat !== null && loopEndBeat > loopStartBeat && /*#__PURE__*/React.createElement("div", {
style: {
left: `${loopStartBeat * pixelsPerBeat}px`,
width: `${(loopEndBeat - loopStartBeat) * pixelsPerBeat}px`,
top: 0, bottom: 0
},
className: "absolute bg-emerald-500/10 border-l border-r border-emerald-400/50 pointer-events-none"
})))), scaleMenuPos && renderScaleContextMenu());
},
className: "relative"
}, React.createElement("canvas", {
ref: canvasRef,
onMouseDown: handleGridMouseDown,
onMouseMove: handleGridMouseMove,
onMouseUp: handleGridMouseUp,
onMouseLeave: handleGridMouseUp,
onContextMenu: handleContextMenu,
className: "absolute inset-0 cursor-crosshair"
}), loopStartBeat !== null && loopEndBeat !== null && loopEndBeat > loopStartBeat && React.createElement("div", {
style: {
left: `${loopStartBeat * pixelsPerBeat}px`,
width: `${(loopEndBeat - loopStartBeat) * pixelsPerBeat}px`,
top: 0, bottom: 0
},
className: "absolute bg-emerald-500/10 border-l border-r border-emerald-400/50 pointer-events-none"
})))),
/* 4. CC / VELOCITY LANE (ĐẶT Ở ĐÁY) */
showCC && React.createElement("div", {
style: { height: `${ccHeight}px` },
className: "bg-[#161616] border-t border-zinc-900 flex shrink-0 relative"
}, React.createElement("div", {
onMouseDown: e => {
e.preventDefault();
const startY = e.clientY;
const startH = ccHeight;
const onMove = ev => { setCcHeight(Math.max(40, startH + startY - ev.clientY)); };
const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); };
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
},
className: "absolute top-0 left-0 right-0 h-1.5 cursor-n-resize z-10 hover:bg-purple-600/30"
}), React.createElement("div", {
className: "w-[60px] bg-[#222222] border-r border-zinc-900 flex items-center justify-center text-[10px] text-zinc-500 font-mono shrink-0 font-bold"
}, ccMode.toUpperCase()), React.createElement("div", {
ref: ccWrapperRef,
className: "flex-1 overflow-x-hidden min-w-0"
}, React.createElement("div", {
style: {
width: `${viewWidth}px`,
height: '100%'
},
className: "relative"
}, React.createElement("canvas", {
ref: ccCanvasRef,
onMouseDown: handleCCMouseDown,
onMouseMove: handleCCMouseMove,
onMouseUp: () => { if (ccDragRef.current) ccDragRef.current.selectedMode = false; ccDragRef.current = null; },
onMouseLeave: () => { if (ccDragRef.current) ccDragRef.current.selectedMode = false; ccDragRef.current = null; },
className: "absolute inset-0"
}), loopStartBeat !== null && loopEndBeat !== null && loopEndBeat > loopStartBeat && React.createElement("div", {
style: {
left: `${loopStartBeat * pixelsPerBeat}px`,
width: `${(loopEndBeat - loopStartBeat) * pixelsPerBeat}px`,
top: 0, bottom: 0
},
className: "absolute bg-emerald-500/10 border-l border-r border-emerald-400/50 pointer-events-none"
})))),
/* 5. OVERLAY / CONTEXT MENU */
scaleMenuPos && renderScaleContextMenu()
);
};
const serializeTracksList = (tracksList, secondsPerBar) => {
File diff suppressed because one or more lines are too long