PageSourceSearch

https://www.hamienet.com/assets/TabVisualizer-cB2rx8tp.js

js hamienet.com collected 2026-10-02 05:01:06 UTC 38,136 bytes, 762 lines download raw bytes

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.