@behindthemusictree/app-kit-ui 29.0.1 → 30.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -580,13 +580,13 @@ function AuthCallbackHandler({ renderStatus, ...options }) {
580
580
  }
581
581
 
582
582
  // src/genre-tree/GenreTreeView.tsx
583
- var import_react15 = require("react");
583
+ var import_react17 = require("react");
584
584
  var import_react_query3 = require("@tanstack/react-query");
585
- var import_lucide_react15 = require("lucide-react");
586
- var import_ui16 = require("@behindthemusictree/ui");
585
+ var import_lucide_react16 = require("lucide-react");
586
+ var import_ui18 = require("@behindthemusictree/ui");
587
587
  var import_genre_tree_view4 = require("@behindthemusictree/genre-tree-view");
588
- var import_genre_tree16 = require("@behindthemusictree/app-kit/genre-tree");
589
- var import_genre_tree17 = require("@behindthemusictree/app-kit/genre-tree");
588
+ var import_genre_tree18 = require("@behindthemusictree/app-kit/genre-tree");
589
+ var import_genre_tree19 = require("@behindthemusictree/app-kit/genre-tree");
590
590
  var import_popup6 = require("@behindthemusictree/app-kit/popup");
591
591
 
592
592
  // src/genre-tree/playlist-tree/TreePerRoot.tsx
@@ -1058,6 +1058,7 @@ function GenreTreeWheelHandoff({ skeleton, children, onRevealedChange }) {
1058
1058
  }
1059
1059
 
1060
1060
  // src/genre-tree/GenreSearch.tsx
1061
+ var import_react8 = require("react");
1061
1062
  var import_lucide_react7 = require("lucide-react");
1062
1063
  var import_ui6 = require("@behindthemusictree/ui");
1063
1064
  var import_genre_tree2 = require("@behindthemusictree/app-kit/genre-tree");
@@ -1070,7 +1071,7 @@ function GenreSearch({
1070
1071
  onPrevious,
1071
1072
  placeholder,
1072
1073
  onQueryChange,
1073
- extraResults
1074
+ extraGroups = []
1074
1075
  }) {
1075
1076
  const { query, setQuery, results, canClear, clear, select } = (0, import_genre_tree2.useGenreSearch)({
1076
1077
  genrePlaylists,
@@ -1078,11 +1079,45 @@ function GenreSearch({
1078
1079
  selectedName,
1079
1080
  onClear
1080
1081
  });
1081
- const extraItems = query !== "" && query !== selectedName ? extraResults?.items ?? [] : [];
1082
- const label = extraResults ? "Search" : "Search a genre";
1083
- const loading = query !== "" && query !== selectedName && (extraResults?.loading ?? false);
1084
- const showList = results.length > 0 || extraItems.length > 0;
1085
- const heading = "px-3 pt-2 pb-1 text-xs font-medium uppercase text-[var(--color-text-muted)]";
1082
+ const [open, setOpen] = (0, import_react8.useState)(false);
1083
+ const active = query !== "" && query !== selectedName;
1084
+ const hasExtras = extraGroups.length > 0;
1085
+ const label = hasExtras ? "Search" : "Search a genre";
1086
+ const loading = active && extraGroups.some((group) => group.loading);
1087
+ const status = active ? extraGroups.map((group) => group.status).filter(Boolean).join(". ") : "";
1088
+ const groups = [
1089
+ {
1090
+ key: "genres",
1091
+ label: hasExtras ? "Genres" : void 0,
1092
+ options: results.map((genrePlaylist) => ({
1093
+ key: genrePlaylist.uuid,
1094
+ content: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
1095
+ genrePlaylist.name,
1096
+ genrePlaylist.isUmbrella && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "ml-2 text-xs italic text-[var(--color-text-muted)]", children: "umbrella" })
1097
+ ] })
1098
+ }))
1099
+ },
1100
+ ...extraGroups.map((group) => ({
1101
+ key: group.key,
1102
+ label: group.label,
1103
+ options: active ? group.items.map((item) => ({ key: item.key, content: item.content })) : []
1104
+ }))
1105
+ ];
1106
+ const handlePick = (groupKey, optionKey) => {
1107
+ setOpen(false);
1108
+ if (groupKey === "genres") {
1109
+ const genrePlaylist = results.find((result) => result.uuid === optionKey);
1110
+ if (!genrePlaylist) return;
1111
+ select(genrePlaylist);
1112
+ onQueryChange?.("");
1113
+ return;
1114
+ }
1115
+ const item = extraGroups.find((group) => group.key === groupKey)?.items.find((entry) => entry.key === optionKey);
1116
+ if (!item) return;
1117
+ setQuery("");
1118
+ onQueryChange?.("");
1119
+ item.onSelect();
1120
+ };
1086
1121
  return /* @__PURE__ */ (0, import_jsx_runtime16.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: [
1087
1122
  onPrevious && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1088
1123
  "button",
@@ -1096,47 +1131,45 @@ function GenreSearch({
1096
1131
  }
1097
1132
  ),
1098
1133
  /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1099
- import_ui6.Input,
1134
+ import_ui6.Combobox,
1100
1135
  {
1101
- type: "text",
1136
+ ariaLabel: label,
1137
+ listLabel: `${label} results`,
1102
1138
  value: query,
1103
- onChange: (event) => {
1104
- setQuery(event.target.value);
1105
- onQueryChange?.(event.target.value);
1106
- },
1107
1139
  placeholder: placeholder ?? `${label}\u2026`,
1108
- "aria-label": label,
1109
- "aria-busy": loading,
1110
- inputMode: "search",
1111
- enterKeyHint: "search",
1112
- autoComplete: "off",
1113
- 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"
1140
+ groups,
1141
+ open,
1142
+ onOpenChange: setOpen,
1143
+ onValueChange: (value) => {
1144
+ setQuery(value);
1145
+ onQueryChange?.(value);
1146
+ },
1147
+ onPick: handlePick,
1148
+ status,
1149
+ loading: loading ? "Searching\u2026" : void 0,
1150
+ 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",
1151
+ className: "genre-search-results z-40 rounded-xl shadow-md",
1152
+ listClassName: "max-h-[min(20rem,50dvh)]",
1153
+ optionClassName: "[@media(pointer:coarse)]:min-h-[var(--layout-touch-target)]"
1114
1154
  }
1115
1155
  ),
1116
1156
  /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "absolute right-3 top-1/2 flex items-center -translate-y-1/2", children: [
1117
- /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
1118
- "span",
1119
- {
1120
- "data-loading": loading,
1121
- className: "relative p-2 text-[var(--color-text-muted)] pointer-events-none",
1122
- children: [
1123
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1124
- import_lucide_react7.Search,
1125
- {
1126
- "aria-hidden": "true",
1127
- className: `w-6 h-6 transition-opacity duration-200 motion-reduce:transition-none ${loading ? "opacity-0" : ""}`
1128
- }
1129
- ),
1130
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1131
- import_lucide_react7.LoaderCircle,
1132
- {
1133
- "aria-hidden": "true",
1134
- 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"}`
1135
- }
1136
- )
1137
- ]
1138
- }
1139
- ),
1157
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("span", { "data-loading": loading, className: "relative p-2 text-[var(--color-text-muted)] pointer-events-none", children: [
1158
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1159
+ import_lucide_react7.Search,
1160
+ {
1161
+ "aria-hidden": "true",
1162
+ className: `w-6 h-6 transition-opacity duration-200 motion-reduce:transition-none ${loading ? "opacity-0" : ""}`
1163
+ }
1164
+ ),
1165
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1166
+ import_lucide_react7.LoaderCircle,
1167
+ {
1168
+ "aria-hidden": "true",
1169
+ 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"}`
1170
+ }
1171
+ )
1172
+ ] }),
1140
1173
  canClear && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1141
1174
  "button",
1142
1175
  {
@@ -1150,39 +1183,6 @@ function GenreSearch({
1150
1183
  children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_lucide_react7.X, { "aria-hidden": "true", className: "w-6 h-6" })
1151
1184
  }
1152
1185
  )
1153
- ] }),
1154
- extraResults?.status && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { role: "status", "aria-live": "polite", className: "sr-only", children: extraResults.status }),
1155
- showList && /* @__PURE__ */ (0, import_jsx_runtime16.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: [
1156
- extraResults && results.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("li", { className: heading, children: "Genres" }),
1157
- results.map((genrePlaylist) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
1158
- "button",
1159
- {
1160
- type: "button",
1161
- 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)]",
1162
- onClick: () => {
1163
- select(genrePlaylist);
1164
- onQueryChange?.("");
1165
- },
1166
- children: [
1167
- genrePlaylist.name,
1168
- genrePlaylist.isUmbrella && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "ml-2 text-xs italic text-[var(--color-text-muted)]", children: "umbrella" })
1169
- ]
1170
- }
1171
- ) }, genrePlaylist.uuid)),
1172
- extraResults && extraItems.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("li", { className: heading, children: extraResults.label }),
1173
- extraItems.map((item) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1174
- "button",
1175
- {
1176
- type: "button",
1177
- 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)]",
1178
- onClick: () => {
1179
- setQuery("");
1180
- onQueryChange?.("");
1181
- item.onSelect();
1182
- },
1183
- children: item.content
1184
- }
1185
- ) }, item.key))
1186
1186
  ] })
1187
1187
  ] });
1188
1188
  }
@@ -1198,7 +1198,7 @@ var import_lucide_react10 = require("lucide-react");
1198
1198
  var import_popup4 = require("@behindthemusictree/app-kit/popup");
1199
1199
 
1200
1200
  // src/genre-tree/SharePopup.tsx
1201
- var import_react8 = require("react");
1201
+ var import_react9 = require("react");
1202
1202
  var import_lucide_react9 = require("lucide-react");
1203
1203
  var import_fa3 = require("react-icons/fa");
1204
1204
  var import_si = require("react-icons/si");
