1import { r as reactExports, j as jsxRuntimeExports } from "./vendor-react-Bay9Y7Lb.js"; 2import { gm as ScrollMode, gn as PlayerMode, go as StaveProfile, gp as LayoutMode, gq as LogLevel, gr as AlphaTabApi, gs as _barrel_exports$7, db as Music2, gt as ListFilter, cA as SlidersHorizontal, aW as X, ck as Layers, bA as Eye, ds as ZoomOut, dt as ZoomIn, b6 as LoaderCircle, b1 as CircleAlert } from "./vendor-D0P4XgNj.js"; 3import "./vendor-i18n-BC7ZMXWQ.js"; 4const TabVisualizer = ({ 5 track, 6 currentTime, 7 duration, 8 isPlaying, 9 onSeek, 10 theme = "dark", 11 hiddenTracks, 12 trackVisibility, 13 processedInstruments, 14 uniqueNames 15}) => { 16 const containerRef = reactExports.useRef(null); 17 const viewportRef = reactExports.useRef(null); 18 const tracksMenuRef = reactExports.useRef(null); 19 const alphaTabRef = reactExports.useRef(null); 20 const isPlayerReadyRef = reactExports.useRef(false); 21 const defaultTop6IndicesRef = reactExports.useRef([]); 22 const currentTimeRef = reactExports.useRef(currentTime); 23 currentTimeRef.current = currentTime; 24 const selectedTrackIndicesRef = reactExports.useRef(/* @__PURE__ */ new Set([0])); 25 const [isLoading, setIsLoading] = reactExports.useState(true); 26 const [error, setError] = reactExports.useState(null); 27 const [tracksList, setTracksList] = reactExports.useState([]); 28 const [selectedTrackIndices, setSelectedTrackIndices] = reactExports.useState(/* @__PURE__ */ new Set([0])); 29 selectedTrackIndicesRef.current = selectedTrackIndices; 30 const [isTracksMenuOpen, setIsTracksMenuOpen] = reactExports.useState(false); 31 const [staveProfile, setStaveProfile] = reactExports.useState("ScoreTab"); 32 const [layoutMode, setLayoutMode] = reactExports.useState("Page"); 33 const [zoom, setZoom] = reactExports.useState(1); 34 const [isAutoScale, setIsAutoScale] = reactExports.useState(true); 35 const [followPlayback, setFollowPlayback] = reactExports.useState(true); 36 const isDarkMode = theme === "dark" || theme === "fun"; 37 const calculateOptimalScale = reactExports.useCallback((numTracks, vHeight, profile) => { 38 if (numTracks <= 0) return 1; 39 const trackHeight = profile === "ScoreTab" ? 160 : profile === "Tab" ? 95 : 85; 40 const overhead = 45; 41 const totalNeeded = overhead + numTracks * trackHeight; 42 const available = Math.max(260, vHeight - 20); 43 const rawScale = available / totalNeeded; 44 return Math.max(0.4, Math.min(1, Math.round(rawScale * 100) / 100)); 45 }, []); 46 reactExports.useEffect(() => { 47 if (!containerRef.current) return; 48 let api = null; 49 try { 50 setIsLoading(true); 51 setError(null); 52 const settings = { 53 core: { 54 engine: "html5", 55 fontDirectory: "/fonts/", 56 useWorkers: false, 57 logLevel: LogLevel.Warning 58 }, 59 display: { 60 scale: zoom, 61 layoutMode: layoutMode === "Page" ? LayoutMode.Page : LayoutMode.Horizontal, 62 staveProfile: staveProfile === "Tab" ? StaveProfile.Tab : staveProfile === "Score" ? StaveProfile.Score : StaveProfile.ScoreTab, 63 resources: { 64 staffLineColor: isDarkMode ? "#475569" : "#cbd5e1", 65 mainGlyphColor: isDarkMode ? "#f8fafc" : "#0f172a", 66 secondaryGlyphColor: isDarkMode ? "#94a3b8" : "#64748b", 67 scoreInfoColor: isDarkMode ? "#e2e8f0" : "#1e293b", 68 barSeparatorColor: isDarkMode ? "#475569" : "#cbd5e1", 69 barNumberColor: isDarkMode ? "#38bdf8" : "#0284c7" 70 } 71 }, 72 player: { 73 playerMode: PlayerMode.EnabledExternalMedia, 74 enableCursor: true, 75 enableAnimatedBeatCursor: true, 76 enableElementHighlighting: true, 77 scrollMode: followPlayback ? ScrollMode.Continuous : ScrollMode.Off, 78 scrollElement: viewportRef.current || void 0, 79 scrollSpeed: 300, 80 scrollOffsetY: -70, 81 scrollOffsetX: -30 82 } 83 }; 84 api = new AlphaTabApi(containerRef.current, settings); 85 alphaTabRef.current = api; 86 api.error.on((err) => { 87 console.error("[TabVisualizer] AlphaTab runtime error:", err); 88 setError((err == null ? void 0 : err.message) || "Failed to render tablature"); 89 setIsLoading(false); 90 }); 91 api.scoreLoaded.on((score) => { 92 var _a; 93 const list = score.tracks.map((t, idx) => ({
94 index: idx, 95 name: t.name ? t.name.trim() : `Track ${idx + 1}` 96 })); 97 setTracksList(list); 98 let initialIndices = []; 99 if (score.tracks.length <= 6) { 100 initialIndices = score.tracks.map((_, idx) => idx); 101 } else { 102 const scoredTracks = score.tracks.map((t, idx) => { 103 const name = (t.name || "").toLowerCase(); 104 let noteCount = 0; 105 let firstNoteBar = 999999; 106 if (t.staves) { 107 for (const staff of t.staves) { 108 if (!staff.bars) continue; 109 for (let b = 0; b < staff.bars.length; b++) { 110 const bar = staff.bars[b]; 111 if (!bar || !bar.voices) continue; 112 for (const voice of bar.voices) { 113 if (!voice || !voice.beats) continue; 114 for (const beat of voice.beats) { 115 if (beat.notes && beat.notes.length > 0) { 116 noteCount += beat.notes.length; 117 if (b < firstNoteBar) firstNoteBar = b; 118 } 119 } 120 } 121 } 122 } 123 } 124 let s = noteCount; 125 if (/guitar|gtr|clean|distort|overdrive|lead|rhythm|acoustic|electric/i.test(name)) { 126 s += 1e4; 127 } else if (/bass/i.test(name)) { 128 s += 6e3; 129 } else if (t.isPercussion || /drum|percussion/i.test(name)) { 130 s += 4e3; 131 } else if (/keyboard|piano|organ|synth/i.test(name)) { 132 s += 3e3; 133 } else if (/voice|vocal|lyric/i.test(name)) { 134 s += 1e3; 135 } 136 if (firstNoteBar <= 4) { 137 s += 2e3; 138 } else if (firstNoteBar > 30) { 139 s -= 3e3; 140 } 141 return { index: idx, score: s }; 142 }); 143 scoredTracks.sort((a, b) => b.score - a.score); 144 const top6 = scoredTracks.slice(0, 6).map((x) => x.index); 145 top6.sort((a, b) => a - b); 146 initialIndices = top6; 147 } 148 defaultTop6IndicesRef.current = initialIndices; 149 const initialSet = new Set(initialIndices); 150 setSelectedTrackIndices(initialSet); 151 const vHeight = ((_a = viewportRef.current) == null ? void 0 : _a.clientHeight) || 480; 152 const autoScale = calculateOptimalScale(initialIndices.length, vHeight, staveProfile); 153 setZoom(autoScale); 154 api.settings.display.scale = autoScale; 155 api.updateSettings(); 156 const tracksToRender = initialIndices.map((idx) => score.tracks[idx]).filter((t) => Boolean(t)); 157 if (tracksToRender.length > 0) { 158 api.renderTracks(tracksToRender); 159 } 160 setIsLoading(false); 161 }); 162 api.playerReady.on(() => { 163 isPlayerReadyRef.current = true; 164 if (isPlaying) { 165 try { 166 api.play(); 167 } catch (_) { 168 } 169 } 170 const curTime = currentTimeRef.current; 171 if (curTime > 0) { 172 try { 173 api.timePosition = Math.max(0, curTime * 1e3); 174 if (followPlayback) { 175 api.scrollToCursor(); 176 } 177 } catch (_) { 178 } 179 } 180 }); 181 api.renderFinished.on(() => { 182 setIsLoading(false); 183 if (api.isReadyForPlayback) { 184 isPlayerReadyRef.current = true; 185 } 186 }); 187 api.beatMouseDown.on((beat) => { 188 if (beat && typeof beat.playbackStart === "number") { 189 const targetSec = beat.playbackStart / 1e3; 190 if (targetSec >= 0 && targetSec <= duration) { 191 onSeek(targetSec); 192 } 193 } 194 }); 195 const tabUrl = `/api/tracks/${track.id}/tab`; 196 api.load(tabUrl); 197 } catch (err) { 198 console.error("[TabVisualizer] Initialization error:", err); 199 const msg = err instanceof Error ? err.message : "Failed to initialize tablature visualizer"; 200 setError(msg); 201 setIsLoading(false); 202 } 203 return () => { 204 isPlayerReadyRef.current = false;
205 if (api) { 206 try { 207 api.destroy(); 208 } catch (_) { 209 } 210 } 211 alphaTabRef.current = null; 212 }; 213 }, [track.id, isDarkMode]); 214 reactExports.useEffect(() => { 215 const api = alphaTabRef.current; 216 if (!api) return; 217 api.settings.display.scale = zoom; 218 api.settings.display.layoutMode = layoutMode === "Page" ? LayoutMode.Page : LayoutMode.Horizontal; 219 api.settings.display.staveProfile = staveProfile === "Tab" ? StaveProfile.Tab : staveProfile === "Score" ? StaveProfile.Score : StaveProfile.ScoreTab; 220 api.updateSettings(); 221 api.render(); 222 }, [zoom, layoutMode, staveProfile]); 223 reactExports.useEffect(() => { 224 const handleClickOutside = (e) => { 225 if (tracksMenuRef.current && !tracksMenuRef.current.contains(e.target)) { 226 setIsTracksMenuOpen(false); 227 } 228 }; 229 if (isTracksMenuOpen) { 230 document.addEventListener("mousedown", handleClickOutside); 231 } 232 return () => { 233 document.removeEventListener("mousedown", handleClickOutside); 234 }; 235 }, [isTracksMenuOpen]); 236 const trackToMixerMap = reactExports.useMemo(() => { 237 const map = /* @__PURE__ */ new Map(); 238 if (!tracksList.length || !(processedInstruments == null ? void 0 : processedInstruments.length) || !(uniqueNames == null ? void 0 : uniqueNames.length)) { 239 return map; 240 } 241 const usedUniqueNames = /* @__PURE__ */ new Set(); 242 tracksList.forEach((t) => { 243 const nameLower = t.name.toLowerCase(); 244 if (/drum|percussion/i.test(nameLower)) { 245 const percIdx = processedInstruments.findIndex( 246 (inst, i) => !usedUniqueNames.has(uniqueNames[i]) && (inst.midiChannel === 9 || /drum|percussion/i.test(inst.displayName || inst.name || "")) 247 ); 248 if (percIdx !== -1) { 249 map.set(t.index, uniqueNames[percIdx]); 250 usedUniqueNames.add(uniqueNames[percIdx]); 251 return; 252 } 253 } 254 if (/bass/i.test(nameLower)) { 255 const bassIdx = processedInstruments.findIndex( 256 (inst, i) => !usedUniqueNames.has(uniqueNames[i]) && /bass/i.test(inst.displayName || inst.name || "") 257 ); 258 if (bassIdx !== -1) { 259 map.set(t.index, uniqueNames[bassIdx]); 260 usedUniqueNames.add(uniqueNames[bassIdx]); 261 return; 262 } 263 } 264 const romanMatch = nameLower.match(/guitar\s*([ivx]+|\d+)/i); 265 if (romanMatch) { 266 const rawNum = romanMatch[1]; 267 let num = 1; 268 if (/^\d+$/.test(rawNum)) { 269 num = parseInt(rawNum, 10); 270 } else { 271 const romanMap = { i: 1, ii: 2, iii: 3, iv: 4, v: 5, vi: 6 }; 272 num = romanMap[rawNum.toLowerCase()] || 1; 273 } 274 const guitars = processedInstruments.map((inst, i) => ({ inst, uName: uniqueNames[i] })).filter( 275 ({ inst, uName }) => !usedUniqueNames.has(uName) && /guitar|gtr|overdrive|distortion|clean|acoustic/i.test(inst.displayName || inst.name || "") 276 ); 277 if (guitars.length >= num) { 278 map.set(t.index, guitars[num - 1].uName); 279 usedUniqueNames.add(guitars[num - 1].uName); 280 return; 281 } 282 } 283 const exactIdx = processedInstruments.findIndex((inst, i) => { 284 if (usedUniqueNames.has(uniqueNames[i])) return false; 285 const dName = (inst.displayName || inst.name || "").toLowerCase(); 286 return dName === nameLower || dName.includes(nameLower) || nameLower.includes(dName); 287 }); 288 if (exactIdx !== -1) { 289 map.set(t.index, uniqueNames[exactIdx]); 290 usedUniqueNames.add(uniqueNames[exactIdx]); 291 return; 292 } 293 const musicalInsts = processedInstruments.map((inst, i) => ({ inst, uName: uniqueNames[i] })).filter(({ inst, uName }) => !usedUniqueNames.has(uName) && inst.midiChannel !== -1); 294 if (t.index < musicalInsts.length) { 295 map.set(t.index, musicalInsts[t.index].uName); 296 usedUniqueNames.add(musicalInsts[t.index].uName); 297 } 298 }); 299 return map; 300 }, [tracksList, processedInstruments, uniqueNames]); 301 const isTrackHiddenByMixer = reactExports.useCallback((atTrackIdx) => { 302 if (!hiddenTracks || hiddenTracks.size === 0) return false;
303 const uName = trackToMixerMap.get(atTrackIdx); 304 return uName ? hiddenTracks.has(uName) : false; 305 }, [hiddenTracks, trackToMixerMap]); 306 const renderActiveTracks = reactExports.useCallback((indices) => { 307 const api = alphaTabRef.current; 308 if (!api || !api.score) return; 309 const finalIndices = Array.from(indices).filter((idx) => !isTrackHiddenByMixer(idx)); 310 const tracksToRender = finalIndices.map((idx) => { 311 var _a; 312 return (_a = api.score) == null ? void 0 : _a.tracks[idx]; 313 }).filter((t) => Boolean(t)); 314 if (tracksToRender.length > 0) { 315 if (isAutoScale && viewportRef.current) { 316 const vHeight = viewportRef.current.clientHeight || 480; 317 const optimal = calculateOptimalScale(tracksToRender.length, vHeight, staveProfile); 318 api.settings.display.scale = optimal; 319 api.updateSettings(); 320 setZoom((prev) => Math.abs(prev - optimal) >= 0.02 ? optimal : prev); 321 } 322 api.renderTracks(tracksToRender); 323 const curTime = currentTimeRef.current; 324 if (curTime > 0 && isPlayerReadyRef.current) { 325 setTimeout(() => { 326 try { 327 api.timePosition = Math.max(0, curTime * 1e3); 328 if (followPlayback) { 329 api.scrollToCursor(); 330 } 331 } catch (_) { 332 } 333 }, 50); 334 } 335 } 336 }, [isTrackHiddenByMixer, followPlayback, isAutoScale, calculateOptimalScale, staveProfile]); 337 reactExports.useEffect(() => { 338 const viewport = viewportRef.current; 339 if (!viewport) return; 340 const observer = new ResizeObserver((entries) => { 341 for (const entry of entries) { 342 const height = entry.contentRect.height; 343 if (height > 0 && isAutoScale) { 344 const visibleCount = Array.from(selectedTrackIndices).filter((idx) => !isTrackHiddenByMixer(idx)).length; 345 const optimal = calculateOptimalScale(visibleCount, height, staveProfile); 346 setZoom((prev) => { 347 if (Math.abs(prev - optimal) >= 0.04) { 348 return optimal; 349 } 350 return prev; 351 }); 352 } 353 } 354 }); 355 observer.observe(viewport); 356 return () => observer.disconnect(); 357 }, [isAutoScale, selectedTrackIndices, isTrackHiddenByMixer, calculateOptimalScale, staveProfile]); 358 reactExports.useEffect(() => { 359 if (!alphaTabRef.current || !alphaTabRef.current.score || tracksList.length === 0) return; 360 renderActiveTracks(selectedTrackIndicesRef.current); 361 }, [hiddenTracks, renderActiveTracks]); 362 const isTop6Selected = reactExports.useMemo(() => { 363 if (tracksList.length <= 6) return false; 364 const top6 = defaultTop6IndicesRef.current; 365 if (!top6 || top6.length === 0 || selectedTrackIndices.size !== top6.length) return false; 366 return top6.every((idx) => selectedTrackIndices.has(idx)); 367 }, [tracksList.length, selectedTrackIndices]); 368 const selectTop6Tracks = reactExports.useCallback(() => { 369 let top6 = defaultTop6IndicesRef.current; 370 if (!top6 || top6.length === 0) { 371 top6 = tracksList.slice(0, 6).map((t) => t.index); 372 } 373 const s = new Set(top6); 374 setSelectedTrackIndices(s); 375 renderActiveTracks(s); 376 }, [tracksList, renderActiveTracks]); 377 const handleStaveProfileChange = (newProfile) => { 378 setStaveProfile(newProfile); 379 if (isAutoScale && viewportRef.current) { 380 const vHeight = viewportRef.current.clientHeight || 480; 381 const visibleCount = Array.from(selectedTrackIndices).filter((idx) => !isTrackHiddenByMixer(idx)).length; 382 const optimal = calculateOptimalScale(visibleCount, vHeight, newProfile); 383 setZoom(optimal); 384 } 385 }; 386 const handleQuickTrackSelect = (val) => { 387 if (val === "top6") { 388 selectTop6Tracks(); 389 } else if (val === "all") { 390 const allSet = new Set(tracksList.map((t) => t.index)); 391 setSelectedTrackIndices(allSet); 392 renderActiveTracks(allSet); 393 } else if (val === "custom") { 394 setIsTracksMenuOpen(true); 395 } else { 396 const idx = parseInt(val, 10); 397 if (!isNaN(idx)) { 398 const singleSet = /* @__PURE__ */ new Set([idx]); 399 setSelectedTrackIndices(singleSet); 400 renderActiveTracks(singleSet); 401 } 402 } 403 }; 404 const toggleTrack = (trackIdx) => { 405 setSelectedTrackIndices((prev) => { 406 const next = new Set(prev); 407 if (next.has(trackIdx)) { 408 if (next.size > 1) { 409 next.delete(trackIdx); 410 } 411 } else { 412 next.add(trackIdx); 413 } 414 renderActiveTracks(next); 415 return next; 416 }); 417 }; 418 const soloTrack = (trackIdx) => { 419 const singleSet = /* @__PURE__ */ new Set([trackIdx]); 420 setSelectedTrackIndices(singleSet); 421 renderActiveTracks(singleSet); 422 }; 423 const selectAllTracks = () => { 424 const allSet = new Set(tracksList.map((t) => t.index)); 425 setSelectedTrackIndices(allSet); 426 renderActiveTracks(allSet); 427 }; 428 const deselectAllTracks = () => { 429 const fallbackIdx = tracksList.length > 0 ? tracksList[0].index : 0; 430 const minimalSet = /* @__PURE__ */ new Set([fallbackIdx]); 431 setSelectedTrackIndices(minimalSet); 432 renderActiveTracks(minimalSet); 433 }; 434 const allSelectedHiddenByMixer = reactExports.useMemo(() => { 435 if (!hiddenTracks || hiddenTracks.size === 0 || selectedTrackIndices.size === 0) return false;
436 return Array.from(selectedTrackIndices).every((idx) => isTrackHiddenByMixer(idx)); 437 }, [hiddenTracks, selectedTrackIndices, isTrackHiddenByMixer]); 438 reactExports.useEffect(() => { 439 const api = alphaTabRef.current; 440 if (!api || !isPlayerReadyRef.current) return; 441 try { 442 if (isPlaying) { 443 api.timePosition = Math.max(0, currentTimeRef.current * 1e3); 444 if (api.playerState !== _barrel_exports$7.PlayerState.Playing) { 445 api.play(); 446 } 447 } else { 448 if (api.playerState === _barrel_exports$7.PlayerState.Playing) { 449 api.pause(); 450 } 451 } 452 } catch (err) { 453 console.warn("[TabVisualizer] Failed to sync play/pause state:", err); 454 } 455 }, [isPlaying]); 456 reactExports.useEffect(() => { 457 const api = alphaTabRef.current; 458 if (!api || !isPlayerReadyRef.current || isLoading) return; 459 try { 460 const timeMs = Math.max(0, currentTime * 1e3); 461 if (isPlaying) { 462 const curPos = api.timePosition || 0; 463 if (Math.abs(curPos - timeMs) > 250) { 464 api.timePosition = timeMs; 465 } 466 } else { 467 api.timePosition = timeMs; 468 if (followPlayback) { 469 api.scrollToCursor(); 470 } 471 } 472 } catch (_) { 473 } 474 }, [currentTime, isPlaying, followPlayback, isLoading]); 475 reactExports.useEffect(() => { 476 const api = alphaTabRef.current; 477 if (!api) return; 478 api.settings.player.scrollMode = followPlayback ? ScrollMode.Continuous : ScrollMode.Off; 479 api.updateSettings(); 480 if (followPlayback && isPlayerReadyRef.current) { 481 try { 482 api.scrollToCursor(); 483 } catch (_) { 484 } 485 } 486 }, [followPlayback]); 487 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `relative flex flex-col w-full h-full overflow-hidden ${isDarkMode ? "bg-zinc-950 text-zinc-100" : "bg-zinc-50 text-zinc-900"}`, children: [ 488 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `flex flex-wrap items-center justify-between gap-3 px-4 py-2 border-b text-xs ${isDarkMode ? "bg-zinc-900/90 border-zinc-800" : "bg-white/90 border-zinc-200"} backdrop-blur-sm z-10`, children: [
489 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [ 490 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative flex items-center gap-1.5", ref: tracksMenuRef, children: [ 491 /* @__PURE__ */ jsxRuntimeExports.jsx(Music2, { className: "w-3.5 h-3.5 text-amber-500 shrink-0" }), 492 /* @__PURE__ */ jsxRuntimeExports.jsxs( 493 "select", 494 { 495 value: isTop6Selected ? "top6" : selectedTrackIndices.size === tracksList.length && tracksList.length > 0 ? "all" : selectedTrackIndices.size === 1 ? Array.from(selectedTrackIndices)[0].toString() : "custom", 496 onChange: (e) => handleQuickTrackSelect(e.target.value), 497 className: `px-2 py-1 rounded text-xs border font-medium outline-none transition-colors ${isDarkMode ? "bg-zinc-800 text-zinc-200 border-zinc-700 hover:bg-zinc-750" : "bg-white text-zinc-800 border-zinc-300 hover:bg-zinc-100"}`, 498 children: [ 499 tracksList.length > 6 && /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "top6", className: isDarkMode ? "bg-zinc-800 text-amber-400 font-semibold" : "bg-white text-amber-600 font-semibold", children: "Top 6 Tracks (Auto-Fit)" }),
500 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "all", className: isDarkMode ? "bg-zinc-800 text-zinc-200 font-semibold" : "bg-white text-zinc-800 font-semibold", children: "All Tracks (Full Score)" }), 501 selectedTrackIndices.size > 1 && !isTop6Selected && selectedTrackIndices.size < tracksList.length && /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "custom", className: isDarkMode ? "bg-zinc-800 text-amber-400 font-medium" : "bg-white text-amber-600 font-medium", children: `Custom (${selectedTrackIndices.size} Tracks)` }), 502 /* @__PURE__ */ jsxRuntimeExports.jsx("optgroup", { label: "Single Track Solo", className: isDarkMode ? "bg-zinc-850 text-zinc-400" : "bg-zinc-100 text-zinc-500", children: tracksList.map((t) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: t.index.toString(), className: isDarkMode ? "bg-zinc-800 text-zinc-200" : "bg-white text-zinc-800", children: t.name }, t.index)) }) 503 ] 504 } 505 ), 506 /* @__P
506URE__ */ jsxRuntimeExports.jsxs( 507 "button", 508 { 509 onClick: () => setIsTracksMenuOpen(!isTracksMenuOpen), 510 className: `flex items-center gap-1 px-2 py-1 rounded text-xs border font-medium transition-colors ${isTracksMenuOpen || selectedTrackIndices.size > 1 && selectedTrackIndices.size < tracksList.length ? "bg-amber-500/20 text-amber-400 border-amber-500/40" : isDarkMode ? "bg-zinc-800 text-zinc-400 border-zinc-700 hover:text-zinc-200" : "bg-white text-zinc-600 border-zinc-300 hover:text-zinc-900"}`, 511 title: "Selective Tracks (Checkboxes)", 512 children: [ 513 /* @__PURE__ */ jsxRuntimeExports.jsx(ListFilter, { className: "w-3.5 h-3.5" }), 514 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: `${selectedTrackIndices.size}/${tracksList.length}` }) 515 ] 516 } 517 ), 518 isTracksMenuOpen && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `absolute top-full left-0 mt-1.5 w-64 rounded-lg shadow-xl border z-30 p-2.5 backdrop-blur-md ${isDarkMode ? "bg-zinc-900/95 border-zinc-750 text-zinc-100" : "bg-white/95 border-zinc-300 text-zinc-900"}`, children: [
519 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between pb-2 mb-2 border-b border-zinc-750/50", children: [ 520 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-semibold text-xs flex items-center gap-1.5", children: [ 521 /* @__PURE__ */ jsxRuntimeExports.jsx(SlidersHorizontal, { className: "w-3.5 h-3.5 text-amber-500" }), 522 "Visible Tracks" 523 ] }), 524 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 text-[11px]", children: [ 525 tracksList.length > 6 && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 526 /* @__PURE__ */ jsxRuntimeExports.jsx( 527 "button", 528 { 529 onClick: selectTop6Tracks, 530 className: `font-medium ${isTop6Selected ? "text-amber-400 font-bold" : "text-amber-500 hover:text-amber-400"}`, 531 title: "Select top 6 most active tracks", 532 children: "Top 6" 533 } 534 ),
535 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-600", children: "|" }) 536 ] }), 537 /* @__PURE__ */ jsxRuntimeExports.jsx( 538 "button", 539 { 540 onClick: selectAllTracks, 541 className: "text-amber-500 hover:text-amber-400 font-medium", 542 children: "All" 543 } 544 ), 545 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-600", children: "|" }), 546 /* @__PURE__ */ jsxRuntimeExports.jsx( 547 "button", 548 { 549 onClick: deselectAllTracks, 550 className: "text-zinc-400 hover:text-zinc-300", 551 children: "None" 552 } 553 ), 554 /* @__PURE__ */ jsxRuntimeExports.jsx( 555 "button", 556 { 557 onClick: () => setIsTracksMenuOpen(false), 558 className: "p-0.5 text-zinc-400 hover:text-zinc-200 ml-1", 559 children: /* @__PURE__ */ jsxRuntimeExports.jsx(X, { className: "w-3.5 h-3.5" }) 560 } 561 ) 562 ] }) 563 ] }),
564 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex flex-col gap-1 max-h-56 overflow-y-auto pr-1", children: tracksList.map((t) => { 565 const isChecked = selectedTrackIndices.has(t.index); 566 const isHiddenInMixer = isTrackHiddenByMixer(t.index); 567 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 568 "div", 569 { 570 className: `flex items-center justify-between px-2 py-1 rounded text-xs transition-colors ${isDarkMode ? "hover:bg-zinc-800" : "hover:bg-zinc-100"}`, 571 children: [ 572 /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "flex items-center gap-2 cursor-pointer select-none flex-1 truncate", children: [ 573 /* @__PURE__ */ jsxRuntimeExports.jsx( 574 "input", 575 { 576 type: "checkbox", 577 checked: isChecked, 578 onChange: () => toggleTrack(t.index), 579 className: "rounded border-zinc-600 text-amber-500 focus:ring-amber-500 focus:ring-offset-0 h-3.5 w-3.5 cursor-pointer" 580 } 581 ),
582 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `truncate ${!isChecked ? "text-zinc-500" : isHiddenInMixer ? "line-through text-zinc-500" : ""}`, children: t.name }), 583 isHiddenInMixer && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-amber-500/80 italic ml-1", children: "(mixer muted)" }) 584 ] }), 585 /* @__PURE__ */ jsxRuntimeExports.jsx( 586 "button", 587 { 588 onClick: () => soloTrack(t.index), 589 className: `text-[10px] px-1.5 py-0.5 rounded border transition-colors ml-1 shrink-0 ${selectedTrackIndices.size === 1 && selectedTrackIndices.has(t.index) ? "bg-amber-500 text-zinc-950 font-bold border-amber-400" : isDarkMode ? "border-zinc-700 text-zinc-400 hover:text-zinc-200 hover:border-zinc-500" : "border-zinc-300 text-zinc-600 hover:text-zinc-900"}`, 590 title: "Solo this track", 591 children: "Solo" 592 } 593 ) 594 ] 595 }, 596 t.index 597 ); 598 }) }), 599 hiddenTracks && hiddenTracks.size > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-2 pt-2 border-t border-zinc-750/50 text-[10px] text-zinc-400 flex items-center justify-between", children: [
600 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Mixer sync active" }), 601 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-amber-400", children: `${hiddenTracks.size} hidden in mixer` }) 602 ] }) 603 ] }) 604 ] }), 605 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5", children: [ 606 /* @__PURE__ */ jsxRuntimeExports.jsx(Layers, { className: "w-3.5 h-3.5 text-sky-500" }), 607 /* @__PURE__ */ jsxRuntimeExports.jsxs( 608 "select", 609 { 610 value: staveProfile, 611 onChange: (e) => handleStaveProfileChange(e.target.value), 612 className: `px-2 py-1 rounded text-xs border font-medium outline-none transition-colors ${isDarkMode ? "bg-zinc-800 text-zinc-200 border-zinc-700 hover:bg-zinc-750" : "bg-white text-zinc-800 border-zinc-300 hover:bg-zinc-100"}`, 613 children: [ 614 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "ScoreTab", className: isDarkMode ? "bg-zinc-800 text-zinc-200" : "bg-white text-zinc-800", children: "Score + Tab" }),
615 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "Tab", className: isDarkMode ? "bg-zinc-800 text-zinc-200" : "bg-white text-zinc-800", children: "Tab Only" }), 616 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "Score", className: isDarkMode ? "bg-zinc-800 text-zinc-200" : "bg-white text-zinc-800", children: "Standard Notation" }) 617 ] 618 } 619 ) 620 ] }), 621 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5", children: [ 622 /* @__PURE__ */ jsxRuntimeExports.jsx(Eye, { className: "w-3.5 h-3.5 text-emerald-500" }), 623 /* @__PURE__ */ jsxRuntimeExports.jsxs( 624 "select", 625 { 626 value: layoutMode, 627 onChange: (e) => setLayoutMode(e.target.value), 628 className: `px-2 py-1 rounded text-xs border font-medium outline-none transition-colors ${isDarkMode ? "bg-zinc-800 text-zinc-200 border-zinc-700 hover:bg-zinc-750" : "bg-white text-zinc-800 border-zinc-300 hover:bg-zinc-100"}`, 629 children: [
630 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "Page", className: isDarkMode ? "bg-zinc-800 text-zinc-200" : "bg-white text-zinc-800", children: "Page Scroll" }), 631 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "Horizontal", className: isDarkMode ? "bg-zinc-800 text-zinc-200" : "bg-white text-zinc-800", children: "Horizontal Track" }) 632 ] 633 } 634 ) 635 ] }) 636 ] }), 637 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 638 /* @__PURE__ */ jsxRuntimeExports.jsxs( 639 "button", 640 { 641 onClick: () => setFollowPlayback(!followPlayback), 642 className: `px-2 py-1 rounded text-xs border font-medium transition-colors ${followPlayback ? "bg-amber-500/20 text-amber-400 border-amber-500/40" : isDarkMode ? "bg-zinc-800 text-zinc-400 border-zinc-700" : "bg-white text-zinc-500 border-zinc-300"}`, 643 title: "Auto-scroll with playback", 644 children: [ 645 "Follow Playback: ", 646 followPlayback ? "ON" : "OFF" 647 ] 648 } 649 ),
650 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1 border rounded px-1 py-0.5 border-zinc-700", children: [ 651 /* @__PURE__ */ jsxRuntimeExports.jsx( 652 "button", 653 { 654 onClick: () => { 655 var _a; 656 setIsAutoScale(true); 657 const vHeight = ((_a = viewportRef.current) == null ? void 0 : _a.clientHeight) || 480; 658 const visibleCount = Array.from(selectedTrackIndices).filter((idx) => !isTrackHiddenByMixer(idx)).length; 659 const s = calculateOptimalScale(visibleCount, vHeight, staveProfile); 660 setZoom(s); 661 }, 662 className: `px-1.5 py-0.5 rounded text-[10px] font-semibold border transition-colors ${isAutoScale ? "bg-amber-500/20 text-amber-400 border-amber-500/40" : isDarkMode ? "border-zinc-700 text-zinc-400 hover:text-zinc-200" : "border-zinc-300 text-zinc-600 hover:text-zinc-900"}`, 663 title: "Auto-scale zoom to fit window height", 664 children: "Auto" 665 } 666 ),
667 /* @__PURE__ */ jsxRuntimeExports.jsx( 668 "button", 669 { 670 onClick: () => { 671 setIsAutoScale(false); 672 setZoom((z) => Math.max(0.4, Math.round((z - 0.1) * 10) / 10)); 673 }, 674 className: "p-1 hover:text-amber-400 transition-colors", 675 title: "Zoom Out", 676 children: /* @__PURE__ */ jsxRuntimeExports.jsx(ZoomOut, { className: "w-3.5 h-3.5" }) 677 } 678 ), 679 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[11px] font-mono px-1", children: [ 680 Math.round(zoom * 100), 681 "%" 682 ] }), 683 /* @__PURE__ */ jsxRuntimeExports.jsx( 684 "button", 685 { 686 onClick: () => { 687 setIsAutoScale(false); 688 setZoom((z) => Math.min(2, Math.round((z + 0.1) * 10) / 10)); 689 }, 690 className: "p-1 hover:text-amber-400 transition-colors", 691 title: "Zoom In", 692 children: /* @__PURE__ */ jsxRuntimeExports.jsx(ZoomIn, { className: "w-3.5 h-3.5" }) 693 } 694 ) 695 ] }) 696 ] }) 697 ] }), 698 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { ref: viewportRef, className: "relative flex-1 w-full h-full overflow-auto", children: [
699 /* @__PURE__ */ jsxRuntimeExports.jsx("style", { children: ` 700 .at-cursors { 701 pointer-events: none !important; 702 } 703 .at-cursor-bar { 704 background: ${isDarkMode ? "rgba(245, 158, 11, 0.14)" : "rgba(217, 119, 6, 0.12)"} !important; 705 border-radius: 4px; 706 transition: top 0.2s ease, left 0.2s ease, width 0.2s ease, height 0.2s ease; 707 } 708 .at-cursor-beat { 709 background: ${isDarkMode ? "rgba(245, 158, 11, 0.95)" : "rgba(217, 119, 6, 0.95)"} !important; 710 width: 3px !important; 711 border-radius: 2px; 712 box-shadow: 0 0 10px ${isDarkMode ? "rgba(245, 158, 11, 0.7)" : "rgba(217, 119, 6, 0.5)"}; 713 z-index: 1001 !important; 714 } 715 .at-selection div { 716 background: rgba(56, 189, 248, 0.25) !important; 717 } 718 .at-highlight * { 719 fill: ${isDarkMode ? "#f59e0b" : "#d97706"} !important; 720 stroke: ${isDarkMode ? "#f59e0b" : "#d97706"} !important; 721 } 722 ` }), 723 isLoading && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute inset-0 flex flex-col items-center justify-center bg-zinc-950/70 z-20 backdrop-blur-xs", children: [
724 /* @__PURE__ */ jsxRuntimeExports.jsx(LoaderCircle, { className: "w-8 h-8 text-amber-500 animate-spin mb-2" }), 725 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm font-medium text-zinc-300", children: "Rendering tablature..." }) 726 ] }), 727 allSelectedHiddenByMixer && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute top-4 left-1/2 transform -translate-x-1/2 z-20 px-4 py-2 rounded-lg bg-amber-500/10 border border-amber-500/30 text-amber-400 text-xs flex items-center gap-2 backdrop-blur-md shadow-lg", children: [ 728 /* @__PURE__ */ jsxRuntimeExports.jsx(CircleAlert, { className: "w-4 h-4 shrink-0 text-amber-500" }), 729 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "All selected tracks are currently hidden in the Mixer console. Toggle track visibility on the right to view." }) 730 ] }), 731 error && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute inset-0 flex flex-col items-center justify-center p-6 text-center z-20", children: [
732 /* @__PURE__ */ jsxRuntimeExports.jsx(CircleAlert, { className: "w-10 h-10 text-red-400 mb-2" }), 733 /* @__PURE__ */ jsxRuntimeExports.jsx("h4", { className: "text-base font-semibold text-zinc-200", children: "Unable to load tablature" }), 734 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-zinc-400 max-w-md mt-1 mb-4", children: error }), 735 /* @__PURE__ */ jsxRuntimeExports.jsx( 736 "button", 737 { 738 onClick: () => { 739 var _a; 740 setError(null); 741 setIsLoading(true); 742 (_a = alphaTabRef.current) == null ? void 0 : _a.load(`/api/tracks/${track.id}/tab`); 743 }, 744 className: "px-4 py-1.5 rounded-md bg-amber-600 hover:bg-amber-500 text-white text-xs font-medium transition-colors", 745 children: "Retry" 746 } 747 ) 748 ] }), 749 /* @__PURE__ */ jsxRuntimeExports.jsx( 750 "div", 751 { 752 ref: containerRef, 753 className: "w-full min-h-full p-4 flex flex-col items-center justify-start select-none" 754 } 755 ) 756 ] }) 757 ] }); 758}; 759export { 760 TabVisualizer, 761 TabVisualizer as default 762};
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.