@behindthemusictree/app-kit-ui 33.3.0 → 34.1.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
@@ -587,19 +587,20 @@ function AuthCallbackHandler({ renderStatus, ...options }) {
587
587
  }
588
588
 
589
589
  // src/genre-tree/GenreTreeView.tsx
590
- var import_react16 = require("react");
590
+ var import_react17 = require("react");
591
591
  var import_react_query3 = require("@tanstack/react-query");
592
592
  var import_lucide_react15 = require("lucide-react");
593
593
  var import_lucide_react16 = require("lucide-react");
594
594
  var import_ui19 = require("@behindthemusictree/ui");
595
595
  var import_genre_tree_view4 = require("@behindthemusictree/genre-tree-view");
596
- var import_genre_tree21 = require("@behindthemusictree/app-kit/genre-tree");
596
+ var import_genre_tree27 = require("@behindthemusictree/app-kit/genre-tree");
597
597
  var import_popup6 = require("@behindthemusictree/app-kit/popup");
598
598
 
599
599
  // src/genre-tree/playlist-tree/size-metric.ts
600
+ var import_genre_tree = require("@behindthemusictree/app-kit/genre-tree");
600
601
  var hasViewsCount = (genrePlaylists) => genrePlaylists.every((genrePlaylist) => genrePlaylist.viewsCount !== void 0);
601
602
  var genrePlaylistSize = (genrePlaylist, metric) => {
602
- if (metric === "songs") return genrePlaylist.tracksCount;
603
+ if (metric === "songs") return (0, import_genre_tree.genrePlaylistTrackCount)(genrePlaylist);
603
604
  if (genrePlaylist.viewsCount === void 0) {
604
605
  throw new Error(`Genre playlist ${genrePlaylist.uuid} has no viewsCount to size by views`);
605
606
  }
@@ -623,6 +624,7 @@ function toTreeNodes(genrePlaylists, sizeMetric) {
623
624
  }
624
625
 
625
626
  // src/genre-tree/playlist-tree/TreePerRoot.tsx
627
+ var import_genre_tree3 = require("@behindthemusictree/app-kit/genre-tree");
626
628
  var import_react4 = require("react");
627
629
  var import_genre_tree_view = require("@behindthemusictree/genre-tree-view");
628
630
  var import_player = require("@behindthemusictree/app-kit/player");
@@ -631,29 +633,28 @@ var import_transport3 = require("@behindthemusictree/app-kit/transport");
631
633
 
632
634
  // src/genre-tree/playlist-tree/useGenrePlaylistPlayback.ts
633
635
  var import_react3 = require("react");
634
- var import_genre_tree = require("@behindthemusictree/app-kit/genre-tree");
636
+ var import_genre_tree2 = require("@behindthemusictree/app-kit/genre-tree");
635
637
  function useGenrePlaylistPlayback({
636
638
  scope,
637
639
  genrePlaylists,
638
640
  artist = null
639
641
  }) {
640
- const { trackList, playNewTrackListFromGenrePlaylist, playNewTrackListFromArtist } = (0, import_genre_tree.useTrackList)();
642
+ const { trackList, playNewTrackListFromGenrePlaylist } = (0, import_genre_tree2.useTrackList)();
641
643
  const origin = trackList?.origin;
642
644
  const playingNodeId = (0, import_react3.useMemo)(() => {
643
- if (!origin) return null;
644
- if (!artist) return origin.type === import_genre_tree.TrackListOriginType.GENRE_PLAYLIST ? origin.uuid : null;
645
- if (origin.type !== import_genre_tree.TrackListOriginType.ARTIST || origin.uuid !== artist.uuid) return null;
646
- const { genreUuid } = origin;
647
- return genreUuid ? genrePlaylists.find((gp) => gp.criteria?.uuid === genreUuid)?.uuid ?? null : null;
648
- }, [origin, artist, genrePlaylists]);
649
- const play = (0, import_react3.useCallback)(
650
- async (genrePlaylist) => {
651
- if (!artist) return playNewTrackListFromGenrePlaylist(genrePlaylist, scope);
652
- if (!genrePlaylist.criteria) return;
653
- return playNewTrackListFromArtist(artist, scope, { ordering: "title", genreUuid: genrePlaylist.criteria.uuid });
654
- },
655
- [artist, scope, playNewTrackListFromGenrePlaylist, playNewTrackListFromArtist]
656
- );
645
+ if (!origin || origin.type !== import_genre_tree2.TrackListOriginType.GENRE_PLAYLIST || origin.scope !== scope) return null;
646
+ const playlistOrigin = origin;
647
+ if (playlistOrigin.artistUuid !== artist?.uuid) return null;
648
+ const playlist = genrePlaylists.find((item) => item.uuid === origin.uuid) ?? playlistOrigin.criteriaPlaylist;
649
+ const expected = scope === "reference" && playlist && !("criteria" in playlist && playlist.criteria === null) ? (0, import_genre_tree2.defaultGenreMusicSelection)(import_genre_tree2.GenreCatalogueCountsSchema.parse(playlist)) : void 0;
650
+ if (playlistOrigin.selection && expected && (0, import_genre_tree2.genreMusicSelectionKey)(playlistOrigin.selection).join() !== (0, import_genre_tree2.genreMusicSelectionKey)(expected).join()) return null;
651
+ return origin.uuid;
652
+ }, [origin, artist, genrePlaylists, scope]);
653
+ const play = (0, import_react3.useCallback)(async (playlist) => {
654
+ if (artist && !playlist.criteria) return;
655
+ const selection = scope === "reference" && playlist.criteria !== null ? (0, import_genre_tree2.defaultGenreMusicSelection)(import_genre_tree2.GenreCatalogueCountsSchema.parse(playlist)) : void 0;
656
+ return selection || artist ? playNewTrackListFromGenrePlaylist(playlist, scope, { selection, ...artist && { artistUuid: artist.uuid } }) : playNewTrackListFromGenrePlaylist(playlist, scope);
657
+ }, [artist, scope, playNewTrackListFromGenrePlaylist]);
657
658
  return (0, import_react3.useMemo)(() => ({ playingNodeId, play }), [playingNodeId, play]);
658
659
  }
659
660
 