@@ -1230,8 +1230,8 @@ function ShareDestinationButton({
1230
1230
  var import_lucide_react8 = require("lucide-react");
1231
1231
  var import_jsx_runtime18 = require("react/jsx-runtime");
1232
1232
  function SharePreviewCard({ target, url }) {
1233
- const Icon = target.kind === "genre" ? import_lucide_react8.Music2 : import_lucide_react8.UserRound;
1234
- const typeLabel = target.kind === "genre" ? "Genre" : "Artist";
1233
+ const Icon = { genre: import_lucide_react8.Music2, artist: import_lucide_react8.UserRound, song: import_lucide_react8.Music }[target.kind];
1234
+ const typeLabel = { genre: "Genre", artist: "Artist", song: "Song" }[target.kind];
1235
1235
  return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "rounded-3xl bg-[var(--color-surface-muted)] p-4", children: /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "flex min-h-32 overflow-hidden rounded-2xl bg-[var(--color-surface)] shadow-lg", children: [
1236
1236
  /* @__PURE__ */ (0, import_jsx_runtime18.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_runtime18.jsx)(Icon, { "aria-hidden": "true", className: "h-10 w-10" }) }),
1237
1237
  /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col justify-center gap-1 p-4", children: [
@@ -1276,20 +1276,20 @@ async function copyText(text) {
1276
1276
  }
1277
1277
  }
1278
1278
  function SharePopup({ target, url, onClose }) {
1279
- const [absoluteUrl, setAbsoluteUrl] = (0, import_react8.useState)(url);
1280
- const [copyStatus, setCopyStatus] = (0, import_react8.useState)("idle");
1281
- const [canUseNativeShare, setCanUseNativeShare] = (0, import_react8.useState)(false);
1282
- const [shareError, setShareError] = (0, import_react8.useState)(null);
1283
- const [toastId, setToastId] = (0, import_react8.useState)(0);
1284
- (0, import_react8.useEffect)(() => {
1279
+ const [absoluteUrl, setAbsoluteUrl] = (0, import_react9.useState)(url);
1280
+ const [copyStatus, setCopyStatus] = (0, import_react9.useState)("idle");
1281
+ const [canUseNativeShare, setCanUseNativeShare] = (0, import_react9.useState)(false);
1282
+ const [shareError, setShareError] = (0, import_react9.useState)(null);
1283
+ const [toastId, setToastId] = (0, import_react9.useState)(0);
1284
+ (0, import_react9.useEffect)(() => {
1285
1285
  setAbsoluteUrl(resolveAbsoluteUrl(url) ?? "");
1286
1286
  setCopyStatus("idle");
1287
1287
  setShareError(null);
1288
1288
  }, [url]);
1289
- (0, import_react8.useEffect)(() => {
1289
+ (0, import_react9.useEffect)(() => {
1290
1290
  setCanUseNativeShare(typeof navigator !== "undefined" && typeof navigator.share === "function");
1291
1291
  }, []);
1292
- const destinationUrls = (0, import_react8.useMemo)(() => {
1292
+ const destinationUrls = (0, import_react9.useMemo)(() => {
1293
1293
  if (!absoluteUrl) return null;
1294
1294
  const message = `${target.name} \u2014 ${absoluteUrl}`;
1295
1295
  return {
@@ -1307,7 +1307,7 @@ function SharePopup({ target, url, onClose }) {
1307
1307
  setCopyStatus(copied ? "idle" : "failed");
1308
1308
  if (copied) setToastId((id) => id + 1);
1309
1309
  };
1310
- const dismissToast = (0, import_react8.useCallback)(() => setToastId(0), []);
1310
+ const dismissToast = (0, import_react9.useCallback)(() => setToastId(0), []);
1311
1311
  const handleNativeShare = async () => {
1312
1312
  if (!absoluteUrl || !navigator.share) return;
1313
1313
  setShareError(null);
@@ -1513,13 +1513,13 @@ var import_ui8 = require("@behindthemusictree/ui");
1513
1513
  var import_genre_tree9 = require("@behindthemusictree/app-kit/genre-tree");
1514
1514
 
1515
1515
  // src/genre-tree/ClampedText.tsx
1516
- var import_react9 = require("react");
1516
+ var import_react10 = require("react");
1517
1517
  var import_jsx_runtime22 = require("react/jsx-runtime");
1518
1518
  function ClampedText({ children, className: className2 = "" }) {
1519
- const ref = (0, import_react9.useRef)(null);
1520
- const [expanded, setExpanded] = (0, import_react9.useState)(false);
1521
- const [overflows, setOverflows] = (0, import_react9.useState)(false);
1522
- (0, import_react9.useLayoutEffect)(() => {
1519
+ const ref = (0, import_react10.useRef)(null);
1520
+ const [expanded, setExpanded] = (0, import_react10.useState)(false);
1521
+ const [overflows, setOverflows] = (0, import_react10.useState)(false);
1522
+ (0, import_react10.useLayoutEffect)(() => {
1523
1523
  const element = ref.current;
1524
1524
  if (!element || expanded) return;
1525
1525
  setOverflows(element.scrollHeight > element.clientHeight);
@@ -1540,7 +1540,7 @@ function ClampedText({ children, className: className2 = "" }) {
1540
1540
  }
1541
1541
 
1542
1542
  // src/genre-tree/GenrePlaylistTracks.tsx
1543
- var import_react10 = require("react");
1543
+ var import_react11 = require("react");
1544
1544
  var import_react_query = require("@tanstack/react-query");
1545
1545
  var import_zod = require("zod");
1546
1546
  var import_fa4 = require("react-icons/fa");
@@ -1608,6 +1608,7 @@ function GenrePlaylistTracks({
1608
1608
  scope,
1609
1609
  getBackendBaseUrl,
1610
1610
  renderArtistLink,
1611
+ renderSongLink,
1611
1612
  renderSongLeading,
1612
1613
  renderSongActions,
1613
1614
  renderTracksToolbar,
@@ -1638,8 +1639,8 @@ function GenrePlaylistTracks({
1638
1639
  getNextPageParam: (lastPage) => lastPage.nextPage ?? void 0,
1639
1640
  enabled: scope !== "me" || sessionRestored && !!session?.accessToken
1640
1641
  });
1641
- const sentinelRef = (0, import_react10.useRef)(null);
1642
- (0, import_react10.useEffect)(() => {
1642
+ const sentinelRef = (0, import_react11.useRef)(null);
1643
+ (0, import_react11.useEffect)(() => {
1643
1644
  const sentinel = sentinelRef.current;
1644
1645
  if (!hasNextPage || isFetchingNextPage || !sentinel) return;
1645
1646
  const observer = new IntersectionObserver((entries) => {
@@ -1678,7 +1679,7 @@ function GenrePlaylistTracks({
1678
1679
  const isCurrent = playerTrackObject?.track.id === track.uuid;
1679
1680
  const isPlaying = isCurrent && playState === import_player5.PlayStates.PLAYING;
1680
1681
  const text = /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("span", { className: TEXT_CELL, children: [
1681
- /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { className: TITLE_CELL, children: (0, import_genre_tree6.isSong)(track) ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }),
1682
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { className: TITLE_CELL, children: renderSongLink ? renderSongLink(track) : (0, import_genre_tree6.isSong)(track) ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }),
1682
1683
  track.artists?.length ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { className: SEPARATOR, children: " \u2014 " }) : null,
1683
1684
  /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { className: COLUMN_CELL, children: track.artists?.length ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_genre_tree8.ArtistNames, { artists: track.artists, renderArtistLink }) : null }),
1684
1685
  track.album && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { className: ALBUM_CELL, children: track.album.name })
@@ -1745,6 +1746,7 @@ function GenreDetailExtras({
1745
1746
  criteriaOverviewSchema,
1746
1747
  renderGenreDetailExtras,
1747
1748
  renderArtistLink,
1749
+ renderSongLink,
1748
1750
  renderSummary,
1749
1751
  renderEssentialTracks,
1750
1752
  family,
@@ -1822,6 +1824,7 @@ function GenreDetailExtras({
1822
1824
  scope,
1823
1825
  getBackendBaseUrl,
1824
1826
  renderArtistLink,
1827
+ renderSongLink,
1825
1828
  ...trackSlots
1826
1829
  }
1827
1830
  )
@@ -1830,7 +1833,7 @@ function GenreDetailExtras({
1830
1833
 
1831
1834
  // src/genre-tree/GenreInfoPanelBody.tsx
1832
1835
  var import_jsx_runtime26 = require("react/jsx-runtime");
1833
- var import_react11 = require("react");
1836
+ var import_react12 = require("react");
1834
1837
  function GenreNodeActionBand({ genrePlaylist, playback, ...bandProps }) {
1835
1838
  const { isPlaying, setIsPlaying } = (0, import_player6.usePlayer)();
1836
1839
  const isCurrentTrackList = playback.playingNodeId === genrePlaylist.uuid;
@@ -1867,7 +1870,7 @@ function GenreInfoPanelBody({
1867
1870
  return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(import_jsx_runtime26.Fragment, { children: [
1868
1871
  toolbar,
1869
1872
  /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(GenreNodeActionBand, { ...band, genrePlaylist, playback }),
1870
- /* @__PURE__ */ (0, import_react11.createElement)(
1873
+ /* @__PURE__ */ (0, import_react12.createElement)(
1871
1874
  GenreDetailExtras,
1872
1875
  {
1873
1876
  ...extras,
@@ -1927,9 +1930,9 @@ function PanelEdgeTab({ side, edge, center, controls, className: className2 }) {
1927
1930
  }
1928
1931
 
1929
1932
  // src/genre-tree/useLeftDetailPanelLayout.ts
1930
- var import_react12 = require("react");
1933
+ var import_react13 = require("react");
1931
1934
  function useLeftDetailPanelLayout(isPanelShown, enabled = true) {
1932
- (0, import_react12.useEffect)(() => {
1935
+ (0, import_react13.useEffect)(() => {
1933
1936
  if (!enabled || isPanelShown) return void 0;
1934
1937
  const root = document.documentElement;
1935
1938
  root.style.setProperty("--layout-left-w", "0px");
@@ -1940,7 +1943,7 @@ function useLeftDetailPanelLayout(isPanelShown, enabled = true) {
1940
1943
  }
1941
1944
 
1942
1945
  // src/genre-tree/ChangeParentPopup.tsx
1943
- var import_react13 = require("react");
1946
+ var import_react14 = require("react");
1944
1947
  var import_genre_tree10 = require("@behindthemusictree/app-kit/genre-tree");
1945
1948
  var import_jsx_runtime28 = require("react/jsx-runtime");
1946
1949
  function ChangeParentPopup({
@@ -1952,7 +1955,7 @@ function ChangeParentPopup({
1952
1955
  onParentChanged
1953
1956
  }) {
1954
1957
  const { mutate, isPending, formErrors } = (0, import_genre_tree10.useUpdateGenre)(scope, getBackendBaseUrl);
1955
- const candidates = (0, import_react13.useMemo)(
1958
+ const candidates = (0, import_react14.useMemo)(
1956
1959
  () => (0, import_genre_tree10.getReparentCandidates)(genrePlaylists, genrePlaylist.uuid),
1957
1960
  [genrePlaylists, genrePlaylist.uuid]
1958
1961
  );
@@ -1970,7 +1973,7 @@ function ChangeParentPopup({
1970
1973
  }
1971
1974
  );
1972
1975
  };
1973
- (0, import_react13.useEffect)(() => {
1976
+ (0, import_react14.useEffect)(() => {
1974
1977
  const handleKeyDown = (event) => {
1975
1978
  if (event.key === "Escape") onClose();
1976
1979
  };
@@ -2158,7 +2161,7 @@ function ArtistDiscography({ albums }) {
2158
2161
  }
2159
2162
 
2160
2163
  // src/genre-tree/artist/ArtistSongs.tsx
2161
- var import_react14 = require("react");
2164
+ var import_react15 = require("react");
2162
2165
  var import_react_query2 = require("@tanstack/react-query");
2163
2166
  var import_lucide_react14 = require("lucide-react");
2164
2167
  var import_ui13 = require("@behindthemusictree/ui");
@@ -2231,7 +2234,8 @@ function TrackItem({
2231
2234
  position,
2232
2235
  renderDuration,
2233
2236
  renderActions,
2234
- renderArtistLink
2237
+ renderArtistLink,
2238
+ renderSongLink
2235
2239
  }) {
2236
2240
  const { handlePlayPauseAction, playerTrackObject, loadTrackForPlayer } = (0, import_player9.usePlayer)();
2237
2241
  const { trackList, selectedTrack, toTrackAtPosition, runtimeUnplayableTrackIds } = (0, import_genre_tree11.useTrackList)();
@@ -2271,7 +2275,7 @@ function TrackItem({
2271
2275
  }
2272
2276
  ),
2273
2277
  /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
2274
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "flex max-w-full items-center gap-2", children: /* @__PURE__ */ (0, import_jsx_runtime35.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_runtime35.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }) }),
2278
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "flex max-w-full items-center gap-2", children: /* @__PURE__ */ (0, import_jsx_runtime35.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_runtime35.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }) }),
2275
2279
  track.artists && track.artists.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "artist text-base max-w-full truncate text-[var(--color-text-muted)]", children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_genre_tree13.ArtistNames, { artists: track.artists, renderArtistLink }) }) : ""
2276
2280
  ] }),
2277
2281
  /* @__PURE__ */ (0, import_jsx_runtime35.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_runtime35.jsx)("div", { className: "genre-name max-w-full whitespace-normal break-words p-1 text-xs font-bold", children: track.genre.name }) : "" }),
@@ -2296,9 +2300,9 @@ var SORTS = [
2296
2300
  { ordering: "title", label: "Title" },
2297
2301
  { ordering: "-albumYear", label: "Year" }
2298
2302
  ];
2299
- function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink }) {
2300
- const [page, setPage] = (0, import_react14.useState)(1);
2301
- const [ordering, setOrdering] = (0, import_react14.useState)("title");
2303
+ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLink }) {
2304
+ const [page, setPage] = (0, import_react15.useState)(1);
2305
+ const [ordering, setOrdering] = (0, import_react15.useState)("title");
2302
2306
  const { fetch } = (0, import_transport8.useFetchWrapper)(getBackendBaseUrl);
2303
2307
  const { playNewTrackListFromArtist } = (0, import_genre_tree14.useTrackList)();
2304
2308
  const { playerTrackObject } = (0, import_player10.usePlayer)();
@@ -2357,7 +2361,8 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink }) {
2357
2361
  {
2358
2362
  track: song,
2359
2363
  position: (page - 1) * data.pageSize + index + 1,
2360
- renderArtistLink
2364
+ renderArtistLink,
2365
+ renderSongLink
2361
2366
  }
2362
2367
  ) }, song.uuid)) }),
2363
2368
  /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui13.Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
@@ -2372,6 +2377,7 @@ function useArtistPanel({
2372
2377
  getBackendBaseUrl,
2373
2378
  getShareUrl,
2374
2379
  renderArtistLink,
2380
+ renderSongLink,
2375
2381
  expanded,
2376
2382
  onExpandedChange,
2377
2383
  onClose
@@ -2424,7 +2430,8 @@ function useArtistPanel({
2424
2430
  {
2425
2431
  artist: { uuid: artist.uuid, name: artist.name },
2426
2432
  getBackendBaseUrl,
2427
- renderArtistLink
2433
+ renderArtistLink,
2434
+ renderSongLink
2428
2435
  }
2429
2436
  )
2430
2437
  ] }),
@@ -2434,30 +2441,225 @@ function useArtistPanel({
2434
2441
  }
2435
2442
  }
2436
2443
 
2437
- // src/genre-tree/SizeMetricToggle.tsx
2438
- var import_md2 = require("react-icons/md");
2444
+ // src/genre-tree/song/useSongPanel.tsx
2445
+ var import_ui16 = require("@behindthemusictree/ui");
2446
+ var import_genre_tree17 = require("@behindthemusictree/app-kit/genre-tree");
2447
+
2448
+ // src/genre-tree/song/SongPanelContent.tsx
2449
+ var import_react16 = require("react");
2450
+ var import_lucide_react15 = require("lucide-react");
2439
2451
  var import_ui15 = require("@behindthemusictree/ui");
2452
+ var import_player11 = require("@behindthemusictree/app-kit/player");
2453
+ var import_genre_tree16 = require("@behindthemusictree/app-kit/genre-tree");
2440
2454
  var import_jsx_runtime38 = require("react/jsx-runtime");
2455
+ var numberFormat = new Intl.NumberFormat("en");
2456
+ var dateFormat = new Intl.DateTimeFormat("en", { dateStyle: "medium" });
2457
+ var externalLink = "text-[var(--color-primary)] hover:underline focus-visible:underline";
2458
+ function SongHistory({ uuid, getBackendBaseUrl }) {
2459
+ const [open, setOpen] = (0, import_react16.useState)(false);
2460
+ const history = (0, import_genre_tree16.useSongHistory)(uuid, getBackendBaseUrl, open);
2461
+ let body = null;
2462
+ if (history.status === "loading") body = /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_ui15.Skeleton, { className: "h-10", "aria-busy": "true" });
2463
+ else if (history.status === "error") body = /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("p", { className: "text-red-600", children: "Could not load the history." });
2464
+ else if (history.status === "success")
2465
+ body = history.entries.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "No changes yet." }) : /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("ul", { className: "divide-y divide-[var(--color-border)]", children: history.entries.map((entry) => /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("li", { className: "py-1.5 text-sm", children: [
2466
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { className: "font-medium", children: entry.action }),
2467
+ entry.oldValue !== null || entry.newValue !== null ? /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
2468
+ " ",
2469
+ "\u2014 ",
2470
+ entry.oldValue ?? "none",
2471
+ " \u2192 ",
2472
+ entry.newValue ?? "none"
2473
+ ] }) : null,
2474
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("span", { className: "block text-xs text-[var(--color-text-muted)]", children: [
2475
+ entry.actorPseudo ?? "Anonymous",
2476
+ " \xB7 ",
2477
+ dateFormat.format(new Date(entry.createdOn))
2478
+ ] })
2479
+ ] }, entry.uuid)) });
2480
+ return /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(
2481
+ "details",
2482
+ {
2483
+ onToggle: (event) => setOpen(event.currentTarget.open),
2484
+ className: "px-6 pb-2.5 mt-2.5 border-t border-[var(--color-border)]",
2485
+ children: [
2486
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("summary", { className: "mt-2.5 cursor-pointer text-[0.6875rem] font-semibold uppercase text-[var(--color-text-muted)]", children: "History" }),
2487
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("div", { className: "mt-1.5", children: body })
2488
+ ]
2489
+ }
2490
+ );
2491
+ }
2492
+ function SongPanelContent({
2493
+ song,
2494
+ scope,
2495
+ getBackendBaseUrl,
2496
+ shareUrl,
2497
+ renderArtistLink,
2498
+ genrePath,
2499
+ onSelectGenre
2500
+ }) {
2501
+ const { playNewTrackListFromTrackUuid } = (0, import_genre_tree16.useTrackList)();
2502
+ const { playerTrackObject, playState, handlePlayPauseAction } = (0, import_player11.usePlayer)();
2503
+ const unplayableReason = (0, import_genre_tree16.songUnplayableReason)(song);
2504
+ const isCurrent = playerTrackObject?.track.id === song.uuid;
2505
+ const isPlaying = isCurrent && playState === import_player11.PlayStates.PLAYING;
2506
+ const shareTarget = { kind: "song", uuid: song.uuid, name: song.title, scope };
2507
+ const musicBrainzUrl = (0, import_genre_tree16.musicBrainzRecordingUrl)(song);
2508
+ const fields = [];
2509
+ if (song.genre) {
2510
+ const path = genrePath.length > 0 ? genrePath : null;
2511
+ fields.push({
2512
+ label: "Genre",
2513
+ value: path ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { className: "flex flex-wrap items-center gap-x-1", children: path.map((genrePlaylist, index) => /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("span", { children: [
2514
+ index > 0 && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { "aria-hidden": "true", children: "\u203A " }),
2515
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("button", { type: "button", onClick: () => onSelectGenre(genrePlaylist), className: "hover:underline", children: genrePlaylist.name })
2516
+ ] }, genrePlaylist.uuid)) }) : song.genre.name
2517
+ });
2518
+ } else {
2519
+ fields.push({ label: "Genre", value: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { className: "text-[var(--color-text-muted)]", children: "No genre" }) });
2520
+ }
2521
+ if (song.firstReleaseYear) fields.push({ label: "First released", value: song.firstReleaseYear });
2522
+ const viewCount = song.youtubeVideo?.viewCount;
2523
+ if (viewCount !== null && viewCount !== void 0)
2524
+ fields.push({ label: "YouTube views", value: numberFormat.format(viewCount) });
2525
+ fields.push({ label: "Plays", value: numberFormat.format(song.playCount) });
2526
+ const links = [];
2527
+ if (musicBrainzUrl)
2528
+ links.push(
2529
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("a", { href: musicBrainzUrl, target: "_blank", rel: "noopener noreferrer", className: externalLink, children: "MusicBrainz" }, "mb")
2530
+ );
2531
+ if (song.youtubeVideo)
2532
+ links.push(
2533
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2534
+ "a",
2535
+ {
2536
+ href: `https://www.youtube.com/watch?v=${encodeURIComponent(song.youtubeVideo.id)}`,
2537
+ target: "_blank",
2538
+ rel: "noopener noreferrer",
2539
+ className: externalLink,
2540
+ children: "YouTube"
2541
+ },
2542
+ "yt"
2543
+ )
2544
+ );
2545
+ if (links.length > 0)
2546
+ fields.push({
2547
+ label: "Links",
2548
+ value: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { className: "flex gap-3", children: links })
2549
+ });
2550
+ return /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(import_jsx_runtime38.Fragment, { children: [
2551
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: [
2552
+ unplayableReason ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("p", { className: "flex items-center px-2.5 text-sm text-[var(--color-text-muted)]", children: unplayableReason }) : /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(
2553
+ "button",
2554
+ {
2555
+ type: "button",
2556
+ onClick: () => isCurrent ? handlePlayPauseAction() : playNewTrackListFromTrackUuid(song, scope),
2557
+ className: "group flex flex-col items-center gap-1 w-16 text-xs text-center text-[var(--color-text)] focus-visible:outline-none",
2558
+ children: [
2559
+ /* @__PURE__ */ (0, import_jsx_runtime38.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_runtime38.jsx)(import_lucide_react15.Pause, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_lucide_react15.Play, { "aria-hidden": "true", className: "w-5 h-5" }) }),
2560
+ isPlaying ? "Pause" : "Play"
2561
+ ]
2562
+ }
2563
+ ),
2564
+ shareUrl && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl })
2565
+ ] }) }),
2566
+ song.artists && song.artists.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("p", { className: "px-6 pt-2.5 text-[var(--color-text-muted)]", children: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_genre_tree16.ArtistNames, { artists: song.artists, renderArtistLink }) }),
2567
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_ui15.FieldList, { items: fields }),
2568
+ song.essentialGenres && song.essentialGenres.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_ui15.PanelSection, { title: "Essential in", children: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("ul", { children: song.essentialGenres.map((genre) => /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("li", { className: "py-0.5", children: genre.name }, genre.uuid)) }) }),
2569
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(SongHistory, { uuid: song.uuid, getBackendBaseUrl })
2570
+ ] });
2571
+ }
2572
+
2573
+ // src/genre-tree/song/useSongPanel.tsx
2574
+ var import_jsx_runtime39 = require("react/jsx-runtime");
2575
+ function useSongPanel({
2576
+ uuid,
2577
+ scope,
2578
+ getBackendBaseUrl,
2579
+ getShareUrl,
2580
+ renderArtistLink,
2581
+ getGenrePath,
2582
+ onSelectGenre,
2583
+ expanded,
2584
+ onExpandedChange,
2585
+ onClose
2586
+ }) {
2587
+ const result = (0, import_genre_tree17.useSong)(uuid, getBackendBaseUrl);
2588
+ const panel = { expanded, onExpandedChange, onClose };
2589
+ switch (result.status) {
2590
+ case "idle":
2591
+ return void 0;
2592
+ case "loading":
2593
+ return {
2594
+ ...panel,
2595
+ title: "Song",
2596
+ content: /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
2597
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_ui16.Skeleton, { className: "h-3 w-16" }),
2598
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_ui16.Skeleton, { className: "h-4 w-32" }),
2599
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_ui16.Skeleton, { className: "h-3 w-16" }),
2600
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_ui16.Skeleton, { className: "h-4 w-24" })
2601
+ ] })
2602
+ };
2603
+ case "not-found":
2604
+ return {
2605
+ ...panel,
2606
+ title: "Song not found",
2607
+ content: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This song does not exist." })
2608
+ };
2609
+ case "error":
2610
+ return {
2611
+ ...panel,
2612
+ title: "Song",
2613
+ content: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this song." })
2614
+ };
2615
+ case "success": {
2616
+ const { song } = result;
2617
+ const shareUrl = getShareUrl?.({ kind: "song", uuid: song.uuid, name: song.title, scope });
2618
+ return {
2619
+ ...panel,
2620
+ title: song.title,
2621
+ content: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2622
+ SongPanelContent,
2623
+ {
2624
+ song,
2625
+ scope,
2626
+ getBackendBaseUrl,
2627
+ shareUrl: shareUrl ?? void 0,
2628
+ renderArtistLink,
2629
+ genrePath: song.genre ? getGenrePath(song.genre) : [],
2630
+ onSelectGenre
2631
+ }
2632
+ )
2633
+ };
2634
+ }
2635
+ }
2636
+ }
2637
+
2638
+ // src/genre-tree/SizeMetricToggle.tsx
2639
+ var import_md2 = require("react-icons/md");
2640
+ var import_ui17 = require("@behindthemusictree/ui");
2641
+ var import_jsx_runtime40 = require("react/jsx-runtime");
2441
2642
  function SizeMetricToggle({
2442
2643
  value,
2443
2644
  onChange
2444
2645
  }) {
2445
- return /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2446
- import_ui15.SegmentedControl,
2646
+ return /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2647
+ import_ui17.SegmentedControl,
2447
2648
  {
2649
+ variant: "floating",
2448
2650
  "aria-label": "Size nodes by",
2449
2651
  value,
2450
2652
  onChange,
2451
2653
  options: [
2452
- { value: "songs", label: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_md2.MdMusicNote, { size: 18, "aria-hidden": true }), "aria-label": "Size by songs", title: "Node size by song count" },
2453
- { value: "views", label: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_md2.MdVisibility, { size: 18, "aria-hidden": true }), "aria-label": "Size by views", title: "Node size by song views" }
2654
+ { value: "songs", label: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_md2.MdMusicNote, { size: 18, "aria-hidden": true }), "aria-label": "Size by songs", title: "Node size by song count" },
2655
+ { value: "views", label: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_md2.MdVisibility, { size: 18, "aria-hidden": true }), "aria-label": "Size by views", title: "Node size by song views" }
2454
2656
  ]
