@behindthemusictree/app-kit-ui 29.1.0 → 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,31 +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
  {
2153
2354
  variant: "floating",
2154
2355
  "aria-label": "Size nodes by",
2155
2356
  value,
2156
2357
  onChange,
2157
2358
  options: [
2158
- { 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" },
2159
- { 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" }
2160
2361
  ]
2161
2362
  }
2162
2363
  );
2163
2364
  }
2164
2365
 
2165
2366
  // src/genre-tree/GenreTreeView.tsx
2166
- var import_jsx_runtime33 = require("react/jsx-runtime");
2367
+ var import_jsx_runtime35 = require("react/jsx-runtime");
2167
2368
  var PREVIOUS_BUTTON_CSS = `
2168
2369
  .genre-tree-view .search-overlay .gtv-info-panel-previous { display: none; }
2169
2370
  .genre-tree-view:has(.search-overlay .gtv-info-panel-previous) .genre-search input { padding-left: 2.75rem; }
@@ -2207,6 +2408,13 @@ function GenreTreeView({
2207
2408
  artistSearch = true,
2208
2409
  onArtistSelect,
2209
2410
  artistSearchBaseUrl,
2411
+ songUuid: controlledSongUuid,
2412
+ onSongUuidChange,
2413
+ songExpanded: controlledSongExpanded,
2414
+ onSongExpandedChange,
2415
+ songSearch = true,
2416
+ onSongSelect,
2417
+ renderSongLink,
2210
2418
  renderSummary,
2211
2419
  renderEssentialTracks,
2212
2420
  renderSongLeading,
@@ -2221,14 +2429,21 @@ function GenreTreeView({
2221
2429
  placement = "default",
2222
2430
  ...focus
2223
2431
  }) {
2224
- const getShareUrlRef = (0, import_react15.useRef)(getShareUrl);
2432
+ const getShareUrlRef = (0, import_react17.useRef)(getShareUrl);
2225
2433
  getShareUrlRef.current = getShareUrl;
2226
- const [internalArtistUuid, setInternalArtistUuid] = (0, import_react15.useState)(null);
2434
+ const [internalArtistUuid, setInternalArtistUuid] = (0, import_react17.useState)(null);
2227
2435
  const isArtistControlled = controlledArtistUuid !== void 0;
2228
2436
  const artistUuid = isArtistControlled ? controlledArtistUuid : internalArtistUuid;
2229
- 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");
2230
2445
  const sizeMetric = controlledSizeMetric ?? internalSizeMetric;
2231
- const deferredSizeMetric = (0, import_react15.useDeferredValue)(sizeMetric);
2446
+ const deferredSizeMetric = (0, import_react17.useDeferredValue)(sizeMetric);
2232
2447
  const changeSizeMetric = (metric) => {
2233
2448
  if (controlledSizeMetric === void 0) setInternalSizeMetric(metric);
2234
2449
  onSizeMetricChange?.(metric);
@@ -2255,7 +2470,7 @@ function GenreTreeView({
2255
2470
  allowWheelRotation: internalAllowWheelRotation,
2256
2471
  toggleWheelRotation,
2257
2472
  onNodeHover: handleNodeHover
2258
- } = (0, import_genre_tree17.useGenreTreeViewState)({
2473
+ } = (0, import_genre_tree19.useGenreTreeViewState)({
2259
2474
  scope,
2260
2475
  getBackendBaseUrl,
2261
2476
  viewMode: controlledViewMode,
@@ -2265,7 +2480,7 @@ function GenreTreeView({
2265
2480
  artistUuid
2266
2481
  });
2267
2482
  const restoreRevision = viewportRestore?.revision;
2268
- 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 });
2269
2484
  let restoreRootUuid = stackedRestoreOwner.rootUuid;
2270
2485
  if (stackedRestoreOwner.revision !== restoreRevision) {
2271
2486
  restoreRootUuid = selectedGenrePlaylist?.root.uuid;
@@ -2274,14 +2489,14 @@ function GenreTreeView({
2274
2489
  restoreRootUuid = selectedGenrePlaylist.root.uuid;
2275
2490
  setStackedRestoreOwner({ revision: restoreRevision, rootUuid: restoreRootUuid });
2276
2491
  }
2277
- const isGenreUpdating = (0, import_react_query3.useIsMutating)({ mutationKey: import_genre_tree17.genreUpdateMutationKey }) > 0;
2278
- 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);
2279
2494
  const busy = deferredSizeMetric !== sizeMetric || isTreeFetching || isGenreUpdating;
2280
2495
  const isArtistFiltered = artistUuid !== null;
2281
2496
  const canSizeByViews = !isArtistFiltered && hasViewsCount(visibleGenrePlaylists);
2282
- const artistResult = (0, import_genre_tree17.useArtist)(artistUuid, artistSearchBaseUrl ?? getBackendBaseUrl);
2497
+ const artistResult = (0, import_genre_tree19.useArtist)(artistUuid, artistSearchBaseUrl ?? getBackendBaseUrl);
2283
2498
  const filterArtistName = artistResult.status === "success" ? artistResult.artist.name : null;
2284
- const filterArtist = (0, import_react15.useMemo)(
2499
+ const filterArtist = (0, import_react17.useMemo)(
2285
2500
  () => artistUuid !== null && filterArtistName !== null ? { uuid: artistUuid, name: filterArtistName } : null,
2286
2501
  [artistUuid, filterArtistName]
2287
2502
  );
@@ -2289,27 +2504,72 @@ function GenreTreeView({
2289
2504
  const playback = useGenrePlaylistPlayback({ scope, genrePlaylists: visibleGenrePlaylists, artist: filterArtist });
2290
2505
  const openArtist = (uuid) => {
2291
2506
  if (selectedNodeId !== null) clearSelection();
2507
+ if (songUuid !== null) changeSong(null);
2292
2508
  changeArtist(uuid);
2293
2509
  };
2294
- 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);
2295
2522
  const artistBaseUrl = artistSearchBaseUrl ?? getBackendBaseUrl;
2523
+ const songBaseUrl = getBackendBaseUrl;
2296
2524
  const artistPanel = useArtistPanel({
2297
2525
  uuid: artistUuid,
2298
2526
  scope,
2299
2527
  getBackendBaseUrl: artistBaseUrl,
2300
2528
  getShareUrl: getShareUrlRef.current,
2301
- 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 })),
2302
2530
  expanded: controlledArtistExpanded ?? (artistUuid !== null && expandedArtistUuid === artistUuid),
2303
2531
  onExpandedChange: (on) => {
2304
2532
  if (controlledArtistExpanded === void 0) setExpandedArtistUuid(on ? artistUuid : null);
2305
2533
  onArtistExpandedChange?.(on);
2306
2534
  },
2535
+ renderSongLink: resolvedRenderSongLink,
2307
2536
  onClose: () => changeArtist(null)
2308
2537
  });
2309
- const builtInArtistPanel = overlayPanel || hasSelectedGenreNode ? void 0 : artistPanel;
2310
- const activeOverlayPanel = overlayPanel ?? (builtInArtistPanel && { ...builtInArtistPanel, hideTitle: true, hideClose: true });
2311
- const [uncontrolledCollapsed, setUncontrolledCollapsed] = (0, import_react15.useState)(false);
2312
- 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);
2313
2573
  const isCollapsed = collapsed ?? uncontrolledCollapsed;
2314
2574
  const isExpanded = expanded ?? uncontrolledExpanded;
2315
2575
  const changeCollapsed = (next) => {
@@ -2320,9 +2580,9 @@ function GenreTreeView({
2320
2580
  setUncontrolledExpanded(next);
2321
2581
  onExpandedChange?.(next);
2322
2582
  };
2323
- (0, import_react15.useEffect)(() => {
2583
+ (0, import_react17.useEffect)(() => {
2324
2584
  setUncontrolledCollapsed(false);
2325
- }, [selectedNodeId, artistUuid]);
2585
+ }, [selectedNodeId, artistUuid, songUuid]);
2326
2586
  const isDetailHidden = isCollapsed || !hasSelectedGenreNode && !activeOverlayPanel;
2327
2587
  useLeftDetailPanelLayout(!isDetailHidden, placement === "regions");
2328
2588
  const detailOverlay = placement === "regions" && activeOverlayPanel?.onExpandedChange ? activeOverlayPanel : void 0;
@@ -2336,14 +2596,14 @@ function GenreTreeView({
2336
2596
  hideExpandButton: placement === "regions" ? true : void 0,
2337
2597
  overlayPanel: detailOverlay ? { ...detailOverlay, collapsed: isCollapsed, hideExpandButton: true } : activeOverlayPanel
2338
2598
  };
2339
- const [isWheelRevealed, setIsWheelRevealed] = (0, import_react15.useState)(false);
2599
+ const [isWheelRevealed, setIsWheelRevealed] = (0, import_react17.useState)(false);
2340
2600
  const usesHandoff = viewMode === "wheel" || viewMode === "pop-core";
2341
2601
  const isTreeReady = !isLoading && (!usesHandoff || isWheelRevealed);
2342
- const rootRef = (0, import_react15.useRef)(null);
2602
+ const rootRef = (0, import_react17.useRef)(null);
2343
2603
  const handlePrevious = () => {
2344
2604
  rootRef.current?.querySelector(".search-overlay .gtv-info-panel-previous")?.click();
2345
2605
  };
2346
- (0, import_react15.useEffect)(() => {
2606
+ (0, import_react17.useEffect)(() => {
2347
2607
  if (!isTreeReady || viewMode !== "pop-core" || !hasSelectedGenreNode || !selectedGenrePlaylist) return;
2348
2608
  const root = rootRef.current;
2349
2609
  const centerChip = root?.querySelector(".gtv-wheel-center-node .gtv-wheel-chip");
@@ -2353,21 +2613,22 @@ function GenreTreeView({
2353
2613
  centerChip.click();
2354
2614
  }, [hasSelectedGenreNode, isTreeReady, selectedGenrePlaylist, viewMode]);
2355
2615
  const { showPopup, hidePopup } = (0, import_popup5.usePopup)();
2356
- const [searchText, setSearchText] = (0, import_react15.useState)("");
2357
- const artistSearchResult = (0, import_genre_tree16.useArtistSearch)(artistBaseUrl, artistSearch ? searchText : "");
2358
- 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",
2359
2620
  label: "Artists",
2360
2621
  items: artistSearchResult.artists.map((artist) => ({
2361
2622
  key: artist.uuid,
2362
- content: /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(import_jsx_runtime33.Fragment, { children: [
2363
- /* @__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: [
2364
2625
  artist.name,
2365
- 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: [
2366
2627
  " \xB7 ",
2367
2628
  artist.disambiguation
2368
2629
  ] })
2369
2630
  ] }),
2370
- 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: [
2371
2632
  "also known as ",
2372
2633
  artist.matchedAlias
2373
2634
  ] })
@@ -2380,11 +2641,32 @@ function GenreTreeView({
2380
2641
  status: artistSearchResult.isError ? "Search failed" : artistSearchResult.settled && artistSearchResult.artists.length === 0 ? "No artists found" : "",
2381
2642
  loading: artistSearchResult.pending
2382
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);
2383
2663
  const handleSelectedNodeChange = (node) => {
2664
+ if (node !== null && songUuid !== null) changeSong(null);
2384
2665
  selectNode(node);
2385
2666
  };
2386
2667
  const handleGenreSearchClear = clearSelection;
2387
2668
  const handleGenreSearchSelect = (genrePlaylist) => {
2669
+ if (songUuid !== null) changeSong(null);
2388
2670
  selectGenrePlaylist(genrePlaylist);
2389
2671
  };
2390
2672
  const selectedUuid = selectedGenrePlaylist?.uuid;
@@ -2393,13 +2675,13 @@ function GenreTreeView({
2393
2675
  const handleGenreSearchZoomIn = () => {
2394
2676
  if (selectedUuid) focus.onFocusGenreChange?.(selectedUuid, "zoom-in");
2395
2677
  };
2396
- const zoomInRef = (0, import_react15.useRef)(void 0);
2678
+ const zoomInRef = (0, import_react17.useRef)(void 0);
2397
2679
  zoomInRef.current = canZoomIn ? handleGenreSearchZoomIn : void 0;
2398
- const renderInfoPanelBody = (0, import_react15.useCallback)(
2680
+ const renderInfoPanelBody = (0, import_react17.useCallback)(
2399
2681
  (node, { toolbar, ancestors, family }) => {
2400
2682
  const genrePlaylist = genrePlaylistByNodeId.get(node.id);
2401
2683
  const criteria = genrePlaylist?.criteria;
2402
- 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: [
2403
2685
  toolbar,
2404
2686
  family
2405
2687
  ] }), aside: ancestors };
@@ -2419,7 +2701,7 @@ function GenreTreeView({
2419
2701
  (sibling) => sibling.parent?.uuid === parentPlaylist.uuid && sibling.uuid !== genrePlaylist.uuid && sibling.criteria?.side === "pop"
2420
2702
  ) : void 0;
2421
2703
  return {
2422
- primary: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2704
+ primary: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2423
2705
  GenreInfoPanelBody,
2424
2706
  {
2425
2707
  toolbar,
@@ -2438,7 +2720,7 @@ function GenreTreeView({
2438
2720
  onAddChild: readOnly ? void 0 : () => handleGenreCreationAction(criteria),
2439
2721
  onRename: readOnly ? void 0 : () => handleGenreRenameAction(criteria),
2440
2722
  onChangeParent: readOnly ? void 0 : () => showPopup(
2441
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2723
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2442
2724
  ChangeParentPopup,
2443
2725
  {
2444
2726
  genrePlaylist: { ...genrePlaylist, criteria },
@@ -2459,6 +2741,7 @@ function GenreTreeView({
2459
2741
  criteriaOverviewSchema,
2460
2742
  renderGenreDetailExtras,
2461
2743
  renderArtistLink,
2744
+ renderSongLink: resolvedRenderSongLink,
2462
2745
  artist: filterArtist,
2463
2746
  renderSummary,
2464
2747
  renderEssentialTracks,
@@ -2489,6 +2772,7 @@ function GenreTreeView({
2489
2772
  criteriaOverviewSchema,
2490
2773
  renderGenreDetailExtras,
2491
2774
  renderArtistLink,
2775
+ resolvedRenderSongLink,
2492
2776
  renderSummary,
2493
2777
  renderEssentialTracks,
2494
2778
  renderSongLeading,
@@ -2498,7 +2782,7 @@ function GenreTreeView({
2498
2782
  );
2499
2783
  const isRotationControlled = controlledAllowWheelRotation !== void 0;
2500
2784
  const allowWheelRotation = controlledAllowWheelRotation ?? internalAllowWheelRotation;
2501
- 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)(
2502
2786
  PanelEdgeTab,
2503
2787
  {
2504
2788
  side: "left",
@@ -2508,8 +2792,8 @@ function GenreTreeView({
2508
2792
  ) : null;
2509
2793
  const renderArtistFilter = () => {
2510
2794
  if (isLoading || !filterArtist) return null;
2511
- 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: [
2512
- /* @__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)(
2513
2797
  "button",
2514
2798
  {
2515
2799
  type: "button",
@@ -2521,7 +2805,7 @@ function GenreTreeView({
2521
2805
  children: filterArtist.name
2522
2806
  }
2523
2807
  ),
2524
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2808
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2525
2809
  "button",
2526
2810
  {
2527
2811
  type: "button",
@@ -2533,23 +2817,23 @@ function GenreTreeView({
2533
2817
  )
2534
2818
  ] });
2535
2819
  };
2536
- const actions = /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(import_jsx_runtime33.Fragment, { children: [
2537
- !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)(
2538
2822
  GenreSearch,
2539
2823
  {
2540
2824
  genrePlaylists: visibleGenrePlaylists,
2541
2825
  onSelect: handleGenreSearchSelect,
2542
- selectedName: selectedGenrePlaylist?.name ?? builtInArtistPanel?.title ?? null,
2543
- onClear: builtInArtistPanel ? () => changeArtist(null) : handleGenreSearchClear,
2826
+ selectedName: selectedGenrePlaylist?.name ?? builtInPanel?.title ?? null,
2827
+ onClear: builtInSongPanel ? () => changeSong(null) : builtInArtistPanel ? () => changeArtist(null) : handleGenreSearchClear,
2544
2828
  onPrevious: handlePrevious,
2545
- onQueryChange: artistSearch ? setSearchText : void 0,
2546
- extraResults: artistExtraResults
2829
+ onQueryChange: artistSearch || songSearch ? setSearchText : void 0,
2830
+ extraGroups
2547
2831
  }
2548
2832
  ),
2549
- !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
2550
- /* @__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: [
2551
- !isLoading && !isControlled && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2552
- 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,
2553
2837
  {
2554
2838
  "aria-label": "Tree view mode",
2555
2839
  value: viewMode,
@@ -2562,14 +2846,14 @@ function GenreTreeView({
2562
2846
  ]
2563
2847
  }
2564
2848
  ),
2565
- !isLoading && showDisplayOptions && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2849
+ !isLoading && showDisplayOptions && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2566
2850
  "div",
2567
2851
  {
2568
2852
  className: "flex items-center gap-1",
2569
2853
  role: "group",
2570
2854
  "aria-label": "Tree display options",
2571
- children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2572
- import_ui13.Button,
2855
+ children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2856
+ import_ui15.Button,
2573
2857
  {
2574
2858
  variant: allowWheelRotation ? "default" : "outline",
2575
2859
  size: "sm",
@@ -2579,52 +2863,52 @@ function GenreTreeView({
2579
2863
  )
2580
2864
  }
2581
2865
  ),
2582
- !isLoading && !readOnly && showAddRoot && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2583
- import_ui13.IconTextButton,
2866
+ !isLoading && !readOnly && showAddRoot && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2867
+ import_ui15.IconTextButton,
2584
2868
  {
2585
- icon: import_lucide_react11.Plus,
2869
+ icon: import_lucide_react12.Plus,
2586
2870
  text: "Add root",
2587
2871
  onClick: () => handleGenreCreationAction(null)
2588
2872
  }
2589
2873
  )
2590
2874
  ] })
2591
2875
  ] });
2592
- return /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(
2876
+ return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
2593
2877
  "div",
2594
2878
  {
2595
2879
  ref: rootRef,
2596
2880
  className: ["genre-tree-view relative flex flex-col h-full", placement === "regions" && REGIONS_CLASS, className2].filter(Boolean).join(" "),
2597
2881
  children: [
2598
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("style", { children: PREVIOUS_BUTTON_CSS }),
2599
- /* @__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)(
2600
2884
  "div",
2601
2885
  {
2602
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"}`,
2603
2887
  children: actions
