1const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/ScoreEditorCanvas-Cg6gP8yn.js","assets/vendor-react-Bay9Y7Lb.js","assets/vendor-audio-D-wTggbx.js","assets/vendor-D0P4XgNj.js","assets/vendor-i18n-BC7ZMXWQ.js","assets/vendor-DqKRdbjz.css","assets/useMidiKeyboard-DlyEIN8T.js","assets/MidiEngine-2SYF-_aI.js","assets/vendor-spessasynth-DH8n6o_V.js"])))=>i.map(i=>d[i]); 2import { j as jsxRuntimeExports, r as reactExports, R as React, a as reactDomExports } from "./vendor-react-Bay9Y7Lb.js"; 3import { u as useEditor, a as useScoreEditorEngine, g as getMidiNotesFromBase64, c as convertMidiToMEI, b as getMidiFromMEI, d as getMeiDurAttr, S as ScoreEditorCanvas, e as getBeats, f as getDurationInBeats, E as EditorProvider } from "./ScoreEditorCanvas-Cg6gP8yn.js"; 4import { bG as CircleQuestionMark, b_ as Pause, bV as Play, di as Square, dj as Circle, ck as Layers, cf as Plus, b5 as Trash2, dk as Pencil, dl as Brush, dm as Eraser, bX as VolumeX, dn as Scissors, dp as Split, dq as MousePointer2, dr as Magnet, cg as Activity, ds as ZoomOut, dt as ZoomIn, bF as ChevronRight, cC as ChevronDown, c2 as Copy, dh as SlidersVertical, aW as X, du as Folder, d8 as SquarePen, dv as Wrench, bg as Music, bA as Eye, bM as Clock, b7 as Check, b9 as Settings, dw as GripHorizontal, bz as EyeOff, cc as Pin, cb as PinOff, dx as Ut, dy as Yt, dz as Qt, bI as Hash, dA as Grid3x3, dB as LayoutTemplate, dC as Columns2, cn as ArrowRight, aY as TriangleAlert, ba as Info, cG as Keyboard, dD as Paintbrush, dE as Undo2, dF as Redo2, dG as ClipboardPaste, cd as Save, d1 as Download, dH as LayoutDashboard, cX as Minimize, cY as Maximize, c$ as Minimize2, d0 as Maximize2, b4 as FolderOpen, dI as PanelLeftClose, dJ as PanelLeftOpen, dK as PanelRightClose, dL as PanelRightOpen, dM as Move, dN as MonitorUp, bR as ExternalLink } from "./vendor-D0P4XgNj.js"; 5import { u as useTranslation, i as instance } from "./vendor-i18n-BC7ZMXWQ.js"; 6import { I as INSTRUMENT_CATEGORIES, l as GM_INSTRUMENTS, G as GM_PERCUSSION_MAP, k as getTrackColor } from "./utils-CEVb7nN1.js"; 7import "./index-w5lXls15.js"; 8import { _ as __vitePreload } from "./vendor-react-three-BCww3P1x.js"; 9import { P as ProjectList } from "./ProjectList-CcSw2aTd.js"; 10import "./vendor-audio-D-wTggbx.js"; 11import "./useMidiKeyboard-DlyEIN8T.js"; 12import "./MidiEngine-2SYF-_aI.js"; 13import "./vendor-spessasynth-DH8n6o_V.js"; 14const HelpTip = ({ 15 text, 16 title, 17 className = "", 18 iconSize = 14, 19 position = "top", 20 children 21}) => { 22 const positionClasses = { 23 top: "bottom-full mb-2 left-1/2 -translate-x-1/2", 24 bottom: "top-full mt-2 left-1/2 -translate-x-1/2", 25 left: "right-full mr-2 top-1/2 -translate-y-1/2", 26 right: "left-full ml-2 top-1/2 -translate-y-1/2" 27 }; 28 const arrowClasses = { 29 top: "bottom-0 left-1/2 -translate-x-1/2 translate-y-1/2 w-2.5 h-2.5 border-b border-r", 30 bottom: "top-0 left-1/2 -translate-x-1/2 -translate-y-1/2 w-2.5 h-2.5 border-t border-l", 31 left: "right-0 top-1/2 translate-x-1/2 -translate-y-1/2 w-2.5 h-2.5 border-t border-r", 32 right: "left-0 top-1/2 -translate-x-1/2 -translate-y-1/2 w-2.5 h-2.5 border-b border-l" 33 }; 34 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `relative group/tooltip inline-flex items-center justify-center ${children ? "" : "mx-1 align-middle"} ${className}`, children: [ 35 children || /* @__PURE__ */ jsxRuntimeExports.jsx(CircleQuestionMark, { size: iconSize, className: "text-zinc-400 hover:text-zinc-500 dark:text-zinc-500 dark:hover:text-zinc-400 cursor-help transition-colors" }), 36 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `absolute ${positionClasses[position]} hidden group-hover/tooltip:flex flex-col w-56 p-3 rounded-lg bg-zinc-900/95 dark:bg-zinc-800/95 backdrop-blur-md text-white text-xs leading-relaxed shadow-xl border border-white/10 dark:border-zinc-700/80 z-[9999] transition-all duration-300 animate-in fade-in zoom-in-95`, children: [ 37 title && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-semibold text-amber-400 mb-1 text-left", children: title }),
38 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-zinc-300 font-normal m-0 p-0 text-left", children: text }), 39 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `absolute ${arrowClasses[position]} bg-zinc-900 dark:bg-zinc-800 border-white/10 dark:border-zinc-700/80 rotate-45` }) 40 ] }) 41 ] }); 42}; 43const TransportControls = () => { 44 const { t } = useTranslation(); 45 const { 46 isEngineReady, 47 isPlaying, 48 play, 49 pause, 50 stop, 51 currentTime, 52 duration, 53 seek, 54 engine, 55 playbackMode, 56 setPlaybackMode, 57 isRecording, 58 setIsRecording, 59 isTypingKeyboardEnabled, 60 setIsTypingKeyboardEnabled, 61 isCountdownEnabled, 62 setIsCountdownEnabled, 63 isMetronomeEnabled, 64 setIsMetronomeEnabled, 65 isLoopRecordingEnabled, 66 setIsLoopRecordingEnabled, 67 masterVolume, 68 setMasterVolume, 69 masterPitch, 70 setMasterPitch, 71 tempo, 72 setTempo, 73 easyMode 74 } = useEditor(); 75 const bpmRef = reactExports.useRef(null); 76 const [isBpmEditing, setIsBpmEditing] = reactExports.useState(false); 77 const [bpmInputValue, setBpmInputValue] = reactExports.useState(tempo.toString()); 78 const dragStartY = reactExports.useRef(0); 79 const dragStartBpm = reactExports.useRef(0); 80 const [isDraggingBpm, setIsDraggingBpm] = reactExports.useState(false); 81 const canvasRef = reactExports.useRef(null); 82 const [voicesCount, setVoicesCount] = reactExports.useState(0); 83 const [memoryEstimation, setMemoryEstimation] = reactExports.useState(296); 84 const [cpuLoad, setCpuLoad] = reactExports.useState(4); 85 const formatBST = (timeSecs) => { 86 if (!engine) return "1:1:000"; 87 const ticks = engine.secondsToTicks(timeSecs); 88 const ppq = engine.midi.header.ppq || 96; 89 const ticksPerStep = ppq / 4; 90 const ticksPerBar = ppq * 4; 91 const bar = Math.floor(ticks / ticksPerBar) + 1; 92 const step = Math.floor(ticks % ticksPerBar / ticksPerStep) + 1; 93 const tick = Math.round(ticks % ticksPerStep); 94 const stepStr = step.toString().padStart(2, "0"); 95 const tickStr = tick.toString().padStart(3, "0"); 96 return `${bar}:${stepStr}:${tickStr}`; 97 }; 98 const handleBpmWheel = (e) => { 99 e.preventDefault(); 100 const delta = e.deltaY < 0 ? 1 : -1; 101 const nextBpm = Math.max(10, Math.min(280, Math.round((tempo + delta) * 10) / 10)); 102 setTempo(nextBpm); 103 }; 104 const handleBpmMouseDown = (e) => { 105 if (e.button !== 0) return; 106 dragStartY.current = e.clientY; 107 dragStartBpm.current = tempo; 108 setIsDraggingBpm(true); 109 e.preventDefault(); 110 }; 111 reactExports.useEffect(() => { 112 const handleMouseMove = (e) => { 113 if (!isDraggingBpm) return; 114 const deltaY = dragStartY.current - e.clientY; 115 const bpmChange = Math.round(deltaY * 0.2 * 10) / 10; 116 const nextBpm = Math.max(10, Math.min(280, Math.round((dragStartBpm.current + bpmChange) * 10) / 10)); 117 setTempo(nextBpm); 118 }; 119 const handleMouseUp = () => { 120 setIsDraggingBpm(false); 121 }; 122 if (isDraggingBpm) { 123 window.addEventListener("mousemove", handleMouseMove); 124 window.addEventListener("mouseup", handleMouseUp); 125 } 126 return () => { 127 window.removeEventListener("mousemove", handleMouseMove); 128 window.removeEventListener("mouseup", handleMouseUp); 129 }; 130 }, [isDraggingBpm, setTempo]); 131 const handleBpmDoubleClick = () => { 132 setBpmInputValue(tempo.toFixed(3)); 133 setIsBpmEditing(true); 134 }; 135 const handleBpmInputBlur = () => { 136 const val = parseFloat(bpmInputValue); 137 if (!isNaN(val)) { 138 setTempo(Math.max(10, Math.min(280, val))); 139 } 140 setIsBpmEditing(false); 141 }; 142 const handleBpmInputKeyDown = (e) => { 143 if (e.key === "Enter") { 144 handleBpmInputBlur(); 145 } else if (e.key === "Escape") { 146 setIsBpmEditing(false); 147 } 148 }; 149 reactExports.useEffect(() => { 150 let animationFrameId; 151 const drawOscilloscope = () => { 152 if (canvasRef.current && engine && engine.analyser) { 153 const canvas = canvasRef.current; 154 const ctx = canvas.getContext("2d"); 155 if (ctx) { 156 const analyser = engine.analyser; 157 const bufferLength = analyser.frequencyBinCount; 158 const dataArray = new Uint8Array(bufferLength); 159 analyser.getByteTimeDomainData(dataArray); 160 ctx.fillStyle = "#202528"; 161 ctx.fillRect(0, 0, canvas.width, canvas.height); 162 ctx.strokeStyle = "#2d3336"; 163 ctx.lineWidth = 1; 164 ctx.beginPath(); 165 ctx.moveTo(0, canvas.height / 2); 166 ctx.lineTo(canvas.width, canvas.height / 2); 167 ctx.stroke(); 168 ctx.strokeStyle = isPlaying ? "#ff8c00" : "#4ade80"; 169 ctx.lineWidth = 2; 170 ctx.shadowBlur = isPlaying ? 6 : 2; 171 ctx.shadowColor = isPlaying ? "#ff8c00" : "#4ade80"; 172 ctx.beginPath(); 173 const sliceWidth = canvas.width / bufferLength; 174 let x = 0; 175 for (let i = 0; i < bufferLength; i++) { 176 const v = dataArray[i] / 128; 177 const y = v * canvas.height / 2; 178 if (i === 0) { 179 ctx.moveTo(x, y); 180 } else { 181 ctx.lineTo(x, y); 182 }
183 x += sliceWidth; 184 } 185 ctx.lineTo(canvas.width, canvas.height / 2); 186 ctx.stroke(); 187 ctx.shadowBlur = 0; 188 } 189 } else if (canvasRef.current) { 190 const canvas = canvasRef.current; 191 const ctx = canvas.getContext("2d"); 192 if (ctx) { 193 ctx.fillStyle = "#202528"; 194 ctx.fillRect(0, 0, canvas.width, canvas.height); 195 ctx.strokeStyle = "#4ade80"; 196 ctx.lineWidth = 2; 197 ctx.beginPath(); 198 ctx.moveTo(0, canvas.height / 2); 199 ctx.lineTo(canvas.width, canvas.height / 2); 200 ctx.stroke(); 201 } 202 } 203 }; 204 const updatePerformanceMetrics = () => { 205 if (engine) { 206 const voices = engine.getVoicesAmount(); 207 setVoicesCount(voices); 208 const cpu = Math.min(99, Math.max(1, Math.round(voices * 0.8 + (isPlaying ? 4 : 1) + Math.random() * 1.5))); 209 setCpuLoad(cpu); 210 const memoryPerformance = performance.memory; 211 if (memoryPerformance) { 212 const jsHeap = Math.round(memoryPerformance.usedJSHeapSize / (1024 * 1024)); 213 setMemoryEstimation(jsHeap); 214 } else { 215 const baseMemory = 296; 216 const noise = Math.round(Math.sin(Date.now() / 2e3) * 4); 217 setMemoryEstimation(baseMemory + noise + voices); 218 } 219 } 220 }; 221 const tick = () => { 222 drawOscilloscope(); 223 updatePerformanceMetrics(); 224 animationFrameId = requestAnimationFrame(tick); 225 }; 226 tick(); 227 return () => { 228 cancelAnimationFrame(animationFrameId); 229 }; 230 }, [engine, isPlaying]); 231 if (!isEngineReady) { 232 return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-zinc-500 text-sm font-mono flex items-center h-full px-4 select-none", children: t("editor.initializingTransport", "Initializing Transport...") }); 233 } 234 return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 235 /* @__PURE__ */ jsxRuntimeExports.jsx("style", { children: ` 236 @import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&family=Share+Tech+Mono&display=swap'); 237 238 .digital-clock { 239 font-family: 'Share Tech Mono', 'Orbitron', monospace; 240 text-shadow: 0 0 6px rgba(74, 222, 128, 0.4); 241 } 242 .digital-clock-playing { 243 text-shadow: 0 0 8px rgba(255, 140, 0, 0.6); 244 } 245 .no-scrollbar::-webkit-scrollbar {
246 display: none; 247 } 248 .no-scrollbar { 249 -ms-overflow-style: none; 250 scrollbar-width: none; 251 } 252 ` }), 253 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center bg-[#25292c] border border-zinc-800 rounded px-2 py-1 gap-2.5 h-11 select-none overflow-x-auto no-scrollbar shadow-md", children: [ 254 !easyMode && /* @__PURE__ */ jsxRuntimeExports.jsx(HelpTip, { text: t("editor.playbackModeHelp", "Pattern / Playlist Song playback selector"), position: "bottom", className: "shrink-0", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col border border-zinc-700 bg-zinc-900 rounded overflow-hidden h-9 w-12 text-[10px] font-bold", children: [ 255 /* @__PURE__ */ jsxRuntimeExports.jsx( 256 "button", 257 { 258 onClick: () => setPlaybackMode("pat"), 259 className: `flex-1 flex items-center justify-center transition-all ${playbackMode === "pat" ? "bg-gradient-to-b from-[#ff8c00] to-[#cc7000] text-black shadow-[0_0_10px_rgba(255,140,0,0.5)]" : "text-zinc-500 hover:text-zinc-300 bg-transparent"}`, 260 children: "PAT" 261 } 262 ),
263 /* @__PURE__ */ jsxRuntimeExports.jsx( 264 "button", 265 { 266 onClick: () => setPlaybackMode("song"), 267 className: `flex-1 flex items-center justify-center border-t border-zinc-800 transition-all ${playbackMode === "song" ? "bg-gradient-to-b from-[#ff8c00] to-[#cc7000] text-black shadow-[0_0_10px_rgba(255,140,0,0.5)]" : "text-zinc-500 hover:text-zinc-300 bg-transparent"}`, 268 children: "SONG" 269 } 270 ) 271 ] }) }), 272 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center bg-zinc-900 border border-zinc-700 rounded h-9 px-1 gap-0.5 shrink-0", children: [ 273 /* @__PURE__ */ jsxRuntimeExports.jsx(HelpTip, { text: isPlaying ? t("editor.pause", "Pause") : t("editor.play", "Play"), position: "bottom", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 274 "button", 275 { 276 onClick: isPlaying ? pause : play, 277 className: `w-7 h-7 rounded flex items-center justify-center transition-colors ${isPlaying ? "bg-gradient-to-b from-[#ff9f3b] to-[#e67300] text-black shadow-[0_0_8px_rgba(230,115,0,0.6)]" : "text-zinc-400 hover:text-white hover:bg-zinc-800"}`,
278 children: isPlaying ? /* @__PURE__ */ jsxRuntimeExports.jsx(Pause, { size: 13, fill: "currentColor", stroke: "none" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Play, { size: 13, fill: "currentColor", stroke: "none" }) 279 } 280 ) }), 281 /* @__PURE__ */ jsxRuntimeExports.jsx(HelpTip, { text: t("editor.stopHelp", "Stop playback & Reset playhead"), position: "bottom", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 282 "button", 283 { 284 onClick: stop, 285 className: "w-7 h-7 rounded flex items-center justify-center text-zinc-400 hover:text-white hover:bg-zinc-800 transition-colors", 286 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Square, { size: 12, fill: "currentColor", stroke: "none" }) 287 } 288 ) }), 289 /* @__PURE__ */ jsxRuntimeExports.jsx(HelpTip, { text: t("editor.recordArmHelp", "Arm sequencer for recording MIDI input"), position: "bottom", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 290 "button", 291 { 292 onClick: () => setIsRecording(!isRecording), 293 className: `w-7 h-7 rounded flex items-center justify-center transition-colors ${isRecording ? "bg-red-600/30 text-red-500 border border-red-500/40 shadow-[0_0_8px_rgba(239,68,68,0.4)]" : "text-zinc-400 hover:text-red-500 hover:bg-zinc-800"}`, 294 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Circle, { size: 10, fill: "currentColor", stroke: "none" }) 295 } 296 ) }) 297 ] }),
298 /* @__PURE__ */ jsxRuntimeExports.jsx(HelpTip, { text: t("editor.bpmHelp", "BPM (Double-click to type, Drag up/down, Scroll wheel)"), position: "bottom", className: "shrink-0", children: /* @__PURE__ */ jsxRuntimeExports.jsxs( 299 "div", 300 { 301 ref: bpmRef, 302 onWheel: handleBpmWheel, 303 onMouseDown: handleBpmMouseDown, 304 onDoubleClick: handleBpmDoubleClick, 305 className: `bg-[#2c3236] border border-zinc-700 rounded h-9 w-[74px] flex items-center justify-center font-mono text-sm relative cursor-ns-resize ${isDraggingBpm ? "border-amber-500/80 bg-zinc-800" : "text-zinc-300"}`, 306 children: [ 307 isBpmEditing ? /* @__PURE__ */ jsxRuntimeExports.jsx( 308 "input", 309 { 310 type: "text", 311 value: bpmInputValue, 312 onChange: (e) => setBpmInputValue(e.target.value), 313 onBlur: handleBpmInputBlur, 314 onKeyDown: handleBpmInputKeyDown, 315 autoFocus: true, 316 className: "bg-zinc-950 text-amber-500 font-bold outline-none border border-amber-600 text-center w-full h-full text-xs rounded" 317 } 318 ) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center justify-center w-full select-none", children: [
319 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-zinc-500 leading-3 font-sans font-bold select-none", children: t("editor.tempoLabel", "TEMPO") }), 320 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold tracking-tight text-zinc-200", children: tempo.toFixed(easyMode ? 0 : 3) }) 321 ] }), 322 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute right-0.5 top-0 bottom-0 flex flex-col justify-center gap-0.5 text-zinc-600 pointer-events-none", children: [ 323 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[8px]", children: "â²" }), 324 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[8px]", children: "â¼" }) 325 ] }) 326 ] 327 } 328 ) }), 329 /* @__PURE__ */ jsxRuntimeExports.jsx(HelpTip, { text: t("editor.currentTimeHelp", "Current playback position (Bar:Step:Tick)"), position: "bottom", className: "shrink-0", children: /* @__PURE__ */ jsxRuntimeExports.jsxs( 330 "div", 331 { 332 onClick: () => seek(0), 333 className: "bg-zinc-950 border border-zinc-800 rounded h-9 px-3 min-w-[110px] flex items-center justify-between relative cursor-pointer group hover:border-zinc-700 transition-colors", 334 children: [
335 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `digital-clock text-xl font-bold tracking-widest leading-none ${isPlaying ? "text-[#ff8c00] digital-clock-playing" : "text-emerald-400"}`, children: formatBST(currentTime) }), 336 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex flex-col text-[8px] font-bold text-zinc-500 text-right font-sans pl-1.5 justify-center leading-3", children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: isPlaying ? "text-[#ff8c00]" : "text-emerald-500", children: "B:S:T" }) }) 337 ] 338 } 339 ) }), 340 !easyMode && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center bg-zinc-900 border border-zinc-700 rounded h-9 px-1 gap-1 shrink-0", children: [ 341 /* @__PURE__ */ jsxRuntimeExports.jsx(HelpTip, { text: "Typing keyboard to piano keyboard toggle (Use keys A,W,S,E,D,F,T,G,Y,H,U,J...)", position: "bottom", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 342 "button", 343 { 344 onClick: () => setIsTypingKeyboardEnabled(!isTypingKeyboardEnabled), 345 className: `w-7 h-7 rounded flex items-center justify-center transition-colors ${isTypingKeyboardEnabled ? "bg-gradient-to-b from-[#ff8c00] to-[#cc7000] text-black shadow-[0_0_8px_rgba(255,140,0,0.5)]" : "text-zinc-400 hover:text-white hover:bg-zinc-800"}`, 346 children: /* @__PURE__ */ jsxRuntimeExports.jsx("svg", { viewBox: "0 0 24 24", width: "13", height: "13", fill: "currentColor", children: /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M20 5H4c-1.1 0-1.99.9-1.99 2L2 17c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm-9 3h2v3h-2V8zm0 4h2v3h-2v-3zM8 8h2v3H8V8zm0 4h2v3H8v-3zM5 8h2v3H5V8zm0 4h2v3H5v-3zm14 3h-4v-3h4v3zm0-4h-2V8h2v3zm-3-3h2v3h-2V8z" }) }) 347 } 348 ) }),
349 /* @__PURE__ */ jsxRuntimeExports.jsx(HelpTip, { text: "1-bar Countdown (3.2.1) before recording", position: "bottom", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 350 "button", 351 { 352 onClick: () => setIsCountdownEnabled(!isCountdownEnabled), 353 className: `w-7 h-7 rounded flex items-center justify-center font-mono text-[10px] font-bold tracking-tighter leading-none transition-colors ${isCountdownEnabled ? "bg-gradient-to-b from-[#ff8c00] to-[#cc7000] text-black shadow-[0_0_8px_rgba(255,140,0,0.5)]" : "text-zinc-400 hover:text-white hover:bg-zinc-800"}`, 354 children: "3.2.1" 355 } 356 ) }), 357 /* @__PURE__ */ jsxRuntimeExports.jsx(HelpTip, { text: "Audible Metronome click track", position: "bottom", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 358 "button", 359 { 360 onClick: () => setIsMetronomeEnabled(!isMetronomeEnabled), 361 className: `w-7 h-7 rounded flex items-center justify-center transition-colors ${isMetronomeEnabled ? "bg-gradient-to-b from-[#ff8c00] to-[#cc7000] text-black shadow-[0_0_8px_rgba(255,140,0,0.5)]" : "text-zinc-400 hover:text-white hover:bg-zinc-800"}`, 362 children: /* @__PURE__ */ jsxRuntimeExports.jsx("svg", { viewBox: "0 0 24 24", width: "13", height: "13", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M12 2L5 22h14L12 2zm0 6v10M9 22h6" }) }) 363 } 364 ) }),
365 /* @__PURE__ */ jsxRuntimeExports.jsx(HelpTip, { text: "Loop recording / overdub mode", position: "bottom", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 366 "button", 367 { 368 onClick: () => setIsLoopRecordingEnabled(!isLoopRecordingEnabled), 369 className: `w-7 h-7 rounded flex items-center justify-center transition-colors ${isLoopRecordingEnabled ? "bg-gradient-to-b from-[#ff8c00] to-[#cc7000] text-black shadow-[0_0_8px_rgba(255,140,0,0.5)]" : "text-zinc-400 hover:text-white hover:bg-zinc-800"}`, 370 children: /* @__PURE__ */ jsxRuntimeExports.jsx("svg", { viewBox: "0 0 24 24", width: "13", height: "13", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M21.5 2v6h-6M21.34 15.57a10 10 0 11-.57-8.38l.57-.57" }) }) 371 } 372 ) }) 373 ] }), 374 !easyMode && /* @__PURE__ */ jsxRuntimeExports.jsx(HelpTip, { text: "Real-time Master output Oscilloscope", position: "bottom", className: "shrink-0", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "border border-zinc-700 bg-zinc-950 rounded overflow-hidden h-9 w-[100px] relative", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 375 "canvas", 376 { 377 ref: canvasRef, 378 width: "100", 379 height: "36", 380 className: "w-full h-full block" 381 } 382 ) }) }), 383 !easyMode && /* @__PURE__ */ jsxRuntimeExports.jsx(HelpTip, { text: `Master Volume: ${Math.round(masterVolume * 100)}% | Pitch: ${masterPitch > 0 ? `+${masterPitch}` : masterPitch} semitones`, position: "bottom", className: "shrink-0", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col justify-center border border-zinc-700 bg-zinc-900 rounded h-9 px-2 gap-1 min-w-[120px] shrink-0 text-[9px] font-sans font-bold", children: [
384 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5 w-full", children: [ 385 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-500 w-2.5", children: "V" }), 386 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 h-2.5 bg-zinc-950 rounded border border-zinc-800 relative overflow-hidden flex items-center", children: [ 387 /* @__PURE__ */ jsxRuntimeExports.jsx( 388 "input", 389 { 390 type: "range", 391 min: "0", 392 max: "1.2", 393 step: "0.05", 394 value: masterVolume, 395 onChange: (e) => setMasterVolume(parseFloat(e.target.value)), 396 className: "absolute inset-0 opacity-0 cursor-pointer z-10 w-full", 397 title: `Volume: ${Math.round(masterVolume * 100)}%` 398 } 399 ), 400 /* @__PURE__ */ jsxRuntimeExports.jsx( 401 "div", 402 { 403 className: "h-full bg-gradient-to-r from-amber-500 to-[#ff8c00] shadow-[0_0_6px_rgba(255,140,0,0.6)]", 404 style: { width: `${Math.min(100, masterVolume / 1.2 * 100)}%` } 405 } 406 ) 407 ] }) 408 ] }),
409 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5 w-full", children: [ 410 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-500 w-2.5", children: "P" }), 411 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 h-2.5 bg-zinc-950 rounded border border-zinc-800 relative overflow-hidden flex items-center", children: [ 412 /* @__PURE__ */ jsxRuntimeExports.jsx( 413 "input", 414 { 415 type: "range", 416 min: "-12", 417 max: "12", 418 step: "1", 419 value: masterPitch, 420 onChange: (e) => setMasterPitch(parseInt(e.target.value, 10)), 421 className: "absolute inset-0 opacity-0 cursor-pointer z-10 w-full", 422 title: `Master Pitch: ${masterPitch > 0 ? `+${masterPitch}` : masterPitch} semitones` 423 } 424 ), 425 /* @__PURE__ */ jsxRuntimeExports.jsx( 426 "div", 427 { 428 className: "h-full bg-gradient-to-r from-emerald-500 to-teal-400 shadow-[0_0_6px_rgba(52,211,153,0.6)] absolute transition-all duration-75", 429 style: { 430 left: masterPitch >= 0 ? "50%" : `${50 + masterPitch / 12 * 50}%`, 431 width: `${Math.abs(masterPitch / 12) * 50}%` 432 } 433 } 434 ),
435 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute left-1/2 -translate-x-1/2 top-0 bottom-0 w-0.5 bg-zinc-700" }) 436 ] }) 437 ] }) 438 ] }) }), 439 !easyMode && /* @__PURE__ */ jsxRuntimeExports.jsx(HelpTip, { text: "System telemetry: Polyphony count, memory footprint, CPU load", position: "bottom", className: "shrink-0", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "border border-zinc-700 bg-zinc-900 rounded h-9 px-2 min-w-[130px] flex items-center justify-between gap-2.5 text-[9px] font-mono text-zinc-400", children: [ 440 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col justify-center leading-3", children: [ 441 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1", children: [ 442 /* @__PURE__ */ jsxRuntimeExports.jsx("svg", { viewBox: "0 0 24 24", width: "9", height: "9", fill: "currentColor", className: "text-zinc-500", children: /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M4 6H2v14c0 1.1.9 2 2 2h14v-2H4V6zm16-4H8c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h12c1.1 0-2-.9-2-2V4c0-1.1-.9-2-2-2zm0 14H8V4h12v12z" }) }),
443 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 444 "VOICES: ", 445 /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { className: "text-zinc-200", children: voicesCount }) 446 ] }) 447 ] }), 448 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1", children: [ 449 /* @__PURE__ */ jsxRuntimeExports.jsx("svg", { viewBox: "0 0 24 24", width: "9", height: "9", fill: "currentColor", className: "text-zinc-500", children: /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-4 6h-4v2h4v-2zm0 4h-4v2h4v-2z" }) }), 450 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 451 "MEM: ", 452 /* @__PURE__ */ jsxRuntimeExports.jsxs("strong", { className: "text-zinc-200", children: [ 453 memoryEstimation, 454 " MB" 455 ] }) 456 ] }) 457 ] }) 458 ] }),
459 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col justify-center w-12 gap-0.5 leading-none", children: [ 460 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[8px] text-zinc-500 font-sans font-bold", children: "CPU LOAD" }), 461 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-full h-1.5 bg-zinc-950 border border-zinc-800 rounded-sm relative overflow-hidden flex items-center", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 462 "div", 463 { 464 className: `h-full transition-all duration-100 ${cpuLoad > 60 ? "bg-red-500" : cpuLoad > 30 ? "bg-amber-500" : "bg-emerald-500"}`, 465 style: { width: `${cpuLoad}%` } 466 } 467 ) }), 468 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[8px] text-zinc-300 font-bold self-end", children: [ 469 cpuLoad, 470 "%" 471 ] }) 472 ] }) 473 ] }) }) 474 ] }) 475 ] }); 476}; 477const FLHorizontalScrollbar = ({ 478 containerRef, 479 zoomX, 480 setZoomX, 481 maxTicks, 482 ppq, 483 beatWidth, 484 mode = "piano-roll", 485 patterns = [], 486 playlistClips = [], 487 activePatternId = null, 488 playlistTracks = [] 489}) => { 490 const trackRef = reactExports.useRef(null); 491 const thumbRef = reactExports.useRef(null); 492 const canvasRef = reactExports.useRef(null); 493 const drawMiniMap = reactExports.useCallback(() => { 494 if (!canvasRef.current || !trackRef.current) return; 495 const canvas = canvasRef.current; 496 const ctx = canvas.getContext("2d"); 497 if (!ctx) return; 498 const rect = trackRef.current.getBoundingClientRect(); 499 if (rect.width === 0 || rect.height === 0) return; 500 canvas.width = rect.width; 501 canvas.height = rect.height; 502 ctx.clearRect(0, 0, canvas.width, canvas.height); 503 if (mode === "timeline") { 504 if (!playlistClips || playlistClips.length === 0 || !patterns || !playlistTracks || playlistTracks.length === 0) return; 505 const N = playlistTracks.length; 506 playlistClips.forEach((clip) => { 507 const pattern = patterns.find((p) => p.id === clip.patternId); 508 if (!pattern) return; 509 const left = clip.startTick / maxTicks * canvas.width; 510 const width = clip.durationTicks / maxTicks * canvas.width; 511 const trackH = canvas.height / N; 512 const top = clip.trackIndex * trackH; 513 ctx.fillStyle = pattern.color + "25"; 514 ctx.strokeStyle = pattern.color + "80"; 515 ctx.lineWidth = 1; 516 ctx.fillRect(left, top + 1, width, trackH - 2); 517 ctx.strokeRect(left, top + 1, width, trackH - 2); 518 if (pattern.notes.length === 0) return; 519 const patternLength = pattern.notes.reduce((max, n) => Math.max(max, n.ticks + n.durationTicks), 0) || ppq * 4; 520 if (patternLength <= 0) return; 521 const midis = pattern.notes.map((n) => n.midi); 522 const minM = Math.min(...midis) - 1; 523 const maxM = Math.max(...midis) + 1; 524 const mDiff = maxM - minM || 1; 525 const numRepeats = Math.ceil(clip.durationTicks / patternLength); 526 ctx.fillStyle = pattern.color + "cc"; 527 for (let r = 0; r < numRepeats; r++) { 528 const loopOffset = r * patternLength - clip.offsetTicks; 529 pattern.notes.forEach((note) => { 530 const noteGlobalTick = clip.startTick + loopOffset + note.ticks; 531 if (noteGlobalTick < clip.startTick || noteGlobalTick >= clip.startTick + clip.durationTicks) return; 532 const noteLeft = noteGlobalTick / maxTicks * canvas.width; 533 const noteWidth = note.durationTicks / maxTicks * canvas.width; 534 const noteYRatio = (note.midi - minM) / mDiff; 535 const noteTop = top + 2 + (1 - noteYRatio) * (trackH - 5); 536 ctx.fillRect(noteLeft, noteTop, Math.max(1.5, noteWidth), 1); 537 }); 538 } 539 }); 540 } else if (mode === "piano-roll") { 541 const activePattern = patterns == null ? void 0 : patterns.find((p) => p.id === activePatternId); 542 if (!activePattern || !activePattern.notes || activePattern.notes.length === 0) return; 543 const notes = activePattern.notes; 544 const midis = notes.map((n) => n.midi); 545 const minM = Math.min(...midis) - 2; 546 const maxM = Math.max(...midis) + 2; 547 const midiDiff = maxM - minM || 1; 548 ctx.fillStyle = activePattern.color || "#6366f1";
549 notes.forEach((note) => { 550 const left = note.ticks / maxTicks * canvas.width; 551 const width = note.durationTicks / maxTicks * canvas.width; 552 const noteYRatio = (note.midi - minM) / midiDiff; 553 const top = (1 - noteYRatio) * (canvas.height - 4) + 2; 554 ctx.fillRect(left, top, Math.max(2, width), 2); 555 }); 556 } 557 }, [mode, patterns, playlistClips, playlistTracks, activePatternId, maxTicks, ppq]); 558 const syncThumb = reactExports.useCallback(() => { 559 if (!containerRef.current || !trackRef.current || !thumbRef.current) return; 560 const container = containerRef.current; 561 const trackWidth = trackRef.current.clientWidth; 562 const viewportWidth = container.clientWidth; 563 const scrollLeft = container.scrollLeft; 564 const pxPerTick = beatWidth * zoomX / ppq; 565 const contentWidth = maxTicks * pxPerTick; 566 let f_w = viewportWidth / contentWidth; 567 if (f_w > 1) f_w = 1; 568 const f_x = scrollLeft / contentWidth; 569 const thumbWidth = f_w * trackWidth; 570 const thumbLeft = f_x * trackWidth; 571 thumbRef.current.style.width = `${Math.max(thumbWidth, 24)}px`; 572 thumbRef.current.style.transform = `translateX(${thumbLeft}px)`; 573 drawMiniMap(); 574 }, [zoomX, maxTicks, ppq, beatWidth, containerRef, drawMiniMap]); 575 reactExports.useEffect(() => { 576 const container = containerRef.current; 577 if (container) { 578 container.addEventListener("scroll", syncThumb); 579 window.addEventListener("resize", syncThumb); 580 } 581 syncThumb(); 582 return () => { 583 if (container) container.removeEventListener("scroll", syncThumb); 584 window.removeEventListener("resize", syncThumb); 585 }; 586 }, [syncThumb, containerRef]); 587 reactExports.useEffect(() => { 588 drawMiniMap(); 589 }, [drawMiniMap, patterns, playlistClips, playlistTracks, activePatternId]); 590 const isDraggingRef = reactExports.useRef(false); 591 const dragModeRef = reactExports.useRef(null); 592 const dragStartXRef = reactExports.useRef(0); 593 const startScrollLeftRef = reactExports.useRef(0); 594 const startZoomXRef = reactExports.useRef(0); 595 const MIN_ZOOM = 0.1; 596 const MAX_ZOOM = 10; 597 const handlePointerDown = (e, mode2) => { 598 e.stopPropagation(); 599 e.currentTarget.setPointerCapture(e.pointerId); 600 isDraggingRef.current = true; 601 dragModeRef.current = mode2; 602 dragStartXRef.current = e.clientX; 603 if (containerRef.current) { 604 startScrollLeftRef.current = containerRef.current.scrollLeft; 605 } 606 startZoomXRef.current = zoomX; 607 }; 608 const handlePointerMove = (e) => { 609 if (!isDraggingRef.current || !containerRef.current || !trackRef.current) return; 610 const deltaX = e.clientX - dragStartXRef.current; 611 const trackWidth = trackRef.current.clientWidth; 612 const viewportWidth = containerRef.current.clientWidth; 613 const startPxPerTick = beatWidth * startZoomXRef.current / ppq; 614 const startContentWidth = maxTicks * startPxPerTick; 615 if (dragModeRef.current === "center") { 616 const scrollDelta = deltaX / trackWidth * startContentWidth; 617 containerRef.current.scrollLeft = startScrollLeftRef.current + scrollDelta; 618 } else if (dragModeRef.current === "right") { 619 const f_w_old = viewportWidth / startContentWidth; 620 const thumbWidthOld = f_w_old * trackWidth; 621 let thumbWidthNew = thumbWidthOld + deltaX; 622 thumbWidthNew = Math.max(24, Math.min(trackWidth, thumbWidthNew)); 623 const newContentWidth = viewportWidth * trackWidth / thumbWidthNew; 624 const newPxPerTick = newContentWidth / maxTicks; 625 let newZoomX = newPxPerTick * ppq / beatWidth; 626 newZoomX = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, newZoomX)); 627 const actualNewPxPerTick = beatWidth * newZoomX / ppq; 628 const tickLeft = startScrollLeftRef.current / startPxPerTick; 629 containerRef.current.scrollLeft = tickLeft * actualNewPxPerTick; 630 setZoomX(newZoomX); 631 } else if (dragModeRef.current === "left") { 632 const f_w_old = viewportWidth / startContentWidth; 633 const thumbWidthOld = f_w_old * trackWidth; 634 let thumbWidthNew = thumbWidthOld - deltaX; 635 thumbWidthNew = Math.max(24, Math.min(trackWidth, thumbWidthNew)); 636 const newContentWidth = viewportWidth * trackWidth / thumbWidthNew; 637 const newPxPerTick = newContentWidth / maxTicks; 638 let newZoomX = newPxPerTick * ppq / beatWidth;
639 newZoomX = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, newZoomX)); 640 const actualNewPxPerTick = beatWidth * newZoomX / ppq; 641 const tickRight = (startScrollLeftRef.current + viewportWidth) / startPxPerTick; 642 containerRef.current.scrollLeft = tickRight * actualNewPxPerTick - viewportWidth; 643 setZoomX(newZoomX); 644 } 645 }; 646 const handlePointerUp = (e) => { 647 if (!isDraggingRef.current) return; 648 isDraggingRef.current = false; 649 dragModeRef.current = null; 650 e.currentTarget.releasePointerCapture(e.pointerId); 651 }; 652 const handleTrackClick = (e) => { 653 if (isDraggingRef.current) return; 654 if (!containerRef.current || !trackRef.current) return; 655 if (e.target !== trackRef.current && e.target !== canvasRef.current) return; 656 const rect = trackRef.current.getBoundingClientRect(); 657 const clickX = e.clientX - rect.left; 658 const trackWidth = trackRef.current.clientWidth; 659 const viewportWidth = containerRef.current.clientWidth; 660 const pxPerTick = beatWidth * zoomX / ppq; 661 const contentWidth = maxTicks * pxPerTick; 662 const f_w = viewportWidth / contentWidth; 663 const thumbWidth = f_w * trackWidth; 664 let newThumbLeft = clickX - thumbWidth / 2; 665 newThumbLeft = Math.max(0, Math.min(trackWidth - thumbWidth, newThumbLeft)); 666 const newScrollLeft = newThumbLeft / trackWidth * contentWidth; 667 containerRef.current.scrollLeft = newScrollLeft; 668 }; 669 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 670 "div", 671 { 672 className: "flex-1 relative h-full bg-zinc-300 dark:bg-zinc-800 shadow-inner overflow-hidden cursor-pointer", 673 ref: trackRef, 674 onPointerDown: (e) => { 675 if (e.target === trackRef.current || e.target === canvasRef.current) { 676 handleTrackClick(e); 677 handlePointerDown(e, "center"); 678 } 679 }, 680 onPointerMove: handlePointerMove, 681 onPointerUp: handlePointerUp, 682 onPointerCancel: handlePointerUp, 683 onWheel: (e) => { 684 if (e.ctrlKey || e.metaKey) { 685 e.preventDefault(); 686 const zoomFactor = e.deltaY > 0 ? -0.25 : 0.25; 687 if (!containerRef.current || !trackRef.current) return; 688 const viewportWidth = containerRef.current.clientWidth; 689 const pxPerTick = beatWidth * zoomX / ppq; 690 const centerTick = (containerRef.current.scrollLeft + viewportWidth / 2) / pxPerTick;
691 const newZoomX = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, zoomX + zoomFactor)); 692 const newPxPerTick = beatWidth * newZoomX / ppq; 693 containerRef.current.scrollLeft = centerTick * newPxPerTick - viewportWidth / 2; 694 setZoomX(newZoomX); 695 } else { 696 if (containerRef.current) { 697 containerRef.current.scrollLeft += e.deltaY > 0 ? 100 : -100; 698 } 699 } 700 }, 701 children: [ 702 /* @__PURE__ */ jsxRuntimeExports.jsx( 703 "canvas", 704 { 705 ref: canvasRef, 706 className: "absolute inset-0 w-full h-full pointer-events-none opacity-40 dark:opacity-60" 707 } 708 ), 709 /* @__PURE__ */ jsxRuntimeExports.jsxs( 710 "div", 711 { 712 ref: thumbRef, 713 className: "absolute top-0 h-full bg-zinc-400/20 dark:bg-zinc-500/20 hover:bg-zinc-400/30 dark:hover:bg-zinc-500/30 rounded-sm shadow-inner flex items-center justify-between group border-l border-r border-indigo-500/30 dark:border-indigo-400/30 backdrop-blur-[1px]", 714 style: { left: 0, width: 0, willChange: "transform, width" }, 715 onPointerDown: (e) => handlePointerDown(e, "center"), 716 children: [
717 /* @__PURE__ */ jsxRuntimeExports.jsx( 718 "div", 719 { 720 className: "w-2 h-full cursor-ew-resize bg-zinc-400/70 hover:bg-indigo-500 dark:bg-zinc-600/70 dark:hover:bg-indigo-400 transition-colors shrink-0 z-20", 721 onPointerDown: (e) => handlePointerDown(e, "left") 722 } 723 ), 724 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-[2px] pointer-events-none opacity-40 select-none", children: [ 725 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-[1px] h-3 bg-zinc-900 dark:bg-zinc-200" }), 726 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-[1px] h-3 bg-zinc-900 dark:bg-zinc-200" }) 727 ] }), 728 /* @__PURE__ */ jsxRuntimeExports.jsx( 729 "div", 730 { 731 className: "w-2 h-full cursor-ew-resize bg-zinc-400/70 hover:bg-indigo-500 dark:bg-zinc-600/70 dark:hover:bg-indigo-400 transition-colors shrink-0 z-20", 732 onPointerDown: (e) => handlePointerDown(e, "right") 733 } 734 ) 735 ] 736 } 737 ) 738 ] 739 } 740 ); 741}; 742const BEAT_WIDTH$1 = 40; 743const ArrangementView = ({ isFloating = false, isPoppedOut = false }) => { 744 var _a, _b; 745 const { 746 engine, 747 activeTrackIndex, 748 setActiveTrackIndex, 749 midiVersion, 750 forceSync, 751 patterns, 752 setPatterns, 753 activePatternId, 754 setActivePatternId, 755 playlistTracks, 756 setPlaylistTracks, 757 playlistClips, 758 setPlaylistClips, 759 selectedClipIds, 760 setSelectedClipIds, 761 automationClips, 762 setAutomationClips, 763 timeMarkers, 764 setTimeMarkers, 765 loopRegion, 766 setLoopRegion, 767 gridSnap, 768 setGridSnap, 769 currentPlaylistTool, 770 setCurrentPlaylistTool, 771 easyMode, 772 makePatternUnique, 773 setHint, 774 currentTime, 775 playbackMode, 776 isPlaying 777 } = useEditor(); 778 const [zoomX, setZoomX] = reactExports.useState(1); 779 const [zoomY, setZoomY] = reactExports.useState(1); 780 const [isAutomationVisible, setIsAutomationVisible] = reactExports.useState(true); 781 const [renamingPatternId, setRenamingPatternId] = reactExports.useState(null); 782 const [renamingPatternText, setRenamingPatternText] = reactExports.useState(""); 783 const [renamingTrackId, setRenamingTrackId] = reactExports.useState(null); 784 const [renamingTrackText, setRenamingTrackText] = reactExports.useState(""); 785 const [collapsedGroups, setCollapsedGroups] = reactExports.useState({}); 786 const [patternColWidth, setPatternColWidth] = reactExports.useState(224); 787 const [trackColWidth, setTrackColWidth] = reactExports.useState(240); 788 const [isHelpOpen, setIsHelpOpen] = reactExports.useState(false); 789 const [activeHelpTab, setActiveHelpTab] = reactExports.useState("shortcuts"); 790 const [activeDragClipId, setActiveDragClipId] = reactExports.useState(null); 791 const [activeDragAction, setActiveDragAction] = reactExports.useState(null); 792 const [dragStartTick, setDragStartTick] = reactExports.useState(0); 793 const [dragStartTrackIdx, setDragStartTrackIdx] = reactExports.useState(0); 794 const [dragStartOffsetTicks, setDragStartOffsetTicks] = reactExports.useState(0); 795 const [dragStartClipState, setDragStartClipState] = reactExports.useState(null); 796 const [dragStartMouseX, setDragStartMouseX] = reactExports.useState(0); 797 const [lassoStart, setLassoStart] = reactExports.useState(null); 798 const [lassoCurrent, setLassoCurrent] = reactExports.useState(null); 799 const [sliceStart, setSliceStart] = reactExports.useState(null); 800 const [sliceCurrent, setSliceCurrent] = reactExports.useState(null); 801 const [dragNodeId, setDragNodeId] = reactExports.useState(null); 802 const [dragNodeClipId, setDragNodeClipId] = reactExports.useState(null); 803 const [dragNodeStartProps, setDragNodeStartProps] = reactExports.useState(null); 804 const [dragTensionClipId, setDragTensionClipId] = reactExports.useState(null); 805 const [dragTensionNodeIdx, setDragTensionNodeIdx] = reactExports.useState(null); 806 const [activeColorPickerId, setActiveColorPickerId] = reactExports.useState(null); 807 const [isAltPressed, setIsAltPressed] = reactExports.useState(false); 808 const [isShiftPressed, setIsShiftPressed] = reactExports.useState(false); 809 const [isCtrlPressed, setIsCtrlPressed] = reactExports.useState(false); 810 const [contextMenu, setContextMenu] = reactExports.useState(null); 811 const arrangementPlayheadRef = reactExports.useRef(null); 812 const lastPaintedTickRef = reactExports.useRef(null); 813 const lastPaintedTrackIdxRef = reactExports.useRef(null);
814 const verticalScrollContainerRef = reactExports.useRef(null); 815 const panStartRef = reactExports.useRef(null); 816 const [isPanning, setIsPanning] = reactExports.useState(false); 817 const rulerDragActionRef = reactExports.useRef(null); 818 const rulerStartTickRef = reactExports.useRef(0); 819 const hasRulerDraggedRef = reactExports.useRef(false); 820 reactExports.useEffect(() => { 821 if (easyMode) { 822 setCurrentPlaylistTool("draw"); 823 setGridSnap("beat"); 824 } 825 }, [easyMode, setCurrentPlaylistTool, setGridSnap]); 826 const ppq = ((_b = (_a = engine == null ? void 0 : engine.midi) == null ? void 0 : _a.header) == null ? void 0 : _b.ppq) || 96; 827 const pxPerTick = BEAT_WIDTH$1 * zoomX / ppq; 828 reactExports.useEffect(() => { 829 if (!engine) return; 830 let rafId; 831 const updatePlayhead = () => { 832 const rawTime = engine.isPlaying() ? engine.getCurrentTime() : currentTime; 833 const rawTicks = engine.secondsToTicks(rawTime); 834 let displayTicks = rawTicks; 835 if (playbackMode === "pat") { 836 const activePattern = patterns.find((p) => p.id === activePatternId); 837 let patternLength = ppq * 4 * 4; 838 if (activePattern) { 839 const maxNoteTick = activePattern.notes.reduce((max, n) => Math.max(max, n.ticks + n.durationTicks), 0); 840 const ticksPerBar = ppq * 4; 841 patternLength = Math.max(ticksPerBar * 4, Math.ceil(maxNoteTick / ticksPerBar) * ticksPerBar); 842 } 843 displayTicks = patternLength > 0 ? rawTicks % patternLength : rawTicks; 844 } 845 const playheadX = displayTicks * pxPerTick; 846 if (arrangementPlayheadRef.current) { 847 arrangementPlayheadRef.current.style.left = `${playheadX}px`; 848 } 849 if (engine.isPlaying() && gridHorizontalScrollRef.current) { 850 const container = gridHorizontalScrollRef.current; 851 const scrollLeft = container.scrollLeft; 852 const width = container.clientWidth; 853 if (playheadX > scrollLeft + width - 50) { 854 container.scrollLeft = playheadX - width / 2; 855 } else if (playheadX < scrollLeft) { 856 container.scrollLeft = Math.max(0, playheadX - 50); 857 } 858 } 859 rafId = requestAnimationFrame(updatePlayhead); 860 }; 861 rafId = requestAnimationFrame(updatePlayhead); 862 return () => cancelAnimationFrame(rafId); 863 }, [engine, isPlaying, playbackMode, pxPerTick, activePatternId, patterns, ppq]); 864 reactExports.useEffect(() => { 865 const handleKeyDown = (e) => { 866 if (e.key === "Alt") { 867 setIsAltPressed(true); 868 } 869 if (e.key === "Shift") { 870 setIsShiftPressed(true); 871 } 872 if (e.key === "Control") { 873 setIsCtrlPressed(true); 874 } 875 }; 876 const handleKeyUp = (e) => { 877 if (e.key === "Alt") { 878 setIsAltPressed(false); 879 } 880 if (e.key === "Shift") { 881 setIsShiftPressed(false); 882 } 883 if (e.key === "Control") { 884 setIsCtrlPressed(false); 885 } 886 }; 887 const handleBlur = () => { 888 setIsAltPressed(false); 889 setIsShiftPressed(false); 890 setIsCtrlPressed(false); 891 }; 892 window.addEventListener("keydown", handleKeyDown); 893 window.addEventListener("keyup", handleKeyUp); 894 window.addEventListener("blur", handleBlur); 895 return () => { 896 window.removeEventListener("keydown", handleKeyDown); 897 window.removeEventListener("keyup", handleKeyUp); 898 window.removeEventListener("blur", handleBlur); 899 }; 900 }, []); 901 reactExports.useEffect(() => { 902 const container = gridHorizontalScrollRef.current; 903 if (!container) return; 904 const handleWheelEvent = (e) => { 905 if (e.ctrlKey || e.metaKey) { 906 e.preventDefault(); 907 const grid = gridRef.current; 908 if (!grid) return; 909 const gridRect = grid.getBoundingClientRect(); 910 const containerRect = container.getBoundingClientRect(); 911 const mouseXInGrid = e.clientX - gridRect.left; 912 const targetTick = mouseXInGrid / pxPerTick; 913 const zoomFactor = e.deltaY < 0 ? 1.15 : 0.85; 914 const nextZoomX = Math.max(0.2, Math.min(4, zoomX * zoomFactor)); 915 setZoomX(nextZoomX); 916 const nextPxPerTick = BEAT_WIDTH$1 * nextZoomX / ppq; 917 const mouseXInContainer = e.clientX - containerRect.left; 918 const nextScrollLeft = targetTick * nextPxPerTick - mouseXInContainer; 919 container.scrollLeft = nextScrollLeft; 920 } else if (e.altKey) { 921 e.preventDefault(); 922 const zoomFactor = e.deltaY < 0 ? 1.15 : 0.85; 923 setZoomY((y) => Math.max(0.5, Math.min(2.5, y * zoomFactor))); 924 } 925 }; 926 container.addEventListener("wheel", handleWheelEvent, { passive: false }); 927 return () => { 928 container.removeEventListener("wheel", handleWheelEvent); 929 }; 930 }, [zoomX, pxPerTick, ppq]); 931 reactExports.useEffect(() => { 932 const closeMenu = () => setContextMenu(null); 933 if (contextMenu) { 934 window.addEventListener("pointerdown", closeMenu); 935 window.addEventListener("click", closeMenu); 936 } 937 return () => { 938 window.removeEventListener("pointerdown", closeMenu); 939 window.removeEventListener("click", closeMenu); 940 }; 941 }, [contextMenu]); 942 const gridHorizontalScrollRef = reactExports.useRef(null);
943 const rulerScrollContainerRef = reactExports.useRef(null); 944 const gridRef = reactExports.useRef(null); 945 if (!engine || !engine.midi) return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "p-4 text-zinc-500", children: "Loading playlist..." }); 946 engine.getDuration() || 1; 947 const maxTicks = Math.max(engine.midi.durationTicks + ppq * 4 * 16, ppq * 4 * 32); 948 const totalWidth = maxTicks * pxPerTick; 949 const getSnappedTick = (rawTick2) => { 950 if (easyMode) { 951 const snapInterval = gridSnap === "bar" ? ppq * 4 : ppq; 952 return Math.max(0, Math.round(rawTick2 / snapInterval) * snapInterval); 953 } 954 if (isAltPressed) { 955 return Math.max(0, Math.round(rawTick2)); 956 } 957 let interval = 1; 958 switch (gridSnap) { 959 case "bar": 960 interval = ppq * 4; 961 break; 962 case "beat": 963 interval = ppq; 964 break; 965 case "1/2": 966 interval = ppq / 2; 967 break; 968 case "1/4": 969 interval = ppq / 4; 970 break; 971 case "1/6": 972 interval = ppq / 6; 973 break; 974 case "cell": 975 interval = ppq / 4; 976 break; 977 case "none": 978 return Math.max(0, Math.round(rawTick2)); 979 default: 980 interval = ppq; 981 } 982 return Math.max(0, Math.round(rawTick2 / interval) * interval); 983 }; 984 const handleHorizontalScroll = (e) => { 985 if (rulerScrollContainerRef.current) { 986 rulerScrollContainerRef.current.scrollLeft = e.currentTarget.scrollLeft; 987 } 988 }; 989 const handleAddPattern = () => { 990 const nextId = `pat-${Date.now()}`; 991 const newPat = { 992 id: nextId, 993 name: `Pattern ${patterns.length + 1}`, 994 color: `#${Math.floor((0.3 + 0.6 * Math.random()) * 16777215).toString(16)}`, 995 notes: [] 996 }; 997 setPatterns((prev) => [...prev, newPat]); 998 setActivePatternId(nextId); 999 }; 1000 const handleDeletePattern = (id, e) => { 1001 var _a2; 1002 e.stopPropagation(); 1003 if (patterns.length <= 1) return; 1004 setPatterns((prev) => prev.filter((p) => p.id !== id)); 1005 setPlaylistClips((prev) => prev.filter((c) => c.patternId !== id)); 1006 if (activePatternId === id) { 1007 const remaining = patterns.filter((p) => p.id !== id); 1008 setActivePatternId(((_a2 = remaining[0]) == null ? void 0 : _a2.id) || null); 1009 } 1010 setTimeout(() => forceSync(), 0); 1011 }; 1012 const handleAddPlaylistTrack = () => { 1013 const nextId = `track-${playlistTracks.length}-${Date.now()}`; 1014 const newTrack = { 1015 id: nextId, 1016 name: `Track ${playlistTracks.length + 1}`, 1017 color: `#${Math.floor((0.3 + 0.6 * Math.random()) * 16777215).toString(16)}`, 1018 icon: "synth", 1019 isMuted: false, 1020 isSoloed: false, 1021 volume: 0.8, 1022 panning: 0, 1023 height: 64, 1024 parentId: null 1025 }; 1026 if (easyMode) { 1027 const patId = `pat-easy-${playlistTracks.length}-${Date.now()}`; 1028 const newPat = { 1029 id: patId, 1030 name: newTrack.name, 1031 color: newTrack.color, 1032 notes: [] 1033 }; 1034 const newClip = { 1035 id: `clip-easy-${playlistTracks.length}-${Date.now()}`, 1036 patternId: patId, 1037 trackIndex: playlistTracks.length, 1038 startTick: 0, 1039 durationTicks: 96 * 4 * 128, 1040 // 128 bars 1041 offsetTicks: 0, 1042 isMuted: false 1043 }; 1044 setPatterns((prev) => [...prev, newPat]); 1045 setPlaylistClips((prev) => [...prev, newClip]); 1046 setActivePatternId(patId); 1047 setActiveTrackIndex(playlistTracks.length); 1048 } 1049 setPlaylistTracks((prev) => [...prev, newTrack]); 1050 engine.addTrack(); 1051 setTimeout(() => forceSync(), 0); 1052 }; 1053 const handleDeleteTrack = (idx) => { 1054 if (playlistTracks.length <= 1) return; 1055 playlistTracks[idx]; 1056 setPlaylistTracks((prev) => prev.filter((_, i) => i !== idx)); 1057 setPlaylistClips((prev) => prev.filter((c) => c.trackIndex !== idx).map((c) => { 1058 if (c.trackIndex > idx) return { ...c, trackIndex: c.trackIndex - 1 }; 1059 return c; 1060 })); 1061 setAutomationClips((prev) => prev.filter((c) => c.trackIndex !== idx).map((c) => { 1062 if (c.trackIndex > idx) return { ...c, trackIndex: c.trackIndex - 1 }; 1063 return c; 1064 })); 1065 engine.deleteTrack(idx); 1066 setTimeout(() => forceSync(), 0); 1067 }; 1068 const toggleTrackMute = (idx) => { 1069 setPlaylistTracks((prev) => { 1070 const next = [...prev]; 1071 next[idx].isMuted = !next[idx].isMuted; 1072 return next; 1073 }); 1074 setTimeout(() => forceSync(), 0); 1075 }; 1076 const toggleTrackSolo = (idx) => { 1077 setPlaylistTracks((prev) => { 1078 const next = [...prev]; 1079 next.some((t, i) => i !== idx && t.isSoloed); 1080 const targetSolo = !next[idx].isSoloed; 1081 next[idx].isSoloed = targetSolo; 1082 return next; 1083 }); 1084 setTimeout(() => forceSync(), 0); 1085 }; 1086 const toggleTrackGroup = (idx) => { 1087 if (idx === 0) return; 1088 setPlaylistTracks((prev) => { 1089 const next = [...prev]; 1090 const current = next[idx]; 1091 const parent = next[idx - 1]; 1092 if (current.parentId) { 1093 current.parentId = null; 1094 } else { 1095 current.parentId = parent.id; 1096 } 1097 return next; 1098 }); 1099 }; 1100 const getTrackTop = (trackIdx) => { 1101 let top = 0; 1102 for (let i = 0; i < trackIdx; i++) { 1103 const pt = playlistTracks[i]; 1104 pt.parentId === null && collapsedGroups[pt.id]; 1105 const parentOfI = pt.parentId; 1106 const parentTrack = playlistTracks.find((t) => t.id === parentOfI); 1107 const isChildHidden = parentOfI && parentTrack && collapsedGroups[parentOfI];
1108 if (!isChildHidden) { 1109 top += (pt.height || 64) * zoomY; 1110 } 1111 } 1112 return top; 1113 }; 1114 const getTrackHeight = (trackIdx) => { 1115 const pt = playlistTracks[trackIdx]; 1116 if (!pt) return 0; 1117 const parentOfI = pt.parentId; 1118 const parentTrack = playlistTracks.find((t) => t.id === parentOfI); 1119 if (parentOfI && parentTrack && collapsedGroups[parentOfI]) { 1120 return 0; 1121 } 1122 return (pt.height || 64) * zoomY; 1123 }; 1124 const ticksToPixels = (ticks) => ticks * pxPerTick; 1125 const pixelsToTicks = (px) => px / pxPerTick; 1126 const getPatternLength = (pat) => { 1127 const maxNoteTick = pat.notes.reduce((max, n) => Math.max(max, n.ticks + n.durationTicks), 0); 1128 const ticksPerBar = ppq * 4; 1129 return Math.max(ticksPerBar, maxNoteTick); 1130 }; 1131 const handleGridPointerDown = (e) => { 1132 var _a2, _b2; 1133 if (!gridRef.current) return; 1134 const rect = gridRef.current.getBoundingClientRect(); 1135 const x = e.clientX - rect.left; 1136 const y = e.clientY - rect.top; 1137 const rawTick2 = pixelsToTicks(x); 1138 const snappedTick = getSnappedTick(rawTick2); 1139 if (e.button === 1) { 1140 e.preventDefault(); 1141 panStartRef.current = { 1142 x: e.clientX, 1143 y: e.clientY, 1144 scrollLeft: ((_a2 = gridHorizontalScrollRef.current) == null ? void 0 : _a2.scrollLeft) || 0, 1145 scrollTop: ((_b2 = verticalScrollContainerRef.current) == null ? void 0 : _b2.scrollTop) || 0 1146 }; 1147 setIsPanning(true); 1148 document.body.style.cursor = "grabbing"; 1149 e.currentTarget.setPointerCapture(e.pointerId); 1150 return; 1151 } 1152 let trackIdx = -1; 1153 let cumulativeHeight = 0; 1154 for (let i = 0; i < playlistTracks.length; i++) { 1155 const h = getTrackHeight(i); 1156 if (h === 0) continue; 1157 if (y >= cumulativeHeight && y < cumulativeHeight + h) { 1158 trackIdx = i; 1159 break; 1160 } 1161 cumulativeHeight += h; 1162 } 1163 if (trackIdx === -1) return; 1164 if (easyMode) { 1165 setActiveTrackIndex(trackIdx); 1166 const trackClip = playlistClips.find((c) => c.trackIndex === trackIdx); 1167 if (trackClip) { 1168 setActivePatternId(trackClip.patternId); 1169 } 1170 return; 1171 } 1172 if (e.button === 2) { 1173 e.preventDefault(); 1174 const existingClip = playlistClips.find( 1175 (c) => c.trackIndex === trackIdx && snappedTick >= c.startTick && snappedTick < c.startTick + c.durationTicks 1176 ); 1177 if (existingClip) { 1178 setPlaylistClips((prev) => prev.filter((c) => c.id !== existingClip.id)); 1179 setTimeout(() => forceSync(), 0); 1180 } 1181 setActiveDragAction("right-delete"); 1182 return; 1183 } 1184 const isSelectAction = currentPlaylistTool === "select" || !easyMode && (e.ctrlKey || e.metaKey); 1185 if (isSelectAction) { 1186 setLassoStart({ x, y }); 1187 setLassoCurrent({ x, y }); 1188 setActiveDragAction("select"); 1189 return; 1190 } 1191 if (currentPlaylistTool === "draw" || currentPlaylistTool === "paint") { 1192 if (!activePatternId) return; 1193 const pat = patterns.find((p) => p.id === activePatternId); 1194 if (!pat) return; 1195 const existingClip = playlistClips.find( 1196 (c) => c.trackIndex === trackIdx && c.patternId === activePatternId && snappedTick >= c.startTick && snappedTick < c.startTick + c.durationTicks 1197 ) ?? null; 1198 if (e.button === 0) { 1199 if (currentPlaylistTool === "paint") {
1200 setActiveDragAction("paint"); 1201 const patLen = getPatternLength(pat); 1202 if (!existingClip) { 1203 const newClip = { 1204 id: `clip-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, 1205 patternId: activePatternId, 1206 trackIndex: trackIdx, 1207 startTick: snappedTick, 1208 durationTicks: patLen, 1209 offsetTicks: 0, 1210 isMuted: false 1211 }; 1212 setPlaylistClips((prev) => [...prev, newClip]); 1213 lastPaintedTickRef.current = snappedTick; 1214 lastPaintedTrackIdxRef.current = trackIdx; 1215 setTimeout(() => forceSync(), 0); 1216 } else { 1217 lastPaintedTickRef.current = existingClip.startTick; 1218 lastPaintedTrackIdxRef.current = existingClip.trackIndex; 1219 } 1220 } else { 1221 if (!existingClip) { 1222 engine.stopAllNotePreviews(); 1223 const patLen = getPatternLength(pat); 1224 const newClip = { 1225 id: `clip-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, 1226 patternId: activePatternId, 1227 trackIndex: trackIdx, 1228 startTick: snappedTick, 1229 durationTicks: patLen, 1230 offsetTicks: 0, 1231 isMuted: false 1232 }; 1233 setPlaylistClips((prev) => [...prev, newClip]); 1234 setTimeout(() => forceSync(), 0);
1235 setActiveDragClipId(newClip.id); 1236 setActiveDragAction("move"); 1237 setDragStartTick(snappedTick); 1238 setDragStartTrackIdx(trackIdx); 1239 setDragStartOffsetTicks(0); 1240 setDragStartClipState(newClip); 1241 setDragStartMouseX(e.clientX); 1242 } else { 1243 if (!easyMode && e.shiftKey) { 1244 const cloneId = `clip-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; 1245 const clonedClip = { 1246 ...existingClip, 1247 id: cloneId 1248 }; 1249 setPlaylistClips((prev) => [...prev, clonedClip]); 1250 setActiveDragClipId(cloneId); 1251 setActiveDragAction("move"); 1252 setDragStartTick(snappedTick); 1253 setDragStartTrackIdx(trackIdx); 1254 setDragStartOffsetTicks(snappedTick - existingClip.startTick); 1255 setDragStartClipState(clonedClip); 1256 setDragStartMouseX(e.clientX); 1257 setSelectedClipIds([cloneId]); 1258 } else {
1259 setActiveDragClipId(existingClip.id); 1260 setActiveDragAction("move"); 1261 setDragStartTick(snappedTick); 1262 setDragStartTrackIdx(trackIdx); 1263 setDragStartOffsetTicks(snappedTick - existingClip.startTick); 1264 setDragStartClipState(existingClip); 1265 setDragStartMouseX(e.clientX); 1266 } 1267 } 1268 } 1269 } 1270 } else if (currentPlaylistTool === "delete") { 1271 const existingClip = playlistClips.find( 1272 (c) => c.trackIndex === trackIdx && snappedTick >= c.startTick && snappedTick < c.startTick + c.durationTicks 1273 ); 1274 if (existingClip) { 1275 setPlaylistClips((prev) => prev.filter((c) => c.id !== existingClip.id)); 1276 setTimeout(() => forceSync(), 0); 1277 } 1278 } else if (currentPlaylistTool === "mute") { 1279 const existingClip = playlistClips.find( 1280 (c) => c.trackIndex === trackIdx && snappedTick >= c.startTick && snappedTick < c.startTick + c.durationTicks 1281 ); 1282 if (existingClip) { 1283 setPlaylistClips((prev) => prev.map( 1284 (c) => c.id === existingClip.id ? { ...c, isMuted: !c.isMuted } : c 1285 )); 1286 setTimeout(() => forceSync(), 0); 1287 } 1288 } else if (currentPlaylistTool === "slice") { 1289 if (easyMode) return; 1290 setSliceStart({ x: e.clientX, y: e.clientY }); 1291 setSliceCurrent({ x: e.clientX, y: e.clientY }); 1292 setActiveDragAction("slice"); 1293 setDragStartTrackIdx(trackIdx); 1294 } 1295 }; 1296 const handleGridPointerMove = (e) => { 1297 if (!gridRef.current || !activeDragAction) return; 1298 if (panStartRef.current) { 1299 const dx = e.clientX - panStartRef.current.x; 1300 const dy = e.clientY - panStartRef.current.y; 1301 if (gridHorizontalScrollRef.current) { 1302 gridHorizontalScrollRef.current.scrollLeft = panStartRef.current.scrollLeft - dx; 1303 } 1304 if (verticalScrollContainerRef.current) { 1305 verticalScrollContainerRef.current.scrollTop = panStartRef.current.scrollTop - dy; 1306 } 1307 return; 1308 } 1309 const rect = gridRef.current.getBoundingClientRect(); 1310 const x = e.clientX - rect.left; 1311 const y = e.clientY - rect.top; 1312 const rawTick2 = pixelsToTicks(x); 1313 const snappedTick = getSnappedTick(rawTick2); 1314 let trackIdx = -1; 1315 let cumulativeHeight = 0; 1316 for (let i = 0; i < playlistTracks.length; i++) { 1317 const h = getTrackHeight(i); 1318 if (h === 0) continue; 1319 if (y >= cumulativeHeight && y < cumulativeHeight + h) { 1320 trackIdx = i; 1321 break; 1322 } 1323 cumulativeHeight += h; 1324 } 1325 if (activeDragAction === "right-delete" && trackIdx !== -1) { 1326 const existingClip = playlistClips.find( 1327 (c) => c.trackIndex === trackIdx && snappedTick >= c.startTick && snappedTick < c.startTick + c.durationTicks 1328 ); 1329 if (existingClip) { 1330 setPlaylistClips((prev) => prev.filter((c) => c.id !== existingClip.id)); 1331 } 1332 return; 1333 } 1334 if (activeDragAction === "paint" && activePatternId && trackIdx !== -1) { 1335 const pat = patterns.find((p) => p.id === activePatternId); 1336 if (pat) { 1337 const patLen = getPatternLength(pat); 1338 if (snappedTick !== lastPaintedTickRef.current || trackIdx !== lastPaintedTrackIdxRef.current) { 1339 const existingClip = playlistClips.find( 1340 (c) => c.trackIndex === trackIdx && snappedTick >= c.startTick && snappedTick < c.startTick + c.durationTicks 1341 ); 1342 if (!existingClip) { 1343 const newClip = { 1344 id: `clip-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, 1345 patternId: activePatternId, 1346 trackIndex: trackIdx, 1347 startTick: snappedTick, 1348 durationTicks: patLen, 1349 offsetTicks: 0, 1350 isMuted: false 1351 }; 1352 setPlaylistClips((prev) => [...prev, newClip]); 1353 lastPaintedTickRef.current = snappedTick; 1354 lastPaintedTrackIdxRef.current = trackIdx; 1355 } 1356 } 1357 } 1358 return; 1359 } 1360 if (activeDragAction === "move" && activeDragClipId && dragStartClipState) {
1361 const deltaTicks = snappedTick - dragStartTick; 1362 const newStartTick = Math.max(0, dragStartClipState.startTick + deltaTicks); 1363 const targetTrackIndex = trackIdx !== -1 ? trackIdx : dragStartClipState.trackIndex; 1364 setPlaylistClips((prev) => prev.map((c) => { 1365 if (c.id === activeDragClipId) { 1366 return { 1367 ...c, 1368 startTick: newStartTick, 1369 trackIndex: targetTrackIndex 1370 }; 1371 } 1372 return c; 1373 })); 1374 } else if (activeDragAction === "trim-right" && activeDragClipId && dragStartClipState) { 1375 const newEndTick = Math.max(dragStartClipState.startTick + ppq / 4, snappedTick); 1376 const durationTicks = newEndTick - dragStartClipState.startTick; 1377 setPlaylistClips((prev) => prev.map((c) => { 1378 if (c.id === activeDragClipId) { 1379 return { 1380 ...c, 1381 durationTicks, 1382 isManualLength: true 1383 }; 1384 } 1385 return c; 1386 })); 1387 } else if (activeDragAction === "trim-left" && activeDragClipId && dragStartClipState) { 1388 const origEnd = dragStartClipState.startTick + (dragStartClipState.isManualLength ? dragStartClipState.durationTicks : getPatternLength(patterns.find((p) => p.id === dragStartClipState.patternId))); 1389 const newStart = Math.min(origEnd - ppq / 4, snappedTick); 1390 const durationTicks = origEnd - newStart; 1391 const deltaTicks = newStart - dragStartClipState.startTick; 1392 const offsetTicks = Math.max(0, dragStartClipState.offsetTicks + deltaTicks); 1393 setPlaylistClips((prev) => prev.map((c) => { 1394 if (c.id === activeDragClipId) { 1395 return { 1396 ...c, 1397 startTick: newStart, 1398 durationTicks, 1399 offsetTicks, 1400 isManualLength: true 1401 }; 1402 } 1403 return c; 1404 })); 1405 } else if (activeDragAction === "slip" && activeDragClipId && dragStartClipState) { 1406 if (easyMode) return; 1407 const deltaPx = e.clientX - dragStartMouseX; 1408 const deltaTicks = Math.round(pixelsToTicks(deltaPx)); 1409 const offsetTicks = Math.max(0, dragStartClipState.offsetTicks - deltaTicks); 1410 setPlaylistClips((prev) => prev.map((c) => { 1411 if (c.id === activeDragClipId) { 1412 return { 1413 ...c, 1414 offsetTicks 1415 }; 1416 } 1417 return c; 1418 })); 1419 } else if (activeDragAction === "slice") { 1420 setSliceCurrent({ x: e.clientX, y: e.clientY }); 1421 } else if (activeDragAction === "select" && lassoStart) { 1422 setLassoCurrent({ x, y }); 1423 } else if (activeDragAction === "node-drag" && dragNodeClipId && dragNodeId && dragNodeStartProps) { 1424 const clip = automationClips.find((c) => c.id === dragNodeClipId); 1425 if (!clip) return; 1426 const deltaTicks = Math.round(rawTick2 - dragStartTick); 1427 y - (getTrackTop(clip.trackIndex) + getTrackHeight(clip.trackIndex) / 2); 1428 const h = getTrackHeight(clip.trackIndex); 1429 const rawVal = 1 - (y - getTrackTop(clip.trackIndex)) / h; 1430 const value = Math.max(0, Math.min(1, rawVal)); 1431 const newTick = Math.max(clip.startTick, Math.min(clip.startTick + clip.durationTicks, getSnappedTick(dragNodeStartProps.tick + deltaTicks))); 1432 setAutomationClips((prev) => prev.map((c) => { 1433 if (c.id === dragNodeClipId) { 1434 return { 1435 ...c, 1436 nodes: c.nodes.map((n) => n.id === dragNodeId ? { ...n, tick: newTick, value } : n) 1437 }; 1438 } 1439 return c; 1440 })); 1441 } else if (activeDragAction === "tension-drag" && dragTensionClipId && dragTensionNodeIdx !== null) { 1442 const clip = automationClips.find((c) => c.id === dragTensionClipId); 1443 if (!clip) return; 1444 const node = clip.nodes[dragTensionNodeIdx]; 1445 if (!node) return; 1446 const trackTop = getTrackTop(clip.trackIndex); 1447 const h = getTrackHeight(clip.trackIndex); 1448 const yOffset = y - trackTop; 1449 const relativeY = 1 - yOffset / h; 1450 const tension = Math.max(-0.99, Math.min(0.99, (relativeY - 0.5) * 2)); 1451 setAutomationClips((prev) => prev.map((c) => {
1452 if (c.id === dragTensionClipId) { 1453 const nodes = [...c.nodes]; 1454 nodes[dragTensionNodeIdx] = { 1455 ...nodes[dragTensionNodeIdx], 1456 tension 1457 }; 1458 return { ...c, nodes }; 1459 } 1460 return c; 1461 })); 1462 } 1463 }; 1464 const handleGridPointerUp = (e) => { 1465 if (panStartRef.current) { 1466 e.currentTarget.releasePointerCapture(e.pointerId); 1467 panStartRef.current = null; 1468 setIsPanning(false); 1469 document.body.style.cursor = ""; 1470 } 1471 if (activeDragAction === "slice" && sliceStart && sliceCurrent) { 1472 const rawCutX = sliceCurrent.x - gridRef.current.getBoundingClientRect().left; 1473 const cutTick = getSnappedTick(pixelsToTicks(rawCutX)); 1474 const trackIdx = dragStartTrackIdx; 1475 setPlaylistClips((prevClips) => { 1476 const newClips = []; 1477 let modified = false; 1478 prevClips.forEach((c) => { 1479 if (c.trackIndex === trackIdx && cutTick > c.startTick && cutTick < c.startTick + c.durationTicks) { 1480 modified = true; 1481 const leftDuration = cutTick - c.startTick; 1482 const rightDuration = c.durationTicks - leftDuration; 1483 newClips.push({ 1484 ...c, 1485 durationTicks: leftDuration 1486 }); 1487 newClips.push({ 1488 ...c, 1489 id: `clip-slice-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, 1490 startTick: cutTick, 1491 durationTicks: rightDuration, 1492 offsetTicks: c.offsetTicks + leftDuration 1493 }); 1494 } else { 1495 newClips.push(c); 1496 } 1497 }); 1498 if (modified) { 1499 setTimeout(() => forceSync(), 0); 1500 } 1501 return newClips; 1502 }); 1503 } else if (activeDragAction === "select" && lassoStart && lassoCurrent) { 1504 const x1 = Math.min(lassoStart.x, lassoCurrent.x); 1505 const x2 = Math.max(lassoStart.x, lassoCurrent.x); 1506 const y1 = Math.min(lassoStart.y, lassoCurrent.y); 1507 const y2 = Math.max(lassoStart.y, lassoCurrent.y); 1508 const selectedIds = []; 1509 playlistClips.forEach((clip) => { 1510 const clipLeft = ticksToPixels(clip.startTick); 1511 const clipRight = clipLeft + ticksToPixels(clip.durationTicks); 1512 const clipTop = getTrackTop(clip.trackIndex); 1513 const clipBottom = clipTop + getTrackHeight(clip.trackIndex); 1514 if (clipRight >= x1 && clipLeft <= x2 && clipBottom >= y1 && clipTop <= y2) { 1515 selectedIds.push(clip.id); 1516 } 1517 }); 1518 setSelectedClipIds(selectedIds); 1519 } 1520 setActiveDragAction(null); 1521 setActiveDragClipId(null); 1522 setDragStartClipState(null); 1523 setLassoStart(null); 1524 setLassoCurrent(null); 1525 setSliceStart(null); 1526 setSliceCurrent(null); 1527 setDragNodeId(null); 1528 setDragNodeClipId(null); 1529 setDragNodeStartProps(null); 1530 setDragTensionClipId(null); 1531 setDragTensionNodeIdx(null); 1532 lastPaintedTickRef.current = null; 1533 lastPaintedTrackIdxRef.current = null; 1534 setTimeout(() => forceSync(), 0); 1535 }; 1536 const handleRulerPointerDown = (e) => { 1537 e.preventDefault(); 1538 const rect = e.currentTarget.getBoundingClientRect(); 1539 const x = e.clientX - rect.left; 1540 const tick = getSnappedTick(pixelsToTicks(x)); 1541 rulerStartTickRef.current = tick; 1542 hasRulerDraggedRef.current = false; 1543 if (e.button === 0) { 1544 rulerDragActionRef.current = "seek"; 1545 const clickedTime = engine.ticksToSeconds(tick); 1546 engine.seek(clickedTime); 1547 setTimeout(() => forceSync(), 0); 1548 } else if (e.button === 2) { 1549 rulerDragActionRef.current = "loop"; 1550 setLoopRegion({ startTick: tick, endTick: tick }); 1551 } 1552 e.currentTarget.setPointerCapture(e.pointerId); 1553 }; 1554 const handleRulerPointerMove = (e) => { 1555 if (!rulerDragActionRef.current) return; 1556 const rect = e.currentTarget.getBoundingClientRect(); 1557 const x = e.clientX - rect.left; 1558 const tick = getSnappedTick(pixelsToTicks(x)); 1559 hasRulerDraggedRef.current = true; 1560 if (rulerDragActionRef.current === "seek") { 1561 const clickedTime = engine.ticksToSeconds(tick); 1562 engine.seek(clickedTime); 1563 } else if (rulerDragActionRef.current === "loop") { 1564 const start = rulerStartTickRef.current; 1565 const end = tick; 1566 setLoopRegion({ 1567 startTick: Math.min(start, end), 1568 endTick: Math.max(start, end) 1569 }); 1570 } 1571 setTimeout(() => forceSync(), 0); 1572 }; 1573 const handleRulerPointerUp = (e) => { 1574 if (!rulerDragActionRef.current) return; 1575 e.currentTarget.releasePointerCapture(e.pointerId); 1576 if (rulerDragActionRef.current === "loop" && !hasRulerDraggedRef.current) { 1577 setLoopRegion(null); 1578 if (!easyMode) { 1579 const rect = e.currentTarget.getBoundingClientRect(); 1580 const x = e.clientX - rect.left; 1581 const tick = getSnappedTick(pixelsToTicks(x)); 1582 const label = window.prompt("Enter Marker Name:", "Chorus"); 1583 if (label) { 1584 setTimeMarkers((prev) => [...prev, { id: `marker-${Date.now()}`, tick, label }]); 1585 } 1586 } 1587 } 1588 rulerDragActionRef.current = null; 1589 setTimeout(() => forceSync(), 0); 1590 }; 1591 const handleSliceClipAtTick = (clipId, cutTick) => { 1592 setPlaylistClips((prevClips) => { 1593 const newClips = []; 1594 let modified = false;
1595 prevClips.forEach((c) => { 1596 if (c.id === clipId && cutTick > c.startTick && cutTick < c.startTick + c.durationTicks) { 1597 modified = true; 1598 const leftDuration = cutTick - c.startTick; 1599 const rightDuration = c.durationTicks - leftDuration; 1600 newClips.push({ 1601 ...c, 1602 durationTicks: leftDuration 1603 }); 1604 newClips.push({ 1605 ...c, 1606 id: `clip-slice-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, 1607 startTick: cutTick, 1608 durationTicks: rightDuration, 1609 offsetTicks: c.offsetTicks + leftDuration 1610 }); 1611 } else { 1612 newClips.push(c); 1613 } 1614 }); 1615 if (modified) { 1616 setTimeout(() => forceSync(), 0); 1617 } 1618 return newClips; 1619 }); 1620 }; 1621 const handleCreateAutomationClip = (trackIdx, ccNum) => { 1622 if (easyMode) return; 1623 const newClip = { 1624 id: `auto-${Date.now()}`, 1625 name: "Volume Automation", 1626 trackIndex: trackIdx, 1627 startTick: 0, 1628 durationTicks: ppq * 4 * 16, 1629 // 16 bars default 1630 ccNumber: ccNum, 1631 nodes: [ 1632 { id: `n-0`, tick: 0, value: 0.8, tension: 0 }, 1633 { id: `n-1`, tick: ppq * 4 * 16, value: 0.8, tension: 0 } 1634 ] 1635 }; 1636 setAutomationClips((prev) => [...prev, newClip]); 1637 setTimeout(() => forceSync(), 0); 1638 }; 1639 const handleSelectColor = (id, color, type) => { 1640 if (type === "pattern") { 1641 setPatterns((prev) => prev.map((p) => p.id === id ? { ...p, color } : p)); 1642 } else { 1643 setPlaylistTracks((prev) => prev.map((t) => t.id === id ? { ...t, color } : t)); 1644 } 1645 setActiveColorPickerId(null); 1646 setTimeout(() => forceSync(), 0); 1647 }; 1648 const handleTrackHeaderResize = (trackIdx, e) => { 1649 e.stopPropagation(); 1650 e.preventDefault(); 1651 const startY = e.clientY; 1652 const origHeight = playlistTracks[trackIdx].height || 64; 1653 const handleMouseMove = (moveEvent) => { 1654 const deltaY = moveEvent.clientY - startY; 1655 const newHeight = Math.max(36, Math.min(200, origHeight + deltaY)); 1656 setPlaylistTracks((prev) => prev.map( 1657 (t, idx) => idx === trackIdx ? { ...t, height: newHeight } : t 1658 )); 1659 }; 1660 const handleMouseUp = () => { 1661 window.removeEventListener("mousemove", handleMouseMove); 1662 window.removeEventListener("mouseup", handleMouseUp); 1663 }; 1664 window.addEventListener("mousemove", handleMouseMove); 1665 window.addEventListener("mouseup", handleMouseUp); 1666 }; 1667 const handlePatternResizeMouseDown = (e) => { 1668 e.stopPropagation(); 1669 e.preventDefault(); 1670 const startX = e.clientX; 1671 const startWidth = patternColWidth; 1672 const handleMouseMove = (moveEvent) => { 1673 const deltaX = moveEvent.clientX - startX; 1674 const newWidth = Math.max(100, Math.min(500, startWidth + deltaX)); 1675 setPatternColWidth(newWidth); 1676 }; 1677 const handleMouseUp = () => { 1678 window.removeEventListener("mousemove", handleMouseMove); 1679 window.removeEventListener("mouseup", handleMouseUp); 1680 }; 1681 window.addEventListener("mousemove", handleMouseMove); 1682 window.addEventListener("mouseup", handleMouseUp); 1683 }; 1684 const handleTrackColResizeMouseDown = (e) => { 1685 e.stopPropagation(); 1686 e.preventDefault(); 1687 const startX = e.clientX; 1688 const startWidth = trackColWidth; 1689 const handleMouseMove = (moveEvent) => { 1690 const deltaX = moveEvent.clientX - startX; 1691 const newWidth = Math.max(100, Math.min(500, startWidth + deltaX)); 1692 setTrackColWidth(newWidth); 1693 }; 1694 const handleMouseUp = () => { 1695 window.removeEventListener("mousemove", handleMouseMove); 1696 window.removeEventListener("mouseup", handleMouseUp); 1697 }; 1698 window.addEventListener("mousemove", handleMouseMove); 1699 window.addEventListener("mouseup", handleMouseUp); 1700 }; 1701 const handleClipDoubleClick = (patternId) => { 1702 setActivePatternId(patternId); 1703 }; 1704 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 flex overflow-hidden bg-zinc-50 dark:bg-zinc-950 text-zinc-700 dark:text-zinc-300 select-none font-sans", children: [
1705 !easyMode && /* @__PURE__ */ jsxRuntimeExports.jsxs( 1706 "div", 1707 { 1708 style: { width: patternColWidth }, 1709 className: "border-r border-zinc-200 dark:border-zinc-800 bg-zinc-100 dark:bg-zinc-900/60 flex flex-col shrink-0 relative", 1710 children: [ 1711 /* @__PURE__ */ jsxRuntimeExports.jsx( 1712 "div", 1713 { 1714 onMouseDown: handlePatternResizeMouseDown, 1715 className: "absolute top-0 right-0 w-1 h-full cursor-col-resize hover:bg-indigo-500/50 active:bg-indigo-500 transition-colors z-30" 1716 } 1717 ), 1718 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "h-10 border-b border-zinc-200 dark:border-zinc-800 flex items-center justify-between px-3 bg-zinc-100 dark:bg-zinc-950 shrink-0", children: [ 1719 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[10px] font-bold text-zinc-500 uppercase tracking-widest flex items-center gap-1.5", children: [ 1720 /* @__PURE__ */ jsxRuntimeExports.jsx(Layers, { size: 12, className: "text-indigo-400" }), 1721 " Patterns" 1722 ] }),
1723 /* @__PURE__ */ jsxRuntimeExports.jsx( 1724 "button", 1725 { 1726 onClick: handleAddPattern, 1727 className: "p-1 hover:bg-zinc-800 text-indigo-400 hover:text-indigo-300 rounded transition-colors", 1728 title: "Add Pattern", 1729 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Plus, { size: 14 }) 1730 } 1731 ) 1732 ] }), 1733 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 overflow-y-auto p-2 flex flex-col gap-1 custom-scrollbar", children: patterns.map((pat) => { 1734 const isActive = pat.id === activePatternId; 1735 return /* @__PURE__ */ jsxRuntimeExports.jsx( 1736 "div", 1737 { 1738 onClick: () => setActivePatternId(pat.id), 1739 onDoubleClick: () => { 1740 setRenamingPatternId(pat.id); 1741 setRenamingPatternText(pat.name); 1742 }, 1743 className: `group flex items-center gap-2 p-2 rounded cursor-pointer transition-all border ${isActive ? "bg-indigo-500/15 border-indigo-500/50 text-indigo-600 dark:text-white shadow-sm" : "bg-white dark:bg-zinc-800/20 border-zinc-200 dark:border-transparent hover:bg-zinc-50 dark:hover:bg-zinc-800/40 text-zinc-600 dark:text-zinc-400"}`, 1744 style: { borderLeftColor: pat.color, borderLeftWidth: "4px" }, 1745 children: renamingPatternId === pat.id ? /* @__PURE__ */ jsxRuntimeExports.jsx( 1746 "input", 1747 { 1748 type: "text", 1749 value: renamingPatternText, 1750 onChange: (e) => setRenamingPatternText(e.target.value), 1751 onBlur: () => { 1752 setPatterns((prev) => prev.map((p) => p.id === pat.id ? { ...p, name: renamingPatternText || p.name } : p)); 1753 setRenamingPatternId(null); 1754 }, 1755 onKeyDown: (e) => { 1756 if (e.key === "Enter") { 1757 setPatterns((prev) => prev.map((p) => p.id === pat.id ? { ...p, name: renamingPatternText || p.name } : p)); 1758 setRenamingPatternId(null); 1759 } 1760 }, 1761 autoFocus: true, 1762 className: "bg-white dark:bg-zinc-850 border border-zinc-200 dark:border-zinc-700 rounded text-xs px-1 text-zinc-900 dark:text-white w-full outline-none" 1763 } 1764 ) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
1765 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs font-semibold truncate flex-1", children: pat.name }), 1766 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity", children: [ 1767 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative", children: [ 1768 /* @__PURE__ */ jsxRuntimeExports.jsx( 1769 "button", 1770 { 1771 onClick: (e) => { 1772 e.stopPropagation(); 1773 setActiveColorPickerId(activeColorPickerId === pat.id ? null : pat.id); 1774 }, 1775 className: "w-3.5 h-3.5 rounded border border-white/20", 1776 style: { backgroundColor: pat.color } 1777 } 1778 ), 1779 activeColorPickerId === pat.id && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute left-0 mt-1 bg-white dark:bg-zinc-950 border border-zinc-200 dark:border-zinc-800 p-1.5 rounded shadow-xl flex gap-1 z-50", children: ["#6366f1", "#e11d48", "#10b981", "#f59e0b", "#06b6d4", "#ec4899"].map((c) => /* @__PURE__ */ jsxRuntimeExports.jsx( 1780 "button", 1781 { 1782 onClick: () => handleSelectColor(pat.id, c, "pattern"), 1783 className: "w-4 h-4 rounded border border-white/10", 1784 style: { backgroundColor: c } 1785 }, 1786 c 1787 )) }) 1788 ] }),
1789 /* @__PURE__ */ jsxRuntimeExports.jsx( 1790 "button", 1791 { 1792 onClick: (e) => handleDeletePattern(pat.id, e), 1793 className: "p-0.5 hover:text-red-500 rounded", 1794 title: "Delete Pattern", 1795 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Trash2, { size: 12 }) 1796 } 1797 ) 1798 ] }) 1799 ] }) 1800 }, 1801 pat.id 1802 ); 1803 }) }) 1804 ] 1805 } 1806 ), 1807 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 flex flex-col overflow-hidden", children: [ 1808 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "h-10 border-b border-zinc-200 dark:border-zinc-800 bg-zinc-100 dark:bg-zinc-900 flex justify-between items-center px-4 shrink-0", children: [ 1809 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 1810 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex bg-white dark:bg-zinc-950 p-0.5 rounded-lg border border-zinc-200 dark:border-zinc-800", children: [ 1811 { id: "draw", icon: Pencil, title: "Draw Tool (Left-click grid cell)" }, 1812 { id: "paint", icon: Brush, title: "Paint Tool (Draw contiguous clips)" }, 1813 { id: "delete", icon: Eraser, title: "Delete Tool (Eraser)" }, 1814 { id: "mute", icon: VolumeX, title: "Mute Tool (Click clips to toggle mute)" }, 1815 { id: "slice", icon: Scissors, title: "Slice Knife (Drag line to cut clips)", lockPro: true }, 1816 { id: "slip", icon: Split, title: "Slip Tool (Shift notes inside clip borders)", lockPro: true }, 1817 { id: "select", icon: MousePointer2, title: "Select Box (Marquee)" } 1818 ].map((tool) => { 1819 const isLocked = easyMode && tool.lockPro; 1820 if (isLocked) return null; 1821 const isActive = currentPlaylistTool === tool.id; 1822 return /* @__PURE__ */ jsxRuntimeExports.jsx( 1823 "button", 1824 { 1825 onClick: () => setCurrentPlaylistTool(tool.id), 1826 className: `p-1.5 rounded transition-all ${isActive ? "bg-indigo-600 text-white shadow" : "text-zinc-500 hover:text-zinc-300"}`, 1827 title: tool.title, 1828 children: /* @__PURE__ */ jsxRuntimeExports.jsx(tool.icon, { size: 14 }) 1829 }, 1830 tool.id 1831 ); 1832 }) }),
1833 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-px h-5 bg-zinc-800 mx-1" }), 1834 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5 bg-white dark:bg-zinc-950 border border-zinc-200 dark:border-zinc-800 px-2 py-1 rounded-lg text-xs font-semibold", children: [ 1835 /* @__PURE__ */ jsxRuntimeExports.jsx(Magnet, { size: 12, className: "text-zinc-500" }), 1836 /* @__PURE__ */ jsxRuntimeExports.jsxs( 1837 "select", 1838 { 1839 value: gridSnap, 1840 onChange: (e) => setGridSnap(e.target.value), 1841 className: "bg-transparent text-zinc-700 dark:text-zinc-300 outline-none cursor-pointer pr-1", 1842 children: [ 1843 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "bar", children: "Snap to Bar" }), 1844 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "beat", children: "Snap to Beat" }), 1845 !easyMode && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
1846 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "1/2", children: "Snap to 1/2 Beat" }), 1847 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "1/4", children: "Snap to 1/4 Beat" }), 1848 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "1/6", children: "Snap to 1/6 Beat" }), 1849 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "cell", children: "Snap to Cell" }), 1850 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "none", children: "None (Seamless)" }) 1851 ] }) 1852 ] 1853 } 1854 ) 1855 ] }), 1856 !easyMode && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 1857 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-px h-5 bg-zinc-800 mx-1" }), 1858 /* @__PURE__ */ jsxRuntimeExports.jsxs( 1859 "button", 1860 { 1861 onClick: () => setIsAutomationVisible(!isAutomationVisible), 1862 className: `flex items-center gap-1.5 px-2.5 py-1 rounded-lg border text-xs font-semibold transition-all ${isAutomationVisible ? "bg-white dark:bg-zinc-950 border-indigo-500/30 text-indigo-500 dark:text-indigo-400 shadow-sm" : "border-zinc-200 dark:border-zinc-800 text-zinc-500 dark:text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-300"}`, 1863 children: [
1864 /* @__PURE__ */ jsxRuntimeExports.jsx(Activity, { size: 12 }), 1865 "Automation" 1866 ] 1867 } 1868 ) 1869 ] }) 1870 ] }), 1871 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 1872 /* @__PURE__ */ jsxRuntimeExports.jsx( 1873 "button", 1874 { 1875 onClick: () => setZoomX((z) => Math.max(0.2, z - 0.1)), 1876 className: "p-1.5 hover:bg-zinc-200 dark:hover:bg-zinc-800 text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300 rounded transition-colors border border-zinc-200 dark:border-zinc-800/40 bg-white dark:bg-zinc-950/20", 1877 title: "Zoom X Out", 1878 children: /* @__PURE__ */ jsxRuntimeExports.jsx(ZoomOut, { size: 13 }) 1879 } 1880 ), 1881 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] font-bold text-zinc-600 px-1 uppercase tracking-wider", children: "Zoom" }), 1882 /* @__PURE__ */ jsxRuntimeExports.jsx( 1883 "button", 1884 { 1885 onClick: () => setZoomX((z) => Math.min(3, z + 0.1)), 1886 className: "p-1.5 hover:bg-zinc-200 dark:hover:bg-zinc-800 text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300 rounded transition-colors border border-zinc-200 dark:border-zinc-800/40 bg-white dark:bg-zinc-950/20", 1887 title: "Zoom X In", 1888 children: /* @__PURE__ */ jsxRuntimeExports.jsx(ZoomIn, { size: 13 }) 1889 } 1890 ),
1891 /* @__PURE__ */ jsxRuntimeExports.jsx( 1892 "button", 1893 { 1894 onClick: () => setIsHelpOpen(true), 1895 className: "p-1.5 ml-1 bg-indigo-50/50 hover:bg-indigo-100 dark:bg-indigo-950/30 dark:hover:bg-indigo-900/40 text-indigo-600 hover:text-indigo-700 dark:text-indigo-400 dark:hover:text-indigo-300 rounded-full transition-all border border-indigo-200 dark:border-indigo-800/50 shadow-sm", 1896 title: "Arrangement Help & Shortcuts", 1897 children: /* @__PURE__ */ jsxRuntimeExports.jsx(CircleQuestionMark, { size: 13 }) 1898 } 1899 ) 1900 ] }) 1901 ] }), 1902 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `h-8 flex border-b border-zinc-200 dark:border-zinc-800 bg-zinc-150 dark:bg-zinc-900 shrink-0 relative ${isFloating ? "z-[50]" : "z-[5]"}`, children: [ 1903 /* @__PURE__ */ jsxRuntimeExports.jsxs( 1904 "div", 1905 { 1906 style: { width: trackColWidth }, 1907 className: "border-r border-zinc-200 dark:border-zinc-800 flex items-center px-4 font-bold text-[9px] text-zinc-500 dark:text-zinc-400 uppercase tracking-widest shrink-0 relative bg-zinc-100 dark:bg-zinc-950 select-none", 1908 children: [ 1909 "Timeline Map",
1910 /* @__PURE__ */ jsxRuntimeExports.jsx( 1911 "div", 1912 { 1913 onMouseDown: handleTrackColResizeMouseDown, 1914 className: "absolute top-0 right-0 w-1 h-full cursor-col-resize hover:bg-indigo-500/50 active:bg-indigo-500 transition-colors z-30" 1915 } 1916 ) 1917 ] 1918 } 1919 ), 1920 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 relative h-full flex overflow-hidden", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 1921 FLHorizontalScrollbar, 1922 { 1923 containerRef: gridHorizontalScrollRef, 1924 zoomX, 1925 setZoomX, 1926 maxTicks, 1927 ppq, 1928 beatWidth: BEAT_WIDTH$1, 1929 mode: "timeline", 1930 patterns, 1931 playlistClips, 1932 activePatternId, 1933 playlistTracks 1934 } 1935 ) }) 1936 ] }), 1937 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "h-10 flex border-b border-zinc-200 dark:border-zinc-800 bg-zinc-100 dark:bg-zinc-900 shrink-0", children: [
1938 /* @__PURE__ */ jsxRuntimeExports.jsxs( 1939 "div", 1940 { 1941 style: { width: trackColWidth }, 1942 className: "border-r border-zinc-200 dark:border-zinc-800 flex items-center px-4 font-bold text-[10px] text-zinc-500 dark:text-zinc-400 uppercase tracking-widest shrink-0 relative", 1943 children: [ 1944 "Track Lanes", 1945 /* @__PURE__ */ jsxRuntimeExports.jsx( 1946 "div", 1947 { 1948 onMouseDown: handleTrackColResizeMouseDown, 1949 className: "absolute top-0 right-0 w-1 h-full cursor-col-resize hover:bg-indigo-500/50 active:bg-indigo-500 transition-colors z-30" 1950 } 1951 ) 1952 ] 1953 } 1954 ), 1955 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { ref: rulerScrollContainerRef, className: "flex-1 overflow-hidden pointer-events-none relative", children: /* @__PURE__ */ jsxRuntimeExports.jsxs( 1956 "div", 1957 { 1958 className: "h-full relative bg-zinc-50 dark:bg-zinc-950 pointer-events-auto cursor-col-resize", 1959 style: { width: totalWidth }, 1960 onPointerDown: handleRulerPointerDown, 1961 onPointerMove: handleRulerPointerMove, 1962 onPointerUp: handleRulerPointerUp, 1963 onContextMenu: (e) => e.preventDefault(), 1964 children: [ 1965 Array.from({ length: Math.ceil(maxTicks / (ppq * 4)) }).map((_, barIdx) => { 1966 const left = barIdx * ppq * 4 * pxPerTick; 1967 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 1968 "div", 1969 { 1970 className: "absolute h-full border-l border-zinc-200 dark:border-zinc-800 text-[9px] font-bold text-zinc-500 pl-1 pt-0.5 flex flex-col justify-between", 1971 style: { left }, 1972 children: [
1973 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 1974 "Bar ", 1975 barIdx + 1 1976 ] }), 1977 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-[3px] text-[7px] font-semibold text-zinc-700 leading-none pb-0.5", children: [ 1978 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "." }), 1979 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "." }), 1980 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "." }) 1981 ] }) 1982 ] 1983 }, 1984 barIdx 1985 ); 1986 }), 1987 loopRegion && /* @__PURE__ */ jsxRuntimeExports.jsx( 1988 "div", 1989 { 1990 className: "absolute top-0 h-full bg-red-500/10 border-l border-r border-red-500/40 pointer-events-none", 1991 style: { 1992 left: ticksToPixels(loopRegion.startTick), 1993 width: ticksToPixels(loopRegion.endTick - loopRegion.startTick) 1994 } 1995 } 1996 ), 1997 timeMarkers.map((m) => /* @__PURE__ */ jsxRuntimeExports.jsxs( 1998 "div", 1999 { 2000 className: "absolute top-1 bg-teal-500/20 text-teal-300 text-[8px] font-bold px-1 py-0.5 rounded border border-teal-500/40 z-30", 2001 style: { left: ticksToPixels(m.tick), transform: "translateX(-50%)" }, 2002 title: "Right-click to delete marker", 2003 onContextMenu: (e) => { 2004 e.preventDefault(); 2005 e.stopPropagation(); 2006 setTimeMarkers((prev) => prev.filter((x) => x.id !== m.id)); 2007 }, 2008 children: [ 2009 "ð© ", 2010 m.label 2011 ] 2012 }, 2013 m.id 2014 )) 2015 ] 2016 } 2017 ) }) 2018 ] }),
2019 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { ref: verticalScrollContainerRef, className: "flex-1 overflow-y-auto bg-white dark:bg-zinc-950 flex flex-col relative custom-scrollbar", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex min-h-full", children: [ 2020 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2021 "div", 2022 { 2023 style: { width: trackColWidth }, 2024 className: "border-r border-zinc-200 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/40 shrink-0 flex flex-col relative overflow-hidden", 2025 children: [ 2026 /* @__PURE__ */ jsxRuntimeExports.jsx( 2027 "div", 2028 { 2029 onMouseDown: handleTrackColResizeMouseDown, 2030 className: "absolute top-0 right-0 w-1 h-full cursor-col-resize hover:bg-indigo-500/50 active:bg-indigo-500 transition-colors z-30" 2031 } 2032 ), 2033 playlistTracks.map((pt, idx) => { 2034 const h = getTrackHeight(idx); 2035 if (h === 0) return null; 2036 const isCollapsed = collapsedGroups[pt.id]; 2037 const hasChildren = playlistTracks.some((t) => t.parentId === pt.id); 2038 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 2039 "div", 2040 { 2041 style: { height: h }, 2042 onClick: () => setActiveTrackIndex(idx), 2043 className: `border-b border-zinc-200 dark:border-zinc-800/80 px-2 py-1.5 flex flex-col justify-between hover:bg-zinc-200/40 dark:hover:bg-zinc-800/25 relative group overflow-hidden cursor-pointer ${activeTrackIndex === idx ? "bg-indigo-500/10 dark:bg-indigo-500/15 border-l-[3px] border-l-indigo-500" : "bg-zinc-100/20 dark:bg-zinc-900/20 border-l-[3px] border-l-transparent"} ${pt.parentId ? "pl-6" : ""}`, 2044 children: [ 2045 pt.parentId && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute left-2.5 top-0 bottom-1/2 w-2.5 border-l border-b border-zinc-700 rounded-bl-sm" }),
2046 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5 z-10", children: [ 2047 hasChildren && /* @__PURE__ */ jsxRuntimeExports.jsx( 2048 "button", 2049 { 2050 onClick: (e) => { 2051 e.stopPropagation(); 2052 setCollapsedGroups((prev) => ({ ...prev, [pt.id]: !prev[pt.id] })); 2053 }, 2054 className: "p-0.5 text-zinc-500 hover:text-zinc-300 rounded", 2055 children: isCollapsed ? /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronRight, { size: 12 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronDown, { size: 12 }) 2056 } 2057 ), 2058 renamingTrackId === pt.id ? /* @__PURE__ */ jsxRuntimeExports.jsx( 2059 "input", 2060 { 2061 type: "text", 2062 value: renamingTrackText, 2063 onChange: (e) => setRenamingTrackText(e.target.value), 2064 onBlur: () => { 2065 setPlaylistTracks((prev) => prev.map((t) => t.id === pt.id ? { ...t, name: renamingTrackText || t.name } : t)); 2066 setRenamingTrackId(null); 2067 }, 2068 onKeyDown: (e) => { 2069 if (e.key === "Enter") { 2070 setPlaylistTracks((prev) => prev.map((t) => t.id === pt.id ? { ...t, name: renamingTrackText || t.name } : t)); 2071 setRenamingTrackId(null); 2072 } 2073 }, 2074 onFocus: (e) => e.currentTarget.select(), 2075 onDoubleClick: (e) => e.currentTarget.select(), 2076 autoFocus: true, 2077 className: "bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-700 rounded text-xs px-1 text-zinc-900 dark:text-white w-full outline-none" 2078 } 2079 ) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
2080 /* @__PURE__ */ jsxRuntimeExports.jsx( 2081 "span", 2082 { 2083 onDoubleClick: (e) => { 2084 e.stopPropagation(); 2085 setRenamingTrackId(pt.id); 2086 setRenamingTrackText(pt.name); 2087 }, 2088 className: "text-xs font-semibold text-zinc-700 dark:text-zinc-200 truncate flex-1 cursor-default select-none", 2089 children: pt.name 2090 } 2091 ), 2092 /* @__PURE__ */ jsxRuntimeExports.jsx( 2093 "button", 2094 { 2095 onClick: (e) => { 2096 e.stopPropagation(); 2097 setRenamingTrackId(pt.id); 2098 setRenamingTrackText(pt.name); 2099 }, 2100 className: "opacity-0 group-hover:opacity-100 text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-200 p-0.5 rounded transition-opacity shrink-0", 2101 title: "Rename Track", 2102 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Pencil, { size: 11 }) 2103 } 2104 ) 2105 ] }),
2106 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[10px] text-zinc-500/80 shrink-0 font-medium", children: [ 2107 "#", 2108 idx + 1 2109 ] }) 2110 ] }), 2111 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between gap-2 z-10", onClick: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), children: [ 2112 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-1", children: [ 2113 /* @__PURE__ */ jsxRuntimeExports.jsx( 2114 "button", 2115 { 2116 onClick: () => toggleTrackMute(idx), 2117 className: `w-5 h-5 rounded text-[10px] font-bold transition-colors ${pt.isMuted ? "bg-yellow-600 text-black shadow-inner" : "bg-zinc-200 dark:bg-zinc-800 text-zinc-500 hover:text-zinc-800 dark:hover:text-zinc-300"}`, 2118 title: "Mute Track", 2119 children: "M" 2120 } 2121 ),
2122 /* @__PURE__ */ jsxRuntimeExports.jsx( 2123 "button", 2124 { 2125 onClick: () => toggleTrackSolo(idx), 2126 className: `w-5 h-5 rounded text-[10px] font-bold transition-colors ${pt.isSoloed ? "bg-red-600 text-white shadow" : "bg-zinc-200 dark:bg-zinc-800 text-zinc-500 hover:text-zinc-800 dark:hover:text-zinc-300"}`, 2127 title: "Solo Track", 2128 children: "S" 2129 } 2130 ), 2131 !easyMode && /* @__PURE__ */ jsxRuntimeExports.jsx( 2132 "button", 2133 { 2134 onClick: () => toggleTrackGroup(idx), 2135 className: "p-1 text-zinc-500 hover:text-zinc-300 rounded hover:bg-zinc-800/40", 2136 title: "Indent / Outdent Track", 2137 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Split, { size: 10 }) 2138 } 2139 ) 2140 ] }), 2141 !easyMode && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [
2142 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center", title: `Volume: ${Math.round(pt.volume * 100)}%`, children: [ 2143 /* @__PURE__ */ jsxRuntimeExports.jsx( 2144 "input", 2145 { 2146 type: "range", 2147 min: "0", 2148 max: "1", 2149 step: "0.05", 2150 value: pt.volume, 2151 onChange: (e) => { 2152 const val = parseFloat(e.target.value); 2153 setPlaylistTracks((prev) => prev.map((t, i) => i === idx ? { ...t, volume: val } : t)); 2154 setTimeout(() => forceSync(), 0); 2155 }, 2156 className: "w-10 h-1 accent-indigo-500 cursor-pointer" 2157 } 2158 ),
2159 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[7px] text-zinc-500 font-bold uppercase mt-0.5", children: "Vol" }) 2160 ] }), 2161 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center", title: `Panning: ${Math.round(pt.panning * 100)}`, children: [ 2162 /* @__PURE__ */ jsxRuntimeExports.jsx( 2163 "input", 2164 { 2165 type: "range", 2166 min: "-1", 2167 max: "1", 2168 step: "0.1", 2169 value: pt.panning, 2170 onChange: (e) => { 2171 const val = parseFloat(e.target.value); 2172 setPlaylistTracks((prev) => prev.map((t, i) => i === idx ? { ...t, panning: val } : t)); 2173 setTimeout(() => forceSync(), 0); 2174 }, 2175 className: "w-10 h-1 accent-indigo-500 cursor-pointer" 2176 } 2177 ),
2178 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[7px] text-zinc-500 font-bold uppercase mt-0.5", children: "Pan" }) 2179 ] }), 2180 /* @__PURE__ */ jsxRuntimeExports.jsx( 2181 "button", 2182 { 2183 onClick: () => handleCreateAutomationClip(idx, 7), 2184 className: "p-1 hover:bg-zinc-850 hover:text-indigo-400 text-zinc-600 rounded transition-all", 2185 title: "Add Volume Automation Envelope", 2186 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Activity, { size: 11 }) 2187 } 2188 ) 2189 ] }) 2190 ] }), 2191 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between opacity-0 group-hover:opacity-100 transition-opacity z-10 mt-1", children: [ 2192 /* @__P
2192URE__ */ jsxRuntimeExports.jsxs("div", { className: "relative", children: [ 2193 /* @__PURE__ */ jsxRuntimeExports.jsx( 2194 "button", 2195 { 2196 onClick: () => setActiveColorPickerId(activeColorPickerId === pt.id ? null : pt.id), 2197 className: "w-3.5 h-3.5 rounded border border-white/20", 2198 style: { backgroundColor: pt.color } 2199 } 2200 ), 2201 activeColorPickerId === pt.id && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute left-0 mt-1 bg-white dark:bg-zinc-950 border border-zinc-200 dark:border-zinc-800 p-1 rounded flex gap-1 z-50", children: ["#6366f1", "#e11d48", "#10b981", "#f59e0b", "#06b6d4", "#ec4899"].map((c) => /* @__PURE__ */ jsxRuntimeExports.jsx( 2202 "button", 2203 { 2204 onClick: () => handleSelectColor(pt.id, c, "track"), 2205 className: "w-4.5 h-4.5 rounded border border-white/10", 2206 style: { backgroundColor: c } 2207 }, 2208 c 2209 )) }) 2210 ] }),
2211 /* @__PURE__ */ jsxRuntimeExports.jsx( 2212 "button", 2213 { 2214 onClick: () => handleDeleteTrack(idx), 2215 className: "p-0.5 text-zinc-500 hover:text-red-500", 2216 title: "Delete Track", 2217 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Trash2, { size: 11 }) 2218 } 2219 ) 2220 ] }), 2221 !easyMode && /* @__PURE__ */ jsxRuntimeExports.jsx( 2222 "div", 2223 { 2224 onMouseDown: (e) => handleTrackHeaderResize(idx, e), 2225 className: "absolute bottom-0 left-0 w-full h-1 cursor-ns-resize hover:bg-indigo-500/50 transition-colors z-20" 2226 } 2227 ) 2228 ] 2229 }, 2230 pt.id 2231 ); 2232 }), 2233 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "h-12 border-b border-zinc-200 dark:border-zinc-800 flex items-center justify-center p-2", children: /* @__PURE__ */ jsxRuntimeExports.jsxs( 2234 "button", 2235 { 2236 onClick: handleAddPlaylistTrack, 2237 className: "flex items-center gap-1 px-3 py-1.5 text-[10px] font-bold text-zinc-500 dark:text-zinc-400 hover:text-indigo-500 dark:hover:text-indigo-400 hover:bg-indigo-500/10 rounded border border-dashed border-zinc-300 dark:border-zinc-800 hover:border-indigo-500/30 transition-all w-full justify-center", 2238 children: [
2239 /* @__PURE__ */ jsxRuntimeExports.jsx(Plus, { size: 12 }), 2240 " Add track lane" 2241 ] 2242 } 2243 ) }) 2244 ] 2245 } 2246 ), 2247 /* @__PURE__ */ jsxRuntimeExports.jsx( 2248 "div", 2249 { 2250 ref: gridHorizontalScrollRef, 2251 onScroll: handleHorizontalScroll, 2252 className: "flex-1 overflow-x-auto relative custom-scrollbar select-none", 2253 children: /* @__PURE__ */ jsxRuntimeExports.jsxs( 2254 "div", 2255 { 2256 ref: gridRef, 2257 onPointerDown: handleGridPointerDown, 2258 onPointerMove: handleGridPointerMove, 2259 onPointerUp: handleGridPointerUp, 2260 className: "h-full relative overflow-hidden bg-white dark:bg-zinc-950 bg-grid-pattern", 2261 style: { 2262 width: totalWidth, 2263 // CSS Grid beat/bar visual separator lines using linear gradients 2264 backgroundImage: ` 2265 linear-gradient(to right, rgba(128,128,128,0.12) 1px, transparent 1px), 2266 linear-gradient(to right, rgba(128,128,128,0.04) 1px, transparent 1px) 2267 `, 2268 backgroundSize: `${ppq * 4 * pxPerTick}px 100%, ${ppq * pxPerTick}px 100%`, 2269 minHeight: playlistTracks.reduce((sum, _, i) => sum + getTrackHeight(i), 0) + 100 2270 }, 2271 onContextMenu: (e) => e.preventDefault(), 2272 children: [ 2273 playlistTracks.map((pt, idx) => { 2274 const h = getTrackHeight(idx); 2275 if (h === 0) return null;
2276 const top = getTrackTop(idx); 2277 return /* @__PURE__ */ jsxRuntimeExports.jsx( 2278 "div", 2279 { 2280 className: "absolute left-0 w-full border-b border-zinc-200 dark:border-zinc-900 pointer-events-none", 2281 style: { top: top + h - 1, height: 1 } 2282 }, 2283 `sep-${pt.id}` 2284 ); 2285 }), 2286 playlistClips.map((clip) => { 2287 const trackTop = getTrackTop(clip.trackIndex); 2288 const trackHeight = getTrackHeight(clip.trackIndex); 2289 if (trackHeight === 0) return null; 2290 const pattern = patterns.find((p) => p.id === clip.patternId); 2291 if (!pattern) return null; 2292 const patternLength = getPatternLength(pattern); 2293 const actualDurationTicks = clip.isManualLength ? clip.durationTicks : patternLength; 2294 const clipLeft = ticksToPixels(clip.startTick); 2295 const clipWidth = ticksToPixels(actualDurationTicks); 2296 const clipHeight = trackHeight - 12; 2297 const isSelected = selectedClipIds.includes(clip.id); 2298 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 2299 "div", 2300 { 2301 onDoubleClick: () => !easyMode && handleClipDoubleClick(clip.patternId), 2302 className: `absolute rounded transition-shadow flex flex-col overflow-hidden ${clip.isMuted ? "opacity-40" : ""} ${!easyMode && isSelected ? "ring-2 ring-indigo-500 border border-white z-40" : "border border-zinc-300/50 dark:border-zinc-700/40 z-10"}`, 2303 style: { 2304 left: clipLeft, 2305 width: clipWidth, 2306 top: trackTop + 6, 2307 height: clipHeight, 2308 backgroundColor: easyMode ? pattern.color + "18" : pattern.color + "25", 2309 borderColor: pattern.color 2310 }, 2311 onPointerDown: (e) => { 2312 var _a2, _b2, _c; 2313 e.stopPropagation(); 2314 if (e.button === 1) { 2315 e.preventDefault(); 2316 panStartRef.current = { 2317 x: e.clientX, 2318 y: e.clientY, 2319 scrollLeft: ((_a2 = gridHorizontalScrollRef.current) == null ? void 0 : _a2.scrollLeft) || 0, 2320 scrollTop: ((_b2 = verticalScrollContainerRef.current) == null ? void 0 : _b2.scrollTop) || 0 2321 }; 2322 setIsPanning(true); 2323 document.body.style.cursor = "grabbing"; 2324 (_c = gridRef.current) == null ? void 0 : _c.setPointerCapture(e.pointerId); 2325 return; 2326 } 2327 if (easyMode) { 2328 if (e.button === 0) {
2329 setActiveTrackIndex(clip.trackIndex); 2330 setActivePatternId(clip.patternId); 2331 } 2332 return; 2333 } 2334 if (e.button === 0) { 2335 const isRightEdge = e.clientX - e.currentTarget.getBoundingClientRect().right > -8; 2336 const isLeftEdge = e.clientX - e.currentTarget.getBoundingClientRect().left < 8; 2337 if (!easyMode && e.shiftKey && !isRightEdge && !isLeftEdge) { 2338 const cloneId = `clip-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; 2339 const clonedClip = { 2340 ...clip, 2341 id: cloneId 2342 }; 2343 setPlaylistClips((prev) => [...prev, clonedClip]); 2344 setActiveDragClipId(cloneId); 2345 setDragStartClipState(clonedClip); 2346 setDragStartMouseX(e.clientX);
2347 setActiveDragAction("move"); 2348 setDragStartTick(clip.startTick); 2349 setDragStartTrackIdx(clip.trackIndex); 2350 setSelectedClipIds([cloneId]); 2351 } else { 2352 setActiveDragClipId(clip.id); 2353 setDragStartClipState(clip); 2354 setDragStartMouseX(e.clientX); 2355 if (isRightEdge) { 2356 setActiveDragAction("trim-right"); 2357 } else if (isLeftEdge) { 2358 setActiveDragAction("trim-left"); 2359 } else { 2360 if (currentPlaylistTool === "slip") { 2361 setActiveDragAction("slip"); 2362 } else if (currentPlaylistTool === "paint") { 2363 setActiveDragAction("paint"); 2364 lastPaintedTickRef.current = clip.startTick; 2365 lastPaintedTrackIdxRef.current = clip.trackIndex; 2366 } else {
2367 setActiveDragAction("move"); 2368 setDragStartTick(clip.startTick); 2369 setDragStartTrackIdx(clip.trackIndex); 2370 } 2371 } 2372 if (e.shiftKey) { 2373 setSelectedClipIds((prev) => [...prev, clip.id]); 2374 } else if (!isSelected) { 2375 setSelectedClipIds([clip.id]); 2376 } 2377 } 2378 } else if (e.button === 2) { 2379 e.preventDefault(); 2380 e.stopPropagation(); 2381 if (!easyMode && e.altKey) { 2382 const rect = gridRef.current.getBoundingClientRect(); 2383 const clickX = e.clientX - rect.left; 2384 const clickTick = getSnappedTick(pixelsToTicks(clickX)); 2385 setContextMenu({ 2386 x: e.clientX, 2387 y: e.clientY, 2388 clipId: clip.id, 2389 clickTick 2390 }); 2391 } else { 2392 setPlaylistClips((prev) => prev.filter((c) => c.id !== clip.id));
2393 setActiveDragAction("right-delete"); 2394 setTimeout(() => forceSync(), 0); 2395 } 2396 } 2397 }, 2398 children: [ 2399 !easyMode && /* @__PURE__ */ jsxRuntimeExports.jsxs( 2400 "div", 2401 { 2402 className: "h-4 px-1.5 text-[9px] font-bold text-white flex items-center justify-between border-b border-black/10 select-none", 2403 style: { backgroundColor: pattern.color + "40" }, 2404 children: [ 2405 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "truncate flex-1", children: pattern.name }), 2406 clip.isMuted && /* @__PURE__ */ jsxRuntimeExports.jsx(VolumeX, { size: 8, className: "text-red-400" }) 2407 ] 2408 } 2409 ),
2410 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 relative overflow-hidden pointer-events-none", children: Array.from({ length: Math.ceil(actualDurationTicks / patternLength) }).map((_, loopIdx) => { 2411 const loopOffset = loopIdx * patternLength - clip.offsetTicks; 2412 const loopLeft = ticksToPixels(loopOffset); 2413 const loopWidth = ticksToPixels(patternLength); 2414 return /* @__PURE__ */ jsxRuntimeExports.jsx( 2415 "div", 2416 { 2417 className: "absolute h-full border-r border-dashed border-white/10", 2418 style: { left: loopLeft, width: loopWidth }, 2419 children: pattern.notes.map((note, nIdx) => { 2420 const noteLeft = ticksToPixels(note.ticks); 2421 const noteWidth = Math.max(ticksToPixels(note.durationTicks), 2); 2422 const noteMidis = pattern.notes.map((n) => n.midi); 2423 const minN = noteMidis.length > 0 ? Math.min(...noteMidis) - 2 : 36; 2424 const maxN = noteMidis.length > 0 ? Math.max(...noteMidis) + 2 : 96; 2425 const noteRange = maxN - minN || 1; 2426 const noteY = 100 - (note.midi - minN) / noteRange * 100; 2427 return /* @__PURE__ */ jsxRuntimeExports.jsx( 2428 "div", 2429 { 2430 className: "absolute rounded-sm", 2431 style: { 2432 left: noteLeft, 2433 width: noteWidth, 2434 top: `${Math.max(5, Math.min(95, noteY))}%`, 2435 backgroundColor: pattern.color, 2436 height: "2px", 2437 opacity: 0.85 2438 } 2439 }, 2440 nIdx 2441 ); 2442 }) 2443 }, 2444 loopIdx 2445 ); 2446 }) }),
2447 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute left-0 top-0 w-2 h-full cursor-col-resize z-30" }), 2448 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute right-0 top-0 w-2 h-full cursor-col-resize z-30" }) 2449 ] 2450 }, 2451 clip.id 2452 ); 2453 }), 2454 isAutomationVisible && !easyMode && automationClips.map((clip) => { 2455 const trackTop = getTrackTop(clip.trackIndex); 2456 const trackHeight = getTrackHeight(clip.trackIndex); 2457 if (trackHeight === 0) return null; 2458 const clipLeft = ticksToPixels(clip.startTick); 2459 const clipWidth = ticksToPixels(clip.durationTicks); 2460 const sortedNodes = [...clip.nodes].sort((a, b) => a.tick - b.tick); 2461 const points = []; 2462 sortedNodes.forEach((node, nodeIdx) => { 2463 const x = ticksToPixels(node.tick); 2464 const y = trackTop + (1 - node.value) * trackHeight; 2465 if (nodeIdx === 0) { 2466 points.push(`M ${x} ${y}`); 2467 } else { 2468 const prevNode = sortedNodes[nodeIdx - 1]; 2469 const prevX = ticksToPixels(prevNode.tick); 2470 const prevY = trackTop + (1 - prevNode.value) * trackHeight; 2471 if (Math.abs(prevNode.tension || 0) < 0.01) { 2472 points.push(`L ${x} ${y}`); 2473 } else { 2474 const k = (prevNode.tension || 0) * 5; 2475 for (let step = 1; step <= 20; step++) { 2476 const t = step / 20; 2477 const curveT = (Math.exp(t * k) - 1) / (Math.exp(k) - 1); 2478 const currX = prevX + t * (x - prevX); 2479 const currY = prevY + curveT * (y - prevY); 2480 points.push(`L ${currX} ${currY}`); 2481 } 2482 } 2483 } 2484 }); 2485 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute inset-0 pointer-events-none", children: [
2486 /* @__PURE__ */ jsxRuntimeExports.jsx( 2487 "div", 2488 { 2489 className: "absolute bg-teal-500/5 border border-dashed border-teal-500/25 rounded z-0", 2490 style: { 2491 left: clipLeft, 2492 width: clipWidth, 2493 top: trackTop + 3, 2494 height: trackHeight - 6 2495 } 2496 } 2497 ), 2498 /* @__PURE__ */ jsxRuntimeExports.jsx("svg", { className: "absolute inset-0 w-full h-full z-20", children: sortedNodes.length > 1 && /* @__PURE__ */ jsxRuntimeExports.jsx( 2499 "path", 2500 { 2501 d: points.join(" "), 2502 fill: "none", 2503 stroke: "#14b8a6", 2504 strokeWidth: "2", 2505 strokeDasharray: "4 2" 2506 } 2507 ) }), 2508 sortedNodes.map((node, nodeIdx) => { 2509 const nodeX = ticksToPixels(node.tick); 2510 const nodeY = trackTop + (1 - node.value) * trackHeight; 2511 let tensionHandle = null; 2512 if (nodeIdx < sortedNodes.length - 1) { 2513 const nextNode = sortedNodes[nodeIdx + 1]; 2514 const nextX = ticksToPixels(nextNode.tick); 2515 const nextY = trackTop + (1 - nextNode.value) * trackHeight; 2516 const midX = (nodeX + nextX) / 2; 2517 const k = (node.tension || 0) * 5; 2518 const curveT = Math.abs(k) < 0.01 ? 0.5 : (Math.exp(0.5 * k) - 1) / (Math.exp(k) - 1); 2519 const midY = nodeY + curveT * (nextY - nodeY); 2520 tensionHandle = /* @__PURE__ */ jsxRuntimeExports.jsx( 2521 "div", 2522 { 2523 onPointerDown: (e) => { 2524 e.stopPropagation(); 2525 e.preventDefault();
2526 setActiveDragAction("tension-drag"); 2527 setDragTensionClipId(clip.id); 2528 setDragTensionNodeIdx(nodeIdx); 2529 }, 2530 className: "absolute w-2 h-2 bg-indigo-400 border border-white rounded-full cursor-ns-resize pointer-events-auto transform -translate-x-1/2 -translate-y-1/2 hover:scale-125 transition-transform z-35", 2531 style: { left: midX, top: midY }, 2532 title: "Drag to curve envelope" 2533 }, 2534 `tension-${node.id}` 2535 ); 2536 } 2537 return /* @__PURE__ */ jsxRuntimeExports.jsxs(React.Fragment, { children: [ 2538 /* @__PURE__ */ jsxRuntimeExports.jsx( 2539 "div", 2540 { 2541 onPointerDown: (e) => { 2542 e.stopPropagation(); 2543 e.preventDefault(); 2544 if (e.button === 0) {
2545 setActiveDragAction("node-drag"); 2546 setDragNodeClipId(clip.id); 2547 setDragNodeId(node.id); 2548 setDragNodeStartProps({ tick: node.tick, value: node.value }); 2549 setDragStartTick(rawTick); 2550 } else if (e.button === 2) { 2551 if (clip.nodes.length > 2) { 2552 setAutomationClips((prev) => prev.map( 2553 (c) => c.id === clip.id ? { ...c, nodes: c.nodes.filter((n) => n.id !== node.id) } : c 2554 )); 2555 setTimeout(() => forceSync(), 0); 2556 } 2557 } 2558 }, 2559 onDoubleClick: (e) => { 2560 e.stopPropagation(); 2561 const newValText = window.prompt("Enter node value (0.0 to 1.0):", node.value.toFixed(2)); 2562 if (newValText) { 2563 const value = Math.max(0, Math.min(1, parseFloat(newValText))); 2564 setAutomationClips((prev) => prev.map( 2565 (c) => c.id === clip.id ? { ...c, nodes: c.nodes.map((n) => n.id === node.id ? { ...n, value } : n) } : c 2566 )); 2567 setTimeout(() => forceSync(), 0); 2568 } 2569 }, 2570 className: "absolute w-3.5 h-3.5 bg-teal-400 border-2 border-white dark:border-zinc-950 rounded-full cursor-move pointer-events-auto transform -translate-x-1/2 -translate-y-1/2 hover:scale-125 transition-transform shadow-md z-30", 2571 style: { left: nodeX, top: nodeY }, 2572 title: "Drag to edit node. Double click to type value. Right click to delete node." 2573 } 2574 ), 2575 tensionHandle 2576 ] }, node.id); 2577 }),
2578 /* @__PURE__ */ jsxRuntimeExports.jsx( 2579 "div", 2580 { 2581 className: "absolute pointer-events-auto z-10 cursor-crosshair opacity-0 hover:opacity-10", 2582 style: { 2583 left: clipLeft, 2584 width: clipWidth, 2585 top: trackTop + 3, 2586 height: trackHeight - 6 2587 }, 2588 onPointerDown: (e) => { 2589 if (e.button === 0 && e.ctrlKey) { 2590 e.stopPropagation(); 2591 const bounding = e.currentTarget.getBoundingClientRect(); 2592 const xPos = e.clientX - bounding.left; 2593 const nodeTick = getSnappedTick(pixelsToTicks(xPos)); 2594 const yPos = e.clientY - bounding.top; 2595 const nodeVal = 1 - yPos / (trackHeight - 6); 2596 const newNode = { 2597 id: `n-${Date.now()}`, 2598 tick: nodeTick, 2599 value: Math.max(0, Math.min(1, nodeVal)), 2600 tension: 0 2601 }; 2602 setAutomationClips((prev) => prev.map( 2603 (c) => c.id === clip.id ? { ...c, nodes: [...c.nodes, newNode] } : c 2604 )); 2605 setTimeout(() => forceSync(), 0); 2606 } 2607 }, 2608 title: "Ctrl+Left Click to place automation node" 2609 } 2610 ) 2611 ] }, clip.id); 2612 }),
2613 /* @__PURE__ */ jsxRuntimeExports.jsx( 2614 "div", 2615 { 2616 ref: arrangementPlayheadRef, 2617 className: "absolute top-0 h-full w-[2px] bg-amber-500 z-50 pointer-events-none shadow", 2618 style: { left: ticksToPixels(engine ? engine.secondsToTicks(currentTime) : 0) } 2619 } 2620 ), 2621 activeDragAction === "select" && lassoStart && lassoCurrent && /* @__PURE__ */ jsxRuntimeExports.jsx( 2622 "div", 2623 { 2624 className: "absolute border border-indigo-400 bg-indigo-500/10 z-40 pointer-events-none", 2625 style: { 2626 left: Math.min(lassoStart.x, lassoCurrent.x), 2627 top: Math.min(lassoStart.y, lassoCurrent.y), 2628 width: Math.abs(lassoStart.x - lassoCurrent.x), 2629 height: Math.abs(lassoStart.y - lassoCurrent.y) 2630 } 2631 } 2632 ), 2633 activeDragAction === "slice" && sliceStart && sliceCurrent && /* @__PURE__ */ jsxRuntimeExports.jsx( 2634 "div", 2635 { 2636 className: "absolute border-l-2 border-dashed border-red-500 z-40 pointer-events-none", 2637 style: { 2638 left: sliceCurrent.x - gridRef.current.getBoundingClientRect().left, 2639 top: getTrackTop(dragStartTrackIdx), 2640 height: getTrackHeight(dragStartTrackIdx) 2641 } 2642 } 2643 ) 2644 ] 2645 } 2646 ) 2647 } 2648 ) 2649 ] }) }) 2650 ] }), 2651 contextMenu && /* @__PURE__ */ jsxRuntimeExports.jsxs( 2652 "div", 2653 { 2654 className: "fixed bg-white/95 dark:bg-zinc-900/95 border border-zinc-200 dark:border-zinc-800 rounded-lg shadow-xl py-1 z-[100] w-42 backdrop-blur-md text-xs font-semibold text-zinc-700 dark:text-zinc-300", 2655 style: { left: contextMenu.x, top: contextMenu.y }, 2656 onClick: (e) => e.stopPropagation(), 2657 children: [
2658 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2659 "button", 2660 { 2661 onClick: () => { 2662 makePatternUnique(contextMenu.clipId); 2663 setContextMenu(null); 2664 }, 2665 className: "w-full text-left px-3 py-1.5 hover:bg-indigo-600 hover:text-white transition-colors flex items-center gap-2", 2666 children: [ 2667 /* @__PURE__ */ jsxRuntimeExports.jsx(Copy, { size: 13 }), 2668 " Make Unique" 2669 ] 2670 } 2671 ), 2672 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2673 "button", 2674 { 2675 onClick: () => { 2676 const clip = playlistClips.find((c) => c.id === contextMenu.clipId); 2677 const pattern = patterns.find((p) => p.id === (clip == null ? void 0 : clip.patternId)); 2678 if (pattern) { 2679 const newName = window.prompt("Rename Pattern:", pattern.name); 2680 if (newName) { 2681 setPatterns((prev) => prev.map((p) => p.id === pattern.id ? { ...p, name: newName } : p)); 2682 } 2683 const newColor = window.prompt("Enter Color Hex (e.g. #ff0055):", pattern.color); 2684 if (newColor) { 2685 setPatterns((prev) => prev.map((p) => p.id === pattern.id ? { ...p, color: newColor } : p)); 2686 } 2687 } 2688 setContextMenu(null); 2689 }, 2690 className: "w-full text-left px-3 py-1.5 hover:bg-indigo-600 hover:text-white transition-colors flex items-center gap-2", 2691 children: [
2692 /* @__PURE__ */ jsxRuntimeExports.jsx(Pencil, { size: 13 }), 2693 " Rename/Color" 2694 ] 2695 } 2696 ), 2697 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2698 "button", 2699 { 2700 onClick: () => { 2701 handleSliceClipAtTick(contextMenu.clipId, contextMenu.clickTick); 2702 setContextMenu(null); 2703 }, 2704 className: "w-full text-left px-3 py-1.5 hover:bg-indigo-600 hover:text-white transition-colors flex items-center gap-2", 2705 children: [ 2706 /* @__PURE__ */ jsxRuntimeExports.jsx(Scissors, { size: 13 }), 2707 " Chop/Slice" 2708 ] 2709 } 2710 ), 2711 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2712 "button", 2713 { 2714 onClick: () => { 2715 setPlaylistClips((prev) => prev.map((c) => c.id === contextMenu.clipId ? { ...c, isManualLength: false, offsetTicks: 0 } : c)); 2716 setContextMenu(null); 2717 setTimeout(() => forceSync(), 0); 2718 }, 2719 className: "w-full text-left px-3 py-1.5 hover:bg-indigo-600 hover:text-white transition-colors flex items-center gap-2", 2720 children: [
2721 /* @__PURE__ */ jsxRuntimeExports.jsx(SlidersVertical, { size: 13 }), 2722 " Reset Clip Length" 2723 ] 2724 } 2725 ) 2726 ] 2727 } 2728 ), 2729 isHelpOpen && /* @__PURE__ */ jsxRuntimeExports.jsx( 2730 "div", 2731 { 2732 className: "fixed inset-0 bg-black/40 dark:bg-black/60 backdrop-blur-sm z-[200] flex items-center justify-center p-4 animate-fade-in", 2733 onClick: () => setIsHelpOpen(false), 2734 children: /* @__PURE__ */ jsxRuntimeExports.jsxs( 2735 "div", 2736 { 2737 className: "bg-white/90 dark:bg-zinc-900/90 border border-zinc-200 dark:border-zinc-800 w-full max-w-2xl rounded-2xl shadow-2xl overflow-hidden backdrop-blur-md flex flex-col h-[520px] max-h-[90vh] transition-transform scale-100 transform duration-300 hover:shadow-indigo-500/10", 2738 onClick: (e) => e.stopPropagation(), 2739 children: [ 2740 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-4 border-b border-zinc-200 dark:border-zinc-800 flex items-center justify-between bg-zinc-50/50 dark:bg-zinc-950/40", children: [
2741 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 2742 /* @__PURE__ */ jsxRuntimeExports.jsx(CircleQuestionMark, { className: "text-indigo-500 dark:text-indigo-400", size: 18 }), 2743 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold text-sm text-zinc-800 dark:text-zinc-100 uppercase tracking-wider", children: "Arrangement View Guide" }) 2744 ] }), 2745 /* @__PURE__ */ jsxRuntimeExports.jsx( 2746 "button", 2747 { 2748 onClick: () => setIsHelpOpen(false), 2749 className: "p-1 hover:bg-zinc-200 dark:hover:bg-zinc-800 text-zinc-500 dark:text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-200 rounded-full transition-colors", 2750 children: /* @__PURE__ */ jsxRuntimeExports.jsx(X, { size: 16 }) 2751 } 2752 ) 2753 ] }), 2754 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex border-b border-zinc-100 dark:border-zinc-800 bg-zinc-50/20 dark:bg-zinc-950/10 p-1.5 gap-1 shrink-0", children: [
2755 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2756 "button", 2757 { 2758 onClick: () => setActiveHelpTab("shortcuts"), 2759 className: `flex-1 py-2 px-4 rounded-xl text-xs font-bold transition-all flex items-center justify-center gap-2 ${activeHelpTab === "shortcuts" ? "bg-indigo-600 text-white shadow-md shadow-indigo-600/20" : "text-zinc-600 dark:text-zinc-400 hover:text-zinc-800 dark:hover:text-zinc-200 hover:bg-zinc-100 dark:hover:bg-zinc-800/40"}`, 2760 children: [ 2761 /* @__PURE__ */ jsxRuntimeExports.jsx(MousePointer2, { size: 13 }), 2762 "Controls & Shortcuts" 2763 ] 2764 } 2765 ), 2766 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2767 "button", 2768 { 2769 onClick: () => setActiveHelpTab("about"), 2770 className: `flex-1 py-2 px-4 rounded-xl text-xs font-bold transition-all flex items-center justify-center gap-2 ${activeHelpTab === "about" ? "bg-indigo-600 text-white shadow-md shadow-indigo-600/20" : "text-zinc-600 dark:text-zinc-400 hover:text-zinc-800 dark:hover:text-zinc-200 hover:bg-zinc-100 dark:hover:bg-zinc-800/40"}`, 2771 children: [
2772 /* @__PURE__ */ jsxRuntimeExports.jsx(Layers, { size: 13 }), 2773 "About Arrangement" 2774 ] 2775 } 2776 ) 2777 ] }), 2778 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 overflow-y-auto p-5 text-xs text-zinc-600 dark:text-zinc-300 leading-relaxed scrollbar-thin scrollbar-thumb-zinc-300 dark:scrollbar-thumb-zinc-700", children: activeHelpTab === "shortcuts" ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4", children: [ 2779 easyMode && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-3 bg-amber-50 dark:bg-amber-950/20 border border-amber-200/50 dark:border-amber-800/30 rounded-xl text-amber-800 dark:text-amber-300 font-semibold mb-3 flex items-start gap-2", children: [ 2780 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-amber-500", children: "â ï¸" }), 2781 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [
2782 /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { children: "Easy Mode Active:" }), 2783 " Some advanced tools and shortcuts (e.g. Duplication, Copy/Paste, Nudging) are disabled. Switch to Pro Mode in settings for full DAW functionality." 2784 ] }) 2785 ] }), 2786 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 2787 /* @__PURE__ */ jsxRuntimeExports.jsx("h4", { className: "font-bold text-zinc-800 dark:text-zinc-100 mb-2 border-b border-zinc-100 dark:border-zinc-800 pb-1 uppercase tracking-wider text-[10px]", children: "ð±ï¸ Mouse Controls & Actions" }), 2788 /* @__PURE__ */ jsxRuntimeExports.jsxs("table", { className: "w-full text-left border-collapse", children: [ 2789 /* @__PURE__ */ jsxRuntimeExports.jsx("thead", { children: /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { className: "border-b border-zinc-100 dark:border-zinc-800 text-zinc-400 text-[9px] uppercase", children: [ 2790 /* @__P
2790URE__ */ jsxRuntimeExports.jsx("th", { className: "py-1 font-semibold w-1/3", children: "Action" }), 2791 /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "py-1 font-semibold", children: "Behavior" }) 2792 ] }) }), 2793 /* @__PURE__ */ jsxRuntimeExports.jsxs("tbody", { className: "divide-y divide-zinc-100/50 dark:divide-zinc-800/50", children: [ 2794 /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { children: [ 2795 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "py-1.5 font-bold text-zinc-700 dark:text-zinc-300", children: "Left-Click (Draw/Paint)" }), 2796 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "py-1.5", children: "Place active pattern on snapped grid timeline. Click & drag to move existing clip." }) 2797 ] }), 2798 /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { children: [ 2799 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "py-1.5 font-bold text-zinc-700 dark:text-zinc-300", children: "Left/Right Edge Drag" }),
2800 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "py-1.5", children: "Trim clip boundaries left or right (adjusts duration)." }) 2801 ] }), 2802 /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { children: [ 2803 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "py-1.5 font-bold text-zinc-700 dark:text-zinc-300", children: "Shift + Drag Clip" }), 2804 /* @__PURE__ */ jsxRuntimeExports.jsxs("td", { className: "py-1.5", children: [ 2805 "Clone/duplicate the clip to a new position. ", 2806 !easyMode ? /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-green-500 font-medium", children: "(Pro Only)" }) : /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-500 font-medium", children: "(Locked)" }) 2807 ] }) 2808 ] }), 2809 /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { children: [ 2810 /* @__P
2810URE__ */ jsxRuntimeExports.jsx("td", { className: "py-1.5 font-bold text-zinc-700 dark:text-zinc-300", children: "Right-Click Clip" }), 2811 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "py-1.5", children: "Quickly delete a clip from the playlist grid." }) 2812 ] }), 2813 /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { children: [ 2814 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "py-1.5 font-bold text-zinc-700 dark:text-zinc-300", children: "Alt + Right-Click Clip" }), 2815 /* @__PURE__ */ jsxRuntimeExports.jsxs("td", { className: "py-1.5", children: [ 2816 "Open Context Menu (Make Unique, Rename/Color, Chop/Slice). ", 2817 !easyMode ? /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-green-500 font-medium", children: "(Pro Only)" }) : /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-500 font-medium", children: "(Locked)" }) 2818 ] }) 2819 ] }),
2820 /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { children: [ 2821 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "py-1.5 font-bold text-zinc-700 dark:text-zinc-300", children: "Middle Mouse Button (MMB)" }), 2822 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "py-1.5", children: "Press and drag anywhere on the grid canvas to pan the view." }) 2823 ] }), 2824 /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { children: [ 2825 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "py-1.5 font-bold text-zinc-700 dark:text-zinc-300", children: "Ctrl + Scroll Wheel" }), 2826 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "py-1.5", children: "Zoom view horizontally, centered around the mouse cursor." }) 2827 ] }), 2828 /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { children: [ 2829 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "py-1.5 font-bold text-zinc-700 dark:text-zinc-300", children: "Alt + Scroll Wheel" }),
2830 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "py-1.5", children: "Zoom track lane heights vertically." }) 2831 ] }) 2832 ] }) 2833 ] }) 2834 ] }), 2835 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "pt-2", children: [ 2836 /* @__PURE__ */ jsxRuntimeExports.jsx("h4", { className: "font-bold text-zinc-800 dark:text-zinc-100 mb-2 border-b border-zinc-100 dark:border-zinc-800 pb-1 uppercase tracking-wider text-[10px]", children: "â¨ï¸ Keyboard Hotkeys" }), 2837 /* @__PURE__ */ jsxRuntimeExports.jsxs("table", { className: "w-full text-left border-collapse", children: [ 2838 /* @__PURE__ */ jsxRuntimeExports.jsx("thead", { children: /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { className: "border-b border-zinc-100 dark:border-zinc-800 text-zinc-400 text-[9px] uppercase", children: [ 2839 /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "py-1 font-semibold w-1/3", children: "Shortcut" }),
2840 /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "py-1 font-semibold", children: "Command" }) 2841 ] }) }), 2842 /* @__PURE__ */ jsxRuntimeExports.jsxs("tbody", { className: "divide-y divide-zinc-100/50 dark:divide-zinc-800/50", children: [ 2843 /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { children: [ 2844 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "py-1.5", children: /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "px-1.5 py-0.5 rounded bg-zinc-200 dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-700 text-[10px] font-mono", children: "Spacebar" }) }), 2845 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "py-1.5", children: "Play / Pause playback." }) 2846 ] }), 2847 /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { children: [ 2848 /* @__PURE__ */ jsxRuntimeExports.jsxs("td", { className: "py-1.5", children: [
2849 /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "px-1.5 py-0.5 rounded bg-zinc-200 dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-700 text-[10px] font-mono", children: "Ctrl + Space" }), 2850 " or ", 2851 /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "px-1.5 py-0.5 rounded bg-zinc-200 dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-700 text-[10px] font-mono", children: "Numpad 0" }) 2852 ] }), 2853 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "py-1.5", children: "Stop playback and return playhead." }) 2854 ] }), 2855 /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { children: [ 2856 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "py-1.5", children: /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "px-1.5 py-0.5 rounded bg-zinc-200 dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-700 text-[10px] font-mono", children: "Escape" }) }),
2857 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "py-1.5", children: "Deselect all selected clips and notes." }) 2858 ] }), 2859 /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { children: [ 2860 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "py-1.5", children: /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "px-1.5 py-0.5 rounded bg-zinc-200 dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-700 text-[10px] font-mono", children: "Ctrl + A" }) }), 2861 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "py-1.5", children: "Select all clips on the grid canvas." }) 2862 ] }), 2863 /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { children: [ 2864 /* @__PURE__ */ jsxRuntimeExports.jsxs("td", { className: "py-1.5", children: [ 2865 /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "px-1.5 py-0.5 rounded bg-zinc-200 dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-700 text-[10px] font-mono", children: "Del" }), 2866 " or ",
2867 /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "px-1.5 py-0.5 rounded bg-zinc-200 dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-700 text-[10px] font-mono", children: "Backspace" }) 2868 ] }), 2869 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "py-1.5", children: "Delete selected clips (or selected notes)." }) 2870 ] }), 2871 /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { children: [ 2872 /* @__PURE__ */ jsxRuntimeExports.jsxs("td", { className: "py-1.5", children: [ 2873 /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "px-1.5 py-0.5 rounded bg-zinc-200 dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-700 text-[10px] font-mono", children: "Ctrl + C" }), 2874 " / ", 2875 /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "px-1.5 py-0.5 rounded bg-zinc-200 dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-700 text-[10px] font-mono", children: "X" }), 2876 " / ",
2877 /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "px-1.5 py-0.5 rounded bg-zinc-200 dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-700 text-[10px] font-mono", children: "V" }) 2878 ] }), 2879 /* @__PURE__ */ jsxRuntimeExports.jsxs("td", { className: "py-1.5", children: [ 2880 "Copy / Cut / Paste selected clips relative to playhead. ", 2881 !easyMode ? /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-green-500 font-medium", children: "(Pro Only)" }) : /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-500 font-medium", children: "(Locked)" }) 2882 ] }) 2883 ] }), 2884 /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { children: [ 2885 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "py-1.5", children: /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "px-1.5 py-0.5 rounded bg-zinc-200 dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-700 text-[10px] font-mono", children: "Ctrl + B" }) }),
2886 /* @__PURE__ */ jsxRuntimeExports.jsxs("td", { className: "py-1.5", children: [ 2887 "Contiguously duplicate selected clips down the timeline. ", 2888 !easyMode ? /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-green-500 font-medium", children: "(Pro Only)" }) : /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-500 font-medium", children: "(Locked)" }) 2889 ] }) 2890 ] }), 2891 /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { children: [ 2892 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "py-1.5", children: /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "px-1.5 py-0.5 rounded bg-zinc-200 dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-700 text-[10px] font-mono", children: "Ctrl + â / â" }) }), 2893 /* @__PURE__ */ jsxRuntimeExports.jsxs("td", { className: "py-1.5", children: [ 2894 "Nudge selected clips horizontally by snapped grid interval. ", 2895 !easyMode ? /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-green-500 font-medium", children: "(Pro Only)" }) : /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-500 font-medium", children: "(Locked)" }) 2896 ] }) 2897 ] }),
2898 /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { children: [ 2899 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "py-1.5", children: /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "px-1.5 py-0.5 rounded bg-zinc-200 dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-700 text-[10px] font-mono", children: "Shift + â / â" }) }), 2900 /* @__PURE__ */ jsxRuntimeExports.jsxs("td", { className: "py-1.5", children: [ 2901 "Move selected clips vertically across track lanes. ", 2902 !easyMode ? /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-green-500 font-medium", children: "(Pro Only)" }) : /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-500 font-medium", children: "(Locked)" }) 2903 ] }) 2904 ] }), 2905 /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { children: [ 2906 /* @__PURE__ */ jsxRuntimeExports.jsxs("td", { className: "py-1.5", children: [
2907 /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "px-1.5 py-0.5 rounded bg-zinc-200 dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-700 text-[10px] font-mono", children: "Ctrl + Z" }), 2908 " / ", 2909 /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "px-1.5 py-0.5 rounded bg-zinc-200 dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-700 text-[10px] font-mono", children: "Y" }) 2910 ] }), 2911 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "py-1.5", children: "Undo / Redo editor history states." }) 2912 ] }) 2913 ] }) 2914 ] }) 2915 ] }) 2916 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4", children: [ 2917 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [ 2918 /* @__PURE__ */ jsxRuntimeExports.jsx("h4", { className: "font-bold text-zinc-800 dark:text-zinc-100 uppercase tracking-wider text-[10px] border-b border-zinc-100 dark:border-zinc-800 pb-1", children: "ðµ What is the Arrangement View?" }),
2919 /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { children: [ 2920 "The ", 2921 /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { children: "Arrangement View" }), 2922 " (inspired by FL Studio's Playlist) serves as the structural orchestrator for your entire project. Unlike the Piano Roll, which focuses on editing notes inside a single pattern, the Arrangement View allows you to build the overarching flow, verse-chorus transitions, and automated filter sweeps of your song." 2923 ] }) 2924 ] }), 2925 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [ 2926 /* @__PURE__ */ jsxRuntimeExports.jsx("h4", { className: "font-bold text-zinc-800 dark:text-zinc-100 uppercase tracking-wider text-[10px] border-b border-zinc-100 dark:border-zinc-800 pb-1", children: "ð£ï¸ Multi-Pattern Track Lanes & Grouping" }), 2927 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { children: "Track lanes in this playlist are fully generic. A single track can contain clips from different MIDI Patterns (e.g. lead melody, basslines, and drum beats)." }),
2928 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { children: "Each track lane has dynamic controls:" }), 2929 /* @__PURE__ */ jsxRuntimeExports.jsxs("ul", { className: "list-disc pl-5 space-y-1", children: [ 2930 /* @__PURE__ */ jsxRuntimeExports.jsxs("li", { children: [ 2931 /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { children: "Mute / Solo:" }), 2932 " Toggle audible states to solo a specific line or mute distractions." 2933 ] }), 2934 /* @__PURE__ */ jsxRuntimeExports.jsxs("li", { children: [ 2935 /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { children: "Volume & Pan Sliders:" }), 2936 " Customize audio mix settings directly for each track lane." 2937 ] }), 2938 /* @__PURE__ */ jsxRuntimeExports.jsxs("li", { children: [ 2939 /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { children: "Sub-tracks & Grouping:" }), 2940 " Drag or toggle tracks to form collapsible parent/child groups. Click the collapse arrow to clean up large projects and hide secondary automation lines." 2941 ] }) 2942 ] }) 2943 ] }),
2944 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [ 2945 /* @__PURE__ */ jsxRuntimeExports.jsx("h4", { className: "font-bold text-zinc-800 dark:text-zinc-100 uppercase tracking-wider text-[10px] border-b border-zinc-100 dark:border-zinc-800 pb-1", children: "ð¹ Integration with the Piano Roll" }), 2946 /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { children: [ 2947 "Any clip you place in the grid points to a parent ", 2948 /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { children: "Pattern" }), 2949 ". Double-clicking any pattern clip immediately opens its MIDI data inside the Piano Roll editor below, letting you fine-tune its notes." 2950 ] }), 2951 /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { children: [ 2952 "If you want to edit a clip independently without changing other clips of the same pattern, use ", 2953 /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "px-1.5 py-0.5 rounded bg-zinc-200 dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-700 text-[10px] font-mono", children: "Alt + Right-Click" }), 2954 " (in Pro Mode) and select ",
2955 /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { children: "Make Unique" }), 2956 ". This duplicates the pattern under a new name, letting you edit its notes separately." 2957 ] }) 2958 ] }) 2959 ] }) }), 2960 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "p-3 border-t border-zinc-200 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-950 flex items-center justify-end", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 2961 "button", 2962 { 2963 onClick: () => setIsHelpOpen(false), 2964 className: "px-4 py-1.5 bg-zinc-800 hover:bg-zinc-700 dark:bg-zinc-800 dark:hover:bg-zinc-700 text-zinc-100 rounded-xl text-xs font-bold transition-all shadow-sm", 2965 children: "Got it" 2966 } 2967 ) }) 2968 ] 2969 } 2970 ) 2971 } 2972 ) 2973 ] }); 2974}; 2975const TrackInspector = () => { 2976 const { t } = useTranslation(); 2977 const { engine, activeTrackIndex, forceSync, selectedNoteIds, updateNote, selectedTempoIndex, selectedTimeSigIndex } = useEditor(); 2978 const track = engine && engine.midi && activeTrackIndex !== null ? engine.midi.tracks[activeTrackIndex] : null; 2979 const ppq = engine && engine.midi ? engine.midi.header.ppq : 480; 2980 const [selectedCategory, setSelectedCategory] = reactExports.useState("Piano"); 2981 reactExports.useEffect(() => { 2982 if (!track) return; 2983 const isPerc = track.channel === 9; 2984 if (isPerc) { 2985 setSelectedCategory("Percussion"); 2986 } else { 2987 const cat = INSTRUMENT_CATEGORIES.find((c) => track.instrument.number >= c.start && track.instrument.number <= c.end); 2988 if (cat) setSelectedCategory(cat.name); 2989 } 2990 }, [track == null ? void 0 : track.channel, track == null ? void 0 : track.instrument.number, activeTrackIndex]); 2991 if (!engine || !engine.midi || activeTrackIndex === null || !track) { 2992 return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "p-4 text-sm text-zinc-500", children: t("editor.inspector.selectTrack", "Select a track to edit properties.") }); 2993 } 2994 const handleCategoryChange = (e) => { 2995 const newCat = e.target.value; 2996 setSelectedCategory(newCat); 2997 if (newCat === "Percussion") { 2998 engine.updateTrackChannel(activeTrackIndex, 9); 2999 forceSync(); 3000 } else { 3001 if (track.channel === 9) { 3002 engine.updateTrackChannel(activeTrackIndex, 0); 3003 } 3004 const catInfo = INSTRUMENT_CATEGORIES.find((c) => c.name === newCat); 3005 if (catInfo) { 3006 engine.updateTrackInstrument(activeTrackIndex, catInfo.start); 3007 forceSync(); 3008 } 3009 } 3010 }; 3011 const handleInstrumentChange = (e) => { 3012 engine.updateTrackInstrument(activeTrackIndex, parseInt(e.target.value, 10)); 3013 forceSync(); 3014 }; 3015 const handleNameChange = (e) => { 3016 engine.updateTrackName(activeTrackIndex, e.target.value); 3017 forceSync(); 3018 }; 3019 const handleChannelChange = (e) => { 3020 engine.updateTrackChannel(activeTrackIndex, parseInt(e.target.value, 10)); 3021 forceSync(); 3022 }; 3023 const handleNoteChange = (field, val) => { 3024 if (selectedNoteIds.length > 0 && !isNaN(val)) { 3025 selectedNoteIds.forEach((idx) => updateNote(activeTrackIndex, idx, { [field]: val })); 3026 forceSync(); 3027 } 3028 }; 3029 const handleNoteMBBChange = (val) => { 3030 if (engine && selectedNoteIds.length > 0) { 3031 const ticks = engine.mbbToTicks(val); 3032 selectedNoteIds.forEach((idx) => updateNote(activeTrackIndex, idx, { ticks })); 3033 forceSync(); 3034 } 3035 }; 3036 const handleTempoChange = (field, val) => { 3037 if (engine && engine.midi && selectedTempoIndex !== null) { 3038 const t2 = engine.midi.header.tempos[selectedTempoIndex]; 3039 if (!t2) return; 3040 const newTicks = field === "ticks" ? val : t2.ticks; 3041 const newBpm = field === "bpm" ? val : t2.bpm; 3042 engine.updateTempo(t2.ticks, newTicks, newBpm); 3043 forceSync(); 3044 } 3045 }; 3046 const handleTempoMBBChange = (val) => { 3047 if (engine && engine.midi && selectedTempoIndex !== null) { 3048 const t2 = engine.midi.header.tempos[selectedTempoIndex]; 3049 if (!t2) return; 3050 const newTicks = engine.mbbToTicks(val); 3051 engine.updateTempo(t2.ticks, newTicks, t2.bpm); 3052 forceSync(); 3053 } 3054 }; 3055 const handleTimeSigChange = (field, val) => { 3056 if (engine && engine.midi && selectedTimeSigIndex !== null) { 3057 const ts = engine.midi.header.timeSignatures[selectedTimeSigIndex]; 3058 if (!ts) return; 3059 const newTicks = field === "ticks" ? val : ts.ticks; 3060 const num = field === "numerator" ? val : ts.timeSignature[0]; 3061 const den = field === "denominator" ? val : ts.timeSignature[1]; 3062 engine.updateTimeSignature(ts.ticks, newTicks, num, den); 3063 forceSync(); 3064 } 3065 }; 3066 const handleTimeSigMBBChange = (val) => { 3067 if (engine && engine.midi && selectedTimeSigIndex !== null) { 3068 const ts = engine.midi.header.timeSignatures[selectedTimeSigIndex]; 3069 if (!ts) return; 3070 const newTicks = engine.mbbToTicks(val); 3071 engine.updateTimeSignature(ts.ticks, newTicks, ts.timeSignature[0], ts.timeSignature[1]); 3072 forceSync(); 3073 } 3074 }; 3075 const categoryInfo = INSTRUMENT_CATEGORIES.find((c) => c.name === selectedCategory); 3076 let instrumentOptions = []; 3077 if (categoryInfo) { 3078 for (let i = categoryInfo.start; i <= categoryInfo.end; i++) { 3079 instrumentOptions.push({ val: i, label: `${instance.t(`instruments.${i}`, { defaultValue: GM_INSTRUMENTS[i] })} (${i})` }); 3080 } 3081 } 3082 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-4 pb-24 flex flex-col gap-4", children: [
3083 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 3084 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center mb-1", children: /* @__PURE__ */ jsxRuntimeExports.jsx(HelpTip, { text: t("editor.inspector.trackNameHelp", "The display name for this track."), position: "right", children: /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-xs font-bold text-zinc-500 cursor-help", children: t("editor.inspector.trackName", "Track Name") }) }) }), 3085 /* @__PURE__ */ jsxRuntimeExports.jsx( 3086 "input", 3087 { 3088 type: "text", 3089 value: track.name, 3090 onChange: handleNameChange, 3091 onFocus: (e) => e.currentTarget.select(), 3092 onDoubleClick: (e) => e.currentTarget.select(), 3093 className: "w-full bg-zinc-100 dark:bg-zinc-950 border border-zinc-300 dark:border-zinc-800 rounded px-2 py-1.5 text-sm text-zinc-900 dark:text-white focus:outline-none focus:border-indigo-500 transition-colors" 3094 } 3095 ) 3096 ] }), 3097 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-zinc-50 dark:bg-zinc-900/50 border border-zinc-200 dark:border-zinc-800 rounded-md p-3", children: [
3098 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center mb-1", children: /* @__PURE__ */ jsxRuntimeExports.jsx(HelpTip, { text: t("editor.inspector.categoryHelp", "The instrument group for this track. Selecting Percussion switches the track to Channel 10."), position: "right", children: /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-xs font-bold text-zinc-500 cursor-help", children: t("editor.inspector.category", "Category") }) }) }), 3099 /* @__PURE__ */ jsxRuntimeExports.jsxs( 3100 "select", 3101 { 3102 value: selectedCategory, 3103 onChange: handleCategoryChange, 3104 className: "w-full bg-zinc-100 dark:bg-zinc-950 border border-zinc-300 dark:border-zinc-800 rounded px-2 py-1.5 text-sm text-zinc-900 dark:text-white focus:outline-none focus:border-indigo-500 transition-colors", 3105 children: [ 3106 INSTRUMENT_CATEGORIES.map((c) => /* @__PURE__ */ jsxRuntimeExports.jsxs("option", { value: c.name, children: [ 3107 c.name, 3108 " (", 3109 c.start, 3110 "-", 3111 c.end, 3112 ")" 3113 ] }, c.name)),
3114 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "Percussion", children: t("editor.inspector.drumKitPercussion", "Drum Kit / Percussion") }) 3115 ] 3116 } 3117 ), 3118 selectedCategory !== "Percussion" && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 3119 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center mb-1 mt-3", children: /* @__PURE__ */ jsxRuntimeExports.jsx(HelpTip, { text: t("editor.inspector.instrumentHelp", "The specific General MIDI instrument used for playback."), position: "right", children: /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-xs font-bold text-zinc-500 cursor-help", children: t("editor.inspector.instrument", "Instrument") }) }) }), 3120 /* @__PURE__ */ jsxRuntimeExports.jsx( 3121 "select", 3122 { 3123 value: track.instrument.number, 3124 onChange: handleInstrumentChange, 3125 className: "w-full bg-zinc-100 dark:bg-zinc-950 border border-zinc-300 dark:border-zinc-800 rounded px-2 py-1.5 text-sm text-zinc-900 dark:text-white focus:outline-none focus:border-indigo-500 transition-colors", 3126 children: instrumentOptions.map((opt) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: opt.val, children: opt.label }, opt.val)) 3127 } 3128 ) 3129 ] }), 3130 selectedCategory === "Percussion" && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs text-zinc-500 italic mt-2", children: t("editor.inspector.drumKitInfo", "Drum Kits use Channel 10. Draw notes in the Piano Roll to trigger different drum sounds!") }) 3131 ] }), 3132 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
3133 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center mb-1", children: /* @__PURE__ */ jsxRuntimeExports.jsx(HelpTip, { text: t("editor.inspector.channelHelp", "MIDI Channel. Channel 10 (index 9) is reserved for Percussion."), position: "right", children: /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-xs font-bold text-zinc-500 cursor-help", children: t("editor.inspector.channel", "Channel (0-15)") }) }) }), 3134 /* @__PURE__ */ jsxRuntimeExports.jsx( 3135 "input", 3136 { 3137 type: "number", 3138 value: track.channel, 3139 onChange: handleChannelChange, 3140 onFocus: (e) => e.currentTarget.select(), 3141 onDoubleClick: (e) => e.currentTarget.select(), 3142 className: "w-full bg-zinc-100 dark:bg-zinc-950 border border-zinc-300 dark:border-zinc-800 rounded px-2 py-1.5 text-sm text-zinc-900 dark:text-white focus:outline-none focus:border-indigo-500 transition-colors", 3143 min: "0", 3144 max: "15" 3145 } 3146 ) 3147 ] }), 3148 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-4 border-t border-zinc-200 dark:border-zinc-800 pt-4", children: [
3149 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between items-center mb-2", children: [ 3150 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center", children: /* @__PURE__ */ jsxRuntimeExports.jsx(HelpTip, { text: t("editor.inspector.eventPropertiesHelp", "Properties of the currently selected notes or events."), position: "right", children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm font-bold cursor-help", children: t("editor.inspector.eventProperties", "Event Properties") }) }) }), 3151 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs bg-zinc-200 dark:bg-zinc-800 px-2 py-1 rounded-full text-zinc-600 dark:text-zinc-400", children: selectedTempoIndex !== null ? t("editor.inspector.tempo", "Tempo") : selectedTimeSigIndex !== null ? t("editor.inspector.timeSignature", "Time Signature") : `${track.notes.length} ${t("editor.inspector.total", "Total")}` }) 3152 ] }), 3153 selectedTempoIndex !== null && engine.midi.header.tempos[selectedTempoIndex] ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-2 gap-3 mt-3", children: [
3154 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "col-span-2", children: [ 3155 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-[10px] uppercase font-bold text-zinc-500 mb-1", children: t("editor.inspector.bpm", "BPM") }), 3156 /* @__PURE__ */ jsxRuntimeExports.jsx( 3157 "input", 3158 { 3159 type: "number", 3160 value: Math.round(engine.midi.header.tempos[selectedTempoIndex].bpm), 3161 onChange: (e) => handleTempoChange("bpm", parseInt(e.target.value, 10)), 3162 onFocus: (e) => e.currentTarget.select(), 3163 onDoubleClick: (e) => e.currentTarget.select(), 3164 className: "w-full bg-zinc-100 dark:bg-zinc-950 border border-zinc-300 dark:border-zinc-800 rounded px-2 py-1 text-sm text-zinc-900 dark:text-white focus:outline-none focus:border-indigo-500 transition-colors", 3165 min: "10", 3166 max: "300" 3167 } 3168 ) 3169 ] }), 3170 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
3171 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-[10px] uppercase font-bold text-zinc-500 mb-1", children: t("editor.inspector.startTicks", "Start (Ticks)") }), 3172 /* @__PURE__ */ jsxRuntimeExports.jsx( 3173 "input", 3174 { 3175 type: "number", 3176 value: engine.midi.header.tempos[selectedTempoIndex].ticks, 3177 onChange: (e) => handleTempoChange("ticks", parseInt(e.target.value, 10)), 3178 onFocus: (e) => e.currentTarget.select(), 3179 onDoubleClick: (e) => e.currentTarget.select(), 3180 className: "w-full bg-zinc-100 dark:bg-zinc-950 border border-zinc-300 dark:border-zinc-800 rounded px-2 py-1 text-sm text-zinc-900 dark:text-white focus:outline-none focus:border-indigo-500 transition-colors", 3181 min: "0" 3182 } 3183 ) 3184 ] }), 3185 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 3186 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-[10px] uppercase font-bold text-zinc-500 mb-1", children: t("editor.inspector.startMBT", "Start (M:B:T)") }),
3187 /* @__PURE__ */ jsxRuntimeExports.jsx( 3188 "input", 3189 { 3190 type: "text", 3191 value: engine.ticksToMBB(engine.midi.header.tempos[selectedTempoIndex].ticks), 3192 onBlur: (e) => handleTempoMBBChange(e.target.value), 3193 onKeyDown: (e) => e.key === "Enter" && handleTempoMBBChange(e.currentTarget.value), 3194 onFocus: (e) => e.currentTarget.select(), 3195 onDoubleClick: (e) => e.currentTarget.select(), 3196 className: "w-full bg-zinc-100 dark:bg-zinc-950 border border-zinc-300 dark:border-zinc-800 rounded px-2 py-1 text-sm text-zinc-900 dark:text-white focus:outline-none focus:border-indigo-500 transition-colors" 3197 } 3198 ) 3199 ] }) 3200 ] }) : selectedTimeSigIndex !== null && engine.midi.header.timeSignatures[selectedTimeSigIndex] ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-2 gap-3 mt-3", children: [ 3201 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
3202 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-[10px] uppercase font-bold text-zinc-500 mb-1", children: t("editor.inspector.numerator", "Numerator") }), 3203 /* @__PURE__ */ jsxRuntimeExports.jsx( 3204 "input", 3205 { 3206 type: "number", 3207 value: engine.midi.header.timeSignatures[selectedTimeSigIndex].timeSignature[0], 3208 onChange: (e) => handleTimeSigChange("numerator", parseInt(e.target.value, 10)), 3209 onFocus: (e) => e.currentTarget.select(), 3210 onDoubleClick: (e) => e.currentTarget.select(), 3211 className: "w-full bg-zinc-100 dark:bg-zinc-950 border border-zinc-300 dark:border-zinc-800 rounded px-2 py-1 text-sm text-zinc-900 dark:text-white focus:outline-none focus:border-indigo-500 transition-colors", 3212 min: "1", 3213 max: "32" 3214 } 3215 ) 3216 ] }), 3217 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 3218 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-[10px] uppercase font-bold text-zinc-500 mb-1", children: t("editor.inspector.denominator", "Denominator") }),
3219 /* @__PURE__ */ jsxRuntimeExports.jsxs( 3220 "select", 3221 { 3222 value: engine.midi.header.timeSignatures[selectedTimeSigIndex].timeSignature[1], 3223 onChange: (e) => handleTimeSigChange("denominator", parseInt(e.target.value, 10)), 3224 className: "w-full bg-zinc-100 dark:bg-zinc-950 border border-zinc-300 dark:border-zinc-800 rounded px-2 py-1 text-sm text-zinc-900 dark:text-white focus:outline-none focus:border-indigo-500 transition-colors", 3225 children: [ 3226 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "2", children: "2" }), 3227 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "4", children: "4" }), 3228 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "8", children: "8" }), 3229 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "16", children: "16" }) 3230 ] 3231 } 3232 ) 3233 ] }), 3234 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
3235 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-[10px] uppercase font-bold text-zinc-500 mb-1", children: t("editor.inspector.startTicks", "Start (Ticks)") }), 3236 /* @__PURE__ */ jsxRuntimeExports.jsx( 3237 "input", 3238 { 3239 type: "number", 3240 value: engine.midi.header.timeSignatures[selectedTimeSigIndex].ticks, 3241 onChange: (e) => handleTimeSigChange("ticks", parseInt(e.target.value, 10)), 3242 onFocus: (e) => e.currentTarget.select(), 3243 onDoubleClick: (e) => e.currentTarget.select(), 3244 className: "w-full bg-zinc-100 dark:bg-zinc-950 border border-zinc-300 dark:border-zinc-800 rounded px-2 py-1 text-sm text-zinc-900 dark:text-white focus:outline-none focus:border-indigo-500 transition-colors", 3245 min: "0" 3246 } 3247 ) 3248 ] }), 3249 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 3250 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-[10px] uppercase font-bold text-zinc-500 mb-1", children: t("editor.inspector.startMBT", "Start (M:B:T)") }),
3251 /* @__PURE__ */ jsxRuntimeExports.jsx( 3252 "input", 3253 { 3254 type: "text", 3255 value: engine.ticksToMBB(engine.midi.header.timeSignatures[selectedTimeSigIndex].ticks), 3256 onBlur: (e) => handleTimeSigMBBChange(e.target.value), 3257 onKeyDown: (e) => e.key === "Enter" && handleTimeSigMBBChange(e.currentTarget.value), 3258 onFocus: (e) => e.currentTarget.select(), 3259 onDoubleClick: (e) => e.currentTarget.select(), 3260 className: "w-full bg-zinc-100 dark:bg-zinc-950 border border-zinc-300 dark:border-zinc-800 rounded px-2 py-1 text-sm text-zinc-900 dark:text-white focus:outline-none focus:border-indigo-500 transition-colors" 3261 } 3262 ) 3263 ] }) 3264 ] }) : selectedNoteIds.length > 0 && track.notes[selectedNoteIds[0]] ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-2 gap-3 mt-3", children: [ 3265 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 3266 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-[10px] uppercase font-bold text-zinc-500 mb-1", children: t("editor.inspector.pitch", "Pit
3266ch (0-127)") }), 3267 /* @__PURE__ */ jsxRuntimeExports.jsx( 3268 "input", 3269 { 3270 type: "number", 3271 value: track.notes[selectedNoteIds[0]].midi, 3272 onChange: (e) => handleNoteChange("midi", parseInt(e.target.value, 10)), 3273 onFocus: (e) => e.currentTarget.select(), 3274 onDoubleClick: (e) => e.currentTarget.select(), 3275 className: "w-full bg-zinc-100 dark:bg-zinc-950 border border-zinc-300 dark:border-zinc-800 rounded px-2 py-1 text-sm text-zinc-900 dark:text-white focus:outline-none focus:border-indigo-500 transition-colors" 3276 } 3277 ) 3278 ] }), 3279 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 3280 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-[10px] uppercase font-bold text-zinc-500 mb-1", children: t("editor.inspector.velocity", "Velocity (0-127)") }), 3281 /* @__PURE__ */ jsxRuntimeExports.jsx( 3282 "input", 3283 { 3284 type: "number", 3285 value: Math.round(track.notes[selectedNoteIds[0]].velocity * 127), 3286 onChange: (e) => handleNoteChange("velocity", parseInt(e.target.value, 10) / 127), 3287 onFocus: (e) => e.currentTarget.select(), 3288 onDoubleClick: (e) => e.currentTarget.select(), 3289 className: "w-full bg-zinc-100 dark:bg-zinc-950 border border-zinc-300 dark:border-zinc-800 rounded px-2 py-1 text-sm text-zinc-900 dark:text-white focus:outline-none focus:border-indigo-500 transition-colors", 3290 min: "0", 3291 max: "127" 3292 } 3293 ) 3294 ] }),
3295 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 3296 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-[10px] uppercase font-bold text-zinc-500 mb-1", children: t("editor.inspector.startTicks", "Start (Ticks)") }), 3297 /* @__PURE__ */ jsxRuntimeExports.jsx( 3298 "input", 3299 { 3300 type: "number", 3301 value: track.notes[selectedNoteIds[0]].ticks, 3302 onChange: (e) => handleNoteChange("ticks", parseInt(e.target.value, 10)), 3303 onFocus: (e) => e.currentTarget.select(), 3304 onDoubleClick: (e) => e.currentTarget.select(), 3305 className: "w-full bg-zinc-100 dark:bg-zinc-950 border border-zinc-300 dark:border-zinc-800 rounded px-2 py-1 text-sm text-zinc-900 dark:text-white focus:outline-none focus:border-indigo-500 transition-colors", 3306 min: "0" 3307 } 3308 ) 3309 ] }), 3310 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 3311 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-[10px] uppercase font-bold text-zinc-500 mb-1", children: t("editor.inspector.startMBT", "Start (M:B:T)") }),
3312 /* @__PURE__ */ jsxRuntimeExports.jsx( 3313 "input", 3314 { 3315 type: "text", 3316 value: engine.ticksToMBB(track.notes[selectedNoteIds[0]].ticks), 3317 onBlur: (e) => handleNoteMBBChange(e.target.value), 3318 onKeyDown: (e) => e.key === "Enter" && handleNoteMBBChange(e.currentTarget.value), 3319 onFocus: (e) => e.currentTarget.select(), 3320 onDoubleClick: (e) => e.currentTarget.select(), 3321 className: "w-full bg-zinc-100 dark:bg-zinc-950 border border-zinc-300 dark:border-zinc-800 rounded px-2 py-1 text-sm text-zinc-900 dark:text-white focus:outline-none focus:border-indigo-500 transition-colors" 3322 } 3323 ) 3324 ] }), 3325 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "col-span-2", children: [ 3326 /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "flex justify-between items-center text-[10px] uppercase font-bold text-zinc-500 mb-1", children: [ 3327 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("editor.inspector.durationTicks", "Duration (Ticks)") }),
3328 /* @__PURE__ */ jsxRuntimeExports.jsxs( 3329 "select", 3330 { 3331 className: "bg-transparent border-none text-[10px] text-zinc-400 hover:text-indigo-500 cursor-pointer focus:outline-none", 3332 onChange: (e) => { 3333 if (e.target.value) { 3334 handleNoteChange("durationTicks", ppq * (4 / Number(e.target.value))); 3335 e.target.value = ""; 3336 } 3337 }, 3338 value: "", 3339 children: [ 3340 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "", disabled: true, children: t("editor.inspector.snapTo", "Snap To...") }), 3341 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "1", children: "1/1" }), 3342 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "2", children: "1/2" }), 3343 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "4", children: "1/4" }), 3344 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "8", children: "1/8" }),
3345 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "16", children: "1/16" }), 3346 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "32", children: "1/32" }) 3347 ] 3348 } 3349 ) 3350 ] }), 3351 /* @__PURE__ */ jsxRuntimeExports.jsx( 3352 "input", 3353 { 3354 type: "number", 3355 value: track.notes[selectedNoteIds[0]].durationTicks, 3356 onChange: (e) => handleNoteChange("durationTicks", parseInt(e.target.value, 10)), 3357 onFocus: (e) => e.currentTarget.select(), 3358 onDoubleClick: (e) => e.currentTarget.select(), 3359 className: "w-full bg-zinc-100 dark:bg-zinc-950 border border-zinc-300 dark:border-zinc-800 rounded px-2 py-1 text-sm text-zinc-900 dark:text-white focus:outline-none focus:border-indigo-500 transition-colors", 3360 min: "1" 3361 } 3362 ) 3363 ] }) 3364 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs text-zinc-500 italic mt-2", children: t("editor.inspector.selectEvent", "Select an event (Note, Tempo, Time Sig) to edit its properties.") }) 3365 ] }) 3366 ] }); 3367}; 3368const TrackMeter = ({ channel, isPlaying, engine }) => { 3369 const [levelL, setLevelL] = React.useState(0); 3370 const [levelR, setLevelR] = React.useState(0); 3371 const lastLevelLRef = React.useRef(0); 3372 const lastLevelRRef = React.useRef(0); 3373 React.useEffect(() => { 3374 let animId; 3375 const update = () => { 3376 let targetL = 0; 3377 let targetR = 0; 3378 if (isPlaying && engine) { 3379 try { 3380 const activeNotes = engine.getActiveNotes(channel); 3381 if (activeNotes && activeNotes.size > 0) { 3382 targetL = 0.35 + Math.random() * 0.45; 3383 targetR = targetL * (0.95 + Math.random() * 0.1); 3384 } 3385 } catch (e) { 3386 } 3387 } 3388 const curL = lastLevelLRef.current; 3389 const curR = lastLevelRRef.current; 3390 let nextL = targetL > curL ? curL + (targetL - curL) * 0.4 : curL - 0.06; 3391 let nextR = targetR > curR ? curR + (targetR - curR) * 0.4 : curR - 0.06; 3392 nextL = Math.max(0, Math.min(1, nextL)); 3393 nextR = Math.max(0, Math.min(1, nextR)); 3394 lastLevelLRef.current = nextL; 3395 lastLevelRRef.current = nextR; 3396 setLevelL(nextL); 3397 setLevelR(nextR); 3398 animId = requestAnimationFrame(update); 3399 }; 3400 animId = requestAnimationFrame(update); 3401 return () => cancelAnimationFrame(animId); 3402 }, [channel, isPlaying, engine]); 3403 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-[1px] h-32 items-center opacity-85", children: [ 3404 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-[3px] h-full bg-zinc-900 dark:bg-black rounded-sm overflow-hidden relative", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 3405 "div", 3406 { 3407 className: "absolute bottom-0 w-full bg-gradient-to-t from-emerald-500 via-yellow-400 to-red-500 transition-all duration-75", 3408 style: { height: `${levelL * 100}%` } 3409 } 3410 ) }), 3411 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-[3px] h-full bg-zinc-900 dark:bg-black rounded-sm overflow-hidden relative", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 3412 "div", 3413 { 3414 className: "absolute bottom-0 w-full bg-gradient-to-t from-emerald-500 via-yellow-400 to-red-500 transition-all duration-75", 3415 style: { height: `${levelR * 100}%` } 3416 } 3417 ) }) 3418 ] }); 3419}; 3420const MasterMeter = ({ isPlaying, engine }) => { 3421 const [levelL, setLevelL] = React.useState(0); 3422 const [levelR, setLevelR] = React.useState(0); 3423 const analyserRef = React.useRef(null); 3424 const dataArrayRef = React.useRef(null); 3425 React.useEffect(() => { 3426 if (engine && engine.analyser) { 3427 analyserRef.current = engine.analyser; 3428 const bufferLength = engine.analyser.frequencyBinCount; 3429 dataArrayRef.current = new Uint8Array(bufferLength); 3430 } 3431 }, [engine]); 3432 React.useEffect(() => { 3433 let animId; 3434 const update = () => { 3435 let targetL = 0; 3436 let targetR = 0; 3437 if (isPlaying && analyserRef.current && dataArrayRef.current) { 3438 analyserRef.current.getByteTimeDomainData(dataArrayRef.current); 3439 let sum = 0; 3440 for (let i = 0; i < dataArrayRef.current.length; i++) { 3441 const val = (dataArrayRef.current[i] - 128) / 128; 3442 sum += val * val; 3443 } 3444 const rms = Math.sqrt(sum / dataArrayRef.current.length); 3445 targetL = Math.min(1, rms * 4.5); 3446 targetR = targetL * (0.95 + Math.random() * 0.1); 3447 targetR = Math.min(1, targetR); 3448 } 3449 setLevelL((prev) =>
3449 prev + (targetL - prev) * 0.45 - (targetL < prev ? 0.05 : 0)); 3450 setLevelR((prev) => prev + (targetR - prev) * 0.45 - (targetR < prev ? 0.05 : 0)); 3451 animId = requestAnimationFrame(update); 3452 }; 3453 animId = requestAnimationFrame(update); 3454 return () => cancelAnimationFrame(animId); 3455 }, [isPlaying]); 3456 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-[2px] h-36 items-center", children: [ 3457 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-[5px] h-full bg-zinc-900 rounded overflow-hidden relative border border-zinc-800", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 3458 "div", 3459 { 3460 className: "absolute bottom-0 w-full bg-gradient-to-t from-emerald-500 via-yellow-400 to-red-500 transition-all duration-75", 3461 style: { height: `${Math.max(0, Math.min(100, levelL * 100))}%` } 3462 } 3463 ) }), 3464 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-[5px] h-full bg-zinc-900 rounded overflow-hidden relative border border-zinc-800", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 3465 "div", 3466 { 3467 className: "absolute bottom-0 w-full bg-gradient-to-t from-emerald-500 via-yellow-400 to-red-500 transition-all duration-75", 3468 style: { height: `${Math.max(0, Math.min(100, levelR * 100))}%` } 3469 } 3470 ) }) 3471 ] }); 3472}; 3473const PanKnob = ({ value, onChange }) => { 3474 const { t } = useTranslation(); 3475 const handleMouseDown = (e) => { 3476 e.preventDefault(); 3477 const startY = e.clientY; 3478 const startVal = value; 3479 const handleMouseMove = (moveEvent) => { 3480 const deltaY = startY - moveEvent.clientY; 3481 const sensitivity = 0.015; 3482 let newVal = startVal + deltaY * sensitivity; 3483 newVal = Math.max(-1, Math.min(1, newVal)); 3484 onChange(newVal); 3485 }; 3486 const handleMouseUp = () => { 3487 document.removeEventListener("mousemove", handleMouseMove); 3488 document.removeEventListener("mouseup", handleMouseUp); 3489 }; 3490 document.addEventListener("mousemove", handleMouseMove); 3491 document.addEventListener("mouseup", handleMouseUp); 3492 }; 3493 const angle = value * 135; 3494 let label = "C"; 3495 if (value < -0.05) { 3496 label = `L${Math.round(Math.abs(value) * 100)}`; 3497 } else if (value > 0.05) { 3498 label = `R${Math.round(value * 100)}`; 3499 } 3500 return /* @__PURE__ */ jsxRuntimeExports.jsx(HelpTip, { text: t("editor.mixer.panHelp", "Pan. Drag up/down to adjust."), position: "top", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 3501 "div", 3502 { 3503 className: "flex flex-col items-center cursor-ns-resize group relative", 3504 onMouseDown: handleMouseDown, 3505 onDoubleClick: () => onChange(0), 3506 children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-8 h-8 rounded-full bg-zinc-100 dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-800 shadow flex items-center justify-center relative select-none", children: [
3507 /* @__PURE__ */ jsxRuntimeExports.jsx( 3508 "div", 3509 { 3510 className: "w-1.5 h-1.5 bg-indigo-500 rounded-full absolute top-1 transition-transform origin-[center_12px]", 3511 style: { transform: `rotate(${angle}deg)` } 3512 } 3513 ), 3514 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[7px] text-zinc-500 dark:text-zinc-400 group-hover:text-indigo-500 dark:group-hover:text-indigo-400 font-mono font-bold mt-2", children: label }) 3515 ] }) 3516 } 3517 ) }); 3518}; 3519const VolumeFader = ({ value, onChange }) => { 3520 const { t } = useTranslation(); 3521 const handleMouseDown = (e) => { 3522 e.preventDefault(); 3523 const startY = e.clientY; 3524 const startVal = value; 3525 const handleMouseMove = (moveEvent) => { 3526 const deltaY = startY - moveEvent.clientY; 3527 const sensitivity = 8e-3; 3528 let newVal = startVal + deltaY * sensitivity; 3529 newVal = Math.max(0, Math.min(1.2, newVal)); 3530 onChange(newVal); 3531 }; 3532 const handleMouseUp = () => { 3533 document.removeEventListener("mousemove", handleMouseMove); 3534 document.removeEventListener("mouseup", handleMouseUp); 3535 }; 3536 document.addEventListener("mousemove", handleMouseMove); 3537 document.addEventListener("mouseup", handleMouseUp); 3538 }; 3539 const percentage = Math.min(100, value / 1.2 * 100); 3540 value > 0 ? (20 * Math.log10(value)).toFixed(1) : "-inf"; 3541 return /* @__PURE__ */ jsxRuntimeExports.jsx(HelpTip, { text: t("editor.mixer.volumeHelp", "Volume. Drag to adjust."), position: "right", children: /* @__PURE__ */ jsxRuntimeExports.jsxs( 3542 "div", 3543 { 3544 className: "w-6 h-32 bg-zinc-200 dark:bg-zinc-950 rounded relative border border-zinc-300 dark:border-zinc-800 shadow-inner flex justify-center cursor-ns-resize", 3545 onMouseDown: handleMouseDown, 3546 onDoubleClick: () => onChange(0.8), 3547 children: [
3548 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute top-0 bottom-0 w-[2px] bg-zinc-300 dark:bg-zinc-800" }), 3549 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute left-0 right-0 top-1/4 h-[1px] bg-zinc-300 dark:bg-zinc-800" }), 3550 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute left-0 right-0 top-2/4 h-[1px] bg-zinc-300 dark:bg-zinc-800" }), 3551 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute left-0 right-0 top-3/4 h-[1px] bg-zinc-300 dark:bg-zinc-800" }), 3552 /* @__PURE__ */ jsxRuntimeExports.jsx( 3553 "div", 3554 { 3555 className: "absolute w-5 h-5 bg-zinc-100 dark:bg-zinc-700 hover:bg-white dark:hover:bg-zinc-600 rounded shadow-md border border-zinc-400 dark:border-zinc-800 flex flex-col justify-center items-center z-10", 3556 style: { bottom: `calc(${percentage}% - 10px)` }, 3557 children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-full h-[2px] bg-indigo-500" }) 3558 } 3559 ) 3560 ] 3561 } 3562 ) }); 3563}; 3564const Mixer = () => { 3565 const { t } = useTranslation(); 3566 const { 3567 engine, 3568 activeTrackIndex, 3569 setActiveTrackIndex, 3570 playlistTracks, 3571 setTrackVolume, 3572 setTrackPanning, 3573 toggleTrackMute, 3574 toggleTrackSolo, 3575 masterVolume, 3576 setMasterVolume, 3577 isPlaying 3578 } = useEditor(); 3579 if (!engine || !engine.midi) return null; 3580 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "h-full flex gap-1.5 p-2 bg-zinc-50 dark:bg-zinc-900 border-t border-zinc-200 dark:border-zinc-800 overflow-x-auto select-none", children: [ 3581 playlistTracks.map((track, idx) => { 3582 var _a; 3583 const midiTrack = engine.midi.tracks[idx]; 3584 const instName = ((_a = midiTrack == null ? void 0 : midiTrack.instrument) == null ? void 0 : _a.name) || "Synth"; 3585 const channel = (midiTrack == null ? void 0 : midiTrack.channel) ?? idx % 16; 3586 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 3587 "div", 3588 { 3589 className: `w-24 flex flex-col border rounded-lg overflow-hidden transition-all duration-150 cursor-pointer ${activeTrackIndex === idx ? "bg-indigo-50/20 dark:bg-indigo-950/10 border-indigo-500/50 shadow-md shadow-indigo-500/5" : "bg-white dark:bg-zinc-950 border-zinc-200 dark:border-zinc-800/80 hover:border-zinc-300 dark:hover:border-zinc-700"}`, 3590 onClick: () => setActiveTrackIndex(idx), 3591 children: [
3592 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `h-7 border-b flex items-center justify-between px-2 text-[10px] font-bold truncate transition-colors ${activeTrackIndex === idx ? "bg-indigo-500 text-white" : "bg-zinc-100 dark:bg-zinc-900 text-zinc-600 dark:text-zinc-400 border-zinc-200 dark:border-zinc-800"}`, children: [ 3593 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "truncate", children: track.name || t("editor.mixer.trackNumber", "Track {{number}}", { number: idx + 1 }) }), 3594 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[8px] opacity-60 font-mono", children: [ 3595 "Ch", 3596 channel + 1 3597 ] }) 3598 ] }), 3599 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex justify-center pt-3 pb-1 border-b border-zinc-100 dark:border-zinc-900/50", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 3600 PanKnob, 3601 { 3602 value: track.panning, 3603 onChange: (val) => setTrackPanning(idx, val) 3604 } 3605 ) }),
3606 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 flex justify-center items-center gap-2 py-4 px-2 bg-zinc-50/50 dark:bg-zinc-950/30", children: [ 3607 /* @__PURE__ */ jsxRuntimeExports.jsx( 3608 VolumeFader, 3609 { 3610 value: track.volume, 3611 onChange: (val) => setTrackVolume(idx, val) 3612 } 3613 ), 3614 /* @__PURE__ */ jsxRuntimeExports.jsx( 3615 TrackMeter, 3616 { 3617 channel, 3618 isPlaying, 3619 engine 3620 } 3621 ) 3622 ] }), 3623 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "h-20 border-t border-zinc-200 dark:border-zinc-800 flex flex-col p-1.5 gap-1.5 bg-zinc-50 dark:bg-zinc-900/40", children: [ 3624 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-1 justify-center items-center", children: [ 3625 /* @__PURE__ */ jsxRuntimeExports.jsx(HelpTip, { text: t("editor.mixer.muteHelp", "Mute: Silence this track"), position: "top", className: "flex-1", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 3626 "button", 3627 { 3628 className: `w-full py-1 text-[10px] font-bold rounded border transition-all duration-150 ${track.isMuted ? "bg-amber-500 border-amber-600 text-white font-black shadow-inner shadow-amber-700/30" : "bg-zinc-200 dark:bg-zinc-800 border-zinc-300 dark:border-zinc-700 text-zinc-500 hover:bg-zinc-300 dark:hover:bg-zinc-700"}`, 3629 onClick: (e) => { 3630 e.stopPropagation(); 3631 toggleTrackMute(idx); 3632 }, 3633 children: "M" 3634 } 3635 ) }),
3636 /* @__PURE__ */ jsxRuntimeExports.jsx(HelpTip, { text: t("editor.mixer.soloHelp", "Solo: Silence all other tracks"), position: "top", className: "flex-1", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 3637 "button", 3638 { 3639 className: `w-full py-1 text-[10px] font-bold rounded border transition-all duration-150 ${track.isSoloed ? "bg-rose-500 border-rose-600 text-white font-black shadow-inner shadow-rose-700/30 animate-pulse" : "bg-zinc-200 dark:bg-zinc-800 border-zinc-300 dark:border-zinc-700 text-zinc-500 hover:bg-zinc-300 dark:hover:bg-zinc-700"}`, 3640 onClick: (e) => { 3641 e.stopPropagation(); 3642 toggleTrackSolo(idx); 3643 }, 3644 children: "S" 3645 } 3646 ) }) 3647 ] }), 3648 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-center text-[9px] font-medium text-zinc-400 dark:text-zinc-500 truncate select-none border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-950 py-0.5 rounded px-1", title: instName, children: instName }) 3649 ] }) 3650 ] 3651 }, 3652 track.id || idx 3653 ); 3654 }), 3655 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-28 ml-3 flex flex-col border border-rose-200 dark:border-rose-950 rounded-lg overflow-hidden bg-rose-50/10 dark:bg-zinc-950/60 shadow-lg shadow-rose-500/5", children: [
3656 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "h-7 bg-rose-500 text-white flex items-center justify-center text-[10px] font-bold uppercase tracking-wider", children: t("editor.mixer.master", "Master") }), 3657 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 flex justify-center items-center gap-3 py-4 px-2", children: [ 3658 /* @__PURE__ */ jsxRuntimeExports.jsx( 3659 VolumeFader, 3660 { 3661 value: masterVolume, 3662 onChange: (val) => setMasterVolume(val) 3663 } 3664 ), 3665 /* @__PURE__ */ jsxRuntimeExports.jsx( 3666 MasterMeter, 3667 { 3668 isPlaying, 3669 engine 3670 } 3671 ) 3672 ] }), 3673 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "h-20 flex flex-col items-center justify-center p-2 bg-rose-500/5 border-t border-rose-200/50 dark:border-rose-950/50", children: [ 3674 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[8px] font-mono uppercase text-rose-500 font-bold tracking-widest", children: t("editor.mixer.stereoOut", "Stereo Out") }),
3675 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[10px] text-zinc-400 font-medium mt-1", children: [ 3676 Math.round(masterVolume * 100), 3677 "%" 3678 ] }) 3679 ] }) 3680 ] }) 3681 ] }); 3682}; 3683const NOTE_NAMES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]; 3684const ContextMenu = ({ 3685 x, 3686 y, 3687 onClose, 3688 toolMode, 3689 setToolMode, 3690 scaleRoot, 3691 setScaleRoot, 3692 scaleType, 3693 setScaleType, 3694 chordStamp, 3695 setChordStamp, 3696 gridSnap, 3697 setGridSnap, 3698 isDrumMode 3699}) => { 3700 const menuRef = reactExports.useRef(null); 3701 const [activeSubmenu, setActiveSubmenu] = reactExports.useState(null); 3702 const [submenuHoverTimeout, setSubmenuHoverTimeout] = reactExports.useState(null); 3703 const [coords, setCoords] = reactExports.useState({ left: x, top: y }); 3704 const [submenuDirection, setSubmenuDirection] = reactExports.useState("right"); 3705 reactExports.useEffect(() => { 3706 const handleClickOutside = (e) => { 3707 if (menuRef.current && !menuRef.current.contains(e.target)) { 3708 onClose(); 3709 } 3710 }; 3711 document.addEventListener("mousedown", handleClickOutside); 3712 return () => document.removeEventListener("mousedown", handleClickOutside); 3713 }, [onClose]); 3714 reactExports.useEffect(() => { 3715 if (menuRef.current) { 3716 const rect = menuRef.current.getBoundingClientRect(); 3717 const winWidth = window.innerWidth; 3718 const winHeight = window.innerHeight; 3719 let adjustedX = x; 3720 let adjustedY = y; 3721 if (x + rect.width > winWidth) { 3722 adjustedX = winWidth - rect.width - 10; 3723 } 3724 if (y + rect.height > winHeight) { 3725 adjustedY = winHeight - rect.height - 10; 3726 } 3727 if (adjustedX < 10) adjustedX = 10; 3728 if (adjustedY < 10) adjustedY = 10; 3729 setCoords({ left: adjustedX, top: adjustedY }); 3730 if (adjustedX + rect.width + 192 > winWidth) { 3731 setSubmenuDirection("left"); 3732 } else { 3733 setSubmenuDirection("right"); 3734 } 3735 } 3736 }, [x, y]); 3737 const handleMouseEnterSubmenu = (menuName) => { 3738 if (submenuHoverTimeout) clearTimeout(submenuHoverTimeout); 3739 setActiveSubmenu(menuName); 3740 }; 3741 const handleMouseLeaveSubmenu = () => { 3742 const timeout = setTimeout(() => { 3743 setActiveSubmenu(null); 3744 }, 100); 3745 setSubmenuHoverTimeout(timeout); 3746 }; 3747 const handleToolSelect = (mode, e) => { 3748 e.stopPropagation(); 3749 setToolMode(mode); 3750 onClose(); 3751 }; 3752 const renderSnapSubmenu = () => /* @__PURE__ */ jsxRuntimeExports.jsx( 3753 "div", 3754 { 3755 className: `absolute top-0 ${submenuDirection === "left" ? "right-full mr-1" : "left-full ml-1"} w-48 bg-zinc-100 dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-700 shadow-xl py-1 text-sm text-zinc-800 dark:text-zinc-200`, 3756 onMouseEnter: () => handleMouseEnterSubmenu("snap"), 3757 onMouseLeave: handleMouseLeaveSubmenu, 3758 children: [ 3759 { label: "Off", val: 0 }, 3760 { label: "1 Bar (1/1)", val: 1 }, 3761 { label: "1/2", val: 2 }, 3762 { label: "1/4", val: 4 }, 3763 { label: "1/8", val: 8 }, 3764 { label: "1/16", val: 16 }, 3765 { label: "1/32", val: 32 } 3766 ].map((snap) => /* @__PURE__ */ jsxRuntimeExports.jsxs( 3767 "button", 3768 { 3769 className: "w-full text-left px-6 py-1 hover:bg-zinc-200 dark:hover:bg-zinc-700 flex items-center gap-2", 3770 onClick: (e) => { 3771 e.stopPropagation(); 3772 setGridSnap(snap.val); 3773 onClose(); 3774 }, 3775 children: [
3776 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "w-4", children: gridSnap === snap.val && /* @__PURE__ */ jsxRuntimeExports.jsx(Check, { size: 14 }) }), 3777 snap.label 3778 ] 3779 }, 3780 snap.val 3781 )) 3782 } 3783 ); 3784 const renderStampSubmenu = () => /* @__PURE__ */ jsxRuntimeExports.jsxs( 3785 "div", 3786 { 3787 className: `absolute top-0 ${submenuDirection === "left" ? "right-full mr-1" : "left-full ml-1"} w-48 bg-zinc-100 dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-700 shadow-xl py-1 text-sm text-zinc-800 dark:text-zinc-200 max-h-[80vh] overflow-y-auto`, 3788 onMouseEnter: () => handleMouseEnterSubmenu("stamp"), 3789 onMouseLeave: handleMouseLeaveSubmenu, 3790 children: [ 3791 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "px-3 py-1 text-xs font-semibold text-zinc-500 uppercase", children: "Chords" }), 3792 [ 3793 { label: "None", val: "none" }, 3794 { label: "Major", val: "major" }, 3795 { label: "Minor", val: "minor" }, 3796 { label: "Major 7th", val: "maj7" }, 3797 { label: "Minor 7th", val: "min7" }, 3798 { label: "Dominant 7th", val: "dom7" }, 3799 { label: "Diminished", val: "dim" }, 3800 { label: "Augmented", val: "aug" } 3801 ].map((stamp) => /* @__PURE__ */ jsxRuntimeExports.jsxs( 3802 "button", 3803 { 3804 disabled: isDrumMode, 3805 className: "w-full text-left px-6 py-1 hover:bg-zinc-200 dark:hover:bg-zinc-700 flex items-center gap-2 disabled:opacity-50", 3806 onClick: (e) => { 3807 e.stopPropagation(); 3808 setChordStamp(stamp.val); 3809 onClose(); 3810 }, 3811 children: [
3812 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "w-4", children: chordStamp === stamp.val && /* @__PURE__ */ jsxRuntimeExports.jsx(Check, { size: 14 }) }), 3813 stamp.label 3814 ] 3815 }, 3816 stamp.val 3817 )), 3818 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "border-t border-zinc-300 dark:border-zinc-700 my-1" }), 3819 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "px-3 py-1 text-xs font-semibold text-zinc-500 uppercase", children: "Scale Helpers" }), 3820 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-6 py-1 flex items-center gap-2", children: [ 3821 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-500 text-xs w-8", children: "Root:" }), 3822 /* @__PURE__ */ jsxRuntimeExports.jsxs( 3823 "select", 3824 { 3825 value: scaleRoot === null ? "none" : scaleRoot, 3826 onChange: (e) => setScaleRoot(e.target.value === "none" ? null : Number(e.target.value)), 3827 className: "flex-1 bg-zinc-200 dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-600 rounded px-1 py-0.5 text-xs focus:outline-none", 3828 onClick: (e) => e.stopPropagation(), 3829 children: [
3830 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "none", children: "Off" }), 3831 NOTE_NAMES.map((n, i) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: i, children: n }, i)) 3832 ] 3833 } 3834 ) 3835 ] }), 3836 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-6 py-1 pb-2 flex items-center gap-2", children: [ 3837 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-500 text-xs w-8", children: "Type:" }), 3838 /* @__PURE__ */ jsxRuntimeExports.jsxs( 3839 "select", 3840 { 3841 value: scaleType, 3842 onChange: (e) => setScaleType(e.target.value), 3843 disabled: scaleRoot === null, 3844 className: "flex-1 bg-zinc-200 dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-600 rounded px-1 py-0.5 text-xs focus:outline-none disabled:opacity-50", 3845 onClick: (e) => e.stopPropagation(), 3846 children: [ 3847 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "major", children: "Major" }),
3848 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "minor", children: "Minor" }), 3849 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "harmonic_minor", children: "Harmonic Minor" }), 3850 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "melodic_minor", children: "Melodic Minor" }), 3851 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "pentatonic_major", children: "Pentatonic Major" }), 3852 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "pentatonic_minor", children: "Pentatonic Minor" }), 3853 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "blues", children: "Blues" }) 3854 ] 3855 } 3856 ) 3857 ] }) 3858 ] 3859 } 3860 ); 3861 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 3862 "div", 3863 { 3864 ref: menuRef, 3865 className: "fixed z-[100] flex bg-zinc-100 dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-700 shadow-2xl select-none", 3866 style: { left: coords.left, top: coords.top }, 3867 onContextMenu: (e) => e.preventDefault(), 3868 children: [
3869 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col bg-zinc-200 dark:bg-zinc-900 border-r border-zinc-300 dark:border-zinc-700 p-1", children: [ 3870 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[10px] text-zinc-500 uppercase font-semibold text-center mb-1", children: "Mode" }), 3871 /* @__PURE__ */ jsxRuntimeExports.jsx( 3872 "button", 3873 { 3874 onClick: (e) => handleToolSelect("draw", e), 3875 className: `p-2 rounded mb-1 ${toolMode === "draw" ? "bg-zinc-300 dark:bg-zinc-700" : "hover:bg-zinc-300 dark:hover:bg-zinc-800"}`, 3876 title: "Draw", 3877 children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 3878 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `w-1.5 h-1.5 rounded-full ${toolMode === "draw" ? "bg-indigo-500" : "bg-transparent"}` }), 3879 /* @__PURE__ */ jsxRuntimeExports.jsx(Pencil, { size: 18, className: "text-zinc-700 dark:text-zinc-300" }) 3880 ] }) 3881 } 3882 ),
3883 /* @__PURE__ */ jsxRuntimeExports.jsx( 3884 "button", 3885 { 3886 onClick: (e) => handleToolSelect("paint", e), 3887 className: `p-2 rounded mb-1 ${toolMode === "paint" ? "bg-zinc-300 dark:bg-zinc-700" : "hover:bg-zinc-300 dark:hover:bg-zinc-800"}`, 3888 title: "Paint", 3889 children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 3890 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `w-1.5 h-1.5 rounded-full ${toolMode === "paint" ? "bg-indigo-500" : "bg-transparent"}` }), 3891 /* @__PURE__ */ jsxRuntimeExports.jsx(Brush, { size: 18, className: "text-zinc-700 dark:text-zinc-300" }) 3892 ] }) 3893 } 3894 ), 3895 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { disabled: true, className: "p-2 rounded mb-1 opacity-40", title: "Paint (Drum Sequencer Mode) - Not Implemented", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [
3896 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-transparent" }), 3897 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative", children: [ 3898 /* @__PURE__ */ jsxRuntimeExports.jsx(Brush, { size: 18, className: "text-zinc-700 dark:text-zinc-300" }), 3899 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "absolute -top-1 -right-1 text-[10px] font-bold", children: "+" }) 3900 ] }) 3901 ] }) }), 3902 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { disabled: true, className: "p-2 rounded mb-1 opacity-40", title: "Delete - Not Implemented", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 3903 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-transparent" }), 3904 /* @__PURE__ */ jsxRuntimeExports.jsx(VolumeX, { size: 18, className: "text-zinc-700 dark:text-zinc-300" }) 3905 ] }) }), 3906 /* @__P
3906URE__ */ jsxRuntimeExports.jsx( 3907 "button", 3908 { 3909 onClick: (e) => handleToolSelect("slice", e), 3910 className: `p-2 rounded mb-1 ${toolMode === "slice" ? "bg-zinc-300 dark:bg-zinc-700" : "hover:bg-zinc-300 dark:hover:bg-zinc-800"}`, 3911 title: "Slice", 3912 children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 3913 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `w-1.5 h-1.5 rounded-full ${toolMode === "slice" ? "bg-indigo-500" : "bg-transparent"}` }), 3914 /* @__PURE__ */ jsxRuntimeExports.jsx(Scissors, { size: 18, className: "text-zinc-700 dark:text-zinc-300" }) 3915 ] }) 3916 } 3917 ), 3918 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { disabled: true, className: "p-2 rounded mb-1 opacity-40", title: "Select - Not Implemented", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [
3919 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-transparent" }), 3920 /* @__PURE__ */ jsxRuntimeExports.jsx(MousePointer2, { size: 18, className: "text-zinc-700 dark:text-zinc-300" }) 3921 ] }) }), 3922 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { disabled: true, className: "p-2 rounded mb-1 opacity-40", title: "Zoom - Not Implemented", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 3923 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-transparent" }), 3924 /* @__PURE__ */ jsxRuntimeExports.jsx(ZoomIn, { size: 18, className: "text-zinc-700 dark:text-zinc-300" }) 3925 ] }) }), 3926 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { disabled: true, className: "p-2 rounded opacity-40", title: "Playback - Not Implemented", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [
3927 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-transparent" }), 3928 /* @__PURE__ */ jsxRuntimeExports.jsx(Play, { size: 18, className: "text-zinc-700 dark:text-zinc-300" }) 3929 ] }) }) 3930 ] }), 3931 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col py-1 min-w-[220px] text-sm text-zinc-800 dark:text-zinc-200", children: [ 3932 /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { disabled: true, className: "w-full text-left px-4 py-1.5 hover:bg-zinc-200 dark:hover:bg-zinc-700 flex items-center justify-between opacity-50", children: [ 3933 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-2", children: [ 3934 /* @__PURE__ */ jsxRuntimeExports.jsx(Folder, { size: 15, className: "text-zinc-400" }), 3935 "File" 3936 ] }), 3937 /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronRight, { size: 14 }) 3938 ] }), 3939 /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { disabled: true, className: "w-full text-left px-4 py-1.5 hover:bg-zinc-200 dark:hover:bg-zinc-700 flex items-center justify-between opacity-50", children: [
3940 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-2", children: [ 3941 /* @__PURE__ */ jsxRuntimeExports.jsx(SquarePen, { size: 15, className: "text-zinc-400" }), 3942 "Edit" 3943 ] }), 3944 /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronRight, { size: 14 }) 3945 ] }), 3946 /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { disabled: true, className: "w-full text-left px-4 py-1.5 hover:bg-zinc-200 dark:hover:bg-zinc-700 flex items-center justify-between opacity-50", children: [ 3947 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-2", children: [ 3948 /* @__PURE__ */ jsxRuntimeExports.jsx(Wrench, { size: 15, className: "text-zinc-400" }), 3949 "Tools" 3950 ] }), 3951 /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronRight, { size: 14 }) 3952 ] }), 3953 /* @__PURE__ */ jsxRuntimeExports.jsxs( 3954 "div", 3955 { 3956 className: "relative w-full text-left px-4 py-1.5 hover:bg-zinc-200 dark:hover:bg-zinc-700 flex items-center justify-between cursor-pointer", 3957 onMouseEnter: () => handleMouseEnterSubmenu("stamp"), 3958 onMouseLeave: handleMouseLeaveSubmenu, 3959 children: [
3960 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-2", children: [ 3961 /* @__PURE__ */ jsxRuntimeExports.jsx(Music, { size: 15, className: "text-indigo-400" }), 3962 "Stamp" 3963 ] }), 3964 /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronRight, { size: 14 }), 3965 activeSubmenu === "stamp" && renderStampSubmenu() 3966 ] 3967 } 3968 ), 3969 /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { disabled: true, className: "w-full text-left px-4 py-1.5 hover:bg-zinc-200 dark:hover:bg-zinc-700 flex items-center justify-between opacity-50", children: [ 3970 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-2", children: [ 3971 /* @__PURE__ */ jsxRuntimeExports.jsx(Eye, { size: 15, className: "text-zinc-400" }), 3972 "View" 3973 ] }), 3974 /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronRight, { size: 14 }) 3975 ] }),
3976 /* @__PURE__ */ jsxRuntimeExports.jsxs( 3977 "div", 3978 { 3979 className: "relative w-full text-left px-4 py-1.5 hover:bg-zinc-200 dark:hover:bg-zinc-700 flex items-center justify-between cursor-pointer", 3980 onMouseEnter: () => handleMouseEnterSubmenu("snap"), 3981 onMouseLeave: handleMouseLeaveSubmenu, 3982 children: [ 3983 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-2", children: [ 3984 /* @__PURE__ */ jsxRuntimeExports.jsx(Magnet, { size: 15, className: "text-indigo-400" }), 3985 "Snap" 3986 ] }), 3987 /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronRight, { size: 14 }), 3988 activeSubmenu === "snap" && renderSnapSubmenu() 3989 ] 3990 } 3991 ), 3992 /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { disabled: true, className: "w-full text-left px-4 py-1.5 hover:bg-zinc-200 dark:hover:bg-zinc-700 flex items-center justify-between opacity-50", children: [
3993 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-2", children: [ 3994 /* @__PURE__ */ jsxRuntimeExports.jsx(MousePointer2, { size: 15, className: "text-zinc-400" }), 3995 "Select" 3996 ] }), 3997 /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronRight, { size: 14 }) 3998 ] }), 3999 /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { disabled: true, className: "w-full text-left px-4 py-1.5 hover:bg-zinc-200 dark:hover:bg-zinc-700 flex items-center justify-between opacity-50", children: [ 4000 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-2", children: [ 4001 /* @__PURE__ */ jsxRuntimeExports.jsx(Layers, { size: 15, className: "text-zinc-400" }), 4002 "Group" 4003 ] }), 4004 /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronRight, { size: 14 }) 4005 ] }), 4006 /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { disabled: true, className: "w-full text-left px-4 py-1.5 hover:bg-zinc-200 dark:hover:bg-zinc-700 flex items-center justify-between opacity-50", children: [
4007 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-2", children: [ 4008 /* @__PURE__ */ jsxRuntimeExports.jsx(ZoomIn, { size: 15, className: "text-zinc-400" }), 4009 "Zoom" 4010 ] }), 4011 /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronRight, { size: 14 }) 4012 ] }), 4013 /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { disabled: true, className: "w-full text-left px-4 py-1.5 hover:bg-zinc-200 dark:hover:bg-zinc-700 flex items-center justify-between opacity-50", children: [ 4014 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-2", children: [ 4015 /* @__PURE__ */ jsxRuntimeExports.jsx(Clock, { size: 15, className: "text-zinc-400" }), 4016 "Time markers" 4017 ] }), 4018 /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronRight, { size: 14 }) 4019 ] }), 4020 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "border-t border-zinc-300 dark:border-zinc-700 my-1" }),
4021 /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { disabled: true, className: "w-full text-left px-4 py-1.5 hover:bg-zinc-200 dark:hover:bg-zinc-700 flex items-center justify-between opacity-50", children: [ 4022 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-2", children: [ 4023 /* @__PURE__ */ jsxRuntimeExports.jsx(Music, { size: 15, className: "text-zinc-400" }), 4024 "Target channel" 4025 ] }), 4026 /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronRight, { size: 14 }) 4027 ] }), 4028 /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { disabled: true, className: "w-full text-left px-4 py-1.5 hover:bg-zinc-200 dark:hover:bg-zinc-700 flex items-center justify-between opacity-50", children: [ 4029 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-2", children: [ 4030 /* @__PURE__ */ jsxRuntimeExports.jsx(SlidersVertical, { size: 15, className: "text-zinc-400" }), 4031 "Target control" 4032 ] }),
4033 /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronRight, { size: 14 }) 4034 ] }), 4035 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "border-t border-zinc-300 dark:border-zinc-700 my-1" }), 4036 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { disabled: true, className: "w-full text-left px-4 py-1.5 hover:bg-zinc-200 dark:hover:bg-zinc-700 flex items-center justify-between opacity-50", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-2", children: [ 4037 /* @__PURE__ */ jsxRuntimeExports.jsx(Check, { size: 14 }), 4038 " Auto smoothing" 4039 ] }) }), 4040 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { disabled: true, className: "w-full text-left px-4 py-1.5 hover:bg-zinc-200 dark:hover:bg-zinc-700 flex items-center justify-between opacity-50", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-2", children: [ 4041 /* @__PURE__ */ jsxRuntimeExports.jsx(Check, { size: 14 }), 4042 " Preview notes during playback" 4043 ] }) }),
4044 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { disabled: true, className: "w-full text-left px-10 py-1.5 hover:bg-zinc-200 dark:hover:bg-zinc-700 opacity-50", children: "Collapse all grouped tracks" }), 4045 /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { disabled: true, className: "w-full text-left px-10 py-1.5 hover:bg-zinc-200 dark:hover:bg-zinc-700 flex items-center justify-between opacity-50", children: [ 4046 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Center" }), 4047 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-500 text-xs italic", children: "Shift+0" }) 4048 ] }), 4049 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "border-t border-zinc-300 dark:border-zinc-700 my-1" }), 4050 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { disabled: true, className: "w-full text-left px-4 py-1.5 hover:bg-zinc-200 dark:hover:bg-zinc-700 flex items-center justify-between opacity-50", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-2", children: [
4051 /* @__PURE__ */ jsxRuntimeExports.jsx(Check, { size: 14 }), 4052 /* @__PURE__ */ jsxRuntimeExports.jsx(Settings, { size: 15, className: "text-zinc-400 ml-1 inline" }), 4053 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "ml-1", children: "Detached" }) 4054 ] }) }) 4055 ] }) 4056 ] 4057 } 4058 ); 4059}; 4060const MIN_NOTE = 0; 4061const MAX_NOTE = 127; 4062const KEY_HEIGHT = 16; 4063const BEAT_WIDTH = 40; 4064const isBlackKey = (midiNote) => { 4065 const noteClass = midiNote % 12; 4066 return [1, 3, 6, 8, 10].includes(noteClass); 4067}; 4068const noteNames = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]; 4069const getNoteName = (midiNote) => { 4070 const octave = Math.floor(midiNote / 12) - 1; 4071 const name = noteNames[midiNote % 12]; 4072 return `${name}${octave}`; 4073}; 4074const formatTicksToFlTime = (ticks, ppqVal = 96) => { 4075 const beatsPerBar = 4; 4076 const ticksPerBeat = ppqVal; 4077 const ticksPerStep = ppqVal / 4; 4078 const ticksPerBar = beatsPerBar * ticksPerBeat; 4079 const bar = Math.floor(ticks / ticksPerBar) + 1; 4080 const remainingTicksInBar = ticks % ticksPerBar; 4081 const step = Math.floor(remainingTicksInBar / ticksPerStep) + 1; 4082 const tick = Math.floor(remainingTicksInBar % ticksPerStep); 4083 const stepStr = String(step).padStart(2, "0"); 4084 const tickStr = String(tick).padStart(2, "0"); 4085 return `${bar}:${stepStr}:${tickStr}`; 4086}; 4087const formatDurationTicksToFlTime = (ticks, ppqVal = 96) => { 4088 const beatsPerBar = 4; 4089 const ticksPerBeat = ppqVal; 4090 const ticksPerStep = ppqVal / 4; 4091 const ticksPerBar = beatsPerBar * ticksPerBeat; 4092 const bar = Math.floor(ticks / ticksPerBar); 4093 const remainingTicksInBar = ticks % ticksPerBar; 4094 const step = Math.floor(remainingTicksInBar / ticksPerStep); 4095 const tick = Math.floor(remainingTicksInBar % ticksPerStep); 4096 const stepStr = String(step).padStart(2, "0"); 4097 const tickStr = String(tick).padStart(2, "0"); 4098 return `${bar}:${stepStr}:${tickStr}`; 4099}; 4100const PianoRoll = () => { 4101 var _a, _b; 4102 const { engine, activeTrackIndex, setActiveTrackIndex, setActivePatternId, midiVersion, activePattern, addNote, addNotes, updateNote, updateNotes, deleteNote, forceSync, selectedNoteIds, setSelectedNoteIds, deleteSelection, splitNote, setHint, currentTime, loopMarkerTick, setLoopMarkerTick, playbackMode, playlistClips, activePatternId, isPlaying, patterns, activeMidiKeys } = useEditor(); 4103 const [zoomX, setZoomX] = reactExports.useState(1); 4104 const [gridSnap, setGridSnap] = reactExports.useState(16); 4105 const containerRef = reactExports.useRef(null); 4106 const gridRef = reactExports.useRef(null); 4107 const keyboardRef = reactExports.useRef(null); 4108 const [metaModal, setMetaModal] = reactExports.useState(null); 4109 const [metaType, setMetaType] = reactExports.useState("tempo"); 4110 const [metaBpm, setMetaBpm] = reactExports.useState("120"); 4111 const [metaNum, setMetaNum] = reactExports.useState("4"); 4112 const [metaDen, setMetaDen] = reactExports.useState("4"); 4113 const [draggingNoteIdx, setDraggingNoteIdx] = reactExports.useState(null); 4114 const [dragStartPos, setDragStartPos] = reactExports.useState(null); 4115 const [dragStartProps, setDragStartProps] = reactExports.useState(null); 4116 const [dragType, setDragType] = reactExports.useState(null); 4117 const [isShiftDrag, setIsShiftDrag] = reactExports.useState(false); 4118 const [dragConstraint, setDragConstraint] = reactExports.useState("none"); 4119 const dragSelectedNotesRef = reactExports.useRef([]); 4120 const [draggingMeta, setDraggingMeta] = reactExports.useState(null); 4121 const [metaDragTicks, setMetaDragTicks] = reactExports.useState(null); 4122 const [lassoStart, setLassoStart] = reactExports.useState(null); 4123 const [lassoCurrent, setLassoCurrent] = reactExports.useState(null); 4124 const [contextMenuPos, setContextMenuPos] = reactExports.useState(null); 4125 const draggedRef = reactExports.useRef(false); 4126 const [sliceStart, setSliceStart] = reactExports.useState(null); 4127 const [sliceCurrent, setSliceCurrent] = reactExports.useState(null); 4128 const [panStart, setPanStart] = reactExports.useState(null); 4129 const [showHelp, setShowHelp] = reactExports.useState(false); 4130 const [toolMode, setToolMode] = reactExports.useState("draw"); 4131 const [isPainting, setIsPainting] = reactExports.useState(false); 4132 const [lastPaintedTick, setLastPaintedTick] = reactExports.useState(null); 4133 const auditionPitchRef = reactExports.useRef(null); 4134 const lastNoteDurationRef = reactExports.useRef(null); 4135 const playheadElRef = reactExports.useRef(null); 4136 const currentTimeRef = reactExports.useRef(currentTime); 4137 reactExports.useEffect(() => { 4138 currentTimeRef.current = currentTime; 4139 }, [currentTime]); 4140 const [activePreviewMidi, setActivePreviewMidi] = reactExports.useState(null); 4141 const [gridPreviewMidi, setGridPreviewMidi] = reactExports.useState(null); 4142 const bottomPaneRef = reactExports.useRef(null); 4143 const [bottomPaneMode, setBottomPaneMode] = reactExports.useState("velocity"); 4144 const [draggingVelocityIdx, setDraggingVelocityIdx] = reactExports.useState(null); 4145 const dragStartValRef = reactExports.useRef(null); 4146 const dragSelectedNoteValsRef = reactExports.useRef([]); 4147 const [activeVoice, setActiveVoice] = reactExports.useState(1); 4148 const [showShortcutsModal, setShowShortcutsModal] = reactExports.useState(false); 4149 const [showGhostNotes, setShowGhostNotes] = reactExports.useState(true); 4150 const [ghostTrackVisibility, setGhostTrackVisibility] = reactExports.useState({}); 4151 const [showLayersDropdown, setShowLayersDropdown] = reactExports.useState(false); 4152 const layersDropdownRef = reactExports.useRef(null); 4153 const [scaleRoot, setScaleRoot] = reactExports.useState(null); 4154 const [scaleType, setScaleType] = reactExports.useState("major"); 4155 const SCALES = { 4156 "major": [0, 2, 4, 5, 7, 9, 11], 4157 "minor": [0, 2, 3, 5, 7, 8, 10], 4158 "harmonic_minor": [0, 2, 3, 5, 7, 8, 11], 4159 "melodic_minor": [0, 2, 3, 5, 7, 9, 11], 4160 "pentatonic_major": [0, 2, 4, 7, 9], 4161 "pentatonic_minor": [0, 3, 5, 7, 10], 4162 "blues": [0, 3, 5, 6, 7, 10] 4163 }; 4164 const [chordStamp, setChordStamp] = reactExports.useState("none"); 4165 const CHORDS = { 4166 "major": [0, 4, 7], 4167 "minor": [0, 3, 7], 4168 "maj7": [0, 4, 7, 11], 4169 "min7": [0, 3, 7, 10], 4170 "dom7": [0, 4, 7, 10], 4171 "dim": [0, 3, 6], 4172 "aug": [0, 4, 8] 4173 }; 4174 const [isToolbarFloating, setIsToolbarFloating] = reactExports.useState(false); 4175 const [toolbarPos, setToolbarPos] = reactExports.useState({ x: 300, y: 16 }); 4176 const [isDraggingToolbar, setIsDraggingToolbar] = reactExports.useState(false); 4177 const [toolbarDragStart, setToolbarDragStart] = reactExports.useState({ x: 0, y: 0, startPosX: 0, startPosY: 0 }); 4178 const handleToolbarPointerDown = (e) => { 4179 if (!isToolbarFloating) return; 4180 if (e.target.closest("button, select, input")) return; 4181 e.stopPropagation(); 4182 e.currentTarget.setPointerCapture(e.pointerId); 4183 setIsDraggingToolbar(true); 4184 setToolbarDragStart({ 4185 x: e.clientX, 4186 y: e.clientY, 4187 startPosX: toolbarPos.x, 4188 startPosY: toolbarPos.y 4189 }); 4190 }; 4191 const handleToolbarPointerMove = (e) => { 4192 if (!isDraggingToolbar) return; 4193 const dx = e.clientX - toolbarDragStart.x; 4194 const dy = e.clientY - toolbarDragStart.y; 4195 setToolbarPos({ 4196 x: Math.max(0, toolbarDragStart.startPosX + dx), 4197 y: Math.max(0, toolbarDragStart.startPosY + dy) 4198 }); 4199 }; 4200 const handleToolbarPointerUp = (e) => { 4201 if (!isDraggingToolbar) return; 4202 setIsDraggingToolbar(false); 4203 e.currentTarget.releasePointerCapture(e.pointerId); 4204 }; 4205 reactExports.useEffect(() => { 4206 if (containerRef.current && activeTrackIndex !== null && engine && engine.midi) { 4207 const tr = engine.midi.tracks[activeTrackIndex]; 4208 const isDrum = tr.channel === 9; 4209 const maxN = isDrum ? 81 : MAX_NOTE; 4210 const targetNote = isDrum ? 38 : 60; 4211 const targetIndex = maxN - targetNote; 4212 if (targetIndex >= 0) { 4213 containerRef.current.scrollTop = targetIndex * KEY_HEIGHT - containerRef.current.clientHeight / 2; 4214 } 4215 } 4216 }, [activeTrackIndex, engine]); 4217 reactExports.useEffect(() => { 4218 const handleClickOutside = (e) => { 4219 if (layersDropdownRef.current && !layersDropdownRef.current.contains(e.target)) { 4220 setShowLayersDropdown(false); 4221 } 4222 }; 4223 document.addEventListener("mousedown", handleClickOutside); 4224 return () => document.removeEventListener("mousedown", handleClickOutside); 4225 }, []); 4226 if (!engine || !engine.midi || activeTrackIndex === null) { 4227 return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "p-4 text-zinc-500 h-full flex items-center justify-center", children: "Select a track to edit in the Piano Roll." }); 4228 } 4229 const track = engine.midi.tracks[activeTrackIndex]; 4230 const notes = activePattern ? activePattern.notes : []; 4231 const isDrumMode = track.channel === 9; 4232 const getNotePropertyValue = (n, mode) => { 4233 switch (mode) { 4234 case "velocity": 4235 return n.velocity ?? 0.78; 4236 case "release": 4237 return n.release ?? 0.5; 4238 case "pan": 4239 return n.pan ?? 0.5; 4240 case "pitch": 4241 return n.pitch ?? 0.5; 4242 case "modX": 4243 return n.modX ?? 0.5; 4244 case "modY": 4245 return n.modY ?? 0.5; 4246 case "shift": 4247 return n.shift ?? 0.5; 4248 default: 4249 return 0.5; 4250 } 4251 }; 4252 const getPropertyUpdateObject = (mode, val) => { 4253 switch (mode) { 4254 case "velocity": 4255 return { velocity: val }; 4256 case "release": 4257 return { release: val }; 4258 case "pan": 4259 return { pan: val }; 4260 case "pitch": 4261 return { pitch: val }; 4262 case "modX": 4263 return { modX: val }; 4264 case "modY": 4265 return { modY: val }; 4266 case "shift": 4267 return { shift: val }; 4268 default: 4269 return {}; 4270 } 4271 }; 4272 const formatTooltipValue = (mode, val) => { 4273 switch (mode) { 4274 case "velocity": 4275 return `Velocity: ${Math.round(val * 100)}%`; 4276 case "release": 4277 return `Release: ${Math.round(val * 100)}%`; 4278 case "pan": { 4279 if (Math.abs(val - 0.5) < 0.02) return "Panning: Center"; 4280 return val < 0.5 ? `Panning: ${Math.round((0.5 - val) * 200)}
4280% Left` : `Panning: ${Math.round((val - 0.5) * 200)}% Right`; 4281 } 4282 case "pitch": { 4283 const cents = Math.round((val - 0.5) * 2400); 4284 if (cents === 0) return "Fine Pitch: Center"; 4285 return cents > 0 ? `Fine Pitch: +${cents} cents` : `Fine Pitch: ${cents} cents`; 4286 } 4287 case "modX": 4288 return `Filter Cutoff (Mod X): ${Math.round(val * 100)}%`; 4289 case "modY": 4290 return `Filter Resonance (Mod Y): ${Math.round(val * 100)}%`; 4291 case "shift": { 4292 const ticksOffset = Math.round((val - 0.5) * 480); 4293 if (ticksOffset === 0) return "Start Time Shift: None"; 4294 return ticksOffset > 0 ? `Start Time Shift: +${ticksOffset} ticks` : `Start Time Shift: ${ticksOffset} ticks`; 4295 } 4296 default: 4297 return ""; 4298 } 4299 }; 4300 const findNoteAtX = (x) => { 4301 let closestIdx = -1; 4302 let minDistance = Infinity; 4303 notes.forEach((n, idx) => { 4304 const left = n.ticks * pxPerTick; 4305 const w = Math.max(n.durationTicks * pxPerTick, 4); 4306 const right = left + w; 4307 if (x >= left && x <= right) { 4308 closestIdx = idx; 4309 minDistance = 0; 4310 } else if (minDistance > 0) { 4311 const dist = Math.abs(x - left); 4312 if (dist < minDistance) { 4313 minDistance = dist; 4314 closestIdx = idx; 4315 } 4316 } 4317 }); 4318 return minDistance < 50 ? closestIdx : -1; 4319 }; 4320 const activePlaybackMidis = React.useMemo(() => { 4321 if (!engine) return /* @__PURE__ */ new Set(); 4322 const activeMidis = /* @__PURE__ */ new Set(); 4323 if (engine.isPlaying()) { 4324 const currentTicks2 = engine.secondsToTicks(currentTime); 4325 let patternLength2 = 0; 4326 if (activePattern) { 4327 const maxNoteTick = activePattern.notes.reduce((max, n) => Math.max(max, n.ticks + n.durationTicks), 0); 4328 const ppq2 = engine.midi.header.ppq || 96; 4329 const ticksPerBar = ppq2 * 4; 4330 patternLength2 = activePattern.durationTicks || Math.max(ticksPerBar * 4, Math.ceil(maxNoteTick / ticksPerBar) * ticksPerBar); 4331 } 4332 let showPlayhead2 = false; 4333 let playheadTick2 = 0; 4334 if (playbackMode === "pat") { 4335 showPlayhead2 = true; 4336 playheadTick2 = patternLength2 > 0 ? currentTicks2 % patternLength2 : currentTicks2; 4337 } else { 4338 const playingClip = playlistClips.find( 4339 (clip) => clip.patternId === activePatternId && !clip.isMuted && currentTicks2 >= clip.startTick && currentTicks2 < clip.startTick + clip.durationTicks 4340 ); 4341 if (playingClip) { 4342 showPlayhead2 = true; 4343 playheadTick2 = patternLength2 > 0 ? (currentTicks2 - playingClip.startTick + playingClip.offsetTicks) % patternLength2 : currentTicks2 - playingClip.startTick + playingClip.offsetTicks; 4344 } 4345 } 4346 if (showPlayhead2) { 4347 notes.forEach((note) => { 4348 if (playheadTick2 >= note.ticks && playheadTick2 < note.ticks + note.durationTicks) { 4349 activeMidis.add(note.midi); 4350 } 4351 }); 4352 } 4353 console.log(`[Keyboard Debug] currentTime: ${currentTime.toFixed(3)}, currentTicks: ${currentTicks2}, playheadTick: ${playheadTick2}, activeMidis:`, Array.from(activeMidis)); 4354 } 4355 if (track && track.channel !== void 0) { 4356 try { 4357 const liveNotes = engine.getActiveNotes(track.channel); 4358 liveNotes.forEach((m) => activeMidis.add(m)); 4359 if (liveNotes.size > 0) { 4360 console.log(`[Keyboard Debug] Live synthesizer notes on channel ${track.channel}:`, Array.from(liveNotes)); 4361 } 4362 } catch (e) { 4363 } 4364 } 4365 return activeMidis; 4366 }, [currentTime, engine, notes, track, playbackMode, playlistClips, activePatternId, activePattern]); 4367 const isNoteActive = (m) => { 4368 if (activePreviewMidi === m) return true; 4369 if (gridPreviewMidi === m) return true; 4370 if (activePlaybackMidis.has(m)) return true; 4371 if (activeMidiKeys == null ? void 0 : activeMidiKeys.has(m)) return true; 4372 return false; 4373 }; 4374 const getWhiteKeyBg = (m) => { 4375 if (isNoteActive(m)) { 4376 return "linear-gradient(to right, #f97316 0%, #ffa043 30%, #ffb066 100%)"; 4377 } 4378 let inScale = false;
4379 if (scaleRoot !== null) { 4380 const noteClass = m % 12; 4381 const intervals = SCALES[scaleType] || []; 4382 inScale = intervals.some((interval) => (scaleRoot + interval) % 12 === noteClass); 4383 } 4384 if (m % 12 === 0) { 4385 return inScale ? "#dbeafe" : "#e3e4e6"; 4386 } else { 4387 return inScale ? "linear-gradient(to right, #dbeafe 0%, #eff6ff 35%, #f8fafc 100%)" : "linear-gradient(to right, #e2e2e2 0%, #f2f2f2 35%, #ffffff 100%)"; 4388 } 4389 }; 4390 const getBlackKeyBg = (m) => { 4391 if (isNoteActive(m)) { 4392 return "linear-gradient(to right, #d35400 0%, #e67e22 80%, #ffb066 100%)"; 4393 } 4394 return "linear-gradient(to right, #242424 0%, #363636 80%, #4c4c4c 100%)"; 4395 }; 4396 const handleKeyMouseDown = (e, m) => { 4397 if (!engine) return; 4398 const rect = e.currentTarget.getBoundingClientRect(); 4399 const x = e.clientX - rect.left; 4400 const width = rect.width; 4401 const isBlack = !isDrumMode && isBlackKey(m); 4402 let playMidi = m; 4403 if (isBlack && x > width * 0.6) { 4404 const y = e.clientY - rect.top; 4405 if (y < rect.height / 2) { 4406 playMidi = m + 1; 4407 } else { 4408 playMidi = m - 1; 4409 } 4410 } 4411 setActivePreviewMidi(playMidi); 4412 engine.startNotePreview(playMidi, track.channel); 4413 }; 4414 const handleKeyMouseUp = (e, m) => { 4415 if (activePreviewMidi !== null && engine) { 4416 engine.stopNotePreview(activePreviewMidi, track.channel); 4417 } 4418 setActivePreviewMidi(null); 4419 }; 4420 const handleKeyMouseMove = (e, m) => { 4421 const rect = e.currentTarget.getBoundingClientRect(); 4422 const x = e.clientX - rect.left; 4423 const width = rect.width; 4424 const isBlack = !isDrumMode && isBlackKey(m); 4425 let hoverMidi = m; 4426 if (isBlack && x > width * 0.6) { 4427 const y = e.clientY - rect.top; 4428 if (y < rect.height / 2) { 4429 hoverMidi = m + 1; 4430 } else { 4431 hoverMidi = m - 1; 4432 } 4433 } 4434 if (e.buttons === 1 && hoverMidi !== activePreviewMidi && engine) { 4435 if (activePreviewMidi !== null) { 4436 engine.stopNotePreview(activePreviewMidi, track.channel); 4437 } 4438 setActivePreviewMidi(hoverMidi); 4439 engine.startNotePreview(hoverMidi, track.channel); 4440 } 4441 const noteNameStr = isDrumMode ? GM_PERCUSSION_MAP[hoverMidi] || `Drum ${hoverMidi}` : getNoteName(hoverMidi); 4442 setHint({ 4443 label: isDrumMode ? "Percussion Key" : "Piano Keyboard", 4444 positionText: "", 4445 noteText: `${noteNameStr} / ${hoverMidi}`, 4446 iconType: "keyboard" 4447 }); 4448 }; 4449 const handleKeyMouseLeave = (e, m) => { 4450 if (activePreviewMidi !== null && engine) { 4451 engine.stopNotePreview(activePreviewMidi, track.channel); 4452 } 4453 setActivePreviewMidi(null); 4454 setHint(null); 4455 }; 4456 const handleKeyMouseEnter = (e, m) => { 4457 if (e.buttons === 1 && engine) { 4458 const rect = e.currentTarget.getBoundingClientRect(); 4459 const x = e.clientX - rect.left; 4460 const width = rect.width; 4461 const isBlack = !isDrumMode && isBlackKey(m); 4462 let playMidi = m; 4463 if (isBlack && x > width * 0.6) { 4464 const y = e.clientY - rect.top; 4465 if (y < rect.height / 2) { 4466 playMidi = m + 1; 4467 } else { 4468 playMidi = m - 1; 4469 } 4470 } 4471 setActivePreviewMidi(playMidi); 4472 engine.startNotePreview(playMidi, track.channel); 4473 } 4474 }; 4475 const ppq = engine.midi.header.ppq; 4476 const maxTicks = engine.midi.durationTicks + ppq * 4 * 8; 4477 const pxPerTick = BEAT_WIDTH * zoomX / ppq; 4478 const totalWidth = maxTicks * pxPerTick; 4479 const actualMinNote = isDrumMode ? 35 : MIN_NOTE; 4480 const actualMaxNote = isDrumMode ? 81 : MAX_NOTE; 4481 const actualNumKeys = actualMaxNote - actualMinNote + 1; 4482 const handleMouseMove = (e) => { 4483 if (isPainting && toolMode === "paint" && gridRef.current) { 4484 const rect = gridRef.current.getBoundingClientRect(); 4485 const x = e.clientX - rect.left; 4486 const y = e.clientY - rect.top; 4487 const snapTicks2 = gridSnap === 0 ? 1 : ppq * (4 / gridSnap); 4488 const rawTicks = Math.max(0, Math.round(x / pxPerTick)); 4489 const ticks = gridSnap === 0 ? rawTicks : Math.round(rawTicks / snapTicks2) * snapTicks2; 4490 const pitchIdx = Math.floor(y / KEY_HEIGHT); 4491 const midiNote = actualMaxNote - pitchIdx; 4492 if (midiNote >= actualMinNote && midiNote <= actualMaxNote) { 4493 if (lastPaintedTick !== ticks) { 4494 const defaultDuration = gridSnap === 0 ? ppq : snapTicks2; 4495 const existingNote = notes.find((n) => n.midi === midiNote && n.ticks === ticks); 4496 if (!existingNote) { 4497 addNote(activeTrackIndex, midiNote, ticks, defaultDuration, 0.8, "pianoroll", activeVoice); 4498 setLastPaintedTick(ticks); 4499 if (engine) { 4500 engine.startNotePreview(midiNote, track.channel); 4501 setGridPreviewMidi(midiNote); 4502 setTimeout(() => { 4503 engine.stopNotePreview(midiNote, track.channel); 4504 setGridPreviewMidi((prev) => prev === midiNote ? null : prev); 4505 }, 100); 4506 } 4507 } 4508 } 4509 } 4510 return; 4511 } 4512 if (panStart && containerRef.current) { 4513 const dx2 = e.clientX - panStart.x; 4514 const dy2 = e.clientY - panStart.y; 4515 containerRef.current.scrollLeft = panStart.scrollX - dx2; 4516 containerRef.current.scrollTop = panStart.scrollY - dy2; 4517 return; 4518 } 4519 if (draggingMeta && dragStartPos) { 4520 const dx2 = e.clientX - dragStartPos.x; 4521 const dticks2 = Math.round(dx2 / pxPerTick); 4522 const newTicks = Math.max(0, draggingMeta.originalTick + dticks2); 4523 setMetaDragTicks(newTicks); 4524 return; 4525 } 4526 if (lassoStart && gridRef.current && containerRef.current) { 4527 draggedRef.current = true; 4528 const rect = gridRef.current.getBoundingClientRect(); 4529 const x = e.clientX - rect.left + (containerRef.current.scrollLeft - lassoStart.scrollX); 4530 const y = e.clientY - rect.top + (containerRef.current.scrollTop - lassoStart.scrollY); 4531 setLassoCurrent({ x, y }); 4532 return; 4533 } 4534 if (sliceStart && gridRef.current && containerRef.current) { 4535 draggedRef.current = true; 4536 const rect = gridRef.current.getBoundingClientRect(); 4537 const x = e.clientX - rect.left + (containerRef.current.scrollLeft - sliceStart.scrollX); 4538 const y = e.clientY - rect.top + (containerRef.current.scrollTop - sliceStart.scrollY); 4539 setSliceCurrent({ x, y }); 4540 return; 4541 } 4542 if (draggingNoteIdx === null || !dragStartPos || !dragStartProps || !gridRef.current) { 4543 if (gridRef.current) { 4544 const rect = gridRef.current.getBoundingClientRect(); 4545 const x = e.clientX - rect.left; 4546 const y = e.clientY - rect.top; 4547 const rawTicks = Math.max(0, x / pxPerTick); 4548 const snapTicks2 = gridSnap === 0 ? 1 : ppq * (4 / gridSnap); 4549 const ticks = gridSnap === 0 ? Math.round(rawTicks) : Math.round(rawTicks / snapTicks2) * snapTicks2; 4550 const pitchIdx = Math.floor(y / KEY_HEIGHT); 4551 const midiNote = actualMaxNote - pitchIdx; 4552 if (midiNote >= actualMinNote && midiNote <= actualMaxNote) { 4553 const noteNameStr = isDrumMode ? GM_PERCUSSION_MAP[midiNote] || `Drum ${midiNote}` : getNoteName(midiNote); 4554 const posStr = formatTicksToFlTime(ticks, ppq); 4555 setHint({ 4556 label: isDrumMode ? "Percussion Grid" : `Draw Notes: ${toolMode}`, 4557 positionText: posStr, 4558 noteText: `${noteNameStr} / ${midiNote}`, 4559 iconType: toolMode 4560 }); 4561 } 4562 } 4563 return; 4564 } 4565 draggedRef.current = true; 4566 const dx = e.clientX - dragStartPos.x; 4567 const dy = e.clientY - dragStartPos.y; 4568 let dticks = Math.round(dx / pxPerTick); 4569 let dPitch = -Math.round(dy / KEY_HEIGHT); 4570 if (isShiftDrag) { 4571 let activeConstraint = dragConstraint; 4572 if (activeConstraint === "none") { 4573 if (Math.abs(dx) > 10 || Math.abs(dy) > 10) { 4574 if (Math.abs(dx) >= Math.abs(dy)) { 4575 activeConstraint = "horizontal"; 4576 setDragConstraint("horizontal"); 4577 } else { 4578 activeConstraint = "vertical"; 4579 setDragConstraint("vertical"); 4580 } 4581 } 4582 } 4583 if (activeConstraint === "horizontal") { 4584 dPitch = 0; 4585 } else if (activeConstraint === "vertical") { 4586 dticks = 0; 4587 } 4588 } 4589 const snapTicks = gridSnap === 0 ? 1 : ppq * (4 / gridSnap); 4590 if (dragType === "move") { 4591 const updates = dragSelectedNotesRef.current.map((item) => { 4592 const rawTicks = item.ticks + dticks; 4593 const newTicks = Math.max(0, gridSnap === 0 ? rawTicks : Math.round(rawTicks / snapTicks) * snapTicks); 4594 const newPitch = Math.max(actualMinNote, Math.min(actualMaxNote, item.midi + dPitch)); 4595 if (item.idx === draggingNoteIdx && newPitch !== auditionPitchRef.current) { 4596 if (auditionPitchRef.current !== null && engine) { 4597 engine.stopNotePreview(auditionPitchRef.current, track.channel); 4598 } 4599 if (engine) { 4600 engine.startNotePreview(newPitch, track.channel); 4601 setGridPreviewMidi(newPitch); 4602 } 4603 auditionPitchRef.current = newPitch; 4604 } 4605 return { 4606 noteIndex: item.idx, 4607 newProps: { 4608 ticks: newTicks, 4609 midi: newPitch 4610 } 4611 }; 4612 }); 4613 updateNotes(activeTrackIndex, updates, "pianoroll"); 4614 } else if (dragType === "resize-right") { 4615 const rawDuration = dragStartProps.durationTicks + dticks; 4616 const snappedDuration = gridSnap === 0 ? rawDuration : Math.round(rawDuration / snapTicks) * snapTicks; 4617 const newDuration = Math.max(1, snappedDuration); 4618 updateNote(activeTrackIndex, draggingNoteIdx, { durationTicks: newDuration }, "pianoroll"); 4619 } else if (dragType === "resize-left") { 4620 const rawTicks = dragStartProps.ticks + dticks; 4621 const snappedTicks = gridSnap === 0 ? rawTicks : Math.round(rawTicks / snapTicks) * snapTicks; 4622 const newTicks = Math.max(0, snappedTicks); 4623 const originalEndTicks = dragStartProps.ticks + dragStartProps.durationTicks; 4624 const newDuration = Math.max(1, originalEndTicks - newTicks); 4625 if (originalEndTicks - newTicks > 0) { 4626 updateNote(activeTrackIndex, draggingNoteIdx, { ticks: newTicks, durationTicks: newDuration }, "pianoroll"); 4627 } 4628 } 4629 }; 4630 const handleMouseUp = (e) => { 4631 if (draggingMeta && metaDragTicks !== null) { 4632 if (draggingMeta.type === "tempo") { 4633 engine.updateTempo(draggingMeta.originalTick, metaDragTicks, draggingMeta.bpm); 4634 } else { 4635 engine.updateTimeSignature(draggingMeta.originalTick, metaDragTicks, draggingMeta.num, draggingMeta.den); 4636 } 4637 forceSync(); 4638 } 4639 if (auditionPitchRef.current !== null && engine) { 4640 engine.stopNotePreview(auditionPitchRef.current, track.channel); 4641 auditionPitchRef.current = null; 4642 } 4643 setGridPreviewMidi(null); 4644 if (draggingNoteIdx !== null && (dragType === "resize-left" || dragType === "resize-right")) { 4645 const resizedNote = notes[draggingNoteIdx]; 4646 if (resizedNote) { 4647 lastNoteDurationRef.current = resizedNote.durationTicks; 4648 } 4649 } 4650 if (lassoStart && lassoCurrent && engine && engine.midi) { 4651 const x1 = Math.min(lassoStart.x, lassoCurrent.x); 4652 const x2 = Math.max(lassoStart.x, lassoCurrent.x); 4653 const y1 = Math.min(lassoStart.y, lassoCurrent.y); 4654 const y2 = Math.max(lassoStart.y, lassoCurrent.y); 4655 engine.midi.tracks[activeTrackIndex]; 4656 const notes2 = activePattern ? activePattern.notes : []; 4657 const newSelectedIds = []; 4658 notes2.forEach((note, idx) => { 4659 const noteLeft = note.ticks * pxPerTick; 4660 const noteRight = noteLeft + note.durationTicks * pxPerTick; 4661 const noteTop = (actualMaxNote - note.midi) * KEY_HEIGHT; 4662 const noteBottom = noteTop + KEY_HEIGHT; 4663 if (noteRight >= x1 && noteLeft <= x2 && noteBottom >= y1 && noteTop <= y2) { 4664 newSelectedIds.push(idx); 4665 } 4666 }); 4667 if (e == null ? void 0 : e.shiftKey) { 4668 const combined = Array.from(/* @__PURE__ */ new Set([...selectedNoteIds, ...newSelectedIds])); 4669 setSelectedNoteIds(combined); 4670 } else { 4671 setSelectedNoteIds(newSelectedIds); 4672 } 4673 } 4674 if (sliceStart && sliceCurrent && engine && engine.midi) { 4675 engine.midi.tracks[activeTrackIndex]; 4676 const notes2 = activePattern ? activePattern.notes : []; 4677 const x1 = sliceStart.x; 4678 const y1 = sliceStart.y; 4679 const x2 = sliceCurrent.x; 4680 const y2 = sliceCurrent.y; 4681 const splitsToApply = []; 4682 notes2.forEach((note, idx) => { 4683 const noteLeft = note.ticks * pxPerTick; 4684 const noteRight = noteLeft + note.durationTicks * pxPerTick; 4685 const noteTop = (actualMaxNote - note.midi) * KEY_HEIGHT; 4686 const noteCenterY = noteTop + KEY_HEIGHT / 2; 4687 const minY = Math.min(y1, y2); 4688 const maxY = Math.max(y1, y2); 4689 if (noteCenterY >= minY && noteCenterY <= Math.max(maxY, minY + 1)) { 4690 let intersectX; 4691 if (y1 === y2) { 4692 intersectX = x1; 4693 } else { 4694 intersectX = x1 + (noteCenterY - y1) * (x2 - x1) / (y2 - y1); 4695 } 4696 if (intersectX > noteLeft && intersectX < noteRight) { 4697 const splitTick = Math.round(intersectX / pxPerTick); 4698 splitsToApply.push({ noteIdx: idx, splitTick }); 4699 } 4700 } 4701 }); 4702 splitsToApply.sort((a, b) =>
4702 b.noteIdx - a.noteIdx).forEach((split) => { 4703 splitNote(activeTrackIndex, split.noteIdx, split.splitTick, "pianoroll"); 4704 }); 4705 } 4706 setLassoStart(null); 4707 setLassoCurrent(null); 4708 setSliceStart(null); 4709 setSliceCurrent(null); 4710 setTimeout(() => { 4711 draggedRef.current = false; 4712 }, 50); 4713 setDraggingNoteIdx(null); 4714 setDragStartPos(null); 4715 setDragStartProps(null); 4716 setDragType(null); 4717 setDraggingMeta(null); 4718 setMetaDragTicks(null); 4719 setIsPainting(false); 4720 setLastPaintedTick(null); 4721 setPanStart(null); 4722 }; 4723 const keys = []; 4724 for (let midi = actualMaxNote; midi >= actualMinNote; midi--) { 4725 const isBlack = !isDrumMode && isBlackKey(midi); 4726 const name = isDrumMode ? GM_PERCUSSION_MAP[midi] || `Drum ${midi}` : getNoteName(midi); 4727 const octave = Math.floor(midi / 12) - 1; 4728 let isInScale = false; 4729 if (!isDrumMode && scaleRoot !== null) { 4730 const noteClass = midi % 12; 4731 const intervals = SCALES[scaleType] || []; 4732 isInScale = intervals.some((interval) => (scaleRoot + interval) % 12 === noteClass); 4733 } 4734 if (isDrumMode) { 4735 const bgClass = isInScale ? "bg-indigo-100 dark:bg-indigo-900/30" : "bg-white dark:bg-zinc-800"; 4736 const textClass = isNoteActive(midi) ? "text-white font-bold" : isInScale ? "text-indigo-800 dark:text-indigo-300" : "text-zinc-600 dark:text-zinc-400"; 4737 const activeBg = isNoteActive(midi) ? "#ffb066" : void 0; 4738 keys.push(
4739 /* @__PURE__ */ jsxRuntimeExports.jsx( 4740 "div", 4741 { 4742 className: `flex items-center justify-end px-2 border-b select-none cursor-pointer relative w-full border-zinc-200 dark:border-zinc-800 hover:brightness-110 ${bgClass} ${textClass}`, 4743 style: { height: KEY_HEIGHT, fontSize: "9px", backgroundColor: activeBg }, 4744 onMouseDown: (e) => handleKeyMouseDown(e, midi), 4745 onMouseUp: (e) => handleKeyMouseUp(), 4746 onMouseMove: (e) => handleKeyMouseMove(e, midi), 4747 onMouseLeave: (e) => handleKeyMouseLeave(), 4748 onMouseEnter: (e) => handleKeyMouseEnter(e, midi), 4749 children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold pointer-events-none truncate z-20", title: name, children: name }) 4750 }, 4751 midi 4752 ) 4753 ); 4754 } else { 4755 if (isBlack) { 4756 keys.push( 4757 /* @__PURE__ */ jsxRuntimeExports.jsxs( 4758 "div", 4759 { 4760 className: "select-none cursor-pointer relative w-full hover:brightness-110 overflow-hidden", 4761 style: { height: KEY_HEIGHT }, 4762 onMouseDown: (e) => handleKeyMouseDown(e, midi), 4763 onMouseUp: (e) => handleKeyMouseUp(), 4764 onMouseMove: (e) => handleKeyMouseMove(e, midi), 4765 onMouseLeave: (e) => handleKeyMouseLeave(), 4766 onMouseEnter: (e) => handleKeyMouseEnter(e, midi), 4767 children: [
4768 /* @__PURE__ */ jsxRuntimeExports.jsx( 4769 "div", 4770 { 4771 className: "absolute right-0 top-0 w-[40%] h-[50%] z-10 pointer-events-none transition-all duration-75", 4772 style: { background: getWhiteKeyBg(midi + 1) } 4773 } 4774 ), 4775 /* @__PURE__ */ jsxRuntimeExports.jsx( 4776 "div", 4777 { 4778 className: "absolute right-0 bottom-0 w-[40%] h-[50%] z-10 pointer-events-none transition-all duration-75", 4779 style: { background: getWhiteKeyBg(midi - 1) } 4780 } 4781 ), 4782 /* @__PURE__ */ jsxRuntimeExports.jsx( 4783 "div", 4784 { 4785 className: "absolute right-0 top-1/2 w-[40%] h-px bg-zinc-300 dark:bg-zinc-700 pointer-events-none z-15", 4786 style: { transform: "translateY(-50%)" } 4787 } 4788 ), 4789 /* @__PURE__ */ jsxRuntimeExports.jsx( 4790 "div", 4791 { 4792 className: "absolute left-0 top-0 w-[60%] h-full z-20 pointer-events-none transition-all duration-75 shadow-sm", 4793 style: { 4794 background: getBlackKeyBg(midi), 4795 borderRight: "1px solid #181a1b", 4796 borderBottom: "1px solid #181a1b" 4797 }, 4798 title: name 4799 } 4800 ) 4801 ] 4802 }, 4803 midi 4804 ) 4805 ); 4806 } else { 4807 const isEOrB = midi % 12 === 4 || midi % 12 === 11; 4808 const borderClass = isEOrB ? "border-b border-zinc-300 dark:border-zinc-700" : ""; 4809 const isC = midi % 12 === 0; 4810 keys.push(
4811 /* @__PURE__ */ jsxRuntimeExports.jsxs( 4812 "div", 4813 { 4814 className: `flex items-center justify-end select-none cursor-pointer relative w-full hover:brightness-110 transition-all duration-75 ${borderClass}`, 4815 style: { 4816 height: KEY_HEIGHT, 4817 fontSize: "9px", 4818 background: getWhiteKeyBg(midi) 4819 }, 4820 onMouseDown: (e) => handleKeyMouseDown(e, midi), 4821 onMouseUp: (e) => handleKeyMouseUp(), 4822 onMouseMove: (e) => handleKeyMouseMove(e, midi), 4823 onMouseLeave: (e) => handleKeyMouseLeave(), 4824 onMouseEnter: (e) => handleKeyMouseEnter(e, midi), 4825 children: [ 4826 isC && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "absolute right-2 font-bold text-[#4c566a] dark:text-[#8fbcbb] pointer-events-none select-none text-[10px] z-20", children: [ 4827 "C", 4828 octave 4829 ] }), 4830 !isC && isInScale && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute right-2 w-1.5 h-1.5 rounded-full bg-indigo-500/40 pointer-events-none z-20" }) 4831 ] 4832 }, 4833 midi 4834 ) 4835 ); 4836 } 4837 } 4838 } 4839 const verticalLines = []; 4840 const timeSignatures = engine.midi.header.timeSignatures; 4841 if (timeSignatures.length === 0) { 4842 timeSignatures.push({ ticks: 0, timeSignature: [4, 4], measures: 0 }); 4843 } 4844 const measureLabels = []; 4845 { 4846 let curMeasureTick = 0; 4847 let tsIndex = 0; 4848 let measureNumber = 1; 4849 while (curMeasureTick <= maxTicks) { 4850 const ts = timeSignatures[tsIndex] || { timeSignature: [4, 4] }; 4851 const nextTs = timeSignatures[tsIndex + 1]; 4852 const num = ts.timeSignature[0]; 4853 const den = ts.timeSignature[1]; 4854 const ticksPerBeat = ppq * (4 / den); 4855 const ticksPerMeasure = ticksPerBeat * num; 4856 const limitTick = nextTs ? nextTs.ticks : maxTicks; 4857 while (curMeasureTick < limitTick) { 4858 measureLabels.push(
4859 /* @__PURE__ */ jsxRuntimeExports.jsx( 4860 "div", 4861 { 4862 className: "absolute bottom-0.5 text-[10px] font-bold text-zinc-400 dark:text-zinc-500 pointer-events-none select-none pl-1", 4863 style: { left: curMeasureTick * pxPerTick }, 4864 children: measureNumber 4865 }, 4866 `m-lbl-${curMeasureTick}` 4867 ) 4868 ); 4869 measureNumber++; 4870 curMeasureTick += ticksPerMeasure; 4871 if (ticksPerMeasure <= 0) break; 4872 } 4873 tsIndex++; 4874 if (tsIndex >= timeSignatures.length) break; 4875 } 4876 } 4877 let currentTick = 0; 4878 let tsIdx = 0; 4879 while (currentTick <= maxTicks) { 4880 const ts = timeSignatures[tsIdx]; 4881 const nextTs = timeSignatures[tsIdx + 1]; 4882 const num = ts.timeSignature[0]; 4883 const den = ts.timeSignature[1]; 4884 const ticksPerBeat = ppq * (4 / den); 4885 const ticksPerMeasure = ticksPerBeat * num; 4886 const limitTick = nextTs ? nextTs.ticks : maxTicks; 4887 while (currentTick < limitTick) { 4888 verticalLines.push(
4889 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute h-full w-[2px] bg-zinc-400 dark:bg-zinc-600 z-0", style: { left: currentTick * pxPerTick } }, `m-${currentTick}`) 4890 ); 4891 for (let b = 1; b < num; b++) { 4892 const beatTick = currentTick + b * ticksPerBeat; 4893 if (beatTick < limitTick) { 4894 verticalLines.push( 4895 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute h-full w-px bg-zinc-200/70 dark:bg-zinc-800/50 z-0", style: { left: beatTick * pxPerTick } }, `b-${beatTick}`) 4896 ); 4897 } 4898 } 4899 if (gridSnap > den) { 4900 const subsPerBeat = gridSnap / den; 4901 const ticksPerSub = ticksPerBeat / subsPerBeat; 4902 for (let b = 0; b < num; b++) { 4903 for (let s = 1; s < subsPerBeat; s++) { 4904 const subTick = currentTick + b * ticksPerBeat + s * ticksPerSub; 4905 if (subTick < limitTick) { 4906 verticalLines.push( 4907 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute h-full w-px bg-zinc-200/30 dark:bg-zinc-800/30 z-0", style: { left: subTick * pxPerTick } }, `s-${subTick}`) 4908 ); 4909 } 4910 } 4911 } 4912 } 4913 currentTick += ticksPerMeasure; 4914 if (ticksPerMeasure <= 0) break; 4915 } 4916 tsIdx++; 4917 if (tsIdx >= timeSignatures.length) break; 4918 } 4919 let patternLength = 0; 4920 if (activePattern) { 4921 const maxNoteTick = activePattern.notes.reduce((max, n) => Math.max(max, n.ticks + n.durationTicks), 0); 4922 const ppq2 = ((_b = (_a = engine == null ? void 0 : engine.midi) == null ? void 0 : _a.header) == null ? void 0 : _b.ppq) || 96; 4923 const ticksPerBar = ppq2 * 4; 4924 patternLength = activePattern.durationTicks || Math.max(ticksPerBar * 4, Math.ceil(maxNoteTick / ticksPerBar) * ticksPerBar); 4925 } 4926 const currentTicks = engine ? engine.secondsToTicks(currentTime) : 0; 4927 let showPlayhead = false; 4928 let playheadTick = 0; 4929 if (playbackMode === "pat") { 4930 showPlayhead = true; 4931 playheadTick = patternLength > 0 ? currentTicks % patternLength : currentTicks; 4932 } else { 4933 const playingClip = playlistClips.find( 4934 (clip) => clip.patternId === activePatternId && !clip.isMuted && currentTicks >= clip.startTick && currentTicks < clip.startTick + clip.durationTicks 4935 ); 4936 if (playingClip) { 4937 showPlayhead = true; 4938 playheadTick = patternLength > 0 ? (currentTicks - playingClip.startTick + playingClip.offsetTicks) % patternLength : currentTicks - playingClip.startTick + playingClip.offsetTicks; 4939 } 4940 } 4941 const playheadX = playheadTick * pxPerTick; 4942 reactExports.useEffect(() => { 4943 console.log(`[Playhead Debug] useEffect triggered. isPlaying: ${isPlaying}, engine.isPlaying: ${engine == null ? void 0 : engine.isPlaying()}`); 4944 if (!engine) return; 4945 let rafId; 4946 const loop = () => { 4947 const rawTime = engine.isPlaying() ? engine.getCurrentTime() : currentTimeRef.current; 4948 const rawTicks = engine.secondsToTicks(rawTime); 4949 let pxOffset = 0; 4950 if (playbackMode === "pat") { 4951 const tick = patternLength > 0 ? rawTicks % patternLength : rawTicks; 4952 pxOffset = tick * pxPerTick; 4953 } else { 4954 const playingClip = playlistClips.find( 4955 (c) => c.patternId === activePatternId && !c.isMuted && rawTicks >= c.startTick && rawTicks < c.startTick + c.durationTicks 4956 ); 4957 if (playingClip) { 4958 const tick = patternLength > 0 ? (rawTicks - playingClip.startTick + playingClip.offsetTicks) % patternLength : rawTicks - playingClip.startTick + playingClip.offsetTicks; 4959 pxOffset = tick * pxPerTick; 4960 } else { 4961 const tick = patternLength > 0 ? rawTicks % patternLength : rawTicks; 4962 pxOffset = tick * pxPerTick; 4963 } 4964 } 4965 if (playheadElRef.current) { 4966 playheadElRef.current.style.left = `${pxOffset}px`; 4967 playheadElRef.current.style.display = "block"; 4968 if (isPlaying && containerRef.current) { 4969 const container = containerRef.current; 4970 const scrollLeft = container.scrollLeft; 4971 const width = container.clientWidth; 4972 if (pxOffset > scrollLeft + width - 50) { 4973 container.scrollLeft = pxOffset - width / 2; 4974 } else if (pxOffset < scrollLeft) { 4975 container.scrollLeft = Math.max(0, pxOffset - 50); 4976 } 4977 } 4978 } 4979 rafId = requestAnimationFrame(loop); 4980 }; 4981 rafId = requestAnimationFrame(loop); 4982 return () => { 4983 console.log("[Playhead Debug] useEffect cleanup called."); 4984 cancelAnimationFrame(rafId); 4985 }; 4986 }, [engine, isPlaying, playbackMode, patternLength, pxPerTick, playlistClips, activePatternId]); 4987 const toolbarJSX = /* @__PURE__ */ jsxRuntimeExports.jsxs( 4988 "div", 4989 { 4990 className: isToolbarFloating ? "absolute z-[60] flex items-center bg-white dark:bg-zinc-800 p-2 rounded-lg shadow-lg border border-zinc-200 dark:border-zinc-700" : "shrink-0 flex items-center bg-zinc-100 dark:bg-zinc-900 px-4 py-2 border-b border-zinc-300 dark:border-zinc-700 w-full z-[60]", 4991 style: isToolbarFloating ? { right: "auto", bottom: "auto", left: toolbarPos.x, top: toolbarPos.y } : {}, 4992 onPointerDown: handleToolbarPointerDown, 4993 onPointerMove: handleToolbarPointerMove, 4994 onPointerUp: handleToolbarPointerUp, 4995 onPointerCancel: handleToolbarPointerUp, 4996 children: [
4997 isToolbarFloating && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mr-2 cursor-move text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-200", children: /* @__PURE__ */ jsxRuntimeExports.jsx(GripHorizontal, { size: 18 }) }), 4998 /* @__PURE__ */ jsxRuntimeExports.jsx(HelpTip, { text: "Draw, paint, or slice notes on the piano roll.", position: "bottom", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1 mr-2 border-r border-zinc-200 dark:border-zinc-700 pr-4", children: [ 4999 /* @__PURE__ */ jsxRuntimeExports.jsx( 5000 "button", 5001 { 5002 onClick: () => setToolMode("draw"), 5003 className: `p-1.5 rounded transition-colors ${toolMode === "draw" ? "bg-indigo-100 text-indigo-600 dark:bg-indigo-900/50 dark:text-indigo-400" : "text-zinc-500 hover:bg-zinc-100 dark:hover:bg-zinc-700"}`, 5004 title: "Draw Tool (Left-click to add, drag to resize)", 5005 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Pencil, { size: 18 }) 5006 } 5007 ),
5008 /* @__PURE__ */ jsxRuntimeExports.jsx( 5009 "button", 5010 { 5011 onClick: () => setToolMode("paint"), 5012 className: `p-1.5 rounded transition-colors ${toolMode === "paint" ? "bg-indigo-100 text-indigo-600 dark:bg-indigo-900/50 dark:text-indigo-400" : "text-zinc-500 hover:bg-zinc-100 dark:hover:bg-zinc-700"}`, 5013 title: "Paint Tool (Click and drag to draw continuously)", 5014 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Brush, { size: 18 }) 5015 } 5016 ), 5017 /* @__PURE__ */ jsxRuntimeExports.jsx( 5018 "button", 5019 { 5020 onClick: () => setToolMode("slice"), 5021 className: `p-1.5 rounded transition-colors ${toolMode === "slice" ? "bg-indigo-100 text-indigo-600 dark:bg-indigo-900/50 dark:text-indigo-400" : "text-zinc-500 hover:bg-zinc-100 dark:hover:bg-zinc-700"}`, 5022 title: "Slice Tool (Click and drag to cut notes)", 5023 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Scissors, { size: 18 }) 5024 } 5025 ) 5026 ] }) }),
5027 /* @__PURE__ */ jsxRuntimeExports.jsx(HelpTip, { text: "Switch voices. Different voices are rendered in different colors.", position: "bottom", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center gap-1 mr-2 border-r border-zinc-200 dark:border-zinc-700 pr-4", children: [1, 2, 3, 4].map((v) => /* @__PURE__ */ jsxRuntimeExports.jsx( 5028 "button", 5029 { 5030 onClick: () => setActiveVoice(v), 5031 className: `w-6 h-6 rounded flex items-center justify-center text-xs font-bold transition-colors ${activeVoice === v ? v === 1 ? "bg-[#0072B2] text-white" : v === 2 ? "bg-[#009E73] text-white" : v === 3 ? "bg-[#D55E00] text-white" : "bg-[#CC79A7] text-white" : "bg-zinc-100 dark:bg-zinc-700 text-zinc-400 hover:bg-zinc-200 dark:hover:bg-zinc-600"}`, 5032 children: v 5033 }, 5034 v 5035 )) }) }), 5036 /* @__PURE__ */ jsxRuntimeExports.jsx(HelpTip, { text: "Grid snapping resolution for drawing and moving notes.", position: "bottom", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 mr-2 border-r border-zinc-200 dark:border-zinc-700 pr-4 relative", children: [
5037 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { className: "p-1.5 text-zinc-500 hover:bg-zinc-100 dark:hover:bg-zinc-700 rounded transition-colors", title: "Snap to Grid", children: /* @__PURE__ */ jsxRuntimeExports.jsx(Magnet, { size: 18 }) }), 5038 /* @__PURE__ */ jsxRuntimeExports.jsxs( 5039 "select", 5040 { 5041 value: gridSnap, 5042 onChange: (e) => setGridSnap(Number(e.target.value)), 5043 className: "absolute inset-0 w-full h-full opacity-0 cursor-pointer", 5044 title: "Snap to Grid", 5045 children: [ 5046 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: 0, children: "Off" }), 5047 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: 1, children: "1 Bar (1/1)" }), 5048 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: 2, children: "1/2" }), 5049 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: 4, children: "1/4" }), 5050 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: 8, children: "1/8" }),
5051 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: 16, children: "1/16" }), 5052 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: 32, children: "1/32" }) 5053 ] 5054 } 5055 ) 5056 ] }) }), 5057 /* @__PURE__ */ jsxRuntimeExports.jsx(HelpTip, { text: "Zoom in or out horizontally.", position: "bottom", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1", children: [ 5058 /* @__PURE__ */ jsxRuntimeExports.jsx( 5059 "button", 5060 { 5061 onClick: () => setZoomX((z) => Math.max(0.25, z - 0.25)), 5062 className: "p-1 hover:bg-zinc-100 dark:hover:bg-zinc-700 rounded text-zinc-600 dark:text-zinc-300", 5063 title: "Zoom Out", 5064 children: /* @__PURE__ */ jsxRuntimeExports.jsx(ZoomOut, { size: 20 }) 5065 } 5066 ), 5067 /* @__PURE__ */ jsxRuntimeExports.jsx( 5068 "button", 5069 { 5070 onClick: () => setZoomX((z) => Math.min(4, z + 0.25)), 5071 className: "p-1 hover:bg-zinc-100 dark:hover:bg-zinc-700 rounded text-zinc-600 dark:text-zinc-300", 5072 title: "Zoom In", 5073 children: /* @__PURE__ */ jsxRuntimeExports.jsx(ZoomIn, { size: 20 }) 5074 } 5075 ) 5076 ] }) }),
5077 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-px h-6 bg-zinc-300 dark:bg-zinc-700 mx-1" }), 5078 /* @__PURE__ */ jsxRuntimeExports.jsx(HelpTip, { text: "Toggle and color-code background track ghost notes.", position: "bottom", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative", ref: layersDropdownRef, children: [ 5079 /* @__PURE__ */ jsxRuntimeExports.jsxs( 5080 "button", 5081 { 5082 onClick: () => setShowLayersDropdown(!showLayersDropdown), 5083 className: `p-1.5 rounded flex items-center gap-1.5 transition-colors ${showLayersDropdown ? "bg-indigo-100 text-indigo-600 dark:bg-indigo-900/50 dark:text-indigo-400" : "text-zinc-500 hover:bg-zinc-100 dark:hover:bg-zinc-700"}`, 5084 title: "Layer Visibility & Ghost Notes", 5085 children: [ 5086 /* @__PURE__ */ jsxRuntimeExports.jsx(Layers, { size: 18 }), 5087 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs font-semibold select-none", children: "Layers" }) 5088 ] 5089 } 5090 ),
5091 showLayersDropdown && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute right-0 mt-2 w-64 bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg shadow-xl z-[90] p-3 text-sm flex flex-col gap-2", children: [ 5092 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between items-center pb-2 border-b border-zinc-100 dark:border-zinc-700", children: [ 5093 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold text-zinc-900 dark:text-zinc-100", children: "Ghost Channels" }), 5094 /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "flex items-center gap-2 cursor-pointer", children: [ 5095 /* @__PURE__ */ jsxRuntimeExports.jsx( 5096 "input", 5097 { 5098 type: "checkbox", 5099 checked: showGhostNotes, 5100 onChange: (e) => setShowGhostNotes(e.target.checked), 5101 className: "rounded text-indigo-600 focus:ring-indigo-500 cursor-pointer" 5102 } 5103 ),
5104 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs font-medium text-zinc-600 dark:text-zinc-400 select-none", children: "Enabled" }) 5105 ] }) 5106 ] }), 5107 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "max-h-48 overflow-y-auto pr-1 flex flex-col gap-1.5 custom-scrollbar", children: engine.midi.tracks.map((t, idx) => { 5108 const isCurrent = idx === activeTrackIndex; 5109 const isVisible = ghostTrackVisibility[idx] !== false; 5110 const color = t.color || getTrackColor(idx); 5111 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 5112 "div", 5113 { 5114 className: `flex items-center justify-between p-1 rounded hover:bg-zinc-50 dark:hover:bg-zinc-750/30 ${isCurrent ? "bg-zinc-50 dark:bg-zinc-750/20" : ""}`, 5115 children: [ 5116 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 truncate", children: [ 5117 /* @__P
5117URE__ */ jsxRuntimeExports.jsx( 5118 "div", 5119 { 5120 className: "w-3 h-3 rounded-full shrink-0", 5121 style: { backgroundColor: color } 5122 } 5123 ), 5124 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `truncate font-medium text-xs ${isCurrent ? "text-zinc-900 dark:text-zinc-100 font-bold" : "text-zinc-600 dark:text-zinc-400"}`, children: t.name || `Track ${idx + 1}` }), 5125 isCurrent && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[9px] bg-indigo-100 text-indigo-700 dark:bg-indigo-900/60 dark:text-indigo-300 px-1 rounded-sm", children: "Active" }) 5126 ] }), 5127 !isCurrent && /* @__PURE__ */ jsxRuntimeExports.jsx( 5128 "button", 5129 { 5130 onClick: () => { 5131 setGhostTrackVisibility((prev) => ({ 5132 ...prev, 5133 [idx]: !isVisible 5134 })); 5135 }, 5136 className: "text-zinc-400 hover:text-zinc-650 dark:hover:text-zinc-200 transition-colors p-0.5", 5137 title: isVisible ? "Hide ghost notes" : "Show ghost notes",
5138 children: isVisible ? /* @__PURE__ */ jsxRuntimeExports.jsx(Eye, { size: 14 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(EyeOff, { size: 14 }) 5139 } 5140 ) 5141 ] 5142 }, 5143 `layer-${idx}` 5144 ); 5145 }) }), 5146 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between items-center pt-2 border-t border-zinc-100 dark:border-zinc-700 text-xs", children: [ 5147 /* @__PURE__ */ jsxRuntimeExports.jsx( 5148 "button", 5149 { 5150 onClick: () => { 5151 const newVis = {}; 5152 engine.midi.tracks.forEach((_, i) => { 5153 newVis[i] = true; 5154 }); 5155 setGhostTrackVisibility(newVis); 5156 }, 5157 className: "text-indigo-600 dark:text-indigo-400 hover:underline font-semibold", 5158 children: "Show All" 5159 } 5160 ),
5161 /* @__PURE__ */ jsxRuntimeExports.jsx( 5162 "button", 5163 { 5164 onClick: () => { 5165 const newVis = {}; 5166 engine.midi.tracks.forEach((_, i) => { 5167 newVis[i] = false; 5168 }); 5169 setGhostTrackVisibility(newVis); 5170 }, 5171 className: "text-zinc-500 hover:text-zinc-750 dark:hover:text-zinc-300 hover:underline font-semibold", 5172 children: "Hide All" 5173 } 5174 ) 5175 ] }) 5176 ] }) 5177 ] }) }), 5178 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-px h-6 bg-zinc-300 dark:bg-zinc-700 mx-1" }), 5179 /* @__PURE__ */ jsxRuntimeExports.jsx( 5180 "button", 5181 { 5182 onClick: () => setShowShortcutsModal(true), 5183 className: "p-1 hover:bg-zinc-100 dark:hover:bg-zinc-700 rounded text-zinc-600 dark:text-zinc-300 ml-1", 5184 title: "Keyboard Shortcuts & Tips", 5185 children: /* @__PURE__ */ jsxRuntimeExports.jsx(CircleQuestionMark, { size: 20 }) 5186 } 5187 ),
5188 /* @__PURE__ */ jsxRuntimeExports.jsx( 5189 "button", 5190 { 5191 onClick: () => setIsToolbarFloating(!isToolbarFloating), 5192 className: "p-1 hover:bg-zinc-100 dark:hover:bg-zinc-700 rounded text-zinc-500 hover:text-indigo-600 dark:hover:text-indigo-400 ml-auto", 5193 title: isToolbarFloating ? "Dock Toolbar" : "Float Toolbar", 5194 children: isToolbarFloating ? /* @__PURE__ */ jsxRuntimeExports.jsx(Pin, { size: 18 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(PinOff, { size: 18 }) 5195 } 5196 ) 5197 ] 5198 } 5199 ); 5200 return /* @__PURE__ */ jsxRuntimeExports.jsxs(Ut, { direction: "vertical", className: "flex-1 w-full h-full bg-zinc-50 dark:bg-zinc-900", children: [ 5201 /* @__PURE__ */ jsxRuntimeExports.jsxs(Yt, { className: "flex relative min-h-[200px] flex-col", children: [ 5202 !isToolbarFloating && toolbarJSX, 5203 /* @__PURE__ */ jsxRuntimeExports.jsxs( 5204 "div", 5205 { 5206 className: "flex-1 flex overflow-hidden relative bg-zinc-50 dark:bg-zinc-900 select-none", 5207 onMouseMove: handleMouseMove, 5208 onMouseUp: handleMouseUp, 5209 onMouseLeave: (e) => { 5210 handleMouseUp(e); 5211 setHint(null); 5212 }, 5213 onContextMenu: (e) => { 5214 e.preventDefault(); 5215 setContextMenuPos({ x: e.clientX, y: e.clientY }); 5216 }, 5217 children: [ 5218 isToolbarFloating && toolbarJSX, 5219 showShortcutsModal && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "fixed inset-0 z-[100] flex items-center justify-center bg-black/50 backdrop-blur-sm", onClick: () => setShowShortcutsModal(false), onContextMenu: (e) => e.preventDefault(), children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-white dark:bg-zinc-900 p-6 rounded-lg shadow-2xl border border-zinc-200 dark:border-zinc-800 max-w-lg w-full max-h-[80vh] overflow-y-auto custom-scrollbar", onClick: (e) => e.stopPropagation(), children: [
5220 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between items-center mb-6 border-b border-zinc-200 dark:border-zinc-800 pb-4", children: [ 5221 /* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { className: "font-bold text-xl text-zinc-900 dark:text-zinc-100 flex items-center gap-2", children: [ 5222 /* @__PURE__ */ jsxRuntimeExports.jsx(CircleQuestionMark, { size: 24, className: "text-indigo-500" }), 5223 "Piano Roll Shortcuts" 5224 ] }), 5225 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => setShowShortcutsModal(false), className: "text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-200", children: "â" }) 5226 ] }), 5227 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-6", children: [ 5228 /* @__PURE__ */ jsxRuntimeExports.jsxs("section", { children: [ 5229 /* @__PURE__ */ jsxRuntimeExports.jsx("h4", { className: "font-bold text-zinc-700 dark:text-zinc-300 mb-2 uppercase text-xs tracking-wider", children: "Navigation" }),
5230 /* @__PURE__ */ jsxRuntimeExports.jsxs("ul", { className: "space-y-2 text-sm text-zinc-600 dark:text-zinc-400", children: [ 5231 /* @__PURE__ */ jsxRuntimeExports.jsxs("li", { className: "flex justify-between", children: [ 5232 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-medium text-zinc-900 dark:text-zinc-100", children: "Middle Click + Drag" }), 5233 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Pan around the Piano Roll" }) 5234 ] }), 5235 /* @__PURE__ */ jsxRuntimeExports.jsxs("li", { className: "flex justify-between", children: [ 5236 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-medium text-zinc-900 dark:text-zinc-100", children: "Scroll Wheel" }), 5237 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Scroll Vertically" }) 5238 ] }), 5239 /* @__PURE__ */ jsxRuntimeExports.jsxs("li", { className: "flex justify-between", children: [
5240 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-medium text-zinc-900 dark:text-zinc-100", children: "Shift + Scroll Wheel" }), 5241 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Scroll Horizontally" }) 5242 ] }) 5243 ] }) 5244 ] }), 5245 /* @__PURE__ */ jsxRuntimeExports.jsxs("section", { children: [ 5246 /* @__PURE__ */ jsxRuntimeExports.jsx("h4", { className: "font-bold text-zinc-700 dark:text-zinc-300 mb-2 uppercase text-xs tracking-wider", children: "Note Editing" }), 5247 /* @__PURE__ */ jsxRuntimeExports.jsxs("ul", { className: "space-y-2 text-sm text-zinc-600 dark:text-zinc-400", children: [ 5248 /* @__PURE__ */ jsxRuntimeExports.jsxs("li", { className: "flex justify-between", children: [ 5249 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-medium text-zinc-900 dark:text-zinc-100", children: "Left Click (Grid)" }),
5250 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Add a new note / Drag to resize" }) 5251 ] }), 5252 /* @__PURE__ */ jsxRuntimeExports.jsxs("li", { className: "flex justify-between", children: [ 5253 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-medium text-zinc-900 dark:text-zinc-100", children: "Right Click (Note)" }), 5254 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Delete the note" }) 5255 ] }), 5256 /* @__PURE__ */ jsxRuntimeExports.jsxs("li", { className: "flex justify-between", children: [ 5257 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-medium text-zinc-900 dark:text-zinc-100", children: "Alt + Click (Note)" }), 5258 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Mute / Unmute the note" }) 5259 ] }), 5260 /* @__PURE__ */ jsxRuntimeExports.jsxs("li", { className: "flex justify-between", children: [
5261 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-medium text-zinc-900 dark:text-zinc-100", children: "Drag Note Ends" }), 5262 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Resize note duration" }) 5263 ] }), 5264 /* @__PURE__ */ jsxRuntimeExports.jsxs("li", { className: "flex justify-between", children: [ 5265 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-medium text-zinc-900 dark:text-zinc-100", children: "Ctrl + Click (Note)" }), 5266 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Toggle note selection" }) 5267 ] }), 5268 /* @__PURE__ */ jsxRuntimeExports.jsxs("li", { className: "flex justify-between", children: [ 5269 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-medium text-zinc-900 dark:text-zinc-100", children: "Shift + Drag (Note)" }), 5270 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Duplicate selected note(s)" }) 5271 ] }),
5272 /* @__PURE__ */ jsxRuntimeExports.jsxs("li", { className: "flex justify-between", children: [ 5273 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-medium text-zinc-900 dark:text-zinc-100", children: "Ctrl + Drag (Grid)" }), 5274 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Lasso select notes (Ctrl+Shift to append)" }) 5275 ] }) 5276 ] }) 5277 ] }), 5278 /* @__PURE__ */ jsxRuntimeExports.jsxs("section", { children: [ 5279 /* @__PURE__ */ jsxRuntimeExports.jsx("h4", { className: "font-bold text-zinc-700 dark:text-zinc-300 mb-2 uppercase text-xs tracking-wider", children: "Tools" }), 5280 /* @__PURE__ */ jsxRuntimeExports.jsxs("ul", { className: "space-y-2 text-sm text-zinc-600 dark:text-zinc-400", children: [ 5281 /* @__PURE__ */ jsxRuntimeExports.jsxs("li", { className: "flex justify-between", children: [ 5282 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-medium text-zinc-900 dark:text-zinc-100", children: "Draw Tool" }),
5283 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Default mode. Click to select, drag to move." }) 5284 ] }), 5285 /* @__PURE__ */ jsxRuntimeExports.jsxs("li", { className: "flex justify-between", children: [ 5286 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-medium text-zinc-900 dark:text-zinc-100", children: "Paint Tool" }), 5287 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Click and drag to draw a continuous line of notes." }) 5288 ] }), 5289 /* @__PURE__ */ jsxRuntimeExports.jsxs("li", { className: "flex justify-between", children: [ 5290 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-medium text-zinc-900 dark:text-zinc-100", children: "Delete / Backspace" }), 5291 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Delete currently selected notes" }) 5292 ] }) 5293 ] }) 5294 ] }),
5295 /* @__PURE__ */ jsxRuntimeExports.jsxs("section", { children: [ 5296 /* @__PURE__ */ jsxRuntimeExports.jsx("h4", { className: "font-bold text-zinc-700 dark:text-zinc-300 mb-2 uppercase text-xs tracking-wider", children: "Timeline" }), 5297 /* @__PURE__ */ jsxRuntimeExports.jsxs("ul", { className: "space-y-2 text-sm text-zinc-600 dark:text-zinc-400", children: [ 5298 /* @__PURE__ */ jsxRuntimeExports.jsxs("li", { className: "flex justify-between", children: [ 5299 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-medium text-zinc-900 dark:text-zinc-100", children: "Click on Timeline Ruler" }), 5300 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Add Tempo / Time Signature change" }) 5301 ] }), 5302 /* @__PURE__ */ jsxRuntimeExports.jsxs("li", { className: "flex justify-between", children: [ 5303 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-medium text-zinc-900 dark:text-zinc-100", children: "Right Click Marker" }),
5304 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Edit existing event" }) 5305 ] }), 5306 /* @__PURE__ */ jsxRuntimeExports.jsxs("li", { className: "flex justify-between", children: [ 5307 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-medium text-zinc-900 dark:text-zinc-100", children: "Drag Marker" }), 5308 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Move event along the timeline" }) 5309 ] }) 5310 ] }) 5311 ] }) 5312 ] }), 5313 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-8 text-center", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 5314 "button", 5315 { 5316 onClick: () => setShowShortcutsModal(false), 5317 className: "px-6 py-2 bg-indigo-500 hover:bg-indigo-600 text-white rounded font-bold transition-colors", 5318 children: "Got it!" 5319 } 5320 ) }) 5321 ] }) }),
5322 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `shrink-0 bg-zinc-100 dark:bg-zinc-950 overflow-hidden relative z-20 shadow-[2px_0_10px_rgba(0,0,0,0.1)] dark:shadow-[2px_0_10px_rgba(0,0,0,0.5)] border-r border-zinc-200 dark:border-zinc-800 flex flex-col ${isDrumMode ? "w-32" : "w-24"}`, children: [ 5323 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "h-8 shrink-0 border-b border-transparent w-full" }), 5324 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-full h-6 shrink-0 sticky top-0 bg-zinc-100/95 dark:bg-zinc-800/95 border-b border-zinc-300 dark:border-zinc-700 z-[60] backdrop-blur" }), 5325 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 relative", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 5326 "div", 5327 { 5328 ref: keyboardRef, 5329 className: "absolute w-full top-0 left-0", 5330 children: keys 5331 } 5332 ) }) 5333 ] }),
5334 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 min-w-0 flex flex-col focus:outline-none overflow-hidden", children: [ 5335 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "h-8 shrink-0 border-b border-zinc-300 dark:border-zinc-700 w-full z-[50]", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 5336 FLHorizontalScrollbar, 5337 { 5338 containerRef, 5339 zoomX, 5340 setZoomX, 5341 maxTicks, 5342 ppq, 5343 beatWidth: BEAT_WIDTH, 5344 mode: "piano-roll", 5345 patterns, 5346 activePatternId 5347 } 5348 ) }), 5349 /* @__PURE__ */ jsxRuntimeExports.jsxs( 5350 "div", 5351 { 5352 ref: containerRef, 5353 className: "flex-1 min-w-0 overflow-auto relative custom-scrollbar flex flex-col focus:outline-none", 5354 tabIndex: 0, 5355 onKeyDown: (e) => { 5356 if (e.key === "Delete" || e.key === "Backspace") { 5357 e.preventDefault(); 5358 deleteSelection(); 5359 } else if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "d") { 5360 e.preventDefault(); 5361 const selectedNotes = selectedNoteIds.map((idx) => notes[idx]).filter(Boolean); 5362 if (selectedNotes.length > 0) { 5363 const minTick = Math.min(...selectedNotes.map((n) => n.ticks)); 5364 const maxEndTick = Math.max(...selectedNotes.map((n) => n.ticks + n.durationTicks)); 5365 const blockDuration = maxEndTick - minTick; 5366 const duplicates = selectedNotes.map((n) => ({ 5367 midi: n.midi, 5368 ticks: n.ticks + blockDuration, 5369 durationTicks: n.durationTicks, 5370 velocity: n.velocity ?? 0.8, 5371 voice: n.voice || 1 5372 })); 5373 addNotes(activeTrackIndex, duplicates, "pianoroll"); 5374 setSelectedNoteIds(duplicates.map((_, i) => notes.length + i)); 5375 setTimeout(() => forceSync(), 0); 5376 } 5377 } 5378 }, 5379 onScroll: (e) => { 5380 const target = e.target; 5381 if (keyboardRef.current) { 5382 keyboardRef.current.style.transform = `translateY(-${target.scrollTop}px)`; 5383 } 5384 if (bottomPaneRef.current) { 5385 bottomPaneRef.current.scrollLeft = target.scrollLeft; 5386 } 5387 }, 5388 onWheel: (e) => { 5389 if (e.ctrlKey || e.metaKey) { 5390 const zoomFactor = e.deltaY > 0 ? -0.25 : 0.25; 5391 setZoomX((z) => Math.max(0.25, Math.min(4, z + zoomFactor))); 5392 } else if (e.shiftKey) { 5393 if (containerRef.current) { 5394 containerRef.current.scrollLeft += e.deltaY; 5395 } 5396 } 5397 }, 5398 children: [
5399 /* @__PURE__ */ jsxRuntimeExports.jsxs( 5400 "div", 5401 { 5402 className: "sticky top-0 h-6 shrink-0 bg-zinc-100/95 dark:bg-zinc-800/95 border-b border-zinc-300 dark:border-zinc-700 z-[45] cursor-pointer shadow-sm backdrop-blur", 5403 style: { width: totalWidth }, 5404 onClick: (e) => { 5405 if (e.target.closest(".meta-marker")) return; 5406 const rect = e.currentTarget.getBoundingClientRect(); 5407 const x = e.clientX - rect.left; 5408 const tick = Math.round(x / pxPerTick); 5409 setMetaModal({ tick }); 5410 }, 5411 onContextMenu: (e) => { 5412 if (e.target.closest(".meta-marker")) return; 5413 e.preventDefault(); 5414 e.stopPropagation(); 5415 const rect = e.currentTarget.getBoundingClientRect(); 5416 const x = e.clientX - rect.left; 5417 const tick = Math.round(x / pxPerTick); 5418 const actionsMenu = confirm("Add Loop Marker at this position?"); 5419 if (actionsMenu) { 5420 setLoopMarkerTick(tick); 5421 } else if (loopMarkerTick !== null) { 5422 const remove = confirm("Remove current Loop Marker?"); 5423 if (remove) { 5424 setLoopMarkerTick(null); 5425 } 5426 } 5427 }, 5428 children: [ 5429 measureLabels, 5430 engine.midi.header.tempos.map((t, i) => { 5431 const isDragging = (draggingMeta == null ? void 0 : draggingMeta.type) === "tempo" && draggingMeta.originalTick === t.ticks; 5432 const tickToRender = isDragging && metaDragTicks !== null ? metaDragTicks : t.ticks; 5433 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 5434 "div", 5435 { 5436 className: `meta-marker absolute top-0 flex flex-col items-center hover:bg-zinc-200 dark:hover:bg-zinc-700 rounded-sm px-1 cursor-ew-resize ${isDragging ? "z-50 opacity-80" : "z-10"}`, 5437 style: { 5438 left: tickToRender * pxPerTick, 5439 transform: tickToRender === 0 ? "translateX(0)" : "translateX(-50%)" 5440 }, 5441 onMouseDown: (e) => { 5442 e.stopPropagation(); 5443 setDragStartPos({ x: e.clientX, y: e.clientY }); 5444 setDraggingMeta({ type: "tempo", originalTick: t.ticks, bpm: t.bpm }); 5445 setMetaDragTicks(t.ticks); 5446 }, 5447 onContextMenu: (e) => { 5448 e.preventDefault(); 5449 e.stopPropagation(); 5450 setMetaType("tempo"); 5451 setMetaBpm(Math.round(t.bpm).toString()); 5452 setMetaModal({ tick: t.ticks, isEdit: true, originalTick: t.ticks }); 5453 }, 5454 children: [
5455 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-0 h-0 border-l-[4px] border-r-[4px] border-t-[6px] border-l-transparent border-r-transparent border-t-rose-500 mb-0.5" }), 5456 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] font-bold text-rose-600 dark:text-rose-400 bg-zinc-100/80 dark:bg-zinc-800/80 px-0.5 rounded leading-none", children: Math.round(t.bpm) }) 5457 ] 5458 }, 5459 `t-${i}` 5460 ); 5461 }), 5462 engine.midi.header.timeSignatures.map((ts, i) => { 5463 const isDragging = (draggingMeta == null ? void 0 : draggingMeta.type) === "timesig" && draggingMeta.originalTick === ts.ticks; 5464 const tickToRender = isDragging && metaDragTicks !== null ? metaDragTicks : ts.ticks; 5465 return /* @__PURE__ */ jsxRuntimeExports.jsx( 5466 "div", 5467 { 5468 className: `meta-marker absolute top-0 flex flex-col items-center hover:bg-zinc-200 dark:hover:bg-zinc-700 rounded-sm px-1 cursor-ew-resize ${isDragging ? "z-50 opacity-80" : "z-10"}`, 5469 style: { 5470 left: tickToRender * pxPerTick, 5471 transform: tickToRender === 0 ? "translateX(0)" : "translateX(-50%)", 5472 marginTop: "12px" 5473 }, 5474 onMouseDown: (e) => { 5475 e.stopPropagation(); 5476 setDragStartPos({ x: e.clientX, y: e.clientY }); 5477 setDraggingMeta({ type: "timesig", originalTick: ts.ticks, num: ts.timeSignature[0], den: ts.timeSignature[1] }); 5478 setMetaDragTicks(ts.ticks); 5479 }, 5480 onContextMenu: (e) => { 5481 e.preventDefault(); 5482 e.stopPropagation(); 5483 setMetaType("timesig"); 5484 setMetaNum(ts.timeSignature[0].toString()); 5485 setMetaDen(ts.timeSignature[1].toString()); 5486 setMetaModal({ tick: ts.ticks, isEdit: true, originalTick: ts.ticks }); 5487 }, 5488 children: /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[10px] font-bold text-blue-600 dark:text-blue-400 bg-zinc-100/80 dark:bg-zinc-800/80 px-0.5 rounded leading-none", children: [ 5489 ts.timeSignature[0], 5490 "/", 5491 ts.timeSignature[1] 5492 ] }) 5493 }, 5494 `ts-${i}` 5495 ); 5496 }), 5497 loopMarkerTick !== null && /* @__PURE__ */ jsxRuntimeExports.jsxs( 5498 "div", 5499 { 5500 className: "meta-marker absolute top-0 flex flex-col items-center z-[50]", 5501 style: { 5502 left: loopMarkerTick * pxPerTick, 5503 transform: "translateX(-50%)" 5504 }, 5505 onContextMenu: (e) => { 5506 e.preventDefault(); 5507 e.stopPropagation(); 5508 const remove = confirm("Remove Loop Marker?"); 5509 if (remove) { 5510 setLoopMarkerTick(null); 5511 } 5512 }, 5513 children: [
5514 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-0 h-0 border-l-[6px] border-r-[6px] border-t-[8px] border-l-transparent border-r-transparent border-t-emerald-500 mb-0.5" }), 5515 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[8px] font-bold text-emerald-600 dark:text-emerald-400 bg-zinc-100/90 dark:bg-zinc-800/90 px-1 rounded leading-none", children: "LOOP" }) 5516 ] 5517 } 5518 ) 5519 ] 5520 } 5521 ), 5522 /* @__PURE__ */ jsxRuntimeExports.jsxs( 5523 "div", 5524 { 5525 ref: gridRef, 5526 className: "relative bg-zinc-50 dark:bg-zinc-900 cursor-crosshair shrink-0", 5527 style: { width: totalWidth, height: actualNumKeys * KEY_HEIGHT }, 5528 onMouseDown: (e) => { 5529 if (!gridRef.current || !containerRef.current) return; 5530 containerRef.current.focus(); 5531 const rect = gridRef.current.getBoundingClientRect(); 5532 const x = e.clientX - rect.left; 5533 const y = e.clientY - rect.top; 5534 if (e.button === 1) { 5535 e.preventDefault(); 5536 setPanStart({ x: e.clientX, y: e.clientY, scrollX: containerRef.current.scrollLeft, scrollY: containerRef.current.scrollTop }); 5537 return; 5538 } 5539 if (toolMode === "paint" && e.button === 0) { 5540 setIsPainting(true); 5541 const snapTicks = gridSnap === 0 ? 1 : ppq * (4 / gridSnap); 5542 const rawTicks = Math.max(0, Math.round(x / pxPerTick)); 5543 const ticks = gridSnap === 0 ? rawTicks : Math.round(rawTicks / snapTicks) * snapTicks; 5544 const pitchIdx = Math.floor(y / KEY_HEIGHT); 5545 const midiNote = actualMaxNote - pitchIdx; 5546 if (midiNote >= actualMinNote && midiNote <= actualMaxNote) { 5547 const defaultDuration = gridSnap === 0 ? ppq : snapTicks; 5548 const existingNote = notes.find((n) => n.midi === midiNote && n.ticks === ticks); 5549 if (!existingNote) { 5550 const intervals = chordStamp !== "none" && !isDrumMode ? CHORDS[chordStamp] : [0]; 5551 const notesToAdd = []; 5552 intervals.forEach((interval) => { 5553 const chordMidi = midiNote + interval; 5554 if (chordMidi >= actualMinNote && chordMidi <= actualMaxNote) { 5555 notesToAdd.push({ 5556 midi: chordMidi, 5557 ticks, 5558 durationTicks: defaultDuration, 5559 velocity: 0.8, 5560 voice: activeVoice 5561 }); 5562 } 5563 }); 5564 addNotes(activeTrackIndex, notesToAdd, "pianoroll"); 5565 setLastPaintedTick(ticks); 5566 if (engine) { 5567 engine.startNotePreview(midiNote, track.channel); 5568 setGridPreviewMidi(midiNote); 5569 setTimeout(() => { 5570 engine.stopNotePreview(midiNote, track.channel); 5571 setGridPreviewMidi((prev) => prev === midiNote ? null : prev); 5572 }, 100); 5573 } 5574 } 5575 } 5576 return; 5577 } 5578 if (toolMode === "slice" && e.button === 0) { 5579 setSliceStart({ x, y, scrollX: containerRef.current.scrollLeft, scrollY: containerRef.current.scrollTop }); 5580 setSliceCurrent({ x, y }); 5581 return; 5582 } 5583 if (toolMode === "draw" && e.button === 0) { 5584 if (e.ctrlKey) { 5585 setLassoStart({ x, y, scrollX: containerRef.current.scrollLeft, scrollY: containerRef.current.scrollTop }); 5586 setLassoCurrent({ x, y }); 5587 if (!e.shiftKey) { 5588 setSelectedNoteIds([]); 5589 } 5590 return; 5591 } 5592 const snapTicks = gridSnap === 0 ? 1 : ppq * (4 / gridSnap); 5593 const rawTicks = Math.max(0, Math.round(x / pxPerTick)); 5594 const ticks = gridSnap === 0 ? rawTicks : Math.round(rawTicks / snapTicks) * snapTicks; 5595 const pitchIdx = Math.floor(y / KEY_HEIGHT); 5596 const midiNote = actualMaxNote - pitchIdx; 5597 if (midiNote >= actualMinNote && midiNote <= actualMaxNote) { 5598 const defaultDuration = lastNoteDurationRef.current ?? ppq; 5599 const intervals = chordStamp !== "none" && !isDrumMode ? CHORDS[chordStamp] : [0]; 5600 const notesToAdd = [];
5601 intervals.forEach((interval) => { 5602 const chordMidi = midiNote + interval; 5603 if (chordMidi >= actualMinNote && chordMidi <= actualMaxNote) { 5604 notesToAdd.push({ 5605 midi: chordMidi, 5606 ticks, 5607 durationTicks: defaultDuration, 5608 velocity: 0.8, 5609 voice: activeVoice 5610 }); 5611 } 5612 }); 5613 addNotes(activeTrackIndex, notesToAdd, "pianoroll"); 5614 if (engine) { 5615 engine.startNotePreview(midiNote, track.channel); 5616 auditionPitchRef.current = midiNote; 5617 setGridPreviewMidi(midiNote); 5618 } 5619 const newNoteIndices = []; 5620 notesToAdd.forEach((n) => { 5621 const foundIdx = notes.findIndex( 5622 (note) => note.midi === n.midi && note.ticks === n.ticks && note.durationTicks === n.durationTicks 5623 ); 5624 if (foundIdx !== -1) { 5625 newNoteIndices.push(foundIdx); 5626 } 5627 }); 5628 if (newNoteIndices.length > 0) { 5629 setSelectedNoteIds(newNoteIndices); 5630 const primaryNoteIdx = newNoteIndices[0]; 5631 const primaryNote = notes[primaryNoteIdx]; 5632 if (primaryNote) { 5633 setDraggingNoteIdx(primaryNoteIdx); 5634 setDragStartPos({ x: e.clientX, y: e.clientY }); 5635 setDragStartProps({ 5636 midi: primaryNote.midi, 5637 ticks: primaryNote.ticks, 5638 durationTicks: primaryNote.durationTicks 5639 }); 5640 setDragType("resize-right"); 5641 } 5642 } 5643 } 5644 return; 5645 } 5646 }, 5647 children: [ 5648 Array.from({ length: actualNumKeys }).map((_, i) => { 5649 const midi = actualMaxNote - i; 5650 const isC = !isDrumMode && midi % 12 === 0; 5651 const isBlack = isBlackKey(midi); 5652 let isInScale = false; 5653 if (!isDrumMode && scaleRoot !== null) { 5654 const noteClass = midi % 12; 5655 const intervals = SCALES[scaleType] || []; 5656 isInScale = intervals.some((interval) => (scaleRoot + interval) % 12 === noteClass); 5657 } 5658 const bgClass = isInScale ? isBlack ? "bg-indigo-500/5 dark:bg-indigo-500/10" : "bg-indigo-500/10 dark:bg-indigo-500/20" : isBlack ? "bg-zinc-100/50 dark:bg-zinc-800/20" : "transparent"; 5659 return /* @__PURE__ */ jsxRuntimeExports.jsxs(React.Fragment, { children: [ 5660 bgClass !== "transparent" && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `absolute w-full pointer-events-none ${bgClass}`, style: { top: i * KEY_HEIGHT, height: KEY_HEIGHT } }),
5661 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `absolute w-full h-px ${isC ? "bg-zinc-300 dark:bg-zinc-700" : "bg-zinc-200/60 dark:bg-zinc-800/40"}`, style: { top: i * KEY_HEIGHT } }) 5662 ] }, i); 5663 }), 5664 verticalLines, 5665 /* @__PURE__ */ jsxRuntimeExports.jsx( 5666 "div", 5667 { 5668 ref: playheadElRef, 5669 className: "absolute top-0 h-full w-[2px] bg-red-500 z-40 pointer-events-none", 5670 style: { left: playheadX, display: showPlayhead ? "block" : "none" } 5671 } 5672 ), 5673 loopMarkerTick !== null && /* @__PURE__ */ jsxRuntimeExports.jsx( 5674 "div", 5675 { 5676 className: "absolute top-0 h-full w-px bg-emerald-500/60 dark:bg-emerald-500/40 z-30 pointer-events-none border-l border-dashed border-emerald-400", 5677 style: { left: loopMarkerTick * pxPerTick } 5678 } 5679 ), 5680 showGhostNotes && engine.midi.tracks.map((t, tIdx) => { 5681 if (tIdx === activeTrackIndex) return null; 5682 if (ghostTrackVisibility[tIdx] === false) return null; 5683 return t.notes.map((note, nIdx) => { 5684 if (note.midi > actualMaxNote || note.midi < actualMinNote) return null; 5685 const top = (actualMaxNote - note.midi) * KEY_HEIGHT; 5686 const left = note.ticks * pxPerTick; 5687 const width = note.durationTicks * pxPerTick; 5688 const trackColor = t.color || getTrackColor(tIdx); 5689 return /* @__PURE__ */ jsxRuntimeExports.jsx( 5690 "div", 5691 { 5692 className: "absolute h-[14px] rounded-sm opacity-20 hover:opacity-50 cursor-pointer border transition-all z-0", 5693 style: { 5694 top: top + 1, 5695 left, 5696 width: Math.max(width, 4), 5697 backgroundColor: trackColor, 5698 borderColor: trackColor 5699 }, 5700 onMouseMove: (e) => { 5701 const noteStartText = formatTicksToFlTime(note.ticks, ppq); 5702 const noteDurText = formatDurationTicksToFlTime(note.durationTicks, ppq); 5703 const noteNameStr = isDrumMode ? GM_PERCUSSION_MAP[note.midi] || `Drum ${note.midi}` : getNoteName(note.midi); 5704 setHint({ 5705 label: `${t.name || `Track ${tIdx + 1}`}: ${noteNameStr}`, 5706 positionText: `${noteStartText} for ${noteDurText}`, 5707 noteText: `${noteNameStr} / ${note.midi}`, 5708 iconType: "note" 5709 }); 5710 }, 5711 onMouseLeave: () => { 5712 setHint(null); 5713 }, 5714 onDoubleClick: (e) => { 5715 e.stopPropagation(); 5716 e.preventDefault();
5717 setActiveTrackIndex(tIdx); 5718 const targetPattern = patterns[tIdx]; 5719 if (targetPattern) { 5720 setActivePatternId(targetPattern.id); 5721 const noteIdx = targetPattern.notes.findIndex( 5722 (n) => n.midi === note.midi && Math.abs(n.ticks - note.ticks) < 2 && Math.abs(n.durationTicks - note.durationTicks) < 2 5723 ); 5724 if (noteIdx !== -1) { 5725 setSelectedNoteIds([noteIdx]); 5726 } else { 5727 setSelectedNoteIds([]); 5728 } 5729 } 5730 } 5731 }, 5732 `ghost-${tIdx}-${nIdx}` 5733 ); 5734 }); 5735 }), 5736 notes.map((note, idx) => { 5737 if (note.midi > actualMaxNote || note.midi < actualMinNote) return null; 5738 const top = (actualMaxNote - note.midi) * KEY_HEIGHT; 5739 const left = note.ticks * pxPerTick; 5740 const width = note.durationTicks * pxPerTick; 5741 const isSelected = selectedNoteIds.includes(idx); 5742 const noteVoice = note.voice || 1; 5743 const isActiveVoice = noteVoice === activeVoice; 5744 const bgClass = isSelected ? "bg-[#ff5252] border-[#9e2a2a]" : noteVoice === 1 ? "bg-[#0072B2] border-[#005f9e]" : noteVoice === 2 ? "bg-[#009E73] border-[#00825f]" : noteVoice === 3 ? "bg-[#D55E00] border-[#b85100]" : "bg-[#CC79A7] border-[#b36a92]"; 5745 const hoverClass = isSelected ? "hover:bg-[#ff3333]" : noteVoice === 1 ? "hover:bg-[#005f9e]" : noteVoice === 2 ? "hover:bg-[#00825f]" : noteVoice === 3 ? "hover:bg-[#b85100]" : "hover:bg-[#b36a92]"; 5746 const shadowClass = isSelected ? "shadow-[0_0_10px_rgba(255,82,82,0.8)]" : noteVoice === 1 ? "shadow-[0_0_10px_rgba(0,114,178,0.8)]" : noteVoice === 2 ? "shadow-[0_0_10px_rgba(0,158,115,0.8)]" : noteVoice === 3 ? "shadow-[0_0_10px_rgba(213,94,0,0.8)]" : "shadow-[0_0_10px_rgba(204,121,167,0.8)]"; 5747 const isMuted = note.isMuted; 5748 const muteClass = isMuted ? "opacity-30 grayscale" : ""; 5749 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 5750 "div", 5751 { 5752 className: `absolute h-[14px] rounded border shadow cursor-move group ${bgClass} ${hoverClass} ${muteClass} ${draggingNoteIdx === idx ? `z-50 ${shadowClass}` : isSelected ? `z-40 ring-1 ring-red-500 shadow-[0_0_8px_rgba(255,82,82,0.6)]` : "z-10"} ${!isActiveVoice ? "opacity-30 pointer-events-none" : ""} flex items-center px-1 overflow-hidden`, 5753 style: { top: top + 1, left, width: Math.max(width, 4) }, 5754 onMouseMove: (e) => { 5755 e.stopPropagation(); 5756 const noteStartText = formatTicksToFlTime(note.ticks, ppq); 5757 const noteDurText = formatDurationTicksToFlTime(note.durationTicks, ppq); 5758 const noteNameStr = isDrumMode ? GM_PERCUSSION_MAP[note.midi] || `Drum ${note.midi}` : getNoteName(note.midi); 5759 setHint({ 5760 label: `Note: ${noteNameStr}`, 5761 positionText: `${noteStartText} for ${noteDurText}`, 5762 noteText: `${noteNameStr} / ${note.midi}`, 5763 iconType: "note" 5764 }); 5765 }, 5766 onMouseLeave: () => { 5767 setHint(null); 5768 }, 5769 onMouseDown: (e) => { 5770 var _a2; 5771 e.stopPropagation(); 5772 (_a2 = containerRef.current) == null ? void 0 : _a2.focus(); 5773 if (e.altKey && e.shiftKey) { 5774 updateNote(activeTrackIndex, idx, { isMuted: !isMuted }, "pianoroll"); 5775 return; 5776 } 5777 if (e.altKey || e.shiftKey) { 5778 const notesToDuplicate = selectedNoteIds.includes(idx) ? selectedNoteIds : [idx]; 5779 const clickedOriginal = notes[idx]; 5780 const duplicates = notesToDuplicate.map((id) => { 5781 const original = notes[id]; 5782 return { 5783 midi: original.midi, 5784 ticks: original.ticks, 5785 durationTicks: original.durationTicks, 5786 velocity: original.velocity ?? 0.8, 5787 voice: original.voice || 1 5788 }; 5789 }); 5790 addNotes(activeTrackIndex, duplicates, "pianoroll"); 5791 const newDuplicateIndices = duplicates.map((_, dIdx) => notes.length + dIdx); 5792 const clickedOriginalIndexInSelection = notesToDuplicate.indexOf(idx); 5793 const dragTargetIdx = clickedOriginalIndexInSelection !== -1 ? newDuplicateIndices[clickedOriginalIndexInSelection] : newDuplicateIndices[0]; 5794 if (newDuplicateIndices.length > 0) { 5795 setSelectedNoteIds(newDuplicateIndices); 5796 } 5797 if (dragTargetIdx !== void 0 && dragTargetIdx !== -1) { 5798 setDraggingNoteIdx(dragTargetIdx); 5799 setDragStartPos({ x: e.clientX, y: e.clientY }); 5800 setDragStartProps({ 5801 midi: clickedOriginal.midi, 5802 ticks: clickedOriginal.ticks, 5803 durationTicks: clickedOriginal.durationTicks 5804 }); 5805 setDragType("move"); 5806 setIsShiftDrag(e.shiftKey); 5807 setDragConstraint("none"); 5808 dragSelectedNotesRef.current = newDuplicateIndices.map((newIdx, dIdx) => { 5809 const origIdx = notesToDuplicate[dIdx]; 5810 const original = notes[origIdx]; 5811 return { 5812 idx: newIdx, 5813 ticks: original.ticks, 5814 midi: original.midi, 5815 durationTicks: original.durationTicks 5816 }; 5817 }); 5818 if (engine && !isMuted) { 5819 engine.startNotePreview(cl
5819ickedOriginal.midi, track.channel); 5820 auditionPitchRef.current = clickedOriginal.midi; 5821 setGridPreviewMidi(clickedOriginal.midi); 5822 } 5823 } 5824 } else { 5825 if (e.ctrlKey) { 5826 if (selectedNoteIds.includes(idx)) { 5827 setSelectedNoteIds(selectedNoteIds.filter((id) => id !== idx)); 5828 } else { 5829 setSelectedNoteIds([...selectedNoteIds, idx]); 5830 } 5831 } else if (!selectedNoteIds.includes(idx)) { 5832 setSelectedNoteIds([idx]); 5833 } 5834 setDraggingNoteIdx(idx); 5835 setDragStartPos({ x: e.clientX, y: e.clientY }); 5836 setDragStartProps({ midi: note.midi, ticks: note.ticks, durationTicks: note.durationTicks }); 5837 setDragType("move"); 5838 setIsShiftDrag(false); 5839 setDragConstraint("none"); 5840 const selectedIds = selectedNoteIds.includes(idx) ? selectedNoteIds : [idx]; 5841 dragSelectedNotesRef.current = selectedIds.map((sIdx) => ({ 5842 idx: sIdx, 5843 ticks: notes[sIdx].ticks, 5844 midi: notes[sIdx].midi, 5845 durationTicks: notes[sIdx].durationTicks 5846 })); 5847 if (engine && !isMuted) { 5848 engine.startNotePreview(note.midi, track.channel); 5849 auditionPitchRef.current = note.midi; 5850 setGridPreviewMidi(note.midi); 5851 } 5852 } 5853 }, 5854 onMouseUp: () => { 5855 if (engine) { 5856 engine.stopNotePreview(note.midi, track.channel); 5857 } 5858 auditionPitchRef.current = null; 5859 setGridPreviewMidi(null); 5860 }, 5861 onContextMenu: (e) => { 5862 e.preventDefault(); 5863 e.stopPropagation(); 5864 deleteNote(activeTrackIndex, idx, "pianoroll"); 5865 forceSync(); 5866 }, 5867 onMouseEnter: (e) => { 5868 if ((e.buttons & 2) !== 0) { 5869 deleteNote(activeTrackIndex, idx, "pianoroll"); 5870 forceSync(); 5871 } 5872 }, 5873 children: [
5874 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[9px] font-bold text-white/95 select-none pointer-events-none truncate z-20", children: isDrumMode ? GM_PERCUSSION_MAP[note.midi] || `Drum ${note.midi}` : getNoteName(note.midi) }), 5875 /* @__PURE__ */ jsxRuntimeExports.jsx( 5876 "div", 5877 { 5878 className: "absolute left-0 top-0 w-2 h-full cursor-ew-resize opacity-0 group-hover:opacity-100 bg-white/20 z-30", 5879 onMouseDown: (e) => { 5880 e.stopPropagation(); 5881 setDraggingNoteIdx(idx); 5882 setDragStartPos({ x: e.clientX, y: e.clientY }); 5883 setDragStartProps({ midi: note.midi, ticks: note.ticks, durationTicks: note.durationTicks }); 5884 setDragType("resize-left"); 5885 } 5886 } 5887 ),
5888 /* @__PURE__ */ jsxRuntimeExports.jsx( 5889 "div", 5890 { 5891 className: "absolute right-0 top-0 w-2 h-full cursor-ew-resize opacity-0 group-hover:opacity-100 bg-white/20 z-30", 5892 onMouseDown: (e) => { 5893 e.stopPropagation(); 5894 setDraggingNoteIdx(idx); 5895 setDragStartPos({ x: e.clientX, y: e.clientY }); 5896 setDragStartProps({ midi: note.midi, ticks: note.ticks, durationTicks: note.durationTicks }); 5897 setDragType("resize-right"); 5898 } 5899 } 5900 ) 5901 ] 5902 }, 5903 `${midiVersion}-${idx}` 5904 ); 5905 }), 5906 lassoStart && lassoCurrent && /* @__PURE__ */ jsxRuntimeExports.jsx( 5907 "div", 5908 { 5909 className: "absolute border border-indigo-500 bg-indigo-500/20 z-50 pointer-events-none", 5910 style: { 5911 left: Math.min(lassoStart.x, lassoCurrent.x), 5912 top: Math.min(lassoStart.y, lassoCurrent.y), 5913 width: Math.abs(lassoCurrent.x - lassoStart.x), 5914 height: Math.abs(lassoCurrent.y - lassoStart.y) 5915 } 5916 } 5917 ), 5918 sliceStart && sliceCurrent && /* @__PURE__ */ jsxRuntimeExports.jsx("svg", { className: "absolute inset-0 pointer-events-none z-[100]", style: { width: totalWidth, height: actualNumKeys * KEY_HEIGHT }, children: /* @__PURE__ */ jsxRuntimeExports.jsx( 5919 "line", 5920 { 5921 x1: sliceStart.x, 5922 y1: sliceStart.y, 5923 x2: sliceCurrent.x, 5924 y2: sliceCurrent.y, 5925 stroke: "#f43f5e", 5926 strokeWidth: "2", 5927 strokeDasharray: "4 2" 5928 } 5929 ) }) 5930 ] 5931 } 5932 ) 5933 ] 5934 } 5935 ),
5936 contextMenuPos && /* @__PURE__ */ jsxRuntimeExports.jsx( 5937 ContextMenu, 5938 { 5939 x: contextMenuPos.x, 5940 y: contextMenuPos.y, 5941 onClose: () => setContextMenuPos(null), 5942 toolMode, 5943 setToolMode, 5944 scaleRoot, 5945 setScaleRoot, 5946 scaleType, 5947 setScaleType, 5948 chordStamp, 5949 setChordStamp, 5950 gridSnap, 5951 setGridSnap, 5952 isDrumMode 5953 } 5954 ), 5955 metaModal && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "fixed inset-0 z-[100] flex items-center justify-center bg-black/50 backdrop-blur-sm", onClick: () => setMetaModal(null), onContextMenu: (e) => e.preventDefault(), children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-white dark:bg-zinc-900 p-5 rounded-lg shadow-2xl border border-zinc-200 dark:border-zinc-800 w-80", onClick: (e) => e.stopPropagation(), children: [
5956 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "font-bold text-lg mb-4 text-zinc-900 dark:text-zinc-100", children: metaModal.isEdit ? "Edit Timeline Event" : "Add Timeline Event" }), 5957 !metaModal.isEdit && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-4 mb-4", children: [ 5958 /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "flex items-center gap-2 cursor-pointer text-sm text-zinc-700 dark:text-zinc-300", children: [ 5959 /* @__PURE__ */ jsxRuntimeExports.jsx("input", { type: "radio", checked: metaType === "tempo", onChange: () => setMetaType("tempo"), className: "accent-indigo-500" }), 5960 " Tempo" 5961 ] }), 5962 /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "flex items-center gap-2 cursor-pointer text-sm text-zinc-700 dark:text-zinc-300", children: [ 5963 /* @__PURE__ */ jsxRuntimeExports.jsx("input", { type: "radio", checked: metaType === "timesig", onChange: () => setMetaType("timesig"), className: "accent-indigo-500" }), 5964 " Time Signature" 5965 ] }) 5966 ] }), 5967 metaType === "tempo" ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mb-4", children: [
5968 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-xs font-bold text-zinc-500 mb-1", children: "BPM" }), 5969 /* @__PURE__ */ jsxRuntimeExports.jsx("input", { type: "number", value: metaBpm, onChange: (e) => setMetaBpm(e.target.value), className: "w-full bg-zinc-100 dark:bg-zinc-950 border border-zinc-300 dark:border-zinc-700 rounded px-3 py-2 text-zinc-900 dark:text-zinc-100 outline-none focus:border-indigo-500" }) 5970 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mb-4 flex gap-2", children: [ 5971 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1", children: [ 5972 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-xs font-bold text-zinc-500 mb-1", children: "Numerator" }), 5973 /* @__PURE__ */ jsxRuntimeExports.jsx("input", { type: "number", value: metaNum, onChange: (e) => setMetaNum(e.target.value), className: "w-full bg-zinc-100 dark:bg-zinc-950 border border-zinc-300 dark:border-zinc-700 rounded px-3 py-2 text-zinc-900 dark:text-zinc-100 outline-none focus:border-indigo-500" }) 5974 ] }),
5975 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1", children: [ 5976 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-xs font-bold text-zinc-500 mb-1", children: "Denominator" }), 5977 /* @__PURE__ */ jsxRuntimeExports.jsx("input", { type: "number", value: metaDen, onChange: (e) => setMetaDen(e.target.value), className: "w-full bg-zinc-100 dark:bg-zinc-950 border border-zinc-300 dark:border-zinc-700 rounded px-3 py-2 text-zinc-900 dark:text-zinc-100 outline-none focus:border-indigo-500" }) 5978 ] }) 5979 ] }), 5980 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between items-center mt-6", children: [ 5981 metaModal.isEdit ? /* @__PURE__ */ jsxRuntimeExports.jsx( 5982 "button", 5983 { 5984 onClick: () => { 5985 if (metaType === "tempo") engine.deleteTempo(metaModal.originalTick); 5986 else engine.deleteTimeSignature(metaModal.originalTick); 5987 forceSync(); 5988 setMetaModal(null); 5989 }, 5990 className: "px-3 py-2 text-red-500 hover:text-red-600 text-sm font-bold transition-colors", 5991 children: "Delete" 5992 } 5993 ) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", {}),
5994 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2", children: [ 5995 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => setMetaModal(null), className: "px-4 py-2 text-sm font-bold text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-zinc-100", children: "Cancel" }), 5996 /* @__PURE__ */ jsxRuntimeExports.jsx( 5997 "button", 5998 { 5999 onClick: () => { 6000 if (metaType === "tempo") { 6001 const bpm = parseFloat(metaBpm); 6002 if (!isNaN(bpm) && bpm > 0) { 6003 if (metaModal.isEdit) engine.updateTempo(metaModal.originalTick, metaModal.tick, bpm); 6004 else engine.addTempo(metaModal.tick, bpm); 6005 forceSync(); 6006 } 6007 } else { 6008 const num = parseInt(metaNum); 6009 const den = parseInt(metaDen); 6010 if (!isNaN(num) && !isNaN(den) && num > 0 && den > 0) { 6011 if (metaModal.isEdit) engine.updateTimeSignature(metaModal.originalTick, metaModal.tick, num, den); 6012 else engine.addTimeSignature(metaModal.tick, num, den); 6013 forceSync(); 6014 } 6015 } 6016 setMetaModal(null); 6017 }, 6018 className: "px-4 py-2 bg-indigo-500 hover:bg-indigo-600 text-white rounded text-sm font-bold shadow-sm transition-colors", 6019 children: metaModal.isEdit ? "Save Changes" : "Save Event" 6020 } 6021 ) 6022 ] }) 6023 ] }) 6024 ] }) }) 6025 ] }) 6026 ] 6027 } 6028 ) 6029 ] }), 6030 /* @__PURE__ */ jsxRuntimeExports.jsx(Qt, { className: "h-2 bg-zinc-200 dark:bg-zinc-800 cursor-row-resize flex items-center justify-center z-50", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-8 h-1 rounded-full bg-zinc-300 dark:bg-zinc-700" }) }), 6031 /* @__PURE__ */ jsxRuntimeExports.jsxs(Yt, { defaultSize: 20, minSize: 10, maxSize: 50, className: "bg-white dark:bg-zinc-900 border-t border-zinc-200 dark:border-zinc-800 relative select-none z-10 flex", children: [
6032 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `h-full shrink-0 border-r border-zinc-200 dark:border-zinc-800 bg-zinc-100 dark:bg-zinc-950 p-2 flex flex-col gap-2 z-20 ${isDrumMode ? "w-32" : "w-24"}`, children: /* @__PURE__ */ jsxRuntimeExports.jsxs( 6033 "select", 6034 { 6035 value: bottomPaneMode, 6036 onChange: (e) => setBottomPaneMode(e.target.value), 6037 className: "w-full bg-zinc-200 dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-700 rounded text-[10px] p-1 font-bold text-zinc-600 dark:text-zinc-300 outline-none cursor-pointer", 6038 children: [ 6039 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "velocity", children: "Note velocity" }), 6040 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "release", children: "Note release" }), 6041 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "pan", children: "Note panning" }), 6042 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "pitch", children: "Note pitch" }),
6043 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "modX", children: "Note mod X" }), 6044 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "modY", children: "Note mod Y" }), 6045 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "shift", children: "Note shift" }) 6046 ] 6047 } 6048 ) }), 6049 /* @__PURE__ */ jsxRuntimeExports.jsxs( 6050 "div", 6051 { 6052 ref: bottomPaneRef, 6053 className: "flex-1 min-w-0 overflow-x-hidden overflow-y-hidden relative custom-scrollbar bg-zinc-50 dark:bg-zinc-900/40", 6054 onMouseDown: (e) => { 6055 const rect = e.currentTarget.getBoundingClientRect(); 6056 const x = e.clientX - rect.left; 6057 const y = e.clientY - rect.top; 6058 const val = Math.max(0, Math.min(1, 1 - y / rect.height)); 6059 const idx = findNoteAtX(x); 6060 if (idx !== -1) { 6061 setDraggingVelocityIdx(idx); 6062 dragStartValRef.current = val; 6063 const isSelected = selectedNoteIds.includes(idx); 6064 if (isSelected) { 6065 dragSelectedNoteValsRef.current = selectedNoteIds.map((sIdx) => ({ 6066 idx: sIdx, 6067 startVal: getNotePropertyValue(notes[sIdx], bottomPaneMode) 6068 })); 6069 } else { 6070 dragSelectedNoteValsRef.current = [{ 6071 idx, 6072 startVal: getNotePropertyValue(notes[idx], bottomPaneMode) 6073 }]; 6074 } 6075 updateNote(activeTrackIndex, idx, getPropertyUpdateObject(bottomPaneMode, val), "pianoroll"); 6076 } 6077 }, 6078 onMouseMove: (e) => { 6079 const rect = e.currentTarget.getBoundingClientRect(); 6080 const x = e.clientX - rect.left; 6081 const y = e.clientY - rect.top; 6082 const val = Math.max(0, Math.min(1, 1 - y / rect.height)); 6083 if (draggingVelocityIdx !== null) { 6084 if (dragStartValRef.current !== null && dragSelectedNoteValsRef.current.length > 0) { 6085 const delta = val - dragStartValRef.current; 6086 dragSelectedNoteValsRef.current.forEach((item) => { 6087 const newVal = Math.max(0, Math.min(1, item.startVal + delta)); 6088 updateNote(activeTrackIndex, item.idx, getPropertyUpdateObject(bottomPaneMode, newVal), "pianoroll"); 6089 }); 6090 } else { 6091 updateNote(activeTrackIndex, draggingVelocityIdx, getPropertyUpdateObject(bottomPaneMode, val), "pianoroll"); 6092 } 6093 setHint({ 6094 label: formatTooltipValue(bottomPaneMode, val), 6095 positionText: "", 6096 noteText: "", 6097 iconType: "velocity" 6098 }); 6099 } else { 6100 const idx = findNoteAtX(x); 6101 if (idx !== -1) { 6102 const noteVal = getNotePropertyValue(notes[idx], bottomPaneMode); 6103 const noteNameStr = isDrumMode ? GM_PERCUSSION_MAP[notes[idx].midi] || `Drum ${notes[idx].midi}` : getNoteName(notes[idx].midi); 6104 setHint({ 6105 label: `${noteNameStr}: ${formatTooltipValue(bottomPaneMode, noteVal)}`, 6106 positionText: `Start: ${formatTicksToFlTime(notes[idx].ticks, ppq)}`, 6107 noteText: `${noteNameStr} / ${notes[idx].midi}`, 6108 iconType: "velocity" 6109 }); 6110 } 6111 } 6112 }, 6113 onMouseUp: () => { 6114 setDraggingVelocityIdx(null); 6115 dragStartValRef.current = null; 6116 dragSelectedNoteValsRef.current = []; 6117 }, 6118 onMouseLeave: () => { 6119 setDraggingVelocityIdx(null); 6120 dragStartValRef.current = null; 6121 dragSelectedNoteValsRef.current = []; 6122 setHint(null); 6123 }, 6124 children: [
6125 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute top-0 h-full flex pointer-events-none", style: { width: totalWidth }, children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `w-full h-1/2 border-b ${bottomPaneMode === "pan" || bottomPaneMode === "pitch" || bottomPaneMode === "shift" ? "border-zinc-400 dark:border-zinc-500 border-dashed z-10" : "border-zinc-200/50 dark:border-zinc-800/50"}` }) }), 6126 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute top-0 h-full", style: { width: totalWidth }, children: notes.map((note, idx) => { 6127 const left = note.ticks * pxPerTick; 6128 const isSelected = selectedNoteIds.includes(idx); 6129 const val = getNotePropertyValue(note, bottomPaneMode); 6130 const isBipolar = bottomPaneMode === "pan" || bottomPaneMode === "pitch" || bottomPaneMode === "shift"; 6131 const noteVoice = note.voice || 1; 6132 const stalkBgClass = isSelected ? "bg-[#ff5252]" : noteVoice === 1 ? "bg-[#0072B2]" : noteVoice === 2 ? "bg-[#009E73]" : noteVoice === 3 ? "bg-[#D55E00]" : "bg-[#CC79A7]"; 6133 const handleBgClass = isSelected ? "bg-[#ff5252] border-white shadow-[0_0_6px_rgba(255,82,82,0.6)]" : noteVoice === 1 ? "bg-[#0072B2] border-white" : noteVoice === 2 ? "bg-[#009E73] border-white" : noteVoice === 3 ? "bg-[#D55E00] border-white" : "bg-[#CC79A7] border-white"; 6134 let stalkStyle = {}; 6135 if (isBipolar) { 6136 if (val >= 0.5) { 6137 stalkStyle = { 6138 bottom: "50%", 6139 height: `${(val - 0.5) * 100}%` 6140 }; 6141 } else { 6142 stalkStyle = { 6143 top: "50%", 6144 height: `${(0.5 - val) * 100}%` 6145 }; 6146 } 6147 } else { 6148 stalkStyle = { 6149 bottom: 0, 6150 height: `${val * 100}%` 6151 }; 6152 } 6153 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 6154 "div", 6155 { 6156 className: "absolute bottom-0 w-3 flex justify-center group cursor-ns-resize z-20", 6157 style: { left: left - 2, height: "100%" }, 6158 onMouseDown: (e) => { 6159 var _a2; 6160 e.stopPropagation(); 6161 setDraggingVelocityIdx(idx); 6162 const rect = (_a2 = bottomPaneRef.current) == null ? void 0 : _a2.getBoundingClientRect(); 6163 if (rect) { 6164 const y = e.clientY - rect.top; 6165 const v = Math.max(0, Math.min(1, 1 - y / rect.height)); 6166 dragStartValRef.current = v; 6167 } else { 6168 dragStartValRef.current = val; 6169 } 6170 const isSel = selectedNoteIds.includes(idx); 6171 if (isSel) { 6172 dragSelectedNoteValsRef.current = selectedNoteIds.map((sIdx) => ({ 6173 idx: sIdx, 6174 startVal: getNotePropertyValue(notes[sIdx], bottomPaneMode) 6175 })); 6176 } else { 6177 dragSelectedNoteValsRef.current = [{ 6178 idx, 6179 startVal: val 6180 }]; 6181 } 6182 }, 6183 children: [
6184 /* @__PURE__ */ jsxRuntimeExports.jsx( 6185 "div", 6186 { 6187 className: `absolute w-[2px] transition-colors ${stalkBgClass}`, 6188 style: stalkStyle 6189 } 6190 ), 6191 /* @__PURE__ */ jsxRuntimeExports.jsx( 6192 "div", 6193 { 6194 className: `absolute w-2.5 h-2.5 rounded-full border shadow-sm transition-colors ${handleBgClass}`, 6195 style: { bottom: `calc(${val * 100}% - 5px)` } 6196 } 6197 ), 6198 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "absolute -top-6 bg-zinc-800 text-white text-[9px] px-1.5 py-0.5 rounded opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity z-50 whitespace-nowrap shadow-md", children: formatTooltipValue(bottomPaneMode, val) }) 6199 ] 6200 }, 6201 `vel-${idx}` 6202 ); 6203 }) }) 6204 ] 6205 } 6206 ) 6207 ] }) 6208 ] }); 6209}; 6210const ScoreRibbon = ({ 6211 currentTool, 6212 onSelectTool, 6213 isPlaying, 6214 onTogglePlay, 6215 onStop, 6216 onZoomIn, 6217 onZoomOut, 6218 showMeasureNumbers, 6219 onToggleMeasureNumbers, 6220 currentAccidental, 6221 onSelectAccidental, 6222 isPercussion, 6223 showKeypad, 6224 onToggleKeypad, 6225 showTextEditor, 6226 onToggleTextEditor, 6227 entryMode, 6228 onSelectEntryMode, 6229 isDotted, 6230 onToggleDotted, 6231 isTieActive, 6232 onToggleTieActive, 6233 isTriplet, 6234 onToggleTriplet, 6235 currentArticulation, 6236 onSelectArticulation, 6237 showGrid = true, 6238 onToggleGrid, 6239 onStartTour 6240}) => { 6241 const [activeTab, setActiveTab] = reactExports.useState("Home"); 6242 const svgPaths = { 6243 whole: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-6 h-6 flex items-center justify-center", children: /* @__PURE__ */ jsxRuntimeExports.jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsxRuntimeExports.jsx("ellipse", { cx: "12", cy: "12", rx: "7", ry: "4", fill: "none", stroke: "currentColor", strokeWidth: "2", transform: "rotate(-15 12 12)" }) }) }), 6244 half: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-6 h-6 flex items-center justify-center", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { width: "14", height: "20", viewBox: "0 0 24 36", children: [
6245 /* @__PURE__ */ jsxRuntimeExports.jsx("ellipse", { cx: "9", cy: "24", rx: "6", ry: "4", fill: "none", stroke: "currentColor", strokeWidth: "2", transform: "rotate(-15 9 24)" }), 6246 /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: "14", y1: "24", x2: "14", y2: "4", stroke: "currentColor", strokeWidth: "2" }) 6247 ] }) }), 6248 quarter: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-6 h-6 flex items-center justify-center", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { width: "14", height: "20", viewBox: "0 0 24 36", children: [ 6249 /* @__PURE__ */ jsxRuntimeExports.jsx("ellipse", { cx: "9", cy: "24", rx: "6", ry: "4", fill: "currentColor", transform: "rotate(-15 9 24)" }), 6250 /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: "14", y1: "24", x2: "14", y2: "4", stroke: "currentColor", strokeWidth: "2" }) 6251 ] }) }), 6252 eighth: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-6 h-6 flex items-center justify-center", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { width: "14", height: "20", viewBox: "0 0 24 36", children: [
6253 /* @__PURE__ */ jsxRuntimeExports.jsx("ellipse", { cx: "9", cy: "24", rx: "6", ry: "4", fill: "currentColor", transform: "rotate(-15 9 24)" }), 6254 /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: "14", y1: "24", x2: "14", y2: "4", stroke: "currentColor", strokeWidth: "2" }), 6255 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M14 4 C 20 8, 18 16, 16 20", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }) 6256 ] }) }), 6257 sixteenth: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-6 h-6 flex items-center justify-center", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { width: "14", height: "20", viewBox: "0 0 24 36", children: [ 6258 /* @__PURE__ */ jsxRuntimeExports.jsx("ellipse", { cx: "9", cy: "24", rx: "6", ry: "4", fill: "currentColor", transform: "rotate(-15 9 24)" }), 6259 /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: "14", y1: "24", x2: "14", y2: "4", stroke: "currentColor", strokeWidth: "2" }), 6260 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M14 4 C 20 8, 18 16, 16 20 M14 12 C 20 16, 18 24, 16 28", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }) 6261 ] }) }), 6262 sharp: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-base font-serif leading-none", children: "â¯" }), 6263 flat: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-base font-serif leading-none", children: "â" }), 6264 natural: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-base font-serif leading-none", children: "â®" }), 6265 tie: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-6 h-6 flex items-center justify-center", children: /* @__PURE__ */ jsxRuntimeExports.jsx("svg", { width: "20", height: "10", viewBox: "0 0 32 16", children: /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M4 12 C 12 0, 20 0, 28 12", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }) }) }), 6266 dot: /* @__PURE__ */ jsxRuntimeExports.jsx("svg", { width: "6", height: "6", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "12", cy: "12", r: "6", fill: "currentColor" }) }), 6267 staccato: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-6 h-6 flex items-center justify-center", children: /* @__PURE__ */ jsxRuntimeExports.jsx("svg", { width: "6", height: "6", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "12", cy: "12", r: "6", fill: "currentColor" }) }) }), 6268 tenuto: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-6 h-6 flex items-center justify-center", children: /* @__PURE__ */ jsxRuntimeExports.jsx("svg", { width: "12", height: "6", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: "4", y: "10", width: "16", height: "4", fill: "currentColor" }) }) }), 6269 accent: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-6 h-6 flex items-center justify-center", children: /* @__PURE__ */ jsxRuntimeExports.jsx("svg", { width: "12", height: "12", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M4 6 L20 12 L4 18 L6 12 Z", fill: "currentColor" }) }) }), 6270 fermata: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-6 h-6 flex items-center justify-center", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", children: [
6271 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M4 14 C 4 4, 20 4, 20 14", fill: "none", stroke: "currentColor", strokeWidth: "2" }), 6272 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "12", cy: "14", r: "2", fill: "currentColor" }) 6273 ] }) }) 6274 }; 6275 const RibbonTab = ({ label }) => /* @__PURE__ */ jsxRuntimeExports.jsx( 6276 "button", 6277 { 6278 onClick: () => setActiveTab(label), 6279 className: `px-3 py-1 text-xs font-medium transition-colors ${activeTab === label ? "bg-[#f0f0f0] dark:bg-[#1e1e1e] border-t-2 border-indigo-500 text-indigo-600 dark:text-indigo-400" : "text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-zinc-200"}`, 6280 children: label 6281 } 6282 ); 6283 const Group = ({ title, children }) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col h-full border-r border-zinc-300 dark:border-zinc-700 px-2 min-w-min justify-between py-1", children: [ 6284 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 flex items-center justify-center gap-1", children }), 6285 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[9px] text-zinc-500 text-center flex items-center justify-center uppercase tracking-wider bg-zinc-100 dark:bg-zinc-800/50 rounded-sm w-full", children: title }) 6286 ] }); 6287 const RibbonBtn = ({ icon, label, onClick, isActive, large = false }) => /* @__PURE__ */ jsxRuntimeExports.jsxs( 6288 "button", 6289 { 6290 onClick, 6291 className: `flex ${large ? "flex-col justify-center px-2" : "items-center px-1.5 py-0.5"} gap-0.5 rounded hover:bg-zinc-200 dark:hover:bg-zinc-700 transition-colors ${isActive ? "bg-indigo-100 dark:bg-indigo-500/30 text-indigo-600 dark:text-indigo-400" : "text-zinc-700 dark:text-zinc-300"} ${large ? "h-10" : "h-6"}`, 6292 children: [
6293 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { children: icon }), 6294 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `text-[10px] ${large ? "text-center leading-tight" : ""}`, children: label }) 6295 ] 6296 } 6297 ); 6298 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col w-full bg-[#e5e5e5] dark:bg-[#2d2d2d] border-b border-zinc-300 dark:border-zinc-800 select-none shrink-0", children: [ 6299 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex px-2 pt-1 gap-1", children: [ 6300 /* @__PURE__ */ jsxRuntimeExports.jsx(RibbonTab, { label: "Home" }), 6301 /* @__PURE__ */ jsxRuntimeExports.jsx(RibbonTab, { label: "Note Input" }), 6302 /* @__PURE__ */ jsxRuntimeExports.jsx(RibbonTab, { label: "Play" }), 6303 /* @__PURE__ */ jsxRuntimeExports.jsx(RibbonTab, { label: "View" }) 6304 ] }), 6305 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "h-16 bg-[#f0f0f0] dark:bg-[#1e1e1e] flex items-stretch px-1 overflow-x-auto", children: [ 6306 activeTab === "Home" && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
6307 /* @__PURE__ */ jsxRuntimeExports.jsx(Group, { title: "Select", children: /* @__PURE__ */ jsxRuntimeExports.jsx(RibbonBtn, { large: true, icon: /* @__PURE__ */ jsxRuntimeExports.jsx(MousePointer2, { size: 24 }), label: "Select", onClick: () => onSelectTool("select"), isActive: currentTool === "select" }) }), 6308 /* @__PURE__ */ jsxRuntimeExports.jsx(Group, { title: "Edit", children: /* @__PURE__ */ jsxRuntimeExports.jsx(RibbonBtn, { large: true, icon: /* @__PURE__ */ jsxRuntimeExports.jsx(Eraser, { size: 24 }), label: "Erase", onClick: () => onSelectTool("erase"), isActive: currentTool === "erase" }) }), 6309 onStartTour && /* @__PURE__ */ jsxRuntimeExports.jsx(Group, { title: "Help", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 6310 RibbonBtn, 6311 { 6312 large: true, 6313 icon: /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", className: "text-indigo-400", children: [
6314 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "12", cy: "12", r: "10" }), 6315 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" }), 6316 /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: "12", y1: "17", x2: "12.01", y2: "17" }) 6317 ] }), 6318 label: "Tour", 6319 onClick: onStartTour 6320 } 6321 ) }) 6322 ] }), 6323 activeTab === "Note Input" && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 6324 /* @__PURE__ */ jsxRuntimeExports.jsxs(Group, { title: "Mode", children: [ 6325 /* @__PURE__ */ jsxRuntimeExports.jsx(RibbonBtn, { large: true, icon: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-lg", children: "â©" }), label: "Note", onClick: () => onSelectEntryMode("note"), isActive: entryMode === "note" }), 6326 /* @__PURE__ */ jsxRuntimeExports.jsx(RibbonBtn, { large: true, icon: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-lg", children: "ð¾" }), label: "Rest", onClick: () => onSelectEntryMode("rest"), isActive: entryMode === "rest" }) 6327 ] }),
6328 /* @__PURE__ */ jsxRuntimeExports.jsxs(Group, { title: "Notes/Rests", children: [ 6329 /* @__PURE__ */ jsxRuntimeExports.jsx(RibbonBtn, { large: true, icon: svgPaths.whole, label: "Whole", onClick: () => onSelectTool("note_whole"), isActive: currentTool === "note_whole" }), 6330 /* @__PURE__ */ jsxRuntimeExports.jsx(RibbonBtn, { large: true, icon: svgPaths.half, label: "Half", onClick: () => onSelectTool("note_half"), isActive: currentTool === "note_half" }), 6331 /* @__PURE__ */ jsxRuntimeExports.jsx(RibbonBtn, { large: true, icon: svgPaths.quarter, label: "Quarter", onClick: () => onSelectTool("note_quarter"), isActive: currentTool === "note_quarter" }), 6332 /* @__PURE__ */ jsxRuntimeExports.jsx(RibbonBtn, { large: true, icon: svgPaths.eighth, label: "Eighth", onClick: () => onSelectTool("note_eighth"), isActive: currentTool === "note_eighth" }), 6333 /* @__PURE__ */ jsxRuntimeExports.jsx(RibbonBtn, { large: true, icon: svgPaths.sixteenth, label: "16th", onClick: () => onSelectTool("note_sixteenth"), isActive: currentTool === "note_sixteenth" }) 6334 ] }),
6335 /* @__PURE__ */ jsxRuntimeExports.jsx(Group, { title: "Accidentals", children: !isPercussion ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 6336 /* @__PURE__ */ jsxRuntimeExports.jsx(RibbonBtn, { large: true, icon: svgPaths.natural, label: "Natural", onClick: () => onSelectAccidental(currentAccidental === "natural" ? null : "natural"), isActive: currentAccidental === "natural" }), 6337 /* @__PURE__ */ jsxRuntimeExports.jsx(RibbonBtn, { large: true, icon: svgPaths.sharp, label: "Sharp", onClick: () => onSelectAccidental(currentAccidental === "sharp" ? null : "sharp"), isActive: currentAccidental === "sharp" }), 6338 /* @__PURE__ */ jsxRuntimeExports.jsx(RibbonBtn, { large: true, icon: svgPaths.flat, label: "Flat", onClick: () => onSelectAccidental(currentAccidental === "flat" ? null : "flat"), isActive: currentAccidental === "flat" }) 6339 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 6340 /* @__PURE__ */ jsxRuntimeExports.jsx(RibbonBtn, { large: true, icon: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] font-bold", children: "KICK" }), label: "Kick", onClick: () => onSelectAccidental(currentAccidental === "kick" ? null : "kick"), isActive: currentAccidental === "kick" }),
6341 /* @__PURE__ */ jsxRuntimeExports.jsx(RibbonBtn, { large: true, icon: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] font-bold", children: "SNARE" }), label: "Snare", onClick: () => onSelectAccidental(currentAccidental === "snare" ? null : "snare"), isActive: currentAccidental === "snare" }), 6342 /* @__PURE__ */ jsxRuntimeExports.jsx(RibbonBtn, { large: true, icon: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] font-bold", children: "HI-HAT" }), label: "Hi-Hat", onClick: () => onSelectAccidental(currentAccidental === "hihat" ? null : "hihat"), isActive: currentAccidental === "hihat" }), 6343 /* @__PURE__ */ jsxRuntimeExports.jsx(RibbonBtn, { large: true, icon: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] font-bold", children: "CYMBAL" }), label: "Cymbal", onClick: () => onSelectAccidental(currentAccidental === "cymbal" ? null : "cymbal"), isActive: currentAccidental === "cymbal" }) 6344 ] }) }), 6345 /* @__PURE__ */ jsxRuntimeExports.jsxs(Group, { title: "Modifiers", children: [
6346 /* @__PURE__ */ jsxRuntimeExports.jsx(RibbonBtn, { large: true, icon: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-6 h-6 flex items-center justify-center", children: /* @__PURE__ */ jsxRuntimeExports.jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", children: svgPaths.dot }) }), label: "Dot", onClick: onToggleDotted, isActive: isDotted }), 6347 /* @__PURE__ */ jsxRuntimeExports.jsx(RibbonBtn, { large: true, icon: svgPaths.tie, label: "Tie", onClick: onToggleTieActive, isActive: isTieActive }), 6348 /* @__PURE__ */ jsxRuntimeExports.jsx(RibbonBtn, { large: true, icon: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold text-lg", children: "3" }), label: "Triplet", onClick: onToggleTriplet, isActive: isTriplet }) 6349 ] }), 6350 /* @__PURE__ */ jsxRuntimeExports.jsxs(Group, { title: "Articulations", children: [ 6351 /* @__PURE__ */ jsxRuntimeExports.jsx(RibbonBtn, { large: true, icon: svgPaths.staccato, label: "Staccato", onClick: () => onSelectArticulation(currentArticulation === "staccato" ? null : "staccato"), isActive: currentArticulation === "staccato" }),
6352 /* @__PURE__ */ jsxRuntimeExports.jsx(RibbonBtn, { large: true, icon: svgPaths.accent, label: "Accent", onClick: () => onSelectArticulation(currentArticulation === "accent" ? null : "accent"), isActive: currentArticulation === "accent" }), 6353 /* @__PURE__ */ jsxRuntimeExports.jsx(RibbonBtn, { large: true, icon: svgPaths.tenuto, label: "Tenuto", onClick: () => onSelectArticulation(currentArticulation === "tenuto" ? null : "tenuto"), isActive: currentArticulation === "tenuto" }), 6354 /* @__PURE__ */ jsxRuntimeExports.jsx(RibbonBtn, { large: true, icon: svgPaths.fermata, label: "Fermata", onClick: () => onSelectArticulation(currentArticulation === "fermata" ? null : "fermata"), isActive: currentArticulation === "fermata" }) 6355 ] }), 6356 /* @__PURE__ */ jsxRuntimeExports.jsx(Group, { title: "Keypad", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 6357 RibbonBtn, 6358 { 6359 large: true, 6360 icon: /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
6361 /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2", ry: "2" }), 6362 /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: "7", y: "7", width: "2", height: "2" }), 6363 /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: "11", y: "7", width: "2", height: "2" }), 6364 /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: "15", y: "7", width: "2", height: "2" }), 6365 /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: "7", y: "11", width: "2", height: "2" }), 6366 /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: "11", y: "11", width: "2", height: "2" }), 6367 /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: "15", y: "11", width: "2", height: "2" }), 6368 /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: "7", y: "15", width: "2", height: "2" }), 6369 /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: "11", y: "15", width: "2", height: "2" }), 6370 /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: "15", y: "15", width: "2", height: "2" }) 6371 ] }), 6372 label: showKeypad ? "Hide Keypad" : "Show Keypad", 6373 onClick: () => onToggleKeypad(!showKeypad), 6374 isActive: showKeypad 6375 } 6376 ) }) 6377 ] }), 6378 activeTab === "Play" && /* @__PURE__ */ jsxRuntimeExports.jsx(jsxRuntimeExports.Fragment, { children: /* @__PURE__ */ jsxRuntimeExports.jsxs(Group, { title: "Transport", children: [
6379 /* @__PURE__ */ jsxRuntimeExports.jsx(RibbonBtn, { large: true, icon: isPlaying ? /* @__PURE__ */ jsxRuntimeExports.jsx(Pause, { size: 24 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Play, { size: 24 }), label: isPlaying ? "Pause" : "Play", onClick: onTogglePlay, isActive: isPlaying }), 6380 /* @__PURE__ */ jsxRuntimeExports.jsx(RibbonBtn, { large: true, icon: /* @__PURE__ */ jsxRuntimeExports.jsx(Square, { size: 24 }), label: "Stop", onClick: onStop || onTogglePlay, isActive: !isPlaying }) 6381 ] }) }), 6382 activeTab === "View" && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 6383 /* @__PURE__ */ jsxRuntimeExports.jsxs(Group, { title: "Zoom", children: [ 6384 /* @__PURE__ */ jsxRuntimeExports.jsx(RibbonBtn, { large: true, icon: /* @__PURE__ */ jsxRuntimeExports.jsx(ZoomIn, { size: 24 }), label: "Zoom In", onClick: onZoomIn }), 6385 /* @__PURE__ */ jsxRuntimeExports.jsx(RibbonBtn, { large: true, icon: /* @__PURE__ */ jsxRuntimeExports.jsx(ZoomOut, { size: 24 }), label: "Zoom Out", onClick: onZoomOut }) 6386 ] }),
6387 /* @__PURE__ */ jsxRuntimeExports.jsxs(Group, { title: "Visibility", children: [ 6388 /* @__PURE__ */ jsxRuntimeExports.jsx(RibbonBtn, { large: true, icon: /* @__PURE__ */ jsxRuntimeExports.jsx(Hash, { size: 24 }), label: "Measure Nos", onClick: onToggleMeasureNumbers, isActive: showMeasureNumbers }), 6389 /* @__PURE__ */ jsxRuntimeExports.jsx(RibbonBtn, { large: true, icon: /* @__PURE__ */ jsxRuntimeExports.jsx(Grid3x3, { size: 24 }), label: "Grid Lines", onClick: onToggleGrid || (() => { 6390 }), isActive: showGrid }), 6391 /* @__PURE__ */ jsxRuntimeExports.jsx( 6392 RibbonBtn, 6393 { 6394 large: true, 6395 icon: /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [ 6396 /* @__PURE__ */ jsxRuntimeExports.jsx("polyline", { points: "16 18 22 12 16 6" }), 6397 /* @__PURE__ */ jsxRuntimeExports.jsx("polyline", { points: "8 6 2 12 8 18" }) 6398 ] }), 6399 label: showTextEditor ? "Hide Code" : "Show Code", 6400 onClick: onToggleTextEditor, 6401 isActive: showTextEditor 6402 } 6403 ) 6404 ] }) 6405 ] }) 6406 ] }) 6407 ] }); 6408}; 6409const ScoreDocumentTabs = () => { 6410 const { engine, activeTrackIndex, setActiveTrackIndex, midiVersion } = useEditor(); 6411 if (!engine || !engine.midi) return null; 6412 const tracks = engine.midi.tracks; 6413 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "h-8 bg-zinc-200 dark:bg-[#1a1a1a] border-b border-zinc-300 dark:border-zinc-800 flex items-end px-2 gap-1 overflow-x-auto shrink-0 select-none", children: [ 6414 /* @__PURE__ */ jsxRuntimeExports.jsx( 6415 "button", 6416 { 6417 onClick: () => setActiveTrackIndex(null), 6418 className: `px-4 py-1.5 text-xs font-medium rounded-t-lg border border-b-0 transition-colors ${activeTrackIndex === null ? "bg-white dark:bg-zinc-900 border-zinc-300 dark:border-zinc-800 text-zinc-900 dark:text-zinc-100" : "bg-zinc-100 dark:bg-zinc-800/50 border-transparent text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300 hover:bg-zinc-200 dark:hover:bg-zinc-700"}`, 6419 children: "Transposed score" 6420 } 6421 ), 6422 tracks.map((track, idx) => { 6423 const isDrums = track.instrument.percussion; 6424 const instName = track.instrument.name || (isDrums ? "Drums" : `Track ${idx + 1}`); 6425 const name = track.name ? track.name : instName; 6426 return /* @__PURE__ */ jsxRuntimeExports.jsx( 6427 "button", 6428 { 6429 onClick: () => setActiveTrackIndex(idx), 6430 className: `px-4 py-1.5 text-xs font-medium rounded-t-lg border border-b-0 transition-colors whitespace-nowrap ${activeTrackIndex === idx ? "bg-white dark:bg-zinc-900 border-zinc-300 dark:border-zinc-800 text-zinc-900 dark:text-zinc-100" : "bg-zinc-100 dark:bg-zinc-800/50 border-transparent text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300 hover:bg-zinc-200 dark:hover:bg-zinc-700"}`, 6431 title: name, 6432 children: name 6433 }, 6434 idx 6435 ); 6436 }) 6437 ] }); 6438}; 6439const ScoreKeypad = ({ 6440 currentTool, 6441 onSelectTool, 6442 currentAccidental, 6443 onSelectAccidental, 6444 currentVoice = 1, 6445 onSelectVoice, 6446 isPercussion, 6447 onClose, 6448 currentArticulation, 6449 onSelectArticulation, 6450 isDotted = false, 6451 onToggleDotted, 6452 isTieActive = false, 6453 onToggleTieActive, 6454 entryMode = "note", 6455 onSelectEntryMode 6456}) => { 6457 const { t } = useTranslation(); 6458 const [activeTab, setActiveTab] = reactExports.useState(1); 6459 const [isMinimized, setIsMinimized] = reactExports.useState(false); 6460 const [scale, setScale] = reactExports.useState(0.75); 6461 const keypadRef = reactExports.useRef(null); 6462 const [position, setPosition] = reactExports.useState({ x: 0, y: 0 }); 6463 const dragRef = reactExports.useRef({ 6464 isDragging: false, 6465 startX: 0, 6466 startY: 0, 6467 initialX: 0, 6468 initialY: 0 6469 }); 6470 const handleMouseDown = (e) => { 6471 if (e.button !== 0) return; 6472 const target = e.target; 6473 if (target.closest("button") || target.closest("input") || target.closest("select") || target.closest("textarea") || target.classList.contains("resize-handle")) { 6474 return; 6475 } 6476 dragRef.current = { 6477 isDragging: true, 6478 startX: e.clientX, 6479 startY: e.clientY, 6480 initialX: position.x, 6481 initialY: position.y 6482 }; 6483 }; 6484 reactExports.useEffect(() => { 6485 const handleMouseMove = (e) => { 6486 if (!dragRef.current.isDragging) return; 6487 const dx = e.clientX - dragRef.current.startX; 6488 const dy = e.clientY - dragRef.current.startY; 6489 let newX = dragRef.current.initialX + dx;
6490 let newY = dragRef.current.initialY + dy; 6491 if (keypadRef.current && keypadRef.current.parentElement) { 6492 const parent = keypadRef.current.parentElement; 6493 const parentRect = parent.getBoundingClientRect(); 6494 const keypadWidth = 240; 6495 const keypadHeight = keypadRef.current.offsetHeight || 380; 6496 const scaledWidth = keypadWidth * scale; 6497 const scaledHeight = keypadHeight * scale; 6498 const minX = 32 + scaledWidth - parentRect.width; 6499 const maxX = 32; 6500 const minY = 32 + scaledHeight - parentRect.height; 6501 const maxY = 32; 6502 newX = Math.max(minX, Math.min(maxX, newX)); 6503 newY = Math.max(minY, Math.min(maxY, newY)); 6504 } 6505 setPosition({ x: newX, y: newY }); 6506 }; 6507 const handleMouseUp = () => { 6508 dragRef.current.isDragging = false; 6509 }; 6510 window.addEventListener("mousemove", handleMouseMove); 6511 window.addEventListener("mouseup", handleMouseUp); 6512 return () => { 6513 window.removeEventListener("mousemove", handleMouseMove); 6514 window.removeEventListener("mouseup", handleMouseUp); 6515 }; 6516 }, [scale]); 6517 reactExports.useEffect(() => { 6518 const clampPosition = () => { 6519 if (!keypadRef.current) return; 6520 const parent = keypadRef.current.parentElement; 6521 if (!parent) return; 6522 const parentRect = parent.getBoundingClientRect(); 6523 const keypadWidth = 240; 6524 const keypadHeight = keypadRef.current.offsetHeight || 380; 6525 const scaledWidth = keypadWidth * scale; 6526 const scaledHeight = keypadHeight * scale; 6527 setPosition((prev) => { 6528 const minX = 32 + scaledWidth - parentRect.width; 6529 const maxX = 32; 6530 const minY = 32 + scaledHeight - parentRect.height; 6531 const maxY = 32; 6532 return { 6533 x: Math.max(minX, Math.min(maxX, prev.x)), 6534 y: Math.max(minY, Math.min(maxY, prev.y)) 6535 }; 6536 }); 6537 }; 6538 clampPosition(); 6539 window.addEventListener("resize", clampPosition); 6540 return () => window.removeEventListener("resize", clampPosition); 6541 }, [scale]); 6542 const handleResizeMouseDown = (corner) => (e) => { 6543 e.stopPropagation(); 6544 e.preventDefault(); 6545 const startX = e.clientX; 6546 const startY = e.clientY; 6547 const startScale = scale; 6548 const handleMouseMove = (moveEvent) => { 6549 const dx = moveEvent.clientX - startX; 6550 const dy = moveEvent.clientY - startY; 6551 let factor = 0; 6552 if (corner === "tl") { 6553 factor = -(dx + dy); 6554 } else if (corner === "tr") { 6555 factor = dx - dy; 6556 } else if (corner === "bl") { 6557 factor = -dx + dy; 6558 } else if (corner === "br") { 6559 factor = dx + dy; 6560 } 6561 const distance = factor * 2e-3; 6562 setScale(Math.min(1.5, Math.max(0.5, startScale + distance))); 6563 }; 6564 const handleMouseUp = () => { 6565 window.removeEventListener("mousemove", handleMouseMove); 6566 window.removeEventListener("mouseup", handleMouseUp); 6567 }; 6568 window.addEventListener("mousemove", handleMouseMove); 6569 window.addEventListener("mouseup", handleMouseUp); 6570 }; 6571 const KeypadTab = ({ num }) => /* @__PURE__ */ jsxRuntimeExports.jsx( 6572 "button", 6573 { 6574 onClick: () => onSelectVoice == null ? void 0 : onSelectVoice(num), 6575 className: `flex-1 py-1.5 text-xs font-bold transition-colors ${currentVoice === num ? "bg-[#6366f1] text-white rounded" : "bg-[#2a2a35] text-zinc-300 hover:bg-[#3b3b4f]"}`, 6576 children: num === "All" ? t("editor.scoreKeypad.voiceAll", "All") : num 6577 } 6578 ); 6579 const KeypadBtn = ({ active, onClick, svg, label, rowSpan = 1 }) => /* @__PURE__ */ jsxRuntimeExports.jsx( 6580 "button", 6581 { 6582 onClick, 6583 className: `rounded flex items-center justify-center transition-all ${active ? "bg-[#6366f1] text-white shadow-[inset_0_2px_4px_rgba(0,0,0,0.5)]" : "bg-[#363640] hover:bg-[#4a4a58] text-zinc-200 shadow-[inset_1px_1px_0px_rgba(255,255,255,0.05),1px_1px_3px_rgba(0,0,0,0.4)]"}`, 6584 style: { gridRow: `span ${rowSpan}`, minHeight: rowSpan === 2 ? "100px" : "48px" }, 6585 title: label, 6586 children: svg 6587 } 6588 ); 6589 const svgPaths = { 6590 whole: /* @__PURE__ */ jsxRuntimeExports.jsx("svg", { width: "24", height: "24", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsxRuntimeExports.jsx("ellipse", { cx: "12", cy: "12", rx: "7", ry: "4", fill: "none", stroke: "currentColor", strokeWidth: "2.5", transform: "rotate(-15 12 12)" }) }), 6591 half: /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { width: "24", height: "32", viewBox: "0 0 24 36", children: [
6592 /* @__PURE__ */ jsxRuntimeExports.jsx("ellipse", { cx: "9", cy: "24", rx: "6", ry: "4", fill: "none", stroke: "currentColor", strokeWidth: "2.5", transform: "rotate(-15 9 24)" }), 6593 /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: "14", y1: "24", x2: "14", y2: "4", stroke: "currentColor", strokeWidth: "2" }) 6594 ] }), 6595 quarter: /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { width: "24", height: "32", viewBox: "0 0 24 36", children: [ 6596 /* @__PURE__ */ jsxRuntimeExports.jsx("ellipse", { cx: "9", cy: "24", rx: "6", ry: "4", fill: "currentColor", transform: "rotate(-15 9 24)" }), 6597 /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: "14", y1: "24", x2: "14", y2: "4", stroke: "currentColor", strokeWidth: "2" }) 6598 ] }), 6599 eighth: /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { width: "24", height: "32", viewBox: "0 0 24 36", children: [ 6600 /* @__PURE__ */ jsxRuntimeExports.jsx("ellipse", { cx: "9", cy: "24", rx: "6", ry: "4", fill: "currentColor", transform: "rotate(-15 9 24)" }), 6601 /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: "14", y1: "24", x2: "14", y2: "4", stroke: "currentColor", strokeWidth: "2" }),
6602 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M14 4 C 20 8, 18 16, 16 20", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round" }) 6603 ] }), 6604 sixteenth: /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { width: "24", height: "32", viewBox: "0 0 24 36", children: [ 6605 /* @__PURE__ */ jsxRuntimeExports.jsx("ellipse", { cx: "9", cy: "24", rx: "6", ry: "4", fill: "currentColor", transform: "rotate(-15 9 24)" }), 6606 /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: "14", y1: "24", x2: "14", y2: "4", stroke: "currentColor", strokeWidth: "2" }), 6607 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M14 4 C 20 8, 18 16, 16 20 M14 12 C 20 16, 18 24, 16 28", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round" }) 6608 ] }), 6609 thirtysecond: /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { width: "24", height: "32", viewBox: "0 0 24 36", children: [ 6610 /* @__PURE__ */ jsxRuntimeExports.jsx("ellipse", { cx: "9", cy: "24", rx: "6", ry: "4", fill: "currentColor", transform: "rotate(-15 9 24)" }),
6611 /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: "14", y1: "24", x2: "14", y2: "4", stroke: "currentColor", strokeWidth: "2" }), 6612 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M14 4 C 20 8, 18 16, 16 20 M14 10 C 20 14, 18 22, 16 26 M14 16 C 20 20, 18 28, 16 32", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round" }) 6613 ] }), 6614 sharp: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-2xl leading-none font-serif", children: "â¯" }), 6615 flat: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-2xl leading-none font-serif", children: "â" }), 6616 natural: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-2xl leading-none font-serif", children: "â®" }), 6617 tie: /* @__PURE__ */ jsxRuntimeExports.jsx("svg", { width: "32", height: "16", viewBox: "0 0 32 16", children: /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M4 12 C 12 0, 20 0, 28 12", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }) }), 6618 dot: /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "12", cy: "12", r: "2.5", fill: "currentColor" }), 6619 restQuarter: /* @__PURE__ */ jsxRuntimeExports.jsx("svg", { width: "16", height: "24", viewBox: "0 0 24 36", children: /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M10 2 L14 10 L8 18 L16 26 C16 30 10 34 6 30 C10 32 12 28 8 24 L14 16 L10 8 Z", fill: "currentColor" }) }), 6620 staccato: /* @__PURE__ */ jsxRuntimeExports.jsx("svg", { width: "24", height: "24", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "12", cy: "12", r: "2", fill: "currentColor" }) }), 6621 tenuto: /* @__PURE__ */ jsxRuntimeExports.jsx("svg", { width: "24", height: "24", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: "6", y: "10", width: "12", height: "2", fill: "currentColor" }) }), 6622 accent: /* @__PURE__ */ jsxRuntimeExports.jsx("svg", { width: "24", height: "24", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M4 6 L20 12 L4 18 L6 12 Z", fill: "currentColor" }) }), 6623 fermata: /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { width: "24", height: "24", viewBox: "0 0 24 24", children: [
6624 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M4 14 C 4 4, 20 4, 20 14", fill: "none", stroke: "currentColor", strokeWidth: "2" }), 6625 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "12", cy: "14", r: "2", fill: "currentColor" }) 6626 ] }) 6627 }; 6628 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 6629 "div", 6630 { 6631 id: "score-keypad-panel", 6632 ref: keypadRef, 6633 className: "absolute z-50 rounded-xl shadow-[0_20px_40px_rgba(0,0,0,0.6)] border border-[#444] flex flex-col select-none overflow-hidden", 6634 style: { 6635 right: "32px", 6636 bottom: "32px", 6637 transform: `translate(${position.x}px, ${position.y}px) scale(${scale})`, 6638 transformOrigin: "bottom right", 6639 backgroundColor: "#2b2b36", 6640 width: "240px" 6641 }, 6642 onMouseDown: handleMouseDown, 6643 children: [ 6644 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute top-0 left-0 w-3 h-3 cursor-nwse-resize z-[60] resize-handle", onMouseDown: handleResizeMouseDown("tl") }), 6645 /* @__P
6645URE__ */ jsxRuntimeExports.jsx("div", { className: "absolute top-0 right-0 w-3 h-3 cursor-nesw-resize z-[60] resize-handle", onMouseDown: handleResizeMouseDown("tr") }), 6646 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute bottom-0 left-0 w-3 h-3 cursor-nesw-resize z-[60] resize-handle", onMouseDown: handleResizeMouseDown("bl") }), 6647 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute bottom-0 right-0 w-3 h-3 cursor-nwse-resize z-[60] resize-handle", onMouseDown: handleResizeMouseDown("br") }), 6648 /* @__PURE__ */ jsxRuntimeExports.jsxs( 6649 "div", 6650 { 6651 className: "h-7 bg-[#343442] flex items-center justify-center px-3 cursor-move border-b border-[#111] relative", 6652 onMouseDown: handleMouseDown, 6653 children: [ 6654 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute left-3 flex gap-1.5 ml-2", children: [ 6655 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: onClose, className: "w-3 h-3 rounded-full bg-[#ff5f56] hover:bg-[#ff5f56]/80", title: t("editor.scoreKeypad.close", "Close") }),
6656 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => setIsMinimized(!isMinimized), className: "w-3 h-3 rounded-full bg-[#ffbd2e] hover:bg-[#ffbd2e]/80", title: t("editor.scoreKeypad.minimize", "Minimize") }), 6657 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { className: "w-3 h-3 rounded-full bg-[#27c93f] hover:bg-[#27c93f]/80", title: t("editor.scoreKeypad.maximize", "Maximize") }) 6658 ] }), 6659 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[12px] text-zinc-400 font-medium pointer-events-none tracking-wide", children: t("editor.scoreKeypad.title", "Keypad") }) 6660 ] 6661 } 6662 ), 6663 !isMinimized && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-2.5 flex flex-col gap-2", children: [ 6664 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-5 gap-1", children: [ 6665 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => setActiveTab(1), className: `col-span-1 rounded flex items-center justify-center py-1 transition-colors ${activeTab === 1 ? "bg-[#6366f1] text-white" : "bg-[#363640] hover:bg-[#4a4a58] text-zinc-300"}`, children: svgPaths.whole }),
6666 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => setActiveTab(2), className: `col-span-1 rounded flex items-center justify-center py-1 transition-colors ${activeTab === 2 ? "bg-[#6366f1] text-white" : "bg-[#363640] hover:bg-[#4a4a58] text-zinc-300"}`, children: svgPaths.fermata }), 6667 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => setActiveTab(3), className: `col-span-1 rounded flex items-center justify-center py-1 transition-colors ${activeTab === 3 ? "bg-[#6366f1] text-white" : "bg-[#363640] hover:bg-[#4a4a58] text-zinc-300"}`, children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-serif", children: "ð½" }) }), 6668 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => setActiveTab(4), className: `col-span-1 rounded flex items-center justify-center py-1 transition-colors ${activeTab === 4 ? "bg-[#6366f1] text-white" : "bg-[#363640] hover:bg-[#4a4a58] text-zinc-300"}`, children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-serif", children: "%" }) }),
6669 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => setActiveTab(5), className: `col-span-1 rounded flex items-center justify-center py-1 transition-colors ${activeTab === 5 ? "bg-[#6366f1] text-white" : "bg-[#363640] hover:bg-[#4a4a58] text-zinc-300"}`, children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-serif text-xs", children: "ââ" }) }) 6670 ] }), 6671 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-4 gap-1.5 mt-1", children: [ 6672 /* @__PURE__ */ jsxRuntimeExports.jsx(KeypadBtn, { active: currentArticulation === "fermata", onClick: () => onSelectArticulation == null ? void 0 : onSelectArticulation(currentArticulation === "fermata" ? null : "fermata"), label: t("editor.scoreKeypad.fermata", "Fermata"), svg: svgPaths.fermata }), 6673 /* @__PURE__ */ jsxRuntimeExports.jsx(KeypadBtn, { active: currentArticulation === "accent", onClick: () => onSelectArticulation == null ? void 0 : onSelectArticulation(currentArticulation === "accent" ? null : "accent"), label: t("editor.scoreKeypad.accent", "Accent"), svg: svgPaths.accent }),
6674 /* @__PURE__ */ jsxRuntimeExports.jsx(KeypadBtn, { active: isDotted, onClick: () => onToggleDotted == null ? void 0 : onToggleDotted(), label: t("editor.scoreKeypad.dot", "Dot"), svg: /* @__PURE__ */ jsxRuntimeExports.jsx("svg", { width: "24", height: "24", viewBox: "0 0 24 24", children: svgPaths.dot }) }), 6675 /* @__PURE__ */ jsxRuntimeExports.jsx(KeypadBtn, { active: currentArticulation === "tenuto", onClick: () => onSelectArticulation == null ? void 0 : onSelectArticulation(currentArticulation === "tenuto" ? null : "tenuto"), label: t("editor.scoreKeypad.tenuto", "Tenuto"), svg: svgPaths.tenuto }), 6676 !isPercussion ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 6677 /* @__PURE__ */ jsxRuntimeExports.jsx(KeypadBtn, { active: currentAccidental === "natural", onClick: () => onSelectAccidental(currentAccidental === "natural" ? null : "natural"), label: t("editor.scoreKeypad.natural", "Natural"), svg: svgPaths.natural }), 6678 /* @__PURE__ */ jsxRuntimeExports.jsx(KeypadBtn, { active: currentAccidental === "sharp", onClick: () => onSelectAccidental(currentAccidental === "sharp" ? null : "sharp"), label: t("editor.scoreKeypad.sharp", "Sharp"), svg: svgPaths.sharp }),
6679 /* @__PURE__ */ jsxRuntimeExports.jsx(KeypadBtn, { active: currentAccidental === "flat", onClick: () => onSelectAccidental(currentAccidental === "flat" ? null : "flat"), label: t("editor.scoreKeypad.flat", "Flat"), svg: svgPaths.flat }), 6680 /* @__PURE__ */ jsxRuntimeExports.jsx(KeypadBtn, { active: currentArticulation === "staccato", onClick: () => onSelectArticulation == null ? void 0 : onSelectArticulation(currentArticulation === "staccato" ? null : "staccato"), label: t("editor.scoreKeypad.staccato", "Staccato"), svg: svgPaths.staccato }) 6681 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 6682 /* @__PURE__ */ jsxRuntimeExports.jsx(KeypadBtn, { active: currentAccidental === "kick", onClick: () => onSelectAccidental(currentAccidental === "kick" ? null : "kick"), label: t("editor.scoreKeypad.kickDrum", "Kick Drum"), svg: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] font-bold uppercase tracking-tighter", children: "KICK" }) }),
6683 /* @__PURE__ */ jsxRuntimeExports.jsx(KeypadBtn, { active: currentAccidental === "snare", onClick: () => onSelectAccidental(currentAccidental === "snare" ? null : "snare"), label: t("editor.scoreKeypad.snareDrum", "Snare Drum"), svg: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] font-bold uppercase tracking-tighter", children: "SNARE" }) }), 6684 /* @__PURE__ */ jsxRuntimeExports.jsx(KeypadBtn, { active: currentAccidental === "hihat", onClick: () => onSelectAccidental(currentAccidental === "hihat" ? null : "hihat"), label: t("editor.scoreKeypad.hiHat", "Hi-Hat"), svg: /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[10px] font-bold uppercase tracking-tighter text-center leading-none", children: [ 6685 "HI", 6686 /* @__PURE__ */ jsxRuntimeExports.jsx("br", {}), 6687 "HAT" 6688 ] }) }), 6689 /* @__PURE__ */ jsxRuntimeExports.jsx(KeypadBtn, { active: currentAccidental === "cymbal", onClick: () => onSelectAccidental(currentAccidental === "cymbal" ? null : "cymbal"), label: t("editor.scoreKeypad.cymbal", "Cymbal"), svg: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] font-bold uppercase tracking-tighter", children: "CYMB" }) }) 6690 ] }),
6691 /* @__PURE__ */ jsxRuntimeExports.jsx(KeypadBtn, { active: currentTool === "note_quarter", onClick: () => onSelectTool("note_quarter"), label: t("editor.scoreKeypad.quarterNote", "Quarter Note"), svg: svgPaths.quarter }), 6692 /* @__PURE__ */ jsxRuntimeExports.jsx(KeypadBtn, { active: currentTool === "note_half", onClick: () => onSelectTool("note_half"), label: t("editor.scoreKeypad.halfNote", "Half Note"), svg: svgPaths.half }), 6693 /* @__PURE__ */ jsxRuntimeExports.jsx(KeypadBtn, { active: currentTool === "note_whole", onClick: () => onSelectTool("note_whole"), label: t("editor.scoreKeypad.wholeNote", "Whole Note"), svg: svgPaths.whole }), 6694 /* @__PURE__ */ jsxRuntimeExports.jsx(KeypadBtn, { active: isTieActive, onClick: () => onToggleTieActive == null ? void 0 : onToggleTieActive(), label: t("editor.scoreKeypad.tie", "Tie"), svg: svgPaths.tie, rowSpan: 2 }), 6695 /* @__PURE__ */ jsxRuntimeExports.jsx(KeypadBtn, { active: currentTool === "note_eighth", onClick: () => onSelectTool("note_eighth"), label: t("editor.scoreKeypad.eighthNote", "Eighth Note"), svg: svgPaths.eighth }),
6696 /* @__PURE__ */ jsxRuntimeExports.jsx(KeypadBtn, { active: currentTool === "note_sixteenth", onClick: () => onSelectTool("note_sixteenth"), label: t("editor.scoreKeypad.sixteenthNote", "16th Note"), svg: svgPaths.sixteenth }), 6697 /* @__PURE__ */ jsxRuntimeExports.jsx(KeypadBtn, { active: currentTool === "note_thirtysecond", onClick: () => onSelectTool("note_thirtysecond"), label: t("editor.scoreKeypad.thirtysecondNote", "32nd Note"), svg: svgPaths.thirtysecond }), 6698 /* @__PURE__ */ jsxRuntimeExports.jsx(KeypadBtn, { active: entryMode === "rest" && currentTool === "note_quarter", onClick: () => { 6699 onSelectEntryMode == null ? void 0 : onSelectEntryMode("rest"); 6700 onSelectTool == null ? void 0 : onSelectTool("note_quarter"); 6701 }, label: t("editor.scoreKeypad.quarterRest", "Quarter Rest"), svg: svgPaths.restQuarter }), 6702 /* @__PURE__ */ jsxRuntimeExports.jsx(KeypadBtn, { active: isDotted && currentTool === "note_half", onClick: () => { 6703 if (!isDotted) onToggleDotted == null ? void 0 : onToggleDotted(); 6704 onSelectTool == null ? void 0 : onSelectTool("note_half"); 6705 onSelectEntryMode == null ? void 0 : onSelectEntryMode("note"); 6706 }, label: t("editor.scoreKeypad.dottedHalf", "Dotted Half"), svg: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex", children: [ 6707 svgPaths.half,
6708 /* @__PURE__ */ jsxRuntimeExports.jsx("svg", { width: "12", height: "32", viewBox: "0 0 12 32", children: /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "6", cy: "24", r: "3", fill: "currentColor" }) }) 6709 ] }) }), 6710 /* @__PURE__ */ jsxRuntimeExports.jsx(KeypadBtn, { active: false, onClick: () => { 6711 }, label: t("editor.scoreKeypad.blank", "Blank"), svg: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "opacity-20 text-xs", children: "..." }) }), 6712 /* @__PURE__ */ jsxRuntimeExports.jsx(KeypadBtn, { active: false, onClick: () => { 6713 }, label: t("editor.scoreKeypad.blank", "Blank"), svg: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "opacity-20 text-xs", children: "..." }) }) 6714 ] }), 6715 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-1 mt-1 bg-[#1c1c24] p-1 rounded-lg", children: [ 6716 /* @__PURE__ */ jsxRuntimeExports.jsx(KeypadTab, { num: 1 }), 6717 /* @__PURE__ */ jsxRuntimeExports.jsx(KeypadTab, { num: 2 }),
6718 /* @__PURE__ */ jsxRuntimeExports.jsx(KeypadTab, { num: 3 }), 6719 /* @__PURE__ */ jsxRuntimeExports.jsx(KeypadTab, { num: 4 }), 6720 /* @__PURE__ */ jsxRuntimeExports.jsx(KeypadTab, { num: "All" }) 6721 ] }) 6722 ] }) 6723 ] 6724 } 6725 ); 6726}; 6727const ScoreStatusBar = ({ 6728 zoom, 6729 onZoomChange, 6730 layout, 6731 onLayoutChange, 6732 syntaxError 6733}) => { 6734 const { t } = useTranslation(); 6735 const { activeTrackIndex, engine } = useEditor(); 6736 const getActiveTrackName = () => { 6737 if (activeTrackIndex === null) return t("editor.scoreStatusBar.transposedScore", "Transposed score"); 6738 if (!engine || !engine.midi) return ""; 6739 const track = engine.midi.tracks[activeTrackIndex]; 6740 return track.name || track.instrument.name || t("editor.scoreStatusBar.trackNumber", "Track {{number}}", { number: activeTrackIndex + 1 }); 6741 }; 6742 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "h-6 bg-[#2d2d2d] border-t border-zinc-800 flex items-center justify-between px-2 text-[10px] text-zinc-400 select-none shrink-0 z-50", children: [ 6743 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-4", children: [
6744 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("editor.scoreStatusBar.page", "Page {{current}} of {{total}}", { current: 1, total: 1 }) }), 6745 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-px h-3 bg-zinc-600" }), 6746 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("editor.scoreStatusBar.bars", "Bars: -") }), 6747 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-px h-3 bg-zinc-600" }), 6748 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("editor.scoreStatusBar.noSelection", "No Selection") }), 6749 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-px h-3 bg-zinc-600" }), 6750 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-semibold text-zinc-300", children: getActiveTrackName() }), 6751 syntaxError !== void 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 6752 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-px h-3 bg-zinc-600" }), 6753 syntaxError ? /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-red-400 font-semibold flex items-center gap-1", children: [
6754 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "w-1.5 h-1.5 rounded-full bg-red-400 animate-pulse" }), 6755 syntaxError 6756 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-emerald-400 font-semibold flex items-center gap-1", children: [ 6757 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "w-1.5 h-1.5 rounded-full bg-emerald-400" }), 6758 t("editor.scoreStatusBar.syncedWithPianoRoll", "Synced with Piano Roll") 6759 ] }) 6760 ] }) 6761 ] }), 6762 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3", children: [ 6763 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1 bg-[#1a1a1a] rounded px-1 border border-[#333]", children: [ 6764 /* @__PURE__ */ jsxRuntimeExports.jsx( 6765 "button", 6766 { 6767 onClick: () => onLayoutChange("panorama"), 6768 className: `p-0.5 rounded transition-colors ${layout === "panorama" ? "bg-[#444] text-white" : "hover:text-zinc-200"}`, 6769 title: t("editor.scoreStatusBar.panoramaView", "Panorama View"), 6770 children: /* @__PURE__ */ jsxRuntimeExports.jsx(LayoutTemplate, { size: 12 }) 6771 } 6772 ),
6773 /* @__PURE__ */ jsxRuntimeExports.jsx( 6774 "button", 6775 { 6776 onClick: () => onLayoutChange("pages"), 6777 className: `p-0.5 rounded transition-colors ${layout === "pages" ? "bg-[#444] text-white" : "hover:text-zinc-200"}`, 6778 title: t("editor.scoreStatusBar.pagesView", "Pages View"), 6779 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Columns2, { size: 12 }) 6780 } 6781 ) 6782 ] }), 6783 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1", children: [ 6784 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "w-8 text-right", children: [ 6785 (zoom * 100).toFixed(0), 6786 "%" 6787 ] }), 6788 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => onZoomChange(Math.max(0.1, zoom - 0.1)), className: "hover:text-white", children: /* @__PURE__ */ jsxRuntimeExports.jsx(ZoomOut, { size: 12 }) }), 6789 /* @__PURE__ */ jsxRuntimeExports.jsx( 6790 "input", 6791 { 6792 type: "range", 6793 min: "0.1", 6794 max: "3", 6795 step: "0.05", 6796 value: zoom, 6797 onChange: (e) => onZoomChange(parseFloat(e.target.value)), 6798 className: "w-20 h-1 accent-indigo-500 bg-zinc-700 rounded-full appearance-none outline-none cursor-pointer", 6799 style: { WebkitAppearance: "none" } 6800 } 6801 ),
6802 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => onZoomChange(Math.min(3, zoom + 0.1)), className: "hover:text-white", children: /* @__PURE__ */ jsxRuntimeExports.jsx(ZoomIn, { size: 12 }) }) 6803 ] }) 6804 ] }) 6805 ] }); 6806}; 6807const LiveFeedbackTour = ({ 6808 onComplete, 6809 humdrumText, 6810 showTextEditor, 6811 setShowTextEditor, 6812 showKeypad, 6813 setShowKeypad 6814}) => { 6815 const [currentStep, setCurrentStep] = reactExports.useState(0); 6816 const [bounds, setBounds] = reactExports.useState(null); 6817 const [tooltipStyle, setTooltipStyle] = reactExports.useState({}); 6818 const tooltipRef = reactExports.useRef(null); 6819 const initialHumdrumRef = reactExports.useRef(null); 6820 const steps = [ 6821 { 6822 targetSelector: null, 6823 title: "MIDI Editor", 6824 content: "Welcome to Hamie Lab's MIDI Editor! Let's see how Live Feedback works. Click 'Next' to begin.", 6825 preferredPlacement: "center" 6826 }, 6827 { 6828 targetSelector: ".humdrum-ace-editor", 6829 title: "The Humdrum Code Editor", 6830 content: "This is where your music notation code lives. Try typing or altering a note representation (e.g., '4c') or modify the text to trigger a live re-render.", 6831 preferredPlacement: "right" 6832 }, 6833 { 6834 targetSelector: "#score-canvas-container", 6835 title: "Sheet Music Renderer", 6836 content: "Look here! As you type or use the keypad, your sheet music is instantly engraved and updated in real-time.", 6837 preferredPlacement: "left" 6838 }, 6839 { 6840 targetSelector: "#score-keypad-panel", 6841 title: "Notation Keypad", 6842 content: "You can also use this Keypad to quickly input note durations, accidentals, and articulations directly onto the staff.", 6843 preferredPlacement: "left" 6844 } 6845 ]; 6846 reactExports.useEffect(() => { 6847 if (currentStep === 1 && !showTextEditor) { 6848 setShowTextEditor(true); 6849 } 6850 if (currentStep === 3 && !showKeypad) { 6851 setShowKeypad(true); 6852 } 6853 }, [currentStep, showTextEditor, showKeypad, setShowTextEditor, setShowKeypad]); 6854 reactExports.useEffect(() => { 6855 if (currentStep === 1) { 6856 if (initialHumdrumRef.current === null) { 6857 initialHumdrumRef.current = humdrumText; 6858 } else if (humdrumText !== initialHumdrumRef.current) { 6859 setTimeout(() => { 6860 handleNext(); 6861 }, 400); 6862 } 6863 } else { 6864 initialHumdrumRef.current = null; 6865 } 6866 }, [currentStep, humdrumText]); 6867 reactExports.useEffect(() => { 6868 const step = steps[currentStep]; 6869 if (!step || !step.targetSelector) { 6870 setBounds(null); 6871 return; 6872 } 6873 const updateBounds = () => { 6874 const el = document.querySelector(step.targetSelector); 6875 if (el) { 6876 setBounds(el.getBoundingClientRect()); 6877 } else { 6878 setBounds(null); 6879 } 6880 }; 6881 updateBounds(); 6882 const interval = setInterval(updateBounds, 150); 6883 window.addEventListener("resize", updateBounds); 6884 window.addEventListener("scroll", updateBounds, true); 6885 return () => { 6886 clearInterval(interval); 6887 window.removeEventListener("resize", updateBounds); 6888 window.removeEventListener("scroll", updateBounds, true); 6889 }; 6890 }, [currentStep]); 6891 reactExports.useEffect(() => { 6892 const step = steps[currentStep]; 6893 const tooltip = tooltipRef.current; 6894 const computePlacement = () => { 6895 const vw = window.innerWidth; 6896 const vh = window.innerHeight; 6897 const isMobile = vw < 768; 6898 if (step.preferredPlacement === "center" || !bounds || isMobile) { 6899 setTooltipStyle({ 6900 position: "fixed", 6901 left: "50%", 6902 top: step.preferredPlacement === "center" ? "40%" : "auto", 6903 bottom: step.preferredPlacement === "center" ? "auto" : "24px", 6904 transform: step.preferredPlacement === "center" ? "translate(-50%, -50%)" : "translateX(-50%)", 6905 width: "calc(100% - 32px)", 6906 maxWidth: "420px", 6907 zIndex: 10000010 6908 }); 6909 return; 6910 } 6911 const tWidth = tooltip ? tooltip.offsetWidth : 360; 6912 const tHeight = tooltip ? tooltip.offsetHeight : 180; 6913 const gap = 20; 6914 let placement = step.preferredPlacement; 6915 if (placement === "right") { 6916 if (bounds.right + tWidth + gap > vw) { 6917 placement = "bottom"; 6918 } 6919 } else if (placement === "left") {
6920 if (bounds.left - tWidth - gap < 0) { 6921 placement = "bottom"; 6922 } 6923 } 6924 let left = 0; 6925 let top = 0; 6926 if (placement === "right") { 6927 left = bounds.right + gap; 6928 top = bounds.top + (bounds.height - tHeight) / 2; 6929 top = Math.max(16, Math.min(vh - tHeight - 16, top)); 6930 } else if (placement === "left") { 6931 left = bounds.left - tWidth - gap; 6932 top = bounds.top + (bounds.height - tHeight) / 2; 6933 top = Math.max(16, Math.min(vh - tHeight - 16, top)); 6934 } else { 6935 left = bounds.left + (bounds.width - tWidth) / 2; 6936 top = bounds.bottom + gap; 6937 left = Math.max(16, Math.min(vw - tWidth - 16, left)); 6938 if (top + tHeight > vh) { 6939 top = bounds.top - tHeight - gap; 6940 } 6941 } 6942 setTooltipStyle({ 6943 position: "fixed", 6944 left: `${left}px`, 6945 top: `${top}px`, 6946 width: `${tWidth}px`, 6947 zIndex: 10000010 6948 }); 6949 }; 6950 computePlacement(); 6951 const timeout = setTimeout(computePlacement, 50); 6952 window.addEventListener("resize", computePlacement); 6953 return () => { 6954 clearTimeout(timeout); 6955 window.removeEventListener("resize", computePlacement); 6956 }; 6957 }, [currentStep, bounds]); 6958 const handleNext = () => { 6959 if (currentStep < steps.length - 1) { 6960 setCurrentStep(currentStep + 1); 6961 } else { 6962 handleComplete(); 6963 } 6964 }; 6965 const handlePrev = () => { 6966 if (currentStep > 0) { 6967 setCurrentStep(currentStep - 1); 6968 } 6969 }; 6970 const handleComplete = () => { 6971 localStorage.setItem("hamienet_live_tour_done", "true"); 6972 onComplete(); 6973 }; 6974 const handleSkip = () => { 6975 handleComplete(); 6976 }; 6977 const getClipPath = () => { 6978 if (!bounds) return "none"; 6979 const top = bounds.top; 6980 const left = bounds.left; 6981 const right = bounds.right; 6982 const bottom = bounds.bottom; 6983 return `polygon( 6984 0% 0%, 6985 0% 100%, 6986 ${left}px 100%, 6987 ${left}px ${top}px, 6988 ${right}px ${top}px, 6989 ${right}px ${bottom}px, 6990 ${left}px ${bottom}px, 6991 ${left}px 100%, 6992 100% 100%, 6993 100% 0% 6994 )`; 6995 }; 6996 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "fixed inset-0 z-[10000000] pointer-events-none font-sans", children: [ 6997 /* @__PURE__ */ jsxRuntimeExports.jsx( 6998 "div", 6999 { 7000 className: "absolute inset-0 bg-black/60 transition-all duration-300 pointer-events-auto", 7001 style: { clipPath: getClipPath() }, 7002 onClick: handleSkip 7003 } 7004 ),
7005 bounds && /* @__PURE__ */ jsxRuntimeExports.jsx( 7006 "div", 7007 { 7008 className: "absolute border border-indigo-500 rounded-lg shadow-[0_0_15px_rgba(99,102,241,0.6)] animate-pulse pointer-events-none transition-all duration-300", 7009 style: { 7010 left: bounds.left - 2, 7011 top: bounds.top - 2, 7012 width: bounds.width + 4, 7013 height: bounds.height + 4 7014 } 7015 } 7016 ), 7017 /* @__PURE__ */ jsxRuntimeExports.jsxs( 7018 "div", 7019 { 7020 ref: tooltipRef, 7021 style: tooltipStyle, 7022 className: "absolute pointer-events-auto bg-[#181824]/95 border border-[#3c3c54]/60 backdrop-blur-md rounded-xl p-4 shadow-2xl text-zinc-100 flex flex-col gap-3 transition-all duration-300", 7023 children: [ 7024 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between border-b border-[#3c3c54]/40 pb-2", children: [ 7025 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5 text-indigo-400 font-bold text-xs uppercase tracking-wider", children: [
7026 /* @__PURE__ */ jsxRuntimeExports.jsx(CircleQuestionMark, { size: 14 }), 7027 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 7028 "Step ", 7029 currentStep + 1, 7030 " of ", 7031 steps.length 7032 ] }) 7033 ] }), 7034 /* @__PURE__ */ jsxRuntimeExports.jsx( 7035 "button", 7036 { 7037 onClick: handleSkip, 7038 className: "text-zinc-550 hover:text-zinc-200 transition", 7039 title: "Skip Tour", 7040 children: /* @__PURE__ */ jsxRuntimeExports.jsx(X, { size: 14 }) 7041 } 7042 ) 7043 ] }), 7044 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-1", children: [ 7045 /* @__PURE__ */ jsxRuntimeExports.jsx("h4", { className: "font-bold text-sm text-white", children: steps[currentStep].title }), 7046 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-zinc-300 leading-relaxed", children: steps[currentStep].content }) 7047 ] }), 7048 currentStep === 1 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-[10px] text-amber-400/90 font-medium flex items-center gap-1.5 bg-amber-500/10 border border-amber-500/20 px-2.5 py-1 rounded", children: [
7049 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "w-1.5 h-1.5 rounded-full bg-amber-400 animate-ping" }), 7050 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Try typing inside the code editor to auto-advance." }) 7051 ] }), 7052 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between mt-2 pt-2 border-t border-[#3c3c54]/30", children: [ 7053 /* @__PURE__ */ jsxRuntimeExports.jsx( 7054 "button", 7055 { 7056 onClick: handleSkip, 7057 className: "text-[10px] text-zinc-450 hover:text-zinc-255 font-bold uppercase transition", 7058 children: "Skip Tour" 7059 } 7060 ), 7061 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 7062 currentStep > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx( 7063 "button", 7064 { 7065 onClick: handlePrev, 7066 className: "px-2.5 py-1 text-[10px] font-bold rounded bg-zinc-800 hover:bg-zinc-700 text-zinc-300 transition", 7067 children: "Back" 7068 } 7069 ),
7070 /* @__PURE__ */ jsxRuntimeExports.jsxs( 7071 "button", 7072 { 7073 onClick: handleNext, 7074 className: "px-3 py-1 text-[10px] font-bold rounded bg-indigo-600 hover:bg-indigo-500 text-white transition flex items-center gap-1", 7075 children: [ 7076 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: currentStep === steps.length - 1 ? "Finish" : "Next" }), 7077 /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowRight, { size: 10 }) 7078 ] 7079 } 7080 ) 7081 ] }) 7082 ] }) 7083 ] 7084 } 7085 ) 7086 ] }); 7087}; 7088class ScoreErrorBoundary extends React.Component { 7089 constructor(props) { 7090 super(props); 7091 this.state = { hasError: false }; 7092 } 7093 static getDerivedStateFromError(error) { 7094 return { hasError: true }; 7095 } 7096 componentDidCatch(error, errorInfo) { 7097 console.error("ScoreErrorBoundary caught rendering error:", error, errorInfo); 7098 } 7099 componentDidUpdate(prevProps) { 7100 if (this.state.hasError && prevProps.svg !== this.props.svg) { 7101 this.setState({ hasError: false }); 7102 } 7103 } 7104 render() { 7105 if (this.state.hasError) { 7106 return this.props.fallback; 7107 } 7108 return this.props.children; 7109 } 7110} 7111const getBeatsPerMeasureFromDoc = (doc) => { 7112 if (!doc) return 4; 7113 try { 7114 const scoreDef = doc.querySelector("scoreDef"); 7115 let count = 4; 7116 let unit = 4; 7117 if (scoreDef) { 7118 const meterCount = scoreDef.getAttribute("meter.count"); 7119 const meterUnit = scoreDef.getAttribute("meter.unit"); 7120 if (meterCount) count = parseInt(meterCount, 10) || 4; 7121 if (meterUnit) unit = parseInt(meterUnit, 10) || 4; 7122 } else { 7123 const staffDef = doc.querySelector("staffDef"); 7124 if (staffDef) { 7125 const meterCount = staffDef.getAttribute("meter.count"); 7126 const meterUnit = staffDef.getAttribute("meter.unit"); 7127 if (meterCount) count = parseInt(meterCount, 10) || 4; 7128 if (meterUnit) unit = parseInt(meterUnit, 10) || 4; 7129 } 7130 } 7131 return count * (4 / unit); 7132 } catch (e) { 7133 console.error("Failed to parse meter from MEI Doc", e); 7134 } 7135 return 4; 7136}; 7137const getBeatsPerMeasureFromMei = (meiString) => { 7138 if (!meiString) return 4; 7139 try { 7140 const parser = new DOMParser(); 7141 const doc = parser.parseFromString(meiString, "application/xml"); 7142 return getBeatsPerMeasureFromDoc(doc); 7143 } catch (e) { 7144 console.error("Failed to parse MEI", e); 7145 } 7146 return 4; 7147}; 7148const getStartTimeInLayer = (layer, targetEl) => { 7149 let eventEl = targetEl; 7150 if (targetEl.parentElement && targetEl.parentElement.tagName === "chord") { 7151 eventEl = targetEl.parentElement; 7152 } 7153 let elapsed = 0; 7154 const children = Array.from(layer.children); 7155 for (const child of children) { 7156 if (child === eventEl) { 7157 break; 7158 } 7159 if (child.tagName === "note" || child.tagName === "chord" || child.tagName === "rest" || child.tagName === "mRest") { 7160 const durAttr = child.getAttribute("dur") || "4"; 7161 const dotsAttr = child.getAttribute("dots") || null; 7162 elapsed += getBeats(durAttr, dotsAttr); 7163 } 7164 } 7165 return elapsed; 7166}; 7167const checkOverlapInLayer = (layer, snappedBeat, beatDur) => { 7168 const beatsPerMeasure = getBeatsPerMeasureFromDoc(layer.ownerDocument); 7169 let currentBeat = 0; 7170 const children = Array.from(layer.children); 7171 for (const child of children) { 7172 if (child.tagName === "note" || child.tagName === "chord") { 7173 const durAttr = child.getAttribute("dur") || "4"; 7174 const dotsAttr = child.getAttribute("dots") || null; 7175 const duration = getBeats(durAttr, dotsAttr); 7176 const start = currentBeat; 7177 const end = start + duration; 7178 if (snappedBeat < end - 1e-3 && start < snappedBeat + beatDur - 1e-3) { 7179 return true; 7180 } 7181 currentBeat += duration; 7182 } else if (child.tagName === "rest") { 7183 const durAttr = child.getAttribute("dur") || "4"; 7184 const dotsAttr = child.getAttribute("dots") || null; 7185 currentBeat += getBeats(durAttr, dotsAttr); 7186 } else if (child.tagName === "mRest") { 7187 currentBeat += beatsPerMeasure; 7188 } 7189 } 7190 return false;
7191}; 7192const getActualNoteStartTime = (noteEl, measureNum, elapsedBeats) => { 7193 const doc = noteEl.ownerDocument; 7194 const beatsPerMeasure = getBeatsPerMeasureFromDoc(doc); 7195 const tie = noteEl.getAttribute("tie"); 7196 const startBeat = (measureNum - 1) * beatsPerMeasure + elapsedBeats; 7197 let startSeconds = startBeat * 0.5; 7198 if (tie === "e") { 7199 const pname = noteEl.getAttribute("pname") || "c"; 7200 const oct = noteEl.getAttribute("oct") || "4"; 7201 const measure = noteEl.closest("measure"); 7202 let prevM = measure == null ? void 0 : measure.previousElementSibling; 7203 while (prevM && prevM.tagName !== "measure") { 7204 prevM = prevM.previousElementSibling; 7205 } 7206 if (prevM) { 7207 const prevNote = Array.from(prevM.querySelectorAll("note")).find( 7208 (n) => n.getAttribute("pname") === pname && n.getAttribute("oct") === oct && n.getAttribute("tie") === "s" 7209 ); 7210 if (prevNote) { 7211 const prevLayer = prevNote.closest("layer"); 7212 if (prevLayer) { 7213 let prevEventEl = prevNote; 7214 if (prevNote.parentElement && prevNote.parentElement.tagName === "chord") { 7215 prevEventEl = prevNote.parentElement; 7216 } 7217 const prevElapsedBeats = getStartTimeInLayer(prevLayer, prevEventEl); 7218 let prevMeasureNum = measureNum - 1; 7219 const prevMeasure = prevLayer.closest("measure"); 7220 if (prevMeasure) { 7221 const prevNAttr = prevMeasure.getAttribute("n"); 7222 if (prevNAttr) prevMeasureNum = parseInt(prevNAttr); 7223 } 7224 const prevStartBeat = (prevMeasureNum - 1) * beatsPerMeasure + prevElapsedBeats; 7225 startSeconds = prevStartBeat * 0.5; 7226 } 7227 } 7228 } 7229 } 7230 return startSeconds; 7231}; 7232const cleanTiesForNote = (doc, noteId) => { 7233 const ties = Array.from(doc.querySelectorAll("tie")); 7234 ties.forEach((tie) => { 7235 const startid = tie.getAttribute("startid"); 7236 const endid = tie.getAttribute("endid"); 7237 if (startid === `#${noteId}` || endid === `#${noteId}`) { 7238 tie.remove(); 7239 } 7240 }); 7241}; 7242const getSnappingOffset = (clientY, staffNode) => { 7243 const staffLines = Array.from(staffNode.querySelectorAll("path")); 7244 if (staffLines.length < 5) return 0; 7245 const pathsWithWidth = staffLines.map((line) => { 7246 const rect = line.getBoundingClientRect(); 7247 return { line, width: rect.width, rect }; 7248 }).sort((a, b) => b.width - a.width); 7249 const rects = pathsWithWidth.slice(0, 5).map((item) => item.rect).sort((a, b) => a.top - b.top); 7250 if (rects.length === 5) { 7251 const topLineY = rects[0].top; 7252 const bottomLineY = rects[4].top; 7253 const avgGap = (bottomLineY - topLineY) / 4; 7254 const stepSize = avgGap / 2; 7255 const relativeY = clientY - topLineY; 7256 return Math.round(relativeY / stepSize); 7257 } 7258 return 0; 7259}; 7260const getXmlElById = (xmlDoc, id) => { 7261 if (!id) return null; 7262 return xmlDoc.getElementById(id) || xmlDoc.querySelector(`[id="${id}"]`) || xmlDoc.querySelector(`[xml\\:id="${id}"]`) || xmlDoc.querySelector(`[*|id="${id}"]`) || Array.from(xmlDoc.getElementsByTagName("*")).find((el) => el.getAttribute("xml:id") === id) || null; 7263}; 7264const ScoreEditor = () => { 7265 var _a, _b; 7266 const { activeTrackIndex, addNote, updateNote, deleteNote, isPlaying, play, pause, stop, engine, meiDocs, setMeiDoc, selectedNoteIds, setSelectedNoteIds, patterns, setPatterns, activePatternId, forceSync, tempo } = useEditor(); 7267 const [currentTool, setCurrentTool] = reactExports.useState("select"); 7268 const [currentAccidental, setCurrentAccidental] = reactExports.useState(null); 7269 const [currentVoice, setCurrentVoice] = reactExports.useState(1); 7270 const [entryMode, setEntryMode] = reactExports.useState("note"); 7271 const [isDotted, setIsDotted] = reactExports.useState(false); 7272 const [isTieActive, setIsTieActive] = reactExports.useState(false); 7273 const [isTriplet, setIsTriplet] = reactExports.useState(false); 7274 const [currentArticulation, setCurrentArticulation] = reactExports.useState(null); 7275 const [showKeypad, setShowKeypad] = reactExports.useState(true); 7276 const containerRef = reactExports.useRef(null); 7277 const [dimensions, setDimensions] = reactExports.useState({ width: 800, height: 600 }); 7278 const [selectedElementId, setSelectedElementId] = reactExports.useState(null); 7279 const [zoom, setZoom] = reactExports.useState(0.42); 7280 const [layout, setLayout] = reactExports.useState("pages"); 7281 const [showGrid, setShowGrid] = reactExports.useState(true); 7282 const [showTextEditor, setShowTextEditor] = reactExports.useState(true);
7283 const [humdrumText, setHumdrumText] = reactExports.useState(""); 7284 const [syntaxError, setSyntaxError] = reactExports.useState(null); 7285 const [aceReady, setAceReady] = reactExports.useState(false); 7286 const [forceRerender, setForceRerender] = reactExports.useState(0); 7287 const aceEditorRef = reactExports.useRef(null); 7288 const aceContainerRef = reactExports.useRef(null); 7289 const editingFromScoreRef = reactExports.useRef(false); 7290 const [lastSyncedNotesStr, setLastSyncedNotesStr] = reactExports.useState(""); 7291 const [showLiveTour, setShowLiveTour] = reactExports.useState(false); 7292 const debounceTimerRef = reactExports.useRef(null); 7293 const audioCtxRef = reactExports.useRef(null); 7294 const playNotePreview = reactExports.useCallback((pitch) => { 7295 try { 7296 if (!audioCtxRef.current) { 7297 audioCtxRef.current = new (window.AudioContext || window.webkitAudioContext)(); 7298 } 7299 const ctx = audioCtxRef.current; 7300 if (ctx.state === "suspended") { 7301 ctx.resume(); 7302 } 7303 const now = ctx.currentTime; 7304 const osc = ctx.createOscillator(); 7305 const gain = ctx.createGain(); 7306 const freq = 440 * Math.pow(2, (pitch - 69) / 12); 7307 osc.frequency.value = freq; 7308 osc.type = "triangle"; 7309 gain.gain.setValueAtTime(0, now); 7310 gain.gain.linearRampToValueAtTime(0.12, now + 0.01); 7311 gain.gain.exponentialRampToValueAtTime(1e-3, now + 0.35); 7312 osc.connect(gain); 7313 gain.connect(ctx.destination); 7314 osc.start(now); 7315 osc.stop(now + 0.35); 7316 } catch (e) { 7317 console.error("Failed to play audio preview:", e); 7318 } 7319 }, []); 7320 const isPercussion = activeTrackIndex !== null && ((_b = (_a = engine == null ? void 0 : engine.midi) == null ? void 0 : _a.tracks[activeTrackIndex]) == null ? void 0 : _b.channel) === 9; 7321 const [showMeasureNumbers, setShowMeasureNumbers] = reactExports.useState(true); 7322 const { 7323 isReady, 7324 svg, 7325 insertNote, 7326 deleteElement, 7327 updateMei, 7328 toolkit, 7329 undo, 7330 redo, 7331 canUndo, 7332 canRedo 7333 } = useScoreEditorEngine({ 7334 dimensions, 7335 zoom, 7336 isPercussion, 7337 initialMei: activeTrackIndex !== null ? meiDocs[activeTrackIndex] : void 0, 7338 showMeasureNumbers, 7339 onMeiUpdate: (newMei) => { 7340 if (activeTrackIndex !== null) { 7341 setMeiDoc(activeTrackIndex, newMei); 7342 if (toolkit) { 7343 try { 7344 toolkit.loadData(newMei); 7345 const hum = toolkit.getHumdrum ? toolkit.getHumdrum() : newMei; 7346 setHumdrumText(hum); 7347 setSyntaxError(null); 7348 } catch (e) { 7349 console.error("Failed to convert MEI to Humdrum:", e); 7350 } 7351 } 7352 } 7353 } 7354 }); 7355 reactExports.useEffect(() => { 7356 if (isReady && !localStorage.getItem("hamienet_live_tour_done")) { 7357 localStorage.setItem("hamienet_live_tour_done", "true"); 7358 const timer = setTimeout(() => { 7359 setShowLiveTour(true); 7360 }, 1e3); 7361 return () => clearTimeout(timer); 7362 } 7363 }, [isReady]); 7364 reactExports.useEffect(() => { 7365 const handleKeyDown = (e) => { 7366 if ((e.ctrlKey || e.metaKey) && !e.shiftKey) { 7367 if (e.key === "z" || e.key === "Z") { 7368 e.preventDefault(); 7369 undo(); 7370 } else if (e.key === "y" || e.key === "Y") { 7371 e.preventDefault(); 7372 redo(); 7373 } 7374 } 7375 }; 7376 window.addEventListener("keydown", handleKeyDown); 7377 return () => { 7378 window.removeEventListener("keydown", handleKeyDown); 7379 }; 7380 }, [undo, redo]); 7381 reactExports.useEffect(() => { 7382 if (!showTextEditor) return; 7383 const injectAce = async () => { 7384 if (window.ace) { 7385 setAceReady(true); 7386 return; 7387 } 7388 try { 7389 await new Promise((resolve, reject) => { 7390 const script = document.createElement("script"); 7391 script.src = "https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.12/ace.js"; 7392 script.async = true; 7393 script.onload = () => resolve(); 7394 script.onerror = () => reject(new Error("Failed to load Ace Editor")); 7395 document.head.appendChild(script); 7396 }); 7397 setAceReady(true); 7398 } catch (err) { 7399 console.error(err); 7400 } 7401 }; 7402 injectAce(); 7403 }, [showTextEditor]); 7404 const handleHumdrumEditorChange = reactExports.useCallback((newHumdrum) => { 7405 if (!toolkit || activeTrackIndex === null || !engine) return; 7406 editingFromScoreRef.current = true; 7407 try { 7408 const success = toolkit.loadData(newHumdrum); 7409 if (success === true || success === 1) { 7410 setSyntaxError(null); 7411 const render = toolkit.renderToSVG(1, {}); 7412 const newMei = toolkit.getMEI(); 7413 setMeiDoc(activeTrackIndex, newMei); 7414 const base64Midi = toolkit.renderToMIDI(); 7415 if (base64Midi) { 7416 const newNotes = getMidiNotesFromBase64(base64Midi, engine.midi.header.ppq); 7417 const newNotesStr = JSON.stringify(newNotes); 7418 setLastSyncedNotesStr(newNotesStr); 7419 setPatterns((prev) => prev.map((p) => { 7420 if (p.id === activePatternId) { 7421 return { 7422 ...p, 7423 notes: newNotes 7424 }; 7425 } 7426 return p; 7427 })); 7428 setTimeout(() => forceSync(), 0); 7429 } 7430 } else { 7431 setSyntaxError("Invalid Humdrum/Notation syntax"); 7432 } 7433 } catch (err) { 7434 console.error("Verovio processing failed:", err); 7435 setSyntaxError((err == null ? void 0 : err.message) || "Invalid Humdrum/Notation syntax"); 7436 } 7437 }, [toolkit, activeTrackIndex, engine, setMeiDoc, activePatternId, setPatterns, forceSync]); 7438 reactExports.useEffect(() => { 7439 if (!aceReady || !showTextEditor || !aceContainerRef.current) return; 7440 const ace = window.ace; 7441 if (!ace) return; 7442 const editor = ace.edit(aceContainerRef.current); 7443 const isDark = document.documentElement.classList.contains("dark"); 7444 editor.setTheme(isDark ? "ace/theme/monokai" : "ace/theme/chrome"); 7445 editor.session.setMode("ace/mode/text"); 7446 editor.setShowPrintMargin(false); 7447 editor.setOption("fontSize", "13px"); 7448 editor.setValue(humdrumText, -1); 7449 const onChange = () => { 7450 const val = editor.getValue(); 7451 if (val !== humdrumText) { 7452 if (debounceTimerRef.current) { 7453 clearTimeout(debounceTimerRef.current); 7454 } 7455 debounceTimerRef.current = setTimeout(() => { 7456 handleHumdrumEditorChange(val); 7457 }, 280); 7458 } 7459 }; 7460 editor.on("change", onChange); 7461 aceEditorRef.current = editor; 7462 return () => { 7463 if (debounceTimerRef.current) { 7464 clearTimeout(debounceTimerRef.current); 7465 } 7466 if (aceEditorRef.current) { 7467 aceEditorRef.current.off("change", onChange); 7468 aceEditorRef.current.destroy(); 7469 aceEditorRef.current = null; 7470 } 7471 }; 7472 }, [aceReady, showTextEditor, handleHumdrumEditorChange]); 7473 reactExports.useEffect(() => { 7474 if (aceEditorRef.current && humdrumText !== aceEditorRef.current.getValue()) { 7475 const cursor = aceEditorRef.current.getCursorPosition(); 7476 aceEditorRef.current.setValue(humdrumText, -1); 7477 aceEditorRef.current.moveCursorToPosition(cursor); 7478 } 7479 }, [humdrumText]); 7480 reactExports.useEffect(() => { 7481 if (!aceEditorRef.current) return; 7482 const isDark = document.documentElement.classList.contains("dark"); 7483 aceEditorRef.current.setTheme(isDark ? "ace/theme/monokai" : "ace/theme/chrome"); 7484 }, [aceReady, showTextEditor]); 7485 reactExports.useEffect(() => { 7486 if (activeTrackIndex === null || !engine || !engine.midi || !engine.midi.tracks[activeTrackIndex]) return; 7487 const track = engine.midi.tracks[activeTrackIndex]; 7488 const currentNotesStr = JSON.stringify(track.notes.map((n) => ({ 7489 midi: n.midi, 7490 ticks: n.ticks, 7491 durationTicks: n.durationTicks, 7492 velocity: n.velocity, 7493 voice: n.voice || 1 7494 }))); 7495 if (currentNotesStr !== lastSyncedNotesStr) { 7496 if (editingFromScoreRef.current) { 7497 editingFromScoreRef.current = false;
7498 setLastSyncedNotesStr(currentNotesStr); 7499 return; 7500 } 7501 setLastSyncedNotesStr(currentNotesStr); 7502 const ppq = engine.midi.header.ppq; 7503 const newMei = convertMidiToMEI(track, ppq); 7504 setMeiDoc(activeTrackIndex, newMei); 7505 if (toolkit) { 7506 try { 7507 toolkit.loadData(newMei); 7508 const hum = toolkit.getHumdrum ? toolkit.getHumdrum() : newMei; 7509 setHumdrumText(hum); 7510 setSyntaxError(null); 7511 } catch (e) { 7512 console.error("Error loading regenerated MEI:", e); 7513 } 7514 } else { 7515 setHumdrumText(newMei); 7516 } 7517 } else { 7518 const isCurrentlyMeiXml = humdrumText.trim().startsWith("<"); 7519 if (toolkit && isCurrentlyMeiXml && activeTrackIndex !== null && meiDocs[activeTrackIndex]) { 7520 try { 7521 const currentMei = meiDocs[activeTrackIndex]; 7522 toolkit.loadData(currentMei); 7523 const hum = toolkit.getHumdrum ? toolkit.getHumdrum() : currentMei; 7524 setHumdrumText(hum); 7525 setSyntaxError(null); 7526 } catch (e) { 7527 console.error("Error on initial conversion:", e); 7528 } 7529 } 7530 } 7531 }, [activeTrackIndex, engine, patterns, toolkit, setMeiDoc, lastSyncedNotesStr, humdrumText, meiDocs]); 7532 reactExports.useEffect(() => { 7533 if (!containerRef.current) return; 7534 const observer = new ResizeObserver((entries) => { 7535 for (const entry of entries) { 7536 if (entry.contentRect.width > 0) { 7537 setDimensions({ width: entry.contentRect.width, height: entry.contentRect.height }); 7538 } 7539 } 7540 }); 7541 observer.observe(containerRef.current); 7542 return () => observer.disconnect(); 7543 }, []); 7544 reactExports.useEffect(() => { 7545 const handleWheel = (e) => { 7546 if (e.shiftKey) { 7547 e.preventDefault(); 7548 const delta = e.deltaY; 7549 setZoom((prevZoom) => {
7550 const newZoom = prevZoom - delta * 1e-3; 7551 return Math.max(0.1, Math.min(3, newZoom)); 7552 }); 7553 } 7554 }; 7555 const el = containerRef.current; 7556 if (el) { 7557 el.addEventListener("wheel", handleWheel, { passive: false }); 7558 } 7559 return () => { 7560 if (el) { 7561 el.removeEventListener("wheel", handleWheel); 7562 } 7563 }; 7564 }, []); 7565 reactExports.useEffect(() => { 7566 var _a2; 7567 if (selectedNoteIds.length > 0 && activeTrackIndex !== null && meiDocs[activeTrackIndex] && ((_a2 = engine == null ? void 0 : engine.midi) == null ? void 0 : _a2.tracks[activeTrackIndex])) { 7568 const track = engine.midi.tracks[activeTrackIndex]; 7569 const note = track.notes[selectedNoteIds[0]]; 7570 if (!note) return; 7571 const parser = new DOMParser(); 7572 const doc = parser.parseFromString(meiDocs[activeTrackIndex], "application/xml"); 7573 const notes = Array.from(doc.querySelectorAll("note")); 7574 let foundId = null; 7575 for (const noteEl of notes) { 7576 const pname = noteEl.getAttribute("pname") || "c"; 7577 const oct = noteEl.getAttribute("oct") || "4"; 7578 const originalAccid = noteEl.getAttribute("accid") || void 0; 7579 const oldMidi = getMidiFromMEI(pname, parseInt(oct), originalAccid); 7580 if (oldMidi === note.midi) { 7581 const layer = noteEl.closest("layer"); 7582 if (layer) { 7583 let eventEl = noteEl; 7584 if (noteEl.parentElement && noteEl.parentElement.tagName === "chord") { 7585 eventEl = noteEl.parentElement; 7586 } 7587 const elapsedBeats = getStartTimeInLayer(layer, eventEl); 7588 const measure = layer.closest("measure"); 7589 let measureNum = 1; 7590 if (measure) { 7591 const nAttr = measure.getAttribute("n"); 7592 if (nAttr) measureNum = parseInt(nAttr); 7593 } 7594 const actualStartSeconds = getActualNoteStartTime(noteEl, measureNum, elapsedBeats); 7595 if (Math.abs(note.time - actualStartSeconds) < 0.05) { 7596 foundId = noteEl.getAttribute("xml:id"); 7597 break; 7598 } 7599 } 7600 } 7601 } 7602 if (foundId && foundId !== selectedElementId) { 7603 setSelectedElementId(foundId); 7604 } 7605 } else if (selectedNoteIds.length === 0 && selectedElementId) { 7606 setSelectedElementId(null); 7607 } 7608 }, [selectedNoteIds, activeTrackIndex, meiDocs, engine]); 7609 const applyAccidentalToNote = reactExports.useCallback((noteId, accidName) => { 7610 editingFromScoreRef.current = true; 7611 updateMei((doc) => { 7612 var _a2; 7613 const noteEl = getXmlElById(doc, noteId); 7614 if (noteEl && noteEl.tagName === "note") { 7615 const pname = noteEl.getAttribute("pname") || "c"; 7616 const oct = noteEl.getAttribute("oct") || "4"; 7617 const originalAccid = noteEl.getAttribute("accid") || void 0; 7618 const oldMidi = getMidiFromMEI(pname, parseInt(oct), originalAccid); 7619 const tie = noteEl.getAttribute("tie"); 7620 const layer = noteEl.closest("layer"); 7621 if (layer) { 7622 let eventEl = noteEl; 7623 if (noteEl.parentElement && noteEl.parentElement.tagName === "chord") { 7624 eventEl = noteEl.parentElement; 7625 } 7626 const elapsedBeats = getStartTimeInLayer(layer, eventEl); 7627 const measure = layer.closest("measure"); 7628 let measureNum = 1; 7629 if (measure) { 7630 const nAttr = measure.getAttribute("n"); 7631 if (nAttr) measureNum = parseInt(nAttr); 7632 } 7633 getBeatsPerMeasureFromDoc(doc); 7634 const actualStartSeconds = getActualNoteStartTime(noteEl, measureNum, elapsedBeats); 7635 const newAccid = accidName === "sharp" ? "s" : accidName === "flat" ? "f" : accidName === "natural" ? "n" : void 0; 7636 if (newAccid) { 7637 noteEl.setAttribute("accid", newAccid); 7638 } else { 7639 noteEl.removeAttribute("accid"); 7640 } 7641 const newMidi = getMidiFromMEI(pname, parseInt(oct), newAccid); 7642 playNotePreview(newMidi); 7643 if (activeTrackIndex !== null && ((_a2 = engine == null ? void 0 : engine.midi) == null ? void 0 : _a2.tracks[activeTrackIndex])) { 7644 const track = engine.midi.tracks[activeTrackIndex]; 7645 const noteIndex = track.notes.findIndex((n) => n.midi === oldMidi && Math.abs(n.time - actualStartSeconds) < 0.05); 7646 if (noteIndex !== -1) { 7647 updateNote(activeTrackIndex, noteIndex, { midi: newMidi }); 7648 } 7649 } 7650 if (tie === "s") { 7651 const nextMeasure = measure == null ? void 0 : measure.nextElementSibling; 7652 let nextM = nextMeasure; 7653 while (nextM && nextM.tagName !== "measure") { 7654 nextM = nextM.nextElementSibling; 7655 } 7656 if (nextM) { 7657 const nextNote = Array.from(nextM.querySelectorAll("note")).find( 7658 (n) => n.getAttribute("pname") === pname && n.getAttribute("oct") === oct && n.getAttribute("tie") === "e" 7659 ); 7660 if (nextNote) { 7661 if (newAccid) nextNote.setAttribute("accid", newAccid); 7662 else nextNote.removeAttribute("accid"); 7663 } 7664 } 7665 } else if (tie === "e") { 7666 let prevM = measure == null ? void 0 : measure.previousElementSibling; 7667 while (prevM && prevM.tagName !== "measure") { 7668 prevM = prevM.previousElementSibling; 7669 } 7670 if (prevM) { 7671 const prevNote = Array.from(prevM.querySelectorAll("note")).find( 7672 (n) => n.getAttribute("pname") === pname && n.getAttribute("oct") === oct && n.getAttribute("tie") === "s" 7673 ); 7674 if (prevNote) { 7675 if (newAccid) prevNote.setAttribute("accid", newAccid); 7676 else prevNote.removeAttribute("accid"); 7677 } 7678 } 7679 } 7680 } 7681 } 7682 }); 7683 }, [activeTrackIndex, engine, updateNote, updateMei]); 7684 const applyDurationToNote = reactExports.useCallback((noteId, tool) => { 7685 editingFromScoreRef.current = true; 7686 const durMap = { 7687 "note_whole": "1", 7688 "note_half": "2", 7689 "note_quarter": "4", 7690 "note_eighth": "8", 7691 "note_sixteenth": "16" 7692 }; 7693 const dur = durMap[tool]; 7694 if (!dur) return; 7695 updateMei((doc) => { 7696 var _a2; 7697 const noteEl = getXmlElById(doc, noteId); 7698 if (noteEl && noteEl.tagName === "note") {
7699 const pname = noteEl.getAttribute("pname") || "c"; 7700 const oct = noteEl.getAttribute("oct") || "4"; 7701 const accid = noteEl.getAttribute("accid") || void 0; 7702 const midiPitch = getMidiFromMEI(pname, parseInt(oct), accid); 7703 playNotePreview(midiPitch); 7704 const tie = noteEl.getAttribute("tie"); 7705 const layer = noteEl.closest("layer"); 7706 if (layer) { 7707 let eventEl = noteEl; 7708 if (noteEl.parentElement && noteEl.parentElement.tagName === "chord") { 7709 eventEl = noteEl.parentElement; 7710 } 7711 const elapsedBeats = getStartTimeInLayer(layer, eventEl); 7712 const measure = layer.closest("measure"); 7713 let measureNum = 1; 7714 if (measure) { 7715 const nAttr = measure.getAttribute("n"); 7716 if (nAttr) measureNum = parseInt(nAttr); 7717 } 7718 getBeatsPerMeasureFromDoc(doc); 7719 const actualStartSeconds = getActualNoteStartTime(noteEl, measureNum, elapsedBeats); 7720 const newBeatDur = getDurationInBeats(dur); 7721 if (activeTrackIndex !== null && ((_a2 = engine == null ? void 0 : engine.midi) == null ? void 0 : _a2.tracks[activeTrackIndex])) { 7722 const track = engine.midi.tracks[activeTrackIndex]; 7723 const noteIndex = track.notes.findIndex((n) => n.midi === midiPitch && Math.abs(n.time - actualStartSeconds) < 0.05); 7724 if (noteIndex !== -1) { 7725 updateNote(activeTrackIndex, noteIndex, { duration: newBeatDur * 0.5 }); 7726 } 7727 } 7728 cleanTiesForNote(doc, noteId); 7729 if (tie === "s") { 7730 const nextMeasure = measure == null ? void 0 : measure.nextElementSibling; 7731 let nextM = nextMeasure; 7732 while (nextM && nextM.tagName !== "measure") { 7733 nextM = nextM.nextElementSibling; 7734 } 7735 if (nextM) { 7736 const nextNote = Array.from(nextM.querySelectorAll("note")).find( 7737 (n) => n.getAttribute("pname") === pname && n.getAttribute("oct") === oct && n.getAttribute("tie") === "e" 7738 ); 7739 if (nextNote) { 7740 const nextNoteId = nextNote.getAttribute("xml:id") || nextNote.getAttribute("id"); 7741 if (nextNoteId) cleanTiesForNote(doc, nextNoteId); 7742 nextNote.remove(); 7743 } 7744 } 7745 } else if (tie === "e") { 7746 let prevM = measure == null ? void 0 : measure.previousElementSibling; 7747 while (prevM && prevM.tagName !== "measure") { 7748 prevM = prevM.previousElementSibling; 7749 } 7750 if (prevM) { 7751 const prevNote = Array.from(prevM.querySelectorAll("note")).find( 7752 (n) => n.getAttribute("pname") === pname && n.getAttribute("oct") === oct && n.getAttribute("tie") === "s" 7753 ); 7754 if (prevNote) { 7755 prevNote.removeAttribute("tie"); 7756 const prevNoteId = prevNote.getAttribute("xml:id") || prevNote.getAttribute("id"); 7757 if (prevNoteId) cleanTiesForNote(doc, prevNoteId); 7758 } 7759 } 7760 } 7761 noteEl.removeAttribute("dots"); 7762 if (elapsedBeats + newBeatDur > 4) { 7763 const ns = "http://www.music-encoding.org/ns/mei"; 7764 const dur1 = 4 - elapsedBeats; 7765 const dur2 = newBeatDur - dur1; 7766 const { dur: dur1Attr, dots: dots1Attr } = getMeiDurAttr(dur1); 7767 noteEl.setAttribute("dur", dur1Attr); 7768 if (dots1Attr) noteEl.setAttribute("dots", dots1Attr); 7769 noteEl.setAttribute("tie", "s"); 7770 noteEl.setAttribute("tmp-start-beat", elapsedBeats.toString()); 7771 let nextMeasure = measure == null ? void 0 : measure.nextElementSibling; 7772 let nextM = nextMeasure; 7773 while (nextM && nextM.tagName !== "measure") { 7774 nextM = nextM.nextElementSibling; 7775 } 7776 if (!nextM && measure && measure.parentNode) { 7777 const parentSection = measure.parentNode; 7778 const nextN = measureNum + 1; 7779 const newMeasure = doc.createElementNS(ns, "measure"); 7780 newMeasure.setAttribute("n", nextN.toString()); 7781 newMeasure.setAttribute("xml:id", `m-${nextN}`); 7782 const staff = doc.createElementNS(ns, "staff"); 7783 staff.setAttribute("n", "1"); 7784 staff.setAttribute("xml:id", `s-${nextN}`); 7785 const newLayer = doc.createElementNS(ns, "layer"); 7786 newLayer.setAttribute("n", "1"); 7787 newLayer.setAttribute("xml:id", `l-${nextN}`); 7788 const mRest = doc.createElementNS(ns, "mRest"); 7789 mRest.setAttribute("xml:id", `mr-${nextN}`); 7790 newLayer.appendChild(mRest); 7791 staff.appendChild(newLayer); 7792 newMeasure.appendChild(staff); 7793 parentSection.appendChild(newMeasure); 7794 nextM = newMeasure; 7795 } 7796 if (nextM) { 7797 const currentLayerN = layer.getAttribute("n") || "1"; 7798 const nextStaff = nextM.querySelector("staff"); 7799 if (nextStaff) { 7800 let nextLayer = nextStaff.querySelector(`layer[n="${currentLayerN}"]`); 7801 if (!nextLayer) { 7802 nextLayer = doc.createElementNS(ns, "layer"); 7803 nextLayer.setAttribute("n", currentLayerN); 7804 const nextN = measureNum + 1; 7805 nextLayer.setAttribute("xml:id", `l-${nextN}-${currentLayerN}`); 7806 nextStaff.appendChild(nextLayer); 7807 } 7808 const nextMRest = nextLayer.querySelector("mRest"); 7809 if (nextMRest) nextMRest.remove(); 7810 const tiedNote = doc.createElementNS(ns, "note"); 7811 const tiedNoteId = "note-" + Math.random().toString(36).substr(2, 9); 7812 tiedNote.setAttribute("xml:id", tiedNoteId); 7813 tiedNote.setAttribute("pname", pname); 7814 tiedNote.setAttribute("oct", oct); 7815 if (accid) tiedNote.setAttribute("accid", accid); 7816 const { dur: dur2Attr, dots: dots2Attr } = getMeiDurAttr(dur2); 7817 tiedNote.setAttribute("dur", dur2Attr); 7818 if (dots2Attr) tiedNote.setAttribute("dots", dots2Attr); 7819 tiedNote.setAttribute("tie", "e"); 7820 tiedNote.setAttribute("tmp-start-beat", "0"); 7821 if (nextLayer.firstChild) { 7822 nextLayer.insertBefore(tiedNote, nextLayer.firstChild); 7823 } else { 7824 nextLayer.appendChild(tiedNote); 7825 } 7826 const tieEl = doc.createElementNS(ns, "tie"); 7827 tieEl.setAttribute("startid", `#${noteId}`); 7828 tieEl.setAttribute("endid", `#${tiedNoteId}`); 7829 tieEl.setAttribute("xml:id", "tie-" + Math.random().toString(36).substr(2, 9)); 7830 measure.appendChild(tieEl); 7831 } 7832 } 7833 } else { 7834 noteEl.setAttribute("dur", dur); 7835 noteEl.removeAttribute("tie"); 7836 noteEl.setAttribute("tmp-start-beat", elapsedBeats.toString()); 7837 } 7838 } 7839 } 7840 }); 7841 }, [activeTrackIndex, engine, updateNote, updateMei]); 7842 const applyArticulationToNote = reactExports.useCallback((noteId, articulationType) => { 7843 editingFromScoreRef.current = true; 7844 updateMei((doc) => { 7845 const noteEl = getXmlElById(doc, noteId); 7846 if (noteEl && noteEl.tagName === "note") { 7847 if (articulationType) { 7848 if (articulationType === "staccato") { 7849 noteEl.setAttribute("artic", "stacc"); 7850 noteEl.removeAttribute("fermata"); 7851 } else if (articulationType === "accent") { 7852 noteEl.setAttribute("artic", "acc"); 7853 noteEl.removeAttribute("fermata"); 7854 } else if (articulationType === "tenuto") { 7855 noteEl.setAttribute("artic", "ten"); 7856 noteEl.removeAttribute("fermata"); 7857 } else if (articulationType === "fermata") { 7858 noteEl.setAttribute("fermata", "above"); 7859 noteEl.removeAttribute("artic"); 7860 } 7861 } else { 7862 noteEl.removeAttribute("artic"); 7863 noteEl.removeAttribute("fermata"); 7864 } 7865 } 7866 }); 7867 }, [updateMei]); 7868 reactExports.useEffect(() => { 7869 if (!selectedElementId || !isReady || activeTrackIndex === null) return; 7870 const parser = new DOMParser(); 7871 const doc = parser.parseFromString(meiDocs[activeTrackIndex] || "", "application/xml"); 7872 const noteEl = getXmlElById(doc, selectedElementId); 7873 if (noteEl && noteEl.tagName === "note") { 7874 const accid = noteEl.getAttribute("accid"); 7875 const dur = noteEl.getAttribute("dur"); 7876 const artic = noteEl.getAttribute("artic"); 7877 const fermata = noteEl.getAttribute("fermata"); 7878 if (accid === "s") setCurrentAccidental("sharp");
7879 else if (accid === "f") setCurrentAccidental("flat"); 7880 else if (accid === "n") setCurrentAccidental("natural"); 7881 else setCurrentAccidental(null); 7882 if (artic === "stacc") setCurrentArticulation("staccato"); 7883 else if (artic === "acc") setCurrentArticulation("accent"); 7884 else if (artic === "ten") setCurrentArticulation("tenuto"); 7885 else if (fermata === "above") setCurrentArticulation("fermata"); 7886 else setCurrentArticulation(null); 7887 const toolMap = { 7888 "1": "note_whole", 7889 "2": "note_half", 7890 "4": "note_quarter", 7891 "8": "note_eighth", 7892 "16": "note_sixteenth" 7893 }; 7894 if (dur && toolMap[dur]) { 7895 setCurrentTool(toolMap[dur]); 7896 } 7897 } 7898 }, [selectedElementId, meiDocs, activeTrackIndex, isReady]); 7899 const handleSelectTool = reactExports.useCallback((tool) => { 7900 setCurrentTool(tool); 7901 if (selectedElementId && tool.startsWith("note_")) { 7902 applyDurationToNote(selectedElementId, tool); 7903 } else if (tool === "select" || tool === "erase") { 7904 setSelectedElementId(null); 7905 } 7906 }, [selectedElementId, applyDurationToNote]); 7907 const handleSelectAccidental = reactExports.useCallback((accid) => { 7908 setCurrentAccidental(accid); 7909 if (selectedElementId && (accid === null || accid === "sharp" || accid === "flat" || accid === "natural")) { 7910 applyAccidentalToNote(selectedElementId, accid); 7911 } 7912 }, [selectedElementId, applyAccidentalToNote]); 7913 const handleSelectArticulation = reactExports.useCallback((artic) => { 7914 setCurrentArticulation(artic); 7915 if (selectedElementId) { 7916 applyArticulationToNote(selectedElementId, artic); 7917 } 7918 }, [selectedElementId, applyArticulationToNote]); 7919 const handleDeleteElement = reactExports.useCallback((elementId) => { 7920 editingFromScoreRef.current = true; 7921 updateMei((doc) => { 7922 const noteEl = getXmlElById(doc, elementId); 7923 if (noteEl && noteEl.tagName === "note") { 7924 const pname = noteEl.getAttribute("pname") || ""; 7925 const oct = noteEl.getAttribute("oct") || ""; 7926 const accid = noteEl.getAttribute("accid") || void 0; 7927 const midiPitch = getMidiFromMEI(pname, parseInt(oct), accid); 7928 const tie = noteEl.getAttribute("tie"); 7929 const layer = noteEl.closest("layer"); 7930 if (layer) { 7931 let eventEl = noteEl; 7932 if (noteEl.parentElement && noteEl.parentElement.tagName === "chord") { 7933 eventEl = noteEl.parentElement; 7934 } 7935 const elapsedBeats = getStartTimeInLayer(layer, eventEl); 7936 const measure = layer.closest("measure"); 7937 let measureNum = 1; 7938 if (measure) { 7939 const nAttr = measure.getAttribute("n"); 7940 if (nAttr) measureNum = parseInt(nAttr); 7941 } 7942 getBeatsPerMeasureFromDoc(doc); 7943 const actualStartSeconds = getActualNoteStartTime(noteEl, measureNum, elapsedBeats); 7944 if (activeTrackIndex !== null && engine && engine.midi && engine.midi.tracks[activeTrackIndex]) { 7945 const track = engine.midi.tracks[activeTrackIndex]; 7946 const idx = track.notes.findIndex((n) => n.midi === midiPitch && Math.abs(n.time - actualStartSeconds) < 0.05); 7947 if (idx !== -1) { 7948 deleteNote(activeTrackIndex, idx, "score"); 7949 } 7950 } 7951 cleanTiesForNote(doc, elementId); 7952 if (tie === "s") { 7953 const nextMeasure = measure == null ? void 0 : measure.nextElementSibling; 7954 let nextM = nextMeasure; 7955 while (nextM && nextM.tagName !== "measure") { 7956 nextM = nextM.nextElementSibling; 7957 } 7958 if (nextM) { 7959 const nextNote = Array.from(nextM.querySelectorAll("note")).find( 7960 (n) => n.getAttribute("pname") === pname && n.getAttribute("oct") === oct && n.getAttribute("tie") === "e" 7961 ); 7962 if (nextNote) { 7963 const nextNoteId = nextNote.getAttribute("xml:id") || nextNote.getAttribute("id"); 7964 if (nextNoteId) cleanTiesForNote(doc, nextNoteId); 7965 nextNote.remove(); 7966 } 7967 } 7968 } else if (tie === "e") { 7969 let prevM = measure == null ? void 0 : measure.previousElementSibling; 7970 while (prevM && prevM.tagName !== "measure") { 7971 prevM = prevM.previousElementSibling; 7972 } 7973 if (prevM) { 7974 const prevNote = Array.from(prevM.querySelectorAll("note")).find( 7975 (n) => n.getAttribute("pname") === pname && n.getAttribute("oct") === oct && n.getAttribute("tie") === "s" 7976 ); 7977 if (prevNote) { 7978 const prevNoteId = prevNote.getAttribute("xml:id") || prevNote.getAttribute("id");
7979 if (prevNoteId) cleanTiesForNote(doc, prevNoteId); 7980 prevNote.remove(); 7981 } 7982 } 7983 } 7984 } 7985 const parent = noteEl.parentNode; 7986 noteEl.remove(); 7987 if (parent && parent.tagName === "chord") { 7988 const remainingNotes = Array.from(parent.querySelectorAll("note")); 7989 if (remainingNotes.length === 1) { 7990 const singleNote = remainingNotes[0]; 7991 const chordDur = parent.getAttribute("dur"); 7992 const chordDots = parent.getAttribute("dots"); 7993 if (chordDur) singleNote.setAttribute("dur", chordDur); 7994 if (chordDots) singleNote.setAttribute("dots", chordDots); 7995 const chordParent = parent.parentNode; 7996 if (chordParent) { 7997 chordParent.insertBefore(singleNote, parent); 7998 parent.remove(); 7999 } 8000 } else if (remainingNotes.length === 0) { 8001 parent.remove(); 8002 } 8003 } 8004 } else if (noteEl) { 8005 noteEl.remove(); 8006 } 8007 }); 8008 if (elementId === selectedElementId) { 8009 setSelectedElementId(null); 8010 } 8011 }, [activeTrackIndex, engine, updateMei, deleteNote, selectedElementId]); 8012 const handleElementClick = reactExports.useCallback((elementId, e, yOffset, passedSnappedBeat) => { 8013 var _a2; 8014 if (!isReady || !containerRef.current) return; 8015 let gNode = containerRef.current.querySelector(`[id="${elementId}"]`); 8016 if (!gNode) return; 8017 let className = gNode.getAttribute("class") || ""; 8018 if (className.includes("bounding-box") || gNode.id.startsWith("bbox-")) { 8019 gNode = gNode.parentElement; 8020 if (!gNode) return; 8021 elementId = gNode.getAttribute("id") || elementId; 8022 className = gNode.getAttribute("class") || ""; 8023 } 8024 if (currentTool === "select") { 8025 if (className.includes("note") || className.includes("chord")) { 8026 const target = e.target; 8027 const closestNote = target.closest("g.note"); 8028 const noteId = (closestNote == null ? void 0 : closestNote.getAttribute("id")) || elementId; 8029 setSelectedElementId(noteId); 8030 if (activeTrackIndex !== null && meiDocs[activeTrackIndex] && ((_a2 = engine == null ? void 0 : engine.midi) == null ? void 0 : _a2.tracks[activeTrackIndex])) { 8031 const parser = new DOMParser(); 8032 const doc = parser.parseFromString(meiDocs[activeTrackIndex], "application/xml"); 8033 const noteEl = getXmlElById(doc, noteId); 8034 if (noteEl && noteEl.tagName === "note") { 8035 const pname = noteEl.getAttribute("pname") || "c"; 8036 const oct = noteEl.getAttribute("oct") || "4"; 8037 const originalAccid = noteEl.getAttribute("accid") || void 0; 8038 const oldMidi = getMidiFromMEI(pname, parseInt(oct), originalAccid); 8039 const layer = noteEl.closest("layer"); 8040 if (layer) { 8041 let eventEl = noteEl; 8042 if (noteEl.parentElement && noteEl.parentElement.tagName === "chord") { 8043 eventEl = noteEl.parentElement; 8044 } 8045 const elapsedBeats = getStartTimeInLayer(layer, eventEl); 8046 const measure = layer.closest("measure"); 8047 let measureNum = 1; 8048 if (measure) { 8049 const nAttr = measure.getAttribute("n"); 8050 if (nAttr) measureNum = parseInt(nAttr); 8051 } 8052 const actualStartSeconds = getActualNoteStartTime(noteEl, measureNum, elapsedBeats); 8053 const track = engine.midi.tracks[activeTrackIndex]; 8054 const noteIndex = track.notes.findIndex((n) => n.midi === oldMidi && Math.abs(n.time - actualStartSeconds) < 0.05); 8055 if (noteIndex !== -1) { 8056 playNotePreview(oldMidi); 8057 if (e && e.shiftKey) { 8058 setSelectedNoteIds(selectedNoteIds.includes(noteIndex) ? selectedNoteIds.filter((id) => id !== noteIndex) : [...selectedNoteIds, noteIndex]); 8059 } else { 8060 setSelectedNoteIds([noteIndex]); 8061 } 8062 } else { 8063 setSelectedNoteIds([]); 8064 } 8065 } 8066 } 8067 } 8068 } else { 8069 setSelectedElementId(null); 8070 setSelectedNoteIds([]); 8071 } 8072 return; 8073 } 8074 if (currentTool === "erase" && className.includes("note")) { 8075 handleDeleteElement(elementId); 8076 return; 8077 } 8078 if (currentTool.startsWith("note_")) { 8079 editingFromScoreRef.current = true; 8080 let mRestIdToRemove = null; 8081 let layerIdToInsert = null; 8082 let layerNode = null; 8083 if (className.includes("mRest")) { 8084 mRestIdToRemove = elementId; 8085 } else { 8086 const measureNode2 = gNode.closest("g.measure") || (className.includes("measure") ? gNode : null); 8087 if (measureNode2) { 8088 const mRestNode = measureNode2.querySelector(".mRest"); 8089 if (mRestNode) { 8090 mRestIdToRemove = mRestNode.getAttribute("id"); 8091 } 8092 } 8093 } 8094 if (className.includes("layer")) { 8095 layerNode = gNode; 8096 layerIdToInsert = elementId; 8097 } else { 8098 layerNode = gNode.closest("g.layer") || gNode.querySelector("g.layer") || gNode.closest(".layer") || gNode.querySelector(".layer"); 8099 if (layerNode) { 8100 layerIdToInsert = layerNode.getAttribute("id") || layerNode.getAttribute("xml:id"); 8101 } 8102 } 8103 if (layerNode) { 8104 const mRestNode = layerNode.querySelector(".mRest[id]"); 8105 if (mRestNode) { 8106 mRestIdToRemove = mRestNode.getAttribute("id"); 8107 } 8108 if (mRestIdToRemove) ; 8109 else {
8110 const noteRests = Array.from(layerNode.querySelectorAll(".note[id], .rest[id]")); 8111 if (noteRests.length > 0) { 8112 const sortedElements = noteRests.map((el) => ({ 8113 id: el.getAttribute("id"), 8114 rect: el.getBoundingClientRect() 8115 })).sort((a, b) => a.rect.left - b.rect.left); 8116 const clickX = e.clientX; 8117 const nextElement = sortedElements.find((item) => { 8118 const centerX = item.rect.left + item.rect.width / 2; 8119 return centerX > clickX; 8120 }); 8121 if (nextElement) { 8122 nextElement.id; 8123 } 8124 } 8125 } 8126 } 8127 const beatsPerMeasure = getBeatsPerMeasureFromMei(activeTrackIndex !== null ? meiDocs[activeTrackIndex] : void 0); 8128 const durMap = { 8129 "note_whole": "1", 8130 "note_half": "2", 8131 "note_quarter": "4", 8132 "note_eighth": "8", 8133 "note_sixteenth": "16" 8134 }; 8135 const dur = durMap[currentTool]; 8136 let beatDur = getDurationInBeats(dur); 8137 if (isTriplet) { 8138 beatDur /= 1.5; 8139 } 8140 if (isDotted) { 8141 beatDur *= 1.5; 8142 } 8143 let pitch = "c"; 8144 let oct = "4"; 8145 if (yOffset !== void 0) { 8146 const step = 38 - yOffset; 8147 const pitchMap = ["c", "d", "e", "f", "g", "a", "b"]; 8148 pitch = pitchMap[(step % 7 + 7) % 7]; 8149 oct = Math.floor(step / 7).toString(); 8150 } 8151 let accid = currentAccidental === "sharp" ? "s" : currentAccidental === "flat" ? "f" : currentAccidental === "natural" ? "n" : void 0; 8152 getMidiFromMEI(pitch, parseInt(oct), accid); 8153 let snappedBeat = passedSnappedBeat; 8154 const measureNode = gNode.closest("g.measure") || (className.includes("measure") ? gNode : null); 8155 if (snappedBeat === void 0 && measureNode) { 8156 const rect = measureNode.getBoundingClientRect(); 8157 let startX = rect.left + 50; 8158 const elements = Array.from(measureNode.querySelectorAll(".note, .rest, .mRest, .chord")); 8159 if (elements.length > 0) { 8160 let minLeft = Infinity; 8161 elements.forEach((el) => { 8162 const elRect = el.getBoundingClientRect(); 8163 if (elRect.left < minLeft && elRect.width > 0) minLeft = elRect.left + elRect.width / 2; 8164 }); 8165 if (minLeft !== Infinity) startX = minLeft; 8166 } 8167 const endX = rect.right - 10; 8168 const usableWidth = endX - startX; 8169 let beat = 0; 8170 if (usableWidth > 0 && e.clientX > startX) { 8171 beat = (e.clientX - startX) / usableWidth * beatsPerMeasure; 8172 } 8173 const gridStep = beatDur >= 1 ? 1 : beatDur; 8174 const roundedBeat = Math.round(beat / gridStep) * gridStep; 8175 snappedBeat = Math.max(0, Math.min(beatsPerMeasure - gridStep, roundedBeat)); 8176 } else if (snappedBeat === void 0) { 8177 snappedBeat = 0; 8178 } 8179 const finishNotePlacement = (midiPitch2) => { 8180 const isRest = entryMode === "rest"; 8181 if (!isRest) { 8182 playNotePreview(midiPitch2); 8183 } 8184 updateMei((doc) => { 8185 const ns = "http://www.music-encoding.org/ns/mei"; 8186 let targetLayer = null; 8187 const clickedEl = getXmlElById(doc, elementId); 8188 if (clickedEl) { 8189 targetLayer = clickedEl.tagName === "layer" ? clickedEl : clickedEl.closest("layer"); 8190 if (!targetLayer) { 8191 targetLayer = clickedEl.querySelector("layer"); 8192 } 8193 } 8194 if (!targetLayer) { 8195 targetLayer = getXmlElById(doc, layerIdToInsert) || doc.querySelector(`layer`); 8196 } 8197 if (targetLayer) { 8198 const measure = targetLayer.closest("measure"); 8199 let measureNum = 1; 8200 if (measure) { 8201 const nAttr = measure.getAttribute("n"); 8202 if (nAttr) measureNum = parseInt(nAttr); 8203 } 8204 let staff = targetLayer.closest("staff"); 8205 if (!staff && measure) { 8206 staff = measure.querySelector("staff"); 8207 } 8208 let targetLayerN = 1; 8209 if (staff) { 8210 if (currentVoice === "All" || currentVoice === 1) { 8211 let layer1 = staff.querySelector('layer[n="1"]'); 8212 if (layer1) { 8213 const overlaps = checkOverlapInLayer(layer1, snappedBeat, beatDur); 8214 if (overlaps) { 8215 targetLayerN = 2; 8216 } 8217 } 8218 } else { 8219 targetLayerN = currentVoice; 8220 } 8221 let layerNode2 = staff.querySelector(`layer[n="${targetLayerN}"]`); 8222 if (!layerNode2) { 8223 layerNode2 = doc.createElementNS(ns, "layer"); 8224 layerNode2.setAttribute("n", targetLayerN.toString()); 8225 layerNode2.setAttribute("xml:id", `l-${measureNum}-${targetLayerN}`); 8226 staff.appendChild(layerNode2); 8227 } 8228 targetLayer = layerNode2; 8229 } 8230 if (targetLayerN !== 1) { 8231 const targetMRest = targetLayer.querySelector("mRest"); 8232 if (targetMRest) targetMRest.remove(); 8233 } else { 8234 if (mRestIdToRemove) { 8235 const mRest = getXmlElById(doc, mRestIdToRemove); 8236 if (mRest) mRest.remove(); 8237 } 8238 const targetMRest = targetLayer.querySelector("mRest"); 8239 if (targetMRest) targetMRest.remove(); 8240 } 8241 const note = doc.createElementNS(ns, isRest ? "rest" : "note"); 8242 const newNoteId = (isRest ? "rest-" : "note-") + Math.random().toString(36).substr(2, 9); 8243 note.setAttribute("xml:id", newNoteId); 8244 if (!isRest) { 8245 if (!isPercussion) { 8246 note.setAttribute("pname", pitch); 8247 note.setAttribute("oct", oct); 8248 if (accid) note.setAttribute("accid", accid); 8249 } else { 8250 note.setAttribute("pname", "c"); 8251 note.setAttribute("oct", "5"); 8252 } 8253 if (currentArticulation) { 8254 if (currentArticulation === "staccato") note.setAttribute("artic", "stacc"); 8255 else if (currentArticulation === "accent") note.setAttribute("artic", "acc"); 8256 else if (currentArticulation === "tenuto") note.setAttribute("artic", "ten"); 8257 else if (currentArticulation === "fermata") note.setAttribute("fermata", "above"); 8258 } 8259 } 8260 if (isDotted) { 8261 note.setAttribute("dots", "1"); 8262 } 8263 if (isTriplet) { 8264 note.setAttribute("num", "3"); 8265 note.setAttribute("numbase", "2"); 8266 } 8267 if (snappedBeat + beatDur > beatsPerMeasure) { 8268 const dur1 = beatsPerMeasure - snappedBeat; 8269 const dur2 = beatDur - dur1; 8270 const { dur: dur1Attr, dots: dots1Attr } = getMeiDurAttr(dur1); 8271 note.setAttribute("dur", dur1Attr); 8272 if (dots1Attr) note.setAttribute("dots", dots1Attr); 8273 if (!isRest) { 8274 note.setAttribute("tie", "s"); 8275 } 8276 note.setAttribute("tmp-start-beat", snappedBeat.toString()); 8277 targetLayer.appendChild(note); 8278 let nextMeasure = measure == null ? void 0 : measure.nextElementSibling; 8279 let nextM = nextMeasure; 8280 while (nextM && nextM.tagName !== "measure") { 8281 nextM = nextM.nextElementSibling; 8282 } 8283 if (!nextM && measure && measure.parentNode) { 8284 const parentSection = measure.parentNode; 8285 const nextN = measureNum + 1; 8286 const newMeasure = doc.createElementNS(ns, "measure"); 8287 newMeasure.setAttribute("n", nextN.toString()); 8288 newMeasure.setAttribute("xml:id", `m-${nextN}`); 8289 const staff2 = doc.createElementNS(ns, "staff"); 8290 staff2.setAttribute("n", "1"); 8291 staff2.setAttribute("xml:id", `s-${nextN}`); 8292 const newLayer = doc.createElementNS(ns, "layer"); 8293 newLayer.setAttribute("n", "1"); 8294 newLayer.setAttribute("xml:id", `l-${nextN}`); 8295 const mRest = doc.createElementNS(ns, "mRest"); 8296 mRest.setAttribute("xml:id", `mr-${nextN}`); 8297 newLayer.appendChild(mRest); 8298 staff2.appendChild(newLayer); 8299 newMeasure.appendChild(staff2); 8300 parentSection.appendChild(newMeasure); 8301 nextM = newMeasure; 8302 } 8303 if (nextM) { 8304 const targetLayerN2 = targetLayer.getAttribute("n") || "1"; 8305 const nextStaff = nextM.querySelector("staff"); 8306 if (nextStaff) { 8307 let nextLayer = nextStaff.querySelector(`layer[n="${targetLayerN2}"]`); 8308 if (!nextLayer) { 8309 nextLayer = doc.createElementNS(ns, "layer"); 8310 nextLayer.setAttribute("n", targetLayerN2); 8311 const nextN = measureNum + 1; 8312 nextLayer.setAttribute("xml:id", `l-${nextN}-${targetLayerN2}`); 8313 nextStaff.appendChild(nextLayer); 8314 } 8315 const nextMRest = nextLayer.querySelector("mRest"); 8316 if (nextMRest) nextMRest.remove(); 8317 const tiedEl = doc.createElementNS(ns, isRest ? "rest" : "note"); 8318 const tiedElId = (isRest ? "rest-" : "note-") + Math.random().toString(36).substr(2, 9); 8319 tiedEl.setAttribute("xml:id", tiedElId); 8320 if (!isRest) { 8321 if (!isPercussion) { 8322 tiedEl.setAttribute("pname", pitch); 8323 tiedEl.setAttribute("oct", oct); 8324 if (accid) tiedEl.setAttribute("accid", accid); 8325 } else { 8326 tiedEl.setAttribute("pname", "c"); 8327 tiedEl.setAttribute("oct", "5"); 8328 } 8329 tiedEl.setAttribute("tie", "e"); 8330 } 8331 const { dur: dur2Attr, dots: dots2Attr } = getMeiDurAttr(dur2); 8332 tiedEl.setAttribute("dur", dur2Attr); 8333 if (dots2Attr) tiedEl.setAttribute("dots", dots2Attr); 8334 tiedEl.setAttribute("tmp-start-beat", "0"); 8335 if (nextLayer.firstChild) { 8336 nextLayer.insertBefore(tiedEl, nextLayer.firstChild); 8337 } else { 8338 nextLayer.appendChild(tiedEl); 8339 } 8340 if (!isRest) { 8341 const tieEl = doc.createElementNS(ns, "tie"); 8342 tieEl.setAttribute("startid", `#${newNoteId}`); 8343 tieEl.setAttribute("endid", `#${tiedElId}`); 8344 tieEl.setAttribute("xml:id", "tie-" + Math.random().toString(36).substr(2, 9)); 8345 if (measure) measure.appendChild(tieEl); 8346 } 8347 } 8348 } 8349 } else { 8350 note.setAttribute("dur", dur); 8351 note.setAttribute("tmp-start-beat", snappedBeat.toString()); 8352 if (isTieActive && !isRest) { 8353 const sisterNotes = Array.from(targetLayer.querySelectorAll("note")); 8354 if (sisterNotes.length > 0) { 8355 const prevNote = sisterNotes[sisterNotes.length - 1]; 8356 if (prevNote && prevNote.getAttribute("pname") === pitch && prevNote.getAttribute("oct") === oct) { 8357 prevNote.setAttribute("tie", "s"); 8358 note.setAttribute("tie", "e"); 8359 const tieEl = doc.createElementNS(ns, "tie"); 8360 tieEl.setAttribute("startid", `#${prevNote.getAttribute("xml:id")}`); 8361 tieEl.setAttribute("endid", `#${newNoteId}`); 8362 tieEl.setAttribute("xml:id", "tie-" + Math.random().toString(36).substr(2, 9)); 8363 const measure2 = targetLayer.closest("measure"); 8364 if (measure2) measure2.appendChild(tieEl); 8365 } 8366 } 8367 } 8368 targetLayer.appendChild(note); 8369 } 8370 if (!isRest && activeTrackIndex !== null && engine) { 8371 const bpm = tempo || 120; 8372 const totalStartBeat = (measureNum - 1) * beatsPerMeasure + snappedBeat; 8373 const totalStartSeconds = totalStartBeat / (bpm / 60); 8374 const durationSeconds = beatDur / (bpm / 60); 8375 addNote(activeTrackIndex, midiPitch2, engine.secondsToTicks(totalStartSeconds), engine.secondsToTicks(durationSeconds), 0.8, "score"); 8376 } 8377 } 8378 }); 8379 if (!isRest && engine) { 8380 engine.startNotePreview(midiPitch2); 8381 } 8382 }; 8383 if (isPercussion) { 8384 if (currentAccidental && ["kick", "snare", "hihat", "cymbal"].includes(currentAccidental)) { 8385 let drumPitch = 38; 8386 if (currentAccidental === "kick") drumPitch = 36; 8387 else if (currentAccidental === "snare") drumPitch = 38; 8388 else if (currentAccidental === "hihat") drumPitch = 42; 8389 else if (currentAccidental === "cymbal") drumPitch = 49; 8390 finishNotePlacement(drumPitch); 8391 return; 8392 } 8393 __vitePreload(async () => { 8394 const { REVERSE_DRUM_MAP, getPitchFromY } = await import("./ScoreEditorCanvas-Cg6gP8yn.js").then((n) => n.h); 8395 return { REVERSE_DRUM_MAP, getPitchFromY }; 8396 }, true ? __vite__mapDeps([0,1,2,3,4,5,6,7,8]) : void 0).then(({ REVERSE_DRUM_MAP, getPitchFromY }) => { 8397 const verticalPitchStr = getPitchFromY(meiDocs[activeTrackIndex], yOffset, snappedBeat); 8398 const pitch2 = verticalPitchStr && REVERSE_DRUM_MAP[verticalPitchStr.toLowerCase()] || 38; 8399 finishNotePlacement(pitch2); 8400 }); 8401 return; 8402 } 8403 if (yOffset !== void 0) { 8404 __vitePreload(async () => { 8405 const { getPitchFromY } = await import("./ScoreEditorCanvas-Cg6gP8yn.js").then((n) => n.h); 8406 return { getPitchFromY }; 8407 }, true ? __vite__mapDeps([0,1,2,3,4,5,6,7,8]) : void 0).then(({ getPitchFromY }) => { 8408 const verticalPitchStr = getPitchFromY(meiDocs[activeTrackIndex], yOffset, snappedBeat); 8409 if (verticalPitchStr) { 8410 const match = verticalPitchStr.match(/^([a-g])(-?\d+)$/i); 8411 if (match) { 8412 const pitch2 = match[1].toLowerCase(); 8413 const oct2 = parseInt(match[2]); 8414 const accid2 = currentAccidental === "sharp" ? "s" : currentAccidental === "flat" ? "f" : currentAccidental === "natural" ? "n" : void 0; 8415 finishNotePlacement(getMidiFromMEI(pitch2, oct2, accid2)); 8416 } else { 8417 finishNotePlacement(60); 8418 } 8419 } else { 8420 const step = 38 - yOffset; 8421 const pitchMap = ["c", "d", "e", "f", "g", "a", "b"]; 8422 const pitch2 = pitchMap[(step % 7 + 7) % 7]; 8423 const oct2 = Math.floor(step / 7).toString(); 8424 const accid2 = currentAccidental === "sharp" ? "s" : currentAccidental === "flat" ? "f" : currentAccidental === "natural" ? "n" : void 0; 8425 finishNotePlacement(getMidiFromMEI(pitch2, parseInt(oct2), accid2)); 8426 } 8427 }); 8428 } else { 8429 finishNotePlacement(60); 8430 } 8431 } 8432 }, [isReady, containerRef, currentTool, currentAccidental, activeTrackIndex, updateMei, addNote, handleDeleteElement, engine, currentVoice, isPercussion, meiDocs]); 8433 const handleElementRightClick = (elementId, className) => { 8434 if (!isReady) return; 8435 if (className.includes("note") || className.includes("rest")) { 8436 handleDeleteElement(elementId); 8437 } 8438 }; 8439 const handleElementDrag = reactExports.useCallback((elementId, dy, clientY) => { 8440 if (!isReady || activeTrackIndex === null || !containerRef.current || !meiDocs[activeTrackIndex]) return; 8441 const noteSVG = containerRef.current.querySelector(`[id="${elementId}"]`); 8442 if (!noteSVG) return; 8443 const staffSVG = noteSVG.closest("g.staff"); 8444 if (!staffSVG) return; 8445 const newOffset = getSnappingOffset(clientY, staffSVG); 8446 editingFromScoreRef.current = true; 8447 updateMei((doc) => { 8448 var _a2; 8449 const noteEl = getXmlElById
8449(doc, elementId); 8450 if (noteEl && noteEl.tagName === "note") { 8451 const pname = noteEl.getAttribute("pname") || "c"; 8452 const oct = noteEl.getAttribute("oct") || "4"; 8453 const originalAccid = noteEl.getAttribute("accid") || void 0; 8454 const oldMidi = getMidiFromMEI(pname, parseInt(oct), originalAccid); 8455 const layer = noteEl.closest("layer"); 8456 if (layer) { 8457 let eventEl = noteEl; 8458 if (noteEl.parentElement && noteEl.parentElement.tagName === "chord") { 8459 eventEl = noteEl.parentElement; 8460 } 8461 const elapsedBeats = getStartTimeInLayer(layer, eventEl); 8462 const measure = noteEl.closest("measure"); 8463 let measureNum = 1; 8464 if (measure) { 8465 const nAttr = measure.getAttribute("n"); 8466 if (nAttr) measureNum = parseInt(nAttr); 8467 } 8468 const actualStartSeconds = getActualNoteStartTime(noteEl, measureNum, elapsedBeats); 8469 const step = 38 - newOffset; 8470 const pitchMap = ["c", "d", "e", "f", "g", "a", "b"]; 8471 const newPitch = pitchMap[(step % 7 + 7) % 7]; 8472 const newOct = Math.floor(step / 7).toString(); 8473 const accid = noteEl.getAttribute("accid") || void 0; 8474 noteEl.setAttribute("pname", newPitch); 8475 noteEl.setAttribute("oct", newOct); 8476 const newMidi = getMidiFromMEI(newPitch, parseInt(newOct), accid); 8477 if (activeTrackIndex !== null && ((_a2 = engine == null ? void 0 : engine.midi) == null ? void 0 : _a2.tracks[activeTrackIndex])) { 8478 const track = engine.midi.tracks[activeTrackIndex]; 8479 const noteIndex = track.notes.findIndex((n) => n.midi === oldMidi && Math.abs(n.time - actualStartSeconds) < 0.05); 8480 if (noteIndex !== -1) { 8481 updateNote(activeTrackIndex, noteIndex, { midi: newMidi }); 8482 } 8483 } 8484 } 8485 } 8486 }); 8487 }, [isReady, activeTrackIndex, meiDocs, updateMei, updateNote, engine]); 8488 const handleTogglePlay = () => { 8489 if (isPlaying) { 8490 pause(); 8491 } else { 8492 play(); 8493 } 8494 }; 8495 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 flex flex-col overflow-hidden relative bg-[#8e8e93] dark:bg-[#202020]", children: [ 8496 /* @__PURE__ */ jsxRuntimeExports.jsx( 8497 ScoreRibbon, 8498 { 8499 currentTool, 8500 onSelectTool: handleSelectTool, 8501 isPlaying, 8502 onTogglePlay: handleTogglePlay, 8503 onStop: stop, 8504 onZoomIn: () => setZoom((z) => Math.min(3, z + 0.1)), 8505 onZoomOut: () => setZoom((z) => Math.max(0.1, z - 0.1)), 8506 showMeasureNumbers, 8507 onToggleMeasureNumbers: () => setShowMeasureNumbers(!showMeasureNumbers), 8508 currentAccidental, 8509 onSelectAccidental: handleSelectAccidental, 8510 isPercussion, 8511 showKeypad, 8512 onToggleKeypad: setShowKeypad, 8513 showTextEditor, 8514 onToggleTextEditor: () => setShowTextEditor(!showTextEditor), 8515 entryMode, 8516 onSelectEntryMode: setEntryMode, 8517 isDotted, 8518 onToggleDotted: () => setIsDotted(!isDotted), 8519 isTieActive, 8520 onToggleTieActive: () => setIsTieActive(!isTieActive), 8521 isTriplet, 8522 onToggleTriplet: () => setIsTriplet(!isTriplet), 8523 currentArticulation, 8524 onSelectArticulation: handleSelectArticulation, 8525 showGrid, 8526 onToggleGrid: () => setShowGrid(!showGrid), 8527 onStartTour: () => setShowLiveTour(true) 8528 } 8529 ), 8530 /* @__PURE__ */ jsxRuntimeExports.jsx(ScoreDocumentTabs, {}), 8531 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 flex overflow-hidden relative", ref: containerRef, children: [ 8532 showTextEditor && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-[35%] border-r border-zinc-300 dark:border-zinc-800 flex flex-col bg-white dark:bg-[#1a1a1a]", children: [
8533 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "h-8 bg-zinc-100 dark:bg-zinc-800 px-3 flex items-center justify-between border-b border-zinc-200 dark:border-zinc-700 shrink-0", children: [ 8534 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] font-semibold text-zinc-600 dark:text-zinc-400 font-mono tracking-wider", children: "HUMDRUM CODE EDITOR" }), 8535 /* @__PURE__ */ jsxRuntimeExports.jsx( 8536 "button", 8537 { 8538 onClick: () => setShowTextEditor(false), 8539 className: "text-zinc-500 hover:text-zinc-800 dark:hover:text-zinc-200 text-[10px] font-semibold uppercase tracking-wider transition-colors", 8540 children: "Close" 8541 } 8542 ) 8543 ] }), 8544 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { ref: aceContainerRef, className: "humdrum-ace-editor flex-1 font-mono text-sm leading-relaxed", style: { width: "100%", height: "100%" } }) 8545 ] }), 8546 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { id: "score-canvas-container", className: "flex-1 overflow-auto bg-[#8e8e93] dark:bg-[#323232] relative p-8", children: [ 8547 syntaxError && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute top-4 right-4 z-10 flex items-center gap-1.5 bg-amber-500/15 border border-amber-500/30 px-3 py-1.5 rounded-lg text-amber-500 text-[11px] font-semibold backdrop-blur shadow-lg animate-fade-in", children: [
8548 /* @__PURE__ */ jsxRuntimeExports.jsx(TriangleAlert, { size: 14, className: "animate-pulse" }), 8549 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Invalid Notation Code (Showing last valid state)" }) 8550 ] }), 8551 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { style: { zoom, margin: "0 auto", width: "fit-content" }, children: /* @__PURE__ */ jsxRuntimeExports.jsx( 8552 ScoreErrorBoundary, 8553 { 8554 svg, 8555 fallback: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center justify-center p-8 bg-zinc-800 border border-zinc-700 rounded-lg text-zinc-300 gap-4 shadow-xl max-w-md mx-auto my-12", children: [ 8556 /* @__PURE__ */ jsxRuntimeExports.jsx(TriangleAlert, { size: 48, className: "text-amber-500 animate-pulse" }), 8557 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-center", children: [ 8558 /* @__PURE__ */ jsxRuntimeExports.jsx("h4", { className: "font-bold text-lg text-white", children: "Rendering Error" }),
8559 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-zinc-400 mt-1", children: "The current notation could not be drawn. Showing the last successful render." }) 8560 ] }) 8561 ] }), 8562 children: /* @__PURE__ */ jsxRuntimeExports.jsx( 8563 ScoreEditorCanvas, 8564 { 8565 svg, 8566 currentTool, 8567 zoom, 8568 mei: activeTrackIndex !== null ? meiDocs[activeTrackIndex] : void 0, 8569 isPlaying, 8570 engine, 8571 currentVoice, 8572 onElementClick: handleElementClick, 8573 onElementRightClick: handleElementRightClick, 8574 onElementDrag: handleElementDrag, 8575 onMouseUp: () => { 8576 if (engine) { 8577 engine.stopAllNotePreviews(); 8578 } 8579 }, 8580 currentAccidental, 8581 isPercussion, 8582 entryMode, 8583 isDotted, 8584 isTriplet, 8585 currentArticulation, 8586 selectedNoteId: selectedElementId, 8587 showGrid 8588 } 8589 ) 8590 } 8591 ) }) 8592 ] }), 8593 showKeypad && /* @__PURE__ */ jsxRuntimeExports.jsx( 8594 ScoreKeypad, 8595 { 8596 currentTool, 8597 onSelectTool: setCurrentTool, 8598 currentAccidental, 8599 onSelectAccidental: handleSelectAccidental, 8600 currentVoice, 8601 onSelectVoice: setCurrentVoice, 8602 isPercussion, 8603 onClose: () => setShowKeypad(false), 8604 currentArticulation, 8605 onSelectArticulation: handleSelectArticulation, 8606 isDotted, 8607 onToggleDotted: () => setIsDotted(!isDotted), 8608 isTieActive, 8609 onToggleTieActive: () => setIsTieActive(!isTieActive), 8610 entryMode, 8611 onSelectEntryMode: setEntryMode 8612 } 8613 ) 8614 ] }), 8615 /* @__PURE__ */ jsxRuntimeExports.jsx( 8616 ScoreStatusBar, 8617 { 8618 zoom, 8619 onZoomChange: setZoom, 8620 layout, 8621 onLayoutChange: setLayout, 8622 syntaxError 8623 } 8624 ), 8625 showLiveTour && /* @__PURE__ */ jsxRuntimeExports.jsx( 8626 LiveFeedbackTour, 8627 { 8628 onComplete: () => setShowLiveTour(false), 8629 humdrumText, 8630 showTextEditor, 8631 setShowTextEditor, 8632 showKeypad, 8633 setShowKeypad 8634 } 8635 ) 8636 ] }); 8637}; 8638const DEFAULT_LAYOUT = { 8639 inspector: { state: "docked" }, 8640 arrangement: { state: "docked" }, 8641 pianoRoll: { state: "docked" }, 8642 scoreEditor: { state: "docked" }, 8643 mixer: { state: "docked" } 8644}; 8645const STORAGE_KEY = "hamienet-editor-workspace-layout"; 8646function useWorkspaceLayout() { 8647 const [layout, setLayout] = reactExports.useState(() => { 8648 try { 8649 const saved = localStorage.getItem(STORAGE_KEY); 8650 if (saved) { 8651 const parsed = JSON.parse(saved); 8652 if (parsed && typeof parsed === "object" && "inspector" in parsed) { 8653 return parsed; 8654 } 8655 } 8656 } catch (e) { 8657 console.error("Failed to load workspace layout", e); 8658 } 8659 return DEFAULT_LAYOUT; 8660 }); 8661 const saveTimeoutRef = reactExports.useRef(null); 8662 const updatePanelLayout = reactExports.useCallback((panel, updates) => { 8663 setLayout((prev) => { 8664 const newLayout = { 8665 ...prev, 8666 [panel]: { 8667 ...prev[panel], 8668 ...updates 8669 } 8670 }; 8671 if (saveTimeoutRef.current) { 8672 clearTimeout(saveTimeoutRef.current); 8673 } 8674 saveTimeoutRef.current = setTimeout(() => { 8675 try { 8676 localStorage.setItem(STORAGE_KEY, JSON.stringify(newLayout)); 8677 } catch (e) { 8678 console.error("Failed to save workspace layout", e); 8679 } 8680 }, 500); 8681 return newLayout; 8682 }); 8683 }, []); 8684 const resetLayout = reactExports.useCallback(() => { 8685 setLayout(DEFAULT_LAYOUT); 8686 try { 8687 localStorage.removeItem(STORAGE_KEY); 8688 const keysToRemove = []; 8689 for (let i = 0; i < localStorage.length; i++) { 8690 const key = localStorage.key(i); 8691 if (key == null ? void 0 : key.startsWith("editor-popout-")) { 8692 keysToRemove.push(key); 8693 } 8694 } 8695 keysToRemove.forEach((k) => localStorage.removeItem(k)); 8696 } catch (e) { 8697 console.error("Failed to reset workspace layout", e); 8698 } 8699 }, []); 8700 const setPanelState = reactExports.useCallback((panel, state) => { 8701 updatePanelLayout(panel, { state }); 8702 }, [updatePanelLayout]); 8703 return { 8704 layout, 8705 updatePanelLayout, 8706 setPanelState, 8707 resetLayout 8708 }; 8709} 8710function copyStyles(sourceDoc, targetDoc) {
8711 Array.from(sourceDoc.styleSheets).forEach((styleSheet) => { 8712 try { 8713 if (styleSheet.href) { 8714 const newLinkEl = sourceDoc.createElement("link"); 8715 newLinkEl.rel = "stylesheet"; 8716 newLinkEl.href = styleSheet.href; 8717 targetDoc.head.appendChild(newLinkEl); 8718 } else if (styleSheet.ownerNode) { 8719 const newStyleEl = sourceDoc.createElement("style"); 8720 newStyleEl.textContent = styleSheet.ownerNode.textContent; 8721 targetDoc.head.appendChild(newStyleEl); 8722 } 8723 } catch (e) { 8724 console.warn("Could not copy stylesheet", e); 8725 } 8726 }); 8727 const tailwindStyles = sourceDoc.querySelectorAll("style[data-vite-dev-id]"); 8728 tailwindStyles.forEach((style) => { 8729 const newStyleEl = sourceDoc.createElement("style"); 8730 newStyleEl.textContent = style.textContent; 8731 targetDoc.head.appendChild(newStyleEl); 8732 }); 8733} 8734function EditorWindowPortal({ 8735 children, 8736 onClose, 8737 title = "External Window", 8738 panelId, 8739 defaultWidth = 800, 8740 defaultHeight = 600 8741}) { 8742 const [container, setContainer] = reactExports.useState(null); 8743 const externalWindow = reactExports.useRef(null); 8744 const geometrySaveTimer = reactExports.useRef(null); 8745 const { undo, redo, copy, paste, engine } = useEditor(); 8746 const onCloseRef = reactExports.useRef(onClose); 8747 reactExports.useEffect(() => { 8748 onCloseRef.current = onClose; 8749 }, [onClose]); 8750 reactExports.useEffect(() => { 8751 let w = defaultWidth; 8752 let h = defaultHeight; 8753 let x = 200; 8754 let y = 200; 8755 const storageKey = `editor-popout-${panelId}`; 8756 try { 8757 const savedGeo = localStorage.getItem(storageKey); 8758 if (savedGeo) { 8759 const parsed = JSON.parse(savedGeo); 8760 if (parsed && typeof parsed.w === "number") { 8761 w = parsed.w; 8762 h = parsed.h; 8763 x = parsed.x; 8764 y = parsed.y; 8765 } 8766 } 8767 } catch (e) { 8768 console.error("Failed to parse saved geometry", e); 8769 } 8770 const features = `popup=yes,width=${w},height=${h},left=${x},top=${y}`; 8771 externalWindow.current = window.open("", panelId, features); 8772 if (!externalWindow.current) { 8773 console.error("Failed to open external window. Popup blocker?"); 8774 alert(`Popup blocker prevented opening the ${title} panel. Please allow popups for this site.`); 8775 if (onCloseRef.current) onCloseRef.current(); 8776 return; 8777 } 8778 const doc = externalWindow.current.document; 8779 doc.title = title; 8780 const el = doc.createElement("div"); 8781 el.style.height = "100vh"; 8782 el.style.width = "100vw"; 8783 el.style.display = "flex"; 8784 el.style.flexDirection = "column"; 8785 doc.body.appendChild(el); 8786 doc.body.style.margin = "0"; 8787 doc.body.style.padding = "0"; 8788 doc.body.style.overflow = "hidden"; 8789 copyStyles(document, doc); 8790 const syncDarkMode = () => { 8791 if (document.documentElement.classList.contains("dark")) { 8792 doc.documentElement.classList.add("dark"); 8793 doc.body.style.backgroundColor = "#18181b"; 8794 } else { 8795 doc.documentElement.classList.remove("dark"); 8796 doc.body.style.backgroundColor = "#ffffff"; 8797 } 8798 }; 8799 syncDarkMode(); 8800 const observer = new MutationObserver((mutations) => { 8801 mutations.forEach((mutation) => { 8802 if (mutation.attributeName === "class") { 8803 syncDarkMode(); 8804 } 8805 }); 8806 }); 8807 observer.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] }); 8808 setContainer(el); 8809 geometrySaveTimer.current = setInterval(() => { 8810 if (externalWindow.current) { 8811 if (externalWindow.current.closed) { 8812 if (onCloseRef.current) onCloseRef.current(); 8813 } else { 8814 try { 8815 const geo = { 8816 w: externalWindow.current.outerWidth, 8817 h: externalWindow.current.outerHeight, 8818 x: externalWindow.current.screenX, 8819 y: externalWindow.current.screenY 8820 }; 8821 localStorage.setItem(storageKey, JSON.stringify(geo)); 8822 } catch (e) { 8823 } 8824 } 8825 } 8826 }, 1e3); 8827 const handleKeyDown = (e) => { 8828 const activeEl = doc.activeElement; 8829 if (activeEl && (activeEl.tagName === "INPUT" || activeEl.tagName === "TEXTAREA" || activeEl.getAttribute("contenteditable") === "true")) { 8830 return; 8831 } 8832 const isCmdOrCtrl = e.ctrlKey || e.metaKey; 8833 const isShift = e.shiftKey; 8834 if (isCmdOrCtrl && !isShift && e.key.toLowerCase() === "z") { 8835 e.preventDefault(); 8836 undo(); 8837 } else if (isCmdOrCtrl && isShift && e.key.toLowerCase() === "z" || isCmdOrCtrl && e.key.toLowerCase() === "y") { 8838 e.preventDefault(); 8839 redo(); 8840 } else if (isCmdOrCtrl && e.key.toLowerCase() === "c") { 8841 copy(); 8842 } else if (isCmdOrCtrl && e.key.toLowerCase() === "v") { 8843 paste(); 8844 } else if (e.code === "Space") { 8845 e.preventDefault(); 8846 if (engine && engine.isReady) { 8847 engine.togglePlayback(); 8848 } 8849 } else if (isCmdOrCtrl && isShift) { 8850 const event = new window.KeyboardEvent("keydown", { 8851 key: e.key, 8852 code: e.code, 8853 ctrlKey: e.ctrlKey, 8854 metaKey: e.metaKey,
8855 shiftKey: e.shiftKey, 8856 altKey: e.altKey, 8857 bubbles: true 8858 }); 8859 window.dispatchEvent(event); 8860 } 8861 }; 8862 doc.addEventListener("keydown", handleKeyDown); 8863 return () => { 8864 observer.disconnect(); 8865 if (geometrySaveTimer.current) clearInterval(geometrySaveTimer.current); 8866 if (externalWindow.current && !externalWindow.current.closed) { 8867 externalWindow.current.close(); 8868 } 8869 }; 8870 }, [title, panelId, defaultWidth, defaultHeight]); 8871 if (!container) return null; 8872 return reactDomExports.createPortal(children, container); 8873} 8874var clipClipboard = []; 8875const useGlobalShortcuts = () => { 8876 const { 8877 undo, 8878 redo, 8879 copy, 8880 paste, 8881 canUndo, 8882 canRedo, 8883 deleteSelection, 8884 selectedClipIds, 8885 setSelectedClipIds, 8886 setSelectedNoteIds, 8887 playlistClips, 8888 setPlaylistClips, 8889 activeTrackIndex, 8890 playlistTracks, 8891 gridSnap, 8892 isPlaying, 8893 play, 8894 pause, 8895 stop, 8896 currentTime, 8897 easyMode, 8898 engine, 8899 forceSync 8900 } = useEditor(); 8901 reactExports.useEffect(() => { 8902 const getSnappedTick = (rawTick2) => { 8903 var _a, _b; 8904 const ppq = ((_b = (_a = engine == null ? void 0 : engine.midi) == null ? void 0 : _a.header) == null ? void 0 : _b.ppq) || 96; 8905 let interval = ppq; 8906 switch (gridSnap) { 8907 case "bar": 8908 interval = ppq * 4; 8909 break; 8910 case "beat": 8911 interval = ppq; 8912 break; 8913 case "1/2": 8914 interval = ppq / 2; 8915 break; 8916 case "1/4": 8917 interval = ppq / 4; 8918 break; 8919 case "1/6": 8920 interval = ppq / 6; 8921 break; 8922 case "cell": 8923 interval = ppq / 4; 8924 break; 8925 case "none": 8926 return Math.max(0, Math.round(rawTick2)); 8927 default: 8928 interval = ppq; 8929 } 8930 return Math.max(0, Math.round(rawTick2 / interval) * interval); 8931 }; 8932 const handleKeyDown = (e) => { 8933 var _a, _b; 8934 const target = e.target; 8935 if (target.tagName === "TEXTAREA" || target.isContentEditable) { 8936 return; 8937 } 8938 if (target.tagName === "INPUT") { 8939 const type = target.type; 8940 if (type !== "range" && type !== "checkbox" && type !== "radio" && type !== "button" && type !== "submit") return; 8941 } 8942 const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0; 8943 const cmdOrCtrl = isMac ? e.metaKey : e.ctrlKey; 8944 if (e.key === "Escape") { 8945 e.preventDefault(); 8946 setSelectedClipIds([]); 8947 setSelectedNoteIds([]); 8948 return; 8949 } 8950 if (cmdOrCtrl && (e.key === "a" || e.key === "A")) { 8951 e.preventDefault(); 8952 setSelectedClipIds(playlistClips.map((c) => c.id)); 8953 return; 8954 } 8955 if (e.key === "Delete" || e.key === "Backspace") { 8956 e.preventDefault(); 8957 if (selectedClipIds.length > 0) { 8958 setPlaylistClips((prev) => prev.filter((c) => !selectedClipIds.includes(c.id))); 8959 setSelectedClipIds([]); 8960 setTimeout(() => forceSync(), 0); 8961 } else { 8962 deleteSelection(); 8963 } 8964 return; 8965 } 8966 if (e.key === " " && !cmdOrCtrl) { 8967 e.preventDefault(); 8968 if (isPlaying) { 8969 pause(); 8970 } else { 8971 play(); 8972 } 8973 return; 8974 } 8975 if (cmdOrCtrl && e.key === " " || e.code === "Numpad0") { 8976 e.preventDefault(); 8977 stop(); 8978 return; 8979 } 8980 if (easyMode) { 8981 if (cmdOrCtrl) { 8982 if (e.key === "z" || e.key === "Z") { 8983 if (e.shiftKey) { 8984 e.preventDefault(); 8985 if (canRedo) redo(); 8986 } else { 8987 e.preventDefault(); 8988 if (canUndo) undo(); 8989 } 8990 } else if (e.key === "y" || e.key === "Y") { 8991 e.preventDefault(); 8992 if (canRedo) redo(); 8993 } 8994 } 8995 return; 8996 } 8997 if (cmdOrCtrl) { 8998 if (e.key === "c" || e.key === "C") { 8999 e.preventDefault(); 9000 if (selectedClipIds.length > 0) { 9001 const selectedClips = playlistClips.filter((c) => selectedClipIds.includes(c.id)); 9002 clipClipboard = selectedClips.map((c) => ({ ...c })); 9003 } else { 9004 copy(); 9005 } 9006 return; 9007 } 9008 if (e.key === "x" || e.key === "X") { 9009 e.preventDefault(); 9010 if (selectedClipIds.length > 0) { 9011 const selectedClips = playlistClips.filter((c) => selectedClipIds.includes(c.id)); 9012 clipClipboard = selectedClips.map((c) => ({ ...c })); 9013 setPlaylistClips((prev) => prev.filter((c) => !selectedClipIds.includes(c.id))); 9014 setSelectedClipIds([]); 9015 setTimeout(() => forceSync(), 0); 9016 } else { 9017 copy(); 9018 deleteSelection(); 9019 } 9020 return; 9021 } 9022 if (e.key === "v" || e.key === "V") { 9023 e.preventDefault(); 9024 if (clipClipboard.length > 0) { 9025 const minStartTick = Math.min(...clipClipboard.map((c) => c.startTick)); 9026 const minTrackIndex = Math.min(...clipClipboard.map((c) => c.trackIndex)); 9027 const playheadTick = engine ? engine.secondsToTicks(currentTime) : 0; 9028 const snappedPlayheadTick = getSnappedTick(playheadTick); 9029 const tickOffset = snappedPlayheadTick - minStartTick; 9030 const trackOffset = (activeTrackIndex !== null ? activeTrackIndex : 0) - minTrackIndex; 9031 const pastedClips = clipClipboard.map((clip) => { 9032 const newStart = Math.max(0, clip.startTick + tickOffset); 9033 const newTrack = Math.max(0, Math.min(playlistTracks.length - 1, clip.trackIndex + trackOffset)); 9034 return { 9035 ...clip, 9036 id: `clip-${Date.now()}-${Math.random()}`, 9037 startTick: newStart, 9038 trackIndex: newTrack 9039 }; 9040 }); 9041 setPlaylistClips((prev) => [...prev, ...pastedClips]); 9042 setSelectedClipIds(pastedClips.map((c) => c.id)); 9043 setTimeout(() => forceSync(), 0); 9044 } else { 9045 paste(); 9046 } 9047 return; 9048 } 9049 if (e.key === "b" || e.key === "B") { 9050 e.preventDefault(); 9051 if (selectedClipIds.length > 0) { 9052 const selectedClips = playlistClips.filter((clip) => selectedClipIds.includes(clip.id)); 9053 const minStart = Math.min(...selectedClips.map((c) => c.startTick)); 9054 const maxEnd = Math.max(...selectedClips.map((c) => c.startTick + c.durationTicks)); 9055 const span = maxEnd - minStart; 9056 const duplicatedClips = selectedClips.map((clip) => ({ 9057 ...clip, 9058 id: `clip-${Date.now()}-${Math.random()}`, 9059 startTick: clip.startTick + span 9060 })); 9061 setPlaylistClips((prev) => [...prev, ...duplicatedClips]); 9062 setSelectedClipIds(duplicatedClips.map((c) => c.id)); 9063 setTimeout(() => forceSync(), 0); 9064 } 9065 return; 9066 } 9067 if (e.key === "ArrowLeft" || e.key === "ArrowRight") { 9068 e.preventDefault(); 9069 if (selectedClipIds.length > 0) { 9070 const selectedClips = playlistClips.filter((clip) => selectedClipIds.includes(clip.id)); 9071 const ppq = ((_b = (_a = engine == null ? void 0 : engine.midi) == null ? void 0 : _a.header) == null ? void 0 : _b.ppq) || 96; 9072 let snapInterval = ppq; 9073 switch (gridSnap) { 9074 case "bar": 9075 snapInterval = ppq * 4; 9076 break; 9077 case "beat": 9078 snapInterval = ppq; 9079 break; 9080 case "1/2": 9081 snapInterval = ppq / 2; 9082 break; 9083 case "1/4": 9084 snapInterval = ppq / 4; 9085 break; 9086 case "1/6": 9087 snapInterval = ppq / 6; 9088 break; 9089 case "cell": 9090 snapInterval = ppq / 4; 9091 break; 9092 case "none": 9093 snapInterval = 24; 9094 break; 9095 } 9096 const direction = e.key === "ArrowLeft" ? -1 : 1; 9097 const shift = direction * snapInterval; 9098 const canMove = selectedClips.every((clip) => clip.startTick + shift >= 0); 9099 if (canMove) { 9100 setPlaylistClips((prev) => prev.map((clip) => { 9101 if (selectedClipIds.includes(clip.id)) { 9102 return { 9103 ...clip, 9104 startTick: clip.startTick + shift 9105 }; 9106 } 9107 return clip; 9108 })); 9109 setTimeout(() => forceSync(), 0); 9110 } 9111 } 9112 return; 9113 } 9114 if (e.key === "z" || e.key === "Z") { 9115 if (e.shiftKey) { 9116 e.preventDefault(); 9117 if (canRedo) redo(); 9118 } else { 9119 e.preventDefault(); 9120 if (canUndo) undo(); 9121 } 9122 return; 9123 } 9124 if (e.key === "y" || e.key === "Y") { 9125 e.preventDefault(); 9126 if (canRedo) redo(); 9127 return; 9128 } 9129 } 9130 if (e.shiftKey && (e.key === "ArrowUp" || e.key === "ArrowDown")) { 9131 e.preventDefault(); 9132 if (selectedClipIds.length > 0) { 9133 const direction = e.key === "ArrowUp" ? -1 : 1; 9134 setPlaylistClips((prev) => prev.map((clip) => { 9135 if (selectedClipIds.includes(clip.id)) { 9136 const nextTrack = Math.max(0, Math.min(playlistTracks.length - 1, clip.trackIndex + direction)); 9137 return { 9138 ...clip, 9139 trackIndex: nextTrack 9140 }; 9141 } 9142 return clip; 9143 })); 9144 setTimeout(() => forceSync(), 0); 9145 } 9146 return; 9147 } 9148 }; 9149 window.addEventListener("keydown", handleKeyDown); 9150 return () => window.removeEventListener("keydown", handleKeyDown); 9151 }, [ 9152 undo, 9153 redo, 9154 copy, 9155 paste, 9156 canUndo, 9157 canRedo, 9158 deleteSelection, 9159 selectedClipIds, 9160 setSelectedClipIds, 9161 setSelectedNoteIds, 9162 playlistClips, 9163 setPlaylistClips, 9164 activeTrackIndex, 9165 playlistTracks, 9166 gridSnap, 9167 isPlaying, 9168 play, 9169 pause, 9170 stop, 9171 currentTime, 9172 easyMode, 9173 engine, 9174 forceSync 9175 ]); 9176}; 9177const FlHintPanel = () => { 9178 var _a, _b; 9179 const { hint, currentTime, engine } = useEditor(); 9180 const ppq = ((_b = (_a = engine == null ? void 0 : engine.midi) == null ? void 0 : _a.header) == null ? void 0 : _b.ppq) || 96; 9181 const formatTicksToFlTime2 = (ticks, ppqVal = 96) => { 9182 const beatsPerBar = 4; 9183 const ticksPerBeat = ppqVal; 9184 const ticksPerStep = ppqVal / 4; 9185 const ticksPerBar = beatsPerBar * ticksPerBeat; 9186 const bar = Math.floor(ticks / ticksPerBar) + 1; 9187 const remainingTicksInBar = ticks % ticksPerBar; 9188 const step = Math.floor(remainingTicksInBar / ticksPerStep) + 1; 9189 const tick = Math.floor(remainingTicksInBar % ticksPerStep); 9190 const stepStr = String(step).padStart(2, "0"); 9191 const tickStr = String(tick).padStart(2, "0"); 9192 return `${bar}:${stepStr}:${tickStr}`; 9193 }; 9194 const defaultPositionText = React.useMemo(() => { 9195 if (!engine) return "1:01:00"; 9196 const currentTicks = engine.secondsToTicks(currentTime); 9197 return formatTicksToFlTime2(currentTicks, ppq); 9198 }, [currentTime, engine, ppq]); 9199 const activeLabel = (hint == null ? void 0 : hint.label) || "(HamieNET)"; 9200 const activePosition = (hint == null ? void 0 : hint.positionText) !== void 0 ? hint.positionText : defaultPositionText; 9201 const activeNote = (hint == null ? void 0 : hint.noteText) || ""; 9202 const activeIcon = (hint == null ? void 0 : hint.iconType) || "info"; 9203 const renderIcon = (type) => { 9204 const size = 14; 9205 const className = "text-indigo-500 dark:text-indigo-400 animate-pulse"; 9206 switch (type.toLowerCase()) { 9207 case "draw": 9208 return /* @__PURE__ */ jsxRuntimeExports.jsx(Pencil, { size, className }); 9209 case "paint": 9210 return /* @__PURE__ */ jsxRuntimeExports.jsx(Paintbrush, { size, className }); 9211 case "slice": 9212 case "cut": 9213 return /* @__PURE__ */ jsxRuntimeExports.jsx(Scissors, { size, className }); 9214 case "select": 9215 return /* @__PURE__ */ jsxRuntimeExports.jsx(MousePointer2, { size, className }); 9216 case "erase": 9217 case "delete": 9218 return /* @__PURE__ */ jsxRuntimeExports.jsx(Trash2, { size, className }); 9219 case "keyboard": 9220 return /* @__PURE__ */ jsxRuntimeExports.jsx(Keyboard, { size, className }); 9221 case "note": 9222 case "note_whole": 9223 case "note_half": 9224 case "note_quarter": 9225 case "note_eighth": 9226 case "note_sixteenth": 9227 return /* @__PURE__ */ jsxRuntimeExports.jsx(Music, { size, className }); 9228 default: 9229 return /* @__PURE__ */ jsxRuntimeExports.jsx(Info, { size, className: "text-zinc-400 dark:text-zinc-500" }); 9230 } 9231 }; 9232 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 9233 "div", 9234 { 9235 className: "flex items-center justify-between px-3 py-1.5 h-10 w-72 bg-zinc-100 dark:bg-zinc-950 border border-zinc-200 dark:border-zinc-800 rounded-lg shadow-sm select-none shrink-0", 9236 title: "HamieNET Hint Panel (Shows hover details or playhead position)", 9237 children: [
9238 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col justify-center flex-1 min-w-0", children: [ 9239 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[9px] font-bold text-indigo-600 dark:text-indigo-400 uppercase tracking-widest truncate leading-tight", children: activeLabel }), 9240 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-baseline gap-2 mt-0.5 min-w-0", children: [ 9241 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs font-mono text-zinc-800 dark:text-zinc-200 font-bold shrink-0 leading-none", children: activePosition }), 9242 activeNote && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs font-mono text-zinc-500 dark:text-zinc-400 font-medium truncate leading-none", children: activeNote }) 9243 ] }) 9244 ] }), 9245 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center justify-center shrink-0 ml-2 bg-zinc-200/50 dark:bg-zinc-900/50 p-1 rounded-md w-6 h-6", children: renderIcon(activeIcon) }) 9246 ] 9247 } 9248 ); 9249}; 9250const EasyToProMergeModal = ({ mergeState, onClose }) => { 9251 const [resolutions, setResolutions] = reactExports.useState(() => { 9252 const initial = {}; 9253 mergeState.tracks.forEach((t) => { 9254 if (t.availablePatterns.length > 0) { 9255 initial[t.trackIndex] = { action: "merge", targetPatternId: t.availablePatterns[0].id }; 9256 } else { 9257 initial[t.trackIndex] = { action: "new-track" }; 9258 } 9259 }); 9260 return initial; 9261 }); 9262 const setResolution = (trackIndex, res) => { 9263 setResolutions((prev) => ({ ...prev, [trackIndex]: res })); 9264 }; 9265 const handleConfirm = () => { 9266 mergeState.resolve(resolutions); 9267 }; 9268 return /* @__PURE__ */ jsxRuntimeExports.jsx( 9269 "div", 9270 { 9271 className: "fixed inset-0 z-50 flex items-center justify-center", 9272 style: { background: "rgba(0,0,0,0.6)", backdropFilter: "blur(4px)" }, 9273 children: /* @__PURE__ */ jsxRuntimeExports.jsxs( 9274 "div", 9275 { 9276 className: "relative bg-zinc-900 border border-zinc-700/60 rounded-xl shadow-2xl max-w-lg w-full mx-4 overflow-hidden", 9277 style: { boxShadow: "0 25px 60px rgba(0,0,0,0.5), 0 0 0 1px rgba(255,255,255,0.05)" }, 9278 children: [
9279 /* @__PURE__ */ jsxRuntimeExports.jsxs( 9280 "div", 9281 { 9282 className: "px-6 pt-6 pb-4 border-b border-zinc-700/50", 9283 style: { background: "linear-gradient(135deg, rgba(99,102,241,0.08) 0%, rgba(168,85,247,0.05) 100%)" }, 9284 children: [ 9285 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3 mb-1", children: [ 9286 /* @__PURE__ */ jsxRuntimeExports.jsx( 9287 "div", 9288 { 9289 className: "w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0", 9290 style: { background: "linear-gradient(135deg, #6366f1, #a855f7)" }, 9291 children: /* @__PURE__ */ jsxRuntimeExports.jsx("svg", { width: "16", height: "16", fill: "none", viewBox: "0 0 24 24", stroke: "white", strokeWidth: 2, children: /* @__PURE__ */ jsxRuntimeExports.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M8 9l4-4 4 4m0 6l-4 4-4-4" }) }) 9292 } 9293 ),
9294 /* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "text-base font-semibold text-white", children: "Switching Back to Pro Mode" }) 9295 ] }), 9296 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-zinc-400 ml-11", children: "You added new notes while in Easy Mode. Choose what to do with them on each track." }) 9297 ] 9298 } 9299 ), 9300 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "px-6 py-4 space-y-4 max-h-96 overflow-y-auto", children: mergeState.tracks.map((track) => { 9301 const res = resolutions[track.trackIndex] || { action: "discard" }; 9302 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "rounded-lg bg-zinc-800/50 border border-zinc-700/40 overflow-hidden", children: [ 9303 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-4 py-2.5 flex items-center gap-2 border-b border-zinc-700/30 bg-zinc-800/80", children: [ 9304 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-2 h-2 rounded-full bg-indigo-400 flex-shrink-0" }),
9305 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm font-medium text-zinc-200", children: track.trackName }), 9306 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "ml-auto text-xs text-zinc-500", children: [ 9307 track.addedNotes.length, 9308 " new note", 9309 track.addedNotes.length !== 1 ? "s" : "" 9310 ] }) 9311 ] }), 9312 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-4 py-3 space-y-2.5", children: [ 9313 track.availablePatterns.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "flex items-start gap-2.5 cursor-pointer group", children: [ 9314 /* @__PURE__ */ jsxRuntimeExports.jsx( 9315 "input", 9316 { 9317 type: "radio", 9318 name: `res-${track.trackIndex}`, 9319 checked: res.action === "merge", 9320 onChange: () => setResolution(track.trackIndex, { 9321 action: "merge", 9322 targetPatternId: res.targetPatternId || track.availablePatterns[0].id 9323 }), 9324 className: "mt-0.5 accent-indigo-500 flex-shrink-0" 9325 } 9326 ),
9327 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1", children: [ 9328 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs text-zinc-300 group-hover:text-white transition-colors", children: "Add to existing pattern:" }), 9329 /* @__PURE__ */ jsxRuntimeExports.jsx( 9330 "select", 9331 { 9332 value: res.action === "merge" ? res.targetPatternId || track.availablePatterns[0].id : track.availablePatterns[0].id, 9333 onChange: (e) => setResolution(track.trackIndex, { action: "merge", targetPatternId: e.target.value }), 9334 onClick: () => setResolution(track.trackIndex, { 9335 action: "merge", 9336 targetPatternId: res.targetPatternId || track.availablePatterns[0].id 9337 }), 9338 className: "mt-1 w-full bg-zinc-700 border border-zinc-600 rounded-md text-xs text-zinc-200 px-2 py-1.5 focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500/30", 9339 children: track.availablePatterns.map((p) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: p.id, children: p.name }, p.id)) 9340 } 9341 ) 9342 ] }) 9343 ] }),
9344 /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "flex items-center gap-2.5 cursor-pointer group", children: [ 9345 /* @__PURE__ */ jsxRuntimeExports.jsx( 9346 "input", 9347 { 9348 type: "radio", 9349 name: `res-${track.trackIndex}`, 9350 checked: res.action === "new-track", 9351 onChange: () => setResolution(track.trackIndex, { action: "new-track" }), 9352 className: "accent-indigo-500 flex-shrink-0" 9353 } 9354 ), 9355 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs text-zinc-300 group-hover:text-white transition-colors", children: "Put in a new track" }) 9356 ] }), 9357 /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "flex items-center gap-2.5 cursor-pointer group", children: [ 9358 /* @__PURE__ */ jsxRuntimeExports.jsx( 9359 "input", 9360 { 9361 type: "radio", 9362 name: `res-${track.trackIndex}`, 9363 checked: res.action === "discard", 9364 onChange: () => setResolution(track.trackIndex, { action: "discard" }), 9365 className: "accent-indigo-500 flex-shrink-0" 9366 } 9367 ),
9368 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs text-zinc-400 group-hover:text-zinc-300 transition-colors", children: "Discard new notes" }) 9369 ] }) 9370 ] }) 9371 ] }, track.trackIndex); 9372 }) }), 9373 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-6 py-4 border-t border-zinc-700/50 flex items-center justify-between gap-3 bg-zinc-900/80", children: [ 9374 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-zinc-500", children: "Deleted notes will be removed from their original patterns." }), 9375 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 flex-shrink-0", children: [ 9376 /* @__PURE__ */ jsxRuntimeExports.jsx( 9377 "button", 9378 { 9379 onClick: onClose, 9380 className: "px-4 py-2 rounded-lg text-xs font-medium text-zinc-400 hover:text-zinc-200 hover:bg-zinc-700/50 transition-colors", 9381 children: "Cancel" 9382 } 9383 ),
9384 /* @__PURE__ */ jsxRuntimeExports.jsx( 9385 "button", 9386 { 9387 onClick: handleConfirm, 9388 className: "px-4 py-2 rounded-lg text-xs font-semibold text-white transition-all", 9389 style: { background: "linear-gradient(135deg, #6366f1, #a855f7)" }, 9390 children: "Switch to Pro Mode" 9391 } 9392 ) 9393 ] }) 9394 ] }) 9395 ] 9396 } 9397 ) 9398 } 9399 ); 9400}; 9401const uint8ArrayToBase64 = (arr) => { 9402 let binary = ""; 9403 const len = arr.byteLength; 9404 for (let i = 0; i < len; i++) { 9405 binary += String.fromCharCode(arr[i]); 9406 } 9407 return btoa(binary); 9408}; 9409const EditorLayout = ({ currentUser, onAuthRequested, projectId, onNavigate }) => { 9410 useGlobalShortcuts(); 9411 const { t } = useTranslation(); 9412 const { 9413 engine, 9414 isEngineReady, 9415 engineError, 9416 loadingStatus, 9417 midiVersion, 9418 loadMidiData, 9419 loadProjectData, 9420 forceSync, 9421 undo, 9422 redo, 9423 canUndo, 9424 canRedo, 9425 copy, 9426 paste, 9427 clipboard, 9428 selectedNoteIds, 9429 patterns, 9430 playlistTracks, 9431 playlistClips, 9432 automationClips, 9433 timeMarkers, 9434 loopRegion, 9435 easyMode, 9436 gridSnap, 9437 activePatternId, 9438 setEasyMode, 9439 easyToProMergeState, 9440 setEasyToProMergeState, 9441 isMidiSupported, 9442 isMidiConnected, 9443 connectMidi, 9444 midiError, 9445 meiDocs 9446 } = useEditor(); 9447 const [bottomPane, setBottomPane] = React.useState(() => { 9448 if (typeof window !== "undefined") { 9449 const params = new URLSearchParams(window.location.search); 9450 const mode = params.get("mode"); 9451 if (mode === "score" || mode && mode.includes("score")) { 9452 return "score-editor"; 9453 } 9454 } 9455 return "piano-roll"; 9456 }); 9457 const [isSaving, setIsSaving] = React.useState(false); 9458 const [isProjectModalOpen, setIsProjectModalOpen] = React.useState(false); 9459 const [isSettingsOpen, setIsSettingsOpen] = React.useState(false); 9460 const [activeSettingsTab, setActiveSettingsTab] = React.useState("general"); 9461 const { layout, setPanelState, resetLayout, updatePanelLayout } = useWorkspaceLayout(); 9462 const inspectorState = layout.inspector.state; 9463 const arrangementState = layout.arrangement.state; 9464 const pianoRollState = layout.pianoRoll.state; 9465 const scoreEditorState = layout.scoreEditor.state; 9466 const mixerState = layout.mixer.state; 9467 const isInspectorFloating = inspectorState === "floating"; 9468 const isArrangementFloating = arrangementState === "floating"; 9469 const dockedRightTabs = [ 9470 pianoRollState !== "popped-out" && pianoRollState !== "hidden" ? "piano-roll" : null, 9471 scoreEditorState !== "popped-out" && scoreEditorState !== "hidden" ? "score-editor" : null, 9472 mixerState !== "popped-out" && mixerState !== "hidden" ? "mixer" : null 9473 ].filter(Boolean); 9474 const hasDockedRightPane = dockedRightTabs.length > 0; 9475 React.useEffect(() => { 9476 if (!hasDockedRightPane) return; 9477 if (bottomPane === "piano-roll" && pianoRollState === "popped-out" || bottomPane === "score-editor" && scoreEditorState === "popped-out" || bottomPane === "mixer" && mixerState === "popped-out") { 9478 setBottomPane(dockedRightTabs[0]); 9479 } 9480 }, [bottomPane, pianoRollState, scoreEditorState, mixerState, hasDockedRightPane]); 9481 const [isFullscreen, setIsFullscreen] = React.useState(false); 9482 const [isFocusMode, setIsFocusMode] = React.useState(false); 9483 const editorRef = React.useRef(null); 9484 const fileInputRef = React.useRef(null); 9485 const [isExportDropdownOpen, setIsExportDropdownOpen] = React.useState(false); 9486 const exportDropdownRef = React.useRef(null); 9487 const [draggedPanel, setDraggedPanel] = reactExports.useState(null); 9488 const [dragStart, setDragStart] = reactExports.useState({ x: 0, y: 0 }); 9489 const [resizingPanel, setResizingPanel] = reactExports.useState(null); 9490 const [resizeStart, setResizeStart] = reactExports.useState({ x: 0, y: 0, w: 0, h: 0 }); 9491 const inspectorPos = layout.inspector.floatPos || { x: 50, y: 150 }; 9492 const inspectorSize = layout.inspector.floatSize || { width: 280, height: 450 }; 9493 const arrangementPos = layout.arrangement.floatPos || { x: 300, y: 150 }; 9494 const arrangementSize = layout.arrangement.floatSize || { width: 600, height: 400 }; 9495 const bottomPanePos = layout.pianoRoll.floatPos || { x: 600, y: 150 }; 9496 const bottomPaneSize = layout.pianoRoll.floatSize || { width: 650, height: 480 }; 9497 const isRightPaneFloating = pianoRollState === "floating" || scoreEditorState === "floating" || mi
9497xerState === "floating"; 9498 const setInspectorPos = (pos) => updatePanelLayout("inspector", { floatPos: pos }); 9499 const setArrangementPos = (pos) => updatePanelLayout("arrangement", { floatPos: pos }); 9500 const setBottomPanePos = (pos) => { 9501 updatePanelLayout("pianoRoll", { floatPos: pos }); 9502 updatePanelLayout("scoreEditor", { floatPos: pos }); 9503 updatePanelLayout("mixer", { floatPos: pos }); 9504 }; 9505 const setInspectorSize = (size) => updatePanelLayout("inspector", { floatSize: size }); 9506 const setArrangementSize = (size) => updatePanelLayout("arrangement", { floatSize: size }); 9507 const setBottomPaneSize = (size) => { 9508 updatePanelLayout("pianoRoll", { floatSize: size }); 9509 updatePanelLayout("scoreEditor", { floatSize: size }); 9510 updatePanelLayout("mixer", { floatSize: size }); 9511 }; 9512 const setShowInspector = (show) => setPanelState("inspector", show ? "docked" : "hidden"); 9513 const setShowArrangement = (show) => setPanelState("arrangement", show ? "docked" : "hidden"); 9514 const setShowBottomPane = (show) => { 9515 if (show) { 9516 if (bottomPane === "piano-roll") setPanelState("pianoRoll", "docked"); 9517 else if (bottomPane === "score-editor") setPanelState("scoreEditor", "docked"); 9518 else setPanelState("mixer", "docked"); 9519 } else { 9520 setPanelState("pianoRoll", "hidden"); 9521 setPanelState("scoreEditor", "hidden"); 9522 setPanelState("mixer", "hidden"); 9523 } 9524 }; 9525 const handlePanelDragStart = (panel, e) => { 9526 setDraggedPanel(panel); 9527 const currentPos = panel === "inspector" ? inspectorPos : panel === "arrangement" ? arrangementPos : bottomPanePos; 9528 setDragStart({ x: e.clientX - currentPos.x, y: e.clientY - currentPos.y }); 9529 e.preventDefault(); 9530 }; 9531 const handlePanelResizeStart = (panel, e) => { 9532 e.stopPropagation(); 9533 e.preventDefault(); 9534 setResizingPanel(panel); 9535 const currentSize = panel === "inspector" ? inspectorSize : panel === "arrangement" ? arrangementSize : bottomPaneSize; 9536 setResizeStart({ 9537 x: e.clientX, 9538 y: e.clientY, 9539 w: currentSize.width, 9540 h: currentSize.height 9541 }); 9542 }; 9543 const handleGlobalDragMove = (e) => { 9544 if (draggedPanel) { 9545 const newX = e.clientX - dragStart.x; 9546 const newY = e.clientY - dragStart.y; 9547 if (draggedPanel === "inspector") { 9548 setInspectorPos({ x: newX, y: newY }); 9549 } else if (draggedPanel === "arrangement") { 9550 setArrangementPos({ x: newX, y: newY }); 9551 } else if (draggedPanel === "bottom-pane") { 9552 setBottomPanePos({ x: newX, y: newY }); 9553 } 9554 } else if (resizingPanel) { 9555 const deltaX = e.clientX - resizeStart.x; 9556 const deltaY = e.clientY - resizeStart.y; 9557 const newW = Math.max(200, resizeStart.w + deltaX); 9558 const newH = Math.max(150, resizeStart.h + deltaY); 9559 if (resizingPanel === "inspector") { 9560 setInspectorSize({ width: newW, height: newH }); 9561 } else if (resizingPanel === "arrangement") { 9562 setArrangementSize({ width: newW, height: newH }); 9563 } else if (resizingPanel === "bottom-pane") { 9564 setBottomPaneSize({ width: newW, height: newH }); 9565 } 9566 } 9567 }; 9568 const handleGlobalDragEnd = () => { 9569 setDraggedPanel(null); 9570 setResizingPanel(null); 9571 }; 9572 React.useEffect(() => { 9573 const handleClickOutside = (e) => { 9574 if (exportDropdownRef.current && !exportDropdownRef.current.contains(e.target)) { 9575 setIsExportDropdownOpen(false); 9576 } 9577 }; 9578 document.addEventListener("mousedown", handleClickOutside); 9579 return () => document.removeEventListener("mousedown", handleClickOutside); 9580 }, []); 9581 const toggleFullscreen = () => { 9582 var _a; 9583 if (!document.fullscreenElement) { 9584 (_a = editorRef.current) == null ? void 0 : _a.requestFullscreen().catch((err) => { 9585 console.error(`Error attempting to enable fullscreen: ${err.message}`); 9586 }); 9587 } else { 9588 document.exitFullscreen(); 9589 } 9590 }; 9591 React.useEffect(() => { 9592 const handleFullscreenChange = () => { 9593 setIsFullscreen(!!document.fullscreenElement); 9594 }; 9595 document.addEventListener("fullscreenchange", handleFullscreenChange); 9596 return () => document.removeEventListener("fullscreenchange", handleFullscreenChange); 9597 }, []); 9598 const [songName, setSongName] = React.useState(""); 9599 React.useEffect(() => { 9600 if (engine == null ? void 0 : engine.midi) { 9601 setSongName(engine.midi.name || "Untitled MIDI Project"); 9602 } 9603 }, [engine == null ? void 0 : engine.midi, midiVersion]); 9604 React.useEffect(() => { 9605 const handleFocusPianoRoll = () => { 9606 setBottomPane("piano-roll"); 9607 setShowBottomPane(true); 9608 }; 9609 window.addEventListener("focus-piano-roll", handleFocusPianoRoll); 9610 return () => window.removeEventListener("focus-piano-roll", handleFocusPianoRoll); 9611 }, []); 9612 React.useEffect(() => { 9613 const handleKeyDown = (e) => { 9614 const activeEl = document.activeElement; 9615 if (activeEl && (activeEl.tagName === "INPUT" || activeEl.tagName === "TEXTAREA" || activeEl.getAttribute("contenteditable") === "true")) { 9616 return; 9617 } 9618 const isCmdOrCtrl = e.ctrlKey || e.metaKey; 9619 const isShift = e.shiftKey; 9620 if (isCmdOrCtrl && isShift) { 9621 if (e.key.toLowerCase() === "i") { 9622 e.preventDefault(); 9623 setShowInspector((prev) => !prev); 9624 } else if (e.key.toLowerCase() === "a") { 9625 e.preventDefault();
9626 setShowArrangement((prev) => !prev); 9627 } else if (e.key.toLowerCase() === "b") { 9628 e.preventDefault(); 9629 setShowBottomPane(!hasDockedRightPane); 9630 } 9631 } 9632 }; 9633 window.addEventListener("keydown", handleKeyDown); 9634 return () => window.removeEventListener("keydown", handleKeyDown); 9635 }, []); 9636 const handleNameChange = (e) => { 9637 setSongName(e.target.value); 9638 if (engine && engine.midi) { 9639 engine.midi.name = e.target.value; 9640 } 9641 }; 9642 const handleSave = async () => { 9643 var _a; 9644 if (!currentUser) { 9645 onAuthRequested(); 9646 return; 9647 } 9648 if (!engine) return; 9649 setIsSaving(true); 9650 try { 9651 const midiData = engine.exportMidi(); 9652 const base64Data = uint8ArrayToBase64(midiData); 9653 const payload = { 9654 title: songName || ((_a = engine.midi) == null ? void 0 : _a.name) || "Untitled MIDI Project", 9655 projectData: { 9656 midi: base64Data, 9657 patterns, 9658 playlistTracks, 9659 playlistClips, 9660 automationClips, 9661 timeMarkers, 9662 loopRegion, 9663 easyMode, 9664 gridSnap, 9665 activePatternId, 9666 meiDocs 9667 }, 9668 originalMidiId: projectId && !isNaN(Number(projectId)) ? projectId : null 9669 // if we loaded a track id to start 9670 }; 9671 const isSavedProject = projectId && isNaN(Number(projectId)); 9672 const method = isSavedProject ? "PUT" : "POST"; 9673 const url = method === "PUT" ? `/api/projects/${projectId}` : "/api/projects"; 9674 const res = await fetch(url, { 9675 method, 9676 headers: { "Content-Type": "application/json" }, 9677 body: JSON.stringify(payload) 9678 }); 9679 if (!res.ok) { 9680 const errData = await res.json(); 9681 throw new Error(errData.error || "Failed to save project"); 9682 } 9683 const savedProject = await res.json(); 9684 if (method === "POST" && onNavigate) { 9685 onNavigate("editor", savedProject.id); 9686 } 9687 alert("Project saved successfully!"); 9688 } catch (err) { 9689 console.error("Save error:", err); 9690 alert(err.message || "Failed to save project. Please make sure you are logged in."); 9691 } finally { 9692 setIsSaving(false); 9693 } 9694 }; 9695 const [isImporting, setIsImporting] = React.useState(false); 9696 const [importProgress, setImportProgress] = React.useState(""); 9697 const { setMeiDoc, setActiveTrackIndex, pushHistoryState } = useEditor(); 9698 const handleImportClick = () => { 9699 var _a; 9700 if (engine && engine.midi && engine.midi.tracks.some((t2) => t2.notes.length > 0)) { 9701 if (!window.confirm("Importing a new project file will replace your current project. Do you want to continue?")) { 9702 return; 9703 } 9704 } 9705 (_a = fileInputRef.current) == null ? void 0 : _a.click(); 9706 }; 9707 const handleFileChange = async (e) => { 9708 var _a, _b; 9709 const file = (_a = e.target.files) == null ? void 0 : _a[0]; 9710 if (!file) return; 9711 const isProjectFile = file.name.toLowerCase().endsWith(".ham") || file.name.toLowerCase().endsWith(".json"); 9712 if (isProjectFile) { 9713 try { 9714 setIsImporting(true); 9715 setImportProgress("Reading project file..."); 9716 const reader = new FileReader(); 9717 const textPromise = new Promise((resolve, reject) => { 9718 reader.onload = () => resolve(reader.result); 9719 reader.onerror = () => reject(reader.error); 9720 reader.readAsText(file); 9721 }); 9722 const text = await textPromise; 9723 const data = JSON.parse(text); 9724 const projectData = data.projectData || (data.patterns ? data : null); 9725 if (!projectData || !projectData.patterns) { 9726 throw new Error("Invalid project file: missing patterns or project data"); 9727 } 9728 setImportProgress("Loading project..."); 9729 const name = data.title || projectData.title || file.name.replace(/\.[^/.]+$/, ""); 9730 if (engine && engine.midi) { 9731 engine.midi.name = name; 9732 } 9733 setSongName(name); 9734 loadProjectData(projectData); 9735 setIsImporting(false); 9736 setImportProgress(""); 9737 } catch (err) { 9738 console.error("Project import error:", err); 9739 alert("Failed to load project file: " + (err.message || err)); 9740 setIsImporting(false); 9741 setImportProgress(""); 9742 } finally { 9743 if (fileInputRef.current) fileInputRef.current.value = ""; 9744 } 9745 return; 9746 } 9747 try { 9748 setIsImporting(true); 9749 setImportProgress("Uploading MIDI... (This uses 1 conversion credit)"); 9750 const formData = new FormData(); 9751 formData.append("midiFile", file); 9752 formData.append("generateScore", "true"); 9753 const uploadRes = await fetch("/api/convert", { 9754 method: "POST", 9755 body: formData 9756 }); 9757 if (!uploadRes.ok) { 9758 const errData = await uploadRes.json(); 9759 if (errData.limitReached) { 9760 alert(errData.error); 9761 } else { 9762 throw new Error(errData.error || "Failed to upload MIDI"); 9763 } 9764 const buffer2 = await file.arrayBuffer(); 9765 pushHistoryState(); 9766 loadMidiData(buffer2); 9767 setIsImporting(false); 9768 return; 9769 } 9770 const { track } = await uploadRes.json(); 9771 const jobId = track.id; 9772 setImportProgress("Processing Sheet Music..."); 9773 let isComplete = false;
9774 let jobData = null; 9775 let attempts = 0; 9776 while (!isComplete && attempts < 120) { 9777 await new Promise((r) => setTimeout(r, 2e3)); 9778 attempts++; 9779 const statusRes = await fetch(`/api/convert/status/${jobId}`); 9780 if (statusRes.ok) { 9781 jobData = await statusRes.json(); 9782 if (jobData.status === "completed") { 9783 isComplete = true; 9784 } else if (jobData.status === "error" || jobData.status === "failed") { 9785 throw new Error("Worker failed to process MIDI"); 9786 } else { 9787 const percentStr = typeof jobData.percent === "number" ? ` (${jobData.percent}%)` : ""; 9788 setImportProgress(`Processing: ${jobData.status}${percentStr}`); 9789 } 9790 } 9791 } 9792 if (!isComplete) throw new Error("Processing timed out"); 9793 const buffer = await file.arrayBuffer(); 9794 pushHistoryState(); 9795 loadMidiData(buffer); 9796 const meiPath = jobData.studioPath || ((_b = jobData.midiMetadata) == null ? void 0 : _b.studioPath); 9797 if (meiPath) { 9798 setImportProgress("Loading Sheet Music..."); 9799 const meiRes = await fetch(meiPath); 9800 if (meiRes.ok) { 9801 const meiText = await meiRes.text(); 9802 setMeiDoc(0, meiText);
9803 setActiveTrackIndex(0); 9804 } 9805 } 9806 } catch (err) { 9807 console.error("Import error:", err); 9808 alert("Failed to generate sheet music. Loading audio only."); 9809 try { 9810 const buffer = await file.arrayBuffer(); 9811 pushHistoryState(); 9812 loadMidiData(buffer); 9813 } catch (e2) { 9814 } 9815 } finally { 9816 setIsImporting(false); 9817 setImportProgress(""); 9818 if (fileInputRef.current) fileInputRef.current.value = ""; 9819 } 9820 }; 9821 const handleExportHAM = () => { 9822 if (!engine) return; 9823 try { 9824 const midiData = engine.exportMidi(); 9825 const base64Data = uint8ArrayToBase64(midiData); 9826 const payload = { 9827 version: 1, 9828 type: "hamienet_project", 9829 title: songName || "Untitled MIDI Project", 9830 projectData: { 9831 midi: base64Data, 9832 patterns, 9833 playlistTracks, 9834 playlistClips, 9835 automationClips, 9836 timeMarkers, 9837 loopRegion, 9838 easyMode, 9839 gridSnap, 9840 activePatternId, 9841 meiDocs 9842 } 9843 }; 9844 const jsonString = JSON.stringify(payload, null, 2); 9845 const blob = new Blob([jsonString], { type: "application/json" }); 9846 const url = URL.createObjectURL(blob); 9847 const a = document.createElement("a"); 9848 a.href = url; 9849 const fileName = (songName || "Untitled MIDI Project").trim().replace(/[^a-z0-9_-]/gi, "_") + ".ham"; 9850 a.download = fileName; 9851 a.click(); 9852 URL.revokeObjectURL(url); 9853 } catch (err) { 9854 console.error("Export HAM error:", err); 9855 alert("Failed to export project: " + err); 9856 } 9857 }; 9858 const handleExportMIDI = async () => { 9859 if (!engine) return; 9860 if (!currentUser) { 9861 onAuthRequested(); 9862 return; 9863 } 9864 if (currentUser.role !== "ADMIN") { 9865 const confirmMsg = "Exporting this MIDI will deduct 1 credit from your account. Do you want to proceed?"; 9866 if (!confirm(confirmMsg)) return; 9867 } else { 9868 const confirmMsg = "Export MIDI? (Admin: No credits deducted)"; 9869 if (!confirm(confirmMsg)) return; 9870 } 9871 try { 9872 const res = await fetch("/api/projects/charge-export", { 9873 method: "POST", 9874 headers: { "Content-Type": "application/json" } 9875 }); 9876 if (res.status === 401) { 9877 localStorage.removeItem("hamie_session"); 9878 alert("Your session has expired. Please log in again."); 9879 onAuthRequested(); 9880 return; 9881 } 9882 const data = await res.json(); 9883 if (!res.ok) { 9884 alert(data.error || "Failed to deduct credits for MIDI export"); 9885 return; 9886 } 9887 if (currentUser && typeof data.newCredits === "number") { 9888 currentUser.credits = data.newCredits; 9889 } 9890 const midiData = engine.exportMidi(); 9891 const blob = new Blob([midiData], { type: "audio/midi" }); 9892 const url = URL.createObjectURL(blob); 9893 const a = document.createElement("a"); 9894 a.href = url; 9895 const fileName = (songName || "Untitled MIDI Project").trim().replace(/[^a-z0-9_-]/gi, "_") + ".mid"; 9896 a.download = fileName; 9897 a.click(); 9898 URL.revokeObjectURL(url); 9899 alert(`Exported successfully! Remaining credits: ${data.newCredits}`); 9900 } catch (err) { 9901 console.error("Export MIDI error:", err); 9902 alert("Failed to export MIDI: " + err.message); 9903 } 9904 }; 9905 const isExpandedScoreView = bottomPane === "score-editor" && isFocusMode; 9906 return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 9907 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { ref: editorRef, onMouseMove: handleGlobalDragMove, onMouseUp: handleGlobalDragEnd, onContextMenu: (e) => e.preventDefault(), className: `flex flex-col w-full bg-white dark:bg-zinc-900 text-zinc-900 dark:text-white animate-in fade-in ${isFullscreen ? "h-screen" : "h-[calc(100vh-64px)]"}`, children: [ 9908 isExpandedScoreView ? ( 9909 /* Single Compact Toolbar for Expanded Score View */
9910 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "h-12 border-b border-zinc-200 dark:border-zinc-800 flex justify-between items-center bg-zinc-50 dark:bg-zinc-950 px-4 shrink-0 gap-3", children: [ 9911 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3 min-w-0 flex-1", children: [ 9912 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 shrink-0", children: [ 9913 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "p-1 bg-indigo-500/20 text-indigo-400 rounded text-xs", children: "ð¹" }), 9914 /* @__PURE__ */ jsxRuntimeExports.jsx( 9915 "input", 9916 { 9917 type: "text", 9918 value: songName, 9919 onChange: handleNameChange, 9920 onBlur: () => forceSync(), 9921 className: "bg-transparent border-b border-transparent hover:border-zinc-300 dark:hover:border-zinc-700 focus:border-indigo-500 outline-none w-32 sm:w-44 transition-colors font-bold text-xs", 9922 placeholder: t("editor.untitledProject", "Untitled MIDI Project") 9923 } 9924 ) 9925 ] }),
9926 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-px h-5 bg-zinc-200 dark:bg-zinc-800 shrink-0" }), 9927 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "scale-90 origin-left shrink-0", children: /* @__PURE__ */ jsxRuntimeExports.jsx(TransportControls, {}) }), 9928 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-px h-5 bg-zinc-200 dark:bg-zinc-800 shrink-0" }), 9929 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-0.5 shrink-0", children: [ 9930 /* @__PURE__ */ jsxRuntimeExports.jsx( 9931 "button", 9932 { 9933 onClick: undo, 9934 disabled: !canUndo, 9935 title: "Undo (Ctrl+Z)", 9936 className: `p-1.5 rounded transition-colors ${canUndo ? "text-zinc-700 hover:bg-zinc-200 dark:text-zinc-200 dark:hover:bg-zinc-800" : "text-zinc-400 dark:text-zinc-600 opacity-40 cursor-not-allowed"}`, 9937 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Undo2, { size: 16 }) 9938 } 9939 ),
9940 /* @__PURE__ */ jsxRuntimeExports.jsx( 9941 "button", 9942 { 9943 onClick: redo, 9944 disabled: !canRedo, 9945 title: "Redo (Ctrl+Y)", 9946 className: `p-1.5 rounded transition-colors ${canRedo ? "text-zinc-700 hover:bg-zinc-200 dark:text-zinc-200 dark:hover:bg-zinc-800" : "text-zinc-400 dark:text-zinc-600 opacity-40 cursor-not-allowed"}`, 9947 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Redo2, { size: 16 }) 9948 } 9949 ), 9950 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-px h-4 bg-zinc-300 dark:bg-zinc-700 mx-1.5" }), 9951 /* @__PURE__ */ jsxRuntimeExports.jsx( 9952 "button", 9953 { 9954 onClick: copy, 9955 disabled: selectedNoteIds.length === 0, 9956 title: "Copy (Ctrl+C)", 9957 className: `p-1.5 rounded transition-colors ${selectedNoteIds.length > 0 ? "text-zinc-700 hover:bg-zinc-200 dark:text-zinc-200 dark:hover:bg-zinc-800" : "text-zinc-400 dark:text-zinc-600 opacity-40 cursor-not-allowed"}`, 9958 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Copy, { size: 16 }) 9959 } 9960 ),
9961 /* @__PURE__ */ jsxRuntimeExports.jsx( 9962 "button", 9963 { 9964 onClick: paste, 9965 disabled: !clipboard || clipboard.notes.length === 0, 9966 title: "Paste (Ctrl+V)", 9967 className: `p-1.5 rounded transition-colors ${clipboard && clipboard.notes.length > 0 ? "text-zinc-700 hover:bg-zinc-200 dark:text-zinc-200 dark:hover:bg-zinc-800" : "text-zinc-400 dark:text-zinc-600 opacity-40 cursor-not-allowed"}`, 9968 children: /* @__PURE__ */ jsxRuntimeExports.jsx(ClipboardPaste, { size: 16 }) 9969 } 9970 ) 9971 ] }) 9972 ] }), 9973 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3 shrink-0", children: [ 9974 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1", children: [ 9975 /* @__PURE__ */ jsxRuntimeExports.jsx(HelpTip, { text: t("editor.saveHelp", "Save project"), position: "bottom", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 9976 "button", 9977 { 9978 onClick: handleSave, 9979 disabled: isSaving || !isEngineReady, 9980 title: t("editor.save", "Save"), 9981 className: "p-1.5 text-zinc-500 hover:text-zinc-800 dark:text-zinc-400 dark:hover:text-zinc-200 hover:bg-zinc-200 dark:hover:bg-zinc-800 rounded disabled:opacity-50 transition-colors",
9982 children: isSaving ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-4 h-4 border-2 border-zinc-500 border-t-transparent rounded-full animate-spin" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Save, { size: 16 }) 9983 } 9984 ) }), 9985 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative", ref: exportDropdownRef, children: [ 9986 /* @__PURE__ */ jsxRuntimeExports.jsx(HelpTip, { text: t("editor.exportHelp", "Export"), position: "bottom", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 9987 "button", 9988 { 9989 onClick: () => setIsExportDropdownOpen((prev) => !prev), 9990 disabled: !isEngineReady, 9991 title: t("editor.export", "Export"), 9992 className: "p-1.5 text-zinc-500 hover:text-zinc-800 dark:text-zinc-400 dark:hover:text-zinc-200 hover:bg-zinc-200 dark:hover:bg-zinc-800 rounded disabled:opacity-50 transition-colors flex items-center", 9993 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Download, { size: 16 }) 9994 } 9995 ) }), 9996 isExportDropdownOpen && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute right-0 mt-1 w-56 rounded-md shadow-lg bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 z-50 py-1", children: [
9997 /* @__PURE__ */ jsxRuntimeExports.jsxs( 9998 "button", 9999 { 10000 onClick: () => { 10001 setIsExportDropdownOpen(false); 10002 handleExportHAM(); 10003 }, 10004 className: "w-full text-left px-4 py-2 text-sm text-zinc-700 dark:text-zinc-200 hover:bg-zinc-100 dark:hover:bg-zinc-700 transition-colors flex flex-col gap-0.5", 10005 children: [ 10006 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-semibold text-zinc-900 dark:text-white", children: t("editor.exportHam", "Export Project (.ham)") }), 10007 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs text-zinc-400", children: t("editor.exportHamDesc", "Save arrangement, patterns & sheet music") }) 10008 ] 10009 } 10010 ), 10011 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "border-t border-zinc-200 dark:border-zinc-700 my-1" }),
10012 /* @__PURE__ */ jsxRuntimeExports.jsxs( 10013 "button", 10014 { 10015 onClick: () => { 10016 setIsExportDropdownOpen(false); 10017 handleExportMIDI(); 10018 }, 10019 className: "w-full text-left px-4 py-2 text-sm text-zinc-700 dark:text-zinc-200 hover:bg-zinc-100 dark:hover:bg-zinc-700 transition-colors flex flex-col gap-0.5", 10020 children: [ 10021 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-semibold text-zinc-900 dark:text-white", children: t("editor.exportMidi", "Export MIDI (.mid)") }), 10022 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs text-zinc-400", children: t("editor.exportMidiDesc", "Standard MIDI notes only") }) 10023 ] 10024 } 10025 ) 10026 ] }) 10027 ] }), 10028 /* @__PURE__ */ jsxRuntimeExports.jsx(HelpTip, { text: t("editor.settingsHelp", "Settings"), position: "bottom", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 10029 "button", 10030 { 10031 onClick: () => setIsSettingsOpen(true), 10032 title: t("editor.settings", "Settings"), 10033 className: "p-1.5 text-zinc-500 hover:text-zinc-800 dark:text-zinc-400 dark:hover:text-zinc-200 hover:bg-zinc-200 dark:hover:bg-zinc-800 rounded transition-colors", 10034 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Settings, { size: 16 }) 10035 } 10036 ) }) 10037 ] }),
10038 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-px h-5 bg-zinc-200 dark:bg-zinc-800" }), 10039 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1", children: [ 10040 /* @__PURE__ */ jsxRuntimeExports.jsx( 10041 "button", 10042 { 10043 onClick: resetLayout, 10044 title: "Reset Workspace", 10045 className: "p-1.5 rounded transition-colors ml-1 text-zinc-500 hover:bg-zinc-200 dark:hover:bg-zinc-800", 10046 children: /* @__PURE__ */ jsxRuntimeExports.jsx(LayoutDashboard, { size: 18 }) 10047 } 10048 ), 10049 /* @__PURE__ */ jsxRuntimeExports.jsx( 10050 "button", 10051 { 10052 onClick: toggleFullscreen, 10053 title: isFullscreen ? "Exit Fullscreen" : "Enter Fullscreen", 10054 className: `p-1.5 rounded transition-colors ${isFullscreen ? "bg-indigo-100 text-indigo-600 dark:bg-indigo-500/20 dark:text-indigo-400" : "text-zinc-500 hover:bg-zinc-200 dark:hover:bg-zinc-800"}`, 10055 children: isFullscreen ? /* @__PURE__ */ jsxRuntimeExports.jsx(Minimize, { size: 16 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Maximize, { size: 16 }) 10056 } 10057 ),
10058 /* @__PURE__ */ jsxRuntimeExports.jsx( 10059 "button", 10060 { 10061 onClick: () => setIsFocusMode(!isFocusMode), 10062 title: isFocusMode ? "Exit Focus Mode" : "Focus Mode", 10063 className: `p-1.5 rounded transition-colors ${isFocusMode ? "bg-indigo-100 text-indigo-600 dark:bg-indigo-500/20 dark:text-indigo-400" : "text-zinc-500 hover:bg-zinc-200 dark:hover:bg-zinc-800"}`, 10064 children: isFocusMode ? /* @__PURE__ */ jsxRuntimeExports.jsx(Minimize2, { size: 16 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Maximize2, { size: 16 }) 10065 } 10066 ) 10067 ] }) 10068 ] }) 10069 ] }) 10070 ) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 10071 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "min-h-14 py-2 border-b border-zinc-200 dark:border-zinc-800 flex flex-wrap justify-between items-center bg-zinc-50 dark:bg-zinc-950 px-4 shrink-0 gap-3", children: [ 10072 /* @__P
10072URE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 lg:gap-6 min-w-0 flex-wrap", children: [ 10073 /* @__PURE__ */ jsxRuntimeExports.jsxs("h1", { className: "text-xl font-bold flex items-center gap-2 shrink-0", children: [ 10074 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "p-1.5 bg-indigo-500/20 text-indigo-400 rounded-lg", children: "ð¹" }), 10075 /* @__PURE__ */ jsxRuntimeExports.jsx( 10076 "input", 10077 { 10078 type: "text", 10079 value: songName, 10080 onChange: handleNameChange, 10081 onBlur: () => forceSync(), 10082 className: "bg-transparent border-b border-transparent hover:border-zinc-300 dark:hover:border-zinc-700 focus:border-indigo-500 outline-none w-48 sm:w-64 transition-colors hidden md:inline font-bold", 10083 placeholder: t("editor.untitledProject", "Untitled MIDI Project") 10084 } 10085 ) 10086 ] }), 10087 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-px h-6 bg-zinc-200 dark:bg-zinc-800 hidden sm:block mx-1" }),
10088 /* @__PURE__ */ jsxRuntimeExports.jsx(TransportControls, {}) 10089 ] }), 10090 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2 items-center flex-wrap shrink-0", children: [ 10091 isImporting && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "px-3 py-1 text-xs text-indigo-400 self-center animate-pulse truncate max-w-[120px] sm:max-w-[200px]", children: importProgress }), 10092 projectId && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "px-3 py-1 bg-zinc-800 rounded-full text-xs sm:text-sm text-zinc-400 self-center truncate max-w-[100px] sm:max-w-[150px]", children: [ 10093 "ID: ", 10094 projectId 10095 ] }), 10096 /* @__PURE__ */ jsxRuntimeExports.jsx("input", { type: "file", accept: ".mid,.midi,.ham,.json", ref: fileInputRef, className: "hidden", onChange: handleFileChange }), 10097 /* @__PURE__ */ jsxRuntimeExports.jsx(HelpTip, { text: t("editor.importHelp", "Import a MIDI (.mid) or Project (.ham) file from your computer. Note: this will replace your current project."), position: "bottom", children: /* @__PURE__ */ jsxRuntimeExports.jsxs( 10098 "button", 10099 { 10100 onClick: handleImportClick, 10101 disabled: isImporting, 10102 className: "px-3 sm:px-4 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-white rounded font-bold text-xs sm:text-sm transition-colors border border-zinc-700 shadow-sm flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed", 10103 children: [
10104 isImporting ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-3 h-3 border-2 border-white border-t-transparent rounded-full animate-spin" }) : null, 10105 isImporting ? t("editor.importing", "Importing...") : t("editor.import", "Import") 10106 ] 10107 } 10108 ) }), 10109 /* @__PURE__ */ jsxRuntimeExports.jsx(HelpTip, { text: t("editor.projectsHelp", "Open your saved HamieNET MIDI projects."), position: "bottom", children: /* @__PURE__ */ jsxRuntimeExports.jsxs( 10110 "button", 10111 { 10112 onClick: () => setIsProjectModalOpen(true), 10113 disabled: !isEngineReady, 10114 className: "px-3 sm:px-4 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-white rounded font-bold text-xs sm:text-sm transition-colors border border-zinc-700 flex items-center gap-2 shadow-sm disabled:opacity-50 disabled:cursor-not-allowed", 10115 children: [ 10116 /* @__PURE__ */ jsxRuntimeExports.jsx(FolderOpen, { size: 14 }), 10117 t("editor.projects", "Projects") 10118 ] 10119 } 10120 ) }),
10121 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative", ref: exportDropdownRef, children: [ 10122 /* @__PURE__ */ jsxRuntimeExports.jsx(HelpTip, { text: t("editor.exportHelp", "Export the current project as a .ham or standard .mid file."), position: "bottom", children: /* @__PURE__ */ jsxRuntimeExports.jsxs( 10123 "button", 10124 { 10125 onClick: () => setIsExportDropdownOpen((prev) => !prev), 10126 disabled: !isEngineReady, 10127 className: "px-3 sm:px-4 py-1.5 bg-indigo-600 hover:bg-indigo-500 text-white rounded font-bold text-xs sm:text-sm transition-colors shadow-lg disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1.5", 10128 children: [ 10129 t("editor.export", "Export"), 10130 /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronDown, { size: 14 }) 10131 ] 10132 } 10133 ) }), 10134 isExportDropdownOpen && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute right-0 mt-1 w-56 rounded-md shadow-lg bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 z-50 py-1", children: [
10135 /* @__PURE__ */ jsxRuntimeExports.jsxs( 10136 "button", 10137 { 10138 onClick: () => { 10139 setIsExportDropdownOpen(false); 10140 handleExportHAM(); 10141 }, 10142 className: "w-full text-left px-4 py-2 text-sm text-zinc-700 dark:text-zinc-200 hover:bg-zinc-100 dark:hover:bg-zinc-700 transition-colors flex flex-col gap-0.5", 10143 children: [ 10144 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-semibold text-zinc-900 dark:text-white", children: t("editor.exportHam", "Export Project (.ham)") }), 10145 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs text-zinc-400", children: t("editor.exportHamDesc", "Save arrangement, patterns & sheet music") }) 10146 ] 10147 } 10148 ), 10149 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "border-t border-zinc-200 dark:border-zinc-700 my-1" }),
10150 /* @__PURE__ */ jsxRuntimeExports.jsxs( 10151 "button", 10152 { 10153 onClick: () => { 10154 setIsExportDropdownOpen(false); 10155 handleExportMIDI(); 10156 }, 10157 className: "w-full text-left px-4 py-2 text-sm text-zinc-700 dark:text-zinc-200 hover:bg-zinc-100 dark:hover:bg-zinc-700 transition-colors flex flex-col gap-0.5", 10158 children: [ 10159 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-semibold text-zinc-900 dark:text-white", children: t("editor.exportMidi", "Export MIDI (.mid)") }), 10160 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs text-zinc-400", children: t("editor.exportMidiDesc", "Standard MIDI notes only") }) 10161 ] 10162 } 10163 ) 10164 ] }) 10165 ] }), 10166 /* @__PURE__ */ jsxRuntimeExports.jsx(HelpTip, { text: t("editor.saveHelp", "Save your project to the cloud so you can access it later."), position: "bottom", children: /* @__PURE__ */ jsxRuntimeExports.jsxs( 10167 "button", 10168 { 10169 onClick: handleSave, 10170 disabled: isSaving || !isEngineReady, 10171 className: "px-3 sm:px-4 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-white rounded font-bold text-xs sm:text-sm transition-colors border border-zinc-700 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed", 10172 children: [
10173 isSaving ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-3 h-3 border-2 border-white border-t-transparent rounded-full animate-spin" }) : null, 10174 t("editor.save", "Save") 10175 ] 10176 } 10177 ) }), 10178 /* @__PURE__ */ jsxRuntimeExports.jsx(HelpTip, { text: t("editor.settingsHelp", "Settings & MIDI Configuration"), position: "bottom", children: /* @__PURE__ */ jsxRuntimeExports.jsxs( 10179 "button", 10180 { 10181 onClick: () => setIsSettingsOpen(true), 10182 className: "px-3 sm:px-4 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-white rounded font-bold text-xs sm:text-sm transition-colors border border-zinc-700 flex items-center gap-2 shadow-sm", 10183 children: [ 10184 /* @__PURE__ */ jsxRuntimeExports.jsx(Settings, { size: 14 }), 10185 t("editor.settings", "Settings") 10186 ] 10187 } 10188 ) }) 10189 ] }) 10190 ] }), 10191 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "h-12 border-b border-zinc-200 dark:border-zinc-800 flex justify-between items-center bg-zinc-50/80 dark:bg-zinc-950/80 px-4 shrink-0", children: [
10192 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-6", children: [ 10193 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center gap-2", children: /* @__PURE__ */ jsxRuntimeExports.jsx(HelpTip, { text: "Undo, redo, copy, or paste notes in the editor.", position: "bottom", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 10194 /* @__PURE__ */ jsxRuntimeExports.jsx( 10195 "button", 10196 { 10197 onClick: undo, 10198 disabled: !canUndo, 10199 title: "Undo (Ctrl+Z)", 10200 className: `p-1.5 rounded transition-colors ${canUndo ? "text-zinc-700 hover:bg-zinc-200 dark:text-zinc-200 dark:hover:bg-zinc-800" : "text-zinc-400 dark:text-zinc-600 opacity-40 cursor-not-allowed"}`, 10201 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Undo2, { size: 18 }) 10202 } 10203 ), 10204 /* @__PURE__ */ jsxRuntimeExports.jsx( 10205 "button", 10206 { 10207 onClick: redo, 10208 disabled: !canRedo, 10209 title: "Redo (Ctrl+Y)", 10210 className: `p-1.5 rounded transition-colors ${canRedo ? "text-zinc-700 hover:bg-zinc-200 dark:text-zinc-200 dark:hover:bg-zinc-800" : "text-zinc-400 dark:text-zinc-600 opacity-40 cursor-not-allowed"}`, 10211 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Redo2, { size: 18 }) 10212 } 10213 ),
10214 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-px h-4 bg-zinc-300 dark:bg-zinc-700 mx-2" }), 10215 /* @__PURE__ */ jsxRuntimeExports.jsx( 10216 "button", 10217 { 10218 onClick: copy, 10219 disabled: selectedNoteIds.length === 0, 10220 title: "Copy (Ctrl+C)", 10221 className: `p-1.5 rounded transition-colors ${selectedNoteIds.length > 0 ? "text-zinc-700 hover:bg-zinc-200 dark:text-zinc-200 dark:hover:bg-zinc-800" : "text-zinc-400 dark:text-zinc-600 opacity-40 cursor-not-allowed"}`, 10222 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Copy, { size: 18 }) 10223 } 10224 ), 10225 /* @__PURE__ */ jsxRuntimeExports.jsx( 10226 "button", 10227 { 10228 onClick: paste, 10229 disabled: !clipboard || clipboard.notes.length === 0, 10230 title: "Paste (Ctrl+V)", 10231 className: `p-1.5 rounded transition-colors ${clipboard && clipboard.notes.length > 0 ? "text-zinc-700 hover:bg-zinc-200 dark:text-zinc-200 dark:hover:bg-zinc-800" : "text-zinc-400 dark:text-zinc-600 opacity-40 cursor-not-allowed"}`, 10232 children: /* @__PURE__ */ jsxRuntimeExports.jsx(ClipboardPaste, { size: 18 }) 10233 } 10234 ) 10235 ] }) }) }),
10236 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-px h-5 bg-zinc-200 dark:bg-zinc-800" }), 10237 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center gap-2", children: /* @__PURE__ */ jsxRuntimeExports.jsx(HelpTip, { text: "Toggle editor panels, fullscreen, or focus mode.", position: "bottom", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 10238 /* @__PURE__ */ jsxRuntimeExports.jsx( 10239 "button", 10240 { 10241 onClick: () => setShowInspector(inspectorState === "hidden"), 10242 title: inspectorState !== "hidden" ? "Hide Inspector" : "Show Inspector", 10243 className: `p-1.5 rounded transition-colors ${inspectorState !== "hidden" ? "bg-indigo-100 text-indigo-600 dark:bg-indigo-500/20 dark:text-indigo-400" : "text-zinc-500 hover:bg-zinc-200 dark:hover:bg-zinc-800"}`, 10244 children: inspectorState !== "hidden" ? /* @__PURE__ */ jsxRuntimeExports.jsx(PanelLeftClose, { size: 18 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(PanelLeftOpen, { size: 18 }) 10245 } 10246 ),
10247 /* @__PURE__ */ jsxRuntimeExports.jsx( 10248 "button", 10249 { 10250 onClick: () => setShowArrangement(arrangementState === "hidden"), 10251 title: arrangementState !== "hidden" ? "Hide Track Lanes" : "Show Track Lanes", 10252 className: `p-1.5 rounded transition-colors ${arrangementState !== "hidden" ? "bg-indigo-100 text-indigo-600 dark:bg-indigo-500/20 dark:text-indigo-400" : "text-zinc-500 hover:bg-zinc-200 dark:hover:bg-zinc-800"}`, 10253 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Layers, { size: 18 }) 10254 } 10255 ), 10256 /* @__PURE__ */ jsxRuntimeExports.jsx( 10257 "button", 10258 { 10259 onClick: () => setShowBottomPane(!hasDockedRightPane), 10260 title: hasDockedRightPane ? "Hide Right Pane" : "Show Right Pane", 10261 className: `p-1.5 rounded transition-colors ${hasDockedRightPane ? "bg-indigo-100 text-indigo-600 dark:bg-indigo-500/20 dark:text-indigo-400" : "text-zinc-500 hover:bg-zinc-200 dark:hover:bg-zinc-800"}`, 10262 children: hasDockedRightPane ? /* @__PURE__ */ jsxRuntimeExports.jsx(PanelRightClose, { size: 18 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(PanelRightOpen, { size: 18 }) 10263 } 10264 ),
10265 /* @__PURE__ */ jsxRuntimeExports.jsx( 10266 "button", 10267 { 10268 onClick: toggleFullscreen, 10269 title: isFullscreen ? "Exit Fullscreen" : "Enter Fullscreen", 10270 className: `p-1.5 rounded transition-colors ml-1 ${isFullscreen ? "bg-indigo-100 text-indigo-600 dark:bg-indigo-500/20 dark:text-indigo-400" : "text-zinc-500 hover:bg-zinc-200 dark:hover:bg-zinc-800"}`, 10271 children: isFullscreen ? /* @__PURE__ */ jsxRuntimeExports.jsx(Minimize, { size: 18 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Maximize, { size: 18 }) 10272 } 10273 ), 10274 /* @__PURE__ */ jsxRuntimeExports.jsx( 10275 "button", 10276 { 10277 onClick: () => setIsFocusMode(!isFocusMode), 10278 title: isFocusMode ? "Exit Focus Mode" : "Enter Focus Mode", 10279 className: `p-1.5 rounded transition-colors ml-1 ${isFocusMode ? "bg-indigo-100 text-indigo-600 dark:bg-indigo-500/20 dark:text-indigo-400" : "text-zinc-500 hover:bg-zinc-200 dark:hover:bg-zinc-800"}`, 10280 children: isFocusMode ? /* @__PURE__ */ jsxRuntimeExports.jsx(Minimize2, { size: 18 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Maximize2, { size: 18 }) 10281 } 10282 ) 10283 ] }) }) }),
10284 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-px h-5 bg-zinc-200 dark:bg-zinc-800" }), 10285 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1 bg-zinc-200/50 dark:bg-zinc-800/50 rounded-lg p-0.5 shadow-inner", children: [ 10286 /* @__PURE__ */ jsxRuntimeExports.jsx( 10287 "button", 10288 { 10289 onClick: () => setEasyMode(true), 10290 className: `px-2.5 py-1 rounded text-xs font-bold transition-all ${easyMode ? "bg-indigo-600 text-white shadow-sm" : "text-zinc-500 hover:text-zinc-800 dark:hover:text-zinc-200"}`, 10291 children: "Easy Mode" 10292 } 10293 ), 10294 /* @__PURE__ */ jsxRuntimeExports.jsx( 10295 "button", 10296 { 10297 onClick: () => setEasyMode(false), 10298 className: `px-2.5 py-1 rounded text-xs font-bold transition-all ${!easyMode ? "bg-indigo-600 text-white shadow-sm" : "text-zinc-500 hover:text-zinc-800 dark:hover:text-zinc-200"}`, 10299 children: "Pro Mode" 10300 } 10301 ) 10302 ] }) 10303 ] }),
10304 /* @__PURE__ */ jsxRuntimeExports.jsx(FlHintPanel, {}) 10305 ] }) 10306 ] }), 10307 engineError ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex flex-col items-center justify-center h-full w-full bg-zinc-50 dark:bg-zinc-900 text-zinc-900 dark:text-white p-6", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-red-50 dark:bg-red-500/10 border border-red-200 dark:border-red-500/30 rounded-xl p-8 max-w-lg w-full text-center", children: [ 10308 /* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "text-xl font-bold text-red-400 mb-4", children: "DAW Initialization Failed" }), 10309 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-zinc-300 text-sm mb-6", children: engineError }), 10310 /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-zinc-400 text-xs italic", children: [ 10311 "Note: For security reasons, the audio synthesis engine (AudioWorklet) requires a secure context. Please ensure you are accessing the site via ", 10312 /* @__P
10312URE__ */ jsxRuntimeExports.jsx("strong", { children: "HTTPS" }), 10313 " or ", 10314 /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { children: "localhost" }), 10315 "." 10316 ] }) 10317 ] }) }) : !isEngineReady ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center justify-center h-full w-full bg-zinc-50 dark:bg-zinc-900 text-zinc-900 dark:text-white gap-4", children: [ 10318 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-8 h-8 border-4 border-indigo-500 border-t-transparent rounded-full animate-spin" }), 10319 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-zinc-500 dark:text-zinc-400 font-medium", children: loadingStatus || "Loading DAW Engine..." }) 10320 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 flex overflow-hidden relative", children: [ 10321 inspectorState === "floating" && (isInspectorFloating ? /* @__PURE__ */ jsxRuntimeExports.jsxs( 10322 "div", 10323 { 10324 style: { left: inspectorPos.x, top: inspectorPos.y, width: `${inspectorSize.width}px`, height: `${inspectorSize.height}px` }, 10325 className: "fixed bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-lg shadow-2xl flex flex-col z-50 overflow-hidden", 10326 children: [
10327 /* @__PURE__ */ jsxRuntimeExports.jsxs( 10328 "div", 10329 { 10330 onMouseDown: (e) => handlePanelDragStart("inspector", e), 10331 className: "h-9 border-b border-zinc-200 dark:border-zinc-800 flex items-center justify-between px-3 bg-zinc-50 dark:bg-zinc-950 text-xs font-bold text-zinc-500 uppercase tracking-widest cursor-move select-none shrink-0", 10332 children: [ 10333 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-1.5", children: [ 10334 /* @__PURE__ */ jsxRuntimeExports.jsx(Move, { size: 12 }), 10335 " ", 10336 t("editor.inspector", "Inspector") 10337 ] }), 10338 /* @__PURE__ */ jsxRuntimeExports.jsx( 10339 "button", 10340 { 10341 onClick: () => setPanelState("inspector", "docked"), 10342 className: "text-zinc-400 hover:text-zinc-200 p-1 rounded hover:bg-zinc-800 transition-all", 10343 title: t("editor.dock", "Dock"), 10344 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Pin, { size: 14 }) 10345 } 10346 ),
10347 /* @__PURE__ */ jsxRuntimeExports.jsx( 10348 "button", 10349 { 10350 onClick: () => { 10351 setPanelState("inspector", "popped-out"); 10352 }, 10353 className: "text-zinc-400 hover:text-zinc-200 p-1 rounded hover:bg-zinc-800 transition-all", 10354 title: "Pop Out", 10355 children: /* @__PURE__ */ jsxRuntimeExports.jsx(MonitorUp, { size: 14 }) 10356 } 10357 ) 10358 ] 10359 } 10360 ), 10361 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 overflow-y-auto", children: /* @__PURE__ */ jsxRuntimeExports.jsx(TrackInspector, {}) }), 10362 /* @__PURE__ */ jsxRuntimeExports.jsx( 10363 "div", 10364 { 10365 onMouseDown: (e) => handlePanelResizeStart("inspector", e), 10366 className: "absolute bottom-0 right-0 w-4 h-4 cursor-se-resize flex items-end justify-end p-0.5 z-[100]", 10367 children: /* @__PURE__ */ jsxRuntimeExports.jsx("svg", { width: "8", height: "8", viewBox: "0 0 8 8", className: "text-zinc-400 dark:text-zinc-600 fill-current", children: /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M6 0 L8 0 L8 8 L0 8 L0 6 L4 6 L4 4 L6 4 Z" }) }) 10368 } 10369 ) 10370 ] 10371 } 10372 ) : null), 10373 inspectorState === "popped-out" && /* @__PURE__ */ jsxRuntimeExports.jsx(EditorWindowPortal, { panelId: "inspector", title: "HamieNET Editor - Inspector", onClose: () => setPanelState("inspector", "docked"), defaultWidth: 300, defaultHeight: 600, children: /* @__PURE__ */ jsxRuntimeExports.jsx(TrackInspector, {}) }), 10374 arrangementState === "popped-out" && /* @__PURE__ */ jsxRuntimeExports.jsx(EditorWindowPortal, { panelId: "arrangement", title: "HamieNET Editor - Arrangement", onClose: () => setPanelState("arrangement", "docked"), defaultWidth: 800, defaultHeight: 400, children: /* @__PURE__ */ jsxRuntimeExports.jsx(ArrangementView, { isFloating: true, isPoppedOut: true }) }), 10375 pianoRollState === "popped-out" && /* @__PURE__ */ jsxRuntimeExports.jsx(EditorWindowPortal, { panelId: "pianoRoll", title: "HamieNET Editor - Piano Roll", onClose: () => setPanelState("pianoRoll", "docked"), defaultWidth: 1e3, defaultHeight: 500, children: /* @__PURE__ */ jsxRuntimeExports.jsx(PianoRoll, {}) }), 10376 scoreEditorState === "popped-out" && /* @__PURE__ */ jsxRuntimeExports.jsx(EditorWindowPortal, { panelId: "scoreEditor", title: "HamieNET Editor - Score Editor", onClose: () => setPanelState("scoreEditor", "docked"), defaultWidth: 1e3, defaultHeight: 500, children: /* @__PURE__ */ jsxRuntimeExports.jsx(ScoreEditor, {}) }), 10377 mixerState === "popped-out" && /* @__PURE__ */ jsxRuntimeExports.jsx(EditorWindowPortal, { panelId: "mixer", title: "HamieNET Editor - Mixer", onClose: () => setPanelState("mixer", "docked"), defaultWidth: 800, defaultHeight: 400, children: /* @__PURE__ */ jsxRuntimeExports.jsx(Mixer, {}) }),
10378 !isFocusMode && isArrangementFloating ? /* @__PURE__ */ jsxRuntimeExports.jsxs( 10379 "div", 10380 { 10381 style: { left: arrangementPos.x, top: arrangementPos.y, width: `${arrangementSize.width}px`, height: `${arrangementSize.height}px` }, 10382 className: "fixed bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-lg shadow-2xl flex flex-col z-50 overflow-hidden", 10383 children: [ 10384 /* @__PURE__ */ jsxRuntimeExports.jsxs( 10385 "div", 10386 { 10387 onMouseDown: (e) => handlePanelDragStart("arrangement", e), 10388 className: "h-9 border-b border-zinc-200 dark:border-zinc-800 flex items-center justify-between px-3 bg-zinc-50 dark:bg-zinc-950 text-xs font-bold text-zinc-500 uppercase tracking-widest cursor-move select-none shrink-0", 10389 children: [ 10390 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-1.5", children: [ 10391 /* @__P
10391URE__ */ jsxRuntimeExports.jsx(Move, { size: 12 }), 10392 " ", 10393 t("editor.arrangement", "Arrangement") 10394 ] }), 10395 /* @__PURE__ */ jsxRuntimeExports.jsx( 10396 "button", 10397 { 10398 onClick: () => setPanelState("arrangement", "docked"), 10399 className: "text-zinc-400 hover:text-zinc-200 p-1 rounded hover:bg-zinc-800 transition-all", 10400 title: t("editor.dock", "Dock"), 10401 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Pin, { size: 14 }) 10402 } 10403 ), 10404 /* @__PURE__ */ jsxRuntimeExports.jsx( 10405 "button", 10406 { 10407 onClick: () => { 10408 setPanelState("arrangement", "popped-out"); 10409 }, 10410 className: "text-zinc-400 hover:text-zinc-200 p-1 rounded hover:bg-zinc-800 transition-all", 10411 title: "Pop Out", 10412 children: /* @__PURE__ */ jsxRuntimeExports.jsx(MonitorUp, { size: 14 }) 10413 } 10414 ) 10415 ] 10416 } 10417 ),
10418 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 overflow-hidden relative", children: /* @__PURE__ */ jsxRuntimeExports.jsx(ArrangementView, { isFloating: true }) }), 10419 /* @__PURE__ */ jsxRuntimeExports.jsx( 10420 "div", 10421 { 10422 onMouseDown: (e) => handlePanelResizeStart("arrangement", e), 10423 className: "absolute bottom-0 right-0 w-4 h-4 cursor-se-resize flex items-end justify-end p-0.5 z-[100]", 10424 children: /* @__PURE__ */ jsxRuntimeExports.jsx("svg", { width: "8", height: "8", viewBox: "0 0 8 8", className: "text-zinc-400 dark:text-zinc-600 fill-current", children: /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M6 0 L8 0 L8 8 L0 8 L0 6 L4 6 L4 4 L6 4 Z" }) }) 10425 } 10426 ) 10427 ] 10428 } 10429 ) : null, 10430 isRightPaneFloating ? /* @__PURE__ */ jsxRuntimeExports.jsxs( 10431 "div", 10432 { 10433 style: { left: bottomPanePos.x, top: bottomPanePos.y, width: `${bottomPaneSize.width}px`, height: `${bottomPaneSize.height}px` }, 10434 className: "fixed bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-lg shadow-2xl flex flex-col z-50 overflow-hidden", 10435 children: [
10436 /* @__PURE__ */ jsxRuntimeExports.jsxs( 10437 "div", 10438 { 10439 onMouseDown: (e) => handlePanelDragStart("bottom-pane", e), 10440 className: "h-9 border-b border-zinc-200 dark:border-zinc-800 flex justify-between items-center px-2 bg-zinc-50 dark:bg-zinc-950 shrink-0 cursor-move select-none", 10441 children: [ 10442 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-1 h-full items-center", children: [ 10443 pianoRollState !== "popped-out" && /* @__PURE__ */ jsxRuntimeExports.jsx( 10444 "button", 10445 { 10446 onClick: () => setBottomPane("piano-roll"), 10447 className: `px-3 py-1 text-[10px] sm:text-xs font-bold transition-colors border-t-2 ${bottomPane === "piano-roll" ? "border-indigo-500 bg-white dark:bg-zinc-900 text-indigo-600 dark:text-indigo-400" : "border-transparent text-zinc-500 hover:text-zinc-700"}`, 10448 children: t("editor.pianoRoll", "PIANO ROLL") 10449 } 10450 ),
10451 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-px h-4 bg-zinc-300 dark:bg-zinc-700 mx-1" }), 10452 scoreEditorState !== "popped-out" && /* @__PURE__ */ jsxRuntimeExports.jsx( 10453 "button", 10454 { 10455 onClick: () => setBottomPane("score-editor"), 10456 className: `px-3 py-1 text-[10px] sm:text-xs font-bold transition-colors border-t-2 ${bottomPane === "score-editor" ? "border-indigo-500 bg-white dark:bg-zinc-900 text-indigo-600 dark:text-indigo-400" : "border-transparent text-zinc-500 hover:text-zinc-700"}`, 10457 children: t("editor.scoreEditor", "SCORE EDITOR") 10458 } 10459 ), 10460 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-px h-4 bg-zinc-300 dark:bg-zinc-700 mx-1" }), 10461 mixerState !== "popped-out" && /* @__PURE__ */ jsxRuntimeExports.jsx( 10462 "button", 10463 { 10464 onClick: () => setBottomPane("mixer"), 10465 className: `px-3 py-1 text-[10px] sm:text-xs font-bold transition-colors border-t-2 ${bottomPane === "mixer" ? "border-indigo-500 bg-white dark:bg-zinc-900 text-indigo-600 dark:text-indigo-400" : "border-transparent text-zinc-500 hover:text-zinc-700"}`, 10466 children: t("editor.mixer", "MIXER") 10467 } 10468 ) 10469 ] }),
10470 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center gap-1.5 px-2", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 10471 "button", 10472 { 10473 onClick: () => { 10474 setPanelState("pianoRoll", "docked"); 10475 setPanelState("scoreEditor", "docked"); 10476 setPanelState("mixer", "docked"); 10477 }, 10478 className: "text-zinc-400 hover:text-zinc-200 p-1 rounded hover:bg-zinc-800 transition-all", 10479 title: t("editor.dock", "Dock"), 10480 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Pin, { size: 14 }) 10481 } 10482 ) }) 10483 ] 10484 } 10485 ), 10486 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 overflow-hidden flex flex-col relative bg-zinc-50 dark:bg-zinc-950", children: bottomPane === "piano-roll" ? /* @__PURE__ */ jsxRuntimeExports.jsx(PianoRoll, {}) : bottomPane === "score-editor" ? /* @__PURE__ */ jsxRuntimeExports.jsx(ScoreEditor, {}) : /* @__PURE__ */ jsxRuntimeExports.jsx(Mixer, {}) }),
10487 /* @__PURE__ */ jsxRuntimeExports.jsx( 10488 "div", 10489 { 10490 onMouseDown: (e) => handlePanelResizeStart("bottom-pane", e), 10491 className: "absolute bottom-0 right-0 w-4 h-4 cursor-se-resize flex items-end justify-end p-0.5 z-[100]", 10492 children: /* @__PURE__ */ jsxRuntimeExports.jsx("svg", { width: "8", height: "8", viewBox: "0 0 8 8", className: "text-zinc-400 dark:text-zinc-600 fill-current", children: /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M6 0 L8 0 L8 8 L0 8 L0 6 L4 6 L4 4 L6 4 Z" }) }) 10493 } 10494 ) 10495 ] 10496 } 10497 ) : null, 10498 /* @__PURE__ */ jsxRuntimeExports.jsxs(Ut, { direction: "horizontal", className: "relative", children: [ 10499 !inspectorState === "docked" && !isFocusMode && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute left-0 top-1/2 -translate-y-1/2 z-[45]", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 10500 "button", 10501 { 10502 onClick: () => setShowInspector(true), 10503 className: "w-4 h-12 bg-zinc-250 dark:bg-zinc-800 border border-l-0 border-zinc-300 dark:border-zinc-700 hover:bg-indigo-500 dark:hover:bg-indigo-600 hover:text-white rounded-r shadow flex items-center justify-center cursor-pointer transition-colors", 10504 title: "Expand Inspector", 10505 children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] font-bold select-none", children: ">" }) 10506 } 10507 ) }), 10508 inspectorState === "docked" && !isFocusMode && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
10509 /* @__PURE__ */ jsxRuntimeExports.jsxs(Yt, { defaultSize: "200px", minSize: "200px", onResize: () => { 10510 }, className: "bg-white dark:bg-zinc-900 flex flex-col z-[51] shadow-sm border-r border-zinc-200 dark:border-zinc-800", style: { overflow: "visible" }, children: [ 10511 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "h-8 border-b border-zinc-200 dark:border-zinc-800 flex items-center justify-between px-4 bg-zinc-50 dark:bg-zinc-950 text-xs font-bold text-zinc-500 uppercase tracking-widest shrink-0", children: [ 10512 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("editor.inspector", "Inspector") }), 10513 /* @__PURE__ */ jsxRuntimeExports.jsx( 10514 "button", 10515 { 10516 onClick: () => setPanelState("inspector", "floating"), 10517 className: "text-zinc-400 hover:text-zinc-200 p-1 rounded hover:bg-zinc-800 transition-all", 10518 title: t("editor.float", "Float"), 10519 children: /* @__PURE__ */ jsxRuntimeExports.jsx(ExternalLink, { size: 14 }) 10520 } 10521 ),
10522 /* @__PURE__ */ jsxRuntimeExports.jsx( 10523 "button", 10524 { 10525 onClick: () => { 10526 setPanelState("inspector", "popped-out"); 10527 }, 10528 className: "text-zinc-400 hover:text-zinc-200 p-1 rounded hover:bg-zinc-800 transition-all ml-1", 10529 title: "Pop Out", 10530 children: /* @__PURE__ */ jsxRuntimeExports.jsx(MonitorUp, { size: 14 }) 10531 } 10532 ) 10533 ] }), 10534 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 overflow-y-auto", children: /* @__PURE__ */ jsxRuntimeExports.jsx(TrackInspector, {}) }) 10535 ] }), 10536 (arrangementState === "docked" || hasDockedRightPane) && /* @__PURE__ */ jsxRuntimeExports.jsx(Qt, { className: "w-1.5 bg-zinc-200 dark:bg-zinc-800 hover:bg-indigo-500/50 active:bg-indigo-500 cursor-col-resize transition-colors shrink-0 z-20 flex items-center justify-center relative group", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 10537 "button", 10538 { 10539 onClick: (e) => { 10540 e.stopPropagation(); 10541 setShowInspector(false); 10542 }, 10543 className: "absolute w-4 h-8 bg-zinc-300 dark:bg-zinc-700 hover:bg-indigo-500 dark:hover:bg-indigo-600 hover:text-white text-zinc-650 dark:text-zinc-300 rounded-r shadow flex items-center justify-center cursor-pointer left-0 z-50 border border-l-0 border-zinc-200 dark:border-zinc-600 opacity-0 group-hover:opacity-100 transition-opacity", 10544 title: "Collapse Inspector", 10545 children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] font-bold select-none", children: "<" }) 10546 } 10547 ) }) 10548 ] }), 10549 !isFocusMode && arrangementState === "docked" && /* @__PURE__ */ jsxRuntimeExports.jsxs(Yt, { defaultSize: "40%", minSize: "150px", onResize: () => { 10550 }, className: "bg-zinc-50 dark:bg-zinc-950 flex flex-col relative overflow-hidden", children: [
10551 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute right-3 top-2 z-[45] flex items-center", children: [ 10552 /* @__PURE__ */ jsxRuntimeExports.jsx( 10553 "button", 10554 { 10555 onClick: () => setPanelState("arrangement", "floating"), 10556 className: "text-zinc-400 hover:text-zinc-200 p-1 rounded hover:bg-zinc-800/80 backdrop-blur-sm transition-all shadow", 10557 title: t("editor.floatArrangement", "Float Arrangement"), 10558 children: /* @__PURE__ */ jsxRuntimeExports.jsx(ExternalLink, { size: 14 }) 10559 } 10560 ), 10561 /* @__PURE__ */ jsxRuntimeExports.jsx( 10562 "button", 10563 { 10564 onClick: () => { 10565 setPanelState("arrangement", "popped-out"); 10566 }, 10567 className: "text-zinc-400 hover:text-zinc-200 p-1 rounded hover:bg-zinc-800/80 backdrop-blur-sm transition-all shadow ml-1", 10568 title: "Pop Out", 10569 children: /* @__PURE__ */ jsxRuntimeExports.jsx(MonitorUp, { size: 14 }) 10570 } 10571 ) 10572 ] }),
10573 /* @__PURE__ */ jsxRuntimeExports.jsx(ArrangementView, { isFloating: false }) 10574 ] }), 10575 hasDockedRightPane && !isFocusMode && !isRightPaneFloating && (inspectorState === "docked" && !isInspectorFloating || arrangementState === "docked") && /* @__PURE__ */ jsxRuntimeExports.jsx(Qt, { className: "w-1.5 bg-zinc-200 dark:bg-zinc-800 hover:bg-indigo-500/50 active:bg-indigo-500 cursor-col-resize transition-colors shrink-0 z-30 border-l border-zinc-200 dark:border-zinc-850 flex items-center justify-center relative group", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 10576 "button", 10577 { 10578 onClick: (e) => { 10579 e.stopPropagation(); 10580 setShowArrangement(false); 10581 }, 10582 className: "absolute w-4 h-8 bg-zinc-300 dark:bg-zinc-700 hover:bg-indigo-500 dark:hover:bg-indigo-600 hover:text-white text-zinc-650 dark:text-zinc-300 rounded-r shadow flex items-center justify-center cursor-pointer left-0 z-50 border border-l-0 border-zinc-200 dark:border-zinc-600 opacity-0 group-hover:opacity-100 transition-opacity", 10583 title: "Collapse Arrangement", 10584 children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] font-bold select-none", children: "<" }) 10585 } 10586 ) }), 10587 (hasDockedRightPane || isFocusMode) && !isRightPaneFloating && /* @__PURE__ */ jsxRuntimeExports.jsxs(Yt, { defaultSize: isFocusMode ? "100%" : "45%", minSize: isFocusMode ? "100%" : "350px", onResize: () => { 10588 }, className: "bg-white dark:bg-zinc-900 flex flex-col relative z-[60] shadow-[-5px_0_15px_rgba(0,0,0,0.05)] dark:shadow-[-5px_0_25px_rgba(0,0,0,0.5)]", style: { overflow: "visible" }, children: [ 10589 arrangementState === "hidden" && !isFocusMode && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute left-0 top-1/2 -translate-y-1/2 z-[45]", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 10590 "button", 10591 { 10592 onClick: () =>
10592 setShowArrangement(true), 10593 className: "w-4 h-12 bg-zinc-250 dark:bg-zinc-800 border border-l-0 border-zinc-300 dark:border-zinc-700 hover:bg-indigo-500 dark:hover:bg-indigo-600 hover:text-white rounded-r shadow flex items-center justify-center cursor-pointer transition-colors", 10594 title: "Expand Arrangement", 10595 children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] font-bold select-none", children: ">" }) 10596 } 10597 ) }), 10598 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "h-8 border-b border-zinc-200 dark:border-zinc-800 flex justify-between items-center px-2 bg-zinc-50 dark:bg-zinc-950 shrink-0", children: [ 10599 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-1 h-full items-center", children: [ 10600 dockedRightTabs.includes("piano-roll") && /* @__PURE__ */ jsxRuntimeExports.jsx(HelpTip, { text: t("editor.pianoRollHelp", "Detailed view of MIDI notes and velocities."), position: "bottom", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 10601 "button", 10602 { 10603 onClick: () => setBottomPane("piano-roll"), 10604 className: `px-4 py-1 text-xs font-bold transition-colors border-t-2 ${bottomPane === "piano-roll" ? "border-indigo-500 bg-white dark:bg-zinc-900 text-indigo-600 dark:text-indigo-400" : "border-transparent text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300 hover:bg-zinc-200 dark:hover:bg-zinc-800"}`, 10605 children: t("editor.pianoRollTab", "PIANO ROLL") 10606 } 10607 ) }), 10608 dockedRightTabs.includes("piano-roll") && dockedRightTabs.includes("score-editor") && /* @__P
10608URE__ */ jsxRuntimeExports.jsx("div", { className: "w-px h-4 bg-zinc-300 dark:bg-zinc-700 mx-1" }), 10609 dockedRightTabs.includes("score-editor") && /* @__PURE__ */ jsxRuntimeExports.jsx(HelpTip, { text: t("editor.scoreEditorHelp", "Traditional sheet music view and tools."), position: "bottom", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 10610 "button", 10611 { 10612 onClick: () => setBottomPane("score-editor"), 10613 className: `px-4 py-1 text-xs font-bold transition-colors border-t-2 ${bottomPane === "score-editor" ? "border-indigo-500 bg-white dark:bg-zinc-900 text-indigo-600 dark:text-indigo-400" : "border-transparent text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300 hover:bg-zinc-200 dark:hover:bg-zinc-800"}`, 10614 children: t("editor.scoreEditorTab", "SCORE EDITOR") 10615 } 10616 ) }), 10617 (dockedRightTabs.includes("piano-roll") || dockedRightTabs.includes("score-editor")) && dockedRightTabs.includes("mixer") && /* @__P
10617URE__ */ jsxRuntimeExports.jsx("div", { className: "w-px h-4 bg-zinc-300 dark:bg-zinc-700 mx-1" }), 10618 dockedRightTabs.includes("mixer") && /* @__PURE__ */ jsxRuntimeExports.jsx(HelpTip, { text: t("editor.mixerHelp", "Adjust track volumes, panning, and effects."), position: "bottom", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 10619 "button", 10620 { 10621 onClick: () => setBottomPane("mixer"), 10622 className: `px-4 py-1 text-xs font-bold transition-colors border-t-2 ${bottomPane === "mixer" ? "border-indigo-500 bg-white dark:bg-zinc-900 text-indigo-600 dark:text-indigo-400" : "border-transparent text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300 hover:bg-zinc-200 dark:hover:bg-zinc-800"}`, 10623 children: t("editor.mixerTab", "MIXER") 10624 } 10625 ) }) 10626 ] }), 10627 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1 px-2", children: [ 10628 /* @__P
10628URE__ */ jsxRuntimeExports.jsx( 10629 "button", 10630 { 10631 onClick: () => setPanelState(bottomPane === "piano-roll" ? "pianoRoll" : bottomPane === "score-editor" ? "scoreEditor" : "mixer", "floating"), 10632 className: "text-zinc-400 hover:text-zinc-200 p-1 rounded hover:bg-zinc-800 transition-all", 10633 title: t("editor.float", "Float"), 10634 children: /* @__PURE__ */ jsxRuntimeExports.jsx(ExternalLink, { size: 14 }) 10635 } 10636 ), 10637 /* @__PURE__ */ jsxRuntimeExports.jsx( 10638 "button", 10639 { 10640 onClick: () => { 10641 const pid = bottomPane === "piano-roll" ? "pianoRoll" : bottomPane === "score-editor" ? "scoreEditor" : "mixer"; 10642 setPanelState(pid, "popped-out"); 10643 }, 10644 className: "text-zinc-400 hover:text-zinc-200 p-1 rounded hover:bg-zinc-800 transition-all", 10645 title: "Pop Out", 10646 children: /* @__PURE__ */ jsxRuntimeExports.jsx(MonitorUp, { size: 14 }) 10647 } 10648 ),
10649 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-px h-4 bg-zinc-300 dark:bg-zinc-700 mx-1" }), 10650 /* @__PURE__ */ jsxRuntimeExports.jsx( 10651 "button", 10652 { 10653 onClick: () => setIsFocusMode(!isFocusMode), 10654 title: isFocusMode ? "Exit Focus Mode" : "Focus Mode", 10655 className: `p-1.5 rounded transition-colors ${isFocusMode ? "bg-indigo-100 text-indigo-600 dark:bg-indigo-500/20 dark:text-indigo-400" : "text-zinc-500 hover:bg-zinc-200 dark:hover:bg-zinc-800"}`, 10656 children: isFocusMode ? /* @__PURE__ */ jsxRuntimeExports.jsx(Minimize2, { size: 16 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Maximize2, { size: 16 }) 10657 } 10658 ) 10659 ] }) 10660 ] }), 10661 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 overflow-hidden flex flex-col relative bg-zinc-50 dark:bg-zinc-950", children: bottomPane === "piano-roll" ? /* @__PURE__ */ jsxRuntimeExports.jsx(PianoRoll, {}) : bottomPane === "score-editor" ? /* @__PURE__ */ jsxRuntimeExports.jsx(ScoreEditor, {}) : /* @__PURE__ */ jsxRuntimeExports.jsx(Mixer, {}) }) 10662 ] }) 10663 ] }) 10664 ] }), 10665 isProjectModalOpen && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "fixed inset-0 z-[100] flex items-center justify-center p-4 text-zinc-950 dark:text-white", children: [
10666 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 bg-black/85 backdrop-blur-sm", onClick: () => setIsProjectModalOpen(false) }), 10667 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative w-full max-w-4xl bg-white dark:bg-zinc-900 rounded-xl shadow-2xl p-6 border border-zinc-200 dark:border-zinc-800 animate-in zoom-in-95 max-h-[90vh] overflow-y-auto custom-scrollbar", children: [ 10668 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between items-center mb-6", children: [ 10669 /* @__PURE__ */ jsxRuntimeExports.jsxs("h2", { className: "text-xl font-bold text-zinc-900 dark:text-white flex items-center gap-2", children: [ 10670 /* @__PURE__ */ jsxRuntimeExports.jsx(FolderOpen, { className: "text-indigo-500" }), 10671 "My Saved Projects" 10672 ] }), 10673 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => setIsProjectModalOpen(false), children: /* @__PURE__ */ jsxRuntimeExports.jsx(X, { size: 20, className: "text-zinc-500 hover:text-zinc-900 dark:hover:text-white" }) }) 10674 ] }),
10675 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "py-2", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 10676 ProjectList, 10677 { 10678 onSelectProject: (project) => { 10679 setIsProjectModalOpen(false); 10680 if (onNavigate) { 10681 onNavigate("editor", project.id); 10682 } 10683 }, 10684 layout: "grid" 10685 } 10686 ) }) 10687 ] }) 10688 ] }), 10689 isSettingsOpen && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "fixed inset-0 z-[100] flex items-center justify-center p-4 text-zinc-950 dark:text-white", children: [ 10690 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 bg-black/85 backdrop-blur-sm", onClick: () => setIsSettingsOpen(false) }), 10691 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative w-full max-w-lg bg-white dark:bg-zinc-900 rounded-xl shadow-2xl p-6 border border-zinc-200 dark:border-zinc-800 animate-in zoom-in-95 max-h-[90vh] overflow-y-auto custom-scrollbar", children: [
10692 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between items-center mb-6", children: [ 10693 /* @__PURE__ */ jsxRuntimeExports.jsxs("h2", { className: "text-xl font-bold text-zinc-900 dark:text-white flex items-center gap-2", children: [ 10694 /* @__PURE__ */ jsxRuntimeExports.jsx(Settings, { className: "text-indigo-500 animate-spin-slow" }), 10695 "Editor Settings" 10696 ] }), 10697 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => setIsSettingsOpen(false), children: /* @__PURE__ */ jsxRuntimeExports.jsx(X, { size: 20, className: "text-zinc-500 hover:text-zinc-900 dark:hover:text-white" }) }) 10698 ] }), 10699 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex border-b border-zinc-200 dark:border-zinc-800 mb-6", children: [ 10700 /* @__PURE__ */ jsxRuntimeExports.jsx( 10701 "button", 10702 { 10703 onClick: () => setActiveSettingsTab("general"), 10704 className: `px-4 py-2 font-bold text-sm border-b-2 transition-colors ${activeSettingsTab === "general" ? "border-indigo-500 text-indigo-500" : "border-transparent text-zinc-500 hover:text-zinc-800 dark:hover:text-white"}`, 10705 children: "General" 10706 } 10707 ),
10708 /* @__PURE__ */ jsxRuntimeExports.jsx( 10709 "button", 10710 { 10711 onClick: () => setActiveSettingsTab("midi"), 10712 className: `px-4 py-2 font-bold text-sm border-b-2 transition-colors ${activeSettingsTab === "midi" ? "border-indigo-500 text-indigo-500" : "border-transparent text-zinc-500 hover:text-zinc-800 dark:hover:text-white"}`, 10713 children: "MIDI Setup" 10714 } 10715 ) 10716 ] }), 10717 activeSettingsTab === "general" && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "space-y-4", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between p-3 bg-zinc-50 dark:bg-zinc-950 rounded-lg", children: [ 10718 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 10719 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-bold text-sm", children: "Easy Mode" }), 10720 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs text-zinc-500", children: "Simplify the DAW interface for beginner users." }) 10721 ] }),
10722 /* @__PURE__ */ jsxRuntimeExports.jsx( 10723 "button", 10724 { 10725 onClick: () => setEasyMode(!easyMode), 10726 className: `px-3 py-1 text-xs font-bold rounded-lg transition-colors ${easyMode ? "bg-indigo-600 text-white" : "bg-zinc-200 dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300"}`, 10727 children: easyMode ? "Enabled" : "Disabled" 10728 } 10729 ) 10730 ] }) }), 10731 activeSettingsTab === "midi" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4", children: [ 10732 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between p-3 bg-zinc-50 dark:bg-zinc-950 rounded-lg", children: [ 10733 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 10734 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-bold text-sm", children: "Web MIDI Support" }), 10735 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs text-zinc-500", children: "Required browser API for hardware MIDI input." }) 10736 ] }),
10737 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `px-2 py-0.5 text-xs font-bold rounded ${isMidiSupported ? "bg-emerald-500/20 text-emerald-500" : "bg-rose-500/20 text-rose-500"}`, children: isMidiSupported ? "Supported" : "Unsupported" }) 10738 ] }), 10739 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between p-3 bg-zinc-50 dark:bg-zinc-950 rounded-lg", children: [ 10740 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 10741 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-bold text-sm", children: "MIDI Keyboard Connection" }), 10742 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs text-zinc-500", children: "Status of connected external hardware MIDI controllers." }) 10743 ] }), 10744 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `px-2 py-0.5 text-xs font-bold rounded ${isMidiConnected ? "bg-emerald-500/20 text-emerald-500" : "bg-zinc-500/20 text-zinc-500"}`, children: isMidiConnected ? "Connected" : "Disconnected" }) 10745 ] }), 10746 midiError && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "p-3 bg-rose-500/10 border border-rose-500/20 rounded-lg text-xs text-rose-500", children: midiError }),
10747 /* @__PURE__ */ jsxRuntimeExports.jsx( 10748 "button", 10749 { 10750 onClick: async () => { 10751 const success = await connectMidi(); 10752 if (success) { 10753 alert("MIDI devices successfully scanned and connected!"); 10754 } else { 10755 alert("Failed to connect MIDI devices. Check permissions or browser support."); 10756 } 10757 }, 10758 className: "w-full py-2 bg-indigo-600 hover:bg-indigo-500 text-white rounded-lg font-bold text-sm transition-colors shadow-lg", 10759 children: "Scan for MIDI Device" 10760 } 10761 ) 10762 ] }) 10763 ] }) 10764 ] }) 10765 ] }), 10766 easyToProMergeState && /* @__PURE__ */ jsxRuntimeExports.jsx( 10767 EasyToProMergeModal, 10768 { 10769 mergeState: easyToProMergeState, 10770 onClose: () => setEasyToProMergeState(null) 10771 } 10772 ) 10773 ] }); 10774}; 10775const EditorPage = ({ currentUser, onAuthRequested, id, onNavigate }) => { 10776 return /* @__PURE__ */ jsxRuntimeExports.jsx(EditorProvider, { projectId: id, children: /* @__PURE__ */ jsxRuntimeExports.jsx( 10777 EditorLayout, 10778 { 10779 currentUser, 10780 onAuthRequested, 10781 projectId: id, 10782 onNavigate 10783 } 10784 ) }); 10785}; 10786export { 10787 EditorPage as default 10788};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.