2455
2657
  }
2456
2658
  );
2457
2659
  }
2458
2660
 
2459
2661
  // src/genre-tree/GenreTreeView.tsx
2460
- var import_jsx_runtime39 = require("react/jsx-runtime");
2662
+ var import_jsx_runtime41 = require("react/jsx-runtime");
2461
2663
  var PREVIOUS_BUTTON_CSS = `
2462
2664
  .genre-tree-view .search-overlay .gtv-info-panel-previous { display: none; }
2463
2665
  .genre-tree-view:has(.search-overlay .gtv-info-panel-previous) .genre-search input { padding-left: 2.75rem; }
@@ -2501,6 +2703,13 @@ function GenreTreeView({
2501
2703
  artistSearch = true,
2502
2704
  onArtistSelect,
2503
2705
  artistSearchBaseUrl,
2706
+ songUuid: controlledSongUuid,
2707
+ onSongUuidChange,
2708
+ songExpanded: controlledSongExpanded,
2709
+ onSongExpandedChange,
2710
+ songSearch = true,
2711
+ onSongSelect,
2712
+ renderSongLink,
2504
2713
  renderSummary,
2505
2714
  renderEssentialTracks,
2506
2715
  renderSongLeading,
@@ -2515,14 +2724,21 @@ function GenreTreeView({
2515
2724
  placement = "default",
2516
2725
  ...focus
2517
2726
  }) {
2518
- const getShareUrlRef = (0, import_react15.useRef)(getShareUrl);
2727
+ const getShareUrlRef = (0, import_react17.useRef)(getShareUrl);
2519
2728
  getShareUrlRef.current = getShareUrl;
2520
- const [internalArtistUuid, setInternalArtistUuid] = (0, import_react15.useState)(null);
2729
+ const [internalArtistUuid, setInternalArtistUuid] = (0, import_react17.useState)(null);
2521
2730
  const isArtistControlled = controlledArtistUuid !== void 0;
2522
2731
  const artistUuid = isArtistControlled ? controlledArtistUuid : internalArtistUuid;
2523
- const [internalSizeMetric, setInternalSizeMetric] = (0, import_react15.useState)("songs");
2732
+ const [internalSongUuid, setInternalSongUuid] = (0, import_react17.useState)(null);
2733
+ const isSongControlled = controlledSongUuid !== void 0;
2734
+ const songUuid = isSongControlled ? controlledSongUuid : internalSongUuid;
2735
+ const changeSong = (uuid) => {
2736
+ if (!isSongControlled) setInternalSongUuid(uuid);
2737
+ onSongUuidChange?.(uuid);
2738
+ };
2739
+ const [internalSizeMetric, setInternalSizeMetric] = (0, import_react17.useState)("songs");
2524
2740
  const sizeMetric = controlledSizeMetric ?? internalSizeMetric;
2525
- const deferredSizeMetric = (0, import_react15.useDeferredValue)(sizeMetric);
2741
+ const deferredSizeMetric = (0, import_react17.useDeferredValue)(sizeMetric);
2526
2742
  const changeSizeMetric = (metric) => {
2527
2743
  if (controlledSizeMetric === void 0) setInternalSizeMetric(metric);
2528
2744
  onSizeMetricChange?.(metric);
@@ -2549,7 +2765,7 @@ function GenreTreeView({
2549
2765
  allowWheelRotation: internalAllowWheelRotation,
2550
2766
  toggleWheelRotation,
2551
2767
  onNodeHover: handleNodeHover
2552
- } = (0, import_genre_tree17.useGenreTreeViewState)({
2768
+ } = (0, import_genre_tree19.useGenreTreeViewState)({
2553
2769
  scope,
2554
2770
  getBackendBaseUrl,
2555
2771
  viewMode: controlledViewMode,
@@ -2559,7 +2775,7 @@ function GenreTreeView({
2559
2775
  artistUuid
2560
2776
  });
2561
2777
  const restoreRevision = viewportRestore?.revision;
2562
- const [stackedRestoreOwner, setStackedRestoreOwner] = (0, import_react15.useState)({ revision: restoreRevision, rootUuid: selectedGenrePlaylist?.root.uuid });
2778
+ const [stackedRestoreOwner, setStackedRestoreOwner] = (0, import_react17.useState)({ revision: restoreRevision, rootUuid: selectedGenrePlaylist?.root.uuid });
2563
2779
  let restoreRootUuid = stackedRestoreOwner.rootUuid;
2564
2780
  if (stackedRestoreOwner.revision !== restoreRevision) {
2565
2781
  restoreRootUuid = selectedGenrePlaylist?.root.uuid;
@@ -2568,14 +2784,14 @@ function GenreTreeView({
2568
2784
  restoreRootUuid = selectedGenrePlaylist.root.uuid;
2569
2785
  setStackedRestoreOwner({ revision: restoreRevision, rootUuid: restoreRootUuid });
2570
2786
  }
2571
- const isGenreUpdating = (0, import_react_query3.useIsMutating)({ mutationKey: import_genre_tree17.genreUpdateMutationKey }) > 0;
2572
- const { mutate: updateGenre } = (0, import_genre_tree17.useUpdateGenre)(scope, getBackendBaseUrl);
2787
+ const isGenreUpdating = (0, import_react_query3.useIsMutating)({ mutationKey: import_genre_tree19.genreUpdateMutationKey }) > 0;
2788
+ const { mutate: updateGenre } = (0, import_genre_tree19.useUpdateGenre)(scope, getBackendBaseUrl);
2573
2789
  const busy = deferredSizeMetric !== sizeMetric || isTreeFetching || isGenreUpdating;
2574
2790
  const isArtistFiltered = artistUuid !== null;
2575
2791
  const canSizeByViews = !isArtistFiltered && hasViewsCount(visibleGenrePlaylists);
2576
- const artistResult = (0, import_genre_tree17.useArtist)(artistUuid, artistSearchBaseUrl ?? getBackendBaseUrl);
2792
+ const artistResult = (0, import_genre_tree19.useArtist)(artistUuid, artistSearchBaseUrl ?? getBackendBaseUrl);
2577
2793
  const filterArtistName = artistResult.status === "success" ? artistResult.artist.name : null;
2578
- const filterArtist = (0, import_react15.useMemo)(
2794
+ const filterArtist = (0, import_react17.useMemo)(
2579
2795
  () => artistUuid !== null && filterArtistName !== null ? { uuid: artistUuid, name: filterArtistName } : null,
2580
2796
  [artistUuid, filterArtistName]
2581
2797
  );
@@ -2583,27 +2799,72 @@ function GenreTreeView({
2583
2799
  const playback = useGenrePlaylistPlayback({ scope, genrePlaylists: visibleGenrePlaylists, artist: filterArtist });
2584
2800
  const openArtist = (uuid) => {
2585
2801
  if (selectedNodeId !== null) clearSelection();
2802
+ if (songUuid !== null) changeSong(null);
2586
2803
  changeArtist(uuid);
2587
2804
  };
2588
- const [expandedArtistUuid, setExpandedArtistUuid] = (0, import_react15.useState)(null);
2805
+ const openSong = (uuid) => {
2806
+ if (selectedNodeId !== null) clearSelection();
2807
+ changeSong(uuid);
2808
+ };
2809
+ const openSongRef = (0, import_react17.useRef)(openSong);
2810
+ openSongRef.current = openSong;
2811
+ const resolvedRenderSongLink = (0, import_react17.useCallback)(
2812
+ (song) => renderSongLink ? renderSongLink(song) : /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("button", { type: "button", onClick: () => openSongRef.current(song.uuid), className: "hover:underline", children: song.title }),
2813
+ [renderSongLink]
2814
+ );
2815
+ const [expandedSongUuid, setExpandedSongUuid] = (0, import_react17.useState)(null);
2816
+ const [expandedArtistUuid, setExpandedArtistUuid] = (0, import_react17.useState)(null);
2589
2817
  const artistBaseUrl = artistSearchBaseUrl ?? getBackendBaseUrl;
2818
+ const songBaseUrl = getBackendBaseUrl;
2590
2819
  const artistPanel = useArtistPanel({
2591
2820
  uuid: artistUuid,
2592
2821
  scope,
2593
2822
  getBackendBaseUrl: artistBaseUrl,
2594
2823
  getShareUrl: getShareUrlRef.current,
2595
- renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name })),
2824
+ renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name })),
2596
2825
  expanded: controlledArtistExpanded ?? (artistUuid !== null && expandedArtistUuid === artistUuid),
2597
2826
  onExpandedChange: (on) => {
2598
2827
  if (controlledArtistExpanded === void 0) setExpandedArtistUuid(on ? artistUuid : null);
2599
2828
  onArtistExpandedChange?.(on);
2600
2829
  },
2830
+ renderSongLink: resolvedRenderSongLink,
2601
2831
  onClose: () => changeArtist(null)
2602
2832
  });
2603
- const builtInArtistPanel = overlayPanel || hasSelectedGenreNode ? void 0 : artistPanel;
2604
- const activeOverlayPanel = overlayPanel ?? (builtInArtistPanel && { ...builtInArtistPanel, hideTitle: true, hideClose: true });
2605
- const [uncontrolledCollapsed, setUncontrolledCollapsed] = (0, import_react15.useState)(false);
2606
- const [uncontrolledExpanded, setUncontrolledExpanded] = (0, import_react15.useState)(false);
2833
+ const resolvedRenderArtistLink = renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name }));
2834
+ const getGenrePath = (genre) => {
2835
+ const path = [];
2836
+ let current = visibleGenrePlaylists.find((playlist) => playlist.criteria?.uuid === genre.uuid);
2837
+ while (current) {
2838
+ path.unshift(current);
2839
+ const parentUuid = current.parent?.uuid;
2840
+ current = parentUuid ? visibleGenrePlaylists.find((playlist) => playlist.uuid === parentUuid) : void 0;
2841
+ }
2842
+ return path;
2843
+ };
2844
+ const songPanel = useSongPanel({
2845
+ uuid: songUuid,
2846
+ scope,
2847
+ getBackendBaseUrl: songBaseUrl,
2848
+ getShareUrl: getShareUrlRef.current,
2849
+ renderArtistLink: resolvedRenderArtistLink,
2850
+ getGenrePath,
2851
+ onSelectGenre: (genrePlaylist) => {
2852
+ changeSong(null);
2853
+ selectGenrePlaylist(genrePlaylist);
2854
+ },
2855
+ expanded: controlledSongExpanded ?? (songUuid !== null && expandedSongUuid === songUuid),
2856
+ onExpandedChange: (on) => {
2857
+ if (controlledSongExpanded === void 0) setExpandedSongUuid(on ? songUuid : null);
2858
+ onSongExpandedChange?.(on);
2859
+ },
2860
+ onClose: () => changeSong(null)
2861
+ });
2862
+ const builtInSongPanel = overlayPanel || hasSelectedGenreNode ? void 0 : songPanel;
2863
+ const builtInArtistPanel = overlayPanel || hasSelectedGenreNode || builtInSongPanel ? void 0 : artistPanel;
2864
+ const builtInPanel = builtInSongPanel ?? builtInArtistPanel;
2865
+ const activeOverlayPanel = overlayPanel ?? (builtInPanel && { ...builtInPanel, hideTitle: true, hideClose: true });
2866
+ const [uncontrolledCollapsed, setUncontrolledCollapsed] = (0, import_react17.useState)(false);
2867
+ const [uncontrolledExpanded, setUncontrolledExpanded] = (0, import_react17.useState)(false);
2607
2868
  const isCollapsed = collapsed ?? uncontrolledCollapsed;
2608
2869
  const isExpanded = expanded ?? uncontrolledExpanded;
2609
2870
  const changeCollapsed = (next) => {
@@ -2614,9 +2875,9 @@ function GenreTreeView({
2614
2875
  setUncontrolledExpanded(next);
2615
2876
  onExpandedChange?.(next);
2616
2877
  };
2617
- (0, import_react15.useEffect)(() => {
2878
+ (0, import_react17.useEffect)(() => {
2618
2879
  setUncontrolledCollapsed(false);
2619
- }, [selectedNodeId, artistUuid]);
2880
+ }, [selectedNodeId, artistUuid, songUuid]);
2620
2881
  const isDetailHidden = isCollapsed || !hasSelectedGenreNode && !activeOverlayPanel;
2621
2882
  useLeftDetailPanelLayout(!isDetailHidden, placement === "regions");
2622
2883
  const detailOverlay = placement === "regions" && activeOverlayPanel?.onExpandedChange ? activeOverlayPanel : void 0;
@@ -2630,14 +2891,14 @@ function GenreTreeView({
2630
2891
  hideExpandButton: placement === "regions" ? true : void 0,
2631
2892
  overlayPanel: detailOverlay ? { ...detailOverlay, collapsed: isCollapsed, hideExpandButton: true } : activeOverlayPanel
2632
2893
  };
2633
- const [isWheelRevealed, setIsWheelRevealed] = (0, import_react15.useState)(false);
2894
+ const [isWheelRevealed, setIsWheelRevealed] = (0, import_react17.useState)(false);
2634
2895
  const usesHandoff = viewMode === "wheel" || viewMode === "pop-core";
2635
2896
  const isTreeReady = !isLoading && (!usesHandoff || isWheelRevealed);
2636
- const rootRef = (0, import_react15.useRef)(null);
2897
+ const rootRef = (0, import_react17.useRef)(null);
2637
2898
  const handlePrevious = () => {
2638
2899
  rootRef.current?.querySelector(".search-overlay .gtv-info-panel-previous")?.click();
2639
2900
  };
2640
- (0, import_react15.useEffect)(() => {
2901
+ (0, import_react17.useEffect)(() => {
2641
2902
  if (!isTreeReady || viewMode !== "pop-core" || !hasSelectedGenreNode || !selectedGenrePlaylist) return;
2642
2903
  const root = rootRef.current;
2643
2904
  const centerChip = root?.querySelector(".gtv-wheel-center-node .gtv-wheel-chip");
@@ -2647,21 +2908,22 @@ function GenreTreeView({
2647
2908
  centerChip.click();
2648
2909
  }, [hasSelectedGenreNode, isTreeReady, selectedGenrePlaylist, viewMode]);
2649
2910
  const { showPopup, hidePopup } = (0, import_popup6.usePopup)();
2650
- const [searchText, setSearchText] = (0, import_react15.useState)("");
2651
- const artistSearchResult = (0, import_genre_tree16.useArtistSearch)(artistBaseUrl, artistSearch ? searchText : "");
2652
- const artistExtraResults = artistSearch ? {
2911
+ const [searchText, setSearchText] = (0, import_react17.useState)("");
2912
+ const artistSearchResult = (0, import_genre_tree18.useArtistSearch)(artistBaseUrl, artistSearch ? searchText : "");
2913
+ const artistGroup = artistSearch ? {
2914
+ key: "artists",
2653
2915
  label: "Artists",
2654
2916
  items: artistSearchResult.artists.map((artist) => ({
2655
2917
  key: artist.uuid,
2656
- content: /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(import_jsx_runtime39.Fragment, { children: [
2657
- /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("span", { className: "block truncate font-medium", children: [
2918
+ content: /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(import_jsx_runtime41.Fragment, { children: [
2919
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("span", { className: "block truncate font-medium", children: [
2658
2920
  artist.name,
2659
- artist.disambiguation && /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("span", { className: "font-normal text-[var(--color-neutral-500)]", children: [
2921
+ artist.disambiguation && /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("span", { className: "font-normal text-[var(--color-neutral-500)]", children: [
2660
2922
  " \xB7 ",
2661
2923
  artist.disambiguation
2662
2924
  ] })
2663
2925
  ] }),
2664
- artist.matchedAlias && /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("span", { className: "block truncate text-xs text-[var(--color-neutral-500)]", children: [
2926
+ artist.matchedAlias && /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("span", { className: "block truncate text-xs text-[var(--color-neutral-500)]", children: [
2665
2927
  "also known as ",
2666
2928
  artist.matchedAlias
2667
2929
  ] })
@@ -2674,11 +2936,32 @@ function GenreTreeView({
2674
2936
  status: artistSearchResult.isError ? "Search failed" : artistSearchResult.settled && artistSearchResult.artists.length === 0 ? "No artists found" : "",
2675
2937
  loading: artistSearchResult.pending
2676
2938
  } : void 0;
2939
+ const songSearchResult = (0, import_genre_tree18.useSongSearch)(songBaseUrl, songSearch ? searchText : "");
2940
+ const songGroup = songSearch ? {
2941
+ key: "songs",
2942
+ label: "Songs",
2943
+ items: songSearchResult.songs.map((song) => ({
2944
+ key: song.uuid,
2945
+ content: /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("span", { className: (0, import_genre_tree18.isSongSearchResultPlayable)(song) ? void 0 : "opacity-60", children: [
2946
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("span", { className: "block truncate font-medium", children: song.title }),
2947
+ /* @__PURE__ */ (0, import_jsx_runtime41.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 ") })
2948
+ ] }),
2949
+ onSelect: () => {
2950
+ openSong(song.uuid);
2951
+ onSongSelect?.(song);
2952
+ }
2953
+ })),
2954
+ status: songSearchResult.isError ? "Search failed" : songSearchResult.settled && songSearchResult.songs.length === 0 ? "No songs found" : "",
2955
+ loading: songSearchResult.pending
2956
+ } : void 0;
2957
+ const extraGroups = [artistGroup, songGroup].filter((group) => group !== void 0);
2677
2958
  const handleSelectedNodeChange = (node) => {
2959
+ if (node !== null && songUuid !== null) changeSong(null);
2678
2960
  selectNode(node);
2679
2961
  };
2680
2962
  const handleGenreSearchClear = clearSelection;
2681
2963
  const handleGenreSearchSelect = (genrePlaylist) => {
2964
+ if (songUuid !== null) changeSong(null);
2682
2965
  selectGenrePlaylist(genrePlaylist);
2683
2966
  };
2684
2967
  const selectedUuid = selectedGenrePlaylist?.uuid;
@@ -2687,13 +2970,13 @@ function GenreTreeView({
2687
2970
  const handleGenreSearchZoomIn = () => {
2688
2971
  if (selectedUuid) focus.onFocusGenreChange?.(selectedUuid, "zoom-in");
2689
2972
  };
2690
- const zoomInRef = (0, import_react15.useRef)(void 0);
2973
+ const zoomInRef = (0, import_react17.useRef)(void 0);
2691
2974
  zoomInRef.current = canZoomIn ? handleGenreSearchZoomIn : void 0;
2692
- const renderInfoPanelBody = (0, import_react15.useCallback)(
2975
+ const renderInfoPanelBody = (0, import_react17.useCallback)(
2693
2976
  (node, { toolbar, ancestors, family }) => {
2694
2977
  const genrePlaylist = genrePlaylistByNodeId.get(node.id);
2695
2978
  const criteria = genrePlaylist?.criteria;
2696
- if (!genrePlaylist || !criteria) return { primary: /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(import_jsx_runtime39.Fragment, { children: [
2979
+ if (!genrePlaylist || !criteria) return { primary: /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(import_jsx_runtime41.Fragment, { children: [
2697
2980
  toolbar,
2698
2981
  family
2699
2982
  ] }), aside: ancestors };
@@ -2713,7 +2996,7 @@ function GenreTreeView({
2713
2996
  (sibling) => sibling.parent?.uuid === parentPlaylist.uuid && sibling.uuid !== genrePlaylist.uuid && sibling.criteria?.side === "pop"
2714
2997
  ) : void 0;
2715
2998
  return {
2716
- primary: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2999
+ primary: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2717
3000
  GenreInfoPanelBody,
2718
3001
  {
2719
3002
  toolbar,
@@ -2732,7 +3015,7 @@ function GenreTreeView({
2732
3015
  onAddChild: readOnly ? void 0 : () => handleGenreCreationAction(criteria),
2733
3016
  onRename: readOnly ? void 0 : () => handleGenreRenameAction(criteria),
2734
3017
  onChangeParent: readOnly ? void 0 : () => showPopup(
2735
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3018
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2736
3019
  ChangeParentPopup,
2737
3020
  {
2738
3021
  genrePlaylist: { ...genrePlaylist, criteria },
@@ -2753,6 +3036,7 @@ function GenreTreeView({
2753
3036
  criteriaOverviewSchema,
2754
3037
  renderGenreDetailExtras,
2755
3038
  renderArtistLink,
3039
+ renderSongLink: resolvedRenderSongLink,
2756
3040
  artist: filterArtist,
2757
3041
  renderSummary,
2758
3042
  renderEssentialTracks,
@@ -2783,6 +3067,7 @@ function GenreTreeView({
2783
3067
  criteriaOverviewSchema,
2784
3068
  renderGenreDetailExtras,
2785
3069
  renderArtistLink,
3070
+ resolvedRenderSongLink,
2786
3071
  renderSummary,
2787
3072
  renderEssentialTracks,
2788
3073
  renderSongLeading,
@@ -2792,7 +3077,7 @@ function GenreTreeView({
2792
3077
  );
2793
3078
  const isRotationControlled = controlledAllowWheelRotation !== void 0;
2794
3079
  const allowWheelRotation = controlledAllowWheelRotation ?? internalAllowWheelRotation;
2795
- const detailTab = placement === "regions" && (hasSelectedGenreNode || detailOverlay) ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3080
+ const detailTab = placement === "regions" && (hasSelectedGenreNode || detailOverlay) ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2796
3081
  PanelEdgeTab,
2797
3082
  {
2798
3083
  side: "left",
@@ -2802,8 +3087,8 @@ function GenreTreeView({
2802
3087
  ) : null;
2803
3088
  const renderArtistFilter = () => {
2804
3089
  if (isLoading || !filterArtist) return null;
2805
- return /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-sm", children: [
2806
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3090
+ return /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-sm", children: [
3091
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2807
3092
  "button",
2808
3093
  {
2809
3094
  type: "button",
@@ -2815,7 +3100,7 @@ function GenreTreeView({
2815
3100
  children: filterArtist.name
2816
3101
  }
2817
3102
  ),
2818
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3103
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2819
3104
  "button",
2820
3105
  {
2821
3106
  type: "button",
@@ -2827,23 +3112,23 @@ function GenreTreeView({
2827
3112
  )
2828
3113
  ] });
2829
3114
  };
2830
- const actions = /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(import_jsx_runtime39.Fragment, { children: [
2831
- !isLoading && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3115
+ const actions = /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(import_jsx_runtime41.Fragment, { children: [
3116
+ !isLoading && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2832
3117
  GenreSearch,
2833
3118
  {
2834
3119
  genrePlaylists: visibleGenrePlaylists,
2835
3120
  onSelect: handleGenreSearchSelect,
2836
- selectedName: selectedGenrePlaylist?.name ?? builtInArtistPanel?.title ?? null,
2837
- onClear: builtInArtistPanel ? () => changeArtist(null) : handleGenreSearchClear,
3121
+ selectedName: selectedGenrePlaylist?.name ?? builtInPanel?.title ?? null,
3122
+ onClear: builtInSongPanel ? () => changeSong(null) : builtInArtistPanel ? () => changeArtist(null) : handleGenreSearchClear,
2838
3123
  onPrevious: handlePrevious,
2839
- onQueryChange: artistSearch ? setSearchText : void 0,
2840
- extraResults: artistExtraResults
3124
+ onQueryChange: artistSearch || songSearch ? setSearchText : void 0,
3125
+ extraGroups
2841
3126
  }
2842
3127
  ),
2843
- !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
2844
- /* @__PURE__ */ (0, import_jsx_runtime39.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: [
2845
- !isLoading && !isControlled && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2846
- import_ui16.SegmentedControl,
3128
+ !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
3129
+ /* @__PURE__ */ (0, import_jsx_runtime41.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: [
3130
+ !isLoading && !isControlled && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3131
+ import_ui18.SegmentedControl,
2847
3132
  {
2848
3133
  "aria-label": "Tree view mode",
2849
3134
  value: viewMode,
@@ -2856,14 +3141,14 @@ function GenreTreeView({
2856
3141
  ]
2857
3142
  }
2858
3143
  ),
2859
- !isLoading && showDisplayOptions && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3144
+ !isLoading && showDisplayOptions && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2860
3145
  "div",
2861
3146
  {
2862
3147
  className: "flex items-center gap-1",
2863
3148
  role: "group",
2864
3149
  "aria-label": "Tree display options",
2865
- children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2866
- import_ui16.Button,
3150
+ children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3151
+ import_ui18.Button,
2867
3152
  {
2868
3153
  variant: allowWheelRotation ? "default" : "outline",
2869
3154
  size: "sm",
@@ -2873,52 +3158,52 @@ function GenreTreeView({
2873
3158
  )
2874
3159
  }
2875
3160
  ),
2876
- !isLoading && !readOnly && showAddRoot && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2877
- import_ui16.IconTextButton,
3161
+ !isLoading && !readOnly && showAddRoot && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3162
+ import_ui18.IconTextButton,
2878
3163
  {
2879
- icon: import_lucide_react15.Plus,
3164
+ icon: import_lucide_react16.Plus,
2880
3165
  text: "Add root",
2881
3166
  onClick: () => handleGenreCreationAction(null)
2882
3167
  }
2883
3168
  )
2884
3169
  ] })
2885
3170
  ] });
2886
- return /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(
3171
+ return /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(
2887
3172
  "div",
2888
3173
  {
2889
3174
  ref: rootRef,
2890
3175
  className: ["genre-tree-view relative flex flex-col h-full", placement === "regions" && REGIONS_CLASS, className2].filter(Boolean).join(" "),
2891
3176
  children: [
2892
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("style", { children: PREVIOUS_BUTTON_CSS }),
2893
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3177
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("style", { children: PREVIOUS_BUTTON_CSS }),
3178
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2894
3179
  "div",
2895
3180
  {
2896
3181
  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"}`,
