@behindthemusictree/app-kit-ui 29.0.1 → 30.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -42,13 +42,13 @@ __export(genre_tree_exports, {
42
42
  module.exports = __toCommonJS(genre_tree_exports);
43
43
 
44
44
  // src/genre-tree/GenreTreeView.tsx
45
- var import_react15 = require("react");
45
+ var import_react17 = require("react");
46
46
  var import_react_query3 = require("@tanstack/react-query");
47
- var import_lucide_react11 = require("lucide-react");
48
- var import_ui13 = require("@behindthemusictree/ui");
47
+ var import_lucide_react12 = require("lucide-react");
48
+ var import_ui15 = require("@behindthemusictree/ui");
49
49
  var import_genre_tree_view4 = require("@behindthemusictree/genre-tree-view");
50
- var import_genre_tree16 = require("@behindthemusictree/app-kit/genre-tree");
51
- var import_genre_tree17 = require("@behindthemusictree/app-kit/genre-tree");
50
+ var import_genre_tree18 = require("@behindthemusictree/app-kit/genre-tree");
51
+ var import_genre_tree19 = require("@behindthemusictree/app-kit/genre-tree");
52
52
  var import_popup5 = require("@behindthemusictree/app-kit/popup");
53
53
 
54
54
  // src/genre-tree/playlist-tree/TreePerRoot.tsx
@@ -736,6 +736,7 @@ function GenreTreeWheelHandoff({ skeleton, children, onRevealedChange }) {
736
736
  }
737
737
 
738
738
  // src/genre-tree/GenreSearch.tsx
739
+ var import_react7 = require("react");
739
740
  var import_lucide_react3 = require("lucide-react");
740
741
  var import_ui3 = require("@behindthemusictree/ui");
741
742
  var import_genre_tree2 = require("@behindthemusictree/app-kit/genre-tree");
@@ -748,7 +749,7 @@ function GenreSearch({
748
749
  onPrevious,
749
750
  placeholder,
750
751
  onQueryChange,
751
- extraResults
752
+ extraGroups = []
752
753
  }) {
753
754
  const { query, setQuery, results, canClear, clear, select } = (0, import_genre_tree2.useGenreSearch)({
754
755
  genrePlaylists,
@@ -756,11 +757,45 @@ function GenreSearch({
756
757
  selectedName,
757
758
  onClear
758
759
  });
759
- const extraItems = query !== "" && query !== selectedName ? extraResults?.items ?? [] : [];
760
- const label = extraResults ? "Search" : "Search a genre";
761
- const loading = query !== "" && query !== selectedName && (extraResults?.loading ?? false);
762
- const showList = results.length > 0 || extraItems.length > 0;
763
- const heading = "px-3 pt-2 pb-1 text-xs font-medium uppercase text-[var(--color-text-muted)]";
760
+ const [open, setOpen] = (0, import_react7.useState)(false);
761
+ const active = query !== "" && query !== selectedName;
762
+ const hasExtras = extraGroups.length > 0;
763
+ const label = hasExtras ? "Search" : "Search a genre";
764
+ const loading = active && extraGroups.some((group) => group.loading);
765
+ const status = active ? extraGroups.map((group) => group.status).filter(Boolean).join(". ") : "";
766
+ const groups = [
767
+ {
768
+ key: "genres",
769
+ label: hasExtras ? "Genres" : void 0,
770
+ options: results.map((genrePlaylist) => ({
771
+ key: genrePlaylist.uuid,
772
+ content: /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_jsx_runtime9.Fragment, { children: [
773
+ genrePlaylist.name,
774
+ genrePlaylist.isUmbrella && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "ml-2 text-xs italic text-[var(--color-text-muted)]", children: "umbrella" })
775
+ ] })
776
+ }))
777
+ },
778
+ ...extraGroups.map((group) => ({
779
+ key: group.key,
780
+ label: group.label,
781
+ options: active ? group.items.map((item) => ({ key: item.key, content: item.content })) : []
782
+ }))
783
+ ];
784
+ const handlePick = (groupKey, optionKey) => {
785
+ setOpen(false);
786
+ if (groupKey === "genres") {
787
+ const genrePlaylist = results.find((result) => result.uuid === optionKey);
788
+ if (!genrePlaylist) return;
789
+ select(genrePlaylist);
790
+ onQueryChange?.("");
791
+ return;
792
+ }
793
+ const item = extraGroups.find((group) => group.key === groupKey)?.items.find((entry) => entry.key === optionKey);
794
+ if (!item) return;
795
+ setQuery("");
796
+ onQueryChange?.("");
797
+ item.onSelect();
798
+ };
764
799
  return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "genre-search relative w-[calc(var(--layout-detail-panel-w,25.5rem)-2*var(--layout-panel-inset,1rem))] shrink-0 max-md:w-full", children: [
765
800
  onPrevious && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
766
801
  "button",
@@ -774,47 +809,45 @@ function GenreSearch({
774
809
  }
775
810
  ),
776
811
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
777
- import_ui3.Input,
812
+ import_ui3.Combobox,
778
813
  {
779
- type: "text",
814
+ ariaLabel: label,
815
+ listLabel: `${label} results`,
780
816
  value: query,
781
- onChange: (event) => {
782
- setQuery(event.target.value);
783
- onQueryChange?.(event.target.value);
784
- },
785
817
  placeholder: placeholder ?? `${label}\u2026`,
786
- "aria-label": label,
787
- "aria-busy": loading,
788
- inputMode: "search",
789
- enterKeyHint: "search",
790
- autoComplete: "off",
791
- className: "h-[var(--layout-search-h,3rem)] pl-6 pr-20 text-base transition-[padding] duration-200 ease-out motion-reduce:transition-none bg-[var(--color-surface)] border-[var(--color-border)] rounded-full shadow-md"
818
+ groups,
819
+ open,
820
+ onOpenChange: setOpen,
821
+ onValueChange: (value) => {
822
+ setQuery(value);
823
+ onQueryChange?.(value);
824
+ },
825
+ onPick: handlePick,
826
+ status,
827
+ loading: loading ? "Searching\u2026" : void 0,
828
+ inputClassName: "h-[var(--layout-search-h,3rem)] pl-6 pr-20 text-base transition-[padding] duration-200 ease-out motion-reduce:transition-none bg-[var(--color-surface)] border-[var(--color-border)] rounded-full shadow-md",
829
+ className: "genre-search-results z-40 rounded-xl shadow-md",
830
+ listClassName: "max-h-[min(20rem,50dvh)]",
831
+ optionClassName: "[@media(pointer:coarse)]:min-h-[var(--layout-touch-target)]"
792
832
  }
793
833
  ),
794
834
  /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "absolute right-3 top-1/2 flex items-center -translate-y-1/2", children: [
795
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
796
- "span",
797
- {
798
- "data-loading": loading,
799
- className: "relative p-2 text-[var(--color-text-muted)] pointer-events-none",
800
- children: [
801
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
802
- import_lucide_react3.Search,
803
- {
804
- "aria-hidden": "true",
805
- className: `w-6 h-6 transition-opacity duration-200 motion-reduce:transition-none ${loading ? "opacity-0" : ""}`
806
- }
807
- ),
808
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
809
- import_lucide_react3.LoaderCircle,
810
- {
811
- "aria-hidden": "true",
812
- className: `absolute inset-2 w-6 h-6 transition-opacity duration-200 motion-reduce:transition-none ${loading ? "opacity-100 delay-300 animate-spin motion-reduce:animate-none" : "opacity-0"}`
813
- }
814
- )
815
- ]
816
- }
817
- ),
835
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { "data-loading": loading, className: "relative p-2 text-[var(--color-text-muted)] pointer-events-none", children: [
836
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
837
+ import_lucide_react3.Search,
838
+ {
839
+ "aria-hidden": "true",
840
+ className: `w-6 h-6 transition-opacity duration-200 motion-reduce:transition-none ${loading ? "opacity-0" : ""}`
841
+ }
842
+ ),
843
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
844
+ import_lucide_react3.LoaderCircle,
845
+ {
846
+ "aria-hidden": "true",
847
+ className: `absolute inset-2 w-6 h-6 transition-opacity duration-200 motion-reduce:transition-none ${loading ? "opacity-100 delay-300 animate-spin motion-reduce:animate-none" : "opacity-0"}`
848
+ }
849
+ )
850
+ ] }),
818
851
  canClear && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
819
852
  "button",
820
853
  {
@@ -828,39 +861,6 @@ function GenreSearch({
828
861
  children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_lucide_react3.X, { "aria-hidden": "true", className: "w-6 h-6" })
829
862
  }
830
863
  )
831
- ] }),
832
- extraResults?.status && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { role: "status", "aria-live": "polite", className: "sr-only", children: extraResults.status }),
833
- showList && /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("ul", { className: "genre-search-results absolute z-40 w-full max-h-[min(16rem,50dvh)] mt-1 overflow-y-auto bg-[var(--color-surface)] border border-[var(--color-border)] rounded-xl shadow-md", children: [
834
- extraResults && results.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("li", { className: heading, children: "Genres" }),
835
- results.map((genrePlaylist) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
836
- "button",
837
- {
838
- type: "button",
839
- className: "w-full px-3 py-1.5 [@media(pointer:coarse)]:min-h-[var(--layout-touch-target)] text-left hover:bg-[var(--color-surface-muted)]",
840
- onClick: () => {
841
- select(genrePlaylist);
842
- onQueryChange?.("");
843
- },
844
- children: [
845
- genrePlaylist.name,
846
- genrePlaylist.isUmbrella && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "ml-2 text-xs italic text-[var(--color-text-muted)]", children: "umbrella" })
847
- ]
848
- }
849
- ) }, genrePlaylist.uuid)),
850
- extraResults && extraItems.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("li", { className: heading, children: extraResults.label }),
851
- extraItems.map((item) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
852
- "button",
853
- {
854
- type: "button",
855
- className: "w-full px-3 py-1.5 [@media(pointer:coarse)]:min-h-[var(--layout-touch-target)] text-left hover:bg-[var(--color-surface-muted)]",
856
- onClick: () => {
857
- setQuery("");
858
- onQueryChange?.("");
859
- item.onSelect();
860
- },
861
- children: item.content
862
- }
863
- ) }, item.key))
864
864
  ] })
865
865
  ] });
866
866
  }
@@ -876,18 +876,18 @@ var import_lucide_react6 = require("lucide-react");
876
876
  var import_popup3 = require("@behindthemusictree/app-kit/popup");
877
877
 
878
878
  // src/genre-tree/SharePopup.tsx
879
- var import_react8 = require("react");
879
+ var import_react9 = require("react");
880
880
  var import_lucide_react5 = require("lucide-react");
881
881
  var import_fa = require("react-icons/fa");
882
882
  var import_si = require("react-icons/si");
883
883
 
884
884
  // src/popup/Toast.tsx
885
- var import_react7 = require("react");
885
+ var import_react8 = require("react");
886
886
  var import_react_dom2 = require("react-dom");
887
887
  var import_jsx_runtime10 = require("react/jsx-runtime");
