@behindthemusictree/app-kit-ui 29.1.0 → 30.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,31 +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
  {
2448
2649
  variant: "floating",
2449
2650
  "aria-label": "Size nodes by",
2450
2651
  value,
2451
2652
  onChange,
2452
2653
  options: [
2453
- { 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" },
2454
- { 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" }
2455
2656
  ]
2456
2657
  }
2457
2658
  );
2458
2659
  }
2459
2660
 
2460
2661
  // src/genre-tree/GenreTreeView.tsx
2461
- var import_jsx_runtime39 = require("react/jsx-runtime");
2662
+ var import_jsx_runtime41 = require("react/jsx-runtime");
2462
2663
  var PREVIOUS_BUTTON_CSS = `
2463
2664
  .genre-tree-view .search-overlay .gtv-info-panel-previous { display: none; }
2464
2665
  .genre-tree-view:has(.search-overlay .gtv-info-panel-previous) .genre-search input { padding-left: 2.75rem; }
@@ -2502,6 +2703,13 @@ function GenreTreeView({
2502
2703
  artistSearch = true,
2503
2704
  onArtistSelect,
2504
2705
  artistSearchBaseUrl,
2706
+ songUuid: controlledSongUuid,
2707
+ onSongUuidChange,
2708
+ songExpanded: controlledSongExpanded,
2709
+ onSongExpandedChange,
2710
+ songSearch = true,
2711
+ onSongSelect,
2712
+ renderSongLink,
2505
2713
  renderSummary,
2506
2714
  renderEssentialTracks,
2507
2715
  renderSongLeading,
@@ -2516,14 +2724,21 @@ function GenreTreeView({
2516
2724
  placement = "default",
2517
2725
  ...focus
2518
2726
  }) {
2519
- const getShareUrlRef = (0, import_react15.useRef)(getShareUrl);
2727
+ const getShareUrlRef = (0, import_react17.useRef)(getShareUrl);
2520
2728
  getShareUrlRef.current = getShareUrl;
2521
- const [internalArtistUuid, setInternalArtistUuid] = (0, import_react15.useState)(null);
2729
+ const [internalArtistUuid, setInternalArtistUuid] = (0, import_react17.useState)(null);
2522
2730
  const isArtistControlled = controlledArtistUuid !== void 0;
2523
2731
  const artistUuid = isArtistControlled ? controlledArtistUuid : internalArtistUuid;
2524
- 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");
2525
2740
  const sizeMetric = controlledSizeMetric ?? internalSizeMetric;
2526
- const deferredSizeMetric = (0, import_react15.useDeferredValue)(sizeMetric);
2741
+ const deferredSizeMetric = (0, import_react17.useDeferredValue)(sizeMetric);
2527
2742
  const changeSizeMetric = (metric) => {
2528
2743
  if (controlledSizeMetric === void 0) setInternalSizeMetric(metric);
2529
2744
  onSizeMetricChange?.(metric);
@@ -2550,7 +2765,7 @@ function GenreTreeView({
2550
2765
  allowWheelRotation: internalAllowWheelRotation,
2551
2766
  toggleWheelRotation,
2552
2767
  onNodeHover: handleNodeHover
2553
- } = (0, import_genre_tree17.useGenreTreeViewState)({
2768
+ } = (0, import_genre_tree19.useGenreTreeViewState)({
2554
2769
  scope,
2555
2770
  getBackendBaseUrl,
2556
2771
  viewMode: controlledViewMode,
@@ -2560,7 +2775,7 @@ function GenreTreeView({
2560
2775
  artistUuid
2561
2776
  });
2562
2777
  const restoreRevision = viewportRestore?.revision;
2563
- 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 });
2564
2779
  let restoreRootUuid = stackedRestoreOwner.rootUuid;
2565
2780
  if (stackedRestoreOwner.revision !== restoreRevision) {
2566
2781
  restoreRootUuid = selectedGenrePlaylist?.root.uuid;
@@ -2569,14 +2784,14 @@ function GenreTreeView({
2569
2784
  restoreRootUuid = selectedGenrePlaylist.root.uuid;
2570
2785
  setStackedRestoreOwner({ revision: restoreRevision, rootUuid: restoreRootUuid });
2571
2786
  }
2572
- const isGenreUpdating = (0, import_react_query3.useIsMutating)({ mutationKey: import_genre_tree17.genreUpdateMutationKey }) > 0;
2573
- 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);
2574
2789
  const busy = deferredSizeMetric !== sizeMetric || isTreeFetching || isGenreUpdating;
2575
2790
  const isArtistFiltered = artistUuid !== null;
2576
2791
  const canSizeByViews = !isArtistFiltered && hasViewsCount(visibleGenrePlaylists);
2577
- const artistResult = (0, import_genre_tree17.useArtist)(artistUuid, artistSearchBaseUrl ?? getBackendBaseUrl);
2792
+ const artistResult = (0, import_genre_tree19.useArtist)(artistUuid, artistSearchBaseUrl ?? getBackendBaseUrl);
2578
2793
  const filterArtistName = artistResult.status === "success" ? artistResult.artist.name : null;
2579
- const filterArtist = (0, import_react15.useMemo)(
2794
+ const filterArtist = (0, import_react17.useMemo)(
2580
2795
  () => artistUuid !== null && filterArtistName !== null ? { uuid: artistUuid, name: filterArtistName } : null,
2581
2796
  [artistUuid, filterArtistName]
2582
2797
  );
@@ -2584,27 +2799,72 @@ function GenreTreeView({
2584
2799
  const playback = useGenrePlaylistPlayback({ scope, genrePlaylists: visibleGenrePlaylists, artist: filterArtist });
2585
2800
  const openArtist = (uuid) => {
2586
2801
  if (selectedNodeId !== null) clearSelection();
2802
+ if (songUuid !== null) changeSong(null);
2587
2803
  changeArtist(uuid);
2588
2804
  };
2589
- 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);
2590
2817
  const artistBaseUrl = artistSearchBaseUrl ?? getBackendBaseUrl;
2818
+ const songBaseUrl = getBackendBaseUrl;
2591
2819
  const artistPanel = useArtistPanel({
2592
2820
  uuid: artistUuid,
2593
2821
  scope,
2594
2822
  getBackendBaseUrl: artistBaseUrl,
2595
2823
  getShareUrl: getShareUrlRef.current,
2596
- 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 })),
2597
2825
  expanded: controlledArtistExpanded ?? (artistUuid !== null && expandedArtistUuid === artistUuid),
2598
2826
  onExpandedChange: (on) => {
2599
2827
  if (controlledArtistExpanded === void 0) setExpandedArtistUuid(on ? artistUuid : null);
2600
2828
  onArtistExpandedChange?.(on);
2601
2829
  },
2830
+ renderSongLink: resolvedRenderSongLink,
2602
2831
  onClose: () => changeArtist(null)
2603
2832
  });
2604
- const builtInArtistPanel = overlayPanel || hasSelectedGenreNode ? void 0 : artistPanel;
2605
- const activeOverlayPanel = overlayPanel ?? (builtInArtistPanel && { ...builtInArtistPanel, hideTitle: true, hideClose: true });
2606
- const [uncontrolledCollapsed, setUncontrolledCollapsed] = (0, import_react15.useState)(false);
2607
- 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);
2608
2868
  const isCollapsed = collapsed ?? uncontrolledCollapsed;
2609
2869
  const isExpanded = expanded ?? uncontrolledExpanded;
2610
2870
  const changeCollapsed = (next) => {
@@ -2615,9 +2875,9 @@ function GenreTreeView({
2615
2875
  setUncontrolledExpanded(next);
2616
2876
  onExpandedChange?.(next);
2617
2877
  };
2618
- (0, import_react15.useEffect)(() => {
2878
+ (0, import_react17.useEffect)(() => {
2619
2879
  setUncontrolledCollapsed(false);
2620
- }, [selectedNodeId, artistUuid]);
2880
+ }, [selectedNodeId, artistUuid, songUuid]);
2621
2881
  const isDetailHidden = isCollapsed || !hasSelectedGenreNode && !activeOverlayPanel;
2622
2882
  useLeftDetailPanelLayout(!isDetailHidden, placement === "regions");
2623
2883
  const detailOverlay = placement === "regions" && activeOverlayPanel?.onExpandedChange ? activeOverlayPanel : void 0;
@@ -2631,14 +2891,14 @@ function GenreTreeView({
2631
2891
  hideExpandButton: placement === "regions" ? true : void 0,
2632
2892
  overlayPanel: detailOverlay ? { ...detailOverlay, collapsed: isCollapsed, hideExpandButton: true } : activeOverlayPanel
2633
2893
  };
2634
- const [isWheelRevealed, setIsWheelRevealed] = (0, import_react15.useState)(false);
2894
+ const [isWheelRevealed, setIsWheelRevealed] = (0, import_react17.useState)(false);
2635
2895
  const usesHandoff = viewMode === "wheel" || viewMode === "pop-core";
2636
2896
  const isTreeReady = !isLoading && (!usesHandoff || isWheelRevealed);
2637
- const rootRef = (0, import_react15.useRef)(null);
2897
+ const rootRef = (0, import_react17.useRef)(null);
2638
2898
  const handlePrevious = () => {
2639
2899
  rootRef.current?.querySelector(".search-overlay .gtv-info-panel-previous")?.click();
2640
2900
  };
2641
- (0, import_react15.useEffect)(() => {
2901
+ (0, import_react17.useEffect)(() => {
2642
2902
  if (!isTreeReady || viewMode !== "pop-core" || !hasSelectedGenreNode || !selectedGenrePlaylist) return;
2643
2903
  const root = rootRef.current;
2644
2904
  const centerChip = root?.querySelector(".gtv-wheel-center-node .gtv-wheel-chip");
@@ -2648,21 +2908,22 @@ function GenreTreeView({
2648
2908
  centerChip.click();
2649
2909
  }, [hasSelectedGenreNode, isTreeReady, selectedGenrePlaylist, viewMode]);
2650
2910
  const { showPopup, hidePopup } = (0, import_popup6.usePopup)();
2651
- const [searchText, setSearchText] = (0, import_react15.useState)("");
2652
- const artistSearchResult = (0, import_genre_tree16.useArtistSearch)(artistBaseUrl, artistSearch ? searchText : "");
2653
- 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",
2654
2915
  label: "Artists",
2655
2916
  items: artistSearchResult.artists.map((artist) => ({
2656
2917
  key: artist.uuid,
2657
- content: /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(import_jsx_runtime39.Fragment, { children: [
2658
- /* @__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: [
2659
2920
  artist.name,
2660
- 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: [
2661
2922
  " \xB7 ",
2662
2923
  artist.disambiguation
2663
2924
  ] })
2664
2925
  ] }),
2665
- 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: [
2666
2927
  "also known as ",
2667
2928
  artist.matchedAlias
2668
2929
  ] })
@@ -2675,11 +2936,32 @@ function GenreTreeView({
2675
2936
  status: artistSearchResult.isError ? "Search failed" : artistSearchResult.settled && artistSearchResult.artists.length === 0 ? "No artists found" : "",
2676
2937
  loading: artistSearchResult.pending
2677
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);
2678
2958
  const handleSelectedNodeChange = (node) => {
2959
+ if (node !== null && songUuid !== null) changeSong(null);
2679
2960
  selectNode(node);
2680
2961
  };
2681
2962
  const handleGenreSearchClear = clearSelection;
2682
2963
  const handleGenreSearchSelect = (genrePlaylist) => {
2964
+ if (songUuid !== null) changeSong(null);
2683
2965
  selectGenrePlaylist(genrePlaylist);
2684
2966
  };
2685
2967
  const selectedUuid = selectedGenrePlaylist?.uuid;
@@ -2688,13 +2970,13 @@ function GenreTreeView({
2688
2970
  const handleGenreSearchZoomIn = () => {
2689
2971
  if (selectedUuid) focus.onFocusGenreChange?.(selectedUuid, "zoom-in");
2690
2972
  };
2691
- const zoomInRef = (0, import_react15.useRef)(void 0);
2973
+ const zoomInRef = (0, import_react17.useRef)(void 0);
2692
2974
  zoomInRef.current = canZoomIn ? handleGenreSearchZoomIn : void 0;
2693
- const renderInfoPanelBody = (0, import_react15.useCallback)(
2975
+ const renderInfoPanelBody = (0, import_react17.useCallback)(
2694
2976
  (node, { toolbar, ancestors, family }) => {
2695
2977
  const genrePlaylist = genrePlaylistByNodeId.get(node.id);
2696
2978
  const criteria = genrePlaylist?.criteria;
2697
- 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: [
2698
2980
  toolbar,
2699
2981
  family
2700
2982
  ] }), aside: ancestors };
@@ -2714,7 +2996,7 @@ function GenreTreeView({
2714
2996
  (sibling) => sibling.parent?.uuid === parentPlaylist.uuid && sibling.uuid !== genrePlaylist.uuid && sibling.criteria?.side === "pop"
2715
2997
  ) : void 0;
2716
2998
  return {
2717
- primary: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2999
+ primary: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2718
3000
  GenreInfoPanelBody,
2719
3001
  {
2720
3002
  toolbar,
@@ -2733,7 +3015,7 @@ function GenreTreeView({
2733
3015
  onAddChild: readOnly ? void 0 : () => handleGenreCreationAction(criteria),
2734
3016
  onRename: readOnly ? void 0 : () => handleGenreRenameAction(criteria),
2735
3017
  onChangeParent: readOnly ? void 0 : () => showPopup(
2736
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3018
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2737
3019
  ChangeParentPopup,
2738
3020
  {
2739
3021
  genrePlaylist: { ...genrePlaylist, criteria },
@@ -2754,6 +3036,7 @@ function GenreTreeView({
2754
3036
  criteriaOverviewSchema,
2755
3037
  renderGenreDetailExtras,
2756
3038
  renderArtistLink,
3039
+ renderSongLink: resolvedRenderSongLink,
2757
3040
  artist: filterArtist,
2758
3041
  renderSummary,
2759
3042
  renderEssentialTracks,
@@ -2784,6 +3067,7 @@ function GenreTreeView({
2784
3067
  criteriaOverviewSchema,
2785
3068
  renderGenreDetailExtras,
2786
3069
  renderArtistLink,
3070
+ resolvedRenderSongLink,
2787
3071
  renderSummary,
2788
3072
  renderEssentialTracks,
2789
3073
  renderSongLeading,
@@ -2793,7 +3077,7 @@ function GenreTreeView({
2793
3077
  );
2794
3078
  const isRotationControlled = controlledAllowWheelRotation !== void 0;
2795
3079
  const allowWheelRotation = controlledAllowWheelRotation ?? internalAllowWheelRotation;
2796
- 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)(
2797
3081
  PanelEdgeTab,
2798
3082
  {
2799
3083
  side: "left",
@@ -2803,8 +3087,8 @@ function GenreTreeView({
2803
3087
  ) : null;
2804
3088
  const renderArtistFilter = () => {
2805
3089
  if (isLoading || !filterArtist) return null;
2806
- 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: [
2807
- /* @__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)(
2808
3092
  "button",
2809
3093
  {
2810
3094
  type: "button",
@@ -2816,7 +3100,7 @@ function GenreTreeView({
2816
3100
  children: filterArtist.name
2817
3101
  }
2818
3102
  ),
2819
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3103
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2820
3104
  "button",
2821
3105
  {
2822
3106
  type: "button",
@@ -2828,23 +3112,23 @@ function GenreTreeView({
2828
3112
  )
2829
3113
  ] });
2830
3114
  };
2831
- const actions = /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(import_jsx_runtime39.Fragment, { children: [
2832
- !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)(
2833
3117
  GenreSearch,
2834
3118
  {
2835
3119
  genrePlaylists: visibleGenrePlaylists,
2836
3120
  onSelect: handleGenreSearchSelect,
2837
- selectedName: selectedGenrePlaylist?.name ?? builtInArtistPanel?.title ?? null,
2838
- onClear: builtInArtistPanel ? () => changeArtist(null) : handleGenreSearchClear,
3121
+ selectedName: selectedGenrePlaylist?.name ?? builtInPanel?.title ?? null,
3122
+ onClear: builtInSongPanel ? () => changeSong(null) : builtInArtistPanel ? () => changeArtist(null) : handleGenreSearchClear,
2839
3123
  onPrevious: handlePrevious,
2840
- onQueryChange: artistSearch ? setSearchText : void 0,
2841
- extraResults: artistExtraResults
3124
+ onQueryChange: artistSearch || songSearch ? setSearchText : void 0,
3125
+ extraGroups
2842
3126
  }
2843
3127
  ),
2844
- !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
2845
- /* @__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: [
2846
- !isLoading && !isControlled && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2847
- 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,
2848
3132
  {
2849
3133
  "aria-label": "Tree view mode",
2850
3134
  value: viewMode,
@@ -2857,14 +3141,14 @@ function GenreTreeView({
2857
3141
  ]
2858
3142
  }
2859
3143
  ),
2860
- !isLoading && showDisplayOptions && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3144
+ !isLoading && showDisplayOptions && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2861
3145
  "div",
2862
3146
  {
2863
3147
  className: "flex items-center gap-1",
2864
3148
  role: "group",
2865
3149
  "aria-label": "Tree display options",
2866
- children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2867
- import_ui16.Button,
3150
+ children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3151
+ import_ui18.Button,
2868
3152
  {
2869
3153
  variant: allowWheelRotation ? "default" : "outline",
2870
3154
  size: "sm",
@@ -2874,52 +3158,52 @@ function GenreTreeView({
2874
3158
  )
2875
3159
  }
2876
3160
  ),
2877
- !isLoading && !readOnly && showAddRoot && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2878
- import_ui16.IconTextButton,
3161
+ !isLoading && !readOnly && showAddRoot && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3162
+ import_ui18.IconTextButton,
2879
3163
  {
2880
- icon: import_lucide_react15.Plus,
3164
+ icon: import_lucide_react16.Plus,
2881
3165
  text: "Add root",
2882
3166
  onClick: () => handleGenreCreationAction(null)
2883
3167
  }
2884
3168
  )
2885
3169
  ] })
2886
3170
  ] });
2887
- return /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(
3171
+ return /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(
2888
3172
  "div",
2889
3173
  {
2890
3174
  ref: rootRef,
2891
3175
  className: ["genre-tree-view relative flex flex-col h-full", placement === "regions" && REGIONS_CLASS, className2].filter(Boolean).join(" "),
2892
3176
  children: [
2893
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("style", { children: PREVIOUS_BUTTON_CSS }),
2894
- /* @__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)(
2895
3179
  "div",
2896
3180
  {
2897
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"}`,
2898
3182
  children: actions