@@ -688,7 +689,7 @@ function GenrePlaylistTreePerRoot({
688
689
  id: genrePlaylist.uuid,
689
690
  parentId: genrePlaylist.parent?.uuid ?? null,
690
691
  name: genrePlaylist.name,
691
- itemCount: genrePlaylist.tracksCount,
692
+ itemCount: (0, import_genre_tree3.genrePlaylistTrackCount)(genrePlaylist),
692
693
  actionable: Boolean(genrePlaylist.criteria),
693
694
  side: genrePlaylist.criteria?.side ?? void 0,
694
695
  wikidataId: genrePlaylist.criteria?.wikidataId ?? null,
@@ -704,7 +705,7 @@ function GenrePlaylistTreePerRoot({
704
705
  setIsPlaying(!isPlaying);
705
706
  return;
706
707
  }
707
- if (genrePlaylist.tracksCount === 0) {
708
+ if ((0, import_genre_tree3.genrePlaylistTrackCount)(genrePlaylist) === 0) {
708
709
  return;
709
710
  }
710
711
  play(genrePlaylist).catch((error) => {
@@ -768,6 +769,7 @@ function GenrePlaylistTreePerRoot({
768
769
  }
769
770
 
770
771
  // src/genre-tree/playlist-tree/TreeWheel.tsx
772
+ var import_genre_tree4 = require("@behindthemusictree/app-kit/genre-tree");
771
773
  var import_react5 = require("react");
772
774
  var import_genre_tree_view2 = require("@behindthemusictree/genre-tree-view");
773
775
  var import_player2 = require("@behindthemusictree/app-kit/player");
@@ -820,7 +822,7 @@ function GenrePlaylistTreeWheel({
820
822
  setIsPlaying(!isPlaying);
821
823
  return;
822
824
  }
823
- if (genrePlaylist.tracksCount === 0) {
825
+ if ((0, import_genre_tree4.genrePlaylistTrackCount)(genrePlaylist) === 0) {
824
826
  return;
825
827
  }
826
828
  play(genrePlaylist).catch((error) => {
@@ -885,6 +887,7 @@ function GenrePlaylistTreeWheel({
885
887
  }
886
888
 
887
889
  // src/genre-tree/playlist-tree/TreeWheelRadialPopCore.tsx
890
+ var import_genre_tree5 = require("@behindthemusictree/app-kit/genre-tree");
888
891
  var import_react6 = require("react");
889
892
  var import_genre_tree_view3 = require("@behindthemusictree/genre-tree-view");
890
893
  var import_player3 = require("@behindthemusictree/app-kit/player");
@@ -929,7 +932,7 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
929
932
  setIsPlaying(!isPlaying);
930
933
  return;
931
934
  }
932
- if (genrePlaylist.tracksCount === 0) {
935
+ if ((0, import_genre_tree5.genrePlaylistTrackCount)(genrePlaylist) === 0) {
933
936
  return;
934
937
  }
935
938
  play(genrePlaylist).catch((error) => {
@@ -1024,10 +1027,13 @@ function GenreTreeWheelHandoff({ skeleton, children, onRevealedChange }) {
1024
1027
  ] });
1025
1028
  }
1026
1029
 
1030
+ // src/genre-tree/GenreTreeSearch.tsx
1031
+ var import_react9 = require("react");
1032
+
1027
1033
  // src/genre-tree/GenreSearch.tsx
1028
1034
  var import_lucide_react7 = require("lucide-react");
1029
1035
  var import_ui7 = require("@behindthemusictree/ui");
1030
- var import_genre_tree5 = require("@behindthemusictree/app-kit/genre-tree");
1036
+ var import_genre_tree9 = require("@behindthemusictree/app-kit/genre-tree");
1031
1037
 
1032
1038
  // src/genre-tree/search/SearchBox.tsx
1033
1039
  var import_react8 = require("react");
@@ -1083,10 +1089,10 @@ var SEARCH_FAILED_STATUS = "Search failed";
1083
1089
  var searchStatus = (state, count, noun) => state.isError ? SEARCH_FAILED_STATUS : state.settled && count === 0 ? `No ${noun} found` : "";
1084
1090
 
1085
1091
  // src/genre-tree/search/useArtistSearchGroup.tsx
1086
- var import_genre_tree2 = require("@behindthemusictree/app-kit/genre-tree");
1092
+ var import_genre_tree6 = require("@behindthemusictree/app-kit/genre-tree");
1087
1093
  var import_jsx_runtime17 = require("react/jsx-runtime");
1088
1094
  function useArtistSearchGroup(getBaseUrl, text, onSelect, enabled = true) {
1089
- const search = (0, import_genre_tree2.useArtistSearch)(getBaseUrl, enabled ? text : "");
1095
+ const search = (0, import_genre_tree6.useArtistSearch)(getBaseUrl, enabled ? text : "");
1090
1096
  if (!enabled) return void 0;
1091
1097
  return {
1092
1098
  key: "artists",
@@ -1114,17 +1120,17 @@ function useArtistSearchGroup(getBaseUrl, text, onSelect, enabled = true) {
1114
1120
  }
1115
1121
 
1116
1122
  // src/genre-tree/search/useSongSearchGroup.tsx
1117
- var import_genre_tree3 = require("@behindthemusictree/app-kit/genre-tree");
1123
+ var import_genre_tree7 = require("@behindthemusictree/app-kit/genre-tree");
1118
1124
  var import_jsx_runtime18 = require("react/jsx-runtime");
1119
1125
  function useSongSearchGroup(getBaseUrl, text, onSelect, enabled = true) {
1120
- const search = (0, import_genre_tree3.useSongSearch)(getBaseUrl, enabled ? text : "");
1126
+ const search = (0, import_genre_tree7.useSongSearch)(getBaseUrl, enabled ? text : "");
1121
1127
  if (!enabled) return void 0;
1122
1128
  return {
1123
1129
  key: "songs",
1124
1130
  label: "Songs",
1125
1131
  items: search.songs.map((song) => ({
1126
1132
  key: song.uuid,
1127
- content: /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("span", { className: (0, import_genre_tree3.isSongSearchResultPlayable)(song) ? void 0 : "opacity-60", children: [
1133
+ content: /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("span", { className: (0, import_genre_tree7.isSongSearchResultPlayable)(song) ? void 0 : "opacity-60", children: [
1128
1134
  /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "block truncate font-medium", children: song.title }),
1129
1135
  /* @__PURE__ */ (0, import_jsx_runtime18.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 ") })
1130
1136
  ] }),
@@ -1136,10 +1142,10 @@ function useSongSearchGroup(getBaseUrl, text, onSelect, enabled = true) {
1136
1142
  }
1137
1143
 
1138
1144
  // src/genre-tree/search/useGenreRemoteSearchGroup.tsx
1139
- var import_genre_tree4 = require("@behindthemusictree/app-kit/genre-tree");
1145
+ var import_genre_tree8 = require("@behindthemusictree/app-kit/genre-tree");
1140
1146
  var import_jsx_runtime19 = require("react/jsx-runtime");
1141
1147
  function useGenreRemoteSearchGroup(getBaseUrl, text, onSelect, { key = "genres", label = "Genres", filter, describe, ...options } = {}) {
1142
- const search = (0, import_genre_tree4.useGenreRemoteSearch)(getBaseUrl, text, options);
1148
+ const search = (0, import_genre_tree8.useGenreRemoteSearch)(getBaseUrl, text, options);
1143
1149
  const genres = filter ? search.genres.filter(filter) : search.genres;
1144
1150
  return {
1145
1151
  key,
@@ -1173,7 +1179,7 @@ function GenreSearch({
1173
1179
  extraGroups = [],
1174
1180
  onFocus
1175
1181
  }) {
1176
- const { query, setQuery, results, canClear, clear, select } = (0, import_genre_tree5.useGenreSearch)({
1182
+ const { query, setQuery, results, canClear, clear, select } = (0, import_genre_tree9.useGenreSearch)({
1177
1183
  genrePlaylists,
1178
1184
  onSelect,
1179
1185
  selectedName,
@@ -1224,7 +1230,47 @@ function GenreSearch({
1224
1230
  ) });
1225
1231
  }
1226
1232
 
1233
+ // src/genre-tree/GenreTreeSearch.tsx
1234
+ var import_jsx_runtime21 = require("react/jsx-runtime");
1235
+ function GenreTreeSearch({
1236
+ artistBaseUrl,
1237
+ songBaseUrl,
1238
+ onArtistSelect,
1239
+ onSongSelect,
1240
+ artistSearch,
1241
+ songSearch,
1242
+ SearchGroups,
1243
+ extraGroups = [],
1244
+ onQueryChange,
1245
+ onFocus,
1246
+ ...props
1247
+ }) {
1248
+ const [query, setQuery] = (0, import_react9.useState)("");
1249
+ const [focused, setFocused] = (0, import_react9.useState)(false);
1250
+ const deferredQuery = (0, import_react9.useDeferredValue)(query);
1251
+ const artistGroup = useArtistSearchGroup(artistBaseUrl, deferredQuery, onArtistSelect, artistSearch);
1252
+ const songGroup = useSongSearchGroup(songBaseUrl, deferredQuery, onSongSelect, songSearch);
1253
+ const render = (hostGroups) => /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
1254
+ GenreSearch,
1255
+ {
1256
+ ...props,
1257
+ onQueryChange: (text) => {
1258
+ setQuery(text);
1259
+ onQueryChange?.(text);
1260
+ },
1261
+ onFocus: () => {
1262
+ setFocused(true);
1263
+ onFocus?.();
1264
+ },
1265
+ extraGroups: [...extraGroups, ...hostGroups, artistGroup, songGroup].filter((group) => group !== void 0)
1266
+ }
1267
+ );
1268
+ return SearchGroups ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(SearchGroups, { query: deferredQuery, focused, children: render }) : render([]);
1269
+ }
1270
+
1227
1271
  // src/genre-tree/GenreInfoPanelBody.tsx
1272
+ var import_genre_tree18 = require("@behindthemusictree/app-kit/genre-tree");
1273
+ var import_track_noun4 = require("@behindthemusictree/app-kit/track-noun");
1228
1274
  var import_player6 = require("@behindthemusictree/app-kit/player");
1229
1275
 
1230
1276
  // src/genre-tree/GenreActionBand.tsx
@@ -1235,18 +1281,18 @@ var import_lucide_react10 = require("lucide-react");
1235
1281
  var import_popup4 = require("@behindthemusictree/app-kit/popup");
1236
1282
 
1237
1283
  // src/genre-tree/SharePopup.tsx
1238
- var import_react9 = require("react");
1284
+ var import_react10 = require("react");
1239
1285
  var import_lucide_react9 = require("lucide-react");
1240
1286
  var import_fa3 = require("react-icons/fa");
1241
1287
  var import_si = require("react-icons/si");
1242
1288
 
1243
1289
  // src/genre-tree/ShareDestinationButton.tsx
1244
- var import_jsx_runtime21 = require("react/jsx-runtime");
1290
+ var import_jsx_runtime22 = require("react/jsx-runtime");
1245
1291
  var className = "group flex min-w-0 flex-1 flex-col items-center justify-center gap-1.5 rounded-xl px-2 py-2 text-xs text-[var(--color-text)] transition-colors hover:bg-[var(--color-surface-muted)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] disabled:cursor-not-allowed disabled:opacity-50";
1246
1292
  function Contents({ icon, label }) {
1247
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(import_jsx_runtime21.Fragment, { children: [
1248
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "flex h-12 w-12 items-center justify-center rounded-full bg-[var(--color-surface-muted)] transition-colors group-hover:bg-[var(--color-border)]", children: icon }),
1249
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "max-w-full truncate", children: label })
1293
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(import_jsx_runtime22.Fragment, { children: [
1294
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: "flex h-12 w-12 items-center justify-center rounded-full bg-[var(--color-surface-muted)] transition-colors group-hover:bg-[var(--color-border)]", children: icon }),
1295
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: "max-w-full truncate", children: label })
1250
1296
  ] });
1251
1297
  }
1252
1298
  function ShareDestinationButton({
@@ -1258,29 +1304,29 @@ function ShareDestinationButton({
1258
1304
  ariaLabel
1259
1305
  }) {
1260
1306
  if (href) {
1261
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer", "aria-label": ariaLabel, className, children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Contents, { icon, label }) });
1307
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer", "aria-label": ariaLabel, className, children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(Contents, { icon, label }) });
1262
1308
  }
1263
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("button", { type: "button", onClick, disabled, "aria-label": ariaLabel, className, children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Contents, { icon, label }) });
1309
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("button", { type: "button", onClick, disabled, "aria-label": ariaLabel, className, children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(Contents, { icon, label }) });
1264
1310
  }
1265
1311
 
1266
1312
  // src/genre-tree/SharePreviewCard.tsx
1267
1313
  var import_lucide_react8 = require("lucide-react");
1268
- var import_jsx_runtime22 = require("react/jsx-runtime");
1314
+ var import_jsx_runtime23 = require("react/jsx-runtime");
1269
1315
  function SharePreviewCard({ target, url }) {
1270
1316
  const Icon = { genre: import_lucide_react8.Music2, artist: import_lucide_react8.UserRound, song: import_lucide_react8.Music }[target.kind];
1271
1317
  const typeLabel = { genre: "Genre", artist: "Artist", song: "Song" }[target.kind];
1272
- return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("div", { className: "rounded-3xl bg-[var(--color-surface-muted)] p-4", children: /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { className: "flex min-h-32 overflow-hidden rounded-2xl bg-[var(--color-surface)] shadow-lg", children: [
1273
- /* @__PURE__ */ (0, import_jsx_runtime22.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_runtime22.jsx)(Icon, { "aria-hidden": "true", className: "h-10 w-10" }) }),
1274
- /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col justify-center gap-1 p-4", children: [
1275
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: "text-xs font-medium uppercase tracking-wide text-[var(--color-text-muted)]", children: typeLabel }),
1276
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: "truncate text-base font-semibold text-[var(--color-text)]", title: target.name, children: target.name }),
1277
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: "truncate text-sm text-[var(--color-primary)]", title: url, children: url })
1318
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("div", { className: "rounded-3xl bg-[var(--color-surface-muted)] p-4", children: /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("div", { className: "flex min-h-32 overflow-hidden rounded-2xl bg-[var(--color-surface)] shadow-lg", children: [
1319
+ /* @__PURE__ */ (0, import_jsx_runtime23.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_runtime23.jsx)(Icon, { "aria-hidden": "true", className: "h-10 w-10" }) }),
1320
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col justify-center gap-1 p-4", children: [
1321
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "text-xs font-medium uppercase tracking-wide text-[var(--color-text-muted)]", children: typeLabel }),
1322
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "truncate text-base font-semibold text-[var(--color-text)]", title: target.name, children: target.name }),
1323
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "truncate text-sm text-[var(--color-primary)]", title: url, children: url })
1278
1324
  ] })
1279
1325
  ] }) });
1280
1326
  }
1281
1327
 
1282
1328
  // src/genre-tree/SharePopup.tsx
1283
- var import_jsx_runtime23 = require("react/jsx-runtime");
1329
+ var import_jsx_runtime24 = require("react/jsx-runtime");
1284
1330
  function resolveAbsoluteUrl(url) {
1285
1331
  try {
1286
1332
  const parsed = new URL(url, window.location.href);
@@ -1313,20 +1359,20 @@ async function copyText(text) {
1313
1359
  }
1314
1360
  }
1315
1361
  function SharePopup({ target, url, onClose }) {
1316
- const [absoluteUrl, setAbsoluteUrl] = (0, import_react9.useState)(url);
1317
- const [copyStatus, setCopyStatus] = (0, import_react9.useState)("idle");
1318
- const [canUseNativeShare, setCanUseNativeShare] = (0, import_react9.useState)(false);
1319
- const [shareError, setShareError] = (0, import_react9.useState)(null);
1320
- const [toastId, setToastId] = (0, import_react9.useState)(0);
1321
- (0, import_react9.useEffect)(() => {
1362
+ const [absoluteUrl, setAbsoluteUrl] = (0, import_react10.useState)(url);
1363
+ const [copyStatus, setCopyStatus] = (0, import_react10.useState)("idle");
1364
+ const [canUseNativeShare, setCanUseNativeShare] = (0, import_react10.useState)(false);
1365
+ const [shareError, setShareError] = (0, import_react10.useState)(null);
1366
+ const [toastId, setToastId] = (0, import_react10.useState)(0);
1367
+ (0, import_react10.useEffect)(() => {
1322
1368
  setAbsoluteUrl(resolveAbsoluteUrl(url) ?? "");
1323
1369
  setCopyStatus("idle");
1324
1370
  setShareError(null);
1325
1371
  }, [url]);
1326
- (0, import_react9.useEffect)(() => {
1372
+ (0, import_react10.useEffect)(() => {
1327
1373
  setCanUseNativeShare(typeof navigator !== "undefined" && typeof navigator.share === "function");
1328
1374
  }, []);
1329
- const destinationUrls = (0, import_react9.useMemo)(() => {
1375
+ const destinationUrls = (0, import_react10.useMemo)(() => {
1330
1376
  if (!absoluteUrl) return null;
1331
1377
  const message = `${target.name} \u2014 ${absoluteUrl}`;
1332
1378
  return {
@@ -1344,7 +1390,7 @@ function SharePopup({ target, url, onClose }) {
1344
1390
  setCopyStatus(copied ? "idle" : "failed");
1345
1391
  if (copied) setToastId((id) => id + 1);
1346
1392
  };
1347
- const dismissToast = (0, import_react9.useCallback)(() => setToastId(0), []);
1393
+ const dismissToast = (0, import_react10.useCallback)(() => setToastId(0), []);
1348
1394
  const handleNativeShare = async () => {
1349
1395
  if (!absoluteUrl || !navigator.share) return;
1350
1396
  setShareError(null);
@@ -1355,8 +1401,8 @@ function SharePopup({ target, url, onClose }) {
1355
1401
  setShareError("Could not open the share options.");
1356
1402
  }
1357
1403
  };
1358
- return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(import_jsx_runtime23.Fragment, { children: [
1359
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
1404
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(import_jsx_runtime24.Fragment, { children: [
1405
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
1360
1406
  BasePopup_default,
1361
1407
  {
1362
1408
  title: "Share",
@@ -1371,67 +1417,67 @@ function SharePopup({ target, url, onClose }) {
1371
1417
  titleTextClassName: "!text-xl !font-semibold !text-[var(--color-text)]",
1372
1418
  closeButtonClassName: "!h-10 !w-10 !rounded-none !border-0 !bg-transparent !p-0 !text-[var(--color-text)] hover:!bg-transparent hover:!text-[var(--color-text)]",
1373
1419
  contentClassName: "!rounded-none !bg-transparent !px-6 !pb-6 !pt-2",
1374
- children: /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("div", { className: "flex flex-col gap-4", children: [
1375
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(SharePreviewCard, { target, url: absoluteUrl || url }),
1376
- /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("div", { className: "grid grid-cols-3 gap-1 rounded-3xl bg-[var(--color-surface)] p-3 sm:grid-cols-4", children: [
1377
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
1420
+ children: /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { className: "flex flex-col gap-4", children: [
1421
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(SharePreviewCard, { target, url: absoluteUrl || url }),
1422
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { className: "grid grid-cols-3 gap-1 rounded-3xl bg-[var(--color-surface)] p-3 sm:grid-cols-4", children: [
1423
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
1378
1424
  ShareDestinationButton,
1379
1425
  {
1380
1426
  label: "Copy Link",
1381
- icon: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_lucide_react9.Link2, { "aria-hidden": "true", className: "h-6 w-6" }),
1427
+ icon: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_lucide_react9.Link2, { "aria-hidden": "true", className: "h-6 w-6" }),
1382
1428
  onClick: () => void handleCopy(),
1383
1429
  ariaLabel: "Copy link",
1384
1430
  disabled: !absoluteUrl
1385
1431
  }
1386
1432
  ),
1387
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
1433
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
1388
1434
  ShareDestinationButton,
1389
1435
  {
1390
1436
  label: "WhatsApp",
1391
- icon: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_fa3.FaWhatsapp, { "aria-hidden": "true", className: "h-9 w-9 text-[#25D366]" }),
1437
+ icon: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_fa3.FaWhatsapp, { "aria-hidden": "true", className: "h-9 w-9 text-[#25D366]" }),
1392
1438
  href: destinationUrls?.whatsapp,
1393
1439
  ariaLabel: `Share ${target.name} via WhatsApp`,
1394
1440
  disabled: !absoluteUrl
1395
1441
  }
1396
1442
  ),
1397
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
1443
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
1398
1444
  ShareDestinationButton,
1399
1445
  {
1400
1446
  label: "Gmail",
1401
- icon: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_si.SiGmail, { "aria-hidden": "true", className: "h-8 w-8 text-[#EA4335]" }),
1447
+ icon: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_si.SiGmail, { "aria-hidden": "true", className: "h-8 w-8 text-[#EA4335]" }),
1402
1448
  href: destinationUrls?.gmail,
1403
1449
  ariaLabel: `Email ${target.name} with Gmail`,
1404
1450
  disabled: !absoluteUrl
1405
1451
  }
1406
1452
  ),
1407
- canUseNativeShare && /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
1453
+ canUseNativeShare && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
1408
1454
  ShareDestinationButton,
1409
1455
  {
1410
1456
  label: "More",
1411
- icon: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_lucide_react9.Share2, { "aria-hidden": "true", className: "h-6 w-6" }),
1457
+ icon: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_lucide_react9.Share2, { "aria-hidden": "true", className: "h-6 w-6" }),
1412
1458
  onClick: () => void handleNativeShare(),
1413
1459
  ariaLabel: "More sharing options",
1414
1460
  disabled: !absoluteUrl
1415
1461
  }
1416
1462
  )
1417
1463
  ] }),
1418
- copyStatus === "failed" && /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("p", { role: "alert", className: "text-center text-sm text-[var(--color-red-500)]", children: "Could not copy the link." }),
1419
- shareError && /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("p", { role: "alert", className: "text-center text-sm text-[var(--color-red-500)]", children: shareError })
1464
+ copyStatus === "failed" && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("p", { role: "alert", className: "text-center text-sm text-[var(--color-red-500)]", children: "Could not copy the link." }),
1465
+ shareError && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("p", { role: "alert", className: "text-center text-sm text-[var(--color-red-500)]", children: shareError })
1420
1466
  ] })
1421
1467
  }
1422
1468
  ),
1423
- toastId > 0 && /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(Toast, { message: "Link copied", onClose: dismissToast }, toastId)
1469
+ toastId > 0 && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(Toast, { message: "Link copied", onClose: dismissToast }, toastId)
1424
1470
  ] });
1425
1471
  }
1426
1472
 
1427
1473
  // src/genre-tree/ShareActionButton.tsx
1428
- var import_jsx_runtime24 = require("react/jsx-runtime");
1474
+ var import_jsx_runtime25 = require("react/jsx-runtime");
1429
1475
  function ShareActionButton({ target, url }) {
1430
1476
  const { showPopup, hidePopup } = (0, import_popup4.usePopup)();
1431
1477
  const openSharePopup = () => {
1432
- showPopup(/* @__PURE__ */ (0, import_jsx_runtime24.jsx)(SharePopup, { target, url, onClose: hidePopup }));
1478
+ showPopup(/* @__PURE__ */ (0, import_jsx_runtime25.jsx)(SharePopup, { target, url, onClose: hidePopup }));
1433
1479
  };
1434
- return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
1480
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
1435
1481
  "button",
1436
1482
  {
1437
1483
  type: "button",
@@ -1440,7 +1486,7 @@ function ShareActionButton({ target, url }) {
1440
1486
  "aria-label": `Share ${target.name}`,
1441
1487
  title: `Share ${target.name}`,
1442
1488
  children: [
1443
- /* @__PURE__ */ (0, import_jsx_runtime24.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: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_lucide_react10.Share2, { "aria-hidden": "true", className: "w-5 h-5" }) }),
1489
+ /* @__PURE__ */ (0, import_jsx_runtime25.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: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_lucide_react10.Share2, { "aria-hidden": "true", className: "w-5 h-5" }) }),
1444
1490
  "Share"
1445
1491
  ]
1446
1492
  }
@@ -1448,7 +1494,7 @@ function ShareActionButton({ target, url }) {
1448
1494
  }
1449
1495
 
1450
1496
  // src/genre-tree/GenreActionBand.tsx
1451
- var import_jsx_runtime25 = require("react/jsx-runtime");
1497
+ var import_jsx_runtime26 = require("react/jsx-runtime");
1452
1498
  function GenreActionBand({
1453
1499
  node,
1454
1500
  onPlayPause,
@@ -1470,7 +1516,7 @@ function GenreActionBand({
1470
1516
  items.push({
1471
1517
  key: "play-pause",
1472
1518
  label: isPlaying ? "Pause" : "Play",
1473
- icon: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_lucide_react11.Pause, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_lucide_react11.Play, { "aria-hidden": "true", className: "w-5 h-5" }),
1519
+ icon: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react11.Pause, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react11.Play, { "aria-hidden": "true", className: "w-5 h-5" }),
1474
1520
  onClick: onPlayPause
1475
1521
  });
1476
1522
  }
@@ -1478,24 +1524,24 @@ function GenreActionBand({
1478
1524
  items.push({
1479
1525
  key: "zoom-in",
1480
1526
  label: "Focus",
1481
- icon: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_lucide_react11.ZoomIn, { "aria-hidden": "true", className: "w-5 h-5" }),
1527
+ icon: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react11.ZoomIn, { "aria-hidden": "true", className: "w-5 h-5" }),
1482
1528
  onClick: onZoomIn
1483
1529
  });
1484
1530
  }
1485
1531
  if (onAddChild) {
1486
- adminItems.push({ key: "add-child", label: "Add", icon: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_lucide_react11.Plus, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onAddChild });
1532
+ adminItems.push({ key: "add-child", label: "Add", icon: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react11.Plus, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onAddChild });
1487
1533
  }
1488
1534
  if (onRename) {
1489
- adminItems.push({ key: "rename", label: "Rename", icon: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_lucide_react11.Pencil, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onRename });
1535
+ adminItems.push({ key: "rename", label: "Rename", icon: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react11.Pencil, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onRename });
1490
1536
  }
1491
1537
  if (onChangeParent) {
1492
- adminItems.push({ key: "change-parent", label: "Parent", icon: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_lucide_react11.MoveUpRight, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onChangeParent });
1538
+ adminItems.push({ key: "change-parent", label: "Parent", icon: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react11.MoveUpRight, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onChangeParent });
1493
1539
  }
1494
1540
  if (onToggleSide) {
1495
1541
  adminItems.push({
1496
1542
  key: "toggle-side",
1497
1543
  label: side === "pop" ? "Make core" : "Make pop",
1498
- icon: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_lucide_react11.ArrowLeftRight, { "aria-hidden": "true", className: "w-5 h-5" }),
1544
+ icon: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react11.ArrowLeftRight, { "aria-hidden": "true", className: "w-5 h-5" }),
1499
1545
  onClick: onToggleSide,
1500
1546
  disabled: !!toggleSideDisabledReason,
1501
1547
  title: toggleSideDisabledReason
@@ -1511,7 +1557,7 @@ function GenreActionBand({
1511
1557
  });
1512
1558
  }
1513
1559
  if (items.length === 0 && (!shareTarget || !shareUrl) && adminItems.length === 0) return null;
1514
- const renderItems = (list) => list.map((item) => /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
1560
+ const renderItems = (list) => list.map((item) => /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(
1515
1561
  "button",
1516
1562
  {
1517
1563
  type: "button",
@@ -1520,25 +1566,25 @@ function GenreActionBand({
1520
1566
  onClick: item.onClick,
1521
1567
  className: "group flex flex-col items-center gap-1 w-16 text-xs text-center text-[var(--color-text)] focus-visible:outline-none disabled:opacity-50",
1522
1568
  children: [
1523
- /* @__PURE__ */ (0, import_jsx_runtime25.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)] group-disabled:group-hover:bg-[var(--color-surface-muted)]", children: item.icon }),
1569
+ /* @__PURE__ */ (0, import_jsx_runtime26.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)] group-disabled:group-hover:bg-[var(--color-surface-muted)]", children: item.icon }),
1524
1570
  item.label
1525
1571
  ]
1526
1572
  },
1527
1573
  item.key
1528
1574
  ));
1529
- return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: [
1530
- (items.length > 0 || shareTarget && shareUrl) && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: [
1575
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: [
1576
+ (items.length > 0 || shareTarget && shareUrl) && /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: [
1531
1577
  renderItems(items),
1532
- shareTarget && shareUrl && /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl })
1578
+ shareTarget && shareUrl && /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl })
1533
1579
  ] }),
1534
- adminItems.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
1580
+ adminItems.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(
1535
1581
  "section",
1536
1582
  {
1537
1583
  "aria-label": "Admin actions",
1538
1584
  className: `px-3.5 py-3 ${items.length > 0 ? "border-t border-[var(--color-border)]" : ""}`,
1539
1585
  children: [
1540
- /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("h3", { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-[var(--color-text-muted)]", children: "Admin" }),
1541
- /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("div", { className: "flex flex-wrap gap-1", children: renderItems(adminItems) })
1586
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("h3", { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-[var(--color-text-muted)]", children: "Admin" }),
1587
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { className: "flex flex-wrap gap-1", children: renderItems(adminItems) })
1542
1588
  ]
1543
1589
  }
1544
1590
  )
@@ -1547,23 +1593,23 @@ function GenreActionBand({
1547
1593
 
1548
1594
  // src/genre-tree/GenreDetailExtras.tsx
1549
1595
  var import_ui9 = require("@behindthemusictree/ui");
1550
- var import_genre_tree12 = require("@behindthemusictree/app-kit/genre-tree");
1596
+ var import_genre_tree17 = require("@behindthemusictree/app-kit/genre-tree");
1551
1597
 
1552
1598
  // src/genre-tree/ClampedText.tsx
1553
- var import_react10 = require("react");
1554
- var import_jsx_runtime26 = require("react/jsx-runtime");
1599
+ var import_react11 = require("react");
1600
+ var import_jsx_runtime27 = require("react/jsx-runtime");
1555
1601
  function ClampedText({ children, className: className2 = "" }) {
1556
- const ref = (0, import_react10.useRef)(null);
1557
- const [expanded, setExpanded] = (0, import_react10.useState)(false);
1558
- const [overflows, setOverflows] = (0, import_react10.useState)(false);
1559
- (0, import_react10.useLayoutEffect)(() => {
1602
+ const ref = (0, import_react11.useRef)(null);
1603
+ const [expanded, setExpanded] = (0, import_react11.useState)(false);
1604
+ const [overflows, setOverflows] = (0, import_react11.useState)(false);
1605
+ (0, import_react11.useLayoutEffect)(() => {
1560
1606
  const element = ref.current;
1561
1607
  if (!element || expanded) return;
1562
1608
  setOverflows(element.scrollHeight > element.clientHeight);
1563
1609
  }, [children, expanded]);
1564
- return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(import_jsx_runtime26.Fragment, { children: [
1565
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("p", { ref, className: `${className2} ${expanded ? "" : "line-clamp-3"}`, children }),
1566
- (overflows || expanded) && /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
1610
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(import_jsx_runtime27.Fragment, { children: [
1611
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("p", { ref, className: `${className2} ${expanded ? "" : "line-clamp-3"}`, children }),
1612
+ (overflows || expanded) && /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
1567
1613
  "button",
1568
1614
  {
1569
1615
  type: "button",
@@ -1577,7 +1623,7 @@ function ClampedText({ children, className: className2 = "" }) {
1577
1623
  }
1578
1624
 
1579
1625
  // src/genre-tree/GenrePlaylistTracks.tsx
1580
- var import_react11 = require("react");
1626
+ var import_react12 = require("react");
1581
1627
  var import_react_query = require("@tanstack/react-query");
1582
1628
  var import_zod = require("zod");
1583
1629
  var import_fa4 = require("react-icons/fa");
@@ -1590,22 +1636,23 @@ var import_player4 = require("@behindthemusictree/app-kit/player");
1590
1636
  var import_player5 = require("@behindthemusictree/app-kit/player");
1591
1637
  var import_popup5 = require("@behindthemusictree/app-kit/popup");
1592
1638
  var import_transport8 = require("@behindthemusictree/app-kit/transport");
1593
- var import_genre_tree7 = require("@behindthemusictree/app-kit/genre-tree");
1594
- var import_genre_tree8 = require("@behindthemusictree/app-kit/genre-tree");
1595
- var import_genre_tree9 = require("@behindthemusictree/app-kit/genre-tree");
1596
- var import_genre_tree10 = require("@behindthemusictree/app-kit/genre-tree");
1639
+ var import_genre_tree11 = require("@behindthemusictree/app-kit/genre-tree");
1640
+ var import_genre_tree12 = require("@behindthemusictree/app-kit/genre-tree");
1641
+ var import_genre_tree13 = require("@behindthemusictree/app-kit/genre-tree");
1642
+ var import_genre_tree14 = require("@behindthemusictree/app-kit/genre-tree");
1643
+ var import_genre_tree15 = require("@behindthemusictree/app-kit/genre-tree");
1597
1644
 
1598
1645
  // src/genre-tree/MusicBrainzRecordingLink.tsx
1599
- var import_genre_tree6 = require("@behindthemusictree/app-kit/genre-tree");
1600
- var import_jsx_runtime27 = require("react/jsx-runtime");
1646
+ var import_genre_tree10 = require("@behindthemusictree/app-kit/genre-tree");
1647
+ var import_jsx_runtime28 = require("react/jsx-runtime");
1601
1648
  function MusicBrainzRecordingLink({
1602
1649
  song,
1603
1650
  children,
1604
1651
  className: className2 = ""
1605
1652
  }) {
1606
- const url = (0, import_genre_tree6.musicBrainzRecordingUrl)(song);
1607
- if (!url) return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_jsx_runtime27.Fragment, { children });
1608
- return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
1653
+ const url = (0, import_genre_tree10.musicBrainzRecordingUrl)(song);
1654
+ if (!url) return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_jsx_runtime28.Fragment, { children });
1655
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
1609
1656
  "a",
1610
1657
  {
1611
1658
  href: url,
@@ -1620,11 +1667,11 @@ function MusicBrainzRecordingLink({
1620
1667
  }
1621
1668
 
1622
1669
  // src/genre-tree/GenrePlaylistTracks.tsx
1623
- var import_genre_tree11 = require("@behindthemusictree/app-kit/genre-tree");
1670
+ var import_genre_tree16 = require("@behindthemusictree/app-kit/genre-tree");
1624
1671
  var import_track_noun2 = require("@behindthemusictree/app-kit/track-noun");
1625
- var import_jsx_runtime28 = require("react/jsx-runtime");
1672
+ var import_jsx_runtime29 = require("react/jsx-runtime");
1626
1673
  var CATALOGUE_PAGE_SIZE = 100;
1627
- var SongPageSchema = (0, import_transport6.PaginatedResponseSchema)(import_genre_tree9.SongDetailedSchema).extend({
1674
+ var SongPageSchema = (0, import_transport6.PaginatedResponseSchema)(import_genre_tree14.SongDetailedSchema).extend({
1628
1675
  overallTotal: import_zod.z.number().nullable(),
1629
1676
  page: import_zod.z.number().nullable(),
1630
1677
  totalPages: import_zod.z.number().nullable()
@@ -1642,6 +1689,7 @@ var nextCursor = (next) => {
1642
1689
  function GenrePlaylistTracks({
1643
1690
  genreUuid,
1644
1691
  genrePlaylist,
1692
+ selection,
1645
1693
  scope,
1646
1694
  getBackendBaseUrl,
1647
1695
  renderArtistLink,
@@ -1651,7 +1699,7 @@ function GenrePlaylistTracks({
1651
1699
  renderTracksToolbar,
1652
1700
  artist = null
1653
1701
  }) {
1654
- const { fetchGenrePlaylistTracksPage, playNewTrackListFromGenrePlaylist, playNewTrackListFromArtist } = (0, import_genre_tree10.useTrackList)();
1702
+ const { fetchGenrePlaylistTracksPage, playNewTrackListFromGenrePlaylist, playNewTrackListFromArtist, playNewTrackListFromGenreCatalogue } = (0, import_genre_tree15.useTrackList)();
1655
1703
  const { session, sessionRestored } = (0, import_auth2.useSession)();
1656
1704
  const { playerTrackObject, playState, handlePlayPauseAction } = (0, import_player4.usePlayer)();
1657
1705
  const { showPopup } = (0, import_popup5.usePopup)();
@@ -1660,15 +1708,15 @@ function GenrePlaylistTracks({
1660
1708
  const { fetch } = (0, import_transport5.useFetchWrapper)(getBackendBaseUrl);
1661
1709
  const playback = (0, import_transport5.usePlaybackContext)();
1662
1710
  const { data, isPending, isError, refetch, hasNextPage, isFetchingNextPage, fetchNextPage } = (0, import_react_query.useInfiniteQuery)({
1663
- queryKey: [...scope === "me" ? import_genre_tree7.genrePlaylistQueryKeys.me.tracks(uuid) : artist ? [...import_genre_tree8.libraryQueryKeys.reference.songs.byGenre(getBackendBaseUrl(), genreUuid), "artist", artist.uuid] : import_genre_tree8.libraryQueryKeys.reference.songs.byGenre(getBackendBaseUrl(), genreUuid), ...playback.key ? [playback.key] : []],
1711
+ queryKey: [...scope === "me" ? import_genre_tree12.genrePlaylistQueryKeys.me.tracks(uuid) : artist ? [...import_genre_tree13.libraryQueryKeys.reference.songs.byGenre(getBackendBaseUrl(), genreUuid, selection), "artist", artist.uuid] : import_genre_tree13.libraryQueryKeys.reference.songs.byGenre(getBackendBaseUrl(), genreUuid, selection), ...playback.key ? [playback.key] : []],
1664
1712
  queryFn: async ({ pageParam }) => {
1665
1713
  if (scope === "me") return fetchGenrePlaylistTracksPage(uuid, scope, Number(pageParam));
1666
1714
  const response = await fetch(
1667
- import_genre_tree8.libraryEndpoints.reference.songs.list(),
1715
+ import_genre_tree13.libraryEndpoints.reference.songs.list(),
1668
1716
  true,
1669
1717
  false,
1670
1718
  {},
1671
- { genre: genreUuid, ...artist && { artist: artist.uuid }, pageSize: CATALOGUE_PAGE_SIZE, withTotal: false, ...pageParam && { cursor: pageParam } }
1719
+ { genre: genreUuid, ...(0, import_genre_tree11.genreMusicQueryParams)(selection), ...artist && { artist: artist.uuid }, pageSize: CATALOGUE_PAGE_SIZE, withTotal: false, ...pageParam && { cursor: pageParam } }
1672
1720
  );
1673
1721
  const parsed = (0, import_transport7.parseWithLog)(SongPageSchema, response, "GenrePlaylistTracks");
1674
1722
  return { tracks: parsed.results, total: parsed.overallTotal, nextPage: parsed.next ? nextCursor(parsed.next) : null };
@@ -1677,8 +1725,8 @@ function GenrePlaylistTracks({
1677
1725
  getNextPageParam: (lastPage) => lastPage.nextPage ?? void 0,
1678
1726
  enabled: playback.ready && (scope !== "me" || sessionRestored && !!session?.accessToken)
1679
1727
  });
1680
- const [sentinel, setSentinel] = (0, import_react11.useState)(null);
1681
- (0, import_react11.useEffect)(() => {
1728
+ const [sentinel, setSentinel] = (0, import_react12.useState)(null);
1729
+ (0, import_react12.useEffect)(() => {
1682
1730
  if (!hasNextPage || isFetchingNextPage || !sentinel) return;
1683
1731
  const observer = new IntersectionObserver((entries) => {
1684
1732
  if (entries.some((entry) => entry.isIntersecting)) {
@@ -1692,38 +1740,43 @@ function GenrePlaylistTracks({
1692
1740
  const tracks = pages.flatMap((page) => page.tracks);
1693
1741
  const total = pages[0]?.total ?? void 0;
1694
1742
  const play = (startSongUuid) => {
1695
- const started = artist ? playNewTrackListFromArtist(artist, scope, { startSongUuid, ordering: "title", genreUuid }) : playNewTrackListFromGenrePlaylist(genrePlaylist, scope, { startSongUuid });
1743
+ const started = artist ? playNewTrackListFromArtist(artist, scope, {
1744
+ startSongUuid,
1745
+ ordering: "title",
1746
+ genreUuid,
1747
+ ...selection && { selection }
1748
+ }) : scope === "reference" ? playNewTrackListFromGenreCatalogue({ uuid: genreUuid, name: genrePlaylist.name }, scope, { startSongUuid, ...selection && { selection } }) : playNewTrackListFromGenrePlaylist(genrePlaylist, scope, { startSongUuid });
1696
1749
  started.catch((error) => {
1697
1750
  console.error("Failed to play genre playlist:", error);
1698
- showPopup(/* @__PURE__ */ (0, import_jsx_runtime28.jsx)(InternalErrorPopup, { errorCode: import_transport8.ErrorCode.CLIENT_UNKNOWN }));
1751
+ showPopup(/* @__PURE__ */ (0, import_jsx_runtime29.jsx)(InternalErrorPopup, { errorCode: import_transport8.ErrorCode.CLIENT_UNKNOWN }));
1699
1752
  });
1700
1753
  };
1701
- return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_ui8.PanelSection, { title: `${(0, import_track_noun2.capitalize)(trackNoun.plural)}${total !== void 0 ? ` (${total})` : ""}`, children: /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("div", { children: [
1754
+ return /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_ui8.PanelSection, { title: `${(0, import_track_noun2.capitalize)(trackNoun.plural)}${total !== void 0 ? ` (${total})` : ""}`, children: /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { children: [
1702
1755
  renderTracksToolbar?.(),
1703
- isPending ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_ui8.Skeleton, { className: "h-4 w-3/4", "aria-busy": "true" }) : isError && tracks.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("p", { role: "alert", children: [
1756
+ isPending ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_ui8.Skeleton, { className: "h-4 w-3/4", "aria-busy": "true" }) : isError && tracks.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("p", { role: "alert", children: [
1704
1757
  "Couldn't load ",
1705
1758
  trackNoun.plural,
1706
1759
  ".",
1707
1760
  " ",
1708
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("button", { type: "button", className: "underline", onClick: () => void refetch(), children: "Retry" })
1709
- ] }) : tracks.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("p", { children: "\u2014" }) : /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("ul", { className: "mt-1 max-h-64 overflow-y-auto text-[0.8125rem] leading-[1.45] [container-type:inline-size]", "aria-label": `${genrePlaylist.name} ${trackNoun.plural}`, children: [
1761
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("button", { type: "button", className: "underline", onClick: () => void refetch(), children: "Retry" })
1762
+ ] }) : tracks.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("p", { children: "\u2014" }) : /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("ul", { className: "mt-1 max-h-64 overflow-y-auto text-[0.8125rem] leading-[1.45] [container-type:inline-size]", "aria-label": `${genrePlaylist.name} ${trackNoun.plural}`, children: [
1710
1763
  tracks.map((track, index) => {
1711
1764
  const label = track.artists?.length ? `${track.title} \u2014 ${track.artists.map((artist2) => artist2.name).join(", ")}` : track.title;
1712
1765
  const isCurrent = playerTrackObject?.track.id === track.uuid;
1713
1766
  const isPlaying = isCurrent && playState === import_player5.PlayStates.PLAYING;
1714
- const text = /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("span", { className: TEXT_CELL, children: [
1715
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { className: TITLE_CELL, children: renderSongLink ? renderSongLink(track) : (0, import_genre_tree9.isSong)(track) ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }),
1716
- track.artists?.length ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { className: SEPARATOR, children: " \u2014 " }) : null,
1717
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { className: COLUMN_CELL, children: track.artists?.length ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_genre_tree11.ArtistNames, { artists: track.artists, renderArtistLink }) : null }),
1718
- track.album && /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { className: ALBUM_CELL, children: track.album.name })
1767
+ const text = /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("span", { className: TEXT_CELL, children: [
1768
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("span", { className: TITLE_CELL, children: renderSongLink ? renderSongLink(track) : (0, import_genre_tree14.isSong)(track) ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }),
1769
+ track.artists?.length ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("span", { className: SEPARATOR, children: " \u2014 " }) : null,
1770
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("span", { className: COLUMN_CELL, children: track.artists?.length ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_genre_tree16.ArtistNames, { artists: track.artists, renderArtistLink }) : null }),
1771
+ track.album && /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("span", { className: ALBUM_CELL, children: track.album.name })
1719
1772
  ] });
1720
1773
  const leading = renderSongLeading?.(track, index);
1721
1774
  const actions = renderSongActions?.(track);
1722
- const isUnplayable = (0, import_genre_tree9.isSong)(track) && (0, import_genre_tree9.songUnplayableReason)(track) !== null;
1775
+ const isUnplayable = (0, import_genre_tree14.isSong)(track) && (0, import_genre_tree14.songUnplayableReason)(track) !== null;
1723
1776
  if (isUnplayable) {
1724
- return /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("li", { className: "flex min-h-8 items-center gap-1 px-1 opacity-50", children: [
1777
+ return /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("li", { className: "flex min-h-8 items-center gap-1 px-1 opacity-50", children: [
1725
1778
  leading,
1726
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
1779
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
1727
1780
  "button",
1728
1781
  {
1729
1782
  type: "button",
@@ -1731,27 +1784,27 @@ function GenrePlaylistTracks({
1731
1784
  "aria-disabled": "true",
1732
1785
  "aria-label": label,
1733
1786
  className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full opacity-0 cursor-not-allowed",
1734
- children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_fa4.FaPlay, { size: 10 })
1787
+ children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_fa4.FaPlay, { size: 10 })
1735
1788
  }
1736
1789
  ),
1737
1790
  text,
1738
1791
  actions
1739
1792
  ] }, track.uuid);
1740
1793
  }
1741
- return /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(
1794
+ return /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
1742
1795
  "li",
1743
1796
  {
1744
1797
  className: `group flex min-h-8 items-center gap-1 rounded px-1 hover:bg-[var(--color-neutral-100)] ${isCurrent ? "font-medium text-[var(--color-primary)]" : ""}`,
1745
1798
  children: [
1746
1799
  leading,
1747
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
1800
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
1748
1801
  "button",
1749
1802
  {
1750
1803
  type: "button",
1751
1804
  "aria-label": `${isPlaying ? "Pause" : "Play"} ${label}`,
1752
1805
  className: `flex h-6 w-6 shrink-0 items-center justify-center rounded-full cursor-pointer group-hover:opacity-100 focus-visible:opacity-100 ${isCurrent && playState !== import_player5.PlayStates.STOPPED ? "opacity-100" : "opacity-0 [@media(pointer:coarse)]:opacity-100"}`,
1753
1806
  onClick: () => isCurrent ? handlePlayPauseAction() : play(track.uuid),
1754
- children: isCurrent && playState === import_player5.PlayStates.LOADING ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_ui8.RingLoader, { size: 10 }) : isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_fa4.FaPause, { size: 10 }) : /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_fa4.FaPlay, { size: 10 })
1807
+ children: isCurrent && playState === import_player5.PlayStates.LOADING ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_ui8.RingLoader, { size: 10 }) : isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_fa4.FaPause, { size: 10 }) : /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_fa4.FaPlay, { size: 10 })
1755
1808
  }
1756
1809
  ),
1757
1810
  text,
@@ -1761,7 +1814,7 @@ function GenrePlaylistTracks({
1761
1814
  track.uuid
1762
1815
  );
1763
1816
  }),
1764
- hasNextPage && /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("li", { ref: setSentinel, "aria-hidden": "true", className: "h-4" })
1817
+ hasNextPage && /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("li", { ref: setSentinel, "aria-hidden": "true", className: "h-4" })
1765
1818
  ] })
1766
1819
  ] }) });
1767
1820
  }
@@ -1769,10 +1822,11 @@ function GenrePlaylistTracks({
1769
1822
  // src/genre-tree/GenreDetailExtras.tsx
1770
1823
  var import_track_noun3 = require("@behindthemusictree/app-kit/track-noun");
1771
1824
  var import_si2 = require("react-icons/si");
1772
- var import_jsx_runtime29 = require("react/jsx-runtime");
1825
+ var import_jsx_runtime30 = require("react/jsx-runtime");
1773
1826
  function GenreDetailExtras({
1774
1827
  genreUuid,
1775
1828
  genrePlaylist,
1829
+ selection,
1776
1830
  wikidataId,
1777
1831
  scope,
1778
1832
  getBackendBaseUrl,
@@ -1786,24 +1840,24 @@ function GenreDetailExtras({
1786
1840
  ...trackSlots
1787
1841
  }) {
1788
1842
  const trackNoun = (0, import_track_noun3.useTrackNoun)();
1789
- const { data: overview, isPending } = (0, import_genre_tree12.useFetchGenreOverview)(
1843
+ const { data: overview, isPending } = (0, import_genre_tree17.useFetchGenreOverview)(
1790
1844
  genreUuid,
1791
1845
  scope,
1792
1846
  getBackendBaseUrl,
1793
1847
  criteriaOverviewSchema
1794
1848
  );
1795
1849
  if (isPending) {
1796
- return /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(import_jsx_runtime29.Fragment, { children: [
1797
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_ui9.PanelSection, { title: "Summary", children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { "aria-busy": "true", "data-testid": "genre-detail-extras-skeleton", children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_ui9.Skeleton, { className: "h-4 w-3/4" }) }) }),
1850
+ return /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(import_jsx_runtime30.Fragment, { children: [
1851
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui9.PanelSection, { title: "Summary", children: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { "aria-busy": "true", "data-testid": "genre-detail-extras-skeleton", children: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui9.Skeleton, { className: "h-4 w-3/4" }) }) }),
1798
1852
  family
1799
1853
  ] });
1800
1854
  }
1801
- if (!overview) return /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_jsx_runtime29.Fragment, { children: family });
1855
+ if (!overview) return /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_jsx_runtime30.Fragment, { children: family });
1802
1856
  const { summary, summarySourceUrl, essentialSongs } = overview;
1803
1857
  const wikidataUrl = wikidataId ? `https://www.wikidata.org/wiki/${encodeURIComponent(wikidataId)}` : null;
1804
- return /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(import_jsx_runtime29.Fragment, { children: [
1805
- (summarySourceUrl || wikidataUrl) && /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_ui9.PanelSection, { title: "Links", children: /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { className: "flex flex-wrap gap-x-3 gap-y-1 text-xs text-[var(--color-text)]", children: [
1806
- summarySourceUrl && /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
1858
+ return /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(import_jsx_runtime30.Fragment, { children: [
1859
+ (summarySourceUrl || wikidataUrl) && /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui9.PanelSection, { title: "Links", children: /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { className: "flex flex-wrap gap-x-3 gap-y-1 text-xs text-[var(--color-text)]", children: [
1860
+ summarySourceUrl && /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(
1807
1861
  "a",
1808
1862
  {
1809
1863
  href: summarySourceUrl,
@@ -1811,12 +1865,12 @@ function GenreDetailExtras({
1811
1865
  rel: "noopener noreferrer",
1812
1866
  className: "inline-flex items-center gap-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text)]",
1813
1867
  children: [
1814
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_si2.SiWikipedia, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1868
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_si2.SiWikipedia, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1815
1869
  "Wikipedia"
1816
1870
  ]
1817
1871
  }
1818
1872
  ),
1819
- wikidataUrl && /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
1873
+ wikidataUrl && /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(
1820
1874
  "a",
1821
1875
  {
1822
1876
  href: wikidataUrl,
@@ -1824,21 +1878,22 @@ function GenreDetailExtras({
1824
1878
  rel: "noopener noreferrer",
1825
1879
  className: "inline-flex items-center gap-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text)]",
1826
1880
  children: [
1827
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_si2.SiWikidata, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1881
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_si2.SiWikidata, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
1828
1882
  "Wikidata"
1829
1883
  ]
1830
1884
  }
1831
1885
  )
1832
1886
  ] }) }),
1833
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_ui9.PanelSection, { title: "Summary", children: renderSummary ? renderSummary(overview) : /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(ClampedText, { className: "text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: summary ?? "\u2014" }, genreUuid) }),
1887
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui9.PanelSection, { title: "Summary", children: renderSummary ? renderSummary(overview) : /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ClampedText, { className: "text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: summary ?? "\u2014" }, genreUuid) }),
1834
1888
  family,
1835
1889
  renderGenreDetailExtras?.(overview),
1836
- (renderEssentialTracks || essentialSongs) && /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_ui9.PanelSection, { title: `Essential ${trackNoun.plural}`, children: renderEssentialTracks ? renderEssentialTracks(overview) : essentialSongs && essentialSongs.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("ul", { className: "text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: essentialSongs.map((track) => /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("li", { className: "py-1", children: track.title }, track.uuid)) }) : /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("p", { className: "text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: "\u2014" }) }),
1837
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
1890
+ (renderEssentialTracks || essentialSongs) && /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_ui9.PanelSection, { title: `Essential ${trackNoun.plural}`, children: renderEssentialTracks ? renderEssentialTracks(overview) : essentialSongs && essentialSongs.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("ul", { className: "text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: essentialSongs.map((track) => /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("li", { className: "py-1", children: track.title }, track.uuid)) }) : /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("p", { className: "text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: "\u2014" }) }),
1891
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
1838
1892
  GenrePlaylistTracks,
1839
1893
  {
1840
1894
  genreUuid,
1841
1895
  genrePlaylist,
1896
+ selection,
1842
1897
  scope,
1843
1898
  getBackendBaseUrl,
1844
1899
  renderArtistLink,
@@ -1850,8 +1905,8 @@ function GenreDetailExtras({
1850
1905
  }
1851
1906
 
1852
1907
  // src/genre-tree/GenreInfoPanelBody.tsx
1853
- var import_jsx_runtime30 = require("react/jsx-runtime");
1854
- var import_react12 = require("react");
1908
+ var import_jsx_runtime31 = require("react/jsx-runtime");
1909
+ var import_react13 = require("react");
1855
1910
  function GenreNodeActionBand({ genrePlaylist, playback, ...bandProps }) {
1856
1911
  const { isPlaying, setIsPlaying } = (0, import_player6.usePlayer)();
1857
1912
  const isCurrentTrackList = playback.playingNodeId === genrePlaylist.uuid;
@@ -1864,11 +1919,11 @@ function GenreNodeActionBand({ genrePlaylist, playback, ...bandProps }) {
1864
1919
  console.error("Failed to load genre playlist tracks:", error);
1865
1920
  });
1866
1921
  };
1867
- return /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
1922
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
1868
1923
  GenreActionBand,
1869
1924
  {
1870
1925
  ...bandProps,
1871
- onPlayPause: genrePlaylist.tracksCount > 0 ? handlePlayPause : void 0,
1926
+ onPlayPause: ("genreOnlyTracksCount" in genrePlaylist ? (0, import_genre_tree18.genreMusicCount)(genrePlaylist) : genrePlaylist.tracksCount) > 0 ? handlePlayPause : void 0,
1872
1927
  isPlaying: isCurrentTrackList && isPlaying
1873
1928
  }
1874
1929
  );
@@ -1878,6 +1933,7 @@ function GenreInfoPanelBody({
1878
1933
  family,
1879
1934
  genreUuid,
1880
1935
  genrePlaylist,
1936
+ catalogue,
1881
1937
  wikidataId,
1882
1938
  scope,
1883
1939
  getBackendBaseUrl,
@@ -1885,10 +1941,31 @@ function GenreInfoPanelBody({
1885
1941
  band,
1886
1942
  extras
1887
1943
  }) {
1888
- return /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(import_jsx_runtime30.Fragment, { children: [
1944
+ const noun = (0, import_track_noun4.useTrackNoun)();
1945
+ const summary = scope === "reference" ? import_genre_tree18.GenreCatalogueCountsSchema.parse(genrePlaylist) : void 0;
1946
+ const selection = summary ? (0, import_genre_tree18.defaultGenreMusicSelection)(summary) : void 0;
1947
+ const describeCounts = (counts) => {
1948
+ const scopes = counts.genreAndSubgenresTracksCount;
1949
+ return "canonical" in scopes ? `${counts.genreOnlyTracksCount} this genre only \xB7 ${scopes.canonical} with canonical subgenres \xB7 ${scopes.regional} with regional subgenres \xB7 ${scopes.all} with all subgenres` : `${counts.genreOnlyTracksCount} this genre only \xB7 ${scopes.all} with subgenres`;
1950
+ };
1951
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(import_jsx_runtime31.Fragment, { children: [
1889
1952
  toolbar,
1890
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(GenreNodeActionBand, { ...band, genrePlaylist, playback }),
1891
- /* @__PURE__ */ (0, import_react12.createElement)(
1953
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(GenreNodeActionBand, { ...band, genrePlaylist, playback }),
1954
+ summary && catalogue && /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { className: "px-3.5 py-3 text-sm text-[var(--color-text)]", children: [
1955
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("p", { children: [
1956
+ "Catalogue ",
1957
+ noun.plural,
1958
+ ": ",
1959
+ describeCounts(catalogue)
1960
+ ] }),
1961
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("p", { children: [
1962
+ "Playlist ",
1963
+ noun.plural,
1964
+ ": ",
1965
+ describeCounts(summary)
1966
+ ] })
1967
+ ] }),
1968
+ /* @__PURE__ */ (0, import_react13.createElement)(
1892
1969
  GenreDetailExtras,
1893
1970
  {
1894
1971
  ...extras,
@@ -1896,6 +1973,7 @@ function GenreInfoPanelBody({
1896
1973
  family,
1897
1974
  genreUuid,
1898
1975
  genrePlaylist,
1976
+ selection,
1899
1977
  wikidataId,
1900
1978
  scope,
1901
1979
  getBackendBaseUrl
@@ -1905,9 +1983,9 @@ function GenreInfoPanelBody({
1905
1983
  }
1906
1984
 
1907
1985
  // src/genre-tree/ChangeParentPopup.tsx
1908
- var import_react13 = require("react");
1909
- var import_genre_tree13 = require("@behindthemusictree/app-kit/genre-tree");
1910
- var import_jsx_runtime31 = require("react/jsx-runtime");
1986
+ var import_react14 = require("react");
1987
+ var import_genre_tree19 = require("@behindthemusictree/app-kit/genre-tree");
1988
+ var import_jsx_runtime32 = require("react/jsx-runtime");
1911
1989
  function ChangeParentPopup({
1912
1990
  genrePlaylist,
1913
1991
  genrePlaylists,
@@ -1916,9 +1994,9 @@ function ChangeParentPopup({
1916
1994
  onClose,
1917
1995
  onParentChanged
1918
1996
  }) {
1919
- const { mutate, isPending, formErrors } = (0, import_genre_tree13.useUpdateGenre)(scope, getBackendBaseUrl);
1920
- const candidates = (0, import_react13.useMemo)(
1921
- () => (0, import_genre_tree13.getReparentCandidates)(genrePlaylists, genrePlaylist.uuid),
1997
+ const { mutate, isPending, formErrors } = (0, import_genre_tree19.useUpdateGenre)(scope, getBackendBaseUrl);
1998
+ const candidates = (0, import_react14.useMemo)(
1999
+ () => (0, import_genre_tree19.getReparentCandidates)(genrePlaylists, genrePlaylist.uuid),
1922
2000
  [genrePlaylists, genrePlaylist.uuid]
1923
2001
  );
1924
2002
  const handleSelect = (newParent) => {
@@ -1935,19 +2013,19 @@ function ChangeParentPopup({
1935
2013
  }
1936
2014
  );
1937
2015
  };
1938
- (0, import_react13.useEffect)(() => {
2016
+ (0, import_react14.useEffect)(() => {
1939
2017
  const handleKeyDown = (event) => {
1940
2018
  if (event.key === "Escape") onClose();
1941
2019
  };
1942
2020
  window.addEventListener("keydown", handleKeyDown);
1943
2021
  return () => window.removeEventListener("keydown", handleKeyDown);
1944
2022
  }, [onClose]);
1945
- return /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(BasePopup_default, { title: `Change parent of ${genrePlaylist.name}`, onClose, showCancelButton: true, fitContent: true, children: [
1946
- /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("p", { className: "mb-3 text-sm text-[var(--color-text-muted)]", children: [
2023
+ return /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(BasePopup_default, { title: `Change parent of ${genrePlaylist.name}`, onClose, showCancelButton: true, fitContent: true, children: [
2024
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("p", { className: "mb-3 text-sm text-[var(--color-text-muted)]", children: [
1947
2025
  "Current parent: ",
1948
2026
  genrePlaylist.parent?.name ?? "none"
1949
2027
  ] }),
1950
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("div", { "aria-busy": isPending, className: isPending ? "pointer-events-none opacity-60" : void 0, children: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
2028
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("div", { "aria-busy": isPending, className: isPending ? "pointer-events-none opacity-60" : void 0, children: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
1951
2029
  GenreSearch,
1952
2030
  {
1953
2031
  genrePlaylists: candidates,
@@ -1955,13 +2033,13 @@ function ChangeParentPopup({
1955
2033
  placeholder: "Search a new parent\u2026"
1956
2034
  }
1957
2035
  ) }),
1958
- formErrors.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("p", { role: "alert", className: "mt-3 text-sm text-[var(--color-red-500)]", children: formErrors.map(({ message }) => message).join(" ") })
2036
+ formErrors.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("p", { role: "alert", className: "mt-3 text-sm text-[var(--color-red-500)]", children: formErrors.map(({ message }) => message).join(" ") })
1959
2037
  ] });
1960
2038
  }
1961
2039
 
1962
2040
  // src/genre-tree/artist/useArtistPanel.tsx
1963
2041
  var import_ui15 = require("@behindthemusictree/ui");
1964
- var import_genre_tree18 = require("@behindthemusictree/app-kit/genre-tree");
2042
+ var import_genre_tree24 = require("@behindthemusictree/app-kit/genre-tree");
1965
2043
 
1966
2044
  // src/genre-tree/artist/ArtistHeader.tsx
1967
2045
  var import_ui11 = require("@behindthemusictree/ui");
@@ -1970,7 +2048,7 @@ var import_ui11 = require("@behindthemusictree/ui");
1970
2048
  var import_lucide_react12 = require("lucide-react");
1971
2049
  var import_si3 = require("react-icons/si");
1972
2050
  var import_ui10 = require("@behindthemusictree/ui");
1973
- var import_jsx_runtime32 = require("react/jsx-runtime");
2051
+ var import_jsx_runtime33 = require("react/jsx-runtime");
1974
2052
  var LINK_KINDS = {
1975
2053
  official: { label: "Official website", Icon: import_lucide_react12.Globe },
1976
2054
  wikipedia: { label: "Wikipedia", Icon: import_si3.SiWikipedia },
@@ -2007,7 +2085,7 @@ function artistLinks(artist) {
2007
2085
  return links;
2008
2086
  }
2009
2087
  function ArtistLinks({ links }) {
2010
- return /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("ul", { className: "flex flex-wrap gap-1.5", children: links.map(({ key, url, Icon, label }) => /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_ui10.Chip, { href: url, icon: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(Icon, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": "true" }), children: label }) }, key)) });
2088
+ return /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("ul", { className: "flex flex-wrap gap-1.5", children: links.map(({ key, url, Icon, label }) => /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_ui10.Chip, { href: url, icon: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(Icon, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": "true" }), children: label }) }, key)) });
2011
2089
  }
2012
2090
 
2013
2091
  // src/genre-tree/artist/formatYears.ts
@@ -2018,7 +2096,7 @@ function formatYears(beginYear, endYear, ended) {
2018
2096
  }
2019
2097
 
2020
2098
  // src/genre-tree/artist/ArtistHeader.tsx
2021
- var import_jsx_runtime33 = require("react/jsx-runtime");
2099
+ var import_jsx_runtime34 = require("react/jsx-runtime");
2022
2100
  var countryNames = new Intl.DisplayNames(["en"], { type: "region" });
2023
2101
  function ArtistHeader({ artist }) {
2024
2102
  const fields = [];
@@ -2028,35 +2106,35 @@ function ArtistHeader({ artist }) {
2028
2106
  const years = formatYears(artist.beginYear, artist.endYear, artist.ended);
2029
2107
  if (years) fields.push({ label: "Active", value: years });
2030
2108
  const links = artistLinks(artist);
2031
- if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(ArtistLinks, { links }) });
2109
+ if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(ArtistLinks, { links }) });
2032
2110
  if (fields.length === 0) return null;
2033
- return /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_ui11.FieldList, { items: fields });
2111
+ return /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_ui11.FieldList, { items: fields });
2034
2112
  }
2035
2113
 
2036
2114
  // src/genre-tree/artist/ArtistPhoto.tsx
2037
- var import_jsx_runtime34 = require("react/jsx-runtime");
2115
+ var import_jsx_runtime35 = require("react/jsx-runtime");
2038
2116
  function ExternalLink({ href, children }) {
2039
- return /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-2 hover:underline", children });
2117
+ return /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-2 hover:underline", children });
2040
2118
  }
2041
2119
  function ArtistPhoto({ image, name }) {
2042
- return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("figure", { className: "flex flex-col gap-1.5 px-6 py-2.5", children: [
2043
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("img", { src: image.url, alt: name, className: "h-64 w-full object-contain bg-[var(--color-surface-muted)] rounded" }),
2044
- /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("figcaption", { className: "text-xs text-[var(--color-text-muted)]", children: [
2120
+ return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("figure", { className: "flex flex-col gap-1.5 px-6 py-2.5", children: [
2121
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("img", { src: image.url, alt: name, className: "h-64 w-full object-contain bg-[var(--color-surface-muted)] rounded" }),
2122
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("figcaption", { className: "text-xs text-[var(--color-text-muted)]", children: [
2045
2123
  "Photo: ",
2046
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(ExternalLink, { href: image.pageUrl, children: image.author ?? "Wikimedia Commons" }),
2124
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(ExternalLink, { href: image.pageUrl, children: image.author ?? "Wikimedia Commons" }),
2047
2125
  ",",
2048
2126
  " ",
2049
- image.licenseUrl ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(ExternalLink, { href: image.licenseUrl, children: image.license }) : image.license
2127
+ image.licenseUrl ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(ExternalLink, { href: image.licenseUrl, children: image.license }) : image.license
2050
2128
  ] })
2051
2129
  ] });
2052
2130
  }
2053
2131
 
2054
2132
  // src/genre-tree/artist/ArtistMembers.tsx
2055
2133
  var import_ui12 = require("@behindthemusictree/ui");
2056
- var import_jsx_runtime35 = require("react/jsx-runtime");
2134
+ var import_jsx_runtime36 = require("react/jsx-runtime");
2057
2135
  function MembershipList({ title, memberships, renderArtistLink }) {
2058
2136
  if (memberships.length === 0) return null;
2059
- return /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_ui12.PanelGroup, { title, children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("ul", { children: memberships.map((membership, index) => {
2137
+ return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui12.PanelGroup, { title, children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("ul", { children: memberships.map((membership, index) => {
2060
2138
  const details = [
2061
2139
  formatYears(membership.beginYear, membership.endYear, membership.ended),
2062
2140
  ...membership.roles
@@ -2064,9 +2142,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
2064
2142
  const detailsText = details.filter(Boolean).join(" \xB7 ");
2065
2143
  return (
2066
2144
  // A member can have several periods in the same group.
2067
- /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("li", { className: "truncate py-0.5", title: detailsText || void 0, children: [
2068
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("span", { className: "font-medium text-[var(--color-text)]", children: renderArtistLink(membership.artist) }),
2069
- detailsText && /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
2145
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("li", { className: "truncate py-0.5", title: detailsText || void 0, children: [
2146
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("span", { className: "font-medium text-[var(--color-text)]", children: renderArtistLink(membership.artist) }),
2147
+ detailsText && /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
2070
2148
  " \u2014 ",
2071
2149
  detailsText
2072
2150
  ] })
@@ -2077,9 +2155,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
2077
2155
  function ArtistMembers({ title, memberships, renderArtistLink }) {
2078
2156
  if (memberships.length === 0) return null;
2079
2157
  const isCurrent = (membership) => !membership.ended && membership.endYear === null;
2080
- return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(import_ui12.PanelSection, { title, children: [
2081
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
2082
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
2158
+ return /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(import_ui12.PanelSection, { title, children: [
2159
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
2160
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
2083
2161
  MembershipList,
2084
2162
  {
2085
2163
  title: "Past",
@@ -2092,7 +2170,7 @@ function ArtistMembers({ title, memberships, renderArtistLink }) {
2092
2170
 
2093
2171
  // src/genre-tree/artist/ArtistDiscography.tsx
2094
2172
  var import_ui13 = require("@behindthemusictree/ui");
2095
- var import_jsx_runtime36 = require("react/jsx-runtime");
2173
+ var import_jsx_runtime37 = require("react/jsx-runtime");
2096
2174
  var GROUPS = ["Albums", "EPs", "Singles", "Live", "Compilations", "Other"];
2097
2175
  var PRIMARY_GROUPS = { album: "Albums", ep: "EPs", single: "Singles" };
2098
2176
  function discographyGroup({ primaryType, secondaryTypes }) {
@@ -2112,34 +2190,34 @@ function byYear(a, b) {
2112
2190
  function ArtistDiscography({ albums }) {
2113
2191
  if (albums.length === 0) return null;
2114
2192
  const sorted = [...albums].sort(byYear);
2115
- return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui13.PanelSection, { title: "Discography", children: GROUPS.map((group) => {
2193
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui13.PanelSection, { title: "Discography", children: GROUPS.map((group) => {
2116
2194
  const groupAlbums = sorted.filter((album) => discographyGroup(album) === group);
2117
2195
  if (groupAlbums.length === 0) return null;
2118
- return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ui13.PanelGroup, { title: group, children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("ul", { children: groupAlbums.map((album) => /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("li", { className: "grid grid-cols-[2.5rem_minmax(0,1fr)] gap-2 py-0.5", children: [
2119
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("span", { className: "tabular-nums text-[var(--color-text-muted)]", children: album.year ?? "\u2014" }),
2120
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("span", { className: "truncate text-[var(--color-text)]", title: album.name, children: album.name })
2196
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ui13.PanelGroup, { title: group, children: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("ul", { children: groupAlbums.map((album) => /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("li", { className: "grid grid-cols-[2.5rem_minmax(0,1fr)] gap-2 py-0.5", children: [
2197
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "tabular-nums text-[var(--color-text-muted)]", children: album.year ?? "\u2014" }),
2198
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "truncate text-[var(--color-text)]", title: album.name, children: album.name })
2121
2199
  ] }, album.uuid)) }) }, group);
2122
2200
  }) });
2123
2201
  }
2124
2202
 
2125
2203
  // src/genre-tree/artist/ArtistSongs.tsx
2126
- var import_react14 = require("react");
2204
+ var import_react15 = require("react");
2127
2205
  var import_react_query2 = require("@tanstack/react-query");
2128
2206
  var import_lucide_react13 = require("lucide-react");
2129
2207
  var import_ui14 = require("@behindthemusictree/ui");
2130
2208
  var import_player10 = require("@behindthemusictree/app-kit/player");
2131
- var import_genre_tree17 = require("@behindthemusictree/app-kit/genre-tree");
2209
+ var import_genre_tree23 = require("@behindthemusictree/app-kit/genre-tree");
2132
2210
  var import_transport9 = require("@behindthemusictree/app-kit/transport");
2133
2211
 
2134
2212
  // src/genre-tree/track-list-sidebar/TrackItem.tsx
2135
2213
  var import_player9 = require("@behindthemusictree/app-kit/player");
2136
- var import_genre_tree14 = require("@behindthemusictree/app-kit/genre-tree");
2214
+ var import_genre_tree20 = require("@behindthemusictree/app-kit/genre-tree");
2137
2215
 
2138
2216
  // src/genre-tree/TrackPositionPlayPause.tsx
2139
2217
  var import_fa5 = require("react-icons/fa");
2140
2218
  var import_player7 = require("@behindthemusictree/app-kit/player");
2141
2219
  var import_player8 = require("@behindthemusictree/app-kit/player");
2142
- var import_jsx_runtime37 = require("react/jsx-runtime");
2220
+ var import_jsx_runtime38 = require("react/jsx-runtime");
2143
2221
  function TrackPositionPlayPause({
2144
2222
  position,
2145
2223
  uuid,
@@ -2153,14 +2231,14 @@ function TrackPositionPlayPause({
2153
2231
  const isPlaying = isThisTrack && isConfirmedPlayable && playState === import_player8.PlayStates.PLAYING;
2154
2232
  const isPaused = isThisTrack && isConfirmedPlayable && playState === import_player8.PlayStates.PAUSED;
2155
2233
  const isCurrentTrackActive = isPlaying || isPaused;
2156
- return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(
2234
+ return /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(
2157
2235
  "div",
2158
2236
  {
2159
2237
  className: "track-position-play-pause relative flex items-center justify-center gap-2 text-lg w-16 cursor-pointer",
2160
2238
  style: { minWidth: "4rem", maxWidth: "4rem" },
2161
2239
  onClick: handlePlayPauseClick,
2162
2240
  children: [
2163
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("div", { className: "track-position-play-pause__status group-hover:hidden flex items-center justify-center", children: isAwaitingPlayback ? /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "track-position-play-pause__spinner", role: "status", "aria-label": "Loading track" }) : isCurrentTrackActive ? /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(
2241
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("div", { className: "track-position-play-pause__status group-hover:hidden flex items-center justify-center", children: isAwaitingPlayback ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { className: "track-position-play-pause__spinner", role: "status", "aria-label": "Loading track" }) : isCurrentTrackActive ? /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(
2164
2242
  "span",
2165
2243
  {
2166
2244
  className: "track-position-play-pause__bars",
@@ -2168,18 +2246,18 @@ function TrackPositionPlayPause({
2168
2246
  role: "img",
2169
2247
  "aria-label": isPlaying ? "Playing" : "Paused",
2170
2248
  children: [
2171
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "track-position-play-pause__bar" }),
2172
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "track-position-play-pause__bar" }),
2173
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "track-position-play-pause__bar" })
2249
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { className: "track-position-play-pause__bar" }),
2250
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { className: "track-position-play-pause__bar" }),
2251
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { className: "track-position-play-pause__bar" })
2174
2252
  ]
2175
2253
  }
2176
2254
  ) : position }),
2177
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
2255
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2178
2256
  "div",
2179
2257
  {
2180
2258
  className: `hidden group-hover:flex items-center justify-center ${isCurrentTrackActive ? "" : "absolute inset-0"}`,
2181
2259
  "aria-hidden": "true",
2182
- children: isAwaitingPlayback ? /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "track-position-play-pause__spinner" }) : isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_fa5.FaPause, {}) : /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_fa5.FaPlay, {})
2260
+ children: isAwaitingPlayback ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { className: "track-position-play-pause__spinner" }) : isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_fa5.FaPause, {}) : /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_fa5.FaPlay, {})
2183
2261
  }
2184
2262
  )
2185
2263
  ]
@@ -2188,9 +2266,9 @@ function TrackPositionPlayPause({
2188
2266
  }
2189
2267
 
2190
2268
  // src/genre-tree/track-list-sidebar/TrackItem.tsx
2191
- var import_genre_tree15 = require("@behindthemusictree/app-kit/genre-tree");
2192
- var import_genre_tree16 = require("@behindthemusictree/app-kit/genre-tree");
2193
- var import_jsx_runtime38 = require("react/jsx-runtime");
2269
+ var import_genre_tree21 = require("@behindthemusictree/app-kit/genre-tree");
2270
+ var import_genre_tree22 = require("@behindthemusictree/app-kit/genre-tree");
2271
+ var import_jsx_runtime39 = require("react/jsx-runtime");
2194
2272
  function TrackItem({
2195
2273
  track,
2196
2274
  position,
@@ -2200,11 +2278,11 @@ function TrackItem({
2200
2278
  renderSongLink
2201
2279
  }) {
2202
2280
  const { handlePlayPauseAction, playerTrackObject, loadTrackForPlayer } = (0, import_player9.usePlayer)();
2203
- const { trackList, selectedTrack, toTrackAtPosition, runtimeUnplayableTrackIds } = (0, import_genre_tree14.useTrackList)();
2281
+ const { trackList, selectedTrack, toTrackAtPosition, runtimeUnplayableTrackIds } = (0, import_genre_tree20.useTrackList)();
2204
2282
  const scope = trackList?.origin?.scope ?? null;
2205
2283
  const isCurrentTrack = playerTrackObject?.track.id === track.uuid;
2206
2284
  const hasPlaybackError = isCurrentTrack && !!playerTrackObject?.loadError;
2207
- const isUnplayable = hasPlaybackError || runtimeUnplayableTrackIds?.has(track.uuid) === true || (0, import_genre_tree15.isSong)(track) && (0, import_genre_tree15.songUnplayableReason)(track) !== null;
2285
+ const isUnplayable = hasPlaybackError || runtimeUnplayableTrackIds?.has(track.uuid) === true || (0, import_genre_tree21.isSong)(track) && (0, import_genre_tree21.songUnplayableReason)(track) !== null;
2208
2286
  const handlePlayPauseClick = (event) => {
2209
2287
  event.stopPropagation();
2210
2288
  if (isCurrentTrack) {
@@ -2214,12 +2292,12 @@ function TrackItem({
2214
2292
  loadTrackForPlayer(track.uuid);
2215
2293
  }
2216
2294
  };
2217
- return /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(
2295
+ return /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(
2218
2296
  "div",
2219
2297
  {
2220
2298
  className: `track-item flex h-14 text-[var(--color-text)] ${isUnplayable ? "opacity-50" : "hover:bg-[var(--color-surface-muted)] group"}`,
2221
2299
  children: [
2222
- isUnplayable ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2300
+ isUnplayable ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2223
2301
  "div",
2224
2302
  {
2225
2303
  className: "flex items-center justify-center text-lg w-16",
@@ -2227,7 +2305,7 @@ function TrackItem({
2227
2305
  "aria-disabled": "true",
2228
2306
  children: position
2229
2307
  }
2230
- ) : /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2308
+ ) : /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2231
2309
  TrackPositionPlayPause,
2232
2310
  {
2233
2311
  position,
@@ -2236,12 +2314,12 @@ function TrackItem({
2236
2314
  isSelected: selectedTrack?.uuid === track.uuid
2237
2315
  }
2238
2316
  ),
2239
- /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
2240
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("div", { className: "flex max-w-full items-center gap-2", children: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("div", { className: "title text-lg font-bold text-[var(--color-text)] max-w-full truncate", children: renderSongLink ? renderSongLink(track) : (0, import_genre_tree15.isSong)(track) ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }) }),
2241
- track.artists && track.artists.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("div", { className: "artist text-base max-w-full truncate text-[var(--color-text-muted)]", children: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_genre_tree16.ArtistNames, { artists: track.artists, renderArtistLink }) }) : ""
2317
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
2318
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "flex max-w-full items-center gap-2", children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "title text-lg font-bold text-[var(--color-text)] max-w-full truncate", children: renderSongLink ? renderSongLink(track) : (0, import_genre_tree21.isSong)(track) ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }) }),
2319
+ track.artists && track.artists.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "artist text-base max-w-full truncate text-[var(--color-text-muted)]", children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_genre_tree22.ArtistNames, { artists: track.artists, renderArtistLink }) }) : ""
2242
2320
  ] }),
2243
- /* @__PURE__ */ (0, import_jsx_runtime38.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_runtime38.jsx)("div", { className: "genre-name max-w-full whitespace-normal break-words p-1 text-xs font-bold", children: track.genre.name }) : "" }),
2244
- renderDuration && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2321
+ /* @__PURE__ */ (0, import_jsx_runtime39.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_runtime39.jsx)("div", { className: "genre-name max-w-full whitespace-normal break-words p-1 text-xs font-bold", children: track.genre.name }) : "" }),
2322
+ renderDuration && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2245
2323
  "div",
2246
2324
  {
2247
2325
  className: "duration flex text-base w-16 items-center justify-center",
@@ -2249,28 +2327,28 @@ function TrackItem({
2249
2327
  children: renderDuration(track)
2250
2328
  }
2251
2329
  ),
2252
- renderActions && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("div", { className: "edit flex text-base w-6 items-center justify-center mr-2 cursor-pointer", children: renderActions(track) })
2330
+ renderActions && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "edit flex text-base w-6 items-center justify-center mr-2 cursor-pointer", children: renderActions(track) })
2253
2331
  ]
2254
2332
  }
2255
2333
  );
2256
2334
  }
2257
2335
 
2258
2336
  // src/genre-tree/artist/ArtistSongs.tsx
2259
- var import_jsx_runtime39 = require("react/jsx-runtime");
2260
- var SongPageSchema2 = (0, import_transport9.PaginatedResponseSchema)(import_genre_tree17.SongDetailedSchema);
2337
+ var import_jsx_runtime40 = require("react/jsx-runtime");
2338
+ var SongPageSchema2 = (0, import_transport9.PaginatedResponseSchema)(import_genre_tree23.SongDetailedSchema);
2261
2339
  var SORTS = [
2262
2340
  { ordering: "title", label: "Title" },
2263
2341
  { ordering: "-albumYear", label: "Year" }
2264
2342
  ];
2265
2343
  function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLink }) {
2266
- const [page, setPage] = (0, import_react14.useState)(1);
2267
- const [ordering, setOrdering] = (0, import_react14.useState)("title");
2344
+ const [page, setPage] = (0, import_react15.useState)(1);
2345
+ const [ordering, setOrdering] = (0, import_react15.useState)("title");
2268
2346
  const { fetch } = (0, import_transport9.useFetchWrapper)(getBackendBaseUrl);
2269
- const { playNewTrackListFromArtist } = (0, import_genre_tree17.useTrackList)();
2347
+ const { playNewTrackListFromArtist } = (0, import_genre_tree23.useTrackList)();
2270
2348
  const { playerTrackObject } = (0, import_player10.usePlayer)();
2271
2349
  const { data, isPending, isError } = (0, import_transport9.useQueryWithParse)({
2272
2350
  queryKey: ["referenceSongs", "artist", artist.uuid, ordering, page],
2273
- queryFn: () => fetch(import_genre_tree17.libraryEndpoints.reference.songs.list(), true, false, void 0, {
2351
+ queryFn: () => fetch(import_genre_tree23.libraryEndpoints.reference.songs.list(), true, false, void 0, {
2274
2352
  artist: artist.uuid,
2275
2353
  ordering,
2276
2354
  page
@@ -2285,25 +2363,25 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
2285
2363
  });
2286
2364
  const handleRowClickCapture = (event, song) => {
2287
2365
  if (!(event.target instanceof Element) || !event.target.closest(".track-position-play-pause")) return;
2288
- if (playerTrackObject?.track.id === song.uuid || (0, import_genre_tree17.songUnplayableReason)(song) !== null) return;
2366
+ if (playerTrackObject?.track.id === song.uuid || (0, import_genre_tree23.songUnplayableReason)(song) !== null) return;
2289
2367
  event.stopPropagation();
2290
2368
  void play(song.uuid);
2291
2369
  };
2292
- return /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_ui14.PanelSection, { title: "Songs", children: isPending ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_ui14.Skeleton, { className: "h-14", "aria-busy": "true" }) : isError ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("p", { className: "text-red-600", children: "Could not load songs." }) : data.results.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "No songs yet." }) : /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(import_jsx_runtime39.Fragment, { children: [
2293
- /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "flex flex-wrap items-center justify-between gap-2 mb-2", children: [
2294
- /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(
2370
+ return /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_ui14.PanelSection, { title: "Songs", children: isPending ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_ui14.Skeleton, { className: "h-14", "aria-busy": "true" }) : isError ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("p", { className: "text-red-600", children: "Could not load songs." }) : data.results.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "No songs yet." }) : /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(import_jsx_runtime40.Fragment, { children: [
2371
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: "flex flex-wrap items-center justify-between gap-2 mb-2", children: [
2372
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(
2295
2373
  "button",
2296
2374
  {
2297
2375
  type: "button",
2298
2376
  onClick: () => void play(),
2299
2377
  className: "flex w-fit items-center gap-1.5 px-3 py-1 text-xs font-medium bg-gray-900 text-white rounded-full hover:bg-gray-700",
2300
2378
  children: [
2301
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_lucide_react13.Play, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
2379
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react13.Play, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
2302
2380
  "Play all"
2303
2381
  ]
2304
2382
  }
2305
2383
  ),
2306
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { role: "group", "aria-label": "Sort songs by", className: "flex gap-1 text-xs", children: SORTS.map((sort) => /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2384
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { role: "group", "aria-label": "Sort songs by", className: "flex gap-1 text-xs", children: SORTS.map((sort) => /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2307
2385
  "button",
2308
2386
  {
2309
2387
  type: "button",
@@ -2318,7 +2396,7 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
2318
2396
  sort.ordering
2319
2397
  )) })
2320
2398
  ] }),
2321
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("ul", { className: "overflow-hidden mb-2 bg-zinc-950 rounded-lg", children: data.results.map((song, index) => /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("li", { onClickCapture: (event) => handleRowClickCapture(event, song), children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2399
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("ul", { className: "overflow-hidden mb-2 bg-zinc-950 rounded-lg", children: data.results.map((song, index) => /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("li", { onClickCapture: (event) => handleRowClickCapture(event, song), children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2322
2400
  TrackItem,
2323
2401
  {
2324
2402
  track: song,
@@ -2327,12 +2405,12 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink, renderSongLi
2327
2405
  renderSongLink
2328
2406
  }
2329
2407
  ) }, song.uuid)) }),
2330
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_ui14.Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
2408
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_ui14.Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
2331
2409
  ] }) });
2332
2410
  }
2333
2411
 
2334
2412
  // src/genre-tree/artist/useArtistPanel.tsx
2335
- var import_jsx_runtime40 = require("react/jsx-runtime");
2413
+ var import_jsx_runtime41 = require("react/jsx-runtime");
2336
2414
  function useArtistPanel({
2337
2415
  uuid,
2338
2416
  scope,
@@ -2344,7 +2422,7 @@ function useArtistPanel({
2344
2422
  onExpandedChange,
2345
2423
  onClose
2346
2424
  }) {
2347
- const result = (0, import_genre_tree18.useArtist)(uuid, getBackendBaseUrl);
2425
+ const result = (0, import_genre_tree24.useArtist)(uuid, getBackendBaseUrl);
2348
2426
  const panel = { expanded, onExpandedChange, onClose };
2349
2427
  switch (result.status) {
2350
2428
  case "idle":
@@ -2353,26 +2431,26 @@ function useArtistPanel({
2353
2431
  return {
2354
2432
  ...panel,
2355
2433
  title: "Artist",
2356
- content: /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
2357
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_ui15.Skeleton, { className: "h-3 w-16" }),
2358
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_ui15.Skeleton, { className: "h-4 w-32" }),
2359
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_ui15.Skeleton, { className: "h-3 w-16" }),
2360
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_ui15.Skeleton, { className: "h-4 w-24" }),
2361
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_ui15.Skeleton, { className: "h-16 mt-2.5" }),
2362
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_ui15.Skeleton, { className: "h-16" })
2434
+ content: /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
2435
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_ui15.Skeleton, { className: "h-3 w-16" }),
2436
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_ui15.Skeleton, { className: "h-4 w-32" }),
2437
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_ui15.Skeleton, { className: "h-3 w-16" }),
2438
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_ui15.Skeleton, { className: "h-4 w-24" }),
2439
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_ui15.Skeleton, { className: "h-16 mt-2.5" }),
2440
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_ui15.Skeleton, { className: "h-16" })
2363
2441
  ] })
2364
2442
  };
2365
2443
  case "not-found":
2366
2444
  return {
2367
2445
  ...panel,
2368
2446
  title: "Artist not found",
2369
- content: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This artist does not exist." })
2447
+ content: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This artist does not exist." })
2370
2448
  };
2371
2449
  case "error":
2372
2450
  return {
2373
2451
  ...panel,
2374
2452
  title: "Artist",
2375
- content: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this artist." })
2453
+ content: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this artist." })
2376
2454
  };
2377
2455
  case "success": {
2378
2456
  const { artist } = result;
@@ -2381,13 +2459,13 @@ function useArtistPanel({
2381
2459
  return {
2382
2460
  ...panel,
2383
2461
  title: artist.name,
2384
- content: /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(import_jsx_runtime40.Fragment, { children: [
2385
- shareUrl && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: shareTarget && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl }) }) }),
2386
- artist.image && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(ArtistPhoto, { image: artist.image, name: artist.name }),
2387
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(ArtistHeader, { artist }),
2388
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(ArtistMembers, { title: "Members", memberships: artist.members, renderArtistLink }),
2389
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(ArtistMembers, { title: "Member of", memberships: artist.memberOf, renderArtistLink }),
2390
- artist.tracksCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2462
+ content: /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(import_jsx_runtime41.Fragment, { children: [
2463
+ shareUrl && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: shareTarget && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl }) }) }),
2464
+ artist.image && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(ArtistPhoto, { image: artist.image, name: artist.name }),
2465
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(ArtistHeader, { artist }),
2466
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(ArtistMembers, { title: "Members", memberships: artist.members, renderArtistLink }),
2467
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(ArtistMembers, { title: "Member of", memberships: artist.memberOf, renderArtistLink }),
2468
+ artist.tracksCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2391
2469
  ArtistSongs,
2392
2470
  {
2393
2471
  artist: { uuid: artist.uuid, name: artist.name },
@@ -2397,7 +2475,7 @@ function useArtistPanel({
2397
2475
  }
2398
2476
  )
2399
2477
  ] }),
2400
- extraContent: artist.albums.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(ArtistDiscography, { albums: artist.albums }) : void 0
2478
+ extraContent: artist.albums.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(ArtistDiscography, { albums: artist.albums }) : void 0
2401
2479
  };
2402
2480
  }
2403
2481
  }
@@ -2405,48 +2483,48 @@ function useArtistPanel({
2405
2483
 
2406
2484
  // src/genre-tree/song/useSongPanel.tsx
2407
2485
  var import_ui17 = require("@behindthemusictree/ui");
2408
- var import_genre_tree20 = require("@behindthemusictree/app-kit/genre-tree");
2486
+ var import_genre_tree26 = require("@behindthemusictree/app-kit/genre-tree");
2409
2487
 
2410
2488
  // src/genre-tree/song/SongPanelContent.tsx
2411
- var import_react15 = require("react");
2489
+ var import_react16 = require("react");
2412
2490
  var import_lucide_react14 = require("lucide-react");
2413
2491
  var import_ui16 = require("@behindthemusictree/ui");
2414
2492
  var import_player11 = require("@behindthemusictree/app-kit/player");
2415
- var import_genre_tree19 = require("@behindthemusictree/app-kit/genre-tree");
2416
- var import_jsx_runtime41 = require("react/jsx-runtime");
2493
+ var import_genre_tree25 = require("@behindthemusictree/app-kit/genre-tree");
2494
+ var import_jsx_runtime42 = require("react/jsx-runtime");
2417
2495
  var numberFormat = new Intl.NumberFormat("en");
2418
2496
  var dateFormat = new Intl.DateTimeFormat("en", { dateStyle: "medium" });
2419
2497
  var externalLink = "text-[var(--color-primary)] hover:underline focus-visible:underline";
2420
2498
  function SongHistory({ uuid, getBackendBaseUrl }) {
2421
- const [open, setOpen] = (0, import_react15.useState)(false);
2422
- const history = (0, import_genre_tree19.useSongHistory)(uuid, getBackendBaseUrl, open);
2499
+ const [open, setOpen] = (0, import_react16.useState)(false);
2500
+ const history = (0, import_genre_tree25.useSongHistory)(uuid, getBackendBaseUrl, open);
2423
2501
  let body = null;
2424
- if (history.status === "loading") body = /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_ui16.Skeleton, { className: "h-10", "aria-busy": "true" });
2425
- else if (history.status === "error") body = /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("p", { className: "text-red-600", children: "Could not load the history." });
2502
+ if (history.status === "loading") body = /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui16.Skeleton, { className: "h-10", "aria-busy": "true" });
2503
+ else if (history.status === "error") body = /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("p", { className: "text-red-600", children: "Could not load the history." });
2426
2504
  else if (history.status === "success")
2427
- body = history.entries.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "No changes yet." }) : /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("ul", { className: "divide-y divide-[var(--color-border)]", children: history.entries.map((entry) => /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("li", { className: "py-1.5 text-sm", children: [
2428
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("span", { className: "font-medium", children: entry.action }),
2429
- entry.oldValue !== null || entry.newValue !== null ? /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
2505
+ body = history.entries.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("p", { className: "text-[var(--color-text-muted)]", children: "No changes yet." }) : /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("ul", { className: "divide-y divide-[var(--color-border)]", children: history.entries.map((entry) => /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("li", { className: "py-1.5 text-sm", children: [
2506
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { className: "font-medium", children: entry.action }),
2507
+ entry.oldValue !== null || entry.newValue !== null ? /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("span", { className: "text-[var(--color-text-muted)]", children: [
2430
2508
  " ",
2431
2509
  "\u2014 ",
2432
2510
  entry.oldValue ?? "none",
2433
2511
  " \u2192 ",
2434
2512
  entry.newValue ?? "none"
2435
2513
  ] }) : null,
2436
- /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("span", { className: "block text-xs text-[var(--color-text-muted)]", children: [
2514
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("span", { className: "block text-xs text-[var(--color-text-muted)]", children: [
2437
2515
  entry.actorPseudo ?? "Anonymous",
2438
2516
  " \xB7 ",
2439
2517
  dateFormat.format(new Date(entry.createdOn))
2440
2518
  ] })
2441
2519
  ] }, entry.uuid)) });
2442
- return /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(
2520
+ return /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(
2443
2521
  "details",
2444
2522
  {
2445
2523
  onToggle: (event) => setOpen(event.currentTarget.open),
2446
2524
  className: "px-6 pb-2.5 mt-2.5 border-t border-[var(--color-border)]",
2447
2525
  children: [
2448
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("summary", { className: "mt-2.5 cursor-pointer text-[0.6875rem] font-semibold uppercase text-[var(--color-text-muted)]", children: "History" }),
2449
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("div", { className: "mt-1.5", children: body })
2526
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("summary", { className: "mt-2.5 cursor-pointer text-[0.6875rem] font-semibold uppercase text-[var(--color-text-muted)]", children: "History" }),
2527
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("div", { className: "mt-1.5", children: body })
2450
2528
  ]
2451
2529
  }
2452
2530
  );
@@ -2460,25 +2538,25 @@ function SongPanelContent({
2460
2538
  genrePath,
2461
2539
  onSelectGenre
2462
2540
  }) {
2463
- const { playNewTrackListFromTrackUuid } = (0, import_genre_tree19.useTrackList)();
2541
+ const { playNewTrackListFromTrackUuid } = (0, import_genre_tree25.useTrackList)();
2464
2542
  const { playerTrackObject, playState, handlePlayPauseAction } = (0, import_player11.usePlayer)();
2465
- const unplayableReason = (0, import_genre_tree19.songUnplayableReason)(song);
2543
+ const unplayableReason = (0, import_genre_tree25.songUnplayableReason)(song);
2466
2544
  const isCurrent = playerTrackObject?.track.id === song.uuid;
2467
2545
  const isPlaying = isCurrent && playState === import_player11.PlayStates.PLAYING;
2468
2546
  const shareTarget = { kind: "song", uuid: song.uuid, name: song.title, scope };
2469
- const musicBrainzUrl = (0, import_genre_tree19.musicBrainzRecordingUrl)(song);
2547
+ const musicBrainzUrl = (0, import_genre_tree25.musicBrainzRecordingUrl)(song);
2470
2548
  const fields = [];
2471
2549
  if (song.genre) {
2472
2550
  const path = genrePath.length > 0 ? genrePath : null;
2473
2551
  fields.push({
2474
2552
  label: "Genre",
2475
- value: path ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("span", { className: "flex flex-wrap items-center gap-x-1", children: path.map((genrePlaylist, index) => /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("span", { children: [
2476
- index > 0 && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("span", { "aria-hidden": "true", children: "\u203A " }),
2477
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("button", { type: "button", onClick: () => onSelectGenre(genrePlaylist), className: "hover:underline", children: genrePlaylist.name })
2553
+ value: path ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { className: "flex flex-wrap items-center gap-x-1", children: path.map((genrePlaylist, index) => /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("span", { children: [
2554
+ index > 0 && /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { "aria-hidden": "true", children: "\u203A " }),
2555
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("button", { type: "button", onClick: () => onSelectGenre(genrePlaylist), className: "hover:underline", children: genrePlaylist.name })
2478
2556
  ] }, genrePlaylist.uuid)) }) : song.genre.name
2479
2557
  });
2480
2558
  } else {
2481
- fields.push({ label: "Genre", value: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("span", { className: "text-[var(--color-text-muted)]", children: "No genre" }) });
2559
+ fields.push({ label: "Genre", value: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { className: "text-[var(--color-text-muted)]", children: "No genre" }) });
2482
2560
  }
2483
2561
  if (song.firstReleaseYear) fields.push({ label: "First released", value: song.firstReleaseYear });
2484
2562
  const viewCount = song.youtubeVideo?.viewCount;
@@ -2488,11 +2566,11 @@ function SongPanelContent({
2488
2566
  const links = [];
2489
2567
  if (musicBrainzUrl)
2490
2568
  links.push(
2491
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("a", { href: musicBrainzUrl, target: "_blank", rel: "noopener noreferrer", className: externalLink, children: "MusicBrainz" }, "mb")
2569
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("a", { href: musicBrainzUrl, target: "_blank", rel: "noopener noreferrer", className: externalLink, children: "MusicBrainz" }, "mb")
2492
2570
  );
2493
2571
  if (song.youtubeVideo)
2494
2572
  links.push(
2495
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2573
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
2496
2574
  "a",
2497
2575
  {
2498
2576
  href: `https://www.youtube.com/watch?v=${encodeURIComponent(song.youtubeVideo.id)}`,
@@ -2507,33 +2585,33 @@ function SongPanelContent({
2507
2585
  if (links.length > 0)
2508
2586
  fields.push({
2509
2587
  label: "Links",
2510
- value: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("span", { className: "flex gap-3", children: links })
2588
+ value: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { className: "flex gap-3", children: links })
2511
2589
  });
2512
- return /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(import_jsx_runtime41.Fragment, { children: [
2513
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: [
2514
- unplayableReason ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("p", { className: "flex items-center px-2.5 text-sm text-[var(--color-text-muted)]", children: unplayableReason }) : /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(
2590
+ return /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(import_jsx_runtime42.Fragment, { children: [
2591
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: [
2592
+ unplayableReason ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("p", { className: "flex items-center px-2.5 text-sm text-[var(--color-text-muted)]", children: unplayableReason }) : /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(
2515
2593
  "button",
2516
2594
  {
2517
2595
  type: "button",
2518
2596
  onClick: () => isCurrent ? handlePlayPauseAction() : playNewTrackListFromTrackUuid(song, scope),
2519
2597
  className: "group flex flex-col items-center gap-1 w-16 text-xs text-center text-[var(--color-text)] focus-visible:outline-none",
2520
2598
  children: [
2521
- /* @__PURE__ */ (0, import_jsx_runtime41.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_runtime41.jsx)(import_lucide_react14.Pause, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_lucide_react14.Play, { "aria-hidden": "true", className: "w-5 h-5" }) }),
2599
+ /* @__PURE__ */ (0, import_jsx_runtime42.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_runtime42.jsx)(import_lucide_react14.Pause, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_lucide_react14.Play, { "aria-hidden": "true", className: "w-5 h-5" }) }),
2522
2600
  isPlaying ? "Pause" : "Play"
2523
2601
  ]
2524
2602
  }
2525
2603
  ),
2526
- shareUrl && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl })
2604
+ shareUrl && /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(ShareActionButton, { target: shareTarget, url: shareUrl })
2527
2605
  ] }) }),
2528
- song.artists && song.artists.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("p", { className: "px-6 pt-2.5 text-[var(--color-text-muted)]", children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_genre_tree19.ArtistNames, { artists: song.artists, renderArtistLink }) }),
2529
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_ui16.FieldList, { items: fields }),
2530
- song.essentialGenres && song.essentialGenres.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_ui16.PanelSection, { title: "Essential in", children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("ul", { children: song.essentialGenres.map((genre) => /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("li", { className: "py-0.5", children: genre.name }, genre.uuid)) }) }),
2531
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(SongHistory, { uuid: song.uuid, getBackendBaseUrl })
2606
+ song.artists && song.artists.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("p", { className: "px-6 pt-2.5 text-[var(--color-text-muted)]", children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_genre_tree25.ArtistNames, { artists: song.artists, renderArtistLink }) }),
2607
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui16.FieldList, { items: fields }),
2608
+ song.essentialGenres && song.essentialGenres.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui16.PanelSection, { title: "Essential in", children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("ul", { children: song.essentialGenres.map((genre) => /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("li", { className: "py-0.5", children: genre.name }, genre.uuid)) }) }),
2609
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(SongHistory, { uuid: song.uuid, getBackendBaseUrl })
2532
2610
  ] });
2533
2611
  }
2534
2612
 
2535
2613
  // src/genre-tree/song/useSongPanel.tsx
2536
- var import_jsx_runtime42 = require("react/jsx-runtime");
2614
+ var import_jsx_runtime43 = require("react/jsx-runtime");
2537
2615
  function useSongPanel({
2538
2616
  uuid,
2539
2617
  scope,
@@ -2546,7 +2624,7 @@ function useSongPanel({
2546
2624
  onExpandedChange,
2547
2625
  onClose
2548
2626
  }) {
2549
- const result = (0, import_genre_tree20.useSong)(uuid, getBackendBaseUrl);
2627
+ const result = (0, import_genre_tree26.useSong)(uuid, getBackendBaseUrl);
2550
2628
  const panel = { expanded, onExpandedChange, onClose };
2551
2629
  switch (result.status) {
2552
2630
  case "idle":
@@ -2555,24 +2633,24 @@ function useSongPanel({
2555
2633
  return {
2556
2634
  ...panel,
2557
2635
  title: "Song",
2558
- content: /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
2559
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui17.Skeleton, { className: "h-3 w-16" }),
2560
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui17.Skeleton, { className: "h-4 w-32" }),
2561
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui17.Skeleton, { className: "h-3 w-16" }),
2562
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_ui17.Skeleton, { className: "h-4 w-24" })
2636
+ content: /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
2637
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_ui17.Skeleton, { className: "h-3 w-16" }),
2638
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_ui17.Skeleton, { className: "h-4 w-32" }),
2639
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_ui17.Skeleton, { className: "h-3 w-16" }),
2640
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_ui17.Skeleton, { className: "h-4 w-24" })
2563
2641
  ] })
2564
2642
  };
2565
2643
  case "not-found":
2566
2644
  return {
2567
2645
  ...panel,
2568
2646
  title: "Song not found",
2569
- content: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This song does not exist." })
2647
+ content: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This song does not exist." })
2570
2648
  };
2571
2649
  case "error":
2572
2650
  return {
2573
2651
  ...panel,
2574
2652
  title: "Song",
2575
- content: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this song." })
2653
+ content: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this song." })
2576
2654
  };
2577
2655
  case "success": {
2578
2656
  const { song } = result;
@@ -2580,7 +2658,7 @@ function useSongPanel({
2580
2658
  return {
2581
2659
  ...panel,
2582
2660
  title: song.title,
2583
- content: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
2661
+ content: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
2584
2662
  SongPanelContent,
2585
2663
  {
2586
2664
  song,
@@ -2600,12 +2678,12 @@ function useSongPanel({
2600
2678
  // src/genre-tree/SizeMetricToggle.tsx
2601
2679
  var import_md2 = require("react-icons/md");
2602
2680
  var import_ui18 = require("@behindthemusictree/ui");
2603
- var import_jsx_runtime43 = require("react/jsx-runtime");
2681
+ var import_jsx_runtime44 = require("react/jsx-runtime");
2604
2682
  function SizeMetricToggle({
2605
2683
  value,
2606
2684
  onChange
2607
2685
  }) {
2608
- return /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
2686
+ return /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
2609
2687
  import_ui18.SegmentedControl,
2610
2688
  {
2611
2689
  variant: "floating",
@@ -2613,15 +2691,15 @@ function SizeMetricToggle({
2613
2691
  value,
2614
2692
  onChange,
2615
2693
  options: [
2616
- { value: "songs", label: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_md2.MdMusicNote, { size: 18, "aria-hidden": true }), "aria-label": "Size by songs", title: "Node size by song count" },
2617
- { value: "views", label: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_md2.MdVisibility, { size: 18, "aria-hidden": true }), "aria-label": "Size by views", title: "Node size by song views" }
2694
+ { value: "songs", label: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_md2.MdMusicNote, { size: 18, "aria-hidden": true }), "aria-label": "Size by songs", title: "Node size by song count" },
2695
+ { value: "views", label: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_md2.MdVisibility, { size: 18, "aria-hidden": true }), "aria-label": "Size by views", title: "Node size by song views" }
2618
2696
  ]
2619
2697
  }
2620
2698
  );
2621
2699
  }
2622
2700
 
2623
2701
  // src/genre-tree/GenreTreeView.tsx
2624
- var import_jsx_runtime44 = require("react/jsx-runtime");
2702
+ var import_jsx_runtime45 = require("react/jsx-runtime");
2625
2703
  var NO_POP_ROOT_TITLE = "This genre tree has no 'Mainstream Pop' root yet";
2626
2704
  var REGIONS_CLASS = "[--gtv-center-left:var(--layout-center-left)] [--gtv-center-right:var(--layout-center-right)] [--gtv-center-top:var(--layout-band-top)]";
2627
2705
  var noop = () => {
@@ -2656,6 +2734,7 @@ function GenreTreeView({
2656
2734
  onArtistExpandedChange,
2657
2735
  getShareUrl,
2658
2736
  extraSearchGroups = [],
2737
+ SearchGroups,
2659
2738
  onSearchQueryChange,
2660
2739
  onSearchFocus,
2661
2740
  artistSearch = true,
@@ -2682,21 +2761,21 @@ function GenreTreeView({
2682
2761
  placement = "default",
2683
2762
  ...focus
2684
2763
  }) {
2685
- const getShareUrlRef = (0, import_react16.useRef)(getShareUrl);
2764
+ const getShareUrlRef = (0, import_react17.useRef)(getShareUrl);
2686
2765
  getShareUrlRef.current = getShareUrl;
2687
- const [internalArtistUuid, setInternalArtistUuid] = (0, import_react16.useState)(null);
2766
+ const [internalArtistUuid, setInternalArtistUuid] = (0, import_react17.useState)(null);
2688
2767
  const isArtistControlled = controlledArtistUuid !== void 0;
2689
2768
  const artistUuid = isArtistControlled ? controlledArtistUuid : internalArtistUuid;
2690
- const [internalSongUuid, setInternalSongUuid] = (0, import_react16.useState)(null);
2769
+ const [internalSongUuid, setInternalSongUuid] = (0, import_react17.useState)(null);
2691
2770
  const isSongControlled = controlledSongUuid !== void 0;
2692
2771
  const songUuid = isSongControlled ? controlledSongUuid : internalSongUuid;
2693
2772
  const changeSong = (uuid) => {
2694
2773
  if (!isSongControlled) setInternalSongUuid(uuid);
2695
2774
  onSongUuidChange?.(uuid);
2696
2775
  };
2697
- const [internalSizeMetric, setInternalSizeMetric] = (0, import_react16.useState)("songs");
2776
+ const [internalSizeMetric, setInternalSizeMetric] = (0, import_react17.useState)("songs");
2698
2777
  const sizeMetric = controlledSizeMetric ?? internalSizeMetric;
2699
- const deferredSizeMetric = (0, import_react16.useDeferredValue)(sizeMetric);
2778
+ const deferredSizeMetric = (0, import_react17.useDeferredValue)(sizeMetric);
2700
2779
  const changeSizeMetric = (metric) => {
2701
2780
  if (controlledSizeMetric === void 0) setInternalSizeMetric(metric);
2702
2781
  onSizeMetricChange?.(metric);
@@ -2723,7 +2802,7 @@ function GenreTreeView({
2723
2802
  allowWheelRotation: internalAllowWheelRotation,
2724
2803
  toggleWheelRotation,
2725
2804
  onNodeHover: handleNodeHover
2726
- } = (0, import_genre_tree21.useGenreTreeViewState)({
2805
+ } = (0, import_genre_tree27.useGenreTreeViewState)({
2727
2806
  scope,
2728
2807
  getBackendBaseUrl,
2729
2808
  viewMode: controlledViewMode,
@@ -2733,7 +2812,7 @@ function GenreTreeView({
2733
2812
  artistUuid
2734
2813
  });
2735
2814
  const restoreRevision = viewportRestore?.revision;
2736
- const [stackedRestoreOwner, setStackedRestoreOwner] = (0, import_react16.useState)({ revision: restoreRevision, rootUuid: selectedGenrePlaylist?.root.uuid });
2815
+ const [stackedRestoreOwner, setStackedRestoreOwner] = (0, import_react17.useState)({ revision: restoreRevision, rootUuid: selectedGenrePlaylist?.root.uuid });
2737
2816
  let restoreRootUuid = stackedRestoreOwner.rootUuid;
2738
2817
  if (stackedRestoreOwner.revision !== restoreRevision) {
2739
2818
  restoreRootUuid = selectedGenrePlaylist?.root.uuid;
@@ -2742,14 +2821,14 @@ function GenreTreeView({
2742
2821
  restoreRootUuid = selectedGenrePlaylist.root.uuid;
2743
2822
  setStackedRestoreOwner({ revision: restoreRevision, rootUuid: restoreRootUuid });
2744
2823
  }
2745
- const isGenreUpdating = (0, import_react_query3.useIsMutating)({ mutationKey: import_genre_tree21.genreUpdateMutationKey }) > 0;
2746
- const { mutate: updateGenre } = (0, import_genre_tree21.useUpdateGenre)(scope, getBackendBaseUrl);
2824
+ const isGenreUpdating = (0, import_react_query3.useIsMutating)({ mutationKey: import_genre_tree27.genreUpdateMutationKey }) > 0;
2825
+ const { mutate: updateGenre } = (0, import_genre_tree27.useUpdateGenre)(scope, getBackendBaseUrl);
2747
2826
  const busy = deferredSizeMetric !== sizeMetric || isTreeFetching || isGenreUpdating;
2748
2827
  const isArtistFiltered = artistUuid !== null;
2749
2828
  const canSizeByViews = !isArtistFiltered && hasViewsCount(visibleGenrePlaylists);
2750
- const artistResult = (0, import_genre_tree21.useArtist)(artistUuid, artistSearchBaseUrl ?? getBackendBaseUrl);
2829
+ const artistResult = (0, import_genre_tree27.useArtist)(artistUuid, artistSearchBaseUrl ?? getBackendBaseUrl);
2751
2830
  const filterArtistName = artistResult.status === "success" ? artistResult.artist.name : null;
2752
- const filterArtist = (0, import_react16.useMemo)(
2831
+ const filterArtist = (0, import_react17.useMemo)(
2753
2832
  () => artistUuid !== null && filterArtistName !== null ? { uuid: artistUuid, name: filterArtistName } : null,
2754
2833
  [artistUuid, filterArtistName]
2755
2834
  );
@@ -2764,14 +2843,14 @@ function GenreTreeView({
2764
2843
  if (selectedNodeId !== null) clearSelection();
2765
2844
  changeSong(uuid);
2766
2845
  };
2767
- const openSongRef = (0, import_react16.useRef)(openSong);
2846
+ const openSongRef = (0, import_react17.useRef)(openSong);
2768
2847
  openSongRef.current = openSong;
2769
- const resolvedRenderSongLink = (0, import_react16.useCallback)(
2770
- (song) => renderSongLink ? renderSongLink(song) : /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("button", { type: "button", onClick: () => openSongRef.current(song.uuid), className: "hover:underline", children: song.title }),
2848
+ const resolvedRenderSongLink = (0, import_react17.useCallback)(
2849
+ (song) => renderSongLink ? renderSongLink(song) : /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("button", { type: "button", onClick: () => openSongRef.current(song.uuid), className: "hover:underline", children: song.title }),
2771
2850
  [renderSongLink]
2772
2851
  );
2773
- const [expandedSongUuid, setExpandedSongUuid] = (0, import_react16.useState)(null);
2774
- const [expandedArtistUuid, setExpandedArtistUuid] = (0, import_react16.useState)(null);
2852
+ const [expandedSongUuid, setExpandedSongUuid] = (0, import_react17.useState)(null);
2853
+ const [expandedArtistUuid, setExpandedArtistUuid] = (0, import_react17.useState)(null);
2775
2854
  const artistBaseUrl = artistSearchBaseUrl ?? getBackendBaseUrl;
2776
2855
  const songBaseUrl = getBackendBaseUrl;
2777
2856
  const artistPanel = useArtistPanel({
@@ -2779,7 +2858,7 @@ function GenreTreeView({
2779
2858
  scope,
2780
2859
  getBackendBaseUrl: artistBaseUrl,
2781
2860
  getShareUrl: getShareUrlRef.current,
2782
- renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name })),
2861
+ renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name })),
2783
2862
  expanded: controlledArtistExpanded ?? (artistUuid !== null && expandedArtistUuid === artistUuid),
2784
2863
  onExpandedChange: (on) => {
2785
2864
  if (controlledArtistExpanded === void 0) setExpandedArtistUuid(on ? artistUuid : null);
@@ -2788,7 +2867,7 @@ function GenreTreeView({
2788
2867
  renderSongLink: resolvedRenderSongLink,
2789
2868
  onClose: () => changeArtist(null)
2790
2869
  });
2791
- const resolvedRenderArtistLink = renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name }));
2870
+ const resolvedRenderArtistLink = renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name }));
2792
2871
  const getGenrePath = (genre) => {
2793
2872
  const path = [];
2794
2873
  let current = visibleGenrePlaylists.find((playlist) => playlist.criteria?.uuid === genre.uuid);
@@ -2820,8 +2899,8 @@ function GenreTreeView({
2820
2899
  const builtInSongPanel = overlayPanel || hasSelectedGenreNode ? void 0 : songPanel;
2821
2900
  const builtInArtistPanel = overlayPanel || hasSelectedGenreNode || builtInSongPanel ? void 0 : artistPanel;
2822
2901
  const builtInPanel = builtInSongPanel ?? builtInArtistPanel;
2823
- const [uncontrolledCollapsed, setUncontrolledCollapsed] = (0, import_react16.useState)(false);
2824
- const [uncontrolledExpanded, setUncontrolledExpanded] = (0, import_react16.useState)(false);
2902
+ const [uncontrolledCollapsed, setUncontrolledCollapsed] = (0, import_react17.useState)(false);
2903
+ const [uncontrolledExpanded, setUncontrolledExpanded] = (0, import_react17.useState)(false);
2825
2904
  const isCollapsed = collapsed ?? uncontrolledCollapsed;
2826
2905
  const isExpanded = expanded ?? uncontrolledExpanded;
2827
2906
  const changeCollapsed = (next) => {
@@ -2832,28 +2911,28 @@ function GenreTreeView({
2832
2911
  setUncontrolledExpanded(next);
2833
2912
  onExpandedChange?.(next);
2834
2913
  };
2835
- (0, import_react16.useEffect)(() => {
2914
+ (0, import_react17.useEffect)(() => {
2836
2915
  setUncontrolledCollapsed(false);
2837
2916
  }, [selectedNodeId, artistUuid, songUuid]);
2838
- const [isWheelRevealed, setIsWheelRevealed] = (0, import_react16.useState)(false);
2917
+ const [isWheelRevealed, setIsWheelRevealed] = (0, import_react17.useState)(false);
2839
2918
  const usesHandoff = viewMode === "wheel" || viewMode === "pop-core";
2840
2919
  const isTreeReady = !isLoading && (!usesHandoff || isWheelRevealed);
2841
- const [resolveStyle, setResolveStyle] = (0, import_react16.useState)(null);
2842
- const handleNodeStyleResolverChange = (0, import_react16.useCallback)(
2920
+ const [resolveStyle, setResolveStyle] = (0, import_react17.useState)(null);
2921
+ const handleNodeStyleResolverChange = (0, import_react17.useCallback)(
2843
2922
  (resolver) => setResolveStyle(() => resolver),
2844
2923
  []
2845
2924
  );
2846
- const panelRef = (0, import_react16.useRef)(null);
2925
+ const panelRef = (0, import_react17.useRef)(null);
2847
2926
  const obscuredInset = (0, import_ui19.useObscuredInset)(panelRef);
2848
2927
  const hostPanel = { obscuredInset, onNodeStyleResolverChange: handleNodeStyleResolverChange };
2849
2928
  const nodeMetric = isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric;
2850
- const treeNodes = (0, import_react16.useMemo)(() => toTreeNodes(visibleGenrePlaylists, nodeMetric), [visibleGenrePlaylists, nodeMetric]);
2929
+ const treeNodes = (0, import_react17.useMemo)(() => toTreeNodes(visibleGenrePlaylists, nodeMetric), [visibleGenrePlaylists, nodeMetric]);
2851
2930
  const relations = (0, import_genre_tree_view4.useGenreNodeRelations)(
2852
2931
  treeNodes,
2853
2932
  !overlayPanel && hasSelectedGenreNode ? selectedNodeId : null,
2854
2933
  resolveStyle
2855
2934
  );
2856
- const [selectionHistory, setSelectionHistory] = (0, import_react16.useState)({
2935
+ const [selectionHistory, setSelectionHistory] = (0, import_react17.useState)({
2857
2936
  current: selectedNodeId,
2858
2937
  previous: null
2859
2938
  });
@@ -2866,28 +2945,6 @@ function GenreTreeView({
2866
2945
  const previousPlaylist = selectionHistory.previous !== null && selectionHistory.previous !== selectedNodeId ? genrePlaylistByNodeId.get(selectionHistory.previous) : void 0;
2867
2946
  const handlePrevious = previousPlaylist && visibleGenrePlaylists.includes(previousPlaylist) ? () => handleGenreSearchSelect(previousPlaylist) : void 0;
2868
2947
  const { showPopup, hidePopup } = (0, import_popup6.usePopup)();
2869
- const [searchText, setSearchText] = (0, import_react16.useState)("");
2870
- const artistGroup = useArtistSearchGroup(
2871
- artistBaseUrl,
2872
- searchText,
2873
- (artist) => {
2874
- openArtist(artist.uuid);
2875
- onArtistSelect?.(artist);
2876
- },
2877
- Boolean(artistSearch)
2878
- );
2879
- const songGroup = useSongSearchGroup(
2880
- songBaseUrl,
2881
- searchText,
2882
- (song) => {
2883
- openSong(song.uuid);
2884
- onSongSelect?.(song);
2885
- },
2886
- Boolean(songSearch)
2887
- );
2888
- const extraGroups = [...extraSearchGroups, artistGroup, songGroup].filter(
2889
- (group) => group !== void 0
2890
- );
2891
2948
  const handleSelectedNodeChange = (node) => {
2892
2949
  if (node !== null && songUuid !== null) changeSong(null);
2893
2950
  selectNode(node);
@@ -2903,7 +2960,7 @@ function GenreTreeView({
2903
2960
  const handleGenreSearchZoomIn = () => {
2904
2961
  if (selectedUuid) focus.onFocusGenreChange?.(selectedUuid, "zoom-in");
2905
2962
  };
2906
- const zoomInRef = (0, import_react16.useRef)(void 0);
2963
+ const zoomInRef = (0, import_react17.useRef)(void 0);
2907
2964
  zoomInRef.current = canZoomIn ? handleGenreSearchZoomIn : void 0;
2908
2965
  const selectGenreFromPanel = (nodeId) => {
2909
2966
  const genrePlaylist = genrePlaylistByNodeId.get(nodeId);
@@ -2911,7 +2968,7 @@ function GenreTreeView({
2911
2968
  };
2912
2969
  const renderGenreBody = (relation) => {
2913
2970
  const { node } = relation;
2914
- const family = /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
2971
+ const family = /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
2915
2972
  import_genre_tree_view4.GenreNodeFamily,
2916
2973
  {
2917
2974
  node,
@@ -2922,10 +2979,10 @@ function GenreTreeView({
2922
2979
  onSelectNode: selectGenreFromPanel
2923
2980
  }
2924
2981
  );
2925
- const ancestors = /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_genre_tree_view4.GenreNodeAncestors, { ancestorNodes: relation.ancestorNodes, onSelectNode: selectGenreFromPanel });
2982
+ const ancestors = /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_genre_tree_view4.GenreNodeAncestors, { ancestorNodes: relation.ancestorNodes, onSelectNode: selectGenreFromPanel });
2926
2983
  const genrePlaylist = genrePlaylistByNodeId.get(node.id);
2927
2984
  const criteria = genrePlaylist?.criteria;
2928
- if (!genrePlaylist || !criteria) return { primary: /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)(import_jsx_runtime44.Fragment, { children: [
2985
+ if (!genrePlaylist || !criteria) return { primary: /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(import_jsx_runtime45.Fragment, { children: [
2929
2986
  family,
2930
2987
  ancestors
2931
2988
  ] }) };
@@ -2945,16 +3002,17 @@ function GenreTreeView({
2945
3002
  (sibling) => sibling.parent?.uuid === parentPlaylist.uuid && sibling.uuid !== genrePlaylist.uuid && sibling.criteria?.side === "pop"
2946
3003
  ) : void 0;
2947
3004
  return {
2948
- primary: /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)(import_jsx_runtime44.Fragment, { children: [
2949
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3005
+ primary: /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(import_jsx_runtime45.Fragment, { children: [
3006
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
2950
3007
  GenreInfoPanelBody,
2951
3008
  {
2952
- family: /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)(import_jsx_runtime44.Fragment, { children: [
3009
+ family: /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(import_jsx_runtime45.Fragment, { children: [
2953
3010
  family,
2954
3011
  ancestors
2955
3012
  ] }),
2956
3013
  genreUuid,
2957
3014
  genrePlaylist,
3015
+ catalogue: genrePlaylist.catalogue,
2958
3016
  wikidataId: criteria.wikidataId,
2959
3017
  scope,
2960
3018
  getBackendBaseUrl,
@@ -2967,7 +3025,7 @@ function GenreTreeView({
2967
3025
  onAddChild: readOnly ? void 0 : () => handleGenreCreationAction(criteria),
2968
3026
  onRename: readOnly ? void 0 : () => handleGenreRenameAction(criteria),
2969
3027
  onChangeParent: readOnly ? void 0 : () => showPopup(
2970
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3028
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
2971
3029
  ChangeParentPopup,
2972
3030
  {
2973
3031
  genrePlaylist: { ...genrePlaylist, criteria },
@@ -3042,19 +3100,19 @@ function GenreTreeView({
3042
3100
  edge: activePanel.expanded ? { label: "Shrink panel", onClick: () => activePanel.onExpandedChange?.(false) } : isCollapsed ? null : { label: "Hide panel", onClick: () => changeCollapsed(true) },
3043
3101
  center: activePanel.expanded ? null : isCollapsed ? { label: "Show panel", onClick: () => changeCollapsed(false) } : activePanel.onExpandedChange ? { label: "Extend panel", onClick: () => activePanel.onExpandedChange?.(true) } : null
3044
3102
  } : void 0;
3045
- const expandButton = placement === "default" && activePanel?.onExpandedChange ? /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3103
+ const expandButton = placement === "default" && activePanel?.onExpandedChange ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3046
3104
  import_ui19.IconButton,
3047
3105
  {
3048
3106
  "aria-label": activePanel.expanded ? "Shrink panel" : "Extend panel",
3049
3107
  style: activePanel.headerAccent && { color: "inherit" },
3050
3108
  onClick: () => activePanel.onExpandedChange?.(!activePanel.expanded),
3051
- children: activePanel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_lucide_react16.Minimize2, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_lucide_react16.Maximize2, { "aria-hidden": "true", className: "h-4 w-4" })
3109
+ children: activePanel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_lucide_react16.Minimize2, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_lucide_react16.Maximize2, { "aria-hidden": "true", className: "h-4 w-4" })
3052
3110
  }
3053
3111
  ) : null;
3054
3112
  const renderArtistFilter = () => {
3055
3113
  if (isLoading || !filterArtist) return null;
3056
- return /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-sm", children: [
3057
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3114
+ return /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-sm", children: [
3115
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3058
3116
  "button",
3059
3117
  {
3060
3118
  type: "button",
@@ -3066,7 +3124,7 @@ function GenreTreeView({
3066
3124
  children: filterArtist.name
3067
3125
  }
3068
3126
  ),
3069
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3127
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3070
3128
  "button",
3071
3129
  {
3072
3130
  type: "button",
@@ -3078,26 +3136,36 @@ function GenreTreeView({
3078
3136
  )
3079
3137
  ] });
3080
3138
  };
3081
- const actions = /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)(import_jsx_runtime44.Fragment, { children: [
3082
- !isLoading && /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3083
- GenreSearch,
3139
+ const actions = /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(import_jsx_runtime45.Fragment, { children: [
3140
+ !isLoading && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3141
+ GenreTreeSearch,
3084
3142
  {
3143
+ artistBaseUrl,
3144
+ songBaseUrl,
3145
+ artistSearch: Boolean(artistSearch),
3146
+ songSearch: Boolean(songSearch),
3147
+ onArtistSelect: (artist) => {
3148
+ openArtist(artist.uuid);
3149
+ onArtistSelect?.(artist);
3150
+ },
3151
+ onSongSelect: (song) => {
3152
+ openSong(song.uuid);
3153
+ onSongSelect?.(song);
3154
+ },
3155
+ SearchGroups,
3085
3156
  genrePlaylists: visibleGenrePlaylists,
3086
3157
  onSelect: handleGenreSearchSelect,
3087
3158
  selectedName: selectedGenrePlaylist?.name ?? (typeof builtInPanel?.title === "string" ? builtInPanel.title : null),
3088
3159
  onClear: builtInSongPanel ? () => changeSong(null) : builtInArtistPanel ? () => changeArtist(null) : handleGenreSearchClear,
3089
3160
  onPrevious: handlePrevious,
3090
- onQueryChange: (text) => {
3091
- setSearchText(text);
3092
- onSearchQueryChange?.(text);
3093
- },
3161
+ onQueryChange: onSearchQueryChange,
3094
3162
  onFocus: onSearchFocus,
3095
- extraGroups
3163
+ extraGroups: extraSearchGroups
3096
3164
  }
3097
3165
  ),
3098
- !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
3099
- /* @__PURE__ */ (0, import_jsx_runtime44.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: [
3100
- !isLoading && !isControlled && /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3166
+ !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
3167
+ /* @__PURE__ */ (0, import_jsx_runtime45.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: [
3168
+ !isLoading && !isControlled && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3101
3169
  import_ui19.SegmentedControl,
3102
3170
  {
3103
3171
  "aria-label": "Tree view mode",
@@ -3111,13 +3179,13 @@ function GenreTreeView({
3111
3179
  ]
3112
3180
  }
3113
3181
  ),
3114
- !isLoading && showDisplayOptions && /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3182
+ !isLoading && showDisplayOptions && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3115
3183
  "div",
3116
3184
  {
3117
3185
  className: "flex items-center gap-1",
3118
3186
  role: "group",
3119
3187
  "aria-label": "Tree display options",
3120
- children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3188
+ children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3121
3189
  import_ui19.Button,
3122
3190
  {
3123
3191
  variant: allowWheelRotation ? "default" : "outline",
@@ -3128,7 +3196,7 @@ function GenreTreeView({
3128
3196
  )
3129
3197
  }
3130
3198
  ),
3131
- !isLoading && !readOnly && showAddRoot && /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3199
+ !isLoading && !readOnly && showAddRoot && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3132
3200
  import_ui19.IconTextButton,
3133
3201
  {
3134
3202
  icon: import_lucide_react15.Plus,
@@ -3138,34 +3206,34 @@ function GenreTreeView({
3138
3206
  )
3139
3207
  ] })
3140
3208
  ] });
3141
- return /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)(
3209
+ return /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(
3142
3210
  "div",
3143
3211
  {
3144
3212
  "data-left-panel": isPanelShown && !isCollapsed ? void 0 : "hidden",
3145
3213
  className: ["genre-tree-view relative flex flex-col h-full", placement === "regions" && REGIONS_CLASS, className2].filter(Boolean).join(" "),
3146
3214
  children: [
3147
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3215
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3148
3216
  "div",
3149
3217
  {
3150
3218
  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"}`,
3151
3219
  children: actions
3152
3220
  }
3153
3221
  ),
3154
- !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3222
+ !isLoading && filterArtist && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3155
3223
  "div",
3156
3224
  {
3157
3225
  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"}`,
3158
3226
  children: renderArtistFilter()
3159
3227
  }
3160
3228
  ),
3161
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("div", { className: "content-container flex-1 min-h-0 flex flex-row gap-4", children: /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("div", { className: "tree-view-container relative flex-1 min-w-0 flex flex-col h-full", children: [
3162
- !isLoading && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
3163
- isLoading ? /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_genre_tree_view4.GenreTreeViewSkeleton, { viewMode }) : viewMode === "wheel" ? (
3229
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("div", { className: "content-container flex-1 min-h-0 flex flex-row gap-4", children: /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("div", { className: "tree-view-container relative flex-1 min-w-0 flex flex-col h-full", children: [
3230
+ !isLoading && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
3231
+ isLoading ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_genre_tree_view4.GenreTreeViewSkeleton, { viewMode }) : viewMode === "wheel" ? (
3164
3232
  // The info panel's top edge aligns with the tree's; the search bar floats `--layout-panel-inset` below it,
3165
3233
  // and the header inset (the bar's height plus that inset) extends the header's fill behind the bar,
3166
3234
  // Google Maps style; the 8px bottom padding mirrors the inset above the bar, and the hidden title's height is zeroed.
3167
3235
  // The panel's own Zoom in button is hidden: the search bar carries it.
3168
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 ", children: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_genre_tree_view4.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3236
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 ", children: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_genre_tree_view4.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3169
3237
  GenrePlaylistTreeWheel,
3170
3238
  {
3171
3239
  scope,
@@ -3182,9 +3250,9 @@ function GenreTreeView({
3182
3250
  allowWheelRotation,
3183
3251
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
3184
3252
  busy,
3185
- floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("div", { className: "flex items-center gap-2", children: [
3253
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("div", { className: "flex items-center gap-2", children: [
3186
3254
  floatingControlsLeading,
3187
- canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
3255
+ canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
3188
3256
  ] }) : void 0,
3189
3257
  defaultViewport,
3190
3258
  viewportRestore,
@@ -3193,7 +3261,7 @@ function GenreTreeView({
3193
3261
  ) }) })
3194
3262
  ) : viewMode === "pop-core" ? (
3195
3263
  // Same as the wheel: the header inset extends its fill behind the floating search bar, Google Maps style.
3196
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 ", children: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_genre_tree_view4.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3264
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 ", children: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_genre_tree_view4.GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3197
3265
  GenrePlaylistTreeWheelRadialPopCore,
3198
3266
  {
3199
3267
  scope,
@@ -3210,9 +3278,9 @@ function GenreTreeView({
3210
3278
  allowWheelRotation,
3211
3279
  sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
3212
3280
  busy,
3213
- floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("div", { className: "flex items-center gap-2", children: [
3281
+ floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("div", { className: "flex items-center gap-2", children: [
3214
3282
  floatingControlsLeading,
3215
- canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
3283
+ canSizeByViews && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
3216
3284
  ] }) : void 0,
3217
3285
  defaultViewport,
3218
3286
  viewportRestore,
@@ -3220,7 +3288,7 @@ function GenreTreeView({
3220
3288
  ...focus
3221
3289
  }
3222
3290
  ) }) })
3223
- ) : viewMode === "outline" ? /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("div", { className: "tree-container flex-1 min-h-0 w-full overflow-y-auto relative", children: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3291
+ ) : viewMode === "outline" ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("div", { className: "tree-container flex-1 min-h-0 w-full overflow-y-auto relative", children: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3224
3292
  GenrePlaylistTreeWheelRadialPopCore,
3225
3293
  {
3226
3294
  outline: true,
@@ -3236,13 +3304,13 @@ function GenreTreeView({
3236
3304
  selectedNodeId,
3237
3305
  readOnly
3238
3306
  }
3239
- ) }) : /* @__PURE__ */ (0, import_jsx_runtime44.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)]", children: Object.entries(groupedGenrePlaylistsByRoot).map(
3307
+ ) }) : /* @__PURE__ */ (0, import_jsx_runtime45.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)]", children: Object.entries(groupedGenrePlaylistsByRoot).map(
3240
3308
  ([uuid, genrePlaylistTreePerRoot]) => {
3241
- return /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3309
+ return /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3242
3310
  "div",
3243
3311
  {
3244
3312
  className: "tree-per-root-container relative shrink-0 h-[28rem] mt-2 mr-16 p-2 bg-[var(--color-neutral-100)] rounded-lg",
3245
- children: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3313
+ children: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3246
3314
  GenrePlaylistTreePerRoot,
3247
3315
  {
3248
3316
  scope,
@@ -3268,7 +3336,7 @@ function GenreTreeView({
3268
3336
  );
3269
3337
  }
3270
3338
  ) }),
3271
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
3339
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3272
3340
  import_ui19.DetailPanel,
3273
3341
  {
3274
3342
  ref: panelRef,
@@ -3285,7 +3353,7 @@ function GenreTreeView({
3285
3353
  aside: activePanel?.aside,
3286
3354
  edgeTab: detailTab,
3287
3355
  headerClassName: activePanel?.compact ? "pt-[calc(var(--layout-search-h)+var(--layout-panel-inset)+0.5rem)]" : void 0,
3288
- children: activePanel?.aside && !activePanel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)(import_jsx_runtime44.Fragment, { children: [
3356
+ children: activePanel?.aside && !activePanel.expanded ? /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(import_jsx_runtime45.Fragment, { children: [
3289
3357
  activePanel.content,
3290
3358
  activePanel.aside
3291
3359
  ] }) : activePanel?.content
@@ -3298,12 +3366,12 @@ function GenreTreeView({
3298
3366
  }
3299
3367
 
3300
3368
  // src/genre-tree/GenreDetailPanel.tsx
3301
- var import_react17 = require("react");
3369
+ var import_react18 = require("react");
3302
3370
  var import_genre_tree_view5 = require("@behindthemusictree/genre-tree-view");
3303
3371
  var import_ui20 = require("@behindthemusictree/ui");
3304
3372
  var import_lucide_react17 = require("lucide-react");
3305
- var import_genre_tree22 = require("@behindthemusictree/app-kit/genre-tree");
3306
- var import_jsx_runtime45 = require("react/jsx-runtime");
3373
+ var import_genre_tree28 = require("@behindthemusictree/app-kit/genre-tree");
3374
+ var import_jsx_runtime46 = require("react/jsx-runtime");
3307
3375
  var RELATIVE_ITEM_COUNT = 1;
3308
3376
  function toNode(genre, parentId) {
3309
3377
  return {
@@ -3330,9 +3398,9 @@ function GenreDetailPanel({
3330
3398
  edgeTab,
3331
3399
  extras
3332
3400
  }) {
3333
- const { data: detail, isPending, isError } = (0, import_genre_tree22.useFetchGenreDetail)(genreUuid, scope, getBackendBaseUrl);
3401
+ const { data: detail, isPending, isError } = (0, import_genre_tree28.useFetchGenreDetail)(genreUuid, scope, getBackendBaseUrl);
3334
3402
  const playback = useGenrePlaylistPlayback({ scope, genrePlaylists: [] });
3335
- const view = (0, import_react17.useMemo)(() => {
3403
+ const view = (0, import_react18.useMemo)(() => {
3336
3404
  if (!detail) return null;
3337
3405
  const rootColor = (0, import_genre_tree_view5.getGenreTreeColor)(detail.root.wikidataId);
3338
3406
  const relative = (genre, parentId) => ({
@@ -3346,7 +3414,7 @@ function GenreDetailPanel({
3346
3414
  id: detail.uuid,
3347
3415
  parentId: detail.parent?.uuid ?? null,
3348
3416
  name: detail.name,
3349
- itemCount: detail.tracksCount,
3417
+ itemCount: scope === "reference" ? (0, import_genre_tree28.genreMusicCount)(import_genre_tree28.GenrePlaylistSummarySchema.parse(detail.criteriaPlaylist)) : detail.tracksCount,
3350
3418
  wikidataId: detail.wikidataId
3351
3419
  },
3352
3420
  fill: rootColor,
@@ -3356,19 +3424,19 @@ function GenreDetailPanel({
3356
3424
  ({ ascendant }, index) => relative(ascendant, index === 0 ? null : ancestors2[index - 1].ascendant.uuid)
3357
3425
  )
3358
3426
  };
3359
- }, [detail]);
3427
+ }, [detail, scope]);
3360
3428
  if (isError) {
3361
- return /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("div", { role: "alert", className: "p-4 text-sm text-[var(--color-text)]", children: "This genre could not be loaded." });
3429
+ return /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("div", { role: "alert", className: "p-4 text-sm text-[var(--color-text)]", children: "This genre could not be loaded." });
3362
3430
  }
3363
3431
  if (isPending || !detail || !view) {
3364
- return /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("div", { className: "flex flex-col gap-3 p-4", "aria-busy": "true", "data-testid": "genre-detail-panel-skeleton", children: [
3365
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_ui20.Skeleton, { className: "h-6 w-2/3" }),
3366
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_ui20.Skeleton, { className: "h-4 w-full" }),
3367
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_ui20.Skeleton, { className: "h-4 w-3/4" })
3432
+ return /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("div", { className: "flex flex-col gap-3 p-4", "aria-busy": "true", "data-testid": "genre-detail-panel-skeleton", children: [
3433
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(import_ui20.Skeleton, { className: "h-6 w-2/3" }),
3434
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(import_ui20.Skeleton, { className: "h-4 w-full" }),
3435
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(import_ui20.Skeleton, { className: "h-4 w-3/4" })
3368
3436
  ] });
3369
3437
  }
3370
3438
  const selectGenre = (nodeId) => onSelectGenre(nodeId);
3371
- const family = /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3439
+ const family = /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3372
3440
  import_genre_tree_view5.GenreNodeFamily,
3373
3441
  {
3374
3442
  node: view.node,
@@ -3379,20 +3447,20 @@ function GenreDetailPanel({
3379
3447
  onSelectNode: selectGenre
3380
3448
  }
3381
3449
  );
3382
- const ancestors = /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_genre_tree_view5.GenreNodeAncestors, { ancestorNodes: view.ancestorNodes, onSelectNode: selectGenre });
3383
- return /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(
3450
+ const ancestors = /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(import_genre_tree_view5.GenreNodeAncestors, { ancestorNodes: view.ancestorNodes, onSelectNode: selectGenre });
3451
+ return /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)(
3384
3452
  import_ui20.DetailPanel,
3385
3453
  {
3386
3454
  side: "left",
3387
3455
  title: view.node.name,
3388
3456
  headerAccent: { background: view.fill, color: import_genre_tree_view5.ACCENT_TEXT_COLOR },
3389
- headerActions: onExpandedChange ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3457
+ headerActions: onExpandedChange ? /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3390
3458
  import_ui20.IconButton,
3391
3459
  {
3392
3460
  "aria-label": expanded ? "Shrink panel" : "Extend panel",
3393
3461
  style: { color: "inherit" },
3394
3462
  onClick: () => onExpandedChange(!expanded),
3395
- children: expanded ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_lucide_react17.Minimize2, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_lucide_react17.Maximize2, { "aria-hidden": "true", className: "h-4 w-4" })
3463
+ children: expanded ? /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(import_lucide_react17.Minimize2, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(import_lucide_react17.Maximize2, { "aria-hidden": "true", className: "h-4 w-4" })
3396
3464
  }
3397
3465
  ) : void 0,
3398
3466
  onClose,
@@ -3401,15 +3469,16 @@ function GenreDetailPanel({
3401
3469
  onExpandedChange,
3402
3470
  edgeTab,
3403
3471
  children: [
3404
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3472
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3405
3473
  GenreInfoPanelBody,
3406
3474
  {
3407
- family: /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(import_jsx_runtime45.Fragment, { children: [
3475
+ family: /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)(import_jsx_runtime46.Fragment, { children: [
3408
3476
  family,
3409
3477
  ancestors
3410
3478
  ] }),
3411
3479
  genreUuid: detail.uuid,
3412
- genrePlaylist: { ...detail.criteriaPlaylist, tracksCount: detail.tracksCount },
3480
+ genrePlaylist: scope === "reference" ? import_genre_tree28.GenrePlaylistSummarySchema.parse(detail.criteriaPlaylist) : { ...detail.criteriaPlaylist, tracksCount: detail.tracksCount },
3481
+ catalogue: detail.catalogue,
3413
3482
  wikidataId: detail.wikidataId,
3414
3483
  scope,
3415
3484
  getBackendBaseUrl,
@@ -3428,13 +3497,13 @@ function GenreDetailPanel({
3428
3497
  var import_genre_tree_view6 = require("@behindthemusictree/genre-tree-view");
3429
3498
 
3430
3499
  // src/genre-tree/track-list-sidebar/TrackListSidebar.tsx
3431
- var import_react18 = require("react");
3500
+ var import_react19 = require("react");
3432
3501
  var import_lucide_react18 = require("lucide-react");
3433
- var import_genre_tree23 = require("@behindthemusictree/app-kit/genre-tree");
3434
- var import_genre_tree24 = require("@behindthemusictree/app-kit/genre-tree");
3435
- var import_track_noun4 = require("@behindthemusictree/app-kit/track-noun");
3436
- var import_jsx_runtime46 = require("react/jsx-runtime");
3437
- var useBrowserLayoutEffect = typeof window === "undefined" ? import_react18.useEffect : import_react18.useLayoutEffect;
3502
+ var import_genre_tree29 = require("@behindthemusictree/app-kit/genre-tree");
3503
+ var import_genre_tree30 = require("@behindthemusictree/app-kit/genre-tree");
3504
+ var import_track_noun5 = require("@behindthemusictree/app-kit/track-noun");
3505
+ var import_jsx_runtime47 = require("react/jsx-runtime");
3506
+ var useBrowserLayoutEffect = typeof window === "undefined" ? import_react19.useEffect : import_react19.useLayoutEffect;
3438
3507
  function TrackListSidebar({
3439
3508
  className: className2 = "",
3440
3509
  renderDuration,
@@ -3446,18 +3515,18 @@ function TrackListSidebar({
3446
3515
  renderArtistLink,
3447
3516
  renderSongLink
3448
3517
  }) {
3449
- const { trackList, loadMore, autoAdvanceScrollRequest } = (0, import_genre_tree23.useTrackList)();
3450
- const trackNoun = (0, import_track_noun4.useTrackNoun)();
3451
- const trackListRef = (0, import_react18.useRef)(null);
3452
- const previousOriginRef = (0, import_react18.useRef)(trackList?.origin ?? null);
3453
- const handledAutoAdvanceRequestIdRef = (0, import_react18.useRef)(null);
3454
- const shouldFollowPlaybackRef = (0, import_react18.useRef)(true);
3455
- const programmaticScrollTopRef = (0, import_react18.useRef)(null);
3456
- const userScrollIntentRef = (0, import_react18.useRef)(false);
3457
- const userScrollIntentTimerRef = (0, import_react18.useRef)(null);
3458
- const loadMoreSentinelRef = (0, import_react18.useRef)(null);
3518
+ const { trackList, loadMore, autoAdvanceScrollRequest } = (0, import_genre_tree29.useTrackList)();
3519
+ const trackNoun = (0, import_track_noun5.useTrackNoun)();
3520
+ const trackListRef = (0, import_react19.useRef)(null);
3521
+ const previousOriginRef = (0, import_react19.useRef)(trackList?.origin ?? null);
3522
+ const handledAutoAdvanceRequestIdRef = (0, import_react19.useRef)(null);
3523
+ const shouldFollowPlaybackRef = (0, import_react19.useRef)(true);
3524
+ const programmaticScrollTopRef = (0, import_react19.useRef)(null);
3525
+ const userScrollIntentRef = (0, import_react19.useRef)(false);
3526
+ const userScrollIntentTimerRef = (0, import_react19.useRef)(null);
3527
+ const loadMoreSentinelRef = (0, import_react19.useRef)(null);
3459
3528
  const hasMore = !!trackList && trackList.nextPage !== null;
3460
- (0, import_react18.useEffect)(() => {
3529
+ (0, import_react19.useEffect)(() => {
3461
3530
  const sentinel = loadMoreSentinelRef.current;
3462
3531
  if (!hasMore || !sentinel) return;
3463
3532
  const observer = new IntersectionObserver((entries) => {
@@ -3477,11 +3546,11 @@ function TrackListSidebar({
3477
3546
  programmaticScrollTopRef.current = null;
3478
3547
  if (trackListRef.current) trackListRef.current.scrollTop = 0;
3479
3548
  }, [trackList?.origin]);
3480
- (0, import_react18.useEffect)(() => {
3549
+ (0, import_react19.useEffect)(() => {
3481
3550
  const list = trackListRef.current;
3482
3551
  if (!isCollapsed && list?.scrollTop === 0) shouldFollowPlaybackRef.current = true;
3483
3552
  }, [isCollapsed]);
3484
- (0, import_react18.useEffect)(
3553
+ (0, import_react19.useEffect)(
3485
3554
  () => () => {
3486
3555
  if (userScrollIntentTimerRef.current !== null) clearTimeout(userScrollIntentTimerRef.current);
3487
3556
  },
@@ -3535,25 +3604,25 @@ function TrackListSidebar({
3535
3604
  }
3536
3605
  };
3537
3606
  const positionClasses = layout === "inline" ? "relative w-full flex h-full flex-col" : "fixed right-0 w-[50rem] flex flex-col";
3538
- return trackList ? /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)(
3607
+ return trackList ? /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)(
3539
3608
  "div",
3540
3609
  {
3541
3610
  "data-theme": "dark",
3542
3611
  className: `track-list-sidebar ${positionClasses} rounded-2xl bg-[var(--color-surface)] pb-1 ${className2}`,
3543
3612
  style: layout === "fixed" ? { top: "63.5px", maxHeight: "calc(100vh - 63.5px - 79px)" } : void 0,
3544
3613
  children: [
3545
- /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("div", { className: "header flex h-16 items-center px-4 py-2 text-[var(--color-text)]", children: [
3546
- /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("div", { className: "min-w-0 flex flex-1 flex-col justify-center", children: [
3547
- /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("div", { className: "origin flex text-xl", children: [
3548
- /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
3549
- /* @__PURE__ */ (0, import_jsx_runtime46.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_runtime46.jsx)("div", { className: "name max-w-full truncate", children: trackList.origin.label }) })
3614
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("div", { className: "header flex h-16 items-center px-4 py-2 text-[var(--color-text)]", children: [
3615
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("div", { className: "min-w-0 flex flex-1 flex-col justify-center", children: [
3616
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("div", { className: "origin flex text-xl", children: [
3617
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
3618
+ /* @__PURE__ */ (0, import_jsx_runtime47.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_runtime47.jsx)("div", { className: "name max-w-full truncate", children: trackList.origin.label }) })
3550
3619
  ] }),
3551
- /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("div", { className: "info flex items-center text-sm text-[var(--color-text-muted)]", children: [
3552
- trackList.origin.type === import_genre_tree24.TrackListOriginType.GENRE_PLAYLIST ? "Genre \u2022 " : trackList.origin.type === import_genre_tree24.TrackListOriginType.ARTIST ? "Artist \u2022 " : `${trackNoun.singular} playlist \u2022 `,
3620
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("div", { className: "info flex items-center text-sm text-[var(--color-text-muted)]", children: [
3621
+ trackList.origin.type === import_genre_tree30.TrackListOriginType.GENRE_PLAYLIST ? "Genre \u2022 " : trackList.origin.type === import_genre_tree30.TrackListOriginType.ARTIST ? "Artist \u2022 " : `${trackNoun.singular} playlist \u2022 `,
3553
3622
  `${trackList.total} ${trackList.total > 1 ? trackNoun.plural : trackNoun.singular}`
3554
3623
  ] })
3555
3624
  ] }),
3556
- (isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3625
+ (isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
3557
3626
  "button",
3558
3627
  {
3559
3628
  type: "button",
@@ -3562,11 +3631,11 @@ function TrackListSidebar({
3562
3631
  "aria-expanded": !isCollapsed,
3563
3632
  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"}`,
3564
3633
  onClick: isCollapsed ? onExpand : onMinimize,
3565
- children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(import_lucide_react18.ListMusic, { "aria-hidden": "true", className: "h-5 w-5" })
3634
+ children: /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_lucide_react18.ListMusic, { "aria-hidden": "true", className: "h-5 w-5" })
3566
3635
  }
3567
3636
  )
3568
3637
  ] }),
3569
- !isCollapsed && /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)(
3638
+ !isCollapsed && /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)(
3570
3639
  "ul",
3571
3640
  {
3572
3641
  ref: trackListRef,
@@ -3576,7 +3645,7 @@ function TrackListSidebar({
3576
3645
  onPointerDown: markUserScrollIntent,
3577
3646
  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",
3578
3647
  children: [
3579
- trackList.tracks.map((track, index) => /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("li", { "data-track-uuid": track.uuid, children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3648
+ trackList.tracks.map((track, index) => /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("li", { "data-track-uuid": track.uuid, children: /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
3580
3649
  TrackItem,
3581
3650
  {
3582
3651
  track,
@@ -3587,7 +3656,7 @@ function TrackListSidebar({
3587
3656
  renderSongLink
3588
3657
  }
3589
3658
  ) }, track.uuid)),
3590
- hasMore && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
3659
+ hasMore && /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
3591
3660
  ]
3592
3661
  }
3593
3662
  )
@@ -3597,17 +3666,17 @@ function TrackListSidebar({
3597
3666
  }
3598
3667
 
3599
3668
  // src/genre-tree/PlayerPanel.tsx
3600
- var import_react19 = require("react");
3669
+ var import_react20 = require("react");
3601
3670
  var import_tailwind_merge = require("tailwind-merge");
3602
3671
  var import_player13 = require("@behindthemusictree/app-kit/player");
3603
- var import_genre_tree26 = require("@behindthemusictree/app-kit/genre-tree");
3672
+ var import_genre_tree32 = require("@behindthemusictree/app-kit/genre-tree");
3604
3673
  var import_ui21 = require("@behindthemusictree/ui");
3605
3674
 
3606
3675
  // src/genre-tree/PlayerControls.tsx
3607
3676
  var import_lucide_react19 = require("lucide-react");
3608
- var import_genre_tree25 = require("@behindthemusictree/app-kit/genre-tree");
3677
+ var import_genre_tree31 = require("@behindthemusictree/app-kit/genre-tree");
3609
3678
  var import_player12 = require("@behindthemusictree/app-kit/player");
3610
- var import_jsx_runtime47 = require("react/jsx-runtime");
3679
+ var import_jsx_runtime48 = require("react/jsx-runtime");
3611
3680
  function PlayerControls() {
3612
3681
  const {
3613
3682
  duration,
@@ -3619,22 +3688,22 @@ function PlayerControls() {
3619
3688
  setVolume,
3620
3689
  volume
3621
3690
  } = (0, import_player12.usePlayer)();
3622
- const { hasNextTrack, hasPreviousTrack, playNextTrack, playPreviousTrack } = (0, import_genre_tree25.useTrackList)();
3691
+ const { hasNextTrack, hasPreviousTrack, playNextTrack, playPreviousTrack } = (0, import_genre_tree31.useTrackList)();
3623
3692
  const currentTime = (0, import_player12.useCurrentTime)();
3624
3693
  const isBusy = isLoading || playState === import_player12.PlayStates.LOADING;
3625
3694
  const isReady = !!playerTrackObject?.isReady && !isBusy && !playerTrackObject.loadError;
3626
3695
  const safeDuration = Number.isFinite(duration) && duration > 0 ? duration : 0;
3627
3696
  const safeCurrentTime = Number.isFinite(currentTime) ? Math.max(0, Math.min(currentTime, safeDuration)) : 0;
3628
3697
  const isPlaying = playState === import_player12.PlayStates.PLAYING;
3629
- return /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)(
3698
+ return /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)(
3630
3699
  "section",
3631
3700
  {
3632
3701
  "aria-label": "Player controls",
3633
3702
  className: "player-controls flex shrink-0 flex-col gap-2 border-b border-zinc-800 px-3 py-2 text-zinc-200",
3634
3703
  children: [
3635
- /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("div", { className: "flex min-w-0 items-center gap-2 text-xs tabular-nums text-zinc-400", children: [
3636
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("span", { "aria-label": "Elapsed time", children: (0, import_genre_tree25.formatTime)(safeCurrentTime) }),
3637
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
3704
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "flex min-w-0 items-center gap-2 text-xs tabular-nums text-zinc-400", children: [
3705
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { "aria-label": "Elapsed time", children: (0, import_genre_tree31.formatTime)(safeCurrentTime) }),
3706
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
3638
3707
  "input",
3639
3708
  {
3640
3709
  "aria-label": "Seek",
@@ -3648,11 +3717,11 @@ function PlayerControls() {
3648
3717
  onChange: (event) => seekTo(Number(event.currentTarget.value))
3649
3718
  }
3650
3719
  ),
3651
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("span", { "aria-label": "Duration", children: (0, import_genre_tree25.formatTime)(safeDuration) })
3720
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { "aria-label": "Duration", children: (0, import_genre_tree31.formatTime)(safeDuration) })
3652
3721
  ] }),
3653
- /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("div", { className: "grid min-h-9 grid-cols-[1fr_auto_1fr] items-center gap-3", children: [
3654
- /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("div", { className: "col-start-2 flex items-center justify-center gap-2", children: [
3655
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
3722
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "grid min-h-9 grid-cols-[1fr_auto_1fr] items-center gap-3", children: [
3723
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "col-start-2 flex items-center justify-center gap-2", children: [
3724
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
3656
3725
  "button",
3657
3726
  {
3658
3727
  type: "button",
@@ -3661,10 +3730,10 @@ function PlayerControls() {
3661
3730
  disabled: !isReady || !hasPreviousTrack,
3662
3731
  onClick: playPreviousTrack,
3663
3732
  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",
3664
- children: /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_lucide_react19.SkipBack, { "aria-hidden": "true", className: "h-4 w-4" })
3733
+ children: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_lucide_react19.SkipBack, { "aria-hidden": "true", className: "h-4 w-4" })
3665
3734
  }
3666
3735
  ),
3667
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
3736
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
3668
3737
  "button",
3669
3738
  {
3670
3739
  type: "button",
@@ -3673,10 +3742,10 @@ function PlayerControls() {
3673
3742
  disabled: !isReady,
3674
3743
  onClick: handlePlayPauseAction,
3675
3744
  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",
3676
- children: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_lucide_react19.Pause, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" }) : /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_lucide_react19.Play, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" })
3745
+ children: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_lucide_react19.Pause, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" }) : /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_lucide_react19.Play, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" })
3677
3746
  }
3678
3747
  ),
3679
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
3748
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
3680
3749
  "button",
3681
3750
  {
3682
3751
  type: "button",
@@ -3685,14 +3754,14 @@ function PlayerControls() {
3685
3754
  disabled: !isReady || !hasNextTrack,
3686
3755
  onClick: playNextTrack,
3687
3756
  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",
3688
- children: /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_lucide_react19.SkipForward, { "aria-hidden": "true", className: "h-4 w-4" })
3757
+ children: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_lucide_react19.SkipForward, { "aria-hidden": "true", className: "h-4 w-4" })
3689
3758
  }
3690
3759
  )
3691
3760
  ] }),
3692
- /* @__PURE__ */ (0, import_jsx_runtime47.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: [
3693
- volume === 0 ? /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_lucide_react19.VolumeX, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }) : /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_lucide_react19.Volume2, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }),
3694
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("span", { className: "sr-only", children: "Volume" }),
3695
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
3761
+ /* @__PURE__ */ (0, import_jsx_runtime48.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: [
3762
+ volume === 0 ? /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_lucide_react19.VolumeX, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }) : /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_lucide_react19.Volume2, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }),
3763
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "sr-only", children: "Volume" }),
3764
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
3696
3765
  "input",
3697
3766
  {
3698
3767
  "aria-label": "Volume",
@@ -3714,7 +3783,7 @@ function PlayerControls() {
3714
3783
  }
3715
3784
 
3716
3785
  // src/genre-tree/PlayerPanel.tsx
3717
- var import_jsx_runtime48 = require("react/jsx-runtime");
3786
+ var import_jsx_runtime49 = require("react/jsx-runtime");
3718
3787
  var PANEL_ID = "player-panel";
3719
3788
  var DEFAULT_EXTENDED_WIDTH = "calc(100vw - var(--layout-center-left) - var(--layout-gutter))";
3720
3789
  function PlayerPanel({
@@ -3727,13 +3796,13 @@ function PlayerPanel({
3727
3796
  renderSongLink
3728
3797
  }) {
3729
3798
  const { playerTrackObject, retryPlayback } = (0, import_player13.usePlayer)();
3730
- const { toggleTrackListSidebar, isTrackListSidebarVisible } = (0, import_genre_tree26.useTrackListSidebarVisibility)();
3731
- const [lastLoadedTrackTitle, setLastLoadedTrackTitle] = (0, import_react19.useState)("");
3799
+ const { toggleTrackListSidebar, isTrackListSidebarVisible } = (0, import_genre_tree32.useTrackListSidebarVisibility)();
3800
+ const [lastLoadedTrackTitle, setLastLoadedTrackTitle] = (0, import_react20.useState)("");
3732
3801
  const loadedTrackTitle = playerTrackObject?.track.title;
3733
- (0, import_react19.useEffect)(() => {
3802
+ (0, import_react20.useEffect)(() => {
3734
3803
  if (loadedTrackTitle) setLastLoadedTrackTitle(loadedTrackTitle);
3735
3804
  }, [loadedTrackTitle]);
3736
- const [uncontrolledExtended, setUncontrolledExtended] = (0, import_react19.useState)(false);
3805
+ const [uncontrolledExtended, setUncontrolledExtended] = (0, import_react20.useState)(false);
3737
3806
  const isExtendRequested = expanded ?? uncontrolledExtended;
3738
3807
  const setIsExtendRequested = (next) => {
3739
3808
  setUncontrolledExtended(next);
@@ -3754,7 +3823,7 @@ function PlayerPanel({
3754
3823
  onClick: () => setIsExtendRequested(true),
3755
3824
  expanded: false
3756
3825
  } : null;
3757
- return /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)(
3826
+ return /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(
3758
3827
  "div",
3759
3828
  {
3760
3829
  id: PANEL_ID,
@@ -3767,7 +3836,7 @@ function PlayerPanel({
3767
3836
  ),
3768
3837
  style: isExtended ? { width: extendedWidth } : void 0,
3769
3838
  children: [
3770
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
3839
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
3771
3840
  import_ui21.PanelEdgeTab,
3772
3841
  {
3773
3842
  side: "right",
@@ -3777,7 +3846,7 @@ function PlayerPanel({
3777
3846
  className: "absolute right-full top-1/2 -translate-y-1/2"
3778
3847
  }
3779
3848
  ),
3780
- /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)(
3849
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(
3781
3850
  "div",
3782
3851
  {
3783
3852
  className: (0, import_tailwind_merge.twMerge)(
@@ -3785,20 +3854,20 @@ function PlayerPanel({
3785
3854
  isExtended ? "flex-row" : "flex-col"
3786
3855
  ),
3787
3856
  children: [
3788
- /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "flex w-[var(--layout-right-w)] shrink-0 flex-col", children: [
3789
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("div", { className: "flex shrink-0 items-center justify-between gap-2 border-b border-zinc-800 p-2", children: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "min-w-0 truncate text-sm font-semibold", children: loadedTrackTitle || lastLoadedTrackTitle }) }),
3790
- /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "relative w-full shrink-0", children: [
3791
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_player13.PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
3792
- playerTrackObject?.loadError && /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("span", { className: "absolute bottom-2 left-2 right-2 text-xs text-red-400", children: [
3857
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("div", { className: "flex w-[var(--layout-right-w)] shrink-0 flex-col", children: [
3858
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("div", { className: "flex shrink-0 items-center justify-between gap-2 border-b border-zinc-800 p-2", children: /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("span", { className: "min-w-0 truncate text-sm font-semibold", children: loadedTrackTitle || lastLoadedTrackTitle }) }),
3859
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("div", { className: "relative w-full shrink-0", children: [
3860
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(import_player13.PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
3861
+ playerTrackObject?.loadError && /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("span", { className: "absolute bottom-2 left-2 right-2 text-xs text-red-400", children: [
3793
3862
  playerTrackObject.loadError,
3794
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("button", { type: "button", className: "ml-2 underline", onClick: () => {
3863
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("button", { type: "button", className: "ml-2 underline", onClick: () => {
3795
3864
  void retryPlayback();
3796
3865
  }, children: "Retry" })
3797
3866
  ] })
3798
3867
  ] }),
3799
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(PlayerControls, {})
3868
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(PlayerControls, {})
3800
3869
  ] }),
3801
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
3870
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
3802
3871
  TrackListSidebar,
3803
3872
  {
3804
3873
  layout: "inline",
@@ -3824,33 +3893,33 @@ function PlayerPanel({
3824
3893
  // src/genre-tree/Rating.tsx
3825
3894
  var import_uuid = require("uuid");
3826
3895
  var import_ai = require("react-icons/ai");
3827
- var import_genre_tree27 = require("@behindthemusictree/app-kit/genre-tree");
3828
- var import_jsx_runtime49 = require("react/jsx-runtime");
3896
+ var import_genre_tree33 = require("@behindthemusictree/app-kit/genre-tree");
3897
+ var import_jsx_runtime50 = require("react/jsx-runtime");
3829
3898
  function Rating({ rating, handleChange }) {
3830
3899
  const instanceId = (0, import_uuid.v4)();
3831
- return /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("div", { className: "flex items-center", children: [...Array(6)].map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("div", { className: `relative ${i === 0 ? "mr-4" : "mr-1"}`, children: [
3832
- /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
3900
+ return /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("div", { className: "flex items-center", children: [...Array(6)].map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)("div", { className: `relative ${i === 0 ? "mr-4" : "mr-1"}`, children: [
3901
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
3833
3902
  "input",
3834
3903
  {
3835
3904
  id: `star-${instanceId}-${i}`,
3836
3905
  className: "w-6 h-6 opacity-0",
3837
3906
  type: "radio",
3838
3907
  name: "rating",
3839
- value: i === 0 ? import_genre_tree27.FORM_RATING_NULL_VALUE : i,
3908
+ value: i === 0 ? import_genre_tree33.FORM_RATING_NULL_VALUE : i,
3840
3909
  checked: rating === (i === 0 ? null : i),
3841
3910
  onChange: handleChange
3842
3911
  }
3843
3912
  ),
3844
- /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("label", { htmlFor: `star-${instanceId}-${i}`, className: "cursor-pointer", children: i === 0 ? /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("div", { className: "flex", children: [
3845
- /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
3913
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("label", { htmlFor: `star-${instanceId}-${i}`, className: "cursor-pointer", children: i === 0 ? /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)("div", { className: "flex", children: [
3914
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
3846
3915
  import_ai.AiOutlineStar,
3847
3916
  {
3848
- className: `w-7 h-7 absolute top-0 left-0 ${rating === import_genre_tree27.FORM_RATING_NULL_VALUE ? "text-[var(--color-accent-rating)]" : "text-[var(--color-text-muted)]"}`
3917
+ className: `w-7 h-7 absolute top-0 left-0 ${rating === import_genre_tree33.FORM_RATING_NULL_VALUE ? "text-[var(--color-accent-rating)]" : "text-[var(--color-text-muted)]"}`
3849
3918
  }
3850
3919
  ),
3851
- /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("span", { className: "flex-grow w-8" }),
3920
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("span", { className: "flex-grow w-8" }),
3852
3921
  " "
3853
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
3922
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
3854
3923
  import_ai.AiFillStar,
3855
3924
  {
3856
3925
  className: `w-7 h-7 absolute top-0 left-0 ${(rating ?? -1) >= i ? "text-[var(--color-accent-rating)]" : "text-[var(--color-text-muted)]"}`