2897
3182
  children: actions
2898
3183
  }
2899
3184
  ),
2900
- !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3185
+ !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2901
3186
  "div",
2902
3187
  {
2903
3188
  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"}`,
2904
3189
  children: renderArtistFilter()
2905
3190
  }
2906
3191
  ),
2907
- detailTab && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3192
+ detailTab && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2908
3193
  "div",
2909
3194
  {
2910
3195
  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))]"}`,
2911
3196
  children: detailTab
2912
3197
  }
2913
3198
  ),
2914
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "content-container flex-1 min-h-0 flex flex-row gap-4", children: /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "tree-view-container relative flex-1 min-w-0 flex flex-col h-full", children: [
2915
- !isLoading && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
2916
- isLoading ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_genre_tree_view4.GenreTreeViewSkeleton, { viewMode }) : viewMode === "wheel" ? (
3199
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("div", { className: "content-container flex-1 min-h-0 flex flex-row gap-4", children: /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "tree-view-container relative flex-1 min-w-0 flex flex-col h-full", children: [
3200
+ !isLoading && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
3201
+ isLoading ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_genre_tree_view4.GenreTreeViewSkeleton, { viewMode }) : viewMode === "wheel" ? (
2917
3202
  // The info panel's top edge aligns with the tree's; the search bar floats `--layout-panel-inset` below it,
2918
3203
  // and the header inset (the bar's height plus that inset) extends the header's fill behind the bar,
2919
3204
  // Google Maps style; the 8px bottom padding mirrors the inset above the bar, and the hidden title's height is zeroed.
2920
3205
  // The panel's own Zoom in button is hidden: the search bar carries it.
2921
- /* @__PURE__ */ (0, import_jsx_runtime39.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_runtime39.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_genre_tree_view4.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3206
+ /* @__PURE__ */ (0, import_jsx_runtime41.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_runtime41.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_genre_tree_view4.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2922
3207
  GenrePlaylistTreeWheel,
2923
3208
  {
2924
3209
  scope,
@@ -2937,9 +3222,9 @@ function GenreTreeView({
2937
3222
  allowWheelRotation,
2938
3223
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
2939
3224
  busy,
2940
- floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "flex items-center gap-2", children: [
3225
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "flex items-center gap-2", children: [
2941
3226
  floatingControlsLeading,
2942
- canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
3227
+ canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2943
3228
  ] }) : void 0,
2944
3229
  defaultViewport,
2945
3230
  viewportRestore,
@@ -2949,7 +3234,7 @@ function GenreTreeView({
2949
3234
  ) }) })
2950
3235
  ) : viewMode === "pop-core" ? (
2951
3236
  // Same as the wheel: the header inset extends its fill behind the floating search bar, Google Maps style.
2952
- /* @__PURE__ */ (0, import_jsx_runtime39.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_runtime39.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_genre_tree_view4.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3237
+ /* @__PURE__ */ (0, import_jsx_runtime41.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_runtime41.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_genre_tree_view4.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2953
3238
  GenrePlaylistTreeWheelRadialPopCore,
2954
3239
  {
2955
3240
  scope,
@@ -2968,9 +3253,9 @@ function GenreTreeView({
2968
3253
  allowWheelRotation,
2969
3254
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
2970
3255
  busy,
2971
- floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "flex items-center gap-2", children: [
3256
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "flex items-center gap-2", children: [
2972
3257
  floatingControlsLeading,
2973
- canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
3258
+ canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
2974
3259
  ] }) : void 0,
2975
3260
  defaultViewport,
2976
3261
  viewportRestore,
@@ -2979,7 +3264,7 @@ function GenreTreeView({
2979
3264
  ...focus
2980
3265
  }
2981
3266
  ) }) })
2982
- ) : viewMode === "outline" ? /* @__PURE__ */ (0, import_jsx_runtime39.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_runtime39.jsx)(
3267
+ ) : viewMode === "outline" ? /* @__PURE__ */ (0, import_jsx_runtime41.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_runtime41.jsx)(
2983
3268
  GenrePlaylistTreeWheelRadialPopCore,
2984
3269
  {
2985
3270
  outline: true,
@@ -2998,13 +3283,13 @@ function GenreTreeView({
2998
3283
  readOnly,
2999
3284
  ...infoPanelCuration
3000
3285
  }
3001
- ) }) : /* @__PURE__ */ (0, import_jsx_runtime39.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(
3286
+ ) }) : /* @__PURE__ */ (0, import_jsx_runtime41.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(
3002
3287
  ([uuid, genrePlaylistTreePerRoot], rootIndex) => {
3003
- return /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3288
+ return /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3004
3289
  "div",
3005
3290
  {
3006
3291
  className: "tree-per-root-container relative shrink-0 h-[28rem] mt-2 mr-16 p-2 bg-[var(--color-neutral-100)] rounded-lg",
3007
- children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3292
+ children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3008
3293
  GenrePlaylistTreePerRoot,
3009
3294
  {
3010
3295
  scope,
@@ -3041,11 +3326,11 @@ function GenreTreeView({
3041
3326
  }
3042
3327
 
3043
3328
  // src/genre-tree/GenreDetailPanel.tsx
3044
- var import_react16 = require("react");
3329
+ var import_react18 = require("react");
3045
3330
  var import_genre_tree_view5 = require("@behindthemusictree/genre-tree-view");
3046
- var import_ui17 = require("@behindthemusictree/ui");
3047
- var import_genre_tree18 = require("@behindthemusictree/app-kit/genre-tree");
3048
- var import_jsx_runtime40 = require("react/jsx-runtime");
3331
+ var import_ui19 = require("@behindthemusictree/ui");
3332
+ var import_genre_tree20 = require("@behindthemusictree/app-kit/genre-tree");
3333
+ var import_jsx_runtime42 = require("react/jsx-runtime");
3049
3334
  var ITEM_COUNT_LABEL = "Songs";
3050
3335
  var RELATIVE_ITEM_COUNT = 1;
3051
3336
  function toNode(genre, parentId) {
@@ -3072,9 +3357,9 @@ function GenreDetailPanel({
3072
3357
  onExpandedChange,
3073
3358
  extras
3074
3359
  }) {
3075
- const { data: detail, isPending, isError } = (0, import_genre_tree18.useFetchGenreDetail)(genreUuid, scope, getBackendBaseUrl);
3360
+ const { data: detail, isPending, isError } = (0, import_genre_tree20.useFetchGenreDetail)(genreUuid, scope, getBackendBaseUrl);
3076
3361
  const playback = useGenrePlaylistPlayback({ scope, genrePlaylists: [] });
3077
- const view = (0, import_react16.useMemo)(() => {
3362
+ const view = (0, import_react18.useMemo)(() => {
3078
3363
  if (!detail) return null;
3079
3364
  const rootColor = (0, import_genre_tree_view5.getGenreTreeColor)(detail.root.wikidataId);
3080
3365
  const relative = (genre, parentId) => ({
@@ -3100,17 +3385,17 @@ function GenreDetailPanel({
3100
3385
  };
3101
3386
  }, [detail]);
3102
3387
  if (isError) {
3103
- return /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { role: "alert", className: "p-4 text-sm text-[var(--color-text)]", children: "This genre could not be loaded." });
3388
+ return /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("div", { role: "alert", className: "p-4 text-sm text-[var(--color-text)]", children: "This genre could not be loaded." });
3104
3389
  }
3105
3390
  if (isPending || !detail || !view) {
3106
- return /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: "flex flex-col gap-3 p-4", "aria-busy": "true", "data-testid": "genre-detail-panel-skeleton", children: [
3107
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_ui17.Skeleton, { className: "h-6 w-2/3" }),
3108
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_ui17.Skeleton, { className: "h-4 w-full" }),
3109
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_ui17.Skeleton, { className: "h-4 w-3/4" })
3391
+ return /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: "flex flex-col gap-3 p-4", "aria-busy": "true", "data-testid": "genre-detail-panel-skeleton", children: [
3392
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui19.Skeleton, { className: "h-6 w-2/3" }),
3393
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui19.Skeleton, { className: "h-4 w-full" }),
3394
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui19.Skeleton, { className: "h-4 w-3/4" })
3110
3395
  ] });
3111
3396
  }
3112
3397
  const renderBody = (node, sections) => ({
3113
- primary: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3398
+ primary: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3114
3399
  GenreInfoPanelBody,
3115
3400
  {
3116
3401
  toolbar: sections.toolbar,
@@ -3127,7 +3412,7 @@ function GenreDetailPanel({
3127
3412
  ),
3128
3413
  aside: null
3129
3414
  });
3130
- return /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3415
+ return /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3131
3416
  import_genre_tree_view5.InfoPanel,
3132
3417
  {
3133
3418
  node: view.node,
@@ -3153,13 +3438,13 @@ function GenreDetailPanel({
3153
3438
  var import_genre_tree_view6 = require("@behindthemusictree/genre-tree-view");
3154
3439
 
3155
3440
  // src/genre-tree/track-list-sidebar/TrackListSidebar.tsx
3156
- var import_react17 = require("react");
3157
- var import_lucide_react16 = require("lucide-react");
3158
- var import_genre_tree19 = require("@behindthemusictree/app-kit/genre-tree");
3159
- var import_genre_tree20 = require("@behindthemusictree/app-kit/genre-tree");
3441
+ var import_react19 = require("react");
3442
+ var import_lucide_react17 = require("lucide-react");
3443
+ var import_genre_tree21 = require("@behindthemusictree/app-kit/genre-tree");
3444
+ var import_genre_tree22 = require("@behindthemusictree/app-kit/genre-tree");
3160
3445
  var import_track_noun7 = require("@behindthemusictree/app-kit/track-noun");
3161
- var import_jsx_runtime41 = require("react/jsx-runtime");
3162
- var useBrowserLayoutEffect = typeof window === "undefined" ? import_react17.useEffect : import_react17.useLayoutEffect;
3446
+ var import_jsx_runtime43 = require("react/jsx-runtime");
3447
+ var useBrowserLayoutEffect = typeof window === "undefined" ? import_react19.useEffect : import_react19.useLayoutEffect;
3163
3448
  function TrackListSidebar({
3164
3449
  className: className2 = "",
3165
3450
  renderDuration,
@@ -3168,20 +3453,21 @@ function TrackListSidebar({
3168
3453
  isCollapsed = false,
3169
3454
  onExpand,
3170
3455
  onMinimize,
3171
- renderArtistLink
3456
+ renderArtistLink,
3457
+ renderSongLink
3172
3458
  }) {
3173
- const { trackList, loadMore, autoAdvanceScrollRequest } = (0, import_genre_tree19.useTrackList)();
3459
+ const { trackList, loadMore, autoAdvanceScrollRequest } = (0, import_genre_tree21.useTrackList)();
3174
3460
  const trackNoun = (0, import_track_noun7.useTrackNoun)();
3175
- const trackListRef = (0, import_react17.useRef)(null);
3176
- const previousOriginRef = (0, import_react17.useRef)(trackList?.origin ?? null);
3177
- const handledAutoAdvanceRequestIdRef = (0, import_react17.useRef)(null);
3178
- const shouldFollowPlaybackRef = (0, import_react17.useRef)(true);
3179
- const programmaticScrollTopRef = (0, import_react17.useRef)(null);
3180
- const userScrollIntentRef = (0, import_react17.useRef)(false);
3181
- const userScrollIntentTimerRef = (0, import_react17.useRef)(null);
3182
- const loadMoreSentinelRef = (0, import_react17.useRef)(null);
3461
+ const trackListRef = (0, import_react19.useRef)(null);
3462
+ const previousOriginRef = (0, import_react19.useRef)(trackList?.origin ?? null);
3463
+ const handledAutoAdvanceRequestIdRef = (0, import_react19.useRef)(null);
3464
+ const shouldFollowPlaybackRef = (0, import_react19.useRef)(true);
3465
+ const programmaticScrollTopRef = (0, import_react19.useRef)(null);
3466
+ const userScrollIntentRef = (0, import_react19.useRef)(false);
3467
+ const userScrollIntentTimerRef = (0, import_react19.useRef)(null);
3468
+ const loadMoreSentinelRef = (0, import_react19.useRef)(null);
3183
3469
  const hasMore = !!trackList && trackList.nextPage !== null;
3184
- (0, import_react17.useEffect)(() => {
3470
+ (0, import_react19.useEffect)(() => {
3185
3471
  const sentinel = loadMoreSentinelRef.current;
3186
3472
  if (!hasMore || !sentinel) return;
3187
3473
  const observer = new IntersectionObserver((entries) => {
@@ -3201,11 +3487,11 @@ function TrackListSidebar({
3201
3487
  programmaticScrollTopRef.current = null;
3202
3488
  if (trackListRef.current) trackListRef.current.scrollTop = 0;
3203
3489
  }, [trackList?.origin]);
3204
- (0, import_react17.useEffect)(() => {
3490
+ (0, import_react19.useEffect)(() => {
3205
3491
  const list = trackListRef.current;
3206
3492
  if (!isCollapsed && list?.scrollTop === 0) shouldFollowPlaybackRef.current = true;
3207
3493
  }, [isCollapsed]);
3208
- (0, import_react17.useEffect)(
3494
+ (0, import_react19.useEffect)(
3209
3495
  () => () => {
3210
3496
  if (userScrollIntentTimerRef.current !== null) clearTimeout(userScrollIntentTimerRef.current);
3211
3497
  },
@@ -3259,25 +3545,25 @@ function TrackListSidebar({
3259
3545
  }
3260
3546
  };
3261
3547
  const positionClasses = layout === "inline" ? "relative w-full flex h-full flex-col" : "fixed right-0 w-[50rem] flex flex-col";
3262
- return trackList ? /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(
3548
+ return trackList ? /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)(
3263
3549
  "div",
3264
3550
  {
3265
3551
  "data-theme": "dark",
3266
3552
  className: `track-list-sidebar ${positionClasses} rounded-2xl bg-[var(--color-surface)] pb-1 ${className2}`,
3267
3553
  style: layout === "fixed" ? { top: "63.5px", maxHeight: "calc(100vh - 63.5px - 79px)" } : void 0,
3268
3554
  children: [
3269
- /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "header flex h-16 items-center px-4 py-2 text-[var(--color-text)]", children: [
3270
- /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "min-w-0 flex flex-1 flex-col justify-center", children: [
3271
- /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "origin flex text-xl", children: [
3272
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
3273
- /* @__PURE__ */ (0, import_jsx_runtime41.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_runtime41.jsx)("div", { className: "name max-w-full truncate", children: trackList.origin.label }) })
3555
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { className: "header flex h-16 items-center px-4 py-2 text-[var(--color-text)]", children: [
3556
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { className: "min-w-0 flex flex-1 flex-col justify-center", children: [
3557
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { className: "origin flex text-xl", children: [
3558
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
3559
+ /* @__PURE__ */ (0, import_jsx_runtime43.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_runtime43.jsx)("div", { className: "name max-w-full truncate", children: trackList.origin.label }) })
3274
3560
  ] }),
3275
- /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "info flex items-center text-sm text-[var(--color-text-muted)]", children: [
3276
- 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 `,
3561
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { className: "info flex items-center text-sm text-[var(--color-text-muted)]", children: [
3562
+ 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 `,
3277
3563
  `${trackList.total} ${trackList.total > 1 ? trackNoun.plural : trackNoun.singular}`
3278
3564
  ] })
3279
3565
  ] }),
3280
- (isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3566
+ (isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
3281
3567
  "button",
3282
3568
  {
3283
3569
  type: "button",
@@ -3286,11 +3572,11 @@ function TrackListSidebar({
3286
3572
  "aria-expanded": !isCollapsed,
3287
3573
  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"}`,
3288
3574
  onClick: isCollapsed ? onExpand : onMinimize,
3289
- children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_lucide_react16.ListMusic, { "aria-hidden": "true", className: "h-5 w-5" })
3575
+ children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_lucide_react17.ListMusic, { "aria-hidden": "true", className: "h-5 w-5" })
3290
3576
  }
3291
3577
  )
3292
3578
  ] }),
3293
- !isCollapsed && /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(
3579
+ !isCollapsed && /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)(
3294
3580
  "ul",
3295
3581
  {
3296
3582
  ref: trackListRef,
@@ -3300,17 +3586,18 @@ function TrackListSidebar({
3300
3586
  onPointerDown: markUserScrollIntent,
3301
3587
  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",
3302
3588
  children: [
3303
- trackList.tracks.map((track, index) => /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("li", { "data-track-uuid": track.uuid, children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3589
+ trackList.tracks.map((track, index) => /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("li", { "data-track-uuid": track.uuid, children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
3304
3590
  TrackItem,
3305
3591
  {
3306
3592
  track,
3307
3593
  position: index + 1,
3308
3594
  renderDuration,
3309
3595
  renderActions,
3310
- renderArtistLink
3596
+ renderArtistLink,
3597
+ renderSongLink
3311
3598
  }
3312
3599
  ) }, track.uuid)),
3313
- hasMore && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
3600
+ hasMore && /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
3314
3601
  ]
3315
3602
  }
3316
3603
  )
@@ -3320,16 +3607,16 @@ function TrackListSidebar({
3320
3607
  }
3321
3608
 
3322
3609
  // src/genre-tree/PlayerPanel.tsx
3323
- var import_react18 = require("react");
3610
+ var import_react20 = require("react");
3324
3611
  var import_tailwind_merge2 = require("tailwind-merge");
3325
- var import_player12 = require("@behindthemusictree/app-kit/player");
3326
- var import_genre_tree22 = require("@behindthemusictree/app-kit/genre-tree");
3612
+ var import_player13 = require("@behindthemusictree/app-kit/player");
3613
+ var import_genre_tree24 = require("@behindthemusictree/app-kit/genre-tree");
3327
3614
 
3328
3615
  // src/genre-tree/PlayerControls.tsx
3329
- var import_lucide_react17 = require("lucide-react");
3330
- var import_genre_tree21 = require("@behindthemusictree/app-kit/genre-tree");
3331
- var import_player11 = require("@behindthemusictree/app-kit/player");
3332
- var import_jsx_runtime42 = require("react/jsx-runtime");
3616
+ var import_lucide_react18 = require("lucide-react");
3617
+ var import_genre_tree23 = require("@behindthemusictree/app-kit/genre-tree");
3618
+ var import_player12 = require("@behindthemusictree/app-kit/player");
3619
+ var import_jsx_runtime44 = require("react/jsx-runtime");
3333
3620
  function PlayerControls() {
3334
3621
  const {
3335
3622
  duration,
@@ -3340,23 +3627,23 @@ function PlayerControls() {
3340
3627
  seekTo,
3341
3628
  setVolume,
3342
3629
  volume
3343
- } = (0, import_player11.usePlayer)();
3344
- const { hasNextTrack, hasPreviousTrack, playNextTrack, playPreviousTrack } = (0, import_genre_tree21.useTrackList)();
3345
- const currentTime = (0, import_player11.useCurrentTime)();
3346
- const isBusy = isLoading || playState === import_player11.PlayStates.LOADING;
3630
+ } = (0, import_player12.usePlayer)();
3631
+ const { hasNextTrack, hasPreviousTrack, playNextTrack, playPreviousTrack } = (0, import_genre_tree23.useTrackList)();
3632
+ const currentTime = (0, import_player12.useCurrentTime)();
3633
+ const isBusy = isLoading || playState === import_player12.PlayStates.LOADING;
3347
3634
  const isReady = !!playerTrackObject?.isReady && !isBusy && !playerTrackObject.loadError;
3348
3635
  const safeDuration = Number.isFinite(duration) && duration > 0 ? duration : 0;
3349
3636
  const safeCurrentTime = Number.isFinite(currentTime) ? Math.max(0, Math.min(currentTime, safeDuration)) : 0;
3350
- const isPlaying = playState === import_player11.PlayStates.PLAYING;
3351
- return /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(
3637
+ const isPlaying = playState === import_player12.PlayStates.PLAYING;
3638
+ return /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)(
3352
3639
  "section",
3353
3640
  {
3354
3641
  "aria-label": "Player controls",
3355
3642
  className: "player-controls flex shrink-0 flex-col gap-2 border-b border-zinc-800 px-3 py-2 text-zinc-200",
3356
3643
  children: [
3357
- /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: "flex min-w-0 items-center gap-2 text-xs tabular-nums text-zinc-400", children: [
3358
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { "aria-label": "Elapsed time", children: (0, import_genre_tree21.formatTime)(safeCurrentTime) }),
3359
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3644
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("div", { className: "flex min-w-0 items-center gap-2 text-xs tabular-nums text-zinc-400", children: [
3645
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("span", { "aria-label": "Elapsed time", children: (0, import_genre_tree23.formatTime)(safeCurrentTime) }),
3646
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3360
3647
  "input",
3361
3648
  {
3362
3649
  "aria-label": "Seek",
@@ -3370,11 +3657,11 @@ function PlayerControls() {
3370
3657
  onChange: (event) => seekTo(Number(event.currentTarget.value))
3371
3658
  }
3372
3659
  ),
3373
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { "aria-label": "Duration", children: (0, import_genre_tree21.formatTime)(safeDuration) })
3660
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("span", { "aria-label": "Duration", children: (0, import_genre_tree23.formatTime)(safeDuration) })
3374
3661
  ] }),
3375
- /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: "grid min-h-9 grid-cols-[1fr_auto_1fr] items-center gap-3", children: [
3376
- /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: "col-start-2 flex items-center justify-center gap-2", children: [
3377
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3662
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("div", { className: "grid min-h-9 grid-cols-[1fr_auto_1fr] items-center gap-3", children: [
3663
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("div", { className: "col-start-2 flex items-center justify-center gap-2", children: [
3664
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3378
3665
  "button",
3379
3666
  {
3380
3667
  type: "button",
@@ -3383,10 +3670,10 @@ function PlayerControls() {
3383
3670
  disabled: !isReady || !hasPreviousTrack,
3384
3671
  onClick: playPreviousTrack,
3385
3672
  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",
3386
- children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_lucide_react17.SkipBack, { "aria-hidden": "true", className: "h-4 w-4" })
3673
+ children: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_lucide_react18.SkipBack, { "aria-hidden": "true", className: "h-4 w-4" })
3387
3674
  }
3388
3675
  ),
3389
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3676
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3390
3677
  "button",
3391
3678
  {
3392
3679
  type: "button",
@@ -3395,10 +3682,10 @@ function PlayerControls() {
3395
3682
  disabled: !isReady,
3396
3683
  onClick: handlePlayPauseAction,
3397
3684
  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",
3398
- children: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_lucide_react17.Pause, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" }) : /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_lucide_react17.Play, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" })
3685
+ children: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_lucide_react18.Pause, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" }) : /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_lucide_react18.Play, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" })
3399
3686
  }
3400
3687
  ),
3401
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3688
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3402
3689
  "button",
3403
3690
  {
3404
3691
  type: "button",
@@ -3407,14 +3694,14 @@ function PlayerControls() {
3407
3694
  disabled: !isReady || !hasNextTrack,
3408
3695
  onClick: playNextTrack,
3409
3696
  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",
3410
- children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_lucide_react17.SkipForward, { "aria-hidden": "true", className: "h-4 w-4" })
3697
+ children: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_lucide_react18.SkipForward, { "aria-hidden": "true", className: "h-4 w-4" })
3411
3698
  }
3412
3699
  )
3413
3700
  ] }),
3414
- /* @__PURE__ */ (0, import_jsx_runtime42.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: [
3415
- volume === 0 ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_lucide_react17.VolumeX, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }) : /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_lucide_react17.Volume2, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }),
3416
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { className: "sr-only", children: "Volume" }),
3417
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3701
+ /* @__PURE__ */ (0, import_jsx_runtime44.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: [
3702
+ volume === 0 ? /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_lucide_react18.VolumeX, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }) : /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_lucide_react18.Volume2, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }),
3703
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("span", { className: "sr-only", children: "Volume" }),
3704
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3418
3705
  "input",
3419
3706
  {
3420
3707
  "aria-label": "Volume",
@@ -3436,7 +3723,7 @@ function PlayerControls() {
3436
3723
  }
3437
3724
 
3438
3725
  // src/genre-tree/PlayerPanel.tsx
3439
- var import_jsx_runtime43 = require("react/jsx-runtime");
3726
+ var import_jsx_runtime45 = require("react/jsx-runtime");
3440
3727
  var PANEL_ID = "player-panel";
3441
3728
  var DEFAULT_EXTENDED_WIDTH = "calc(100vw - var(--layout-center-left) - var(--layout-gutter))";
3442
3729
  function PlayerPanel({
@@ -3445,16 +3732,17 @@ function PlayerPanel({
3445
3732
  expanded,
3446
3733
  onExpandedChange,
3447
3734
  extendedWidth = DEFAULT_EXTENDED_WIDTH,
3448
- renderArtistLink
3735
+ renderArtistLink,
3736
+ renderSongLink
3449
3737
  }) {
3450
- const { playerTrackObject } = (0, import_player12.usePlayer)();
3451
- const { toggleTrackListSidebar, isTrackListSidebarVisible } = (0, import_genre_tree22.useTrackListSidebarVisibility)();
3452
- const [lastLoadedTrackTitle, setLastLoadedTrackTitle] = (0, import_react18.useState)("");
3738
+ const { playerTrackObject } = (0, import_player13.usePlayer)();
3739
+ const { toggleTrackListSidebar, isTrackListSidebarVisible } = (0, import_genre_tree24.useTrackListSidebarVisibility)();
3740
+ const [lastLoadedTrackTitle, setLastLoadedTrackTitle] = (0, import_react20.useState)("");
3453
3741
  const loadedTrackTitle = playerTrackObject?.track.title;
3454
- (0, import_react18.useEffect)(() => {
3742
+ (0, import_react20.useEffect)(() => {
3455
3743
  if (loadedTrackTitle) setLastLoadedTrackTitle(loadedTrackTitle);
3456
3744
  }, [loadedTrackTitle]);
3457
- const [uncontrolledExtended, setUncontrolledExtended] = (0, import_react18.useState)(false);
3745
+ const [uncontrolledExtended, setUncontrolledExtended] = (0, import_react20.useState)(false);
3458
3746
  const isExtendRequested = expanded ?? uncontrolledExtended;
3459
3747
  const setIsExtendRequested = (next) => {
3460
3748
  setUncontrolledExtended(next);
@@ -3475,7 +3763,7 @@ function PlayerPanel({
3475
3763
  onClick: () => setIsExtendRequested(true),
3476
3764
  expanded: false
3477
3765
  } : null;
3478
- return /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)(
3766
+ return /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(
3479
3767
  "div",
3480
3768
  {
3481
3769
  id: PANEL_ID,
@@ -3488,7 +3776,7 @@ function PlayerPanel({
3488
3776
  ),
3489
3777
  style: isExtended ? { width: extendedWidth } : void 0,
3490
3778
  children: [
3491
- /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
3779
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3492
3780
  PanelEdgeTab,
3493
3781
  {
3494
3782
  side: "right",
@@ -3498,7 +3786,7 @@ function PlayerPanel({
3498
3786
  className: "absolute right-full top-1/2 -translate-y-1/2"
3499
3787
  }
3500
3788
  ),
3501
- /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)(
3789
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(
3502
3790
  "div",
3503
3791
  {
3504
3792
  className: (0, import_tailwind_merge2.twMerge)(
@@ -3506,15 +3794,15 @@ function PlayerPanel({
3506
3794
  isExtended ? "flex-row" : "flex-col"
3507
3795
  ),
3508
3796
  children: [
3509
- /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { className: "flex w-[var(--layout-right-w)] shrink-0 flex-col", children: [
3510
- /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("div", { className: "flex shrink-0 items-center justify-between gap-2 border-b border-zinc-800 p-2", children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { className: "min-w-0 truncate text-sm font-semibold", children: loadedTrackTitle || lastLoadedTrackTitle }) }),
3511
- /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { className: "relative w-full shrink-0", children: [
3512
- /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_player12.PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
3513
- playerTrackObject?.loadError && /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { className: "absolute bottom-2 left-2 right-2 text-xs text-red-400", children: playerTrackObject.loadError })
3797
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("div", { className: "flex w-[var(--layout-right-w)] shrink-0 flex-col", children: [
3798
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("div", { className: "flex shrink-0 items-center justify-between gap-2 border-b border-zinc-800 p-2", children: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "min-w-0 truncate text-sm font-semibold", children: loadedTrackTitle || lastLoadedTrackTitle }) }),
3799
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("div", { className: "relative w-full shrink-0", children: [
3800
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_player13.PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
3801
+ playerTrackObject?.loadError && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "absolute bottom-2 left-2 right-2 text-xs text-red-400", children: playerTrackObject.loadError })
3514
3802
  ] }),
3515
- /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(PlayerControls, {})
3803
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(PlayerControls, {})
3516
3804
  ] }),
3517
- /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
3805
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3518
3806
  TrackListSidebar,
3519
3807
  {
3520
3808
  layout: "inline",
@@ -3525,7 +3813,8 @@ function PlayerPanel({
3525
3813
  "min-h-0 min-w-0 flex-1",
3526
3814
  isExtended && "border-l border-zinc-800"
3527
3815
  ),
3528
- renderArtistLink
3816
+ renderArtistLink,
3817
+ renderSongLink
3529
3818
  }
3530
3819
  )
3531
3820
  ]
@@ -3539,33 +3828,33 @@ function PlayerPanel({
3539
3828
  // src/genre-tree/Rating.tsx
3540
3829
  var import_uuid = require("uuid");
3541
3830
  var import_ai = require("react-icons/ai");
3542
- var import_genre_tree23 = require("@behindthemusictree/app-kit/genre-tree");
3543
- var import_jsx_runtime44 = require("react/jsx-runtime");
3831
+ var import_genre_tree25 = require("@behindthemusictree/app-kit/genre-tree");
3832
+ var import_jsx_runtime46 = require("react/jsx-runtime");
3544
3833
  function Rating({ rating, handleChange }) {
3545
3834
  const instanceId = (0, import_uuid.v4)();
3546
- return /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("div", { className: "flex items-center", children: [...Array(6)].map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("div", { className: `relative ${i === 0 ? "mr-4" : "mr-1"}`, children: [
3547
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3835
+ return /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("div", { className: "flex items-center", children: [...Array(6)].map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("div", { className: `relative ${i === 0 ? "mr-4" : "mr-1"}`, children: [
3836
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3548
3837
  "input",
3549
3838
  {
3550
3839
  id: `star-${instanceId}-${i}`,
3551
3840
  className: "w-6 h-6 opacity-0",
3552
3841
  type: "radio",
3553
3842
  name: "rating",
3554
- value: i === 0 ? import_genre_tree23.FORM_RATING_NULL_VALUE : i,
3843
+ value: i === 0 ? import_genre_tree25.FORM_RATING_NULL_VALUE : i,
3555
3844
  checked: rating === (i === 0 ? null : i),
3556
3845
  onChange: handleChange
3557
3846
  }
3558
3847
  ),
3559
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("label", { htmlFor: `star-${instanceId}-${i}`, className: "cursor-pointer", children: i === 0 ? /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("div", { className: "flex", children: [
3560
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3848
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("label", { htmlFor: `star-${instanceId}-${i}`, className: "cursor-pointer", children: i === 0 ? /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("div", { className: "flex", children: [
3849
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3561
3850
  import_ai.AiOutlineStar,
3562
3851
  {
3563
- 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)]"}`
3852
+ 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)]"}`
3564
3853
  }
3565
3854
  ),
3566
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("span", { className: "flex-grow w-8" }),
3855
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("span", { className: "flex-grow w-8" }),
3567
3856
  " "
3568
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3857
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3569
3858
  import_ai.AiFillStar,
3570
3859
  {
3571
3860
  className: `w-7 h-7 absolute top-0 left-0 ${(rating ?? -1) >= i ? "text-[var(--color-accent-rating)]" : "text-[var(--color-text-muted)]"}`