888
888
  function Toast({ message, onClose, durationMs = 2200 }) {
889
- const [mounted, setMounted] = (0, import_react7.useState)(false);
890
- (0, import_react7.useEffect)(() => {
889
+ const [mounted, setMounted] = (0, import_react8.useState)(false);
890
+ (0, import_react8.useEffect)(() => {
891
891
  setMounted(true);
892
892
  const timeout = window.setTimeout(onClose, durationMs);
893
893
  return () => window.clearTimeout(timeout);
@@ -935,8 +935,8 @@ function ShareDestinationButton({
935
935
  var import_lucide_react4 = require("lucide-react");
936
936
  var import_jsx_runtime12 = require("react/jsx-runtime");
937
937
  function SharePreviewCard({ target, url }) {
938
- const Icon = target.kind === "genre" ? import_lucide_react4.Music2 : import_lucide_react4.UserRound;
939
- const typeLabel = target.kind === "genre" ? "Genre" : "Artist";
938
+ const Icon = { genre: import_lucide_react4.Music2, artist: import_lucide_react4.UserRound, song: import_lucide_react4.Music }[target.kind];
939
+ const typeLabel = { genre: "Genre", artist: "Artist", song: "Song" }[target.kind];
940
940
  return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "rounded-3xl bg-[var(--color-surface-muted)] p-4", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex min-h-32 overflow-hidden rounded-2xl bg-[var(--color-surface)] shadow-lg", children: [
941
941
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "flex w-28 shrink-0 items-center justify-center bg-[var(--color-primary)]/15 text-[var(--color-primary)]", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Icon, { "aria-hidden": "true", className: "h-10 w-10" }) }),
942
942
  /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col justify-center gap-1 p-4", children: [
@@ -981,20 +981,20 @@ async function copyText(text) {
981
981
  }
982
982
  }
983
983
  function SharePopup({ target, url, onClose }) {
984
- const [absoluteUrl, setAbsoluteUrl] = (0, import_react8.useState)(url);
985
- const [copyStatus, setCopyStatus] = (0, import_react8.useState)("idle");
986
- const [canUseNativeShare, setCanUseNativeShare] = (0, import_react8.useState)(false);
987
- const [shareError, setShareError] = (0, import_react8.useState)(null);
988
- const [toastId, setToastId] = (0, import_react8.useState)(0);
989
- (0, import_react8.useEffect)(() => {
984
+ const [absoluteUrl, setAbsoluteUrl] = (0, import_react9.useState)(url);
985
+ const [copyStatus, setCopyStatus] = (0, import_react9.useState)("idle");
986
+ const [canUseNativeShare, setCanUseNativeShare] = (0, import_react9.useState)(false);
987
+ const [shareError, setShareError] = (0, import_react9.useState)(null);
988
+ const [toastId, setToastId] = (0, import_react9.useState)(0);
989
+ (0, import_react9.useEffect)(() => {
990
990
  setAbsoluteUrl(resolveAbsoluteUrl(url) ?? "");
991
991
  setCopyStatus("idle");
992
992
  setShareError(null);
993
993
  }, [url]);
994
- (0, import_react8.useEffect)(() => {
994
+ (0, import_react9.useEffect)(() => {
995
995
  setCanUseNativeShare(typeof navigator !== "undefined" && typeof navigator.share === "function");
996
996
  }, []);
997
- const destinationUrls = (0, import_react8.useMemo)(() => {
997
+ const destinationUrls = (0, import_react9.useMemo)(() => {
998
998
  if (!absoluteUrl) return null;
999
999
  const message = `${target.name} \u2014 ${absoluteUrl}`;
1000
1000
  return {
@@ -1012,7 +1012,7 @@ function SharePopup({ target, url, onClose }) {
1012
1012
  setCopyStatus(copied ? "idle" : "failed");
1013
1013
  if (copied) setToastId((id) => id + 1);
1014
1014
  };
1015
- const dismissToast = (0, import_react8.useCallback)(() => setToastId(0), []);
1015
+ const dismissToast = (0, import_react9.useCallback)(() => setToastId(0), []);
1016
1016
  const handleNativeShare = async () => {
1017
1017
  if (!absoluteUrl || !navigator.share) return;
1018
1018
  setShareError(null);
@@ -1218,13 +1218,13 @@ var import_ui5 = require("@behindthemusictree/ui");
1218
1218
  var import_genre_tree9 = require("@behindthemusictree/app-kit/genre-tree");
1219
1219
 
1220
1220
  // src/genre-tree/ClampedText.tsx
1221
- var import_react9 = require("react");
1221
+ var import_react10 = require("react");
1222
1222
  var import_jsx_runtime16 = require("react/jsx-runtime");
1223
1223
  function ClampedText({ children, className: className2 = "" }) {
1224
- const ref = (0, import_react9.useRef)(null);
1225
- const [expanded, setExpanded] = (0, import_react9.useState)(false);
1226
- const [overflows, setOverflows] = (0, import_react9.useState)(false);
1227
- (0, import_react9.useLayoutEffect)(() => {
1224
+ const ref = (0, import_react10.useRef)(null);
1225
+ const [expanded, setExpanded] = (0, import_react10.useState)(false);
1226
+ const [overflows, setOverflows] = (0, import_react10.useState)(false);
1227
+ (0, import_react10.useLayoutEffect)(() => {
1228
1228
  const element = ref.current;
1229
1229
  if (!element || expanded) return;
1230
1230
  setOverflows(element.scrollHeight > element.clientHeight);
@@ -1245,7 +1245,7 @@ function ClampedText({ children, className: className2 = "" }) {
1245
1245
  }
1246
1246
 
1247
1247
  // src/genre-tree/GenrePlaylistTracks.tsx
1248
- var import_react10 = require("react");
1248
+ var import_react11 = require("react");
1249
1249
  var import_react_query = require("@tanstack/react-query");
1250
1250
  var import_zod = require("zod");
1251
1251
  var import_fa2 = require("react-icons/fa");
@@ -1313,6 +1313,7 @@ function GenrePlaylistTracks({
1313
1313
  scope,
1314
1314
  getBackendBaseUrl,
1315
1315
  renderArtistLink,
1316
+ renderSongLink,
1316
1317
  renderSongLeading,
1317
1318
  renderSongActions,
1318
1319
  renderTracksToolbar,
@@ -1343,8 +1344,8 @@ function GenrePlaylistTracks({
1343
1344
  getNextPageParam: (lastPage) => lastPage.nextPage ?? void 0,
1344
1345
  enabled: scope !== "me" || sessionRestored && !!session?.accessToken
1345
1346
  });
1346
- const sentinelRef = (0, import_react10.useRef)(null);
1347
- (0, import_react10.useEffect)(() => {
1347
+ const sentinelRef = (0, import_react11.useRef)(null);
1348
+ (0, import_react11.useEffect)(() => {
1348
1349
  const sentinel = sentinelRef.current;
1349
1350
  if (!hasNextPage || isFetchingNextPage || !sentinel) return;
1350
1351
  const observer = new IntersectionObserver((entries) => {
@@ -1383,7 +1384,7 @@ function GenrePlaylistTracks({
1383
1384
  const isCurrent = playerTrackObject?.track.id === track.uuid;
1384
1385
  const isPlaying = isCurrent && playState === import_player5.PlayStates.PLAYING;
1385
1386
  const text = /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("span", { className: TEXT_CELL, children: [
1386
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: TITLE_CELL, children: (0, import_genre_tree6.isSong)(track) ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }),
1387
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: TITLE_CELL, children: renderSongLink ? renderSongLink(track) : (0, import_genre_tree6.isSong)(track) ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }),
1387
1388
  track.artists?.length ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: SEPARATOR, children: " \u2014 " }) : null,
1388
1389
  /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: COLUMN_CELL, children: track.artists?.length ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_genre_tree8.ArtistNames, { artists: track.artists, renderArtistLink }) : null }),
1389
1390
  track.album && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: ALBUM_CELL, children: track.album.name })
@@ -1450,6 +1451,7 @@ function GenreDetailExtras({
1450
1451
  criteriaOverviewSchema,
1451
1452
  renderGenreDetailExtras,
1452
1453
  renderArtistLink,
1454
+ renderSongLink,
1453
1455
  renderSummary,
1454
1456
  renderEssentialTracks,
1455
1457
  family,
@@ -1527,6 +1529,7 @@ function GenreDetailExtras({
1527
1529
  scope,
1528
1530
  getBackendBaseUrl,
1529
1531
  renderArtistLink,
1532
+ renderSongLink,
1530
1533
  ...trackSlots
1531
1534
  }
1532
1535
  )
@@ -1535,7 +1538,7 @@ function GenreDetailExtras({
1535
1538
 
1536
1539
  // src/genre-tree/GenreInfoPanelBody.tsx
1537
1540
  var import_jsx_runtime20 = require("react/jsx-runtime");
1538
- var import_react11 = require("react");
1541
+ var import_react12 = require("react");
1539
1542
  function GenreNodeActionBand({ genrePlaylist, playback, ...bandProps }) {
1540
1543
  const { isPlaying, setIsPlaying } = (0, import_player6.usePlayer)();
1541
1544
  const isCurrentTrackList = playback.playingNodeId === genrePlaylist.uuid;
@@ -1572,7 +1575,7 @@ function GenreInfoPanelBody({
1572
1575
  return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(import_jsx_runtime20.Fragment, { children: [
1573
1576
  toolbar,
1574
1577
  /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(GenreNodeActionBand, { ...band, genrePlaylist, playback }),
1575
- /* @__PURE__ */ (0, import_react11.createElement)(
1578
+ /* @__PURE__ */ (0, import_react12.createElement)(
1576
1579
  GenreDetailExtras,
1577
1580
  {
1578
1581
  ...extras,
@@ -1632,9 +1635,9 @@ function PanelEdgeTab({ side, edge, center, controls, className: className2 }) {
1632
1635
  }
1633
1636
 
1634
1637
  // src/genre-tree/useLeftDetailPanelLayout.ts
1635
- var import_react12 = require("react");
1638
+ var import_react13 = require("react");
1636
1639
  function useLeftDetailPanelLayout(isPanelShown, enabled = true) {
1637
- (0, import_react12.useEffect)(() => {
1640
+ (0, import_react13.useEffect)(() => {
1638
1641
  if (!enabled || isPanelShown) return void 0;
1639
1642
  const root = document.documentElement;
1640
1643
  root.style.setProperty("--layout-left-w", "0px");
@@ -1645,7 +1648,7 @@ function useLeftDetailPanelLayout(isPanelShown, enabled = true) {
1645
1648
  }
1646
1649
 
1647
1650
  // src/genre-tree/ChangeParentPopup.tsx
1648
- var import_react13 = require("react");
1651
+ var import_react14 = require("react");
1649
1652
  var import_genre_tree10 = require("@behindthemusictree/app-kit/genre-tree");
1650
1653
  var import_jsx_runtime22 = require("react/jsx-runtime");
1651
1654
  function ChangeParentPopup({
@@ -1657,7 +1660,7 @@ function ChangeParentPopup({
1657
1660
  onParentChanged
1658
1661
  }) {
1659
1662
  const { mutate, isPending, formErrors } = (0, import_genre_tree10.useUpdateGenre)(scope, getBackendBaseUrl);
1660
- const candidates = (0, import_react13.useMemo)(
1663
+ const candidates = (0, import_react14.useMemo)(
1661
1664
  () => (0, import_genre_tree10.getReparentCandidates)(genrePlaylists, genrePlaylist.uuid),
1662
1665
  [genrePlaylists, genrePlaylist.uuid]
1663
1666
  );
@@ -1675,7 +1678,7 @@ function ChangeParentPopup({
1675
1678
  }
1676
1679
  );
1677
1680
  };
1678
- (0, import_react13.useEffect)(() => {
1681
+ (0, import_react14.useEffect)(() => {
1679
1682
  const handleKeyDown = (event) => {
1680
1683
  if (event.key === "Escape") onClose();
1681
1684
  };
@@ -1863,7 +1866,7 @@ function ArtistDiscography({ albums }) {
1863
1866
  }
1864
1867
 
1865
1868
  // src/genre-tree/artist/ArtistSongs.tsx
1866
- var import_react14 = require("react");
1869
+ var import_react15 = require("react");
1867
1870
  var import_react_query2 = require("@tanstack/react-query");
1868
1871
  var import_lucide_react10 = require("lucide-react");
1869
1872
  var import_ui10 = require("@behindthemusictree/ui");
@@ -1936,7 +1939,8 @@ function TrackItem({
1936
1939
  position,
1937
1940
  renderDuration,
1938
1941
  renderActions,
1939
- renderArtistLink
1942
+ renderArtistLink,
1943
+ renderSongLink
1940
1944
  }) {
1941
1945
  const { handlePlayPauseAction, playerTrackObject, loadTrackForPlayer } = (0, import_player9.usePlayer)();
1942
1946
  const { trackList, selectedTrack, toTrackAtPosition, runtimeUnplayableTrackIds } = (0, import_genre_tree11.useTrackList)();
@@ -1976,7 +1980,7 @@ function TrackItem({
1976
1980
  }
1977
1981
  ),
1978
1982
  /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
1979
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { className: "flex max-w-full items-center gap-2", children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { className: "title text-lg font-bold text-[var(--color-text)] max-w-full truncate", children: (0, import_genre_tree12.isSong)(track) ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }) }),
1983
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { className: "flex max-w-full items-center gap-2", children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { className: "title text-lg font-bold text-[var(--color-text)] max-w-full truncate", children: renderSongLink ? renderSongLink(track) : (0, import_genre_tree12.isSong)(track) ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }) }),
1980
1984
  track.artists && track.artists.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { className: "artist text-base max-w-full truncate text-[var(--color-text-muted)]", children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_genre_tree13.ArtistNames, { artists: track.artists, renderArtistLink }) }) : ""
1981
1985
  ] }),
1982
1986
  /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { className: "genre-name-container flex items-center justify-start w-[21.6%] min-w-0 shrink-0", children: track.genre ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { className: "genre-name max-w-full whitespace-normal break-words p-1 text-xs font-bold", children: track.genre.name }) : "" }),
@@ -2001,9 +2005,9 @@ var SORTS = [
2001
2005
  { ordering: "title", label: "Title" },
2002
2006
  { ordering: "-albumYear", label: "Year" }
2003
2007
  ];
2004
- function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink }) {
2005
- const [page, setPage] = (0, import_react14.useState)(1);
2006
- const [ordering, setOrdering] = (0, import_react14.useState)("title");
2008
+ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLink }) {
2009
+ const [page, setPage] = (0, import_react15.useState)(1);
2010
+ const [ordering, setOrdering] = (0, import_react15.useState)("title");
2007
2011
  const { fetch } = (0, import_transport7.useFetchWrapper)(getBackendBaseUrl);
2008
2012
  const { playNewTrackListFromArtist } = (0, import_genre_tree14.useTrackList)();
2009
2013
  const { playerTrackObject } = (0, import_player10.usePlayer)();
@@ -2062,7 +2066,8 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink }) {
2062
2066
  {
2063
2067
  track: song,
2064
2068
  position: (page - 1) * data.pageSize + index + 1,
2065
- renderArtistLink
2069
+ renderArtistLink,
2070
+ renderSongLink
2066
2071
  }
2067
2072
  ) }, song.uuid)) }),
2068
2073
  /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui10.Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
@@ -2077,6 +2082,7 @@ function useArtistPanel({
2077
2082
  getBackendBaseUrl,
2078
2083
  getShareUrl,
2079
2084
  renderArtistLink,
2085
+ renderSongLink,
2080
2086
  expanded,
2081
2087
  onExpandedChange,
2082
2088
  onClose
@@ -2129,7 +2135,8 @@ function useArtistPanel({
2129
2135
  {
2130
2136
  artist: { uuid: artist.uuid, name: artist.name },
2131
2137
  getBackendBaseUrl,
2132
- renderArtistLink
2138
+ renderArtistLink,
2139
+ renderSongLink
2133
2140
  }
2134
2141
  )
2135
2142
  ] }),
@@ -2139,30 +2146,225 @@ function useArtistPanel({
2139
2146
  }
2140
2147
  }
2141
2148
 
2142
- // src/genre-tree/SizeMetricToggle.tsx
2143
- var import_md = require("react-icons/md");
2149
+ // src/genre-tree/song/useSongPanel.tsx
2150
+ var import_ui13 = require("@behindthemusictree/ui");
2151
+ var import_genre_tree17 = require("@behindthemusictree/app-kit/genre-tree");
2152
+
2153
+ // src/genre-tree/song/SongPanelContent.tsx
2154
+ var import_react16 = require("react");
2155
+ var import_lucide_react11 = require("lucide-react");
2144
2156
  var import_ui12 = require("@behindthemusictree/ui");
2157
+ var import_player11 = require("@behindthemusictree/app-kit/player");
2158
+ var import_genre_tree16 = require("@behindthemusictree/app-kit/genre-tree");
2145
2159
  var import_jsx_runtime32 = require("react/jsx-runtime");
2160
+ var numberFormat = new Intl.NumberFormat("en");
2161
+ var dateFormat = new Intl.DateTimeFormat("en", { dateStyle: "medium" });
2162
+ var externalLink = "text-[var(--color-primary)] hover:underline focus-visible:underline";
2163
+ function SongHistory({ uuid, getBackendBaseUrl }) {
2164
+ const [open, setOpen] = (0, import_react16.useState)(false);
2165
+ const history = (0, import_genre_tree16.useSongHistory)(uuid, getBackendBaseUrl, open);
2166
+ let body = null;
2167
+ if (history.status === "loading") body = /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_ui12.Skeleton, { className: "h-10", "aria-busy": "true" });
2168
+ else if (history.status === "error") body = /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("p", { className: "text-red-600", children: "Could not load the history." });
2169
+ else if (history.status === "success")
2170
+ body = history.entries.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "No changes yet." }) : /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("ul", { className: "divide-y divide-[var(--color-border)]", children: history.entries.map((entry) => /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("li", { className: "py-1.5 text-sm", children: [
2171
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("span", { className: "font-medium", children: entry.action }),
2172
+ entry.oldValue !== null || entry.newValue !== null ? /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
2173
+ " ",
2174
+ "\u2014 ",
2175
+ entry.oldValue ?? "none",
2176
+ " \u2192 ",
2177
+ entry.newValue ?? "none"
2178
+ ] }) : null,
2179
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("span", { className: "block text-xs text-[var(--color-text-muted)]", children: [
2180
+ entry.actorPseudo ?? "Anonymous",
2181
+ " \xB7 ",
2182
+ dateFormat.format(new Date(entry.createdOn))
2183
+ ] })
2184
+ ] }, entry.uuid)) });
2185
+ return /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(
2186
+ "details",
2187
+ {
2188
+ onToggle: (event) => setOpen(event.currentTarget.open),
2189
+ className: "px-6 pb-2.5 mt-2.5 border-t border-[var(--color-border)]",
2190
+ children: [
2191
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("summary", { className: "mt-2.5 cursor-pointer text-[0.6875rem] font-semibold uppercase text-[var(--color-text-muted)]", children: "History" }),
2192
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("div", { className: "mt-1.5", children: body })
2193
+ ]
2194
+ }
2195
+ );
2196
+ }
2197
+ function SongPanelContent({
2198
+ song,
2199
+ scope,
2200
+ getBackendBaseUrl,
2201
+ shareUrl,
2202
+ renderArtistLink,
2203
+ genrePath,
2204
+ onSelectGenre
2205
+ }) {
2206
+ const { playNewTrackListFromTrackUuid } = (0, import_genre_tree16.useTrackList)();
2207
+ const { playerTrackObject, playState, handlePlayPauseAction } = (0, import_player11.usePlayer)();
2208
+ const unplayableReason = (0, import_genre_tree16.songUnplayableReason)(song);
2209
+ const isCurrent = playerTrackObject?.track.id === song.uuid;
2210
+ const isPlaying = isCurrent && playState === import_player11.PlayStates.PLAYING;
2211
+ const shareTarget = { kind: "song", uuid: song.uuid, name: song.title, scope };
2212
+ const musicBrainzUrl = (0, import_genre_tree16.musicBrainzRecordingUrl)(song);
2213
+ const fields = [];
2214
+ if (song.genre) {
2215
+ const path = genrePath.length > 0 ? genrePath : null;
2216
+ fields.push({
2217
+ label: "Genre",
2218
+ value: path ? /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("span", { className: "flex flex-wrap items-center gap-x-1", children: path.map((genrePlaylist, index) => /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("span", { children: [
2219
+ index > 0 && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("span", { "aria-hidden": "true", children: "\u203A " }),
2220
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("button", { type: "button", onClick: () => onSelectGenre(genrePlaylist), className: "hover:underline", children: genrePlaylist.name })
2221
+ ] }, genrePlaylist.uuid)) }) : song.genre.name
2222
+ });
2223
+ } else {
2224
+ fields.push({ label: "Genre", value: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("span", { className: "text-[var(--color-text-muted)]", children: "No genre" }) });
2225
+ }
2226
+ if (song.firstReleaseYear) fields.push({ label: "First released", value: song.firstReleaseYear });
2227
+ const viewCount = song.youtubeVideo?.viewCount;
2228
+ if (viewCount !== null && viewCount !== void 0)
2229
+ fields.push({ label: "YouTube views", value: numberFormat.format(viewCount) });
2230
+ fields.push({ label: "Plays", value: numberFormat.format(song.playCount) });
2231
+ const links = [];
2232
+ if (musicBrainzUrl)
2233
+ links.push(
2234
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("a", { href: musicBrainzUrl, target: "_blank", rel: "noopener noreferrer", className: externalLink, children: "MusicBrainz" }, "mb")
2235
+ );
2236
+ if (song.youtubeVideo)
2237
+ links.push(
2238
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
2239
+ "a",
2240
+ {
2241
+ href: `https://www.youtube.com/watch?v=${encodeURIComponent(song.youtubeVideo.id)}`,
2242
+ target: "_blank",
2243
+ rel: "noopener noreferrer",
2244
+ className: externalLink,
2245
+ children: "YouTube"
2246
+ },
2247
+ "yt"
2248
+ )
2249
+ );
2250
+ if (links.length > 0)
2251
+ fields.push({
2252
+ label: "Links",
2253
+ value: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("span", { className: "flex gap-3", children: links })
2254
+ });
2255
+ return /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(import_jsx_runtime32.Fragment, { children: [
2256
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: [
2257
+ unplayableReason ? /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("p", { className: "flex items-center px-2.5 text-sm text-[var(--color-text-muted)]", children: unplayableReason }) : /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(
2258
+ "button",
2259
+ {
2260
+ type: "button",
2261
+ onClick: () => isCurrent ? handlePlayPauseAction() : playNewTrackListFromTrackUuid(song, scope),
2262
+ className: "group flex flex-col items-center gap-1 w-16 text-xs text-center text-[var(--color-text)] focus-visible:outline-none",
2263
+ children: [
2264
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("span", { className: "flex items-center justify-center w-10 h-10 rounded-full group-focus-visible:ring-2 group-focus-visible:ring-[var(--color-focus-ring)] bg-[var(--color-surface-muted)] transition-colors group-hover:bg-[var(--color-border)]", children: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_lucide_react11.Pause, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_lucide_react11.Play, { "aria-hidden": "true", className: "w-5 h-5" }) }),
2265
+ isPlaying ? "Pause" : "Play"
2266
+ ]
2267
+ }
2268
+ ),
2269
+ shareUrl && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl })
2270
+ ] }) }),
2271
+ song.artists && song.artists.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("p", { className: "px-6 pt-2.5 text-[var(--color-text-muted)]", children: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_genre_tree16.ArtistNames, { artists: song.artists, renderArtistLink }) }),
2272
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_ui12.FieldList, { items: fields }),
2273
+ song.essentialGenres && song.essentialGenres.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_ui12.PanelSection, { title: "Essential in", children: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("ul", { children: song.essentialGenres.map((genre) => /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("li", { className: "py-0.5", children: genre.name }, genre.uuid)) }) }),
2274
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(SongHistory, { uuid: song.uuid, getBackendBaseUrl })
2275
+ ] });
2276
+ }
2277
+
2278
+ // src/genre-tree/song/useSongPanel.tsx
2279
+ var import_jsx_runtime33 = require("react/jsx-runtime");
2280
+ function useSongPanel({
2281
+ uuid,
2282
+ scope,
2283
+ getBackendBaseUrl,
2284
+ getShareUrl,
2285
+ renderArtistLink,
2286
+ getGenrePath,
2287
+ onSelectGenre,
2288
+ expanded,
2289
+ onExpandedChange,
2290
+ onClose
2291
+ }) {
2292
+ const result = (0, import_genre_tree17.useSong)(uuid, getBackendBaseUrl);
2293
+ const panel = { expanded, onExpandedChange, onClose };
2294
+ switch (result.status) {
2295
+ case "idle":
2296
+ return void 0;
2297
+ case "loading":
2298
+ return {
2299
+ ...panel,
2300
+ title: "Song",
2301
+ content: /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
2302
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_ui13.Skeleton, { className: "h-3 w-16" }),
2303
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_ui13.Skeleton, { className: "h-4 w-32" }),
2304
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_ui13.Skeleton, { className: "h-3 w-16" }),
2305
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_ui13.Skeleton, { className: "h-4 w-24" })
2306
+ ] })
2307
+ };
2308
+ case "not-found":
2309
+ return {
2310
+ ...panel,
2311
+ title: "Song not found",
2312
+ content: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This song does not exist." })
2313
+ };
2314
+ case "error":
2315
+ return {
2316
+ ...panel,
2317
+ title: "Song",
2318
+ content: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this song." })
2319
+ };
2320
+ case "success": {
2321
+ const { song } = result;
2322
+ const shareUrl = getShareUrl?.({ kind: "song", uuid: song.uuid, name: song.title, scope });
2323
+ return {
2324
+ ...panel,
2325
+ title: song.title,
2326
+ content: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2327
+ SongPanelContent,
2328
+ {
2329
+ song,
2330
+ scope,
2331
+ getBackendBaseUrl,
2332
+ shareUrl: shareUrl ?? void 0,
2333
+ renderArtistLink,
2334
+ genrePath: song.genre ? getGenrePath(song.genre) : [],
2335
+ onSelectGenre
2336
+ }
2337
+ )
2338
+ };
2339
+ }
2340
+ }
2341
+ }
2342
+
2343
+ // src/genre-tree/SizeMetricToggle.tsx
2344
+ var import_md = require("react-icons/md");
2345
+ var import_ui14 = require("@behindthemusictree/ui");
2346
+ var import_jsx_runtime34 = require("react/jsx-runtime");
2146
2347
  function SizeMetricToggle({
2147
2348
  value,
2148
2349
  onChange
2149
2350
  }) {
2150
- return /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
2151
- import_ui12.SegmentedControl,
2351
+ return /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2352
+ import_ui14.SegmentedControl,
2152
2353
  {
2354
+ variant: "floating",
2153
2355
  "aria-label": "Size nodes by",
2154
2356
  value,
2155
2357
  onChange,
2156
2358
  options: [
2157
- { value: "songs", label: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_md.MdMusicNote, { size: 18, "aria-hidden": true }), "aria-label": "Size by songs", title: "Node size by song count" },
2158
- { value: "views", label: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_md.MdVisibility, { size: 18, "aria-hidden": true }), "aria-label": "Size by views", title: "Node size by song views" }
2359
+ { value: "songs", label: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_md.MdMusicNote, { size: 18, "aria-hidden": true }), "aria-label": "Size by songs", title: "Node size by song count" },
2360
+ { value: "views", label: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_md.MdVisibility, { size: 18, "aria-hidden": true }), "aria-label": "Size by views", title: "Node size by song views" }
2159
2361
  ]
2160
2362
  }