2899
3183
  }
2900
3184
  ),
2901
- !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3185
+ !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2902
3186
  "div",
2903
3187
  {
2904
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"}`,
2905
3189
  children: renderArtistFilter()
2906
3190
  }
2907
3191
  ),
2908
- detailTab && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3192
+ detailTab && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2909
3193
  "div",
2910
3194
  {
2911
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))]"}`,
2912
3196
  children: detailTab
2913
3197
  }
2914
3198
  ),
2915
- /* @__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: [
2916
- !isLoading && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
2917
- 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" ? (
2918
3202
  // The info panel's top edge aligns with the tree's; the search bar floats `--layout-panel-inset` below it,
2919
3203
  // and the header inset (the bar's height plus that inset) extends the header's fill behind the bar,
2920
3204
  // Google Maps style; the 8px bottom padding mirrors the inset above the bar, and the hidden title's height is zeroed.
2921
3205
  // The panel's own Zoom in button is hidden: the search bar carries it.
2922
- /* @__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)(
2923
3207
  GenrePlaylistTreeWheel,
2924
3208
  {
2925
3209
  scope,
@@ -2938,9 +3222,9 @@ function GenreTreeView({
2938
3222
  allowWheelRotation,
2939
3223
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
2940
3224
  busy,
2941
- 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: [
2942
3226
  floatingControlsLeading,
2943
- 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 })
2944
3228
  ] }) : void 0,
2945
3229
  defaultViewport,
2946
3230
  viewportRestore,
@@ -2950,7 +3234,7 @@ function GenreTreeView({
2950
3234
  ) }) })
2951
3235
  ) : viewMode === "pop-core" ? (
2952
3236
  // Same as the wheel: the header inset extends its fill behind the floating search bar, Google Maps style.
2953
- /* @__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)(
2954
3238
  GenrePlaylistTreeWheelRadialPopCore,
2955
3239
  {
2956
3240
  scope,
@@ -2969,9 +3253,9 @@ function GenreTreeView({
2969
3253
  allowWheelRotation,
2970
3254
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
2971
3255
  busy,
2972
- 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: [
2973
3257
  floatingControlsLeading,
2974
- 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 })
2975
3259
  ] }) : void 0,
2976
3260
  defaultViewport,
2977
3261
  viewportRestore,
@@ -2980,7 +3264,7 @@ function GenreTreeView({
2980
3264
  ...focus
2981
3265
  }
2982
3266
  ) }) })
2983
- ) : 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)(
2984
3268
  GenrePlaylistTreeWheelRadialPopCore,
2985
3269
  {
2986
3270
  outline: true,
@@ -2999,13 +3283,13 @@ function GenreTreeView({
2999
3283
  readOnly,
3000
3284
  ...infoPanelCuration
3001
3285
  }
3002
- ) }) : /* @__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(
3003
3287
  ([uuid, genrePlaylistTreePerRoot], rootIndex) => {
3004
- return /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3288
+ return /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3005
3289
  "div",
3006
3290
  {
3007
3291
  className: "tree-per-root-container relative shrink-0 h-[28rem] mt-2 mr-16 p-2 bg-[var(--color-neutral-100)] rounded-lg",
3008
- 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)(
3009
3293
  GenrePlaylistTreePerRoot,
3010
3294
  {
3011
3295
  scope,
@@ -3042,11 +3326,11 @@ function GenreTreeView({
3042
3326
  }
3043
3327
 
3044
3328
  // src/genre-tree/GenreDetailPanel.tsx
3045
- var import_react16 = require("react");
3329
+ var import_react18 = require("react");
3046
3330
  var import_genre_tree_view5 = require("@behindthemusictree/genre-tree-view");
3047
- var import_ui17 = require("@behindthemusictree/ui");
3048
- var import_genre_tree18 = require("@behindthemusictree/app-kit/genre-tree");
3049
- 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");
3050
3334
  var ITEM_COUNT_LABEL = "Songs";
3051
3335
  var RELATIVE_ITEM_COUNT = 1;
3052
3336
  function toNode(genre, parentId) {
@@ -3073,9 +3357,9 @@ function GenreDetailPanel({
3073
3357
  onExpandedChange,
3074
3358
  extras
3075
3359
  }) {
3076
- 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);
3077
3361
  const playback = useGenrePlaylistPlayback({ scope, genrePlaylists: [] });
3078
- const view = (0, import_react16.useMemo)(() => {
3362
+ const view = (0, import_react18.useMemo)(() => {
3079
3363
  if (!detail) return null;
3080
3364
  const rootColor = (0, import_genre_tree_view5.getGenreTreeColor)(detail.root.wikidataId);
3081
3365
  const relative = (genre, parentId) => ({
@@ -3101,17 +3385,17 @@ function GenreDetailPanel({
3101
3385
  };
3102
3386
  }, [detail]);
3103
3387
  if (isError) {
3104
- 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." });
3105
3389
  }
3106
3390
  if (isPending || !detail || !view) {
3107
- 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: [
3108
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_ui17.Skeleton, { className: "h-6 w-2/3" }),
3109
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_ui17.Skeleton, { className: "h-4 w-full" }),
3110
- /* @__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" })
3111
3395
  ] });
3112
3396
  }
3113
3397
  const renderBody = (node, sections) => ({
3114
- primary: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3398
+ primary: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3115
3399
  GenreInfoPanelBody,
3116
3400
  {
3117
3401
  toolbar: sections.toolbar,
@@ -3128,7 +3412,7 @@ function GenreDetailPanel({
3128
3412
  ),
3129
3413
  aside: null
3130
3414
  });
3131
- return /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
3415
+ return /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3132
3416
  import_genre_tree_view5.InfoPanel,
3133
3417
  {
3134
3418
  node: view.node,
@@ -3154,13 +3438,13 @@ function GenreDetailPanel({
3154
3438
  var import_genre_tree_view6 = require("@behindthemusictree/genre-tree-view");
3155
3439
 
3156
3440
  // src/genre-tree/track-list-sidebar/TrackListSidebar.tsx
3157
- var import_react17 = require("react");
3158
- var import_lucide_react16 = require("lucide-react");
3159
- var import_genre_tree19 = require("@behindthemusictree/app-kit/genre-tree");
3160
- 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");
3161
3445
  var import_track_noun7 = require("@behindthemusictree/app-kit/track-noun");
3162
- var import_jsx_runtime41 = require("react/jsx-runtime");
3163
- 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;
3164
3448
  function TrackListSidebar({
3165
3449
  className: className2 = "",
3166
3450
  renderDuration,
@@ -3169,20 +3453,21 @@ function TrackListSidebar({
3169
3453
  isCollapsed = false,
3170
3454
  onExpand,
3171
3455
  onMinimize,
3172
- renderArtistLink
3456
+ renderArtistLink,
3457
+ renderSongLink
3173
3458
  }) {
3174
- const { trackList, loadMore, autoAdvanceScrollRequest } = (0, import_genre_tree19.useTrackList)();
3459
+ const { trackList, loadMore, autoAdvanceScrollRequest } = (0, import_genre_tree21.useTrackList)();
3175
3460
  const trackNoun = (0, import_track_noun7.useTrackNoun)();
3176
- const trackListRef = (0, import_react17.useRef)(null);
3177
- const previousOriginRef = (0, import_react17.useRef)(trackList?.origin ?? null);
3178
- const handledAutoAdvanceRequestIdRef = (0, import_react17.useRef)(null);
3179
- const shouldFollowPlaybackRef = (0, import_react17.useRef)(true);
3180
- const programmaticScrollTopRef = (0, import_react17.useRef)(null);
3181
- const userScrollIntentRef = (0, import_react17.useRef)(false);
3182
- const userScrollIntentTimerRef = (0, import_react17.useRef)(null);
3183
- 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);
3184
3469
  const hasMore = !!trackList && trackList.nextPage !== null;
3185
- (0, import_react17.useEffect)(() => {
3470
+ (0, import_react19.useEffect)(() => {
3186
3471
  const sentinel = loadMoreSentinelRef.current;
3187
3472
  if (!hasMore || !sentinel) return;
3188
3473
  const observer = new IntersectionObserver((entries) => {
@@ -3202,11 +3487,11 @@ function TrackListSidebar({
3202
3487
  programmaticScrollTopRef.current = null;
3203
3488
  if (trackListRef.current) trackListRef.current.scrollTop = 0;
3204
3489
  }, [trackList?.origin]);
3205
- (0, import_react17.useEffect)(() => {
3490
+ (0, import_react19.useEffect)(() => {
3206
3491
  const list = trackListRef.current;
3207
3492
  if (!isCollapsed && list?.scrollTop === 0) shouldFollowPlaybackRef.current = true;
3208
3493
  }, [isCollapsed]);
3209
- (0, import_react17.useEffect)(
3494
+ (0, import_react19.useEffect)(
3210
3495
  () => () => {
3211
3496
  if (userScrollIntentTimerRef.current !== null) clearTimeout(userScrollIntentTimerRef.current);
3212
3497
  },
@@ -3260,25 +3545,25 @@ function TrackListSidebar({
3260
3545
  }
3261
3546
  };
3262
3547
  const positionClasses = layout === "inline" ? "relative w-full flex h-full flex-col" : "fixed right-0 w-[50rem] flex flex-col";
3263
- return trackList ? /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(
3548
+ return trackList ? /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)(
3264
3549
  "div",
3265
3550
  {
3266
3551
  "data-theme": "dark",
3267
3552
  className: `track-list-sidebar ${positionClasses} rounded-2xl bg-[var(--color-surface)] pb-1 ${className2}`,
3268
3553
  style: layout === "fixed" ? { top: "63.5px", maxHeight: "calc(100vh - 63.5px - 79px)" } : void 0,
3269
3554
  children: [
3270
- /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "header flex h-16 items-center px-4 py-2 text-[var(--color-text)]", children: [
3271
- /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "min-w-0 flex flex-1 flex-col justify-center", children: [
3272
- /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "origin flex text-xl", children: [
3273
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
3274
- /* @__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 }) })
3275
3560
  ] }),
3276
- /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "info flex items-center text-sm text-[var(--color-text-muted)]", children: [
3277
- 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 `,
3278
3563
  `${trackList.total} ${trackList.total > 1 ? trackNoun.plural : trackNoun.singular}`
3279
3564
  ] })
3280
3565
  ] }),
3281
- (isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
3566
+ (isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
3282
3567
  "button",
3283
3568
  {
3284
3569
  type: "button",
@@ -3287,11 +3572,11 @@ function TrackListSidebar({
3287
3572
  "aria-expanded": !isCollapsed,
3288
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"}`,
3289
3574
  onClick: isCollapsed ? onExpand : onMinimize,
3290
- 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" })
3291
3576
  }
3292
3577
  )
3293
3578
  ] }),
3294
- !isCollapsed && /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(
3579
+ !isCollapsed && /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)(
3295
3580
  "ul",
3296
3581
  {
3297
3582
  ref: trackListRef,
@@ -3301,17 +3586,18 @@ function TrackListSidebar({
3301
3586
  onPointerDown: markUserScrollIntent,
3302
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",
3303
3588
  children: [
3304
- 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)(
3305
3590
  TrackItem,
3306
3591
  {
3307
3592
  track,
3308
3593
  position: index + 1,
3309
3594
  renderDuration,
3310
3595
  renderActions,
3311
- renderArtistLink
3596
+ renderArtistLink,
3597
+ renderSongLink
3312
3598
  }
3313
3599
  ) }, track.uuid)),
3314
- 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" })
3315
3601
  ]
3316
3602
  }
3317
3603
  )
@@ -3321,16 +3607,16 @@ function TrackListSidebar({
3321
3607
  }
3322
3608
 
3323
3609
  // src/genre-tree/PlayerPanel.tsx
3324
- var import_react18 = require("react");
3610
+ var import_react20 = require("react");
3325
3611
  var import_tailwind_merge2 = require("tailwind-merge");
3326
- var import_player12 = require("@behindthemusictree/app-kit/player");
3327
- 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");
3328
3614
 
3329
3615
  // src/genre-tree/PlayerControls.tsx
3330
- var import_lucide_react17 = require("lucide-react");
3331
- var import_genre_tree21 = require("@behindthemusictree/app-kit/genre-tree");
3332
- var import_player11 = require("@behindthemusictree/app-kit/player");
3333
- 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");
3334
3620
  function PlayerControls() {
3335
3621
  const {
3336
3622
  duration,
@@ -3341,23 +3627,23 @@ function PlayerControls() {
3341
3627
  seekTo,
3342
3628
  setVolume,
3343
3629
  volume
3344
- } = (0, import_player11.usePlayer)();
3345
- const { hasNextTrack, hasPreviousTrack, playNextTrack, playPreviousTrack } = (0, import_genre_tree21.useTrackList)();
3346
- const currentTime = (0, import_player11.useCurrentTime)();
3347
- 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;
3348
3634
  const isReady = !!playerTrackObject?.isReady && !isBusy && !playerTrackObject.loadError;
3349
3635
  const safeDuration = Number.isFinite(duration) && duration > 0 ? duration : 0;
3350
3636
  const safeCurrentTime = Number.isFinite(currentTime) ? Math.max(0, Math.min(currentTime, safeDuration)) : 0;
3351
- const isPlaying = playState === import_player11.PlayStates.PLAYING;
3352
- return /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(
3637
+ const isPlaying = playState === import_player12.PlayStates.PLAYING;
3638
+ return /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)(
3353
3639
  "section",
3354
3640
  {
3355
3641
  "aria-label": "Player controls",
3356
3642
  className: "player-controls flex shrink-0 flex-col gap-2 border-b border-zinc-800 px-3 py-2 text-zinc-200",
3357
3643
  children: [
3358
- /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: "flex min-w-0 items-center gap-2 text-xs tabular-nums text-zinc-400", children: [
3359
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { "aria-label": "Elapsed time", children: (0, import_genre_tree21.formatTime)(safeCurrentTime) }),
3360
- /* @__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)(
3361
3647
  "input",
3362
3648
  {
3363
3649
  "aria-label": "Seek",
@@ -3371,11 +3657,11 @@ function PlayerControls() {
3371
3657
  onChange: (event) => seekTo(Number(event.currentTarget.value))
3372
3658
  }
3373
3659
  ),
3374
- /* @__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) })
3375
3661
  ] }),
3376
- /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: "grid min-h-9 grid-cols-[1fr_auto_1fr] items-center gap-3", children: [
3377
- /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: "col-start-2 flex items-center justify-center gap-2", children: [
3378
- /* @__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)(
3379
3665
  "button",
3380
3666
  {
3381
3667
  type: "button",
@@ -3384,10 +3670,10 @@ function PlayerControls() {
3384
3670
  disabled: !isReady || !hasPreviousTrack,
3385
3671
  onClick: playPreviousTrack,
3386
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",
3387
- 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" })
3388
3674
  }
3389
3675
  ),
3390
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3676
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3391
3677
  "button",
3392
3678
  {
3393
3679
  type: "button",
@@ -3396,10 +3682,10 @@ function PlayerControls() {
3396
3682
  disabled: !isReady,
3397
3683
  onClick: handlePlayPauseAction,
3398
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",
3399
- 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" })
3400
3686
  }
3401
3687
  ),
3402
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3688
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3403
3689
  "button",
3404
3690
  {
3405
3691
  type: "button",
@@ -3408,14 +3694,14 @@ function PlayerControls() {
3408
3694
  disabled: !isReady || !hasNextTrack,
3409
3695
  onClick: playNextTrack,
3410
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",
3411
- 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" })
3412
3698
  }
3413
3699
  )
3414
3700
  ] }),
3415
- /* @__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: [
3416
- 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" }),
3417
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { className: "sr-only", children: "Volume" }),
3418
- /* @__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)(
3419
3705
  "input",
3420
3706
  {
3421
3707
  "aria-label": "Volume",
@@ -3437,7 +3723,7 @@ function PlayerControls() {
3437
3723
  }
3438
3724
 
3439
3725
  // src/genre-tree/PlayerPanel.tsx
3440
- var import_jsx_runtime43 = require("react/jsx-runtime");
3726
+ var import_jsx_runtime45 = require("react/jsx-runtime");
3441
3727
  var PANEL_ID = "player-panel";
3442
3728
  var DEFAULT_EXTENDED_WIDTH = "calc(100vw - var(--layout-center-left) - var(--layout-gutter))";
3443
3729
  function PlayerPanel({
@@ -3446,16 +3732,17 @@ function PlayerPanel({
3446
3732
  expanded,
3447
3733
  onExpandedChange,
3448
3734
  extendedWidth = DEFAULT_EXTENDED_WIDTH,
3449
- renderArtistLink
3735
+ renderArtistLink,
3736
+ renderSongLink
3450
3737
  }) {
3451
- const { playerTrackObject } = (0, import_player12.usePlayer)();
3452
- const { toggleTrackListSidebar, isTrackListSidebarVisible } = (0, import_genre_tree22.useTrackListSidebarVisibility)();
3453
- 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)("");
3454
3741
  const loadedTrackTitle = playerTrackObject?.track.title;
3455
- (0, import_react18.useEffect)(() => {
3742
+ (0, import_react20.useEffect)(() => {
3456
3743
  if (loadedTrackTitle) setLastLoadedTrackTitle(loadedTrackTitle);
3457
3744
  }, [loadedTrackTitle]);
3458
- const [uncontrolledExtended, setUncontrolledExtended] = (0, import_react18.useState)(false);
3745
+ const [uncontrolledExtended, setUncontrolledExtended] = (0, import_react20.useState)(false);
3459
3746
  const isExtendRequested = expanded ?? uncontrolledExtended;
3460
3747
  const setIsExtendRequested = (next) => {
3461
3748
  setUncontrolledExtended(next);
@@ -3476,7 +3763,7 @@ function PlayerPanel({
3476
3763
  onClick: () => setIsExtendRequested(true),
3477
3764
  expanded: false
3478
3765
  } : null;
3479
- return /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)(
3766
+ return /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(
3480
3767
  "div",
3481
3768
  {
3482
3769
  id: PANEL_ID,
@@ -3489,7 +3776,7 @@ function PlayerPanel({
3489
3776
  ),
3490
3777
  style: isExtended ? { width: extendedWidth } : void 0,
3491
3778
  children: [
3492
- /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
3779
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3493
3780
  PanelEdgeTab,
3494
3781
  {
3495
3782
  side: "right",
@@ -3499,7 +3786,7 @@ function PlayerPanel({
3499
3786
  className: "absolute right-full top-1/2 -translate-y-1/2"
3500
3787
  }
3501
3788
  ),
3502
- /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)(
3789
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(
3503
3790
  "div",
3504
3791
  {
3505
3792
  className: (0, import_tailwind_merge2.twMerge)(
@@ -3507,15 +3794,15 @@ function PlayerPanel({
3507
3794
  isExtended ? "flex-row" : "flex-col"
3508
3795
  ),
3509
3796
  children: [
3510
- /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { className: "flex w-[var(--layout-right-w)] shrink-0 flex-col", children: [
3511
- /* @__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 }) }),
3512
- /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { className: "relative w-full shrink-0", children: [
3513
- /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_player12.PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
3514
- 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 })
3515
3802
  ] }),
3516
- /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(PlayerControls, {})
3803
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(PlayerControls, {})
3517
3804
  ] }),
3518
- /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
3805
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3519
3806
  TrackListSidebar,
3520
3807
  {
3521
3808
  layout: "inline",
@@ -3526,7 +3813,8 @@ function PlayerPanel({
3526
3813
  "min-h-0 min-w-0 flex-1",
3527
3814
  isExtended && "border-l border-zinc-800"
3528
3815
  ),
3529
- renderArtistLink
3816
+ renderArtistLink,
3817
+ renderSongLink
3530
3818
  }
3531
3819
  )
3532
3820
  ]
@@ -3540,33 +3828,33 @@ function PlayerPanel({
3540
3828
  // src/genre-tree/Rating.tsx
3541
3829
  var import_uuid = require("uuid");
3542
3830
  var import_ai = require("react-icons/ai");
3543
- var import_genre_tree23 = require("@behindthemusictree/app-kit/genre-tree");
3544
- 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");
3545
3833
  function Rating({ rating, handleChange }) {
3546
3834
  const instanceId = (0, import_uuid.v4)();
3547
- 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: [
3548
- /* @__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)(
3549
3837
  "input",
3550
3838
  {
3551
3839
  id: `star-${instanceId}-${i}`,
3552
3840
  className: "w-6 h-6 opacity-0",
3553
3841
  type: "radio",
3554
3842
  name: "rating",
3555
- value: i === 0 ? import_genre_tree23.FORM_RATING_NULL_VALUE : i,
3843
+ value: i === 0 ? import_genre_tree25.FORM_RATING_NULL_VALUE : i,
3556
3844
  checked: rating === (i === 0 ? null : i),
3557
3845
  onChange: handleChange
3558
3846
  }
3559
3847
  ),
3560
- /* @__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: [
3561
- /* @__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)(
3562
3850
  import_ai.AiOutlineStar,
3563
3851
  {
3564
- 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)]"}`
3565
3853
  }
3566
3854
  ),
3567
- /* @__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" }),
3568
3856
  " "
3569
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3857
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3570
3858
  import_ai.AiFillStar,
3571
3859
  {
3572
3860
  className: `w-7 h-7 absolute top-0 left-0 ${(rating ?? -1) >= i ? "text-[var(--color-accent-rating)]" : "text-[var(--color-text-muted)]"}`