2604
2888
  }
2605
2889
  ),
2606
- !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2890
+ !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2607
2891
  "div",
2608
2892
  {
2609
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"}`,
2610
2894
  children: renderArtistFilter()
2611
2895
  }
2612
2896
  ),
2613
- detailTab && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2897
+ detailTab && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2614
2898
  "div",
2615
2899
  {
2616
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))]"}`,
2617
2901
  children: detailTab
2618
2902
  }
2619
2903
  ),
2620
- /* @__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: [
2621
- !isLoading && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
2622
- 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" ? (
2623
2907
  // The info panel's top edge aligns with the tree's; the search bar floats `--layout-panel-inset` below it,
2624
2908
  // and the header inset (the bar's height plus that inset) extends the header's fill behind the bar,
2625
2909
  // Google Maps style; the 8px bottom padding mirrors the inset above the bar, and the hidden title's height is zeroed.
2626
2910
  // The panel's own Zoom in button is hidden: the search bar carries it.
2627
- /* @__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)(
2628
2912
  GenrePlaylistTreeWheel,
2629
2913
  {
2630
2914
  scope,
@@ -2643,9 +2927,9 @@ function GenreTreeView({
2643
2927
  allowWheelRotation,
2644
2928
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
2645
2929
  busy,
2646
- 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: [
2647
2931
  floatingControlsLeading,
2648
- 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 })
2649
2933
  ] }) : void 0,
2650
2934
  defaultViewport,
2651
2935
  viewportRestore,
@@ -2655,7 +2939,7 @@ function GenreTreeView({
2655
2939
  ) }) })
2656
2940
  ) : viewMode === "pop-core" ? (
2657
2941
  // Same as the wheel: the header inset extends its fill behind the floating search bar, Google Maps style.
2658
- /* @__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)(
2659
2943
  GenrePlaylistTreeWheelRadialPopCore,
2660
2944
  {
2661
2945
  scope,
@@ -2674,9 +2958,9 @@ function GenreTreeView({
2674
2958
  allowWheelRotation,
2675
2959
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
2676
2960
  busy,
2677
- 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: [
2678
2962
  floatingControlsLeading,
2679
- 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 })
2680
2964
  ] }) : void 0,
2681
2965
  defaultViewport,
2682
2966
  viewportRestore,
@@ -2685,7 +2969,7 @@ function GenreTreeView({
2685
2969
  ...focus
2686
2970
  }
2687
2971
  ) }) })
2688
- ) : 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)(
2689
2973
  GenrePlaylistTreeWheelRadialPopCore,
2690
2974
  {
2691
2975
  outline: true,
@@ -2704,13 +2988,13 @@ function GenreTreeView({
2704
2988
  readOnly,
2705
2989
  ...infoPanelCuration
2706
2990
  }
2707
- ) }) : /* @__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(
2708
2992
  ([uuid, genrePlaylistTreePerRoot], rootIndex) => {
2709
- return /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
2993
+ return /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2710
2994
  "div",
2711
2995
  {
2712
2996
  className: "tree-per-root-container relative shrink-0 h-[28rem] mt-2 mr-16 p-2 bg-[var(--color-neutral-100)] rounded-lg",
2713
- 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)(
2714
2998
  GenrePlaylistTreePerRoot,
2715
2999
  {
2716
3000
  scope,
@@ -2747,11 +3031,11 @@ function GenreTreeView({
2747
3031
  }
2748
3032
 
2749
3033
  // src/genre-tree/GenreDetailPanel.tsx
2750
- var import_react16 = require("react");
3034
+ var import_react18 = require("react");
2751
3035
  var import_genre_tree_view5 = require("@behindthemusictree/genre-tree-view");
2752
- var import_ui14 = require("@behindthemusictree/ui");
2753
- var import_genre_tree18 = require("@behindthemusictree/app-kit/genre-tree");
2754
- 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");
2755
3039
  var ITEM_COUNT_LABEL = "Songs";
2756
3040
  var RELATIVE_ITEM_COUNT = 1;
2757
3041
  function toNode(genre, parentId) {
@@ -2778,9 +3062,9 @@ function GenreDetailPanel({
2778
3062
  onExpandedChange,
2779
3063
  extras
2780
3064
  }) {
2781
- 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);
2782
3066
  const playback = useGenrePlaylistPlayback({ scope, genrePlaylists: [] });
2783
- const view = (0, import_react16.useMemo)(() => {
3067
+ const view = (0, import_react18.useMemo)(() => {
2784
3068
  if (!detail) return null;
2785
3069
  const rootColor = (0, import_genre_tree_view5.getGenreTreeColor)(detail.root.wikidataId);
2786
3070
  const relative = (genre, parentId) => ({
@@ -2806,17 +3090,17 @@ function GenreDetailPanel({
2806
3090
  };
2807
3091
  }, [detail]);
2808
3092
  if (isError) {
2809
- 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." });
2810
3094
  }
2811
3095
  if (isPending || !detail || !view) {
2812
- 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: [
2813
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_ui14.Skeleton, { className: "h-6 w-2/3" }),
2814
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_ui14.Skeleton, { className: "h-4 w-full" }),
2815
- /* @__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" })
2816
3100
  ] });
2817
3101
  }
2818
3102
  const renderBody = (node, sections) => ({
2819
- primary: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
3103
+ primary: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
2820
3104
  GenreInfoPanelBody,
2821
3105
  {
2822
3106
  toolbar: sections.toolbar,
@@ -2833,7 +3117,7 @@ function GenreDetailPanel({
2833
3117
  ),
2834
3118
  aside: null
2835
3119
  });
2836
- return /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
3120
+ return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
2837
3121
  import_genre_tree_view5.InfoPanel,
2838
3122
  {
2839
3123
  node: view.node,
@@ -2859,13 +3143,13 @@ function GenreDetailPanel({
2859
3143
  var import_genre_tree_view6 = require("@behindthemusictree/genre-tree-view");
2860
3144
 
2861
3145
  // src/genre-tree/track-list-sidebar/TrackListSidebar.tsx
2862
- var import_react17 = require("react");
2863
- var import_lucide_react12 = require("lucide-react");
2864
- var import_genre_tree19 = require("@behindthemusictree/app-kit/genre-tree");
2865
- 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");
2866
3150
  var import_track_noun6 = require("@behindthemusictree/app-kit/track-noun");
2867
- var import_jsx_runtime35 = require("react/jsx-runtime");
2868
- 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;
2869
3153
  function TrackListSidebar({
2870
3154
  className: className2 = "",
2871
3155
  renderDuration,
@@ -2874,20 +3158,21 @@ function TrackListSidebar({
2874
3158
  isCollapsed = false,
2875
3159
  onExpand,
2876
3160
  onMinimize,
2877
- renderArtistLink
3161
+ renderArtistLink,
3162
+ renderSongLink
2878
3163
  }) {
2879
- const { trackList, loadMore, autoAdvanceScrollRequest } = (0, import_genre_tree19.useTrackList)();
3164
+ const { trackList, loadMore, autoAdvanceScrollRequest } = (0, import_genre_tree21.useTrackList)();
2880
3165
  const trackNoun = (0, import_track_noun6.useTrackNoun)();
2881
- const trackListRef = (0, import_react17.useRef)(null);
2882
- const previousOriginRef = (0, import_react17.useRef)(trackList?.origin ?? null);
2883
- const handledAutoAdvanceRequestIdRef = (0, import_react17.useRef)(null);
2884
- const shouldFollowPlaybackRef = (0, import_react17.useRef)(true);
2885
- const programmaticScrollTopRef = (0, import_react17.useRef)(null);
2886
- const userScrollIntentRef = (0, import_react17.useRef)(false);
2887
- const userScrollIntentTimerRef = (0, import_react17.useRef)(null);
2888
- 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);
2889
3174
  const hasMore = !!trackList && trackList.nextPage !== null;
2890
- (0, import_react17.useEffect)(() => {
3175
+ (0, import_react19.useEffect)(() => {
2891
3176
  const sentinel = loadMoreSentinelRef.current;
2892
3177
  if (!hasMore || !sentinel) return;
2893
3178
  const observer = new IntersectionObserver((entries) => {
@@ -2907,11 +3192,11 @@ function TrackListSidebar({
2907
3192
  programmaticScrollTopRef.current = null;
2908
3193
  if (trackListRef.current) trackListRef.current.scrollTop = 0;
2909
3194
  }, [trackList?.origin]);
2910
- (0, import_react17.useEffect)(() => {
3195
+ (0, import_react19.useEffect)(() => {
2911
3196
  const list = trackListRef.current;
2912
3197
  if (!isCollapsed && list?.scrollTop === 0) shouldFollowPlaybackRef.current = true;
2913
3198
  }, [isCollapsed]);
2914
- (0, import_react17.useEffect)(
3199
+ (0, import_react19.useEffect)(
2915
3200
  () => () => {
2916
3201
  if (userScrollIntentTimerRef.current !== null) clearTimeout(userScrollIntentTimerRef.current);
2917
3202
  },
@@ -2965,25 +3250,25 @@ function TrackListSidebar({
2965
3250
  }
2966
3251
  };
2967
3252
  const positionClasses = layout === "inline" ? "relative w-full flex h-full flex-col" : "fixed right-0 w-[50rem] flex flex-col";
2968
- return trackList ? /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
3253
+ return trackList ? /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(
2969
3254
  "div",
2970
3255
  {
2971
3256
  "data-theme": "dark",
2972
3257
  className: `track-list-sidebar ${positionClasses} rounded-2xl bg-[var(--color-surface)] pb-1 ${className2}`,
2973
3258
  style: layout === "fixed" ? { top: "63.5px", maxHeight: "calc(100vh - 63.5px - 79px)" } : void 0,
2974
3259
  children: [
2975
- /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "header flex h-16 items-center px-4 py-2 text-[var(--color-text)]", children: [
2976
- /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "min-w-0 flex flex-1 flex-col justify-center", children: [
2977
- /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "origin flex text-xl", children: [
2978
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
2979
- /* @__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 }) })
2980
3265
  ] }),
2981
- /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "info flex items-center text-sm text-[var(--color-text-muted)]", children: [
2982
- 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 `,
2983
3268
  `${trackList.total} ${trackList.total > 1 ? trackNoun.plural : trackNoun.singular}`
2984
3269
  ] })
2985
3270
  ] }),
2986
- (isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
3271
+ (isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
2987
3272
  "button",
2988
3273
  {
2989
3274
  type: "button",
@@ -2992,11 +3277,11 @@ function TrackListSidebar({
2992
3277
  "aria-expanded": !isCollapsed,
2993
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"}`,
2994
3279
  onClick: isCollapsed ? onExpand : onMinimize,
2995
- 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" })
2996
3281
  }
2997
3282
  )
2998
3283
  ] }),
2999
- !isCollapsed && /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
3284
+ !isCollapsed && /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(
3000
3285
  "ul",
3001
3286
  {
3002
3287
  ref: trackListRef,
@@ -3006,17 +3291,18 @@ function TrackListSidebar({
3006
3291
  onPointerDown: markUserScrollIntent,
3007
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",
3008
3293
  children: [
3009
- 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)(
3010
3295
  TrackItem,
3011
3296
  {
3012
3297
  track,
3013
3298
  position: index + 1,
3014
3299
  renderDuration,
3015
3300
  renderActions,
3016
- renderArtistLink
3301
+ renderArtistLink,
3302
+ renderSongLink
3017
3303
  }
3018
3304
  ) }, track.uuid)),
3019
- 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" })
3020
3306
  ]
3021
3307
  }
3022
3308
  )
@@ -3026,16 +3312,16 @@ function TrackListSidebar({
3026
3312
  }
3027
3313
 
3028
3314
  // src/genre-tree/PlayerPanel.tsx
3029
- var import_react18 = require("react");
3315
+ var import_react20 = require("react");
3030
3316
  var import_tailwind_merge2 = require("tailwind-merge");
3031
- var import_player12 = require("@behindthemusictree/app-kit/player");
3032
- 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");
3033
3319
 
3034
3320
  // src/genre-tree/PlayerControls.tsx
3035
- var import_lucide_react13 = require("lucide-react");
3036
- var import_genre_tree21 = require("@behindthemusictree/app-kit/genre-tree");
3037
- var import_player11 = require("@behindthemusictree/app-kit/player");
3038
- 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");
3039
3325
  function PlayerControls() {
3040
3326
  const {
3041
3327
  duration,
@@ -3046,23 +3332,23 @@ function PlayerControls() {
3046
3332
  seekTo,
3047
3333
  setVolume,
3048
3334
  volume
3049
- } = (0, import_player11.usePlayer)();
3050
- const { hasNextTrack, hasPreviousTrack, playNextTrack, playPreviousTrack } = (0, import_genre_tree21.useTrackList)();
3051
- const currentTime = (0, import_player11.useCurrentTime)();
3052
- 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;
3053
3339
  const isReady = !!playerTrackObject?.isReady && !isBusy && !playerTrackObject.loadError;
3054
3340
  const safeDuration = Number.isFinite(duration) && duration > 0 ? duration : 0;
3055
3341
  const safeCurrentTime = Number.isFinite(currentTime) ? Math.max(0, Math.min(currentTime, safeDuration)) : 0;
3056
- const isPlaying = playState === import_player11.PlayStates.PLAYING;
3057
- return /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(
3342
+ const isPlaying = playState === import_player12.PlayStates.PLAYING;
3343
+ return /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(
3058
3344
  "section",
3059
3345
  {
3060
3346
  "aria-label": "Player controls",
3061
3347
  className: "player-controls flex shrink-0 flex-col gap-2 border-b border-zinc-800 px-3 py-2 text-zinc-200",
3062
3348
  children: [
3063
- /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "flex min-w-0 items-center gap-2 text-xs tabular-nums text-zinc-400", children: [
3064
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("span", { "aria-label": "Elapsed time", children: (0, import_genre_tree21.formatTime)(safeCurrentTime) }),
3065
- /* @__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)(
3066
3352
  "input",
3067
3353
  {
3068
3354
  "aria-label": "Seek",
@@ -3076,11 +3362,11 @@ function PlayerControls() {
3076
3362
  onChange: (event) => seekTo(Number(event.currentTarget.value))
3077
3363
  }
3078
3364
  ),
3079
- /* @__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) })
3080
3366
  ] }),
3081
- /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "grid min-h-9 grid-cols-[1fr_auto_1fr] items-center gap-3", children: [
3082
- /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "col-start-2 flex items-center justify-center gap-2", children: [
3083
- /* @__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)(
3084
3370
  "button",
3085
3371
  {
3086
3372
  type: "button",
@@ -3089,10 +3375,10 @@ function PlayerControls() {
3089
3375
  disabled: !isReady || !hasPreviousTrack,
3090
3376
  onClick: playPreviousTrack,
3091
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",
3092
- 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" })
3093
3379
  }
3094
3380
  ),
3095
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
3381
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
3096
3382
  "button",
3097
3383
  {
3098
3384
  type: "button",
@@ -3101,10 +3387,10 @@ function PlayerControls() {
3101
3387
  disabled: !isReady,
3102
3388
  onClick: handlePlayPauseAction,
3103
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",
3104
- 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" })
3105
3391
  }
3106
3392
  ),
3107
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
3393
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
3108
3394
  "button",
3109
3395
  {
3110
3396
  type: "button",
@@ -3113,14 +3399,14 @@ function PlayerControls() {
3113
3399
  disabled: !isReady || !hasNextTrack,
3114
3400
  onClick: playNextTrack,
3115
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",
3116
- 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" })
3117
3403
  }
3118
3404
  )
3119
3405
  ] }),
3120
- /* @__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: [
3121
- 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" }),
3122
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("span", { className: "sr-only", children: "Volume" }),
3123
- /* @__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)(
3124
3410
  "input",
3125
3411
  {
3126
3412
  "aria-label": "Volume",
@@ -3142,7 +3428,7 @@ function PlayerControls() {
3142
3428
  }
3143
3429
 
3144
3430
  // src/genre-tree/PlayerPanel.tsx
3145
- var import_jsx_runtime37 = require("react/jsx-runtime");
3431
+ var import_jsx_runtime39 = require("react/jsx-runtime");
3146
3432
  var PANEL_ID = "player-panel";
3147
3433
  var DEFAULT_EXTENDED_WIDTH = "calc(100vw - var(--layout-center-left) - var(--layout-gutter))";
3148
3434
  function PlayerPanel({
@@ -3151,16 +3437,17 @@ function PlayerPanel({
3151
3437
  expanded,
3152
3438
  onExpandedChange,
3153
3439
  extendedWidth = DEFAULT_EXTENDED_WIDTH,
3154
- renderArtistLink
3440
+ renderArtistLink,
3441
+ renderSongLink
3155
3442
  }) {
3156
- const { playerTrackObject } = (0, import_player12.usePlayer)();
3157
- const { toggleTrackListSidebar, isTrackListSidebarVisible } = (0, import_genre_tree22.useTrackListSidebarVisibility)();
3158
- 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)("");
3159
3446
  const loadedTrackTitle = playerTrackObject?.track.title;
3160
- (0, import_react18.useEffect)(() => {
3447
+ (0, import_react20.useEffect)(() => {
3161
3448
  if (loadedTrackTitle) setLastLoadedTrackTitle(loadedTrackTitle);
3162
3449
  }, [loadedTrackTitle]);
3163
- const [uncontrolledExtended, setUncontrolledExtended] = (0, import_react18.useState)(false);
3450
+ const [uncontrolledExtended, setUncontrolledExtended] = (0, import_react20.useState)(false);
3164
3451
  const isExtendRequested = expanded ?? uncontrolledExtended;
3165
3452
  const setIsExtendRequested = (next) => {
3166
3453
  setUncontrolledExtended(next);
@@ -3181,7 +3468,7 @@ function PlayerPanel({
3181
3468
  onClick: () => setIsExtendRequested(true),
3182
3469
  expanded: false
3183
3470
  } : null;
3184
- return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(
3471
+ return /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(
3185
3472
  "div",
3186
3473
  {
3187
3474
  id: PANEL_ID,
@@ -3194,7 +3481,7 @@ function PlayerPanel({
3194
3481
  ),
3195
3482
  style: isExtended ? { width: extendedWidth } : void 0,
3196
3483
  children: [
3197
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
3484
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3198
3485
  PanelEdgeTab,
3199
3486
  {
3200
3487
  side: "right",
@@ -3204,7 +3491,7 @@ function PlayerPanel({
3204
3491
  className: "absolute right-full top-1/2 -translate-y-1/2"
3205
3492
  }
3206
3493
  ),
3207
- /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(
3494
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(
3208
3495
  "div",
3209
3496
  {
3210
3497
  className: (0, import_tailwind_merge2.twMerge)(
@@ -3212,15 +3499,15 @@ function PlayerPanel({
3212
3499
  isExtended ? "flex-row" : "flex-col"
3213
3500
  ),
3214
3501
  children: [
3215
- /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("div", { className: "flex w-[var(--layout-right-w)] shrink-0 flex-col", children: [
3216
- /* @__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 }) }),
3217
- /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("div", { className: "relative w-full shrink-0", children: [
3218
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_player12.PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
3219
- 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 })
3220
3507
  ] }),
3221
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(PlayerControls, {})
3508
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(PlayerControls, {})
3222
3509
  ] }),
3223
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
3510
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3224
3511
  TrackListSidebar,
3225
3512
  {
3226
3513
  layout: "inline",
@@ -3231,7 +3518,8 @@ function PlayerPanel({
3231
3518
  "min-h-0 min-w-0 flex-1",
3232
3519
  isExtended && "border-l border-zinc-800"
3233
3520
  ),
3234
- renderArtistLink
3521
+ renderArtistLink,
3522
+ renderSongLink
3235
3523
  }
3236
3524
  )
3237
3525
  ]
@@ -3245,33 +3533,33 @@ function PlayerPanel({
3245
3533
  // src/genre-tree/Rating.tsx
3246
3534
  var import_uuid = require("uuid");
3247
3535
  var import_ai = require("react-icons/ai");
3248
- var import_genre_tree23 = require("@behindthemusictree/app-kit/genre-tree");
3249
- 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");
3250
3538
  function Rating({ rating, handleChange }) {
3251
3539
  const instanceId = (0, import_uuid.v4)();
3252
- 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: [
3253
- /* @__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)(
3254
3542
  "input",
3255
3543
  {
3256
3544
  id: `star-${instanceId}-${i}`,
3257
3545
  className: "w-6 h-6 opacity-0",
3258
3546
  type: "radio",
3259
3547
  name: "rating",
3260
- value: i === 0 ? import_genre_tree23.FORM_RATING_NULL_VALUE : i,
3548
+ value: i === 0 ? import_genre_tree25.FORM_RATING_NULL_VALUE : i,
3261
3549
  checked: rating === (i === 0 ? null : i),
3262
3550
  onChange: handleChange
3263
3551
  }
3264
3552
  ),
3265
- /* @__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: [
3266
- /* @__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)(
3267
3555
  import_ai.AiOutlineStar,
3268
3556
  {
3269
- 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)]"}`
3270
3558
  }
3271
3559
  ),
3272
- /* @__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" }),
3273
3561
  " "
3274
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
3562
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3275
3563
  import_ai.AiFillStar,
3276
3564
  {
3277
3565
  className: `w-7 h-7 absolute top-0 left-0 ${(rating ?? -1) >= i ? "text-[var(--color-accent-rating)]" : "text-[var(--color-text-muted)]"}`