2161
2363
  );
2162
2364
  }
2163
2365
 
2164
2366
  // src/genre-tree/GenreTreeView.tsx
2165
- var import_jsx_runtime33 = require("react/jsx-runtime");
2367
+ var import_jsx_runtime35 = require("react/jsx-runtime");
2166
2368
  var PREVIOUS_BUTTON_CSS = `
2167
2369
  .genre-tree-view .search-overlay .gtv-info-panel-previous { display: none; }
2168
2370
  .genre-tree-view:has(.search-overlay .gtv-info-panel-previous) .genre-search input { padding-left: 2.75rem; }
@@ -2206,6 +2408,13 @@ function GenreTreeView({
2206
2408
  artistSearch = true,
2207
2409
  onArtistSelect,
2208
2410
  artistSearchBaseUrl,
2411
+ songUuid: controlledSongUuid,
2412
+ onSongUuidChange,
2413
+ songExpanded: controlledSongExpanded,
2414
+ onSongExpandedChange,
2415
+ songSearch = true,
2416
+ onSongSelect,
2417
+ renderSongLink,
2209
2418
  renderSummary,
2210
2419
  renderEssentialTracks,
2211
2420
  renderSongLeading,
@@ -2220,14 +2429,21 @@ function GenreTreeView({
2220
2429
  placement = "default",
2221
2430
  ...focus
2222
2431
  }) {
2223
- const getShareUrlRef = (0, import_react15.useRef)(getShareUrl);
2432
+ const getShareUrlRef = (0, import_react17.useRef)(getShareUrl);
2224
2433
  getShareUrlRef.current = getShareUrl;
2225
- const [internalArtistUuid, setInternalArtistUuid] = (0, import_react15.useState)(null);
2434
+ const [internalArtistUuid, setInternalArtistUuid] = (0, import_react17.useState)(null);
2226
2435
  const isArtistControlled = controlledArtistUuid !== void 0;
2227
2436
  const artistUuid = isArtistControlled ? controlledArtistUuid : internalArtistUuid;
2228
- const [internalSizeMetric, setInternalSizeMetric] = (0, import_react15.useState)("songs");
2437
+ const [internalSongUuid, setInternalSongUuid] = (0, import_react17.useState)(null);
2438
+ const isSongControlled = controlledSongUuid !== void 0;
2439
+ const songUuid = isSongControlled ? controlledSongUuid : internalSongUuid;
2440
+ const changeSong = (uuid) => {
2441
+ if (!isSongControlled) setInternalSongUuid(uuid);
2442
+ onSongUuidChange?.(uuid);
2443
+ };
2444
+ const [internalSizeMetric, setInternalSizeMetric] = (0, import_react17.useState)("songs");
2229
2445
  const sizeMetric = controlledSizeMetric ?? internalSizeMetric;
2230
- const deferredSizeMetric = (0, import_react15.useDeferredValue)(sizeMetric);
2446
+ const deferredSizeMetric = (0, import_react17.useDeferredValue)(sizeMetric);
2231
2447
  const changeSizeMetric = (metric) => {
2232
2448
  if (controlledSizeMetric === void 0) setInternalSizeMetric(metric);
2233
2449
  onSizeMetricChange?.(metric);
@@ -2254,7 +2470,7 @@ function GenreTreeView({
2254
2470
  allowWheelRotation: internalAllowWheelRotation,
2255
2471
  toggleWheelRotation,
2256
2472
  onNodeHover: handleNodeHover
2257
- } = (0, import_genre_tree17.useGenreTreeViewState)({
2473
+ } = (0, import_genre_tree19.useGenreTreeViewState)({
2258
2474
  scope,
2259
2475
  getBackendBaseUrl,
2260
2476
  viewMode: controlledViewMode,
@@ -2264,7 +2480,7 @@ function GenreTreeView({
2264
2480
  artistUuid
2265
2481
  });
2266
2482
  const restoreRevision = viewportRestore?.revision;
2267
- const [stackedRestoreOwner, setStackedRestoreOwner] = (0, import_react15.useState)({ revision: restoreRevision, rootUuid: selectedGenrePlaylist?.root.uuid });
2483
+ const [stackedRestoreOwner, setStackedRestoreOwner] = (0, import_react17.useState)({ revision: restoreRevision, rootUuid: selectedGenrePlaylist?.root.uuid });
2268
2484
  let restoreRootUuid = stackedRestoreOwner.rootUuid;
2269
2485
  if (stackedRestoreOwner.revision !== restoreRevision) {
2270
2486
  restoreRootUuid = selectedGenrePlaylist?.root.uuid;
@@ -2273,14 +2489,14 @@ function GenreTreeView({
2273
2489
  restoreRootUuid = selectedGenrePlaylist.root.uuid;
2274
2490
  setStackedRestoreOwner({ revision: restoreRevision, rootUuid: restoreRootUuid });
2275
2491
  }
2276
- const isGenreUpdating = (0, import_react_query3.useIsMutating)({ mutationKey: import_genre_tree17.genreUpdateMutationKey }) > 0;
2277
- const { mutate: updateGenre } = (0, import_genre_tree17.useUpdateGenre)(scope, getBackendBaseUrl);
2492
+ const isGenreUpdating = (0, import_react_query3.useIsMutating)({ mutationKey: import_genre_tree19.genreUpdateMutationKey }) > 0;
2493
+ const { mutate: updateGenre } = (0, import_genre_tree19.useUpdateGenre)(scope, getBackendBaseUrl);
2278
2494
  const busy = deferredSizeMetric !== sizeMetric || isTreeFetching || isGenreUpdating;
2279
2495
  const isArtistFiltered = artistUuid !== null;
2280
2496
  const canSizeByViews = !isArtistFiltered && hasViewsCount(visibleGenrePlaylists);
2281
- const artistResult = (0, import_genre_tree17.useArtist)(artistUuid, artistSearchBaseUrl ?? getBackendBaseUrl);
2497
+ const artistResult = (0, import_genre_tree19.useArtist)(artistUuid, artistSearchBaseUrl ?? getBackendBaseUrl);
2282
2498
  const filterArtistName = artistResult.status === "success" ? artistResult.artist.name : null;
2283
- const filterArtist = (0, import_react15.useMemo)(
2499
+ const filterArtist = (0, import_react17.useMemo)(
2284
2500
  () => artistUuid !== null && filterArtistName !== null ? { uuid: artistUuid, name: filterArtistName } : null,
2285
2501
  [artistUuid, filterArtistName]
2286
2502
  );
@@ -2288,27 +2504,72 @@ function GenreTreeView({
2288
2504
  const playback = useGenrePlaylistPlayback({ scope, genrePlaylists: visibleGenrePlaylists, artist: filterArtist });
2289
2505
  const openArtist = (uuid) => {
2290
2506
  if (selectedNodeId !== null) clearSelection();
2507
+ if (songUuid !== null) changeSong(null);
2291
2508
  changeArtist(uuid);
2292
2509
  };
2293
- const [expandedArtistUuid, setExpandedArtistUuid] = (0, import_react15.useState)(null);
2510
+ const openSong = (uuid) => {
2511
+ if (selectedNodeId !== null) clearSelection();
2512
+ changeSong(uuid);
2513
+ };
2514
+ const openSongRef = (0, import_react17.useRef)(openSong);
2515
+ openSongRef.current = openSong;
2516
+ const resolvedRenderSongLink = (0, import_react17.useCallback)(
2517
+ (song) => renderSongLink ? renderSongLink(song) : /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("button", { type: "button", onClick: () => openSongRef.current(song.uuid), className: "hover:underline", children: song.title }),
2518
+ [renderSongLink]
2519
+ );
2520
+ const [expandedSongUuid, setExpandedSongUuid] = (0, import_react17.useState)(null);
2521
+ const [expandedArtistUuid, setExpandedArtistUuid] = (0, import_react17.useState)(null);
2294
2522
  const artistBaseUrl = artistSearchBaseUrl ?? getBackendBaseUrl;
2523
+ const songBaseUrl = getBackendBaseUrl;
2295
2524
  const artistPanel = useArtistPanel({
2296
2525
  uuid: artistUuid,
2297
2526
  scope,
2298
2527
  getBackendBaseUrl: artistBaseUrl,
2299
2528
  getShareUrl: getShareUrlRef.current,
2300
- renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name })),
2529
+ renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name })),
2301
2530
  expanded: controlledArtistExpanded ?? (artistUuid !== null && expandedArtistUuid === artistUuid),
2302
2531
  onExpandedChange: (on) => {
2303
2532
  if (controlledArtistExpanded === void 0) setExpandedArtistUuid(on ? artistUuid : null);
2304
2533
  onArtistExpandedChange?.(on);
2305
2534
  },
2535
+ renderSongLink: resolvedRenderSongLink,
2306
2536
  onClose: () => changeArtist(null)
2307
2537
  });
2308
- const builtInArtistPanel = overlayPanel || hasSelectedGenreNode ? void 0 : artistPanel;
2309
- const activeOverlayPanel = overlayPanel ?? (builtInArtistPanel && { ...builtInArtistPanel, hideTitle: true, hideClose: true });
2310
- const [uncontrolledCollapsed, setUncontrolledCollapsed] = (0, import_react15.useState)(false);
2311
- const [uncontrolledExpanded, setUncontrolledExpanded] = (0, import_react15.useState)(false);
2538
+ const resolvedRenderArtistLink = renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name }));
2539
+ const getGenrePath = (genre) => {
2540
+ const path = [];
2541
+ let current = visibleGenrePlaylists.find((playlist) => playlist.criteria?.uuid === genre.uuid);
2542
+ while (current) {
2543
+ path.unshift(current);
2544
+ const parentUuid = current.parent?.uuid;
2545
+ current = parentUuid ? visibleGenrePlaylists.find((playlist) => playlist.uuid === parentUuid) : void 0;
2546
+ }
2547
+ return path;
2548
+ };
2549
+ const songPanel = useSongPanel({
2550
+ uuid: songUuid,
2551
+ scope,
2552
+ getBackendBaseUrl: songBaseUrl,
2553
+ getShareUrl: getShareUrlRef.current,
2554
+ renderArtistLink: resolvedRenderArtistLink,
2555
+ getGenrePath,
2556
+ onSelectGenre: (genrePlaylist) => {
2557
+ changeSong(null);
2558
+ selectGenrePlaylist(genrePlaylist);
2559
+ },
2560
+ expanded: controlledSongExpanded ?? (songUuid !== null && expandedSongUuid === songUuid),
2561
+ onExpandedChange: (on) => {
2562
+ if (controlledSongExpanded === void 0) setExpandedSongUuid(on ? songUuid : null);
2563
+ onSongExpandedChange?.(on);
2564
+ },
2565
+ onClose: () => changeSong(null)
2566
+ });
2567
+ const builtInSongPanel = overlayPanel || hasSelectedGenreNode ? void 0 : songPanel;
2568
+ const builtInArtistPanel = overlayPanel || hasSelectedGenreNode || builtInSongPanel ? void 0 : artistPanel;
2569
+ const builtInPanel = builtInSongPanel ?? builtInArtistPanel;
2570
+ const activeOverlayPanel = overlayPanel ?? (builtInPanel && { ...builtInPanel, hideTitle: true, hideClose: true });
2571
+ const [uncontrolledCollapsed, setUncontrolledCollapsed] = (0, import_react17.useState)(false);
2572
+ const [uncontrolledExpanded, setUncontrolledExpanded] = (0, import_react17.useState)(false);
2312
2573
  const isCollapsed = collapsed ?? uncontrolledCollapsed;
2313
2574
  const isExpanded = expanded ?? uncontrolledExpanded;
2314
2575
  const changeCollapsed = (next) => {
@@ -2319,9 +2580,9 @@ function GenreTreeView({
2319
2580
  setUncontrolledExpanded(next);
2320
2581
  onExpandedChange?.(next);
2321
2582
  };
2322
- (0, import_react15.useEffect)(() => {
2583
+ (0, import_react17.useEffect)(() => {
2323
2584
  setUncontrolledCollapsed(false);
2324
- }, [selectedNodeId, artistUuid]);
2585
+ }, [selectedNodeId, artistUuid, songUuid]);
2325
2586
  const isDetailHidden = isCollapsed || !hasSelectedGenreNode && !activeOverlayPanel;
2326
2587
  useLeftDetailPanelLayout(!isDetailHidden, placement === "regions");
2327
2588
  const detailOverlay = placement === "regions" && activeOverlayPanel?.onExpandedChange ? activeOverlayPanel : void 0;
@@ -2335,14 +2596,14 @@ function GenreTreeView({
2335
2596
  hideExpandButton: placement === "regions" ? true : void 0,
2336
2597
  overlayPanel: detailOverlay ? { ...detailOverlay, collapsed: isCollapsed, hideExpandButton: true } : activeOverlayPanel
2337
2598
  };
2338
- const [isWheelRevealed, setIsWheelRevealed] = (0, import_react15.useState)(false);
2599
+ const [isWheelRevealed, setIsWheelRevealed] = (0, import_react17.useState)(false);
2339
2600
  const usesHandoff = viewMode === "wheel" || viewMode === "pop-core";
2340
2601
  const isTreeReady = !isLoading && (!usesHandoff || isWheelRevealed);
2341
- const rootRef = (0, import_react15.useRef)(null);
2602
+ const rootRef = (0, import_react17.useRef)(null);
2342
2603
  const handlePrevious = () => {
2343
2604
  rootRef.current?.querySelector(".search-overlay .gtv-info-panel-previous")?.click();
2344
2605
  };
2345
- (0, import_react15.useEffect)(() => {
2606
+ (0, import_react17.useEffect)(() => {
2346
2607
  if (!isTreeReady || viewMode !== "pop-core" || !hasSelectedGenreNode || !selectedGenrePlaylist) return;
2347
2608
  const root = rootRef.current;
2348
2609
  const centerChip = root?.querySelector(".gtv-wheel-center-node .gtv-wheel-chip");
@@ -2352,21 +2613,22 @@ function GenreTreeView({
2352
2613
  centerChip.click();
2353
2614
  }, [hasSelectedGenreNode, isTreeReady, selectedGenrePlaylist, viewMode]);
2354
2615
  const { showPopup, hidePopup } = (0, import_popup5.usePopup)();
2355
- const [searchText, setSearchText] = (0, import_react15.useState)("");
2356
- const artistSearchResult = (0, import_genre_tree16.useArtistSearch)(artistBaseUrl, artistSearch ? searchText : "");
2357
- const artistExtraResults = artistSearch ? {
2616
+ const [searchText, setSearchText] = (0, import_react17.useState)("");
2617
+ const artistSearchResult = (0, import_genre_tree18.useArtistSearch)(artistBaseUrl, artistSearch ? searchText : "");
2618
+ const artistGroup = artistSearch ? {
2619
+ key: "artists",
2358
2620
  label: "Artists",
2359
2621
  items: artistSearchResult.artists.map((artist) => ({
2360
2622
  key: artist.uuid,
2361
- content: /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(import_jsx_runtime33.Fragment, { children: [
2362
- /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("span", { className: "block truncate font-medium", children: [
2623
+ content: /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(import_jsx_runtime35.Fragment, { children: [
2624
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("span", { className: "block truncate font-medium", children: [
2363
2625
  artist.name,
2364
- artist.disambiguation && /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("span", { className: "font-normal text-[var(--color-neutral-500)]", children: [
2626
+ artist.disambiguation && /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("span", { className: "font-normal text-[var(--color-neutral-500)]", children: [
2365
2627
  " \xB7 ",
2366
2628
  artist.disambiguation
2367
2629
  ] })
2368
2630
  ] }),
2369
- artist.matchedAlias && /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("span", { className: "block truncate text-xs text-[var(--color-neutral-500)]", children: [
2631
+ artist.matchedAlias && /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("span", { className: "block truncate text-xs text-[var(--color-neutral-500)]", children: [
2370
2632
  "also known as ",
2371
2633
  artist.matchedAlias
2372
2634
  ] })
@@ -2379,11 +2641,32 @@ function GenreTreeView({
2379
2641
  status: artistSearchResult.isError ? "Search failed" : artistSearchResult.settled && artistSearchResult.artists.length === 0 ? "No artists found" : "",
2380
2642
  loading: artistSearchResult.pending
2381
2643
  } : void 0;
2644
+ const songSearchResult = (0, import_genre_tree18.useSongSearch)(songBaseUrl, songSearch ? searchText : "");
2645
+ const songGroup = songSearch ? {
2646
+ key: "songs",
2647
+ label: "Songs",
2648
+ items: songSearchResult.songs.map((song) => ({
2649
+ key: song.uuid,
2650
+ content: /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("span", { className: (0, import_genre_tree18.isSongSearchResultPlayable)(song) ? void 0 : "opacity-60", children: [
2651
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("span", { className: "block truncate font-medium", children: song.title }),
2652
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("span", { className: "block truncate text-xs text-[var(--color-neutral-500)]", children: [song.artists.map((artist) => artist.name).join(", "), song.genre?.name ?? "No genre"].filter(Boolean).join(" \xB7 ") })
2653
+ ] }),
2654
+ onSelect: () => {
2655
+ openSong(song.uuid);
2656
+ onSongSelect?.(song);
2657
+ }
2658
+ })),
2659
+ status: songSearchResult.isError ? "Search failed" : songSearchResult.settled && songSearchResult.songs.length === 0 ? "No songs found" : "",
2660
+ loading: songSearchResult.pending
2661
+ } : void 0;
2662
+ const extraGroups = [artistGroup, songGroup].filter((group) => group !== void 0);
2382
2663
  const handleSelectedNodeChange = (node) => {
2664
+ if (node !== null && songUuid !== null) changeSong(null);
2383
2665
  selectNode(node);
2384
2666
  };
2385
2667
  const handleGenreSearchClear = clearSelection;
2386
2668
  const handleGenreSearchSelect = (genrePlaylist) => {
2669
+ if (songUuid !== null) changeSong(null);
2387
2670
  selectGenrePlaylist(genrePlaylist);
2388
2671
  };
2389
2672
  const selectedUuid = selectedGenrePlaylist?.uuid;
@@ -2392,13 +2675,13 @@ function GenreTreeView({
2392
2675
  const handleGenreSearchZoomIn = () => {
2393
2676
  if (selectedUuid) focus.onFocusGenreChange?.(selectedUuid, "zoom-in");
2394
2677
  };
2395
- const zoomInRef = (0, import_react15.useRef)(void 0);
2678
+ const zoomInRef = (0, import_react17.useRef)(void 0);
2396
2679
  zoomInRef.current = canZoomIn ? handleGenreSearchZoomIn : void 0;
2397
- const renderInfoPanelBody = (0, import_react15.useCallback)(
2680
+ const renderInfoPanelBody = (0, import_react17.useCallback)(
2398
2681
  (node, { toolbar, ancestors, family }) => {
2399
2682
  const genrePlaylist = genrePlaylistByNodeId.get(node.id);
2400
2683
  const criteria = genrePlaylist?.criteria;
2401
- if (!genrePlaylist || !criteria) return { primary: /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(import_jsx_runtime33.Fragment, { children: [
2684
+ if (!genrePlaylist || !criteria) return { primary: /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(import_jsx_runtime35.Fragment, { children: [
2402
2685
  toolbar,
2403
2686
  family
2404
2687
  ] }), aside: ancestors };
@@ -2418,7 +2701,7 @@ function GenreTreeView({
2418
2701
  (sibling) => sibling.parent?.uuid === parentPlaylist.uuid && sibling.uuid !== genrePlaylist.uuid && sibling.criteria?.side === "pop"
2419
2702
  ) : void 0;
2420
2703
  return {
2421
- primary: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2704
+ primary: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2422
2705
  GenreInfoPanelBody,
2423
2706
  {
2424
2707
  toolbar,
@@ -2437,7 +2720,7 @@ function GenreTreeView({
2437
2720
  onAddChild: readOnly ? void 0 : () => handleGenreCreationAction(criteria),
2438
2721
  onRename: readOnly ? void 0 : () => handleGenreRenameAction(criteria),
2439
2722
  onChangeParent: readOnly ? void 0 : () => showPopup(
2440
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2723
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2441
2724
  ChangeParentPopup,
2442
2725
  {
2443
2726
  genrePlaylist: { ...genrePlaylist, criteria },
@@ -2458,6 +2741,7 @@ function GenreTreeView({
2458
2741
  criteriaOverviewSchema,
2459
2742
  renderGenreDetailExtras,
2460
2743
  renderArtistLink,
2744
+ renderSongLink: resolvedRenderSongLink,
2461
2745
  artist: filterArtist,
2462
2746
  renderSummary,
2463
2747
  renderEssentialTracks,
@@ -2488,6 +2772,7 @@ function GenreTreeView({
2488
2772
  criteriaOverviewSchema,
2489
2773
  renderGenreDetailExtras,
2490
2774
  renderArtistLink,
2775
+ resolvedRenderSongLink,
2491
2776
  renderSummary,
2492
2777
  renderEssentialTracks,
2493
2778
  renderSongLeading,
@@ -2497,7 +2782,7 @@ function GenreTreeView({
2497
2782
  );
2498
2783
  const isRotationControlled = controlledAllowWheelRotation !== void 0;
2499
2784
  const allowWheelRotation = controlledAllowWheelRotation ?? internalAllowWheelRotation;
2500
- const detailTab = placement === "regions" && (hasSelectedGenreNode || detailOverlay) ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2785
+ const detailTab = placement === "regions" && (hasSelectedGenreNode || detailOverlay) ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2501
2786
  PanelEdgeTab,
2502
2787
  {
2503
2788
  side: "left",
@@ -2507,8 +2792,8 @@ function GenreTreeView({
2507
2792
  ) : null;
2508
2793
  const renderArtistFilter = () => {
2509
2794
  if (isLoading || !filterArtist) return null;
2510
- return /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-sm", children: [
2511
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2795
+ return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-sm", children: [
2796
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2512
2797
  "button",
2513
2798
  {
2514
2799
  type: "button",
@@ -2520,7 +2805,7 @@ function GenreTreeView({
2520
2805
  children: filterArtist.name
2521
2806
  }
2522
2807
  ),
2523
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2808
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2524
2809
  "button",
2525
2810
  {
2526
2811
  type: "button",
@@ -2532,23 +2817,23 @@ function GenreTreeView({
2532
2817
  )
2533
2818
  ] });
2534
2819
  };
2535
- const actions = /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(import_jsx_runtime33.Fragment, { children: [
2536
- !isLoading && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2820
+ const actions = /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(import_jsx_runtime35.Fragment, { children: [
2821
+ !isLoading && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2537
2822
  GenreSearch,
2538
2823
  {
2539
2824
  genrePlaylists: visibleGenrePlaylists,
2540
2825
  onSelect: handleGenreSearchSelect,
2541
- selectedName: selectedGenrePlaylist?.name ?? builtInArtistPanel?.title ?? null,
2542
- onClear: builtInArtistPanel ? () => changeArtist(null) : handleGenreSearchClear,
2826
+ selectedName: selectedGenrePlaylist?.name ?? builtInPanel?.title ?? null,
2827
+ onClear: builtInSongPanel ? () => changeSong(null) : builtInArtistPanel ? () => changeArtist(null) : handleGenreSearchClear,
2543
2828
  onPrevious: handlePrevious,
2544
- onQueryChange: artistSearch ? setSearchText : void 0,
2545
- extraResults: artistExtraResults
2829
+ onQueryChange: artistSearch || songSearch ? setSearchText : void 0,
2830
+ extraGroups
2546
2831
  }
2547
2832
  ),
2548
- !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
2549
- /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("div", { className: "contents max-md:flex max-md:w-full max-md:items-center max-md:gap-2 max-md:overflow-x-auto max-md:[scrollbar-width:none] max-md:[&>*]:shrink-0", children: [
2550
- !isLoading && !isControlled && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2551
- import_ui13.SegmentedControl,
2833
+ !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
2834
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "contents max-md:flex max-md:w-full max-md:items-center max-md:gap-2 max-md:overflow-x-auto max-md:[scrollbar-width:none] max-md:[&>*]:shrink-0", children: [
2835
+ !isLoading && !isControlled && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2836
+ import_ui15.SegmentedControl,
2552
2837
  {
2553
2838
  "aria-label": "Tree view mode",
2554
2839
  value: viewMode,
@@ -2561,14 +2846,14 @@ function GenreTreeView({
2561
2846
  ]
2562
2847
  }
2563
2848
  ),
2564
- !isLoading && showDisplayOptions && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2849
+ !isLoading && showDisplayOptions && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2565
2850
  "div",
2566
2851
  {
2567
2852
  className: "flex items-center gap-1",
2568
2853
  role: "group",
2569
2854
  "aria-label": "Tree display options",
2570
- children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2571
- import_ui13.Button,
2855
+ children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2856
+ import_ui15.Button,
2572
2857
  {
2573
2858
  variant: allowWheelRotation ? "default" : "outline",
2574
2859
  size: "sm",
@@ -2578,52 +2863,52 @@ function GenreTreeView({
2578
2863
  )
2579
2864
  }
2580
2865
  ),
2581
- !isLoading && !readOnly && showAddRoot && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2582
- import_ui13.IconTextButton,
2866
+ !isLoading && !readOnly && showAddRoot && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2867
+ import_ui15.IconTextButton,
2583
2868
  {
2584
- icon: import_lucide_react11.Plus,
2869
+ icon: import_lucide_react12.Plus,
2585
2870
  text: "Add root",
2586
2871
  onClick: () => handleGenreCreationAction(null)
2587
2872
  }
2588
2873
  )
2589
2874
  ] })
2590
2875
  ] });
2591
- return /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(
2876
+ return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
2592
2877
  "div",
2593
2878
  {
2594
2879
  ref: rootRef,
2595
2880
  className: ["genre-tree-view relative flex flex-col h-full", placement === "regions" && REGIONS_CLASS, className2].filter(Boolean).join(" "),
2596
2881
  children: [
2597
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("style", { children: PREVIOUS_BUTTON_CSS }),
2598
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2882
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("style", { children: PREVIOUS_BUTTON_CSS }),
2883
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2599
2884
  "div",
2600
2885
  {
2601
2886
  className: `actions-container absolute z-30 flex items-center gap-2 ${placement === "regions" ? "left-[calc(var(--layout-gutter)+var(--layout-panel-inset))] top-[calc(var(--layout-gutter)+var(--layout-panel-inset))] max-md:right-[calc(var(--layout-gutter)+var(--layout-panel-inset))] max-md:top-[var(--layout-band-top)] max-md:flex-col max-md:items-stretch" : "left-5 top-5"}`,
2602
2887
  children: actions
2603
2888
  }
2604
2889
  ),
2605
- !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2890
+ !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2606
2891
  "div",
2607
2892
  {
2608
2893
  className: `absolute left-1/2 z-30 hidden -translate-x-1/2 md:block ${placement === "regions" ? "top-[calc(var(--layout-gutter)+var(--layout-panel-inset))]" : "top-5"}`,
2609
2894
  children: renderArtistFilter()
2610
2895
  }
2611
2896
  ),
2612
- detailTab && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2897
+ detailTab && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2613
2898
  "div",
2614
2899
  {
2615
2900
  className: `absolute top-1/2 z-30 -translate-y-1/2 max-md:hidden transition-[opacity,visibility] duration-150 ease-out ${isTreeReady ? "opacity-100" : "pointer-events-none invisible opacity-0"} ${detailExpanded ? "right-[calc(var(--layout-center-right)-var(--layout-edge-tab-w))]" : isCollapsed ? "left-0" : "left-[calc(var(--layout-gutter)+var(--layout-left-w))]"}`,
2616
2901
  children: detailTab
2617
2902
  }
2618
2903
  ),
2619
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("div", { className: "content-container flex-1 min-h-0 flex flex-row gap-4", children: /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("div", { className: "tree-view-container relative flex-1 min-w-0 flex flex-col h-full", children: [
2620
- !isLoading && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
2621
- isLoading ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_genre_tree_view4.GenreTreeViewSkeleton, { viewMode }) : viewMode === "wheel" ? (
2904
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "content-container flex-1 min-h-0 flex flex-row gap-4", children: /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "tree-view-container relative flex-1 min-w-0 flex flex-col h-full", children: [
2905
+ !isLoading && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
2906
+ isLoading ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_genre_tree_view4.GenreTreeViewSkeleton, { viewMode }) : viewMode === "wheel" ? (
2622
2907
  // The info panel's top edge aligns with the tree's; the search bar floats `--layout-panel-inset` below it,
2623
2908
  // and the header inset (the bar's height plus that inset) extends the header's fill behind the bar,
2624
2909
  // Google Maps style; the 8px bottom padding mirrors the inset above the bar, and the hidden title's height is zeroed.
2625
2910
  // The panel's own Zoom in button is hidden: the search bar carries it.
2626
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 [--gtv-info-panel-header-inset:calc(var(--layout-search-h,2.5rem)+var(--layout-panel-inset,0.5rem))] [&_.gtv-info-panel-header]:pb-2 [&_.gtv-info-panel-title--hidden]:h-0 [&_.gtv-toolbar]:hidden", children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_genre_tree_view4.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2911
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 [--gtv-info-panel-header-inset:calc(var(--layout-search-h,2.5rem)+var(--layout-panel-inset,0.5rem))] [&_.gtv-info-panel-header]:pb-2 [&_.gtv-info-panel-title--hidden]:h-0 [&_.gtv-toolbar]:hidden", children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_genre_tree_view4.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2627
2912
  GenrePlaylistTreeWheel,
2628
2913
  {
2629
2914
  scope,
@@ -2642,9 +2927,9 @@ function GenreTreeView({
2642
2927
  allowWheelRotation,
2643
2928
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
2644
2929
  busy,
2645
- floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("div", { className: "flex items-center gap-2", children: [
2930
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "flex items-center gap-2", children: [
2646
2931
  floatingControlsLeading,
2647
- canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2932
+ canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2648
2933
  ] }) : void 0,
2649
2934
  defaultViewport,
2650
2935
  viewportRestore,
@@ -2654,7 +2939,7 @@ function GenreTreeView({
2654
2939
  ) }) })
2655
2940
  ) : viewMode === "pop-core" ? (
2656
2941
  // Same as the wheel: the header inset extends its fill behind the floating search bar, Google Maps style.
2657
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 [--gtv-info-panel-header-inset:calc(var(--layout-search-h,2.5rem)+var(--layout-panel-inset,0.5rem))] [&_.gtv-info-panel-header]:pb-2 [&_.gtv-info-panel-title--hidden]:h-0 [&_.gtv-toolbar]:hidden [&_.gtv-info-panel-zoom-in]:hidden", children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_genre_tree_view4.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2942
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 [--gtv-info-panel-header-inset:calc(var(--layout-search-h,2.5rem)+var(--layout-panel-inset,0.5rem))] [&_.gtv-info-panel-header]:pb-2 [&_.gtv-info-panel-title--hidden]:h-0 [&_.gtv-toolbar]:hidden [&_.gtv-info-panel-zoom-in]:hidden", children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_genre_tree_view4.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2658
2943
  GenrePlaylistTreeWheelRadialPopCore,
2659
2944
  {
2660
2945
  scope,
@@ -2673,9 +2958,9 @@ function GenreTreeView({
2673
2958
  allowWheelRotation,
2674
2959
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
2675
2960
  busy,
2676
- floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("div", { className: "flex items-center gap-2", children: [
2961
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "flex items-center gap-2", children: [
2677
2962
  floatingControlsLeading,
2678
- canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2963
+ canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2679
2964
  ] }) : void 0,
2680
2965
  defaultViewport,
2681
2966
  viewportRestore,
@@ -2684,7 +2969,7 @@ function GenreTreeView({
2684
2969
  ...focus
2685
2970
  }
2686
2971
  ) }) })
2687
- ) : viewMode === "outline" ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("div", { className: "tree-container flex-1 min-h-0 w-full overflow-y-auto relative [--gtv-info-panel-header-inset:calc(var(--layout-search-h,2.5rem)+var(--layout-panel-inset,0.5rem)+0.75rem)] [&_.gtv-toolbar]:hidden", children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2972
+ ) : viewMode === "outline" ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "tree-container flex-1 min-h-0 w-full overflow-y-auto relative [--gtv-info-panel-header-inset:calc(var(--layout-search-h,2.5rem)+var(--layout-panel-inset,0.5rem)+0.75rem)] [&_.gtv-toolbar]:hidden", children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2688
2973
  GenrePlaylistTreeWheelRadialPopCore,
2689
2974
  {
2690
2975
  outline: true,
@@ -2703,13 +2988,13 @@ function GenreTreeView({
2703
2988
  readOnly,
2704
2989
  ...infoPanelCuration
2705
2990
  }
2706
- ) }) : /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("div", { className: "tree-container relative flex flex-col gap-4 w-full pt-14 overflow-x-auto overflow-y-auto text-[var(--color-text)] [&_.gtv-toolbar]:hidden", children: Object.entries(groupedGenrePlaylistsByRoot).map(
2991
+ ) }) : /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "tree-container relative flex flex-col gap-4 w-full pt-14 overflow-x-auto overflow-y-auto text-[var(--color-text)] [&_.gtv-toolbar]:hidden", children: Object.entries(groupedGenrePlaylistsByRoot).map(
2707
2992
  ([uuid, genrePlaylistTreePerRoot], rootIndex) => {
2708
- return /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2993
+ return /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2709
2994
  "div",
2710
2995
  {
2711
2996
  className: "tree-per-root-container relative shrink-0 h-[28rem] mt-2 mr-16 p-2 bg-[var(--color-neutral-100)] rounded-lg",
2712
- children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2997
+ children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2713
2998
  GenrePlaylistTreePerRoot,
2714
2999
  {
2715
3000
  scope,
@@ -2746,11 +3031,11 @@ function GenreTreeView({
2746
3031
  }
2747
3032
 
2748
3033
  // src/genre-tree/GenreDetailPanel.tsx
2749
- var import_react16 = require("react");
3034
+ var import_react18 = require("react");
2750
3035
  var import_genre_tree_view5 = require("@behindthemusictree/genre-tree-view");
2751
- var import_ui14 = require("@behindthemusictree/ui");
2752
- var import_genre_tree18 = require("@behindthemusictree/app-kit/genre-tree");
2753
- var import_jsx_runtime34 = require("react/jsx-runtime");
3036
+ var import_ui16 = require("@behindthemusictree/ui");
3037
+ var import_genre_tree20 = require("@behindthemusictree/app-kit/genre-tree");
3038
+ var import_jsx_runtime36 = require("react/jsx-runtime");
2754
3039
  var ITEM_COUNT_LABEL = "Songs";
2755
3040
  var RELATIVE_ITEM_COUNT = 1;
2756
3041
  function toNode(genre, parentId) {
@@ -2777,9 +3062,9 @@ function GenreDetailPanel({
2777
3062
  onExpandedChange,
2778
3063
  extras
2779
3064
  }) {
2780
- const { data: detail, isPending, isError } = (0, import_genre_tree18.useFetchGenreDetail)(genreUuid, scope, getBackendBaseUrl);
3065
+ const { data: detail, isPending, isError } = (0, import_genre_tree20.useFetchGenreDetail)(genreUuid, scope, getBackendBaseUrl);
2781
3066
  const playback = useGenrePlaylistPlayback({ scope, genrePlaylists: [] });
2782
- const view = (0, import_react16.useMemo)(() => {
3067
+ const view = (0, import_react18.useMemo)(() => {
2783
3068
  if (!detail) return null;
2784
3069
  const rootColor = (0, import_genre_tree_view5.getGenreTreeColor)(detail.root.wikidataId);
2785
3070
  const relative = (genre, parentId) => ({
@@ -2805,17 +3090,17 @@ function GenreDetailPanel({
2805
3090
  };
2806
3091
  }, [detail]);
2807
3092
  if (isError) {
2808
- return /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { role: "alert", className: "p-4 text-sm text-[var(--color-text)]", children: "This genre could not be loaded." });
3093
+ return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("div", { role: "alert", className: "p-4 text-sm text-[var(--color-text)]", children: "This genre could not be loaded." });
2809
3094
  }
2810
3095
  if (isPending || !detail || !view) {
2811
- return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("div", { className: "flex flex-col gap-3 p-4", "aria-busy": "true", "data-testid": "genre-detail-panel-skeleton", children: [
2812
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_ui14.Skeleton, { className: "h-6 w-2/3" }),
2813
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_ui14.Skeleton, { className: "h-4 w-full" }),
2814
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_ui14.Skeleton, { className: "h-4 w-3/4" })
3096
+ return /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "flex flex-col gap-3 p-4", "aria-busy": "true", "data-testid": "genre-detail-panel-skeleton", children: [
3097
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui16.Skeleton, { className: "h-6 w-2/3" }),
3098
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui16.Skeleton, { className: "h-4 w-full" }),
3099
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui16.Skeleton, { className: "h-4 w-3/4" })
2815
3100
  ] });
2816
3101
  }
2817
3102
  const renderBody = (node, sections) => ({
2818
- primary: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
3103
+ primary: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
2819
3104
  GenreInfoPanelBody,
2820
3105
  {
2821
3106
  toolbar: sections.toolbar,
@@ -2832,7 +3117,7 @@ function GenreDetailPanel({
2832
3117
  ),
2833
3118
  aside: null
2834
3119
  });
2835
- return /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
3120
+ return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
2836
3121
  import_genre_tree_view5.InfoPanel,
2837
3122
  {
2838
3123
  node: view.node,
@@ -2858,13 +3143,13 @@ function GenreDetailPanel({
2858
3143
  var import_genre_tree_view6 = require("@behindthemusictree/genre-tree-view");
2859
3144
 
2860
3145
  // src/genre-tree/track-list-sidebar/TrackListSidebar.tsx
2861
- var import_react17 = require("react");
2862
- var import_lucide_react12 = require("lucide-react");
2863
- var import_genre_tree19 = require("@behindthemusictree/app-kit/genre-tree");
2864
- var import_genre_tree20 = require("@behindthemusictree/app-kit/genre-tree");
3146
+ var import_react19 = require("react");
3147
+ var import_lucide_react13 = require("lucide-react");
3148
+ var import_genre_tree21 = require("@behindthemusictree/app-kit/genre-tree");
3149
+ var import_genre_tree22 = require("@behindthemusictree/app-kit/genre-tree");
2865
3150
  var import_track_noun6 = require("@behindthemusictree/app-kit/track-noun");
2866
- var import_jsx_runtime35 = require("react/jsx-runtime");
2867
- var useBrowserLayoutEffect = typeof window === "undefined" ? import_react17.useEffect : import_react17.useLayoutEffect;
3151
+ var import_jsx_runtime37 = require("react/jsx-runtime");
3152
+ var useBrowserLayoutEffect = typeof window === "undefined" ? import_react19.useEffect : import_react19.useLayoutEffect;
2868
3153
  function TrackListSidebar({
2869
3154
  className: className2 = "",
2870
3155
  renderDuration,
@@ -2873,20 +3158,21 @@ function TrackListSidebar({
2873
3158
  isCollapsed = false,
2874
3159
  onExpand,
2875
3160
  onMinimize,
2876
- renderArtistLink
3161
+ renderArtistLink,
3162
+ renderSongLink
2877
3163
  }) {
2878
- const { trackList, loadMore, autoAdvanceScrollRequest } = (0, import_genre_tree19.useTrackList)();
3164
+ const { trackList, loadMore, autoAdvanceScrollRequest } = (0, import_genre_tree21.useTrackList)();
2879
3165
  const trackNoun = (0, import_track_noun6.useTrackNoun)();
2880
- const trackListRef = (0, import_react17.useRef)(null);
2881
- const previousOriginRef = (0, import_react17.useRef)(trackList?.origin ?? null);
2882
- const handledAutoAdvanceRequestIdRef = (0, import_react17.useRef)(null);
2883
- const shouldFollowPlaybackRef = (0, import_react17.useRef)(true);
2884
- const programmaticScrollTopRef = (0, import_react17.useRef)(null);
2885
- const userScrollIntentRef = (0, import_react17.useRef)(false);
2886
- const userScrollIntentTimerRef = (0, import_react17.useRef)(null);
2887
- const loadMoreSentinelRef = (0, import_react17.useRef)(null);
3166
+ const trackListRef = (0, import_react19.useRef)(null);
3167
+ const previousOriginRef = (0, import_react19.useRef)(trackList?.origin ?? null);
3168
+ const handledAutoAdvanceRequestIdRef = (0, import_react19.useRef)(null);
3169
+ const shouldFollowPlaybackRef = (0, import_react19.useRef)(true);
3170
+ const programmaticScrollTopRef = (0, import_react19.useRef)(null);
3171
+ const userScrollIntentRef = (0, import_react19.useRef)(false);
3172
+ const userScrollIntentTimerRef = (0, import_react19.useRef)(null);
3173
+ const loadMoreSentinelRef = (0, import_react19.useRef)(null);
2888
3174
  const hasMore = !!trackList && trackList.nextPage !== null;
2889
- (0, import_react17.useEffect)(() => {
3175
+ (0, import_react19.useEffect)(() => {
2890
3176
  const sentinel = loadMoreSentinelRef.current;
2891
3177
  if (!hasMore || !sentinel) return;
2892
3178
  const observer = new IntersectionObserver((entries) => {
@@ -2906,11 +3192,11 @@ function TrackListSidebar({
2906
3192
  programmaticScrollTopRef.current = null;
2907
3193
  if (trackListRef.current) trackListRef.current.scrollTop = 0;
2908
3194
  }, [trackList?.origin]);
2909
- (0, import_react17.useEffect)(() => {
3195
+ (0, import_react19.useEffect)(() => {
2910
3196
  const list = trackListRef.current;
2911
3197
  if (!isCollapsed && list?.scrollTop === 0) shouldFollowPlaybackRef.current = true;
2912
3198
  }, [isCollapsed]);
2913
- (0, import_react17.useEffect)(
3199
+ (0, import_react19.useEffect)(
2914
3200
  () => () => {
2915
3201
  if (userScrollIntentTimerRef.current !== null) clearTimeout(userScrollIntentTimerRef.current);
2916
3202
  },
@@ -2964,25 +3250,25 @@ function TrackListSidebar({
2964
3250
  }
2965
3251
  };
2966
3252
  const positionClasses = layout === "inline" ? "relative w-full flex h-full flex-col" : "fixed right-0 w-[50rem] flex flex-col";
2967
- return trackList ? /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
3253
+ return trackList ? /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(
2968
3254
  "div",
2969
3255
  {
2970
3256
  "data-theme": "dark",
2971
3257
  className: `track-list-sidebar ${positionClasses} rounded-2xl bg-[var(--color-surface)] pb-1 ${className2}`,
2972
3258
  style: layout === "fixed" ? { top: "63.5px", maxHeight: "calc(100vh - 63.5px - 79px)" } : void 0,
2973
3259
  children: [
2974
- /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "header flex h-16 items-center px-4 py-2 text-[var(--color-text)]", children: [
2975
- /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "min-w-0 flex flex-1 flex-col justify-center", children: [
2976
- /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "origin flex text-xl", children: [
2977
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
2978
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "name-container flex flex-col justify-center items-center text-[var(--color-text)] font-bold pr-2 max-w-[27.5rem]", children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "name max-w-full truncate", children: trackList.origin.label }) })
3260
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("div", { className: "header flex h-16 items-center px-4 py-2 text-[var(--color-text)]", children: [
3261
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("div", { className: "min-w-0 flex flex-1 flex-col justify-center", children: [
3262
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("div", { className: "origin flex text-xl", children: [
3263
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
3264
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("div", { className: "name-container flex flex-col justify-center items-center text-[var(--color-text)] font-bold pr-2 max-w-[27.5rem]", children: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("div", { className: "name max-w-full truncate", children: trackList.origin.label }) })
2979
3265
  ] }),
2980
- /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "info flex items-center text-sm text-[var(--color-text-muted)]", children: [
2981
- trackList.origin.type === import_genre_tree20.TrackListOriginType.GENRE_PLAYLIST ? "Genre \u2022 " : trackList.origin.type === import_genre_tree20.TrackListOriginType.ARTIST ? "Artist \u2022 " : `${trackNoun.singular} playlist \u2022 `,
3266
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("div", { className: "info flex items-center text-sm text-[var(--color-text-muted)]", children: [
3267
+ trackList.origin.type === import_genre_tree22.TrackListOriginType.GENRE_PLAYLIST ? "Genre \u2022 " : trackList.origin.type === import_genre_tree22.TrackListOriginType.ARTIST ? "Artist \u2022 " : `${trackNoun.singular} playlist \u2022 `,
2982
3268
  `${trackList.total} ${trackList.total > 1 ? trackNoun.plural : trackNoun.singular}`
2983
3269
  ] })
2984
3270
  ] }),
2985
- (isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
3271
+ (isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
2986
3272
  "button",
2987
3273
  {
2988
3274
  type: "button",
@@ -2991,11 +3277,11 @@ function TrackListSidebar({
2991
3277
  "aria-expanded": !isCollapsed,
2992
3278
  className: `ml-2 flex h-8 w-8 shrink-0 items-center justify-center rounded-md hover:bg-[var(--color-surface-muted)] ${isCollapsed ? "text-[var(--color-text-muted)]" : "text-white"}`,
2993
3279
  onClick: isCollapsed ? onExpand : onMinimize,
2994
- children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_lucide_react12.ListMusic, { "aria-hidden": "true", className: "h-5 w-5" })
3280
+ children: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_lucide_react13.ListMusic, { "aria-hidden": "true", className: "h-5 w-5" })
2995
3281
  }
2996
3282
  )
2997
3283
  ] }),
2998
- !isCollapsed && /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
3284
+ !isCollapsed && /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(
2999
3285
  "ul",
3000
3286
  {
3001
3287
  ref: trackListRef,
@@ -3005,17 +3291,18 @@ function TrackListSidebar({
3005
3291
  onPointerDown: markUserScrollIntent,
3006
3292
  className: layout === "inline" ? "track-list flex-1 min-h-0 overflow-auto list-none p-0 m-0" : "track-list flex-1 min-h-0 overflow-auto list-none p-0 m-0",
3007
3293
  children: [
3008
- trackList.tracks.map((track, index) => /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("li", { "data-track-uuid": track.uuid, children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
3294
+ trackList.tracks.map((track, index) => /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("li", { "data-track-uuid": track.uuid, children: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
3009
3295
  TrackItem,
3010
3296
  {
3011
3297
  track,
3012
3298
  position: index + 1,
3013
3299
  renderDuration,
3014
3300
  renderActions,
3015
- renderArtistLink
3301
+ renderArtistLink,
3302
+ renderSongLink
3016
3303
  }
3017
3304
  ) }, track.uuid)),
3018
- hasMore && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
3305
+ hasMore && /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
3019
3306
  ]
3020
3307
  }
3021
3308
  )
@@ -3025,16 +3312,16 @@ function TrackListSidebar({
3025
3312
  }
3026
3313
 
3027
3314
  // src/genre-tree/PlayerPanel.tsx
3028
- var import_react18 = require("react");
3315
+ var import_react20 = require("react");
3029
3316
  var import_tailwind_merge2 = require("tailwind-merge");
3030
- var import_player12 = require("@behindthemusictree/app-kit/player");
3031
- var import_genre_tree22 = require("@behindthemusictree/app-kit/genre-tree");
3317
+ var import_player13 = require("@behindthemusictree/app-kit/player");
3318
+ var import_genre_tree24 = require("@behindthemusictree/app-kit/genre-tree");
3032
3319
 
3033
3320
  // src/genre-tree/PlayerControls.tsx
3034
- var import_lucide_react13 = require("lucide-react");
3035
- var import_genre_tree21 = require("@behindthemusictree/app-kit/genre-tree");
3036
- var import_player11 = require("@behindthemusictree/app-kit/player");
3037
- var import_jsx_runtime36 = require("react/jsx-runtime");
3321
+ var import_lucide_react14 = require("lucide-react");
3322
+ var import_genre_tree23 = require("@behindthemusictree/app-kit/genre-tree");
3323
+ var import_player12 = require("@behindthemusictree/app-kit/player");
3324
+ var import_jsx_runtime38 = require("react/jsx-runtime");
3038
3325
  function PlayerControls() {
3039
3326
  const {
3040
3327
  duration,
@@ -3045,23 +3332,23 @@ function PlayerControls() {
3045
3332
  seekTo,
3046
3333
  setVolume,
3047
3334
  volume
3048
- } = (0, import_player11.usePlayer)();
3049
- const { hasNextTrack, hasPreviousTrack, playNextTrack, playPreviousTrack } = (0, import_genre_tree21.useTrackList)();
3050
- const currentTime = (0, import_player11.useCurrentTime)();
3051
- const isBusy = isLoading || playState === import_player11.PlayStates.LOADING;
3335
+ } = (0, import_player12.usePlayer)();
3336
+ const { hasNextTrack, hasPreviousTrack, playNextTrack, playPreviousTrack } = (0, import_genre_tree23.useTrackList)();
3337
+ const currentTime = (0, import_player12.useCurrentTime)();
3338
+ const isBusy = isLoading || playState === import_player12.PlayStates.LOADING;
3052
3339
  const isReady = !!playerTrackObject?.isReady && !isBusy && !playerTrackObject.loadError;
3053
3340
  const safeDuration = Number.isFinite(duration) && duration > 0 ? duration : 0;
3054
3341
  const safeCurrentTime = Number.isFinite(currentTime) ? Math.max(0, Math.min(currentTime, safeDuration)) : 0;
3055
- const isPlaying = playState === import_player11.PlayStates.PLAYING;
3056
- return /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(
3342
+ const isPlaying = playState === import_player12.PlayStates.PLAYING;
3343
+ return /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(
3057
3344
  "section",
3058
3345
  {
3059
3346
  "aria-label": "Player controls",
3060
3347
  className: "player-controls flex shrink-0 flex-col gap-2 border-b border-zinc-800 px-3 py-2 text-zinc-200",
3061
3348
  children: [
3062
- /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "flex min-w-0 items-center gap-2 text-xs tabular-nums text-zinc-400", children: [
3063
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("span", { "aria-label": "Elapsed time", children: (0, import_genre_tree21.formatTime)(safeCurrentTime) }),
3064
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
3349
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("div", { className: "flex min-w-0 items-center gap-2 text-xs tabular-nums text-zinc-400", children: [
3350
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { "aria-label": "Elapsed time", children: (0, import_genre_tree23.formatTime)(safeCurrentTime) }),
3351
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
3065
3352
  "input",
3066
3353
  {
3067
3354
  "aria-label": "Seek",
@@ -3075,11 +3362,11 @@ function PlayerControls() {
3075
3362
  onChange: (event) => seekTo(Number(event.currentTarget.value))
3076
3363
  }
3077
3364
  ),
3078
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("span", { "aria-label": "Duration", children: (0, import_genre_tree21.formatTime)(safeDuration) })
3365
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { "aria-label": "Duration", children: (0, import_genre_tree23.formatTime)(safeDuration) })
3079
3366
  ] }),
3080
- /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "grid min-h-9 grid-cols-[1fr_auto_1fr] items-center gap-3", children: [
3081
- /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "col-start-2 flex items-center justify-center gap-2", children: [
3082
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
3367
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("div", { className: "grid min-h-9 grid-cols-[1fr_auto_1fr] items-center gap-3", children: [
3368
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("div", { className: "col-start-2 flex items-center justify-center gap-2", children: [
3369
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
3083
3370
  "button",
3084
3371
  {
3085
3372
  type: "button",
@@ -3088,10 +3375,10 @@ function PlayerControls() {
3088
3375
  disabled: !isReady || !hasPreviousTrack,
3089
3376
  onClick: playPreviousTrack,
3090
3377
  className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-zinc-300 hover:bg-zinc-800 hover:text-white disabled:cursor-not-allowed disabled:opacity-40",
3091
- children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_lucide_react13.SkipBack, { "aria-hidden": "true", className: "h-4 w-4" })
3378
+ children: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_lucide_react14.SkipBack, { "aria-hidden": "true", className: "h-4 w-4" })
3092
3379
  }
3093
3380
  ),
3094
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
3381
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
3095
3382
  "button",
3096
3383
  {
3097
3384
  type: "button",
@@ -3100,10 +3387,10 @@ function PlayerControls() {
3100
3387
  disabled: !isReady,
3101
3388
  onClick: handlePlayPauseAction,
3102
3389
  className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-white text-zinc-950 hover:bg-zinc-200 disabled:cursor-not-allowed disabled:opacity-40",
3103
- children: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_lucide_react13.Pause, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" }) : /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_lucide_react13.Play, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" })
3390
+ children: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_lucide_react14.Pause, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" }) : /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_lucide_react14.Play, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" })
3104
3391
  }
3105
3392
  ),
3106
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
3393
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
3107
3394
  "button",
3108
3395
  {
3109
3396
  type: "button",
@@ -3112,14 +3399,14 @@ function PlayerControls() {
3112
3399
  disabled: !isReady || !hasNextTrack,
3113
3400
  onClick: playNextTrack,
3114
3401
  className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-zinc-300 hover:bg-zinc-800 hover:text-white disabled:cursor-not-allowed disabled:opacity-40",
3115
- children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_lucide_react13.SkipForward, { "aria-hidden": "true", className: "h-4 w-4" })
3402
+ children: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_lucide_react14.SkipForward, { "aria-hidden": "true", className: "h-4 w-4" })
3116
3403
  }
3117
3404
  )
3118
3405
  ] }),
3119
- /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("label", { className: "col-start-3 flex w-[4.2rem] shrink-0 items-center gap-[0.35rem] justify-self-end text-zinc-400 sm:w-[4.9rem]", children: [
3120
- volume === 0 ? /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_lucide_react13.VolumeX, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }) : /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_lucide_react13.Volume2, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }),
3121
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("span", { className: "sr-only", children: "Volume" }),
3122
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
3406
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("label", { className: "col-start-3 flex w-[4.2rem] shrink-0 items-center gap-[0.35rem] justify-self-end text-zinc-400 sm:w-[4.9rem]", children: [
3407
+ volume === 0 ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_lucide_react14.VolumeX, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }) : /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_lucide_react14.Volume2, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }),
3408
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { className: "sr-only", children: "Volume" }),
3409
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
3123
3410
  "input",
3124
3411
  {
3125
3412
  "aria-label": "Volume",
@@ -3141,7 +3428,7 @@ function PlayerControls() {
3141
3428
  }
3142
3429
 
3143
3430
  // src/genre-tree/PlayerPanel.tsx
3144
- var import_jsx_runtime37 = require("react/jsx-runtime");
3431
+ var import_jsx_runtime39 = require("react/jsx-runtime");
3145
3432
  var PANEL_ID = "player-panel";
3146
3433
  var DEFAULT_EXTENDED_WIDTH = "calc(100vw - var(--layout-center-left) - var(--layout-gutter))";
3147
3434
  function PlayerPanel({
@@ -3150,16 +3437,17 @@ function PlayerPanel({
3150
3437
  expanded,
3151
3438
  onExpandedChange,
3152
3439
  extendedWidth = DEFAULT_EXTENDED_WIDTH,
3153
- renderArtistLink
3440
+ renderArtistLink,
3441
+ renderSongLink
3154
3442
  }) {
3155
- const { playerTrackObject } = (0, import_player12.usePlayer)();
3156
- const { toggleTrackListSidebar, isTrackListSidebarVisible } = (0, import_genre_tree22.useTrackListSidebarVisibility)();
3157
- const [lastLoadedTrackTitle, setLastLoadedTrackTitle] = (0, import_react18.useState)("");
3443
+ const { playerTrackObject } = (0, import_player13.usePlayer)();
3444
+ const { toggleTrackListSidebar, isTrackListSidebarVisible } = (0, import_genre_tree24.useTrackListSidebarVisibility)();
3445
+ const [lastLoadedTrackTitle, setLastLoadedTrackTitle] = (0, import_react20.useState)("");
3158
3446
  const loadedTrackTitle = playerTrackObject?.track.title;
3159
- (0, import_react18.useEffect)(() => {
3447
+ (0, import_react20.useEffect)(() => {
3160
3448
  if (loadedTrackTitle) setLastLoadedTrackTitle(loadedTrackTitle);
3161
3449
  }, [loadedTrackTitle]);
3162
- const [uncontrolledExtended, setUncontrolledExtended] = (0, import_react18.useState)(false);
3450
+ const [uncontrolledExtended, setUncontrolledExtended] = (0, import_react20.useState)(false);
3163
3451
  const isExtendRequested = expanded ?? uncontrolledExtended;
3164
3452
  const setIsExtendRequested = (next) => {
3165
3453
  setUncontrolledExtended(next);
@@ -3180,7 +3468,7 @@ function PlayerPanel({
3180
3468
  onClick: () => setIsExtendRequested(true),
3181
3469
  expanded: false
3182
3470
  } : null;
3183
- return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(
3471
+ return /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(
3184
3472
  "div",
3185
3473
  {
3186
3474
  id: PANEL_ID,
@@ -3193,7 +3481,7 @@ function PlayerPanel({
3193
3481
  ),
3194
3482
  style: isExtended ? { width: extendedWidth } : void 0,
3195
3483
  children: [
3196
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
3484
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3197
3485
  PanelEdgeTab,
3198
3486
  {
3199
3487
  side: "right",
@@ -3203,7 +3491,7 @@ function PlayerPanel({
3203
3491
  className: "absolute right-full top-1/2 -translate-y-1/2"
3204
3492
  }
3205
3493
  ),
3206
- /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(
3494
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(
3207
3495
  "div",
3208
3496
  {
3209
3497
  className: (0, import_tailwind_merge2.twMerge)(
@@ -3211,15 +3499,15 @@ function PlayerPanel({
3211
3499
  isExtended ? "flex-row" : "flex-col"
3212
3500
  ),
3213
3501
  children: [
3214
- /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("div", { className: "flex w-[var(--layout-right-w)] shrink-0 flex-col", children: [
3215
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("div", { className: "flex shrink-0 items-center justify-between gap-2 border-b border-zinc-800 p-2", children: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "min-w-0 truncate text-sm font-semibold", children: loadedTrackTitle || lastLoadedTrackTitle }) }),
3216
- /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("div", { className: "relative w-full shrink-0", children: [
3217
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_player12.PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
3218
- playerTrackObject?.loadError && /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "absolute bottom-2 left-2 right-2 text-xs text-red-400", children: playerTrackObject.loadError })
3502
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "flex w-[var(--layout-right-w)] shrink-0 flex-col", children: [
3503
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "flex shrink-0 items-center justify-between gap-2 border-b border-zinc-800 p-2", children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("span", { className: "min-w-0 truncate text-sm font-semibold", children: loadedTrackTitle || lastLoadedTrackTitle }) }),
3504
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "relative w-full shrink-0", children: [
3505
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_player13.PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
3506
+ playerTrackObject?.loadError && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("span", { className: "absolute bottom-2 left-2 right-2 text-xs text-red-400", children: playerTrackObject.loadError })
3219
3507
  ] }),
3220
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(PlayerControls, {})
3508
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(PlayerControls, {})
3221
3509
  ] }),
3222
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
3510
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3223
3511
  TrackListSidebar,
3224
3512
  {
3225
3513
  layout: "inline",
@@ -3230,7 +3518,8 @@ function PlayerPanel({
3230
3518
  "min-h-0 min-w-0 flex-1",
3231
3519
  isExtended && "border-l border-zinc-800"
3232
3520
  ),
3233
- renderArtistLink
3521
+ renderArtistLink,
3522
+ renderSongLink
3234
3523
  }
3235
3524
  )
3236
3525
  ]
@@ -3244,33 +3533,33 @@ function PlayerPanel({
3244
3533
  // src/genre-tree/Rating.tsx
3245
3534
  var import_uuid = require("uuid");
3246
3535
  var import_ai = require("react-icons/ai");
3247
- var import_genre_tree23 = require("@behindthemusictree/app-kit/genre-tree");
3248
- var import_jsx_runtime38 = require("react/jsx-runtime");
3536
+ var import_genre_tree25 = require("@behindthemusictree/app-kit/genre-tree");
3537
+ var import_jsx_runtime40 = require("react/jsx-runtime");
3249
3538
  function Rating({ rating, handleChange }) {
3250
3539
  const instanceId = (0, import_uuid.v4)();
3251
- return /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("div", { className: "flex items-center", children: [...Array(6)].map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("div", { className: `relative ${i === 0 ? "mr-4" : "mr-1"}`, children: [
3252
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
3540
+ return /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "flex items-center", children: [...Array(6)].map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: `relative ${i === 0 ? "mr-4" : "mr-1"}`, children: [
3541
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3253
3542
  "input",
3254
3543
  {
3255
3544
  id: `star-${instanceId}-${i}`,
3256
3545
  className: "w-6 h-6 opacity-0",
3257
3546
  type: "radio",
3258
3547
  name: "rating",
3259
- value: i === 0 ? import_genre_tree23.FORM_RATING_NULL_VALUE : i,
3548
+ value: i === 0 ? import_genre_tree25.FORM_RATING_NULL_VALUE : i,
3260
3549
  checked: rating === (i === 0 ? null : i),
3261
3550
  onChange: handleChange
3262
3551
  }
3263
3552
  ),
3264
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("label", { htmlFor: `star-${instanceId}-${i}`, className: "cursor-pointer", children: i === 0 ? /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("div", { className: "flex", children: [
3265
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
3553
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("label", { htmlFor: `star-${instanceId}-${i}`, className: "cursor-pointer", children: i === 0 ? /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: "flex", children: [
3554
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3266
3555
  import_ai.AiOutlineStar,
3267
3556
  {
3268
- className: `w-7 h-7 absolute top-0 left-0 ${rating === import_genre_tree23.FORM_RATING_NULL_VALUE ? "text-[var(--color-accent-rating)]" : "text-[var(--color-text-muted)]"}`
3557
+ className: `w-7 h-7 absolute top-0 left-0 ${rating === import_genre_tree25.FORM_RATING_NULL_VALUE ? "text-[var(--color-accent-rating)]" : "text-[var(--color-text-muted)]"}`
3269
3558
  }
3270
3559
  ),
3271
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { className: "flex-grow w-8" }),
3560
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("span", { className: "flex-grow w-8" }),
3272
3561
  " "
3273
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
3562
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3274
3563
  import_ai.AiFillStar,
3275
3564
  {
3276
3565
  className: `w-7 h-7 absolute top-0 left-0 ${(rating ?? -1) >= i ? "text-[var(--color-accent-rating)]" : "text-[var(--color-text-muted)]"}`